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.
package/src/core/tools.js CHANGED
@@ -23,6 +23,20 @@ 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';
32
+ import {
33
+ normalizePathArgs,
34
+ normalizePatternArgs,
35
+ normalizeReadArgs,
36
+ normalizeWebFetchArgs,
37
+ normalizeWebSearchArgs,
38
+ normalizeWriteArgs
39
+ } from './tool-args.js';
26
40
  const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
27
41
  const BACKGROUND_TASK_POLL_MS = 150;
28
42
  const MAX_AST_ENCLOSING_BYTES = 300_000;
@@ -94,133 +108,6 @@ function splitLines(text) {
94
108
  return String(text || '').split('\n');
95
109
  }
96
110
 
97
- function parseInlineReadRange(value) {
98
- const text = String(value || '').trim();
99
- if (!text) return null;
100
- const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
101
- if (!match) return null;
102
- const [, maybePath, startRaw, endRaw] = match;
103
- if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
104
- const startLine = Number(startRaw);
105
- const endLine = Number(endRaw || startRaw);
106
- if (!Number.isFinite(startLine) || startLine <= 0) return null;
107
- if (!Number.isFinite(endLine) || endLine < startLine) return null;
108
- return {
109
- path: maybePath,
110
- start_line: startLine,
111
- end_line: endLine
112
- };
113
- }
114
-
115
- function normalizeReadArgs(rawArgs) {
116
- const source =
117
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
118
- ? { ...rawArgs }
119
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
120
-
121
- const normalized = { ...source };
122
- const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
123
- if (aliasPath) normalized.path = aliasPath;
124
-
125
- if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
126
- normalized.start_line = Number(source.offset);
127
- }
128
-
129
- if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
130
- const startLine = Number(normalized.start_line);
131
- const limit = Number(source.limit);
132
- if (startLine > 0 && limit > 0) {
133
- normalized.end_line = startLine + limit - 1;
134
- }
135
- }
136
-
137
- const inlineRange = parseInlineReadRange(normalized.path);
138
- if (inlineRange) {
139
- normalized.path = inlineRange.path;
140
- if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
141
- if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
142
- }
143
-
144
- return normalized;
145
- }
146
-
147
- function normalizePathArgs(rawArgs, aliases = []) {
148
- const source =
149
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
150
- ? { ...rawArgs }
151
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
152
- const normalized = { ...source };
153
- const keys = ['path', ...aliases];
154
- for (const key of keys) {
155
- const value = String(source?.[key] || '').trim();
156
- if (value) {
157
- normalized.path = value;
158
- break;
159
- }
160
- }
161
- return normalized;
162
- }
163
-
164
- function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
165
- const source =
166
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
167
- ? { ...rawArgs }
168
- : { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
169
- const normalized = { ...source };
170
- for (const key of ['pattern', ...aliases]) {
171
- const value = String(source?.[key] || '').trim();
172
- if (value) {
173
- normalized.pattern = value;
174
- break;
175
- }
176
- }
177
- for (const key of ['path', ...defaultPathAliases]) {
178
- const value = String(source?.[key] || '').trim();
179
- if (value) {
180
- normalized.path = value;
181
- break;
182
- }
183
- }
184
- return normalized;
185
- }
186
-
187
- function normalizeWriteArgs(rawArgs) {
188
- const source =
189
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
190
- ? { ...rawArgs }
191
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
192
- const normalized = { ...source };
193
- const filePath = String(source.path || source.file_path || source.file || '').trim();
194
- if (filePath) normalized.path = filePath;
195
- if (normalized.content == null) {
196
- if (source.text != null) normalized.content = source.text;
197
- if (source.new_content != null) normalized.content = source.new_content;
198
- }
199
- return normalized;
200
- }
201
-
202
- function normalizeWebFetchArgs(rawArgs) {
203
- const source =
204
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
205
- ? { ...rawArgs }
206
- : { url: typeof rawArgs === 'string' ? rawArgs : '' };
207
- const normalized = { ...source };
208
- const url = String(source.url || source.href || source.link || source.target || '').trim();
209
- if (url) normalized.url = url;
210
- return normalized;
211
- }
212
-
213
- function normalizeWebSearchArgs(rawArgs) {
214
- const source =
215
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
216
- ? { ...rawArgs }
217
- : { query: typeof rawArgs === 'string' ? rawArgs : '' };
218
- const normalized = { ...source };
219
- const query = String(source.query || source.q || source.keyword || '').trim();
220
- if (query) normalized.query = query;
221
- return normalized;
222
- }
223
-
224
111
  function clampNumber(value, min, max, fallback) {
225
112
  const num = Number(value);
226
113
  if (!Number.isFinite(num)) return fallback;
@@ -333,11 +220,10 @@ async function webFetchPage(args = {}) {
333
220
  const html = await page.content();
334
221
  const $ = cheerio.load(html);
335
222
  const bodyText = $('body').text() || $.root().text();
336
- const text = normalizeWhitespace(bodyText);
223
+ const text = String(bodyText || '').replace(/[^\S\n]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
337
224
  const title = trimPreview($('title').first().text() || (await page.title()), 240);
338
225
  const description = extractHtmlMeta($, 'description') || extractHtmlMeta($, 'og:description');
339
226
  const links = collectPageLinks($, finalUrl, maxLinks);
340
- const htmlExcerpt = html.length > 4000 ? `${html.slice(0, 4000)}...` : html;
341
227
 
342
228
  return {
343
229
  url,
@@ -345,7 +231,6 @@ async function webFetchPage(args = {}) {
345
231
  title,
346
232
  description,
347
233
  text,
348
- html_excerpt: htmlExcerpt,
349
234
  links,
350
235
  metadata: {
351
236
  status: response?.status?.() ?? null,
@@ -747,40 +632,6 @@ function findEnclosingSymbolLine(lines, anchorLine) {
747
632
  return 0;
748
633
  }
749
634
 
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
635
  async function getFileState(root, relativePath) {
785
636
  const target = await resolveInWorkspace(root, relativePath);
786
637
  const stat = await fs.stat(target);
@@ -924,12 +775,15 @@ async function writeFile(root, args) {
924
775
  }
925
776
  const previewStart = Math.max(0, (changeLine || 1) - 1);
926
777
  const previewLines = afterLines.slice(previewStart, previewStart + 6);
778
+ const changed = countChangedLines(before, after);
927
779
  return {
928
780
  ok: true,
929
781
  path: rawPath,
930
782
  action: normalizedArgs?.append ? 'append' : existed ? 'overwrite' : 'create',
931
783
  changed_line: changeLine || Math.max(1, afterLines.length),
932
- diff_preview: previewLines.map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n')
784
+ diff_preview: previewLines.map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n'),
785
+ lines_added: changed.added,
786
+ lines_removed: changed.removed
933
787
  };
934
788
  }
935
789
 
@@ -1045,10 +899,9 @@ function shellCommandForBackgroundTask(command, shellSpec) {
1045
899
  }
1046
900
 
1047
901
  function appendRecentOutput(task, chunk) {
1048
- const lines = String(chunk || '')
1049
- .split(/\r?\n/)
1050
- .map((line) => trimLinePreview(line, 220))
1051
- .filter(Boolean);
902
+ const lines = sanitizePreviewLines(chunk, { maxLineLength: 220 }).map((line) =>
903
+ trimLinePreview(line, 220)
904
+ );
1052
905
  if (lines.length === 0) return;
1053
906
  for (const line of lines) {
1054
907
  backgroundTaskLogCursorCounter += 1;
@@ -1499,18 +1352,47 @@ async function validateEdit(root, args) {
1499
1352
  throw new Error(`validate_edit does not support kind: ${kind}`);
1500
1353
  }
1501
1354
 
1355
+ function countChangedLines(beforeContent, afterContent) {
1356
+ const before = splitLines(beforeContent);
1357
+ const after = splitLines(afterContent);
1358
+ const m = before.length;
1359
+ const n = after.length;
1360
+ // LCS via rolling DP — O(m*n) time, O(min(m,n)) space
1361
+ const short = m <= n ? before : after;
1362
+ const long = m <= n ? after : before;
1363
+ const shortLen = short.length;
1364
+ const longLen = long.length;
1365
+ let prev = new Array(longLen + 1).fill(0);
1366
+ let curr = new Array(longLen + 1).fill(0);
1367
+ for (let i = 1; i <= shortLen; i++) {
1368
+ for (let j = 1; j <= longLen; j++) {
1369
+ if (short[i - 1] === long[j - 1]) {
1370
+ curr[j] = prev[j - 1] + 1;
1371
+ } else {
1372
+ curr[j] = Math.max(prev[j], curr[j - 1]);
1373
+ }
1374
+ }
1375
+ [prev, curr] = [curr, prev];
1376
+ curr.fill(0);
1377
+ }
1378
+ const lcsLen = prev[longLen];
1379
+ return { added: n - lcsLen, removed: m - lcsLen };
1380
+ }
1381
+
1502
1382
  function editResult(pathText, action, beforeContent, afterContent, changedLine = 1) {
1503
1383
  const afterLines = splitLines(afterContent);
1504
1384
  const previewStart = Math.max(0, changedLine - 1);
1505
1385
  const diffPreview = afterLines.slice(previewStart, previewStart + 6).map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n');
1386
+ const changed = countChangedLines(beforeContent, afterContent);
1506
1387
  return {
1507
1388
  ok: true,
1508
1389
  path: pathText,
1509
1390
  action,
1510
1391
  changed_line: changedLine,
1511
1392
  diff_preview: diffPreview,
1512
- diff: buildUnifiedDiff(beforeContent, afterContent, pathText),
1513
- new_hash: sha256(afterContent)
1393
+ new_hash: sha256(afterContent),
1394
+ lines_added: changed.added,
1395
+ lines_removed: changed.removed
1514
1396
  };
1515
1397
  }
1516
1398
 
@@ -1767,7 +1649,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1767
1649
  };
1768
1650
  const ensureProjectIndex = async () => {
1769
1651
  const eventId = `project-index:${Date.now()}`;
1770
- const name = 'project_index(.codemini-project/project-map.json,.codemini-project/file-index.json)';
1652
+ const name = 'project_index(.codemini/project-map.json,.codemini/file-index.json)';
1771
1653
  try {
1772
1654
  const result = await initializeProjectIndex(workspaceRoot);
1773
1655
  if (result?.skipped || !result?.summary) {
@@ -1799,7 +1681,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1799
1681
  type: 'system_tool:end',
1800
1682
  id: eventId,
1801
1683
  name,
1802
- summary: result?.summary || `updated .codemini-project for ${relativePath}`
1684
+ summary: result?.summary || `updated .codemini for ${relativePath}`
1803
1685
  });
1804
1686
  return result;
1805
1687
  } catch (error) {
@@ -1818,20 +1700,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1818
1700
  function: {
1819
1701
  name: 'read',
1820
1702
  description:
1821
- '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.',
1822
1704
  parameters: {
1823
1705
  type: 'object',
1824
1706
  properties: {
1825
1707
  path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
1826
- file_path: { type: 'string', description: 'Alias for path' },
1827
1708
  start_line: { type: 'number', description: '1-based start line' },
1828
1709
  end_line: { type: 'number', description: 'Inclusive end line' },
1829
- offset: { type: 'number', description: 'Alias for start_line' },
1830
- limit: { type: 'number', description: 'Number of lines to read starting from offset/start_line' },
1831
1710
  max_chars: { type: 'number', description: 'Max chars to return' },
1832
- include_content: { type: 'boolean', description: 'Legacy compatibility flag. Content is returned by default.' },
1833
- read_token: { type: 'string', description: 'Legacy compatibility token. No longer required for content reads.' },
1834
- metadata_only: { type: 'boolean', description: 'Set true to return metadata without content.' },
1835
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.' },
1836
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.' },
1837
1713
  capture_name: { type: 'string', description: 'Optional capture name to select when query is provided.' },
@@ -1846,14 +1722,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1846
1722
  function: {
1847
1723
  name: 'grep',
1848
1724
  description:
1849
- '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.',
1850
1726
  parameters: {
1851
1727
  type: 'object',
1852
1728
  properties: {
1853
1729
  pattern: { type: 'string', description: 'Search pattern' },
1854
- query: { type: 'string', description: 'Alias for pattern' },
1855
1730
  path: { type: 'string', description: 'Directory or file to search' },
1856
- directory: { type: 'string', description: 'Alias for path' },
1857
1731
  regex: { type: 'boolean', description: 'Treat pattern as regex' },
1858
1732
  case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
1859
1733
  max_results: { type: 'number', description: 'Max matches to return' },
@@ -1868,12 +1742,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1868
1742
  type: 'function',
1869
1743
  function: {
1870
1744
  name: 'list',
1871
- 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.',
1872
1746
  parameters: {
1873
1747
  type: 'object',
1874
1748
  properties: {
1875
1749
  path: { type: 'string', description: 'Directory path to list' },
1876
- directory: { type: 'string', description: 'Alias for path' },
1877
1750
  include_hidden: { type: 'boolean', description: 'Include dotfiles' }
1878
1751
  }
1879
1752
  }
@@ -1884,14 +1757,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1884
1757
  function: {
1885
1758
  name: 'glob',
1886
1759
  description:
1887
- '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.',
1888
1761
  parameters: {
1889
1762
  type: 'object',
1890
1763
  properties: {
1891
1764
  pattern: { type: 'string', description: 'Glob pattern' },
1892
1765
  path: { type: 'string', description: 'Directory to search' },
1893
- query: { type: 'string', description: 'Alias for pattern' },
1894
- directory: { type: 'string', description: 'Alias for path' },
1895
1766
  include_hidden: { type: 'boolean', description: 'Include dotfiles' },
1896
1767
  max_results: { type: 'number', description: 'Max results' }
1897
1768
  },
@@ -1922,18 +1793,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1922
1793
  function: {
1923
1794
  name: 'edit',
1924
1795
  description:
1925
- '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.',
1926
1797
  parameters: {
1927
1798
  type: 'object',
1928
1799
  properties: {
1929
- file: { type: 'string', description: 'File path to edit' },
1930
- path: { type: 'string', description: 'Alias for file' },
1931
- file_path: { type: 'string', description: 'Alias for file, compatible with simpler demo-style tool calls' },
1800
+ path: { type: 'string', description: 'File path to edit' },
1932
1801
  new_content: { type: 'string', description: 'Replacement content' },
1933
1802
  old_text: { type: 'string', description: 'Exact text to replace' },
1934
1803
  new_text: { type: 'string', description: 'Replacement text' },
1935
- old_string: { type: 'string', description: 'Alias for old_text' },
1936
- new_string: { type: 'string', description: 'Alias for new_text' },
1937
1804
  anchor_text: { type: 'string', description: 'Anchor text for inserts' },
1938
1805
  content: { type: 'string', description: 'Content to insert or append' },
1939
1806
  position: { type: 'string', description: 'before or after' },
@@ -1944,7 +1811,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1944
1811
  line: { type: 'number', description: 'Line to target' },
1945
1812
  edit: { type: 'object', description: 'Structured edit input' }
1946
1813
  },
1947
- required: ['file']
1814
+ required: ['path']
1948
1815
  }
1949
1816
  }
1950
1817
  },
@@ -1953,16 +1820,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1953
1820
  function: {
1954
1821
  name: 'write',
1955
1822
  description:
1956
- '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.',
1957
1824
  parameters: {
1958
1825
  type: 'object',
1959
1826
  properties: {
1960
1827
  path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
1961
- file_path: { type: 'string', description: 'Alias for path, compatible with simpler demo-style tool calls' },
1962
- file: { type: 'string', description: 'Alias for path' },
1963
1828
  content: { type: 'string', description: 'Content to write' },
1964
- text: { type: 'string', description: 'Alias for content' },
1965
- new_content: { type: 'string', description: 'Alias for content' },
1966
1829
  append: { type: 'boolean', description: 'Append instead of overwrite' },
1967
1830
  full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
1968
1831
  },
@@ -1975,15 +1838,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1975
1838
  function: {
1976
1839
  name: 'delete',
1977
1840
  description:
1978
- '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.',
1979
1842
  parameters: {
1980
1843
  type: 'object',
1981
1844
  properties: {
1982
- path: { type: 'string', description: 'File or directory path to delete' },
1983
- file: { type: 'string', description: 'Alias for path' },
1984
- file_path: { type: 'string', description: 'Alias for path' },
1985
- directory: { type: 'string', description: 'Alias for path' },
1986
- dir: { type: 'string', description: 'Alias for path' }
1845
+ path: { type: 'string', description: 'File or directory path to delete' }
1987
1846
  },
1988
1847
  required: ['path']
1989
1848
  }
@@ -2210,52 +2069,23 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2210
2069
  }
2211
2070
  }
2212
2071
  },
2213
- remember_user: {
2214
- type: 'function',
2215
- 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: {
2072
+ save_memory: {
2231
2073
  type: 'function',
2232
2074
  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.',
2075
+ name: 'save_memory',
2076
+ description:
2077
+ '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
2078
  parameters: {
2253
2079
  type: 'object',
2254
2080
  properties: {
2255
- content: { type: 'string' },
2256
- kind: { type: 'string' },
2257
- summary: { type: 'string' },
2258
- replace_similar: { type: 'boolean' }
2081
+ content: { type: 'string', description: 'The knowledge or observation to remember' },
2082
+ summary: { type: 'string', description: 'Short summary for the memory index (under 80 chars)' },
2083
+ scope: {
2084
+ type: 'string',
2085
+ 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".'
2086
+ },
2087
+ kind: { type: 'string', description: 'Memory kind: preference, pattern, correction, observation, decision, failure, win, gap, convention. Default: observation' },
2088
+ replace_similar: { type: 'boolean', description: 'Replace an existing similar memory when true. Default: true.' }
2259
2089
  },
2260
2090
  required: ['content']
2261
2091
  }
@@ -2305,26 +2135,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2305
2135
  }
2306
2136
  }
2307
2137
  },
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
2138
  dream_consolidate: {
2329
2139
  type: 'function',
2330
2140
  function: {
@@ -2584,42 +2394,32 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2584
2394
  hasPendingApproval: nextPlan?.status === 'pending_approval'
2585
2395
  };
2586
2396
  },
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 = {}) => {
2397
+ run: Object.assign(
2398
+ (args) => runCommand(workspaceRoot, config, args),
2399
+ {
2400
+ prepareApproval: async (args) => ({
2401
+ command: args?.command || '',
2402
+ risk: args?._risk || 'high',
2403
+ evaluation: args?._evaluation || null
2404
+ })
2405
+ }
2406
+ ),
2407
+ save_memory: async (args = {}) => {
2408
+ const rawScope = String(args.scope || 'global').toLowerCase();
2409
+ const memoryScope = rawScope === 'repo' || rawScope === 'project' ? 'project'
2410
+ : rawScope === 'user' ? 'user'
2411
+ : 'global';
2613
2412
  const saved = await rememberMemory({
2614
- scope: 'project',
2413
+ scope: memoryScope,
2615
2414
  content: args.content,
2616
- kind: args.kind,
2617
- summary: args.summary,
2415
+ kind: args.kind || 'observation',
2416
+ summary: args.summary || String(args.content || '').slice(0, 80),
2417
+ source: 'tool',
2618
2418
  replaceSimilar: args.replace_similar !== false,
2619
2419
  workspaceRoot,
2620
2420
  config
2621
2421
  });
2622
- return { ok: true, scope: 'project', memory: saved };
2422
+ return { ok: true, scope: memoryScope, memory: saved };
2623
2423
  },
2624
2424
  list_memory: async (args = {}) => ({
2625
2425
  scope: String(args.scope || ''),
@@ -2634,18 +2434,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2634
2434
  ok: true,
2635
2435
  ...(await forgetMemory({ scope: args.scope, id: args.id, workspaceRoot }))
2636
2436
  }),
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
2437
  dream_consolidate: async (args = {}) => {
2650
2438
  return runDreamConsolidation({
2651
2439
  dryRun: args.dry_run === true,
@@ -2681,7 +2469,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2681
2469
  }
2682
2470
  };
2683
2471
 
2684
- const formatters = {
2472
+ const rawFormatters = {
2685
2473
  read(result) {
2686
2474
  if (typeof result === 'string') return result;
2687
2475
  if (!result || typeof result !== 'object') return String(result);
@@ -2880,9 +2668,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2880
2668
  }
2881
2669
  return parts.join('\n');
2882
2670
  }
2671
+ const runSummary = summarizeRunOutput(result);
2672
+ if (runSummary) return runSummary;
2883
2673
  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);
2674
+ const stdout = String(result.stdout || '');
2675
+ const stderr = String(result.stderr || '');
2886
2676
  const code = result.code ?? 0;
2887
2677
  const parts = [`[exit: ${code}]`];
2888
2678
  if (command) parts.push(`command: ${command}`);
@@ -2903,6 +2693,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2903
2693
  return result?.memory?.content ? `stored project memory: ${result.memory.content}` : JSON.stringify(result);
2904
2694
  },
2905
2695
 
2696
+ save_memory(result) {
2697
+ const scope = result?.scope || 'global';
2698
+ return result?.memory?.content ? `stored ${scope} memory: ${result.memory.content}` : JSON.stringify(result);
2699
+ },
2700
+
2906
2701
  list_memory(result) {
2907
2702
  if (!result || typeof result !== 'object' || !Array.isArray(result.items)) return JSON.stringify(result);
2908
2703
  if (result.items.length === 0) return `No ${result.scope || ''} memories found.`;
@@ -2938,8 +2733,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2938
2733
  const kind = result.kind || '';
2939
2734
  const content = result.content || result.source || '';
2940
2735
  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)}`;
2736
+ return `${header}\n${content}`;
2943
2737
  },
2944
2738
 
2945
2739
  web_fetch(result) {
@@ -2951,7 +2745,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2951
2745
  if (Array.isArray(result.links) && result.links.length > 0) {
2952
2746
  lines.push(`links: ${result.links.slice(0, 5).map((item) => item.href).join(', ')}`);
2953
2747
  }
2954
- if (result.text) lines.push(trimPreview(result.text, 1200));
2748
+ if (result.text) {
2749
+ lines.push(result.text);
2750
+ }
2955
2751
  return lines.join('\n');
2956
2752
  },
2957
2753
 
@@ -2992,6 +2788,13 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2992
2788
  }
2993
2789
  };
2994
2790
 
2791
+ const formatters = Object.fromEntries(
2792
+ Object.entries(rawFormatters).map(([name, formatter]) => [
2793
+ name,
2794
+ (result, args) => sanitizeTextForModel(formatter(result, args), getToolOutputSanitizeOptions(name))
2795
+ ])
2796
+ );
2797
+
2995
2798
  async function dispose() {
2996
2799
  if (activeFffAdapter?.dispose) {
2997
2800
  try {