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 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 legacy skills: `.coder/skills/<name>/SKILL.md`
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
- - `CODEMINI_CONFIG_DIR`
82
- - `COMPANY_CODER_CONFIG_DIR`
83
- - Windows: `%APPDATA%\\codemini-cli\\`
84
- - macOS: `~/Library/Preferences/codemini-cli`
85
- - Linux/XDG: `$XDG_CONFIG_HOME/codemini-cli`
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: `.coder/`
108
- - Global user data on Windows: `%APPDATA%\\codemini-cli\\`
109
- - Restricted-environment fallback: `.codemini-cli/`
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
- - 项目级旧式 skill:`.coder/skills/<name>/SKILL.md`
207
+ - 项目级 skill:`.codemini/skills/<name>/SKILL.md`
193
208
 
194
209
  `base-config-dir` 的解析顺序是:
195
210
 
196
- - `CODEMINI_CONFIG_DIR`
197
- - `COMPANY_CODER_CONFIG_DIR`
198
- - Windows:`%APPDATA%\\codemini-cli\\`
199
- - macOS:`~/Library/Preferences/codemini-cli`
200
- - Linux / XDG:`$XDG_CONFIG_HOME/codemini-cli`
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
- - 项目工作区数据:`.coder/`
219
- - Windows 全局用户数据:`%APPDATA%\\codemini-cli\\`
220
- - 受限环境回退目录:`.codemini-cli/`
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.0",
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.18';
7
+ const VERSION = '0.2.1';
8
8
 
9
9
  function printHelp() {
10
10
  console.log(`codemini ${VERSION}
@@ -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
  );
@@ -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
+ }
@@ -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 writeMarkdownInCoderDir(subDir, title, body, fallbackName, sessionId) {
879
- const parts = [process.cwd(), '.coder', subDir];
880
- if (sessionId) parts.push(String(sessionId));
881
- const dir = path.join(...parts);
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 === '.coder') continue;
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
- path.join(process.cwd(), '.coder', 'specs', String(sessionId || '')),
1166
- path.join(process.cwd(), '.coder', 'specs')
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 writeMarkdownInCoderDir(
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 .coder/specs' },
1783
- { name: 'plan', description: 'create an implementation plan markdown file in .coder/plans' },
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 writeMarkdownInCoderDir(
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 writeMarkdownInCoderDir(
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 writeMarkdownInCoderDir(
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 path.join(cwd, '.coder', 'checkpoints');
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(getLegacyGlobalSkillsDir(), 'global', commands);
192
- loadLegacySkillsFromDir(getLegacyProjectSkillsDir(cwd), 'project', commands);
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
 
@@ -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
- if (process.env.CODEMINI_CONFIG_DIR || process.env.COMPANY_CODER_CONFIG_DIR) {
197
- return normalizePolicyLists(structuredClone(DEFAULT_CONFIG));
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
- return normalizePolicyLists(structuredClone(DEFAULT_CONFIG));
207
+ await saveConfig(defaultConfig);
208
+ return defaultConfig;
206
209
  }
207
210
  }
208
211
  }