codemini-cli 0.2.5 → 0.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
package/src/cli.js CHANGED
@@ -4,7 +4,7 @@ import { handleConfig } from './commands/config.js';
4
4
  import { handleDoctor } from './commands/doctor.js';
5
5
  import { handleSkill } from './commands/skill.js';
6
6
 
7
- const VERSION = '0.2.5';
7
+ const VERSION = '0.2.7';
8
8
 
9
9
  function printHelp() {
10
10
  console.log(`codemini ${VERSION}
@@ -7,7 +7,10 @@ function safeJsonParse(raw) {
7
7
  try {
8
8
  return JSON.parse(raw);
9
9
  } catch {
10
- return {};
10
+ return {
11
+ _raw: String(raw),
12
+ _invalid_json: true
13
+ };
11
14
  }
12
15
  }
13
16
 
@@ -36,6 +36,38 @@ function isMiniMaxModel(model) {
36
36
  return String(model || '').toLowerCase().includes('minimax');
37
37
  }
38
38
 
39
+ function normalizeToolCallArguments(argumentsText) {
40
+ const raw = typeof argumentsText === 'string' ? argumentsText : JSON.stringify(argumentsText ?? {});
41
+ try {
42
+ const parsed = JSON.parse(raw);
43
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
44
+ return JSON.stringify(parsed);
45
+ }
46
+ } catch {}
47
+ return '{}';
48
+ }
49
+
50
+ function sanitizeGatewayMessages(messages) {
51
+ const source = Array.isArray(messages) ? messages : [];
52
+ return source
53
+ .filter((message) => message && typeof message === 'object')
54
+ .map((message) => {
55
+ if (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0) {
56
+ return message;
57
+ }
58
+ return {
59
+ ...message,
60
+ tool_calls: message.tool_calls.map((toolCall) => ({
61
+ ...toolCall,
62
+ function: {
63
+ ...toolCall?.function,
64
+ arguments: normalizeToolCallArguments(toolCall?.function?.arguments)
65
+ }
66
+ }))
67
+ };
68
+ });
69
+ }
70
+
39
71
  function sanitizeMiniMaxMessages(messages) {
40
72
  const source = Array.isArray(messages) ? messages : [];
41
73
  const out = [];
@@ -64,10 +96,11 @@ function sanitizeMiniMaxMessages(messages) {
64
96
  }
65
97
 
66
98
  function buildPayload({ model, temperature, messages, tools, stream = false }) {
99
+ const sanitizedMessages = sanitizeGatewayMessages(messages);
67
100
  const payload = {
68
101
  model,
69
102
  temperature,
70
- messages: isMiniMaxModel(model) ? sanitizeMiniMaxMessages(messages) : messages
103
+ messages: isMiniMaxModel(model) ? sanitizeMiniMaxMessages(sanitizedMessages) : sanitizedMessages
71
104
  };
72
105
  if (stream) {
73
106
  payload.stream = true;
@@ -142,6 +142,10 @@ Some tools are loaded on demand. If a needed tool is not listed, call tool_searc
142
142
 
143
143
  # Doing tasks
144
144
 
145
+ - You are a terminal-first CLI coding agent, not a generic chat assistant
146
+ - The user shares your workspace with you; prefer inspecting the project yourself before asking them to paste files that should be discoverable
147
+ - Before substantial tool work, send a short progress update to the user about what you are about to inspect or do
148
+ - Do not jump straight into tools without a brief user-facing note when the task is actionable
145
149
  - Search or read before editing unless the exact target is already known
146
150
  - If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools
147
151
  - For AST-scoped edits, if edit rejects due to missing or stale ast_target, fix arguments and retry
package/src/core/tools.js CHANGED
@@ -755,7 +755,7 @@ async function readFile(root, args) {
755
755
  }
756
756
 
757
757
  async function writeFile(root, args) {
758
- const rawPath = String(args?.path || '').trim();
758
+ const rawPath = String(args?.path || args?.file_path || '').trim();
759
759
  if (!rawPath) {
760
760
  throw new Error('write requires a file path like weather/WeatherForecast.js');
761
761
  }
@@ -1536,7 +1536,7 @@ async function openTarget(root, args) {
1536
1536
  }
1537
1537
 
1538
1538
  function normalizeEditTargetArgs(args = {}) {
1539
- const file = String(args?.file || args?.path || '').trim();
1539
+ const file = String(args?.file || args?.path || args?.file_path || '').trim();
1540
1540
  const nestedEdit = args?.edit && typeof args.edit === 'object' ? args.edit : null;
1541
1541
  if (nestedEdit) {
1542
1542
  const normalizedEdit = { ...nestedEdit };
@@ -1561,6 +1561,8 @@ function normalizeEditTargetArgs(args = {}) {
1561
1561
  new_content: args?.new_content ?? args?.content,
1562
1562
  old_text: args?.old_text,
1563
1563
  new_text: args?.new_text,
1564
+ old_string: args?.old_string,
1565
+ new_string: args?.new_string,
1564
1566
  anchor_text: args?.anchor_text,
1565
1567
  content: args?.content
1566
1568
  }
@@ -1573,6 +1575,12 @@ async function editTarget(root, args) {
1573
1575
  const astTarget = normalized.ast_target;
1574
1576
  const edit = normalized.edit || {};
1575
1577
  let kind = String(edit.kind || '').trim();
1578
+ if (edit.old_text == null && edit.old_string != null) {
1579
+ edit.old_text = edit.old_string;
1580
+ }
1581
+ if (edit.new_text == null && edit.new_string != null) {
1582
+ edit.new_text = edit.new_string;
1583
+ }
1576
1584
  const hasContent = edit.new_content != null || edit.content != null;
1577
1585
  const hasTargetHint = Boolean(edit.symbol || args?.symbol || edit.line || args?.line || edit.target);
1578
1586
  if (!kind) {
@@ -1586,7 +1594,14 @@ async function editTarget(root, args) {
1586
1594
  kind = 'rewrite_file';
1587
1595
  }
1588
1596
  }
1589
- if (!file || !kind) throw new Error('edit requires file and edit.kind');
1597
+ if (!file || !kind) {
1598
+ const recentFile = String(args?.recent_file || '').trim();
1599
+ const rawArgs = typeof args?._raw === 'string' && args._raw.trim() ? ` Raw tool arguments: ${args._raw.trim()}.` : '';
1600
+ const hint = recentFile
1601
+ ? ` If you meant the recently read file ${recentFile}, use edit with {file:"${recentFile}", old_text:"...", new_text:"..."} for a text replacement, or {file:"${recentFile}", edit:{kind:"rewrite_file", new_content:"..."}} for a full rewrite.`
1602
+ : ' Use edit with {file:"path", old_text:"...", new_text:"..."} for a text replacement, or {file:"path", edit:{kind:"rewrite_file", new_content:"..."}} for a full rewrite.';
1603
+ throw new Error(`edit requires file and edit.kind.${rawArgs}${hint}`);
1604
+ }
1590
1605
  if (astTarget) {
1591
1606
  if (kind !== 'replace_block') {
1592
1607
  throw new Error('AST-scoped edit only supports replace_block');
@@ -1659,6 +1674,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1659
1674
  };
1660
1675
  const astSelectionCache = new Map();
1661
1676
  let lastAstTarget = null;
1677
+ let lastReadPath = '';
1662
1678
  const rememberAstSelection = (filePath, astTarget) => {
1663
1679
  const key = String(filePath || '').trim();
1664
1680
  if (!key || !astTarget) return;
@@ -1812,15 +1828,18 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1812
1828
  function: {
1813
1829
  name: 'edit',
1814
1830
  description:
1815
- 'Edit existing files. Use block edits, exact replacements, or anchored inserts. When ast_target is provided, keep the edit constrained to that node. Read first unless the exact target is already known. Prefer this over write for code changes.',
1831
+ '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. Prefer this over write for existing code changes.',
1816
1832
  parameters: {
1817
1833
  type: 'object',
1818
1834
  properties: {
1819
1835
  file: { type: 'string', description: 'File path to edit' },
1820
1836
  path: { type: 'string', description: 'Alias for file' },
1837
+ file_path: { type: 'string', description: 'Alias for file, compatible with simpler demo-style tool calls' },
1821
1838
  new_content: { type: 'string', description: 'Replacement content' },
1822
1839
  old_text: { type: 'string', description: 'Exact text to replace' },
1823
1840
  new_text: { type: 'string', description: 'Replacement text' },
1841
+ old_string: { type: 'string', description: 'Alias for old_text' },
1842
+ new_string: { type: 'string', description: 'Alias for new_text' },
1824
1843
  anchor_text: { type: 'string', description: 'Anchor text for inserts' },
1825
1844
  content: { type: 'string', description: 'Content to insert or append' },
1826
1845
  position: { type: 'string', description: 'before or after' },
@@ -1840,11 +1859,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1840
1859
  function: {
1841
1860
  name: 'write',
1842
1861
  description:
1843
- 'Create a new file or overwrite a file. Always include path and content. Use this for new files or explicit full rewrites only. If the file path is not decided yet, do not call write yet. Prefer edit for existing code changes.',
1862
+ 'Create a new file or overwrite a file. Always include path (or file_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.',
1844
1863
  parameters: {
1845
1864
  type: 'object',
1846
1865
  properties: {
1847
1866
  path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
1867
+ file_path: { type: 'string', description: 'Alias for path, compatible with simpler demo-style tool calls' },
1848
1868
  content: { type: 'string', description: 'Content to write' },
1849
1869
  append: { type: 'boolean', description: 'Append instead of overwrite' },
1850
1870
  full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
@@ -2049,6 +2069,10 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2049
2069
  typeof args?.max_chars === 'number'
2050
2070
  ? args.max_chars
2051
2071
  : config.context?.read_file_max_chars ?? 24000
2072
+ }).then((result) => {
2073
+ const readPath = String(result?.path || args?.path || '').trim();
2074
+ if (readPath) lastReadPath = readPath;
2075
+ return result;
2052
2076
  }),
2053
2077
  grep: (args) => grep(workspaceRoot, args),
2054
2078
  glob: (args) => glob(workspaceRoot, args),
@@ -2069,7 +2093,10 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2069
2093
  await ensureProjectIndex();
2070
2094
  const normalizedKind = String(args?.edit?.kind || args?.kind || '').trim();
2071
2095
  const astTarget = resolveCachedAstTarget(args, { requireAstScope: normalizedKind === 'replace_block' });
2072
- const result = await editTarget(workspaceRoot, astTarget ? { ...args, ast_target: astTarget } : args);
2096
+ const result = await editTarget(
2097
+ workspaceRoot,
2098
+ astTarget ? { ...args, ast_target: astTarget, recent_file: lastReadPath } : { ...args, recent_file: lastReadPath }
2099
+ );
2073
2100
  if (result?.path) await refreshProjectFile(result.path);
2074
2101
  return result;
2075
2102
  },
@@ -375,6 +375,39 @@ function isCodeGenerationActivityName(name) {
375
375
  return String(name || '').trim() === 'Code generation';
376
376
  }
377
377
 
378
+ export function buildPreToolNotice(name, copy) {
379
+ const parsed = parseToolDisplayName(name);
380
+ const base = parsed.base;
381
+ const target = parsed.target ? trimText(parsed.target, 48) : '';
382
+ const isEnglish = String(copy?.roleLabels?.coder || '').trim() === 'CODER' && String(copy?.roleLabels?.you || '').trim() === 'YOU';
383
+
384
+ if (isEnglish) {
385
+ if (base === 'read') return target ? `I'll inspect ${target} first.` : `I'll inspect the relevant file first.`;
386
+ if (base === 'list' || base === 'glob') return target ? `I'll inspect the ${target} directory first.` : `I'll inspect the relevant directory first.`;
387
+ if (base === 'grep') return `I'll search the relevant code first.`;
388
+ if (base === 'edit' || base === 'write' || base === 'patch' || base === 'generate_diff') {
389
+ return `I'll inspect the current code first, then make the change.`;
390
+ }
391
+ if (base === 'run') return `I'll verify the current project state first.`;
392
+ return `I'll check the relevant project context first.`;
393
+ }
394
+
395
+ if (base === 'read') return target ? `我先查看 ${target} 的内容。` : '我先查看相关文件内容。';
396
+ if (base === 'list' || base === 'glob') return target ? `我先查看 ${target} 目录里的内容。` : '我先查看相关目录内容。';
397
+ if (base === 'grep') return '我先搜索相关代码位置。';
398
+ if (base === 'edit' || base === 'write' || base === 'patch' || base === 'generate_diff') return '我先确认当前代码上下文,再动手修改。';
399
+ if (base === 'run') return '我先检查当前项目状态。';
400
+ return '我先查看相关上下文。';
401
+ }
402
+
403
+ export function shouldInjectPreToolNotice(msg) {
404
+ if (!msg) return false;
405
+ const text = String(msg.text || '').trim();
406
+ const segments = Array.isArray(msg.segments) ? msg.segments : [];
407
+ const hasTextSegment = segments.some((segment) => segment?.type === 'text' && String(segment.text || '').trim());
408
+ return !text && !hasTextSegment;
409
+ }
410
+
378
411
  function formatDurationMs(ms) {
379
412
  const safeMs = Math.max(0, Number(ms) || 0);
380
413
  return `${(safeMs / 1000).toFixed(1)}s`;
@@ -2280,11 +2313,12 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2280
2313
  } else {
2281
2314
  segments.push({ type: 'text', text: delta });
2282
2315
  }
2283
- const nextText = `${m.text}${delta}`;
2316
+ const nextText = m.syntheticPrelude ? delta : `${m.text}${delta}`;
2284
2317
  return {
2285
2318
  ...m,
2286
2319
  text: nextText,
2287
- segments
2320
+ segments,
2321
+ syntheticPrelude: false
2288
2322
  };
2289
2323
  })
2290
2324
  );
@@ -2634,8 +2668,15 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2634
2668
  prev.map((m) => {
2635
2669
  if (m.id !== targetId) return m;
2636
2670
  const nextMessage = isCodeActivityName(event.name) ? finishCodeGeneration(m, finishedAt) : m;
2671
+ const withPrelude = shouldInjectPreToolNotice(nextMessage)
2672
+ ? {
2673
+ ...nextMessage,
2674
+ text: buildPreToolNotice(event.name, copy),
2675
+ syntheticPrelude: true
2676
+ }
2677
+ : nextMessage;
2637
2678
  return {
2638
- ...nextMessage,
2679
+ ...withPrelude,
2639
2680
  loading: true,
2640
2681
  phase: 'tooling',
2641
2682
  liveStatus: detail