codemini-cli 0.4.2 → 0.4.3

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
@@ -345,30 +345,107 @@ async function webSearchQuery(config, args = {}) {
345
345
  if (!query) throw new Error('web_search requires query');
346
346
 
347
347
  const maxResults = clampNumber(normalizedArgs.max_results, 1, 20, 8);
348
- const [{ search, SafeSearchType }] = await Promise.all([import('duck-duck-scrape')]);
349
- const response = await search(query, {
350
- safeSearch: SafeSearchType.MODERATE,
351
- locale: String(normalizedArgs.locale || 'en-us').trim() || 'en-us',
352
- region: String(normalizedArgs.region || 'wt-wt').trim() || 'wt-wt'
348
+ const locale = String(normalizedArgs.locale || config?.web?.search_locale || 'en-US').trim() || 'en-US';
349
+ const region = String(normalizedArgs.region || normalizedArgs.cc || config?.web?.search_region || (locale.toLowerCase().endsWith('-cn') ? 'CN' : 'US')).trim() || 'US';
350
+ const searchUrl = buildBingRssSearchUrl({
351
+ baseUrl: config?.web?.search_base_url,
352
+ query,
353
+ locale,
354
+ region
353
355
  });
356
+ const timeoutMs = clampNumber(normalizedArgs.timeout_ms || config?.web?.search_timeout_ms, 1_000, 60_000, 15_000);
357
+ const controller = new AbortController();
358
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
359
+ let response;
360
+ try {
361
+ response = await fetch(searchUrl, {
362
+ redirect: 'follow',
363
+ signal: controller.signal,
364
+ headers: {
365
+ 'user-agent': 'CodeMiniCLI/0.4 web_search',
366
+ accept: 'application/rss+xml, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.5',
367
+ 'accept-language': `${locale},en;q=0.8`
368
+ }
369
+ });
370
+ } finally {
371
+ clearTimeout(timeout);
372
+ }
373
+ if (!response.ok) {
374
+ throw new Error(`web_search Bing RSS request failed: HTTP ${response.status}`);
375
+ }
376
+
377
+ const xml = await response.text();
378
+ const cheerio = await import('cheerio');
379
+ const parsed = parseBingRssResults(cheerio, xml, maxResults);
354
380
 
355
381
  return {
356
382
  query,
357
- no_results: response?.noResults === true,
358
- results: Array.isArray(response?.results)
359
- ? response.results.slice(0, maxResults).map((item) => ({
360
- title: String(item?.title || '').trim(),
361
- url: String(item?.url || '').trim(),
362
- description: normalizeWhitespace(item?.description || item?.rawDescription || ''),
363
- hostname: String(item?.hostname || '').trim()
364
- }))
365
- : [],
366
- related: Array.isArray(response?.related)
367
- ? response.related.slice(0, 8).map((item) => String(item?.text || item?.raw || '').trim()).filter(Boolean)
368
- : []
383
+ engine: 'bing_rss',
384
+ source_url: response.url || searchUrl,
385
+ no_results: parsed.results.length === 0,
386
+ results: parsed.results,
387
+ related: []
369
388
  };
370
389
  }
371
390
 
391
+ function buildBingRssSearchUrl({ baseUrl, query, locale, region }) {
392
+ const url = new URL(String(baseUrl || 'https://cn.bing.com/search'));
393
+ url.searchParams.set('q', query);
394
+ url.searchParams.set('mkt', locale);
395
+ url.searchParams.set('setlang', locale);
396
+ url.searchParams.set('cc', region);
397
+ url.searchParams.set('format', 'rss');
398
+ return url.toString();
399
+ }
400
+
401
+ function parseBingRssResults(cheerio, xml, maxResults) {
402
+ const $ = cheerio.load(xml, { xmlMode: true });
403
+ const results = [];
404
+ const seenUrls = new Set();
405
+ $('item').each((_, element) => {
406
+ if (results.length >= maxResults) return false;
407
+ const title = normalizeWhitespace($(element).find('title').first().text());
408
+ const url = normalizeSearchResultUrl($(element).find('link').first().text());
409
+ if (!title || !url || seenUrls.has(url)) return undefined;
410
+ seenUrls.add(url);
411
+ results.push({
412
+ title,
413
+ url,
414
+ description: normalizeRssDescription(cheerio, $(element).find('description').first().text()),
415
+ hostname: hostnameFromUrl(url),
416
+ published_at: normalizeWhitespace($(element).find('pubDate').first().text())
417
+ });
418
+ return undefined;
419
+ });
420
+ return { results };
421
+ }
422
+
423
+ function normalizeSearchResultUrl(value) {
424
+ const text = String(value || '').trim();
425
+ if (!text) return '';
426
+ try {
427
+ const parsed = new URL(text);
428
+ if (!['http:', 'https:'].includes(parsed.protocol)) return '';
429
+ return parsed.toString();
430
+ } catch {
431
+ return '';
432
+ }
433
+ }
434
+
435
+ function normalizeRssDescription(cheerio, value) {
436
+ const text = String(value || '').trim();
437
+ if (!text) return '';
438
+ return normalizeWhitespace(cheerio.load(text).text() || text);
439
+ }
440
+
441
+ function hostnameFromUrl(value) {
442
+ try {
443
+ return new URL(value).hostname;
444
+ } catch {
445
+ return '';
446
+ }
447
+ }
448
+
372
449
  function findUniqueLineBlock(lines, blockContent) {
373
450
  const probeLines = splitLines(blockContent);
374
451
  if (probeLines.length === 0 || (probeLines.length === 1 && probeLines[0] === '')) return null;
@@ -967,7 +1044,7 @@ async function runCommand(root, config, args) {
967
1044
  command,
968
1045
  cwd: root,
969
1046
  shell: config.shell.default,
970
- timeoutMs: config.shell.timeout_ms
1047
+ timeoutMs: Number(args?.timeout || args?.timeout_ms || args?.timeoutMs || config.shell.timeout_ms)
971
1048
  });
972
1049
  return { ...result, command };
973
1050
  }
@@ -1842,24 +1919,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1842
1919
  }
1843
1920
  }
1844
1921
  },
1845
- {
1846
- type: 'function',
1847
- function: {
1848
- name: 'glob',
1849
- description:
1850
- 'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts.',
1851
- parameters: {
1852
- type: 'object',
1853
- properties: {
1854
- pattern: { type: 'string', description: 'Glob pattern' },
1855
- path: { type: 'string', description: 'Directory to search' },
1856
- include_hidden: { type: 'boolean', description: 'Include dotfiles' },
1857
- max_results: { type: 'number', description: 'Max results' }
1858
- },
1859
- required: ['pattern']
1860
- }
1861
- }
1862
- },
1863
1922
  {
1864
1923
  type: 'function',
1865
1924
  function: {
@@ -2085,6 +2144,24 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2085
2144
  ];
2086
2145
 
2087
2146
  const deferredDefinitions = {
2147
+ glob: {
2148
+ type: 'function',
2149
+ function: {
2150
+ name: 'glob',
2151
+ description:
2152
+ 'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts.',
2153
+ parameters: {
2154
+ type: 'object',
2155
+ properties: {
2156
+ pattern: { type: 'string', description: 'Glob pattern' },
2157
+ path: { type: 'string', description: 'Directory to search' },
2158
+ include_hidden: { type: 'boolean', description: 'Include dotfiles' },
2159
+ max_results: { type: 'number', description: 'Max results' }
2160
+ },
2161
+ required: ['pattern']
2162
+ }
2163
+ }
2164
+ },
2088
2165
  ast_query: {
2089
2166
  type: 'function',
2090
2167
  function: {
@@ -2145,15 +2222,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2145
2222
  function: {
2146
2223
  name: 'web_search',
2147
2224
  description:
2148
- '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.',
2225
+ 'Run a live web search by fetching Bing RSS results. Use this for keyword-based internet search. This tool respects config.web.search_enabled and will fail when network search is disabled.',
2149
2226
  parameters: {
2150
2227
  type: 'object',
2151
2228
  properties: {
2152
2229
  query: { type: 'string', description: 'Search query' },
2153
2230
  q: { type: 'string', description: 'Alias for query' },
2154
2231
  max_results: { type: 'number', description: 'Max results to return' },
2155
- locale: { type: 'string', description: 'DuckDuckGo locale such as en-us' },
2156
- region: { type: 'string', description: 'DuckDuckGo region such as wt-wt' }
2232
+ locale: { type: 'string', description: 'Bing market and language such as en-US or zh-CN' },
2233
+ region: { type: 'string', description: 'Bing country code such as US or CN' }
2157
2234
  },
2158
2235
  required: ['query']
2159
2236
  }
@@ -45,6 +45,22 @@ const ROLE_STYLES = {
45
45
  badgeText: 'black',
46
46
  chrome: 'gray'
47
47
  },
48
+ general: {
49
+ accent: 'greenBright',
50
+ border: 'green',
51
+ text: 'greenBright',
52
+ badgeBg: 'green',
53
+ badgeText: 'black',
54
+ chrome: 'gray'
55
+ },
56
+ advisor: {
57
+ accent: 'blueBright',
58
+ border: 'blue',
59
+ text: 'blueBright',
60
+ badgeBg: 'blue',
61
+ badgeText: 'white',
62
+ chrome: 'gray'
63
+ },
48
64
  planner: {
49
65
  accent: 'magentaBright',
50
66
  border: 'magenta',
@@ -105,7 +121,7 @@ const ROLE_STYLES = {
105
121
 
106
122
  const TUI_COPY = {
107
123
  zh: {
108
- roleLabels: { you: '👤 你', coder: '💻 CODER', planner: '📋 PLANNER', reviewer: '🔍 REVIEWER', tester: '🧪 TESTER', summarizer: '📝 SUMMARIZER', system: '⚙️ 系统', error: '❌ 错误', pending: '⏳ 等待中' },
124
+ roleLabels: { you: '👤 你', general: 'GENERAL', advisor: '💡 ADVISOR', coder: '💻 CODER', planner: '📋 PLANNER', reviewer: '🔍 REVIEWER', tester: '🧪 TESTER', summarizer: '📝 SUMMARIZER', system: '⚙️ 系统', error: '❌ 错误', pending: '⏳ 等待中' },
109
125
  generic: {
110
126
  waitingForInput: '等待输入',
111
127
  ready: '就绪',
@@ -219,6 +235,8 @@ const TUI_COPY = {
219
235
  doingProjectIndex: '正在初始化项目索引',
220
236
  doneFileIndex: '已刷新文件索引',
221
237
  doingFileIndex: '正在刷新文件索引',
238
+ donePromptBudget: '已测量 Prompt 预算',
239
+ doingPromptBudget: '正在测量 Prompt 预算',
222
240
  toolFailed: (name) => `工具执行失败: ${name}`,
223
241
  waitingModelContinue: (detail) => `${detail},等待模型继续`,
224
242
  waitingModelAdjust: (detail) => `${detail},等待模型调整`
@@ -330,7 +348,7 @@ const TUI_COPY = {
330
348
  }
331
349
  },
332
350
  en: {
333
- roleLabels: { you: 'YOU', coder: 'CODER', planner: 'PLANNER', reviewer: 'REVIEWER', tester: 'TESTER', summarizer: 'SUMMARIZER', system: 'SYSTEM', error: 'ERROR', pending: 'PENDING' },
351
+ roleLabels: { you: 'YOU', general: 'GENERAL', advisor: 'ADVISOR', coder: 'CODER', planner: 'PLANNER', reviewer: 'REVIEWER', tester: 'TESTER', summarizer: 'SUMMARIZER', system: 'SYSTEM', error: 'ERROR', pending: 'PENDING' },
334
352
  generic: {
335
353
  waitingForInput: 'waiting for input',
336
354
  ready: 'ready',
@@ -444,6 +462,8 @@ const TUI_COPY = {
444
462
  doingProjectIndex: 'Initializing project index',
445
463
  doneFileIndex: 'File index refreshed',
446
464
  doingFileIndex: 'Refreshing file index',
465
+ donePromptBudget: 'Prompt budget measured',
466
+ doingPromptBudget: 'Measuring prompt budget',
447
467
  toolFailed: (name) => `Tool failed: ${name}`,
448
468
  waitingModelContinue: (detail) => `${detail}, waiting for model to continue`,
449
469
  waitingModelAdjust: (detail) => `${detail}, waiting for model to adjust`
@@ -572,7 +592,7 @@ function roleStyle(label) {
572
592
  return ROLE_STYLES[label] || ROLE_STYLES.system;
573
593
  }
574
594
 
575
- const PLAN_AGENT_ROLES = new Set(['planner', 'coder', 'reviewer', 'tester', 'summarizer']);
595
+ const PLAN_AGENT_ROLES = new Set(['planner', 'advisor', 'coder', 'reviewer', 'tester', 'summarizer']);
576
596
 
577
597
  function normalizePlanAgentRole(role) {
578
598
  const roleKey = String(role || '').trim().toLowerCase();
@@ -945,7 +965,7 @@ export function buildUiMessagesFromSessionHistory(sessionMessages, nextId) {
945
965
  continue;
946
966
  }
947
967
  if (message.role === 'assistant') {
948
- out.push({ id: nextId(), label: 'coder', text, color: 'greenBright' });
968
+ out.push({ id: nextId(), label: 'general', text, color: 'greenBright' });
949
969
  continue;
950
970
  }
951
971
  if (message.role === 'system') {
@@ -1325,7 +1345,7 @@ export function isIndexSystemToolName(name) {
1325
1345
  export function shouldShowCompletionFooter(msg) {
1326
1346
  if (!msg || msg.loading || (msg.phase || '').trim()) return false;
1327
1347
  const label = (msg.label || '').toLowerCase();
1328
- return label === 'coder' || label === 'planner' || label === 'reviewer' || label === 'tester';
1348
+ return label === 'general' || label === 'advisor' || label === 'coder' || label === 'planner' || label === 'reviewer' || label === 'tester';
1329
1349
  }
1330
1350
 
1331
1351
  function describeToolActivity(name, copy, { done = false, blocked = false } = {}) {
@@ -2506,13 +2526,27 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
2506
2526
  }
2507
2527
  };
2508
2528
 
2529
+ const visiblePendingToolCalls = (existingCalls = []) => {
2530
+ const pendingToolCalls = Array.isArray(msg?.pendingToolCalls) ? msg.pendingToolCalls : [];
2531
+ return pendingToolCalls.filter((pending) => {
2532
+ if (!pending) return false;
2533
+ if (pending.id && existingCalls.some((tool) => tool?.id && tool.id === pending.id)) return false;
2534
+ const pendingBase = parseToolDisplayName(pending.name).base;
2535
+ return !existingCalls.some(
2536
+ (tool) => parseToolDisplayName(tool?.name).base === pendingBase && tool?.status === 'running'
2537
+ );
2538
+ });
2539
+ };
2540
+
2509
2541
  if (Array.isArray(msg?.segments) && msg.segments.length > 0) {
2510
- const totalTools = msg.segments.filter(
2542
+ const segmentTools = msg.segments.filter(
2511
2543
  (segment) =>
2512
2544
  segment.type === 'tool' ||
2513
2545
  segment.type === 'skill' ||
2514
2546
  (segment.type === 'system_tool' && (showToolDetails || !isIndexSystemToolName(segment.name)))
2515
- ).length;
2547
+ );
2548
+ const pendingToolCalls = visiblePendingToolCalls(segmentTools);
2549
+ const totalTools = segmentTools.length + pendingToolCalls.length;
2516
2550
  let toolIndex = 0;
2517
2551
  for (const segment of msg.segments) {
2518
2552
  if (segment.type === 'tool' || segment.type === 'skill' || segment.type === 'system_tool') {
@@ -2525,19 +2559,14 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
2525
2559
  pushTextRows(segment.text || '');
2526
2560
  }
2527
2561
  }
2562
+ pendingToolCalls.forEach((tool) => {
2563
+ pushActivityRows(tool, toolIndex, totalTools);
2564
+ toolIndex += 1;
2565
+ });
2528
2566
  } else {
2529
2567
  pushTextRows(msg?.text || '');
2530
2568
  const toolCalls = Array.isArray(msg?.toolCalls) ? msg.toolCalls : [];
2531
- const pendingToolCalls = Array.isArray(msg?.pendingToolCalls) ? msg.pendingToolCalls : [];
2532
- const visibleCalls = [
2533
- ...toolCalls,
2534
- ...pendingToolCalls.filter((pending) => {
2535
- if (!pending) return false;
2536
- if (pending.id && toolCalls.some((tool) => tool?.id && tool.id === pending.id)) return false;
2537
- const pendingBase = parseToolDisplayName(pending.name).base;
2538
- return !toolCalls.some((tool) => parseToolDisplayName(tool?.name).base === pendingBase && tool?.status === 'running');
2539
- })
2540
- ];
2569
+ const visibleCalls = [...toolCalls, ...visiblePendingToolCalls(toolCalls)];
2541
2570
  visibleCalls.forEach((tool, idx) => pushActivityRows(tool, idx, visibleCalls.length));
2542
2571
  }
2543
2572
 
@@ -3324,6 +3353,7 @@ function formatRuntimeSnapshot(snapshot) {
3324
3353
  if (!snapshot || typeof snapshot !== 'object') return '';
3325
3354
  return [
3326
3355
  `mode=${snapshot.mode || '-'}`,
3356
+ `role=${snapshot.agentRole || 'general'}`,
3327
3357
  `model=${snapshot.model || '-'}`,
3328
3358
  `max_ctx=${snapshot.maxContextTokens || '-'}`,
3329
3359
  `session=${snapshot.sessionId || '-'}`
@@ -3553,7 +3583,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3553
3583
  const current = Number(last[1]);
3554
3584
  const total = Number(last[2]);
3555
3585
  const role = String(last[3] || '').trim().toLowerCase();
3556
- const normalizedRole = ['planner', 'coder', 'reviewer', 'tester', 'summarizer'].includes(role) ? role : 'coder';
3586
+ const normalizedRole = PLAN_AGENT_ROLES.has(role) ? role : 'coder';
3557
3587
  const title = String(last[4] || '').trim();
3558
3588
 
3559
3589
  // Detect step transition — finalize old assistant and create a new one
@@ -3761,7 +3791,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3761
3791
  ...prev,
3762
3792
  {
3763
3793
  id: nextId(),
3764
- label: 'coder',
3794
+ label: 'general',
3765
3795
  text: sanitizeRenderableText(displayText),
3766
3796
  color: 'greenBright',
3767
3797
  autoSkillNames: activeAssistantAutoSkillNamesRef.current
@@ -3901,8 +3931,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3901
3931
  const aid = nextId();
3902
3932
  activeAssistantIdRef.current = aid;
3903
3933
  const planRole = activePlanStepRoleRef.current;
3904
- const label = planRole || 'coder';
3905
- const style = ROLE_STYLES[label] || ROLE_STYLES.coder;
3934
+ const label = planRole || 'general';
3935
+ const style = ROLE_STYLES[label] || ROLE_STYLES.general;
3906
3936
  const planStepInfo = activePlanStepInfoRef.current;
3907
3937
  const planStepTitle = activePlanStepTitleRef.current;
3908
3938
  const planStepDisplay = planStepInfo ? `${planStepInfo.current}/${planStepInfo.total} · ${planStepTitle}` : undefined;
@@ -4090,7 +4120,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4090
4120
  const cleanedStandaloneText = stripPlanExecutionResult(String(displayText || event.text)).trim();
4091
4121
  setMessages((prev) => [
4092
4122
  ...prev,
4093
- { id: nextId(), label: 'coder', text: cleanedStandaloneText, color: 'greenBright' }
4123
+ { id: nextId(), label: 'general', text: cleanedStandaloneText, color: 'greenBright' }
4094
4124
  ]);
4095
4125
  }
4096
4126
  }
@@ -10,5 +10,11 @@ export function describeSystemToolActivity(copy, parsed, { done = false, blocked
10
10
  if (blocked) return makeBlocked(copy, safeTarget);
11
11
  return done ? `${copy.toolActivity.doneFileIndex}: ${safeTarget}` : `${copy.toolActivity.doingFileIndex}: ${safeTarget}`;
12
12
  }
13
+ if (parsed.base === 'prompt_budget') {
14
+ if (blocked) return makeBlocked(copy, 'prompt_budget');
15
+ return done
16
+ ? (copy.toolActivity.donePromptBudget || 'Prompt budget measured')
17
+ : (copy.toolActivity.doingPromptBudget || 'Measuring prompt budget');
18
+ }
13
19
  return '';
14
20
  }