codemini-cli 0.4.0 → 0.4.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
@@ -144,7 +144,7 @@ Typical flow:
144
144
  - Unified shell execution model:
145
145
  - one-off commands via `run`
146
146
  - long-running commands via `run` with `run_in_background=true`
147
- - Lightweight project index under `.codemini-project/`
147
+ - Lightweight project index under `.codemini/`
148
148
  - Tree-sitter based structured editing for function, class, and method-level changes
149
149
  - Reply language control via `ui.reply_language`
150
150
  - Safe mode enabled by default
@@ -179,7 +179,7 @@ Execution mode behavior:
179
179
 
180
180
  ### Project Index
181
181
 
182
- CodeMini CLI maintains a lightweight project index inside `.codemini-project/`:
182
+ CodeMini CLI maintains a lightweight project index inside `.codemini/`:
183
183
 
184
184
  - `project-map.json` — high-level repository facts such as languages, source roots, test roots, and entry candidates
185
185
  - `file-index.json` — per-file structure such as imports, exports, functions, classes, and lightweight symbol hints
@@ -191,7 +191,7 @@ The index is initialized when entering a project and refreshed incrementally aft
191
191
 
192
192
  - Global session state: `<base-config-dir>/sessions/`
193
193
  - Project workspace state: `.codemini/`
194
- - Lightweight project index: `.codemini-project/`
194
+ - Lightweight project index: `.codemini/`
195
195
  - Bundled repo skills: `skills/<name>/SKILL.md`
196
196
  - Project-scoped skills: `.codemini/skills/<name>/SKILL.md`
197
197
  - Global installed skills: `<base-config-dir>/skills/<name>/SKILL.md`
@@ -377,7 +377,7 @@ CodeMini CLI 把工具分成两层:
377
377
  - 统一的 shell 执行模型:
378
378
  - 一次性命令直接 `run`
379
379
  - 长运行命令通过 `run` + `run_in_background=true`
380
- - 在 `.codemini-project/` 下维护轻量项目索引,帮助模型更快理解仓库
380
+ - 在 `.codemini/` 下维护轻量项目索引,帮助模型更快理解仓库
381
381
  - 基于 Tree-sitter 的结构化编辑能力,适合函数级、类级、方法级改动
382
382
  - 支持通过 `ui.reply_language` 控制回复语言
383
383
  - safe mode 默认开启
@@ -412,7 +412,7 @@ Inbox 和持久记忆的区别:
412
412
 
413
413
  ### 项目索引
414
414
 
415
- CodeMini CLI 会在 `.codemini-project/` 下维护一份轻量项目索引:
415
+ CodeMini CLI 会在 `.codemini/` 下维护一份轻量项目索引:
416
416
 
417
417
  - `project-map.json` — 记录仓库的高层结构事实,比如语言、源码目录、测试目录、入口候选
418
418
  - `file-index.json` — 记录文件级结构信息,比如 imports、exports、functions、classes 和轻量 symbol 提示
@@ -424,7 +424,7 @@ CodeMini CLI 会在 `.codemini-project/` 下维护一份轻量项目索引:
424
424
 
425
425
  - 全局会话状态:`<base-config-dir>/sessions/`
426
426
  - 项目工作区状态:`.codemini/`
427
- - 轻量项目索引:`.codemini-project/`
427
+ - 轻量项目索引:`.codemini/`
428
428
  - 仓库内置 skill:`skills/<name>/SKILL.md`
429
429
  - 项目级 skill:`.codemini/skills/<name>/SKILL.md`
430
430
  - 全局已安装 skill:`<base-config-dir>/skills/<name>/SKILL.md`
package/deployment.md CHANGED
@@ -13,13 +13,13 @@ npm pack
13
13
  Expected output:
14
14
 
15
15
  ```text
16
- codemini-cli-0.4.0.tgz
16
+ codemini-cli-0.4.1.tgz
17
17
  ```
18
18
 
19
19
  If you want to verify the package contents:
20
20
 
21
21
  ```bash
22
- tar -tf codemini-cli-0.4.0.tgz
22
+ tar -tf codemini-cli-0.4.1.tgz
23
23
  ```
24
24
 
25
25
  ## 2. Copy To The Target Machine
@@ -34,7 +34,7 @@ Copy the generated `.tgz` file to the Win10 machine by one of these methods:
34
34
  Recommended target path:
35
35
 
36
36
  ```powershell
37
- C:\temp\codemini-cli-0.4.0.tgz
37
+ C:\temp\codemini-cli-0.4.1.tgz
38
38
  ```
39
39
 
40
40
  ## 3. Environment Requirements
@@ -58,7 +58,7 @@ npm -v
58
58
  Global install:
59
59
 
60
60
  ```powershell
61
- npm install -g C:\temp\codemini-cli-0.4.0.tgz
61
+ npm install -g C:\temp\codemini-cli-0.4.1.tgz
62
62
  ```
63
63
 
64
64
  If global install is blocked by company policy, install in a working directory instead:
@@ -66,7 +66,7 @@ If global install is blocked by company policy, install in a working directory i
66
66
  ```powershell
67
67
  mkdir C:\temp\coder-test
68
68
  cd C:\temp\coder-test
69
- npm install C:\temp\codemini-cli-0.4.0.tgz
69
+ npm install C:\temp\codemini-cli-0.4.1.tgz
70
70
  ```
71
71
 
72
72
  ## 5. Confirm Installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
@@ -6,6 +6,7 @@ import { trimInline as _trimInline, normalizePath } from './string-utils.js';
6
6
  import { captureToInbox, listInbox } from './memory-store.js';
7
7
  import { requiresApprovalEvaluation } from './command-risk.js';
8
8
  import { getToolOutputSanitizeOptions, sanitizeTextForModel } from './tool-output.js';
9
+ import { normalizeToolArguments } from './tool-args.js';
9
10
 
10
11
  /**
11
12
  * 安全解析 JSON 字符串。
@@ -25,20 +26,6 @@ function safeJsonParse(raw) {
25
26
  }
26
27
  }
27
28
 
28
- function parseInlineRangePath(value) {
29
- const text = String(value || '').trim();
30
- if (!text) return null;
31
- const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
32
- if (!match) return null;
33
- const [, maybePath, startRaw, endRaw] = match;
34
- if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
35
- const start = Number(startRaw);
36
- const end = Number(endRaw || startRaw);
37
- if (!Number.isFinite(start) || start <= 0) return null;
38
- if (!Number.isFinite(end) || end < start) return null;
39
- return { path: maybePath, start_line: start, end_line: end };
40
- }
41
-
42
29
  function buildDeleteApprovalDetails(source, rawPath) {
43
30
  const existing =
44
31
  source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
@@ -74,90 +61,6 @@ function buildDeleteCancellationResult(args) {
74
61
  };
75
62
  }
76
63
 
77
- function normalizeToolArguments(toolName, args, rawArguments) {
78
- const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
79
- const primitive =
80
- args == null || Array.isArray(args) || typeof args !== 'object'
81
- ? args
82
- : null;
83
- const source =
84
- args && typeof args === 'object' && !Array.isArray(args)
85
- ? { ...args }
86
- : {};
87
-
88
- if (primitive != null && typeof primitive !== 'object') {
89
- source._raw = rawText || String(primitive);
90
- } else if (!source._raw && rawText && source._invalid_json) {
91
- source._raw = rawText;
92
- }
93
-
94
- const stringValue =
95
- typeof primitive === 'string'
96
- ? primitive.trim()
97
- : String(source._raw || '').trim();
98
-
99
- if (toolName === 'read') {
100
- const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
101
- if (value) source.path = value;
102
- if (source.offset != null && source.start_line == null) source.start_line = source.offset;
103
- if (source.limit != null && source.end_line == null && Number(source.start_line) > 0) {
104
- source.end_line = Number(source.start_line) + Number(source.limit) - 1;
105
- }
106
- const range = parseInlineRangePath(source.path);
107
- if (range) {
108
- source.path = range.path;
109
- if (source.start_line == null) source.start_line = range.start_line;
110
- if (source.end_line == null) source.end_line = range.end_line;
111
- }
112
- return source;
113
- }
114
-
115
- if (toolName === 'list') {
116
- const value = String(source.path || source.dir || source.directory || stringValue || '.').trim();
117
- return { ...source, path: value || '.' };
118
- }
119
-
120
- if (toolName === 'glob') {
121
- const pattern = String(source.pattern || source.glob || source.query || stringValue || '').trim();
122
- if (pattern) source.pattern = pattern;
123
- if (!source.path && source.directory) source.path = source.directory;
124
- return source;
125
- }
126
-
127
- if (toolName === 'grep') {
128
- const pattern = String(source.pattern || source.query || source.symbol || source.q || stringValue || '').trim();
129
- if (pattern) source.pattern = pattern;
130
- if (!source.path && (source.directory || source.dir || source.cwd)) {
131
- source.path = source.directory || source.dir || source.cwd;
132
- }
133
- return source;
134
- }
135
-
136
- if (toolName === 'write') {
137
- const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
138
- if (value) source.path = value;
139
- if (source.content == null && source.text != null) source.content = source.text;
140
- if (source.content == null && source.new_content != null) source.content = source.new_content;
141
- return source;
142
- }
143
-
144
- if (toolName === 'edit') {
145
- const value = String(source.path || source.file || source.file_path || '').trim();
146
- if (value && !source.path) source.path = value;
147
- return source;
148
- }
149
-
150
- if (toolName === 'delete') {
151
- const value = String(source.path || source.file_path || source.file || source.target || source.directory || source.dir || stringValue || '').trim();
152
- if (value) source.path = value;
153
- const approval = buildDeleteApprovalDetails(source, source.path);
154
- if (approval) source.approval = approval;
155
- return source;
156
- }
157
-
158
- return source;
159
- }
160
-
161
64
  function emptyToolResultMarker(toolName) {
162
65
  const name = String(toolName || 'tool').trim() || 'tool';
163
66
  return `(${name} completed with no output)`;
@@ -408,18 +311,19 @@ function shouldAutoCaptureError(toolName, message) {
408
311
  return true;
409
312
  }
410
313
 
411
- function fireAndForgetCapture(toolName, message, args) {
314
+ async function captureToolFailure(toolName, message, args, config = {}) {
315
+ if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return;
412
316
  const summary = `[${toolName}] ${String(message).slice(0, 120)}`;
413
317
  const details = args
414
318
  ? `Tool: ${toolName}\nError: ${message}\nArgs: ${JSON.stringify(args).slice(0, 300)}`
415
319
  : `Tool: ${toolName}\nError: ${message}`;
416
- captureToInbox({
417
- scope: 'auto',
320
+ await captureToInbox({
321
+ scope: 'repo',
418
322
  type: 'failure',
419
323
  summary,
420
324
  details,
421
325
  source: 'auto-capture'
422
- }).catch(() => {});
326
+ });
423
327
  }
424
328
 
425
329
  async function checkAutoDreamThreshold(config) {
@@ -750,6 +654,14 @@ function formatToolDisplayName(name, args) {
750
654
  const command = trimInline(args?.command || '', 96);
751
655
  return command ? `run(${command})` : name;
752
656
  }
657
+ if (name === 'web_fetch') {
658
+ const url = trimInline(args?.url || args?.href || '', 96);
659
+ return url ? `web_fetch(${url})` : name;
660
+ }
661
+ if (name === 'web_search') {
662
+ const query = trimInline(args?.query || args?.q || '', 96);
663
+ return query ? `web_search(${query})` : name;
664
+ }
753
665
  if (name === 'edit') {
754
666
  const target = trimInline(args?.path || args?.file || '.', 96) || '.';
755
667
  return `edit(${target})`;
@@ -1099,7 +1011,7 @@ export async function runAgentLoop({
1099
1011
  onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: trimInline(message, 120) });
1100
1012
  }
1101
1013
  if (shouldAutoCaptureError(toolName, message)) {
1102
- fireAndForgetCapture(toolName, message, effectiveArgs);
1014
+ await captureToolFailure(toolName, message, effectiveArgs, config).catch(() => {});
1103
1015
  }
1104
1016
  return {
1105
1017
  callId: call.id,
@@ -1122,13 +1034,13 @@ export async function runAgentLoop({
1122
1034
  if (typeof exitCode === 'number' && exitCode !== 0 && stderr) {
1123
1035
  const failMsg = `exit ${exitCode}: ${stderr.slice(0, 120)}`;
1124
1036
  if (shouldAutoCaptureError(toolName, failMsg)) {
1125
- fireAndForgetCapture(toolName, failMsg, effectiveArgs);
1037
+ await captureToolFailure(toolName, failMsg, effectiveArgs, config).catch(() => {});
1126
1038
  }
1127
1039
  }
1128
1040
  if (toolResult.error) {
1129
1041
  const errMsg = String(toolResult.error).slice(0, 120);
1130
1042
  if (shouldAutoCaptureError(toolName, errMsg)) {
1131
- fireAndForgetCapture(toolName, errMsg, effectiveArgs);
1043
+ await captureToolFailure(toolName, errMsg, effectiveArgs, config).catch(() => {});
1132
1044
  }
1133
1045
  }
1134
1046
  }
@@ -25,7 +25,7 @@ import { buildSystemPromptWithSoul } from './soul.js';
25
25
  import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSessionsDir } from './paths.js';
26
26
  import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
27
27
  import { buildMemorySnapshot } from './memory-prompt.js';
28
- import { forgetMemory, listMemories, searchMemories, captureToInbox, listInbox } from './memory-store.js';
28
+ import { forgetMemory, listMemories, rememberMemory, searchMemories, captureToInbox, listInbox } from './memory-store.js';
29
29
  import { runDreamConsolidation } from './dream-consolidate.js';
30
30
  import { normalizePlanState } from './plan-state.js';
31
31
  import { countActiveTodos, normalizeTodos } from './todo-state.js';
@@ -3312,6 +3312,75 @@ export async function createChatRuntime({
3312
3312
  await saveSession(currentSession);
3313
3313
  };
3314
3314
 
3315
+ const captureCompactSummary = async ({ summary, mode, beforeTokens, afterTokens }) => {
3316
+ if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return null;
3317
+ const normalizedSummary = String(summary || '').trim();
3318
+ if (!normalizedSummary) return null;
3319
+ const entrySummary = `Context compacted (${mode}): ${beforeTokens} -> ${afterTokens} tokens`;
3320
+ return captureToInbox({
3321
+ scope: 'repo',
3322
+ type: 'observation',
3323
+ summary: entrySummary,
3324
+ details: normalizedSummary,
3325
+ tags: ['compact', 'context-summary'],
3326
+ source: 'auto-compact'
3327
+ }).catch(() => null);
3328
+ };
3329
+
3330
+ const shouldAutoCaptureUserPrompt = (text) => {
3331
+ if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return false;
3332
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
3333
+ if (value.length < 12) return false;
3334
+ const actionPattern =
3335
+ /\b(add|build|fix|implement|change|update|refactor|test|debug|remember|capture|continue|review)\b|实现|增加|添加|修复|修改|更新|重构|测试|调试|记住|继续|检查|沉淀|捕获/i;
3336
+ return actionPattern.test(value);
3337
+ };
3338
+
3339
+ const classifyDirectMemoryPrompt = (text) => {
3340
+ if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return null;
3341
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
3342
+ if (value.length < 6) return null;
3343
+ const userPreferencePattern =
3344
+ /(?:记住|请记住|以后|后续|我偏好|我的偏好|我喜欢|我习惯|不要再|别再|always remember|remember that|i prefer|my preference|don't|do not)/i;
3345
+ if (!userPreferencePattern.test(value)) return null;
3346
+ const projectPattern = /(?:本项目|这个项目|当前项目|这个仓库|当前仓库|repo|repository|project)/i;
3347
+ const isProject = projectPattern.test(value);
3348
+ return {
3349
+ scope: isProject ? 'project' : 'user',
3350
+ kind: isProject ? 'workflow' : 'preference',
3351
+ content: value
3352
+ };
3353
+ };
3354
+
3355
+ const saveDirectMemoryPrompt = async (text) => {
3356
+ const direct = classifyDirectMemoryPrompt(text);
3357
+ if (!direct) return null;
3358
+ return rememberMemory({
3359
+ scope: direct.scope,
3360
+ content: direct.content,
3361
+ kind: direct.kind,
3362
+ summary: direct.content.slice(0, 80),
3363
+ source: 'auto-user-directive',
3364
+ replaceSimilar: true,
3365
+ workspaceRoot: process.cwd(),
3366
+ config
3367
+ }).catch(() => null);
3368
+ };
3369
+
3370
+ const captureUserPromptForDream = async (text) => {
3371
+ if (classifyDirectMemoryPrompt(text)) return null;
3372
+ if (!shouldAutoCaptureUserPrompt(text)) return null;
3373
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
3374
+ return captureToInbox({
3375
+ scope: 'repo',
3376
+ type: 'observation',
3377
+ summary: `User task: ${value.slice(0, 120)}`,
3378
+ details: value,
3379
+ tags: ['user-prompt'],
3380
+ source: 'auto-user-prompt'
3381
+ }).catch(() => null);
3382
+ };
3383
+
3315
3384
  const buildActiveSystemPrompt = async () => {
3316
3385
  const soulPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
3317
3386
  const memorySnapshot = await buildMemorySnapshot({
@@ -4005,6 +4074,12 @@ export async function createChatRuntime({
4005
4074
  compactState.backupMessages = structuredClone(currentSession.messages);
4006
4075
  currentSession.messages = result.compacted.map((m) => ({ ...m, at: new Date().toISOString() }));
4007
4076
  await saveSession(currentSession);
4077
+ await captureCompactSummary({
4078
+ summary: result.summary,
4079
+ mode: compactState.mode,
4080
+ beforeTokens,
4081
+ afterTokens
4082
+ });
4008
4083
  await persistLocalExchange(line, report, { includeUser: false });
4009
4084
  return { type: 'system', text: report };
4010
4085
  }
@@ -4125,6 +4200,12 @@ export async function createChatRuntime({
4125
4200
  at: new Date().toISOString()
4126
4201
  }));
4127
4202
  await saveSession(currentSession);
4203
+ await captureCompactSummary({
4204
+ summary: autoResult.summary,
4205
+ mode: compactState.mode,
4206
+ beforeTokens: currentTokens,
4207
+ afterTokens: estimateMessagesTokens(currentSession.messages)
4208
+ });
4128
4209
  if (onAgentEvent) {
4129
4210
  onAgentEvent({
4130
4211
  type: 'compact:auto',
@@ -4137,6 +4218,7 @@ export async function createChatRuntime({
4137
4218
  }
4138
4219
 
4139
4220
  const expandedText = await expandFileMentions(parsedInput.text, process.cwd());
4221
+ await saveDirectMemoryPrompt(expandedText);
4140
4222
  const autoRoute = classifyAutoRoute(expandedText);
4141
4223
  if (autoRoute.autoPlan) {
4142
4224
  await maybeAutoDreamFromRuntime();
@@ -4188,6 +4270,7 @@ export async function createChatRuntime({
4188
4270
  executionMode,
4189
4271
  signal
4190
4272
  });
4273
+ await captureUserPromptForDream(expandedText);
4191
4274
  return { type: 'assistant', text: result.text, aborted: !!result.aborted };
4192
4275
  };
4193
4276
 
@@ -65,6 +65,7 @@ const DEFAULT_CONFIG = {
65
65
  memory: {
66
66
  enabled: true,
67
67
  auto_write: true,
68
+ auto_capture: true,
68
69
  inject_on_session_start: true,
69
70
  auto_dream_threshold: 10,
70
71
  max_items_per_scope: 12,
@@ -165,6 +166,7 @@ function normalizePolicyLists(config) {
165
166
  next.memory = next.memory || {};
166
167
  next.memory.enabled = next.memory.enabled !== false;
167
168
  next.memory.auto_write = next.memory.auto_write !== false;
169
+ next.memory.auto_capture = next.memory.auto_capture !== false;
168
170
  next.memory.inject_on_session_start = next.memory.inject_on_session_start !== false;
169
171
  next.memory.max_items_per_scope = Math.max(1, Number(next.memory.max_items_per_scope || 12));
170
172
  next.memory.auto_dream_threshold = Number(next.memory.auto_dream_threshold ?? 10);
@@ -37,20 +37,30 @@ function modeToKeepRecent(mode) {
37
37
  }
38
38
 
39
39
  function buildLocalSummary(messages) {
40
- const lines = [];
40
+ const goal = [];
41
+ const constraints = [];
42
+ const changedFiles = new Set();
43
+ const verification = [];
44
+ const openThreads = [];
41
45
  const limit = 16;
42
46
  for (const msg of messages.slice(-limit)) {
43
47
  if (msg.role === 'tool') {
44
- // Try to parse tool result as JSON for semantic summary
45
48
  const text = textFromContent(msg.content);
46
49
  let parsed;
47
50
  try { parsed = JSON.parse(text); } catch { parsed = null; }
48
51
  if (parsed && typeof parsed === 'object') {
49
52
  const summary = summarizeToolResult(parsed);
50
- lines.push(`- tool_result: ${summary}`);
53
+ if (parsed.path) changedFiles.add(String(parsed.path));
54
+ if (parsed.command || parsed.code != null || parsed.stderr || parsed.stdout) {
55
+ verification.push(summary);
56
+ } else {
57
+ openThreads.push(`tool_result: ${summary}`);
58
+ }
51
59
  } else {
52
60
  const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
53
- lines.push(`- tool_result: ${clipped}`);
61
+ const match = clipped.match(/([A-Za-z0-9_./-]+\.[A-Za-z0-9]+):\d+/);
62
+ if (match) changedFiles.add(match[1]);
63
+ openThreads.push(`tool_result: ${clipped}`);
54
64
  }
55
65
  continue;
56
66
  }
@@ -59,21 +69,35 @@ function buildLocalSummary(messages) {
59
69
  const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
60
70
  const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
61
71
  const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
62
- lines.push(`- assistant: ${clipped}${toolInfo}`);
72
+ if (clipped) openThreads.push(`assistant: ${clipped}${toolInfo}`);
63
73
  continue;
64
74
  }
65
75
  if (msg.role === 'user') {
66
76
  const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
67
77
  const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
68
- lines.push(`- user: ${clipped}`);
78
+ if (goal.length === 0) goal.push(clipped);
79
+ else constraints.push(clipped);
69
80
  continue;
70
81
  }
71
82
  const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
72
83
  if (!text) continue;
73
84
  const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
74
- lines.push(`- ${msg.role}: ${clipped}`);
85
+ openThreads.push(`${msg.role}: ${clipped}`);
75
86
  }
76
- return `Context Summary\n${lines.join('\n')}`.trim();
87
+ const lines = [
88
+ 'Context Summary',
89
+ 'Goal:',
90
+ goal.length > 0 ? `- ${goal[0]}` : '- Unknown from compacted context',
91
+ 'Key Constraints:',
92
+ ...(constraints.length > 0 ? constraints.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
93
+ 'Changed Files:',
94
+ ...(changedFiles.size > 0 ? [...changedFiles].slice(0, 8).map((item) => `- ${item}`) : ['- None recorded']),
95
+ 'Verification:',
96
+ ...(verification.length > 0 ? verification.slice(-4).map((item) => `- ${item}`) : ['- None recorded']),
97
+ 'Open Threads:',
98
+ ...(openThreads.length > 0 ? openThreads.slice(-8).map((item) => `- ${item}`) : ['- None recorded'])
99
+ ];
100
+ return lines.join('\n').trim();
77
101
  }
78
102
 
79
103
  export function compactMessagesLocally(messages, { mode = 'default' } = {}) {
@@ -9,14 +9,14 @@ function getToolFewShotBlock() {
9
9
  Use these as style examples for tool calls:
10
10
 
11
11
  Current working directory: ${cwd}
12
- When a tool takes file_path, build it from the current working directory and prefer absolute paths.
12
+ When a tool takes path, build it from the current working directory and prefer absolute paths.
13
13
  If the user mentions a project-relative path like src/app.ts, resolve it from ${cwd} instead of guessing parent directories.
14
14
 
15
15
  1. File discovery then read
16
16
  User: compare the auth flow
17
17
  Assistant: first narrow the search with the project index
18
18
  Tool: query_project_index({"query":"auth flow","path":"src","max_results":3})
19
- Tool: read({"file_path":"${cwd}/src/auth/service.ts"})
19
+ Tool: read({"path":"${cwd}/src/auth/service.ts"})
20
20
 
21
21
  If the visible tool list does not include a needed capability, load it with tool_search instead of assuming it does not exist.
22
22
  Example:
@@ -27,7 +27,7 @@ Tool: glob({"pattern":"src/**/*.ts"})
27
27
  User: rename loginUser to signInUser
28
28
  Assistant: first find the exact occurrences
29
29
  Tool: grep({"pattern":"loginUser","path":"src"})
30
- Tool: edit({"file_path":"${cwd}/src/auth/service.ts","old_string":"loginUser","new_string":"signInUser"})
30
+ Tool: edit({"path":"${cwd}/src/auth/service.ts","old_text":"loginUser","new_text":"signInUser"})
31
31
 
32
32
  3. Read a specific range
33
33
  User: inspect the reducer around line 120
@@ -43,7 +43,7 @@ Assistant: keep the checklist updated as each phase finishes, and do not give a
43
43
  5. Create a new file
44
44
  User: add a notes file
45
45
  Assistant: create the file directly
46
- Tool: write({"file":"${cwd}/notes.txt","text":"todo\\n"})
46
+ Tool: write({"path":"${cwd}/notes.txt","content":"todo\\n"})
47
47
 
48
48
  6. Save a high-signal observation to memory
49
49
  When you notice a reusable pattern, a user correction, a repeated failure, or a stable preference — save it to persistent memory. Choose scope carefully:
@@ -73,7 +73,7 @@ Tool: tool_search({"query":"web_search"})
73
73
  Tool: web_search({"query":"latest pnpm release","max_results":5})
74
74
 
75
75
  Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
76
- Prefer explicit absolute file_path values when the current working directory is known.`;
76
+ Prefer explicit absolute path values when the current working directory is known.`;
77
77
  }
78
78
 
79
79
  function getEnvBlock() {
@@ -111,7 +111,7 @@ class FffMcpClient {
111
111
  capabilities: {},
112
112
  clientInfo: {
113
113
  name: 'codemini-cli',
114
- version: '0.4.0'
114
+ version: '0.4.1'
115
115
  }
116
116
  });
117
117
  this.sendNotification('notifications/initialized', {});
@@ -54,6 +54,36 @@ async function parseJsonResponse(response) {
54
54
  return response.json();
55
55
  }
56
56
 
57
+ function isRetryableStatus(status) {
58
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
59
+ }
60
+
61
+ function isRetryableError(error) {
62
+ const name = String(error?.name || '');
63
+ if (name === 'AbortError' || name === 'TimeoutError') return false;
64
+ const message = String(error?.message || error || '');
65
+ return /fetch failed|network|socket|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(message);
66
+ }
67
+
68
+ async function fetchWithRetry(url, init, { maxRetries = 0 } = {}) {
69
+ const attempts = Math.max(0, Number(maxRetries) || 0) + 1;
70
+ let lastError;
71
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
72
+ try {
73
+ const response = await fetch(url, init);
74
+ if (response.ok || !isRetryableStatus(response.status) || attempt === attempts - 1) {
75
+ return response;
76
+ }
77
+ await response.arrayBuffer().catch(() => null);
78
+ } catch (error) {
79
+ lastError = error;
80
+ if (!isRetryableError(error) || attempt === attempts - 1) throw error;
81
+ }
82
+ await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
83
+ }
84
+ throw lastError || new Error('Gateway request failed');
85
+ }
86
+
57
87
  async function* iterateSseEvents(stream) {
58
88
  const decoder = new TextDecoder();
59
89
  let buffer = '';
@@ -318,12 +348,12 @@ export async function createChatCompletion({
318
348
  maxRetries = 2
319
349
  }) {
320
350
  const payload = buildPayload({ model, temperature, messages, tools });
321
- const response = await fetch(buildChatCompletionsUrl(baseUrl), {
351
+ const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
322
352
  method: 'POST',
323
353
  headers: createHeaders(apiKey),
324
354
  body: JSON.stringify(payload),
325
355
  signal: AbortSignal.timeout(timeoutMs)
326
- });
356
+ }, { maxRetries });
327
357
  const data = await parseJsonResponse(response);
328
358
  const message = data?.choices?.[0]?.message || {};
329
359
  const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
@@ -386,12 +416,12 @@ export async function createChatCompletionStream({
386
416
  }
387
417
  }
388
418
  const payload = buildPayload({ model, temperature, messages, tools, stream: true });
389
- const response = await fetch(buildChatCompletionsUrl(baseUrl), {
419
+ const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
390
420
  method: 'POST',
391
421
  headers: createHeaders(apiKey),
392
422
  body: JSON.stringify(payload),
393
423
  signal: controller.signal
394
- });
424
+ }, { maxRetries });
395
425
  if (!response.ok || !response.body) {
396
426
  const text = await response.text().catch(() => '');
397
427
  throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
@@ -402,7 +432,8 @@ export async function createChatCompletionStream({
402
432
  let usage = null;
403
433
  let miniMaxStreamState = { rawContent: '', visibleText: '' };
404
434
 
405
- for await (const chunk of iterateSseEvents(response.body)) {
435
+ try {
436
+ for await (const chunk of iterateSseEvents(response.body)) {
406
437
  usage = chunk?.usage || usage;
407
438
  const choice0 = chunk?.choices?.[0] || {};
408
439
  const delta = choice0?.delta || {};
@@ -452,6 +483,10 @@ export async function createChatCompletionStream({
452
483
  if (choice0?.finish_reason) {
453
484
  break;
454
485
  }
486
+ }
487
+ } finally {
488
+ timeoutSignal.removeEventListener('abort', onAbort);
489
+ if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
455
490
  }
456
491
 
457
492
  const result = buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
@@ -146,11 +146,11 @@ export function getShellSystemPrompt(value) {
146
146
  ALWAYS prefer dedicated tools over raw shell commands:
147
147
  - The visible default tool list is intentionally small. If a needed capability is not currently listed, do not assume it is unavailable — call tool_search to load additional tools first
148
148
  - Use query_project_index first for broad repository understanding. It combines project-map metadata with indexed file symbols so you can narrow candidates before reading source files
149
- - Use read to inspect files — NEVER use cat, head, or tail via run. read returns content directly by default; demo-style shapes like {file_path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {file_path:"src/app.ts", offset:10, limit:30} are accepted
149
+ - Use read to inspect files — NEVER use cat, head, or tail via run. Use canonical shapes like {path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {path:"src/app.ts", start_line:10, end_line:40}
150
150
  - Use grep to search file contents — NEVER use grep or rg via run
151
151
  - Use list for directory-by-directory filesystem discovery. If you specifically need pattern-based file lookup like src/**/*.ts, load glob with tool_search instead of falling back to run
152
- - Use edit to modify existing files — this is the DEFAULT path for code changes. Demo-style aliases like {file_path:"src/app.ts", old_string:"foo", new_string:"bar"} are accepted
153
- - Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Aliases like {file:"notes.txt", text:"..."} are accepted
152
+ - Use edit to modify existing files — this is the DEFAULT path for code changes. Prefer {path:"src/app.ts", old_text:"foo", new_text:"bar"}
153
+ - Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Prefer {path:"notes.txt", content:"..."}
154
154
  - Use update_todos to manage the session todo checklist for complex work. Provide the full current list each time and usually keep exactly one item in_progress
155
155
  - Use read_plan and update_plan to recover or sync structured plan state when plan progress was interrupted (for example by transient gateway/model errors)
156
156
  - Use run for shell commands. For long-running processes (dev servers, watchers), set run_in_background=true when you know you do not need the final result immediately. Long-running commands may also be backgrounded automatically
@@ -181,15 +181,15 @@ For background commands: use run to launch. If you need management tools that ar
181
181
  Common tool call patterns:
182
182
  - Query the project index first: {query:"login auth flow", path:"src", max_results:5}
183
183
  - Load a deferred tool when needed: {query:"glob"} or {query:"all"}
184
- - Read a file: {path:"src/app.ts"} or {file_path:"src/app.ts", offset:20, limit:40}
184
+ - Read a file: {path:"src/app.ts"} or {path:"src/app.ts", start_line:20, end_line:60}
185
185
  - Read a specific range inline: {path:"src/app.ts:20-60"}
186
186
  - Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
187
187
  - List a directory first: {path:"src"}
188
188
  - After loading glob, find files by pattern: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
189
- - Edit exact text: {file_path:"src/app.ts", old_string:"foo", new_string:"bar"}
189
+ - Edit exact text: {path:"src/app.ts", old_text:"foo", new_text:"bar"}
190
190
  - Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
191
- - Write a new file: {file:"notes.txt", text:"..."} or {path:"src/page.tsx", content:"..."}
192
- - When the environment provides a Working directory, prefer absolute file_path values rooted there instead of guessing prefixes
191
+ - Write a new file: {path:"notes.txt", content:"..."} or {path:"src/page.tsx", content:"..."}
192
+ - When the environment provides a Working directory, prefer absolute path values rooted there instead of guessing prefixes
193
193
  - If the user gives a relative path like src/app.ts, resolve it from the current Working directory rather than inventing ../ or sibling folders
194
194
 
195
195
  # Doing tasks
@@ -218,7 +218,7 @@ Common tool call patterns:
218
218
  - Keep answers compact and easy to scan
219
219
  - Lead with the answer or next action, not scene-setting
220
220
  - Do not restate the user's request unless a brief restatement prevents ambiguity
221
- - When referencing code, use file_path:line_number format
221
+ - When referencing code, use path:line_number format
222
222
  - Keep technical wording, commands, paths, and error details exact
223
223
  - Only use emojis if the user explicitly requests it`;
224
224
  }
@@ -0,0 +1,181 @@
1
+ import path from 'node:path';
2
+
3
+ export function parseInlineRangePath(value) {
4
+ const text = String(value || '').trim();
5
+ if (!text) return null;
6
+ const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
7
+ if (!match) return null;
8
+ const [, maybePath, startRaw, endRaw] = match;
9
+ if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
10
+ const startLine = Number(startRaw);
11
+ const endLine = Number(endRaw || startRaw);
12
+ if (!Number.isFinite(startLine) || startLine <= 0) return null;
13
+ if (!Number.isFinite(endLine) || endLine < startLine) return null;
14
+ return {
15
+ path: maybePath,
16
+ start_line: startLine,
17
+ end_line: endLine
18
+ };
19
+ }
20
+
21
+ export function normalizeReadArgs(rawArgs) {
22
+ const source =
23
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
24
+ ? { ...rawArgs }
25
+ : { path: typeof rawArgs === 'string' ? rawArgs : '' };
26
+
27
+ const normalized = { ...source };
28
+ const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
29
+ if (aliasPath) normalized.path = aliasPath;
30
+
31
+ if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
32
+ normalized.start_line = Number(source.offset);
33
+ }
34
+
35
+ if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
36
+ const startLine = Number(normalized.start_line);
37
+ const limit = Number(source.limit);
38
+ if (startLine > 0 && limit > 0) {
39
+ normalized.end_line = startLine + limit - 1;
40
+ }
41
+ }
42
+
43
+ const inlineRange = parseInlineRangePath(normalized.path);
44
+ if (inlineRange) {
45
+ normalized.path = inlineRange.path;
46
+ if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
47
+ if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
48
+ }
49
+
50
+ return normalized;
51
+ }
52
+
53
+ export function normalizePathArgs(rawArgs, aliases = []) {
54
+ const source =
55
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
56
+ ? { ...rawArgs }
57
+ : { path: typeof rawArgs === 'string' ? rawArgs : '' };
58
+ const normalized = { ...source };
59
+ const keys = ['path', ...aliases];
60
+ for (const key of keys) {
61
+ const value = String(source?.[key] || '').trim();
62
+ if (value) {
63
+ normalized.path = value;
64
+ break;
65
+ }
66
+ }
67
+ return normalized;
68
+ }
69
+
70
+ export function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
71
+ const source =
72
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
73
+ ? { ...rawArgs }
74
+ : { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
75
+ const normalized = { ...source };
76
+ for (const key of ['pattern', ...aliases]) {
77
+ const value = String(source?.[key] || '').trim();
78
+ if (value) {
79
+ normalized.pattern = value;
80
+ break;
81
+ }
82
+ }
83
+ for (const key of ['path', ...defaultPathAliases]) {
84
+ const value = String(source?.[key] || '').trim();
85
+ if (value) {
86
+ normalized.path = value;
87
+ break;
88
+ }
89
+ }
90
+ return normalized;
91
+ }
92
+
93
+ export function normalizeWriteArgs(rawArgs) {
94
+ const source =
95
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
96
+ ? { ...rawArgs }
97
+ : { path: typeof rawArgs === 'string' ? rawArgs : '' };
98
+ const normalized = { ...source };
99
+ const filePath = String(source.path || source.file_path || source.file || '').trim();
100
+ if (filePath) normalized.path = filePath;
101
+ if (normalized.content == null) {
102
+ if (source.text != null) normalized.content = source.text;
103
+ if (source.new_content != null) normalized.content = source.new_content;
104
+ }
105
+ return normalized;
106
+ }
107
+
108
+ export function normalizeWebFetchArgs(rawArgs) {
109
+ const normalized = normalizePathArgs(rawArgs, ['url', 'href', 'link', 'target']);
110
+ const url = String(normalized.url || normalized.path || '').trim();
111
+ return { ...normalized, url };
112
+ }
113
+
114
+ export function normalizeWebSearchArgs(rawArgs) {
115
+ const normalized = normalizePatternArgs(rawArgs, ['query', 'q', 'keyword']);
116
+ const query = String(normalized.query || normalized.pattern || '').trim();
117
+ return { ...normalized, query };
118
+ }
119
+
120
+ function buildDeleteApprovalDetails(source, rawPath) {
121
+ const existing =
122
+ source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
123
+ ? source.approval
124
+ : {};
125
+ const approvalPath = String(existing.path || rawPath || '').trim();
126
+ const approvalName = String(existing.name || (approvalPath ? path.basename(approvalPath) : '') || '').trim();
127
+ const approvalType = String(existing.type || '').trim();
128
+
129
+ const approval = {};
130
+ if (approvalPath) approval.path = approvalPath;
131
+ if (approvalName) approval.name = approvalName;
132
+ if (approvalType) approval.type = approvalType;
133
+ return Object.keys(approval).length > 0 ? approval : undefined;
134
+ }
135
+
136
+ export function normalizeToolArguments(toolName, args, rawArguments) {
137
+ const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
138
+ const primitive =
139
+ args == null || Array.isArray(args) || typeof args !== 'object'
140
+ ? args
141
+ : null;
142
+ const source =
143
+ args && typeof args === 'object' && !Array.isArray(args)
144
+ ? { ...args }
145
+ : {};
146
+
147
+ if (primitive != null && typeof primitive !== 'object') {
148
+ source._raw = rawText || String(primitive);
149
+ } else if (!source._raw && rawText && source._invalid_json) {
150
+ source._raw = rawText;
151
+ }
152
+
153
+ const stringValue =
154
+ typeof primitive === 'string'
155
+ ? primitive.trim()
156
+ : String(source._raw || '').trim();
157
+
158
+ if (toolName === 'read') return normalizeReadArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) });
159
+ if (toolName === 'list') return normalizePathArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) }, ['dir', 'directory']);
160
+ if (toolName === 'glob') return normalizePatternArgs({ ...source, ...(stringValue && !source.pattern ? { pattern: stringValue } : {}) }, ['glob', 'query'], ['directory']);
161
+ if (toolName === 'grep') return normalizePatternArgs({ ...source, ...(stringValue && !source.pattern ? { pattern: stringValue } : {}) }, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
162
+ if (toolName === 'write') return normalizeWriteArgs({ ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) });
163
+
164
+ if (toolName === 'edit') {
165
+ const value = String(source.path || source.file || source.file_path || '').trim();
166
+ if (value && !source.path) source.path = value;
167
+ return source;
168
+ }
169
+
170
+ if (toolName === 'delete') {
171
+ const normalized = normalizePathArgs(
172
+ { ...source, ...(stringValue && !source.path ? { path: stringValue } : {}) },
173
+ ['file_path', 'file', 'target', 'directory', 'dir']
174
+ );
175
+ const approval = buildDeleteApprovalDetails(normalized, normalized.path);
176
+ if (approval) normalized.approval = approval;
177
+ return normalized;
178
+ }
179
+
180
+ return source;
181
+ }
package/src/core/tools.js CHANGED
@@ -29,6 +29,14 @@ import {
29
29
  sanitizeTextForModel,
30
30
  summarizeRunOutput
31
31
  } from './tool-output.js';
32
+ import {
33
+ normalizePathArgs,
34
+ normalizePatternArgs,
35
+ normalizeReadArgs,
36
+ normalizeWebFetchArgs,
37
+ normalizeWebSearchArgs,
38
+ normalizeWriteArgs
39
+ } from './tool-args.js';
32
40
  const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
33
41
  const BACKGROUND_TASK_POLL_MS = 150;
34
42
  const MAX_AST_ENCLOSING_BYTES = 300_000;
@@ -100,133 +108,6 @@ function splitLines(text) {
100
108
  return String(text || '').split('\n');
101
109
  }
102
110
 
103
- function parseInlineReadRange(value) {
104
- const text = String(value || '').trim();
105
- if (!text) return null;
106
- const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
107
- if (!match) return null;
108
- const [, maybePath, startRaw, endRaw] = match;
109
- if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
110
- const startLine = Number(startRaw);
111
- const endLine = Number(endRaw || startRaw);
112
- if (!Number.isFinite(startLine) || startLine <= 0) return null;
113
- if (!Number.isFinite(endLine) || endLine < startLine) return null;
114
- return {
115
- path: maybePath,
116
- start_line: startLine,
117
- end_line: endLine
118
- };
119
- }
120
-
121
- function normalizeReadArgs(rawArgs) {
122
- const source =
123
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
124
- ? { ...rawArgs }
125
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
126
-
127
- const normalized = { ...source };
128
- const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
129
- if (aliasPath) normalized.path = aliasPath;
130
-
131
- if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
132
- normalized.start_line = Number(source.offset);
133
- }
134
-
135
- if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
136
- const startLine = Number(normalized.start_line);
137
- const limit = Number(source.limit);
138
- if (startLine > 0 && limit > 0) {
139
- normalized.end_line = startLine + limit - 1;
140
- }
141
- }
142
-
143
- const inlineRange = parseInlineReadRange(normalized.path);
144
- if (inlineRange) {
145
- normalized.path = inlineRange.path;
146
- if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
147
- if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
148
- }
149
-
150
- return normalized;
151
- }
152
-
153
- function normalizePathArgs(rawArgs, aliases = []) {
154
- const source =
155
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
156
- ? { ...rawArgs }
157
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
158
- const normalized = { ...source };
159
- const keys = ['path', ...aliases];
160
- for (const key of keys) {
161
- const value = String(source?.[key] || '').trim();
162
- if (value) {
163
- normalized.path = value;
164
- break;
165
- }
166
- }
167
- return normalized;
168
- }
169
-
170
- function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
171
- const source =
172
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
173
- ? { ...rawArgs }
174
- : { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
175
- const normalized = { ...source };
176
- for (const key of ['pattern', ...aliases]) {
177
- const value = String(source?.[key] || '').trim();
178
- if (value) {
179
- normalized.pattern = value;
180
- break;
181
- }
182
- }
183
- for (const key of ['path', ...defaultPathAliases]) {
184
- const value = String(source?.[key] || '').trim();
185
- if (value) {
186
- normalized.path = value;
187
- break;
188
- }
189
- }
190
- return normalized;
191
- }
192
-
193
- function normalizeWriteArgs(rawArgs) {
194
- const source =
195
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
196
- ? { ...rawArgs }
197
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
198
- const normalized = { ...source };
199
- const filePath = String(source.path || source.file_path || source.file || '').trim();
200
- if (filePath) normalized.path = filePath;
201
- if (normalized.content == null) {
202
- if (source.text != null) normalized.content = source.text;
203
- if (source.new_content != null) normalized.content = source.new_content;
204
- }
205
- return normalized;
206
- }
207
-
208
- function normalizeWebFetchArgs(rawArgs) {
209
- const source =
210
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
211
- ? { ...rawArgs }
212
- : { url: typeof rawArgs === 'string' ? rawArgs : '' };
213
- const normalized = { ...source };
214
- const url = String(source.url || source.href || source.link || source.target || '').trim();
215
- if (url) normalized.url = url;
216
- return normalized;
217
- }
218
-
219
- function normalizeWebSearchArgs(rawArgs) {
220
- const source =
221
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
222
- ? { ...rawArgs }
223
- : { query: typeof rawArgs === 'string' ? rawArgs : '' };
224
- const normalized = { ...source };
225
- const query = String(source.query || source.q || source.keyword || '').trim();
226
- if (query) normalized.query = query;
227
- return normalized;
228
- }
229
-
230
111
  function clampNumber(value, min, max, fallback) {
231
112
  const num = Number(value);
232
113
  if (!Number.isFinite(num)) return fallback;
@@ -1819,20 +1700,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1819
1700
  function: {
1820
1701
  name: 'read',
1821
1702
  description:
1822
- 'Inspect code or text files. Use read(path) for normal file or line-window reads, read(ast_target=...) for a node-scoped AST read, and read(path, query=..., capture_name=...) to run an inline Tree-sitter query before returning the first matched node. Prefer the AST forms when targeting a function, class, or method and you want tighter context. Demo-style aliases like file_path, offset, and limit are accepted. Use metadata_only=true only when you want file metadata without content. Do not use run with cat, head, or tail for file reads.',
1703
+ 'Inspect code or text files. Use read(path) for normal file or line-window reads. Use start_line and end_line for ranges, or path:"src/app.ts:10-40" for inline ranges. Prefer this over run with cat, head, or tail.',
1823
1704
  parameters: {
1824
1705
  type: 'object',
1825
1706
  properties: {
1826
1707
  path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
1827
- file_path: { type: 'string', description: 'Alias for path' },
1828
1708
  start_line: { type: 'number', description: '1-based start line' },
1829
1709
  end_line: { type: 'number', description: 'Inclusive end line' },
1830
- offset: { type: 'number', description: 'Alias for start_line' },
1831
- limit: { type: 'number', description: 'Number of lines to read starting from offset/start_line' },
1832
1710
  max_chars: { type: 'number', description: 'Max chars to return' },
1833
- include_content: { type: 'boolean', description: 'Legacy compatibility flag. Content is returned by default.' },
1834
- read_token: { type: 'string', description: 'Legacy compatibility token. No longer required for content reads.' },
1835
- metadata_only: { type: 'boolean', description: 'Set true to return metadata without content.' },
1836
1711
  ast_target: { type: 'object', description: 'AST target from ast_query or a prior AST selection. When provided, read returns that node instead of a line window.' },
1837
1712
  query: { type: 'string', description: 'Optional Tree-sitter query to run inline before reading the first matched AST node. Use with path for one-shot function/class/method reads.' },
1838
1713
  capture_name: { type: 'string', description: 'Optional capture name to select when query is provided.' },
@@ -1847,14 +1722,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1847
1722
  function: {
1848
1723
  name: 'grep',
1849
1724
  description:
1850
- 'Search file contents. Use this for code search before read or edit. Aliases like query and directory are accepted. Do not use run with grep or rg for normal code search.',
1725
+ 'Search file contents. Use this for code search before read or edit. Do not use run with grep or rg for normal code search.',
1851
1726
  parameters: {
1852
1727
  type: 'object',
1853
1728
  properties: {
1854
1729
  pattern: { type: 'string', description: 'Search pattern' },
1855
- query: { type: 'string', description: 'Alias for pattern' },
1856
1730
  path: { type: 'string', description: 'Directory or file to search' },
1857
- directory: { type: 'string', description: 'Alias for path' },
1858
1731
  regex: { type: 'boolean', description: 'Treat pattern as regex' },
1859
1732
  case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
1860
1733
  max_results: { type: 'number', description: 'Max matches to return' },
@@ -1869,12 +1742,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1869
1742
  type: 'function',
1870
1743
  function: {
1871
1744
  name: 'list',
1872
- description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads. Aliases like directory are accepted, and plain string paths are tolerated by the runtime.',
1745
+ description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.',
1873
1746
  parameters: {
1874
1747
  type: 'object',
1875
1748
  properties: {
1876
1749
  path: { type: 'string', description: 'Directory path to list' },
1877
- directory: { type: 'string', description: 'Alias for path' },
1878
1750
  include_hidden: { type: 'boolean', description: 'Include dotfiles' }
1879
1751
  }
1880
1752
  }
@@ -1885,14 +1757,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1885
1757
  function: {
1886
1758
  name: 'glob',
1887
1759
  description:
1888
- 'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts. Aliases like query and directory are accepted.',
1760
+ 'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts.',
1889
1761
  parameters: {
1890
1762
  type: 'object',
1891
1763
  properties: {
1892
1764
  pattern: { type: 'string', description: 'Glob pattern' },
1893
1765
  path: { type: 'string', description: 'Directory to search' },
1894
- query: { type: 'string', description: 'Alias for pattern' },
1895
- directory: { type: 'string', description: 'Alias for path' },
1896
1766
  include_hidden: { type: 'boolean', description: 'Include dotfiles' },
1897
1767
  max_results: { type: 'number', description: 'Max results' }
1898
1768
  },
@@ -1923,18 +1793,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1923
1793
  function: {
1924
1794
  name: 'edit',
1925
1795
  description:
1926
- 'Edit existing files. Prefer one of these shapes: 1) {file, old_text, new_text} for exact text replacement, 2) {file, symbol, edit:{kind:"replace_block", new_content:"..."}} for block replacement, 3) {file, anchor_text, position:"before"|"after", content:"..."} for inserts. Demo-style aliases {file_path, old_string, new_string} are also accepted. Read first unless the exact target is already known, and prefer read(ast_target=...) or read(path, query=...) before symbol- or block-level edits when you want tighter context. Prefer this over write for existing code changes.',
1796
+ 'Edit existing files. Prefer one of these shapes: 1) {path, old_text, new_text} for exact text replacement, 2) {path, symbol, edit:{kind:"replace_block", new_content:"..."}} for block replacement, 3) {path, anchor_text, position:"before"|"after", content:"..."} for inserts. Read first unless the exact target is already known. Prefer this over write for existing code changes.',
1927
1797
  parameters: {
1928
1798
  type: 'object',
1929
1799
  properties: {
1930
- file: { type: 'string', description: 'File path to edit' },
1931
- path: { type: 'string', description: 'Alias for file' },
1932
- file_path: { type: 'string', description: 'Alias for file, compatible with simpler demo-style tool calls' },
1800
+ path: { type: 'string', description: 'File path to edit' },
1933
1801
  new_content: { type: 'string', description: 'Replacement content' },
1934
1802
  old_text: { type: 'string', description: 'Exact text to replace' },
1935
1803
  new_text: { type: 'string', description: 'Replacement text' },
1936
- old_string: { type: 'string', description: 'Alias for old_text' },
1937
- new_string: { type: 'string', description: 'Alias for new_text' },
1938
1804
  anchor_text: { type: 'string', description: 'Anchor text for inserts' },
1939
1805
  content: { type: 'string', description: 'Content to insert or append' },
1940
1806
  position: { type: 'string', description: 'before or after' },
@@ -1945,7 +1811,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1945
1811
  line: { type: 'number', description: 'Line to target' },
1946
1812
  edit: { type: 'object', description: 'Structured edit input' }
1947
1813
  },
1948
- required: ['file']
1814
+ required: ['path']
1949
1815
  }
1950
1816
  }
1951
1817
  },
@@ -1954,16 +1820,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1954
1820
  function: {
1955
1821
  name: 'write',
1956
1822
  description:
1957
- 'Create a new file or overwrite a file. Always include path and content. Aliases like file, file_path, text, and new_content are accepted. Use this for new files or explicit full rewrites only. Example: {path:"src/page.html", content:"..."} . If the file path is not decided yet, do not call write yet. Prefer edit for existing code changes.',
1823
+ 'Create a new file or overwrite a file. Always include path and content. Use this for new files or explicit full rewrites only. Example: {path:"src/page.html", content:"..."} . If the file path is not decided yet, do not call write yet. Prefer edit for existing code changes.',
1958
1824
  parameters: {
1959
1825
  type: 'object',
1960
1826
  properties: {
1961
1827
  path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
1962
- file_path: { type: 'string', description: 'Alias for path, compatible with simpler demo-style tool calls' },
1963
- file: { type: 'string', description: 'Alias for path' },
1964
1828
  content: { type: 'string', description: 'Content to write' },
1965
- text: { type: 'string', description: 'Alias for content' },
1966
- new_content: { type: 'string', description: 'Alias for content' },
1967
1829
  append: { type: 'boolean', description: 'Append instead of overwrite' },
1968
1830
  full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
1969
1831
  },
@@ -1976,15 +1838,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1976
1838
  function: {
1977
1839
  name: 'delete',
1978
1840
  description:
1979
- 'Delete a file or directory inside the workspace. Use path, file, or file_path to point at the target. Missing targets fail. Workspace escape attempts are rejected.',
1841
+ 'Delete a file or directory inside the workspace. Missing targets fail. Workspace escape attempts are rejected.',
1980
1842
  parameters: {
1981
1843
  type: 'object',
1982
1844
  properties: {
1983
- path: { type: 'string', description: 'File or directory path to delete' },
1984
- file: { type: 'string', description: 'Alias for path' },
1985
- file_path: { type: 'string', description: 'Alias for path' },
1986
- directory: { type: 'string', description: 'Alias for path' },
1987
- dir: { type: 'string', description: 'Alias for path' }
1845
+ path: { type: 'string', description: 'File or directory path to delete' }
1988
1846
  },
1989
1847
  required: ['path']
1990
1848
  }
@@ -180,6 +180,10 @@ const TUI_COPY = {
180
180
  doingGlob: '正在按模式查找文件',
181
181
  doneGrep: '已搜索关键词',
182
182
  doingGrep: '正在搜索关键词',
183
+ doneWebFetch: '已抓取网页',
184
+ doingWebFetch: '正在抓取网页',
185
+ doneWebSearch: '已搜索网页',
186
+ doingWebSearch: '正在搜索网页',
183
187
  doneCommand: '已执行命令',
184
188
  doingCommand: '正在执行命令',
185
189
  doneUpdateTodos: '已更新待办',
@@ -388,6 +392,10 @@ const TUI_COPY = {
388
392
  doingGlob: 'Matching files by pattern',
389
393
  doneGrep: 'Searched keywords',
390
394
  doingGrep: 'Searching keywords',
395
+ doneWebFetch: 'Fetched page',
396
+ doingWebFetch: 'Fetching page',
397
+ doneWebSearch: 'Searched web',
398
+ doingWebSearch: 'Searching web',
391
399
  doneCommand: 'Ran command',
392
400
  doingCommand: 'Running command',
393
401
  doneUpdateTodos: 'Updated todos',
@@ -1175,6 +1183,8 @@ function getActivityDisplayParts(activity) {
1175
1183
  patch: 'Patch',
1176
1184
  run: 'Run',
1177
1185
  grep: 'Search',
1186
+ web_fetch: 'Fetch',
1187
+ web_search: 'Web Search',
1178
1188
  glob: 'Glob',
1179
1189
  list: 'List',
1180
1190
  list_background_tasks: 'Tasks',
@@ -1193,6 +1203,8 @@ function getActivityDisplayParts(activity) {
1193
1203
  patch: '🩹',
1194
1204
  run: '⚙️',
1195
1205
  grep: '🔍',
1206
+ web_fetch: '🌐',
1207
+ web_search: '🌐',
1196
1208
  glob: '🧭',
1197
1209
  list: '📂',
1198
1210
  list_background_tasks: '🗃️',
@@ -2401,7 +2413,17 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
2401
2413
  } else {
2402
2414
  pushTextRows(msg?.text || '');
2403
2415
  const toolCalls = Array.isArray(msg?.toolCalls) ? msg.toolCalls : [];
2404
- toolCalls.forEach((tool, idx) => pushActivityRows(tool, idx, toolCalls.length));
2416
+ const pendingToolCalls = Array.isArray(msg?.pendingToolCalls) ? msg.pendingToolCalls : [];
2417
+ const visibleCalls = [
2418
+ ...toolCalls,
2419
+ ...pendingToolCalls.filter((pending) => {
2420
+ if (!pending) return false;
2421
+ if (pending.id && toolCalls.some((tool) => tool?.id && tool.id === pending.id)) return false;
2422
+ const pendingBase = parseToolDisplayName(pending.name).base;
2423
+ return !toolCalls.some((tool) => parseToolDisplayName(tool?.name).base === pendingBase && tool?.status === 'running');
2424
+ })
2425
+ ];
2426
+ visibleCalls.forEach((tool, idx) => pushActivityRows(tool, idx, visibleCalls.length));
2405
2427
  }
2406
2428
 
2407
2429
  const codeGenerationRows = getCodeGenerationActivityRows(msg);
@@ -12,5 +12,19 @@ export function describeMiscToolActivity(copy, parsed, rawName, { done = false,
12
12
  if (parsed.base === 'update_todos') {
13
13
  return blocked ? makeBlocked(copy, 'update_todos') : done ? copy.toolActivity.doneUpdateTodos : copy.toolActivity.doingUpdateTodos;
14
14
  }
15
+ if (parsed.base === 'web_fetch') {
16
+ const target = parsed.target || parsed.raw;
17
+ const label = done
18
+ ? (copy.toolActivity.doneWebFetch || copy.toolActivity.doneGeneric)
19
+ : (copy.toolActivity.doingWebFetch || copy.toolActivity.doingGeneric);
20
+ return blocked ? makeBlocked(copy, target) : `${label}: ${target}`;
21
+ }
22
+ if (parsed.base === 'web_search') {
23
+ const target = parsed.target || parsed.raw;
24
+ const label = done
25
+ ? (copy.toolActivity.doneWebSearch || copy.toolActivity.doneGeneric)
26
+ : (copy.toolActivity.doingWebSearch || copy.toolActivity.doingGeneric);
27
+ return blocked ? makeBlocked(copy, target) : `${label}: ${target}`;
28
+ }
15
29
  return blocked ? `${copy.toolActivity.blocked}: ${parsed.raw}` : done ? `${copy.toolActivity.doneGeneric}: ${parsed.raw}` : `${copy.toolActivity.doingGeneric}: ${parsed.raw}`;
16
30
  }