codemini-cli 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/core/tools.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  import { evaluateCommandPolicy } from './command-policy.js';
16
16
  import { findEnclosingSymbol, queryAst, readAstNode, resolveAstTarget } from './ast.js';
17
17
  import { initializeProjectIndex, queryProjectIndex, refreshIndexedFile } from './project-index.js';
18
- import { checkReadDedup } from './agent-loop.js';
18
+ import { checkReadDedup } from './tool-result-store.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
21
  import { forgetMemory, listMemories, rememberMemory, searchMemories, captureToInbox } from './memory-store.js';
@@ -29,6 +29,14 @@ import {
29
29
  sanitizeTextForModel,
30
30
  summarizeRunOutput
31
31
  } from './tool-output.js';
32
+ import {
33
+ normalizePathArgs,
34
+ normalizePatternArgs,
35
+ normalizeReadArgs,
36
+ normalizeWebFetchArgs,
37
+ normalizeWebSearchArgs,
38
+ normalizeWriteArgs
39
+ } from './tool-args.js';
32
40
  const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
33
41
  const BACKGROUND_TASK_POLL_MS = 150;
34
42
  const MAX_AST_ENCLOSING_BYTES = 300_000;
@@ -100,133 +108,6 @@ function splitLines(text) {
100
108
  return String(text || '').split('\n');
101
109
  }
102
110
 
103
- function parseInlineReadRange(value) {
104
- const text = String(value || '').trim();
105
- if (!text) return null;
106
- const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
107
- if (!match) return null;
108
- const [, maybePath, startRaw, endRaw] = match;
109
- if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
110
- const startLine = Number(startRaw);
111
- const endLine = Number(endRaw || startRaw);
112
- if (!Number.isFinite(startLine) || startLine <= 0) return null;
113
- if (!Number.isFinite(endLine) || endLine < startLine) return null;
114
- return {
115
- path: maybePath,
116
- start_line: startLine,
117
- end_line: endLine
118
- };
119
- }
120
-
121
- function normalizeReadArgs(rawArgs) {
122
- const source =
123
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
124
- ? { ...rawArgs }
125
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
126
-
127
- const normalized = { ...source };
128
- const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
129
- if (aliasPath) normalized.path = aliasPath;
130
-
131
- if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
132
- normalized.start_line = Number(source.offset);
133
- }
134
-
135
- if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
136
- const startLine = Number(normalized.start_line);
137
- const limit = Number(source.limit);
138
- if (startLine > 0 && limit > 0) {
139
- normalized.end_line = startLine + limit - 1;
140
- }
141
- }
142
-
143
- const inlineRange = parseInlineReadRange(normalized.path);
144
- if (inlineRange) {
145
- normalized.path = inlineRange.path;
146
- if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
147
- if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
148
- }
149
-
150
- return normalized;
151
- }
152
-
153
- function normalizePathArgs(rawArgs, aliases = []) {
154
- const source =
155
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
156
- ? { ...rawArgs }
157
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
158
- const normalized = { ...source };
159
- const keys = ['path', ...aliases];
160
- for (const key of keys) {
161
- const value = String(source?.[key] || '').trim();
162
- if (value) {
163
- normalized.path = value;
164
- break;
165
- }
166
- }
167
- return normalized;
168
- }
169
-
170
- function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
171
- const source =
172
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
173
- ? { ...rawArgs }
174
- : { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
175
- const normalized = { ...source };
176
- for (const key of ['pattern', ...aliases]) {
177
- const value = String(source?.[key] || '').trim();
178
- if (value) {
179
- normalized.pattern = value;
180
- break;
181
- }
182
- }
183
- for (const key of ['path', ...defaultPathAliases]) {
184
- const value = String(source?.[key] || '').trim();
185
- if (value) {
186
- normalized.path = value;
187
- break;
188
- }
189
- }
190
- return normalized;
191
- }
192
-
193
- function normalizeWriteArgs(rawArgs) {
194
- const source =
195
- rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
196
- ? { ...rawArgs }
197
- : { path: typeof rawArgs === 'string' ? rawArgs : '' };
198
- const normalized = { ...source };
199
- const filePath = String(source.path || source.file_path || source.file || '').trim();
200
- if (filePath) normalized.path = filePath;
201
- if (normalized.content == null) {
202
- if (source.text != null) normalized.content = source.text;
203
- if (source.new_content != null) normalized.content = source.new_content;
204
- }
205
- return normalized;
206
- }
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
111
  function clampNumber(value, min, max, fallback) {
231
112
  const num = Number(value);
232
113
  if (!Number.isFinite(num)) return fallback;
@@ -291,6 +172,55 @@ function collectPageLinks($, pageUrl, maxLinks = 20) {
291
172
  return links;
292
173
  }
293
174
 
175
+ function extractPageContent(cheerio, html, pageUrl, { maxLinks, status = null, contentType = '', fetchMode = 'static' } = {}) {
176
+ const $ = cheerio.load(html);
177
+ $('script, style, noscript').remove();
178
+ const bodyText = $('body').text() || $.root().text();
179
+ const text = String(bodyText || '').replace(/[^\S\n]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
180
+ const title = trimPreview($('title').first().text(), 240);
181
+ const description = extractHtmlMeta($, 'description') || extractHtmlMeta($, 'og:description');
182
+ const links = collectPageLinks($, pageUrl, maxLinks);
183
+
184
+ return {
185
+ final_url: pageUrl,
186
+ title,
187
+ description,
188
+ text,
189
+ links,
190
+ metadata: {
191
+ status,
192
+ fetched_at: new Date().toISOString(),
193
+ content_type: contentType,
194
+ fetch_mode: fetchMode,
195
+ lang: String($('html').attr('lang') || '').trim()
196
+ }
197
+ };
198
+ }
199
+
200
+ function shouldTryBrowserRender(html, text) {
201
+ if (String(text || '').trim().length >= 120) return false;
202
+ return /<script\b/i.test(html) ||
203
+ /id=["']__(?:next|nuxt)["']/i.test(html) ||
204
+ /data-reactroot|ng-version|window\.__/i.test(html);
205
+ }
206
+
207
+ function playwrightInstallHint() {
208
+ return 'For JavaScript-rendered pages, install Playwright for richer web_fetch results: npm install -g playwright && playwright install chromium';
209
+ }
210
+
211
+ async function loadOptionalPlaywright() {
212
+ try {
213
+ return await import('playwright');
214
+ } catch (error) {
215
+ const code = String(error?.code || '');
216
+ const message = String(error?.message || '');
217
+ if (code === 'ERR_MODULE_NOT_FOUND' || /Cannot find package 'playwright'|Cannot find module 'playwright'/i.test(message)) {
218
+ return null;
219
+ }
220
+ throw error;
221
+ }
222
+ }
223
+
294
224
  async function buildPlaywrightLaunchEnv() {
295
225
  const localLibDir = path.join(
296
226
  process.env.HOME || '',
@@ -323,44 +253,85 @@ async function webFetchPage(args = {}) {
323
253
  ? String(normalizedArgs.wait_until).trim()
324
254
  : 'domcontentloaded';
325
255
 
326
- const [{ chromium }, cheerio] = await Promise.all([import('playwright'), import('cheerio')]);
256
+ const cheerio = await import('cheerio');
257
+ let staticResult = null;
258
+ let staticHtml = '';
259
+ let staticError = null;
260
+ try {
261
+ const controller = new AbortController();
262
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
263
+ let response;
264
+ try {
265
+ response = await fetch(url, {
266
+ redirect: 'follow',
267
+ signal: controller.signal,
268
+ headers: {
269
+ 'user-agent': 'CodeMiniCLI/0.4 web_fetch'
270
+ }
271
+ });
272
+ } finally {
273
+ clearTimeout(timeout);
274
+ }
275
+ staticHtml = await response.text();
276
+ staticResult = {
277
+ url,
278
+ ...extractPageContent(cheerio, staticHtml, response.url || url, {
279
+ maxLinks,
280
+ status: response.status,
281
+ contentType: response.headers.get('content-type') || '',
282
+ fetchMode: 'static'
283
+ })
284
+ };
285
+ if (!shouldTryBrowserRender(staticHtml, staticResult.text)) {
286
+ return staticResult;
287
+ }
288
+ } catch (error) {
289
+ staticError = error;
290
+ }
327
291
 
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.
292
+ const playwright = await loadOptionalPlaywright();
293
+ if (!playwright) {
294
+ if (staticResult) {
295
+ return {
296
+ ...staticResult,
297
+ warnings: [playwrightInstallHint()]
298
+ };
299
+ }
300
+ throw new Error(`web_fetch failed and browser rendering is unavailable. ${playwrightInstallHint()}. Static fetch error: ${staticError?.message || staticError}`);
301
+ }
330
302
 
331
- const browser = await chromium.launch({
332
- headless: true,
333
- env: await buildPlaywrightLaunchEnv()
334
- });
303
+ let browser;
335
304
  try {
305
+ browser = await playwright.chromium.launch({
306
+ headless: true,
307
+ env: await buildPlaywrightLaunchEnv()
308
+ });
336
309
  const page = await browser.newPage();
337
310
  const response = await page.goto(url, { waitUntil, timeout: timeoutMs });
338
311
  const finalUrl = page.url();
339
312
  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 {
313
+ const rendered = {
348
314
  url,
349
- final_url: finalUrl,
350
- title,
351
- description,
352
- text,
353
- links,
354
- metadata: {
315
+ ...extractPageContent(cheerio, html, finalUrl, {
316
+ maxLinks,
355
317
  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
- }
318
+ contentType: response?.headers?.()['content-type'] || '',
319
+ fetchMode: 'browser'
320
+ })
361
321
  };
322
+ rendered.metadata.wait_until = waitUntil;
323
+ rendered.title = rendered.title || trimPreview(await page.title(), 240);
324
+ return rendered;
325
+ } catch (error) {
326
+ if (staticResult) {
327
+ return {
328
+ ...staticResult,
329
+ warnings: [`Browser rendering fallback failed: ${error?.message || error}`]
330
+ };
331
+ }
332
+ throw error;
362
333
  } finally {
363
- await browser.close();
334
+ if (browser) await browser.close();
364
335
  }
365
336
  }
366
337
 
@@ -1819,20 +1790,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1819
1790
  function: {
1820
1791
  name: 'read',
1821
1792
  description:
1822
- '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.',
1793
+ '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.',
1823
1794
  parameters: {
1824
1795
  type: 'object',
1825
1796
  properties: {
1826
1797
  path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
1827
- file_path: { type: 'string', description: 'Alias for path' },
1828
1798
  start_line: { type: 'number', description: '1-based start line' },
1829
1799
  end_line: { type: 'number', description: 'Inclusive end line' },
1830
- offset: { type: 'number', description: 'Alias for start_line' },
1831
- limit: { type: 'number', description: 'Number of lines to read starting from offset/start_line' },
1832
1800
  max_chars: { type: 'number', description: 'Max chars to return' },
1833
- include_content: { type: 'boolean', description: 'Legacy compatibility flag. Content is returned by default.' },
1834
- read_token: { type: 'string', description: 'Legacy compatibility token. No longer required for content reads.' },
1835
- metadata_only: { type: 'boolean', description: 'Set true to return metadata without content.' },
1836
1801
  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.' },
1837
1802
  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.' },
1838
1803
  capture_name: { type: 'string', description: 'Optional capture name to select when query is provided.' },
@@ -1847,14 +1812,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1847
1812
  function: {
1848
1813
  name: 'grep',
1849
1814
  description:
1850
- '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.',
1815
+ 'Search file contents. Use this for code search before read or edit. Do not use run with grep or rg for normal code search.',
1851
1816
  parameters: {
1852
1817
  type: 'object',
1853
1818
  properties: {
1854
1819
  pattern: { type: 'string', description: 'Search pattern' },
1855
- query: { type: 'string', description: 'Alias for pattern' },
1856
1820
  path: { type: 'string', description: 'Directory or file to search' },
1857
- directory: { type: 'string', description: 'Alias for path' },
1858
1821
  regex: { type: 'boolean', description: 'Treat pattern as regex' },
1859
1822
  case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
1860
1823
  max_results: { type: 'number', description: 'Max matches to return' },
@@ -1869,12 +1832,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1869
1832
  type: 'function',
1870
1833
  function: {
1871
1834
  name: 'list',
1872
- 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.',
1835
+ description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.',
1873
1836
  parameters: {
1874
1837
  type: 'object',
1875
1838
  properties: {
1876
1839
  path: { type: 'string', description: 'Directory path to list' },
1877
- directory: { type: 'string', description: 'Alias for path' },
1878
1840
  include_hidden: { type: 'boolean', description: 'Include dotfiles' }
1879
1841
  }
1880
1842
  }
@@ -1885,14 +1847,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1885
1847
  function: {
1886
1848
  name: 'glob',
1887
1849
  description:
1888
- '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.',
1850
+ 'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts.',
1889
1851
  parameters: {
1890
1852
  type: 'object',
1891
1853
  properties: {
1892
1854
  pattern: { type: 'string', description: 'Glob pattern' },
1893
1855
  path: { type: 'string', description: 'Directory to search' },
1894
- query: { type: 'string', description: 'Alias for pattern' },
1895
- directory: { type: 'string', description: 'Alias for path' },
1896
1856
  include_hidden: { type: 'boolean', description: 'Include dotfiles' },
1897
1857
  max_results: { type: 'number', description: 'Max results' }
1898
1858
  },
@@ -1923,18 +1883,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1923
1883
  function: {
1924
1884
  name: 'edit',
1925
1885
  description:
1926
- '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.',
1886
+ '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.',
1927
1887
  parameters: {
1928
1888
  type: 'object',
1929
1889
  properties: {
1930
- file: { type: 'string', description: 'File path to edit' },
1931
- path: { type: 'string', description: 'Alias for file' },
1932
- file_path: { type: 'string', description: 'Alias for file, compatible with simpler demo-style tool calls' },
1890
+ path: { type: 'string', description: 'File path to edit' },
1933
1891
  new_content: { type: 'string', description: 'Replacement content' },
1934
1892
  old_text: { type: 'string', description: 'Exact text to replace' },
1935
1893
  new_text: { type: 'string', description: 'Replacement text' },
1936
- old_string: { type: 'string', description: 'Alias for old_text' },
1937
- new_string: { type: 'string', description: 'Alias for new_text' },
1938
1894
  anchor_text: { type: 'string', description: 'Anchor text for inserts' },
1939
1895
  content: { type: 'string', description: 'Content to insert or append' },
1940
1896
  position: { type: 'string', description: 'before or after' },
@@ -1945,7 +1901,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1945
1901
  line: { type: 'number', description: 'Line to target' },
1946
1902
  edit: { type: 'object', description: 'Structured edit input' }
1947
1903
  },
1948
- required: ['file']
1904
+ required: ['path']
1949
1905
  }
1950
1906
  }
1951
1907
  },
@@ -1954,16 +1910,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1954
1910
  function: {
1955
1911
  name: 'write',
1956
1912
  description:
1957
- '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.',
1913
+ '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.',
1958
1914
  parameters: {
1959
1915
  type: 'object',
1960
1916
  properties: {
1961
1917
  path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
1962
- file_path: { type: 'string', description: 'Alias for path, compatible with simpler demo-style tool calls' },
1963
- file: { type: 'string', description: 'Alias for path' },
1964
1918
  content: { type: 'string', description: 'Content to write' },
1965
- text: { type: 'string', description: 'Alias for content' },
1966
- new_content: { type: 'string', description: 'Alias for content' },
1967
1919
  append: { type: 'boolean', description: 'Append instead of overwrite' },
1968
1920
  full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
1969
1921
  },
@@ -1976,15 +1928,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1976
1928
  function: {
1977
1929
  name: 'delete',
1978
1930
  description:
1979
- '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.',
1931
+ 'Delete a file or directory inside the workspace. Missing targets fail. Workspace escape attempts are rejected.',
1980
1932
  parameters: {
1981
1933
  type: 'object',
1982
1934
  properties: {
1983
- path: { type: 'string', description: 'File or directory path to delete' },
1984
- file: { type: 'string', description: 'Alias for path' },
1985
- file_path: { type: 'string', description: 'Alias for path' },
1986
- directory: { type: 'string', description: 'Alias for path' },
1987
- dir: { type: 'string', description: 'Alias for path' }
1935
+ path: { type: 'string', description: 'File or directory path to delete' }
1988
1936
  },
1989
1937
  required: ['path']
1990
1938
  }
@@ -2178,7 +2126,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2178
2126
  function: {
2179
2127
  name: 'web_fetch',
2180
2128
  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.',
2129
+ 'Fetch and read a live web page. Uses a lightweight fetch + Cheerio reader by default, then falls back to optional Playwright browser rendering for JavaScript-heavy pages when Playwright is installed. Use this for direct URL reads, not for keyword search.',
2182
2130
  parameters: {
2183
2131
  type: 'object',
2184
2132
  properties: {
@@ -2282,7 +2230,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2282
2230
  function: {
2283
2231
  name: 'dream_consolidate',
2284
2232
  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.',
2233
+ 'Run a dream loop pass over inbox entries and existing memory buckets. Reads recent inbox items, deduplicates, evaluates lifecycle progression (observed → candidate → operational/longterm), promotes stable patterns into persistent memory, then uses LLM maintenance to merge/summarize/clean stale user/global/project memories when their bucket changed since the last maintenance marker. Writes an audit report. Use during off-hours or explicit maintenance.',
2286
2234
  parameters: {
2287
2235
  type: 'object',
2288
2236
  properties: {
@@ -2884,6 +2832,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2884
2832
  if (result.title) lines.push(`title: ${result.title}`);
2885
2833
  if (result.description) lines.push(`description: ${trimPreview(result.description, 200)}`);
2886
2834
  if (result.metadata?.status) lines.push(`status: ${result.metadata.status}`);
2835
+ if (result.metadata?.fetch_mode) lines.push(`mode: ${result.metadata.fetch_mode}`);
2836
+ if (Array.isArray(result.warnings)) {
2837
+ for (const warning of result.warnings.slice(0, 3)) {
2838
+ if (warning) lines.push(`warning: ${warning}`);
2839
+ }
2840
+ }
2887
2841
  if (Array.isArray(result.links) && result.links.length > 0) {
2888
2842
  lines.push(`links: ${result.links.slice(0, 5).map((item) => item.href).join(', ')}`);
2889
2843
  }