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.
@@ -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: ['run_command', 'read_file', 'write_file'],
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
- ['run_command', 'read_file', 'write_file', ...rawTools].filter((name) => String(name) !== 'list_files')
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. For codebase exploration, use the allowed shell search and context commands that best fit the task, then use read_file only when command output is not enough. Use write_file for edits and always provide a concrete file path, not a directory. Avoid unnecessary tool calls.`;
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
- function resolveShell(defaultShell) {
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, command], {
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.kill('SIGTERM');
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
- clearTimeout(timer);
59
- reject(err);
181
+ finalizeReject(err);
60
182
  });
61
183
 
62
184
  child.on('close', (code) => {
63
- clearTimeout(timer);
185
+ if (finalized) return;
64
186
  if (timedOut) {
65
- reject(new Error(`Command timed out after ${timeoutMs}ms`));
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
- resolve({ code, stdout, stderr });
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(baseSystemPrompt || '').trim()}\n\n${guard}\n\n${soulPrompt}`.trim();
56
+ return `${String(promptWithReplyLanguage || '').trim()}\n\n${guard}\n\n${soulPrompt}`.trim();
55
57
  }