@tech-leads-club/harness-toolkit 0.3.0 → 0.3.2
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/README.md +46 -11
- package/bin/tlc-build.mjs +35 -6
- package/config.example.json +1 -1
- package/dist/chunks/compact-before-1e4qg1qt.mjs +1185 -0
- package/dist/chunks/compact-before-2hpbfxm5.mjs +5782 -0
- package/dist/chunks/compact-before-49j320yp.mjs +1283 -0
- package/dist/chunks/compact-before-4jrq0sqs.mjs +61 -0
- package/dist/chunks/compact-before-6w8n1vh1.mjs +186 -0
- package/dist/chunks/compact-before-7sdmwswh.mjs +52 -0
- package/dist/chunks/compact-before-beqpmqrm.mjs +187 -0
- package/dist/chunks/compact-before-j9y4jgn4.mjs +845 -0
- package/dist/chunks/compact-before-pk86tqx2.mjs +118 -0
- package/dist/chunks/compact-before-pkqk5v29.mjs +137 -0
- package/dist/chunks/compact-before-w1293m4n.mjs +315 -0
- package/dist/chunks/compact-before-wnnds45y.mjs +26 -0
- package/dist/chunks/compact-before-wt2c3nh4.mjs +551 -0
- package/dist/compact-before.mjs +13 -7961
- package/dist/doctor.mjs +33 -8480
- package/dist/help-topic.mjs +6 -14
- package/dist/init-project.mjs +36 -793
- package/dist/install-runtime.mjs +18 -1039
- package/dist/lessons-cli.mjs +25 -7043
- package/dist/obs-cli.mjs +27 -7037
- package/dist/price-lookup.mjs +11 -201
- package/dist/prompt-submit.mjs +14 -7974
- package/dist/refresh-model-prices.mjs +26 -7061
- package/dist/response-after.mjs +11 -7961
- package/dist/run.mjs +10 -7960
- package/dist/session-end.mjs +14 -8029
- package/dist/session-start.mjs +20 -8075
- package/dist/shim.mjs +18 -7024
- package/dist/stop.mjs +29 -8042
- package/dist/subagent-start.mjs +13 -7983
- package/dist/subagent-stop.mjs +11 -7961
- package/dist/support.mjs +21 -7168
- package/dist/tlc-cli.mjs +65 -8200
- package/dist/tool-after.mjs +20 -8176
- package/dist/tool-before.mjs +15 -7990
- package/dist/tool-failure.mjs +14 -7961
- package/dist/uninstall-runtime.mjs +61 -1008
- package/docs/log.md +4 -0
- package/package.json +2 -2
- package/src/core/release/release.version.ts +10 -4
- package/tools/install-runtime.ts +21 -1
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import {
|
|
2
|
+
claudeWiring,
|
|
3
|
+
cursorWiring
|
|
4
|
+
} from "./compact-before-w1293m4n.mjs";
|
|
5
|
+
import {
|
|
6
|
+
isEffortLevel,
|
|
7
|
+
readTail,
|
|
8
|
+
sanitizeSegment
|
|
9
|
+
} from "./compact-before-49j320yp.mjs";
|
|
10
|
+
import {
|
|
11
|
+
projectConfigPath
|
|
12
|
+
} from "./compact-before-4jrq0sqs.mjs";
|
|
13
|
+
|
|
14
|
+
// src/providers/claude/claude.lessons-view.ts
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
var IMPORT_LINE = "@.tlc/harness/lessons.md";
|
|
18
|
+
function claudeLessonsSourcePath(root) {
|
|
19
|
+
return join(dirname(projectConfigPath(root)), "lessons.md");
|
|
20
|
+
}
|
|
21
|
+
function claudeMdPath(root) {
|
|
22
|
+
return join(root, "CLAUDE.md");
|
|
23
|
+
}
|
|
24
|
+
function renderClaudeLessonsView(root) {
|
|
25
|
+
if (!existsSync(claudeLessonsSourcePath(root))) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const path = claudeMdPath(root);
|
|
29
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
30
|
+
const alreadyImported = existing.split(`
|
|
31
|
+
`).some((line) => line.trim() === IMPORT_LINE);
|
|
32
|
+
if (alreadyImported) {
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
35
|
+
const separator = existing.length > 0 && !existing.endsWith(`
|
|
36
|
+
`) ? `
|
|
37
|
+
` : "";
|
|
38
|
+
const content = existing.length > 0 ? `${existing}${separator}
|
|
39
|
+
${IMPORT_LINE}
|
|
40
|
+
` : `${IMPORT_LINE}
|
|
41
|
+
`;
|
|
42
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
43
|
+
writeFileSync(path, content, "utf8");
|
|
44
|
+
return path;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/providers/claude/claude.transcript.ts
|
|
48
|
+
var DEFAULT_TAIL_LINES = 200;
|
|
49
|
+
function asNumber(value) {
|
|
50
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
51
|
+
}
|
|
52
|
+
function asRecord(value) {
|
|
53
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
54
|
+
}
|
|
55
|
+
function usageOf(record) {
|
|
56
|
+
const message = asRecord(asRecord(record)?.message);
|
|
57
|
+
return asRecord(message?.usage);
|
|
58
|
+
}
|
|
59
|
+
function readClaudeUsage(transcriptPath, tailLines = DEFAULT_TAIL_LINES) {
|
|
60
|
+
if (!transcriptPath) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
let records;
|
|
64
|
+
try {
|
|
65
|
+
records = readTail(transcriptPath, tailLines);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
let inputTokens = 0;
|
|
70
|
+
let outputTokens = 0;
|
|
71
|
+
let cacheReadTokens = 0;
|
|
72
|
+
let cacheWriteTokens = 0;
|
|
73
|
+
let found = false;
|
|
74
|
+
for (const record of records) {
|
|
75
|
+
const usage = usageOf(record);
|
|
76
|
+
if (!usage) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
found = true;
|
|
80
|
+
inputTokens += asNumber(usage.input_tokens);
|
|
81
|
+
outputTokens += asNumber(usage.output_tokens);
|
|
82
|
+
cacheReadTokens += asNumber(usage.cache_read_input_tokens);
|
|
83
|
+
cacheWriteTokens += asNumber(usage.cache_creation_input_tokens);
|
|
84
|
+
}
|
|
85
|
+
return found ? { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/providers/cursor/cursor.lessons-view.ts
|
|
89
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
90
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
91
|
+
function cursorLessonsSourcePath(root) {
|
|
92
|
+
return join2(dirname2(projectConfigPath(root)), "lessons.md");
|
|
93
|
+
}
|
|
94
|
+
function cursorLessonsViewPath(root) {
|
|
95
|
+
return join2(root, ".cursor", "rules", "harness-lessons.mdc");
|
|
96
|
+
}
|
|
97
|
+
function renderCursorLessonsView(root) {
|
|
98
|
+
const sourcePath = cursorLessonsSourcePath(root);
|
|
99
|
+
if (!existsSync2(sourcePath)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const body = readFileSync2(sourcePath, "utf8");
|
|
103
|
+
const path = cursorLessonsViewPath(root);
|
|
104
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
105
|
+
const content = `---
|
|
106
|
+
description: Harness-learned lessons (auto-synced from gate failures)
|
|
107
|
+
alwaysApply: true
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
${body}`;
|
|
111
|
+
writeFileSync2(path, content, "utf8");
|
|
112
|
+
return path;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/providers/provider.degrade.ts
|
|
116
|
+
var DEGRADE_RULES = { rewriteUnavailable: "rewrite-unavailable" };
|
|
117
|
+
var ESCALATION_PREFIX = "Escalation unavailable on this provider — ";
|
|
118
|
+
var NO_HUMAN_PREFIX = "No operator is answering prompts in this permission mode — ";
|
|
119
|
+
var NO_HUMAN_MODES = new Set(["bypassPermissions", "dontAsk"]);
|
|
120
|
+
var ADVISORY_PREFIX = "ADVISORY — this provider cannot enforce: ";
|
|
121
|
+
var TRUNCATION_MARKER = `
|
|
122
|
+
…(truncated — over context budget)`;
|
|
123
|
+
function isEnforcing(decision) {
|
|
124
|
+
return decision.kind === "deny" || decision.kind === "ask" || decision.kind === "continue" || decision.kind === "rewriteInput";
|
|
125
|
+
}
|
|
126
|
+
function describeDecision(decision) {
|
|
127
|
+
switch (decision.kind) {
|
|
128
|
+
case "deny":
|
|
129
|
+
case "ask":
|
|
130
|
+
return decision.reason;
|
|
131
|
+
case "continue":
|
|
132
|
+
return decision.text;
|
|
133
|
+
case "rewriteInput":
|
|
134
|
+
return `${decision.reason} (proposed input: ${JSON.stringify(decision.input)})`;
|
|
135
|
+
default:
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function truncateContext(text, budgetChars) {
|
|
140
|
+
if (text.length <= budgetChars) {
|
|
141
|
+
return text;
|
|
142
|
+
}
|
|
143
|
+
if (budgetChars <= 0) {
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
const marker = TRUNCATION_MARKER.length <= budgetChars ? TRUNCATION_MARKER : TRUNCATION_MARKER.slice(0, budgetChars);
|
|
147
|
+
const keep = Math.max(0, budgetChars - marker.length);
|
|
148
|
+
return `${text.slice(0, keep)}${marker}`;
|
|
149
|
+
}
|
|
150
|
+
function canCarryContext(event, capabilities) {
|
|
151
|
+
if (event.event === "tool.before") {
|
|
152
|
+
return capabilities.contextAtToolBefore;
|
|
153
|
+
}
|
|
154
|
+
if (event.event === "tool.after") {
|
|
155
|
+
return capabilities.contextAtToolAfter;
|
|
156
|
+
}
|
|
157
|
+
if (event.event === "stop") {
|
|
158
|
+
return capabilities.contextAtStop;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
function applyContextBudget(decision, budgetChars) {
|
|
163
|
+
if (decision.kind !== "context" || budgetChars === undefined) {
|
|
164
|
+
return decision;
|
|
165
|
+
}
|
|
166
|
+
const truncated = truncateContext(decision.text, budgetChars);
|
|
167
|
+
if (truncated === decision.text) {
|
|
168
|
+
return decision;
|
|
169
|
+
}
|
|
170
|
+
return { ...decision, text: truncated };
|
|
171
|
+
}
|
|
172
|
+
function degrade(decision, event, capabilities, options = {}) {
|
|
173
|
+
if (!capabilities.enforcesHooks && isEnforcing(decision)) {
|
|
174
|
+
return applyContextBudget({ kind: "context", text: `${ADVISORY_PREFIX}${describeDecision(decision)}` }, options.contextBudgetChars);
|
|
175
|
+
}
|
|
176
|
+
if (decision.kind === "ask") {
|
|
177
|
+
if (!capabilities.askSupportedOn.includes(event.event)) {
|
|
178
|
+
return {
|
|
179
|
+
kind: "deny",
|
|
180
|
+
reason: `${ESCALATION_PREFIX}${decision.reason}`,
|
|
181
|
+
userNote: decision.userNote,
|
|
182
|
+
rule: decision.rule
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (event.permissionMode !== undefined && NO_HUMAN_MODES.has(event.permissionMode)) {
|
|
186
|
+
return {
|
|
187
|
+
kind: "deny",
|
|
188
|
+
reason: `${NO_HUMAN_PREFIX}${decision.reason}`,
|
|
189
|
+
userNote: decision.userNote,
|
|
190
|
+
rule: decision.rule
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return decision;
|
|
194
|
+
}
|
|
195
|
+
if (decision.kind === "rewriteInput" && !capabilities.toolInputRewrite) {
|
|
196
|
+
return {
|
|
197
|
+
kind: "ask",
|
|
198
|
+
reason: `Input rewrite unavailable on this provider — proposed input: ${JSON.stringify(decision.input)}. ${decision.reason}`,
|
|
199
|
+
rule: DEGRADE_RULES.rewriteUnavailable
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (decision.kind === "context") {
|
|
203
|
+
if (!canCarryContext(event, capabilities)) {
|
|
204
|
+
return { kind: "abstain" };
|
|
205
|
+
}
|
|
206
|
+
if (decision.env && !capabilities.sessionEnv) {
|
|
207
|
+
const { env: _droppedEnv, ...withoutEnv } = decision;
|
|
208
|
+
return applyContextBudget(withoutEnv, options.contextBudgetChars);
|
|
209
|
+
}
|
|
210
|
+
return applyContextBudget(decision, options.contextBudgetChars);
|
|
211
|
+
}
|
|
212
|
+
return decision;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/providers/claude/claude.capabilities.ts
|
|
216
|
+
function claudeCapabilities() {
|
|
217
|
+
return {
|
|
218
|
+
enforcesHooks: true,
|
|
219
|
+
askSupportedOn: ["tool.before", "shell.before", "mcp.before", "read.before"],
|
|
220
|
+
sessionEnv: false,
|
|
221
|
+
nativeLoopCounter: false,
|
|
222
|
+
dedicatedShellEvent: false,
|
|
223
|
+
toolInputRewrite: true,
|
|
224
|
+
toolOutputRewrite: true,
|
|
225
|
+
contextAtToolBefore: true,
|
|
226
|
+
contextAtToolAfter: true,
|
|
227
|
+
contextAtStop: true,
|
|
228
|
+
sessionStartContextReliable: true,
|
|
229
|
+
toolOutputAtAfter: true,
|
|
230
|
+
usageInPayload: false,
|
|
231
|
+
effortSignal: true,
|
|
232
|
+
thoughtEvent: false
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/providers/claude/claude.detect.ts
|
|
237
|
+
var PASCAL_CASE_EVENT_NAME = /^[A-Z][a-zA-Z0-9]*$/;
|
|
238
|
+
function detectClaude(raw) {
|
|
239
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
const value = raw;
|
|
243
|
+
const eventName = value.hook_event_name;
|
|
244
|
+
if (typeof eventName !== "string" || !PASCAL_CASE_EVENT_NAME.test(eventName)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return typeof value.cwd === "string" || typeof value.transcript_path === "string";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/providers/claude/claude.inbound.ts
|
|
251
|
+
var EVENT_KIND_BY_HOOK = {
|
|
252
|
+
SessionStart: "session.start",
|
|
253
|
+
SessionEnd: "session.end",
|
|
254
|
+
UserPromptSubmit: "prompt.submit",
|
|
255
|
+
PostToolUseFailure: "tool.failure",
|
|
256
|
+
SubagentStart: "subagent.start",
|
|
257
|
+
SubagentStop: "subagent.stop",
|
|
258
|
+
Stop: "stop",
|
|
259
|
+
PreCompact: "compact.before",
|
|
260
|
+
MessageDisplay: "response.after"
|
|
261
|
+
};
|
|
262
|
+
var MCP_TOOL_NAME = /^mcp__/;
|
|
263
|
+
function asString(value) {
|
|
264
|
+
return typeof value === "string" ? value : undefined;
|
|
265
|
+
}
|
|
266
|
+
function asNumber2(value) {
|
|
267
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
268
|
+
}
|
|
269
|
+
function asRecord2(value) {
|
|
270
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
271
|
+
}
|
|
272
|
+
function asStatus(value) {
|
|
273
|
+
return value === "completed" || value === "aborted" || value === "error" ? value : undefined;
|
|
274
|
+
}
|
|
275
|
+
function sessionKeyFor(raw) {
|
|
276
|
+
const seed = asString(raw.session_id) ?? "default";
|
|
277
|
+
return `claude-${sanitizeSegment(seed)}`;
|
|
278
|
+
}
|
|
279
|
+
function projectDirFor(raw) {
|
|
280
|
+
const envDir = process.env.CLAUDE_PROJECT_DIR;
|
|
281
|
+
if (envDir) {
|
|
282
|
+
return envDir;
|
|
283
|
+
}
|
|
284
|
+
const cwd = asString(raw.cwd);
|
|
285
|
+
if (cwd) {
|
|
286
|
+
return cwd;
|
|
287
|
+
}
|
|
288
|
+
return process.cwd();
|
|
289
|
+
}
|
|
290
|
+
function effortFor(raw) {
|
|
291
|
+
const effort = asRecord2(raw.effort);
|
|
292
|
+
const level = effort?.level;
|
|
293
|
+
return isEffortLevel(level) ? level : undefined;
|
|
294
|
+
}
|
|
295
|
+
function preToolUseKind(toolName) {
|
|
296
|
+
if (toolName === "Bash") {
|
|
297
|
+
return "shell.before";
|
|
298
|
+
}
|
|
299
|
+
if (toolName && MCP_TOOL_NAME.test(toolName)) {
|
|
300
|
+
return "mcp.before";
|
|
301
|
+
}
|
|
302
|
+
if (toolName === "Read") {
|
|
303
|
+
return "read.before";
|
|
304
|
+
}
|
|
305
|
+
return "tool.before";
|
|
306
|
+
}
|
|
307
|
+
function postToolUseKind(toolName) {
|
|
308
|
+
if (toolName === "Bash") {
|
|
309
|
+
return "shell.after";
|
|
310
|
+
}
|
|
311
|
+
if (toolName && MCP_TOOL_NAME.test(toolName)) {
|
|
312
|
+
return "mcp.after";
|
|
313
|
+
}
|
|
314
|
+
if (toolName === "Edit" || toolName === "Write") {
|
|
315
|
+
return "edit.after";
|
|
316
|
+
}
|
|
317
|
+
return "tool.after";
|
|
318
|
+
}
|
|
319
|
+
function claudeToEvent(raw) {
|
|
320
|
+
const hookEventName = asString(raw.hook_event_name);
|
|
321
|
+
if (!hookEventName) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const toolName = asString(raw.tool_name);
|
|
325
|
+
const toolInput = asRecord2(raw.tool_input);
|
|
326
|
+
let eventKind;
|
|
327
|
+
if (hookEventName === "PreToolUse") {
|
|
328
|
+
eventKind = preToolUseKind(toolName);
|
|
329
|
+
} else if (hookEventName === "PostToolUse") {
|
|
330
|
+
eventKind = postToolUseKind(toolName);
|
|
331
|
+
} else {
|
|
332
|
+
eventKind = EVENT_KIND_BY_HOOK[hookEventName];
|
|
333
|
+
}
|
|
334
|
+
if (!eventKind) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const event = {
|
|
338
|
+
provider: "claude",
|
|
339
|
+
event: eventKind,
|
|
340
|
+
sessionKey: sessionKeyFor(raw),
|
|
341
|
+
projectDir: projectDirFor(raw),
|
|
342
|
+
raw
|
|
343
|
+
};
|
|
344
|
+
const permissionMode = asString(raw.permission_mode);
|
|
345
|
+
if (permissionMode) {
|
|
346
|
+
event.permissionMode = permissionMode;
|
|
347
|
+
}
|
|
348
|
+
const isSpawnEvent = eventKind === "subagent.start" || eventKind === "subagent.stop";
|
|
349
|
+
const model = isSpawnEvent ? undefined : asString(raw.model);
|
|
350
|
+
if (model) {
|
|
351
|
+
event.model = model;
|
|
352
|
+
}
|
|
353
|
+
const effort = effortFor(raw);
|
|
354
|
+
if (effort) {
|
|
355
|
+
event.effort = effort;
|
|
356
|
+
}
|
|
357
|
+
const contextUsagePercent = asNumber2(raw.context_usage_percent);
|
|
358
|
+
if (contextUsagePercent !== undefined) {
|
|
359
|
+
event.contextUsagePercent = contextUsagePercent;
|
|
360
|
+
}
|
|
361
|
+
const transcriptPath = asString(raw.transcript_path);
|
|
362
|
+
if (transcriptPath) {
|
|
363
|
+
event.transcriptPath = transcriptPath;
|
|
364
|
+
}
|
|
365
|
+
if (!isSpawnEvent) {
|
|
366
|
+
const callerAgentType = asString(raw.agent_type);
|
|
367
|
+
if (callerAgentType) {
|
|
368
|
+
event.subagentType = callerAgentType;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
switch (eventKind) {
|
|
372
|
+
case "prompt.submit": {
|
|
373
|
+
const text = asString(raw.prompt);
|
|
374
|
+
if (text !== undefined) {
|
|
375
|
+
event.text = text;
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "response.after": {
|
|
380
|
+
const text = asString(raw.text);
|
|
381
|
+
if (text !== undefined) {
|
|
382
|
+
event.text = text;
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
case "shell.before":
|
|
387
|
+
case "shell.after": {
|
|
388
|
+
const command = toolInput ? asString(toolInput.command) : undefined;
|
|
389
|
+
if (command !== undefined) {
|
|
390
|
+
event.command = command;
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case "mcp.before":
|
|
395
|
+
case "mcp.after": {
|
|
396
|
+
if (toolName) {
|
|
397
|
+
event.toolName = toolName;
|
|
398
|
+
}
|
|
399
|
+
if (toolInput) {
|
|
400
|
+
event.toolInput = toolInput;
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case "read.before": {
|
|
405
|
+
const filePath = toolInput ? asString(toolInput.file_path) : undefined;
|
|
406
|
+
if (filePath !== undefined) {
|
|
407
|
+
event.filePath = filePath;
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "edit.after": {
|
|
412
|
+
if (toolName) {
|
|
413
|
+
event.toolName = toolName;
|
|
414
|
+
}
|
|
415
|
+
const filePath = toolInput ? asString(toolInput.file_path) : undefined;
|
|
416
|
+
if (filePath !== undefined) {
|
|
417
|
+
event.filePath = filePath;
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case "tool.before":
|
|
422
|
+
case "tool.after":
|
|
423
|
+
case "tool.failure": {
|
|
424
|
+
if (toolName) {
|
|
425
|
+
event.toolName = toolName;
|
|
426
|
+
}
|
|
427
|
+
const toolOutput = raw.tool_response;
|
|
428
|
+
if (toolOutput !== undefined && toolOutput !== null) {
|
|
429
|
+
event.toolOutput = typeof toolOutput === "string" ? toolOutput : JSON.stringify(toolOutput);
|
|
430
|
+
}
|
|
431
|
+
if (toolInput) {
|
|
432
|
+
event.toolInput = toolInput;
|
|
433
|
+
}
|
|
434
|
+
const filePath = toolInput ? asString(toolInput.file_path) : undefined;
|
|
435
|
+
if (filePath !== undefined) {
|
|
436
|
+
event.filePath = filePath;
|
|
437
|
+
}
|
|
438
|
+
const spawnSubagentType = toolInput ? asString(toolInput.subagent_type) : undefined;
|
|
439
|
+
if (spawnSubagentType) {
|
|
440
|
+
event.spawnSubagentType = spawnSubagentType;
|
|
441
|
+
}
|
|
442
|
+
const spawnModel = toolInput ? asString(toolInput.model) : undefined;
|
|
443
|
+
if (spawnModel) {
|
|
444
|
+
event.spawnModel = spawnModel;
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case "subagent.start":
|
|
449
|
+
case "subagent.stop": {
|
|
450
|
+
const spawnSubagentType = asString(raw.agent_type) ?? asString(raw.subagent_type) ?? (toolInput ? asString(toolInput.subagent_type) : undefined);
|
|
451
|
+
if (spawnSubagentType) {
|
|
452
|
+
event.spawnSubagentType = spawnSubagentType;
|
|
453
|
+
}
|
|
454
|
+
const spawnModel = (toolInput ? asString(toolInput.model) : undefined) ?? asString(raw.model);
|
|
455
|
+
if (spawnModel) {
|
|
456
|
+
event.spawnModel = spawnModel;
|
|
457
|
+
}
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case "stop": {
|
|
461
|
+
const status = asStatus(raw.status);
|
|
462
|
+
if (status) {
|
|
463
|
+
event.status = status;
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
default:
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
return event;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/providers/claude/claude.outbound.ts
|
|
474
|
+
var HOOK_EVENT_NAME_BY_KIND = {
|
|
475
|
+
"session.start": "SessionStart",
|
|
476
|
+
"session.end": "SessionEnd",
|
|
477
|
+
"prompt.submit": "UserPromptSubmit",
|
|
478
|
+
"tool.before": "PreToolUse",
|
|
479
|
+
"tool.after": "PostToolUse",
|
|
480
|
+
"tool.failure": "PostToolUseFailure",
|
|
481
|
+
"shell.before": "PreToolUse",
|
|
482
|
+
"shell.after": "PostToolUse",
|
|
483
|
+
"mcp.before": "PreToolUse",
|
|
484
|
+
"mcp.after": "PostToolUse",
|
|
485
|
+
"read.before": "PreToolUse",
|
|
486
|
+
"edit.after": "PostToolUse",
|
|
487
|
+
"subagent.start": "SubagentStart",
|
|
488
|
+
"subagent.stop": "SubagentStop",
|
|
489
|
+
stop: "Stop",
|
|
490
|
+
"compact.before": "PreCompact",
|
|
491
|
+
"response.after": "MessageDisplay",
|
|
492
|
+
"thought.after": "MessageDisplay"
|
|
493
|
+
};
|
|
494
|
+
function renderPermission(permissionDecision, hookEventName, reason) {
|
|
495
|
+
const hookSpecificOutput = { hookEventName, permissionDecision };
|
|
496
|
+
if (reason !== undefined) {
|
|
497
|
+
hookSpecificOutput.permissionDecisionReason = reason;
|
|
498
|
+
}
|
|
499
|
+
return JSON.stringify({ hookSpecificOutput });
|
|
500
|
+
}
|
|
501
|
+
function claudeRender(decision, event) {
|
|
502
|
+
const hookEventName = HOOK_EVENT_NAME_BY_KIND[event.event];
|
|
503
|
+
switch (decision.kind) {
|
|
504
|
+
case "abstain":
|
|
505
|
+
return { stdout: null, exitCode: 0 };
|
|
506
|
+
case "allow":
|
|
507
|
+
return { stdout: renderPermission("allow", hookEventName, undefined), exitCode: 0 };
|
|
508
|
+
case "deny":
|
|
509
|
+
return { stdout: renderPermission("deny", hookEventName, decision.reason), exitCode: 0 };
|
|
510
|
+
case "ask":
|
|
511
|
+
return { stdout: renderPermission("ask", hookEventName, decision.reason), exitCode: 0 };
|
|
512
|
+
case "context":
|
|
513
|
+
return {
|
|
514
|
+
stdout: JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext: decision.text } }),
|
|
515
|
+
exitCode: 0
|
|
516
|
+
};
|
|
517
|
+
case "continue":
|
|
518
|
+
return { stdout: JSON.stringify({ decision: "block", reason: decision.text }), exitCode: 0 };
|
|
519
|
+
case "rewriteInput":
|
|
520
|
+
return {
|
|
521
|
+
stdout: JSON.stringify({ hookSpecificOutput: { hookEventName, updatedInput: decision.input } }),
|
|
522
|
+
exitCode: 0
|
|
523
|
+
};
|
|
524
|
+
default: {
|
|
525
|
+
const exhaustive = decision;
|
|
526
|
+
throw new Error(`unreachable decision kind: ${JSON.stringify(exhaustive)}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/providers/claude/claude.policy-defaults.ts
|
|
532
|
+
function claudePolicyDefaults() {
|
|
533
|
+
return {
|
|
534
|
+
blockedPatterns: [],
|
|
535
|
+
minEffort: null,
|
|
536
|
+
untrustedTools: ["WebFetch", "WebSearch"]
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/providers/claude/index.ts
|
|
541
|
+
var claudeProvider = {
|
|
542
|
+
name: "claude",
|
|
543
|
+
detect: detectClaude,
|
|
544
|
+
capabilities: claudeCapabilities,
|
|
545
|
+
policyDefaults: claudePolicyDefaults,
|
|
546
|
+
toEvent: claudeToEvent,
|
|
547
|
+
render: claudeRender,
|
|
548
|
+
wiring: claudeWiring
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// src/providers/cursor/cursor.capabilities.ts
|
|
552
|
+
function cursorCapabilities() {
|
|
553
|
+
return {
|
|
554
|
+
enforcesHooks: true,
|
|
555
|
+
askSupportedOn: ["shell.before", "mcp.before"],
|
|
556
|
+
sessionEnv: true,
|
|
557
|
+
nativeLoopCounter: true,
|
|
558
|
+
dedicatedShellEvent: true,
|
|
559
|
+
toolInputRewrite: true,
|
|
560
|
+
toolOutputRewrite: true,
|
|
561
|
+
contextAtToolBefore: false,
|
|
562
|
+
contextAtToolAfter: true,
|
|
563
|
+
contextAtStop: false,
|
|
564
|
+
sessionStartContextReliable: false,
|
|
565
|
+
toolOutputAtAfter: true,
|
|
566
|
+
usageInPayload: true,
|
|
567
|
+
effortSignal: false,
|
|
568
|
+
thoughtEvent: true
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/providers/cursor/cursor.detect.ts
|
|
573
|
+
var CAMEL_CASE_EVENT_NAME = /^[a-z][a-zA-Z0-9]*$/;
|
|
574
|
+
function detectCursor(raw) {
|
|
575
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
const value = raw;
|
|
579
|
+
const eventName = value.hook_event_name;
|
|
580
|
+
if (typeof eventName !== "string" || !CAMEL_CASE_EVENT_NAME.test(eventName)) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
return Array.isArray(value.workspace_roots);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/providers/cursor/cursor.inbound.ts
|
|
587
|
+
var EVENT_KIND_BY_HOOK2 = {
|
|
588
|
+
sessionStart: "session.start",
|
|
589
|
+
sessionEnd: "session.end",
|
|
590
|
+
beforeSubmitPrompt: "prompt.submit",
|
|
591
|
+
preToolUse: "tool.before",
|
|
592
|
+
postToolUse: "tool.after",
|
|
593
|
+
postToolUseFailure: "tool.failure",
|
|
594
|
+
beforeShellExecution: "shell.before",
|
|
595
|
+
afterShellExecution: "shell.after",
|
|
596
|
+
beforeMCPExecution: "mcp.before",
|
|
597
|
+
afterMCPExecution: "mcp.after",
|
|
598
|
+
beforeReadFile: "read.before",
|
|
599
|
+
afterFileEdit: "edit.after",
|
|
600
|
+
subagentStart: "subagent.start",
|
|
601
|
+
subagentStop: "subagent.stop",
|
|
602
|
+
stop: "stop",
|
|
603
|
+
preCompact: "compact.before",
|
|
604
|
+
afterAgentResponse: "response.after",
|
|
605
|
+
afterAgentThought: "thought.after"
|
|
606
|
+
};
|
|
607
|
+
function asString2(value) {
|
|
608
|
+
return typeof value === "string" ? value : undefined;
|
|
609
|
+
}
|
|
610
|
+
function asNumber3(value) {
|
|
611
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
612
|
+
}
|
|
613
|
+
function asRecord3(value) {
|
|
614
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
615
|
+
}
|
|
616
|
+
function asStatus2(value) {
|
|
617
|
+
return value === "completed" || value === "aborted" || value === "error" ? value : undefined;
|
|
618
|
+
}
|
|
619
|
+
function sessionKeyFor2(raw) {
|
|
620
|
+
const seed = asString2(raw.conversation_id) || asString2(raw.session_id) || "default";
|
|
621
|
+
return `cursor-${sanitizeSegment(seed)}`;
|
|
622
|
+
}
|
|
623
|
+
function projectDirFor2(raw) {
|
|
624
|
+
const envDir = process.env.CURSOR_PROJECT_DIR;
|
|
625
|
+
if (envDir) {
|
|
626
|
+
return envDir;
|
|
627
|
+
}
|
|
628
|
+
const roots = raw.workspace_roots;
|
|
629
|
+
if (Array.isArray(roots)) {
|
|
630
|
+
const first = asString2(roots[0]);
|
|
631
|
+
if (first) {
|
|
632
|
+
return first;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return process.cwd();
|
|
636
|
+
}
|
|
637
|
+
function cursorToEvent(raw) {
|
|
638
|
+
const hookEventName = asString2(raw.hook_event_name);
|
|
639
|
+
const eventKind = hookEventName ? EVENT_KIND_BY_HOOK2[hookEventName] : undefined;
|
|
640
|
+
if (!eventKind) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const event = {
|
|
644
|
+
provider: "cursor",
|
|
645
|
+
event: eventKind,
|
|
646
|
+
sessionKey: sessionKeyFor2(raw),
|
|
647
|
+
projectDir: projectDirFor2(raw),
|
|
648
|
+
raw
|
|
649
|
+
};
|
|
650
|
+
const model = asString2(raw.model);
|
|
651
|
+
if (model) {
|
|
652
|
+
event.model = model;
|
|
653
|
+
}
|
|
654
|
+
const contextUsagePercent = asNumber3(raw.context_usage_percent);
|
|
655
|
+
if (contextUsagePercent !== undefined) {
|
|
656
|
+
event.contextUsagePercent = contextUsagePercent;
|
|
657
|
+
}
|
|
658
|
+
switch (eventKind) {
|
|
659
|
+
case "prompt.submit": {
|
|
660
|
+
const text = asString2(raw.prompt);
|
|
661
|
+
if (text !== undefined) {
|
|
662
|
+
event.text = text;
|
|
663
|
+
}
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "response.after": {
|
|
667
|
+
const text = asString2(raw.text);
|
|
668
|
+
if (text !== undefined) {
|
|
669
|
+
event.text = text;
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
case "thought.after": {
|
|
674
|
+
const text = asString2(raw.thought) ?? asString2(raw.text);
|
|
675
|
+
if (text !== undefined) {
|
|
676
|
+
event.text = text;
|
|
677
|
+
}
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
case "tool.before":
|
|
681
|
+
case "tool.after":
|
|
682
|
+
case "tool.failure": {
|
|
683
|
+
const toolName = asString2(raw.tool_name);
|
|
684
|
+
if (toolName) {
|
|
685
|
+
event.toolName = toolName;
|
|
686
|
+
}
|
|
687
|
+
const toolInput = asRecord3(raw.tool_input);
|
|
688
|
+
if (toolInput) {
|
|
689
|
+
event.toolInput = toolInput;
|
|
690
|
+
}
|
|
691
|
+
const subagentType = asString2(raw.subagent_type);
|
|
692
|
+
if (subagentType) {
|
|
693
|
+
event.subagentType = subagentType;
|
|
694
|
+
}
|
|
695
|
+
const toolSpawnModel = toolInput ? asString2(toolInput.model) : undefined;
|
|
696
|
+
if (toolSpawnModel) {
|
|
697
|
+
event.spawnModel = toolSpawnModel;
|
|
698
|
+
}
|
|
699
|
+
const toolSpawnType = toolInput ? asString2(toolInput.subagent_type) : undefined;
|
|
700
|
+
if (toolSpawnType) {
|
|
701
|
+
event.spawnSubagentType = toolSpawnType;
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
case "shell.before":
|
|
706
|
+
case "shell.after": {
|
|
707
|
+
const command = asString2(raw.command);
|
|
708
|
+
if (command !== undefined) {
|
|
709
|
+
event.command = command;
|
|
710
|
+
}
|
|
711
|
+
const output = asString2(raw.output);
|
|
712
|
+
if (output !== undefined) {
|
|
713
|
+
event.toolOutput = output;
|
|
714
|
+
}
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case "mcp.before":
|
|
718
|
+
case "mcp.after": {
|
|
719
|
+
const resultJson = asString2(raw.result_json);
|
|
720
|
+
if (resultJson !== undefined) {
|
|
721
|
+
event.toolOutput = resultJson;
|
|
722
|
+
}
|
|
723
|
+
const toolName = asString2(raw.tool_name);
|
|
724
|
+
if (toolName) {
|
|
725
|
+
event.toolName = toolName;
|
|
726
|
+
}
|
|
727
|
+
const toolInput = asRecord3(raw.tool_input);
|
|
728
|
+
if (toolInput) {
|
|
729
|
+
event.toolInput = toolInput;
|
|
730
|
+
}
|
|
731
|
+
const command = asString2(raw.command);
|
|
732
|
+
if (command !== undefined) {
|
|
733
|
+
event.command = command;
|
|
734
|
+
}
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
case "read.before":
|
|
738
|
+
case "edit.after": {
|
|
739
|
+
const filePath = asString2(raw.file_path);
|
|
740
|
+
if (filePath !== undefined) {
|
|
741
|
+
event.filePath = filePath;
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case "subagent.start":
|
|
746
|
+
case "subagent.stop": {
|
|
747
|
+
const spawnSubagentType = asString2(raw.subagent_type);
|
|
748
|
+
if (spawnSubagentType) {
|
|
749
|
+
event.spawnSubagentType = spawnSubagentType;
|
|
750
|
+
}
|
|
751
|
+
const spawnModel = asString2(raw.subagent_model);
|
|
752
|
+
if (spawnModel) {
|
|
753
|
+
event.spawnModel = spawnModel;
|
|
754
|
+
}
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "stop": {
|
|
758
|
+
const status = asStatus2(raw.status);
|
|
759
|
+
if (status) {
|
|
760
|
+
event.status = status;
|
|
761
|
+
}
|
|
762
|
+
const loopCount = asNumber3(raw.loop_count);
|
|
763
|
+
if (loopCount !== undefined) {
|
|
764
|
+
event.loopCount = loopCount;
|
|
765
|
+
}
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
default:
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
return event;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/providers/cursor/cursor.outbound.ts
|
|
775
|
+
function renderDenyOrAsk(permission, decision) {
|
|
776
|
+
const body = { permission };
|
|
777
|
+
if (decision.userNote !== undefined) {
|
|
778
|
+
body.user_message = decision.userNote;
|
|
779
|
+
}
|
|
780
|
+
body.agent_message = decision.reason;
|
|
781
|
+
return JSON.stringify(body);
|
|
782
|
+
}
|
|
783
|
+
function cursorRender(decision, _event) {
|
|
784
|
+
switch (decision.kind) {
|
|
785
|
+
case "abstain":
|
|
786
|
+
return { stdout: "{}", exitCode: 0 };
|
|
787
|
+
case "allow":
|
|
788
|
+
return { stdout: JSON.stringify({ permission: "allow" }), exitCode: 0 };
|
|
789
|
+
case "deny":
|
|
790
|
+
return { stdout: renderDenyOrAsk("deny", decision), exitCode: 0 };
|
|
791
|
+
case "ask":
|
|
792
|
+
return { stdout: renderDenyOrAsk("ask", decision), exitCode: 0 };
|
|
793
|
+
case "context": {
|
|
794
|
+
const body = {};
|
|
795
|
+
if (decision.env !== undefined) {
|
|
796
|
+
body.env = decision.env;
|
|
797
|
+
}
|
|
798
|
+
body.additional_context = decision.text;
|
|
799
|
+
return { stdout: JSON.stringify(body), exitCode: 0 };
|
|
800
|
+
}
|
|
801
|
+
case "continue":
|
|
802
|
+
return { stdout: JSON.stringify({ followup_message: decision.text }), exitCode: 0 };
|
|
803
|
+
case "rewriteInput":
|
|
804
|
+
return { stdout: JSON.stringify({ updated_input: decision.input }), exitCode: 0 };
|
|
805
|
+
default: {
|
|
806
|
+
const exhaustive = decision;
|
|
807
|
+
throw new Error(`unreachable decision kind: ${JSON.stringify(exhaustive)}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/providers/cursor/cursor.policy-defaults.ts
|
|
813
|
+
function cursorPolicyDefaults() {
|
|
814
|
+
return {
|
|
815
|
+
blockedPatterns: ["-fast(?:$|[^a-z0-9])", "/fast(?:$|[^a-z0-9])", "composer-2\\.5-fast"],
|
|
816
|
+
minEffort: null,
|
|
817
|
+
untrustedTools: ["Fetch", "WebSearch"]
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/providers/cursor/index.ts
|
|
822
|
+
var cursorProvider = {
|
|
823
|
+
name: "cursor",
|
|
824
|
+
detect: detectCursor,
|
|
825
|
+
capabilities: cursorCapabilities,
|
|
826
|
+
policyDefaults: cursorPolicyDefaults,
|
|
827
|
+
toEvent: cursorToEvent,
|
|
828
|
+
render: cursorRender,
|
|
829
|
+
wiring: cursorWiring
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// src/providers/provider.registry.ts
|
|
833
|
+
var providers = [cursorProvider, claudeProvider];
|
|
834
|
+
function resolveFromRegistry(raw, registry) {
|
|
835
|
+
const matched = registry.filter((provider) => provider.detect(raw));
|
|
836
|
+
if (matched.length === 0) {
|
|
837
|
+
return { provider: null, ambiguous: false, matchedNames: [] };
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
provider: matched[0] ?? null,
|
|
841
|
+
ambiguous: matched.length > 1,
|
|
842
|
+
matchedNames: matched.map((provider) => provider.name)
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
export { renderClaudeLessonsView, readClaudeUsage, renderCursorLessonsView, degrade, providers, resolveFromRegistry };
|