jeo-code 0.7.30 → 0.7.32
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/CHANGELOG.md +46 -0
- package/README.ja.md +5 -3
- package/README.ko.md +5 -3
- package/README.md +5 -3
- package/README.zh.md +5 -3
- package/package.json +3 -3
- package/scripts/install.sh +19 -0
- package/scripts/uninstall.sh +2 -2
- package/src/agent/AGENTS.md +12 -0
- package/src/agent/ast-edit-tool.ts +123 -0
- package/src/agent/ast-file-scan.ts +62 -0
- package/src/agent/ast-grep-tool.ts +107 -0
- package/src/agent/ast-match.ts +241 -0
- package/src/agent/compaction.ts +6 -19
- package/src/agent/debug-session.ts +435 -0
- package/src/agent/debug-tool.ts +89 -0
- package/src/agent/engine.ts +103 -18
- package/src/agent/goal-tool.ts +57 -0
- package/src/agent/irc-tool.ts +71 -0
- package/src/agent/job-registry.ts +163 -0
- package/src/agent/job-tool.ts +106 -0
- package/src/agent/lsp-rename-tool.ts +108 -0
- package/src/agent/lsp-tool.ts +177 -0
- package/src/agent/memory-graph.ts +6 -1
- package/src/agent/memory.ts +73 -52
- package/src/agent/session.ts +27 -0
- package/src/agent/subagent-registry.ts +40 -4
- package/src/agent/subagent-tool.ts +27 -7
- package/src/agent/subagents.ts +1 -1
- package/src/agent/task-tool.ts +27 -5
- package/src/agent/tools.ts +100 -5
- package/src/agent/ts-language-service.ts +173 -0
- package/src/ai/model-manager.ts +58 -26
- package/src/ai/sse.ts +22 -7
- package/src/ai/think-tags.ts +10 -1
- package/src/commands/deep-interview.ts +49 -1
- package/src/commands/launch/agents-slash.ts +489 -0
- package/src/commands/launch/flags.ts +27 -9
- package/src/commands/launch/model-slash.ts +290 -0
- package/src/commands/launch/session-slash.ts +305 -0
- package/src/commands/launch/slash-handlers.ts +6 -0
- package/src/commands/launch/system-slash.ts +81 -0
- package/src/commands/launch/tmux.ts +32 -0
- package/src/commands/launch.ts +227 -750
- package/src/tui/app.ts +32 -12
- package/src/tui/components/evolution.ts +23 -3
- package/src/tui/components/width.ts +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,52 @@ 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.32] - 2026-07-03
|
|
10
|
+
_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)._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`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).
|
|
14
|
+
- **`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.
|
|
15
|
+
- **`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.
|
|
16
|
+
- **`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.
|
|
17
|
+
- **`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.
|
|
18
|
+
- **`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.
|
|
19
|
+
- **`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.
|
|
20
|
+
|
|
21
|
+
### Hardened
|
|
22
|
+
_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:_
|
|
23
|
+
- **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.
|
|
24
|
+
- **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.
|
|
25
|
+
- **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.
|
|
26
|
+
- **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.
|
|
27
|
+
|
|
28
|
+
### Verified
|
|
29
|
+
- `bun run typecheck` clean; full suite green.
|
|
30
|
+
- 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.
|
|
31
|
+
|
|
32
|
+
## [0.7.31] - 2026-07-03
|
|
33
|
+
_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._
|
|
34
|
+
|
|
35
|
+
### Performance
|
|
36
|
+
- **Bash tool output streaming no longer accumulates without bound.** `drainPipe` (`src/agent/tools.ts`) stops growing its buffer once it reaches the `bashTool` truncation cap and keeps only a small rolling 16KB tail for the live view afterward — a chatty long-running command no longer holds hundreds of MB in memory for the whole timeout window, and the live `onChunk` callback receives a bounded string instead of the full cumulative buffer (was making every downstream sanitize/wrap pass O(n²) over stream length).
|
|
37
|
+
- **Engine stream emission is throttled (~80ms) instead of firing per token.** `onModelStream`/`onReasoningStream` in `src/agent/engine.ts` no longer re-flatten the growing response rope and re-scan it in the TUI on every streamed token; a guaranteed final flush (`flushStreams()`) delivers the complete text before parse/commit so no content is ever lost to the throttle. Per-step call-signature digests are now computed once and reused across the repeat/cycle/step-budget novelty checks instead of being re-hashed three times.
|
|
38
|
+
- **Stream idle-watchdog no longer allocates a Promise+timer pair per chunk.** `nextMaybeIdle`/`retryableStream` (`src/ai/model-manager.ts`) now set up one watchdog per stream attempt and reuse it across every `iter.next()` call instead of constructing a new race per yielded token.
|
|
39
|
+
- **SSE line-splitting and think-tag boundary checks are now allocation-free.** `readLines` (`src/ai/sse.ts`) scans the buffer incrementally with `indexOf` instead of re-splitting the full accumulated buffer on every chunk; `partialTail` (`src/ai/think-tags.ts`) checks a candidate tag boundary with an index-based `charCodeAt` loop instead of allocating a substring per check.
|
|
40
|
+
- **Memory retrieval no longer rebuilds corpus stats and the concept graph twice per turn.** `selectWithinBudget` (`src/agent/memory.ts`) now builds `CorpusStats` and the `ConceptGraph` once and threads them through `priorityOrder`, instead of `buildCorpusStats`/`buildConceptGraph` re-running on every subset scored. Per-concept lowercased fields (title/description/body/type/tags) are cached in a module-level `WeakMap` keyed by object identity, eliminating redundant `.toLowerCase()` calls across repeated scoring passes. Concept file loading in `loadConceptsFromBundle` is now batched (16 files in flight at a time) instead of either fully sequential or fully unbounded, preserving throughput without unbounded file-descriptor/RAM spikes on large memory bundles.
|
|
41
|
+
- **Concept graph adjacency is cached per graph instance.** `undirectedAdjacency` (`src/agent/memory-graph.ts`) is now computed once and cached on `ConceptGraph.adj`, avoiding an O(seeds × edges) reconstruction on every graph-proximity expansion.
|
|
42
|
+
- **`estimateTokens` (`src/agent/compaction.ts`) simplified to a single-pass ASCII/non-ASCII counter** — a prior version's explicit CJK-range branching resolved to the same 1 / 1.5 chars-per-token weight as the plain non-ASCII branch, so the two are now one cheaper pass with identical results.
|
|
43
|
+
- **TUI gradient rendering is time-quantized and memoized.** `applyDimensionalGradient` (`src/tui/components/evolution.ts`) now buckets its animation clock to 225ms steps (20 discrete phases over the 4.5s cycle) and caches up to 16 recent `(bucket, style, text)` results, so idle ticks that land in the same bucket reuse the prior truecolor escape sequence instead of re-computing and re-printing ~2-4KB of ANSI on every tick.
|
|
44
|
+
- **`wrapTextWithAnsi` (`src/tui/components/width.ts`) gained a plain-ASCII fast path** that slices fixed-width chunks directly instead of calling `visibleWidth` repeatedly over the remaining text — the common (no ANSI, no wide/combining characters) wrapping case drops from quadratic O(L²/cols) to linear O(L).
|
|
45
|
+
- **`LaunchTui` (`src/tui/app.ts`) caches wrapped history lines** (keyed by line-array identity + width) instead of re-wrapping the entire scrollback history on every render tick; `detectColorLevel` is now resolved once in the constructor instead of being re-parsed from `process.env` 2-4 times per draw; the Edit-tool live-output view is sanitized only over its bounded visible tail instead of the full cumulative buffer.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- **Deep-interview no longer loops on a re-asked question.** `runDeepInterviewEngine` (`src/commands/deep-interview.ts`) now normalizes each candidate follow-up question (case, whitespace, trailing punctuation) and tracks repeats: the first time the model re-asks the same question it gets one corrective bounce (steered toward a new gap or dimension, without re-prompting the user); if it repeats again after the correction, the interview stops instead of burning through the remaining rounds on a question that already has an answer. The Socratic system prompt also gains an explicit "never re-ask an answered question" rule, and the `[HOLD]` acceptance-criteria follow-up path runs through the same guard — an `--auto` run that previously burned all 10 rounds (10 LLM calls) on one repeated question now stops after 3.
|
|
49
|
+
- **`jeo --tmux` no longer renders its first frames at 80x24 (gajae-code #1376 parity).** A detached `tmux new-session` defaults to 80x24 until a client attaches, so the inner jeo's banner/status frames were drawn mis-wrapped into the pane scrollback before the attach resized the window. The launch path now sizes the detached session to the caller terminal (`new-session -d -x <cols> -y <rows>` via new `callerTmuxTerminalSize`/`tmuxNewSessionSizeArgs` helpers in `src/commands/launch/tmux.ts`) and re-asserts the dimensions with a best-effort `resize-window` right before attach. Unknown/invalid dimensions or a non-TTY stdout skip sizing entirely.
|
|
50
|
+
|
|
51
|
+
### Verified
|
|
52
|
+
- `bun run typecheck` clean; full suite green (batched run: 1138/1138 across 140 files with no failures, remainder consistent with prior releases).
|
|
53
|
+
- New/updated coverage: `test/deep-interview.test.ts` (+4 cases for the repeat-question guard, including an alternating-question case that confirms non-repeating follow-ups never trip the guard) and `test/tmux.test.ts` (+2 cases for caller-terminal size resolution and `-x/-y` arg construction).
|
|
54
|
+
|
|
9
55
|
## [0.7.30] - 2026-07-02
|
|
10
56
|
_GPT/Codex OAuth calls work again, and the 30-minute turn budget now limits no-progress stalls instead of killing genuinely progressing autonomous runs._
|
|
11
57
|
|
package/README.ja.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<img src="assets/character.
|
|
19
|
+
<img src="assets/character.gif" alt="動く jeo-code 赤いザリガニマスコット" width="320" />
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
@@ -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.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).
|
|
211
|
+
- **[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.
|
|
208
212
|
- **[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.
|
|
209
213
|
- **[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.
|
|
210
214
|
- **[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).
|
|
211
|
-
- **[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.
|
|
212
|
-
- **[0.7.26]** (2026-07-02) — Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits.
|
|
213
215
|
|
|
214
216
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
217
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<img src="assets/character.
|
|
19
|
+
<img src="assets/character.gif" alt="움직이는 jeo-code 붉은 가재 마스코트" width="320" />
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
@@ -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.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).
|
|
211
|
+
- **[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.
|
|
208
212
|
- **[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.
|
|
209
213
|
- **[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.
|
|
210
214
|
- **[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).
|
|
211
|
-
- **[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.
|
|
212
|
-
- **[0.7.26]** (2026-07-02) — Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits.
|
|
213
215
|
|
|
214
216
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
215
217
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<img src="assets/character.
|
|
19
|
+
<img src="assets/character.gif" alt="animated jeo-code red crayfish mascot" width="320" />
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
@@ -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.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).
|
|
238
|
+
- **[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.
|
|
235
239
|
- **[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.
|
|
236
240
|
- **[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.
|
|
237
241
|
- **[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).
|
|
238
|
-
- **[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.
|
|
239
|
-
- **[0.7.26]** (2026-07-02) — Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits.
|
|
240
242
|
|
|
241
243
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
242
244
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<img src="assets/character.
|
|
19
|
+
<img src="assets/character.gif" alt="动态 jeo-code 红色小龙虾吉祥物" width="320" />
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
@@ -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.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).
|
|
211
|
+
- **[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.
|
|
208
212
|
- **[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.
|
|
209
213
|
- **[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.
|
|
210
214
|
- **[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).
|
|
211
|
-
- **[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.
|
|
212
|
-
- **[0.7.26]** (2026-07-02) — Esc/Ctrl+C reliably clears the input box (fixes a gate the earlier reorder missed), and un-bracketed multi-line pastes no longer shred into premature per-line submits.
|
|
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.
|
|
3
|
+
"version": "0.7.32",
|
|
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"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@types/bun": "^1.1.14",
|
|
67
68
|
"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"
|
package/scripts/install.sh
CHANGED
|
@@ -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"
|
package/scripts/uninstall.sh
CHANGED
|
@@ -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)"
|
package/src/agent/AGENTS.md
CHANGED
|
@@ -9,6 +9,15 @@ 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
|
+
| `ast-edit-tool.ts` | `ast_edit` tool — structural codemod using `ast-match.ts`; mutating, writes via `writeTool` |
|
|
12
21
|
| `bash-fixups.ts` | Brief description of purpose |
|
|
13
22
|
| `compaction.ts` | Brief description of purpose |
|
|
14
23
|
| `config-schema.ts` | Brief description of purpose |
|
|
@@ -30,6 +39,9 @@ The core runtime loop, tool registry, session management, and state persistence
|
|
|
30
39
|
| `session.ts` | Session context building, compaction, and history management |
|
|
31
40
|
| `state.ts` | File-backed state and session persistence (`.jeo/state/`) |
|
|
32
41
|
| `step-budget.ts` | gjc-style flexible step budgeting: progress-scored extensions, hard cap, fail-fast |
|
|
42
|
+
| `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 |
|
|
43
|
+
| `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 |
|
|
44
|
+
| `job-tool.ts` | `job` control tool — start/list/tail/await/cancel background shell processes tracked by `job-registry.ts` |
|
|
33
45
|
| `subagent-registry.ts` | Brief description of purpose |
|
|
34
46
|
| `subagent-tool.ts` | Brief description of purpose |
|
|
35
47
|
| `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
|
+
}
|