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 +50 -28
- package/README.zh-CN.md +45 -23
- package/bin/kage.mjs +188 -104
- package/package.json +2 -2
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
|
|
20
|
-
# ...commit
|
|
21
|
-
kage finish
|
|
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
|
|
51
|
-
|
|
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
|
-
##
|
|
66
|
+
## Usage
|
|
61
67
|
|
|
62
68
|
| Command | Run from | What it does |
|
|
63
69
|
|---|---|---|
|
|
64
|
-
| `kage [path] [--name x] [--agent
|
|
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 |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
you resumed comes back as a separate new session, so the origin's original is never mutated. (Codex is
|
|
111
|
-
exception โ
|
|
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
|
|
19
|
-
#
|
|
20
|
-
kage finish
|
|
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
|
|
47
|
-
|
|
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
|
|
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]` | ๅไปๅบ / ๅ่บซๅ
|
|
|
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
|
|
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
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
104
|
+
## Shell ้ๆ๏ผๆจ่๏ผ
|
|
82
105
|
|
|
83
106
|
```bash
|
|
84
107
|
eval "$(kage shell-init)" # ๅ ๅฐ ~/.zshrc ๆ ~/.bashrc
|
|
85
108
|
```
|
|
86
109
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
121
|
+
agent ็ resume ้ๆฉๅจๅจไฝ ๅฏๅจๅฎๆถ่ฝ็ๅฐๅฎไปฌ๏ผไฝไฝ ไปไธไธช**ๅ
จๆฐ** session ๅผๅงใ`finish` ๆถ๏ผๅ่บซ่ชๅทฑไบง็็ session ๆดไปฝ
|
|
99
122
|
ๆทๅ๏ผไฝ resume ่ฟ็ๆทๅ
ฅ session ไผไฝไธบไธไธช็ฌ็ซ็ๆฐ session ๅๆฅ๏ผๅไปๅบ็ๅๅง session ็ปไธ่ขซๆนๅจใ๏ผCodex ๆฏ
|
|
100
|
-
ไพๅค โโ
|
|
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
|
-
*
|
|
7
|
-
* back into the
|
|
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
|
|
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
|
|
16
|
-
* 4. The origin is read-only to kage โ it only copies out
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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(
|
|
479
|
+
rmSync(d, { recursive: true, force: true });
|
|
445
480
|
}
|
|
446
|
-
catch {
|
|
447
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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:
|
|
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
|
|
625
|
-
//
|
|
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
|
|
656
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}:`, [
|
|
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 = `
|
|
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
|
-
//
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
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)
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
//
|
|
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 (!
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1013
|
-
--open <cmd>
|
|
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
|
|
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>
|
|
1031
|
-
|
|
1032
|
-
--
|
|
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,
|
|
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
|
|
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
|
-
"description": "๐ฅท Shadow Clone Jutsu for your git repo: copy it into an isolated folder,
|
|
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",
|