pi-kage 0.4.0 โ†’ 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,12 +15,17 @@ can't collide.
15
15
 
16
16
  ```bash
17
17
  npm install -g pi-kage
18
+ eval "$(kage shell-init)" # so kage can cd your shell in & out (recommended)
19
+
18
20
  cd my-app
19
- kage # ๐Ÿฅท copy โ†’ ../my-app--kage-<ts>, open a fresh pi
20
- # ...commit, push, open a PR, quit pi...
21
- kage finish # ๐Ÿ’จ merge the clone's sessions back, delete the clone
21
+ kage --agent pi # ๐Ÿฅท copy โ†’ ../my-app--<name>, cd in, open a fresh pi
22
+ # ...edit ยท commit ยท push ยท open a PR ยท quit pi...
23
+ kage finish # ๐Ÿ’จ merge pi's session memory back, delete the clone, cd you home
22
24
  ```
23
25
 
26
+ Launching an agent is optional: bare `kage` just makes the clone and cd's you in โ€” `--agent` (or
27
+ `kage config agent`, or `$KAGE_AGENT`) is what tells kage to start one for you.
28
+
24
29
  Code comes back through git (a PR, or a branch fetch). The agent's session memory comes back through
25
30
  its own session store (`~/.pi`, `~/.claude`, or `~/.codex`). kage never copies a working tree back onto
26
31
  the origin โ€” that's the whole point.
@@ -47,8 +52,9 @@ npx pi-kage # run without installing
47
52
  curl -fsSL https://raw.githubusercontent.com/kid7st/kage/main/install.sh | sh
48
53
  ```
49
54
 
50
- Requires **git**, [**pi**](https://github.com/earendil-works), and **Node โ‰ฅ 18** on your `PATH`.
51
- kage has no runtime dependencies.
55
+ Requires **git** and **Node โ‰ฅ 18** on your `PATH`, plus at least one coding-agent CLI to drive โ€”
56
+ [**pi**](https://github.com/earendil-works) (the default), [**Claude Code**](https://github.com/anthropics/claude-code),
57
+ or [**Codex**](https://github.com/openai/codex). kage itself has no runtime dependencies.
52
58
 
53
59
  From source:
54
60
 
@@ -57,43 +63,60 @@ git clone https://github.com/kid7st/kage
57
63
  cd kage && npm install && npm link # npm install builds bin/kage.mjs from src/
58
64
  ```
59
65
 
60
- ## Commands
66
+ ## Usage
61
67
 
62
68
  | Command | Run from | What it does |
63
69
  |---|---|---|
64
- | `kage [path] [--name x] [--agent pi\|claude\|codex]` | origin repo | Copy the repo to `../<repo>--<name>` (default `kage-<ts>`), copy in the origin's 5 most recent sessions for that agent (resumable, never replayed), and launch a **fresh** agent (pi by default). `--name` only names the folder โ€” kage never creates a branch. No args + existing clones โ†’ interactive menu. |
70
+ | `kage [path] [--name x] [--agent <id>]` | origin repo | Copy the repo to `../<repo>--<name>` (default `kage-<ts>`), import the origin's recent sessions (resumable, never replayed), **cd you into the clone**, and launch an agent only if you named one (`--agent`/`$KAGE_AGENT`/`kage config agent`) โ€” else just drop you in. `--name` only names the folder; kage never creates a branch. No args + existing clones โ†’ interactive menu. |
65
71
  | `kage status [--pr]` | origin repo | Dashboard: branch, dirty/clean, ahead/behind, "safe to clean". `--pr` adds PR state via `gh`. |
66
- | `kage finish [name] [--force] [--push] [--pr]` | origin / inside clone | Refuse if the clone has uncommitted or unpushed work, merge its **new** sessions back, delete it. `--push` pushes the branch first; `--pr` pushes + opens a PR via `gh`; `--force` skips the guard. |
72
+ | `kage finish [name] [--force] [--push] [--pr]` | origin / inside clone | Preserve the clone's commits (push, or with no remote a local `kage/<name>` branch), merge its **new** sessions back, delete it, cd you home. Refuses uncommitted changes โ€” and, with a remote, unpushed commits โ€” unless `--force`. `--push`/`--pr` push first (and open a PR via `gh`). |
67
73
  | `kage rm [name] [--force]` | origin / inside clone | Discard a clone **without** merging memory. Refuses local-only work unless `--force`. |
68
74
  | `kage pull <path...>` | inside a clone | Copy specific files/dirs (even gitignored, e.g. a generated `.env`) back to the origin. |
69
- | `kage shell-init` | shell rc | Shell wrapper (cd back to origin after `finish`/`rm`) + tab completion. Use `eval "$(kage shell-init)"`. |
75
+ | `kage config [<key> [value]] [--unset]` | anywhere | Get/set persisted defaults. One key today: `agent` (your default launch agent), validated against known agents. |
76
+ | `kage shell-init` | shell rc | Shell wrapper (cd into a clone on create, back to origin on `finish`/`rm`) + tab completion. Use `eval "$(kage shell-init)"`. |
70
77
  | `kage --help` / `--version` | anywhere | Usage / version. |
71
78
 
72
79
  Run bare `kage` inside a repo that already has clones to get an interactive picker: create a new clone,
73
80
  or **enter** / **finish** / **remove** an existing one. `finish` and `rm` show the same picker when you
74
81
  have several clones and don't name one.
75
82
 
76
- ### Other agents (Claude Code, Codex)
83
+ ## Agents
84
+
85
+ kage is agent-agnostic โ€” and launching an agent is **opt-in**. By default `kage` just creates the clone and
86
+ cd's your shell into it; you work however you like. To have kage launch one for you, name it with `--agent`,
87
+ or set a default with `kage config agent` (or `$KAGE_AGENT`) โ€” precedence `--agent` > `$KAGE_AGENT` >
88
+ `kage config agent`; with none set, nothing is launched. Whichever way you work, every agent gets the same
89
+ isolated clone and git flow-back โ€” what differs is **memory flow-back**, i.e. whether the sessions you
90
+ create in the clone come home to the origin:
91
+
92
+ | Agent | Launch it with | Memory flow-back |
93
+ |---|---|---|
94
+ | **pi** | `kage --agent pi` | โœ… full round-trip โ€” recent sessions copied in on create, new ones merged back on `finish` |
95
+ | **Claude Code** | `kage --agent claude` | โœ… full round-trip (same as pi) |
96
+ | **Codex** | `kage --agent codex` | โš ๏ธ git only โ€” history stays global, reach it with `codex resume --all` |
97
+
98
+ **Why Codex is git-only.** pi and Claude Code key their session stores by working directory, so kage can
99
+ copy a clone's sessions over to the origin's directory and back. Codex keeps one global, sqlite-indexed
100
+ store that kage can't cleanly re-home โ€” so `--agent codex` gives you the isolated clone + git flow-back,
101
+ and your Codex history stays globally reachable. Details in
102
+ [`docs/multi-agent-design.md`](./docs/multi-agent-design.md).
103
+
104
+ You can run several agents at once โ€” a clone each, or different agents in one clone. Each agent uses its
105
+ own store, so they never collide, and `finish` merges back whichever stores gained new sessions.
77
106
 
78
- kage isn't pi-only. `--agent claude` or `--agent codex` launches that agent instead. For **pi and Claude
79
- Code**, memory flows the same way โ€” through that agent's own session store (whether you drive it from the
80
- CLI, the IDE extension, or the desktop app), and two agents can even work one repo in parallel (a clone
81
- each, or different agents in one clone); `finish` merges back whichever stores have new sessions.
82
- **Codex is launch-only**: its sessions live in one global, sqlite-indexed store kage can't cleanly
83
- re-home, so `--agent codex` gives you the isolated clone + git flow-back while your Codex history stays
84
- globally available via `codex resume --all`. For a GUI/desktop app kage can't spawn, use `--open <cmd>`
85
- (e.g. `kage --open code`) or `--no-launch` to just make the clone and open it yourself. `KAGE_AGENT` sets
86
- your default agent.
107
+ **Driving a GUI/IDE agent kage can't spawn?** Use `--open <cmd>` to create the clone and open it yourself
108
+ (e.g. `kage --open code` runs `code <clone>`), or `--no-launch` to just make the clone and print its path.
109
+ Memory still flows for any agent whose store kage manages, no matter how you opened the clone.
87
110
 
88
- ### Shell integration (optional)
111
+ ## Shell integration (recommended)
89
112
 
90
113
  ```bash
91
114
  eval "$(kage shell-init)" # add to ~/.zshrc or ~/.bashrc
92
115
  ```
93
116
 
94
- Running `finish`/`rm` from inside a clone deletes the directory your shell is sitting in. The wrapper
95
- cd's you back to the origin automatically (a CLI can't change its parent shell otherwise) and adds tab
96
- completion for subcommands and clone names.
117
+ `kage` cd's your shell **into** the new clone, and `finish`/`rm` cd it **back** to the origin afterward
118
+ (a CLI can't move its parent shell otherwise, so this needs the wrapper โ€” without it, kage just prints the
119
+ `cd` for you to run). It also adds tab completion for subcommands and clone names.
97
120
 
98
121
  ## How it works
99
122
 
@@ -105,11 +128,10 @@ completion for subcommands and clone names.
105
128
  `kage/<name>-<sha>` branch (origin working tree untouched โ€” `git merge` it when you like). Because a
106
129
  fetch can't preserve uncommitted work, `finish` refuses to delete a dirty clone unless `--force`.
107
130
  - **Memory flows via the agent's own session store, never replayed.** On create, the origin's 5 most
108
- recent sessions for that agent are copied in โ€” the agent's resume picker surfaces them, but the clone
109
- opens a **fresh** session. On `finish`, sessions the clone created come back whole; a copied-in session
110
- you resumed comes back as a separate new session, so the origin's original is never mutated. (Codex is the
111
- exception โ€” its sessions live in one global, sqlite-indexed store kage can't cleanly re-home, so kage
112
- doesn't manage Codex memory; find Codex history with `codex resume --all`. See `docs/multi-agent-design.md`.)
131
+ recent sessions for that agent are copied in โ€” the agent's resume picker surfaces them when you launch it, but you
132
+ start a **fresh** session. On `finish`, sessions the clone created come back whole; a copied-in session
133
+ you resumed comes back as a separate new session, so the origin's original is never mutated. (Codex is
134
+ the exception โ€” git-only flow-back; see [Agents](#agents).)
113
135
  - **The origin is read-only to kage.** It only copies out and writes session memory โ€” it never touches
114
136
  the origin's working tree, even while another session is live there.
115
137
 
package/README.zh-CN.md CHANGED
@@ -14,12 +14,17 @@
14
14
 
15
15
  ```bash
16
16
  npm install -g pi-kage
17
+ eval "$(kage shell-init)" # ่ฎฉ kage ่ƒฝ cd ไฝ ็š„ shell ่ฟ› / ๅ‡บ๏ผˆๆŽจ่๏ผ‰
18
+
17
19
  cd my-app
18
- kage # ๐Ÿฅท ๅคๅˆถ โ†’ ../my-app--kage-<ts>๏ผŒๆ‰“ๅผ€ไธ€ไธชๅ…จๆ–ฐ็š„ pi
19
- # ...ๆไบคใ€pushใ€ๅผ€ PRใ€้€€ๅ‡บ pi...
20
- kage finish # ๐Ÿ’จ ๆŠŠๅˆ†่บซ็š„ session ๅˆๅนถๅ›žๆฅ๏ผŒๅˆ ๆމๅˆ†่บซ
20
+ kage --agent pi # ๐Ÿฅท ๅคๅˆถ โ†’ ../my-app--<name>๏ผŒcd ่ฟ›ๅŽป๏ผŒๅผ€ไธ€ไธชๅ…จๆ–ฐ็š„ pi
21
+ # ...็ผ–่พ‘ ยท ๆไบค ยท push ยท ๅผ€ PR ยท ้€€ๅ‡บ pi...
22
+ kage finish # ๐Ÿ’จ ๆŠŠ pi ็š„ session ่ฎฐๅฟ†ๅˆๅนถๅ›žๆฅ๏ผŒๅˆ ๆމๅˆ†่บซ๏ผŒๅ†ๆŠŠไฝ  cd ๅ›žๅŽŸไป“ๅบ“
21
23
  ```
22
24
 
25
+ ๅฏๅŠจ agent ๆ˜ฏๅฏ้€‰็š„๏ผš็บฏ่ท‘ `kage` ๅชๆ˜ฏๅปบๅฅฝๅˆ†่บซๅนถๆŠŠไฝ  cd ่ฟ›ๅŽป โ€”โ€” `--agent`๏ผˆๆˆ– `kage config agent`ใ€ๆˆ–
26
+ `$KAGE_AGENT`๏ผ‰ๆ‰ๆ˜ฏๅ‘Š่ฏ‰ kage ๆ›ฟไฝ ๅฏๅŠจไธ€ไธช็š„ๅผ€ๅ…ณใ€‚
27
+
23
28
  ไปฃ็ ้€š่ฟ‡ git ๅ›žๆต๏ผˆไธ€ไธช PR๏ผŒๆˆ–่€… fetch ๅˆ†ๆ”ฏ๏ผ‰๏ผ›agent ็š„ session ่ฎฐๅฟ†้€š่ฟ‡ๅฎƒ่‡ชๅทฑ็š„ session ๅญ˜ๅ‚จ๏ผˆ`~/.pi`ใ€
24
29
  `~/.claude` ๆˆ– `~/.codex`๏ผ‰ๅ›žๆตใ€‚kage ไปŽไธๆŠŠๅทฅไฝœๅŒบๅคๅˆถๅ›žๅŽŸไป“ๅบ“ โ€”โ€” ่ฟ™ๆญฃๆ˜ฏๅฎƒๅญ˜ๅœจ็š„ๆ„ไน‰ใ€‚
25
30
 
@@ -43,8 +48,9 @@ npx pi-kage # ไธๅฎ‰่ฃ…็›ดๆŽฅ่ฟ่กŒ
43
48
  curl -fsSL https://raw.githubusercontent.com/kid7st/kage/main/install.sh | sh
44
49
  ```
45
50
 
46
- ้œ€่ฆ `PATH` ไธŠๆœ‰ **git**ใ€[**pi**](https://github.com/earendil-works) ๅ’Œ **Node โ‰ฅ 18**ใ€‚
47
- kage ๆฒกๆœ‰ไปปไฝ•่ฟ่กŒๆ—ถไพ่ต–ใ€‚
51
+ ้œ€่ฆ `PATH` ไธŠๆœ‰ **git** ๅ’Œ **Node โ‰ฅ 18**๏ผŒๅค–ๅŠ ่‡ณๅฐ‘ไธ€ไธช coding-agent CLI ๆฅ้ฉฑๅŠจ โ€”โ€”
52
+ [**pi**](https://github.com/earendil-works)๏ผˆ้ป˜่ฎค๏ผ‰ใ€[**Claude Code**](https://github.com/anthropics/claude-code)
53
+ ๆˆ– [**Codex**](https://github.com/openai/codex)ใ€‚kage ๆœฌ่บซๆฒกๆœ‰ไปปไฝ•่ฟ่กŒๆ—ถไพ่ต–ใ€‚
48
54
 
49
55
  ไปŽๆบ็ ๅฎ‰่ฃ…๏ผš
50
56
 
@@ -53,39 +59,56 @@ git clone https://github.com/kid7st/kage
53
59
  cd kage && npm install && npm link # npm install ไผšไปŽ src/ ็ผ–่ฏ‘ๅ‡บ bin/kage.mjs
54
60
  ```
55
61
 
56
- ## ๅ‘ฝไปค
62
+ ## ไฝฟ็”จ
57
63
 
58
64
  | ๅ‘ฝไปค | ๅœจๅ“ช่ฟ่กŒ | ไฝœ็”จ |
59
65
  |---|---|---|
60
- | `kage [path] [--name x] [--agent pi\|claude\|codex]` | ๅŽŸไป“ๅบ“ | ๆŠŠไป“ๅบ“ๅคๅˆถๅˆฐ `../<repo>--<name>`๏ผˆ้ป˜่ฎค `kage-<ts>`๏ผ‰๏ผŒๆ‹ทๅ…ฅๅŽŸไป“ๅบ“ๆœ€่ฟ‘ 5 ไธช่ฏฅ agent ็š„ session๏ผˆๅฏ resume๏ผŒ็ปไธ้‡ๆ”พ๏ผ‰๏ผŒๅนถๅฏๅŠจไธ€ไธช**ๅ…จๆ–ฐ**็š„ agent๏ผˆ้ป˜่ฎค pi๏ผ‰ใ€‚`--name` ๅชๅ‘ฝๅๆ–‡ไปถๅคน โ€”โ€” kage ไปŽไธๅปบๅˆ†ๆ”ฏใ€‚ๆ— ๅ‚ๆ•ฐ + ๅทฒๆœ‰ๅˆ†่บซ โ†’ ่ฟ›ๅ…ฅไบคไบ’่œๅ•ใ€‚ |
66
+ | `kage [path] [--name x] [--agent <id>]` | ๅŽŸไป“ๅบ“ | ๆŠŠไป“ๅบ“ๅคๅˆถๅˆฐ `../<repo>--<name>`๏ผˆ้ป˜่ฎค `kage-<ts>`๏ผ‰๏ผŒๆ‹ทๅ…ฅๅŽŸไป“ๅบ“ๆœ€่ฟ‘็š„ session๏ผˆๅฏ resume๏ผŒ็ปไธ้‡ๆ”พ๏ผ‰๏ผŒ**ๆŠŠไฝ  cd ่ฟ›ๅˆ†่บซ**๏ผŒไป…ๅฝ“ไฝ ๆŒ‡ๅฎšไบ† agent๏ผˆ`--agent`/`$KAGE_AGENT`/`kage config agent`๏ผ‰ๆ‰ๅฏๅŠจๅฎƒ โ€”โ€” ๅฆๅˆ™ๅชๆŠŠไฝ ๆ”พ่ฟ›ๅŽปใ€‚`--name` ๅชๅ‘ฝๅๆ–‡ไปถๅคน๏ผ›kage ไปŽไธๅปบๅˆ†ๆ”ฏใ€‚ๆ— ๅ‚ๆ•ฐ + ๅทฒๆœ‰ๅˆ†่บซ โ†’ ่ฟ›ๅ…ฅไบคไบ’่œๅ•ใ€‚ |
61
67
  | `kage status [--pr]` | ๅŽŸไป“ๅบ“ | ไปช่กจ็›˜๏ผšๅˆ†ๆ”ฏใ€ๆ˜ฏๅฆๆœ‰ๆ”นๅŠจใ€ahead/behindใ€ๆ˜ฏๅฆใ€Œๅฏๅฎ‰ๅ…จๆธ…็†ใ€ใ€‚`--pr` ้€š่ฟ‡ `gh` ้™„ๅธฆ PR ็Šถๆ€ใ€‚ |
62
- | `kage finish [name] [--force] [--push] [--pr]` | ๅŽŸไป“ๅบ“ / ๅˆ†่บซๅ†… | ่‹ฅๅˆ†่บซๆœ‰ๆœชๆไบคๆˆ–ๆœช push ็š„ๆ”นๅŠจๅˆ™ๆ‹’็ป๏ผŒๆŠŠๅฎƒ**ๆ–ฐไบง็”Ÿ**็š„ session ๅˆๅนถๅ›žๆฅ๏ผŒๅ†ๅˆ ้™คๅฎƒใ€‚`--push` ๅ…ˆ push ๅˆ†ๆ”ฏ๏ผ›`--pr` push ๅนถ้€š่ฟ‡ `gh` ๅผ€ PR๏ผ›`--force` ่ทณ่ฟ‡ๆฃ€ๆŸฅใ€‚ |
68
+ | `kage finish [name] [--force] [--push] [--pr]` | ๅŽŸไป“ๅบ“ / ๅˆ†่บซๅ†… | ๅ…ˆไฟ็•™ๅˆ†่บซ็š„ commit๏ผˆpush๏ผŒๆˆ–ๆ—  remote ๆ—ถๅญ˜ๆˆๅŽŸไป“ๅบ“ๆœฌๅœฐๅˆ†ๆ”ฏ `kage/<name>`๏ผ‰๏ผŒๆŠŠๅฎƒ**ๆ–ฐไบง็”Ÿ**็š„ session ๅˆๅนถๅ›žๆฅ๏ผŒๅˆ ๆމๅˆ†่บซ๏ผŒๅ†ๆŠŠไฝ  cd ๅ›žๅŽŸไป“ๅบ“ใ€‚ๆœ‰ๆœชๆไบคๆ”นๅŠจ โ€”โ€” ไปฅๅŠๆœ‰ remote ๆ—ถ่ฟ˜ๆœ‰ๆœช push ็š„ commit โ€”โ€” ๅˆ™ๆ‹’็ป๏ผŒ้™ค้žๅŠ  `--force`ใ€‚`--push`/`--pr` ๅ…ˆ push๏ผˆๅนถ้€š่ฟ‡ `gh` ๅผ€ PR๏ผ‰ใ€‚ |
63
69
  | `kage rm [name] [--force]` | ๅŽŸไป“ๅบ“ / ๅˆ†่บซๅ†… | **ไธ**ๅˆๅนถ่ฎฐๅฟ†ๅœฐไธขๅผƒไธ€ไธชๅˆ†่บซใ€‚่‹ฅๆœ‰ไป…ๅญ˜ๅœจไบŽๆœฌๅœฐ็š„ๅทฅไฝœๅˆ™ๆ‹’็ป๏ผŒ้™ค้žๅŠ  `--force`ใ€‚ |
64
70
  | `kage pull <path...>` | ๅˆ†่บซๅ†… | ๆŠŠๆŒ‡ๅฎšๆ–‡ไปถ/็›ฎๅฝ•๏ผˆๅŒ…ๆ‹ฌ่ขซ gitignore ็š„๏ผŒๆฏ”ๅฆ‚็”Ÿๆˆ็š„ `.env`๏ผ‰ๆŒ‰็›ธๅŒ็›ธๅฏน่ทฏๅพ„ๆ‹ทๅ›žๅŽŸไป“ๅบ“ใ€‚ |
65
- | `kage shell-init` | shell ้…็ฝฎ | shell ๅŒ…่ฃ…ๅ‡ฝๆ•ฐ๏ผˆ`finish`/`rm` ๅŽ่‡ชๅŠจ cd ๅ›žๅŽŸไป“ๅบ“๏ผ‰+ tab ่กฅๅ…จใ€‚็”จ `eval "$(kage shell-init)"`ใ€‚ |
71
+ | `kage config [<key> [value]] [--unset]` | ไปปๆ„ไฝ็ฝฎ | ่ฏปๅ–/่ฎพ็ฝฎๆŒไน…ๅŒ–้ป˜่ฎคๅ€ผใ€‚็›ฎๅ‰ๅชๆœ‰ไธ€ไธช key๏ผš`agent`๏ผˆ้ป˜่ฎคๅฏๅŠจ็š„ agent๏ผ‰๏ผŒไผšๆ ก้ชŒๆ˜ฏๅฆไธบๅทฒ็Ÿฅ agentใ€‚ |
72
+ | `kage shell-init` | shell ้…็ฝฎ | shell ๅŒ…่ฃ…ๅ‡ฝๆ•ฐ๏ผˆๅˆ›ๅปบๆ—ถ cd ่ฟ›ๅˆ†่บซ๏ผŒ`finish`/`rm` ๅŽ cd ๅ›žๅŽŸไป“ๅบ“๏ผ‰+ tab ่กฅๅ…จใ€‚็”จ `eval "$(kage shell-init)"`ใ€‚ |
66
73
  | `kage --help` / `--version` | ไปปๆ„ไฝ็ฝฎ | ็”จๆณ• / ็‰ˆๆœฌใ€‚ |
67
74
 
68
75
  ๅœจๅทฒๆœ‰ๅˆ†่บซ็š„ไป“ๅบ“้‡Œ็›ดๆŽฅ่ฟ่กŒ `kage`๏ผŒไผšๅผนๅ‡บไบคไบ’้€‰ๆ‹ฉๅ™จ๏ผšๆ–ฐๅปบไธ€ไธชๅˆ†่บซ๏ผŒๆˆ–ๅฏนๅทฒๆœ‰ๅˆ†่บซๆ‰ง่กŒ **่ฟ›ๅ…ฅ** / **finish** /
69
76
  **ๅˆ ้™ค**ใ€‚ๅฝ“ๆœ‰ๅคšไธชๅˆ†่บซๅˆๆฒกๆŒ‡ๅฎšๅๅญ—ๆ—ถ๏ผŒ`finish` ๅ’Œ `rm` ไนŸไผšๅผนๅ‡บๅŒๆ ท็š„้€‰ๆ‹ฉๅ™จใ€‚
70
77
 
71
- ### ๅ…ถๅฎƒ agent๏ผˆClaude Codeใ€Codex๏ผ‰
78
+ ## Agents
79
+
80
+ kage ไธŽๅ…ทไฝ“ agent ๆ— ๅ…ณ โ€”โ€” ่€Œไธ”ๅฏๅŠจ agent ๆ˜ฏ**ๅฏ้€‰็š„**ใ€‚้ป˜่ฎคไธ‹ `kage` ๅชๅˆ›ๅปบๅˆ†่บซๅนถๆŠŠไฝ ็š„ shell cd ่ฟ›ๅŽป๏ผŒ
81
+ ๆŽฅไธ‹ๆฅไฝ ็ˆฑๆ€Žไนˆๅนฒๅฐฑๆ€Žไนˆๅนฒใ€‚ๆƒณ่ฎฉ kage ๆ›ฟไฝ ๅฏๅŠจไธ€ไธช agent๏ผŒๅฐฑ็”จ `--agent` ๆŒ‡ๅฎš๏ผŒๆˆ–็”จ `kage config agent`
82
+ ๏ผˆๆˆ– `$KAGE_AGENT`๏ผ‰่ฎพไธช้ป˜่ฎคๅ€ผ โ€”โ€” ไผ˜ๅ…ˆ็บง `--agent` > `$KAGE_AGENT` > `kage config agent`๏ผ›ไธ€ไธช้ƒฝๆฒก่ฎพๅฐฑ
83
+ ไป€ไนˆ้ƒฝไธๅฏๅŠจใ€‚ไธ็ฎกๆ€Žไนˆๅนฒ๏ผŒๆฏไธช agent ้ƒฝๅพ—ๅˆฐๅŒๆ ท็š„้š”็ฆปๅˆ†่บซๅ’Œ git ๅ›žๆต โ€”โ€” ๅŒบๅˆซๅœจไบŽ**่ฎฐๅฟ†ๅ›žๆต**๏ผŒไนŸๅฐฑๆ˜ฏ
84
+ ไฝ ๅœจๅˆ†่บซ้‡Œไบง็”Ÿ็š„ session ๆ˜ฏๅฆไผšๅ›žๅˆฐๅŽŸไป“ๅบ“๏ผš
85
+
86
+ | Agent | ๆ€ŽไนˆๅฏๅŠจ | ่ฎฐๅฟ†ๅ›žๆต |
87
+ |---|---|---|
88
+ | **pi** | `kage --agent pi` | โœ… ๅฎŒๆ•ดๅพ€่ฟ” โ€”โ€” ๅˆ›ๅปบๆ—ถๆ‹ทๅ…ฅๆœ€่ฟ‘็š„ session๏ผŒ`finish` ๆ—ถๆŠŠๆ–ฐไบง็”Ÿ็š„ๅˆๅนถๅ›žๆฅ |
89
+ | **Claude Code** | `kage --agent claude` | โœ… ๅฎŒๆ•ดๅพ€่ฟ”๏ผˆๅŒ pi๏ผ‰ |
90
+ | **Codex** | `kage --agent codex` | โš ๏ธ ไป… git โ€”โ€” ๅކๅฒไฟๆŒๅ…จๅฑ€๏ผŒ็”จ `codex resume --all` ๆ‰พๅ›ž |
91
+
92
+ **ไธบไป€ไนˆ Codex ๅชๆœ‰ git ๅ›žๆตใ€‚** pi ๅ’Œ Claude Code ็š„ session ๅญ˜ๅ‚จๆŒ‰ๅทฅไฝœ็›ฎๅฝ•็ดขๅผ•๏ผŒๆ‰€ไปฅ kage ่ƒฝๆŠŠๅˆ†่บซ็š„
93
+ session ๆ‹ทๅˆฐๅŽŸไป“ๅบ“็š„็›ฎๅฝ•ใ€ๅ†ๆ‹ทๅ›žๆฅใ€‚Codex ็”จ็š„ๆ˜ฏไธ€ไธช็”ฑ sqlite ็ดขๅผ•็š„ๅ…จๅฑ€ store๏ผŒkage ๆ— ๆณ•ๅนฒๅ‡€ๅœฐๆฌ่ฟ โ€”โ€”
94
+ ๆ‰€ไปฅ `--agent codex` ็ป™ไฝ ้š”็ฆปๅˆ†่บซ + git ๅ›žๆต๏ผŒ่€Œ Codex ๅކๅฒไฟๆŒๅ…จๅฑ€ๅฏ่พพใ€‚่ฏฆ่ง
95
+ [`docs/multi-agent-design.md`](./docs/multi-agent-design.md)ใ€‚
96
+
97
+ ไฝ ๅฏไปฅๅŒๆ—ถ่ท‘ๅคšไธช agent โ€”โ€” ๅ„ๅผ€ไธ€ไธชๅˆ†่บซ๏ผŒๆˆ–ๅœจๅŒไธ€ไธชๅˆ†่บซ้‡Œๆข็€็”จใ€‚ๆฏไธช agent ็”จ่‡ชๅทฑ็š„ store๏ผŒไบ’ไธๅ†ฒ็ช๏ผŒ
98
+ `finish` ไผšๆŠŠๆœ‰ๆ–ฐ session ็š„้‚ฃไบ› store ๅ„่‡ชๅˆๅนถๅ›žๆฅใ€‚
72
99
 
73
- kage ไธๅชๆ”ฏๆŒ piใ€‚`--agent claude` ๆˆ– `--agent codex` ไผšๆ”นไธบๅฏๅŠจๅฏนๅบ” agentใ€‚ๅฏน **pi ๅ’Œ Claude Code**๏ผŒ่ฎฐๅฟ†็…งๆ ท
74
- ๆตๅŠจ โ€”โ€” ้€š่ฟ‡่ฏฅ agent **่‡ชๅทฑ็š„** session ๅญ˜ๅ‚จ๏ผˆๆ— ่ฎบ็”จ CLIใ€IDE ๆ‰ฉๅฑ•่ฟ˜ๆ˜ฏๆกŒ้ข App๏ผ‰๏ผŒไธคไธช agent ็”š่‡ณ่ƒฝๅนถ่กŒ
75
- ๅค„็†ๅŒไธ€ไธชไป“ๅบ“๏ผˆๅ„ๅผ€ไธ€ไธชๅˆ†่บซ๏ผŒๆˆ–ๅŒไธ€ไธชๅˆ†่บซ้‡Œๆข็€็”จ๏ผ‰๏ผ›`finish` ไผšๆŠŠๆœ‰ๆ–ฐ session ็š„้‚ฃไบ› store ๅ„่‡ชๅˆๅนถๅ›žๆฅใ€‚
76
- **Codex ๆ˜ฏ launch-only**๏ผšๅฎƒ็š„ session ๅญ˜ๅœจไธ€ไธช็”ฑ sqlite ็ดขๅผ•็š„ๅ…จๅฑ€ store ้‡Œ๏ผŒkage ๆ— ๆณ•ๅนฒๅ‡€ๅœฐๆฌ่ฟ๏ผŒๆ‰€ไปฅ
77
- `--agent codex` ็ป™ไฝ ้š”็ฆปๅˆ†่บซ + git ๅ›žๆต๏ผŒ่€Œ Codex ๅކๅฒ้€š่ฟ‡ `codex resume --all` ไพ็„ถๅ…จๅฑ€ๅฏ่งใ€‚ๅฏนไบŽ kage ๆฒกๆณ•
78
- ็›ดๆŽฅๆ‹‰่ตท็š„ GUI / ๆกŒ้ข App๏ผŒ็”จ `--open <cmd>`๏ผˆๆฏ”ๅฆ‚ `kage --open code`๏ผ‰ๆˆ– `--no-launch` ๅชๅปบๅˆ†่บซใ€ไฝ ่‡ชๅทฑๅŽปๆ‰“ๅผ€ใ€‚
79
- `KAGE_AGENT` ่ฎพ็ฝฎ้ป˜่ฎค agentใ€‚
100
+ **่ฆ็”จ kage ๆ‹‰ไธ่ตทๆฅ็š„ GUI/IDE agent๏ผŸ** ็”จ `--open <cmd>` ๅปบๅฅฝๅˆ†่บซๅŽ่‡ชๅทฑๆ‰“ๅผ€ๅฎƒ๏ผˆๆฏ”ๅฆ‚ `kage --open code`
101
+ ไผšๆ‰ง่กŒ `code <clone>`๏ผ‰๏ผŒๆˆ–็”จ `--no-launch` ๅชๅปบๅˆ†่บซๅนถๆ‰“ๅฐ่ทฏๅพ„ใ€‚ๆ— ่ฎบไฝ ๆ€Žไนˆๆ‰“ๅผ€ๅˆ†่บซ๏ผŒๅช่ฆๆ˜ฏ kage ็ฎก็†ๅ…ถ
102
+ store ็š„ agent๏ผŒ่ฎฐๅฟ†็…งๆ ทๅ›žๆตใ€‚
80
103
 
81
- ### Shell ้›†ๆˆ๏ผˆๅฏ้€‰๏ผ‰
104
+ ## Shell ้›†ๆˆ๏ผˆๆŽจ่๏ผ‰
82
105
 
83
106
  ```bash
84
107
  eval "$(kage shell-init)" # ๅŠ ๅˆฐ ~/.zshrc ๆˆ– ~/.bashrc
85
108
  ```
86
109
 
87
- ๅœจๅˆ†่บซๅ†…่ฟ่กŒ `finish`/`rm` ไผšๅˆ ๆމไฝ  shell ๅฝ“ๅ‰ๆ‰€ๅœจ็š„็›ฎๅฝ•ใ€‚่ฟ™ไธชๅŒ…่ฃ…ๅ‡ฝๆ•ฐไผš่‡ชๅŠจๆŠŠไฝ  cd ๅ›žๅŽŸไป“ๅบ“๏ผˆๅฆๅˆ™ CLI
88
- ๆฒกๆณ•ๆ”นๅ˜็ˆถ shell ็š„็›ฎๅฝ•๏ผ‰๏ผŒๅนถไธบๅญๅ‘ฝไปคๅ’Œๅˆ†่บซๅๅŠ ไธŠ tab ่กฅๅ…จใ€‚
110
+ `kage` ไผšๆŠŠไฝ ็š„ shell cd **่ฟ›**ๆ–ฐๅˆ†่บซ๏ผŒ`finish`/`rm` ๅ†ๆŠŠๅฎƒ cd **ๅ›ž**ๅŽŸไป“ๅบ“๏ผˆCLI ๆฒกๆณ•ๆ”นๅ˜็ˆถ shell ็š„็›ฎๅฝ•๏ผŒ
111
+ ๆ‰€ไปฅ่ฟ™้œ€่ฆๅŒ…่ฃ…ๅ‡ฝๆ•ฐ โ€”โ€” ๆฒก่ฃ…็š„่ฏ๏ผŒkage ๅฐฑๅชๆŠŠ `cd` ๅ‘ฝไปคๆ‰“ๅฐๅ‡บๆฅ่ฎฉไฝ ่‡ชๅทฑ่ท‘๏ผ‰ใ€‚ๅฎƒ่ฟ˜ไธบๅญๅ‘ฝไปคๅ’Œๅˆ†่บซๅๅŠ ไธŠ tab ่กฅๅ…จใ€‚
89
112
 
90
113
  ## ๅทฅไฝœๅŽŸ็†
91
114
 
@@ -95,10 +118,9 @@ eval "$(kage shell-init)" # ๅŠ ๅˆฐ ~/.zshrc ๆˆ– ~/.bashrc
95
118
  ๅˆ†่บซ็š„ๅˆ†ๆ”ฏ fetch ่ฟ›ๅŽŸไป“ๅบ“็š„ git๏ผŒๅญ˜ๆˆๆœฌๅœฐๅˆ†ๆ”ฏ `kage/<name>-<sha>`๏ผˆๅŽŸไป“ๅบ“ๅทฅไฝœๅŒบไธๅŠจ โ€”โ€” ไฝ ๆƒณๅˆๅนถๆ—ถๅ†
96
119
  `git merge`๏ผ‰ใ€‚็”ฑไบŽ fetch ๆ— ๆณ•ไฟ็•™ๆœชๆไบค็š„ๆ”นๅŠจ๏ผŒ`finish` ๆ‹’็ปๅˆ ้™คๆœ‰ๆ”นๅŠจ็š„ๅˆ†่บซ๏ผŒ้™ค้žๅŠ  `--force`ใ€‚
97
120
  - **่ฎฐๅฟ†็ป็”ฑ่ฏฅ agent ่‡ชๅทฑ็š„ session ๅญ˜ๅ‚จๅ›žๆต๏ผŒ็ปไธ้‡ๆ”พใ€‚** ๅˆ›ๅปบๆ—ถๆ‹ทๅ…ฅๅŽŸไป“ๅบ“ๆœ€่ฟ‘ 5 ไธช่ฏฅ agent ็š„ session โ€”โ€” ่ฏฅ
98
- agent ็š„ resume ้€‰ๆ‹ฉๅ™จ่ƒฝ็œ‹ๅˆฐๅฎƒไปฌ๏ผŒไฝ†ๅˆ†่บซๆœฌ่บซๆ‰“ๅผ€็š„ๆ˜ฏ**ๅ…จๆ–ฐ** sessionใ€‚`finish` ๆ—ถ๏ผŒๅˆ†่บซ่‡ชๅทฑไบง็”Ÿ็š„ session ๆ•ดไปฝ
121
+ agent ็š„ resume ้€‰ๆ‹ฉๅ™จๅœจไฝ ๅฏๅŠจๅฎƒๆ—ถ่ƒฝ็œ‹ๅˆฐๅฎƒไปฌ๏ผŒไฝ†ไฝ ไปŽไธ€ไธช**ๅ…จๆ–ฐ** session ๅผ€ๅง‹ใ€‚`finish` ๆ—ถ๏ผŒๅˆ†่บซ่‡ชๅทฑไบง็”Ÿ็š„ session ๆ•ดไปฝ
99
122
  ๆ‹ทๅ›ž๏ผ›ไฝ  resume ่ฟ‡็š„ๆ‹ทๅ…ฅ session ไผšไฝœไธบไธ€ไธช็‹ฌ็ซ‹็š„ๆ–ฐ session ๅ›žๆฅ๏ผŒๅŽŸไป“ๅบ“็š„ๅŽŸๅง‹ session ็ปไธ่ขซๆ”นๅŠจใ€‚๏ผˆCodex ๆ˜ฏ
100
- ไพ‹ๅค– โ€”โ€” ๅฎƒ็š„ session ๅœจไธ€ไธช sqlite ็ดขๅผ•็š„ๅ…จๅฑ€ store ้‡Œ๏ผŒkage ๆ— ๆณ•ๅนฒๅ‡€ๆฌ่ฟ๏ผŒๆ‰€ไปฅไธ็ฎก็† Codex ่ฎฐๅฟ†๏ผ›Codex
101
- ๅކๅฒ็”จ `codex resume --all` ๆŸฅใ€‚่ฏฆ่ง `docs/multi-agent-design.md`ใ€‚๏ผ‰
123
+ ไพ‹ๅค– โ€”โ€” ไป… git ๅ›žๆต๏ผ›่ง [Agents](#agents)ใ€‚๏ผ‰
102
124
  - **ๅฏน kage ่€Œ่จ€ๅŽŸไป“ๅบ“ๆ˜ฏๅช่ฏป็š„ใ€‚** ๅฎƒๅชๅพ€ๅค–ๅคๅˆถใ€ๅชๅ†™ session ่ฎฐๅฟ† โ€”โ€” ๅณไฝฟๅŽŸไป“ๅบ“้‡Œๅฆๆœ‰ไธ€ไธช session ๆญฃๆดป่ทƒ๏ผŒ
103
125
  ๅฎƒไนŸ็ปไธ็ขฐๅŽŸไป“ๅบ“็š„ๅทฅไฝœๅŒบใ€‚
104
126
 
package/bin/kage.mjs CHANGED
@@ -2,25 +2,23 @@
2
2
  /**
3
3
  * kage ๐Ÿฅท โ€” cast the Shadow Clone Jutsu on a git repo.
4
4
  *
5
- * Copy the current repo into an isolated sibling folder (its own working tree and .git),
6
- * drop straight into `pi` to work in parallel, then `kage finish` merges the session memory
7
- * back into the original and deletes the clone.
5
+ * Copy the current repo into an isolated sibling folder (its own working tree and .git) and cd
6
+ * into it; optionally launch a coding agent there. `kage finish` merges the agent's session memory
7
+ * back into the origin and deletes the clone. Run `kage --help` for the command surface.
8
8
  *
9
9
  * Design invariants:
10
10
  * 1. Isolation โ€” a clone is a full independent copy (its own .git).
11
11
  * 2. Code flows back via git only โ€” a remote PR, or (no remote) a fetch of the clone's branch
12
12
  * into the origin's git on finish; kage never copies the working tree back onto the origin.
13
- * 3. Memory flows via ~/.pi โ€” the origin's session history is copied into the clone on create
13
+ * 3. Memory flows via each agent's own session store (pi/Claude Code keyed by cwd; Codex is
14
+ * launch-only, not kage-managed) โ€” the origin's history is copied into the clone on create
14
15
  * (resumable, never replayed) and the clone's new sessions are merged back on finish. These
15
- * are session .jsonl files, not the working tree, so there's no collision.
16
- * 4. The origin is read-only to kage โ€” it only copies out and writes session memory.
16
+ * are session files, not the working tree, so there's no collision.
17
+ * 4. The origin's working tree is read-only to kage โ€” it only copies out, writes the kage/<name>
18
+ * branch into the origin's git (no-remote finish), and writes session memory.
17
19
  *
18
- * Commands:
19
- * kage [path] [--name x] [--agent id] clone repo + launch an agent (no args: interactive)
20
- * kage status [--pr] dashboard of clones (+ PR status via gh)
21
- * kage finish [name] [--force] check -> merge memory back -> delete clone
22
- * kage rm [name] [--force] discard a clone (no merge)
23
- * kage pull <path...> (inside a clone) copy files back to the origin
20
+ * Launch is opt-in and detachable (see docs/multi-agent-design.md): with no agent named
21
+ * (--agent / $KAGE_AGENT / `kage config agent`), kage just creates the clone and cd's you in.
24
22
  */
25
23
  import { spawn, spawnSync } from "node:child_process";
26
24
  import { randomUUID } from "node:crypto";
@@ -28,7 +26,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync,
28
26
  import { homedir } from "node:os";
29
27
  import { basename, dirname, join, resolve, sep } from "node:path";
30
28
  import readline from "node:readline";
31
- const VERSION = "0.4.0"; // keep in sync with package.json (enforced by test)
29
+ const VERSION = "0.5.0"; // keep in sync with package.json (enforced by test)
32
30
  const MARKER = ".kage.json";
33
31
  const RECENT_SESSIONS = 5; // how many of the origin's most-recent sessions to copy into a clone
34
32
  /** Typed accessors for the loose flags bag, so consumers don't re-derive the shape inline. */
@@ -86,6 +84,34 @@ function readMarker(dir) {
86
84
  return undefined;
87
85
  }
88
86
  }
87
+ const CONFIG_KEYS = ["agent"];
88
+ function configPath() {
89
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
90
+ return join(base, "kage", "config.json");
91
+ }
92
+ function readConfig() {
93
+ const p = configPath();
94
+ if (!existsSync(p))
95
+ return {};
96
+ try {
97
+ const v = JSON.parse(readFileSync(p, "utf8"));
98
+ if (typeof v !== "object" || v === null)
99
+ return {};
100
+ const o = v;
101
+ const cfg = {};
102
+ if (typeof o.agent === "string")
103
+ cfg.agent = o.agent;
104
+ return cfg;
105
+ }
106
+ catch {
107
+ return {};
108
+ }
109
+ }
110
+ function writeConfig(cfg) {
111
+ const p = configPath();
112
+ mkdirSync(dirname(p), { recursive: true });
113
+ writeFileSync(p, `${JSON.stringify(cfg, null, 2)}\n`);
114
+ }
89
115
  /** Copy a whole directory: clonefile on macOS, reflink on Linux, plain copy as fallback. */
90
116
  function copyTree(src, dst) {
91
117
  const isMac = process.platform === "darwin";
@@ -246,6 +272,14 @@ function prInfo(dir, branch) {
246
272
  }
247
273
  /** True when the clone has no local-only work (clean + pushed) -> safe to remove. */
248
274
  const isSafeToClean = (s) => !s.dirty && s.hasUpstream && s.ahead === 0;
275
+ /** Single-glyph clone state, shared by the dashboard and the menu/picker so a clone always reads the
276
+ * same: โ— uncommitted ยท โ†‘ clean but unpushed/local-only ยท โœ“ clean & pushed (safe to remove). */
277
+ const statusTag = (s) => (s.dirty ? paint.yellow("โ—") : isSafeToClean(s) ? paint.green("โœ“") : paint.yellow("โ†‘"));
278
+ /** One-line clone label for the menu and the finish/rm picker: name, branch, state tag. */
279
+ function cloneLabel(c) {
280
+ const s = cloneStatus(c.dir);
281
+ return `${c.name} ${paint.cyan(s.branch)} ${statusTag(s)}`;
282
+ }
249
283
  /** True if the clone has committed work that lives only in the clone (not on a remote, not yet in the origin). */
250
284
  function hasUnpreservedCommits(originRepo, cloneDir, s) {
251
285
  if (s.hasUpstream && s.ahead === 0)
@@ -345,7 +379,7 @@ async function pickClone(action, name) {
345
379
  const first = clones[0];
346
380
  if (first && clones.length === 1)
347
381
  return { originRepo, clone: first };
348
- const idx = await select(`Multiple clones โ€” pick one to ${action}:`, clones.map((c) => `${c.name} ${paint.dim(cloneStatus(c.dir).branch)}`));
382
+ const idx = await select(`Multiple clones โ€” pick one to ${action}:`, clones.map(cloneLabel));
349
383
  const chosen = idx < 0 ? undefined : clones[idx];
350
384
  if (!chosen)
351
385
  return null;
@@ -434,17 +468,18 @@ function dirStore(cfg) {
434
468
  try {
435
469
  rmSync(srcDir, { recursive: true, force: true });
436
470
  }
437
- catch {
438
- /* ignore */
471
+ catch (e) {
472
+ info(paint.dim(` (couldn't clear the clone's ${cfg.id} sessions at ${srcDir}: ${e.message})`));
439
473
  }
440
474
  return n;
441
475
  },
442
476
  discard(cloneDir) {
477
+ const d = dirFor(cloneDir);
443
478
  try {
444
- rmSync(dirFor(cloneDir), { recursive: true, force: true });
479
+ rmSync(d, { recursive: true, force: true });
445
480
  }
446
- catch {
447
- /* ignore */
481
+ catch (e) {
482
+ info(paint.dim(` (couldn't discard the clone's ${cfg.id} sessions at ${d}: ${e.message})`));
448
483
  }
449
484
  },
450
485
  hasActivity(cwd) {
@@ -599,30 +634,57 @@ const codexStore = {
599
634
  return codexRollouts().some((r) => r.cwd === cwd);
600
635
  },
601
636
  };
602
- /**
603
- * We just deleted the clone we were running inside, so the parent shell is now in a
604
- * deleted directory. A CLI can't cd its parent shell, so: if the shell wrapper is active
605
- * (KAGE_CD_FILE set by `eval "$(kage shell-init)"`), hand it the origin path to cd into;
606
- * otherwise print a copy-pasteable `cd` and how to enable the auto version.
607
- */
608
- function leaveClone(originRepo) {
637
+ // A CLI can't cd its parent shell. If the shell wrapper is active (KAGE_CD_FILE, set by
638
+ // `eval "$(kage shell-init)"`), hand it a path to cd into once kage exits; otherwise we can only
639
+ // print a copy-pasteable `cd`. armCd does the handoff; leaveClone/cdIntoClone do the messaging.
640
+ function armCd(dir) {
609
641
  const f = process.env.KAGE_CD_FILE;
610
- if (f) {
611
- try {
612
- writeFileSync(f, originRepo);
613
- info(paint.dim(` โ†ฉ back to ${originRepo}`));
614
- return;
615
- }
616
- catch {
617
- /* fall through to the manual hint */
618
- }
642
+ if (!f)
643
+ return false;
644
+ try {
645
+ writeFileSync(f, dir);
646
+ return true;
647
+ }
648
+ catch {
649
+ return false;
650
+ }
651
+ }
652
+ const SHELL_INIT_HINT = `add eval "$(kage shell-init)" to your ~/.zshrc`;
653
+ /** After deleting the clone we were inside, the parent shell sits in a now-deleted dir โ€” send it
654
+ * back to the origin (or print how, without the wrapper). */
655
+ function leaveClone(originRepo) {
656
+ if (armCd(originRepo)) {
657
+ info(paint.dim(` โ†ฉ back to ${originRepo}`));
658
+ return;
619
659
  }
620
660
  info(paint.yellow(` โ†ฉ your shell is still in the deleted clone โ€” run: ${paint.bold(`cd ${originRepo}`)}`));
621
- info(paint.dim(` enable auto cd-back: add eval "$(kage shell-init)" to your ~/.zshrc`));
661
+ info(paint.dim(` enable auto cd-back: ${SHELL_INIT_HINT}`));
662
+ }
663
+ /** Settle the shell inside a clone we just created/entered: cd there if the wrapper is active,
664
+ * else print a copy-pasteable `cd` + the finish hint. */
665
+ function cdIntoClone(cloneDir, name) {
666
+ info("");
667
+ if (armCd(cloneDir)) {
668
+ info(paint.dim(` โ†ช now in the clone โ€” when done: ${paint.bold("kage finish")}`));
669
+ return;
670
+ }
671
+ info(` โ–ธ enter it: ${paint.bold(`cd ${cloneDir}`)}`);
672
+ info(paint.dim(` when done: ${paint.bold(`kage finish ${name}`)}`));
673
+ info(paint.dim(` tip: ${SHELL_INIT_HINT} โ€” then kage cd's you in & out automatically`));
674
+ }
675
+ /** chdir the kage process out of a clone it's about to delete (rmSync of the cwd fails otherwise).
676
+ * Surfaced rather than swallowed โ€” a silent failure here is why a later rmSync would fail. */
677
+ function chdirToOrigin(originRepo) {
678
+ try {
679
+ process.chdir(originRepo);
680
+ }
681
+ catch (e) {
682
+ info(paint.dim(` (couldn't chdir to ${originRepo}: ${e.message})`));
683
+ }
622
684
  }
623
685
  // Registered agents. Memory sync iterates this list, so each new agent is one entry (+ its
624
- // SessionStore) with no change to any call site. Codex (a flat, cwd-in-content store) lands
625
- // here in Phase 3.
686
+ // SessionStore) with no change to any call site. Codex is the odd one โ€” a flat, cwd-in-content
687
+ // store that kage doesn't memory-manage (see codexStore).
626
688
  const AGENTS = [
627
689
  { id: "pi", store: piStore, cli: { bin: "pi", freshArgs: [], resumeArgs: ["-c"] } },
628
690
  { id: "claude", store: claudeStore, cli: { bin: "claude", freshArgs: [], resumeArgs: ["--continue"] } },
@@ -652,24 +714,19 @@ function launchOpen(cmd, cwd) {
652
714
  die(`failed to run --open ${cmd}: ${err.message}`);
653
715
  }
654
716
  }
655
- /** Re-enter a clone: resume whichever agent has activity here (ask if several), else start fresh. */
656
- async function enterClone(cloneDir) {
717
+ /** Re-enter a clone from the menu: resume an agent that has activity here (ask if several), then
718
+ * leave the shell in the clone. With no activity โ€” or if you cancel the resume picker โ€” just cd in;
719
+ * kage never auto-launches an agent, and Enter always lands you in the clone. */
720
+ async function enterClone(cloneDir, name) {
657
721
  const active = AGENTS.filter((a) => a.cli && a.store.hasActivity(cloneDir));
658
- if (active.length === 0) {
659
- const def = agentById(process.env.KAGE_AGENT ?? "pi") ?? AGENTS[0];
660
- if (def?.cli)
661
- launchCli(def.cli, cloneDir, def.cli.freshArgs);
662
- return;
663
- }
664
- let chosen = active[0];
722
+ let chosen = active[0]; // undefined when nothing has activity here
665
723
  if (active.length > 1) {
666
- const idx = await select("Resume which agent?", active.map((a) => a.id));
667
- if (idx < 0)
668
- return info("cancelled");
669
- chosen = active[idx];
724
+ const idx = await select("Resume which agent? (Esc = just cd in)", active.map((a) => a.id));
725
+ chosen = idx < 0 ? undefined : active[idx]; // cancel โ†’ don't resume, but still cd in below
670
726
  }
671
727
  if (chosen?.cli)
672
728
  launchCli(chosen.cli, cloneDir, chosen.cli.resumeArgs);
729
+ cdIntoClone(cloneDir, name);
673
730
  }
674
731
  // โ”€โ”€ subcommands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
675
732
  async function cmdNew(argv) {
@@ -680,24 +737,22 @@ async function cmdNew(argv) {
680
737
  const repoRoot = repoTopLevel(process.cwd());
681
738
  const clones = repoRoot ? listClones(repoRoot) : [];
682
739
  if (repoRoot && clones.length > 0) {
683
- const labels = [
684
- "๏ผ‹ Create a new shadow clone",
685
- ...clones.map((c) => {
686
- const s = cloneStatus(c.dir);
687
- const tag = s.dirty ? paint.yellow(" โ—") : isSafeToClean(s) ? paint.green(" โœ“") : "";
688
- return `โ†’ Enter ${c.name} ${paint.cyan(s.branch)}${tag}`;
689
- }),
690
- ];
691
- const idx = await select(`Shadow clones of ${basename(repoRoot)} โ€” pick one, or create:`, labels);
740
+ const labels = ["๏ผ‹ Create a new clone", ...clones.map(cloneLabel)];
741
+ const idx = await select(`Shadow clones of ${basename(repoRoot)} โ€” pick one to act on, or create:`, labels);
692
742
  if (idx < 0)
693
743
  return info("cancelled");
694
744
  if (idx > 0) {
695
745
  const clone = clones[idx - 1];
696
746
  if (!clone)
697
747
  return info("cancelled");
698
- const act = await select(`${clone.name}:`, ["Enter (resume)", "Finish (merge memory & remove)", "Remove (discard)", "Cancel"]);
748
+ const act = await select(`${clone.name}:`, [
749
+ "Enter (resume agent + cd in)",
750
+ "Finish (merge memory & remove)",
751
+ "Remove (discard, no merge)",
752
+ "Cancel",
753
+ ]);
699
754
  if (act === 0)
700
- return enterClone(clone.dir);
755
+ return enterClone(clone.dir, clone.name);
701
756
  if (act === 1)
702
757
  return cmdFinish([clone.name]);
703
758
  if (act === 2)
@@ -719,17 +774,18 @@ async function cmdNew(argv) {
719
774
  let name = strFlag(flags, "name") ?? "";
720
775
  if (!name) {
721
776
  const def = tsName();
722
- const prompt = `Kage name: ${basename(repoRoot)}--`;
777
+ const prompt = `Clone name: ${basename(repoRoot)}--`;
723
778
  name = (process.stdin.isTTY ? await ask(prompt, def) : "") || def;
724
779
  }
725
780
  const safe = slug(name);
726
781
  const cloneDir = join(dirname(repoRoot), `${basename(repoRoot)}--${safe}`);
727
782
  if (existsSync(cloneDir))
728
783
  die(`directory already exists: ${cloneDir}`);
729
- // Resolve the agent to launch up front, so a typo fails before we copy anything.
730
- const agentId = strFlag(flags, "agent") ?? process.env.KAGE_AGENT ?? "pi";
731
- const agent = agentById(agentId);
732
- if (!agent)
784
+ // Launch is opt-in: a CLI launches only if you named one (--agent / $KAGE_AGENT / `kage config agent`).
785
+ // Resolve it up front so a typo fails before we copy; with none set, kage just cd's you in (below).
786
+ const agentId = strFlag(flags, "agent") ?? process.env.KAGE_AGENT ?? readConfig().agent;
787
+ const agent = agentId ? agentById(agentId) : undefined;
788
+ if (agentId && !agent)
733
789
  die(`unknown agent: ${agentId} (supported: ${AGENTS.map((a) => a.id).join(", ")})`);
734
790
  const cp = await copyRepo(repoRoot, cloneDir);
735
791
  if (!cp.ok)
@@ -750,26 +806,22 @@ async function cmdNew(argv) {
750
806
  info(`๐Ÿฅท ${paint.bold("Shadow clone ready")}: ${cloneDir}`);
751
807
  info(` origin: ${repoRoot} branch: ${paint.cyan(curBranch)}`);
752
808
  if (histN > 0)
753
- info(paint.dim(` origin's ${histN} session(s) are available via resume (pi: pick from the list)`));
754
- info(paint.dim(` when done: kage finish ${safe}`));
755
- info("");
756
- // Launch mode: --no-launch (just build), --open <cmd> (open + return), else spawn the CLI.
757
- if (boolFlag(flags, "no-launch"))
758
- return;
809
+ info(paint.dim(` origin's ${histN} session(s) imported โ€” resume them from inside the clone`));
810
+ // Optionally hand off to an agent, then settle the shell in the clone (always):
811
+ // --open <cmd> โ†’ run it (e.g. an editor); an agent named โ†’ spawn it (blocking);
812
+ // else (--no-launch, or no agent set) โ†’ launch nothing.
759
813
  const openCmd = strFlag(flags, "open");
760
814
  if (openCmd || boolFlag(flags, "open")) {
761
815
  if (!openCmd)
762
816
  die("--open needs a command, e.g. --open code");
763
817
  launchOpen(openCmd, cloneDir);
764
- info("");
765
- info(`โ†ฉ๏ธŽ opened ${cloneDir} with ${openCmd}. To finish: ${paint.bold(`kage finish ${safe}`)}`);
766
- return;
767
818
  }
768
- if (!agent.cli)
769
- die(`agent ${agent.id} has no CLI to launch โ€” use --open <cmd> or --no-launch`);
770
- launchCli(agent.cli, cloneDir, agent.cli.freshArgs);
771
- info("");
772
- info(`โ†ฉ๏ธŽ left the clone's ${agent.cli.bin}. To finish: ${paint.bold(`kage finish ${safe}`)}`);
819
+ else if (agent && !boolFlag(flags, "no-launch")) {
820
+ if (!agent.cli)
821
+ die(`agent ${agent.id} has no CLI to launch โ€” use --open <cmd> or --no-launch`);
822
+ launchCli(agent.cli, cloneDir, agent.cli.freshArgs);
823
+ }
824
+ cdIntoClone(cloneDir, safe);
773
825
  }
774
826
  async function cmdFinish(argv) {
775
827
  const { positional, flags } = parseArgs(argv);
@@ -838,12 +890,7 @@ async function cmdFinish(argv) {
838
890
  let n = 0;
839
891
  for (const a of AGENTS)
840
892
  n += a.store.mergeBack(clone.dir, originRepo);
841
- try {
842
- process.chdir(originRepo);
843
- }
844
- catch {
845
- /* ignore */
846
- }
893
+ chdirToOrigin(originRepo);
847
894
  rmSync(clone.dir, { recursive: true, force: true });
848
895
  info(`๐Ÿ’จ Clone dispelled: merged ${n} session(s) back, removed ${clone.dir}`);
849
896
  if (insideClone)
@@ -865,12 +912,7 @@ async function cmdRm(argv) {
865
912
  if (!(await confirm(`Discard clone ${clone.name} without merging its memory?`)))
866
913
  return info("aborted");
867
914
  }
868
- try {
869
- process.chdir(originRepo);
870
- }
871
- catch {
872
- /* ignore */
873
- }
915
+ chdirToOrigin(originRepo);
874
916
  for (const a of AGENTS)
875
917
  a.store.discard(clone.dir);
876
918
  rmSync(clone.dir, { recursive: true, force: true });
@@ -896,7 +938,7 @@ function cmdList(argv) {
896
938
  const s = cloneStatus(c.dir);
897
939
  const pr = boolFlag(flags, "pr") ? prInfo(c.dir, s.branch) : undefined;
898
940
  // header: status glyph ยท name ยท branch ยท age
899
- const glyph = s.dirty ? paint.yellow("โ—") : isSafeToClean(s) ? paint.green("โœ“") : paint.cyan("ยท");
941
+ const glyph = statusTag(s);
900
942
  const age = paint.dim(`created ${ago(c.marker.createdAt)}`);
901
943
  info(` ${glyph} ${paint.bold(c.name)} ${paint.cyan(s.branch)} ${age}`);
902
944
  // detail: working-tree state ยท sync ยท PR ยท safe-to-clean
@@ -973,6 +1015,41 @@ function cmdPull(argv) {
973
1015
  }
974
1016
  info(`๐Ÿ“ค Pulled ${done}/${positional.length} path(s) from the clone back to the origin (${originRepo})`);
975
1017
  }
1018
+ /** get/set kage's small persisted config (~/.config/kage/config.json). Keys are validated so a
1019
+ * typo can't silently become a "default". `kage config` (no args) prints the file path + every key. */
1020
+ function cmdConfig(argv) {
1021
+ const { positional, flags } = parseArgs(argv);
1022
+ const cfg = readConfig();
1023
+ const key = positional[0] ?? (typeof flags.unset === "string" ? flags.unset : undefined);
1024
+ const value = positional[1];
1025
+ if (!key) {
1026
+ info(paint.dim(`# ${configPath()}`));
1027
+ for (const k of CONFIG_KEYS)
1028
+ info(`${k} = ${cfg[k] ?? paint.dim("(unset)")}`);
1029
+ return;
1030
+ }
1031
+ if (!CONFIG_KEYS.includes(key)) {
1032
+ die(`unknown config key: ${key} (known: ${CONFIG_KEYS.join(", ")})`);
1033
+ }
1034
+ const k = key;
1035
+ if (boolFlag(flags, "unset")) {
1036
+ delete cfg[k];
1037
+ writeConfig(cfg);
1038
+ info(`unset ${k}`);
1039
+ return;
1040
+ }
1041
+ if (value === undefined) {
1042
+ // a bare read goes to stdout so `X=$(kage config <key>)` works; other output stays on stderr
1043
+ process.stdout.write(`${cfg[k] ?? ""}\n`);
1044
+ return;
1045
+ }
1046
+ if (k === "agent" && !agentById(value)) {
1047
+ die(`unknown agent: ${value} (supported: ${AGENTS.map((a) => a.id).join(", ")})`);
1048
+ }
1049
+ cfg[k] = value;
1050
+ writeConfig(cfg);
1051
+ info(`${k} = ${value}`);
1052
+ }
976
1053
  const SHELL_INIT = `# kage shell integration โ€” add to ~/.zshrc or ~/.bashrc: eval "$(kage shell-init)"
977
1054
  kage() {
978
1055
  local f; f="$(mktemp "\${TMPDIR:-/tmp}/kage-cd.XXXXXX")"
@@ -983,18 +1060,20 @@ kage() {
983
1060
  }
984
1061
  if [ -n "$ZSH_VERSION" ]; then
985
1062
  _kage() {
986
- if (( CURRENT == 2 )); then compadd new status finish rm pull; return; fi
1063
+ if (( CURRENT == 2 )); then compadd new status finish rm pull config; return; fi
987
1064
  case "\${words[2]}" in
988
1065
  finish|rm) compadd $(command kage __clones 2>/dev/null);;
1066
+ config) compadd agent;;
989
1067
  esac
990
1068
  }
991
1069
  compdef _kage kage
992
1070
  elif [ -n "$BASH_VERSION" ]; then
993
1071
  _kage() {
994
1072
  local cur="\${COMP_WORDS[COMP_CWORD]}"
995
- if [ "$COMP_CWORD" -eq 1 ]; then COMPREPLY=( $(compgen -W "new status finish rm pull" -- "$cur") ); return; fi
1073
+ if [ "$COMP_CWORD" -eq 1 ]; then COMPREPLY=( $(compgen -W "new status finish rm pull config" -- "$cur") ); return; fi
996
1074
  case "\${COMP_WORDS[1]}" in
997
1075
  finish|rm) COMPREPLY=( $(compgen -W "$(command kage __clones 2>/dev/null)" -- "$cur") );;
1076
+ config) COMPREPLY=( $(compgen -W "agent" -- "$cur") );;
998
1077
  esac
999
1078
  }
1000
1079
  complete -F _kage kage
@@ -1009,8 +1088,8 @@ function cmdClones() {
1009
1088
  const HELP = `kage ๐Ÿฅท โ€” Shadow Clone Jutsu for your git repo
1010
1089
 
1011
1090
  Usage:
1012
- kage [path] [--name <x>] [--agent <id>] clone repo + launch a fresh agent (default: pi)
1013
- --open <cmd> | --no-launch: open it yourself instead
1091
+ kage [path] [--name <x>] [--agent <id>] clone the repo and cd into it (launch an agent only if one is set)
1092
+ --open <cmd> opens it elsewhere; --no-launch skips the agent
1014
1093
  (no args inside a repo with clones: interactive menu)
1015
1094
  kage status [--pr] dashboard of clones (--pr adds PR status via gh)
1016
1095
  kage finish [name] [--force] [--push] [--pr] preserve work -> merge memory back -> delete clone
@@ -1018,7 +1097,8 @@ Usage:
1018
1097
  kept in the origin as a local 'kage/<name>-<sha>' branch)
1019
1098
  kage rm [name] [--force] discard a clone without merging
1020
1099
  kage pull <path...> (inside a clone) copy files back to the origin
1021
- kage shell-init shell wrapper (cd-back) + tab completion
1100
+ kage config [<key> [value]] [--unset] get/set persisted defaults (e.g. the launch agent)
1101
+ kage shell-init shell wrapper (cd into clones / back) + tab completion
1022
1102
  kage --help | --version show this help / print the version
1023
1103
 
1024
1104
  With no args inside a repo that already has clones, kage opens an interactive menu
@@ -1027,15 +1107,16 @@ With no args inside a repo that already has clones, kage opens an interactive me
1027
1107
  Options:
1028
1108
  --name <x> name the clone folder /<repo>--<x> (default: kage-<timestamp>); skips the name prompt
1029
1109
  (sanitized to a git-ref-safe slug, since the name is also used as a branch name)
1030
- --agent <id> which agent CLI to launch (default: pi, or $KAGE_AGENT); memory still syncs for all agents
1031
- --open <cmd> after cloning, run '<cmd> <clone>' (e.g. --open code) and return, instead of spawning a CLI
1032
- --no-launch just create the clone (and import memory), print its path; you open it yourself
1110
+ --agent <id> launch this agent CLI in the clone (one-off). with none set (--agent > $KAGE_AGENT >
1111
+ 'kage config agent') kage just cd's you into the clone. memory syncs for every agent
1112
+ --open <cmd> after cloning, run '<cmd> <clone>' (e.g. --open code), then cd into the clone
1113
+ --no-launch don't launch an agent even if one is configured โ€” just create the clone and cd in
1033
1114
  --pr (finish) push the branch and open a GitHub PR via gh, then finish
1034
1115
  --push (finish) push the branch before finishing (implied by --pr)
1035
1116
  --force skip the safety checks: uncommitted/unpushed guard (finish) or local-only guard (rm)
1036
1117
 
1037
1118
  Examples:
1038
- kage # clone the current repo, pick a name, open a fresh pi to work in
1119
+ kage # clone the current repo, pick a name, and cd into the clone
1039
1120
  kage --name fix-login # same, but name the clone ../<repo>--fix-login (no prompt)
1040
1121
  kage ~/code/other-repo # clone a different repo instead of the current dir
1041
1122
 
@@ -1049,9 +1130,10 @@ Examples:
1049
1130
  kage rm experiment # throw a clone away without merging its memory
1050
1131
 
1051
1132
  kage pull .env # inside a clone: copy a gitignored file back to the origin
1133
+ kage config agent claude # launch claude on create (instead of just cd-ing into the clone)
1052
1134
 
1053
1135
  Env (each agent's own native var โ€” kage just honors it, so kage and the agent always agree):
1054
- KAGE_AGENT default agent to launch when --agent is omitted (default: pi)
1136
+ KAGE_AGENT agent to launch on create when --agent is omitted (none set = just cd into the clone)
1055
1137
  PI_CODING_AGENT_DIR pi's agent dir; sessions read from <it>/sessions (default: ~/.pi/agent)
1056
1138
  CLAUDE_CONFIG_DIR Claude Code's config dir; sessions read from <it>/projects (default: ~/.claude)
1057
1139
  CODEX_HOME Codex's home dir; sessions read from <it>/sessions (default: ~/.codex)`;
@@ -1069,6 +1151,8 @@ async function main() {
1069
1151
  return cmdRm(rest);
1070
1152
  case "pull":
1071
1153
  return cmdPull(rest);
1154
+ case "config":
1155
+ return cmdConfig(rest);
1072
1156
  case "shell-init": {
1073
1157
  process.stdout.write(`${SHELL_INIT}\n`);
1074
1158
  // When a human runs this directly (stdout is a TTY, not captured by `$(...)`),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-kage",
3
- "version": "0.4.0",
4
- "description": "๐Ÿฅท Shadow Clone Jutsu for your git repo: copy it into an isolated folder, work in parallel with pi, then merge the session memory back",
3
+ "version": "0.5.0",
4
+ "description": "๐Ÿฅท Shadow Clone Jutsu for your git repo: copy it into an isolated sibling folder, run your coding agent (pi, Claude Code, Codex) in parallel, then merge the session memory back",
5
5
  "keywords": [
6
6
  "pi",
7
7
  "coding-agent",