@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.
|
|
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.
|
|
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": "^
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
230
|
+
console.error(`[mcp] Failed to connect "${name}": ${reason}`)
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
return failures
|
package/src/mcp/registry.ts
CHANGED
|
@@ -117,21 +117,19 @@ export function registerMcpServerTools(
|
|
|
117
117
|
const tool = convertMcpTool(serverName, mcpTool)
|
|
118
118
|
|
|
119
119
|
if (toolsMap.has(tool.name)) {
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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.
|
|
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
|
-
|
|
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.
|