codemini-cli 0.3.8 → 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
@@ -18,9 +18,17 @@ import { initializeProjectIndex, queryProjectIndex, refreshIndexedFile } from '.
18
18
  import { checkReadDedup } from './agent-loop.js';
19
19
  import { TOOL_SKIP_DIRS as SKIP_DIRS, TEXT_EXTENSIONS, CODE_WRITE_GUARD_EXTENSIONS, LANGUAGE_FILE_TYPES } from './constants.js';
20
20
  import { sha256Prefixed as sha256, sha256 as sha256Hash } from './crypto-utils.js';
21
- import { forgetMemory, listMemories, rememberMemory, searchMemories } from './memory-store.js';
21
+ import { forgetMemory, listMemories, rememberMemory, searchMemories, captureToInbox } from './memory-store.js';
22
+ import { runDreamConsolidation } from './dream-consolidate.js';
22
23
  import { normalizePlanState } from './plan-state.js';
23
24
  import { normalizeTodos } from './todo-state.js';
25
+ import { createFffAdapter } from './fff-adapter.js';
26
+ import {
27
+ getToolOutputSanitizeOptions,
28
+ sanitizePreviewLines,
29
+ sanitizeTextForModel,
30
+ summarizeRunOutput
31
+ } from './tool-output.js';
24
32
  const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
25
33
  const BACKGROUND_TASK_POLL_MS = 150;
26
34
  const MAX_AST_ENCLOSING_BYTES = 300_000;
@@ -197,6 +205,199 @@ function normalizeWriteArgs(rawArgs) {
197
205
  return normalized;
198
206
  }
199
207
 
208
+ function normalizeWebFetchArgs(rawArgs) {
209
+ const source =
210
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
211
+ ? { ...rawArgs }
212
+ : { url: typeof rawArgs === 'string' ? rawArgs : '' };
213
+ const normalized = { ...source };
214
+ const url = String(source.url || source.href || source.link || source.target || '').trim();
215
+ if (url) normalized.url = url;
216
+ return normalized;
217
+ }
218
+
219
+ function normalizeWebSearchArgs(rawArgs) {
220
+ const source =
221
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
222
+ ? { ...rawArgs }
223
+ : { query: typeof rawArgs === 'string' ? rawArgs : '' };
224
+ const normalized = { ...source };
225
+ const query = String(source.query || source.q || source.keyword || '').trim();
226
+ if (query) normalized.query = query;
227
+ return normalized;
228
+ }
229
+
230
+ function clampNumber(value, min, max, fallback) {
231
+ const num = Number(value);
232
+ if (!Number.isFinite(num)) return fallback;
233
+ return Math.min(max, Math.max(min, num));
234
+ }
235
+
236
+ function normalizeWhitespace(value) {
237
+ return String(value || '')
238
+ .replace(/\s+/g, ' ')
239
+ .trim();
240
+ }
241
+
242
+ function trimPreview(value, maxLen = 300) {
243
+ const text = normalizeWhitespace(value);
244
+ if (text.length <= maxLen) return text;
245
+ return `${text.slice(0, Math.max(0, maxLen - 3))}...`;
246
+ }
247
+
248
+ function normalizeWebUrl(value) {
249
+ const text = String(value || '').trim();
250
+ if (!text) return '';
251
+ let parsed;
252
+ try {
253
+ parsed = new URL(text);
254
+ } catch {
255
+ throw new Error(`Invalid URL: ${text}`);
256
+ }
257
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
258
+ throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
259
+ }
260
+ return parsed.toString();
261
+ }
262
+
263
+ function extractHtmlMeta($, name, attribute = 'content') {
264
+ return String(
265
+ $(`meta[name="${name}"]`).attr(attribute) ||
266
+ $(`meta[property="${name}"]`).attr(attribute) ||
267
+ ''
268
+ ).trim();
269
+ }
270
+
271
+ function collectPageLinks($, pageUrl, maxLinks = 20) {
272
+ const links = [];
273
+ const seen = new Set();
274
+ $('a[href]').each((_, element) => {
275
+ if (links.length >= maxLinks) return false;
276
+ const hrefRaw = String($(element).attr('href') || '').trim();
277
+ if (!hrefRaw) return undefined;
278
+ try {
279
+ const href = new URL(hrefRaw, pageUrl).toString();
280
+ if (seen.has(href)) return undefined;
281
+ seen.add(href);
282
+ links.push({
283
+ href,
284
+ text: trimPreview($(element).text(), 160)
285
+ });
286
+ } catch {
287
+ return undefined;
288
+ }
289
+ return undefined;
290
+ });
291
+ return links;
292
+ }
293
+
294
+ async function buildPlaywrightLaunchEnv() {
295
+ const localLibDir = path.join(
296
+ process.env.HOME || '',
297
+ '.cache',
298
+ 'codemini',
299
+ 'playwright-libs',
300
+ 'usr',
301
+ 'lib',
302
+ 'x86_64-linux-gnu'
303
+ );
304
+ try {
305
+ await fs.access(localLibDir);
306
+ } catch {
307
+ return process.env;
308
+ }
309
+
310
+ const existing = String(process.env.LD_LIBRARY_PATH || '').trim();
311
+ return {
312
+ ...process.env,
313
+ LD_LIBRARY_PATH: existing ? `${localLibDir}:${existing}` : localLibDir
314
+ };
315
+ }
316
+
317
+ async function webFetchPage(args = {}) {
318
+ const normalizedArgs = normalizeWebFetchArgs(args);
319
+ const url = normalizeWebUrl(normalizedArgs.url);
320
+ const timeoutMs = clampNumber(normalizedArgs.timeout_ms, 1_000, 120_000, 20_000);
321
+ const maxLinks = clampNumber(normalizedArgs.max_links, 0, 100, 20);
322
+ const waitUntil = ['domcontentloaded', 'load', 'networkidle'].includes(String(normalizedArgs.wait_until || '').trim())
323
+ ? String(normalizedArgs.wait_until).trim()
324
+ : 'domcontentloaded';
325
+
326
+ const [{ chromium }, cheerio] = await Promise.all([import('playwright'), import('cheerio')]);
327
+
328
+ // Crawlee is intentionally disabled for now so single-page reads stay lightweight.
329
+ // If we later need multi-URL crawl orchestration, retries, or request queues, we can re-enable it here.
330
+
331
+ const browser = await chromium.launch({
332
+ headless: true,
333
+ env: await buildPlaywrightLaunchEnv()
334
+ });
335
+ try {
336
+ const page = await browser.newPage();
337
+ const response = await page.goto(url, { waitUntil, timeout: timeoutMs });
338
+ const finalUrl = page.url();
339
+ const html = await page.content();
340
+ const $ = cheerio.load(html);
341
+ const bodyText = $('body').text() || $.root().text();
342
+ const text = String(bodyText || '').replace(/[^\S\n]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
343
+ const title = trimPreview($('title').first().text() || (await page.title()), 240);
344
+ const description = extractHtmlMeta($, 'description') || extractHtmlMeta($, 'og:description');
345
+ const links = collectPageLinks($, finalUrl, maxLinks);
346
+
347
+ return {
348
+ url,
349
+ final_url: finalUrl,
350
+ title,
351
+ description,
352
+ text,
353
+ links,
354
+ metadata: {
355
+ status: response?.status?.() ?? null,
356
+ fetched_at: new Date().toISOString(),
357
+ content_type: response?.headers?.()['content-type'] || '',
358
+ wait_until: waitUntil,
359
+ lang: String($('html').attr('lang') || '').trim()
360
+ }
361
+ };
362
+ } finally {
363
+ await browser.close();
364
+ }
365
+ }
366
+
367
+ async function webSearchQuery(config, args = {}) {
368
+ if (config?.web?.search_enabled === false) {
369
+ throw new Error('web_search is disabled by config. Set web.search_enabled=true to enable network search.');
370
+ }
371
+
372
+ const normalizedArgs = normalizeWebSearchArgs(args);
373
+ const query = String(normalizedArgs.query || '').trim();
374
+ if (!query) throw new Error('web_search requires query');
375
+
376
+ const maxResults = clampNumber(normalizedArgs.max_results, 1, 20, 8);
377
+ const [{ search, SafeSearchType }] = await Promise.all([import('duck-duck-scrape')]);
378
+ const response = await search(query, {
379
+ safeSearch: SafeSearchType.MODERATE,
380
+ locale: String(normalizedArgs.locale || 'en-us').trim() || 'en-us',
381
+ region: String(normalizedArgs.region || 'wt-wt').trim() || 'wt-wt'
382
+ });
383
+
384
+ return {
385
+ query,
386
+ no_results: response?.noResults === true,
387
+ results: Array.isArray(response?.results)
388
+ ? response.results.slice(0, maxResults).map((item) => ({
389
+ title: String(item?.title || '').trim(),
390
+ url: String(item?.url || '').trim(),
391
+ description: normalizeWhitespace(item?.description || item?.rawDescription || ''),
392
+ hostname: String(item?.hostname || '').trim()
393
+ }))
394
+ : [],
395
+ related: Array.isArray(response?.related)
396
+ ? response.related.slice(0, 8).map((item) => String(item?.text || item?.raw || '').trim()).filter(Boolean)
397
+ : []
398
+ };
399
+ }
400
+
200
401
  function findUniqueLineBlock(lines, blockContent) {
201
402
  const probeLines = splitLines(blockContent);
202
403
  if (probeLines.length === 0 || (probeLines.length === 1 && probeLines[0] === '')) return null;
@@ -550,40 +751,6 @@ function findEnclosingSymbolLine(lines, anchorLine) {
550
751
  return 0;
551
752
  }
552
753
 
553
- function buildUnifiedDiff(oldContent, newContent, filePath = 'file') {
554
- const oldLines = splitLines(oldContent);
555
- const newLines = splitLines(newContent);
556
- let prefix = 0;
557
- while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
558
- prefix += 1;
559
- }
560
-
561
- let suffix = 0;
562
- while (
563
- suffix < oldLines.length - prefix &&
564
- suffix < newLines.length - prefix &&
565
- oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
566
- ) {
567
- suffix += 1;
568
- }
569
-
570
- const oldChanged = oldLines.slice(prefix, oldLines.length - suffix);
571
- const newChanged = newLines.slice(prefix, newLines.length - suffix);
572
- const oldStart = prefix + 1;
573
- const newStart = prefix + 1;
574
- const oldCount = Math.max(1, oldChanged.length);
575
- const newCount = Math.max(1, newChanged.length);
576
-
577
- const body = [
578
- `--- ${filePath}`,
579
- `+++ ${filePath}`,
580
- `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`,
581
- ...oldChanged.map((line) => `-${line}`),
582
- ...newChanged.map((line) => `+${line}`)
583
- ];
584
- return body.join('\n');
585
- }
586
-
587
754
  async function getFileState(root, relativePath) {
588
755
  const target = await resolveInWorkspace(root, relativePath);
589
756
  const stat = await fs.stat(target);
@@ -727,12 +894,15 @@ async function writeFile(root, args) {
727
894
  }
728
895
  const previewStart = Math.max(0, (changeLine || 1) - 1);
729
896
  const previewLines = afterLines.slice(previewStart, previewStart + 6);
897
+ const changed = countChangedLines(before, after);
730
898
  return {
731
899
  ok: true,
732
900
  path: rawPath,
733
901
  action: normalizedArgs?.append ? 'append' : existed ? 'overwrite' : 'create',
734
902
  changed_line: changeLine || Math.max(1, afterLines.length),
735
- 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
736
906
  };
737
907
  }
738
908
 
@@ -848,10 +1018,9 @@ function shellCommandForBackgroundTask(command, shellSpec) {
848
1018
  }
849
1019
 
850
1020
  function appendRecentOutput(task, chunk) {
851
- const lines = String(chunk || '')
852
- .split(/\r?\n/)
853
- .map((line) => trimLinePreview(line, 220))
854
- .filter(Boolean);
1021
+ const lines = sanitizePreviewLines(chunk, { maxLineLength: 220 }).map((line) =>
1022
+ trimLinePreview(line, 220)
1023
+ );
855
1024
  if (lines.length === 0) return;
856
1025
  for (const line of lines) {
857
1026
  backgroundTaskLogCursorCounter += 1;
@@ -1133,7 +1302,7 @@ async function stopBackgroundTask(_root, args) {
1133
1302
  return { ...snapshotBackgroundTask(task), stopped: true };
1134
1303
  }
1135
1304
 
1136
- async function grep(root, args) {
1305
+ async function builtinGrep(root, args) {
1137
1306
  const normalizedArgs = normalizePatternArgs(args, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
1138
1307
  const pattern = String(normalizedArgs?.pattern || '').trim();
1139
1308
  if (!pattern) throw new Error('grep requires pattern');
@@ -1168,7 +1337,7 @@ async function grep(root, args) {
1168
1337
  return { pattern, matches, truncated: false };
1169
1338
  }
1170
1339
 
1171
- async function glob(root, args) {
1340
+ async function builtinGlob(root, args) {
1172
1341
  const normalizedArgs = normalizePatternArgs(args, ['glob', 'query'], ['directory', 'dir', 'cwd']);
1173
1342
  const pattern = String(normalizedArgs?.pattern || '').trim();
1174
1343
  if (!pattern) throw new Error('glob requires pattern');
@@ -1188,7 +1357,7 @@ async function glob(root, args) {
1188
1357
  };
1189
1358
  }
1190
1359
 
1191
- async function list(root, args) {
1360
+ async function builtinList(root, args) {
1192
1361
  const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'target']);
1193
1362
  const relativePath = String(normalizedArgs?.path || '.').trim() || '.';
1194
1363
  const target = await resolveInWorkspace(root, relativePath);
@@ -1302,18 +1471,47 @@ async function validateEdit(root, args) {
1302
1471
  throw new Error(`validate_edit does not support kind: ${kind}`);
1303
1472
  }
1304
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
+
1305
1501
  function editResult(pathText, action, beforeContent, afterContent, changedLine = 1) {
1306
1502
  const afterLines = splitLines(afterContent);
1307
1503
  const previewStart = Math.max(0, changedLine - 1);
1308
1504
  const diffPreview = afterLines.slice(previewStart, previewStart + 6).map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n');
1505
+ const changed = countChangedLines(beforeContent, afterContent);
1309
1506
  return {
1310
1507
  ok: true,
1311
1508
  path: pathText,
1312
1509
  action,
1313
1510
  changed_line: changedLine,
1314
1511
  diff_preview: diffPreview,
1315
- diff: buildUnifiedDiff(beforeContent, afterContent, pathText),
1316
- new_hash: sha256(afterContent)
1512
+ new_hash: sha256(afterContent),
1513
+ lines_added: changed.added,
1514
+ lines_removed: changed.removed
1317
1515
  };
1318
1516
  }
1319
1517
 
@@ -1535,7 +1733,7 @@ async function editTarget(root, args) {
1535
1733
  throw new Error(`edit does not support kind: ${kind}`);
1536
1734
  }
1537
1735
 
1538
- export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate, getPlanState, onPlanStateUpdate }) {
1736
+ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate, getPlanState, onPlanStateUpdate, fffAdapter }) {
1539
1737
  const emitSystemTool = (event) => {
1540
1738
  if (typeof onSystemEvent === 'function' && event) onSystemEvent(event);
1541
1739
  };
@@ -1570,7 +1768,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1570
1768
  };
1571
1769
  const ensureProjectIndex = async () => {
1572
1770
  const eventId = `project-index:${Date.now()}`;
1573
- 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)';
1574
1772
  try {
1575
1773
  const result = await initializeProjectIndex(workspaceRoot);
1576
1774
  if (result?.skipped || !result?.summary) {
@@ -1602,7 +1800,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1602
1800
  type: 'system_tool:end',
1603
1801
  id: eventId,
1604
1802
  name,
1605
- summary: result?.summary || `updated .codemini-project for ${relativePath}`
1803
+ summary: result?.summary || `updated .codemini for ${relativePath}`
1606
1804
  });
1607
1805
  return result;
1608
1806
  } catch (error) {
@@ -1975,52 +2173,61 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1975
2173
  }
1976
2174
  }
1977
2175
  },
1978
- remember_user: {
2176
+ web_fetch: {
1979
2177
  type: 'function',
1980
2178
  function: {
1981
- name: 'remember_user',
1982
- 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.',
2179
+ name: 'web_fetch',
2180
+ description:
2181
+ 'Fetch and read a live web page. Uses Playwright to render the page, Cheerio to extract structured content, and Crawlee request handling to normalize the fetch flow. Use this for direct URL reads, not for keyword search.',
1983
2182
  parameters: {
1984
2183
  type: 'object',
1985
2184
  properties: {
1986
- content: { type: 'string', description: 'Stable preference or instruction to remember' },
1987
- kind: { type: 'string', description: 'preference, workflow, constraint, or warning' },
1988
- summary: { type: 'string', description: 'Short summary for the memory index' },
1989
- replace_similar: { type: 'boolean', description: 'Replace an existing similar memory when true' }
2185
+ url: { type: 'string', description: 'Absolute http or https URL to fetch' },
2186
+ href: { type: 'string', description: 'Alias for url' },
2187
+ timeout_ms: { type: 'number', description: 'Navigation timeout in milliseconds' },
2188
+ wait_until: { type: 'string', description: 'domcontentloaded, load, or networkidle' },
2189
+ max_links: { type: 'number', description: 'Max number of links to extract from the page' }
1990
2190
  },
1991
- required: ['content']
2191
+ required: ['url']
1992
2192
  }
1993
2193
  }
1994
2194
  },
1995
- remember_global: {
2195
+ web_search: {
1996
2196
  type: 'function',
1997
2197
  function: {
1998
- name: 'remember_global',
1999
- 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.',
2198
+ name: 'web_search',
2199
+ description:
2200
+ 'Run a live web search through DuckDuckGo. Use this for keyword-based internet search. This tool respects config.web.search_enabled and will fail when network search is disabled.',
2000
2201
  parameters: {
2001
2202
  type: 'object',
2002
2203
  properties: {
2003
- content: { type: 'string' },
2004
- kind: { type: 'string' },
2005
- summary: { type: 'string' },
2006
- replace_similar: { type: 'boolean' }
2204
+ query: { type: 'string', description: 'Search query' },
2205
+ q: { type: 'string', description: 'Alias for query' },
2206
+ max_results: { type: 'number', description: 'Max results to return' },
2207
+ locale: { type: 'string', description: 'DuckDuckGo locale such as en-us' },
2208
+ region: { type: 'string', description: 'DuckDuckGo region such as wt-wt' }
2007
2209
  },
2008
- required: ['content']
2210
+ required: ['query']
2009
2211
  }
2010
2212
  }
2011
2213
  },
2012
- remember_project: {
2214
+ save_memory: {
2013
2215
  type: 'function',
2014
2216
  function: {
2015
- name: 'remember_project',
2016
- 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.',
2017
2220
  parameters: {
2018
2221
  type: 'object',
2019
2222
  properties: {
2020
- content: { type: 'string' },
2021
- kind: { type: 'string' },
2022
- summary: { type: 'string' },
2023
- 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.' }
2024
2231
  },
2025
2232
  required: ['content']
2026
2233
  }
@@ -2070,6 +2277,21 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2070
2277
  }
2071
2278
  }
2072
2279
  },
2280
+ dream_consolidate: {
2281
+ type: 'function',
2282
+ function: {
2283
+ name: 'dream_consolidate',
2284
+ description:
2285
+ 'Run a dream loop consolidation pass over inbox entries. Reads recent inbox items, deduplicates, evaluates lifecycle progression (observed → candidate → operational/longterm), promotes stable patterns into persistent memory, archives expired items, and writes an audit report. Use during off-hours or explicit maintenance.',
2286
+ parameters: {
2287
+ type: 'object',
2288
+ properties: {
2289
+ scope: { type: 'string', description: 'Optional scope filter: global, repo, or thread' },
2290
+ dry_run: { type: 'boolean', description: 'If true, only preview what would change without making changes' }
2291
+ }
2292
+ }
2293
+ }
2294
+ },
2073
2295
  list_background_tasks: {
2074
2296
  type: 'function',
2075
2297
  function: {
@@ -2113,6 +2335,47 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2113
2335
  };
2114
2336
 
2115
2337
  const definitions = [...primaryDefinitions];
2338
+ const activeFffAdapter = fffAdapter || createFffAdapter({ workspaceRoot, config });
2339
+ let fffConnected = false;
2340
+
2341
+ async function ensureFffConnected() {
2342
+ if (!activeFffAdapter?.connect || fffConnected) return;
2343
+ await activeFffAdapter.connect();
2344
+ fffConnected = true;
2345
+ }
2346
+
2347
+ async function grep(args) {
2348
+ if (activeFffAdapter?.grep) {
2349
+ try {
2350
+ await ensureFffConnected();
2351
+ const result = await activeFffAdapter.grep(args);
2352
+ if (result && Array.isArray(result.matches)) return result;
2353
+ } catch {}
2354
+ }
2355
+ return builtinGrep(workspaceRoot, args);
2356
+ }
2357
+
2358
+ async function glob(args) {
2359
+ if (activeFffAdapter?.glob) {
2360
+ try {
2361
+ await ensureFffConnected();
2362
+ const result = await activeFffAdapter.glob(args);
2363
+ if (result && Array.isArray(result.matches)) return result;
2364
+ } catch {}
2365
+ }
2366
+ return builtinGlob(workspaceRoot, args);
2367
+ }
2368
+
2369
+ async function list(args) {
2370
+ if (activeFffAdapter?.list) {
2371
+ try {
2372
+ await ensureFffConnected();
2373
+ const result = await activeFffAdapter.list(args);
2374
+ if (result && Array.isArray(result.items)) return result;
2375
+ } catch {}
2376
+ }
2377
+ return builtinList(workspaceRoot, args);
2378
+ }
2116
2379
 
2117
2380
  const handlers = {
2118
2381
  read: async (args) => {
@@ -2176,9 +2439,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2176
2439
  await ensureProjectIndex();
2177
2440
  return queryProjectIndex(workspaceRoot, args);
2178
2441
  },
2179
- grep: (args) => grep(workspaceRoot, args),
2180
- glob: (args) => glob(workspaceRoot, args),
2181
- list: (args) => list(workspaceRoot, args),
2442
+ grep,
2443
+ glob,
2444
+ list,
2182
2445
  ast_query: async (args) => {
2183
2446
  const result = await queryAst(workspaceRoot, args);
2184
2447
  const firstTarget = result?.matches?.[0]?.ast_target;
@@ -2191,6 +2454,8 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2191
2454
  if (astTarget.path) rememberAstSelection(astTarget.path, astTarget);
2192
2455
  return readAstNode(workspaceRoot, { ...args, ast_target: astTarget });
2193
2456
  },
2457
+ web_fetch: (args) => webFetchPage(args),
2458
+ web_search: (args) => webSearchQuery(config, args),
2194
2459
  edit: async (args) => {
2195
2460
  await ensureProjectIndex();
2196
2461
  const normalizedKind = String(args?.edit?.kind || args?.kind || '').trim();
@@ -2271,42 +2536,32 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2271
2536
  hasPendingApproval: nextPlan?.status === 'pending_approval'
2272
2537
  };
2273
2538
  },
2274
- run: (args) => runCommand(workspaceRoot, config, args),
2275
- remember_user: async (args = {}) => {
2276
- const saved = await rememberMemory({
2277
- scope: 'user',
2278
- content: args.content,
2279
- kind: args.kind,
2280
- summary: args.summary,
2281
- replaceSimilar: args.replace_similar !== false,
2282
- workspaceRoot,
2283
- config
2284
- });
2285
- return { ok: true, scope: 'user', memory: saved };
2286
- },
2287
- remember_global: async (args = {}) => {
2288
- const saved = await rememberMemory({
2289
- scope: 'global',
2290
- content: args.content,
2291
- kind: args.kind,
2292
- summary: args.summary,
2293
- replaceSimilar: args.replace_similar !== false,
2294
- workspaceRoot,
2295
- config
2296
- });
2297
- return { ok: true, scope: 'global', memory: saved };
2298
- },
2299
- 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';
2300
2554
  const saved = await rememberMemory({
2301
- scope: 'project',
2555
+ scope: memoryScope,
2302
2556
  content: args.content,
2303
- kind: args.kind,
2304
- summary: args.summary,
2557
+ kind: args.kind || 'observation',
2558
+ summary: args.summary || String(args.content || '').slice(0, 80),
2559
+ source: 'tool',
2305
2560
  replaceSimilar: args.replace_similar !== false,
2306
2561
  workspaceRoot,
2307
2562
  config
2308
2563
  });
2309
- return { ok: true, scope: 'project', memory: saved };
2564
+ return { ok: true, scope: memoryScope, memory: saved };
2310
2565
  },
2311
2566
  list_memory: async (args = {}) => ({
2312
2567
  scope: String(args.scope || ''),
@@ -2321,6 +2576,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2321
2576
  ok: true,
2322
2577
  ...(await forgetMemory({ scope: args.scope, id: args.id, workspaceRoot }))
2323
2578
  }),
2579
+ dream_consolidate: async (args = {}) => {
2580
+ return runDreamConsolidation({
2581
+ dryRun: args.dry_run === true,
2582
+ scope: args.scope || null,
2583
+ workspaceRoot,
2584
+ config,
2585
+ writeAudit: true
2586
+ });
2587
+ },
2324
2588
  list_background_tasks: () => listBackgroundTasks(workspaceRoot),
2325
2589
  get_background_task: (args) => getBackgroundTask(workspaceRoot, args),
2326
2590
  stop_background_task: (args) => stopBackgroundTask(workspaceRoot, args),
@@ -2347,7 +2611,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2347
2611
  }
2348
2612
  };
2349
2613
 
2350
- const formatters = {
2614
+ const rawFormatters = {
2351
2615
  read(result) {
2352
2616
  if (typeof result === 'string') return result;
2353
2617
  if (!result || typeof result !== 'object') return String(result);
@@ -2546,9 +2810,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2546
2810
  }
2547
2811
  return parts.join('\n');
2548
2812
  }
2813
+ const runSummary = summarizeRunOutput(result);
2814
+ if (runSummary) return runSummary;
2549
2815
  const command = String(result.command || '').slice(0, 200);
2550
- const stdout = String(result.stdout || '').slice(0, 500);
2551
- const stderr = String(result.stderr || '').slice(0, 500);
2816
+ const stdout = String(result.stdout || '');
2817
+ const stderr = String(result.stderr || '');
2552
2818
  const code = result.code ?? 0;
2553
2819
  const parts = [`[exit: ${code}]`];
2554
2820
  if (command) parts.push(`command: ${command}`);
@@ -2569,6 +2835,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2569
2835
  return result?.memory?.content ? `stored project memory: ${result.memory.content}` : JSON.stringify(result);
2570
2836
  },
2571
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
+
2572
2843
  list_memory(result) {
2573
2844
  if (!result || typeof result !== 'object' || !Array.isArray(result.items)) return JSON.stringify(result);
2574
2845
  if (result.items.length === 0) return `No ${result.scope || ''} memories found.`;
@@ -2604,8 +2875,37 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2604
2875
  const kind = result.kind || '';
2605
2876
  const content = result.content || result.source || '';
2606
2877
  const header = `${kind} ${name}`;
2607
- if (typeof content !== 'string' || content.length <= 2000) return `${header}\n${content}`;
2608
- return `${header}\n${content.slice(0, 1200)}\n... [omitted ${content.length - 1600} chars] ...\n${content.slice(-400)}`;
2878
+ return `${header}\n${content}`;
2879
+ },
2880
+
2881
+ web_fetch(result) {
2882
+ if (!result || typeof result !== 'object') return String(result);
2883
+ const lines = [`[web_fetch: ${result.final_url || result.url || '?'}]`];
2884
+ if (result.title) lines.push(`title: ${result.title}`);
2885
+ if (result.description) lines.push(`description: ${trimPreview(result.description, 200)}`);
2886
+ if (result.metadata?.status) lines.push(`status: ${result.metadata.status}`);
2887
+ if (Array.isArray(result.links) && result.links.length > 0) {
2888
+ lines.push(`links: ${result.links.slice(0, 5).map((item) => item.href).join(', ')}`);
2889
+ }
2890
+ if (result.text) {
2891
+ lines.push(result.text);
2892
+ }
2893
+ return lines.join('\n');
2894
+ },
2895
+
2896
+ web_search(result) {
2897
+ if (!result || typeof result !== 'object') return String(result);
2898
+ const lines = [result.query ? `[web_search: "${result.query}"]` : '[web_search]'];
2899
+ if (!Array.isArray(result.results) || result.results.length === 0) {
2900
+ lines.push(result.no_results ? 'No results found.' : 'No search results returned.');
2901
+ return lines.join('\n');
2902
+ }
2903
+ for (const item of result.results.slice(0, 8)) {
2904
+ lines.push(`- ${item.title || item.url}`);
2905
+ if (item.url) lines.push(` ${item.url}`);
2906
+ if (item.description) lines.push(` ${trimPreview(item.description, 180)}`);
2907
+ }
2908
+ return lines.join('\n');
2609
2909
  },
2610
2910
 
2611
2911
  list_background_tasks(result) {
@@ -2630,5 +2930,20 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2630
2930
  }
2631
2931
  };
2632
2932
 
2633
- return { definitions, handlers, formatters, deferredDefinitions };
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
+
2940
+ async function dispose() {
2941
+ if (activeFffAdapter?.dispose) {
2942
+ try {
2943
+ await activeFffAdapter.dispose();
2944
+ } catch {}
2945
+ }
2946
+ }
2947
+
2948
+ return { definitions, handlers, formatters, deferredDefinitions, dispose };
2634
2949
  }