@miphamai/cli 0.80.0 → 0.81.0

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": "@miphamai/cli",
3
- "version": "0.80.0",
3
+ "version": "0.81.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -51,10 +51,10 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@mipham/shared": "workspace:*",
54
- "@types/bun": "^1.3.14",
54
+ "@types/bun": "^1.4.2",
55
55
  "@types/node": "^22.19.19",
56
56
  "@types/react": "^19.3.0",
57
57
  "ink-testing-library": "^4.0.0",
58
- "vitest": "^4.1.7"
58
+ "vitest": "^5.0.0"
59
59
  }
60
60
  }
@@ -63,6 +63,56 @@ const WRITER_COMMANDS = new Set([
63
63
  'truncate',
64
64
  ])
65
65
 
66
+ /**
67
+ * Commands that wrap another command as their payload. A Read/Edit deny rule
68
+ * must still apply when the reader/writer is wrapped (`sudo cat X`, `env -C / X`,
69
+ * `timeout 5 X`) — otherwise the wrapper silently bypasses the rule.
70
+ */
71
+ const PREFIX_COMMANDS = new Set([
72
+ 'sudo',
73
+ 'doas',
74
+ 'nohup',
75
+ 'command',
76
+ 'exec',
77
+ 'nice',
78
+ 'timeout',
79
+ 'env',
80
+ 'xargs',
81
+ 'eval',
82
+ 'stdbuf',
83
+ ])
84
+
85
+ /** Value-taking options of wrapper commands (consume the following token). */
86
+ const PREFIX_VALUE_OPTIONS = new Set([
87
+ '-u',
88
+ '--user',
89
+ '-g',
90
+ '--group',
91
+ '-h',
92
+ '--host',
93
+ '-p',
94
+ '--prompt', // sudo/doas
95
+ '-n',
96
+ '--adjustment', // nice (and xargs --max-args)
97
+ '-k',
98
+ '--kill-after',
99
+ '-s',
100
+ '--signal', // timeout
101
+ '-C',
102
+ '--chdir',
103
+ '--unset',
104
+ '-S',
105
+ '--split-string', // env
106
+ '-a',
107
+ '--arg-file',
108
+ '-E',
109
+ '--eof',
110
+ '-I',
111
+ '--replace',
112
+ '-P',
113
+ '--max-procs', // xargs
114
+ ])
115
+
66
116
  /**
67
117
  * Split a (possibly compound) shell command into simple-command segments, so a
68
118
  * Bash(pattern) rule matches any segment rather than only the whole string
@@ -156,6 +206,8 @@ function flattenCommand(command: string): string[] {
156
206
  const out: string[] = []
157
207
  for (const seg of splitShellSegments(command)) {
158
208
  out.push(seg)
209
+ const stripped = stripPrefixCommand(seg)
210
+ if (stripped !== seg) out.push(stripped)
159
211
  for (const inner of extractSubstitutions(seg)) {
160
212
  out.push(...flattenCommand(inner))
161
213
  }
@@ -163,6 +215,46 @@ function flattenCommand(command: string): string[] {
163
215
  return out
164
216
  }
165
217
 
218
+ /**
219
+ * Resolve the effective command and its arguments after any wrapper prefix
220
+ * commands (`sudo`, `env`, `timeout`, …). Skips the wrapper and its own options
221
+ * (flags, the values of value-taking flags, `env`'s `VAR=value` assignments, and
222
+ * `timeout`'s positional duration) to reach the real command. Conservative, not a
223
+ * full parser: only recognized wrappers are stripped, so an unrecognized token is
224
+ * always treated as the command (never skipped) — which over-matches, the safe
225
+ * direction for a deny rule.
226
+ */
227
+ function effectiveCommand(tokens: string[]): { base: string; args: string[] } {
228
+ let i = 0
229
+ while (i < tokens.length) {
230
+ const name = (tokens[i] || '').split('/').pop() || ''
231
+ if (!PREFIX_COMMANDS.has(name)) break
232
+ i++ // skip the wrapper
233
+ while (i < tokens.length && tokens[i]!.startsWith('-')) {
234
+ const opt = tokens[i]!
235
+ i++
236
+ if (PREFIX_VALUE_OPTIONS.has(opt) && i < tokens.length) i++ // skip the flag's value
237
+ }
238
+ if (name === 'env') {
239
+ while (i < tokens.length && tokens[i]!.includes('=')) i++ // `VAR=value` assignments
240
+ }
241
+ if (name === 'timeout') i++ // positional duration
242
+ }
243
+ if (i >= tokens.length) return { base: '', args: [] }
244
+ return { base: (tokens[i] || '').split('/').pop() || '', args: tokens.slice(i + 1) }
245
+ }
246
+
247
+ /** A command with any wrapper prefix commands stripped, so `sudo rm -rf /`
248
+ * matches a `Bash(rm *)` rule. Returns the input unchanged when there is no
249
+ * wrapper (or nothing but wrappers). */
250
+ function stripPrefixCommand(command: string): string {
251
+ const tokens = command.split(/\s+/).filter(Boolean)
252
+ if (tokens.length === 0) return command
253
+ const { base, args } = effectiveCommand(tokens)
254
+ if (!base) return command
255
+ return [base, ...args].join(' ')
256
+ }
257
+
166
258
  /**
167
259
  * Detect reader/writer commands at the front of each shell segment and recurse
168
260
  * into command substitutions, so `echo $(cat .git-credentials)` is caught.
@@ -171,8 +263,7 @@ function scanReaderWriterCommands(command: string, read: string[], write: string
171
263
  for (const seg of splitShellSegments(command)) {
172
264
  const tokens = seg.split(/\s+/).filter(Boolean)
173
265
  if (tokens.length > 0) {
174
- const base = (tokens[0] || '').split('/').pop() || ''
175
- const args = tokens.slice(1)
266
+ const { base, args } = effectiveCommand(tokens)
176
267
  if (READER_COMMANDS.has(base)) {
177
268
  // `sed -i` / `perl -i` read AND write their file args.
178
269
  const inPlace = args.some((a) => a === '-i' || a.startsWith('--in-place'))
package/src/index.tsx CHANGED
@@ -216,7 +216,7 @@ async function connectMcpServers(
216
216
  await mcp.connect(server)
217
217
  const count = registerMcpServerTools(server.name, tools)
218
218
  if (count > 0) {
219
- process.stderr.write(`[mcp] "${server.name}": registered ${count} tools\n`)
219
+ console.log(`[mcp] "${server.name}": registered ${count} tools`)
220
220
  }
221
221
  }),
222
222
  )
@@ -227,7 +227,7 @@ async function connectMcpServers(
227
227
  const name = mcpServers[i]!.name
228
228
  const reason = String(result.reason)
229
229
  failures.push({ name, reason })
230
- process.stderr.write(`[mcp] Failed to connect "${name}": ${reason}\n`)
230
+ console.error(`[mcp] Failed to connect "${name}": ${reason}`)
231
231
  }
232
232
  }
233
233
  return failures
@@ -117,21 +117,19 @@ export function registerMcpServerTools(
117
117
  const tool = convertMcpTool(serverName, mcpTool)
118
118
 
119
119
  if (toolsMap.has(tool.name)) {
120
- process.stderr.write(
121
- t('errors.mcp_register_collision', { name: tool.name, server: serverName }) + '\n',
122
- )
120
+ console.error(t('errors.mcp_register_collision', { name: tool.name, server: serverName }))
123
121
  continue
124
122
  }
125
123
 
126
124
  toolsMap.set(tool.name, tool)
127
125
  registered++
128
126
  } catch (err) {
129
- process.stderr.write(
127
+ console.error(
130
128
  t('errors.mcp_register_failed', {
131
129
  tool: mcpTool.name,
132
130
  server: serverName,
133
131
  error: String(err),
134
- }) + '\n',
132
+ }),
135
133
  )
136
134
  }
137
135
  }
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.80.0' as const
12
+ export const PACKAGE_VERSION = '0.81.0' as const
13
13
 
14
14
  /** npm install 全局安装命令 */
15
15
  export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
@@ -23,7 +23,21 @@ function detectParallelism(): number {
23
23
  return os.cpus().length || 1
24
24
  }
25
25
 
26
- const MAX_CONCURRENT = Math.max(1, Math.min(16, detectParallelism()))
26
+ /**
27
+ * Resolve the concurrency cap for parallel() fan-out. Defaults to CPU-derived
28
+ * parallelism capped at 16. An explicit `MIPHAM_WORKFLOW_MAX_CONCURRENT_AGENTS`
29
+ * (1–256) overrides it, for inference-bound fan-outs whose bottleneck is LLM
30
+ * latency rather than CPU. Invalid / out-of-range values fall back to the default.
31
+ */
32
+ export function resolveMaxConcurrent(envValue: string | undefined): number {
33
+ if (envValue !== undefined && envValue !== '') {
34
+ const n = Number(envValue)
35
+ if (Number.isInteger(n) && n >= 1) return Math.min(n, 256)
36
+ }
37
+ return Math.max(1, Math.min(16, detectParallelism()))
38
+ }
39
+
40
+ const MAX_CONCURRENT = resolveMaxConcurrent(process.env.MIPHAM_WORKFLOW_MAX_CONCURRENT_AGENTS)
27
41
 
28
42
  /**
29
43
  * Simple async semaphore for concurrency limiting.