howtofixcli 0.7.14 → 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 (165) 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 -551
  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/acp.js +0 -234
  48. package/dist/activity.js +0 -124
  49. package/dist/agent-defs.js +0 -237
  50. package/dist/agent-limiter.js +0 -68
  51. package/dist/agents.js +0 -554
  52. package/dist/anim.js +0 -361
  53. package/dist/args.js +0 -250
  54. package/dist/at-file.js +0 -28
  55. package/dist/brand-extension.js +0 -87
  56. package/dist/brand.js +0 -76
  57. package/dist/browser.js +0 -58
  58. package/dist/cache-report.js +0 -139
  59. package/dist/checkpoint.js +0 -437
  60. package/dist/claims.js +0 -167
  61. package/dist/cli-auth.js +0 -142
  62. package/dist/cli-checkin.js +0 -120
  63. package/dist/cli-credentials.js +0 -254
  64. package/dist/clipboard.js +0 -149
  65. package/dist/collapse.js +0 -129
  66. package/dist/commands.js +0 -294
  67. package/dist/compaction-guard.js +0 -81
  68. package/dist/context-extension.js +0 -17
  69. package/dist/dcp/commands.js +0 -172
  70. package/dist/dcp/compress-state.js +0 -139
  71. package/dist/dcp/compress.js +0 -329
  72. package/dist/dcp/dcp-extension.js +0 -295
  73. package/dist/dcp/dedup.js +0 -93
  74. package/dist/dcp/ids.js +0 -189
  75. package/dist/dcp/index.js +0 -25
  76. package/dist/dcp/middle-prune.js +0 -76
  77. package/dist/dcp/prompts.js +0 -68
  78. package/dist/dcp/protected-content.js +0 -221
  79. package/dist/dcp/protected.js +0 -161
  80. package/dist/dcp/prune.js +0 -418
  81. package/dist/dcp/state.js +0 -176
  82. package/dist/device-id.js +0 -90
  83. package/dist/device-id.test.js +0 -23
  84. package/dist/errors.js +0 -63
  85. package/dist/extensions.js +0 -48
  86. package/dist/fatal.js +0 -35
  87. package/dist/feed.js +0 -498
  88. package/dist/fs-atomic.js +0 -54
  89. package/dist/gateway-errors.js +0 -90
  90. package/dist/git.js +0 -230
  91. package/dist/goal-extension.js +0 -88
  92. package/dist/goal.js +0 -222
  93. package/dist/hooks-extension.js +0 -176
  94. package/dist/hooks.js +0 -443
  95. package/dist/howtofix-account.js +0 -735
  96. package/dist/hud-line.js +0 -153
  97. package/dist/hwid.js +0 -130
  98. package/dist/interrupt.js +0 -60
  99. package/dist/json-events.js +0 -50
  100. package/dist/level-style.js +0 -112
  101. package/dist/loop-guard.js +0 -95
  102. package/dist/lsp-tools.js +0 -32
  103. package/dist/lsp.js +0 -429
  104. package/dist/mcp-config.js +0 -85
  105. package/dist/mcp-lifecycle.js +0 -147
  106. package/dist/model-catalog.js +0 -217
  107. package/dist/model-facts.js +0 -35
  108. package/dist/model-resolve.js +0 -65
  109. package/dist/notes.js +0 -78
  110. package/dist/oauth-login.js +0 -568
  111. package/dist/paths.js +0 -61
  112. package/dist/permissions.js +0 -298
  113. package/dist/plan-mode.js +0 -363
  114. package/dist/preset-command.js +0 -227
  115. package/dist/preset-extension.js +0 -35
  116. package/dist/preset-mcp.js +0 -292
  117. package/dist/presets.js +0 -727
  118. package/dist/prompt-files.js +0 -103
  119. package/dist/prompt-text.js +0 -41
  120. package/dist/proxy.js +0 -137
  121. package/dist/quality/ast.js +0 -196
  122. package/dist/quality/cognitive.js +0 -83
  123. package/dist/quality/cyclomatic.js +0 -30
  124. package/dist/quality/halstead.js +0 -83
  125. package/dist/quality/index.js +0 -18
  126. package/dist/quality/length.js +0 -3
  127. package/dist/quality/maintainability.js +0 -15
  128. package/dist/quality/nesting.js +0 -57
  129. package/dist/quality/report.js +0 -803
  130. package/dist/quality/smells.js +0 -67
  131. package/dist/quality-tool.js +0 -63
  132. package/dist/read-only-commands.js +0 -597
  133. package/dist/repo-map.js +0 -292
  134. package/dist/roblox-connect.js +0 -125
  135. package/dist/rpc.js +0 -91
  136. package/dist/rtk.js +0 -193
  137. package/dist/sandbox.js +0 -105
  138. package/dist/schema.js +0 -214
  139. package/dist/serve.js +0 -50
  140. package/dist/session-lifecycle.js +0 -19
  141. package/dist/session-retention.js +0 -111
  142. package/dist/session.js +0 -447
  143. package/dist/sessions.js +0 -100
  144. package/dist/settings.js +0 -148
  145. package/dist/skills.js +0 -35
  146. package/dist/slop-tool.js +0 -96
  147. package/dist/spend-cap.js +0 -43
  148. package/dist/subagent.js +0 -171
  149. package/dist/tar.js +0 -146
  150. package/dist/theme.js +0 -79
  151. package/dist/thinking.js +0 -45
  152. package/dist/todo.js +0 -114
  153. package/dist/tool-loadout.js +0 -97
  154. package/dist/tool-render.js +0 -769
  155. package/dist/tools.js +0 -176
  156. package/dist/ui.js +0 -4814
  157. package/dist/update.js +0 -551
  158. package/dist/usage-segment.js +0 -69
  159. package/dist/web-tools.js +0 -46
  160. package/dist/web.js +0 -265
  161. package/dist/welcome.js +0 -179
  162. package/dist/workflow-engine.js +0 -246
  163. package/dist/workflow-progress.js +0 -446
  164. package/dist/workflow-view.js +0 -195
  165. 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
- }