codemini-cli 0.4.0 → 0.4.2

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.
@@ -1,11 +1,10 @@
1
- import os from 'node:os';
2
1
  import path from 'node:path';
3
- import fs from 'node:fs/promises';
4
- import { BoundedCache } from './bounded-cache.js';
5
2
  import { trimInline as _trimInline, normalizePath } from './string-utils.js';
6
3
  import { captureToInbox, listInbox } from './memory-store.js';
7
4
  import { requiresApprovalEvaluation } from './command-risk.js';
8
5
  import { getToolOutputSanitizeOptions, sanitizeTextForModel } from './tool-output.js';
6
+ import { normalizeToolArguments } from './tool-args.js';
7
+ import { storeResultIfNeeded, summarizeToolResult } from './tool-result-store.js';
9
8
 
10
9
  /**
11
10
  * 安全解析 JSON 字符串。
@@ -25,20 +24,6 @@ function safeJsonParse(raw) {
25
24
  }
26
25
  }
27
26
 
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
27
  function buildDeleteApprovalDetails(source, rawPath) {
43
28
  const existing =
44
29
  source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
@@ -74,90 +59,6 @@ function buildDeleteCancellationResult(args) {
74
59
  };
75
60
  }
76
61
 
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
62
  function emptyToolResultMarker(toolName) {
162
63
  const name = String(toolName || 'tool').trim() || 'tool';
163
64
  return `(${name} completed with no output)`;
@@ -259,107 +160,6 @@ function compactToolResult(result, toolName, args, maxChars = 12000) {
259
160
  return clipToolResult(obj, Math.min(maxChars, 4000));
260
161
  }
261
162
 
262
- // ─── P0: Large result disk store ─────────────────────────────────────
263
-
264
- const TOOL_RESULT_DISK_THRESHOLD = 6000;
265
- const PREVIEW_SIZE_BYTES = 2000;
266
- const TOOL_RESULTS_SUBDIR = 'tool-results';
267
-
268
- let currentResultDir = null;
269
- let resultDirReady = false;
270
- const storedResults = new BoundedCache({
271
- maxSize: 64,
272
- ttlMs: 30 * 60 * 1000,
273
- onEvict(key, value) {
274
- if (value?.filePath) {
275
- fs.unlink(value.filePath).catch(() => {});
276
- }
277
- }
278
- }); // callId -> { filePath, summary }
279
- const readCache = new BoundedCache({ maxSize: 128, ttlMs: 10 * 60 * 1000 }); // "path:startLine:endLine:mtimeMs" -> true
280
-
281
- function generatePreview(content) {
282
- if (content.length <= PREVIEW_SIZE_BYTES) {
283
- return { preview: content, hasMore: false };
284
- }
285
- const truncated = content.slice(0, PREVIEW_SIZE_BYTES);
286
- const lastNewline = truncated.lastIndexOf('\n');
287
- const cutPoint = lastNewline > PREVIEW_SIZE_BYTES * 0.5 ? lastNewline : PREVIEW_SIZE_BYTES;
288
- return { preview: content.slice(0, cutPoint), hasMore: true };
289
- }
290
-
291
- function formatFileSize(chars) {
292
- if (chars < 1024) return `${chars} B`;
293
- return `${(chars / 1024).toFixed(1)} KB`;
294
- }
295
-
296
- export function setResultDir(dir) {
297
- currentResultDir = dir ? path.join(dir, TOOL_RESULTS_SUBDIR) : null;
298
- resultDirReady = false;
299
- }
300
-
301
- async function ensureResultDir() {
302
- if (!currentResultDir) return false;
303
- if (!resultDirReady) {
304
- await fs.mkdir(currentResultDir, { recursive: true });
305
- resultDirReady = true;
306
- }
307
- return true;
308
- }
309
-
310
- async function storeResultIfNeeded(callId, formattedContent, rawResult) {
311
- if (formattedContent.length <= TOOL_RESULT_DISK_THRESHOLD) {
312
- return formattedContent;
313
- }
314
- try {
315
- const ready = await ensureResultDir();
316
- const dir = ready ? currentResultDir : path.join(os.tmpdir(), 'codemini-results');
317
- if (!resultDirReady && dir === currentResultDir) {
318
- await fs.mkdir(dir, { recursive: true });
319
- } else if (!resultDirReady) {
320
- await fs.mkdir(dir, { recursive: true });
321
- }
322
- const filePath = path.join(dir, `${callId}.txt`);
323
- const payload = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
324
- await fs.writeFile(filePath, payload, 'utf-8');
325
- const summary = summarizeToolResult(rawResult);
326
- const { preview, hasMore } = generatePreview(payload);
327
- storedResults.set(callId, { filePath, summary });
328
-
329
- return `<persisted-output>
330
- Output too large (${formatFileSize(payload.length)}). Full output saved to: ${filePath}
331
-
332
- Preview (first ${formatFileSize(PREVIEW_SIZE_BYTES)}):
333
- ${preview}${hasMore ? '\n...' : ''}
334
-
335
- Summary: ${summary}
336
- </persisted-output>`;
337
- } catch {
338
- return formattedContent;
339
- }
340
- }
341
-
342
- export function clearResultStore() {
343
- const files = [];
344
- for (const [, val] of storedResults.entries()) {
345
- files.push(val.filePath);
346
- }
347
- storedResults.clear();
348
- readCache.clear();
349
- return Promise.allSettled(files.map((f) => fs.unlink(f).catch(() => {})));
350
- }
351
-
352
- // ─── Read deduplication ─────────────────────────────────────────────
353
-
354
- export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
355
- const key = `${filePath}:${startLine || 0}:${endLine || 0}:${mtimeMs}`;
356
- if (readCache.has(key)) {
357
- return true;
358
- }
359
- readCache.set(key, true);
360
- return false;
361
- }
362
-
363
163
  // ─── P1a: Read-only tool classification ──────────────────────────────
364
164
 
365
165
  const READ_ONLY_TOOLS = new Set([
@@ -408,18 +208,19 @@ function shouldAutoCaptureError(toolName, message) {
408
208
  return true;
409
209
  }
410
210
 
411
- function fireAndForgetCapture(toolName, message, args) {
211
+ async function captureToolFailure(toolName, message, args, config = {}) {
212
+ if (config?.memory?.enabled === false || config?.memory?.auto_capture === false) return;
412
213
  const summary = `[${toolName}] ${String(message).slice(0, 120)}`;
413
214
  const details = args
414
215
  ? `Tool: ${toolName}\nError: ${message}\nArgs: ${JSON.stringify(args).slice(0, 300)}`
415
216
  : `Tool: ${toolName}\nError: ${message}`;
416
- captureToInbox({
417
- scope: 'auto',
217
+ await captureToInbox({
218
+ scope: 'repo',
418
219
  type: 'failure',
419
220
  summary,
420
221
  details,
421
222
  source: 'auto-capture'
422
- }).catch(() => {});
223
+ });
423
224
  }
424
225
 
425
226
  async function checkAutoDreamThreshold(config) {
@@ -462,108 +263,6 @@ function extractFileChange(toolName, result) {
462
263
  return null;
463
264
  }
464
265
 
465
- export function summarizeToolResult(result) {
466
- if (result === null || result === undefined) return 'no output';
467
- if (typeof result === 'string') {
468
- const oneLine = result.replace(/\s+/g, ' ').trim();
469
- return oneLine.length > 90 ? `${oneLine.slice(0, 87)}...` : oneLine || 'empty string';
470
- }
471
- if (typeof result === 'object') {
472
- const obj = result;
473
- if (Array.isArray(obj)) return `array(${obj.length})`;
474
- if ('deleted' in obj && 'path' in obj) {
475
- const kind = trimInline(obj.type || 'item', 16);
476
- const target = trimInline(obj.path || '', 96);
477
- if (obj.deleted) return target ? `deleted ${kind} ${target}` : `deleted ${kind}`;
478
- if (obj.cancelled) return target ? `cancelled delete ${target}` : 'cancelled delete';
479
- }
480
- if ('path' in obj && 'action' in obj) {
481
- const p = String(obj.path || '');
482
- const action = String(obj.action || 'write');
483
- const line = Number(obj.changed_line || 1);
484
- const suffix =
485
- action === 'delete'
486
- ? 'deleted'
487
- : action === 'create'
488
- ? 'created'
489
- : action === 'patch'
490
- ? 'patched'
491
- : action === 'replace_block' || action === 'replace_text'
492
- ? 'edited'
493
- : action === 'append'
494
- ? 'appended'
495
- : 'updated';
496
- return p ? `${suffix} ${p}${line > 0 ? ` @L${line}` : ''}` : suffix;
497
- }
498
- if ('path' in obj && 'phase' in obj) {
499
- const phase = String(obj.phase || '');
500
- const p = String(obj.path || '');
501
- const total = Number(obj.total_lines);
502
- const start =
503
- Number(obj.suggested_start_line || obj.start_line) > 0
504
- ? Number(obj.suggested_start_line || obj.start_line)
505
- : 1;
506
- const end =
507
- Number(obj.suggested_end_line || obj.end_line) >= start
508
- ? Number(obj.suggested_end_line || obj.end_line)
509
- : start;
510
- const rangeText = start > 0 && end >= start ? ` lines ${start}-${end}` : '';
511
- const totalText = total > 0 ? ` of ${total}` : '';
512
- const enclosingText = obj.enclosing_symbol ? ` in ${obj.enclosing_symbol}` : '';
513
- const errorText = obj.error ? ` (${trimInline(obj.error, 64)})` : '';
514
- const truncatedText = obj.truncated ? ' [truncated]' : '';
515
- return phase === 'metadata'
516
- ? `metadata for ${p}${rangeText}${totalText}${errorText}`
517
- : `content from ${p}${rangeText}${totalText}${enclosingText}${truncatedText}`;
518
- }
519
- if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
520
- const stdout = trimInline(obj.stdout || '', 96);
521
- const stderr = trimInline(obj.stderr || '', 96);
522
- const command = trimInline(obj.command || '', 72);
523
- const lead = command ? `${command} -> ` : '';
524
- if (stdout) return `${lead}exit ${obj.code ?? 0}\nstdout: ${stdout}`;
525
- if (stderr) return `${lead}exit ${obj.code ?? 0}\nstderr: ${stderr}`;
526
- return `${lead}exit ${obj.code ?? 0}`;
527
- }
528
- if ('task_id' in obj && 'startup_confirmed' in obj) {
529
- const status = trimInline(obj.status || 'unknown', 32);
530
- const taskId = trimInline(obj.task_id || '', 24);
531
- const source = trimInline(obj.startup_source || '', 24);
532
- const outputFile = trimInline(obj.output_file || '', 72);
533
- const output = Array.isArray(obj.recent_output) ? trimInline(obj.recent_output.slice(-1)[0] || '', 96) : '';
534
- return `${taskId || 'task'} ${status}${source ? ` (${source})` : ''}${outputFile ? ` -> ${outputFile}` : ''}${output ? `\n${output}` : ''}`;
535
- }
536
- if ('tasks' in obj && Array.isArray(obj.tasks)) {
537
- const count = obj.tasks.length;
538
- const first = obj.tasks[0];
539
- const lead = first?.task_id ? `${trimInline(first.task_id, 24)} ${trimInline(first.status || 'unknown', 24)}` : '';
540
- return `tasks(${count})${lead ? `\n${lead}` : ''}`;
541
- }
542
- if ('files' in obj && Array.isArray(obj.files)) {
543
- return `patched ${obj.files.length} file(s)`;
544
- }
545
- if ('diff' in obj && 'new_hash' in obj && 'path' in obj) {
546
- const p = String(obj.path || '');
547
- return p ? `diff preview for ${p}` : 'diff preview';
548
- }
549
- if ('created' in obj && Array.isArray(obj.created)) {
550
- return `created ${obj.created.length} task(s)`;
551
- }
552
- if ('tasks' in obj && Array.isArray(obj.tasks)) {
553
- return `${obj.tasks.length} task(s)`;
554
- }
555
- if ('newTodos' in obj && Array.isArray(obj.newTodos)) {
556
- return obj.newTodos.length > 0 ? `updated ${obj.newTodos.length} todo item(s)` : 'cleared todo list';
557
- }
558
- if ('newPlan' in obj) {
559
- return obj.newPlan ? `updated plan state (${String(obj.newPlan.status || 'draft')})` : 'cleared plan state';
560
- }
561
- const keys = Object.keys(obj);
562
- return keys.length > 0 ? `keys: ${keys.slice(0, 5).join(',')}` : 'object';
563
- }
564
- return String(result);
565
- }
566
-
567
266
  export const trimInline = _trimInline;
568
267
 
569
268
  function normalizeAssistantText(value) {
@@ -750,6 +449,14 @@ function formatToolDisplayName(name, args) {
750
449
  const command = trimInline(args?.command || '', 96);
751
450
  return command ? `run(${command})` : name;
752
451
  }
452
+ if (name === 'web_fetch') {
453
+ const url = trimInline(args?.url || args?.href || '', 96);
454
+ return url ? `web_fetch(${url})` : name;
455
+ }
456
+ if (name === 'web_search') {
457
+ const query = trimInline(args?.query || args?.q || '', 96);
458
+ return query ? `web_search(${query})` : name;
459
+ }
753
460
  if (name === 'edit') {
754
461
  const target = trimInline(args?.path || args?.file || '.', 96) || '.';
755
462
  return `edit(${target})`;
@@ -1099,7 +806,7 @@ export async function runAgentLoop({
1099
806
  onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: trimInline(message, 120) });
1100
807
  }
1101
808
  if (shouldAutoCaptureError(toolName, message)) {
1102
- fireAndForgetCapture(toolName, message, effectiveArgs);
809
+ await captureToolFailure(toolName, message, effectiveArgs, config).catch(() => {});
1103
810
  }
1104
811
  return {
1105
812
  callId: call.id,
@@ -1122,13 +829,13 @@ export async function runAgentLoop({
1122
829
  if (typeof exitCode === 'number' && exitCode !== 0 && stderr) {
1123
830
  const failMsg = `exit ${exitCode}: ${stderr.slice(0, 120)}`;
1124
831
  if (shouldAutoCaptureError(toolName, failMsg)) {
1125
- fireAndForgetCapture(toolName, failMsg, effectiveArgs);
832
+ await captureToolFailure(toolName, failMsg, effectiveArgs, config).catch(() => {});
1126
833
  }
1127
834
  }
1128
835
  if (toolResult.error) {
1129
836
  const errMsg = String(toolResult.error).slice(0, 120);
1130
837
  if (shouldAutoCaptureError(toolName, errMsg)) {
1131
- fireAndForgetCapture(toolName, errMsg, effectiveArgs);
838
+ await captureToolFailure(toolName, errMsg, effectiveArgs, config).catch(() => {});
1132
839
  }
1133
840
  }
1134
841
  }