codemini-cli 0.1.12 → 0.1.14
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/README.md +58 -0
- package/package.json +1 -1
- package/src/core/agent-loop.js +46 -0
- package/src/core/chat-runtime.js +364 -44
- package/src/core/config-store.js +49 -3
- package/src/core/reply-language.js +25 -0
- package/src/core/shell-profile.js +1 -1
- package/src/core/shell.js +135 -9
- package/src/core/soul.js +3 -1
- package/src/core/tools.js +1284 -10
- package/src/tui/chat-app.js +295 -36
package/src/core/config-store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getConfigFilePath, getLegacyConfigDir } from './paths.js';
|
|
4
|
+
import { normalizeReplyLanguage } from './reply-language.js';
|
|
4
5
|
import { normalizeShellName } from './shell-profile.js';
|
|
5
6
|
|
|
6
7
|
function normalizeUiLanguage(value) {
|
|
@@ -32,7 +33,28 @@ const DEFAULT_CONFIG = {
|
|
|
32
33
|
},
|
|
33
34
|
execution: {
|
|
34
35
|
mode: 'auto',
|
|
35
|
-
always_allow_tools: [
|
|
36
|
+
always_allow_tools: [
|
|
37
|
+
'locate',
|
|
38
|
+
'open_target',
|
|
39
|
+
'edit_target',
|
|
40
|
+
'search_code',
|
|
41
|
+
'read_block',
|
|
42
|
+
'read_symbol_context',
|
|
43
|
+
'validate_edit',
|
|
44
|
+
'replace_block',
|
|
45
|
+
'replace_text',
|
|
46
|
+
'insert_before',
|
|
47
|
+
'insert_after',
|
|
48
|
+
'generate_diff',
|
|
49
|
+
'start_service',
|
|
50
|
+
'list_services',
|
|
51
|
+
'get_service_status',
|
|
52
|
+
'get_service_logs',
|
|
53
|
+
'stop_service',
|
|
54
|
+
'run_command',
|
|
55
|
+
'read_file',
|
|
56
|
+
'write_file'
|
|
57
|
+
],
|
|
36
58
|
max_steps: 16
|
|
37
59
|
},
|
|
38
60
|
sessions: {
|
|
@@ -44,7 +66,8 @@ const DEFAULT_CONFIG = {
|
|
|
44
66
|
timeout_ms: 120000
|
|
45
67
|
},
|
|
46
68
|
ui: {
|
|
47
|
-
language: 'zh'
|
|
69
|
+
language: 'zh',
|
|
70
|
+
reply_language: 'zh'
|
|
48
71
|
},
|
|
49
72
|
soul: {
|
|
50
73
|
preset: 'default',
|
|
@@ -110,10 +133,33 @@ function normalizePolicyLists(config) {
|
|
|
110
133
|
? next.execution.always_allow_tools
|
|
111
134
|
: [];
|
|
112
135
|
next.execution.always_allow_tools = uniqueStrings(
|
|
113
|
-
[
|
|
136
|
+
[
|
|
137
|
+
'locate',
|
|
138
|
+
'open_target',
|
|
139
|
+
'edit_target',
|
|
140
|
+
'search_code',
|
|
141
|
+
'read_block',
|
|
142
|
+
'read_symbol_context',
|
|
143
|
+
'validate_edit',
|
|
144
|
+
'replace_block',
|
|
145
|
+
'replace_text',
|
|
146
|
+
'insert_before',
|
|
147
|
+
'insert_after',
|
|
148
|
+
'generate_diff',
|
|
149
|
+
'start_service',
|
|
150
|
+
'list_services',
|
|
151
|
+
'get_service_status',
|
|
152
|
+
'get_service_logs',
|
|
153
|
+
'stop_service',
|
|
154
|
+
'run_command',
|
|
155
|
+
'read_file',
|
|
156
|
+
'write_file',
|
|
157
|
+
...rawTools
|
|
158
|
+
].filter((name) => String(name) !== 'list_files')
|
|
114
159
|
);
|
|
115
160
|
next.ui = next.ui || {};
|
|
116
161
|
next.ui.language = normalizeUiLanguage(next.ui.language);
|
|
162
|
+
next.ui.reply_language = normalizeReplyLanguage(next.ui.reply_language);
|
|
117
163
|
next.policy = next.policy || {};
|
|
118
164
|
next.policy.command_allowlist = uniqueStrings(
|
|
119
165
|
Array.isArray(next.policy.command_allowlist) ? next.policy.command_allowlist : []
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function normalizeReplyLanguage(value) {
|
|
2
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
3
|
+
if (!raw) return 'zh';
|
|
4
|
+
if (['en', 'en-us', 'en_us', 'english'].includes(raw)) return 'en';
|
|
5
|
+
if (['zh', 'zh-cn', 'zh_cn', 'cn', 'chinese', '中文', '简体中文'].includes(raw)) return 'zh';
|
|
6
|
+
return 'zh';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildSystemPromptWithReplyLanguage(baseSystemPrompt, config = {}) {
|
|
10
|
+
const replyLanguage = normalizeReplyLanguage(config?.ui?.reply_language);
|
|
11
|
+
const directive =
|
|
12
|
+
replyLanguage === 'en'
|
|
13
|
+
? [
|
|
14
|
+
'[Reply language]',
|
|
15
|
+
'Respond in English.',
|
|
16
|
+
'Write generated documentation, user-facing text, and code comments in English unless the user explicitly asks for a different language.'
|
|
17
|
+
].join('\n')
|
|
18
|
+
: [
|
|
19
|
+
'[Reply language]',
|
|
20
|
+
'Respond in Simplified Chinese.',
|
|
21
|
+
'Write generated documentation, user-facing text, and code comments in Simplified Chinese unless the user explicitly asks for a different language.'
|
|
22
|
+
].join('\n');
|
|
23
|
+
|
|
24
|
+
return `${String(baseSystemPrompt || '').trim()}\n\n${directive}`.trim();
|
|
25
|
+
}
|
|
@@ -118,5 +118,5 @@ export function getEffectivePolicy(config) {
|
|
|
118
118
|
|
|
119
119
|
export function getShellSystemPrompt(value) {
|
|
120
120
|
const profile = getShellProfile(value);
|
|
121
|
-
return `You are CodeMini CLI working in a ${profile.label} shell environment.
|
|
121
|
+
return `You are CodeMini CLI working in a ${profile.label} shell environment. Prefer the high-level structured workflow first: use locate to find candidates, open_target to inspect the smallest useful block and receive edit metadata, and edit_target to apply minimal edits. When you need lower-level control, use search_code, read_block, read_symbol_context, validate_edit, replace_block, replace_text, insert_before, insert_after, and generate_diff. Use start_service, list_services, get_service_status, get_service_logs, and stop_service for long-running servers or watchers. Use run_command only for one-shot commands that should exit on their own. Use read_file only when structured reads are not enough. Use write_file only for full-file writes and always provide a concrete file path, not a directory. Avoid unnecessary tool calls.`;
|
|
122
122
|
}
|
package/src/core/shell.js
CHANGED
|
@@ -1,6 +1,79 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const LONG_RUNNING_COMMAND_RE =
|
|
4
|
+
/\b(npm\s+(?:run\s+)?(?:start|dev)\b|pnpm\s+(?:run\s+)?(?:start|dev)\b|yarn\s+(?:start|dev)\b|vite\b|next\s+dev\b|webpack\s+serve\b|python\s+-m\s+http\.server\b|serve\b|java\s+-jar\b|mvn(?:w)?\s+spring-boot:run\b|gradle(?:w)?\s+bootRun\b|gradle(?:w)?\s+run\b|java\b.*\bserver\b|dotnet\s+run\b|go\s+run\b.*\b(server|cmd\/server|main\.go)\b|air\b|cargo\s+run\b.*\b(server|api|web)\b|cargo\s+watch\s+-x\s+run\b)/i;
|
|
5
|
+
const GENERIC_LONG_RUNNING_HINT_RE = /\b(start|serve|server|dev|preview|watch)\b/i;
|
|
6
|
+
const READY_OUTPUT_PATTERNS = [
|
|
7
|
+
/\bready\b/i,
|
|
8
|
+
/\bcompiled successfully\b/i,
|
|
9
|
+
/\blocal:\s*https?:\/\//i,
|
|
10
|
+
/\blistening on\b/i,
|
|
11
|
+
/\bserver running\b/i,
|
|
12
|
+
/\brunning at\b/i,
|
|
13
|
+
/\bserving at\b/i,
|
|
14
|
+
/\bstarted\b.*\bin\b/i,
|
|
15
|
+
/\bstarted\s+[A-Za-z0-9_$.-]+\s+in\b/i,
|
|
16
|
+
/\btomcat started on port\(s\):/i,
|
|
17
|
+
/\bnetty started on port/i,
|
|
18
|
+
/\bnow listening on:\s*https?:\/\//i,
|
|
19
|
+
/\bapplication started\./i,
|
|
20
|
+
/\bserving http on\b/i,
|
|
21
|
+
/\bstarting development server at\b/i,
|
|
22
|
+
/\bactix web server running on\b/i,
|
|
23
|
+
/\bhttp:\/\/127\.0\.0\.1\b/i,
|
|
24
|
+
/\bhttp:\/\/localhost\b/i
|
|
25
|
+
];
|
|
26
|
+
const AUTO_STOP_GRACE_MS = 150;
|
|
27
|
+
const LONG_RUNNING_STARTUP_WINDOW_MS = 1500;
|
|
28
|
+
|
|
29
|
+
export function isLikelyLongRunningCommand(command) {
|
|
30
|
+
const value = String(command || '');
|
|
31
|
+
return LONG_RUNNING_COMMAND_RE.test(value) || GENERIC_LONG_RUNNING_HINT_RE.test(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function hasReadyOutput(text) {
|
|
35
|
+
const value = String(text || '');
|
|
36
|
+
return READY_OUTPUT_PATTERNS.some((pattern) => pattern.test(value));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function collectDescendantPids(rootPid, seen = new Set()) {
|
|
40
|
+
const pid = Number(rootPid);
|
|
41
|
+
if (!Number.isInteger(pid) || pid <= 0 || seen.has(pid) || process.platform === 'win32') {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
seen.add(pid);
|
|
45
|
+
const result = spawnSync('ps', ['-o', 'pid=', '--ppid', String(pid)], { encoding: 'utf8' });
|
|
46
|
+
if (result.status !== 0 || !result.stdout) return [];
|
|
47
|
+
const directChildren = result.stdout
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map((value) => Number(String(value || '').trim()))
|
|
50
|
+
.filter((value) => Number.isInteger(value) && value > 0);
|
|
51
|
+
const descendants = [];
|
|
52
|
+
for (const childPid of directChildren) {
|
|
53
|
+
if (seen.has(childPid)) continue;
|
|
54
|
+
descendants.push(childPid);
|
|
55
|
+
descendants.push(...collectDescendantPids(childPid, seen));
|
|
56
|
+
}
|
|
57
|
+
return descendants;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function terminateChild(child, signal = 'SIGTERM') {
|
|
61
|
+
if (!child) return;
|
|
62
|
+
const pid = Number(child.pid);
|
|
63
|
+
if (process.platform !== 'win32' && Number.isInteger(pid) && pid > 0) {
|
|
64
|
+
const descendants = collectDescendantPids(pid);
|
|
65
|
+
for (const targetPid of descendants.reverse()) {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(targetPid, signal);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
child.kill(signal);
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveShell(defaultShell) {
|
|
4
77
|
if (process.platform === 'win32') {
|
|
5
78
|
if (defaultShell === 'cmd') {
|
|
6
79
|
return { command: 'cmd.exe', args: ['/d', '/s', '/c'] };
|
|
@@ -30,9 +103,13 @@ export function runShellCommand({
|
|
|
30
103
|
timeoutMs = 120000
|
|
31
104
|
}) {
|
|
32
105
|
const shellSpec = resolveShell(shell);
|
|
106
|
+
const shellCommand =
|
|
107
|
+
process.platform !== 'win32' && /(?:^|\/)bash(?:\.exe)?$/i.test(shellSpec.command)
|
|
108
|
+
? `exec ${command}`
|
|
109
|
+
: command;
|
|
33
110
|
|
|
34
111
|
return new Promise((resolve, reject) => {
|
|
35
|
-
const child = spawn(shellSpec.command, [...shellSpec.args,
|
|
112
|
+
const child = spawn(shellSpec.command, [...shellSpec.args, shellCommand], {
|
|
36
113
|
cwd,
|
|
37
114
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
38
115
|
});
|
|
@@ -40,32 +117,81 @@ export function runShellCommand({
|
|
|
40
117
|
let stdout = '';
|
|
41
118
|
let stderr = '';
|
|
42
119
|
let timedOut = false;
|
|
120
|
+
let autoStopped = false;
|
|
121
|
+
let stopReason = '';
|
|
122
|
+
let finalized = false;
|
|
123
|
+
const longRunningCommand = isLikelyLongRunningCommand(command);
|
|
124
|
+
const autoStopWindowMs = longRunningCommand
|
|
125
|
+
? Math.min(LONG_RUNNING_STARTUP_WINDOW_MS, Math.max(350, Math.floor(timeoutMs * 0.6)))
|
|
126
|
+
: 0;
|
|
127
|
+
|
|
128
|
+
const finalizeResolve = (value) => {
|
|
129
|
+
if (finalized) return;
|
|
130
|
+
finalized = true;
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
if (autoStopTimer) clearTimeout(autoStopTimer);
|
|
133
|
+
resolve(value);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const finalizeReject = (error) => {
|
|
137
|
+
if (finalized) return;
|
|
138
|
+
finalized = true;
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
if (autoStopTimer) clearTimeout(autoStopTimer);
|
|
141
|
+
reject(error);
|
|
142
|
+
};
|
|
43
143
|
|
|
44
144
|
const timer = setTimeout(() => {
|
|
45
145
|
timedOut = true;
|
|
46
|
-
child
|
|
146
|
+
terminateChild(child, 'SIGTERM');
|
|
47
147
|
}, timeoutMs);
|
|
148
|
+
const autoStopTimer =
|
|
149
|
+
autoStopWindowMs > 0
|
|
150
|
+
? setTimeout(() => {
|
|
151
|
+
finalizeAutoStop('startup_window');
|
|
152
|
+
}, autoStopWindowMs)
|
|
153
|
+
: null;
|
|
154
|
+
|
|
155
|
+
const finalizeAutoStop = (reason) => {
|
|
156
|
+
if (timedOut || autoStopped || finalized) return;
|
|
157
|
+
autoStopped = true;
|
|
158
|
+
stopReason = reason;
|
|
159
|
+
terminateChild(child, 'SIGTERM');
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
terminateChild(child, 'SIGKILL');
|
|
162
|
+
}, AUTO_STOP_GRACE_MS);
|
|
163
|
+
finalizeResolve({ code: 0, stdout, stderr, auto_stopped: true, stop_reason: stopReason });
|
|
164
|
+
};
|
|
48
165
|
|
|
49
166
|
child.stdout.on('data', (chunk) => {
|
|
50
167
|
stdout += chunk.toString();
|
|
168
|
+
if (longRunningCommand && hasReadyOutput(stdout)) {
|
|
169
|
+
finalizeAutoStop('ready_output');
|
|
170
|
+
}
|
|
51
171
|
});
|
|
52
172
|
|
|
53
173
|
child.stderr.on('data', (chunk) => {
|
|
54
174
|
stderr += chunk.toString();
|
|
175
|
+
if (longRunningCommand && hasReadyOutput(stderr)) {
|
|
176
|
+
finalizeAutoStop('ready_output');
|
|
177
|
+
}
|
|
55
178
|
});
|
|
56
179
|
|
|
57
180
|
child.on('error', (err) => {
|
|
58
|
-
|
|
59
|
-
reject(err);
|
|
181
|
+
finalizeReject(err);
|
|
60
182
|
});
|
|
61
183
|
|
|
62
184
|
child.on('close', (code) => {
|
|
63
|
-
|
|
185
|
+
if (finalized) return;
|
|
64
186
|
if (timedOut) {
|
|
65
|
-
|
|
187
|
+
finalizeReject(new Error(`Command timed out after ${timeoutMs}ms`));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (autoStopped) {
|
|
191
|
+
finalizeResolve({ code: 0, stdout, stderr, auto_stopped: true, stop_reason: stopReason });
|
|
66
192
|
return;
|
|
67
193
|
}
|
|
68
|
-
|
|
194
|
+
finalizeResolve({ code, stdout, stderr });
|
|
69
195
|
});
|
|
70
196
|
});
|
|
71
197
|
}
|
package/src/core/soul.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { getBaseConfigDir } from './paths.js';
|
|
5
|
+
import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
|
|
5
6
|
|
|
6
7
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const BUNDLED_SOULS_DIR = path.resolve(MODULE_DIR, '..', '..', 'souls');
|
|
@@ -45,11 +46,12 @@ export async function loadSoulPrompt(config = {}) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export async function buildSystemPromptWithSoul(baseSystemPrompt, config = {}) {
|
|
49
|
+
const promptWithReplyLanguage = buildSystemPromptWithReplyLanguage(baseSystemPrompt, config);
|
|
48
50
|
const soulPrompt = await loadSoulPrompt(config);
|
|
49
51
|
const guard = [
|
|
50
52
|
'[Soul guard]',
|
|
51
53
|
'Apply this soul to response tone only.',
|
|
52
54
|
'Response tone only: do not change plans, code, tests, file formats, or technical decisions.'
|
|
53
55
|
].join('\n');
|
|
54
|
-
return `${String(
|
|
56
|
+
return `${String(promptWithReplyLanguage || '').trim()}\n\n${guard}\n\n${soulPrompt}`.trim();
|
|
55
57
|
}
|