<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Recca Tsai</title>
    <description>The latest articles on DEV Community by Recca Tsai (@recca0120).</description>
    <link>https://dev.to/recca0120</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F50274%2F427ea12d-13dd-4a0e-8955-dddfbf0a39ea.png</url>
      <title>DEV Community: Recca Tsai</title>
      <link>https://dev.to/recca0120</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/recca0120"/>
    <language>en</language>
    <item>
      <title>Does a Long Claude Code Session Waste Tokens? A Cost Model Most People Get Wrong</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Mon, 13 Apr 2026 11:06:30 +0000</pubDate>
      <link>https://dev.to/recca0120/does-a-long-claude-code-session-waste-tokens-a-cost-model-most-people-get-wrong-20f7</link>
      <guid>https://dev.to/recca0120/does-a-long-claude-code-session-waste-tokens-a-cost-model-most-people-get-wrong-20f7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/13/claude-code-session-cost-cache-misconception/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A common intuition among developers: Claude Code sessions get expensive over time. Context keeps accumulating, every turn resends the entire history, and token costs add up linearly. The obvious conclusion: &lt;code&gt;/clear&lt;/code&gt; often, start fresh sessions for each task to save money.&lt;/p&gt;

&lt;p&gt;That reasoning is &lt;strong&gt;half right and half wrong&lt;/strong&gt;. The wrong half comes from leaving prompt caching out of the cost model. In practice, &lt;strong&gt;frequent &lt;code&gt;/clear&lt;/code&gt; can cost more than keeping a long session alive&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cumulative Context Really Does Cost
&lt;/h2&gt;

&lt;p&gt;Start with what's true. LLM APIs are stateless — every API call must resend the entire conversation history. After 10 turns with Claude, the 11th request contains all 10 previous turns plus your new question.&lt;/p&gt;

&lt;p&gt;So yes, the input token count per call grows linearly as the session gets longer. It's reasonable to conclude "longer sessions cost more."&lt;/p&gt;

&lt;h2&gt;
  
  
  But Prompt Caching Changes the Rules
&lt;/h2&gt;

&lt;p&gt;Anthropic introduced prompt caching in 2024, and Claude Code enables it by default. The rule is simple: &lt;strong&gt;identical prefixes only cost 10% of the normal price&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sonnet 4.6 pricing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Price (per million tokens)&lt;/th&gt;
&lt;th&gt;Relative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base input (uncached)&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-minute cache write&lt;/td&gt;
&lt;td&gt;$3.75&lt;/td&gt;
&lt;td&gt;125%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-hour cache write&lt;/td&gt;
&lt;td&gt;$6.00&lt;/td&gt;
&lt;td&gt;200%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache read&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Opus is even more dramatic: base input $5, cache read only $0.50.&lt;/p&gt;

&lt;p&gt;Meaning: the first time you send a large context block, it gets written to the cache and you pay a small write premium (25% above base). For the next 5 minutes, resending the same prefix costs 10% of base. The longer the session and the more cache hits accumulate, the lower your average per-token cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Variables That Actually Drive Cost
&lt;/h2&gt;

&lt;p&gt;So the cost model isn't "context size × number of turns." It's these three factors:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cache Hit Rate
&lt;/h3&gt;

&lt;p&gt;In a long, continuous session, every turn's prefix hits the cache written by the previous turn. If a session has accumulated 50K tokens and turn 11 adds 2K new input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Without cache: 51K × $3 = $0.153&lt;/li&gt;
&lt;li&gt;With cache: 50K × $0.30 + 2K × $3 = $0.021&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;About a &lt;strong&gt;7x difference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's worst about aggressive &lt;code&gt;/clear&lt;/code&gt;&lt;/strong&gt;: every new session re-reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, re-learns your project files, re-warms the cache. These warm-up costs can easily exceed the "savings" from keeping context small.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cache Invalidation
&lt;/h3&gt;

&lt;p&gt;Cache requires &lt;strong&gt;100% identical prefixes&lt;/strong&gt; to hit. These actions invalidate the whole cache:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editing any historical message&lt;/li&gt;
&lt;li&gt;Changing tool schemas (adding/removing MCP tools)&lt;/li&gt;
&lt;li&gt;Switching models (Sonnet ↔ Opus)&lt;/li&gt;
&lt;li&gt;Toggling web search or citations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Code's auto-compact is also a cache-destruction event. The moment it squashes your 200K context into a summary, the accumulated cache is gone, and the next turn has to warm from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. TTL (5 Minutes vs 1 Hour)
&lt;/h3&gt;

&lt;p&gt;Default cache TTL is 5 minutes. Pause for more than 5 minutes and the cache expires — the next call pays full base input price.&lt;/p&gt;

&lt;p&gt;Anthropic offers a 1-hour TTL option at 2x write cost ($6 vs $3) in exchange for longer persistence. Whether it's worth it depends on rhythm — bursty work with 10–30 minute gaps may benefit; continuous work never hits the timeout anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counterintuitive: When Long Sessions Are Cheapest
&lt;/h2&gt;

&lt;p&gt;Combine all three and you arrive at the opposite of "longer = more expensive":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long sessions are cheapest when&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Work is continuous, turns within 5 minutes of each other&lt;/li&gt;
&lt;li&gt;No editing of history, no model switches, no MCP churn&lt;/li&gt;
&lt;li&gt;Context stays below the compaction threshold (~155K safe zone)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Short sessions / frequent clearing are costliest when&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every new session re-reads large context (CLAUDE.md, multiple files, skill definitions)&lt;/li&gt;
&lt;li&gt;Every new session pays a "cache warm-up tax"&lt;/li&gt;
&lt;li&gt;You never reap the 10% cache-read discount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My own experience: two hours of continuous work in one session often costs less than splitting the same work into four independent 30-minute sessions — because the latter pays four cold starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Large Context &lt;strong&gt;Is&lt;/strong&gt; Genuinely a Problem
&lt;/h2&gt;

&lt;p&gt;None of this means context can grow forever with no consequence. Two thresholds turn "large context" from a cost problem into a &lt;strong&gt;quality problem&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Approaching the context window limit&lt;/strong&gt; (Sonnet 200K / 1M, Opus 200K)&lt;/p&gt;

&lt;p&gt;Model attention degrades past ~100K tokens, especially on content in the middle (the "lost in the middle" phenomenon). At this point the concern isn't cost — it's that the model &lt;strong&gt;can't find or misuses&lt;/strong&gt; what you gave it earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Auto-compact triggers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code auto-compacts as you approach the limit. Compaction is a major operation — cache fully invalidates, cost spikes, and the result is a summary with possible detail loss.&lt;/p&gt;

&lt;p&gt;So context shouldn't grow unbounded, but the right reset trigger is "task complete" or "about to hit compaction," not "session has been open for X hours."&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Recommendations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mid-task&lt;/td&gt;
&lt;td&gt;Don't &lt;code&gt;/clear&lt;/code&gt;, continue the session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task done, starting a new one&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/clear&lt;/code&gt; so the next session starts clean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle for &amp;gt;5 minutes&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;/resume&lt;/code&gt; instead of opening a new session (TTL expires but history is preserved)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code open often with idle gaps&lt;/td&gt;
&lt;td&gt;Consider 1h TTL — 2x write cost but idle safety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context exceeds 155K&lt;/td&gt;
&lt;td&gt;Proactively end the session; don't wait for auto-compact&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To measure your actual cost, try &lt;a href="https://github.com/ryoppippi/ccusage" rel="noopener noreferrer"&gt;ccusage&lt;/a&gt; or &lt;a href="https://dev.to/en/2026/04/07/claude-view-mission-control/"&gt;claude-view&lt;/a&gt;. A high share of &lt;code&gt;cache_read_input_tokens&lt;/code&gt; means you're working efficiently; rising &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; with low reads means cache keeps invalidating — you're burning money.&lt;/p&gt;

&lt;p&gt;"Longer sessions waste more tokens" is a stateless-era intuition, but prompt caching has been rewriting those rules for two years. Check how you actually use Claude Code — the token savings might surprise you.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Prompt Caching — Claude API Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/costs" rel="noopener noreferrer"&gt;Manage Costs Effectively — Claude Code Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.claudecodecamp.com/p/how-prompt-caching-actually-works-in-claude-code" rel="noopener noreferrer"&gt;How Prompt Caching Actually Works in Claude Code — Claude Code Camp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindstudio.ai/blog/claude-code-context-compounding-explained-2" rel="noopener noreferrer"&gt;How Context Compounding Works in Claude Code — MindStudio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ryoppippi/ccusage" rel="noopener noreferrer"&gt;ccusage — Claude Code Token Usage CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>promptcaching</category>
      <category>agents</category>
      <category>costoptimization</category>
    </item>
    <item>
      <title>chezmoi: One Dotfiles Repo Across macOS, Linux, and Windows</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Mon, 13 Apr 2026 10:41:33 +0000</pubDate>
      <link>https://dev.to/recca0120/chezmoi-one-dotfiles-repo-across-macos-linux-and-windows-2o3</link>
      <guid>https://dev.to/recca0120/chezmoi-one-dotfiles-repo-across-macos-linux-and-windows-2o3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/13/chezmoi-dotfiles-management/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My work machine is a MacBook, my home desktop runs Linux, and the company handed me a Windows NUC. All three need synced &lt;code&gt;.gitconfig&lt;/code&gt;, &lt;code&gt;.zshrc&lt;/code&gt;, &lt;code&gt;.tmux.conf&lt;/code&gt; — but each OS has quirks. Windows needs &lt;code&gt;sslCAInfo&lt;/code&gt; pointing at scoop's git cert bundle; macOS uses Homebrew; Linux uses apt.&lt;/p&gt;

&lt;p&gt;I used to hack it with symlinks and shell scripts. Now I use &lt;a href="https://github.com/twpayne/chezmoi" rel="noopener noreferrer"&gt;chezmoi&lt;/a&gt;. One dotfiles repo, three machines, one-line setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not stow, yadm, or dotbot
&lt;/h2&gt;

&lt;p&gt;Plenty of dotfiles managers exist. chezmoi wins on three fronts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Go templates&lt;/strong&gt;: the same file renders differently per OS, no need to maintain three &lt;code&gt;.gitconfig&lt;/code&gt; variants&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native encryption&lt;/strong&gt;: age and gpg are first-class, so secrets can live in a public repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;onchange scripts&lt;/strong&gt;: the Homebrew bootstrap only re-runs when the package list actually changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;stow is pure symlinks, no templating. yadm wraps git, templating via plugins. dotbot needs a YAML manifest. chezmoi bundles it all into one binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and Init
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;chezmoi

&lt;span class="c"&gt;# Linux&lt;/span&gt;
sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsLS&lt;/span&gt; get.chezmoi.io&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Windows&lt;/span&gt;
winget &lt;span class="nb"&gt;install &lt;/span&gt;twpayne.chezmoi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bootstrap a new machine from an existing repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chezmoi init &lt;span class="nt"&gt;--apply&lt;/span&gt; https://github.com/YOUR_USERNAME/dotfiles.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line clones the repo, runs the template engine, and writes everything to &lt;code&gt;$HOME&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filename Attribute System
&lt;/h2&gt;

&lt;p&gt;chezmoi uses &lt;strong&gt;filename prefixes&lt;/strong&gt; to encode behavior. The repo layout itself is the manifest — no separate config needed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dot_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Target is a hidden file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dot_zshrc&lt;/code&gt; → &lt;code&gt;~/.zshrc&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User-only permissions (0600)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;private_dot_ssh&lt;/code&gt; → &lt;code&gt;~/.ssh&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;executable_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets executable bit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;executable_bin_foo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encrypted_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;age/gpg encrypted&lt;/td&gt;
&lt;td&gt;&lt;code&gt;encrypted_dot_env&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;symlink_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates a symlink&lt;/td&gt;
&lt;td&gt;&lt;code&gt;symlink_dot_bashrc&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;readonly_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Strips write permissions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;readonly_dot_config.toml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.tmpl&lt;/code&gt; suffix&lt;/td&gt;
&lt;td&gt;Run through template engine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dot_gitconfig.tmpl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prefixes stack. My repo has combinations like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private_executable_dot_php-cs-fixer.dist.php  → ~/.php-cs-fixer.dist.php (0700)
private_dot_ssh/                              → ~/.ssh (whole dir at 0700)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Templates for Machine Differences
&lt;/h2&gt;

&lt;p&gt;This is chezmoi's killer feature. My &lt;code&gt;dot_gitconfig.tmpl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[user]
    name = {{ .name | quote }}
    email = {{ .email | quote }}

[http]
    sslBackend = openssl
{{ if eq .chezmoi.os "windows" -}}
    sslCAInfo = {{- .chezmoi.homeDir | replace "\\" "/" -}}/scoop/apps/git/current/mingw64/ssl/certs/ca-bundle.crt
{{ end }}

[core]
    autocrlf = false
    symlinks = true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.name&lt;/code&gt; and &lt;code&gt;.email&lt;/code&gt; come from &lt;code&gt;~/.config/chezmoi/chezmoi.toml&lt;/code&gt;, so each machine can have its own values. The &lt;code&gt;{{ if eq .chezmoi.os "windows" }}&lt;/code&gt; block only expands on Windows. On apply, chezmoi strips the &lt;code&gt;.tmpl&lt;/code&gt; and writes a clean &lt;code&gt;.gitconfig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Built-in variables I reach for constantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{ .chezmoi.os }}              # "darwin" / "linux" / "windows"
{{ .chezmoi.arch }}            # "amd64" / "arm64"
{{ .chezmoi.hostname }}        # machine name
{{ .chezmoi.username }}        # login user
{{ .chezmoi.homeDir }}         # home directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preview a template without applying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chezmoi execute-template &amp;lt; dot_gitconfig.tmpl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Age Encryption for Secrets
&lt;/h2&gt;

&lt;p&gt;My repo is public, but it contains an SSH key and database password backups. Those are encrypted with &lt;a href="https://github.com/FiloSottile/age" rel="noopener noreferrer"&gt;age&lt;/a&gt; before they ever hit a commit.&lt;/p&gt;

&lt;p&gt;Generate an age key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;age-keygen &lt;span class="nt"&gt;-o&lt;/span&gt; ~/key.txt
&lt;span class="c"&gt;# Public key: age1s5xur7evyf9y9un4yt8cwqrqw9vd0k8m67kkl655gy9nmnfrgplsxef583&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure &lt;code&gt;~/.config/chezmoi/chezmoi.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;encryption&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"age"&lt;/span&gt;

&lt;span class="nn"&gt;[age]&lt;/span&gt;
    &lt;span class="py"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"~/key.txt"&lt;/span&gt;
    &lt;span class="py"&gt;recipient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"age1s5xur7evyf9y9un4yt8cwqrqw9vd0k8m67kkl655gy9nmnfrgplsxef583"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add files with &lt;code&gt;--encrypt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chezmoi add &lt;span class="nt"&gt;--encrypt&lt;/span&gt; ~/.ssh/id_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo only ever stores &lt;code&gt;private_dot_ssh/encrypted_private_id_ed25519.age&lt;/code&gt; — opaque ciphertext. On apply, chezmoi decrypts using &lt;code&gt;~/key.txt&lt;/code&gt; and writes the plaintext to the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one catastrophic footgun&lt;/strong&gt;: &lt;code&gt;key.txt&lt;/code&gt; itself &lt;strong&gt;must never land in the repo&lt;/strong&gt;. My workflow: GPG-encrypt it and stash it in a password manager. New machines must restore &lt;code&gt;key.txt&lt;/code&gt; manually before running &lt;code&gt;chezmoi init --apply&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  run_onchange: Reinstall Only When Lists Change
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;.chezmoiscripts/darwin/run_onchange_00_install-packages.sh.tmpl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;eq .chezmoi.os &lt;span class="s2"&gt;"darwin"&lt;/span&gt; -&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

brew &lt;span class="nb"&gt;install &lt;/span&gt;mas
brew &lt;span class="nb"&gt;install &lt;/span&gt;asdf

asdf plugin add nodejs
asdf &lt;span class="nb"&gt;install &lt;/span&gt;nodejs latest
asdf &lt;span class="nb"&gt;set &lt;/span&gt;nodejs latest

&lt;span class="c"&gt;# ... many more asdf installs&lt;/span&gt;
&lt;span class="o"&gt;{{&lt;/span&gt; end -&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;run_onchange_&lt;/code&gt; prefix is the key: chezmoi only runs this script when its &lt;strong&gt;content hash changes&lt;/strong&gt;. Unchanged package list means no re-run — no more five-minute &lt;code&gt;brew install&lt;/code&gt; cycles through already-installed tools on every &lt;code&gt;chezmoi apply&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Script naming variants:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;When It Runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;run_once_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once per machine, ever, for given content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;run_onchange_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whenever the contents change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;run_onchange_before_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Before&lt;/strong&gt; file application (install package manager first)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;run_onchange_after_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;After&lt;/strong&gt; file application (enable fish plugins last)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The numeric prefix (&lt;code&gt;00_&lt;/code&gt;, &lt;code&gt;01_&lt;/code&gt;, &lt;code&gt;02_&lt;/code&gt;) controls execution order.&lt;/p&gt;

&lt;h2&gt;
  
  
  .chezmoiroot: Source Lives in a Subdirectory
&lt;/h2&gt;

&lt;p&gt;All my files live under &lt;code&gt;home/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotfiles/
├── .chezmoiroot        # contains just "home"
├── Readme.md
├── install.sh
├── install.ps1
└── home/
    ├── dot_zshrc.tmpl
    ├── dot_gitconfig.tmpl
    └── .chezmoiscripts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.chezmoiroot&lt;/code&gt; tells chezmoi "source files are under &lt;code&gt;home/&lt;/code&gt;". Now the repo root can host a README, install scripts, and other project artifacts without chezmoi trying to apply them as dotfiles.&lt;/p&gt;

&lt;p&gt;Great for treating your dotfiles repo like a normal project.&lt;/p&gt;

&lt;h2&gt;
  
  
  .chezmoiignore: Skip Certain Files
&lt;/h2&gt;

&lt;p&gt;Same syntax as &lt;code&gt;.gitignore&lt;/code&gt;, but it supports templates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;README.md
LICENSE
&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;ne&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;chezmoi.os&lt;/span&gt; &lt;span class="s2"&gt;"darwin"&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
.aerospace.toml
Library/
&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Non-macOS machines skip the aerospace window manager config and the Library folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Command Cheat Sheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chezmoi add ~/.vimrc              &lt;span class="c"&gt;# track an existing file&lt;/span&gt;
chezmoi add &lt;span class="nt"&gt;--encrypt&lt;/span&gt; ~/.env      &lt;span class="c"&gt;# track encrypted&lt;/span&gt;
chezmoi edit ~/.zshrc             &lt;span class="c"&gt;# edit the source file directly&lt;/span&gt;
chezmoi diff                      &lt;span class="c"&gt;# show pending changes&lt;/span&gt;
chezmoi apply                     &lt;span class="c"&gt;# write to $HOME&lt;/span&gt;
chezmoi apply &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;        &lt;span class="c"&gt;# preview without writing&lt;/span&gt;
chezmoi &lt;span class="nb"&gt;cd&lt;/span&gt;                        &lt;span class="c"&gt;# jump to source directory&lt;/span&gt;
chezmoi update                    &lt;span class="c"&gt;# git pull + apply&lt;/span&gt;
chezmoi doctor                    &lt;span class="c"&gt;# check environment health&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chezmoi doctor&lt;/code&gt; reports the status of encryption tools, template engine, git, and friends. First thing to run when a new machine misbehaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combined with &lt;a href="https://dev.to/en/2026/04/13/zoxide-smarter-cd/"&gt;zoxide&lt;/a&gt;, fish, and More
&lt;/h2&gt;

&lt;p&gt;My fish config, zoxide init, tmux plugins — all managed by chezmoi. New machine ritual:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Restore the age key&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply recca0120&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;run_onchange scripts install CLI tools via brew / apt / scoop&lt;/li&gt;
&lt;li&gt;All configs land in place&lt;/li&gt;
&lt;li&gt;Open fish — zoxide, starship, fzf are already wired up&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;About 20 minutes end to end, most of it waiting on &lt;code&gt;brew install&lt;/code&gt; downloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downsides and Gotchas
&lt;/h2&gt;

&lt;p&gt;chezmoi isn't free of friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Template learning curve&lt;/strong&gt;: Go template syntax isn't beginner-friendly. Whitespace handling with &lt;code&gt;{{- }}&lt;/code&gt; vs. &lt;code&gt;{{ }}&lt;/code&gt; takes practice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Painful debugging&lt;/strong&gt;: template expansion errors are terse — I lean on &lt;code&gt;chezmoi execute-template&lt;/code&gt; to isolate problems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Age key stewardship&lt;/strong&gt;: lose the key, lose every encrypted file forever. Back it up separately (I GPG-encrypt and park it in a password manager)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First apply is destructive&lt;/strong&gt;: if &lt;code&gt;$HOME&lt;/code&gt; already has hand-edited dotfiles, apply overwrites them. Always &lt;code&gt;chezmoi diff&lt;/code&gt; first&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/twpayne/chezmoi" rel="noopener noreferrer"&gt;chezmoi GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.chezmoi.io/" rel="noopener noreferrer"&gt;chezmoi Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.chezmoi.io/quick-start/" rel="noopener noreferrer"&gt;chezmoi Quick Start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/FiloSottile/age" rel="noopener noreferrer"&gt;age — Simple File Encryption&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://natelandau.com/managing-dotfiles-with-chezmoi/" rel="noopener noreferrer"&gt;Managing Dotfiles With Chezmoi — Nathaniel Landau&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>chezmoi</category>
      <category>dotfiles</category>
      <category>age</category>
      <category>go</category>
    </item>
    <item>
      <title>zoxide: Give cd a Memory — Jump to Any Directory in Two Keystrokes</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Mon, 13 Apr 2026 07:10:46 +0000</pubDate>
      <link>https://dev.to/recca0120/zoxide-give-cd-a-memory-jump-to-any-directory-in-two-keystrokes-5bdo</link>
      <guid>https://dev.to/recca0120/zoxide-give-cd-a-memory-jump-to-any-directory-in-two-keystrokes-5bdo</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/13/zoxide-smarter-cd/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My projects are scattered across &lt;code&gt;~/Sites&lt;/code&gt;, &lt;code&gt;~/WebstormProjects&lt;/code&gt;, &lt;code&gt;~/GolandProjects&lt;/code&gt;, &lt;code&gt;~/PycharmProjects&lt;/code&gt; — long paths, no single root. For years I either mashed &lt;code&gt;cd ~/Sites/recca012&amp;lt;TAB&amp;gt;&lt;/code&gt; or dragged folders from Finder into the terminal. Then I installed &lt;a href="https://github.com/ajeetdsouza/zoxide" rel="noopener noreferrer"&gt;zoxide&lt;/a&gt;. Now &lt;code&gt;cd recca&lt;/code&gt; gets me there.&lt;/p&gt;

&lt;p&gt;The trick is that my &lt;code&gt;cd&lt;/code&gt; is no longer a shell builtin. It's zoxide's replacement — same behavior as the original, plus memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What frecency Means
&lt;/h2&gt;

&lt;p&gt;zoxide's algorithm is called frecency (frequency + recency). Every directory you visit earns a score that climbs with use and decays over time. Type &lt;code&gt;cd foo&lt;/code&gt; and zoxide searches its database for paths containing &lt;code&gt;foo&lt;/code&gt;, then jumps to the highest-scoring one.&lt;/p&gt;

&lt;p&gt;Here's a slice of my own database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;zoxide query &lt;span class="nt"&gt;--score&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
 230.0 /Users/recca0120/WebstormProjects
 215.3 /Users/recca0120/Sites
 198.7 /Users/recca0120/Sites/recca0120.github.io
 142.1 /Users/recca0120/Desktop/vscode-phpunit
  98.5 /Users/recca0120/Downloads
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frequently visited projects float to the top; stale ones sink. Data lives in a local file — fully offline, no network calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and Init
&lt;/h2&gt;

&lt;p&gt;macOS via Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;zoxide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linux one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSfL&lt;/span&gt; https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then initialize in your shell config. &lt;strong&gt;The key decision is whether to use &lt;code&gt;--cmd cd&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zoxide init &amp;lt;shell&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adds &lt;code&gt;z&lt;/code&gt;, &lt;code&gt;zi&lt;/code&gt; commands. Builtin &lt;code&gt;cd&lt;/code&gt; untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace cd&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zoxide init --cmd cd &amp;lt;shell&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replaces &lt;code&gt;cd&lt;/code&gt; outright&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I went with the latter. I use &lt;a href="https://dev.to/en/2024/auto-venv-fish/"&gt;fish shell&lt;/a&gt;, so my config reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ~/.config/fish/config.fish
zoxide init --cmd cd fish | source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For zsh / bash, use eval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.zshrc&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;zoxide init &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why replace &lt;code&gt;cd&lt;/code&gt; wholesale? Because zoxide's &lt;code&gt;cd&lt;/code&gt; is a &lt;strong&gt;superset&lt;/strong&gt; of the builtin: absolute paths, relative paths, &lt;code&gt;cd -&lt;/code&gt;, &lt;code&gt;cd ..&lt;/code&gt; all still work. Frecency lookup only kicks in when the argument isn't a valid path. No regression risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Daily Workflows
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Keyword jumps.&lt;/strong&gt; Skip full paths — just type a fragment of the directory name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;github          &lt;span class="c"&gt;# → ~/Sites/recca0120.github.io&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;webstorm        &lt;span class="c"&gt;# → ~/WebstormProjects&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;phpunit         &lt;span class="c"&gt;# → ~/Desktop/vscode-phpunit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Multi-keyword filtering.&lt;/strong&gt; When names collide, chain keywords to narrow down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;sites blog      &lt;span class="c"&gt;# → ~/Sites/scu_blog&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;projects cc     &lt;span class="c"&gt;# → ~/WebstormProjects/cc-office&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match rule: every keyword must appear in the path, and the last one must be in the final segment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Interactive selection via &lt;code&gt;zi&lt;/code&gt;.&lt;/strong&gt; When you can't recall the keyword or have multiple candidates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdi               &lt;span class="c"&gt;# since I used --cmd cd, zi becomes cdi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens an &lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;fzf&lt;/a&gt; UI listing all candidates with live fuzzy filtering. Install fzf first if you haven't: &lt;code&gt;brew install fzf&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Tricks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Space-triggered completion.&lt;/strong&gt; In fish, typing &lt;code&gt;cd mydir&amp;lt;SPACE&amp;gt;&lt;/code&gt; lists multiple candidates — handy when directories share names. Fish users can also install an enhanced completion pack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fisher &lt;span class="nb"&gt;install &lt;/span&gt;icezyclon/zoxide.fish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Query without jumping.&lt;/strong&gt; Preview where zoxide would take you, without actually going:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zoxide query github
&lt;span class="c"&gt;# → /Users/recca0120/Sites/recca0120.github.io&lt;/span&gt;

zoxide query &lt;span class="nt"&gt;--list&lt;/span&gt; github    &lt;span class="c"&gt;# list all matches&lt;/span&gt;
zoxide query &lt;span class="nt"&gt;--score&lt;/span&gt;          &lt;span class="c"&gt;# view frecency scores&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Manually register a directory.&lt;/strong&gt; For a freshly cloned project you haven't visited yet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zoxide add ~/projects/new-repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Exclude noise.&lt;/strong&gt; &lt;code&gt;/tmp&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt;, and friends clutter the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set -gx _ZO_EXCLUDE_DIRS "/tmp/*" "*/node_modules/*" "$HOME/.cache/*"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Echo destination before jumping.&lt;/strong&gt; Helps catch wrong jumps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set -gx _ZO_ECHO 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Migrate from older tools.&lt;/strong&gt; autojump, fasd, z.lua all have import paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zoxide import &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;autojump ~/.local/share/autojump/autojump.txt
zoxide import &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;z ~/.z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Combining with yazi and tmux
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;.zshrc&lt;/code&gt; has a function &lt;code&gt;y&lt;/code&gt; that syncs &lt;a href="https://github.com/sxyazi/yazi" rel="noopener noreferrer"&gt;yazi&lt;/a&gt;'s final directory back to the shell on exit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function y
    set tmp (mktemp -t "yazi-cwd.XXXXXX")
    yazi $argv --cwd-file="$tmp"
    set cwd (cat -- "$tmp")
    if [ -n "$cwd" ] &amp;amp;&amp;amp; [ "$cwd" != "$PWD" ]
        cd -- "$cwd"  # this cd is zoxide
    end
    rm -f -- "$tmp"
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trailing &lt;code&gt;cd&lt;/code&gt; is zoxide's version, so directories I browse through yazi also feed into the frecency database. The two tools cross-pollinate — both get smarter with use.&lt;/p&gt;

&lt;p&gt;In tmux, each pane is its own shell, but zoxide's database is shared globally. Visit a directory in pane A and pane B can jump there with &lt;code&gt;cd foo&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use --cmd cd
&lt;/h2&gt;

&lt;p&gt;Honestly, &lt;code&gt;--cmd cd&lt;/code&gt; isn't uncontroversial. Arguments against overriding the builtin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shell scripts might accidentally inherit zoxide behavior&lt;/li&gt;
&lt;li&gt;Shared terminals could confuse other users&lt;/li&gt;
&lt;li&gt;Certain &lt;code&gt;cd&lt;/code&gt; edge cases (like &lt;code&gt;CDPATH&lt;/code&gt;) may behave differently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;zoxide's implementation only overrides &lt;code&gt;cd&lt;/code&gt; in &lt;strong&gt;interactive shells&lt;/strong&gt;, so the first concern is mostly academic. But if you value purity, sticking with the default &lt;code&gt;z&lt;/code&gt; / &lt;code&gt;zi&lt;/code&gt; gets you 95% of the benefit — you just have to pause each time to pick &lt;code&gt;cd&lt;/code&gt; vs. &lt;code&gt;z&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Personally, I prefer &lt;code&gt;--cmd cd&lt;/code&gt;. Muscle memory doesn't want to change, so the tool should adapt to the human, not the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ajeetdsouza/zoxide" rel="noopener noreferrer"&gt;zoxide GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zoxide.org/" rel="noopener noreferrer"&gt;zoxide Official Tutorials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://batsov.com/articles/2025/06/12/zoxide-tips-and-tricks/" rel="noopener noreferrer"&gt;zoxide: Tips and Tricks — Bozhidar Batsov&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;fzf Fuzzy Finder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/icezyclon/zoxide.fish" rel="noopener noreferrer"&gt;icezyclon/zoxide.fish — enhanced fish completions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>zoxide</category>
      <category>fish</category>
      <category>terminal</category>
      <category>productivity</category>
    </item>
    <item>
      <title>MemPalace: 170 Tokens to Recall Everything — A Long-Term Memory System for AI Agents</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Tue, 07 Apr 2026 17:44:15 +0000</pubDate>
      <link>https://dev.to/recca0120/mempalace-170-tokens-to-recall-everything-a-long-term-memory-system-for-ai-agents-2855</link>
      <guid>https://dev.to/recca0120/mempalace-170-tokens-to-recall-everything-a-long-term-memory-system-for-ai-agents-2855</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/08/mempalace-ai-memory-system/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Six months of daily AI conversations. 19.5 million tokens of history. Start a new session and it remembers nothing. You can dump important things into CLAUDE.md, but that file quickly balloons to thousands of lines, eating up your context window on every startup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/milla-jovovich/mempalace" rel="noopener noreferrer"&gt;MemPalace&lt;/a&gt; takes a different approach: instead of cramming all memories into the prompt, build a structured memory vault that AI queries on demand. Startup loads just 170 tokens, search accuracy hits 96.6%, completely offline, zero API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Memory Palace Architecture
&lt;/h2&gt;

&lt;p&gt;MemPalace uses the ancient Greek memory technique as its organizational metaphor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wing&lt;/strong&gt;: Projects, people, or topics. One wing per major category&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room&lt;/strong&gt;: Sub-topics within a wing — auth, billing, deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hall&lt;/strong&gt;: Memory type corridors shared across all wings

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hall_facts&lt;/code&gt; — locked-in decisions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hall_events&lt;/code&gt; — sessions and milestones&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hall_discoveries&lt;/code&gt; — breakthroughs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hall_preferences&lt;/code&gt; — habits and opinions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hall_advice&lt;/code&gt; — recommendations&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Closet&lt;/strong&gt;: Compressed summaries pointing to original content&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Drawer&lt;/strong&gt;: Verbatim original files, preserved losslessly&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Tunnel&lt;/strong&gt;: Cross-wing connections when the same room appears in multiple wings&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The structure alone improves search accuracy. Benchmark results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Search Scope&lt;/th&gt;
&lt;th&gt;R@10&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All closets&lt;/td&gt;
&lt;td&gt;60.9%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Within wing&lt;/td&gt;
&lt;td&gt;73.1%&lt;/td&gt;
&lt;td&gt;+12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wing + hall&lt;/td&gt;
&lt;td&gt;84.8%&lt;/td&gt;
&lt;td&gt;+24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wing + room&lt;/td&gt;
&lt;td&gt;94.8%&lt;/td&gt;
&lt;td&gt;+34%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Structure alone delivers a 34% accuracy boost — no fancy algorithms needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  AAAK Compression Format
&lt;/h2&gt;

&lt;p&gt;This is MemPalace's most interesting design. AAAK is an AI-readable shorthand achieving 30x compression.&lt;/p&gt;

&lt;p&gt;Original (~1,000 tokens):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Priya manages Driftwood team: Kai (backend, 3 years), Soren (frontend),
Maya (infrastructure), Leo (junior, started last month). Building SaaS
analytics platform. Current sprint: auth migration to Clerk. Kai
recommended Clerk over Auth0 based on pricing and DX.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AAAK format (~120 tokens):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TEAM: PRI(lead) | KAI(backend,3yr) SOR(frontend) MAY(infra) LEO(junior,new)
PROJ: DRIFTWOOD(saas.analytics) | SPRINT: auth.migration→clerk
DECISION: KAI.rec:clerk&amp;gt;auth0(pricing+dx) | ★★★★
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point: no decoder required. Any LLM reads it natively — Claude, GPT, Llama, Mistral. It's essentially structured English abbreviations, not binary encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layered Memory Loading
&lt;/h2&gt;

&lt;p&gt;MemPalace divides memory into four layers, loaded incrementally:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;When Loaded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L0&lt;/td&gt;
&lt;td&gt;Identity — who is this AI&lt;/td&gt;
&lt;td&gt;~50 tokens&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;Critical facts — team, projects, preferences&lt;/td&gt;
&lt;td&gt;~120 tokens (AAAK)&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;Room recall — recent sessions&lt;/td&gt;
&lt;td&gt;On demand&lt;/td&gt;
&lt;td&gt;When topic surfaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;Deep search — semantic across all closets&lt;/td&gt;
&lt;td&gt;On demand&lt;/td&gt;
&lt;td&gt;When explicitly asked&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Startup loads only L0 + L1, about 170 tokens total. Compared to alternatives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Tokens Loaded&lt;/th&gt;
&lt;th&gt;Annual Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Paste everything&lt;/td&gt;
&lt;td&gt;19.5M — impossible&lt;/td&gt;
&lt;td&gt;Impossible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM summaries&lt;/td&gt;
&lt;td&gt;~650K&lt;/td&gt;
&lt;td&gt;~$507&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MemPalace wake-up&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~170&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.70&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MemPalace + 5 searches&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~13,500&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Knowledge Graph: Facts Have Expiry Dates
&lt;/h2&gt;

&lt;p&gt;MemPalace includes a temporal knowledge graph stored in local SQLite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_triple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;works_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Orion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2025-06-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_triple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Maya&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assigned_to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auth-migration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-15&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Kai leaves the Orion project
&lt;/span&gt;&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;works_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Orion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ended&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-03-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Query current state
&lt;/span&gt;&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# → [Kai → works_on → Orion (ended), Kai → recommended → Clerk]
&lt;/span&gt;
&lt;span class="c1"&gt;# Historical queries
&lt;/span&gt;&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Maya&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;as_of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-01-20&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# → [Maya → assigned_to → auth-migration (active)]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every fact has a validity window. Invalidation marks end dates without deletion. This solves the most common CLAUDE.md problem: stale information that nobody cleans up, causing the AI to act on outdated data.&lt;/p&gt;

&lt;p&gt;The knowledge graph also detects contradictions — tasks assigned to the wrong person, tenure mismatches, outdated sprint end dates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code Integration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  MCP Server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add mempalace &lt;span class="nt"&gt;--&lt;/span&gt; python &lt;span class="nt"&gt;-m&lt;/span&gt; mempalace.mcp_server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, Claude Code auto-discovers 19 MCP tools covering search, storage, knowledge graph queries, and agent diaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-Save Hooks
&lt;/h3&gt;

&lt;p&gt;Add two hooks to your Claude Code configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/path/to/mempalace/hooks/mempal_save_hook.sh"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreCompact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/path/to/mempalace/hooks/mempal_precompact_hook.sh"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Save hook&lt;/strong&gt;: Fires every 15 messages, auto-extracts topics, decisions, and code changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PreCompact hook&lt;/strong&gt;: Fires before context compression, emergency-saving current memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No need to manually tell the AI to remember things — it saves automatically.&lt;/p&gt;

&lt;p&gt;I previously covered &lt;a href="https://dev.to/en/2026/04/07/claude-view-mission-control/"&gt;claude-view&lt;/a&gt;, which monitors Claude Code sessions and costs from the outside. MemPalace extends AI's memory from the inside. They're complementary — claude-view shows you what AI did, MemPalace helps AI remember what it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specialist Agents
&lt;/h2&gt;

&lt;p&gt;Create focused agents with independent memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.mempalace/agents/
 ├── reviewer.json    &lt;span class="c"&gt;# code review patterns, bug records&lt;/span&gt;
 ├── architect.json   &lt;span class="c"&gt;# design decisions, trade-offs&lt;/span&gt;
 └── ops.json         &lt;span class="c"&gt;# deploys, incidents, infrastructure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent maintains its own wing and AAAK diary, accumulating domain expertise across sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Agent writes findings
&lt;/span&gt;&lt;span class="nf"&gt;mempalace_diary_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PR#42|auth.bypass.found|missing.middleware.check|pattern:3rd.quarter|★★★★&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Agent reads history
&lt;/span&gt;&lt;span class="nf"&gt;mempalace_diary_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No need to stuff agent descriptions into CLAUDE.md. One line suffices: "You have MemPalace agents. Run mempalace_list_agents to see them."&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation and Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;mempalace

&lt;span class="c"&gt;# Initialize&lt;/span&gt;
mempalace init ~/projects/myapp

&lt;span class="c"&gt;# Mine different sources&lt;/span&gt;
mempalace mine ~/projects/myapp              &lt;span class="c"&gt;# project code&lt;/span&gt;
mempalace mine ~/chats/ &lt;span class="nt"&gt;--mode&lt;/span&gt; convos        &lt;span class="c"&gt;# conversation history&lt;/span&gt;
mempalace mine ~/chats/ &lt;span class="nt"&gt;--mode&lt;/span&gt; convos &lt;span class="nt"&gt;--extract&lt;/span&gt; general  &lt;span class="c"&gt;# classified import&lt;/span&gt;

&lt;span class="c"&gt;# Search&lt;/span&gt;
mempalace search &lt;span class="s2"&gt;"why did we switch to GraphQL"&lt;/span&gt;

&lt;span class="c"&gt;# Generate startup context&lt;/span&gt;
mempalace wake-up &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; context.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supports importing Claude conversations, ChatGPT exports, and Slack exports. Large files can be split first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mempalace &lt;span class="nb"&gt;split&lt;/span&gt; ~/chats/ &lt;span class="nt"&gt;--dry-run&lt;/span&gt;   &lt;span class="c"&gt;# preview&lt;/span&gt;
mempalace &lt;span class="nb"&gt;split&lt;/span&gt; ~/chats/             &lt;span class="c"&gt;# split into individual sessions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compared to CLAUDE.md
&lt;/h2&gt;

&lt;p&gt;CLAUDE.md is a flat text file — all information mixed together, no temporal awareness, fully loaded on every startup. MemPalace is a structured memory vault with layered loading, temporal knowledge graphs, and semantic search.&lt;/p&gt;

&lt;p&gt;That said, MemPalace isn't perfect. It requires a Python environment, MCP server setup, and hook configuration. If you only need to remember a few coding conventions, CLAUDE.md is sufficient. MemPalace's value shows in long-term, large-scale, cross-project memory management.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/milla-jovovich/mempalace" rel="noopener noreferrer"&gt;MemPalace GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/milla-jovovich/mempalace#aaak-compression" rel="noopener noreferrer"&gt;AAAK Compression Format Spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/milla-jovovich/mempalace#benchmarks" rel="noopener noreferrer"&gt;LongMemEval Benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol Specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>claudecode</category>
      <category>mcp</category>
      <category>python</category>
    </item>
    <item>
      <title>NodeWarden: Bitwarden on Cloudflare Workers — No Server Required</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Tue, 07 Apr 2026 09:36:33 +0000</pubDate>
      <link>https://dev.to/recca0120/nodewarden-bitwarden-on-cloudflare-workers-no-server-required-1677</link>
      <guid>https://dev.to/recca0120/nodewarden-bitwarden-on-cloudflare-workers-no-server-required-1677</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/07/nodewarden-bitwarden-cloudflare-workers/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Self-hosting Bitwarden gives you two paths. The official version requires Docker and eats memory. &lt;a href="https://github.com/dani-garcia/vaultwarden" rel="noopener noreferrer"&gt;Vaultwarden&lt;/a&gt; rewrites it in Rust, much lighter, but you still need a VPS, HTTPS configuration, regular updates, and database backups.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/shuaiplus/nodewarden" rel="noopener noreferrer"&gt;NodeWarden&lt;/a&gt; takes a third path: run directly on Cloudflare Workers. No VPS, no SSL management, no uptime monitoring. Cloudflare's free tier is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Differs from Vaultwarden
&lt;/h2&gt;

&lt;p&gt;Vaultwarden is the most popular third-party Bitwarden server, written in Rust, running in Docker. NodeWarden is written in TypeScript, running on Cloudflare Workers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Vaultwarden&lt;/th&gt;
&lt;th&gt;NodeWarden&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Docker / VPS&lt;/td&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite / MySQL / PostgreSQL&lt;/td&gt;
&lt;td&gt;Cloudflare D1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attachment storage&lt;/td&gt;
&lt;td&gt;Local filesystem&lt;/td&gt;
&lt;td&gt;R2 or KV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL&lt;/td&gt;
&lt;td&gt;Self-configured&lt;/td&gt;
&lt;td&gt;Cloudflare handles it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;Manual updates and backups&lt;/td&gt;
&lt;td&gt;Fork + auto-sync upstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;VPS monthly fee&lt;/td&gt;
&lt;td&gt;Cloudflare free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Organizations/Collections&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest difference is operational burden. Vaultwarden needs you to maintain a VPS. NodeWarden is fully serverless. The downside is no organization or collection features, making it unsuitable for teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Architecture
&lt;/h2&gt;

&lt;p&gt;NodeWarden is built entirely on Cloudflare's infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute&lt;/strong&gt;: Cloudflare Workers (serverless)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: D1 (Cloudflare's SQLite)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attachment storage&lt;/strong&gt;: R2 (object storage) or KV (key-value)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Preact (original Web Vault interface)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two storage options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Credit card required&lt;/th&gt;
&lt;th&gt;Max attachment size&lt;/th&gt;
&lt;th&gt;Free quota&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;100 MB (adjustable)&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;25 MiB (hard limit)&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you don't want to add a credit card, use KV mode. 1 GB free quota is more than enough for personal password management. Only consider R2 if you need large attachments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Comparison
&lt;/h2&gt;

&lt;p&gt;Compared to official Bitwarden, everything needed for personal use is covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web Vault password manager interface&lt;/li&gt;
&lt;li&gt;Full sync (&lt;code&gt;/api/sync&lt;/code&gt;), compatible with official clients&lt;/li&gt;
&lt;li&gt;Attachment upload and download&lt;/li&gt;
&lt;li&gt;Send feature (text and files)&lt;/li&gt;
&lt;li&gt;Import/export (Bitwarden JSON/CSV, ZIP with attachments)&lt;/li&gt;
&lt;li&gt;TOTP and Steam TOTP&lt;/li&gt;
&lt;li&gt;Multi-user (invitation code registration)&lt;/li&gt;
&lt;li&gt;Password hints (viewable directly in web, no email required)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NodeWarden adds one feature the official version lacks: a &lt;strong&gt;cloud backup center&lt;/strong&gt;. Supports WebDAV and E3 protocol for scheduled backups, including &lt;code&gt;db.json&lt;/code&gt;, &lt;code&gt;manifest.json&lt;/code&gt;, and &lt;code&gt;attachments/&lt;/code&gt; directory. During restoration, missing attachments are safely skipped without leaving broken records.&lt;/p&gt;

&lt;p&gt;Not supported: organizations, collections, permission management, SSO, SCIM, enterprise directories. These are team features unnecessary for personal use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client Compatibility
&lt;/h3&gt;

&lt;p&gt;Tested and working:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows desktop&lt;/li&gt;
&lt;li&gt;Mobile apps (iOS / Android)&lt;/li&gt;
&lt;li&gt;Browser extensions&lt;/li&gt;
&lt;li&gt;Linux desktop&lt;/li&gt;
&lt;li&gt;macOS desktop (not fully verified)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Web Deployment (Recommended)
&lt;/h3&gt;

&lt;p&gt;The simplest approach, no local tools needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fork the &lt;a href="https://github.com/shuaiplus/nodewarden" rel="noopener noreferrer"&gt;NodeWarden repo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Go to the &lt;a href="https://dash.cloudflare.com" rel="noopener noreferrer"&gt;Cloudflare Workers console&lt;/a&gt; and create a new project&lt;/li&gt;
&lt;li&gt;Choose Continue with GitHub, point to your forked repo&lt;/li&gt;
&lt;li&gt;Keep default settings and deploy&lt;/li&gt;
&lt;li&gt;For KV mode, change the deploy command to &lt;code&gt;npm run deploy:kv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set the &lt;code&gt;JWT_SECRET&lt;/code&gt; environment variable (at least 32 random characters)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole process takes under five minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/shuaiplus/NodeWarden.git
&lt;span class="nb"&gt;cd &lt;/span&gt;NodeWarden
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npx wrangler login

&lt;span class="c"&gt;# R2 mode&lt;/span&gt;
npm run deploy

&lt;span class="c"&gt;# KV mode&lt;/span&gt;
npm run deploy:kv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev      &lt;span class="c"&gt;# R2 mode&lt;/span&gt;
npm run dev:kv   &lt;span class="c"&gt;# KV mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Automatic Updates
&lt;/h2&gt;

&lt;p&gt;After forking, enable the &lt;code&gt;Sync upstream&lt;/code&gt; workflow in GitHub Actions. It auto-syncs with upstream daily at 3am. For manual updates, click Sync fork → Update branch on your fork page.&lt;/p&gt;

&lt;h2&gt;
  
  
  NodeWarden or Vaultwarden
&lt;/h2&gt;

&lt;p&gt;If you already have a stable VPS, Vaultwarden is more feature-complete with a larger community. Organizations, collections, and login 2FA are all supported.&lt;/p&gt;

&lt;p&gt;NodeWarden fits these scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No VPS management&lt;/strong&gt;. No server means no maintenance — no uptime worries, no expired SSL certs, no full disks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero budget&lt;/strong&gt;. Cloudflare's free tier is plenty for personal use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo user&lt;/strong&gt;. No need for organizations and permission management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offsite backups&lt;/strong&gt;. Built-in WebDAV backup is more convenient than Vaultwarden's approach&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main risk is that your password vault runs on Cloudflare's infrastructure. D1 and Workers are relatively new services. While Cloudflare probably won't shut them down suddenly, free tier limits and terms can change anytime. Regular WebDAV backups are essential.&lt;/p&gt;

&lt;p&gt;Also note that NodeWarden hasn't undergone the same level of community security review as Vaultwarden. Password managers are high-sensitivity applications — assess the risk yourself before using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/shuaiplus/nodewarden" rel="noopener noreferrer"&gt;NodeWarden GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/dani-garcia/vaultwarden" rel="noopener noreferrer"&gt;Vaultwarden — Rust-based Bitwarden-compatible Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/d1/" rel="noopener noreferrer"&gt;Cloudflare D1 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/r2/" rel="noopener noreferrer"&gt;Cloudflare R2 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bitwarden.com/" rel="noopener noreferrer"&gt;Bitwarden Official Website&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cloudflare</category>
      <category>bitwarden</category>
      <category>typescript</category>
      <category>serverless</category>
    </item>
    <item>
      <title>claude-view: Mission Control for Claude Code — Live Session Monitoring, Cost Tracking, and Analytics</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Tue, 07 Apr 2026 09:32:23 +0000</pubDate>
      <link>https://dev.to/recca0120/claude-view-mission-control-for-claude-code-live-session-monitoring-cost-tracking-and-analytics-14ik</link>
      <guid>https://dev.to/recca0120/claude-view-mission-control-for-claude-code-live-session-monitoring-cost-tracking-and-analytics-14ik</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/07/claude-view-mission-control/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After using Claude Code for a while, the most common question I get is "how much are you spending per month?" Honestly, I can't answer that. Claude Code's terminal interface doesn't show cumulative token costs, how many sub-agents ran, or which session burned the most money.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tombelieber/claude-view" rel="noopener noreferrer"&gt;claude-view&lt;/a&gt; fills that gap. One command opens a dashboard that monitors every Claude Code session on your machine in real-time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx claude-view
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What It Shows You
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Live Session Monitoring
&lt;/h3&gt;

&lt;p&gt;Open the dashboard and you see all running Claude Code sessions, each card showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Last message&lt;/li&gt;
&lt;li&gt;Model in use (Opus, Sonnet, Haiku)&lt;/li&gt;
&lt;li&gt;Current cost and token count&lt;/li&gt;
&lt;li&gt;Context window utilization (live percentage)&lt;/li&gt;
&lt;li&gt;Prompt cache countdown timer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cards can be arranged in multiple layouts: Grid, List, Kanban, Monitor. Kanban mode groups sessions by project/branch in swimlanes — great when running multiple projects simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversation Browser
&lt;/h3&gt;

&lt;p&gt;Click into any session for the full conversation history. Unlike the terminal view, claude-view visualizes tool calls — file reads, edits, bash commands, and MCP calls each get their own cards.&lt;/p&gt;

&lt;p&gt;A Developer Mode toggle reveals hook metadata, event cards, and raw JSON. Invaluable for debugging.&lt;/p&gt;

&lt;p&gt;Conversations can be exported as Markdown for documentation or feeding back to Claude for continuation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sub-agent Tree View
&lt;/h3&gt;

&lt;p&gt;Claude Code spawns sub-agents for subtasks. In the terminal you only see one level. claude-view renders the full tree structure with per-agent cost and token breakdowns at a glance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full-Text Search
&lt;/h3&gt;

&lt;p&gt;The search engine is &lt;a href="https://github.com/quickwit-oss/tantivy" rel="noopener noreferrer"&gt;Tantivy&lt;/a&gt;, a Rust-native Lucene-class full-text indexer. Search response times across 1,500 sessions stay under 50ms.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Cmd+K&lt;/code&gt; opens a command palette for quick session jumping and view switching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics: Where Did the Money Go
&lt;/h2&gt;

&lt;p&gt;This is where I see the most value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard Metrics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Week-over-week session count, token usage, and cost comparison&lt;/li&gt;
&lt;li&gt;90-day GitHub-style activity heatmap&lt;/li&gt;
&lt;li&gt;Most-used skills, commands, and MCP tools leaderboards&lt;/li&gt;
&lt;li&gt;Most active projects bar chart&lt;/li&gt;
&lt;li&gt;Cross-session totals for edits, reads, and bash commands&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI Contributions Tracking
&lt;/h3&gt;

&lt;p&gt;This feature quantifies Claude Code's output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lines added/removed, files touched, commit counts&lt;/li&gt;
&lt;li&gt;Cost per commit, cost per session, cost per line ROI&lt;/li&gt;
&lt;li&gt;Opus vs Sonnet vs Haiku side-by-side comparison&lt;/li&gt;
&lt;li&gt;Re-edit rate: tracking whether your prompt quality is improving&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also an experimental AI Fluency Score (0-100), calculated from your session history to measure how effectively you use AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  85 MCP Tools
&lt;/h2&gt;

&lt;p&gt;claude-view ships a plugin (&lt;code&gt;@claude-view/plugin&lt;/code&gt;) that auto-loads with every Claude Code session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin add @claude-view/plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin provides 85 MCP tools: 8 hand-crafted core tools plus 77 auto-generated from the OpenAPI spec.&lt;/p&gt;

&lt;p&gt;The core 8:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;list_sessions&lt;/code&gt;, &lt;code&gt;get_session&lt;/code&gt;, &lt;code&gt;search_sessions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_stats&lt;/code&gt;, &lt;code&gt;get_fluency_score&lt;/code&gt;, &lt;code&gt;get_token_stats&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_live_sessions&lt;/code&gt;, &lt;code&gt;get_live_summary&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once installed, you can ask Claude Code "how much did I spend today" or "which session took the longest last week" — it queries claude-view via MCP.&lt;/p&gt;

&lt;h3&gt;
  
  
  9 Built-in Skills
&lt;/h3&gt;

&lt;p&gt;Beyond MCP tools, there are 9 built-in skills:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/session-recap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Summarize commits, metrics, duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/daily-cost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Today's spending and tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/standup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multi-session work log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/coaching&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI usage tips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/insights&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Behavioral pattern analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/project-overview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cross-session project summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Natural language search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/export-data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSV/JSON exports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/team-status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Team activity overview&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Technical Architecture
&lt;/h2&gt;

&lt;p&gt;claude-view uses Rust for the backend and React for the frontend.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web framework&lt;/td&gt;
&lt;td&gt;Axum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search engine&lt;/td&gt;
&lt;td&gt;Tantivy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File I/O&lt;/td&gt;
&lt;td&gt;Memory-mapped I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;td&gt;SSE + WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React + Vite + Dockview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monorepo&lt;/td&gt;
&lt;td&gt;Turbo + Bun&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Performance benchmarks (M-series Mac, 1,493 sessions):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;claude-view&lt;/th&gt;
&lt;th&gt;Typical Electron Dashboard&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Download&lt;/td&gt;
&lt;td&gt;~10 MB&lt;/td&gt;
&lt;td&gt;150-300 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On disk&lt;/td&gt;
&lt;td&gt;~27 MB&lt;/td&gt;
&lt;td&gt;300-500 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup&lt;/td&gt;
&lt;td&gt;&amp;lt;500 ms&lt;/td&gt;
&lt;td&gt;3-8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM&lt;/td&gt;
&lt;td&gt;~50 MB&lt;/td&gt;
&lt;td&gt;300-800 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index 1,500 sessions&lt;/td&gt;
&lt;td&gt;&amp;lt;1 s&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rust's mmap + SIMD-accelerated JSONL parsing enables zero-copy from parse to response. Compared to Electron dashboards, it's 10x smaller and uses 6x less memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Three options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Recommended&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.claudeview.ai/install.sh | sh

&lt;span class="c"&gt;# Or via npx&lt;/span&gt;
npx claude-view

&lt;span class="c"&gt;# Install plugin (auto-starts with Claude Code)&lt;/span&gt;
claude plugin add @claude-view/plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only prerequisite: Claude Code installed. Dashboard runs at &lt;code&gt;http://localhost:47892&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All data stays local, zero telemetry, no account required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compared to Other Tools
&lt;/h2&gt;

&lt;p&gt;There are similar tools, but with different positioning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ccusage&lt;/strong&gt;: CLI tool, token stats only, no GUI, no live monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;opcode&lt;/strong&gt;: Tauri-based GUI with session management but no multi-session chat browsing or search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodePilot&lt;/strong&gt;: Electron chat UI for interacting &lt;em&gt;with&lt;/em&gt; Claude Code, not monitoring it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;claude-view is positioned as monitoring and analytics. If you already work in the terminal with Claude Code, it doesn't change your workflow — it just shows you more information.&lt;/p&gt;

&lt;p&gt;I previously covered &lt;a href="https://dev.to/en/2026/04/07/aionui-ai-cowork-app/"&gt;AionUi&lt;/a&gt;, which unifies multiple agents into one GUI. claude-view takes a different approach: keep working in the terminal, but add a dashboard for tracking. The two can work together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Needs This
&lt;/h2&gt;

&lt;p&gt;If you use Claude Code occasionally, you probably don't need this tool.&lt;/p&gt;

&lt;p&gt;But if you use it daily, run multiple sessions simultaneously, and want to know where the money goes, which model gives the best ROI, and whether your prompt quality is improving — claude-view provides information density that the terminal simply can't match.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tombelieber/claude-view" rel="noopener noreferrer"&gt;claude-view GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claudeview.ai" rel="noopener noreferrer"&gt;claude-view Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/quickwit-oss/tantivy" rel="noopener noreferrer"&gt;Tantivy Full-Text Search Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tokio-rs/axum" rel="noopener noreferrer"&gt;Axum Web Framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claudecode</category>
      <category>agents</category>
      <category>mcp</category>
      <category>rust</category>
    </item>
    <item>
      <title>bb-browser: No Scraping, No API Keys — Your Browser Is the API</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Tue, 07 Apr 2026 03:37:23 +0000</pubDate>
      <link>https://dev.to/recca0120/bb-browser-no-scraping-no-api-keys-your-browser-is-the-api-1j8l</link>
      <guid>https://dev.to/recca0120/bb-browser-no-scraping-no-api-keys-your-browser-is-the-api-1j8l</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/07/bb-browser-your-browser-is-the-api/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Want Twitter search results? Traditional approaches give you three paths: apply for an API key (rate-limited), write a scraper (get IP-banned), or use Playwright with a headless browser (detected as non-human).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/epiral/bb-browser" rel="noopener noreferrer"&gt;bb-browser&lt;/a&gt; takes a fourth path: use the Chrome you already have open. You're logged into Twitter, the cookies are right there, and bb-browser runs &lt;code&gt;fetch()&lt;/code&gt; inside that tab. From the website's perspective, it's just you browsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Differs from Scrapers and Playwright
&lt;/h2&gt;

&lt;p&gt;Let's get the differences clear.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;bb-browser&lt;/th&gt;
&lt;th&gt;Playwright / Selenium&lt;/th&gt;
&lt;th&gt;Scrapers (requests, Scrapy)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser&lt;/td&gt;
&lt;td&gt;Your real Chrome&lt;/td&gt;
&lt;td&gt;Isolated headless browser&lt;/td&gt;
&lt;td&gt;No browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login state&lt;/td&gt;
&lt;td&gt;Already logged in&lt;/td&gt;
&lt;td&gt;Must re-login or inject cookies&lt;/td&gt;
&lt;td&gt;Manual cookie handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anti-bot detection&lt;/td&gt;
&lt;td&gt;Invisible (it IS the real user)&lt;/td&gt;
&lt;td&gt;Easily detected&lt;/td&gt;
&lt;td&gt;Easily blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fingerprint&lt;/td&gt;
&lt;td&gt;Your real fingerprint&lt;/td&gt;
&lt;td&gt;Headless browser fingerprint&lt;/td&gt;
&lt;td&gt;No fingerprint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: bb-browser doesn't launch a new browser instance. It connects to your running Chrome via CDP (Chrome DevTools Protocol) and injects code into tabs. The User-Agent, cookies, and TLS fingerprint the website sees are all real — because it is your actual browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Agent (Claude Code, Codex, Cursor)
         │ CLI or MCP (stdio)
         ▼
bb-browser CLI ──HTTP──▶ Daemon ──CDP WebSocket──▶ Real Browser
                            │
                     ┌──────┴──────┐
                     │ Per-tab event│
                     │ cache (net,  │
                     │ console)     │
                     └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;bb-browser runs a daemon (default &lt;code&gt;127.0.0.1:19824&lt;/code&gt;) that communicates with Chrome via CDP WebSocket. CLI commands go to the daemon, which executes them in the corresponding tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation and Basic Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; bb-browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull community adapters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bb-browser site update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try a command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bb-browser site zhihu/hot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a Zhihu tab (if you're already logged in), uses your cookies to fetch the trending questions list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured Output
&lt;/h3&gt;

&lt;p&gt;All commands support &lt;code&gt;--json&lt;/code&gt; and &lt;code&gt;--jq&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bb-browser site xueqiu/hot-stock 5 &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.items[] | {name, changePercent}'&lt;/span&gt;
&lt;span class="c"&gt;# {"name":"云天化","changePercent":"2.08%"}&lt;/span&gt;
&lt;span class="c"&gt;# {"name":"东芯股份","changePercent":"-7.60%"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Browser Operations
&lt;/h3&gt;

&lt;p&gt;Beyond running adapters, you can directly control the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bb-browser open https://example.com     &lt;span class="c"&gt;# Open URL&lt;/span&gt;
bb-browser snapshot &lt;span class="nt"&gt;-i&lt;/span&gt;                  &lt;span class="c"&gt;# Accessibility tree snapshot&lt;/span&gt;
bb-browser click @3                     &lt;span class="c"&gt;# Click element&lt;/span&gt;
bb-browser fill @5 &lt;span class="s2"&gt;"hello"&lt;/span&gt;             &lt;span class="c"&gt;# Fill input&lt;/span&gt;
bb-browser &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"document.title"&lt;/span&gt;       &lt;span class="c"&gt;# Execute JavaScript&lt;/span&gt;
bb-browser fetch URL &lt;span class="nt"&gt;--json&lt;/span&gt;            &lt;span class="c"&gt;# Authenticated fetch&lt;/span&gt;
bb-browser screenshot                  &lt;span class="c"&gt;# Take screenshot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  36 Platforms, 103 Commands
&lt;/h2&gt;

&lt;p&gt;bb-browser's adapters cover a wide range:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt;: Google, Baidu, Bing, DuckDuckGo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social&lt;/strong&gt;: Twitter/X, Reddit, Weibo, Xiaohongshu, LinkedIn&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev&lt;/strong&gt;: GitHub, StackOverflow, Hacker News, npm, PyPI, arXiv, V2EX, Dev.to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;News&lt;/strong&gt;: BBC, Reuters, 36kr, Toutiao&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video&lt;/strong&gt;: YouTube, Bilibili&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finance&lt;/strong&gt;: Xueqiu, Yahoo Finance, Eastmoney&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge&lt;/strong&gt;: Wikipedia, Zhihu&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each adapter is a single JavaScript file, community-driven. To add a new platform, write a JS file and submit it to the &lt;code&gt;bb-sites&lt;/code&gt; repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Levels of Adapter Complexity
&lt;/h2&gt;

&lt;p&gt;Not every website is equally straightforward. bb-browser categorizes adapters into three levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Dev Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Level 1&lt;/td&gt;
&lt;td&gt;Direct fetch with cookies&lt;/td&gt;
&lt;td&gt;Reddit, GitHub&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Level 2&lt;/td&gt;
&lt;td&gt;Bearer token + CSRF extraction&lt;/td&gt;
&lt;td&gt;Twitter, Zhihu&lt;/td&gt;
&lt;td&gt;~3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Level 3&lt;/td&gt;
&lt;td&gt;Webpack injection or Pinia store&lt;/td&gt;
&lt;td&gt;Twitter search&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Level 1 is simplest — some sites' APIs work with just cookies. Level 3 is most complex, requiring reverse engineering of frontend bundles, extracting data from Webpack's &lt;code&gt;__webpack_require__&lt;/code&gt; or Vue's Pinia store.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Server for AI Agents
&lt;/h2&gt;

&lt;p&gt;This is bb-browser's most compelling use case. Configure it as an MCP server, and Claude Code or Cursor can directly access any website your browser can see.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bb-browser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bb-browser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once configured, you can tell Claude Code "search arXiv for recent RAG papers" and it will search through bb-browser using your real browser.&lt;/p&gt;

&lt;p&gt;Without bb-browser, an AI agent can only work with files and the terminal. With bb-browser, it can access the entire internet — as you.&lt;/p&gt;

&lt;p&gt;I previously wrote about &lt;a href="https://dev.to/en/2026/03/15/cli-anything-agent-native-cli/"&gt;CLI-Anything&lt;/a&gt;, which wraps desktop software as CLIs for agents to call, and &lt;a href="https://dev.to/en/2026/04/07/aionui-ai-cowork-app/"&gt;AionUi&lt;/a&gt;, which provides a unified interface for managing multiple agents. bb-browser extends agent capability from yet another angle: letting it browse the web using your real browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to Consider
&lt;/h2&gt;

&lt;p&gt;A few things to think through before using it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It uses your real account&lt;/strong&gt;. bb-browser acts on your behalf. If the operation frequency is too high, your account might get flagged. It's not an invisible scraper — it IS you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;. The daemon binds to localhost by default, but if you open it to &lt;code&gt;0.0.0.0&lt;/code&gt;, anyone who can reach your machine can control your browser. Use Tailscale or ZeroTier for safer remote access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter quality varies&lt;/strong&gt;. Community-driven means broad coverage, but some adapters may lag behind website redesigns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS users&lt;/strong&gt;: watch for IPv6 issues — add &lt;code&gt;--host 127.0.0.1&lt;/code&gt; to the daemon command&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to Use It
&lt;/h2&gt;

&lt;p&gt;bb-browser isn't for scraping millions of records. Use Scrapy for that.&lt;/p&gt;

&lt;p&gt;It's ideal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Giving AI agents web access without applying for API keys one by one&lt;/li&gt;
&lt;li&gt;Quickly pulling structured data from platforms you're already logged into&lt;/li&gt;
&lt;li&gt;Cross-platform research — query arXiv, Twitter, GitHub, Zhihu, and StackOverflow in under a minute&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One command does what used to require writing a scraper, and it won't get blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/epiral/bb-browser" rel="noopener noreferrer"&gt;bb-browser GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nicepkg/bb-sites" rel="noopener noreferrer"&gt;bb-sites Community Adapters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chromedevtools.github.io/devtools-protocol/" rel="noopener noreferrer"&gt;Chrome DevTools Protocol Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol Specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>mcp</category>
      <category>browserautomation</category>
      <category>cli</category>
    </item>
    <item>
      <title>AionUi: One Interface for 12+ AI Agents — A Free, Open-Source Cowork Desktop App</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Tue, 07 Apr 2026 03:05:44 +0000</pubDate>
      <link>https://dev.to/recca0120/aionui-one-interface-for-12-ai-agents-a-free-open-source-cowork-desktop-app-1mf2</link>
      <guid>https://dev.to/recca0120/aionui-one-interface-for-12-ai-agents-a-free-open-source-cowork-desktop-app-1mf2</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/07/aionui-ai-cowork-app/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You've got Claude Code installed. Also Codex. Maybe Qwen Code for Chinese-language tasks. Each tool gets its own terminal window, MCP configs are duplicated across tools, and conversation history is scattered everywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/iOfficeAI/AionUi" rel="noopener noreferrer"&gt;AionUi&lt;/a&gt; tackles exactly this: one desktop app that brings all your AI agents under a single interface. Free, open-source, Apache 2.0 licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;AionUi is a cross-platform desktop app built with Electron + React, supporting macOS, Windows, and Linux. Its core purpose is unified management of multiple AI coding agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supported Agents
&lt;/h3&gt;

&lt;p&gt;AionUi auto-detects CLI tools installed on your machine. Currently supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code, Codex, Qwen Code, Goose AI, OpenClaw, Augment Code&lt;/li&gt;
&lt;li&gt;iFlow CLI, CodeBuddy, Kimi CLI, OpenCode, Factory Droid, GitHub Copilot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over 12 agents total. No extra configuration needed — install the CLI and it shows up in AionUi. If you don't have any CLI tools installed, AionUi has its own built-in agent that works with Google login or API key authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  20+ Model Platforms
&lt;/h3&gt;

&lt;p&gt;Wide model selection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Major platforms&lt;/strong&gt;: Gemini, Claude, OpenAI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud&lt;/strong&gt;: AWS Bedrock&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chinese platforms&lt;/strong&gt;: Dashscope (Qwen), Zhipu, Moonshot (Kimi), Baidu Qianfan, Tencent Hunyuan, ModelScope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local models&lt;/strong&gt;: Ollama, LM Studio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're in mainland China and can't easily access OpenAI or Claude APIs, just switch to Dashscope or Zhipu. For fully offline work, run Ollama with local models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure MCP Once, Sync Everywhere
&lt;/h3&gt;

&lt;p&gt;This is the most practical design choice. Configure MCP (Model Context Protocol) tools once in AionUi, and all agents sync automatically. No more maintaining separate &lt;code&gt;mcp.json&lt;/code&gt; files for each agent — change it in one place, it applies everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  12 Built-in Professional Assistants
&lt;/h2&gt;

&lt;p&gt;AionUi isn't just an agent launcher. It comes with 12 pre-built assistants:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assistant&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cowork&lt;/td&gt;
&lt;td&gt;Automated task execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PPTX Generator&lt;/td&gt;
&lt;td&gt;Presentation creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF to PPT&lt;/td&gt;
&lt;td&gt;Format conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3D Game&lt;/td&gt;
&lt;td&gt;Single-file game prototyping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI/UX Pro Max&lt;/td&gt;
&lt;td&gt;57 styles, 95 color palettes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beautiful Mermaid&lt;/td&gt;
&lt;td&gt;Flowcharts, sequence diagrams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planning with Files&lt;/td&gt;
&lt;td&gt;File-based project planning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Office features (PPT, Word, Excel) are powered by OfficeCLI, producing editable &lt;code&gt;.pptx&lt;/code&gt;, &lt;code&gt;.docx&lt;/code&gt;, and &lt;code&gt;.xlsx&lt;/code&gt; files — not PDF screenshots. PPT output even supports Morph transition animations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduled Tasks: 24/7 Automation
&lt;/h2&gt;

&lt;p&gt;This feature is uncommon in agent tools. You can set up scheduled tasks using natural language, like "every morning at 9am, summarize yesterday's Git commit log." AionUi converts it to a cron expression and runs it automatically.&lt;/p&gt;

&lt;p&gt;Each scheduled task is bound to a conversation, maintaining context. Results are sent back to the conversation window, and can also be pushed to Telegram, Lark (Feishu), or DingTalk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview Panel
&lt;/h2&gt;

&lt;p&gt;AionUi has a built-in file preview supporting many formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Documents&lt;/strong&gt;: PDF, Word, Excel, PowerPoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code&lt;/strong&gt;: 30+ languages with syntax highlighting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt;: PNG, JPG, SVG, WebP, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markup&lt;/strong&gt;: Markdown and HTML with live editing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also tracks file changes, shows Git version history, and supports one-click rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebUI Remote Access
&lt;/h2&gt;

&lt;p&gt;You don't have to sit in front of your computer to use a desktop app. AionUi can serve a WebUI, accessible via QR code or password login from your phone or another computer. Supports both LAN and cross-network access.&lt;/p&gt;

&lt;p&gt;Combined with Telegram, Lark, and DingTalk bot integration, you can send commands to AI agents from your phone and receive results in your chat groups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Electron&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS&lt;/td&gt;
&lt;td&gt;UnoCSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Vitest + Playwright&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite (local)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All data is stored locally in SQLite — nothing gets uploaded to any server. With local models (Ollama), the entire workflow can run completely offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Download the installer for your platform from &lt;a href="https://github.com/iOfficeAI/AionUi/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;. Homebrew is also supported on macOS.&lt;/p&gt;

&lt;p&gt;Once installed, just open the app. If Claude Code or Codex is already on your machine, AionUi detects them automatically. Otherwise, use the built-in agent with an API key or Google login to get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compared to Using Claude Code Alone
&lt;/h2&gt;

&lt;p&gt;Claude Code is powerful, but it's a terminal tool. AionUi doesn't aim to replace it — it puts Claude Code alongside other agents in one managed workspace.&lt;/p&gt;

&lt;p&gt;Key differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent&lt;/strong&gt;: Claude Code only runs Claude; AionUi runs multiple agents simultaneously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GUI&lt;/strong&gt;: Full desktop interface with file preview, Office generation, and image processing built in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt;: Claude Code has no built-in scheduling; AionUi runs tasks 24/7 automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price&lt;/strong&gt;: Claude Code requires API costs or a $100/month subscription; AionUi itself is free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model choice&lt;/strong&gt;: Not locked to one provider — 20+ platforms available&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're interested in the AI agent tool ecosystem, I previously wrote about &lt;a href="https://dev.to/en/2026/03/15/cli-anything-agent-native-cli/"&gt;CLI-Anything: A Universal Bridge for AI Agents to Operate Any Software&lt;/a&gt;, which approaches the problem from the opposite angle — wrapping existing software as CLIs for agents to call. AionUi takes the agent management perspective instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current State
&lt;/h2&gt;

&lt;p&gt;AionUi is iterating rapidly, with 4,400+ commits on GitHub. The community is active on Discord (English) and WeChat groups (Chinese).&lt;/p&gt;

&lt;p&gt;One caveat: while the star count is impressive, Electron apps typically have significant memory overhead. If you only use one agent, running the CLI in a terminal is lighter weight. AionUi's value shows when you genuinely need multiple agents, want GUI management, or require scheduling and Office generation features.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/iOfficeAI/AionUi" rel="noopener noreferrer"&gt;AionUi GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.aionui.com" rel="noopener noreferrer"&gt;AionUi Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.electronjs.org/" rel="noopener noreferrer"&gt;Electron Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>electron</category>
      <category>claudecode</category>
      <category>mcp</category>
    </item>
    <item>
      <title>League Period: A Swiss Army Knife for Time Intervals in PHP</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Mon, 06 Apr 2026 21:53:30 +0000</pubDate>
      <link>https://dev.to/recca0120/league-period-a-swiss-army-knife-for-time-intervals-in-php-3aog</link>
      <guid>https://dev.to/recca0120/league-period-a-swiss-army-knife-for-time-intervals-in-php-3aog</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/06/league-period/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Have you ever written this kind of code — checking whether two time intervals overlap?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$startA&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$endB&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$startB&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$endA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// overlap&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks simple, but add boundary conditions (include endpoints or not?), more intervals (what about three?), and gap detection (which time slots aren't covered?), and the code explodes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://period.thephpleague.com/" rel="noopener noreferrer"&gt;League Period&lt;/a&gt; wraps time intervals into immutable value objects with built-in overlap, containment, gap, and intersection operations. No hand-written logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require league/period
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires PHP 8.1+.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Intervals
&lt;/h2&gt;

&lt;h3&gt;
  
  
  From Dates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Period\Period&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Basic: specify start and end&lt;/span&gt;
&lt;span class="nv"&gt;$meeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 10:30'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Default is [start, end) — includes start, excludes end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  From Calendar Units
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// All of April 2026&lt;/span&gt;
&lt;span class="nv"&gt;$april&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Q1 2026&lt;/span&gt;
&lt;span class="nv"&gt;$q1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromQuarter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Full year 2026&lt;/span&gt;
&lt;span class="nv"&gt;$year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ISO week 15 of 2026&lt;/span&gt;
&lt;span class="nv"&gt;$week&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromIsoWeek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// A single day&lt;/span&gt;
&lt;span class="nv"&gt;$day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No need to figure out how many days April has or when the quarter starts — Period handles it.&lt;/p&gt;

&lt;h3&gt;
  
  
  From a Point + Duration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 2 hours forward from a point&lt;/span&gt;
&lt;span class="nv"&gt;$slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 14:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2 HOURS'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 30 minutes backward from a point&lt;/span&gt;
&lt;span class="nv"&gt;$before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 14:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'30 MINUTES'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Centered on a point, 1 hour total&lt;/span&gt;
&lt;span class="nv"&gt;$around&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 14:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1 HOUR'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Boundary Control
&lt;/h2&gt;

&lt;p&gt;Default is &lt;code&gt;[start, end)&lt;/code&gt; (include start, exclude end). You can change it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Period\Bounds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Include both ends&lt;/span&gt;
&lt;span class="nv"&gt;$closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Bounds&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;IncludeAll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Exclude both ends&lt;/span&gt;
&lt;span class="nv"&gt;$open&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Bounds&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;ExcludeAll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Exclude start, include end&lt;/span&gt;
&lt;span class="nv"&gt;$leftOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-30'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Bounds&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;ExcludeStartIncludeEnd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hotel booking systems often use &lt;code&gt;[checkin, checkout)&lt;/code&gt; — check-in day is included, check-out day is not. Meeting room reservations work the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containment Checks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$workday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 18:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Is a point inside the interval?&lt;/span&gt;
&lt;span class="nv"&gt;$workday&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 12:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nv"&gt;$workday&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 20:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// false&lt;/span&gt;

&lt;span class="c1"&gt;// Is one interval entirely inside another?&lt;/span&gt;
&lt;span class="nv"&gt;$lunch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 12:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 13:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$workday&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lunch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Overlap Detection
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$meetingA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 10:30'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$meetingB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 10:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 11:30'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$meetingC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 11:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 12:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$meetingA&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;overlaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$meetingB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// true (10:00-10:30 overlap)&lt;/span&gt;
&lt;span class="nv"&gt;$meetingA&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;overlaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$meetingC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more writing &lt;code&gt;if ($startA &amp;lt; $endB &amp;amp;&amp;amp; $startB &amp;lt; $endA)&lt;/code&gt; yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Operations
&lt;/h2&gt;

&lt;p&gt;Period implements Allen's Interval Algebra — 13 possible relations between two intervals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;meets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// a ends exactly where b starts&lt;/span&gt;
&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;overlaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// partial overlap&lt;/span&gt;
&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// a fully contains b&lt;/span&gt;
&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isDuring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// a is inside b (reverse of contains)&lt;/span&gt;
&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// same start, end, and bounds&lt;/span&gt;
&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;abuts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// adjacent (meets or metBy)&lt;/span&gt;
&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;bordersOnStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;bordersOnEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Modification (Immutable)
&lt;/h2&gt;

&lt;p&gt;Period is immutable — all modifications return new objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 10:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Extend the end&lt;/span&gt;
&lt;span class="nv"&gt;$extended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$original&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;endingOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 11:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Move the start&lt;/span&gt;
&lt;span class="nv"&gt;$moved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$original&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;startingOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 08:30'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Expand (add 30 min before and after)&lt;/span&gt;
&lt;span class="nv"&gt;$expanded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$original&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'30 MINUTES'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Shift the whole interval (keep duration)&lt;/span&gt;
&lt;span class="nv"&gt;$shifted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$original&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 HOUR'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$original&lt;/code&gt; is untouched. Safe to pass around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting and Iteration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$april&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Iterate by day&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$april&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dateRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 DAY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$day&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Split a month into weeks&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$april&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;splitForward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 WEEK'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$week&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$week&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toIso80000&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sequence: Collection Operations on Multiple Intervals
&lt;/h2&gt;

&lt;p&gt;This is Period's most powerful feature. When you have a set of intervals and need to find gaps, intersections, or unions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\Period\Sequence&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 10:30'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;// Meeting A&lt;/span&gt;
    &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 11:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 12:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;// Meeting B&lt;/span&gt;
    &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 14:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 15:30'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;// Meeting C&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Finding Gaps
&lt;/h3&gt;

&lt;p&gt;"What time slots are free today?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$gaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sequence&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// 10:30-11:00, 12:00-14:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shift scheduling, meeting room availability, doctor appointment slots — &lt;code&gt;gaps()&lt;/code&gt; handles it in one line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding Intersections
&lt;/h3&gt;

&lt;p&gt;"Which time slots have two or more meetings overlapping?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$overlaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sequence&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;intersections&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Sorting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sequence&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Period&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Period&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;startDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Meeting Room Conflict Detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;hasConflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Period&lt;/span&gt; &lt;span class="nv"&gt;$newBooking&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Sequence&lt;/span&gt; &lt;span class="nv"&gt;$existing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$existing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$booking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$newBooking&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;overlaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$booking&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Available Slot Query
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getAvailableSlots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Period&lt;/span&gt; &lt;span class="nv"&gt;$workday&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Sequence&lt;/span&gt; &lt;span class="nv"&gt;$meetings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Sequence&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$meetings&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$workday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 18:00'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$meetings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 10:30'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 14:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 15:00'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAvailableSlots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$workday&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$meetings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 10:30-14:00, 15:00-18:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reports: Monthly Breakdown
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$year&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;splitForward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 MONTH'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getOrdersInPeriod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$month&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$month&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;startDate&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;': '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" orders&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Formatting
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$period&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-04-06 09:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-06 10:30'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ISO 8601&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$period&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// 2026-04-06T09:00:00+08:00/2026-04-06T10:30:00+08:00&lt;/span&gt;

&lt;span class="c1"&gt;// ISO 80000 (mathematical notation)&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$period&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toIso80000&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d H:i'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// [2026-04-06 09:00, 2026-04-06 10:30)&lt;/span&gt;

&lt;span class="c1"&gt;// JSON&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$period&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Handling time intervals with raw &lt;code&gt;DateTime&lt;/code&gt; is easy to write, hard to debug, and even harder to maintain. League Period wraps it all into a clean API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create intervals without calculating day counts&lt;/li&gt;
&lt;li&gt;Overlap checks without hand-written conditions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sequence&lt;/code&gt;'s &lt;code&gt;gaps()&lt;/code&gt; / &lt;code&gt;intersections()&lt;/code&gt; solve common scheduling, booking, and reporting needs&lt;/li&gt;
&lt;li&gt;Immutable design — safe to pass around without worrying about mutation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good fit for anything involving "a span of time": booking systems, shift scheduling, reports, event dates, contract validity periods.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://period.thephpleague.com/" rel="noopener noreferrer"&gt;League Period Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/thephpleague/period" rel="noopener noreferrer"&gt;league/period GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://packagist.org/packages/league/period" rel="noopener noreferrer"&gt;Packagist: league/period&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Allen%27s_interval_algebra" rel="noopener noreferrer"&gt;Allen's Interval Algebra on Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>opensource</category>
      <category>php</category>
      <category>programming</category>
    </item>
    <item>
      <title>EmDash: A Full-Stack TypeScript CMS Built on Astro + Cloudflare — Can It Replace WordPress?</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Mon, 06 Apr 2026 21:53:14 +0000</pubDate>
      <link>https://dev.to/recca0120/emdash-a-full-stack-typescript-cms-built-on-astro-cloudflare-can-it-replace-wordpress-5584</link>
      <guid>https://dev.to/recca0120/emdash-a-full-stack-typescript-cms-built-on-astro-cloudflare-can-it-replace-wordpress-5584</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/07/emdash-cms-astro-cloudflare/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;WordPress powers 43% of the web, but it was born in 2003. PHP + MySQL, plugins with full database access, content stored as HTML coupled to the DOM. After twenty years, it's fair to rethink the whole thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/emdash-cms/emdash" rel="noopener noreferrer"&gt;EmDash&lt;/a&gt; is that attempt. Full-stack TypeScript, running on Astro, backed by Cloudflare infrastructure. Still in beta, but the architecture is worth examining.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Differs from WordPress
&lt;/h2&gt;

&lt;p&gt;Let's start with the biggest differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sandboxed Plugin Isolation
&lt;/h3&gt;

&lt;p&gt;96% of WordPress security vulnerabilities come from plugins. The reason is straightforward: plugins run in the same PHP process as the core, with full access to the database and filesystem. One bad plugin exposes the entire site.&lt;/p&gt;

&lt;p&gt;EmDash uses Cloudflare Workers' Dynamic Worker Loaders for isolation. Each plugin must declare a capability manifest listing exactly what permissions it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;definePlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read:content&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email:send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content:afterSave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Can only operate within declared permissions&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;write:content&lt;/code&gt; declaration, no write access. This fundamentally limits a plugin's attack surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content Format: Portable Text Instead of HTML
&lt;/h3&gt;

&lt;p&gt;WordPress stores content as HTML. Seems intuitive, but the problem is that HTML is tightly coupled to presentation. If you want the same content for an app, email, or API, you have to re-parse the DOM.&lt;/p&gt;

&lt;p&gt;EmDash uses &lt;a href="https://www.portabletext.org/" rel="noopener noreferrer"&gt;Portable Text&lt;/a&gt;, storing content as structured JSON. One piece of content can be processed by different renderers without reverse-engineering semantics from HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full-Stack TypeScript
&lt;/h3&gt;

&lt;p&gt;WordPress is PHP with JavaScript layered on top for the frontend. EmDash is TypeScript from schema definition to frontend rendering, and schema changes generate types automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx emdash types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command generates TypeScript type definitions from the database schema. Change the schema, and your IDE immediately shows type errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  No Database Lock-in
&lt;/h3&gt;

&lt;p&gt;EmDash uses &lt;a href="https://kysely.dev/" rel="noopener noreferrer"&gt;Kysely&lt;/a&gt; as its database abstraction layer, supporting multiple SQL dialects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Session&lt;/th&gt;
&lt;th&gt;Plugin Isolation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;D1&lt;/td&gt;
&lt;td&gt;R2&lt;/td&gt;
&lt;td&gt;KV&lt;/td&gt;
&lt;td&gt;Worker isolates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;SQLite / PostgreSQL&lt;/td&gt;
&lt;td&gt;S3-compatible / local&lt;/td&gt;
&lt;td&gt;Redis / file&lt;/td&gt;
&lt;td&gt;In-process mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Want to run on Cloudflare? Use D1 + R2. Want to self-host? Use SQLite + local filesystem. No vendor lock-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Astro Integration
&lt;/h3&gt;

&lt;p&gt;EmDash is an Astro integration, configured like any other Astro plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// astro.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;emdash&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emdash/astro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;d1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emdash/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;emdash&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})]&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Querying content uses &lt;code&gt;getEmDashCollection&lt;/code&gt;, with syntax similar to Astro's Content Collections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts");
---

{posts.map((post) =&amp;gt; (
  &amp;lt;article&amp;gt;{post.data.title}&amp;lt;/article&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point: this data is fetched live from the database — no need to rebuild the entire site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Content Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Customizable content types (collections) built through the admin UI&lt;/li&gt;
&lt;li&gt;TipTap rich text editor&lt;/li&gt;
&lt;li&gt;Version control, drafts, scheduled publishing&lt;/li&gt;
&lt;li&gt;FTS5 full-text search&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Passkey-first (WebAuthn) by default, with OAuth and Magic link as alternatives. Four permission levels: Administrator, Editor, Author, Contributor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plugin Capabilities
&lt;/h3&gt;

&lt;p&gt;Plugins go beyond simple hooks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;KV storage&lt;/li&gt;
&lt;li&gt;Settings management&lt;/li&gt;
&lt;li&gt;Admin pages&lt;/li&gt;
&lt;li&gt;Dashboard widgets&lt;/li&gt;
&lt;li&gt;Custom block types&lt;/li&gt;
&lt;li&gt;API routes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI Integration
&lt;/h3&gt;

&lt;p&gt;EmDash natively supports MCP (Model Context Protocol), allowing direct content and schema manipulation through Claude or ChatGPT. Agent skills help with plugin and theme development.&lt;/p&gt;

&lt;h3&gt;
  
  
  WordPress Migration
&lt;/h3&gt;

&lt;p&gt;Supports WXR export import, REST API integration, and WordPress.com import. A &lt;code&gt;gutenberg-to-portable-text&lt;/code&gt; package converts Gutenberg blocks to Portable Text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create emdash@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs a scaffold where you pick a template. Three options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;blog&lt;/strong&gt;: categories, tags, full-text search, RSS, dark mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;marketing&lt;/strong&gt;: hero section, pricing cards, FAQ, contact form&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;portfolio&lt;/strong&gt;: project grid, tag filtering, case study pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/emdash-cms/emdash.git
&lt;span class="nb"&gt;cd &lt;/span&gt;emdash
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm build
pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; emdash-demo seed
pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; emdash-demo dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Admin panel at &lt;code&gt;http://localhost:4321/_emdash/admin&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Limitations
&lt;/h2&gt;

&lt;p&gt;EmDash is still in beta. A few things to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Worker isolate sandboxing requires a paid Cloudflare account; free accounts run in-process mode (no sandbox)&lt;/li&gt;
&lt;li&gt;The ecosystem is just starting — third-party plugins and themes can't compete with WordPress's library&lt;/li&gt;
&lt;li&gt;Documentation is still being built; some features require reading source code&lt;/li&gt;
&lt;li&gt;Portable Text has a steeper learning curve than HTML, especially for custom blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who Is This For
&lt;/h2&gt;

&lt;p&gt;If you're a heavy WordPress user who needs thousands of plugins and clients installing their own themes, EmDash can't replace that yet.&lt;/p&gt;

&lt;p&gt;But if you're already using Astro and want a backend that lets non-technical people edit content — without wiring up a headless CMS API — EmDash's integration is seamless. Full-stack TypeScript, types that update with schema changes, and near-zero-config Cloudflare deployment.&lt;/p&gt;

&lt;p&gt;It's not trying to kill WordPress, but it demonstrates what a CMS designed in 2026 can look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/emdash-cms/emdash" rel="noopener noreferrer"&gt;EmDash GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.portabletext.org/" rel="noopener noreferrer"&gt;Portable Text Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kysely.dev/" rel="noopener noreferrer"&gt;Kysely - Type-safe SQL Query Builder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build/" rel="noopener noreferrer"&gt;Astro Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/workers/" rel="noopener noreferrer"&gt;Cloudflare Workers Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>astro</category>
      <category>cloudflare</category>
      <category>typescript</category>
      <category>cms</category>
    </item>
    <item>
      <title>Typer: Build CLIs With Type Hints, No argparse API to Memorize</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Thu, 02 Apr 2026 20:39:15 +0000</pubDate>
      <link>https://dev.to/recca0120/typer-build-clis-with-type-hints-no-argparse-api-to-memorize-2l3p</link>
      <guid>https://dev.to/recca0120/typer-build-clis-with-type-hints-no-argparse-api-to-memorize-2l3p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/04/02/typer-cli/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After accumulating scripts in a project, at some point you want to consolidate them into one CLI tool.&lt;/p&gt;

&lt;p&gt;The standard approach is &lt;code&gt;argparse&lt;/code&gt;, but argparse is verbose: &lt;code&gt;add_argument&lt;/code&gt;, set &lt;code&gt;type&lt;/code&gt;, set &lt;code&gt;help&lt;/code&gt;, parse, then retrieve values — a simple CLI takes many lines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://typer.tiangolo.com/" rel="noopener noreferrer"&gt;Typer&lt;/a&gt;'s approach: type hints are the specification, the function signature is the CLI interface, &lt;code&gt;help&lt;/code&gt; comes from docstrings. Nothing to declare separately.&lt;/p&gt;

&lt;p&gt;Same author as FastAPI. Same design logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;typer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Simplest Case
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py Alice
&lt;span class="c"&gt;# Hello Alice&lt;/span&gt;

python main.py Alice &lt;span class="nt"&gt;--count&lt;/span&gt; 3
&lt;span class="c"&gt;# Hello Alice&lt;/span&gt;
&lt;span class="c"&gt;# Hello Alice&lt;/span&gt;
&lt;span class="c"&gt;# Hello Alice&lt;/span&gt;

python main.py &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="c"&gt;# Usage: main.py [OPTIONS] NAME&lt;/span&gt;
&lt;span class="c"&gt;# Arguments: NAME  [required]&lt;/span&gt;
&lt;span class="c"&gt;# Options: --count INTEGER  [default: 1]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;name: str&lt;/code&gt; has no default — becomes a required positional argument. &lt;code&gt;count: int = 1&lt;/code&gt; has a default — becomes an optional &lt;code&gt;--count&lt;/code&gt; option. No argument parser declarations needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple Subcommands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Typer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Deploy to an environment.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Force deploying to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deploying to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Rollback to a specific version.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rolling back &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py deploy production
python main.py deploy staging &lt;span class="nt"&gt;--force&lt;/span&gt;
python main.py rollback production v1.2.3

python main.py &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="c"&gt;# Available commands: deploy, rollback&lt;/span&gt;

python main.py deploy &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="c"&gt;# Deploy to an environment.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docstring becomes the &lt;code&gt;--help&lt;/code&gt; description automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bool&lt;/code&gt; types auto-generate both &lt;code&gt;--force&lt;/code&gt; and &lt;code&gt;--no-force&lt;/code&gt; options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arguments vs Options
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# positional argument (required)
&lt;/span&gt;    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# --output (has default)
&lt;/span&gt;    &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;# --verbose / --no-verbose
&lt;/span&gt;    &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Number of workers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# with description
&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;No default → positional argument, passed directly without &lt;code&gt;--&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Has default → option, passed as &lt;code&gt;--name value&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bool&lt;/code&gt; → flag, auto-generates &lt;code&gt;--flag&lt;/code&gt; / &lt;code&gt;--no-flag&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prompts and Confirmation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Are you sure?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deleted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py delete mydb
&lt;span class="c"&gt;# Are you sure? [y/N]: y&lt;/span&gt;
&lt;span class="c"&gt;# Deleted mydb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;prompt=True&lt;/code&gt; or &lt;code&gt;prompt="..."&lt;/code&gt; to dangerous operations for automatic confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Colored Output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_running&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✓ Running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✗ Stopped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Progress Bar
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;progressbar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Built in, no extra packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested Subcommands
&lt;/h2&gt;

&lt;p&gt;For larger tools, group commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Typer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;users_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Typer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;orders_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Typer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_typer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_typer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders_app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@users_app.command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_users&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@orders_app.command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_orders&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nb"&gt;users &lt;/span&gt;list
python main.py orders list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Shell Completion
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install into current shell&lt;/span&gt;
python main.py &lt;span class="nt"&gt;--install-completion&lt;/span&gt;

&lt;span class="c"&gt;# show the completion script without installing&lt;/span&gt;
python main.py &lt;span class="nt"&gt;--show-completion&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bash, Zsh, Fish, and PowerShell supported. One command, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compared to argparse / Click
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;argparse&lt;/th&gt;
&lt;th&gt;Click&lt;/th&gt;
&lt;th&gt;Typer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Declaration&lt;/td&gt;
&lt;td&gt;add_argument()&lt;/td&gt;
&lt;td&gt;
&lt;a class="mentioned-user" href="https://dev.to/click"&gt;@click&lt;/a&gt;.option()&lt;/td&gt;
&lt;td&gt;type hints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Help text&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;from docstring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type conversion&lt;/td&gt;
&lt;td&gt;set type=&lt;/td&gt;
&lt;td&gt;set type=&lt;/td&gt;
&lt;td&gt;inferred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subcommands&lt;/td&gt;
&lt;td&gt;subparsers&lt;/td&gt;
&lt;td&gt;multiple commands&lt;/td&gt;
&lt;td&gt;@app.command()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Typer is built on Click, so Click's ecosystem (plugins, testing utilities) still works.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Use It
&lt;/h2&gt;

&lt;p&gt;I consolidate scattered scripts into a single CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── cli.py          # app = typer.Typer(), entry point
├── commands/
│   ├── db.py       # database commands
│   ├── deploy.py   # deployment
│   └── seed.py     # test data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cli.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;commands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Typer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_typer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_typer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_typer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What used to be &lt;code&gt;python scripts/seed_db.py --env staging --users 100&lt;/code&gt; becomes &lt;code&gt;python cli.py seed users --env staging --count 100&lt;/code&gt;. Single entry point, &lt;code&gt;--help&lt;/code&gt; discovers everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Typer maps Python type hints directly to CLI interfaces. No new API to learn — it reads like normal functions.&lt;/p&gt;

&lt;p&gt;Good for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consolidating scattered admin scripts&lt;/li&gt;
&lt;li&gt;Giving internal tools a proper CLI&lt;/li&gt;
&lt;li&gt;Anything where shell completion would be useful&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://typer.tiangolo.com/" rel="noopener noreferrer"&gt;Typer official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fastapi/typer" rel="noopener noreferrer"&gt;Typer GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://click.palletsprojects.com/" rel="noopener noreferrer"&gt;Click official documentation (Typer's underlying framework)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/argparse.html" rel="noopener noreferrer"&gt;Python argparse official documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cli</category>
      <category>python</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>pytest: assert Is Enough, Forget self.assertEqual</title>
      <dc:creator>Recca Tsai</dc:creator>
      <pubDate>Thu, 02 Apr 2026 20:38:59 +0000</pubDate>
      <link>https://dev.to/recca0120/pytest-assert-is-enough-forget-selfassertequal-2boh</link>
      <guid>https://dev.to/recca0120/pytest-assert-is-enough-forget-selfassertequal-2boh</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://recca0120.github.io/en/2026/03/30/pytest-getting-started/" rel="noopener noreferrer"&gt;recca0120.github.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After switching from unittest to pytest, the thing I noticed most wasn't some killer feature — it was not having to remember all the &lt;code&gt;assertXxx&lt;/code&gt; methods.&lt;/p&gt;

&lt;p&gt;Just write &lt;code&gt;assert result == expected&lt;/code&gt;. pytest knows how to expand the failure message on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why pytest Instead of unittest
&lt;/h2&gt;

&lt;p&gt;unittest ships with the standard library, no install needed, but it has some rough edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests must inherit from &lt;code&gt;TestCase&lt;/code&gt; — you can't just write plain functions&lt;/li&gt;
&lt;li&gt;You need &lt;code&gt;self.assertEqual&lt;/code&gt;, &lt;code&gt;self.assertIn&lt;/code&gt;, &lt;code&gt;self.assertRaises&lt;/code&gt;… hard to keep track of&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;setUp&lt;/code&gt; / &lt;code&gt;tearDown&lt;/code&gt; scope is fixed at the class level, not flexible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three things in pytest that made me not go back:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plain assert&lt;/strong&gt; — failures show the actual values automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixtures injected on demand&lt;/strong&gt; — scope can be function / class / module / session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;parametrize&lt;/strong&gt; — test multiple inputs with one decorator&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Simplest Test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test_calc.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest test_calc.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED test_calc.py::test_add
AssertionError: assert 4 == 3
 +  where 4 = add(2, 2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No guessing which value is which — pytest expands it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixtures: Better Than setUp
&lt;/h2&gt;

&lt;p&gt;unittest's &lt;code&gt;setUp&lt;/code&gt; runs before every test with fixed class scope.&lt;/p&gt;

&lt;p&gt;pytest fixtures let you control scope and share across modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_db_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# teardown goes after yield
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup before &lt;code&gt;yield&lt;/code&gt;, teardown after. Much cleaner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scope
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# one instance per module
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;expensive_resource&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;load_something_slow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scope&lt;/th&gt;
&lt;th&gt;lifetime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;function&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;default — rebuilt for every test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;class&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shared within a class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;module&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shared within a file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shared for the entire test run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I typically set database connections to &lt;code&gt;session&lt;/code&gt; scope, with each test function running inside its own transaction that rolls back. The full test suite runs without being painfully slow.&lt;/p&gt;

&lt;h3&gt;
  
  
  conftest.py
&lt;/h3&gt;

&lt;p&gt;Fixtures in &lt;code&gt;conftest.py&lt;/code&gt; are available to all test files in the same directory — no imports needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/
├── conftest.py       # shared fixtures here
├── test_users.py
└── test_orders.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;admin_user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;test_users.py&lt;/code&gt; and &lt;code&gt;test_orders.py&lt;/code&gt; can use &lt;code&gt;admin_user&lt;/code&gt; without importing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  parametrize: Multiple Inputs at Once
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a, b, expected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each set of inputs runs as a separate test. On failure, it tells you exactly which input set broke:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED test_calc.py::test_add[0-0-1]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use this for boundary conditions — normal values, zero, negatives, extremes all in one go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Exceptions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_divide_by_zero&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ZeroDivisionError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To also check the exception message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_value_error&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;parse_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running Only Some Tests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# specific file&lt;/span&gt;
pytest test_users.py

&lt;span class="c"&gt;# specific function&lt;/span&gt;
pytest test_users.py::test_login

&lt;span class="c"&gt;# by keyword&lt;/span&gt;
pytest &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="s2"&gt;"login or register"&lt;/span&gt;

&lt;span class="c"&gt;# only last failed&lt;/span&gt;
pytest &lt;span class="nt"&gt;--lf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--lf&lt;/code&gt; (last failed) is what I reach for most. Fix a bug, immediately re-run just the tests that were failing — no need to wait through the whole suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Options
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-v&lt;/span&gt;          &lt;span class="c"&gt;# show each test name&lt;/span&gt;
pytest &lt;span class="nt"&gt;-s&lt;/span&gt;          &lt;span class="c"&gt;# don't capture stdout (print shows up)&lt;/span&gt;
pytest &lt;span class="nt"&gt;-x&lt;/span&gt;          &lt;span class="c"&gt;# stop on first failure&lt;/span&gt;
pytest &lt;span class="nt"&gt;--tb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short  &lt;span class="c"&gt;# shorter tracebacks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During development I almost always add &lt;code&gt;-x&lt;/code&gt; — one failure at a time, output doesn't get buried.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The gap between pytest and unittest isn't about features — it's about how comfortable the writing experience is. Plain &lt;code&gt;assert&lt;/code&gt;, on-demand fixture composition, &lt;code&gt;parametrize&lt;/code&gt; for multiple inputs. Once those habits are in place, testing stops feeling like something that requires opening documentation every time.&lt;/p&gt;

&lt;p&gt;If your tests need a lot of fake data, &lt;a href="https://dev.to/en/p/polyfactory-test-data/"&gt;polyfactory&lt;/a&gt; generates it from type hints automatically — no hand-crafting fixture data.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.pytest.org/" rel="noopener noreferrer"&gt;pytest official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pytest.org/en/stable/how-to/fixtures.html" rel="noopener noreferrer"&gt;pytest fixtures — how-to guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pytest.org/en/stable/how-to/parametrize.html" rel="noopener noreferrer"&gt;pytest parametrize documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pytest-dev/pytest" rel="noopener noreferrer"&gt;pytest GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>beginners</category>
      <category>python</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
