howtofixcli 0.7.13 → 0.7.15

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.
Files changed (161) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-3FYCFLNK.js +1 -0
  3. package/dist/chunk-56SEAGAQ.js +1 -0
  4. package/dist/chunk-6FUWMLWO.js +1 -0
  5. package/dist/chunk-7CHDNTH6.js +1 -0
  6. package/dist/chunk-7DTQKDO4.js +2 -0
  7. package/dist/chunk-AGGIYXN7.js +2 -0
  8. package/dist/chunk-AUDMRGWF.js +189 -0
  9. package/dist/chunk-CPJKKHGH.js +1 -0
  10. package/dist/chunk-DAPXGSGD.js +12 -0
  11. package/dist/chunk-DOD7MUGX.js +1 -0
  12. package/dist/chunk-DV2BBBBJ.js +2 -0
  13. package/dist/chunk-EIBOS5KO.js +5 -0
  14. package/dist/chunk-EYWOVCSS.js +6 -0
  15. package/dist/chunk-FPXVDP32.js +8 -0
  16. package/dist/chunk-GXB7RZHH.js +1 -0
  17. package/dist/chunk-HT3MFLAV.js +34 -0
  18. package/dist/chunk-ILDDDLRC.js +1 -0
  19. package/dist/chunk-JFYVRWJI.js +2 -0
  20. package/dist/chunk-JMKK26AT.js +2 -0
  21. package/dist/chunk-LAHVHRCS.js +2 -0
  22. package/dist/chunk-LOOFOBTS.js +1 -0
  23. package/dist/chunk-LSJC73DG.js +1 -0
  24. package/dist/chunk-MOKUBKE7.js +1 -0
  25. package/dist/chunk-N345KWWK.js +1 -0
  26. package/dist/chunk-ONZLNFQ2.js +3 -0
  27. package/dist/chunk-ORJ2QZBG.js +3 -0
  28. package/dist/chunk-PCALBZOO.js +2 -0
  29. package/dist/chunk-SX5VE6XA.js +110 -0
  30. package/dist/chunk-TSEKPGK4.js +1 -0
  31. package/dist/chunk-UNK5HRQR.js +1 -0
  32. package/dist/chunk-UTYFLLSM.js +1 -0
  33. package/dist/chunk-V2RPZMLK.js +1 -0
  34. package/dist/chunk-V3R4FYTJ.js +1 -0
  35. package/dist/chunk-V656XVXV.js +8 -0
  36. package/dist/chunk-VLMQSD25.js +1 -0
  37. package/dist/chunk-WKUJ7CFO.js +1 -0
  38. package/dist/chunk-WOT6VMZA.js +1 -0
  39. package/dist/chunk-WUPU6LTD.js +4 -0
  40. package/dist/chunk-WX3V5P7U.js +5 -0
  41. package/dist/chunk-Y7YM5R75.js +5 -0
  42. package/dist/chunk-YW7NORH3.js +2 -0
  43. package/dist/main.js +99 -540
  44. package/package.json +17 -64
  45. package/dist/account-credential.js +0 -136
  46. package/dist/account-credential.test.js +0 -38
  47. package/dist/activity.js +0 -124
  48. package/dist/agent-defs.js +0 -237
  49. package/dist/agent-limiter.js +0 -68
  50. package/dist/agents.js +0 -554
  51. package/dist/anim.js +0 -361
  52. package/dist/args.js +0 -228
  53. package/dist/at-file.js +0 -28
  54. package/dist/brand-extension.js +0 -87
  55. package/dist/brand.js +0 -76
  56. package/dist/browser.js +0 -58
  57. package/dist/cache-report.js +0 -139
  58. package/dist/checkpoint.js +0 -437
  59. package/dist/claims.js +0 -167
  60. package/dist/cli-auth.js +0 -142
  61. package/dist/cli-checkin.js +0 -120
  62. package/dist/cli-credentials.js +0 -254
  63. package/dist/clipboard.js +0 -149
  64. package/dist/collapse.js +0 -129
  65. package/dist/commands.js +0 -294
  66. package/dist/compaction-guard.js +0 -81
  67. package/dist/context-extension.js +0 -17
  68. package/dist/dcp/commands.js +0 -172
  69. package/dist/dcp/compress-state.js +0 -139
  70. package/dist/dcp/compress.js +0 -329
  71. package/dist/dcp/dcp-extension.js +0 -295
  72. package/dist/dcp/dedup.js +0 -93
  73. package/dist/dcp/ids.js +0 -189
  74. package/dist/dcp/index.js +0 -25
  75. package/dist/dcp/middle-prune.js +0 -76
  76. package/dist/dcp/prompts.js +0 -68
  77. package/dist/dcp/protected-content.js +0 -221
  78. package/dist/dcp/protected.js +0 -161
  79. package/dist/dcp/prune.js +0 -418
  80. package/dist/dcp/state.js +0 -176
  81. package/dist/device-id.js +0 -90
  82. package/dist/device-id.test.js +0 -23
  83. package/dist/errors.js +0 -63
  84. package/dist/extensions.js +0 -48
  85. package/dist/fatal.js +0 -35
  86. package/dist/feed.js +0 -498
  87. package/dist/fs-atomic.js +0 -54
  88. package/dist/gateway-errors.js +0 -90
  89. package/dist/git.js +0 -230
  90. package/dist/goal-extension.js +0 -88
  91. package/dist/goal.js +0 -222
  92. package/dist/hooks-extension.js +0 -176
  93. package/dist/hooks.js +0 -443
  94. package/dist/howtofix-account.js +0 -735
  95. package/dist/hud-line.js +0 -153
  96. package/dist/hwid.js +0 -130
  97. package/dist/interrupt.js +0 -60
  98. package/dist/json-events.js +0 -50
  99. package/dist/level-style.js +0 -112
  100. package/dist/loop-guard.js +0 -95
  101. package/dist/lsp-tools.js +0 -32
  102. package/dist/lsp.js +0 -429
  103. package/dist/mcp-config.js +0 -85
  104. package/dist/mcp-lifecycle.js +0 -147
  105. package/dist/model-catalog.js +0 -217
  106. package/dist/model-facts.js +0 -35
  107. package/dist/model-resolve.js +0 -65
  108. package/dist/notes.js +0 -78
  109. package/dist/oauth-login.js +0 -568
  110. package/dist/paths.js +0 -61
  111. package/dist/permissions.js +0 -298
  112. package/dist/plan-mode.js +0 -363
  113. package/dist/preset-command.js +0 -227
  114. package/dist/preset-extension.js +0 -35
  115. package/dist/preset-mcp.js +0 -292
  116. package/dist/presets.js +0 -727
  117. package/dist/prompt-files.js +0 -103
  118. package/dist/prompt-text.js +0 -41
  119. package/dist/proxy.js +0 -137
  120. package/dist/quality/ast.js +0 -196
  121. package/dist/quality/cognitive.js +0 -83
  122. package/dist/quality/cyclomatic.js +0 -30
  123. package/dist/quality/halstead.js +0 -83
  124. package/dist/quality/index.js +0 -18
  125. package/dist/quality/length.js +0 -3
  126. package/dist/quality/maintainability.js +0 -15
  127. package/dist/quality/nesting.js +0 -57
  128. package/dist/quality/report.js +0 -803
  129. package/dist/quality/smells.js +0 -67
  130. package/dist/quality-tool.js +0 -63
  131. package/dist/read-only-commands.js +0 -597
  132. package/dist/repo-map.js +0 -292
  133. package/dist/rtk.js +0 -193
  134. package/dist/sandbox.js +0 -105
  135. package/dist/schema.js +0 -214
  136. package/dist/session-lifecycle.js +0 -19
  137. package/dist/session-retention.js +0 -111
  138. package/dist/session.js +0 -447
  139. package/dist/sessions.js +0 -100
  140. package/dist/settings.js +0 -148
  141. package/dist/skills.js +0 -35
  142. package/dist/slop-tool.js +0 -96
  143. package/dist/spend-cap.js +0 -43
  144. package/dist/subagent.js +0 -171
  145. package/dist/tar.js +0 -146
  146. package/dist/theme.js +0 -79
  147. package/dist/thinking.js +0 -45
  148. package/dist/todo.js +0 -114
  149. package/dist/tool-loadout.js +0 -97
  150. package/dist/tool-render.js +0 -769
  151. package/dist/tools.js +0 -176
  152. package/dist/ui.js +0 -4778
  153. package/dist/update.js +0 -551
  154. package/dist/usage-segment.js +0 -69
  155. package/dist/web-tools.js +0 -46
  156. package/dist/web.js +0 -265
  157. package/dist/welcome.js +0 -179
  158. package/dist/workflow-engine.js +0 -246
  159. package/dist/workflow-progress.js +0 -446
  160. package/dist/workflow-view.js +0 -195
  161. package/dist/workflow.js +0 -349
package/dist/goal.js DELETED
@@ -1,222 +0,0 @@
1
- /**
2
- * A persistent goal for the session, with usage accounting and a continuation
3
- * loop.
4
- *
5
- * Why this exists: opencode's most-reacted session request (#27167) is a native
6
- * goal, and its own author states the reason it is not a command template — it
7
- * "needs persistence, runtime continuation, usage accounting, and client
8
- * sync/status surfaces". A slash command that prints a prompt can do none of
9
- * those. What a goal buys over a plain message is that the harness knows what
10
- * the run is *for*, so it can:
11
- *
12
- * - keep the goal in the prompt every request (a long task survives a
13
- * compaction that drops the original message),
14
- * - continue working when the agent tries to stop with the goal still open,
15
- * - account the cost against it and stop at a budget instead of running away,
16
- * - refuse to let the model declare victory without evidence.
17
- *
18
- * State lives in the session branch (a `custom` entry), so it survives a resume
19
- * and stays correct across a fork — the same reason `todo.ts` keeps its list
20
- * there. A module-level mirror exists only so the HUD can paint the goal without
21
- * a session handle; it is reseeded from the branch on `session_start`.
22
- *
23
- * Pure where it matters: {@link applyGoalOp} and {@link shouldContinue} hold the
24
- * policy, so both are testable without a session or a model.
25
- */
26
- import { StringEnum } from "@netrodev/hxf-ai";
27
- import { defineTool } from "@netrodev/hxf-coding-agent";
28
- import { Type } from "typebox";
29
- /** Session entry type the goal is persisted under. */
30
- export const GOAL_ENTRY = "howtofix.goal";
31
- /** Custom message type used for the automatic continuation. */
32
- export const GOAL_CONTINUE = "howtofix.goal.continue";
33
- /** Longest goal text accepted. */
34
- export const MAX_GOAL = 400;
35
- /**
36
- * Continuations allowed when the goal sets no budget.
37
- *
38
- * A goal that can continue forever is the failure mode this whole feature is
39
- * meant to avoid, so the default is a cap rather than "unlimited": an unattended
40
- * run stops and says so, and the user can resume it.
41
- */
42
- export const DEFAULT_BUDGET = 25;
43
- const MARK = {
44
- active: "●",
45
- paused: "‖",
46
- done: "✓",
47
- cleared: "○",
48
- };
49
- /** One line for the tool result and the HUD. */
50
- export function formatGoal(goal) {
51
- if (!goal || goal.status === "cleared")
52
- return "No goal set.";
53
- const cap = goal.budget ?? DEFAULT_BUDGET;
54
- const parts = [`${MARK[goal.status]} ${goal.status}`, `${goal.used}/${cap} continuations`];
55
- if (goal.note)
56
- parts.push(goal.note);
57
- return `${parts.join(" · ")}\n${goal.text}`;
58
- }
59
- /** The short form painted in the HUD. Empty when there is nothing worth showing. */
60
- export function goalSegment(goal) {
61
- if (!goal || goal.status === "cleared" || goal.status === "done")
62
- return "";
63
- const cap = goal.budget ?? DEFAULT_BUDGET;
64
- const text = goal.text.length > 48 ? `${goal.text.slice(0, 47)}…` : goal.text;
65
- return `goal ${MARK[goal.status]} ${goal.used}/${cap} · ${text}`;
66
- }
67
- /**
68
- * The block injected into the prompt, so the goal survives compaction.
69
- *
70
- * Deliberately free of anything that changes between turns. This is appended to
71
- * the system message, which is the cacheable prefix of every request, so a
72
- * counter here would invalidate the prompt cache on every continuation — the
73
- * same class of mistake that cost Claude Code 10.2% of fleet-wide cache
74
- * creation tokens when a dynamic agent list sat in a tool schema. The spend is
75
- * reported where it belongs: in the continuation message and the tool result,
76
- * both of which are new content and cannot invalidate a cached prefix.
77
- */
78
- export function renderGoalBlock(goal) {
79
- const lines = ["<goal>", goal.text, `Status: ${goal.status}.`];
80
- if (goal.status === "active") {
81
- lines.push("Work toward this goal until it is genuinely met. When it is, call the `goal` tool with", 'op="complete" and a note naming the evidence; a command you ran or a file you checked.', "If you are blocked and cannot proceed, say so plainly instead of repeating an attempt.");
82
- }
83
- lines.push("</goal>");
84
- return lines.join("\n");
85
- }
86
- /** What the agent is told when the harness decides to keep going. */
87
- export function continuationPrompt(goal) {
88
- return [
89
- "[goal] The goal is still open and you stopped without closing it:",
90
- ` ${goal.text}`,
91
- "",
92
- "Continue from where you left off. Do not repeat work you already did. If the goal is met,",
93
- 'call the `goal` tool with op="complete" and a note naming the evidence. Do not call it',
94
- "complete to escape a hard problem: if you cannot make progress, state what is blocking you.",
95
- `Continuations used: ${goal.used}/${goal.budget ?? DEFAULT_BUDGET}.`,
96
- ].join("\n");
97
- }
98
- /** A stable in-memory mirror for surfaces with no session handle (the HUD). */
99
- let mirror;
100
- export function currentGoal() {
101
- return mirror;
102
- }
103
- export function setMirror(goal) {
104
- mirror = goal;
105
- }
106
- export function shouldContinue(goal) {
107
- if (!goal)
108
- return { action: "stop", reason: "no goal", close: false };
109
- if (goal.status === "done")
110
- return { action: "stop", reason: "goal complete", close: false };
111
- if (goal.status === "cleared")
112
- return { action: "stop", reason: "goal cleared", close: false };
113
- if (goal.status === "paused")
114
- return { action: "stop", reason: "goal paused", close: false };
115
- const cap = goal.budget ?? DEFAULT_BUDGET;
116
- if (goal.used >= cap)
117
- return { action: "stop", reason: `budget exhausted (${cap} continuations)`, close: true };
118
- return { action: "continue" };
119
- }
120
- /** Apply one op. Pure, so the policy is testable without a session. */
121
- export function applyGoalOp(goal, op, text, budget, note) {
122
- switch (op) {
123
- case "view":
124
- return { goal };
125
- case "set": {
126
- if (!text?.trim())
127
- return { goal, error: "set needs text" };
128
- if (text.length > MAX_GOAL)
129
- return { goal, error: `text longer than ${MAX_GOAL} characters` };
130
- if (budget !== undefined && (!Number.isInteger(budget) || budget < 1)) {
131
- return { goal, error: "budget must be a positive integer" };
132
- }
133
- // A new goal resets the accounting: the cap belongs to the goal, not to
134
- // the session, so a fresh goal gets its full budget.
135
- return { goal: { text: text.trim(), status: "active", used: 0, ...(budget === undefined ? {} : { budget }) } };
136
- }
137
- case "pause":
138
- case "resume":
139
- case "complete":
140
- case "clear": {
141
- if (!goal || goal.status === "cleared")
142
- return { goal, error: "no goal to change" };
143
- if (op === "resume") {
144
- if (goal.status === "done")
145
- return { goal, error: "goal is complete; set a new one instead" };
146
- return { goal: { ...goal, status: "active", note: undefined } };
147
- }
148
- if (op === "pause")
149
- return { goal: { ...goal, status: "paused" } };
150
- if (op === "clear")
151
- return { goal: { ...goal, status: "cleared" } };
152
- // `complete` requires evidence. The model may only declare victory with a
153
- // note naming what it verified; a bare "done" is what lets a wrong answer
154
- // ship as a finished task.
155
- if (!note?.trim())
156
- return { goal, error: "complete needs a note naming the evidence (a command you ran or a file you checked)" };
157
- return { goal: { ...goal, status: "done", note: note.trim() } };
158
- }
159
- default:
160
- return { goal, error: `unknown op ${JSON.stringify(op)}` };
161
- }
162
- }
163
- /**
164
- * Rebuild the goal by replaying this branch in order.
165
- *
166
- * Two sources, because two things write it: the `goal` tool (whose result
167
- * `details` are persisted in the transcript, exactly like `todo.ts`) and the
168
- * continuation extension, which appends a `custom` entry to record a spend
169
- * before it continues. Whichever is later in the branch wins, so a resume sees
170
- * the true state and the accounting cannot be reset by a restart.
171
- */
172
- export function readGoal(ctx) {
173
- let goal;
174
- for (const entry of ctx.sessionManager.getBranch()) {
175
- if (entry.type === "custom" && entry.customType === GOAL_ENTRY) {
176
- goal = entry.data;
177
- continue;
178
- }
179
- // The slash command persists through `sendCustomMessage`, which writes a
180
- // `custom_message` carrying the goal in `details` rather than `data`.
181
- if (entry.type === "custom_message" && entry.customType === GOAL_ENTRY) {
182
- goal = entry.details ?? goal;
183
- continue;
184
- }
185
- if (entry.type !== "message")
186
- continue;
187
- const message = entry.message;
188
- if (message.role !== "toolResult" || message.toolName !== "goal")
189
- continue;
190
- const details = message.details;
191
- if (details?.goal)
192
- goal = details.goal;
193
- }
194
- return goal;
195
- }
196
- export const goalParams = Type.Object({
197
- op: StringEnum(["set", "view", "pause", "resume", "complete", "clear"], {
198
- description: "set starts a goal; complete closes it with evidence; pause/resume/clear manage it; view reads it",
199
- }),
200
- text: Type.Optional(Type.String({ description: "The goal, in one sentence, as the user would state it (for set)" })),
201
- budget: Type.Optional(Type.Number({ description: "Optional cap on automatic continuations before the run stops" })),
202
- note: Type.Optional(Type.String({ description: "Evidence that the goal is met (required for complete)" })),
203
- });
204
- export const goalTool = defineTool({
205
- name: "goal",
206
- label: "goal",
207
- description: "Track the goal for this session. The goal is already shown in your context every request, so you rarely need `view`. Call `complete` only when the goal is genuinely met, and pass a note naming the evidence. When a goal is active the harness continues the run if you stop without closing it, so closing it is how you stop.",
208
- promptSnippet: "goal: the session's objective, and how to close it",
209
- parameters: goalParams,
210
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
211
- const before = readGoal(ctx);
212
- const { goal, error } = applyGoalOp(before, params.op, params.text, params.budget, params.note);
213
- setMirror(goal);
214
- const body = formatGoal(goal);
215
- const summary = error ? `Error: ${error}\n${body}` : body;
216
- return {
217
- content: [{ type: "text", text: summary }],
218
- details: { op: params.op, goal },
219
- ...(error ? { isError: true } : {}),
220
- };
221
- },
222
- });
@@ -1,176 +0,0 @@
1
- import { readMessageText, writeMessageText } from "./prompt-text.js";
2
- import { HOOKS_FILE, checkToolInput, loadHooks, resetOnceHooks, runHooksFor } from "./hooks.js";
3
- /** Read once per session; see the note above. */
4
- let config = { hooks: {} };
5
- /** Context a `SessionStart` hook contributed, kept for the session's prompts. */
6
- let sessionContext;
7
- export function createHooksExtension() {
8
- return (hxf) => {
9
- // Read the config HERE, in the factory, not in `session_start`.
10
- //
11
- // Measured: `session_start` does not fire in `-p` print mode — one-shot runs
12
- // are exactly where a `PreToolUse` hook matters most (an unattended agent is
13
- // the one you want policed), and loading in the lifecycle event left `config`
14
- // empty there, so every hook silently did nothing. The factory runs in every
15
- // mode. The read is one small synchronous file; it happens once per process.
16
- const loaded = loadHooks(HOOKS_FILE);
17
- config = loaded.config;
18
- sessionContext = undefined;
19
- // A resumed session re-arms its `once` hooks: the ledger is per process, and
20
- // the session that just opened is a new one from the hook's point of view.
21
- resetOnceHooks();
22
- hxf.on("session_start", async (_event, ctx) => {
23
- for (const diagnostic of loaded.diagnostics) {
24
- ctx.ui.notify(`hooks.json: ${diagnostic.message}`, "warning");
25
- }
26
- const { outcome, ran } = await runHooksFor(config, "SessionStart", { hook_event_name: "SessionStart", cwd: ctx.cwd }, { cwd: ctx.cwd });
27
- if (outcome.context)
28
- sessionContext = outcome.context;
29
- if (outcome.error)
30
- ctx.ui.notify(`SessionStart hook: ${outcome.error}`, "warning");
31
- if (ran && sessionContext) {
32
- ctx.ui.notify(`SessionStart hooks added ${sessionContext.length} characters of context`, "info");
33
- }
34
- });
35
- // `UserPromptSubmit` may block a prompt or add to it. The SDK's `input` event
36
- // is the only place a prompt can be replaced before the run starts.
37
- hxf.on("input", async (event, ctx) => {
38
- if (!config.hooks.UserPromptSubmit)
39
- return undefined;
40
- const payload = {
41
- hook_event_name: "UserPromptSubmit",
42
- cwd: ctx.cwd,
43
- prompt: event.text,
44
- };
45
- const { outcome } = await runHooksFor(config, "UserPromptSubmit", payload, { cwd: ctx.cwd });
46
- if (outcome.error)
47
- ctx.ui.notify(`UserPromptSubmit hook: ${outcome.error}`, "warning");
48
- if (outcome.block) {
49
- ctx.ui.notify(outcome.reason ?? "blocked by a hook", "warning");
50
- return { action: "handled" };
51
- }
52
- if (!outcome.context)
53
- return undefined;
54
- return { action: "transform", text: `${event.text}\n\n${outcome.context}` };
55
- });
56
- // `PreToolUse` is the one that can refuse — or rewrite. It runs before the
57
- // approval gate because a hook is the user's own policy and must not be
58
- // overridable by an approval mode.
59
- hxf.on("tool_call", async (event, ctx) => {
60
- if (!config.hooks.PreToolUse)
61
- return undefined;
62
- const payload = {
63
- hook_event_name: "PreToolUse",
64
- cwd: ctx.cwd,
65
- tool_name: event.toolName,
66
- tool_input: event.input,
67
- };
68
- const { outcome } = await runHooksFor(config, "PreToolUse", payload, { cwd: ctx.cwd });
69
- if (outcome.error)
70
- ctx.ui.notify(`PreToolUse hook: ${outcome.error}`, "warning");
71
- if (outcome.block) {
72
- return { block: true, reason: outcome.reason ?? "blocked by a hook" };
73
- }
74
- // A rewrite replaces what the tool will actually do, so it is validated
75
- // against the tool's own schema before it is used — the same schema the
76
- // provider was given, read from the SDK's own registry. A rewrite that
77
- // fails is dropped and the original arguments run: a guard must not be able
78
- // to turn itself into a way to run something the model never asked for.
79
- if (outcome.updatedInput) {
80
- const tools = hxf.getAllTools();
81
- const schema = tools.find((tool) => tool.name === event.toolName)?.parameters;
82
- if (!schema) {
83
- ctx.ui.notify(`PreToolUse hook rewrote \`${event.toolName}\`, but no schema is known for that tool — the rewrite was ignored`, "warning");
84
- }
85
- else {
86
- const problem = checkToolInput(schema, outcome.updatedInput);
87
- if (problem) {
88
- ctx.ui.notify(`PreToolUse hook rewrite ignored — ${problem}`, "warning");
89
- }
90
- else {
91
- // The SDK's documented way to change arguments: mutate in place.
92
- Object.assign(event.input, outcome.updatedInput);
93
- }
94
- }
95
- }
96
- return undefined;
97
- });
98
- // `PostToolUse` adds context to what the tool returned. The event is
99
- // `tool_result`, and the result's `content` is replaced with the hook's text
100
- // appended — `structuredContent` is returned alongside it so it is not
101
- // dropped (the SDK drops it otherwise).
102
- hxf.on("tool_result", async (event, ctx) => {
103
- if (!config.hooks.PostToolUse)
104
- return undefined;
105
- const text = event.content.map((block) => (block.type === "text" ? block.text : "")).join("\n");
106
- const payload = {
107
- hook_event_name: "PostToolUse",
108
- cwd: ctx.cwd,
109
- tool_name: event.toolName,
110
- tool_input: event.input,
111
- tool_result: text,
112
- tool_is_error: event.isError,
113
- };
114
- const { outcome } = await runHooksFor(config, "PostToolUse", payload, { cwd: ctx.cwd });
115
- if (outcome.error)
116
- ctx.ui.notify(`PostToolUse hook: ${outcome.error}`, "warning");
117
- if (!outcome.context)
118
- return undefined;
119
- return {
120
- content: [...event.content, { type: "text", text: `\n[hook] ${outcome.context}` }],
121
- structuredContent: event.structuredContent,
122
- };
123
- });
124
- // `SessionStart` context reaches the model through the same path the shared
125
- // notes use: appended to the leading system message, which must stay at index
126
- // 0 because that is where the provider reads the prompt from.
127
- hxf.on("context_with_system", async (event) => {
128
- if (!sessionContext)
129
- return undefined;
130
- const head = event.messages[0];
131
- if (!head || head.role !== "system")
132
- return undefined;
133
- const combined = `${readMessageText(head.content)}\n\n${sessionContext}`;
134
- const messages = event.messages.map((message) => (message === head ? { ...head, content: writeMessageText(head.content, combined) } : message));
135
- return { messages };
136
- });
137
- // `PreCompact` observes a compaction before it happens — the moment context
138
- // is about to be thrown away. Notification only: the SDK's compaction result
139
- // is not something a shell command should be able to veto, and a hook that
140
- // could would be a way to wedge a session at its most fragile point.
141
- hxf.on("session_before_compact", async (event, ctx) => {
142
- if (!config.hooks.PreCompact)
143
- return;
144
- const { outcome } = await runHooksFor(config, "PreCompact", {
145
- hook_event_name: "PreCompact",
146
- cwd: ctx.cwd,
147
- compact_reason: event.reason,
148
- }, { cwd: ctx.cwd });
149
- if (outcome.error)
150
- ctx.ui.notify(`PreCompact hook: ${outcome.error}`, "warning");
151
- });
152
- // `SessionEnd` for a session that is **replaced** (`/new`, `/resume`, a fork).
153
- //
154
- // Measured: this event does NOT fire when the process exits — `/quit` and
155
- // double Ctrl+C produced zero events, even for a TypeScript extension writing
156
- // synchronously. The quit path is therefore handled in `ui.ts`'s `shutdown()`,
157
- // which runs the same hooks and awaits them. This handler covers the other
158
- // moment the session ends: being swapped for another one.
159
- hxf.on("session_shutdown", async (event, ctx) => {
160
- if (!config.hooks.SessionEnd)
161
- return;
162
- const { outcome } = await runHooksFor(config, "SessionEnd", { hook_event_name: "SessionEnd", cwd: ctx.cwd, reason: event.reason }, { cwd: ctx.cwd });
163
- if (outcome.error)
164
- ctx.ui.notify(`SessionEnd hook: ${outcome.error}`, "warning");
165
- });
166
- // `Stop` is notification-only: it fires when the run will not continue on its
167
- // own, which is the moment an integration wants to know about.
168
- hxf.on("agent_settled", async (_event, ctx) => {
169
- if (!config.hooks.Stop)
170
- return;
171
- const { outcome } = await runHooksFor(config, "Stop", { hook_event_name: "Stop", cwd: ctx.cwd }, { cwd: ctx.cwd });
172
- if (outcome.error)
173
- ctx.ui.notify(`Stop hook: ${outcome.error}`, "warning");
174
- });
175
- };
176
- }