@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.
- package/package.json +1 -1
- package/src/commands/loop-scaffold.ts +15 -44
- package/src/config/loader.ts +56 -1
- package/src/core/claude-md-fix.ts +28 -0
- package/src/core/crsi-producer.ts +15 -0
- package/src/core/fix-code.ts +167 -0
- package/src/core/fix.ts +176 -0
- package/src/core/hooks-config.ts +10 -5
- package/src/core/hooks-executor.ts +84 -5
- package/src/core/improvement-track.ts +34 -6
- package/src/core/instructions.ts +2 -2
- package/src/core/memory/memory-manager.ts +21 -1
- package/src/core/memory/tfidf.ts +30 -0
- package/src/i18n-core/locales/en-US.json +26 -2
- package/src/i18n-core/locales/zh-CN.json +26 -2
- package/src/index.tsx +12 -0
- package/src/providers/openai-compat.ts +3 -1
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +2 -0
- package/src/skills/community-registry.json +16 -0
- package/src/skills/marketplace.ts +233 -0
- package/src/skills/registry.ts +77 -63
- package/src/tools/exec/bash.ts +4 -1
- package/src/ui/commands.ts +345 -33
|
@@ -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:
|
|
119
|
+
timeout: (cfg.timeout ?? 60) * 1000,
|
|
42
120
|
encoding: 'utf-8',
|
|
43
|
-
stdio: ['
|
|
121
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
122
|
+
input,
|
|
44
123
|
})
|
|
45
124
|
|
|
46
|
-
// Exit code 0 = success
|
|
125
|
+
// Exit code 0 = success — parse the stdout JSON for structured decisions.
|
|
47
126
|
if (result.status === 0) {
|
|
48
|
-
return
|
|
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 {
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/src/core/instructions.ts
CHANGED
|
@@ -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 =
|
package/src/core/memory/tfidf.ts
CHANGED
|
@@ -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\
|
|
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
|
|
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.
|
|
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
|
package/src/shared/types.ts
CHANGED
|
@@ -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
|
]
|