codemini-cli 0.3.9 → 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.
@@ -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,11 +43,18 @@ 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
- 6. Capture a high-signal observation during work
49
- When you notice a reusable pattern, a user correction, a repeated failure, or a stable preference — capture it to the dream loop inbox for later consolidation.
50
- Tool: capture_memory({"summary":"User prefers tab size 2 for all JSON files","scope":"global","type":"preference"})
48
+ 6. Save a high-signal observation to memory
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:
50
+ - scope "user" for personal preferences (language, reply style, interaction habits)
51
+ - scope "global" for cross-project lessons (environment quirks, general tool workflows)
52
+ - scope "project" for project-specific knowledge (architecture conventions, local config, test commands, file locations)
53
+
54
+ Examples:
55
+ Tool: save_memory({"content":"User prefers tab size 2 for all JSON files","scope":"user","kind":"preference"})
56
+ Tool: save_memory({"content":"This project uses vitest, not jest — run tests with npx vitest run","scope":"project","kind":"pattern"})
57
+ Tool: save_memory({"content":"WSL2 bash exec prefix does not support cd as a command","scope":"global","kind":"correction"})
51
58
 
52
59
  7. Run a dream loop consolidation pass
53
60
  When you want to review and consolidate inbox entries into long-term memory.
@@ -66,7 +73,7 @@ Tool: tool_search({"query":"web_search"})
66
73
  Tool: web_search({"query":"latest pnpm release","max_results":5})
67
74
 
68
75
  Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
69
- 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.`;
70
77
  }
71
78
 
72
79
  function getEnvBlock() {
@@ -1,5 +1,6 @@
1
1
  import { listMemories, listInbox, archiveEntry, promoteMemory } from './memory-store.js';
2
2
  import { writeDreamAuditReport } from './dream-audit.js';
3
+ import { evaluateInboxBatch } from './dream-evaluator.js';
3
4
 
4
5
  const LONGTERM_TYPES = new Set(['preference', 'pattern', 'win', 'decision']);
5
6
  const OPERATIONAL_TYPES = new Set(['correction', 'failure', 'gap', 'observation']);
@@ -8,14 +9,6 @@ function normalizeText(value) {
8
9
  return String(value || '').trim().toLowerCase();
9
10
  }
10
11
 
11
- function mapInboxScopeToMemoryScope(scope) {
12
- const value = normalizeText(scope);
13
- if (value === 'repo' || value === 'project') return 'project';
14
- if (value === 'thread') return 'global';
15
- if (value === 'user') return 'user';
16
- return 'global';
17
- }
18
-
19
12
  function chooseLifecycle(type) {
20
13
  const value = normalizeText(type);
21
14
  if (LONGTERM_TYPES.has(value)) return 'longterm';
@@ -55,7 +48,10 @@ export async function runDreamConsolidation({
55
48
  const filesRead = ['memory/inbox/*', 'memory/global.json', 'memory/user.json', 'memory/project/*.json'];
56
49
  const filesChanged = [];
57
50
 
51
+ /* ── Phase 1: 规则预过滤(快速剔除明显垃圾) ─────────────────── */
52
+ const candidates = [];
58
53
  const seen = new Map();
54
+
59
55
  for (const entry of inbox) {
60
56
  const summaryKey = normalizeText(entry.summary);
61
57
  if (!summaryKey) {
@@ -78,14 +74,58 @@ export async function runDreamConsolidation({
78
74
  continue;
79
75
  }
80
76
 
81
- const lifecycle = chooseLifecycle(entry.type);
82
- const promoteScope = mapInboxScopeToMemoryScope(entry.scope);
77
+ candidates.push(entry);
78
+ }
79
+
80
+ if (candidates.length === 0) {
81
+ const report = { timestamp: new Date().toISOString(), filesRead, filesChanged: [], candidatesGenerated: inbox.length, promotions, rejections, archives };
82
+ if (!dryRun && writeAudit) {
83
+ const reportPath = await writeDreamAuditReport(report);
84
+ report.auditReport = reportPath;
85
+ }
86
+ return { ok: true, dryRun, ...report };
87
+ }
88
+
89
+ /* ── Phase 2: LLM 批量评估(质量门控 + scope 分类 + 内容提炼) ── */
90
+ const llmResults = dryRun
91
+ ? candidates.map((e) => ({ id: e.id, action: 'keep', scope: 'global', kind: e.type || 'observation', content: e.details || e.summary, summary: e.summary, confidence: 0.9 }))
92
+ : await evaluateInboxBatch({ entries: candidates, config, workspaceRoot });
93
+
94
+ const resultMap = new Map(llmResults.map((r) => [r.id, r]));
95
+
96
+ /* ── Phase 3: 按评估结果 promote 或 archive ─────────────────── */
97
+ for (const entry of candidates) {
98
+ const evaluation = resultMap.get(entry.id);
99
+
100
+ if (!evaluation || evaluation.action === 'discard') {
101
+ const reason = evaluation?.reason || 'LLM discarded';
102
+ if (!dryRun) await archiveEntry(entry, 'discarded-by-evaluator', reason);
103
+ rejections.push({ summary: entry.summary, reason: `evaluator-discard: ${reason}` });
104
+ continue;
105
+ }
106
+
107
+ const promoteScope = evaluation.scope || 'global';
108
+ const lifecycle = chooseLifecycle(evaluation.kind);
109
+ const enrichedEntry = {
110
+ ...entry,
111
+ /* 用 LLM 提炼后的内容覆盖原始报错 */
112
+ summary: evaluation.summary || entry.summary,
113
+ details: evaluation.content || entry.details || entry.summary,
114
+ type: evaluation.kind || entry.type || 'observation'
115
+ };
83
116
 
84
117
  if (!dryRun) {
85
118
  try {
86
- await promoteMemory({ entry, scope: promoteScope, lifecycle, workspaceRoot, config });
87
- filesChanged.push({ file: `memory/${promoteScope}.json`, why: `Promoted "${entry.summary}" as ${lifecycle}` });
88
- promotions.push({ summary: entry.summary, scope: promoteScope, lifecycle, rationale: entry.type });
119
+ await promoteMemory({
120
+ entry: enrichedEntry,
121
+ scope: promoteScope,
122
+ lifecycle,
123
+ workspaceRoot,
124
+ config,
125
+ confidence: evaluation.confidence || 0.8
126
+ });
127
+ filesChanged.push({ file: `memory/${promoteScope}.json`, why: `Promoted "${enrichedEntry.summary}" as ${lifecycle} (${promoteScope})` });
128
+ promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence });
89
129
  } catch (error) {
90
130
  const reason = String(error?.message || error || 'promotion failed').slice(0, 180);
91
131
  await archiveEntry(entry, 'promotion-failed', reason);
@@ -95,7 +135,7 @@ export async function runDreamConsolidation({
95
135
  continue;
96
136
  }
97
137
 
98
- promotions.push({ summary: entry.summary, scope: promoteScope, lifecycle, rationale: entry.type, dryRun: true });
138
+ promotions.push({ summary: enrichedEntry.summary, scope: promoteScope, lifecycle, rationale: evaluation.kind, confidence: evaluation.confidence, dryRun: true });
99
139
  }
100
140
 
101
141
  const report = {
@@ -0,0 +1,99 @@
1
+ import { createChatCompletion } from './provider/index.js';
2
+
3
+ const EVAL_TIMEOUT_MS = 30000;
4
+
5
+ const SYSTEM_PROMPT = `You are a memory consolidation evaluator for a coding assistant. You receive a batch of inbox items (tool errors, observations, etc.) and decide for each one:
6
+
7
+ 1. **keep or discard** — Does this contain a reusable, durable insight? Discard transient errors, one-off issues, and noise.
8
+ 2. **scope** — "global" for cross-project knowledge (e.g., "WSL bash exec does not support cd"), "project" for project-specific context (e.g., "this project uses vitest for testing").
9
+ 3. **kind** — One of: pattern, observation, correction, decision, failure
10
+ 4. **content** — A refined, actionable sentence describing the insight. NOT the raw error text.
11
+ 5. **summary** — A short label (under 80 chars) for quick scanning.
12
+ 6. **confidence** — 0.5–1.0 based on how certain and durable the insight is.
13
+
14
+ Respond with valid JSON only, no markdown fences:
15
+ {"results":[{"id":"<inbox-id>","action":"keep","scope":"global|project","kind":"pattern|observation|correction|decision|failure","content":"...","summary":"...","confidence":0.8},{"id":"<inbox-id>","action":"discard","reason":"..."}]}
16
+
17
+ Rules:
18
+ - Raw tool error messages are NOT insights by themselves. Only keep if they reveal a reusable lesson.
19
+ - "exit 127", "command not found", "permission denied", "blocked by policy" → always discard (transient/config issues)
20
+ - A repeated pattern across multiple errors → keep as a "pattern" or "correction"
21
+ - Project-specific paths, file names, or commands → scope "project"
22
+ - General coding/environment knowledge → scope "global"
23
+ - If in doubt, discard. Memory is expensive; only promote what future sessions will genuinely benefit from.`;
24
+
25
+ function parseResults(text) {
26
+ try {
27
+ const json = JSON.parse(text);
28
+ if (!json?.results || !Array.isArray(json.results)) return [];
29
+ return json.results.map((r) => ({
30
+ id: String(r.id || ''),
31
+ action: r.action === 'keep' ? 'keep' : 'discard',
32
+ scope: r.scope === 'project' ? 'project' : 'global',
33
+ kind: ['pattern', 'observation', 'correction', 'decision', 'failure'].includes(r.kind) ? r.kind : 'observation',
34
+ content: String(r.content || '').slice(0, 300),
35
+ summary: String(r.summary || '').slice(0, 120),
36
+ confidence: Math.min(1, Math.max(0.5, Number(r.confidence) || 0.7)),
37
+ reason: String(r.reason || '')
38
+ }));
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 用 LLM 批量评估 inbox 条目,决定保留/丢弃、scope、内容提炼。
46
+ * @param {{ entries: Array, config: object, workspaceRoot?: string }} params
47
+ * @returns {Promise<Array<{ id, action, scope?, kind?, content?, summary?, confidence?, reason? }>>}
48
+ */
49
+ export async function evaluateInboxBatch({ entries, config, workspaceRoot }) {
50
+ if (!entries || entries.length === 0) return [];
51
+
52
+ const batch = entries.map((e) => ({
53
+ id: e.id,
54
+ type: e.type || '',
55
+ source: e.source || '',
56
+ summary: (e.summary || '').slice(0, 150),
57
+ details: (e.details || '').slice(0, 400)
58
+ }));
59
+
60
+ try {
61
+ const result = await createChatCompletion({
62
+ sdkProvider: config?.sdk?.provider,
63
+ baseUrl: config?.gateway?.base_url,
64
+ apiKey: config?.gateway?.api_key,
65
+ model: config?.model?.name,
66
+ messages: [
67
+ { role: 'system', content: SYSTEM_PROMPT },
68
+ {
69
+ role: 'user',
70
+ content: `Evaluate these ${batch.length} inbox items. Workspace: ${workspaceRoot || process.cwd()}\n\n${JSON.stringify(batch, null, 2)}`
71
+ }
72
+ ],
73
+ temperature: 0,
74
+ timeoutMs: EVAL_TIMEOUT_MS
75
+ });
76
+
77
+ const text = result?.text || '';
78
+ const parsed = parseResults(text);
79
+ /* 确保每个 entry 都有结果,LLM 没返回的一律 discard */
80
+ const covered = new Set(parsed.map((r) => r.id));
81
+ for (const entry of entries) {
82
+ if (!covered.has(entry.id)) {
83
+ parsed.push({
84
+ id: entry.id,
85
+ action: 'discard',
86
+ reason: 'LLM did not return a result for this entry'
87
+ });
88
+ }
89
+ }
90
+ return parsed;
91
+ } catch {
92
+ /* LLM 调用失败 → 全部 discard(fail-safe) */
93
+ return entries.map((e) => ({
94
+ id: e.id,
95
+ action: 'discard',
96
+ reason: 'LLM evaluation failed'
97
+ }));
98
+ }
99
+ }
@@ -111,7 +111,7 @@ class FffMcpClient {
111
111
  capabilities: {},
112
112
  clientInfo: {
113
113
  name: 'codemini-cli',
114
- version: '0.3.9'
114
+ version: '0.4.1'
115
115
  }
116
116
  });
117
117
  this.sendNotification('notifications/initialized', {});
@@ -375,7 +375,8 @@ export async function promoteMemory({
375
375
  lifecycle = 'operational',
376
376
  workspaceRoot = process.cwd(),
377
377
  projectAlias = '',
378
- config = {}
378
+ config = {},
379
+ confidence = 0.9
379
380
  } = {}) {
380
381
  if (!entry?.summary) throw new Error('Entry with summary is required for promotion');
381
382
  const lc = validateLifecycle(lifecycle);
@@ -386,7 +387,7 @@ export async function promoteMemory({
386
387
  kind: entry.type || 'note',
387
388
  summary: normalizeMemoryText(entry.summary),
388
389
  source: `dream-promote:${entry.id}`,
389
- confidence: 0.9,
390
+ confidence: Math.min(1, Math.max(0.5, confidence)),
390
391
  replaceSimilar: true,
391
392
  workspaceRoot,
392
393
  projectAlias,
package/src/core/paths.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  const GLOBAL_APP_DIR = 'codemini-global';
5
5
  const PROJECT_APP_DIR = '.codemini';
6
- const PROJECT_INDEX_DIR = '.codemini-project';
6
+ const PROJECT_INDEX_DIR = '.codemini';
7
7
 
8
8
  export function getBaseConfigDir() {
9
9
  if (process.env.CODEMINI_GLOBAL_DIR) {
@@ -387,7 +387,7 @@ export async function initializeProjectIndex(cwd = process.cwd()) {
387
387
  projectRoot: targetRoot,
388
388
  projectMap,
389
389
  fileIndex,
390
- summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini-project (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
390
+ summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
391
391
  };
392
392
  })();
393
393
  initCache.set(cacheKey, promise);
@@ -447,7 +447,7 @@ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '')
447
447
  path: projectRelativePath,
448
448
  projectRoot,
449
449
  action,
450
- summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini-project for ${projectRelativePath}`
450
+ summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini for ${projectRelativePath}`
451
451
  };
452
452
  }
453
453
 
@@ -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);
@@ -23,8 +23,10 @@ const SHELL_PROFILES = {
23
23
  'npm',
24
24
  'npx',
25
25
  'python',
26
+ 'python3',
26
27
  'py',
27
28
  'pip',
29
+ 'pip3',
28
30
  'get-childitem',
29
31
  'get-content',
30
32
  'select-string',
@@ -70,7 +72,9 @@ const SHELL_PROFILES = {
70
72
  'npm',
71
73
  'npx',
72
74
  'python',
75
+ 'python3',
73
76
  'pip',
77
+ 'pip3',
74
78
  'ls',
75
79
  'cat',
76
80
  'sed',
@@ -142,11 +146,11 @@ export function getShellSystemPrompt(value) {
142
146
  ALWAYS prefer dedicated tools over raw shell commands:
143
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
144
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
145
- - 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}
146
150
  - Use grep to search file contents — NEVER use grep or rg via run
147
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
148
- - 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
149
- - 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:"..."}
150
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
151
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)
152
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
@@ -164,7 +168,7 @@ Some tools are loaded on demand through tool_search. Common examples:
164
168
  - glob for pattern-based file lookup
165
169
  - ast_query and read_ast_node for advanced AST-scoped reads and edits
166
170
  - list_background_tasks, get_background_task, and stop_background_task for managing long-running background commands
167
- - remember_user, remember_global, remember_project, list_memory, search_memory, and forget_memory for persistent memory operations
171
+ - save_memory, list_memory, search_memory, and forget_memory for persistent memory operations
168
172
 
169
173
  For structural code edits (functions, classes, methods), prefer AST-scoped reads before editing:
170
174
  - Common one-shot workflow: read(path, query=..., capture_name=...) → edit with symbol or ast_target
@@ -177,15 +181,15 @@ For background commands: use run to launch. If you need management tools that ar
177
181
  Common tool call patterns:
178
182
  - Query the project index first: {query:"login auth flow", path:"src", max_results:5}
179
183
  - Load a deferred tool when needed: {query:"glob"} or {query:"all"}
180
- - 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}
181
185
  - Read a specific range inline: {path:"src/app.ts:20-60"}
182
186
  - Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
183
187
  - List a directory first: {path:"src"}
184
188
  - After loading glob, find files by pattern: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
185
- - 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"}
186
190
  - Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
187
- - Write a new file: {file:"notes.txt", text:"..."} or {path:"src/page.tsx", content:"..."}
188
- - 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
189
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
190
194
 
191
195
  # Doing tasks
@@ -214,7 +218,7 @@ Common tool call patterns:
214
218
  - Keep answers compact and easy to scan
215
219
  - Lead with the answer or next action, not scene-setting
216
220
  - Do not restate the user's request unless a brief restatement prevents ambiguity
217
- - When referencing code, use file_path:line_number format
221
+ - When referencing code, use path:line_number format
218
222
  - Keep technical wording, commands, paths, and error details exact
219
223
  - Only use emojis if the user explicitly requests it`;
220
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
+ }