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.
Files changed (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.ja.md +5 -3
  3. package/README.ko.md +5 -3
  4. package/README.md +5 -3
  5. package/README.zh.md +5 -3
  6. package/package.json +3 -3
  7. package/scripts/install.sh +19 -0
  8. package/scripts/uninstall.sh +2 -2
  9. package/src/agent/AGENTS.md +12 -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/compaction.ts +6 -19
  15. package/src/agent/debug-session.ts +435 -0
  16. package/src/agent/debug-tool.ts +89 -0
  17. package/src/agent/engine.ts +103 -18
  18. package/src/agent/goal-tool.ts +57 -0
  19. package/src/agent/irc-tool.ts +71 -0
  20. package/src/agent/job-registry.ts +163 -0
  21. package/src/agent/job-tool.ts +106 -0
  22. package/src/agent/lsp-rename-tool.ts +108 -0
  23. package/src/agent/lsp-tool.ts +177 -0
  24. package/src/agent/memory-graph.ts +6 -1
  25. package/src/agent/memory.ts +73 -52
  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 +100 -5
  32. package/src/agent/ts-language-service.ts +173 -0
  33. package/src/ai/model-manager.ts +58 -26
  34. package/src/ai/sse.ts +22 -7
  35. package/src/ai/think-tags.ts +10 -1
  36. package/src/commands/deep-interview.ts +49 -1
  37. package/src/commands/launch/agents-slash.ts +489 -0
  38. package/src/commands/launch/flags.ts +27 -9
  39. package/src/commands/launch/model-slash.ts +290 -0
  40. package/src/commands/launch/session-slash.ts +305 -0
  41. package/src/commands/launch/slash-handlers.ts +6 -0
  42. package/src/commands/launch/system-slash.ts +81 -0
  43. package/src/commands/launch/tmux.ts +32 -0
  44. package/src/commands/launch.ts +227 -750
  45. package/src/tui/app.ts +32 -12
  46. package/src/tui/components/evolution.ts +23 -3
  47. package/src/tui/components/width.ts +11 -6
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Structural AST pattern matcher for TypeScript/JavaScript (gjc `ast_grep`/`ast_edit`
3
+ * parity, scoped to TS/JS/TSX/JSX — jeo-code has zero native dependencies, so this is
4
+ * a pure-TS reimplementation of ast-grep's metavariable matching against the
5
+ * `typescript` compiler API's AST, NOT the real ast-grep binary and NOT multi-language.
6
+ *
7
+ * Pattern syntax (matches the `ast_grep`/`ast_edit` tool contract):
8
+ * - `$NAME` — capture exactly ONE node; two occurrences of the same name in a
9
+ * pattern must match IDENTICAL source text (e.g. `assertEqual($A, $A)`).
10
+ * - `$_` — wildcard, matches exactly one node without binding a capture.
11
+ * - `$$$NAME`/`$$$` — capture/ignore ZERO OR MORE sibling nodes in a LIST position
12
+ * (call arguments, array/object elements, statement lists, class members,
13
+ * named-import specifiers, parameter lists, ...). At most one `$$$`-style
14
+ * metavariable per list is supported.
15
+ *
16
+ * Implementation notes:
17
+ * - `ts.Node.forEachChild(cbNode, cbNodeArray)` is used to enumerate a node's
18
+ * children in canonical source order, distinguishing single-child "slots" from
19
+ * list ("NodeArray") slots — this is what lets `$$$` bind a variable-length
20
+ * span instead of a single node.
21
+ * - A metavariable identifier is frequently parsed WRAPPED in a structural node
22
+ * rather than standing bare — e.g. `($$$ARGS)` is a lone `ParameterDeclaration`
23
+ * whose `.name` is the identifier, `{ $$$IMPORTS }` is a lone `ImportSpecifier`,
24
+ * `: $_` is a `TypeReferenceNode`, `{ $$$BODY }` is a `Block` containing one
25
+ * `ExpressionStatement`. `unwrapBareWrapper()` sees through these SPECIFIC,
26
+ * well-known "no extra shape" wrappers so metavariable detection is not
27
+ * accidentally defeated by the parser's own AST shape.
28
+ * - Structural comparison otherwise requires the SAME `SyntaxKind` and recurses
29
+ * slot-by-slot. A pattern is allowed to OMIT trailing optional slots the
30
+ * candidate has (e.g. no `else` branch, no return type) and to be missing a
31
+ * LEADING/MIDDLE list-shaped slot the candidate has (typically `modifiers`/
32
+ * `decorators`, e.g. matching un-exported AND exported declarations with the
33
+ * same pattern) — up to a small bounded number of skipped candidate slots.
34
+ */
35
+ import * as ts from "typescript";
36
+
37
+ export type MetaKind = "single" | "multi" | "wild" | "wildmulti";
38
+ export interface Meta { kind: MetaKind; name: string }
39
+
40
+ function unwrapBareWrapper(node: ts.Node): ts.Node | null {
41
+ if (ts.isParameter(node) && ts.isIdentifier(node.name) && !node.type && !node.initializer
42
+ && !node.questionToken && !node.dotDotDotToken && (!node.modifiers || node.modifiers.length === 0)) {
43
+ return node.name;
44
+ }
45
+ if (ts.isImportSpecifier(node) && !node.propertyName && !node.isTypeOnly) return node.name;
46
+ if (ts.isExportSpecifier(node) && !node.propertyName && !node.isTypeOnly) return node.name;
47
+ if (ts.isBindingElement(node) && !node.propertyName && !node.initializer && !node.dotDotDotToken && ts.isIdentifier(node.name)) return node.name;
48
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && !node.typeArguments) return node.typeName;
49
+ if (ts.isExpressionStatement(node)) return node.expression;
50
+ if (ts.isShorthandPropertyAssignment(node) && !node.objectAssignmentInitializer) return node.name;
51
+ return null;
52
+ }
53
+
54
+ /** Classify a (possibly wrapper-unwrapped) node as a metavariable, or `null` for
55
+ * an ordinary AST node that must be matched structurally. */
56
+ export function metaOf(node: ts.Node): Meta | null {
57
+ const inner = unwrapBareWrapper(node);
58
+ if (inner) return metaOf(inner);
59
+ if (!ts.isIdentifier(node)) return null;
60
+ const t = node.text;
61
+ if (t === "$_") return { kind: "wild", name: "_" };
62
+ if (t === "$$$") return { kind: "wildmulti", name: "_" };
63
+ const m3 = /^\$\$\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(t);
64
+ if (m3) return { kind: "multi", name: m3[1]! };
65
+ const m1 = /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(t);
66
+ if (m1) return { kind: "single", name: m1[1]! };
67
+ return null;
68
+ }
69
+
70
+ export interface Bindings {
71
+ single: Map<string, string>;
72
+ multi: Map<string, string[]>;
73
+ }
74
+
75
+ function newBindings(): Bindings {
76
+ return { single: new Map(), multi: new Map() };
77
+ }
78
+
79
+ type Slot = { kind: "node"; node: ts.Node } | { kind: "list"; nodes: readonly ts.Node[] };
80
+
81
+ function slotsOf(node: ts.Node): Slot[] {
82
+ const out: Slot[] = [];
83
+ node.forEachChild(
84
+ child => { out.push({ kind: "node", node: child }); return undefined; },
85
+ arr => { out.push({ kind: "list", nodes: arr }); return undefined; },
86
+ );
87
+ return out;
88
+ }
89
+
90
+ function leafText(node: ts.Node): string | undefined {
91
+ if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) return node.text;
92
+ if (ts.isStringLiteralLike(node)) return node.text;
93
+ if (ts.isNumericLiteral(node) || ts.isBigIntLiteral(node)) return node.text;
94
+ return undefined;
95
+ }
96
+
97
+ /** How many candidate-only slots (e.g. `export`/decorators the pattern omitted) a
98
+ * single structural comparison may skip over before giving up. */
99
+ const MAX_SLOT_SKIPS = 2;
100
+
101
+ function matchNode(pattern: ts.Node, patternSf: ts.SourceFile, candidate: ts.Node, candidateSf: ts.SourceFile, b: Bindings): boolean {
102
+ const meta = metaOf(pattern);
103
+ if (meta) {
104
+ if (meta.kind === "wild") return true;
105
+ if (meta.kind === "single") {
106
+ const text = candidate.getText(candidateSf).trim();
107
+ const prev = b.single.get(meta.name);
108
+ if (prev !== undefined) return prev === text;
109
+ b.single.set(meta.name, text);
110
+ return true;
111
+ }
112
+ // `$$$NAME`/`$$$` reached outside a list slot (unusual pattern shape) — fall back
113
+ // to treating it as a single capture of the whole candidate rather than failing.
114
+ const text = candidate.getText(candidateSf).trim();
115
+ if (meta.kind === "multi") {
116
+ const prev = b.multi.get(meta.name);
117
+ if (prev !== undefined) return prev.join(" ") === text;
118
+ b.multi.set(meta.name, [text]);
119
+ }
120
+ return true;
121
+ }
122
+ if (pattern.kind !== candidate.kind) return false;
123
+ const lt = leafText(pattern);
124
+ if (lt !== undefined) return lt === leafText(candidate);
125
+
126
+ const pSlots = slotsOf(pattern);
127
+ const cSlots = slotsOf(candidate);
128
+ let pi = 0, ci = 0, skips = 0;
129
+ while (pi < pSlots.length) {
130
+ const ps = pSlots[pi]!;
131
+ if (ci >= cSlots.length) return false;
132
+ const cs = cSlots[ci]!;
133
+ if (ps.kind === cs.kind) {
134
+ const ok = ps.kind === "list"
135
+ ? matchList(ps.nodes, patternSf, (cs as { kind: "list"; nodes: readonly ts.Node[] }).nodes, candidateSf, b)
136
+ : matchNode((ps as { kind: "node"; node: ts.Node }).node, patternSf, (cs as { kind: "node"; node: ts.Node }).node, candidateSf, b);
137
+ if (!ok) {
138
+ if (skips < MAX_SLOT_SKIPS && ci + 1 < cSlots.length) { skips++; ci++; continue; }
139
+ return false;
140
+ }
141
+ pi++; ci++;
142
+ continue;
143
+ }
144
+ // Slot-KIND mismatch (list vs single) at this relative position — most commonly a
145
+ // `modifiers`/`decorators` list the candidate has and the pattern omitted. Skip the
146
+ // candidate slot and retry the SAME pattern slot against the next one.
147
+ if (skips < MAX_SLOT_SKIPS) { skips++; ci++; continue; }
148
+ return false;
149
+ }
150
+ return true; // trailing extra candidate slots are fine — unspecified optional parts
151
+ }
152
+
153
+ function matchList(patternNodes: readonly ts.Node[], patternSf: ts.SourceFile, candidateNodes: readonly ts.Node[], candidateSf: ts.SourceFile, b: Bindings): boolean {
154
+ const idx = patternNodes.findIndex(n => { const m = metaOf(n); return m?.kind === "multi" || m?.kind === "wildmulti"; });
155
+ if (idx === -1) {
156
+ if (patternNodes.length !== candidateNodes.length) return false;
157
+ for (let i = 0; i < patternNodes.length; i++) {
158
+ if (!matchNode(patternNodes[i]!, patternSf, candidateNodes[i]!, candidateSf, b)) return false;
159
+ }
160
+ return true;
161
+ }
162
+ const prefixLen = idx;
163
+ const suffixLen = patternNodes.length - idx - 1;
164
+ if (candidateNodes.length < prefixLen + suffixLen) return false;
165
+ for (let i = 0; i < prefixLen; i++) {
166
+ if (!matchNode(patternNodes[i]!, patternSf, candidateNodes[i]!, candidateSf, b)) return false;
167
+ }
168
+ for (let i = 0; i < suffixLen; i++) {
169
+ if (!matchNode(patternNodes[patternNodes.length - 1 - i]!, patternSf, candidateNodes[candidateNodes.length - 1 - i]!, candidateSf, b)) return false;
170
+ }
171
+ const middle = candidateNodes.slice(prefixLen, candidateNodes.length - suffixLen);
172
+ const meta = metaOf(patternNodes[idx]!)!;
173
+ if (meta.kind === "multi") {
174
+ const texts = middle.map(n => n.getText(candidateSf).trim());
175
+ const prev = b.multi.get(meta.name);
176
+ if (prev !== undefined) return prev.join(",") === texts.join(",");
177
+ b.multi.set(meta.name, texts);
178
+ }
179
+ return true;
180
+ }
181
+
182
+ /** Parse a pattern string to a single matchable node. Multi-statement snippets are
183
+ * rejected with a clear error — wrap them in a block/valid context instead (same
184
+ * discipline as real ast-grep). A bare expression statement is unwrapped to its
185
+ * expression so `$A && $A()` matches an Expression, not only an ExpressionStatement. */
186
+ export function parsePattern(src: string): { node: ts.Node; sf: ts.SourceFile } {
187
+ const sf = ts.createSourceFile("__pattern__.tsx", src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
188
+ if (sf.statements.length !== 1) {
189
+ throw new Error(`pattern must parse to exactly one top-level statement (got ${sf.statements.length}) — wrap multi-statement snippets in a block or valid context`);
190
+ }
191
+ let node: ts.Node = sf.statements[0]!;
192
+ if (ts.isExpressionStatement(node)) node = node.expression;
193
+ return { node, sf };
194
+ }
195
+
196
+ export interface MatchCaptures {
197
+ single: Record<string, string>;
198
+ multi: Record<string, string[]>;
199
+ }
200
+
201
+ export interface MatchResult {
202
+ node: ts.Node;
203
+ captures: MatchCaptures;
204
+ }
205
+
206
+ /** Find every non-overlapping-by-construction match of `patternSrc` in `sourceFile`.
207
+ * A fast prefilter only attempts nodes whose `SyntaxKind` equals the pattern's root
208
+ * kind (unless the pattern root is itself a bare metavariable). */
209
+ export function findMatches(patternSrc: string, sourceFile: ts.SourceFile): MatchResult[] {
210
+ const { node: patternNode, sf: patternSf } = parsePattern(patternSrc);
211
+ const patternIsBareMeta = metaOf(patternNode) !== null;
212
+ const out: MatchResult[] = [];
213
+ const visit = (node: ts.Node) => {
214
+ if (patternIsBareMeta || node.kind === patternNode.kind) {
215
+ const b = newBindings();
216
+ if (matchNode(patternNode, patternSf, node, sourceFile, b)) {
217
+ out.push({ node, captures: { single: Object.fromEntries(b.single), multi: Object.fromEntries(b.multi) } });
218
+ }
219
+ }
220
+ node.forEachChild(visit);
221
+ };
222
+ sourceFile.forEachChild(visit);
223
+ return out;
224
+ }
225
+
226
+ /** Render a replacement template, substituting `$NAME`/`$$$NAME` with their bound
227
+ * capture text (multi-captures join with ", "). An unbound metavariable (including
228
+ * `$_`/`$$$`, which never bind) is left as literal text in the output. */
229
+ export function renderReplacement(template: string, captures: MatchCaptures): string {
230
+ return template.replace(/\$\$\$([A-Za-z_][A-Za-z0-9_]*)|\$([A-Za-z_][A-Za-z0-9_]*)/g, (whole, multiName?: string, singleName?: string) => {
231
+ if (multiName !== undefined) {
232
+ const vals = captures.multi[multiName];
233
+ return vals !== undefined ? vals.join(", ") : whole;
234
+ }
235
+ if (singleName !== undefined) {
236
+ const val = captures.single[singleName];
237
+ return val !== undefined ? val : whole;
238
+ }
239
+ return whole;
240
+ });
241
+ }
@@ -40,27 +40,14 @@ export const DEFAULT_MAX_TOKENS = 30_000;
40
40
  export const DEFAULT_SUMMARY_INPUT_TOKENS = 20_000;
41
41
 
42
42
  export function estimateTokens(text: string): number {
43
- let tokens = 0;
43
+ // ASCII ≈ 4 chars/token; everything else (CJK, Hangul, emoji, …) ≈ 1.5 chars/token.
44
+ // A previous version listed the CJK ranges explicitly, but BOTH branches resolved
45
+ // to the same 1/1.5 weight — two counters keep the same result in one cheap pass.
46
+ let ascii = 0;
44
47
  for (let i = 0; i < text.length; i++) {
45
- const code = text.charCodeAt(i);
46
- if (code <= 127) {
47
- tokens += 0.25;
48
- } else if (
49
- (code >= 0xac00 && code <= 0xd7a3) || // 한글 가~힣
50
- (code >= 0x1100 && code <= 0x11ff) || // 한글 자모
51
- (code >= 0x3130 && code <= 0x318f) || // 한글 호환 자모
52
- (code >= 0x4e00 && code <= 0x9fff) || // CJK 통합 한자
53
- (code >= 0x3400 && code <= 0x4dbf) || // CJK 통합 한자 확장 A
54
- (code >= 0x3040 && code <= 0x309f) || // 히라가나
55
- (code >= 0x30a0 && code <= 0x30ff) || // 가타카나
56
- (code >= 0xff00 && code <= 0xffef) // 전각 문자
57
- ) {
58
- tokens += 1 / 1.5;
59
- } else {
60
- tokens += 1 / 1.5; // Default CJK-like weight for non-ASCII
61
- }
48
+ if (text.charCodeAt(i) <= 127) ascii++;
62
49
  }
63
- return tokens;
50
+ return ascii * 0.25 + (text.length - ascii) / 1.5;
64
51
  }
65
52
 
66
53
  /** Rough per-image vision-token cost (provider median for a clipboard screenshot).