@miphamai/cli 0.69.0 → 0.71.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.
@@ -30,22 +30,101 @@ function substituteVars(template: string, ctx: HookContext): string {
30
30
  .replace(/\$SESSION_ID/g, ctx.sessionId)
31
31
  }
32
32
 
33
+ /**
34
+ * Build the Claude Code protocol stdin JSON for a hook script. Mirrors the
35
+ * fields Claude Code passes (session_id / hook_event_name / cwd / tool_name /
36
+ * tool_input / tool_response) so hand-written Claude hooks can migrate
37
+ * unchanged.
38
+ */
39
+ export function buildHookStdin(ctx: HookContext, cwd: string): Record<string, unknown> {
40
+ const payload: Record<string, unknown> = {
41
+ session_id: ctx.sessionId,
42
+ hook_event_name: ctx.event,
43
+ cwd,
44
+ }
45
+ if (ctx.toolName) payload.tool_name = ctx.toolName
46
+ if (ctx.toolInput) payload.tool_input = ctx.toolInput
47
+ if (ctx.toolResult) payload.tool_response = ctx.toolResult
48
+ return payload
49
+ }
50
+
51
+ /**
52
+ * Parse a hook script's stdout JSON into a HookResult, following the Claude
53
+ * Code output contract. Supports the modern `hookSpecificOutput` carrier
54
+ * (permissionDecision / updatedInput / additionalContext) plus the legacy
55
+ * root-level `decision` and `continue` fields. Non-JSON or empty stdout = allow.
56
+ */
57
+ export function parseHookStdout(stdout: string | null | undefined, _ctx: HookContext): HookResult {
58
+ if (!stdout) return { allowed: true }
59
+
60
+ let parsed: Record<string, unknown>
61
+ try {
62
+ parsed = JSON.parse(stdout) as Record<string, unknown>
63
+ } catch {
64
+ return { allowed: true }
65
+ }
66
+
67
+ const hso = parsed.hookSpecificOutput as Record<string, unknown> | undefined
68
+ if (hso) {
69
+ const decision = hso.permissionDecision as string | undefined
70
+ const reason = hso.permissionDecisionReason as string | undefined
71
+ const additionalContext = hso.additionalContext as string | undefined
72
+ const updatedInput = hso.updatedInput as Record<string, unknown> | undefined
73
+
74
+ if (decision === 'deny') {
75
+ return { allowed: false, reason: reason ?? 'Denied by hook', additionalContext }
76
+ }
77
+ if (decision === 'allow') {
78
+ return {
79
+ allowed: true,
80
+ permissionDecision: 'allow',
81
+ modifiedInput: updatedInput,
82
+ additionalContext,
83
+ }
84
+ }
85
+ if (decision === 'ask') {
86
+ return { allowed: true, permissionDecision: 'ask', additionalContext }
87
+ }
88
+ if (decision === 'defer') {
89
+ return { allowed: true, permissionDecision: 'defer', additionalContext }
90
+ }
91
+ if (additionalContext) {
92
+ return { allowed: true, additionalContext }
93
+ }
94
+ }
95
+
96
+ // Legacy root-level decision: block / approve
97
+ if (parsed.decision === 'block') {
98
+ return { allowed: false, reason: (parsed.reason as string) ?? 'Blocked by hook' }
99
+ }
100
+
101
+ // Stop-style events: continue:false
102
+ if (parsed.continue === false) {
103
+ return { allowed: false, reason: (parsed.stopReason as string) ?? 'Stopped by hook' }
104
+ }
105
+
106
+ return { allowed: true }
107
+ }
108
+
33
109
  function executeCommand(cfg: HookConfig, ctx: HookContext): HookResult {
34
110
  if (!cfg.command) return { allowed: true }
35
111
 
36
112
  try {
37
113
  const args = cfg.args ? cfg.args.map((a) => substituteVars(a, ctx)) : []
38
114
 
39
- // Use spawnSync with array args — no shell, no command injection
115
+ // Use spawnSync with array args — no shell, no command injection.
116
+ // Pass the Claude-protocol stdin JSON so scripts can read structured context.
117
+ const input = JSON.stringify(buildHookStdin(ctx, process.cwd()))
40
118
  const result = spawnSync(cfg.command, args, {
41
- timeout: 30_000,
119
+ timeout: (cfg.timeout ?? 60) * 1000,
42
120
  encoding: 'utf-8',
43
- stdio: ['ignore', 'pipe', 'pipe'],
121
+ stdio: ['pipe', 'pipe', 'pipe'],
122
+ input,
44
123
  })
45
124
 
46
- // Exit code 0 = success, allow
125
+ // Exit code 0 = success — parse the stdout JSON for structured decisions.
47
126
  if (result.status === 0) {
48
- return { allowed: true }
127
+ return parseHookStdout(result.stdout, ctx)
49
128
  }
50
129
 
51
130
  // Non-zero exit: check for block signal (exit code 2)
@@ -1,8 +1,9 @@
1
1
  // CRSI 改进轨:噪声自适应改进判定 + 台账 + pending verdict 闸。
2
2
  // A1 不破:verdict / minEffect / 改进率全是确定性算术(均值/标准差/阈值/Wilson),无 LLM 裁判。
3
- import { appendFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'
3
+ import { readFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
  import { homedir } from 'node:os'
6
+ import { atomicWriteFileSync } from '../shared/atomic-write'
6
7
  import type { SkillDeltaSample } from './task-performance'
7
8
 
8
9
  export type ImprovementVerdict = 'improved' | 'regressed' | 'inconclusive'
@@ -115,7 +116,11 @@ export function improvementPath(): string {
115
116
  export function appendImprovement(record: ImprovementRecord): void {
116
117
  const file = improvementPath()
117
118
  mkdirSync(join(homedir(), '.mipham', 'crsi'), { recursive: true })
118
- appendFileSync(file, JSON.stringify(record) + '\n', 'utf-8')
119
+ // 原子激活(④):整账本读-改-写 + temp 文件 rename(见 shared/atomic-write),读者要么见旧要么见新。
120
+ // 非原子的 appendFileSync 写中途崩溃会留撕裂行,readImprovements 会 JSON.parse 抛错。
121
+ const existing = readImprovements()
122
+ existing.push(record)
123
+ atomicWriteFileSync(file, existing.map((r) => JSON.stringify(r)).join('\n') + '\n')
119
124
  }
120
125
 
121
126
  export function readImprovements(): ImprovementRecord[] {
@@ -124,19 +129,42 @@ export function readImprovements(): ImprovementRecord[] {
124
129
  return readFileSync(file, 'utf-8')
125
130
  .split('\n')
126
131
  .filter((line) => line.trim() !== '')
127
- .map((line) => JSON.parse(line) as ImprovementRecord)
132
+ .flatMap((line) => {
133
+ try {
134
+ return [JSON.parse(line) as ImprovementRecord]
135
+ } catch {
136
+ return [] // 残留撕裂/坏行跳过,不抛(原子激活的容错侧)
137
+ }
138
+ })
128
139
  }
129
140
 
130
141
  // ── pending verdict 闸(倒退才拦) ──
142
+ // 原子激活(④):pending verdict 是「激活指针」,持久化为不可变 manifest + 原子替换
143
+ // (temp+rename,见 shared/atomic-write),替代易失内存变量(进程重启即丢、无 manifest)。
131
144
 
132
- let pendingVerdict: ImprovementVerdict | null = null
145
+ export function pendingVerdictPath(): string {
146
+ return join(homedir(), '.mipham', 'crsi', 'pending-verdict.json')
147
+ }
133
148
 
134
149
  export function setPendingVerdict(v: ImprovementVerdict | null): void {
135
- pendingVerdict = v
150
+ const file = pendingVerdictPath()
151
+ if (v === null) {
152
+ rmSync(file, { force: true }) // 原子清除(unlink 原子)
153
+ return
154
+ }
155
+ mkdirSync(join(homedir(), '.mipham', 'crsi'), { recursive: true })
156
+ atomicWriteFileSync(file, JSON.stringify({ verdict: v, timestamp: new Date().toISOString() }))
136
157
  }
137
158
 
138
159
  export function getPendingVerdict(): ImprovementVerdict | null {
139
- return pendingVerdict
160
+ const file = pendingVerdictPath()
161
+ if (!existsSync(file)) return null
162
+ try {
163
+ const parsed = JSON.parse(readFileSync(file, 'utf-8')) as { verdict: ImprovementVerdict }
164
+ return parsed.verdict
165
+ } catch {
166
+ return null // manifest 损坏 → 视为无 pending(与旧 `?? 'inconclusive'` 的 fail-open 一致)
167
+ }
140
168
  }
141
169
 
142
170
  export function shouldBlockApproval(v: ImprovementVerdict): boolean {
@@ -11,12 +11,12 @@ import {
11
11
  type CrsiLessonSummary,
12
12
  } from './crsi-producer'
13
13
 
14
- interface FrontmatterResult {
14
+ export interface FrontmatterResult {
15
15
  data: Record<string, unknown>
16
16
  content: string
17
17
  }
18
18
 
19
- function parseFrontmatter(raw: string): FrontmatterResult {
19
+ export function parseFrontmatter(raw: string): FrontmatterResult {
20
20
  // Strip a leading UTF-8 BOM — otherwise `^---` never matches and a
21
21
  // BOM-prefixed file is silently treated as body text (effectively ignored).
22
22
  const src = raw.replace(/^\uFEFF/, '')
@@ -9,7 +9,7 @@ import {
9
9
  statSync,
10
10
  } from 'node:fs'
11
11
  import { join, extname, basename } from 'node:path'
12
- import { similarities } from './tfidf'
12
+ import { similarities, findNearDuplicates } from './tfidf'
13
13
 
14
14
  export interface MemoryMetadata {
15
15
  type: 'user' | 'feedback' | 'project' | 'reference'
@@ -333,6 +333,26 @@ export class MemoryManager {
333
333
  return { merged, removed }
334
334
  }
335
335
 
336
+ /**
337
+ * 只读「近重复报告」:跨所有记忆(含手写)列出 TF-IDF 余弦 > 阈值的近重复对,
338
+ * 供 /memory dedup 展示。只检测不合并——合并是用户的决定(写时去重已拦新增,
339
+ * 这里报告存量近重复供人工审视)。
340
+ */
341
+ listNearDuplicates(
342
+ threshold: number = DEDUP_THRESHOLD,
343
+ ): Array<{ a: string; b: string; similarity: number }> {
344
+ const entries = [...this.memories.values()]
345
+ const pairs = findNearDuplicates(
346
+ entries.map((e) => e.content),
347
+ threshold,
348
+ )
349
+ return pairs.map(({ i, j, similarity }) => ({
350
+ a: entries[i]!.name,
351
+ b: entries[j]!.name,
352
+ similarity,
353
+ }))
354
+ }
355
+
336
356
  buildSystemReminder(context: string, maxTokens?: number, grounding?: string): string {
337
357
  // Adaptive budget: 5% of context window, min 5000, max 75000
338
358
  const effectiveMaxTokens =
@@ -106,3 +106,33 @@ export function similarities(query: string, docs: string[]): number[] {
106
106
  const queryVec = tfidfVector(queryTokens, df, numDocs)
107
107
  return docTokens.map((tokens) => cosine(queryVec, tfidfVector(tokens, df, numDocs)))
108
108
  }
109
+
110
+ /**
111
+ * All pairs (i < j) of docs whose TF-IDF cosine similarity is `> threshold`,
112
+ * sorted by similarity descending. Deterministic near-duplicate detection —
113
+ * flags candidates for manual merge, never merges automatically.
114
+ */
115
+ export function findNearDuplicates(
116
+ docs: string[],
117
+ threshold: number,
118
+ ): Array<{ i: number; j: number; similarity: number }> {
119
+ const docTokens = docs.map(tokenize)
120
+ const numDocs = docs.length
121
+
122
+ const df = new Map<string, number>()
123
+ for (const tokens of docTokens) {
124
+ for (const term of new Set(tokens)) {
125
+ df.set(term, (df.get(term) ?? 0) + 1)
126
+ }
127
+ }
128
+
129
+ const vectors = docTokens.map((tokens) => tfidfVector(tokens, df, numDocs))
130
+ const pairs: Array<{ i: number; j: number; similarity: number }> = []
131
+ for (let i = 0; i < numDocs; i++) {
132
+ for (let j = i + 1; j < numDocs; j++) {
133
+ const similarity = cosine(vectors[i]!, vectors[j]!)
134
+ if (similarity > threshold) pairs.push({ i, j, similarity })
135
+ }
136
+ }
137
+ return pairs.sort((a, b) => b.similarity - a.similarity)
138
+ }
@@ -235,6 +235,7 @@
235
235
  "title": "── Token Usage (estimated) ──",
236
236
  "context_tokens": "Context tokens:",
237
237
  "usage": "Usage:",
238
+ "prompt_cache": "Prompt cache: ~{cached} cached ({ratio}% hit) · ~{uncached} uncached",
238
239
  "footer": "Token counting is approximate (chars/4).\nActual API usage depends on provider and model."
239
240
  },
240
241
  "model": {
@@ -415,7 +416,10 @@
415
416
  "gc_done": "Done. Archived files moved to ~/.mipham/memory/archive/",
416
417
  "consolidate_title": "─ Memory Consolidate ─",
417
418
  "consolidate_none": "No auto-* memories to consolidate.",
418
- "consolidate_result": "Consolidated: {removed} auto-* removed, {merged} lessons written."
419
+ "consolidate_result": "Consolidated: {removed} auto-* removed, {merged} lessons written.",
420
+ "dedup_title": "─ Memory Near-Duplicates ─",
421
+ "dedup_none": "No near-duplicate memories found.",
422
+ "dedup_footer": "Pairs above the TF-IDF cosine threshold. Merge is manual — run /memory consolidate for auto-* clustering, or edit the files directly."
419
423
  },
420
424
  "upgrade": {
421
425
  "uptodate": "── Upgrade Mipham Code ──\n\nCurrent version: v{current}\nLatest: v{latest}\n\n✓ Already up to date.\n\nTo check manually: https://www.npmjs.com/package/@miphamai/cli",
@@ -579,7 +583,7 @@
579
583
  },
580
584
  "hooks": {
581
585
  "title": "── Hooks ──",
582
- "no_hooks": "── Hooks ──\n\nNo hooks configured.\n\nCreate hook scripts in .mipham/hooks/:\n pre-tool-use.sh — runs before each tool\n post-tool-use.sh — runs after each tool\n stop.sh — runs when session ends\n\nUse /loop init to scaffold hooks directory.",
586
+ "no_hooks": "── Hooks ──\n\nNo hooks configured.\n\nConfigure hooks in .mipham/settings.json under the hooks section:\n PreToolUse — before each tool\n PostToolUse — after each tool\n Stop — on session end\n\nCommand hooks read JSON from stdin; exit 2 blocks (stderr as reason).\nUse /loop init to scaffold settings.json.",
583
587
  "location": "Location: {path}",
584
588
  "found": "{count} hook(s) found.",
585
589
  "error": "── Hooks ──\n\nCould not read hooks directory."
@@ -609,6 +613,26 @@
609
613
  "audit_clean": " ✓ no code-derivable sections found",
610
614
  "audit_found": "Add the sections above to the `prompt-exclude` frontmatter to stop sending them to the model."
611
615
  },
616
+ "fix": {
617
+ "title": "── Fix ──",
618
+ "dryrun_banner": "[dry-run] preview — no changes written",
619
+ "doctor_clean": "✓ no code-derivable CLAUDE.md sections to fix",
620
+ "doctor_fixed": "✓ {path}: added prompt-exclude [{added}]",
621
+ "config_clean": "✓ config healthy, no disabled hooks",
622
+ "config_corrupt": "⚠ {path}: config corrupt",
623
+ "config_restored": "✓ restored config from backup: {path}",
624
+ "hook_disabled": "⚠ hook disabled: {key}",
625
+ "hook_reenabled": "✓ re-enabled hook: {key}",
626
+ "cache_clean": "✓ no corrupt cache lines",
627
+ "cache_found": "⚠ {path}: {count} corrupt line(s)",
628
+ "cache_cleaned": "✓ cleaned {path}: removed {count} line(s)",
629
+ "cache_hint": "Run /fix cache --apply to remove corrupt lines",
630
+ "test_usage": "Usage: /fix test <test-file> [--apply]\n\nLLM-driven repair of a failing test (test frozen, source only). Dry-run by default; --apply writes the fix.",
631
+ "test_no_llm": "No LLM available in this session — cannot generate a fix.",
632
+ "test_fixed": "✓ fixed {test} (source {source}, {attempts} attempt(s))",
633
+ "test_failed": "✗ could not fix {test}: {detail}",
634
+ "test_hint": "Run /fix test <file> --apply to write the fix."
635
+ },
612
636
  "plugins": {
613
637
  "unavailable": "PluginManager not available in this session.",
614
638
  "title": "── Installed Plugins ──",
@@ -235,6 +235,7 @@
235
235
  "title": "── Token 用量(估算)──",
236
236
  "context_tokens": "上下文 Token:",
237
237
  "usage": "使用率:",
238
+ "prompt_cache": "Prompt 缓存: ~{cached} 命中 ({ratio}% hit) · ~{uncached} 未命中",
238
239
  "footer": "Token 计数为近似值(字符数/4)。\n实际 API 用量取决于提供商和模型。"
239
240
  },
240
241
  "model": {
@@ -415,7 +416,10 @@
415
416
  "gc_done": "完成。归档文件已移到 ~/.mipham/memory/archive/",
416
417
  "consolidate_title": "─ 记忆合并 ─",
417
418
  "consolidate_none": "没有需要合并的 auto-* 记忆。",
418
- "consolidate_result": "合并完成:删除 {removed} 条 auto-*,写入 {merged} 条 lesson-*。"
419
+ "consolidate_result": "合并完成:删除 {removed} 条 auto-*,写入 {merged} 条 lesson-*。",
420
+ "dedup_title": "─ 记忆近重复 ─",
421
+ "dedup_none": "未发现近重复记忆。",
422
+ "dedup_footer": "以上为 TF-IDF 余弦超过阈值的近重复对。合并需手动——auto-* 走 /memory consolidate 聚簇,或直接编辑文件。"
419
423
  },
420
424
  "upgrade": {
421
425
  "uptodate": "── 升级 Mipham Code ──\n\n当前版本: v{current}\n最新版本: v{latest}\n\n✓ 已是最新版本。\n\n手动检查: https://www.npmjs.com/package/@miphamai/cli",
@@ -579,7 +583,7 @@
579
583
  },
580
584
  "hooks": {
581
585
  "title": "── 钩子 ──",
582
- "no_hooks": "── 钩子 ──\n\n未配置钩子。\n\n在 .mipham/hooks/ 中创建钩子脚本:\n pre-tool-use.sh — 在每次工具执行前运行\n post-tool-use.sh — 在每次工具执行后运行\n stop.sh — 会话结束时运行\n\n使用 /loop init 初始化钩子目录。",
586
+ "no_hooks": "── 钩子 ──\n\n未配置钩子。\n\n在 .mipham/settings.json 的 hooks 段配置钩子:\n PreToolUse — 工具执行前\n PostToolUse — 工具执行后\n Stop — 会话结束\n\n命令钩子从 stdin 读 JSON,exit 2 拦截(stderr 作理由)。\n使用 /loop init 初始化 settings.json。",
583
587
  "location": "位置: {path}",
584
588
  "found": "找到 {count} 个钩子。",
585
589
  "error": "── 钩子 ──\n\n无法读取钩子目录。"
@@ -609,6 +613,26 @@
609
613
  "audit_clean": " ✓ 未发现可从代码推断的冗余章节",
610
614
  "audit_found": "将上述章节加入 `prompt-exclude` frontmatter,可停止发送给模型、节省 token。"
611
615
  },
616
+ "fix": {
617
+ "title": "── 修复 ──",
618
+ "dryrun_banner": "[dry-run] 预览模式 — 不落盘",
619
+ "doctor_clean": "✓ CLAUDE.md 无需要修复的冗余章节",
620
+ "doctor_fixed": "✓ {path}: 已加 prompt-exclude [{added}]",
621
+ "config_clean": "✓ 配置无损坏、hooks 无禁用",
622
+ "config_corrupt": "⚠ {path}: 配置损坏",
623
+ "config_restored": "✓ 已从备份恢复 config: {path}",
624
+ "hook_disabled": "⚠ hook 已禁用: {key}",
625
+ "hook_reenabled": "✓ 已重新启用 hook: {key}",
626
+ "cache_clean": "✓ 缓存无损坏行",
627
+ "cache_found": "⚠ {path}: {count} 行损坏",
628
+ "cache_cleaned": "✓ 已清理 {path}: 移除 {count} 行",
629
+ "cache_hint": "使用 /fix cache --apply 清理损坏行",
630
+ "test_usage": "用法: /fix test <测试文件> [--apply]\n\nLLM 自动修复失败测试(测试冻结,只改源码)。默认 dry-run,--apply 落盘。",
631
+ "test_no_llm": "当前会话无可用 LLM,无法生成修复。",
632
+ "test_fixed": "✓ 已修复 {test}(源码 {source},{attempts} 次尝试)",
633
+ "test_failed": "✗ 未能修复 {test}: {detail}",
634
+ "test_hint": "使用 /fix test <文件> --apply 落盘修复。"
635
+ },
612
636
  "plugins": {
613
637
  "unavailable": "当前会话中 PluginManager 不可用。",
614
638
  "title": "── 已安装插件 ──",
package/src/index.tsx CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  loadInferenceHookConfig,
11
11
  loadCredentialMaskingConfig,
12
12
  loadCrossSessionConfig,
13
+ loadSettingsJson,
13
14
  } from './config/loader'
14
15
  import {
15
16
  registerActiveSession,
@@ -44,6 +45,7 @@ import { registerMcpServerTools, syncMcpToolsOnChange } from './mcp/registry'
44
45
  import { formatMcpConnectFailures, type McpConnectFailure } from './mcp/connect-failures'
45
46
  import { AgentRegistry } from './agent/agent-registry'
46
47
  import { HookEngine } from './core/hooks'
48
+ import { loadHookConfigs } from './core/hooks-config'
47
49
  import { ArtifactServer } from './artifacts/server'
48
50
  import { getMetrics } from './core/metrics'
49
51
  import { getWorkspaceTrust } from './core/workspace-trust'
@@ -514,6 +516,12 @@ export async function runApp(options: RunOptions): Promise<void> {
514
516
  }
515
517
  }
516
518
 
519
+ // Register settings.json hooks (Claude Code convention — additive across levels)
520
+ const settingsJson = loadSettingsJson(process.cwd())
521
+ for (const def of loadHookConfigs(settingsJson.hooks)) {
522
+ hookEngine.register(def)
523
+ }
524
+
517
525
  // Start artifact server (lazy — first artifact creation triggers listening)
518
526
  const artifactsDir = join(process.cwd(), MIPHAM_DIR, ARTIFACTS_DIR)
519
527
  const artifactServer = new ArtifactServer(artifactsDir, ARTIFACT_PORT)
@@ -547,6 +555,10 @@ export async function runApp(options: RunOptions): Promise<void> {
547
555
  for (const rule of config.permissionRules.deny ?? []) engine.getPermission().deny(rule)
548
556
  }
549
557
 
558
+ // Apply settings.json permissions (allow/deny) — Claude Code convention
559
+ for (const rule of settingsJson.permissions.allow) engine.getPermission().allow(rule)
560
+ for (const rule of settingsJson.permissions.deny) engine.getPermission().deny(rule)
561
+
550
562
  // Initialize agent registry and load plugin agents/skills/MCP/hooks
551
563
  const agentRegistry = new AgentRegistry()
552
564
  agentRegistry.loadUserAgents()
@@ -96,11 +96,12 @@ export class OpenAICompatProvider implements ProviderInstance {
96
96
  if (data === '[DONE]') {
97
97
  // Emit any pending tool calls before stopping
98
98
  for (const [, tc] of pendingToolCalls) {
99
+ if (!tc.name) continue // drop malformed tool call (missing name)
99
100
  yield {
100
101
  type: 'tool_use',
101
102
  toolUse: {
102
103
  type: 'tool_use',
103
- id: tc.id,
104
+ id: tc.id || `call_${Date.now()}`,
104
105
  name: tc.name,
105
106
  input: this.safeParseJson(tc.arguments),
106
107
  },
@@ -155,6 +156,7 @@ export class OpenAICompatProvider implements ProviderInstance {
155
156
  if (choice.finish_reason === 'tool_calls') {
156
157
  // Emit fully accumulated tool calls
157
158
  for (const [, tc] of pendingToolCalls) {
159
+ if (!tc.name) continue // drop malformed tool call (missing name)
158
160
  yield {
159
161
  type: 'tool_use',
160
162
  toolUse: {
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.69.0' as const
12
+ export const PACKAGE_VERSION = '0.71.0' as const
13
13
 
14
14
  /** npm install 全局安装命令 */
15
15
  export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
@@ -234,6 +234,8 @@ export interface HookConfig {
234
234
  headers?: Record<string, string>
235
235
  mcpServer?: string
236
236
  mcpTool?: string
237
+ /** Command timeout in seconds (Claude Code default is 60). */
238
+ timeout?: number
237
239
  continueOnBlock?: boolean
238
240
  }
239
241
 
@@ -77,5 +77,21 @@
77
77
  "file": "frontend-design.SKILL.md",
78
78
  "category": "Design",
79
79
  "author": "Anthropic"
80
+ },
81
+ {
82
+ "name": "grill-me",
83
+ "description": "A relentless one-question-at-a-time interview that stress-tests your plan or design",
84
+ "url": "https://github.com/mattpocock/skills",
85
+ "file": "skills/productivity/grill-me/SKILL.md",
86
+ "category": "Development",
87
+ "author": "Matt Pocock"
88
+ },
89
+ {
90
+ "name": "eli5",
91
+ "description": "Explain any topic like the user knows nothing about it, using an HTML artifact with big pictures and few words",
92
+ "url": "https://github.com/anthropics/claude-plugins-community",
93
+ "file": "eli5/skills/eli5/SKILL.md",
94
+ "category": "Communication",
95
+ "author": "Anthropic"
80
96
  }
81
97
  ]