@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -64,10 +64,6 @@ function writeTemplate(
64
64
  * ├── .mipham/
65
65
  * │ ├── CLAUDE.md
66
66
  * │ ├── settings.json
67
- * │ ├── hooks/
68
- * │ │ ├── pre-tool-use.sh
69
- * │ │ ├── post-tool-use.sh
70
- * │ │ └── stop.sh
71
67
  * │ ├── agents/
72
68
  * │ │ └── verifier.md
73
69
  * │ └── skills/
@@ -103,13 +99,6 @@ export function scaffoldLoopKit(basePath: string): ScaffoldResult {
103
99
  // ── .mipham/settings.json ──
104
100
  writeTemplate(join(miphamDir, 'settings.json'), TEMPLATES.settingsJson, false, created, skipped)
105
101
 
106
- // ── .mipham/hooks/ ──
107
- const hooksDir = join(miphamDir, 'hooks')
108
- ensureDir(hooksDir, created, skipped)
109
- writeTemplate(join(hooksDir, 'pre-tool-use.sh'), TEMPLATES.preToolUse, true, created, skipped)
110
- writeTemplate(join(hooksDir, 'post-tool-use.sh'), TEMPLATES.postToolUse, true, created, skipped)
111
- writeTemplate(join(hooksDir, 'stop.sh'), TEMPLATES.stopHook, true, created, skipped)
112
-
113
102
  // ── .mipham/agents/ ──
114
103
  const agentsDir = join(miphamDir, 'agents')
115
104
  ensureDir(agentsDir, created, skipped)
@@ -180,7 +169,20 @@ const TEMPLATES = {
180
169
  deny: [],
181
170
  },
182
171
  hooks: {
183
- PreToolUse: [],
172
+ // 示例:PreToolUse 拦截 Bash。脚本从 stdin 读 JSON(tool_name/tool_input),
173
+ // 输出 hookSpecificOutput JSON 决定 allow/deny;exit 2 = 拦截(stderr 作理由)。
174
+ PreToolUse: [
175
+ {
176
+ matcher: 'Bash',
177
+ hooks: [
178
+ {
179
+ type: 'command',
180
+ command: 'your-hook-script.sh',
181
+ timeout: 60,
182
+ },
183
+ ],
184
+ },
185
+ ],
184
186
  PostToolUse: [],
185
187
  Stop: [],
186
188
  SessionStart: [],
@@ -192,37 +194,6 @@ const TEMPLATES = {
192
194
  2,
193
195
  ) + '\n',
194
196
 
195
- preToolUse: `#!/bin/bash
196
- # PreToolUse hook — runs before each tool execution.
197
- # Tool name passed as \$1, input JSON as \$2.
198
- # Exit non-zero to block the tool.
199
- # Write JSON to stdout to modify the tool input.
200
-
201
- TOOL_NAME="\$1"
202
- TOOL_INPUT="\$2"
203
-
204
- echo "{\\"decision\\": \\"allow\\"}" >&2
205
- exit 0
206
- `,
207
-
208
- postToolUse: `#!/bin/bash
209
- # PostToolUse hook — runs after each tool execution.
210
- # Tool name passed as \$1, result JSON as \$2.
211
-
212
- TOOL_NAME="\$1"
213
- TOOL_RESULT="\$2"
214
-
215
- exit 0
216
- `,
217
-
218
- stopHook: `#!/bin/bash
219
- # Stop hook — runs when the AI session ends.
220
- # Use for cleanup, notifications, or saving state.
221
-
222
- echo "Session ended at \$(date)" >&2
223
- exit 0
224
- `,
225
-
226
197
  verifierAgent: `# Verifier Agent
227
198
 
228
199
  > Pre-commit audit specialist. Dispatched BEFORE committing to verify changes.
@@ -290,7 +261,7 @@ Verify staged changes against coding standards, security rules, and best practic
290
261
 
291
262
  ## Structure
292
263
 
293
- - \`.mipham/\` — Mipham Code configuration (CLAUDE.md, settings, hooks, agents, skills)
264
+ - \`.mipham/\` — Mipham Code configuration (CLAUDE.md, settings.json, agents, skills)
294
265
  - \`.mcp.json\` — MCP server configuration
295
266
  - \`MEMORY.md\` — AI persistent memory
296
267
  - \`run.sh\` — Project launcher
@@ -28,6 +28,7 @@ import {
28
28
  DEFAULT_CROSS_SESSION_CONFIG,
29
29
  } from './defaults'
30
30
  import { getCredentialKey, encryptApiKey, decryptApiKey, ENC_PREFIX } from './credential-crypto'
31
+ import type { SettingsHooks } from '../core/hooks-config'
31
32
 
32
33
  const MIPHAM_HOME = join(homedir(), '.mipham')
33
34
  const BACKUP_PREFIX = 'config.backup-'
@@ -139,7 +140,7 @@ function backupConfig(configPath: string): void {
139
140
  * Try to restore config from the most recent backup.
140
141
  * Returns true if restored successfully.
141
142
  */
142
- function tryRestoreFromBackup(configPath: string): boolean {
143
+ export function tryRestoreFromBackup(configPath: string): boolean {
143
144
  try {
144
145
  if (!existsSync(MIPHAM_HOME)) return false
145
146
  const files = readdirSync(MIPHAM_HOME)
@@ -216,6 +217,60 @@ function loadMcpJson(cwd: string): McpServerConfig[] {
216
217
  return servers
217
218
  }
218
219
 
220
+ /**
221
+ * Parsed `settings.json` (Claude Code convention): hooks + permissions.
222
+ * Hooks are additive across levels; permissions allow/deny are deduped unions.
223
+ */
224
+ export interface SettingsJson {
225
+ hooks: SettingsHooks
226
+ permissions: { allow: string[]; deny: string[] }
227
+ }
228
+
229
+ /**
230
+ * Load `settings.json` — project-level `.mipham/settings.json` then user-level
231
+ * `~/.mipham/settings.json`. Mirrors the Claude Code convention (hooks additive,
232
+ * permissions merged), so users can migrate their Claude settings unchanged.
233
+ */
234
+ export function loadSettingsJson(cwd: string = process.cwd()): SettingsJson {
235
+ const hooks: SettingsHooks = {}
236
+ const permissions = { allow: [] as string[], deny: [] as string[] }
237
+
238
+ const searchPaths = [join(cwd, '.mipham', 'settings.json'), join(MIPHAM_HOME, 'settings.json')]
239
+
240
+ for (const path of searchPaths) {
241
+ try {
242
+ if (!existsSync(path)) continue
243
+ const raw = readFileSync(path, 'utf-8')
244
+ const parsed = JSON.parse(raw) as {
245
+ hooks?: Record<string, unknown>
246
+ permissions?: { allow?: unknown; deny?: unknown }
247
+ }
248
+
249
+ if (parsed.hooks && typeof parsed.hooks === 'object') {
250
+ for (const [eventName, entries] of Object.entries(parsed.hooks)) {
251
+ if (!Array.isArray(entries)) continue
252
+ const bucket = (hooks as Record<string, unknown[]>)[eventName]
253
+ ;(hooks as Record<string, unknown[]>)[eventName] = [...(bucket ?? []), ...entries]
254
+ }
255
+ }
256
+
257
+ if (parsed.permissions) {
258
+ for (const key of ['allow', 'deny'] as const) {
259
+ const list = parsed.permissions[key]
260
+ if (!Array.isArray(list)) continue
261
+ for (const p of list) {
262
+ if (typeof p === 'string' && !permissions[key].includes(p)) permissions[key].push(p)
263
+ }
264
+ }
265
+ }
266
+ } catch {
267
+ // Silently skip malformed or missing settings.json files
268
+ }
269
+ }
270
+
271
+ return { hooks, permissions }
272
+ }
273
+
219
274
  export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
220
275
  const configPath = join(cwd, '.mipham', 'config.yml')
221
276
  const userConfigPath = join(MIPHAM_HOME, 'config.yml')
@@ -0,0 +1,28 @@
1
+ /**
2
+ * CLAUDE.md fix — write the derivable-section headings that `findDerivableSections`
3
+ * flags into the document's `prompt-exclude` frontmatter, so `stripSections` stops
4
+ * injecting them into the system prompt every session.
5
+ */
6
+ import { stringify as stringifyYaml } from 'yaml'
7
+ import { parseFrontmatter, parsePromptExclude } from './instructions'
8
+
9
+ export interface ApplyPromptExcludeResult {
10
+ content: string
11
+ added: string[]
12
+ }
13
+
14
+ /**
15
+ * Merge `headings` into the document's `prompt-exclude` frontmatter (creating the
16
+ * frontmatter block if absent). Preserves existing exclusions and other frontmatter
17
+ * fields; reports only the headings that were actually new.
18
+ */
19
+ export function applyPromptExclude(content: string, headings: string[]): ApplyPromptExcludeResult {
20
+ const { data, content: body } = parseFrontmatter(content)
21
+ const existing = parsePromptExclude(data['prompt-exclude'])
22
+ const added = [...new Set(headings)].filter((h) => !existing.includes(h))
23
+ if (added.length === 0) return { content, added: [] }
24
+
25
+ data['prompt-exclude'] = [...existing, ...added]
26
+ const frontmatter = stringifyYaml(data)
27
+ return { content: `---\n${frontmatter}---\n${body}`, added }
28
+ }
@@ -235,6 +235,19 @@ export function renderManagedRuleSource(signal: CrsiSignal): string | null {
235
235
  return null
236
236
  }
237
237
 
238
+ /**
239
+ * 只路由不禁用护栏(CRSI 教训 #learning):拒绝「禁用某内置能力」的 blanket 规则。
240
+ * 命中返回 true——producer 拒绝产出 managed rule,强制改写成「prefer X over Y」路由。
241
+ */
242
+ const DISABLE_INTENT_RE =
243
+ /(禁用|禁止|停用|封禁)[^,。\n]{0,20}(工具|能力|功能|tool|capabilit)|\bnever\s+use\b|\bdon'?t\s+use\b|\bdo\s+not\s+use\b/i
244
+
245
+ /** 检测信号是否表达「禁用某能力」的 blanket 意图(而非「prefer X over Y」路由)。 */
246
+ export function hasDisableIntent(signal: CrsiSignal): boolean {
247
+ if (/^(disable|ban|never-use|never_use)$/.test(signal.category)) return true
248
+ return DISABLE_INTENT_RE.test(`${signal.category} ${signal.title} ${signal.suggestion}`)
249
+ }
250
+
238
251
  /** 产出受管理规则变更候选(毕业路径)。无合格信号 / 同名规则已存在时返回 null。 */
239
252
  export function produceRuleProposal(
240
253
  signal: CrsiSignal,
@@ -246,6 +259,8 @@ export function produceRuleProposal(
246
259
  originalContent: string
247
260
  blastRadius: string[]
248
261
  } | null {
262
+ // 只路由不禁用护栏:拒绝「禁用某内置能力」的 blanket 规则。
263
+ if (hasDisableIntent(signal)) return null
249
264
  const ruleSource = renderManagedRuleSource(signal)
250
265
  if (!ruleSource) return null
251
266
 
@@ -0,0 +1,167 @@
1
+ /**
2
+ * `/fix test` — LLM-driven repair of failing tests in the real repo.
3
+ *
4
+ * Reuses the `/crsi bench` closed loop (LLM generates → a frozen test judges the
5
+ * result) but applied to the caller's own codebase: the failing test is the frozen
6
+ * ground truth, and the LLM may only fix the *source*, never the test.
7
+ */
8
+
9
+ /**
10
+ * Extract failing test-file paths from a `vitest run` output stream. Matches the
11
+ * ` FAIL test/xxx.test.ts > describe > it` lines (vitest 4 default reporter).
12
+ */
13
+ export function parseVitestFailures(output: string): string[] {
14
+ const files = new Set<string>()
15
+ const re = /^\s*FAIL\s+(\S+)/gm
16
+ let m
17
+ while ((m = re.exec(output))) {
18
+ files.add(m[1]!)
19
+ }
20
+ return [...files]
21
+ }
22
+
23
+ /**
24
+ * Extract relative import specifiers (`./foo`, `../bar`) from a test file — these
25
+ * are the candidate source files the failing test depends on. Package imports
26
+ * (`vitest`, `lodash`) and node builtins (`node:fs`) are ignored.
27
+ */
28
+ export function collectLocalImports(testContent: string): string[] {
29
+ const imports: string[] = []
30
+ const re = /from\s+['"](\.[^'"]*)['"]/g
31
+ let m
32
+ while ((m = re.exec(testContent))) {
33
+ imports.push(m[1]!)
34
+ }
35
+ return imports
36
+ }
37
+
38
+ /**
39
+ * Build the LLM prompt that asks for a source-only fix. The test is declared
40
+ * frozen ground truth; the model must return the corrected source file contents.
41
+ */
42
+ export function buildFixPrompt(opts: {
43
+ testPath: string
44
+ testContent: string
45
+ sourcePath: string
46
+ sourceContent: string
47
+ failure: string
48
+ }): string {
49
+ const { testPath, testContent, sourcePath, sourceContent, failure } = opts
50
+ return [
51
+ 'You are fixing a failing test in a real codebase.',
52
+ '',
53
+ 'RULES:',
54
+ '- The test file is FROZEN ground truth. Do NOT modify the test.',
55
+ '- Only fix the SOURCE file so the test passes.',
56
+ '- Output ONLY the corrected source file contents — no markdown fences, no explanation.',
57
+ '',
58
+ `Failing test: ${testPath}`,
59
+ '```',
60
+ testContent,
61
+ '```',
62
+ '',
63
+ `Source to fix: ${sourcePath}`,
64
+ '```',
65
+ sourceContent,
66
+ '```',
67
+ '',
68
+ 'Failure:',
69
+ failure,
70
+ ].join('\n')
71
+ }
72
+
73
+ export interface FixCodeDeps {
74
+ runVitest: (testFile: string) => { exitCode: number; output: string }
75
+ readFile: (path: string) => string | null
76
+ writeFile: (path: string, content: string) => void
77
+ generateFix: (prompt: string) => Promise<string>
78
+ resolveSourceFile: (testFile: string, specifier: string) => string
79
+ }
80
+
81
+ export interface FixTargetResult {
82
+ testFile: string
83
+ sourceFile: string | null
84
+ fixed: boolean
85
+ attempts: number
86
+ detail?: string
87
+ }
88
+
89
+ /**
90
+ * Repair one failing test: locate the first local source the test imports, ask
91
+ * the LLM for a source-only fix, verify it against the frozen test, and apply it
92
+ * (or restore the original in dry-run). Retries up to `maxRetries`, feeding each
93
+ * failure back into the next prompt.
94
+ */
95
+ export async function fixCodeTarget(
96
+ deps: FixCodeDeps,
97
+ testFile: string,
98
+ opts?: { apply?: boolean; maxRetries?: number },
99
+ ): Promise<FixTargetResult> {
100
+ const apply = opts?.apply ?? false
101
+ const maxRetries = opts?.maxRetries ?? 3
102
+
103
+ const initial = deps.runVitest(testFile)
104
+ if (initial.exitCode === 0) {
105
+ return { testFile, sourceFile: null, fixed: true, attempts: 0, detail: 'test already passes' }
106
+ }
107
+
108
+ const testContent = deps.readFile(testFile)
109
+ if (testContent === null) {
110
+ return {
111
+ testFile,
112
+ sourceFile: null,
113
+ fixed: false,
114
+ attempts: 0,
115
+ detail: 'cannot read test file',
116
+ }
117
+ }
118
+
119
+ const specifiers = collectLocalImports(testContent)
120
+ if (specifiers.length === 0) {
121
+ return {
122
+ testFile,
123
+ sourceFile: null,
124
+ fixed: false,
125
+ attempts: 0,
126
+ detail: 'no local imports to locate a source file',
127
+ }
128
+ }
129
+
130
+ const sourceFile = deps.resolveSourceFile(testFile, specifiers[0]!)
131
+ const sourceContent = deps.readFile(sourceFile)
132
+ if (sourceContent === null) {
133
+ return { testFile, sourceFile, fixed: false, attempts: 0, detail: 'cannot read source file' }
134
+ }
135
+
136
+ let failure = initial.output
137
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
138
+ const prompt = buildFixPrompt({
139
+ testPath: testFile,
140
+ testContent,
141
+ sourcePath: sourceFile,
142
+ sourceContent,
143
+ failure,
144
+ })
145
+ const fixedContent = await deps.generateFix(prompt)
146
+ if (!fixedContent) continue
147
+
148
+ deps.writeFile(sourceFile, fixedContent)
149
+ const result = deps.runVitest(testFile)
150
+
151
+ if (result.exitCode === 0) {
152
+ if (!apply) deps.writeFile(sourceFile, sourceContent)
153
+ return { testFile, sourceFile, fixed: true, attempts: attempt }
154
+ }
155
+
156
+ deps.writeFile(sourceFile, sourceContent)
157
+ failure = result.output
158
+ }
159
+
160
+ return {
161
+ testFile,
162
+ sourceFile,
163
+ fixed: false,
164
+ attempts: maxRetries,
165
+ detail: 'fix never passed the frozen test',
166
+ }
167
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * `/fix` — deterministic self-repair (no LLM). Orchestrates three repair targets:
3
+ * - doctor: write derivable CLAUDE.md sections into `prompt-exclude` frontmatter
4
+ * - config: restore a corrupted config.yml from backup, re-enable disabled hooks
5
+ * - cache: detect corrupt JSONL state lines under ~/.mipham (dry-run by default)
6
+ *
7
+ * The pure decision functions live here for testability; file I/O and engine calls
8
+ * are injected so each target is testable without touching the real filesystem.
9
+ */
10
+ import { findDerivableSections } from './claude-md-audit'
11
+ import { applyPromptExclude } from './claude-md-fix'
12
+
13
+ export interface DoctorFix {
14
+ content: string
15
+ added: string[]
16
+ }
17
+
18
+ /**
19
+ * Compute the repaired content for one CLAUDE.md document, or null when there is
20
+ * nothing to change (no derivable sections, or all of them already excluded).
21
+ */
22
+ export function computeDoctorFix(content: string): DoctorFix | null {
23
+ const sections = findDerivableSections(content)
24
+ if (sections.length === 0) return null
25
+ const { content: fixed, added } = applyPromptExclude(
26
+ content,
27
+ sections.map((s) => s.heading),
28
+ )
29
+ return added.length > 0 ? { content: fixed, added } : null
30
+ }
31
+
32
+ /**
33
+ * Return the 0-based line numbers of a JSONL document whose non-blank lines are not
34
+ * valid JSON. Blank/whitespace-only lines are ignored.
35
+ */
36
+ export function findCorruptJsonlLines(content: string): number[] {
37
+ const corrupt: number[] = []
38
+ content.split('\n').forEach((line, i) => {
39
+ const trimmed = line.trim()
40
+ if (trimmed === '') return
41
+ try {
42
+ JSON.parse(trimmed)
43
+ } catch {
44
+ corrupt.push(i)
45
+ }
46
+ })
47
+ return corrupt
48
+ }
49
+
50
+ /** Drop the given line numbers from a document, keeping everything else verbatim. */
51
+ export function removeCorruptLines(content: string, corrupt: number[]): string {
52
+ const corruptSet = new Set(corrupt)
53
+ return content
54
+ .split('\n')
55
+ .filter((_, i) => !corruptSet.has(i))
56
+ .join('\n')
57
+ }
58
+
59
+ /**
60
+ * Select the CLAUDE.md files that live inside the current repository (project and
61
+ * directory levels), excluding group/company/user policy files outside the repo.
62
+ */
63
+ export function selectRepoClaudeFiles(
64
+ files: Array<{ path: string; level: string }>,
65
+ ): Array<{ path: string }> {
66
+ return files
67
+ .filter(
68
+ (f) => f.path.endsWith('CLAUDE.md') && (f.level === 'project' || f.level === 'directory'),
69
+ )
70
+ .map((f) => ({ path: f.path }))
71
+ }
72
+
73
+ export interface DoctorReport {
74
+ fixed: Array<{ path: string; added: string[] }>
75
+ }
76
+
77
+ /** Read each CLAUDE.md, apply the derivable-section fix, and write it back. */
78
+ export function fixDoctor(
79
+ files: Array<{ path: string }>,
80
+ io: { read: (path: string) => string | null; write: (path: string, content: string) => void },
81
+ ): DoctorReport {
82
+ const fixed: DoctorReport['fixed'] = []
83
+ for (const f of files) {
84
+ const raw = io.read(f.path)
85
+ if (raw === null) continue
86
+ const result = computeDoctorFix(raw)
87
+ if (result) {
88
+ io.write(f.path, result.content)
89
+ fixed.push({ path: f.path, added: result.added })
90
+ }
91
+ }
92
+ return { fixed }
93
+ }
94
+
95
+ export interface ConfigFixReport {
96
+ /** Config files whose YAML failed to parse (detected). */
97
+ corruptConfigs: string[]
98
+ /** Corrupt config files actually restored from backup. */
99
+ restoredConfigs: string[]
100
+ /** Hooks currently disabled (detected). */
101
+ disabledHooks: string[]
102
+ /** Disabled hooks actually re-enabled. */
103
+ reenabledHooks: string[]
104
+ }
105
+
106
+ /**
107
+ * Detect corrupt config.yml files and disabled hooks. Repairs (restore from
108
+ * backup / re-enable) only happen when `dryRun` is false; in dry-run the report
109
+ * still lists what was detected without mutating anything.
110
+ */
111
+ export function fixConfig(deps: {
112
+ configPaths: string[]
113
+ read: (path: string) => string | null
114
+ parseYaml: (raw: string) => unknown
115
+ restore: (path: string) => boolean
116
+ hookHealth: () => Array<{ key: string; disabled: boolean }>
117
+ reEnableHook: (key: string) => boolean
118
+ dryRun?: boolean
119
+ }): ConfigFixReport {
120
+ const report: ConfigFixReport = {
121
+ corruptConfigs: [],
122
+ restoredConfigs: [],
123
+ disabledHooks: [],
124
+ reenabledHooks: [],
125
+ }
126
+
127
+ for (const p of deps.configPaths) {
128
+ const raw = deps.read(p)
129
+ if (raw === null) continue
130
+ let corrupt = false
131
+ try {
132
+ deps.parseYaml(raw)
133
+ } catch {
134
+ corrupt = true
135
+ }
136
+ if (!corrupt) continue
137
+ report.corruptConfigs.push(p)
138
+ if (!deps.dryRun && deps.restore(p)) report.restoredConfigs.push(p)
139
+ }
140
+
141
+ for (const h of deps.hookHealth()) {
142
+ if (!h.disabled) continue
143
+ report.disabledHooks.push(h.key)
144
+ if (!deps.dryRun && deps.reEnableHook(h.key)) report.reenabledHooks.push(h.key)
145
+ }
146
+ return report
147
+ }
148
+
149
+ export interface CacheFixReport {
150
+ files: Array<{ path: string; corruptLines: number[] }>
151
+ cleaned: Array<{ path: string; removed: number }>
152
+ }
153
+
154
+ /**
155
+ * Scan JSONL state files for corrupt lines. Reports them always; only rewrites the
156
+ * file (dropping corrupt lines) when `apply` is true.
157
+ */
158
+ export function fixCache(
159
+ files: string[],
160
+ io: { read: (path: string) => string | null; write: (path: string, content: string) => void },
161
+ apply: boolean,
162
+ ): CacheFixReport {
163
+ const report: CacheFixReport = { files: [], cleaned: [] }
164
+ for (const path of files) {
165
+ const raw = io.read(path)
166
+ if (raw === null) continue
167
+ const corrupt = findCorruptJsonlLines(raw)
168
+ if (corrupt.length === 0) continue
169
+ report.files.push({ path, corruptLines: corrupt })
170
+ if (apply) {
171
+ io.write(path, removeCorruptLines(raw, corrupt))
172
+ report.cleaned.push({ path, removed: corrupt.length })
173
+ }
174
+ }
175
+ return report
176
+ }
@@ -1,22 +1,27 @@
1
1
  import type { HookConfig, HookEvent, HookDefinition, HookContext } from '../shared/index.ts'
2
2
  import { executeHook } from './hooks-executor'
3
3
 
4
- interface HookConfigEntry {
4
+ export interface HookConfigEntry {
5
5
  matcher: string
6
6
  hooks: HookConfig[]
7
7
  }
8
8
 
9
- interface SettingsHooks {
9
+ /** `settings.json` `hooks` section — one matcher-group list per event. */
10
+ export interface SettingsHooks {
10
11
  PreToolUse?: HookConfigEntry[]
11
12
  PostToolUse?: HookConfigEntry[]
13
+ PostToolUseFailure?: HookConfigEntry[]
14
+ SessionStart?: HookConfigEntry[]
15
+ SessionEnd?: HookConfigEntry[]
16
+ Notification?: HookConfigEntry[]
12
17
  Stop?: HookConfigEntry[]
13
18
  UserPromptSubmit?: HookConfigEntry[]
14
19
  PreCompact?: HookConfigEntry[]
15
20
  PostCompact?: HookConfigEntry[]
16
- SessionStart?: HookConfigEntry[]
17
- SessionEnd?: HookConfigEntry[]
18
- Notification?: HookConfigEntry[]
19
21
  ConfigChange?: HookConfigEntry[]
22
+ SubagentStart?: HookConfigEntry[]
23
+ SubagentStop?: HookConfigEntry[]
24
+ PreInference?: HookConfigEntry[]
20
25
  }
21
26
 
22
27
  /**