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
|
@@ -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
|
+
}
|
package/src/agent/compaction.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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).
|