aiwcli 0.12.6 → 0.12.7
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/bin/dev.cmd +3 -3
- package/bin/dev.js +16 -16
- package/bin/run.cmd +3 -3
- package/bin/run.js +21 -21
- package/dist/commands/branch.js +7 -2
- package/dist/lib/bmad-installer.js +37 -37
- package/dist/lib/terminal.d.ts +2 -0
- package/dist/lib/terminal.js +57 -7
- package/dist/templates/CLAUDE.md +205 -205
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +12 -12
- package/dist/templates/_shared/.claude/commands/handoff.md +12 -12
- package/dist/templates/_shared/.claude/settings.json +65 -65
- package/dist/templates/_shared/.codex/workflows/handoff.md +226 -226
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +226 -226
- package/dist/templates/_shared/handoff-system/CLAUDE.md +421 -421
- package/dist/templates/_shared/handoff-system/lib/document-generator.ts +215 -215
- package/dist/templates/_shared/handoff-system/lib/handoff-reader.ts +158 -158
- package/dist/templates/_shared/handoff-system/scripts/resume_handoff.ts +373 -373
- package/dist/templates/_shared/handoff-system/scripts/save_handoff.ts +469 -469
- package/dist/templates/_shared/handoff-system/workflows/handoff-resume.md +66 -66
- package/dist/templates/_shared/handoff-system/workflows/handoff.md +254 -254
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -2
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -159
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -147
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +128 -128
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -49
- package/dist/templates/_shared/hooks-ts/session_end.ts +196 -196
- package/dist/templates/_shared/hooks-ts/session_start.ts +163 -163
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -48
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -74
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +93 -93
- package/dist/templates/_shared/lib-ts/CLAUDE.md +367 -367
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -138
- package/dist/templates/_shared/lib-ts/base/constants.ts +303 -303
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -58
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +582 -582
- package/dist/templates/_shared/lib-ts/base/inference.ts +301 -301
- package/dist/templates/_shared/lib-ts/base/logger.ts +247 -247
- package/dist/templates/_shared/lib-ts/base/state-io.ts +202 -202
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -184
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -184
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +566 -566
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +524 -524
- package/dist/templates/_shared/lib-ts/context/context-store.ts +712 -712
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +312 -312
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -185
- package/dist/templates/_shared/lib-ts/package.json +20 -20
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +102 -102
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +58 -58
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -13
- package/dist/templates/_shared/lib-ts/types.ts +186 -186
- package/dist/templates/_shared/scripts/resolve_context.ts +33 -33
- package/dist/templates/_shared/scripts/status_line.ts +690 -690
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/ask.md +136 -136
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/index.md +21 -21
- package/dist/templates/cc-native/.claude/commands/cc-native/rlm/overview.md +56 -56
- package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -10
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -8
- package/dist/templates/cc-native/CC-NATIVE-README.md +189 -189
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +304 -304
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +143 -143
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +213 -213
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -70
- package/dist/templates/cc-native/_cc-native/cc-native.config.json +96 -96
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +247 -247
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +76 -76
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -54
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +51 -51
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -53
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -61
- package/dist/templates/cc-native/_cc-native/lib-ts/agent-selection.ts +163 -163
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +156 -156
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/format.ts +597 -597
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/index.ts +26 -26
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/tracker.ts +107 -107
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts/write.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +319 -319
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +144 -144
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -57
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -83
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +119 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +79 -79
- package/dist/templates/cc-native/_cc-native/lib-ts/graduation.ts +132 -132
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +116 -116
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +168 -168
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +70 -70
- package/dist/templates/cc-native/_cc-native/lib-ts/output-builder.ts +130 -130
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-discovery.ts +80 -80
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -41
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +101 -101
- package/dist/templates/cc-native/_cc-native/lib-ts/review-pipeline.ts +511 -511
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +71 -71
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -217
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +12 -12
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +66 -66
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -39
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +196 -196
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +21 -21
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/CLAUDE.md +480 -480
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/embedding-indexer.ts +287 -287
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/hyde.ts +148 -148
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/index.ts +54 -54
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/logger.ts +58 -58
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/ollama-client.ts +208 -208
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/retrieval-pipeline.ts +460 -460
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-indexer.ts +446 -446
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-loader.ts +280 -280
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/transcript-searcher.ts +274 -274
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/types.ts +201 -201
- package/dist/templates/cc-native/_cc-native/lib-ts/rlm/vector-store.ts +278 -278
- package/dist/templates/cc-native/_cc-native/lib-ts/settings.ts +184 -184
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +275 -275
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -18
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +329 -329
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -72
- package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -9
- package/oclif.manifest.json +1 -1
- package/package.json +108 -108
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +0 -3
|
@@ -1,586 +1,586 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common utilities for hook scripts.
|
|
3
|
-
* Standardized boilerplate for JSON parsing, validation, error handling.
|
|
4
|
-
* See SPEC.md §5
|
|
5
|
-
*/
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Common utilities for hook scripts.
|
|
3
|
+
* Standardized boilerplate for JSON parsing, validation, error handling.
|
|
4
|
+
* See SPEC.md §5
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
|
-
|
|
9
|
-
import { getProjectRoot } from "./constants.js";
|
|
10
|
-
import { logDebug, logWarn, hookLog, setSessionId, getContextPath as _getContextPath } from "./logger.js";
|
|
11
|
-
import { getContextBySessionId } from "../context/context-store.js";
|
|
12
|
-
import type { HookInput, HookOutput, PermissionRequestOutput } from "../types.js";
|
|
13
|
-
|
|
14
|
-
// Re-export logger functions for convenience (matches Python hook_utils re-exports)
|
|
15
|
-
export { setSessionId };
|
|
16
|
-
|
|
17
|
-
// Context window baseline: tokens not visible in hook data §5.9
|
|
18
|
-
export const CONTEXT_BASELINE_TOKENS = 22_600;
|
|
19
|
-
export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
|
|
20
|
-
|
|
21
|
-
// Event metadata stash — populated by loadHookInput(), read by runHook()
|
|
22
|
-
let _lastHookEvent: string | null = null;
|
|
23
|
-
let _lastToolName: string | null = null;
|
|
24
|
-
let _cachedHookName: string | null = null;
|
|
25
|
-
|
|
26
|
-
// Pre-fetched input stash
|
|
27
|
-
let _prefetchedInput: Record<string, any> | null = null;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Load and parse JSON from stdin (or return prefetched input if set).
|
|
31
|
-
* Returns null if stdin is empty or invalid JSON.
|
|
32
|
-
* See SPEC.md §5.1
|
|
33
|
-
*/
|
|
34
|
-
export function loadHookInput(): HookInput | null {
|
|
35
|
-
if (_prefetchedInput !== null) {
|
|
36
|
-
const result = _prefetchedInput;
|
|
37
|
-
_prefetchedInput = null; // consume once
|
|
38
|
-
if (result && typeof result === "object") {
|
|
39
|
-
_lastHookEvent = result.hook_event_name ?? null;
|
|
40
|
-
_lastToolName = result.tool_name ?? null;
|
|
41
|
-
}
|
|
42
|
-
return result as HookInput;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
// Read entire stdin using fd 0 (cross-platform, works on Windows)
|
|
47
|
-
const inputData = fs.readFileSync(0, "utf-8").trim();
|
|
48
|
-
if (!inputData) return null;
|
|
49
|
-
|
|
50
|
-
const result = JSON.parse(inputData);
|
|
51
|
-
if (result && typeof result === "object") {
|
|
52
|
-
_lastHookEvent = result.hook_event_name ?? null;
|
|
53
|
-
_lastToolName = result.tool_name ?? null;
|
|
54
|
-
}
|
|
55
|
-
return result as HookInput;
|
|
56
|
-
} catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Validate hook event type and optional tool name.
|
|
63
|
-
* See SPEC.md §5.2
|
|
64
|
-
*/
|
|
65
|
-
export function validateHookEvent(
|
|
66
|
-
payload: HookInput,
|
|
67
|
-
expectedEvent: string,
|
|
68
|
-
expectedTool?: string,
|
|
69
|
-
): boolean {
|
|
70
|
-
if (payload.hook_event_name !== expectedEvent) return false;
|
|
71
|
-
if (expectedTool && payload.tool_name !== expectedTool) return false;
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Extract and validate tool_input from payload.
|
|
77
|
-
* See SPEC.md §5.3
|
|
78
|
-
*/
|
|
79
|
-
export function getToolInput(
|
|
80
|
-
payload: HookInput,
|
|
81
|
-
): Record<string, any> | null {
|
|
82
|
-
const toolInput = payload.tool_input;
|
|
83
|
-
return toolInput && typeof toolInput === "object" ? toolInput : null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Check if persistence should be skipped based on metadata flags.
|
|
88
|
-
* See SPEC.md §5.4
|
|
89
|
-
*/
|
|
90
|
-
export function checkSkipPersistence(
|
|
91
|
-
payload: HookInput,
|
|
92
|
-
hookName = "hook",
|
|
93
|
-
): boolean {
|
|
94
|
-
const toolInput = getToolInput(payload);
|
|
95
|
-
if (!toolInput) return false;
|
|
96
|
-
|
|
97
|
-
const {metadata} = toolInput;
|
|
98
|
-
if (metadata && typeof metadata === "object" && metadata.skip_persistence) {
|
|
99
|
-
logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Emit hookSpecificOutput with additionalContext to stdout.
|
|
107
|
-
* hookEventName is required by Claude Code's Zod validator (discriminated union).
|
|
108
|
-
* Auto-detected from stdin payload (set by loadHookInput/runHook).
|
|
109
|
-
*
|
|
110
|
-
* SubagentStop and Stop events use top-level systemMessage field instead of hookSpecificOutput.
|
|
111
|
-
* See SPEC.md §5.5
|
|
112
|
-
*/
|
|
113
|
-
export function emitContext(additionalContext: string): void {
|
|
114
|
-
const eventName = _lastHookEvent ?? undefined;
|
|
115
|
-
const tool = _lastToolName;
|
|
116
|
-
|
|
117
|
-
// SubagentStop and Stop use top-level systemMessage field
|
|
118
|
-
if (eventName === "SubagentStop" || eventName === "Stop") {
|
|
119
|
-
const out = { systemMessage: additionalContext };
|
|
120
|
-
process.stdout.write(JSON.stringify(out) + "\n");
|
|
121
|
-
_logEmit("systemMessage", additionalContext.length, { event: eventName ?? "unknown", systemMessage: additionalContext });
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// All other events use hookSpecificOutput
|
|
126
|
-
const out: HookOutput = {
|
|
127
|
-
hookSpecificOutput: {
|
|
128
|
-
...(eventName ? { hookEventName: eventName } : {}),
|
|
129
|
-
additionalContext,
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
const json = JSON.stringify(out);
|
|
133
|
-
const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
|
|
134
|
-
_logEmit("context", additionalContext.length, { event: eventDesc, additionalContext });
|
|
135
|
-
process.stdout.write(json + "\n");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Emit hookSpecificOutput that denies the tool call with context and reason.
|
|
140
|
-
* hookEventName is required by Claude Code's Zod validator (discriminated union).
|
|
141
|
-
* Auto-detected from stdin payload (set by loadHookInput/runHook).
|
|
142
|
-
* See SPEC.md §5.6
|
|
143
|
-
*/
|
|
144
|
-
export function emitContextAndBlock(
|
|
145
|
-
additionalContext: string,
|
|
146
|
-
reason: string,
|
|
147
|
-
): void {
|
|
148
|
-
const eventName = _lastHookEvent ?? undefined;
|
|
149
|
-
if (eventName && eventName !== "PreToolUse") {
|
|
150
|
-
logWarn(_cachedHookName ?? "unknown",
|
|
151
|
-
`emitContextAndBlock() called from ${eventName} — permissionDecision only works for PreToolUse. ` +
|
|
152
|
-
`Use emitBlock() or the event-specific function instead.`);
|
|
153
|
-
}
|
|
154
|
-
const tool = _lastToolName;
|
|
155
|
-
const out: HookOutput = {
|
|
156
|
-
hookSpecificOutput: {
|
|
157
|
-
...(eventName ? { hookEventName: eventName } : {}),
|
|
158
|
-
additionalContext,
|
|
159
|
-
permissionDecision: "deny",
|
|
160
|
-
permissionDecisionReason: reason,
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
const json = JSON.stringify(out);
|
|
164
|
-
const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
|
|
165
|
-
_logEmit("block", additionalContext.length, { event: eventDesc, additionalContext, blockReason: reason });
|
|
166
|
-
process.stdout.write(json + "\n");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Log hook output (context, systemMessage, or block) to hook-log.jsonl for visibility. */
|
|
170
|
-
function _logEmit(type: "context" | "systemMessage" | "block", chars: number, payload: Record<string, any>): void {
|
|
171
|
-
const hook = _cachedHookName ?? "unknown";
|
|
172
|
-
const event = payload.event ?? "unknown";
|
|
173
|
-
const mechanism = payload.mechanism ? ` via ${payload.mechanism}` : "";
|
|
174
|
-
const msg = type === "block"
|
|
175
|
-
? `HOOK_OUTPUT [${type}] ${event} ${chars} chars${mechanism}, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
|
|
176
|
-
: `HOOK_OUTPUT [${type}] ${event} ${chars} chars`;
|
|
177
|
-
hookLog("info", hook, msg, { data: payload });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Block a user prompt submission with a reason.
|
|
182
|
-
* Only works for UserPromptSubmit hooks.
|
|
183
|
-
* Output: top-level { decision: "block", reason } + optional hookSpecificOutput.additionalContext
|
|
184
|
-
*/
|
|
185
|
-
export function emitBlockPrompt(reason: string, context?: string): void {
|
|
186
|
-
const eventName = _lastHookEvent ?? undefined;
|
|
187
|
-
if (eventName && eventName !== "UserPromptSubmit") {
|
|
188
|
-
logWarn(_cachedHookName ?? "unknown",
|
|
189
|
-
`emitBlockPrompt() called from ${eventName} — only works for UserPromptSubmit`);
|
|
190
|
-
}
|
|
191
|
-
const out: HookOutput = {
|
|
192
|
-
decision: "block",
|
|
193
|
-
reason,
|
|
194
|
-
...(context ? {
|
|
195
|
-
hookSpecificOutput: {
|
|
196
|
-
...(eventName ? { hookEventName: eventName } : {}),
|
|
197
|
-
additionalContext: context,
|
|
198
|
-
}
|
|
199
|
-
} : {}),
|
|
200
|
-
};
|
|
201
|
-
_logEmit("block", context?.length ?? 0, { event: eventName ?? "unknown", additionalContext: context, blockReason: reason });
|
|
202
|
-
process.stdout.write(JSON.stringify(out) + "\n");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Block via exit code 2 + stderr feedback.
|
|
207
|
-
* Works for PostToolUse, PostToolUseFailure.
|
|
208
|
-
* The reason becomes the stderr message (fed to Claude as system-reminder).
|
|
209
|
-
* If context is provided, it's prepended to the stderr message for richer feedback.
|
|
210
|
-
* NOTE: Exit 2 causes Claude Code to ignore all JSON stdout — only stderr matters.
|
|
211
|
-
*/
|
|
212
|
-
export function emitBlockViaExit(reason: string, context?: string): void {
|
|
213
|
-
const stderrMessage = context ? `${context}\n\n${reason}` : reason;
|
|
214
|
-
_logEmit("block", stderrMessage.length, {
|
|
215
|
-
event: _lastHookEvent ?? "unknown",
|
|
216
|
-
blockReason: reason,
|
|
217
|
-
mechanism: "exit2",
|
|
218
|
-
});
|
|
219
|
-
process.stderr.write(stderrMessage + "\n");
|
|
220
|
-
throw new Error("SystemExit:2");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Block via top-level { decision: "block", reason }.
|
|
225
|
-
* Works for Stop and SubagentStop events.
|
|
226
|
-
* These events do NOT support additionalContext — only reason is available.
|
|
227
|
-
*/
|
|
228
|
-
export function emitBlockTopLevel(reason: string): void {
|
|
229
|
-
const eventName = _lastHookEvent ?? undefined;
|
|
230
|
-
if (eventName && eventName !== "Stop" && eventName !== "SubagentStop") {
|
|
231
|
-
logWarn(_cachedHookName ?? "unknown",
|
|
232
|
-
`emitBlockTopLevel() called from ${eventName} — only works for Stop/SubagentStop`);
|
|
233
|
-
}
|
|
234
|
-
const out = { decision: "block", reason };
|
|
235
|
-
_logEmit("block", reason.length, {
|
|
236
|
-
event: eventName ?? "unknown",
|
|
237
|
-
blockReason: reason,
|
|
238
|
-
mechanism: "topLevelDecision",
|
|
239
|
-
});
|
|
240
|
-
process.stdout.write(JSON.stringify(out) + "\n");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Respond to a PermissionRequest with allow/deny.
|
|
245
|
-
* Only works for PermissionRequest hooks.
|
|
246
|
-
*/
|
|
247
|
-
export function emitPermissionDecision(
|
|
248
|
-
behavior: "allow" | "deny",
|
|
249
|
-
opts?: { message?: string; updatedInput?: Record<string, unknown>; updatedPermissions?: Record<string, unknown> },
|
|
250
|
-
): void {
|
|
251
|
-
const out: PermissionRequestOutput = {
|
|
252
|
-
decision: {
|
|
253
|
-
behavior,
|
|
254
|
-
...(opts?.message ? { message: opts.message } : {}),
|
|
255
|
-
...(opts?.updatedInput ? { updatedInput: opts.updatedInput } : {}),
|
|
256
|
-
...(opts?.updatedPermissions ? { updatedPermissions: opts.updatedPermissions } : {}),
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
_logEmit("block", 0, {
|
|
260
|
-
event: _lastHookEvent ?? "unknown",
|
|
261
|
-
blockReason: `permission:${behavior}`,
|
|
262
|
-
mechanism: "permissionRequest",
|
|
263
|
-
});
|
|
264
|
-
process.stdout.write(JSON.stringify(out) + "\n");
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Unified block dispatcher — auto-detects the correct blocking mechanism
|
|
269
|
-
* based on the current hook event type.
|
|
270
|
-
*
|
|
271
|
-
* PreToolUse → permissionDecision: "deny" (via emitContextAndBlock)
|
|
272
|
-
* UserPromptSubmit → top-level decision: "block" (via emitBlockPrompt)
|
|
273
|
-
* PostToolUse/PostToolUseFailure → exit(2) + stderr (via emitBlockViaExit)
|
|
274
|
-
* Stop/SubagentStop → top-level { decision: "block", reason } (via emitBlockTopLevel)
|
|
275
|
-
* PermissionRequest → decision: { behavior: "deny" } (via emitPermissionDecision)
|
|
276
|
-
* SessionStart/Notification/SubagentStart/SessionEnd/etc. → warn and no-op
|
|
277
|
-
*
|
|
278
|
-
* This is the RECOMMENDED universal blocking API. Hook authors should use
|
|
279
|
-
* emitBlock() and let the library handle event-specific dispatch.
|
|
280
|
-
*/
|
|
281
|
-
export function emitBlock(reason: string, context?: string): void {
|
|
282
|
-
const event = _lastHookEvent;
|
|
283
|
-
switch (event) {
|
|
284
|
-
case "PermissionRequest":
|
|
285
|
-
emitPermissionDecision("deny", { message: reason });
|
|
286
|
-
break;
|
|
287
|
-
case "PostToolUse":
|
|
288
|
-
case "PostToolUseFailure":
|
|
289
|
-
emitBlockViaExit(reason, context);
|
|
290
|
-
break;
|
|
291
|
-
case "PreToolUse":
|
|
292
|
-
emitContextAndBlock(context ?? reason, reason);
|
|
293
|
-
break;
|
|
294
|
-
case "Stop":
|
|
295
|
-
case "SubagentStop":
|
|
296
|
-
emitBlockTopLevel(reason);
|
|
297
|
-
break;
|
|
298
|
-
case "UserPromptSubmit":
|
|
299
|
-
emitBlockPrompt(reason, context);
|
|
300
|
-
break;
|
|
301
|
-
default: {
|
|
302
|
-
logWarn(_cachedHookName ?? "unknown",
|
|
303
|
-
`emitBlock() called from ${event ?? "unknown"} — no blocking mechanism exists for this event type, ignoring`);
|
|
8
|
+
|
|
9
|
+
import { getProjectRoot } from "./constants.js";
|
|
10
|
+
import { logDebug, logWarn, hookLog, setSessionId, getContextPath as _getContextPath } from "./logger.js";
|
|
11
|
+
import { getContextBySessionId } from "../context/context-store.js";
|
|
12
|
+
import type { HookInput, HookOutput, PermissionRequestOutput } from "../types.js";
|
|
13
|
+
|
|
14
|
+
// Re-export logger functions for convenience (matches Python hook_utils re-exports)
|
|
15
|
+
export { setSessionId };
|
|
16
|
+
|
|
17
|
+
// Context window baseline: tokens not visible in hook data §5.9
|
|
18
|
+
export const CONTEXT_BASELINE_TOKENS = 22_600;
|
|
19
|
+
export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
|
|
20
|
+
|
|
21
|
+
// Event metadata stash — populated by loadHookInput(), read by runHook()
|
|
22
|
+
let _lastHookEvent: string | null = null;
|
|
23
|
+
let _lastToolName: string | null = null;
|
|
24
|
+
let _cachedHookName: string | null = null;
|
|
25
|
+
|
|
26
|
+
// Pre-fetched input stash
|
|
27
|
+
let _prefetchedInput: Record<string, any> | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load and parse JSON from stdin (or return prefetched input if set).
|
|
31
|
+
* Returns null if stdin is empty or invalid JSON.
|
|
32
|
+
* See SPEC.md §5.1
|
|
33
|
+
*/
|
|
34
|
+
export function loadHookInput(): HookInput | null {
|
|
35
|
+
if (_prefetchedInput !== null) {
|
|
36
|
+
const result = _prefetchedInput;
|
|
37
|
+
_prefetchedInput = null; // consume once
|
|
38
|
+
if (result && typeof result === "object") {
|
|
39
|
+
_lastHookEvent = result.hook_event_name ?? null;
|
|
40
|
+
_lastToolName = result.tool_name ?? null;
|
|
41
|
+
}
|
|
42
|
+
return result as HookInput;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Read entire stdin using fd 0 (cross-platform, works on Windows)
|
|
47
|
+
const inputData = fs.readFileSync(0, "utf-8").trim();
|
|
48
|
+
if (!inputData) return null;
|
|
49
|
+
|
|
50
|
+
const result = JSON.parse(inputData);
|
|
51
|
+
if (result && typeof result === "object") {
|
|
52
|
+
_lastHookEvent = result.hook_event_name ?? null;
|
|
53
|
+
_lastToolName = result.tool_name ?? null;
|
|
54
|
+
}
|
|
55
|
+
return result as HookInput;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate hook event type and optional tool name.
|
|
63
|
+
* See SPEC.md §5.2
|
|
64
|
+
*/
|
|
65
|
+
export function validateHookEvent(
|
|
66
|
+
payload: HookInput,
|
|
67
|
+
expectedEvent: string,
|
|
68
|
+
expectedTool?: string,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (payload.hook_event_name !== expectedEvent) return false;
|
|
71
|
+
if (expectedTool && payload.tool_name !== expectedTool) return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract and validate tool_input from payload.
|
|
77
|
+
* See SPEC.md §5.3
|
|
78
|
+
*/
|
|
79
|
+
export function getToolInput(
|
|
80
|
+
payload: HookInput,
|
|
81
|
+
): Record<string, any> | null {
|
|
82
|
+
const toolInput = payload.tool_input;
|
|
83
|
+
return toolInput && typeof toolInput === "object" ? toolInput : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if persistence should be skipped based on metadata flags.
|
|
88
|
+
* See SPEC.md §5.4
|
|
89
|
+
*/
|
|
90
|
+
export function checkSkipPersistence(
|
|
91
|
+
payload: HookInput,
|
|
92
|
+
hookName = "hook",
|
|
93
|
+
): boolean {
|
|
94
|
+
const toolInput = getToolInput(payload);
|
|
95
|
+
if (!toolInput) return false;
|
|
96
|
+
|
|
97
|
+
const {metadata} = toolInput;
|
|
98
|
+
if (metadata && typeof metadata === "object" && metadata.skip_persistence) {
|
|
99
|
+
logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Emit hookSpecificOutput with additionalContext to stdout.
|
|
107
|
+
* hookEventName is required by Claude Code's Zod validator (discriminated union).
|
|
108
|
+
* Auto-detected from stdin payload (set by loadHookInput/runHook).
|
|
109
|
+
*
|
|
110
|
+
* SubagentStop and Stop events use top-level systemMessage field instead of hookSpecificOutput.
|
|
111
|
+
* See SPEC.md §5.5
|
|
112
|
+
*/
|
|
113
|
+
export function emitContext(additionalContext: string): void {
|
|
114
|
+
const eventName = _lastHookEvent ?? undefined;
|
|
115
|
+
const tool = _lastToolName;
|
|
116
|
+
|
|
117
|
+
// SubagentStop and Stop use top-level systemMessage field
|
|
118
|
+
if (eventName === "SubagentStop" || eventName === "Stop") {
|
|
119
|
+
const out = { systemMessage: additionalContext };
|
|
120
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
121
|
+
_logEmit("systemMessage", additionalContext.length, { event: eventName ?? "unknown", systemMessage: additionalContext });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// All other events use hookSpecificOutput
|
|
126
|
+
const out: HookOutput = {
|
|
127
|
+
hookSpecificOutput: {
|
|
128
|
+
...(eventName ? { hookEventName: eventName } : {}),
|
|
129
|
+
additionalContext,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const json = JSON.stringify(out);
|
|
133
|
+
const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
|
|
134
|
+
_logEmit("context", additionalContext.length, { event: eventDesc, additionalContext });
|
|
135
|
+
process.stdout.write(json + "\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Emit hookSpecificOutput that denies the tool call with context and reason.
|
|
140
|
+
* hookEventName is required by Claude Code's Zod validator (discriminated union).
|
|
141
|
+
* Auto-detected from stdin payload (set by loadHookInput/runHook).
|
|
142
|
+
* See SPEC.md §5.6
|
|
143
|
+
*/
|
|
144
|
+
export function emitContextAndBlock(
|
|
145
|
+
additionalContext: string,
|
|
146
|
+
reason: string,
|
|
147
|
+
): void {
|
|
148
|
+
const eventName = _lastHookEvent ?? undefined;
|
|
149
|
+
if (eventName && eventName !== "PreToolUse") {
|
|
150
|
+
logWarn(_cachedHookName ?? "unknown",
|
|
151
|
+
`emitContextAndBlock() called from ${eventName} — permissionDecision only works for PreToolUse. ` +
|
|
152
|
+
`Use emitBlock() or the event-specific function instead.`);
|
|
153
|
+
}
|
|
154
|
+
const tool = _lastToolName;
|
|
155
|
+
const out: HookOutput = {
|
|
156
|
+
hookSpecificOutput: {
|
|
157
|
+
...(eventName ? { hookEventName: eventName } : {}),
|
|
158
|
+
additionalContext,
|
|
159
|
+
permissionDecision: "deny",
|
|
160
|
+
permissionDecisionReason: reason,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
const json = JSON.stringify(out);
|
|
164
|
+
const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
|
|
165
|
+
_logEmit("block", additionalContext.length, { event: eventDesc, additionalContext, blockReason: reason });
|
|
166
|
+
process.stdout.write(json + "\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Log hook output (context, systemMessage, or block) to hook-log.jsonl for visibility. */
|
|
170
|
+
function _logEmit(type: "context" | "systemMessage" | "block", chars: number, payload: Record<string, any>): void {
|
|
171
|
+
const hook = _cachedHookName ?? "unknown";
|
|
172
|
+
const event = payload.event ?? "unknown";
|
|
173
|
+
const mechanism = payload.mechanism ? ` via ${payload.mechanism}` : "";
|
|
174
|
+
const msg = type === "block"
|
|
175
|
+
? `HOOK_OUTPUT [${type}] ${event} ${chars} chars${mechanism}, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
|
|
176
|
+
: `HOOK_OUTPUT [${type}] ${event} ${chars} chars`;
|
|
177
|
+
hookLog("info", hook, msg, { data: payload });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Block a user prompt submission with a reason.
|
|
182
|
+
* Only works for UserPromptSubmit hooks.
|
|
183
|
+
* Output: top-level { decision: "block", reason } + optional hookSpecificOutput.additionalContext
|
|
184
|
+
*/
|
|
185
|
+
export function emitBlockPrompt(reason: string, context?: string): void {
|
|
186
|
+
const eventName = _lastHookEvent ?? undefined;
|
|
187
|
+
if (eventName && eventName !== "UserPromptSubmit") {
|
|
188
|
+
logWarn(_cachedHookName ?? "unknown",
|
|
189
|
+
`emitBlockPrompt() called from ${eventName} — only works for UserPromptSubmit`);
|
|
190
|
+
}
|
|
191
|
+
const out: HookOutput = {
|
|
192
|
+
decision: "block",
|
|
193
|
+
reason,
|
|
194
|
+
...(context ? {
|
|
195
|
+
hookSpecificOutput: {
|
|
196
|
+
...(eventName ? { hookEventName: eventName } : {}),
|
|
197
|
+
additionalContext: context,
|
|
198
|
+
}
|
|
199
|
+
} : {}),
|
|
200
|
+
};
|
|
201
|
+
_logEmit("block", context?.length ?? 0, { event: eventName ?? "unknown", additionalContext: context, blockReason: reason });
|
|
202
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Block via exit code 2 + stderr feedback.
|
|
207
|
+
* Works for PostToolUse, PostToolUseFailure.
|
|
208
|
+
* The reason becomes the stderr message (fed to Claude as system-reminder).
|
|
209
|
+
* If context is provided, it's prepended to the stderr message for richer feedback.
|
|
210
|
+
* NOTE: Exit 2 causes Claude Code to ignore all JSON stdout — only stderr matters.
|
|
211
|
+
*/
|
|
212
|
+
export function emitBlockViaExit(reason: string, context?: string): void {
|
|
213
|
+
const stderrMessage = context ? `${context}\n\n${reason}` : reason;
|
|
214
|
+
_logEmit("block", stderrMessage.length, {
|
|
215
|
+
event: _lastHookEvent ?? "unknown",
|
|
216
|
+
blockReason: reason,
|
|
217
|
+
mechanism: "exit2",
|
|
218
|
+
});
|
|
219
|
+
process.stderr.write(stderrMessage + "\n");
|
|
220
|
+
throw new Error("SystemExit:2");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Block via top-level { decision: "block", reason }.
|
|
225
|
+
* Works for Stop and SubagentStop events.
|
|
226
|
+
* These events do NOT support additionalContext — only reason is available.
|
|
227
|
+
*/
|
|
228
|
+
export function emitBlockTopLevel(reason: string): void {
|
|
229
|
+
const eventName = _lastHookEvent ?? undefined;
|
|
230
|
+
if (eventName && eventName !== "Stop" && eventName !== "SubagentStop") {
|
|
231
|
+
logWarn(_cachedHookName ?? "unknown",
|
|
232
|
+
`emitBlockTopLevel() called from ${eventName} — only works for Stop/SubagentStop`);
|
|
233
|
+
}
|
|
234
|
+
const out = { decision: "block", reason };
|
|
235
|
+
_logEmit("block", reason.length, {
|
|
236
|
+
event: eventName ?? "unknown",
|
|
237
|
+
blockReason: reason,
|
|
238
|
+
mechanism: "topLevelDecision",
|
|
239
|
+
});
|
|
240
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Respond to a PermissionRequest with allow/deny.
|
|
245
|
+
* Only works for PermissionRequest hooks.
|
|
246
|
+
*/
|
|
247
|
+
export function emitPermissionDecision(
|
|
248
|
+
behavior: "allow" | "deny",
|
|
249
|
+
opts?: { message?: string; updatedInput?: Record<string, unknown>; updatedPermissions?: Record<string, unknown> },
|
|
250
|
+
): void {
|
|
251
|
+
const out: PermissionRequestOutput = {
|
|
252
|
+
decision: {
|
|
253
|
+
behavior,
|
|
254
|
+
...(opts?.message ? { message: opts.message } : {}),
|
|
255
|
+
...(opts?.updatedInput ? { updatedInput: opts.updatedInput } : {}),
|
|
256
|
+
...(opts?.updatedPermissions ? { updatedPermissions: opts.updatedPermissions } : {}),
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
_logEmit("block", 0, {
|
|
260
|
+
event: _lastHookEvent ?? "unknown",
|
|
261
|
+
blockReason: `permission:${behavior}`,
|
|
262
|
+
mechanism: "permissionRequest",
|
|
263
|
+
});
|
|
264
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Unified block dispatcher — auto-detects the correct blocking mechanism
|
|
269
|
+
* based on the current hook event type.
|
|
270
|
+
*
|
|
271
|
+
* PreToolUse → permissionDecision: "deny" (via emitContextAndBlock)
|
|
272
|
+
* UserPromptSubmit → top-level decision: "block" (via emitBlockPrompt)
|
|
273
|
+
* PostToolUse/PostToolUseFailure → exit(2) + stderr (via emitBlockViaExit)
|
|
274
|
+
* Stop/SubagentStop → top-level { decision: "block", reason } (via emitBlockTopLevel)
|
|
275
|
+
* PermissionRequest → decision: { behavior: "deny" } (via emitPermissionDecision)
|
|
276
|
+
* SessionStart/Notification/SubagentStart/SessionEnd/etc. → warn and no-op
|
|
277
|
+
*
|
|
278
|
+
* This is the RECOMMENDED universal blocking API. Hook authors should use
|
|
279
|
+
* emitBlock() and let the library handle event-specific dispatch.
|
|
280
|
+
*/
|
|
281
|
+
export function emitBlock(reason: string, context?: string): void {
|
|
282
|
+
const event = _lastHookEvent;
|
|
283
|
+
switch (event) {
|
|
284
|
+
case "PermissionRequest":
|
|
285
|
+
emitPermissionDecision("deny", { message: reason });
|
|
286
|
+
break;
|
|
287
|
+
case "PostToolUse":
|
|
288
|
+
case "PostToolUseFailure":
|
|
289
|
+
emitBlockViaExit(reason, context);
|
|
290
|
+
break;
|
|
291
|
+
case "PreToolUse":
|
|
292
|
+
emitContextAndBlock(context ?? reason, reason);
|
|
293
|
+
break;
|
|
294
|
+
case "Stop":
|
|
295
|
+
case "SubagentStop":
|
|
296
|
+
emitBlockTopLevel(reason);
|
|
304
297
|
break;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
process.exit(code);
|
|
583
|
-
}
|
|
584
|
-
|
|
298
|
+
case "UserPromptSubmit":
|
|
299
|
+
emitBlockPrompt(reason, context);
|
|
300
|
+
break;
|
|
301
|
+
default: {
|
|
302
|
+
logWarn(_cachedHookName ?? "unknown",
|
|
303
|
+
`emitBlock() called from ${event ?? "unknown"} — no blocking mechanism exists for this event type, ignoring`);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Auto-detect template origin from the hook script path.
|
|
311
|
+
*/
|
|
312
|
+
function detectTemplate(scriptPath = ""): string {
|
|
313
|
+
const p = (scriptPath || (process.argv[1] ?? "")).replaceAll('\\', "/");
|
|
314
|
+
if (p.includes("/_shared/hooks/") || p.startsWith("_shared/hooks/")) {
|
|
315
|
+
return "shared";
|
|
316
|
+
}
|
|
317
|
+
const match = p.match(/_([a-z][a-z0-9-]*)\/hooks\//);
|
|
318
|
+
if (match?.[1]) return match[1]; // e.g., "cc-native"
|
|
319
|
+
return "unknown";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Parse context window from hook input.
|
|
324
|
+
* Returns [tokensUsed, maxTokens] or [null, null].
|
|
325
|
+
* See SPEC.md §5.9
|
|
326
|
+
*/
|
|
327
|
+
export function parseContextWindow(
|
|
328
|
+
hookInput: HookInput,
|
|
329
|
+
): [number | null, number | null] {
|
|
330
|
+
const contextWindow = hookInput.context_window;
|
|
331
|
+
if (!contextWindow) return [null, null];
|
|
332
|
+
|
|
333
|
+
const currentUsage = contextWindow.current_usage;
|
|
334
|
+
if (!currentUsage) return [null, null];
|
|
335
|
+
|
|
336
|
+
const cacheRead = currentUsage.cache_read_input_tokens ?? 0;
|
|
337
|
+
const inputTokens = currentUsage.input_tokens ?? 0;
|
|
338
|
+
const cacheCreation = currentUsage.cache_creation_input_tokens ?? 0;
|
|
339
|
+
const outputTokens = currentUsage.output_tokens ?? 0;
|
|
340
|
+
|
|
341
|
+
const contentTokens = cacheRead + inputTokens + cacheCreation + outputTokens;
|
|
342
|
+
const tokensUsed = contentTokens + CONTEXT_BASELINE_TOKENS;
|
|
343
|
+
const maxTokens = contextWindow.context_window_size ?? DEFAULT_CONTEXT_WINDOW_SIZE;
|
|
344
|
+
|
|
345
|
+
return [tokensUsed, maxTokens];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get context percentage remaining with fallback.
|
|
350
|
+
* Returns [percentRemaining, tokensUsed, maxTokens] or [null, null, null].
|
|
351
|
+
* See SPEC.md §5.9
|
|
352
|
+
*/
|
|
353
|
+
export function getContextPercentRemaining(
|
|
354
|
+
hookInput: HookInput,
|
|
355
|
+
): [number | null, number | null, number | null] {
|
|
356
|
+
const [tokensUsed, maxTokens] = parseContextWindow(hookInput);
|
|
357
|
+
|
|
358
|
+
if (tokensUsed !== null && maxTokens !== null && maxTokens > 0) {
|
|
359
|
+
const remaining = maxTokens - tokensUsed;
|
|
360
|
+
const percentRemaining = Math.max(
|
|
361
|
+
0,
|
|
362
|
+
Math.min(100, Math.round((remaining / maxTokens) * 100)),
|
|
363
|
+
);
|
|
364
|
+
return [percentRemaining, tokensUsed, maxTokens];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Source 2: context.json fallback (written by status_line.py)
|
|
368
|
+
try {
|
|
369
|
+
const sessionId = hookInput.session_id;
|
|
370
|
+
if (sessionId) {
|
|
371
|
+
const projectRoot = getProjectRoot(hookInput.cwd);
|
|
372
|
+
const context = getContextBySessionId(sessionId, projectRoot);
|
|
373
|
+
if (context?.last_session?.context_remaining_pct !== undefined) {
|
|
374
|
+
return [context.last_session.context_remaining_pct, null, null];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
// Fallback failed — degrade gracefully
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return [null, null, null];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Read stdin early and extract session_id + event metadata.
|
|
386
|
+
* Stashes parsed input for loadHookInput() to consume later.
|
|
387
|
+
*/
|
|
388
|
+
function _earlyReadInput(prefetchedInput?: Record<string, any>): void {
|
|
389
|
+
if (prefetchedInput !== undefined) {
|
|
390
|
+
_prefetchedInput = prefetchedInput;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// If we already have prefetched input, extract metadata from it
|
|
394
|
+
if (_prefetchedInput && typeof _prefetchedInput === "object") {
|
|
395
|
+
_lastHookEvent = _prefetchedInput.hook_event_name ?? null;
|
|
396
|
+
_lastToolName = _prefetchedInput.tool_name ?? null;
|
|
397
|
+
if (_prefetchedInput.session_id) {
|
|
398
|
+
setSessionId(_prefetchedInput.session_id);
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Read stdin now so HOOK_START can include sid
|
|
404
|
+
try {
|
|
405
|
+
const inputData = fs.readFileSync(0, "utf-8").trim();
|
|
406
|
+
if (inputData) {
|
|
407
|
+
const parsed = JSON.parse(inputData);
|
|
408
|
+
if (parsed && typeof parsed === "object") {
|
|
409
|
+
_prefetchedInput = parsed;
|
|
410
|
+
_lastHookEvent = parsed.hook_event_name ?? null;
|
|
411
|
+
_lastToolName = parsed.tool_name ?? null;
|
|
412
|
+
if (parsed.session_id) {
|
|
413
|
+
setSessionId(parsed.session_id);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
// Non-fatal — loadHookInput will return null
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Standard hook entry point with lifecycle logging.
|
|
424
|
+
* See SPEC.md §5.7
|
|
425
|
+
*/
|
|
426
|
+
export function runHook(
|
|
427
|
+
mainFunc: () => number | void,
|
|
428
|
+
hookName = "unknown",
|
|
429
|
+
prefetchedInput?: Record<string, any>,
|
|
430
|
+
): never {
|
|
431
|
+
_earlyReadInput(prefetchedInput);
|
|
432
|
+
_cachedHookName = hookName;
|
|
433
|
+
|
|
434
|
+
const startTime = performance.now();
|
|
435
|
+
const template = detectTemplate();
|
|
436
|
+
const event = _lastHookEvent ?? "unknown";
|
|
437
|
+
const tool = _lastToolName;
|
|
438
|
+
|
|
439
|
+
const startData: Record<string, any> = {
|
|
440
|
+
lifecycle: "start",
|
|
441
|
+
template,
|
|
442
|
+
event,
|
|
443
|
+
};
|
|
444
|
+
if (tool) startData.tool = tool;
|
|
445
|
+
hookLog("info", hookName, "HOOK_START", { data: startData });
|
|
446
|
+
|
|
447
|
+
let exitCode = 0;
|
|
448
|
+
let status = "success";
|
|
449
|
+
let errorInfo: [Error, string] | null = null;
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const result = mainFunc();
|
|
453
|
+
exitCode = typeof result === "number" ? result : 0;
|
|
454
|
+
status = exitCode !== 0 ? "blocked" : "success";
|
|
455
|
+
} catch (error: any) {
|
|
456
|
+
if (error instanceof Error && error.message.startsWith("SystemExit:")) {
|
|
457
|
+
const code = parseInt(error.message.slice(11), 10);
|
|
458
|
+
exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
|
|
459
|
+
status = exitCode !== 0 ? "blocked" : "success";
|
|
460
|
+
} else {
|
|
461
|
+
exitCode = 0; // Non-blocking
|
|
462
|
+
status = "error";
|
|
463
|
+
const stack = error instanceof Error ? error.stack ?? "" : "";
|
|
464
|
+
errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
_emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
|
|
469
|
+
process.exit(exitCode);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Async variant of runHook for hooks that need await (e.g., AI inference).
|
|
474
|
+
* Provides identical structured JSONL lifecycle logging as runHook.
|
|
475
|
+
* See SPEC.md §5.7
|
|
476
|
+
*/
|
|
477
|
+
export function runHookAsync(
|
|
478
|
+
mainFunc: () => Promise<number | void>,
|
|
479
|
+
hookName = "unknown",
|
|
480
|
+
prefetchedInput?: Record<string, any>,
|
|
481
|
+
): void {
|
|
482
|
+
_earlyReadInput(prefetchedInput);
|
|
483
|
+
_cachedHookName = hookName;
|
|
484
|
+
|
|
485
|
+
const startTime = performance.now();
|
|
486
|
+
const template = detectTemplate();
|
|
487
|
+
const event = _lastHookEvent ?? "unknown";
|
|
488
|
+
const tool = _lastToolName;
|
|
489
|
+
|
|
490
|
+
const startData: Record<string, any> = {
|
|
491
|
+
lifecycle: "start",
|
|
492
|
+
template,
|
|
493
|
+
event,
|
|
494
|
+
};
|
|
495
|
+
if (tool) startData.tool = tool;
|
|
496
|
+
hookLog("info", hookName, "HOOK_START", { data: startData });
|
|
497
|
+
|
|
498
|
+
mainFunc()
|
|
499
|
+
.then((result) => {
|
|
500
|
+
const exitCode = typeof result === "number" ? result : 0;
|
|
501
|
+
_emitHookEnd(hookName, startTime, exitCode, exitCode !== 0 ? "blocked" : "success", null, startData, event, tool, template);
|
|
502
|
+
_drainAndExit(exitCode);
|
|
503
|
+
})
|
|
504
|
+
.catch((error: any) => {
|
|
505
|
+
let exitCode = 0;
|
|
506
|
+
let status = "error";
|
|
507
|
+
let errorInfo: [Error, string] | null = null;
|
|
508
|
+
|
|
509
|
+
if (error instanceof Error && error.message.startsWith("SystemExit:")) {
|
|
510
|
+
const code = parseInt(error.message.slice(11), 10);
|
|
511
|
+
exitCode = isNaN(code) ? (error.message.slice(11) ? 1 : 0) : code;
|
|
512
|
+
status = exitCode !== 0 ? "blocked" : "success";
|
|
513
|
+
} else {
|
|
514
|
+
exitCode = 0; // Non-blocking (fail open)
|
|
515
|
+
const stack = error instanceof Error ? error.stack ?? "" : "";
|
|
516
|
+
errorInfo = [error instanceof Error ? error : new Error(String(error)), stack];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_emitHookEnd(hookName, startTime, exitCode, status, errorInfo, startData, event, tool, template);
|
|
520
|
+
_drainAndExit(exitCode);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Shared HOOK_END logic for runHook and runHookAsync */
|
|
525
|
+
function _emitHookEnd(
|
|
526
|
+
hookName: string,
|
|
527
|
+
startTime: number,
|
|
528
|
+
exitCode: number,
|
|
529
|
+
status: string,
|
|
530
|
+
errorInfo: [Error, string] | null,
|
|
531
|
+
startData: Record<string, any>,
|
|
532
|
+
event: string,
|
|
533
|
+
tool: string | null,
|
|
534
|
+
template: string,
|
|
535
|
+
): void {
|
|
536
|
+
// Retroactive HOOK_START to per-context log (context_path resolved after main runs)
|
|
537
|
+
const resolvedAfter = _getContextPath();
|
|
538
|
+
if (resolvedAfter && fs.existsSync(resolvedAfter)) {
|
|
539
|
+
hookLog("info", hookName, "HOOK_START", { data: startData });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const durationMs = Math.round((performance.now() - startTime) * 10) / 10;
|
|
543
|
+
const endEvent = _lastHookEvent ?? event;
|
|
544
|
+
const endTool = _lastToolName ?? tool;
|
|
545
|
+
const endData: Record<string, any> = {
|
|
546
|
+
lifecycle: "end",
|
|
547
|
+
status,
|
|
548
|
+
duration_ms: durationMs,
|
|
549
|
+
exit_code: exitCode,
|
|
550
|
+
template,
|
|
551
|
+
event: endEvent,
|
|
552
|
+
};
|
|
553
|
+
if (endTool) endData.tool = endTool;
|
|
554
|
+
|
|
555
|
+
if (errorInfo) {
|
|
556
|
+
const [err, tb] = errorInfo;
|
|
557
|
+
endData.error_type = err.constructor.name;
|
|
558
|
+
hookLog("error", hookName, `[${endEvent}] ${err.constructor.name}: ${String(err).replaceAll(/[\n\r]/g, " ").slice(0, 200)}`, { traceback_str: tb });
|
|
559
|
+
hookLog("error", hookName, `HOOK_END: ${err}`, { data: endData, traceback_str: tb });
|
|
560
|
+
} else if (status === "blocked") {
|
|
561
|
+
hookLog("warn", hookName, "HOOK_END", { data: endData });
|
|
562
|
+
} else {
|
|
563
|
+
hookLog("info", hookName, "HOOK_END", { data: endData });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Drain stdout before exiting to ensure pipe consumers receive all data.
|
|
569
|
+
* On Windows, stdout to a pipe is fully buffered — process.exit() can
|
|
570
|
+
* discard unflushed data. This waits for the write buffer to drain.
|
|
571
|
+
*/
|
|
572
|
+
function _drainAndExit(code: number): void {
|
|
573
|
+
// If stdout is already finished or not writable, exit immediately
|
|
574
|
+
if (!process.stdout.writable || process.stdout.writableFinished) {
|
|
575
|
+
process.exit(code);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Attempt to end stdout and wait for drain
|
|
579
|
+
const timeout = setTimeout(() => process.exit(code), 1000); // safety fallback
|
|
580
|
+
process.stdout.end(() => {
|
|
581
|
+
clearTimeout(timeout);
|
|
582
|
+
process.exit(code);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
585
|
|
|
586
586
|
export {logInfo, logError, logBlocking, logHookError, logDiagnostic, setContextPath, hookLog, logDebug, logWarn} from "./logger.js";
|