@miphamai/cli 0.62.0 → 0.63.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 +1 -1
- package/src/agent/effectiveness-tracker.ts +2 -6
- package/src/agent/message-bus.ts +10 -0
- package/src/agent/recoverable-failure.ts +16 -10
- package/src/agent/sub-agent.ts +15 -3
- package/src/core/crsi-sandbox.ts +25 -5
- package/src/core/engine.ts +30 -2
- package/src/core/hooks-executor.ts +2 -1
- package/src/core/session-name.ts +12 -0
- package/src/index.tsx +20 -8
- package/src/mcp/connect-failures.ts +15 -0
- package/src/plugin/claude-plugin.ts +1 -1
- package/src/plugin/plugin-validator.ts +1 -1
- package/src/shared/package-info.ts +1 -1
package/package.json
CHANGED
|
@@ -47,11 +47,7 @@ export class EffectivenessTracker {
|
|
|
47
47
|
if (eff) eff.decisionId = decisionId
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
recordApplication(
|
|
51
|
-
ruleId: string,
|
|
52
|
-
success: boolean,
|
|
53
|
-
opts?: { toolName?: string; error?: string },
|
|
54
|
-
): void {
|
|
50
|
+
recordApplication(ruleId: string, success: boolean, opts?: { error?: string }): void {
|
|
55
51
|
let eff = this.data.get(ruleId)
|
|
56
52
|
if (!eff) {
|
|
57
53
|
eff = {
|
|
@@ -69,7 +65,7 @@ export class EffectivenessTracker {
|
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
// 可恢复/环境性失败:仍记录,但不进成功率分母(防误降级/误禁用能用的规则)。
|
|
72
|
-
if (!success && opts?.error && isRecoverableToolFailure(opts.
|
|
68
|
+
if (!success && opts?.error && isRecoverableToolFailure(opts.error)) {
|
|
73
69
|
eff.recoverableCount = (eff.recoverableCount ?? 0) + 1
|
|
74
70
|
return
|
|
75
71
|
}
|
package/src/agent/message-bus.ts
CHANGED
|
@@ -24,6 +24,16 @@ export interface AgentMessage {
|
|
|
24
24
|
type: AgentMessageType
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Format an inbound message for injection into the conversation as a
|
|
29
|
+
* user-role notice. Collapsed to one line (`Message from @<sender>: <summary>`)
|
|
30
|
+
* so cross-session peer messages don't bloat the context; the full body stays
|
|
31
|
+
* in the bus for Ctrl+O expansion.
|
|
32
|
+
*/
|
|
33
|
+
export function formatInboundMessage(msg: AgentMessage): string {
|
|
34
|
+
return `Message from @${msg.from}: ${msg.summary}`
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
export class AgentMessageBus {
|
|
28
38
|
private messages: AgentMessage[] = []
|
|
29
39
|
private idCounter = 0
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 判「可恢复/环境性」失败 vs 真缺陷(borrow
|
|
2
|
+
* 判「可恢复/环境性」失败 vs 真缺陷(borrow openprabs is_recoverable_tool_failure 思想)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 可恢复失败(网络超时、连接拒绝、DNS 解析失败、环境权限)不该进成功率分母,否则会把
|
|
5
|
+
* 成功率拉低、诱导 EffectivenessTracker 禁用能用的规则(openprabs #236 的坑:stale-hash
|
|
6
|
+
* 重试被误判成工具缺陷,把能用的 hashline_edit 禁了)。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 刻意排除 ENOENT(no such file or directory):缺文件/路径错更可能是规则本身指错了
|
|
9
|
+
* 路径(幻觉),而非环境瞬态,须计入分母(2026-08-27 review M1)。
|
|
10
|
+
*
|
|
11
|
+
* 不区分工具:这些错误模式(超时/连接/DNS/权限)本身即工具无关的环境信号。旧的
|
|
12
|
+
* `toolName !== 'Bash'` 守卫漏掉 Write/Grep 规则(2026-08-27 review M2),已移除。
|
|
13
|
+
*
|
|
14
|
+
* fail-closed:无 error 一律按真失败算(证不了可恢复就不排除)。
|
|
9
15
|
*/
|
|
10
16
|
const RECOVERABLE_PATTERNS: RegExp[] = [
|
|
11
|
-
/\btimed?\s?out\b/i, // timeout / timed out(网络慢 /
|
|
12
|
-
/(ECONNREFUSED|connection refused|not connected|network unreachable)/i, // 服务未起
|
|
13
|
-
/(
|
|
17
|
+
/\btimed?\s?out\b/i, // timeout / timed out / timedout(网络慢 / 服务慢)
|
|
18
|
+
/(ECONNREFUSED|ECONNRESET|ETIMEDOUT|connection refused|not connected|network unreachable)/i, // 服务未起 / 网络超时
|
|
19
|
+
/(could not resolve host|name or service not known|getaddrinfo|ENOTFOUND)/i, // DNS 解析失败
|
|
14
20
|
/(permission denied|EACCES)/i, // 环境权限
|
|
15
21
|
]
|
|
16
22
|
|
|
17
|
-
export function isRecoverableToolFailure(
|
|
18
|
-
if (!error
|
|
23
|
+
export function isRecoverableToolFailure(error?: string): boolean {
|
|
24
|
+
if (!error) return false
|
|
19
25
|
const e = error.toLowerCase()
|
|
20
26
|
return RECOVERABLE_PATTERNS.some((re) => re.test(e))
|
|
21
27
|
}
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -319,6 +319,7 @@ export class SubAgent {
|
|
|
319
319
|
|
|
320
320
|
const chunks: string[] = []
|
|
321
321
|
const MAX_TOOL_TURNS = options.maxTurns || 5
|
|
322
|
+
let hitMaxTurns = false
|
|
322
323
|
|
|
323
324
|
let currentMessages = messages
|
|
324
325
|
let currentSystemPrompt = systemPrompt
|
|
@@ -360,7 +361,7 @@ export class SubAgent {
|
|
|
360
361
|
})
|
|
361
362
|
}
|
|
362
363
|
if (chunk.type === 'error') {
|
|
363
|
-
throw new Error(`Sub-agent execution failed: ${chunk.error}`)
|
|
364
|
+
throw new Error(`Sub-agent execution failed (model ${finalModel}): ${chunk.error}`)
|
|
364
365
|
}
|
|
365
366
|
if (chunk.type === 'stop') {
|
|
366
367
|
break
|
|
@@ -501,6 +502,9 @@ export class SubAgent {
|
|
|
501
502
|
|
|
502
503
|
// Don't send system prompt on subsequent turns
|
|
503
504
|
currentSystemPrompt = ''
|
|
505
|
+
|
|
506
|
+
// On the final permitted turn we still used tools → hit the cap.
|
|
507
|
+
if (turn === MAX_TOOL_TURNS - 1) hitMaxTurns = true
|
|
504
508
|
}
|
|
505
509
|
} catch (err) {
|
|
506
510
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
|
@@ -509,10 +513,18 @@ export class SubAgent {
|
|
|
509
513
|
if (err instanceof Error && err.message.startsWith('Sub-agent')) {
|
|
510
514
|
throw err
|
|
511
515
|
}
|
|
512
|
-
throw new Error(`Sub-agent execution failed: ${String(err)}`)
|
|
516
|
+
throw new Error(`Sub-agent execution failed (model ${finalModel}): ${String(err)}`)
|
|
513
517
|
}
|
|
514
518
|
|
|
515
|
-
|
|
519
|
+
const result = chunks.join('')
|
|
520
|
+
if (hitMaxTurns) {
|
|
521
|
+
return (
|
|
522
|
+
`[partial result — sub-agent reached its ${MAX_TOOL_TURNS}-turn limit; ` +
|
|
523
|
+
'task may be incomplete. Use SendMessage to continue this sub-agent.]\n\n' +
|
|
524
|
+
result
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
return result
|
|
516
528
|
}
|
|
517
529
|
|
|
518
530
|
/**
|
package/src/core/crsi-sandbox.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { execSync } from 'node:child_process'
|
|
18
18
|
import { mkdirSync, rmSync, existsSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
|
|
19
|
-
import { join, resolve, sep } from 'node:path'
|
|
19
|
+
import { join, resolve, sep, posix } from 'node:path'
|
|
20
20
|
import { tmpdir, homedir } from 'node:os'
|
|
21
21
|
import { randomUUID } from 'node:crypto'
|
|
22
22
|
|
|
@@ -115,6 +115,12 @@ const PROTECTED_PATHS = [
|
|
|
115
115
|
'apps/cli/src/core/crsi-sandbox.ts',
|
|
116
116
|
'apps/cli/src/core/crsi-producer.ts',
|
|
117
117
|
'apps/cli/src/core/proposal-guard.ts',
|
|
118
|
+
// 2026-08-27 review 补齐:运行时执行/评估器,改掉会削弱 grader 或安全边界
|
|
119
|
+
'apps/cli/src/core/crsi-modify.ts',
|
|
120
|
+
'apps/cli/src/core/rule-engine.ts',
|
|
121
|
+
'apps/cli/src/core/red-team.ts',
|
|
122
|
+
'apps/cli/src/core/error-signature-db.ts',
|
|
123
|
+
'apps/cli/src/core/preflight-checker.ts',
|
|
118
124
|
]
|
|
119
125
|
|
|
120
126
|
/** 是否命中只读边界。前缀匹配,目录条目以 `/` 结尾。 */
|
|
@@ -130,13 +136,24 @@ export function isProtectedPath(filePath: string): boolean {
|
|
|
130
136
|
*
|
|
131
137
|
* 返回错误字符串(拒绝理由),合法时返回 null。
|
|
132
138
|
*/
|
|
133
|
-
export function validateBlastRadius(proposal: {
|
|
139
|
+
export function validateBlastRadius(proposal: {
|
|
140
|
+
filePath?: string
|
|
141
|
+
blastRadius?: string[]
|
|
142
|
+
}): string | null {
|
|
143
|
+
const filePath = proposal.filePath
|
|
134
144
|
if (!proposal.blastRadius || proposal.blastRadius.length === 0) {
|
|
135
145
|
return (
|
|
136
146
|
'blast radius 未声明:自修改必须枚举触及的全部代码路径。' +
|
|
137
147
|
'教训:两条渲染路径只接一条 = 局部正确全局遗漏。'
|
|
138
148
|
)
|
|
139
149
|
}
|
|
150
|
+
// 2026-08-27 review:blastRadius 不能只自证「非空」,必须覆盖被修改文件本身
|
|
151
|
+
// (前缀匹配,目录条目以 / 结尾),否则声明形同虚设。
|
|
152
|
+
if (filePath && !proposal.blastRadius.some((p) => filePath === p || filePath.startsWith(p))) {
|
|
153
|
+
return (
|
|
154
|
+
`blast radius 未覆盖目标文件 "${filePath}":` + '声明必须包含被修改文件本身(前缀匹配)。'
|
|
155
|
+
)
|
|
156
|
+
}
|
|
140
157
|
return null
|
|
141
158
|
}
|
|
142
159
|
|
|
@@ -225,7 +242,10 @@ export class CrsiSandbox {
|
|
|
225
242
|
}
|
|
226
243
|
|
|
227
244
|
const worktreeRoot = resolve(this.worktreePath)
|
|
228
|
-
|
|
245
|
+
// Normalize "./" and "subdir/../" so the protected-path guard can't be evaded
|
|
246
|
+
// by a path that resolves back inside the worktree (raw prefix match would miss it).
|
|
247
|
+
const normalizedFilePath = posix.normalize(mod.filePath)
|
|
248
|
+
const targetPath = resolve(this.worktreePath, normalizedFilePath)
|
|
229
249
|
|
|
230
250
|
// Path traversal guard: reject any target that resolves outside the sandbox worktree
|
|
231
251
|
if (targetPath !== worktreeRoot && !targetPath.startsWith(worktreeRoot + sep)) {
|
|
@@ -237,7 +257,7 @@ export class CrsiSandbox {
|
|
|
237
257
|
|
|
238
258
|
// Protected-path guard: the self-improvement loop must not modify the
|
|
239
259
|
// constitution, eval harness, or improvement machinery itself.
|
|
240
|
-
if (isProtectedPath(
|
|
260
|
+
if (isProtectedPath(normalizedFilePath)) {
|
|
241
261
|
result.error = `Protected path: "${mod.filePath}" is read-only to the self-improvement loop.`
|
|
242
262
|
result.phase = 'failed'
|
|
243
263
|
this.sessionReport.modifications.push(result)
|
|
@@ -275,7 +295,7 @@ export class CrsiSandbox {
|
|
|
275
295
|
|
|
276
296
|
// Generate diff
|
|
277
297
|
try {
|
|
278
|
-
result.diff = execSync(`git diff -- "${
|
|
298
|
+
result.diff = execSync(`git diff -- "${normalizedFilePath}"`, {
|
|
279
299
|
cwd: this.worktreePath,
|
|
280
300
|
timeout: 10_000,
|
|
281
301
|
encoding: 'utf-8',
|
package/src/core/engine.ts
CHANGED
|
@@ -40,7 +40,12 @@ import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './infe
|
|
|
40
40
|
import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
|
|
41
41
|
import { registerWakeupHandler } from '../tools/scheduling/schedule-wakeup'
|
|
42
42
|
import { accumulateGraftSavings } from '../shared/graft-savings'
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
getMessageBus,
|
|
45
|
+
formatInboundMessage,
|
|
46
|
+
type AgentMessage,
|
|
47
|
+
type AgentMessageBus,
|
|
48
|
+
} from '../agent/message-bus'
|
|
44
49
|
import { createT } from '../i18n-core/t'
|
|
45
50
|
import enUS from '../i18n-core/locales/en-US.json'
|
|
46
51
|
import zhCN from '../i18n-core/locales/zh-CN.json'
|
|
@@ -255,6 +260,28 @@ export class QueryEngine {
|
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Drain unread inbound messages from the in-memory bus (addressed to this
|
|
265
|
+
* session or to "main") and inject them into the conversation as user-role
|
|
266
|
+
* notices. Returns the number of messages injected.
|
|
267
|
+
*
|
|
268
|
+
* The bus is the delivery channel for same-process SendMessage ("main",
|
|
269
|
+
* "bg-*", "sub-agent-*") and the re-post target of pollCrossSessionInbox;
|
|
270
|
+
* this is the missing "drain" step that surfaces those messages to the model.
|
|
271
|
+
*/
|
|
272
|
+
drainInboundMessages(bus: AgentMessageBus = getMessageBus()): number {
|
|
273
|
+
const recipients = [...new Set([this.sessionId, 'main'])]
|
|
274
|
+
let injected = 0
|
|
275
|
+
for (const recipient of recipients) {
|
|
276
|
+
for (const msg of bus.poll(recipient)) {
|
|
277
|
+
this.context.addMessage({ role: 'user', content: formatInboundMessage(msg) })
|
|
278
|
+
injected++
|
|
279
|
+
}
|
|
280
|
+
bus.markAllRead(recipient)
|
|
281
|
+
}
|
|
282
|
+
return injected
|
|
283
|
+
}
|
|
284
|
+
|
|
258
285
|
/** Register a hook engine for pre/post tool-use lifecycle events. */
|
|
259
286
|
setHookEngine(hooks: HookEngine): void {
|
|
260
287
|
this.hookEngine = hooks
|
|
@@ -474,6 +501,8 @@ export class QueryEngine {
|
|
|
474
501
|
|
|
475
502
|
// Poll for cross-session messages before each turn
|
|
476
503
|
await this.pollCrossSessionInbox()
|
|
504
|
+
// Drain in-memory bus messages (same-process SendMessage + cross-session re-posts)
|
|
505
|
+
this.drainInboundMessages()
|
|
477
506
|
|
|
478
507
|
// Fire UserPromptSubmit hooks before processing
|
|
479
508
|
if (this.hookEngine) {
|
|
@@ -1204,7 +1233,6 @@ export class QueryEngine {
|
|
|
1204
1233
|
const ruleId = match?.[1]
|
|
1205
1234
|
if (ruleId) {
|
|
1206
1235
|
tracker.recordApplication(ruleId, result.success, {
|
|
1207
|
-
toolName: name,
|
|
1208
1236
|
error: result.error,
|
|
1209
1237
|
})
|
|
1210
1238
|
}
|
|
@@ -49,7 +49,8 @@ function executeCommand(cfg: HookConfig, ctx: HookContext): HookResult {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Non-zero exit: check for block signal (exit code 2)
|
|
52
|
-
|
|
52
|
+
// 截断 stderr 防 MB 级 hook 输出溢出会话(对齐 HTTP hook 的 slice(0,2000))。
|
|
53
|
+
const stderr = (result.stderr?.toString() || '').slice(0, 2000)
|
|
53
54
|
|
|
54
55
|
if (result.status === 2) {
|
|
55
56
|
return {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate the session name (ID) used for cross-session tracking, inbox routing,
|
|
3
|
+
* and session persistence.
|
|
4
|
+
*
|
|
5
|
+
* Includes milliseconds so two sessions launched in the same second do not
|
|
6
|
+
* collide on the shared inbox directory / active-session registry. (2026-08-27
|
|
7
|
+
* review: the old `slice(0, 19)` dropped ms and let same-second launches share
|
|
8
|
+
* one inbox, leaking / misdelivering cross-session messages.)
|
|
9
|
+
*/
|
|
10
|
+
export function generateSessionName(resume: string | undefined, now: Date = new Date()): string {
|
|
11
|
+
return resume || `session-${now.toISOString().replace(/[:.]/g, '-')}`
|
|
12
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -25,6 +25,7 @@ import { loadSessionMemories, getMemoryManager } from './core/memory/memory-load
|
|
|
25
25
|
import { ContextManager } from './core/context'
|
|
26
26
|
import { PrefixCacheTracker } from './core/context-token'
|
|
27
27
|
import { QueryEngine } from './core/engine'
|
|
28
|
+
import { generateSessionName } from './core/session-name'
|
|
28
29
|
import { ExperienceRuleEngine } from './core/rule-engine.js'
|
|
29
30
|
import { SessionLog } from './core/session-log'
|
|
30
31
|
import { SessionStore } from './core/session-store'
|
|
@@ -40,6 +41,7 @@ import { mountConstitution, createConstitution } from './core/constitution-seam'
|
|
|
40
41
|
import { ConstitutionLoader } from './core/constitution-loader'
|
|
41
42
|
import { McpClient } from './mcp/client'
|
|
42
43
|
import { registerMcpServerTools, syncMcpToolsOnChange } from './mcp/registry'
|
|
44
|
+
import { formatMcpConnectFailures, type McpConnectFailure } from './mcp/connect-failures'
|
|
43
45
|
import { AgentRegistry } from './agent/agent-registry'
|
|
44
46
|
import { HookEngine } from './core/hooks'
|
|
45
47
|
import { ArtifactServer } from './artifacts/server'
|
|
@@ -202,8 +204,8 @@ function SetupGate(props: SetupGateProps) {
|
|
|
202
204
|
async function connectMcpServers(
|
|
203
205
|
mcpServers: McpServerConfig[],
|
|
204
206
|
tools: ReturnType<typeof createToolRegistry>,
|
|
205
|
-
): Promise<
|
|
206
|
-
if (mcpServers.length === 0) return
|
|
207
|
+
): Promise<McpConnectFailure[]> {
|
|
208
|
+
if (mcpServers.length === 0) return []
|
|
207
209
|
const mcp = McpClient.getInstance()
|
|
208
210
|
// Wire runtime tool-list changes before connect so mid-connect updates land.
|
|
209
211
|
syncMcpToolsOnChange(mcp, tools)
|
|
@@ -216,14 +218,17 @@ async function connectMcpServers(
|
|
|
216
218
|
}
|
|
217
219
|
}),
|
|
218
220
|
)
|
|
221
|
+
const failures: McpConnectFailure[] = []
|
|
219
222
|
for (let i = 0; i < results.length; i++) {
|
|
220
223
|
const result = results[i]!
|
|
221
224
|
if (result.status === 'rejected') {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
)
|
|
225
|
+
const name = mcpServers[i]!.name
|
|
226
|
+
const reason = String(result.reason)
|
|
227
|
+
failures.push({ name, reason })
|
|
228
|
+
process.stderr.write(`[mcp] Failed to connect "${name}": ${reason}\n`)
|
|
225
229
|
}
|
|
226
230
|
}
|
|
231
|
+
return failures
|
|
227
232
|
}
|
|
228
233
|
|
|
229
234
|
export async function runApp(options: RunOptions): Promise<void> {
|
|
@@ -325,8 +330,7 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
325
330
|
const pluginManager = new PluginManager()
|
|
326
331
|
|
|
327
332
|
// Generate session name for tracking (used by /cd to persist cwd)
|
|
328
|
-
const sessionName =
|
|
329
|
-
options.resume || `session-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`
|
|
333
|
+
const sessionName = generateSessionName(options.resume)
|
|
330
334
|
|
|
331
335
|
// Initialize context — restore saved session if available
|
|
332
336
|
// Read the active model's context window for dynamic max-token sizing.
|
|
@@ -498,7 +502,7 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
498
502
|
// Connect MCP servers lazily in the background: the UI renders immediately,
|
|
499
503
|
// and each server's tools register into the shared registry as it connects.
|
|
500
504
|
// A dead server (e.g. a 15s connect timeout) no longer blocks startup.
|
|
501
|
-
|
|
505
|
+
const mcpConnectPromise = connectMcpServers(config.skills?.mcpServers ?? [], tools)
|
|
502
506
|
|
|
503
507
|
// Initialize hook engine — register skill-defined hooks
|
|
504
508
|
const hookEngine = new HookEngine()
|
|
@@ -562,6 +566,14 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
562
566
|
|
|
563
567
|
// ── Cross-session messaging: register session, heartbeat, shutdown ──
|
|
564
568
|
engine.setSessionId(sessionName)
|
|
569
|
+
|
|
570
|
+
// Surface MCP connect failures to the model once they settle, so it knows
|
|
571
|
+
// those servers' tools are unavailable (rather than silently concluding the
|
|
572
|
+
// tools don't exist). Injected asynchronously — never blocks first paint.
|
|
573
|
+
void mcpConnectPromise.then((failures) => {
|
|
574
|
+
const notice = formatMcpConnectFailures(failures)
|
|
575
|
+
if (notice) engine.getContext().addMessage({ role: 'user', content: notice })
|
|
576
|
+
})
|
|
565
577
|
// Human-readable, cross-session-addressable name. Defaults to the cwd basename
|
|
566
578
|
// so `@mipham-code` can address this session; uniqueness is enforced against
|
|
567
579
|
// other live sessions (suffix -2, -3, …). The id stays a stable `session-<ts>`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface McpConnectFailure {
|
|
2
|
+
name: string
|
|
3
|
+
reason: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format a startup notice for MCP servers that failed to connect, so the model
|
|
8
|
+
* knows their tools are unavailable (instead of concluding the tools don't
|
|
9
|
+
* exist). Empty string when nothing failed.
|
|
10
|
+
*/
|
|
11
|
+
export function formatMcpConnectFailures(failures: McpConnectFailure[]): string {
|
|
12
|
+
if (failures.length === 0) return ''
|
|
13
|
+
const list = failures.map((f) => `- ${f.name}: ${f.reason}`).join('\n')
|
|
14
|
+
return `[MCP] These MCP servers failed to connect — their tools are unavailable:\n${list}`
|
|
15
|
+
}
|
|
@@ -26,7 +26,7 @@ export function readClaudeManifest(dir: string): ClaudeManifest | null {
|
|
|
26
26
|
const path = join(dir, '.claude-plugin', 'plugin.json')
|
|
27
27
|
if (!existsSync(path)) return null
|
|
28
28
|
try {
|
|
29
|
-
return JSON.parse(readFileSync(path, 'utf-8')) as ClaudeManifest
|
|
29
|
+
return JSON.parse(readFileSync(path, 'utf-8').replace(/^\uFEFF/, '')) as ClaudeManifest
|
|
30
30
|
} catch {
|
|
31
31
|
return null
|
|
32
32
|
}
|
|
@@ -52,7 +52,7 @@ export function validatePlugin(dir: string): PluginValidation {
|
|
|
52
52
|
|
|
53
53
|
const errors: string[] = []
|
|
54
54
|
try {
|
|
55
|
-
const raw = readFileSync(resolved.path, 'utf-8')
|
|
55
|
+
const raw = readFileSync(resolved.path, 'utf-8').replace(/^\uFEFF/, '')
|
|
56
56
|
const manifest = JSON.parse(raw) as PluginManifest
|
|
57
57
|
|
|
58
58
|
if (!manifest.name || !/^[a-z0-9-]+$/.test(manifest.name)) {
|
|
@@ -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.63.0' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|