codemini-cli 0.4.1 → 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,12 +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';
9
6
  import { normalizeToolArguments } from './tool-args.js';
7
+ import { storeResultIfNeeded, summarizeToolResult } from './tool-result-store.js';
10
8
 
11
9
  /**
12
10
  * 安全解析 JSON 字符串。
@@ -162,107 +160,6 @@ function compactToolResult(result, toolName, args, maxChars = 12000) {
162
160
  return clipToolResult(obj, Math.min(maxChars, 4000));
163
161
  }
164
162
 
165
- // ─── P0: Large result disk store ─────────────────────────────────────
166
-
167
- const TOOL_RESULT_DISK_THRESHOLD = 6000;
168
- const PREVIEW_SIZE_BYTES = 2000;
169
- const TOOL_RESULTS_SUBDIR = 'tool-results';
170
-
171
- let currentResultDir = null;
172
- let resultDirReady = false;
173
- const storedResults = new BoundedCache({
174
- maxSize: 64,
175
- ttlMs: 30 * 60 * 1000,
176
- onEvict(key, value) {
177
- if (value?.filePath) {
178
- fs.unlink(value.filePath).catch(() => {});
179
- }
180
- }
181
- }); // callId -> { filePath, summary }
182
- const readCache = new BoundedCache({ maxSize: 128, ttlMs: 10 * 60 * 1000 }); // "path:startLine:endLine:mtimeMs" -> true
183
-
184
- function generatePreview(content) {
185
- if (content.length <= PREVIEW_SIZE_BYTES) {
186
- return { preview: content, hasMore: false };
187
- }
188
- const truncated = content.slice(0, PREVIEW_SIZE_BYTES);
189
- const lastNewline = truncated.lastIndexOf('\n');
190
- const cutPoint = lastNewline > PREVIEW_SIZE_BYTES * 0.5 ? lastNewline : PREVIEW_SIZE_BYTES;
191
- return { preview: content.slice(0, cutPoint), hasMore: true };
192
- }
193
-
194
- function formatFileSize(chars) {
195
- if (chars < 1024) return `${chars} B`;
196
- return `${(chars / 1024).toFixed(1)} KB`;
197
- }
198
-
199
- export function setResultDir(dir) {
200
- currentResultDir = dir ? path.join(dir, TOOL_RESULTS_SUBDIR) : null;
201
- resultDirReady = false;
202
- }
203
-
204
- async function ensureResultDir() {
205
- if (!currentResultDir) return false;
206
- if (!resultDirReady) {
207
- await fs.mkdir(currentResultDir, { recursive: true });
208
- resultDirReady = true;
209
- }
210
- return true;
211
- }
212
-
213
- async function storeResultIfNeeded(callId, formattedContent, rawResult) {
214
- if (formattedContent.length <= TOOL_RESULT_DISK_THRESHOLD) {
215
- return formattedContent;
216
- }
217
- try {
218
- const ready = await ensureResultDir();
219
- const dir = ready ? currentResultDir : path.join(os.tmpdir(), 'codemini-results');
220
- if (!resultDirReady && dir === currentResultDir) {
221
- await fs.mkdir(dir, { recursive: true });
222
- } else if (!resultDirReady) {
223
- await fs.mkdir(dir, { recursive: true });
224
- }
225
- const filePath = path.join(dir, `${callId}.txt`);
226
- const payload = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
227
- await fs.writeFile(filePath, payload, 'utf-8');
228
- const summary = summarizeToolResult(rawResult);
229
- const { preview, hasMore } = generatePreview(payload);
230
- storedResults.set(callId, { filePath, summary });
231
-
232
- return `<persisted-output>
233
- Output too large (${formatFileSize(payload.length)}). Full output saved to: ${filePath}
234
-
235
- Preview (first ${formatFileSize(PREVIEW_SIZE_BYTES)}):
236
- ${preview}${hasMore ? '\n...' : ''}
237
-
238
- Summary: ${summary}
239
- </persisted-output>`;
240
- } catch {
241
- return formattedContent;
242
- }
243
- }
244
-
245
- export function clearResultStore() {
246
- const files = [];
247
- for (const [, val] of storedResults.entries()) {
248
- files.push(val.filePath);
249
- }
250
- storedResults.clear();
251
- readCache.clear();
252
- return Promise.allSettled(files.map((f) => fs.unlink(f).catch(() => {})));
253
- }
254
-
255
- // ─── Read deduplication ─────────────────────────────────────────────
256
-
257
- export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
258
- const key = `${filePath}:${startLine || 0}:${endLine || 0}:${mtimeMs}`;
259
- if (readCache.has(key)) {
260
- return true;
261
- }
262
- readCache.set(key, true);
263
- return false;
264
- }
265
-
266
163
  // ─── P1a: Read-only tool classification ──────────────────────────────
267
164
 
268
165
  const READ_ONLY_TOOLS = new Set([
@@ -366,108 +263,6 @@ function extractFileChange(toolName, result) {
366
263
  return null;
367
264
  }
368
265
 
369
- export function summarizeToolResult(result) {
370
- if (result === null || result === undefined) return 'no output';
371
- if (typeof result === 'string') {
372
- const oneLine = result.replace(/\s+/g, ' ').trim();
373
- return oneLine.length > 90 ? `${oneLine.slice(0, 87)}...` : oneLine || 'empty string';
374
- }
375
- if (typeof result === 'object') {
376
- const obj = result;
377
- if (Array.isArray(obj)) return `array(${obj.length})`;
378
- if ('deleted' in obj && 'path' in obj) {
379
- const kind = trimInline(obj.type || 'item', 16);
380
- const target = trimInline(obj.path || '', 96);
381
- if (obj.deleted) return target ? `deleted ${kind} ${target}` : `deleted ${kind}`;
382
- if (obj.cancelled) return target ? `cancelled delete ${target}` : 'cancelled delete';
383
- }
384
- if ('path' in obj && 'action' in obj) {
385
- const p = String(obj.path || '');
386
- const action = String(obj.action || 'write');
387
- const line = Number(obj.changed_line || 1);
388
- const suffix =
389
- action === 'delete'
390
- ? 'deleted'
391
- : action === 'create'
392
- ? 'created'
393
- : action === 'patch'
394
- ? 'patched'
395
- : action === 'replace_block' || action === 'replace_text'
396
- ? 'edited'
397
- : action === 'append'
398
- ? 'appended'
399
- : 'updated';
400
- return p ? `${suffix} ${p}${line > 0 ? ` @L${line}` : ''}` : suffix;
401
- }
402
- if ('path' in obj && 'phase' in obj) {
403
- const phase = String(obj.phase || '');
404
- const p = String(obj.path || '');
405
- const total = Number(obj.total_lines);
406
- const start =
407
- Number(obj.suggested_start_line || obj.start_line) > 0
408
- ? Number(obj.suggested_start_line || obj.start_line)
409
- : 1;
410
- const end =
411
- Number(obj.suggested_end_line || obj.end_line) >= start
412
- ? Number(obj.suggested_end_line || obj.end_line)
413
- : start;
414
- const rangeText = start > 0 && end >= start ? ` lines ${start}-${end}` : '';
415
- const totalText = total > 0 ? ` of ${total}` : '';
416
- const enclosingText = obj.enclosing_symbol ? ` in ${obj.enclosing_symbol}` : '';
417
- const errorText = obj.error ? ` (${trimInline(obj.error, 64)})` : '';
418
- const truncatedText = obj.truncated ? ' [truncated]' : '';
419
- return phase === 'metadata'
420
- ? `metadata for ${p}${rangeText}${totalText}${errorText}`
421
- : `content from ${p}${rangeText}${totalText}${enclosingText}${truncatedText}`;
422
- }
423
- if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
424
- const stdout = trimInline(obj.stdout || '', 96);
425
- const stderr = trimInline(obj.stderr || '', 96);
426
- const command = trimInline(obj.command || '', 72);
427
- const lead = command ? `${command} -> ` : '';
428
- if (stdout) return `${lead}exit ${obj.code ?? 0}\nstdout: ${stdout}`;
429
- if (stderr) return `${lead}exit ${obj.code ?? 0}\nstderr: ${stderr}`;
430
- return `${lead}exit ${obj.code ?? 0}`;
431
- }
432
- if ('task_id' in obj && 'startup_confirmed' in obj) {
433
- const status = trimInline(obj.status || 'unknown', 32);
434
- const taskId = trimInline(obj.task_id || '', 24);
435
- const source = trimInline(obj.startup_source || '', 24);
436
- const outputFile = trimInline(obj.output_file || '', 72);
437
- const output = Array.isArray(obj.recent_output) ? trimInline(obj.recent_output.slice(-1)[0] || '', 96) : '';
438
- return `${taskId || 'task'} ${status}${source ? ` (${source})` : ''}${outputFile ? ` -> ${outputFile}` : ''}${output ? `\n${output}` : ''}`;
439
- }
440
- if ('tasks' in obj && Array.isArray(obj.tasks)) {
441
- const count = obj.tasks.length;
442
- const first = obj.tasks[0];
443
- const lead = first?.task_id ? `${trimInline(first.task_id, 24)} ${trimInline(first.status || 'unknown', 24)}` : '';
444
- return `tasks(${count})${lead ? `\n${lead}` : ''}`;
445
- }
446
- if ('files' in obj && Array.isArray(obj.files)) {
447
- return `patched ${obj.files.length} file(s)`;
448
- }
449
- if ('diff' in obj && 'new_hash' in obj && 'path' in obj) {
450
- const p = String(obj.path || '');
451
- return p ? `diff preview for ${p}` : 'diff preview';
452
- }
453
- if ('created' in obj && Array.isArray(obj.created)) {
454
- return `created ${obj.created.length} task(s)`;
455
- }
456
- if ('tasks' in obj && Array.isArray(obj.tasks)) {
457
- return `${obj.tasks.length} task(s)`;
458
- }
459
- if ('newTodos' in obj && Array.isArray(obj.newTodos)) {
460
- return obj.newTodos.length > 0 ? `updated ${obj.newTodos.length} todo item(s)` : 'cleared todo list';
461
- }
462
- if ('newPlan' in obj) {
463
- return obj.newPlan ? `updated plan state (${String(obj.newPlan.status || 'draft')})` : 'cleared plan state';
464
- }
465
- const keys = Object.keys(obj);
466
- return keys.length > 0 ? `keys: ${keys.slice(0, 5).join(',')}` : 'object';
467
- }
468
- return String(result);
469
- }
470
-
471
266
  export const trimInline = _trimInline;
472
267
 
473
268
  function normalizeAssistantText(value) {