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 +1 -1
- package/src/cli.js +1 -1
- package/src/core/agent-loop.js +4 -1
- package/src/core/provider/openai-compatible.js +34 -1
- package/src/core/shell-profile.js +4 -0
- package/src/core/tools.js +33 -6
- package/src/tui/chat-app.js +44 -3
package/package.json
CHANGED
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.
|
|
7
|
+
const VERSION = '0.2.7';
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log(`codemini ${VERSION}
|
package/src/core/agent-loop.js
CHANGED
|
@@ -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(
|
|
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)
|
|
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.
|
|
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(
|
|
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
|
},
|
package/src/tui/chat-app.js
CHANGED
|
@@ -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
|
-
...
|
|
2679
|
+
...withPrelude,
|
|
2639
2680
|
loading: true,
|
|
2640
2681
|
phase: 'tooling',
|
|
2641
2682
|
liveStatus: detail
|