jeo-code 0.7.31 → 0.7.33

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.ja.md +4 -2
  3. package/README.ko.md +4 -2
  4. package/README.md +4 -2
  5. package/README.zh.md +4 -2
  6. package/package.json +4 -4
  7. package/scripts/install.sh +19 -0
  8. package/scripts/uninstall.sh +2 -2
  9. package/src/agent/AGENTS.md +15 -0
  10. package/src/agent/ast-edit-tool.ts +123 -0
  11. package/src/agent/ast-file-scan.ts +62 -0
  12. package/src/agent/ast-grep-tool.ts +107 -0
  13. package/src/agent/ast-match.ts +241 -0
  14. package/src/agent/browser-session.ts +104 -0
  15. package/src/agent/browser-tab.ts +145 -0
  16. package/src/agent/browser-tool.ts +188 -0
  17. package/src/agent/debug-session.ts +435 -0
  18. package/src/agent/debug-tool.ts +89 -0
  19. package/src/agent/engine.ts +72 -12
  20. package/src/agent/goal-tool.ts +57 -0
  21. package/src/agent/irc-tool.ts +71 -0
  22. package/src/agent/job-registry.ts +163 -0
  23. package/src/agent/job-tool.ts +106 -0
  24. package/src/agent/lsp-rename-tool.ts +108 -0
  25. package/src/agent/lsp-tool.ts +177 -0
  26. package/src/agent/session.ts +27 -0
  27. package/src/agent/subagent-registry.ts +40 -4
  28. package/src/agent/subagent-tool.ts +27 -7
  29. package/src/agent/subagents.ts +1 -1
  30. package/src/agent/task-tool.ts +27 -5
  31. package/src/agent/tools.ts +68 -1
  32. package/src/agent/ts-language-service.ts +173 -0
  33. package/src/commands/launch/agents-slash.ts +489 -0
  34. package/src/commands/launch/flags.ts +27 -9
  35. package/src/commands/launch/model-slash.ts +290 -0
  36. package/src/commands/launch/session-slash.ts +305 -0
  37. package/src/commands/launch/slash-handlers.ts +6 -0
  38. package/src/commands/launch/system-slash.ts +81 -0
  39. package/src/commands/launch.ts +208 -749
package/CHANGELOG.md CHANGED
@@ -6,6 +6,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.7.33] - 2026-07-04
10
+ _Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32._
11
+
12
+ ### Added
13
+ - **`browser` — headless Chromium automation via Playwright (gjc `browser` parity, headless-only for v1 — no existing-profile/CDP-endpoint/Electron attach, no stealth patches).** `browser-session.ts` keeps one shared browser instance alive across calls with named tabs reused ("open once, act many times"); `browser-tab.ts` provides `observe()`-first helpers (tags interactive elements with stable numeric ids instead of a raw DOM dump or a screenshot) plus `click`/`type`/`fill`/`select`/`press`/`scroll`/`goto`/`back`/`extract`/`screenshot`/`evaluate`. The `browser` tool exposes `open`/`close`/`run`/`act`: `act` runs structured steps (navigate/click/type/fill/select/press/scroll/back/wait/observe/extract/screenshot) addressed by numeric `id` (from a prior `observe`) or CSS selector; `run` executes an async-function body with `page`/`browser`/`tab`/`display`/`assert`/`wait` in scope for anything the structured verbs don't cover. Mutating (drives a real browser, `run` executes arbitrary host JS) — excluded from read-only subagent roles. `playwright` moves from `devDependencies` to `dependencies` (it was already bundled for jeo-code's own test suite, so this reuses it rather than adding a second browser-automation engine).
14
+
15
+ ### Verified
16
+ - `bun run typecheck` clean; full suite green.
17
+ - New coverage: `test/browser-tool.test.ts` (open/act round-trip against real headless Chromium — observe → type → click → extract, `run` with display/assert, screenshot-to-file, close/close-all, and error-path guards) with `test.skipIf` fallbacks for environments without the Chromium binary downloaded; `test/subagent-toolset-gating.test.ts` extended to assert `browser` is excluded from every read-only role.
18
+
19
+ ## [0.7.32] - 2026-07-03
20
+ _gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard)._
21
+
22
+ ### Added
23
+ - **`ast_grep`/`ast_edit` — structural TypeScript/JavaScript search and codemods (gjc parity, pure-TS, not the native binary, not multi-language).** `ast-match.ts` implements the `$NAME`/`$_`/`$$$NAME`/`$$$` metavariable matcher over the `typescript` compiler API's AST; `ast-file-scan.ts` shares gitignore/ignored-dir-aware file targeting between them. `ast_grep` is read-only (safe for every subagent role); `ast_edit` is mutating (writes via `writeTool`, excluded from read-only roles).
24
+ - **`lsp`/`lsp_rename` — TypeScript/JavaScript language-service queries (gjc `lsp` parity, in-process `ts.LanguageService`, not a real LSP server).** `ts-language-service.ts` caches one language service per project root with tsconfig-aware root-file discovery, a whole-tree fallback, and stat-based live file-version tracking. `lsp` covers definition/references/hover/symbols/diagnostics and is read-only; cross-file rename is the separate mutating `lsp_rename` tool so the read-only-role toolset filter (which gates by tool name) can't be bypassed by a rename action hidden inside a nominally read-only call.
25
+ - **`debug` — Node.js debugging via an in-process CDP (V8 Inspector Protocol) client (gjc `debug` parity, Node.js only — not Bun, whose inspector speaks a different WebKit dialect).** `debug-session.ts` drives a spawned `node --inspect-brk` process (one active session, singleton); the tool exposes launch/set_breakpoint/remove_breakpoint/continue/step_over/step_in/step_out/pause/evaluate/stack_trace/scopes/variables/threads/output/terminate. Mutating (runs arbitrary code as a real process) — excluded from read-only subagent roles the same way `bash`/`computer` are.
26
+ - **`irc` — parent-to-subagent live peer messaging for DETACHED subagents (gjc `irc` parity).** Built entirely on the existing `SubagentRegistry.steer()` delivery path (no new state store): `list` shows currently running detached subagents as peers, `send` delivers a message to one peer id or to every running peer (`to:"all"`), picked up before that subagent's next step.
27
+ - **`job` — background shell-process control (gjc `job`/async-bash parity).** `job-registry.ts` tracks real parallel OS processes (not just concurrent JS) with a bounded output buffer; the tool exposes start/list/tail/await (optionally time-bounded)/cancel.
28
+ - **`goal` — lets the model itself set/read/clear the session's natural-language stop condition**, wrapping the existing file-persisted `GoalState` that `/goal` and the per-`done` `verifyGoal` check already use — the model gets the same lever a human typing `/goal` has, not a new state store.
29
+ - **`launch.ts` slash-command handlers extracted into `src/commands/launch/{agents,model,session,system}-slash.ts`**, shrinking the monolithic `launch.ts` by roughly 600 net lines without changing observable `/agents`, `/model`, `/fast`, and session-management command behavior.
30
+
31
+ ### Hardened
32
+ _A guardrail audit of the agent loop, subagent spawn gate, and file-edit tools turned up four places where an existing safety mechanism was either inert or had a bypassable escape hatch. All four are now closed:_
33
+ - **Every `bash` call (main agent and every subagent role, including the otherwise-unconstrained `executor`) now runs through a narrow, hard-coded destructive-command denylist** (`bashDestructiveReason` in `src/agent/tools.ts`): a recursive force-delete whose target normalizes to the filesystem root or home directory (`rm -rf /`, `rm -rf ~`, `sudo rm -Rf /`, chained/env-prefixed variants), fork-bomb patterns, raw disk-device overwrites (`dd ... of=/dev/...`), and raw-device filesystem formats (`mkfs ... /dev/...`) are rejected before the shell ever spawns. Everyday destructive-but-scoped commands (`rm -rf node_modules`, `git reset --hard`, force-push) are untouched — this is a narrow safety net, not a policy filter. The role-scoped `bashAllowedPrefixes` allowlist mechanism (`subagents.ts`) remains available for custom config-declared roles but was never wired to any bundled role; this denylist is the actual protection the bundled `executor` role gets today.
34
+ - **The subagent fan-out spawn-gate's justification check accepted any 20-character string.** `task-tool.ts`'s `MAX_FANOUT` bypass required a `justification` string of length ≥20 with no content check, so `"xxxxxxxxxxxxxxxxxxxx"` or `"asdf asdf asdf asdf"` cleared the gate as easily as a real justification. `isMeaningfulJustification` now additionally rejects single-character/repeated-token padding and requires at least 4 distinct words.
35
+ - **The done-verification pushback's "second `done` always passes" escape hatch could be triggered by resending `done` unchanged with zero intervening work.** `engine.ts` now tracks `sawActionSincePushback`: after the one-time pushback (`done_unverified`/`done_stale_verification`/`done_hook_failing`), a `done` retried with no tool call executed in between is bounced again instead of silently accepted. The escape hatch (intended for docs/config-only changes where verification is genuinely not applicable) still works — it now requires at least one real attempt (even a `read`) before honoring it.
36
+ - **A `≔A..B` line-range replace with no hashline anchors and no prior `read` this session applied completely unverified.** `editTool`'s `staleReadError` only guarded a file the agent had already read; a plain, unanchored range-replace against a file never read this session bypassed both the freshness check and the (optional) anchor check. `editTool` now rejects that specific case — anchored edits, `<<<<<<< SEARCH` blocks (self-verifying via the search text), and insert/append directives (non-destructive) are all unaffected, and the check runs after the existing out-of-bounds validation so an invalid range still reports as invalid rather than being masked by this guard.
37
+
38
+ ### Verified
39
+ - `bun run typecheck` clean; full suite green.
40
+ - New/updated coverage: `test/ast-grep-tool.test.ts`, `test/ast-edit-tool.test.ts`, `test/ast-match.test.ts`, `test/lsp-tool.test.ts`, `test/lsp-rename-tool.test.ts`, `test/ts-language-service.test.ts`, `test/debug-tool.test.ts`, `test/irc-tool.test.ts`, `test/job-tool.test.ts`, `test/goal-tool.test.ts`, `test/subagent-toolset-gating.test.ts`, `test/engine-computer-wiring.test.ts`, `test/launch-oneshot-slash.test.ts` for the new tools/refactor; `test/done-verification-guard.test.ts`, `test/post-turn-feedback.test.ts`, `test/repeat-bounce-and-trim.test.ts`, `test/spawn-gate-lite.test.ts`, `test/edit-freshness.test.ts`, `test/tools-fs.test.ts`, `test/engine-multitool.test.ts` updated/extended for the four guardrail hardenings above.
41
+
9
42
  ## [0.7.31] - 2026-07-03
10
43
  _Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity._
11
44
 
package/README.ja.md CHANGED
@@ -55,6 +55,8 @@ bun install -g jeo-code
55
55
  jeo --version
56
56
  ```
57
57
 
58
+ > リネーム前のバージョンからアップグレードしますか? 旧 CLI 名だった `joc` バイナリは `scripts/install.sh` / `scripts/uninstall.sh` が自動的に削除します。手動で削除する場合: `rm -f ~/.local/bin/joc ~/.bun/bin/joc`。
59
+
58
60
  ## クイックスタート
59
61
 
60
62
  ```bash
@@ -205,11 +207,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
205
207
  ## 変更履歴 (Changelog)
206
208
 
207
209
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
210
+ - **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
211
+ - **[0.7.32]** (2026-07-03) — gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard).
208
212
  - **[0.7.31]** (2026-07-03) — Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity.
209
213
  - **[0.7.30]** (2026-07-02) — GPT/Codex OAuth calls work again, and the 30-minute turn budget now limits no-progress stalls instead of killing genuinely progressing autonomous runs.
210
214
  - **[0.7.29]** (2026-07-02) — gajae-code safety-refusal retry parity: Anthropic/OAuth content-classifier refusals no longer end the turn with a fatal "declined to answer" message after the context-reset ladder.
211
- - **[0.7.28]** (2026-07-02) — gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests).
212
- - **[0.7.27]** (2026-07-02) — Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks.
213
215
 
214
216
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
217
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -55,6 +55,8 @@ bun install -g jeo-code
55
55
  jeo --version
56
56
  ```
57
57
 
58
+ > 이름 변경 이전 버전에서 업그레이드하시나요? 이전 CLI 이름이었던 `joc` 바이너리는 이제 `scripts/install.sh` / `scripts/uninstall.sh`가 자동으로 제거합니다. 수동 제거: `rm -f ~/.local/bin/joc ~/.bun/bin/joc`.
59
+
58
60
  ## 빠른 시작
59
61
 
60
62
  ```bash
@@ -205,11 +207,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
205
207
  ## 변경 이력 (Changelog)
206
208
 
207
209
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
210
+ - **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
211
+ - **[0.7.32]** (2026-07-03) — gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard).
208
212
  - **[0.7.31]** (2026-07-03) — Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity.
209
213
  - **[0.7.30]** (2026-07-02) — GPT/Codex OAuth calls work again, and the 30-minute turn budget now limits no-progress stalls instead of killing genuinely progressing autonomous runs.
210
214
  - **[0.7.29]** (2026-07-02) — gajae-code safety-refusal retry parity: Anthropic/OAuth content-classifier refusals no longer end the turn with a fatal "declined to answer" message after the context-reset ladder.
211
- - **[0.7.28]** (2026-07-02) — gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests).
212
- - **[0.7.27]** (2026-07-02) — Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks.
213
215
 
214
216
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
217
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -55,6 +55,8 @@ bun install -g jeo-code
55
55
  jeo --version
56
56
  ```
57
57
 
58
+ > Upgrading from a pre-rename install? A stale `joc` binary (this project's old CLI name) is now auto-removed by `scripts/install.sh` / `scripts/uninstall.sh`; to remove it manually: `rm -f ~/.local/bin/joc ~/.bun/bin/joc`.
59
+
58
60
  ## Quick start
59
61
 
60
62
  ```bash
@@ -232,11 +234,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
232
234
  ## Changelog
233
235
 
234
236
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
237
+ - **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
238
+ - **[0.7.32]** (2026-07-03) — gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard).
235
239
  - **[0.7.31]** (2026-07-03) — Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity.
236
240
  - **[0.7.30]** (2026-07-02) — GPT/Codex OAuth calls work again, and the 30-minute turn budget now limits no-progress stalls instead of killing genuinely progressing autonomous runs.
237
241
  - **[0.7.29]** (2026-07-02) — gajae-code safety-refusal retry parity: Anthropic/OAuth content-classifier refusals no longer end the turn with a fatal "declined to answer" message after the context-reset ladder.
238
- - **[0.7.28]** (2026-07-02) — gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests).
239
- - **[0.7.27]** (2026-07-02) — Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks.
240
242
 
241
243
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
242
244
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -55,6 +55,8 @@ bun install -g jeo-code
55
55
  jeo --version
56
56
  ```
57
57
 
58
+ > 从重命名前的版本升级?旧 CLI 名称 `joc` 的二进制文件现在会被 `scripts/install.sh` / `scripts/uninstall.sh` 自动移除;手动移除: `rm -f ~/.local/bin/joc ~/.bun/bin/joc`。
59
+
58
60
  ## 快速开始
59
61
 
60
62
  ```bash
@@ -205,11 +207,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
205
207
  ## 更新日志 (Changelog)
206
208
 
207
209
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
210
+ - **[0.7.33]** (2026-07-04) — Adds `browser` — headless Chromium automation (gjc `browser` parity) — completing the gjc tool-surface parity pass started in 0.7.32.
211
+ - **[0.7.32]** (2026-07-03) — gjc tool-surface parity — ast_grep/ast_edit, lsp/lsp_rename, debug, irc, job, and goal tools — plus a `launch.ts` slash-command refactor and four agent-loop guardrail hardenings (destructive-bash denylist, spawn-gate justification quality check, done-verification abuse guard, and a blind unanchored line-range-edit guard).
208
212
  - **[0.7.31]** (2026-07-03) — Three-round speed & memory pass across streaming, memory retrieval, and the TUI render loop (O(n²) → O(n) in several hot paths), a deep-interview repeat-question loop guard, and gajae-code managed-tmux launch sizing parity.
209
213
  - **[0.7.30]** (2026-07-02) — GPT/Codex OAuth calls work again, and the 30-minute turn budget now limits no-progress stalls instead of killing genuinely progressing autonomous runs.
210
214
  - **[0.7.29]** (2026-07-02) — gajae-code safety-refusal retry parity: Anthropic/OAuth content-classifier refusals no longer end the turn with a fatal "declined to answer" message after the context-reset ladder.
211
- - **[0.7.28]** (2026-07-02) — gajae-code 0.7.9–0.7.10 OAuth + model-execution parity: generic long rate-limit windows are retried (never fatal), retry waits are abort-cancellable, Codex prompt caching via session correlation, and the Kimi Code device-flow OAuth is completed end-to-end (catalog, gating, discovery, tests).
212
- - **[0.7.27]** (2026-07-02) — Extended-keyboard decode layer: Esc/Ctrl+C work again on kitty-protocol terminals, Shift+Enter breaks lines on every terminal incl. Windows, row/draft-aware Cmd+←→/↑↓ + Home/End/Ctrl+Home/End, Ctrl+V text-paste fallback. Plus Anthropic 5th-gen omp-parity: catalog-derived output budgets end thinking-vs-reply starvation; refusals fail fast at the transport and the recovery ladder strips replayed thinking blocks.
213
215
 
214
216
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
215
217
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.7.31",
3
+ "version": "0.7.33",
4
4
 
5
5
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
6
6
  "type": "module",
@@ -60,12 +60,12 @@
60
60
  "dependencies": {
61
61
  "zod": "^3.24.1",
62
62
  "chalk": "^5.3.0",
63
- "js-tiktoken": "^1.0.21"
63
+ "js-tiktoken": "^1.0.21",
64
+ "typescript": "^5.6.3",
65
+ "playwright": "^1.61.0"
64
66
  },
65
67
  "devDependencies": {
66
68
  "@types/bun": "^1.1.14",
67
- "playwright": "^1.61.0",
68
- "typescript": "^5.6.3",
69
69
  "ws": "^8.21.0"
70
70
  },
71
71
  "packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800"
@@ -124,6 +124,23 @@ require_bun() {
124
124
 
125
125
  bun_bin_dir() { echo "${BUN_INSTALL:-$HOME/.bun}/bin"; }
126
126
 
127
+ # Remove a stale `joc` bin/symlink left by a pre-rename install (jeo-code's
128
+ # CLI was renamed joc -> jeo). Runs on every install so it can't silently
129
+ # reappear for machines/users who installed before the rename.
130
+ cleanup_stale_joc() {
131
+ BUN_BIN="$(bun_bin_dir)"
132
+ for STALE in "$BUN_BIN/joc" "$INSTALL_DIR/joc"; do
133
+ if [ -L "$STALE" ] || [ -f "$STALE" ]; then
134
+ if [ "$DRY_RUN" = "1" ]; then
135
+ echo "+ rm -f $STALE"
136
+ else
137
+ rm -f "$STALE"
138
+ fi
139
+ echo "Removed stale pre-rename joc bin: $STALE"
140
+ fi
141
+ done
142
+ }
143
+
127
144
  registry_key() {
128
145
  if [ -n "$SCOPE" ]; then
129
146
  case "$SCOPE" in
@@ -275,11 +292,13 @@ install_binary() {
275
292
  ( cd "$SRC_DIR" && bun build src/cli.ts --compile --outfile "$INSTALL_DIR/jeo" >/dev/null )
276
293
  chmod +x "$INSTALL_DIR/jeo" 2>/dev/null || true
277
294
  fi
295
+ cleanup_stale_joc
278
296
  }
279
297
 
280
298
  # Add a symlink in INSTALL_DIR so the documented location works even when
281
299
  # Bun's global bin dir is not on PATH.
282
300
  link_compat() {
301
+ cleanup_stale_joc
283
302
  [ "$DRY_RUN" = "1" ] && return 0
284
303
  BUN_BIN="$(bun_bin_dir)"
285
304
  [ -e "$BUN_BIN/jeo" ] && LINKED="$BUN_BIN/jeo"
@@ -8,7 +8,7 @@ INSTALL_DIR="${JEO_INSTALL_DIR:-$HOME/.local/bin}"
8
8
  PURGE=0
9
9
  [ "$1" = "--purge" ] && PURGE=1
10
10
 
11
- for BIN_NAME in jeo; do
11
+ for BIN_NAME in jeo joc; do
12
12
  if [ -L "$INSTALL_DIR/$BIN_NAME" ] || [ -f "$INSTALL_DIR/$BIN_NAME" ]; then
13
13
  rm -f "$INSTALL_DIR/$BIN_NAME"
14
14
  echo "Removed $INSTALL_DIR/$BIN_NAME"
@@ -17,7 +17,7 @@ done
17
17
 
18
18
  # Remove the bun-native link (bin + global registry entry).
19
19
  BUN_BIN="${BUN_INSTALL:-$HOME/.bun}/bin"
20
- for BIN_NAME in jeo; do
20
+ for BIN_NAME in jeo joc; do
21
21
  if [ -L "$BUN_BIN/$BIN_NAME" ] || [ -f "$BUN_BIN/$BIN_NAME" ]; then
22
22
  rm -f "$BUN_BIN/$BIN_NAME"
23
23
  echo "Removed $BUN_BIN/$BIN_NAME (bun link)"
@@ -9,6 +9,18 @@ The core runtime loop, tool registry, session management, and state persistence
9
9
  ## Key Files
10
10
  | File | Description |
11
11
  |------|-------------|
12
+ | `ast-match.ts` | Structural AST metavariable matcher for TypeScript/JavaScript (gjc `ast_grep`/`ast_edit` parity) — `$NAME`/`$_`/`$$$NAME`/`$$$` pattern matching over the `typescript` compiler API's AST, plus replacement-template rendering |
13
+ | `ast-file-scan.ts` | Shared file targeting (files/dirs/globs, gitignore + ignored-dir aware) for `ast_grep`/`ast_edit` |
14
+ | `ast-grep-tool.ts` | `ast_grep` tool — read-only structural code search using `ast-match.ts` |
15
+ | `ts-language-service.ts` | Cached in-process `ts.LanguageService` per project root (gjc `lsp` parity, TS/JS only) — tsconfig-aware root file discovery with a whole-tree fallback, live file-version tracking via stat, and position/selector resolution helpers shared by `lsp-tool.ts`/`lsp-rename-tool.ts` |
16
+ | `lsp-tool.ts` | `lsp` tool — read-only definition/references/hover/symbols/diagnostics via `ts-language-service.ts` |
17
+ | `debug-session.ts` | In-process CDP (V8 Inspector Protocol) client driving a spawned `node --inspect-brk` process (gjc `debug` parity, Node.js only — not Bun, whose inspector speaks a different WebKit dialect); one active session, singleton |
18
+ | `debug-tool.ts` | `debug` tool — launch/set_breakpoint/continue/step/evaluate/stack_trace/scopes/variables/threads/output/terminate via `debug-session.ts`; mutating (runs arbitrary code), excluded from read-only roles |
19
+ | `lsp-rename-tool.ts` | `lsp_rename` tool — cross-file TypeScript/JavaScript rename; mutating, writes via `writeTool`, kept separate from `lsp` so read-only roles can't reach it |
20
+ | `browser-session.ts` | Named-tab headless-Chromium session registry (gjc `browser` parity, via Playwright — already a jeo-code dependency) — one shared browser instance launched lazily, tabs reused by name across calls |
21
+ | `browser-tab.ts` | Per-tab helpers (`observe`/`click`/`type`/`fill`/`select`/`press`/`scroll`/`goto`/`back`/`extract`/`screenshot`/`evaluate`) — `observe()` tags interactive elements with stable numeric ids instead of returning a raw DOM dump or requiring a screenshot |
22
+ | `browser-tool.ts` | `browser` tool — open/close/run/act actions; mutating (drives a real browser, `run` executes arbitrary host JS), excluded from read-only roles |
23
+ | `ast-edit-tool.ts` | `ast_edit` tool — structural codemod using `ast-match.ts`; mutating, writes via `writeTool` |
12
24
  | `bash-fixups.ts` | Brief description of purpose |
13
25
  | `compaction.ts` | Brief description of purpose |
14
26
  | `config-schema.ts` | Brief description of purpose |
@@ -30,6 +42,9 @@ The core runtime loop, tool registry, session management, and state persistence
30
42
  | `session.ts` | Session context building, compaction, and history management |
31
43
  | `state.ts` | File-backed state and session persistence (`.jeo/state/`) |
32
44
  | `step-budget.ts` | gjc-style flexible step budgeting: progress-scored extensions, hard cap, fail-fast |
45
+ | `irc-tool.ts` | `irc` tool — parent/peer live messaging into running DETACHED subagents (list peers, send to one id or "all"); built entirely on `SubagentRegistry.steer()`, gjc `irc` parity |
46
+ | `job-registry.ts` | In-process registry for BACKGROUND shell processes spawned via `job {action:"start"}` — real parallel OS processes (not just concurrent JS), bounded output buffer, list/tail/awaitIds/cancel, gjc `job`/async-bash parity |
47
+ | `job-tool.ts` | `job` control tool — start/list/tail/await/cancel background shell processes tracked by `job-registry.ts` |
33
48
  | `subagent-registry.ts` | Brief description of purpose |
34
49
  | `subagent-tool.ts` | Brief description of purpose |
35
50
  | `subagents.ts` | Brief description of purpose |
@@ -0,0 +1,123 @@
1
+ /**
2
+ * `ast_edit` tool — structural (AST-based) codemod for TypeScript/JavaScript, gjc
3
+ * `ast_edit` parity via the same pure-TS metavariable matcher as `ast_grep`. MUTATING:
4
+ * excluded from read-only subagent roles (see `subagents.ts`'s `MUTATING` set).
5
+ *
6
+ * Each match is replaced by rendering the `replacement` template — `$NAME`/`$$$NAME`
7
+ * substituted with that match's captured text — then splicing the result into the
8
+ * original file text (matches within one file are applied in reverse source order so
9
+ * earlier replacements never shift later offsets; overlapping matches are resolved by
10
+ * keeping the earliest and skipping any later match that starts before it ends).
11
+ * Writes go through the existing `writeTool` so the deep-interview mutation lock and
12
+ * stale-read guard apply exactly as they do for `write`/`edit`.
13
+ */
14
+ import * as fs from "node:fs/promises";
15
+ import * as path from "node:path";
16
+ import * as ts from "typescript";
17
+ import type { ToolHandler } from "./engine";
18
+ import type { ToolResult } from "./tools";
19
+ import { writeTool } from "./tools";
20
+ import { findMatches, parsePattern, renderReplacement, type MatchResult } from "./ast-match";
21
+ import { resolveTargetFiles, scriptKindFor } from "./ast-file-scan";
22
+
23
+ /** One-line protocol description appended to the launch system prompt. */
24
+ export const AST_EDIT_TOOL_PROTOCOL_LINE =
25
+ `ast_edit {pattern, replacement, paths} — structural TypeScript/JavaScript rewrite. Same ` +
26
+ `pattern metavariables as ast_grep; 'replacement' is a template where $NAME/$$$NAME are ` +
27
+ `substituted with that match's captured text (empty replacement deletes matches). Rewrites ` +
28
+ `every match across the given files/dirs/globs and writes the files (mutation lock and ` +
29
+ `stale-read checks apply, same as write/edit).`;
30
+
31
+ function pathsOf(args: Record<string, any>): string[] {
32
+ if (Array.isArray(args.paths)) return args.paths.map((p: unknown) => String(p));
33
+ if (typeof args.paths === "string" && args.paths.trim()) return [args.paths];
34
+ return [];
35
+ }
36
+
37
+ export function createAstEditTool(): ToolHandler {
38
+ return async (args: Record<string, any>, cwd: string): Promise<ToolResult> => {
39
+ const pattern = typeof args.pattern === "string" ? args.pattern.trim() : "";
40
+ if (!pattern) {
41
+ return { success: false, output: "", error: `ast_edit requires a non-empty "pattern".` };
42
+ }
43
+ const replacement = typeof args.replacement === "string" ? args.replacement
44
+ : typeof args.out === "string" ? args.out : undefined;
45
+ if (replacement === undefined) {
46
+ return { success: false, output: "", error: `ast_edit requires a "replacement" template string (use "" to delete matches).` };
47
+ }
48
+ try {
49
+ parsePattern(pattern);
50
+ } catch (err: any) {
51
+ return { success: false, output: "", error: `ast_edit pattern error: ${err.message}` };
52
+ }
53
+
54
+ const rawPaths = pathsOf(args);
55
+ if (rawPaths.length === 0) {
56
+ return { success: false, output: "", error: `ast_edit requires a non-empty "paths" array (files, directories, or globs).` };
57
+ }
58
+
59
+ let files: string[];
60
+ try {
61
+ files = await resolveTargetFiles(rawPaths, cwd);
62
+ } catch (err: any) {
63
+ return { success: false, output: "", error: err.message };
64
+ }
65
+ if (files.length === 0) {
66
+ return {
67
+ success: true,
68
+ output: "No TypeScript/JavaScript files matched the given paths (ast_edit only covers .ts/.tsx/.js/.jsx/.mjs/.cjs/.mts/.cts).",
69
+ };
70
+ }
71
+
72
+ const perFile: { file: string; replaced: number }[] = [];
73
+ let totalReplaced = 0;
74
+
75
+ for (const abs of files) {
76
+ let original: string;
77
+ try {
78
+ original = await fs.readFile(abs, "utf-8");
79
+ } catch {
80
+ continue;
81
+ }
82
+ const sf = ts.createSourceFile(abs, original, ts.ScriptTarget.Latest, true, scriptKindFor(abs));
83
+ const matches = findMatches(pattern, sf);
84
+ if (matches.length === 0) continue;
85
+
86
+ // Greedy non-overlap selection: earliest-starting match wins any overlap.
87
+ const sorted = [...matches].sort((a, b) => a.node.getStart(sf) - b.node.getStart(sf));
88
+ const selected: MatchResult[] = [];
89
+ let lastEnd = -1;
90
+ for (const m of sorted) {
91
+ const start = m.node.getStart(sf);
92
+ if (start < lastEnd) continue;
93
+ selected.push(m);
94
+ lastEnd = m.node.getEnd();
95
+ }
96
+ if (selected.length === 0) continue;
97
+
98
+ const edits = selected.map(m => ({
99
+ start: m.node.getStart(sf),
100
+ end: m.node.getEnd(),
101
+ text: renderReplacement(replacement, m.captures),
102
+ }));
103
+ let newText = original;
104
+ for (const e of [...edits].sort((a, b) => b.start - a.start)) {
105
+ newText = newText.slice(0, e.start) + e.text + newText.slice(e.end);
106
+ }
107
+
108
+ const rel = path.relative(cwd, abs) || abs;
109
+ const res = await writeTool(rel, newText, cwd);
110
+ if (!res.success) {
111
+ return { success: false, output: "", error: `${rel}: ${res.error ?? "write failed"}` };
112
+ }
113
+ perFile.push({ file: rel, replaced: selected.length });
114
+ totalReplaced += selected.length;
115
+ }
116
+
117
+ if (totalReplaced === 0) {
118
+ return { success: true, output: `0 matches for pattern across ${files.length} file(s) — nothing rewritten.` };
119
+ }
120
+ const lines = perFile.map(f => `- ${f.file}: ${f.replaced} replacement(s)`);
121
+ return { success: true, output: `${totalReplaced} replacement(s) across ${perFile.length} file(s):\n${lines.join("\n")}` };
122
+ };
123
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shared file-targeting helpers for `ast_grep`/`ast_edit` — resolves a loose
3
+ * `paths` argument (files, directories, or globs) to a deduplicated, sorted list
4
+ * of TypeScript/JavaScript file absolute paths, respecting `.gitignore` and the
5
+ * same ignored-directory list as `find`/`search` (see `tools.ts`).
6
+ */
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+ import * as ts from "typescript";
10
+ import { IGNORED_DIRS, readGitignore } from "./tools";
11
+
12
+ export const AST_SUPPORTED_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
13
+
14
+ /** Hard cap on how many files a single ast_grep/ast_edit call will scan — a stray
15
+ * `paths: ["**"]` on a huge repo must not hang the turn. */
16
+ export const AST_MAX_FILES = 500;
17
+
18
+ export function scriptKindFor(file: string): ts.ScriptKind {
19
+ const ext = path.extname(file);
20
+ if (ext === ".tsx") return ts.ScriptKind.TSX;
21
+ if (ext === ".jsx") return ts.ScriptKind.JSX;
22
+ if (ext === ".ts" || ext === ".mts" || ext === ".cts") return ts.ScriptKind.TS;
23
+ return ts.ScriptKind.JS;
24
+ }
25
+
26
+ export async function resolveTargetFiles(rawPaths: string[], cwd: string): Promise<string[]> {
27
+ const gi = await readGitignore(cwd);
28
+ const prunedDirs = new Set([...IGNORED_DIRS, ...gi.dirs]);
29
+ const fileGlobs = gi.fileGlobs.map(g => new Bun.Glob(g));
30
+ const out = new Set<string>();
31
+
32
+ for (const raw of rawPaths) {
33
+ if (out.size >= AST_MAX_FILES) break;
34
+ const trimmed = raw.trim();
35
+ if (!trimmed) continue;
36
+
37
+ let pattern = trimmed;
38
+ const abs = path.resolve(cwd, trimmed);
39
+ let isDir = false;
40
+ try { isDir = (await fs.stat(abs)).isDirectory(); } catch { /* not an existing path — treat as a glob/pattern */ }
41
+
42
+ if (isDir) {
43
+ pattern = `${trimmed.replace(/\/+$/, "")}/**/*`;
44
+ } else if (!/[*?[\]]/.test(trimmed)) {
45
+ // A plain existing (or nonexistent) file path — no glob metacharacters.
46
+ if (AST_SUPPORTED_EXT.has(path.extname(trimmed))) out.add(abs);
47
+ continue;
48
+ }
49
+
50
+ for await (const rel of new Bun.Glob(pattern).scan({ cwd, onlyFiles: true })) {
51
+ const norm = rel.replace(/\\/g, "/");
52
+ const segs = norm.split("/");
53
+ if (segs.some(s => prunedDirs.has(s))) continue;
54
+ const base = segs[segs.length - 1] ?? norm;
55
+ if (fileGlobs.some(g => g.match(base))) continue;
56
+ if (!AST_SUPPORTED_EXT.has(path.extname(base))) continue;
57
+ out.add(path.resolve(cwd, norm));
58
+ if (out.size >= AST_MAX_FILES) break;
59
+ }
60
+ }
61
+ return [...out].sort();
62
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `ast_grep` tool — structural (AST-based) code search for TypeScript/JavaScript,
3
+ * gjc `ast_grep` parity via a pure-TS metavariable matcher (see `ast-match.ts`),
4
+ * scoped to `.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`/`.mts`/`.cts` — NOT the native
5
+ * ast-grep binary and NOT multi-language. Read-only; safe for every subagent role.
6
+ */
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+ import * as ts from "typescript";
10
+ import type { ToolHandler } from "./engine";
11
+ import type { ToolResult } from "./tools";
12
+ import { findMatches, parsePattern, type MatchResult } from "./ast-match";
13
+ import { resolveTargetFiles, scriptKindFor } from "./ast-file-scan";
14
+
15
+ /** One-line protocol description appended to the launch system prompt. */
16
+ export const AST_GREP_TOOL_PROTOCOL_LINE =
17
+ `ast_grep {pattern, paths} — structural TypeScript/JavaScript search (files/dirs/globs in ` +
18
+ `'paths'). Pattern metavariables: $NAME captures one node (repeats must match identical ` +
19
+ `code), $_ is a wildcard node, $$$NAME/$$$ capture/ignore zero-or-more sibling nodes in a ` +
20
+ `list (call args, statements, class members, ...). Whitespace/formatting is ignored — this ` +
21
+ `matches AST shape, not text.`;
22
+
23
+ const MAX_MATCHES_PER_FILE = 20;
24
+ const MAX_TOTAL_MATCHES = 200;
25
+
26
+ function lineOf(sf: ts.SourceFile, pos: number): number {
27
+ return sf.getLineAndCharacterOfPosition(pos).line + 1;
28
+ }
29
+
30
+ function snippet(sf: ts.SourceFile, node: ts.Node): string {
31
+ const oneLine = node.getText(sf).replace(/\s+/g, " ").trim();
32
+ return oneLine.length > 140 ? oneLine.slice(0, 139) + "…" : oneLine;
33
+ }
34
+
35
+ function captureSummary(m: MatchResult): string {
36
+ const flat: Record<string, string> = { ...m.captures.single };
37
+ for (const [k, v] of Object.entries(m.captures.multi)) flat[k] = v.join(", ");
38
+ return Object.keys(flat).length ? ` ${JSON.stringify(flat)}` : "";
39
+ }
40
+
41
+ function pathsOf(args: Record<string, any>): string[] {
42
+ if (Array.isArray(args.paths)) return args.paths.map((p: unknown) => String(p));
43
+ if (typeof args.paths === "string" && args.paths.trim()) return [args.paths];
44
+ return [];
45
+ }
46
+
47
+ export function createAstGrepTool(): ToolHandler {
48
+ return async (args: Record<string, any>, cwd: string): Promise<ToolResult> => {
49
+ const pattern = typeof args.pattern === "string" ? args.pattern.trim() : "";
50
+ if (!pattern) {
51
+ return { success: false, output: "", error: `ast_grep requires a non-empty "pattern".` };
52
+ }
53
+ try {
54
+ parsePattern(pattern);
55
+ } catch (err: any) {
56
+ return { success: false, output: "", error: `ast_grep pattern error: ${err.message}` };
57
+ }
58
+
59
+ const rawPaths = pathsOf(args);
60
+ if (rawPaths.length === 0) {
61
+ return { success: false, output: "", error: `ast_grep requires a non-empty "paths" array (files, directories, or globs).` };
62
+ }
63
+
64
+ let files: string[];
65
+ try {
66
+ files = await resolveTargetFiles(rawPaths, cwd);
67
+ } catch (err: any) {
68
+ return { success: false, output: "", error: err.message };
69
+ }
70
+ if (files.length === 0) {
71
+ return {
72
+ success: true,
73
+ output: "No TypeScript/JavaScript files matched the given paths (ast_grep only covers .ts/.tsx/.js/.jsx/.mjs/.cjs/.mts/.cts).",
74
+ };
75
+ }
76
+
77
+ const lines: string[] = [];
78
+ let total = 0;
79
+ let filesWithMatches = 0;
80
+ for (const abs of files) {
81
+ if (total >= MAX_TOTAL_MATCHES) break;
82
+ let text: string;
83
+ try {
84
+ text = await fs.readFile(abs, "utf-8");
85
+ } catch {
86
+ continue;
87
+ }
88
+ const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true, scriptKindFor(abs));
89
+ const matches = findMatches(pattern, sf);
90
+ if (matches.length === 0) continue;
91
+ filesWithMatches++;
92
+ const rel = path.relative(cwd, abs) || abs;
93
+ for (const m of matches.slice(0, MAX_MATCHES_PER_FILE)) {
94
+ if (total >= MAX_TOTAL_MATCHES) break;
95
+ total++;
96
+ const line = lineOf(sf, m.node.getStart(sf));
97
+ lines.push(`${rel}:${line}: ${snippet(sf, m.node)}${captureSummary(m)}`);
98
+ }
99
+ }
100
+
101
+ if (total === 0) {
102
+ return { success: true, output: `0 matches for pattern across ${files.length} file(s).` };
103
+ }
104
+ const capNote = total >= MAX_TOTAL_MATCHES ? ` (truncated at ${MAX_TOTAL_MATCHES})` : "";
105
+ return { success: true, output: `${total} match(es) in ${filesWithMatches} file(s)${capNote}:\n${lines.join("\n")}` };
106
+ };
107
+ }