codemini-cli 0.3.9 → 0.4.0

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/src/core/tools.js CHANGED
@@ -23,6 +23,12 @@ import { runDreamConsolidation } from './dream-consolidate.js';
23
23
  import { normalizePlanState } from './plan-state.js';
24
24
  import { normalizeTodos } from './todo-state.js';
25
25
  import { createFffAdapter } from './fff-adapter.js';
26
+ import {
27
+ getToolOutputSanitizeOptions,
28
+ sanitizePreviewLines,
29
+ sanitizeTextForModel,
30
+ summarizeRunOutput
31
+ } from './tool-output.js';
26
32
  const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
27
33
  const BACKGROUND_TASK_POLL_MS = 150;
28
34
  const MAX_AST_ENCLOSING_BYTES = 300_000;
@@ -333,11 +339,10 @@ async function webFetchPage(args = {}) {
333
339
  const html = await page.content();
334
340
  const $ = cheerio.load(html);
335
341
  const bodyText = $('body').text() || $.root().text();
336
- const text = normalizeWhitespace(bodyText);
342
+ const text = String(bodyText || '').replace(/[^\S\n]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
337
343
  const title = trimPreview($('title').first().text() || (await page.title()), 240);
338
344
  const description = extractHtmlMeta($, 'description') || extractHtmlMeta($, 'og:description');
339
345
  const links = collectPageLinks($, finalUrl, maxLinks);
340
- const htmlExcerpt = html.length > 4000 ? `${html.slice(0, 4000)}...` : html;
341
346
 
342
347
  return {
343
348
  url,
@@ -345,7 +350,6 @@ async function webFetchPage(args = {}) {
345
350
  title,
346
351
  description,
347
352
  text,
348
- html_excerpt: htmlExcerpt,
349
353
  links,
350
354
  metadata: {
351
355
  status: response?.status?.() ?? null,
@@ -747,40 +751,6 @@ function findEnclosingSymbolLine(lines, anchorLine) {
747
751
  return 0;
748
752
  }
749
753
 
750
- function buildUnifiedDiff(oldContent, newContent, filePath = 'file') {
751
- const oldLines = splitLines(oldContent);
752
- const newLines = splitLines(newContent);
753
- let prefix = 0;
754
- while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
755
- prefix += 1;
756
- }
757
-
758
- let suffix = 0;
759
- while (
760
- suffix < oldLines.length - prefix &&
761
- suffix < newLines.length - prefix &&
762
- oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
763
- ) {
764
- suffix += 1;
765
- }
766
-
767
- const oldChanged = oldLines.slice(prefix, oldLines.length - suffix);
768
- const newChanged = newLines.slice(prefix, newLines.length - suffix);
769
- const oldStart = prefix + 1;
770
- const newStart = prefix + 1;
771
- const oldCount = Math.max(1, oldChanged.length);
772
- const newCount = Math.max(1, newChanged.length);
773
-
774
- const body = [
775
- `--- ${filePath}`,
776
- `+++ ${filePath}`,
777
- `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`,
778
- ...oldChanged.map((line) => `-${line}`),
779
- ...newChanged.map((line) => `+${line}`)
780
- ];
781
- return body.join('\n');
782
- }
783
-
784
754
  async function getFileState(root, relativePath) {
785
755
  const target = await resolveInWorkspace(root, relativePath);
786
756
  const stat = await fs.stat(target);
@@ -924,12 +894,15 @@ async function writeFile(root, args) {
924
894
  }
925
895
  const previewStart = Math.max(0, (changeLine || 1) - 1);
926
896
  const previewLines = afterLines.slice(previewStart, previewStart + 6);
897
+ const changed = countChangedLines(before, after);
927
898
  return {
928
899
  ok: true,
929
900
  path: rawPath,
930
901
  action: normalizedArgs?.append ? 'append' : existed ? 'overwrite' : 'create',
931
902
  changed_line: changeLine || Math.max(1, afterLines.length),
932
- diff_preview: previewLines.map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n')
903
+ diff_preview: previewLines.map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n'),
904
+ lines_added: changed.added,
905
+ lines_removed: changed.removed
933
906
  };
934
907
  }
935
908
 
@@ -1045,10 +1018,9 @@ function shellCommandForBackgroundTask(command, shellSpec) {
1045
1018
  }
1046
1019
 
1047
1020
  function appendRecentOutput(task, chunk) {
1048
- const lines = String(chunk || '')
1049
- .split(/\r?\n/)
1050
- .map((line) => trimLinePreview(line, 220))
1051
- .filter(Boolean);
1021
+ const lines = sanitizePreviewLines(chunk, { maxLineLength: 220 }).map((line) =>
1022
+ trimLinePreview(line, 220)
1023
+ );
1052
1024
  if (lines.length === 0) return;
1053
1025
  for (const line of lines) {
1054
1026
  backgroundTaskLogCursorCounter += 1;
@@ -1499,18 +1471,47 @@ async function validateEdit(root, args) {
1499
1471
  throw new Error(`validate_edit does not support kind: ${kind}`);
1500
1472
  }
1501
1473
 
1474
+ function countChangedLines(beforeContent, afterContent) {
1475
+ const before = splitLines(beforeContent);
1476
+ const after = splitLines(afterContent);
1477
+ const m = before.length;
1478
+ const n = after.length;
1479
+ // LCS via rolling DP — O(m*n) time, O(min(m,n)) space
1480
+ const short = m <= n ? before : after;
1481
+ const long = m <= n ? after : before;
1482
+ const shortLen = short.length;
1483
+ const longLen = long.length;
1484
+ let prev = new Array(longLen + 1).fill(0);
1485
+ let curr = new Array(longLen + 1).fill(0);
1486
+ for (let i = 1; i <= shortLen; i++) {
1487
+ for (let j = 1; j <= longLen; j++) {
1488
+ if (short[i - 1] === long[j - 1]) {
1489
+ curr[j] = prev[j - 1] + 1;
1490
+ } else {
1491
+ curr[j] = Math.max(prev[j], curr[j - 1]);
1492
+ }
1493
+ }
1494
+ [prev, curr] = [curr, prev];
1495
+ curr.fill(0);
1496
+ }
1497
+ const lcsLen = prev[longLen];
1498
+ return { added: n - lcsLen, removed: m - lcsLen };
1499
+ }
1500
+
1502
1501
  function editResult(pathText, action, beforeContent, afterContent, changedLine = 1) {
1503
1502
  const afterLines = splitLines(afterContent);
1504
1503
  const previewStart = Math.max(0, changedLine - 1);
1505
1504
  const diffPreview = afterLines.slice(previewStart, previewStart + 6).map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n');
1505
+ const changed = countChangedLines(beforeContent, afterContent);
1506
1506
  return {
1507
1507
  ok: true,
1508
1508
  path: pathText,
1509
1509
  action,
1510
1510
  changed_line: changedLine,
1511
1511
  diff_preview: diffPreview,
1512
- diff: buildUnifiedDiff(beforeContent, afterContent, pathText),
1513
- new_hash: sha256(afterContent)
1512
+ new_hash: sha256(afterContent),
1513
+ lines_added: changed.added,
1514
+ lines_removed: changed.removed
1514
1515
  };
1515
1516
  }
1516
1517
 
@@ -1767,7 +1768,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1767
1768
  };
1768
1769
  const ensureProjectIndex = async () => {
1769
1770
  const eventId = `project-index:${Date.now()}`;
1770
- const name = 'project_index(.codemini-project/project-map.json,.codemini-project/file-index.json)';
1771
+ const name = 'project_index(.codemini/project-map.json,.codemini/file-index.json)';
1771
1772
  try {
1772
1773
  const result = await initializeProjectIndex(workspaceRoot);
1773
1774
  if (result?.skipped || !result?.summary) {
@@ -1799,7 +1800,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1799
1800
  type: 'system_tool:end',
1800
1801
  id: eventId,
1801
1802
  name,
1802
- summary: result?.summary || `updated .codemini-project for ${relativePath}`
1803
+ summary: result?.summary || `updated .codemini for ${relativePath}`
1803
1804
  });
1804
1805
  return result;
1805
1806
  } catch (error) {
@@ -2210,52 +2211,23 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2210
2211
  }
2211
2212
  }
2212
2213
  },
2213
- remember_user: {
2214
+ save_memory: {
2214
2215
  type: 'function',
2215
2216
  function: {
2216
- name: 'remember_user',
2217
- description: 'Store a durable user preference, communication habit, or long-term instruction for future sessions. Use this for things like reply style, language, explanation depth, or stable guardrails. Never store secrets, tokens, passwords, or one-off task details.',
2218
- parameters: {
2219
- type: 'object',
2220
- properties: {
2221
- content: { type: 'string', description: 'Stable preference or instruction to remember' },
2222
- kind: { type: 'string', description: 'preference, workflow, constraint, or warning' },
2223
- summary: { type: 'string', description: 'Short summary for the memory index' },
2224
- replace_similar: { type: 'boolean', description: 'Replace an existing similar memory when true' }
2225
- },
2226
- required: ['content']
2227
- }
2228
- }
2229
- },
2230
- remember_global: {
2231
- type: 'function',
2232
- function: {
2233
- name: 'remember_global',
2234
- description: 'Store a durable cross-project workflow, environment fact, or generally reusable lesson that can help across many repositories. Use this for stable habits like preferred search tools or repeatable debugging workflow. Never store secrets.',
2235
- parameters: {
2236
- type: 'object',
2237
- properties: {
2238
- content: { type: 'string' },
2239
- kind: { type: 'string' },
2240
- summary: { type: 'string' },
2241
- replace_similar: { type: 'boolean' }
2242
- },
2243
- required: ['content']
2244
- }
2245
- }
2246
- },
2247
- remember_project: {
2248
- type: 'function',
2249
- function: {
2250
- name: 'remember_project',
2251
- description: 'Store a durable project-specific convention, architecture note, key module warning, or local workflow expectation. Use this for repository-specific rules, important files, testing conventions, or architectural boundaries. Never store secrets or transient task state.',
2217
+ name: 'save_memory',
2218
+ description:
2219
+ 'Save a durable observation or knowledge to persistent memory. Use this when you notice a reusable pattern, a user correction, a stable preference, a project convention, or a workflow insight. Do NOT use for casual chatter, trivial typos, one-off noise, or secrets. The memory is saved immediately and available in future sessions.',
2252
2220
  parameters: {
2253
2221
  type: 'object',
2254
2222
  properties: {
2255
- content: { type: 'string' },
2256
- kind: { type: 'string' },
2257
- summary: { type: 'string' },
2258
- replace_similar: { type: 'boolean' }
2223
+ content: { type: 'string', description: 'The knowledge or observation to remember' },
2224
+ summary: { type: 'string', description: 'Short summary for the memory index (under 80 chars)' },
2225
+ scope: {
2226
+ type: 'string',
2227
+ description: 'Where to store this memory. "user" = personal preferences (language, style, interaction habits). "global" = cross-project knowledge useful in ANY repository (environment quirks, general workflows, tool tips). "project" = specific to THIS repository only (architecture conventions, local config, test commands, file locations). Default: "global".'
2228
+ },
2229
+ kind: { type: 'string', description: 'Memory kind: preference, pattern, correction, observation, decision, failure, win, gap, convention. Default: observation' },
2230
+ replace_similar: { type: 'boolean', description: 'Replace an existing similar memory when true. Default: true.' }
2259
2231
  },
2260
2232
  required: ['content']
2261
2233
  }
@@ -2305,26 +2277,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2305
2277
  }
2306
2278
  }
2307
2279
  },
2308
- capture_memory: {
2309
- type: 'function',
2310
- function: {
2311
- name: 'capture_memory',
2312
- description:
2313
- 'Capture a high-signal observation into the dream loop inbox during active work. Use this when you notice a reusable pattern, a user correction, a repeated failure with stable fix, a stable preference, or a workflow win. Do NOT use for casual chatter, trivial typos, or one-off noise. This is lightweight — entries land in inbox and are consolidated later.',
2314
- parameters: {
2315
- type: 'object',
2316
- properties: {
2317
- summary: { type: 'string', description: 'Short high-signal summary of the observation' },
2318
- details: { type: 'string', description: 'Detailed explanation of the observation' },
2319
- scope: { type: 'string', description: 'Scope: global, repo, or thread. Default: global' },
2320
- type: { type: 'string', description: 'Observation type: correction, failure, preference, pattern, win, gap, decision. Default: observation' },
2321
- tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags for categorization' },
2322
- suggested_action: { type: 'string', description: 'Optional suggested follow-up action' }
2323
- },
2324
- required: ['summary']
2325
- }
2326
- }
2327
- },
2328
2280
  dream_consolidate: {
2329
2281
  type: 'function',
2330
2282
  function: {
@@ -2584,42 +2536,32 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2584
2536
  hasPendingApproval: nextPlan?.status === 'pending_approval'
2585
2537
  };
2586
2538
  },
2587
- run: (args) => runCommand(workspaceRoot, config, args),
2588
- remember_user: async (args = {}) => {
2589
- const saved = await rememberMemory({
2590
- scope: 'user',
2591
- content: args.content,
2592
- kind: args.kind,
2593
- summary: args.summary,
2594
- replaceSimilar: args.replace_similar !== false,
2595
- workspaceRoot,
2596
- config
2597
- });
2598
- return { ok: true, scope: 'user', memory: saved };
2599
- },
2600
- remember_global: async (args = {}) => {
2601
- const saved = await rememberMemory({
2602
- scope: 'global',
2603
- content: args.content,
2604
- kind: args.kind,
2605
- summary: args.summary,
2606
- replaceSimilar: args.replace_similar !== false,
2607
- workspaceRoot,
2608
- config
2609
- });
2610
- return { ok: true, scope: 'global', memory: saved };
2611
- },
2612
- remember_project: async (args = {}) => {
2539
+ run: Object.assign(
2540
+ (args) => runCommand(workspaceRoot, config, args),
2541
+ {
2542
+ prepareApproval: async (args) => ({
2543
+ command: args?.command || '',
2544
+ risk: args?._risk || 'high',
2545
+ evaluation: args?._evaluation || null
2546
+ })
2547
+ }
2548
+ ),
2549
+ save_memory: async (args = {}) => {
2550
+ const rawScope = String(args.scope || 'global').toLowerCase();
2551
+ const memoryScope = rawScope === 'repo' || rawScope === 'project' ? 'project'
2552
+ : rawScope === 'user' ? 'user'
2553
+ : 'global';
2613
2554
  const saved = await rememberMemory({
2614
- scope: 'project',
2555
+ scope: memoryScope,
2615
2556
  content: args.content,
2616
- kind: args.kind,
2617
- summary: args.summary,
2557
+ kind: args.kind || 'observation',
2558
+ summary: args.summary || String(args.content || '').slice(0, 80),
2559
+ source: 'tool',
2618
2560
  replaceSimilar: args.replace_similar !== false,
2619
2561
  workspaceRoot,
2620
2562
  config
2621
2563
  });
2622
- return { ok: true, scope: 'project', memory: saved };
2564
+ return { ok: true, scope: memoryScope, memory: saved };
2623
2565
  },
2624
2566
  list_memory: async (args = {}) => ({
2625
2567
  scope: String(args.scope || ''),
@@ -2634,18 +2576,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2634
2576
  ok: true,
2635
2577
  ...(await forgetMemory({ scope: args.scope, id: args.id, workspaceRoot }))
2636
2578
  }),
2637
- capture_memory: async (args = {}) => {
2638
- const entry = await captureToInbox({
2639
- scope: args.scope,
2640
- type: args.type,
2641
- summary: args.summary,
2642
- details: args.details,
2643
- suggestedAction: args.suggested_action,
2644
- tags: args.tags,
2645
- source: 'tool'
2646
- });
2647
- return { ok: true, captured: entry };
2648
- },
2649
2579
  dream_consolidate: async (args = {}) => {
2650
2580
  return runDreamConsolidation({
2651
2581
  dryRun: args.dry_run === true,
@@ -2681,7 +2611,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2681
2611
  }
2682
2612
  };
2683
2613
 
2684
- const formatters = {
2614
+ const rawFormatters = {
2685
2615
  read(result) {
2686
2616
  if (typeof result === 'string') return result;
2687
2617
  if (!result || typeof result !== 'object') return String(result);
@@ -2880,9 +2810,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2880
2810
  }
2881
2811
  return parts.join('\n');
2882
2812
  }
2813
+ const runSummary = summarizeRunOutput(result);
2814
+ if (runSummary) return runSummary;
2883
2815
  const command = String(result.command || '').slice(0, 200);
2884
- const stdout = String(result.stdout || '').slice(0, 500);
2885
- const stderr = String(result.stderr || '').slice(0, 500);
2816
+ const stdout = String(result.stdout || '');
2817
+ const stderr = String(result.stderr || '');
2886
2818
  const code = result.code ?? 0;
2887
2819
  const parts = [`[exit: ${code}]`];
2888
2820
  if (command) parts.push(`command: ${command}`);
@@ -2903,6 +2835,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2903
2835
  return result?.memory?.content ? `stored project memory: ${result.memory.content}` : JSON.stringify(result);
2904
2836
  },
2905
2837
 
2838
+ save_memory(result) {
2839
+ const scope = result?.scope || 'global';
2840
+ return result?.memory?.content ? `stored ${scope} memory: ${result.memory.content}` : JSON.stringify(result);
2841
+ },
2842
+
2906
2843
  list_memory(result) {
2907
2844
  if (!result || typeof result !== 'object' || !Array.isArray(result.items)) return JSON.stringify(result);
2908
2845
  if (result.items.length === 0) return `No ${result.scope || ''} memories found.`;
@@ -2938,8 +2875,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2938
2875
  const kind = result.kind || '';
2939
2876
  const content = result.content || result.source || '';
2940
2877
  const header = `${kind} ${name}`;
2941
- if (typeof content !== 'string' || content.length <= 2000) return `${header}\n${content}`;
2942
- return `${header}\n${content.slice(0, 1200)}\n... [omitted ${content.length - 1600} chars] ...\n${content.slice(-400)}`;
2878
+ return `${header}\n${content}`;
2943
2879
  },
2944
2880
 
2945
2881
  web_fetch(result) {
@@ -2951,7 +2887,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2951
2887
  if (Array.isArray(result.links) && result.links.length > 0) {
2952
2888
  lines.push(`links: ${result.links.slice(0, 5).map((item) => item.href).join(', ')}`);
2953
2889
  }
2954
- if (result.text) lines.push(trimPreview(result.text, 1200));
2890
+ if (result.text) {
2891
+ lines.push(result.text);
2892
+ }
2955
2893
  return lines.join('\n');
2956
2894
  },
2957
2895
 
@@ -2992,6 +2930,13 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2992
2930
  }
2993
2931
  };
2994
2932
 
2933
+ const formatters = Object.fromEntries(
2934
+ Object.entries(rawFormatters).map(([name, formatter]) => [
2935
+ name,
2936
+ (result, args) => sanitizeTextForModel(formatter(result, args), getToolOutputSanitizeOptions(name))
2937
+ ])
2938
+ );
2939
+
2995
2940
  async function dispose() {
2996
2941
  if (activeFffAdapter?.dispose) {
2997
2942
  try {