aiwcli 0.11.1 → 0.12.1
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/dist/commands/clear.d.ts +8 -0
- package/dist/commands/clear.js +86 -0
- package/dist/lib/bmad-installer.d.ts +2 -27
- package/dist/lib/bmad-installer.js +3 -43
- package/dist/lib/claude-settings-types.d.ts +2 -1
- package/dist/lib/env-compat.d.ts +0 -8
- package/dist/lib/env-compat.js +0 -12
- package/dist/lib/git/index.d.ts +0 -1
- package/dist/lib/gitignore-manager.d.ts +0 -2
- package/dist/lib/gitignore-manager.js +1 -1
- package/dist/lib/hooks-merger.d.ts +1 -15
- package/dist/lib/hooks-merger.js +1 -1
- package/dist/lib/index.d.ts +3 -7
- package/dist/lib/index.js +3 -11
- package/dist/lib/output.d.ts +2 -1
- package/dist/lib/settings-hierarchy.d.ts +1 -13
- package/dist/lib/settings-hierarchy.js +1 -1
- package/dist/lib/template-installer.d.ts +5 -9
- package/dist/lib/template-installer.js +3 -13
- package/dist/lib/template-linter.d.ts +3 -10
- package/dist/lib/template-linter.js +2 -2
- package/dist/lib/template-resolver.d.ts +6 -0
- package/dist/lib/template-resolver.js +10 -0
- package/dist/lib/template-settings-reconstructor.d.ts +1 -1
- package/dist/lib/template-settings-reconstructor.js +17 -24
- package/dist/lib/terminal.d.ts +3 -14
- package/dist/lib/terminal.js +0 -4
- package/dist/lib/version.d.ts +2 -11
- package/dist/lib/version.js +3 -3
- package/dist/lib/windsurf-hooks-merger.d.ts +1 -15
- package/dist/lib/windsurf-hooks-merger.js +1 -1
- package/dist/templates/_shared/.codex/workflows/handoff.md +1 -1
- package/dist/templates/_shared/.windsurf/workflows/handoff.md +1 -1
- package/dist/templates/_shared/hooks-ts/session_end.ts +75 -4
- package/dist/templates/_shared/hooks-ts/session_start.ts +11 -13
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +6 -8
- package/dist/templates/_shared/lib-ts/CLAUDE.md +56 -7
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +176 -29
- package/dist/templates/_shared/lib-ts/base/logger.ts +1 -1
- package/dist/templates/_shared/lib-ts/base/state-io.ts +11 -2
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +181 -165
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +14 -13
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +3 -2
- package/dist/templates/_shared/lib-ts/package.json +1 -2
- package/dist/templates/_shared/lib-ts/templates/plan-context.ts +27 -34
- package/dist/templates/_shared/lib-ts/types.ts +17 -2
- package/dist/templates/_shared/scripts/resume_handoff.ts +4 -4
- package/dist/templates/_shared/scripts/save_handoff.ts +7 -7
- package/dist/templates/_shared/scripts/status_line.ts +104 -71
- package/dist/templates/_shared/workflows/handoff.md +1 -1
- package/dist/templates/cc-native/.claude/settings.json +182 -175
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +23 -1
- package/dist/templates/cc-native/_cc-native/agents/plan-questions/PLAN-QUESTIONER.md +70 -0
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +6 -1
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +142 -111
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_subagent.ts +54 -0
- package/dist/templates/cc-native/_cc-native/hooks/enhance_plan_post_write.ts +52 -0
- package/dist/templates/cc-native/_cc-native/hooks/mark_questions_asked.ts +53 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +19 -19
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +6 -5
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +114 -83
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +107 -10
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +1 -1
- package/dist/templates/cc-native/_cc-native/lib-ts/corroboration.ts +6 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +0 -4
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +40 -219
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-enhancement.ts +41 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/plan-questions.ts +102 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +26 -227
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/base/base-agent.ts +217 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +4 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/claude-agent.ts +65 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/codex-agent.ts +185 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/gemini-agent.ts +39 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/providers/orchestrator-claude-agent.ts +195 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/schemas.ts +201 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +2 -2
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +17 -16
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +13 -108
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +3 -3
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +2 -14
- package/oclif.manifest.json +1 -1
- package/package.json +1 -2
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +0 -119
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +0 -130
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +0 -107
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-EVOLUTION.md → plan-review/ARCH-EVOLUTION.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-PATTERNS.md → plan-review/ARCH-PATTERNS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ARCH-STRUCTURE.md → plan-review/ARCH-STRUCTURE.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-TRACER.md → plan-review/ASSUMPTION-TRACER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{CLARITY-AUDITOR.md → plan-review/CLARITY-AUDITOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-FEASIBILITY.md → plan-review/COMPLETENESS-FEASIBILITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-GAPS.md → plan-review/COMPLETENESS-GAPS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{COMPLETENESS-ORDERING.md → plan-review/COMPLETENESS-ORDERING.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{CONSTRAINT-VALIDATOR.md → plan-review/CONSTRAINT-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-ADR-VALIDATOR.md → plan-review/DESIGN-ADR-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DESIGN-SCALE-MATCHER.md → plan-review/DESIGN-SCALE-MATCHER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DEVILS-ADVOCATE.md → plan-review/DEVILS-ADVOCATE.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{DOCUMENTATION-PHILOSOPHY.md → plan-review/DOCUMENTATION-PHILOSOPHY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{HANDOFF-READINESS.md → plan-review/HANDOFF-READINESS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY.md → plan-review/HIDDEN-COMPLEXITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{INCREMENTAL-DELIVERY.md → plan-review/INCREMENTAL-DELIVERY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-DEPENDENCY.md → plan-review/RISK-DEPENDENCY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-FMEA.md → plan-review/RISK-FMEA.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-PREMORTEM.md → plan-review/RISK-PREMORTEM.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{RISK-REVERSIBILITY.md → plan-review/RISK-REVERSIBILITY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SCOPE-BOUNDARY.md → plan-review/SCOPE-BOUNDARY.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SIMPLICITY-GUARDIAN.md → plan-review/SIMPLICITY-GUARDIAN.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{SKEPTIC.md → plan-review/SKEPTIC.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-BEHAVIOR-AUDITOR.md → plan-review/TESTDRIVEN-BEHAVIOR-AUDITOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-CHARACTERIZATION.md → plan-review/TESTDRIVEN-CHARACTERIZATION.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-FIRST-VALIDATOR.md → plan-review/TESTDRIVEN-FIRST-VALIDATOR.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TESTDRIVEN-PYRAMID-ANALYZER.md → plan-review/TESTDRIVEN-PYRAMID-ANALYZER.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-COSTS.md → plan-review/TRADEOFF-COSTS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{TRADEOFF-STAKEHOLDERS.md → plan-review/TRADEOFF-STAKEHOLDERS.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-COVERAGE.md → plan-review/VERIFY-COVERAGE.md} +0 -0
- /package/dist/templates/cc-native/_cc-native/agents/{VERIFY-STRENGTH.md → plan-review/VERIFY-STRENGTH.md} +0 -0
|
@@ -4,27 +4,27 @@
|
|
|
4
4
|
* See SPEC.md §5
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import * as fs from "node:fs";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
8
|
|
|
9
9
|
import { getProjectRoot } from "./constants.js";
|
|
10
|
-
import {
|
|
10
|
+
import { logDebug, logWarn, hookLog, setSessionId, getContextPath as _getContextPath } from "./logger.js";
|
|
11
11
|
import { getContextBySessionId } from "../context/context-store.js";
|
|
12
|
-
import type { HookInput, HookOutput } from "../types.js";
|
|
12
|
+
import type { HookInput, HookOutput, PermissionRequestOutput } from "../types.js";
|
|
13
13
|
|
|
14
14
|
// Re-export logger functions for convenience (matches Python hook_utils re-exports)
|
|
15
|
-
|
|
15
|
+
export { setSessionId };
|
|
16
16
|
|
|
17
17
|
// Context window baseline: tokens not visible in hook data §5.9
|
|
18
18
|
export const CONTEXT_BASELINE_TOKENS = 22_600;
|
|
19
19
|
export const DEFAULT_CONTEXT_WINDOW_SIZE = 200_000;
|
|
20
20
|
|
|
21
21
|
// Event metadata stash — populated by loadHookInput(), read by runHook()
|
|
22
|
-
let _lastHookEvent:
|
|
23
|
-
let _lastToolName:
|
|
24
|
-
let _cachedHookName:
|
|
22
|
+
let _lastHookEvent: string | null = null;
|
|
23
|
+
let _lastToolName: string | null = null;
|
|
24
|
+
let _cachedHookName: string | null = null;
|
|
25
25
|
|
|
26
26
|
// Pre-fetched input stash
|
|
27
|
-
let _prefetchedInput:
|
|
27
|
+
let _prefetchedInput: Record<string, any> | null = null;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Load and parse JSON from stdin (or return prefetched input if set).
|
|
@@ -39,13 +39,12 @@ export function loadHookInput(): HookInput | null {
|
|
|
39
39
|
_lastHookEvent = result.hook_event_name ?? null;
|
|
40
40
|
_lastToolName = result.tool_name ?? null;
|
|
41
41
|
}
|
|
42
|
-
|
|
43
42
|
return result as HookInput;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
try {
|
|
47
46
|
// Read entire stdin using fd 0 (cross-platform, works on Windows)
|
|
48
|
-
const inputData = fs.readFileSync(0, "
|
|
47
|
+
const inputData = fs.readFileSync(0, "utf-8").trim();
|
|
49
48
|
if (!inputData) return null;
|
|
50
49
|
|
|
51
50
|
const result = JSON.parse(inputData);
|
|
@@ -53,7 +52,6 @@ export function loadHookInput(): HookInput | null {
|
|
|
53
52
|
_lastHookEvent = result.hook_event_name ?? null;
|
|
54
53
|
_lastToolName = result.tool_name ?? null;
|
|
55
54
|
}
|
|
56
|
-
|
|
57
55
|
return result as HookInput;
|
|
58
56
|
} catch {
|
|
59
57
|
return null;
|
|
@@ -80,7 +78,7 @@ export function validateHookEvent(
|
|
|
80
78
|
*/
|
|
81
79
|
export function getToolInput(
|
|
82
80
|
payload: HookInput,
|
|
83
|
-
):
|
|
81
|
+
): Record<string, any> | null {
|
|
84
82
|
const toolInput = payload.tool_input;
|
|
85
83
|
return toolInput && typeof toolInput === "object" ? toolInput : null;
|
|
86
84
|
}
|
|
@@ -101,7 +99,6 @@ export function checkSkipPersistence(
|
|
|
101
99
|
logDebug(hookName, "Skipping persistence (skip_persistence flag set)");
|
|
102
100
|
return true;
|
|
103
101
|
}
|
|
104
|
-
|
|
105
102
|
return false;
|
|
106
103
|
}
|
|
107
104
|
|
|
@@ -109,10 +106,23 @@ export function checkSkipPersistence(
|
|
|
109
106
|
* Emit hookSpecificOutput with additionalContext to stdout.
|
|
110
107
|
* hookEventName is required by Claude Code's Zod validator (discriminated union).
|
|
111
108
|
* Auto-detected from stdin payload (set by loadHookInput/runHook).
|
|
109
|
+
*
|
|
110
|
+
* SubagentStop and Stop events use top-level systemMessage field instead of hookSpecificOutput.
|
|
112
111
|
* See SPEC.md §5.5
|
|
113
112
|
*/
|
|
114
113
|
export function emitContext(additionalContext: string): void {
|
|
115
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
|
|
116
126
|
const out: HookOutput = {
|
|
117
127
|
hookSpecificOutput: {
|
|
118
128
|
...(eventName ? { hookEventName: eventName } : {}),
|
|
@@ -120,7 +130,8 @@ export function emitContext(additionalContext: string): void {
|
|
|
120
130
|
},
|
|
121
131
|
};
|
|
122
132
|
const json = JSON.stringify(out);
|
|
123
|
-
|
|
133
|
+
const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
|
|
134
|
+
_logEmit("context", additionalContext.length, { event: eventDesc, additionalContext });
|
|
124
135
|
process.stdout.write(json + "\n");
|
|
125
136
|
}
|
|
126
137
|
|
|
@@ -135,6 +146,12 @@ export function emitContextAndBlock(
|
|
|
135
146
|
reason: string,
|
|
136
147
|
): void {
|
|
137
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;
|
|
138
155
|
const out: HookOutput = {
|
|
139
156
|
hookSpecificOutput: {
|
|
140
157
|
...(eventName ? { hookEventName: eventName } : {}),
|
|
@@ -144,19 +161,151 @@ export function emitContextAndBlock(
|
|
|
144
161
|
},
|
|
145
162
|
};
|
|
146
163
|
const json = JSON.stringify(out);
|
|
147
|
-
|
|
164
|
+
const eventDesc = tool ? `${eventName}:${tool}` : eventName ?? "unknown";
|
|
165
|
+
_logEmit("block", additionalContext.length, { event: eventDesc, additionalContext, blockReason: reason });
|
|
148
166
|
process.stdout.write(json + "\n");
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
/** Log hook output (context or block) to hook-log.jsonl for visibility. */
|
|
152
|
-
function _logEmit(type: "
|
|
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 {
|
|
153
171
|
const hook = _cachedHookName ?? "unknown";
|
|
172
|
+
const event = payload.event ?? "unknown";
|
|
173
|
+
const mechanism = payload.mechanism ? ` via ${payload.mechanism}` : "";
|
|
154
174
|
const msg = type === "block"
|
|
155
|
-
? `HOOK_OUTPUT [${type}] ${chars} chars, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
|
|
156
|
-
: `HOOK_OUTPUT [${type}] ${chars} chars`;
|
|
175
|
+
? `HOOK_OUTPUT [${type}] ${event} ${chars} chars${mechanism}, reason="${(payload.blockReason ?? "").slice(0, 80)}"`
|
|
176
|
+
: `HOOK_OUTPUT [${type}] ${event} ${chars} chars`;
|
|
157
177
|
hookLog("info", hook, msg, { data: payload });
|
|
158
178
|
}
|
|
159
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`);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
160
309
|
/**
|
|
161
310
|
* Auto-detect template origin from the hook script path.
|
|
162
311
|
*/
|
|
@@ -165,7 +314,6 @@ function detectTemplate(scriptPath = ""): string {
|
|
|
165
314
|
if (p.includes("/_shared/hooks/") || p.startsWith("_shared/hooks/")) {
|
|
166
315
|
return "shared";
|
|
167
316
|
}
|
|
168
|
-
|
|
169
317
|
const match = p.match(/_([a-z][a-z0-9-]*)\/hooks\//);
|
|
170
318
|
if (match?.[1]) return match[1]; // e.g., "cc-native"
|
|
171
319
|
return "unknown";
|
|
@@ -178,7 +326,7 @@ function detectTemplate(scriptPath = ""): string {
|
|
|
178
326
|
*/
|
|
179
327
|
export function parseContextWindow(
|
|
180
328
|
hookInput: HookInput,
|
|
181
|
-
): [
|
|
329
|
+
): [number | null, number | null] {
|
|
182
330
|
const contextWindow = hookInput.context_window;
|
|
183
331
|
if (!contextWindow) return [null, null];
|
|
184
332
|
|
|
@@ -204,7 +352,7 @@ export function parseContextWindow(
|
|
|
204
352
|
*/
|
|
205
353
|
export function getContextPercentRemaining(
|
|
206
354
|
hookInput: HookInput,
|
|
207
|
-
): [
|
|
355
|
+
): [number | null, number | null, number | null] {
|
|
208
356
|
const [tokensUsed, maxTokens] = parseContextWindow(hookInput);
|
|
209
357
|
|
|
210
358
|
if (tokensUsed !== null && maxTokens !== null && maxTokens > 0) {
|
|
@@ -249,13 +397,12 @@ function _earlyReadInput(prefetchedInput?: Record<string, any>): void {
|
|
|
249
397
|
if (_prefetchedInput.session_id) {
|
|
250
398
|
setSessionId(_prefetchedInput.session_id);
|
|
251
399
|
}
|
|
252
|
-
|
|
253
400
|
return;
|
|
254
401
|
}
|
|
255
402
|
|
|
256
403
|
// Read stdin now so HOOK_START can include sid
|
|
257
404
|
try {
|
|
258
|
-
const inputData = fs.readFileSync(0, "
|
|
405
|
+
const inputData = fs.readFileSync(0, "utf-8").trim();
|
|
259
406
|
if (inputData) {
|
|
260
407
|
const parsed = JSON.parse(inputData);
|
|
261
408
|
if (parsed && typeof parsed === "object") {
|
|
@@ -304,7 +451,7 @@ export function runHook(
|
|
|
304
451
|
try {
|
|
305
452
|
const result = mainFunc();
|
|
306
453
|
exitCode = typeof result === "number" ? result : 0;
|
|
307
|
-
status = exitCode
|
|
454
|
+
status = exitCode !== 0 ? "blocked" : "success";
|
|
308
455
|
} catch (error: any) {
|
|
309
456
|
if (error instanceof Error && error.message.startsWith("SystemExit:")) {
|
|
310
457
|
const code = parseInt(error.message.slice(11), 10);
|
|
@@ -351,7 +498,7 @@ export function runHookAsync(
|
|
|
351
498
|
mainFunc()
|
|
352
499
|
.then((result) => {
|
|
353
500
|
const exitCode = typeof result === "number" ? result : 0;
|
|
354
|
-
_emitHookEnd(hookName, startTime, exitCode, exitCode
|
|
501
|
+
_emitHookEnd(hookName, startTime, exitCode, exitCode !== 0 ? "blocked" : "success", null, startData, event, tool, template);
|
|
355
502
|
_drainAndExit(exitCode);
|
|
356
503
|
})
|
|
357
504
|
.catch((error: any) => {
|
|
@@ -383,7 +530,7 @@ function _emitHookEnd(
|
|
|
383
530
|
errorInfo: [Error, string] | null,
|
|
384
531
|
startData: Record<string, any>,
|
|
385
532
|
event: string,
|
|
386
|
-
tool:
|
|
533
|
+
tool: string | null,
|
|
387
534
|
template: string,
|
|
388
535
|
): void {
|
|
389
536
|
// Retroactive HOOK_START to per-context log (context_path resolved after main runs)
|
|
@@ -435,5 +582,5 @@ function _drainAndExit(code: number): void {
|
|
|
435
582
|
process.exit(code);
|
|
436
583
|
});
|
|
437
584
|
}
|
|
438
|
-
|
|
439
|
-
export {
|
|
585
|
+
|
|
586
|
+
export {logInfo, logError, logBlocking, logHookError, logDiagnostic, setContextPath, hookLog, logDebug, logWarn} from "./logger.js";
|
|
@@ -235,7 +235,7 @@ export function logHookError(
|
|
|
235
235
|
tracebackStr = "",
|
|
236
236
|
): void {
|
|
237
237
|
const errStr = typeof error === "string" ? error : String(error);
|
|
238
|
-
const msg = errStr.
|
|
238
|
+
const msg = errStr.replaceAll(/[\n\r]/g, " ").slice(0, 200);
|
|
239
239
|
const errType =
|
|
240
240
|
typeof error === "object" && error !== null
|
|
241
241
|
? error.constructor.name
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
|
-
import * as path from "node:path";
|
|
8
|
+
import * as path from "node:path";
|
|
9
9
|
|
|
10
10
|
import { atomicWrite } from "./atomic-write.js";
|
|
11
11
|
import { getContextDir } from "./constants.js";
|
|
@@ -30,7 +30,7 @@ export function toDict(state: ContextState): Record<string, unknown> {
|
|
|
30
30
|
if (value !== null && value !== undefined) {
|
|
31
31
|
result[key] = value;
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
|
34
34
|
|
|
35
35
|
return result;
|
|
36
36
|
}
|
|
@@ -112,5 +112,14 @@ export function dictToState(data: Record<string, any>): ContextState {
|
|
|
112
112
|
if ("handoff_path" in data) state.handoff_path = data.handoff_path;
|
|
113
113
|
if ("last_session" in data) state.last_session = data.last_session;
|
|
114
114
|
|
|
115
|
+
// Preserve method-specific extension data (e.g., cc_native) that isn't
|
|
116
|
+
// part of the core ContextState interface. Without this, round-trip
|
|
117
|
+
// read→write cycles silently drop extension fields.
|
|
118
|
+
for (const key of Object.keys(data)) {
|
|
119
|
+
if (!(key in state)) {
|
|
120
|
+
state[key] = data[key];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
return state as ContextState;
|
|
116
125
|
}
|