codemini-cli 0.2.0 → 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/ast.js +310 -0
- package/src/core/chat-runtime.js +37 -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/shell-profile.js +1 -1
- package/src/core/task-store.js +3 -2
- package/src/core/tools.js +173 -6
- package/src/tui/chat-app.js +111 -13
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.2.
|
|
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/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
|
+
}
|
package/src/core/chat-runtime.js
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
} from './context-compact.js';
|
|
29
29
|
import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
|
|
30
30
|
import { buildSystemPromptWithSoul } from './soul.js';
|
|
31
|
+
import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir } from './paths.js';
|
|
32
|
+
import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
|
|
31
33
|
|
|
32
34
|
function toOpenAIMessages(sessionMessages) {
|
|
33
35
|
const mapped = [];
|
|
@@ -875,10 +877,13 @@ async function buildAutoPlanFinalSummary({
|
|
|
875
877
|
}
|
|
876
878
|
}
|
|
877
879
|
|
|
878
|
-
async function
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
880
|
+
async function writeMarkdownInProjectDir(subDir, title, body, fallbackName, sessionId) {
|
|
881
|
+
const dir =
|
|
882
|
+
subDir === 'specs'
|
|
883
|
+
? getProjectSpecsDir(process.cwd(), sessionId)
|
|
884
|
+
: subDir === 'plans'
|
|
885
|
+
? getProjectPlansDir(process.cwd(), sessionId)
|
|
886
|
+
: path.join(getProjectWorkspaceDir(process.cwd()), subDir, ...(sessionId ? [String(sessionId)] : []));
|
|
882
887
|
await fs.mkdir(dir, { recursive: true });
|
|
883
888
|
const slug = slugify(title).slice(0, 64);
|
|
884
889
|
const fileName = `${nowStamp()}-${slug || fallbackName}.md`;
|
|
@@ -1038,7 +1043,7 @@ async function collectLikelyImplementationFiles(cwd) {
|
|
|
1038
1043
|
return;
|
|
1039
1044
|
}
|
|
1040
1045
|
for (const entry of entries) {
|
|
1041
|
-
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.
|
|
1046
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.codemini') continue;
|
|
1042
1047
|
const abs = path.join(dir, entry.name);
|
|
1043
1048
|
if (entry.isDirectory()) {
|
|
1044
1049
|
await visit(abs);
|
|
@@ -1162,8 +1167,8 @@ function stampedMessage(role, content, extra = {}) {
|
|
|
1162
1167
|
async function resolveSpecPath(rawArg = '', sessionId = '') {
|
|
1163
1168
|
const input = String(rawArg || '').trim();
|
|
1164
1169
|
const roots = [
|
|
1165
|
-
|
|
1166
|
-
|
|
1170
|
+
getProjectSpecsDir(process.cwd(), String(sessionId || '')),
|
|
1171
|
+
getProjectSpecsDir(process.cwd())
|
|
1167
1172
|
];
|
|
1168
1173
|
|
|
1169
1174
|
if (input) {
|
|
@@ -1304,10 +1309,16 @@ async function askModel({
|
|
|
1304
1309
|
await saveSession(session);
|
|
1305
1310
|
}
|
|
1306
1311
|
|
|
1312
|
+
const projectContextSnippet = await buildProjectContextSnippet(process.cwd(), text).catch(() => '');
|
|
1313
|
+
const effectiveSystemPrompt = projectContextSnippet
|
|
1314
|
+
? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance. Prefer tools for fresh verification before assuming details.`
|
|
1315
|
+
: systemPrompt;
|
|
1316
|
+
|
|
1307
1317
|
const { definitions, handlers } = getBuiltinTools({
|
|
1308
1318
|
workspaceRoot: process.cwd(),
|
|
1309
1319
|
config,
|
|
1310
|
-
sessionId: session.id
|
|
1320
|
+
sessionId: session.id,
|
|
1321
|
+
onSystemEvent: onAgentEvent
|
|
1311
1322
|
});
|
|
1312
1323
|
|
|
1313
1324
|
let activeAssistantIndex = -1;
|
|
@@ -1353,7 +1364,7 @@ async function askModel({
|
|
|
1353
1364
|
|
|
1354
1365
|
const loopUserPrompt = persistSession ? '' : text;
|
|
1355
1366
|
const loopResult = await runAgentLoop({
|
|
1356
|
-
systemPrompt,
|
|
1367
|
+
systemPrompt: effectiveSystemPrompt,
|
|
1357
1368
|
userPrompt: loopUserPrompt,
|
|
1358
1369
|
model: model || config.model.name,
|
|
1359
1370
|
maxSteps: Number(config.execution?.max_steps || 16),
|
|
@@ -1651,7 +1662,7 @@ async function buildAutoPlanAndRun({
|
|
|
1651
1662
|
lines.push('');
|
|
1652
1663
|
});
|
|
1653
1664
|
|
|
1654
|
-
const filePath = await
|
|
1665
|
+
const filePath = await writeMarkdownInProjectDir(
|
|
1655
1666
|
'plans',
|
|
1656
1667
|
`${goal}-auto`,
|
|
1657
1668
|
lines.join('\n'),
|
|
@@ -1701,6 +1712,16 @@ export async function createChatRuntime({
|
|
|
1701
1712
|
model,
|
|
1702
1713
|
systemPrompt
|
|
1703
1714
|
}) {
|
|
1715
|
+
const startupEvents = [];
|
|
1716
|
+
const initialIndex = await initializeProjectIndex(process.cwd()).catch(() => null);
|
|
1717
|
+
if (initialIndex?.summary) {
|
|
1718
|
+
startupEvents.push({
|
|
1719
|
+
type: 'system_tool',
|
|
1720
|
+
name: 'project_index(.codemini-project/project-map.json,.codemini-project/file-index.json)',
|
|
1721
|
+
status: 'done',
|
|
1722
|
+
summary: initialIndex.summary
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1704
1725
|
let currentSession = session;
|
|
1705
1726
|
let config = initialConfig;
|
|
1706
1727
|
const baseSystemPrompt = systemPrompt;
|
|
@@ -1779,8 +1800,8 @@ export async function createChatRuntime({
|
|
|
1779
1800
|
{ name: 'compact', description: 'compress message context' },
|
|
1780
1801
|
{ name: 'tasks', description: 'task board management' },
|
|
1781
1802
|
{ name: 'checkpoint', description: 'create/list/load conversation checkpoints' },
|
|
1782
|
-
{ name: 'spec', description: 'create a spec markdown file in .
|
|
1783
|
-
{ name: 'plan', description: 'create an implementation plan markdown file in .
|
|
1803
|
+
{ name: 'spec', description: 'create a spec markdown file in .codemini/specs' },
|
|
1804
|
+
{ name: 'plan', description: 'create an implementation plan markdown file in .codemini/plans' },
|
|
1784
1805
|
{ name: 'agents', description: 'run/list sub-agent roles' },
|
|
1785
1806
|
{ name: 'config', description: 'set/get/list/reset config values' },
|
|
1786
1807
|
{ name: 'history', description: 'list/resume sessions' },
|
|
@@ -2320,7 +2341,7 @@ export async function createChatRuntime({
|
|
|
2320
2341
|
content = buildSpecTemplate(topic);
|
|
2321
2342
|
buildNote = `\nGenerated with fallback template because model spec generation failed: ${String(err?.message || err)}`;
|
|
2322
2343
|
}
|
|
2323
|
-
const filePath = await
|
|
2344
|
+
const filePath = await writeMarkdownInProjectDir(
|
|
2324
2345
|
'specs',
|
|
2325
2346
|
topic,
|
|
2326
2347
|
content,
|
|
@@ -2374,7 +2395,7 @@ export async function createChatRuntime({
|
|
|
2374
2395
|
planContent = buildPlanTemplate(specTitle);
|
|
2375
2396
|
buildNote = `\nGenerated with fallback template because model plan generation failed: ${String(err?.message || err)}`;
|
|
2376
2397
|
}
|
|
2377
|
-
const filePath = await
|
|
2398
|
+
const filePath = await writeMarkdownInProjectDir(
|
|
2378
2399
|
'plans',
|
|
2379
2400
|
`${specTitle}-from-spec`,
|
|
2380
2401
|
planContent,
|
|
@@ -2389,7 +2410,7 @@ export async function createChatRuntime({
|
|
|
2389
2410
|
const goal = parsedInput.args.join(' ').trim();
|
|
2390
2411
|
if (!goal) return { type: 'system', text: 'Usage: /plan <goal> | /plan auto <goal> | /plan from-spec <spec-path?>' };
|
|
2391
2412
|
const content = buildPlanTemplate(goal);
|
|
2392
|
-
const filePath = await
|
|
2413
|
+
const filePath = await writeMarkdownInProjectDir(
|
|
2393
2414
|
'plans',
|
|
2394
2415
|
goal,
|
|
2395
2416
|
content,
|
|
@@ -2700,6 +2721,7 @@ export async function createChatRuntime({
|
|
|
2700
2721
|
getCompletionOptions,
|
|
2701
2722
|
isImmediateLocalInput,
|
|
2702
2723
|
submit,
|
|
2724
|
+
consumeStartupEvents: () => startupEvents.splice(0, startupEvents.length),
|
|
2703
2725
|
getInputHistory: () => loadInputHistory(),
|
|
2704
2726
|
getCurrentSessionId: () => currentSession.id,
|
|
2705
2727
|
getRuntimeState: () =>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { getProjectCheckpointsDir } from './paths.js';
|
|
3
4
|
|
|
4
5
|
function checkpointsDir(cwd = process.cwd()) {
|
|
5
|
-
return
|
|
6
|
+
return getProjectCheckpointsDir(cwd);
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
function makeId(name = '') {
|
|
@@ -3,9 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import {
|
|
5
5
|
getCommandsDir,
|
|
6
|
-
getLegacyGlobalSkillsDir,
|
|
7
|
-
getLegacyProjectSkillsDir,
|
|
8
6
|
getProjectCommandsDir,
|
|
7
|
+
getProjectSkillsDir,
|
|
9
8
|
getSkillsDir
|
|
10
9
|
} from './paths.js';
|
|
11
10
|
import { readSkillRegistry } from './skill-registry.js';
|
|
@@ -188,8 +187,8 @@ export async function loadCommandsAndSkills(cwd = process.cwd()) {
|
|
|
188
187
|
loadBundledSkillsFromDir(BUNDLED_SKILLS_DIR, commands);
|
|
189
188
|
loadMarkdownCommandsFromDir(getCommandsDir(), 'global', commands);
|
|
190
189
|
loadMarkdownCommandsFromDir(getProjectCommandsDir(cwd), 'project', commands);
|
|
191
|
-
loadLegacySkillsFromDir(
|
|
192
|
-
loadLegacySkillsFromDir(
|
|
190
|
+
loadLegacySkillsFromDir(getSkillsDir(), 'global', commands);
|
|
191
|
+
loadLegacySkillsFromDir(getProjectSkillsDir(cwd), 'project', commands);
|
|
193
192
|
const registry = await readSkillRegistry();
|
|
194
193
|
loadInstalledSkillsFromRegistry(getSkillsDir(), registry, commands);
|
|
195
194
|
|
package/src/core/config-store.js
CHANGED
|
@@ -193,8 +193,10 @@ export async function loadConfig() {
|
|
|
193
193
|
const parsed = JSON.parse(raw);
|
|
194
194
|
return normalizePolicyLists(deepMerge(DEFAULT_CONFIG, parsed));
|
|
195
195
|
} catch {
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
const defaultConfig = normalizePolicyLists(structuredClone(DEFAULT_CONFIG));
|
|
197
|
+
if (process.env.CODEMINI_GLOBAL_DIR) {
|
|
198
|
+
await saveConfig(defaultConfig);
|
|
199
|
+
return defaultConfig;
|
|
198
200
|
}
|
|
199
201
|
try {
|
|
200
202
|
const legacyPath = path.join(getLegacyConfigDir(), 'config.json');
|
|
@@ -202,7 +204,8 @@ export async function loadConfig() {
|
|
|
202
204
|
const parsed = JSON.parse(raw);
|
|
203
205
|
return normalizePolicyLists(deepMerge(DEFAULT_CONFIG, parsed));
|
|
204
206
|
} catch {
|
|
205
|
-
|
|
207
|
+
await saveConfig(defaultConfig);
|
|
208
|
+
return defaultConfig;
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
}
|