codemini-cli 0.5.10 → 0.5.12
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/OPERATIONS.md +242 -242
- package/README.md +588 -588
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-B-G99D0A.js} +1 -1
- package/codemini-web/dist/assets/{index-BK75hMb2.js → index-DIGUEzan.js} +108 -108
- package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-va2Kl89u.js +1 -0
- package/codemini-web/dist/index.html +35 -23
- package/codemini-web/lib/approval-manager.js +32 -32
- package/codemini-web/lib/runtime-bridge.js +17 -11
- package/codemini-web/server.js +534 -205
- package/deployment.md +212 -212
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +77 -77
- package/skills/codemini.skills.json +40 -40
- package/skills/grill-me/SKILL.md +30 -30
- package/skills/superpowers-lite/SKILL.md +82 -82
- package/src/cli.js +74 -74
- package/src/commands/chat.js +210 -210
- package/src/commands/run.js +313 -313
- package/src/commands/skill.js +438 -304
- package/src/commands/web.js +57 -57
- package/src/core/agent-loop.js +980 -980
- package/src/core/ast.js +309 -307
- package/src/core/chat-runtime.js +6261 -6253
- package/src/core/command-evaluator.js +72 -72
- package/src/core/command-loader.js +311 -311
- package/src/core/command-policy.js +301 -301
- package/src/core/command-risk.js +156 -156
- package/src/core/config-store.js +286 -285
- package/src/core/constants.js +18 -1
- package/src/core/context-compact.js +365 -365
- package/src/core/default-system-prompt.js +114 -107
- package/src/core/dream-audit.js +105 -105
- package/src/core/dream-consolidate.js +229 -229
- package/src/core/dream-evaluator.js +185 -185
- package/src/core/fff-adapter.js +383 -383
- package/src/core/memory-store.js +543 -543
- package/src/core/project-index.js +737 -548
- package/src/core/project-instructions.js +98 -98
- package/src/core/provider/anthropic.js +514 -514
- package/src/core/provider/openai-compatible.js +501 -501
- package/src/core/reflect-skill.js +178 -178
- package/src/core/reply-language.js +40 -40
- package/src/core/session-store.js +474 -474
- package/src/core/shell-profile.js +237 -237
- package/src/core/shell.js +323 -323
- package/src/core/soul.js +69 -69
- package/src/core/system-prompt-composer.js +52 -52
- package/src/core/tool-args.js +199 -154
- package/src/core/tool-output.js +184 -184
- package/src/core/tool-result-store.js +206 -206
- package/src/core/tools.js +3024 -2893
- package/src/core/version.js +11 -11
- package/src/tui/chat-app.js +5173 -5171
- package/src/tui/tool-activity/presenters/misc.js +30 -30
- package/src/tui/tool-activity/presenters/system.js +20 -20
- package/templates/project-requirements/report-shell.html +582 -582
- package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
package/src/core/ast.js
CHANGED
|
@@ -1,312 +1,314 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { createRequire } from 'node:module';
|
|
4
|
-
import { Parser, Language, Query } from 'web-tree-sitter';
|
|
5
|
-
import { LANGUAGE_ALIASES, EXTENSION_LANGUAGE_MAP } from './constants.js';
|
|
6
|
-
import { sha256Prefixed as sha256 } from './crypto-utils.js';
|
|
7
|
-
import { BoundedCache } from './bounded-cache.js';
|
|
8
|
-
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
|
-
|
|
11
|
-
const IDENTIFIER_NODE_TYPES = new Set([
|
|
12
|
-
'identifier',
|
|
13
|
-
'property_identifier',
|
|
14
|
-
'type_identifier',
|
|
15
|
-
'word',
|
|
16
|
-
'field_identifier',
|
|
17
|
-
'name'
|
|
18
|
-
]);
|
|
19
|
-
const WRAPPER_NODE_TYPES = new Set([
|
|
20
|
-
'declarator',
|
|
21
|
-
'qualified_identifier',
|
|
22
|
-
'template_function',
|
|
23
|
-
'template_type'
|
|
24
|
-
]);
|
|
25
|
-
const LANGUAGE_WASM_PATHS = {
|
|
26
|
-
js: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-javascript.wasm'),
|
|
27
|
-
ts: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-typescript.wasm'),
|
|
28
|
-
tsx: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-tsx.wasm'),
|
|
29
|
-
python: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-python.wasm'),
|
|
30
|
-
go: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-go.wasm'),
|
|
31
|
-
c: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-c.wasm'),
|
|
32
|
-
cpp: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-cpp.wasm'),
|
|
33
|
-
bash: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-bash.wasm'),
|
|
34
|
-
java: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-java.wasm'),
|
|
35
|
-
rust: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-rust.wasm'),
|
|
36
|
-
csharp: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm'),
|
|
37
|
-
php: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-php.wasm'),
|
|
38
|
-
ruby: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-ruby.wasm')
|
|
39
|
-
};
|
|
40
|
-
const TREE_SITTER_WASM_PATH = require.resolve('web-tree-sitter/web-tree-sitter.wasm');
|
|
41
|
-
|
|
42
|
-
const parserInitPromise = Parser.init({
|
|
43
|
-
locateFile(scriptName) {
|
|
44
|
-
return scriptName === 'web-tree-sitter.wasm' ? TREE_SITTER_WASM_PATH : scriptName;
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
const languageCache = new BoundedCache({ maxSize: 16, ttlMs: 60 * 60 * 1000 });
|
|
48
|
-
|
|
49
|
-
function clipText(text, maxLen = 220) {
|
|
50
|
-
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
51
|
-
if (normalized.length <= maxLen) return normalized;
|
|
52
|
-
return `${normalized.slice(0, maxLen - 3)}...`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function pointFromTarget(line, column) {
|
|
56
|
-
return {
|
|
57
|
-
row: Math.max(0, Number(line || 1) - 1),
|
|
58
|
-
column: Math.max(0, Number(column || 1) - 1)
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function pointsEqual(left, right) {
|
|
63
|
-
return left.row === right.row && left.column === right.column;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function summarizeNode(node) {
|
|
67
|
-
if (!node) return '';
|
|
68
|
-
const text = clipText(node.text, 96);
|
|
69
|
-
return `${node.type}${text ? `: ${text}` : ''}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function selectEditableNode(node) {
|
|
73
|
-
let current = node;
|
|
74
|
-
while (current?.parent) {
|
|
75
|
-
if (IDENTIFIER_NODE_TYPES.has(current.type)) {
|
|
76
|
-
current = current.parent;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (current.type.endsWith('_declarator') || WRAPPER_NODE_TYPES.has(current.type)) {
|
|
80
|
-
current = current.parent;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
return current;
|
|
86
|
-
}
|
|
87
|
-
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { Parser, Language, Query } from 'web-tree-sitter';
|
|
5
|
+
import { LANGUAGE_ALIASES, EXTENSION_LANGUAGE_MAP } from './constants.js';
|
|
6
|
+
import { sha256Prefixed as sha256 } from './crypto-utils.js';
|
|
7
|
+
import { BoundedCache } from './bounded-cache.js';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
const IDENTIFIER_NODE_TYPES = new Set([
|
|
12
|
+
'identifier',
|
|
13
|
+
'property_identifier',
|
|
14
|
+
'type_identifier',
|
|
15
|
+
'word',
|
|
16
|
+
'field_identifier',
|
|
17
|
+
'name'
|
|
18
|
+
]);
|
|
19
|
+
const WRAPPER_NODE_TYPES = new Set([
|
|
20
|
+
'declarator',
|
|
21
|
+
'qualified_identifier',
|
|
22
|
+
'template_function',
|
|
23
|
+
'template_type'
|
|
24
|
+
]);
|
|
25
|
+
const LANGUAGE_WASM_PATHS = {
|
|
26
|
+
js: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-javascript.wasm'),
|
|
27
|
+
ts: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-typescript.wasm'),
|
|
28
|
+
tsx: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-tsx.wasm'),
|
|
29
|
+
python: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-python.wasm'),
|
|
30
|
+
go: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-go.wasm'),
|
|
31
|
+
c: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-c.wasm'),
|
|
32
|
+
cpp: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-cpp.wasm'),
|
|
33
|
+
bash: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-bash.wasm'),
|
|
34
|
+
java: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-java.wasm'),
|
|
35
|
+
rust: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-rust.wasm'),
|
|
36
|
+
csharp: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm'),
|
|
37
|
+
php: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-php.wasm'),
|
|
38
|
+
ruby: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-ruby.wasm')
|
|
39
|
+
};
|
|
40
|
+
const TREE_SITTER_WASM_PATH = require.resolve('web-tree-sitter/web-tree-sitter.wasm');
|
|
41
|
+
|
|
42
|
+
const parserInitPromise = Parser.init({
|
|
43
|
+
locateFile(scriptName) {
|
|
44
|
+
return scriptName === 'web-tree-sitter.wasm' ? TREE_SITTER_WASM_PATH : scriptName;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const languageCache = new BoundedCache({ maxSize: 16, ttlMs: 60 * 60 * 1000 });
|
|
48
|
+
|
|
49
|
+
function clipText(text, maxLen = 220) {
|
|
50
|
+
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
51
|
+
if (normalized.length <= maxLen) return normalized;
|
|
52
|
+
return `${normalized.slice(0, maxLen - 3)}...`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function pointFromTarget(line, column) {
|
|
56
|
+
return {
|
|
57
|
+
row: Math.max(0, Number(line || 1) - 1),
|
|
58
|
+
column: Math.max(0, Number(column || 1) - 1)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pointsEqual(left, right) {
|
|
63
|
+
return left.row === right.row && left.column === right.column;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function summarizeNode(node) {
|
|
67
|
+
if (!node) return '';
|
|
68
|
+
const text = clipText(node.text, 96);
|
|
69
|
+
return `${node.type}${text ? `: ${text}` : ''}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function selectEditableNode(node) {
|
|
73
|
+
let current = node;
|
|
74
|
+
while (current?.parent) {
|
|
75
|
+
if (IDENTIFIER_NODE_TYPES.has(current.type)) {
|
|
76
|
+
current = current.parent;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (current.type.endsWith('_declarator') || WRAPPER_NODE_TYPES.has(current.type)) {
|
|
80
|
+
current = current.parent;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
return current;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
88
|
function astTargetForNode(relativePath, language, node) {
|
|
89
|
+
const name = String(node.childForFieldName?.('name')?.text || '').trim();
|
|
89
90
|
return {
|
|
90
91
|
path: relativePath,
|
|
91
92
|
language,
|
|
93
|
+
...(name ? { name } : {}),
|
|
92
94
|
node_type: node.type,
|
|
93
|
-
start_line: node.startPosition.row + 1,
|
|
94
|
-
start_column: node.startPosition.column + 1,
|
|
95
|
-
end_line: node.endPosition.row + 1,
|
|
96
|
-
end_column: node.endPosition.column + 1,
|
|
97
|
-
range_hash: sha256(node.text)
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function inferLanguage(filePath, explicitLanguage = '') {
|
|
102
|
-
const alias = LANGUAGE_ALIASES[String(explicitLanguage || '').trim().toLowerCase()];
|
|
103
|
-
if (alias) return alias;
|
|
104
|
-
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
105
|
-
const inferred = EXTENSION_LANGUAGE_MAP[ext];
|
|
106
|
-
if (!inferred) {
|
|
107
|
-
throw new Error(`No Tree-sitter language configured for file: ${filePath}`);
|
|
108
|
-
}
|
|
109
|
-
return inferred;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function loadLanguage(language) {
|
|
113
|
-
await parserInitPromise;
|
|
114
|
-
if (languageCache.has(language)) return languageCache.get(language);
|
|
115
|
-
const wasmPath = LANGUAGE_WASM_PATHS[language];
|
|
116
|
-
if (!wasmPath) throw new Error(`Unsupported Tree-sitter language: ${language}`);
|
|
117
|
-
const loadPromise = Language.load(wasmPath);
|
|
118
|
-
languageCache.set(language, loadPromise);
|
|
119
|
-
try {
|
|
120
|
-
return await loadPromise;
|
|
121
|
-
} catch (error) {
|
|
122
|
-
languageCache.delete(language);
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function getParser(language) {
|
|
128
|
-
const loadedLanguage = await loadLanguage(language);
|
|
129
|
-
const parser = new Parser();
|
|
130
|
-
parser.setLanguage(loadedLanguage);
|
|
131
|
-
return { parser, loadedLanguage };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function deleteParsed(parsed) {
|
|
135
|
-
try {
|
|
136
|
-
parsed?.tree?.delete?.();
|
|
137
|
-
} catch {}
|
|
138
|
-
try {
|
|
139
|
-
parsed?.parser?.delete?.();
|
|
140
|
-
} catch {}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function parseContent(content, language) {
|
|
144
|
-
const { parser, loadedLanguage } = await getParser(language);
|
|
145
|
-
const tree = parser.parse(content);
|
|
146
|
-
return { parser, tree, loadedLanguage };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function parseFile(root, relativePath, explicitLanguage = '') {
|
|
150
|
-
const target = path.resolve(root, relativePath);
|
|
151
|
-
const content = await fs.readFile(target, 'utf8');
|
|
152
|
-
const language = inferLanguage(relativePath, explicitLanguage);
|
|
153
|
-
const parsed = await parseContent(content, language);
|
|
154
|
-
return {
|
|
155
|
-
...parsed,
|
|
156
|
-
path: relativePath,
|
|
157
|
-
absolutePath: target,
|
|
158
|
-
content,
|
|
159
|
-
language
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function exactNodeForTarget(rootNode, target) {
|
|
164
|
-
const start = pointFromTarget(target.start_line, target.start_column);
|
|
165
|
-
const end = pointFromTarget(target.end_line, target.end_column);
|
|
166
|
-
let current = rootNode.namedDescendantForPosition(start, end) || rootNode.descendantForPosition(start, end);
|
|
167
|
-
while (current) {
|
|
168
|
-
if (pointsEqual(current.startPosition, start) && pointsEqual(current.endPosition, end)) {
|
|
169
|
-
return current;
|
|
170
|
-
}
|
|
171
|
-
current = current.parent;
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Find the enclosing named structural symbol (function, class, method, etc.)
|
|
178
|
-
* for a given line range in already-parsed content. Returns null if not found
|
|
179
|
-
* or if the language is unsupported.
|
|
180
|
-
*/
|
|
181
|
-
export async function findEnclosingSymbol(content, filePath, line) {
|
|
182
|
-
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
183
|
-
const language = EXTENSION_LANGUAGE_MAP[ext];
|
|
184
|
-
if (!language) return null;
|
|
185
|
-
let parser = null;
|
|
186
|
-
let tree = null;
|
|
187
|
-
try {
|
|
188
|
-
const parsed = await parseContent(content, language);
|
|
189
|
-
parser = parsed.parser;
|
|
190
|
-
tree = parsed.tree;
|
|
191
|
-
const row = Math.max(0, Number(line || 1) - 1);
|
|
192
|
-
const node = tree.rootNode.descendantForPosition({ row, column: 0 });
|
|
193
|
-
let current = node;
|
|
194
|
-
while (current) {
|
|
195
|
-
if (current.type === 'program' || !current.parent) break;
|
|
196
|
-
const nameChild = current.childForFieldName('name');
|
|
197
|
-
if (nameChild) {
|
|
198
|
-
return {
|
|
199
|
-
name: nameChild.text,
|
|
200
|
-
kind: current.type,
|
|
201
|
-
start_line: current.startPosition.row + 1,
|
|
202
|
-
end_line: current.endPosition.row + 1
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
current = current.parent;
|
|
206
|
-
}
|
|
207
|
-
return null;
|
|
208
|
-
} catch {
|
|
209
|
-
return null;
|
|
210
|
-
} finally {
|
|
211
|
-
deleteParsed({ tree, parser });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export async function queryAst(root, args) {
|
|
216
|
-
const relativePath = String(args?.path || '').trim();
|
|
217
|
-
const querySource = String(args?.query || '').trim();
|
|
218
|
-
if (!relativePath || !querySource) {
|
|
219
|
-
throw new Error('ast_query requires path and query');
|
|
220
|
-
}
|
|
221
|
-
const captureName = String(args?.capture_name || '').trim();
|
|
222
|
-
const maxResults = Math.max(1, Math.min(100, Number(args?.max_results || 12)));
|
|
223
|
-
const parsed = await parseFile(root, relativePath, args?.language);
|
|
224
|
-
const query = new Query(parsed.loadedLanguage, querySource);
|
|
225
|
-
const captures = query.captures(parsed.tree.rootNode);
|
|
226
|
-
const matches = [];
|
|
227
|
-
|
|
228
|
-
for (const capture of captures) {
|
|
229
|
-
if (captureName && capture.name !== captureName) continue;
|
|
230
|
-
const targetNode = selectEditableNode(capture.node);
|
|
231
|
-
matches.push({
|
|
232
|
-
capture: capture.name,
|
|
233
|
-
node_type: targetNode.type,
|
|
234
|
-
start_line: targetNode.startPosition.row + 1,
|
|
235
|
-
start_column: targetNode.startPosition.column + 1,
|
|
236
|
-
end_line: targetNode.endPosition.row + 1,
|
|
237
|
-
end_column: targetNode.endPosition.column + 1,
|
|
238
|
-
text: clipText(targetNode.text),
|
|
239
|
-
ast_target: astTargetForNode(relativePath, parsed.language, targetNode)
|
|
240
|
-
});
|
|
241
|
-
if (matches.length >= maxResults) break;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
query.delete();
|
|
245
|
-
deleteParsed(parsed);
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
path: relativePath,
|
|
249
|
-
language: parsed.language,
|
|
250
|
-
query: querySource,
|
|
251
|
-
capture_name: captureName || undefined,
|
|
252
|
-
matches,
|
|
253
|
-
truncated: captures.length > matches.length
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export async function readAstNode(root, args) {
|
|
258
|
-
const relativePath = String(args?.path || args?.ast_target?.path || '').trim();
|
|
259
|
-
const astTarget = args?.ast_target;
|
|
260
|
-
if (!relativePath || !astTarget) throw new Error('read_ast_node requires path and ast_target');
|
|
261
|
-
const parsed = await parseFile(root, relativePath, astTarget.language || args?.language);
|
|
262
|
-
const node = exactNodeForTarget(parsed.tree.rootNode, astTarget);
|
|
263
|
-
if (!node) {
|
|
264
|
-
throw new Error('AST target no longer matches the current file');
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const result = {
|
|
268
|
-
path: relativePath,
|
|
269
|
-
language: parsed.language,
|
|
270
|
-
node: {
|
|
271
|
-
node_type: node.type,
|
|
272
|
-
start_line: node.startPosition.row + 1,
|
|
273
|
-
start_column: node.startPosition.column + 1,
|
|
274
|
-
end_line: node.endPosition.row + 1,
|
|
275
|
-
end_column: node.endPosition.column + 1
|
|
276
|
-
},
|
|
277
|
-
content: node.text,
|
|
278
|
-
parent_summary: summarizeNode(node.parent),
|
|
279
|
-
child_summaries: node.namedChildren.slice(0, 8).map((child) => summarizeNode(child))
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
deleteParsed(parsed);
|
|
283
|
-
return result;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export async function resolveAstTarget(root, relativePath, astTarget) {
|
|
287
|
-
if (!astTarget || typeof astTarget !== 'object') {
|
|
288
|
-
throw new Error('ast_target is required for AST-scoped edit');
|
|
289
|
-
}
|
|
290
|
-
if (String(astTarget.path || '').trim() !== String(relativePath || '').trim()) {
|
|
291
|
-
throw new Error('ast_target path does not match edit file');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const parsed = await parseFile(root, relativePath, astTarget.language);
|
|
295
|
-
const node = exactNodeForTarget(parsed.tree.rootNode, astTarget);
|
|
296
|
-
if (!node) {
|
|
297
|
-
deleteParsed(parsed);
|
|
298
|
-
throw new Error('AST target no longer matches the current file');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const currentHash = sha256(node.text);
|
|
302
|
-
if (String(astTarget.range_hash || '') !== currentHash) {
|
|
303
|
-
deleteParsed(parsed);
|
|
304
|
-
throw new Error('ast_target range_hash mismatch; the selected node changed and is now stale');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
...parsed,
|
|
309
|
-
node,
|
|
310
|
-
current_hash: currentHash
|
|
311
|
-
};
|
|
312
|
-
}
|
|
95
|
+
start_line: node.startPosition.row + 1,
|
|
96
|
+
start_column: node.startPosition.column + 1,
|
|
97
|
+
end_line: node.endPosition.row + 1,
|
|
98
|
+
end_column: node.endPosition.column + 1,
|
|
99
|
+
range_hash: sha256(node.text)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function inferLanguage(filePath, explicitLanguage = '') {
|
|
104
|
+
const alias = LANGUAGE_ALIASES[String(explicitLanguage || '').trim().toLowerCase()];
|
|
105
|
+
if (alias) return alias;
|
|
106
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
107
|
+
const inferred = EXTENSION_LANGUAGE_MAP[ext];
|
|
108
|
+
if (!inferred) {
|
|
109
|
+
throw new Error(`No Tree-sitter language configured for file: ${filePath}`);
|
|
110
|
+
}
|
|
111
|
+
return inferred;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function loadLanguage(language) {
|
|
115
|
+
await parserInitPromise;
|
|
116
|
+
if (languageCache.has(language)) return languageCache.get(language);
|
|
117
|
+
const wasmPath = LANGUAGE_WASM_PATHS[language];
|
|
118
|
+
if (!wasmPath) throw new Error(`Unsupported Tree-sitter language: ${language}`);
|
|
119
|
+
const loadPromise = Language.load(wasmPath);
|
|
120
|
+
languageCache.set(language, loadPromise);
|
|
121
|
+
try {
|
|
122
|
+
return await loadPromise;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
languageCache.delete(language);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function getParser(language) {
|
|
130
|
+
const loadedLanguage = await loadLanguage(language);
|
|
131
|
+
const parser = new Parser();
|
|
132
|
+
parser.setLanguage(loadedLanguage);
|
|
133
|
+
return { parser, loadedLanguage };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function deleteParsed(parsed) {
|
|
137
|
+
try {
|
|
138
|
+
parsed?.tree?.delete?.();
|
|
139
|
+
} catch {}
|
|
140
|
+
try {
|
|
141
|
+
parsed?.parser?.delete?.();
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function parseContent(content, language) {
|
|
146
|
+
const { parser, loadedLanguage } = await getParser(language);
|
|
147
|
+
const tree = parser.parse(content);
|
|
148
|
+
return { parser, tree, loadedLanguage };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function parseFile(root, relativePath, explicitLanguage = '') {
|
|
152
|
+
const target = path.resolve(root, relativePath);
|
|
153
|
+
const content = await fs.readFile(target, 'utf8');
|
|
154
|
+
const language = inferLanguage(relativePath, explicitLanguage);
|
|
155
|
+
const parsed = await parseContent(content, language);
|
|
156
|
+
return {
|
|
157
|
+
...parsed,
|
|
158
|
+
path: relativePath,
|
|
159
|
+
absolutePath: target,
|
|
160
|
+
content,
|
|
161
|
+
language
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function exactNodeForTarget(rootNode, target) {
|
|
166
|
+
const start = pointFromTarget(target.start_line, target.start_column);
|
|
167
|
+
const end = pointFromTarget(target.end_line, target.end_column);
|
|
168
|
+
let current = rootNode.namedDescendantForPosition(start, end) || rootNode.descendantForPosition(start, end);
|
|
169
|
+
while (current) {
|
|
170
|
+
if (pointsEqual(current.startPosition, start) && pointsEqual(current.endPosition, end)) {
|
|
171
|
+
return current;
|
|
172
|
+
}
|
|
173
|
+
current = current.parent;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Find the enclosing named structural symbol (function, class, method, etc.)
|
|
180
|
+
* for a given line range in already-parsed content. Returns null if not found
|
|
181
|
+
* or if the language is unsupported.
|
|
182
|
+
*/
|
|
183
|
+
export async function findEnclosingSymbol(content, filePath, line) {
|
|
184
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
185
|
+
const language = EXTENSION_LANGUAGE_MAP[ext];
|
|
186
|
+
if (!language) return null;
|
|
187
|
+
let parser = null;
|
|
188
|
+
let tree = null;
|
|
189
|
+
try {
|
|
190
|
+
const parsed = await parseContent(content, language);
|
|
191
|
+
parser = parsed.parser;
|
|
192
|
+
tree = parsed.tree;
|
|
193
|
+
const row = Math.max(0, Number(line || 1) - 1);
|
|
194
|
+
const node = tree.rootNode.descendantForPosition({ row, column: 0 });
|
|
195
|
+
let current = node;
|
|
196
|
+
while (current) {
|
|
197
|
+
if (current.type === 'program' || !current.parent) break;
|
|
198
|
+
const nameChild = current.childForFieldName('name');
|
|
199
|
+
if (nameChild) {
|
|
200
|
+
return {
|
|
201
|
+
name: nameChild.text,
|
|
202
|
+
kind: current.type,
|
|
203
|
+
start_line: current.startPosition.row + 1,
|
|
204
|
+
end_line: current.endPosition.row + 1
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
current = current.parent;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
} finally {
|
|
213
|
+
deleteParsed({ tree, parser });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function queryAst(root, args) {
|
|
218
|
+
const relativePath = String(args?.path || '').trim();
|
|
219
|
+
const querySource = String(args?.query || '').trim();
|
|
220
|
+
if (!relativePath || !querySource) {
|
|
221
|
+
throw new Error('ast_query requires path and query');
|
|
222
|
+
}
|
|
223
|
+
const captureName = String(args?.capture_name || '').trim();
|
|
224
|
+
const maxResults = Math.max(1, Math.min(100, Number(args?.max_results || 12)));
|
|
225
|
+
const parsed = await parseFile(root, relativePath, args?.language);
|
|
226
|
+
const query = new Query(parsed.loadedLanguage, querySource);
|
|
227
|
+
const captures = query.captures(parsed.tree.rootNode);
|
|
228
|
+
const matches = [];
|
|
229
|
+
|
|
230
|
+
for (const capture of captures) {
|
|
231
|
+
if (captureName && capture.name !== captureName) continue;
|
|
232
|
+
const targetNode = selectEditableNode(capture.node);
|
|
233
|
+
matches.push({
|
|
234
|
+
capture: capture.name,
|
|
235
|
+
node_type: targetNode.type,
|
|
236
|
+
start_line: targetNode.startPosition.row + 1,
|
|
237
|
+
start_column: targetNode.startPosition.column + 1,
|
|
238
|
+
end_line: targetNode.endPosition.row + 1,
|
|
239
|
+
end_column: targetNode.endPosition.column + 1,
|
|
240
|
+
text: clipText(targetNode.text),
|
|
241
|
+
ast_target: astTargetForNode(relativePath, parsed.language, targetNode)
|
|
242
|
+
});
|
|
243
|
+
if (matches.length >= maxResults) break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
query.delete();
|
|
247
|
+
deleteParsed(parsed);
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
path: relativePath,
|
|
251
|
+
language: parsed.language,
|
|
252
|
+
query: querySource,
|
|
253
|
+
capture_name: captureName || undefined,
|
|
254
|
+
matches,
|
|
255
|
+
truncated: captures.length > matches.length
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function readAstNode(root, args) {
|
|
260
|
+
const relativePath = String(args?.path || args?.ast_target?.path || '').trim();
|
|
261
|
+
const astTarget = args?.ast_target;
|
|
262
|
+
if (!relativePath || !astTarget) throw new Error('read_ast_node requires path and ast_target');
|
|
263
|
+
const parsed = await parseFile(root, relativePath, astTarget.language || args?.language);
|
|
264
|
+
const node = exactNodeForTarget(parsed.tree.rootNode, astTarget);
|
|
265
|
+
if (!node) {
|
|
266
|
+
throw new Error('AST target no longer matches the current file');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const result = {
|
|
270
|
+
path: relativePath,
|
|
271
|
+
language: parsed.language,
|
|
272
|
+
node: {
|
|
273
|
+
node_type: node.type,
|
|
274
|
+
start_line: node.startPosition.row + 1,
|
|
275
|
+
start_column: node.startPosition.column + 1,
|
|
276
|
+
end_line: node.endPosition.row + 1,
|
|
277
|
+
end_column: node.endPosition.column + 1
|
|
278
|
+
},
|
|
279
|
+
content: node.text,
|
|
280
|
+
parent_summary: summarizeNode(node.parent),
|
|
281
|
+
child_summaries: node.namedChildren.slice(0, 8).map((child) => summarizeNode(child))
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
deleteParsed(parsed);
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function resolveAstTarget(root, relativePath, astTarget) {
|
|
289
|
+
if (!astTarget || typeof astTarget !== 'object') {
|
|
290
|
+
throw new Error('ast_target is required for AST-scoped edit');
|
|
291
|
+
}
|
|
292
|
+
if (String(astTarget.path || '').trim() !== String(relativePath || '').trim()) {
|
|
293
|
+
throw new Error('ast_target path does not match edit file');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const parsed = await parseFile(root, relativePath, astTarget.language);
|
|
297
|
+
const node = exactNodeForTarget(parsed.tree.rootNode, astTarget);
|
|
298
|
+
if (!node) {
|
|
299
|
+
deleteParsed(parsed);
|
|
300
|
+
throw new Error('AST target no longer matches the current file');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const currentHash = sha256(node.text);
|
|
304
|
+
if (String(astTarget.range_hash || '') !== currentHash) {
|
|
305
|
+
deleteParsed(parsed);
|
|
306
|
+
throw new Error('ast_target range_hash mismatch; the selected node changed and is now stale');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
...parsed,
|
|
311
|
+
node,
|
|
312
|
+
current_hash: currentHash
|
|
313
|
+
};
|
|
314
|
+
}
|