howtofixcli 0.7.7 → 0.7.9
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/chunk-27TRKD65.js +2 -0
- package/dist/chunk-3FYCFLNK.js +1 -0
- package/dist/chunk-56SEAGAQ.js +1 -0
- package/dist/chunk-63ZCQDXH.js +6 -0
- package/dist/chunk-6FUWMLWO.js +1 -0
- package/dist/chunk-7CHDNTH6.js +1 -0
- package/dist/chunk-ACOMNDUL.js +185 -0
- package/dist/chunk-AEVPET7X.js +2 -0
- package/dist/chunk-AGGIYXN7.js +2 -0
- package/dist/chunk-BHVBO4SC.js +1 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-GIUYIKH3.js +109 -0
- package/dist/chunk-GQCAJ56I.js +5 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-IGEY3AWF.js +1 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-MVLCGU3G.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-NBQFPCRT.js +1 -0
- package/dist/chunk-OIPSLIZE.js +5 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-PDNBGUYP.js +2 -0
- package/dist/chunk-S656RE4A.js +1 -0
- package/dist/chunk-TSEKPGK4.js +1 -0
- package/dist/chunk-U6CI6KXN.js +34 -0
- package/dist/chunk-UTYFLLSM.js +1 -0
- package/dist/chunk-V3R4FYTJ.js +1 -0
- package/dist/chunk-V656XVXV.js +8 -0
- package/dist/chunk-WKUJ7CFO.js +1 -0
- package/dist/chunk-WOT6VMZA.js +1 -0
- package/dist/chunk-WUPU6LTD.js +4 -0
- package/dist/chunk-WX3V5P7U.js +5 -0
- package/dist/chunk-ZKZ6WCI3.js +2 -0
- package/dist/main.js +94 -536
- package/package.json +15 -62
- package/dist/account-credential.js +0 -136
- package/dist/account-credential.test.js +0 -38
- package/dist/activity.js +0 -124
- package/dist/agent-defs.js +0 -237
- package/dist/agent-limiter.js +0 -68
- package/dist/agents.js +0 -554
- package/dist/anim.js +0 -361
- package/dist/args.js +0 -228
- package/dist/at-file.js +0 -28
- package/dist/brand-extension.js +0 -87
- package/dist/brand.js +0 -76
- package/dist/browser.js +0 -58
- package/dist/cache-report.js +0 -139
- package/dist/checkpoint.js +0 -437
- package/dist/claims.js +0 -167
- package/dist/cli-auth.js +0 -142
- package/dist/cli-checkin.js +0 -116
- package/dist/cli-credentials.js +0 -254
- package/dist/clipboard.js +0 -149
- package/dist/collapse.js +0 -129
- package/dist/commands.js +0 -301
- package/dist/compaction-guard.js +0 -81
- package/dist/context-extension.js +0 -17
- package/dist/dcp/commands.js +0 -172
- package/dist/dcp/compress-state.js +0 -139
- package/dist/dcp/compress.js +0 -329
- package/dist/dcp/dcp-extension.js +0 -288
- package/dist/dcp/dedup.js +0 -93
- package/dist/dcp/ids.js +0 -189
- package/dist/dcp/index.js +0 -25
- package/dist/dcp/prompts.js +0 -68
- package/dist/dcp/protected-content.js +0 -221
- package/dist/dcp/protected.js +0 -161
- package/dist/dcp/prune.js +0 -418
- package/dist/dcp/state.js +0 -176
- package/dist/device-id.js +0 -90
- package/dist/device-id.test.js +0 -23
- package/dist/errors.js +0 -63
- package/dist/extensions.js +0 -48
- package/dist/fatal.js +0 -35
- package/dist/feed.js +0 -498
- package/dist/fs-atomic.js +0 -54
- package/dist/gateway-errors.js +0 -90
- package/dist/git.js +0 -230
- package/dist/goal-extension.js +0 -88
- package/dist/goal.js +0 -222
- package/dist/hooks-extension.js +0 -176
- package/dist/hooks.js +0 -443
- package/dist/howtofix-account.js +0 -715
- package/dist/hud-line.js +0 -153
- package/dist/hwid.js +0 -130
- package/dist/interrupt.js +0 -60
- package/dist/json-events.js +0 -50
- package/dist/level-style.js +0 -112
- package/dist/loop-guard.js +0 -95
- package/dist/lsp-tools.js +0 -32
- package/dist/lsp.js +0 -347
- package/dist/mcp-config.js +0 -85
- package/dist/mcp-lifecycle.js +0 -147
- package/dist/model-catalog.js +0 -217
- package/dist/model-resolve.js +0 -65
- package/dist/notes.js +0 -78
- package/dist/oauth-login.js +0 -568
- package/dist/paths.js +0 -61
- package/dist/permissions.js +0 -298
- package/dist/plan-mode.js +0 -363
- package/dist/preset-command.js +0 -227
- package/dist/preset-extension.js +0 -35
- package/dist/preset-mcp.js +0 -292
- package/dist/presets.js +0 -727
- package/dist/prompt-files.js +0 -103
- package/dist/prompt-text.js +0 -41
- package/dist/proxy.js +0 -137
- package/dist/quality/ast.js +0 -196
- package/dist/quality/cognitive.js +0 -83
- package/dist/quality/cyclomatic.js +0 -30
- package/dist/quality/halstead.js +0 -83
- package/dist/quality/index.js +0 -18
- package/dist/quality/length.js +0 -3
- package/dist/quality/maintainability.js +0 -15
- package/dist/quality/nesting.js +0 -57
- package/dist/quality/report.js +0 -803
- package/dist/quality/smells.js +0 -67
- package/dist/quality-tool.js +0 -63
- package/dist/read-only-commands.js +0 -597
- package/dist/repo-map.js +0 -292
- package/dist/rtk.js +0 -193
- package/dist/sandbox.js +0 -105
- package/dist/schema.js +0 -214
- package/dist/session-lifecycle.js +0 -19
- package/dist/session-retention.js +0 -111
- package/dist/session.js +0 -446
- package/dist/sessions.js +0 -100
- package/dist/settings.js +0 -148
- package/dist/skills.js +0 -35
- package/dist/slop-tool.js +0 -96
- package/dist/spend-cap.js +0 -43
- package/dist/subagent.js +0 -171
- package/dist/tar.js +0 -146
- package/dist/theme.js +0 -79
- package/dist/thinking.js +0 -45
- package/dist/todo.js +0 -114
- package/dist/tool-loadout.js +0 -97
- package/dist/tool-render.js +0 -769
- package/dist/tools.js +0 -176
- package/dist/ui.js +0 -4751
- package/dist/update.js +0 -533
- package/dist/usage-segment.js +0 -69
- package/dist/web-tools.js +0 -46
- package/dist/web.js +0 -265
- package/dist/welcome.js +0 -179
- package/dist/workflow-engine.js +0 -246
- package/dist/workflow-progress.js +0 -446
- package/dist/workflow-view.js +0 -195
- 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
|
-
});
|
package/dist/hooks-extension.js
DELETED
|
@@ -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
|
-
}
|