codemini-cli 0.2.7 → 0.2.9
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 +101 -3
- package/src/core/default-system-prompt.js +38 -0
- package/src/core/provider/openai-compatible.js +34 -4
- package/src/core/shell-profile.js +14 -3
- package/src/core/tools.js +163 -55
- package/src/tui/chat-app.js +515 -193
- package/src/tui/skill-activity/index.js +20 -0
- package/src/tui/tool-activity/common.js +29 -0
- package/src/tui/tool-activity/index.js +17 -0
- package/src/tui/tool-activity/presenters/command.js +29 -0
- package/src/tui/tool-activity/presenters/files.js +26 -0
- package/src/tui/tool-activity/presenters/misc.js +19 -0
- package/src/tui/tool-activity/presenters/system.js +14 -0
- package/src/tui/tool-narration/common.js +37 -0
- package/src/tui/tool-narration/presenters/change.js +109 -0
- package/src/tui/tool-narration/presenters/edit.js +3 -0
- package/src/tui/tool-narration/presenters/generic.js +10 -0
- package/src/tui/tool-narration/presenters/glob.js +11 -0
- package/src/tui/tool-narration/presenters/grep.js +11 -0
- package/src/tui/tool-narration/presenters/list.js +11 -0
- package/src/tui/tool-narration/presenters/patch.js +3 -0
- package/src/tui/tool-narration/presenters/read.js +11 -0
- package/src/tui/tool-narration/presenters/run.js +29 -0
- package/src/tui/tool-narration/presenters/write.js +3 -0
- package/src/tui/tool-narration.js +67 -0
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.9';
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log(`codemini ${VERSION}
|
package/src/core/agent-loop.js
CHANGED
|
@@ -14,6 +14,101 @@ function safeJsonParse(raw) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function parseInlineRangePath(value) {
|
|
18
|
+
const text = String(value || '').trim();
|
|
19
|
+
if (!text) return null;
|
|
20
|
+
const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
const [, maybePath, startRaw, endRaw] = match;
|
|
23
|
+
if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
|
|
24
|
+
const start = Number(startRaw);
|
|
25
|
+
const end = Number(endRaw || startRaw);
|
|
26
|
+
if (!Number.isFinite(start) || start <= 0) return null;
|
|
27
|
+
if (!Number.isFinite(end) || end < start) return null;
|
|
28
|
+
return { path: maybePath, start_line: start, end_line: end };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeToolArguments(toolName, args, rawArguments) {
|
|
32
|
+
const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
|
|
33
|
+
const primitive =
|
|
34
|
+
args == null || Array.isArray(args) || typeof args !== 'object'
|
|
35
|
+
? args
|
|
36
|
+
: null;
|
|
37
|
+
const source =
|
|
38
|
+
args && typeof args === 'object' && !Array.isArray(args)
|
|
39
|
+
? { ...args }
|
|
40
|
+
: {};
|
|
41
|
+
|
|
42
|
+
if (primitive != null && typeof primitive !== 'object') {
|
|
43
|
+
source._raw = rawText || String(primitive);
|
|
44
|
+
} else if (!source._raw && rawText && source._invalid_json) {
|
|
45
|
+
source._raw = rawText;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stringValue =
|
|
49
|
+
typeof primitive === 'string'
|
|
50
|
+
? primitive.trim()
|
|
51
|
+
: String(source._raw || '').trim();
|
|
52
|
+
|
|
53
|
+
if (toolName === 'read') {
|
|
54
|
+
const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
|
|
55
|
+
if (value) source.path = value;
|
|
56
|
+
if (source.offset != null && source.start_line == null) source.start_line = source.offset;
|
|
57
|
+
if (source.limit != null && source.end_line == null && Number(source.start_line) > 0) {
|
|
58
|
+
source.end_line = Number(source.start_line) + Number(source.limit) - 1;
|
|
59
|
+
}
|
|
60
|
+
const range = parseInlineRangePath(source.path);
|
|
61
|
+
if (range) {
|
|
62
|
+
source.path = range.path;
|
|
63
|
+
if (source.start_line == null) source.start_line = range.start_line;
|
|
64
|
+
if (source.end_line == null) source.end_line = range.end_line;
|
|
65
|
+
}
|
|
66
|
+
return source;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (toolName === 'list') {
|
|
70
|
+
const value = String(source.path || source.dir || source.directory || stringValue || '.').trim();
|
|
71
|
+
return { ...source, path: value || '.' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (toolName === 'glob') {
|
|
75
|
+
const pattern = String(source.pattern || source.glob || source.query || stringValue || '').trim();
|
|
76
|
+
if (pattern) source.pattern = pattern;
|
|
77
|
+
if (!source.path && source.directory) source.path = source.directory;
|
|
78
|
+
return source;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (toolName === 'grep') {
|
|
82
|
+
const pattern = String(source.pattern || source.query || source.symbol || source.q || stringValue || '').trim();
|
|
83
|
+
if (pattern) source.pattern = pattern;
|
|
84
|
+
if (!source.path && (source.directory || source.dir || source.cwd)) {
|
|
85
|
+
source.path = source.directory || source.dir || source.cwd;
|
|
86
|
+
}
|
|
87
|
+
return source;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (toolName === 'write') {
|
|
91
|
+
const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
|
|
92
|
+
if (value) source.path = value;
|
|
93
|
+
if (source.content == null && source.text != null) source.content = source.text;
|
|
94
|
+
if (source.content == null && source.new_content != null) source.content = source.new_content;
|
|
95
|
+
return source;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (toolName === 'edit') {
|
|
99
|
+
const value = String(source.path || source.file || source.file_path || '').trim();
|
|
100
|
+
if (value && !source.path) source.path = value;
|
|
101
|
+
return source;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return source;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function emptyToolResultMarker(toolName) {
|
|
108
|
+
const name = String(toolName || 'tool').trim() || 'tool';
|
|
109
|
+
return `(${name} completed with no output)`;
|
|
110
|
+
}
|
|
111
|
+
|
|
17
112
|
function clipToolResult(result, maxChars = 12000) {
|
|
18
113
|
const raw = typeof result === 'string' ? result : JSON.stringify(result);
|
|
19
114
|
if (!maxChars || raw.length <= maxChars) return raw;
|
|
@@ -374,9 +469,12 @@ function formatToolDisplayName(name, args) {
|
|
|
374
469
|
function formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars) {
|
|
375
470
|
if (toolFormatters && typeof toolFormatters[toolName] === 'function') {
|
|
376
471
|
const formatted = toolFormatters[toolName](toolResult, args);
|
|
377
|
-
if (typeof formatted === 'string')
|
|
472
|
+
if (typeof formatted === 'string') {
|
|
473
|
+
return formatted.trim() ? formatted : emptyToolResultMarker(toolName);
|
|
474
|
+
}
|
|
378
475
|
}
|
|
379
|
-
|
|
476
|
+
const fallback = compactToolResult(toolResult, toolName, args, toolResultMaxChars);
|
|
477
|
+
return String(fallback || '').trim() ? fallback : emptyToolResultMarker(toolName);
|
|
380
478
|
}
|
|
381
479
|
|
|
382
480
|
// ─── Main agent loop ────────────────────────────────────────────────
|
|
@@ -469,8 +567,8 @@ export async function runAgentLoop({
|
|
|
469
567
|
// ─── P1a: Partition into read-only (parallel) and write (serial) ──
|
|
470
568
|
|
|
471
569
|
const callsWithMeta = toolCalls.map((call) => {
|
|
472
|
-
const args = safeJsonParse(call.arguments);
|
|
473
570
|
const toolName = normalizeToolCallName(call.name);
|
|
571
|
+
const args = normalizeToolArguments(toolName, safeJsonParse(call.arguments), call.arguments);
|
|
474
572
|
const displayName = formatToolDisplayName(toolName, args);
|
|
475
573
|
const isReadOnly = READ_ONLY_TOOLS.has(toolName);
|
|
476
574
|
return { call, args, toolName, displayName, isReadOnly };
|
|
@@ -2,6 +2,42 @@ import os from 'node:os';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { getShellSystemPrompt } from './shell-profile.js';
|
|
4
4
|
|
|
5
|
+
function getToolFewShotBlock() {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
return `# Tool Examples
|
|
8
|
+
|
|
9
|
+
Use these as style examples for tool calls:
|
|
10
|
+
|
|
11
|
+
Current working directory: ${cwd}
|
|
12
|
+
When a tool takes file_path, build it from the current working directory and prefer absolute paths.
|
|
13
|
+
If the user mentions a project-relative path like src/app.ts, resolve it from ${cwd} instead of guessing parent directories.
|
|
14
|
+
|
|
15
|
+
1. File discovery then read
|
|
16
|
+
User: compare the auth flow
|
|
17
|
+
Assistant: first locate the relevant files
|
|
18
|
+
Tool: glob({"pattern":"src/**/*auth*.ts"})
|
|
19
|
+
Tool: read({"file_path":"${cwd}/src/auth/service.ts"})
|
|
20
|
+
|
|
21
|
+
2. Targeted search then exact text edit
|
|
22
|
+
User: rename loginUser to signInUser
|
|
23
|
+
Assistant: first find the exact occurrences
|
|
24
|
+
Tool: grep({"pattern":"loginUser","path":"src"})
|
|
25
|
+
Tool: edit({"file_path":"${cwd}/src/auth/service.ts","old_string":"loginUser","new_string":"signInUser"})
|
|
26
|
+
|
|
27
|
+
3. Read a specific range
|
|
28
|
+
User: inspect the reducer around line 120
|
|
29
|
+
Assistant: read only the needed range
|
|
30
|
+
Tool: read({"path":"${cwd}/src/store/reducer.ts:110-150"})
|
|
31
|
+
|
|
32
|
+
4. Create a new file
|
|
33
|
+
User: add a notes file
|
|
34
|
+
Assistant: create the file directly
|
|
35
|
+
Tool: write({"file":"${cwd}/notes.txt","text":"todo\\n"})
|
|
36
|
+
|
|
37
|
+
Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
|
|
38
|
+
Prefer explicit absolute file_path values when the current working directory is known.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
function getEnvBlock() {
|
|
6
42
|
const cwd = process.cwd();
|
|
7
43
|
let isGitRepo = false;
|
|
@@ -22,5 +58,7 @@ OS Version: ${os.version || os.release()}
|
|
|
22
58
|
export function buildDefaultSystemPrompt(config = {}) {
|
|
23
59
|
return `${getShellSystemPrompt(config?.shell?.default)}
|
|
24
60
|
|
|
61
|
+
${getToolFewShotBlock()}
|
|
62
|
+
|
|
25
63
|
${getEnvBlock()}`;
|
|
26
64
|
}
|
|
@@ -47,6 +47,16 @@ function normalizeToolCallArguments(argumentsText) {
|
|
|
47
47
|
return '{}';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function normalizeIncomingToolCallArguments(argumentsValue) {
|
|
51
|
+
if (typeof argumentsValue === 'string') return argumentsValue;
|
|
52
|
+
if (argumentsValue == null) return '{}';
|
|
53
|
+
try {
|
|
54
|
+
return JSON.stringify(argumentsValue);
|
|
55
|
+
} catch {
|
|
56
|
+
return '{}';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
function sanitizeGatewayMessages(messages) {
|
|
51
61
|
const source = Array.isArray(messages) ? messages : [];
|
|
52
62
|
return source
|
|
@@ -115,7 +125,18 @@ function buildPayload({ model, temperature, messages, tools, stream = false }) {
|
|
|
115
125
|
return payload;
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
function
|
|
128
|
+
function hasTrailingToolContext(messages) {
|
|
129
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
130
|
+
for (let index = source.length - 1; index >= 0; index -= 1) {
|
|
131
|
+
const message = source[index];
|
|
132
|
+
if (!message || typeof message !== 'object') continue;
|
|
133
|
+
if (message.role === 'tool') return true;
|
|
134
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
119
140
|
const toolCalls = Array.from(toolCallsByIndex.entries())
|
|
120
141
|
.sort((a, b) => a[0] - b[0])
|
|
121
142
|
.map(([, tc], i) => ({
|
|
@@ -126,6 +147,13 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage) {
|
|
|
126
147
|
.filter((tc) => tc.name);
|
|
127
148
|
|
|
128
149
|
if (!text && toolCalls.length === 0) {
|
|
150
|
+
if (hasTrailingToolContext(messages)) {
|
|
151
|
+
return {
|
|
152
|
+
text: '',
|
|
153
|
+
toolCalls: [],
|
|
154
|
+
usage
|
|
155
|
+
};
|
|
156
|
+
}
|
|
129
157
|
throw new Error('Gateway stream returned empty assistant response');
|
|
130
158
|
}
|
|
131
159
|
|
|
@@ -216,7 +244,7 @@ export async function createChatCompletion({
|
|
|
216
244
|
const toolCalls = (message.tool_calls || []).map((tc) => ({
|
|
217
245
|
id: tc.id,
|
|
218
246
|
name: tc.function?.name,
|
|
219
|
-
arguments: tc.function?.arguments
|
|
247
|
+
arguments: normalizeIncomingToolCallArguments(tc.function?.arguments)
|
|
220
248
|
}));
|
|
221
249
|
|
|
222
250
|
if (!text && toolCalls.length === 0) {
|
|
@@ -280,7 +308,9 @@ export async function createChatCompletionStream({
|
|
|
280
308
|
const current = toolCallsByIndex.get(idx) || emptyToolCall(idx);
|
|
281
309
|
if (td.id) current.id = td.id;
|
|
282
310
|
if (td.function?.name) current.name = `${current.name}${td.function.name}`;
|
|
283
|
-
if (td.function?.arguments
|
|
311
|
+
if (td.function?.arguments !== undefined) {
|
|
312
|
+
current.arguments = `${current.arguments}${normalizeIncomingToolCallArguments(td.function.arguments)}`;
|
|
313
|
+
}
|
|
284
314
|
toolCallsByIndex.set(idx, current);
|
|
285
315
|
if (onToolCallDelta) {
|
|
286
316
|
onToolCallDelta({
|
|
@@ -293,5 +323,5 @@ export async function createChatCompletionStream({
|
|
|
293
323
|
}
|
|
294
324
|
}
|
|
295
325
|
|
|
296
|
-
return buildFinalStreamResult(text, toolCallsByIndex, usage);
|
|
326
|
+
return buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
297
327
|
}
|
|
@@ -123,11 +123,11 @@ export function getShellSystemPrompt(value) {
|
|
|
123
123
|
# Using your tools
|
|
124
124
|
|
|
125
125
|
ALWAYS prefer dedicated tools over raw shell commands:
|
|
126
|
-
- Use read to inspect files — NEVER use cat, head, or tail via run
|
|
126
|
+
- Use read to inspect files — NEVER use cat, head, or tail via run. read returns content directly by default; demo-style shapes like {file_path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {file_path:"src/app.ts", offset:10, limit:30} are accepted
|
|
127
127
|
- Use grep to search file contents — NEVER use grep or rg via run
|
|
128
128
|
- Use glob to find files by pattern — NEVER use find via run
|
|
129
|
-
- Use edit to modify existing files — this is the DEFAULT path for code changes
|
|
130
|
-
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files)
|
|
129
|
+
- Use edit to modify existing files — this is the DEFAULT path for code changes. Demo-style aliases like {file_path:"src/app.ts", old_string:"foo", new_string:"bar"} are accepted
|
|
130
|
+
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Aliases like {file:"notes.txt", text:"..."} are accepted
|
|
131
131
|
- Use patch to apply unified diffs
|
|
132
132
|
- Use run for one-shot shell commands: install, build, test, or other finite tasks
|
|
133
133
|
- For long-running processes (dev servers, watchers), use start_service instead of run
|
|
@@ -140,6 +140,17 @@ For services: use start_service to launch, list_services/get_service_status/get_
|
|
|
140
140
|
|
|
141
141
|
Some tools are loaded on demand. If a needed tool is not listed, call tool_search first to load it.
|
|
142
142
|
|
|
143
|
+
Common tool call patterns:
|
|
144
|
+
- Read a file: {path:"src/app.ts"} or {file_path:"src/app.ts", offset:20, limit:40}
|
|
145
|
+
- Read a specific range inline: {path:"src/app.ts:20-60"}
|
|
146
|
+
- Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
|
|
147
|
+
- Find files: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
|
|
148
|
+
- Edit exact text: {file_path:"src/app.ts", old_string:"foo", new_string:"bar"}
|
|
149
|
+
- Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
|
|
150
|
+
- Write a new file: {file:"notes.txt", text:"..."} or {path:"src/page.tsx", content:"..."}
|
|
151
|
+
- When the environment provides a Working directory, prefer absolute file_path values rooted there instead of guessing prefixes
|
|
152
|
+
- If the user gives a relative path like src/app.ts, resolve it from the current Working directory rather than inventing ../ or sibling folders
|
|
153
|
+
|
|
143
154
|
# Doing tasks
|
|
144
155
|
|
|
145
156
|
- You are a terminal-first CLI coding agent, not a generic chat assistant
|