codemini-cli 0.1.19 → 0.2.1
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/README.md +44 -20
- package/package.json +4 -2
- package/src/cli.js +1 -1
- package/src/commands/chat.js +1 -0
- package/src/core/agent-loop.js +7 -2
- package/src/core/ast.js +310 -0
- package/src/core/chat-runtime.js +47 -15
- package/src/core/checkpoint-store.js +2 -1
- package/src/core/command-loader.js +3 -4
- package/src/core/config-store.js +6 -3
- package/src/core/default-system-prompt.js +1 -1
- package/src/core/paths.js +52 -58
- package/src/core/project-index.js +510 -0
- package/src/core/provider/openai-compatible.js +9 -0
- package/src/core/shell-profile.js +1 -1
- package/src/core/shell.js +122 -2
- package/src/core/task-store.js +3 -2
- package/src/core/tools.js +188 -9
- package/src/tui/chat-app.js +694 -47
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ It is designed for teams that want a coding assistant that feels practical, cont
|
|
|
26
26
|
- Configurable reply language through `ui.reply_language` (`zh` / `en`)
|
|
27
27
|
- Richer slash completion with priority sorting, inline descriptions, and left/right paging
|
|
28
28
|
- Structured code tools for small models: `grep`, `read`, `edit`
|
|
29
|
+
- Tree-sitter AST tools for small models: `ast_query`, `read_ast_node`, node-scoped `edit`
|
|
29
30
|
- More conservative `plan auto` acceptance checks with reviewer/tester goal checklists
|
|
30
31
|
- Tone presets through `soul`, without changing plans or code behavior
|
|
31
32
|
- Example soul presets include `professional`, `playful`, `anime`, `pirate`, `caveman`, and `ceo`
|
|
@@ -67,6 +68,8 @@ codemini skill list|install|enable|disable|inspect|reindex
|
|
|
67
68
|
- Ambiguous feature requests can pause for lightweight brainstorming first, and `/brainstorm <question>` gives an explicit way to compare options before coding
|
|
68
69
|
- `plan auto` now turns the original goal into an acceptance checklist, uses a lighter chain only for truly tiny tasks, and treats unmet checklist items as failure signals
|
|
69
70
|
- Structured code tools reduce shell-noise for small models by preferring `grep/read -> edit`
|
|
71
|
+
- Tree-sitter-aware code tools support `grep/read -> ast_query/read_ast_node -> edit(ast_target)` for function/class-level edits with explicit node range limits
|
|
72
|
+
- Current Tree-sitter language support includes JavaScript/JSX, TypeScript/TSX, Python, Go, C, C++, Bash, Java, Rust, C#, PHP, and Ruby
|
|
70
73
|
|
|
71
74
|
### Skill Loading
|
|
72
75
|
|
|
@@ -74,16 +77,15 @@ CodeMini CLI loads skills from these locations:
|
|
|
74
77
|
|
|
75
78
|
- Bundled repo skills: `skills/<name>/SKILL.md`
|
|
76
79
|
- Installed global skills: `<base-config-dir>/skills/<name>/SKILL.md`
|
|
77
|
-
- Project-scoped
|
|
80
|
+
- Project-scoped skills: `.codemini/skills/<name>/SKILL.md`
|
|
78
81
|
|
|
79
82
|
The base config directory is resolved in this order:
|
|
80
83
|
|
|
81
|
-
- `
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
- Fallback in restricted environments: `.codemini-cli/`
|
|
84
|
+
- `CODEMINI_GLOBAL_DIR`
|
|
85
|
+
- Windows: `%APPDATA%\\codemini-global\\`
|
|
86
|
+
- macOS: `~/Library/Preferences/codemini-global`
|
|
87
|
+
- Linux/XDG: `$XDG_CONFIG_HOME/codemini-global`
|
|
88
|
+
- Fallback in restricted environments: `.codemini-global/`
|
|
87
89
|
|
|
88
90
|
### Brainstorming
|
|
89
91
|
|
|
@@ -104,9 +106,19 @@ For information on how to perform a release, please see the [Release Checklist](
|
|
|
104
106
|
|
|
105
107
|
### Data Layout
|
|
106
108
|
|
|
107
|
-
- Project-scoped workspace data: `.
|
|
108
|
-
- Global user data on Windows: `%APPDATA%\\codemini-
|
|
109
|
-
- Restricted-environment fallback: `.codemini-
|
|
109
|
+
- Project-scoped workspace data: `.codemini/`
|
|
110
|
+
- Global user data on Windows: `%APPDATA%\\codemini-global\\`
|
|
111
|
+
- Restricted-environment fallback: `.codemini-global/`
|
|
112
|
+
|
|
113
|
+
### Project Index
|
|
114
|
+
|
|
115
|
+
CodeMini CLI now maintains a lightweight project index inside `.codemini-project/`:
|
|
116
|
+
|
|
117
|
+
- `project-map.json` for project skeleton metadata such as languages, important files, source roots, test roots, and entry candidates
|
|
118
|
+
- `file-index.json` for per-file symbols, imports, exports, functions, classes, and basic call hints
|
|
119
|
+
|
|
120
|
+
The index is initialized once when entering a project and refreshed incrementally after code edits, writes, and patches. Session data stays in `.codemini/`; project structure lives in `.codemini-project/`. The index is intentionally lightweight and fact-based rather than an LLM-generated project report.
|
|
121
|
+
At request time, CodeMini injects a short project-context summary from this index into the model prompt instead of dumping the full index.
|
|
110
122
|
|
|
111
123
|
### Positioning
|
|
112
124
|
|
|
@@ -141,6 +153,7 @@ CodeMini CLI 是一个为小模型工作流优化过的代码助手 CLI,重点
|
|
|
141
153
|
- 支持通过 `ui.reply_language` 配置回复语言,当前支持 `zh` / `en`
|
|
142
154
|
- slash 补全支持优先级排序、右侧简短说明和左右分页
|
|
143
155
|
- 为小模型补了结构化代码工具:`grep`、`read`、`edit`
|
|
156
|
+
- 为小模型补了 Tree-sitter AST 工具:`ast_query`、`read_ast_node`,以及带 node 范围限制的 `edit`
|
|
144
157
|
- `plan auto` 会基于原始目标生成验收清单,并更保守地处理 reviewer/tester 结果
|
|
145
158
|
- `soul` 只影响语气,不影响计划和代码行为
|
|
146
159
|
- 可用的 `soul` 示例包括 `professional`、`playful`、`anime`、`pirate`、`caveman`、`ceo`
|
|
@@ -182,6 +195,8 @@ codemini skill list|install|enable|disable|inspect|reindex
|
|
|
182
195
|
- 对于需求仍不明确的功能请求,CLI 会先偏向轻量 brainstorm;也可以显式使用 `/brainstorm <问题>` 先比较方案再决定是否编码
|
|
183
196
|
- `plan auto` 会先把原始目标展开成验收清单;只有真正很小的任务才会走轻量链路;如果 reviewer 或 tester 标记了未满足或未验证的验收项,就不会按成功处理
|
|
184
197
|
- 为了减少小模型被 shell 原始输出干扰,新增了 `grep/read -> edit` 这套结构化代码工具流
|
|
198
|
+
- 现在也支持 `grep/read -> ast_query/read_ast_node -> edit(ast_target)` 这套 AST 工作流,适合函数级/类级精准编辑,并且能限制小模型只能修改显式选中的 node 范围
|
|
199
|
+
- 当前内置的 Tree-sitter 语言支持包括 JavaScript/JSX、TypeScript/TSX、Python、Go、C、C++、Bash、Java、Rust、C#、PHP、Ruby
|
|
185
200
|
|
|
186
201
|
### Skill 加载位置
|
|
187
202
|
|
|
@@ -189,16 +204,15 @@ CodeMini CLI 会从这些位置读取 skill:
|
|
|
189
204
|
|
|
190
205
|
- 仓库内置 skill:`skills/<name>/SKILL.md`
|
|
191
206
|
- 全局已安装 skill:`<base-config-dir>/skills/<name>/SKILL.md`
|
|
192
|
-
-
|
|
207
|
+
- 项目级 skill:`.codemini/skills/<name>/SKILL.md`
|
|
193
208
|
|
|
194
209
|
`base-config-dir` 的解析顺序是:
|
|
195
210
|
|
|
196
|
-
- `
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
- 受限环境回退:`.codemini-cli/`
|
|
211
|
+
- `CODEMINI_GLOBAL_DIR`
|
|
212
|
+
- Windows:`%APPDATA%\\codemini-global\\`
|
|
213
|
+
- macOS:`~/Library/Preferences/codemini-global`
|
|
214
|
+
- Linux / XDG:`$XDG_CONFIG_HOME/codemini-global`
|
|
215
|
+
- 受限环境回退:`.codemini-global/`
|
|
202
216
|
|
|
203
217
|
### Brainstorm 用法
|
|
204
218
|
|
|
@@ -215,6 +229,16 @@ CodeMini CLI 会从这些位置读取 skill:
|
|
|
215
229
|
|
|
216
230
|
### 数据目录
|
|
217
231
|
|
|
218
|
-
- 项目工作区数据:`.
|
|
219
|
-
- Windows 全局用户数据:`%APPDATA%\\codemini-
|
|
220
|
-
- 受限环境回退目录:`.codemini-
|
|
232
|
+
- 项目工作区数据:`.codemini/`
|
|
233
|
+
- Windows 全局用户数据:`%APPDATA%\\codemini-global\\`
|
|
234
|
+
- 受限环境回退目录:`.codemini-global/`
|
|
235
|
+
|
|
236
|
+
### 项目索引
|
|
237
|
+
|
|
238
|
+
CodeMini CLI 现在会在 `.codemini-project/` 下维护一份轻量项目索引:
|
|
239
|
+
|
|
240
|
+
- `project-map.json`:记录项目骨架信息,例如语言、关键文件、源码目录、测试目录、入口候选
|
|
241
|
+
- `file-index.json`:记录文件级结构信息,例如 symbols、imports、exports、functions、classes、基础 calls
|
|
242
|
+
|
|
243
|
+
这份索引会在进入项目时初始化一次,在 `edit`、`write`、`patch` 后做增量刷新。`.codemini/` 继续保存会话态数据,`.codemini-project/` 保存项目结构索引。它是程序维护的轻量事实索引,不是模型生成的项目总结。
|
|
244
|
+
在真正请求模型时,CodeMini 会从这份索引里裁一小段项目摘要注入 prompt,而不是把整份索引直接塞进去。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codemini-cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -46,9 +46,11 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@cursorless/tree-sitter-wasms": "^0.5.0",
|
|
49
50
|
"ink": "^6.3.1",
|
|
50
51
|
"openai": "^6.33.0",
|
|
51
|
-
"react": "^19.0.0"
|
|
52
|
+
"react": "^19.0.0",
|
|
53
|
+
"web-tree-sitter": "^0.26.7"
|
|
52
54
|
},
|
|
53
55
|
"license": "MIT"
|
|
54
56
|
}
|
package/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { handleConfig } from './commands/config.js';
|
|
|
4
4
|
import { handleDoctor } from './commands/doctor.js';
|
|
5
5
|
import { handleSkill } from './commands/skill.js';
|
|
6
6
|
|
|
7
|
-
const VERSION = '0.1
|
|
7
|
+
const VERSION = '0.2.1';
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log(`codemini ${VERSION}
|
package/src/commands/chat.js
CHANGED
|
@@ -99,6 +99,7 @@ export async function handleChat(args) {
|
|
|
99
99
|
model: parsed.model || config.model.name,
|
|
100
100
|
language: config.ui?.language || 'zh',
|
|
101
101
|
shellName: config.shell?.default || 'powershell',
|
|
102
|
+
safeMode: config.policy?.safe_mode !== false,
|
|
102
103
|
version: pkg.version
|
|
103
104
|
})
|
|
104
105
|
);
|
package/src/core/agent-loop.js
CHANGED
|
@@ -262,7 +262,7 @@ export async function runAgentLoop({
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
if (!approved) {
|
|
265
|
-
if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id });
|
|
265
|
+
if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id, arguments: args });
|
|
266
266
|
const blockedMessage = {
|
|
267
267
|
role: 'tool',
|
|
268
268
|
tool_call_id: call.id,
|
|
@@ -274,6 +274,7 @@ export async function runAgentLoop({
|
|
|
274
274
|
type: 'tool:result',
|
|
275
275
|
name: displayName,
|
|
276
276
|
id: call.id,
|
|
277
|
+
arguments: args,
|
|
277
278
|
content: blockedMessage.content,
|
|
278
279
|
blocked: true
|
|
279
280
|
});
|
|
@@ -281,7 +282,7 @@ export async function runAgentLoop({
|
|
|
281
282
|
continue;
|
|
282
283
|
}
|
|
283
284
|
|
|
284
|
-
if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id });
|
|
285
|
+
if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id, arguments: args });
|
|
285
286
|
const handler = toolHandlers[toolName];
|
|
286
287
|
if (!handler) {
|
|
287
288
|
throw new Error(`Unknown tool: ${call.name}`);
|
|
@@ -297,6 +298,7 @@ export async function runAgentLoop({
|
|
|
297
298
|
type: 'tool:error',
|
|
298
299
|
name: displayName,
|
|
299
300
|
id: call.id,
|
|
301
|
+
arguments: args,
|
|
300
302
|
durationMs,
|
|
301
303
|
summary: trimInline(message, 120)
|
|
302
304
|
});
|
|
@@ -312,6 +314,7 @@ export async function runAgentLoop({
|
|
|
312
314
|
type: 'tool:result',
|
|
313
315
|
name: displayName,
|
|
314
316
|
id: call.id,
|
|
317
|
+
arguments: args,
|
|
315
318
|
content: toolMessage.content,
|
|
316
319
|
error: true
|
|
317
320
|
});
|
|
@@ -324,6 +327,7 @@ export async function runAgentLoop({
|
|
|
324
327
|
type: 'tool:end',
|
|
325
328
|
name: displayName,
|
|
326
329
|
id: call.id,
|
|
330
|
+
arguments: args,
|
|
327
331
|
durationMs,
|
|
328
332
|
summary: summarizeToolResult(toolResult)
|
|
329
333
|
});
|
|
@@ -339,6 +343,7 @@ export async function runAgentLoop({
|
|
|
339
343
|
type: 'tool:result',
|
|
340
344
|
name: displayName,
|
|
341
345
|
id: call.id,
|
|
346
|
+
arguments: args,
|
|
342
347
|
content: toolMessage.content
|
|
343
348
|
});
|
|
344
349
|
}
|
package/src/core/ast.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { Parser, Language, Query } from 'web-tree-sitter';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
const IDENTIFIER_NODE_TYPES = new Set([
|
|
10
|
+
'identifier',
|
|
11
|
+
'property_identifier',
|
|
12
|
+
'type_identifier',
|
|
13
|
+
'word',
|
|
14
|
+
'field_identifier',
|
|
15
|
+
'name'
|
|
16
|
+
]);
|
|
17
|
+
const WRAPPER_NODE_TYPES = new Set([
|
|
18
|
+
'declarator',
|
|
19
|
+
'qualified_identifier',
|
|
20
|
+
'template_function',
|
|
21
|
+
'template_type'
|
|
22
|
+
]);
|
|
23
|
+
const LANGUAGE_ALIASES = {
|
|
24
|
+
javascript: 'js',
|
|
25
|
+
js: 'js',
|
|
26
|
+
jsx: 'js',
|
|
27
|
+
typescript: 'ts',
|
|
28
|
+
ts: 'ts',
|
|
29
|
+
tsx: 'tsx',
|
|
30
|
+
python: 'python',
|
|
31
|
+
py: 'python',
|
|
32
|
+
go: 'go',
|
|
33
|
+
c: 'c',
|
|
34
|
+
cpp: 'cpp',
|
|
35
|
+
'c++': 'cpp',
|
|
36
|
+
bash: 'bash',
|
|
37
|
+
sh: 'bash',
|
|
38
|
+
shell: 'bash',
|
|
39
|
+
java: 'java',
|
|
40
|
+
rust: 'rust',
|
|
41
|
+
rs: 'rust',
|
|
42
|
+
csharp: 'csharp',
|
|
43
|
+
'c#': 'csharp',
|
|
44
|
+
cs: 'csharp',
|
|
45
|
+
php: 'php',
|
|
46
|
+
ruby: 'ruby',
|
|
47
|
+
rb: 'ruby'
|
|
48
|
+
};
|
|
49
|
+
const EXTENSION_LANGUAGE_MAP = {
|
|
50
|
+
'.js': 'js',
|
|
51
|
+
'.jsx': 'js',
|
|
52
|
+
'.mjs': 'js',
|
|
53
|
+
'.cjs': 'js',
|
|
54
|
+
'.ts': 'ts',
|
|
55
|
+
'.tsx': 'tsx',
|
|
56
|
+
'.py': 'python',
|
|
57
|
+
'.go': 'go',
|
|
58
|
+
'.c': 'c',
|
|
59
|
+
'.h': 'c',
|
|
60
|
+
'.cpp': 'cpp',
|
|
61
|
+
'.cc': 'cpp',
|
|
62
|
+
'.cxx': 'cpp',
|
|
63
|
+
'.hpp': 'cpp',
|
|
64
|
+
'.hh': 'cpp',
|
|
65
|
+
'.java': 'java',
|
|
66
|
+
'.rs': 'rust',
|
|
67
|
+
'.cs': 'csharp',
|
|
68
|
+
'.php': 'php',
|
|
69
|
+
'.rb': 'ruby',
|
|
70
|
+
'.sh': 'bash',
|
|
71
|
+
'.bash': 'bash'
|
|
72
|
+
};
|
|
73
|
+
const LANGUAGE_WASM_PATHS = {
|
|
74
|
+
js: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-javascript.wasm'),
|
|
75
|
+
ts: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-typescript.wasm'),
|
|
76
|
+
tsx: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-tsx.wasm'),
|
|
77
|
+
python: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-python.wasm'),
|
|
78
|
+
go: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-go.wasm'),
|
|
79
|
+
c: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-c.wasm'),
|
|
80
|
+
cpp: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-cpp.wasm'),
|
|
81
|
+
bash: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-bash.wasm'),
|
|
82
|
+
java: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-java.wasm'),
|
|
83
|
+
rust: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-rust.wasm'),
|
|
84
|
+
csharp: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm'),
|
|
85
|
+
php: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-php.wasm'),
|
|
86
|
+
ruby: require.resolve('@cursorless/tree-sitter-wasms/out/tree-sitter-ruby.wasm')
|
|
87
|
+
};
|
|
88
|
+
const TREE_SITTER_WASM_PATH = require.resolve('web-tree-sitter/web-tree-sitter.wasm');
|
|
89
|
+
|
|
90
|
+
const parserInitPromise = Parser.init({
|
|
91
|
+
locateFile(scriptName) {
|
|
92
|
+
return scriptName === 'web-tree-sitter.wasm' ? TREE_SITTER_WASM_PATH : scriptName;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const languageCache = new Map();
|
|
96
|
+
|
|
97
|
+
function sha256(input) {
|
|
98
|
+
return `sha256:${crypto.createHash('sha256').update(String(input || '')).digest('hex')}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clipText(text, maxLen = 220) {
|
|
102
|
+
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
|
103
|
+
if (normalized.length <= maxLen) return normalized;
|
|
104
|
+
return `${normalized.slice(0, maxLen - 3)}...`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function pointFromTarget(line, column) {
|
|
108
|
+
return {
|
|
109
|
+
row: Math.max(0, Number(line || 1) - 1),
|
|
110
|
+
column: Math.max(0, Number(column || 1) - 1)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function pointsEqual(left, right) {
|
|
115
|
+
return left.row === right.row && left.column === right.column;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function summarizeNode(node) {
|
|
119
|
+
if (!node) return '';
|
|
120
|
+
const text = clipText(node.text, 96);
|
|
121
|
+
return `${node.type}${text ? `: ${text}` : ''}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function selectEditableNode(node) {
|
|
125
|
+
let current = node;
|
|
126
|
+
while (current?.parent) {
|
|
127
|
+
if (IDENTIFIER_NODE_TYPES.has(current.type)) {
|
|
128
|
+
current = current.parent;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (current.type.endsWith('_declarator') || WRAPPER_NODE_TYPES.has(current.type)) {
|
|
132
|
+
current = current.parent;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
return current;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function astTargetForNode(relativePath, language, node) {
|
|
141
|
+
return {
|
|
142
|
+
path: relativePath,
|
|
143
|
+
language,
|
|
144
|
+
node_type: node.type,
|
|
145
|
+
start_line: node.startPosition.row + 1,
|
|
146
|
+
start_column: node.startPosition.column + 1,
|
|
147
|
+
end_line: node.endPosition.row + 1,
|
|
148
|
+
end_column: node.endPosition.column + 1,
|
|
149
|
+
range_hash: sha256(node.text)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function inferLanguage(filePath, explicitLanguage = '') {
|
|
154
|
+
const alias = LANGUAGE_ALIASES[String(explicitLanguage || '').trim().toLowerCase()];
|
|
155
|
+
if (alias) return alias;
|
|
156
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
157
|
+
const inferred = EXTENSION_LANGUAGE_MAP[ext];
|
|
158
|
+
if (!inferred) {
|
|
159
|
+
throw new Error(`No Tree-sitter language configured for file: ${filePath}`);
|
|
160
|
+
}
|
|
161
|
+
return inferred;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function loadLanguage(language) {
|
|
165
|
+
await parserInitPromise;
|
|
166
|
+
if (languageCache.has(language)) return languageCache.get(language);
|
|
167
|
+
const wasmPath = LANGUAGE_WASM_PATHS[language];
|
|
168
|
+
if (!wasmPath) throw new Error(`Unsupported Tree-sitter language: ${language}`);
|
|
169
|
+
const loaded = await Language.load(wasmPath);
|
|
170
|
+
languageCache.set(language, loaded);
|
|
171
|
+
return loaded;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function parseContent(content, language) {
|
|
175
|
+
const loadedLanguage = await loadLanguage(language);
|
|
176
|
+
const parser = new Parser();
|
|
177
|
+
parser.setLanguage(loadedLanguage);
|
|
178
|
+
const tree = parser.parse(content);
|
|
179
|
+
return { parser, tree, loadedLanguage };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function parseFile(root, relativePath, explicitLanguage = '') {
|
|
183
|
+
const target = path.resolve(root, relativePath);
|
|
184
|
+
const content = await fs.readFile(target, 'utf8');
|
|
185
|
+
const language = inferLanguage(relativePath, explicitLanguage);
|
|
186
|
+
const parsed = await parseContent(content, language);
|
|
187
|
+
return {
|
|
188
|
+
...parsed,
|
|
189
|
+
path: relativePath,
|
|
190
|
+
absolutePath: target,
|
|
191
|
+
content,
|
|
192
|
+
language
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function exactNodeForTarget(rootNode, target) {
|
|
197
|
+
const start = pointFromTarget(target.start_line, target.start_column);
|
|
198
|
+
const end = pointFromTarget(target.end_line, target.end_column);
|
|
199
|
+
let current = rootNode.namedDescendantForPosition(start, end) || rootNode.descendantForPosition(start, end);
|
|
200
|
+
while (current) {
|
|
201
|
+
if (pointsEqual(current.startPosition, start) && pointsEqual(current.endPosition, end)) {
|
|
202
|
+
return current;
|
|
203
|
+
}
|
|
204
|
+
current = current.parent;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function queryAst(root, args) {
|
|
210
|
+
const relativePath = String(args?.path || '').trim();
|
|
211
|
+
const querySource = String(args?.query || '').trim();
|
|
212
|
+
if (!relativePath || !querySource) {
|
|
213
|
+
throw new Error('ast_query requires path and query');
|
|
214
|
+
}
|
|
215
|
+
const captureName = String(args?.capture_name || '').trim();
|
|
216
|
+
const maxResults = Math.max(1, Math.min(100, Number(args?.max_results || 12)));
|
|
217
|
+
const parsed = await parseFile(root, relativePath, args?.language);
|
|
218
|
+
const query = new Query(parsed.loadedLanguage, querySource);
|
|
219
|
+
const captures = query.captures(parsed.tree.rootNode);
|
|
220
|
+
const matches = [];
|
|
221
|
+
|
|
222
|
+
for (const capture of captures) {
|
|
223
|
+
if (captureName && capture.name !== captureName) continue;
|
|
224
|
+
const targetNode = selectEditableNode(capture.node);
|
|
225
|
+
matches.push({
|
|
226
|
+
capture: capture.name,
|
|
227
|
+
node_type: targetNode.type,
|
|
228
|
+
start_line: targetNode.startPosition.row + 1,
|
|
229
|
+
start_column: targetNode.startPosition.column + 1,
|
|
230
|
+
end_line: targetNode.endPosition.row + 1,
|
|
231
|
+
end_column: targetNode.endPosition.column + 1,
|
|
232
|
+
text: clipText(targetNode.text),
|
|
233
|
+
ast_target: astTargetForNode(relativePath, parsed.language, targetNode)
|
|
234
|
+
});
|
|
235
|
+
if (matches.length >= maxResults) break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
query.delete();
|
|
239
|
+
parsed.tree.delete();
|
|
240
|
+
parsed.parser.delete();
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
path: relativePath,
|
|
244
|
+
language: parsed.language,
|
|
245
|
+
query: querySource,
|
|
246
|
+
capture_name: captureName || undefined,
|
|
247
|
+
matches,
|
|
248
|
+
truncated: captures.length > matches.length
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function readAstNode(root, args) {
|
|
253
|
+
const relativePath = String(args?.path || args?.ast_target?.path || '').trim();
|
|
254
|
+
const astTarget = args?.ast_target;
|
|
255
|
+
if (!relativePath || !astTarget) throw new Error('read_ast_node requires path and ast_target');
|
|
256
|
+
const parsed = await parseFile(root, relativePath, astTarget.language || args?.language);
|
|
257
|
+
const node = exactNodeForTarget(parsed.tree.rootNode, astTarget);
|
|
258
|
+
if (!node) {
|
|
259
|
+
throw new Error('AST target no longer matches the current file');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const result = {
|
|
263
|
+
path: relativePath,
|
|
264
|
+
language: parsed.language,
|
|
265
|
+
node: {
|
|
266
|
+
node_type: node.type,
|
|
267
|
+
start_line: node.startPosition.row + 1,
|
|
268
|
+
start_column: node.startPosition.column + 1,
|
|
269
|
+
end_line: node.endPosition.row + 1,
|
|
270
|
+
end_column: node.endPosition.column + 1
|
|
271
|
+
},
|
|
272
|
+
content: node.text,
|
|
273
|
+
parent_summary: summarizeNode(node.parent),
|
|
274
|
+
child_summaries: node.namedChildren.slice(0, 8).map((child) => summarizeNode(child))
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
parsed.tree.delete();
|
|
278
|
+
parsed.parser.delete();
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function resolveAstTarget(root, relativePath, astTarget) {
|
|
283
|
+
if (!astTarget || typeof astTarget !== 'object') {
|
|
284
|
+
throw new Error('ast_target is required for AST-scoped edit');
|
|
285
|
+
}
|
|
286
|
+
if (String(astTarget.path || '').trim() !== String(relativePath || '').trim()) {
|
|
287
|
+
throw new Error('ast_target path does not match edit file');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const parsed = await parseFile(root, relativePath, astTarget.language);
|
|
291
|
+
const node = exactNodeForTarget(parsed.tree.rootNode, astTarget);
|
|
292
|
+
if (!node) {
|
|
293
|
+
parsed.tree.delete();
|
|
294
|
+
parsed.parser.delete();
|
|
295
|
+
throw new Error('AST target no longer matches the current file');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const currentHash = sha256(node.text);
|
|
299
|
+
if (String(astTarget.range_hash || '') !== currentHash) {
|
|
300
|
+
parsed.tree.delete();
|
|
301
|
+
parsed.parser.delete();
|
|
302
|
+
throw new Error('ast_target range_hash mismatch; the selected node changed and is now stale');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
...parsed,
|
|
307
|
+
node,
|
|
308
|
+
current_hash: currentHash
|
|
309
|
+
};
|
|
310
|
+
}
|