@tech-leads-club/harness-toolkit 0.3.2 → 0.3.3
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/bin/tlc-build.mjs +15 -29
- package/bin/tlc-cli.ts +63 -2
- package/dist/compact-before.mjs +7961 -13
- package/dist/doctor.mjs +8480 -33
- package/dist/help-topic.mjs +14 -6
- package/dist/init-project.mjs +793 -36
- package/dist/install-runtime.mjs +7268 -17
- package/dist/lessons-cli.mjs +7044 -26
- package/dist/obs-cli.mjs +7037 -27
- package/dist/price-lookup.mjs +201 -11
- package/dist/prompt-submit.mjs +7975 -15
- package/dist/refresh-model-prices.mjs +7060 -25
- package/dist/response-after.mjs +7962 -12
- package/dist/run.mjs +7960 -10
- package/dist/session-end.mjs +8030 -15
- package/dist/session-start.mjs +8076 -21
- package/dist/shim.mjs +7024 -18
- package/dist/stop.mjs +8042 -29
- package/dist/subagent-start.mjs +7983 -13
- package/dist/subagent-stop.mjs +7962 -12
- package/dist/support.mjs +7168 -21
- package/dist/tlc-cli.mjs +8235 -65
- package/dist/tool-after.mjs +8177 -21
- package/dist/tool-before.mjs +7991 -16
- package/dist/tool-failure.mjs +7962 -15
- package/dist/uninstall-runtime.mjs +1008 -61
- package/docs/log.md +1 -1
- package/package.json +1 -1
- package/dist/chunks/compact-before-1e4qg1qt.mjs +0 -1185
- package/dist/chunks/compact-before-2hpbfxm5.mjs +0 -5782
- package/dist/chunks/compact-before-49j320yp.mjs +0 -1283
- package/dist/chunks/compact-before-4jrq0sqs.mjs +0 -61
- package/dist/chunks/compact-before-6w8n1vh1.mjs +0 -186
- package/dist/chunks/compact-before-7sdmwswh.mjs +0 -52
- package/dist/chunks/compact-before-beqpmqrm.mjs +0 -187
- package/dist/chunks/compact-before-j9y4jgn4.mjs +0 -845
- package/dist/chunks/compact-before-pk86tqx2.mjs +0 -118
- package/dist/chunks/compact-before-pkqk5v29.mjs +0 -137
- package/dist/chunks/compact-before-w1293m4n.mjs +0 -315
- package/dist/chunks/compact-before-wnnds45y.mjs +0 -26
- package/dist/chunks/compact-before-wt2c3nh4.mjs +0 -551
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
renderClaudeLessonsView,
|
|
3
|
-
renderCursorLessonsView
|
|
4
|
-
} from "./compact-before-j9y4jgn4.mjs";
|
|
5
|
-
import {
|
|
6
|
-
coreFacade
|
|
7
|
-
} from "./compact-before-2hpbfxm5.mjs";
|
|
8
|
-
import {
|
|
9
|
-
runProcess
|
|
10
|
-
} from "./compact-before-49j320yp.mjs";
|
|
11
|
-
|
|
12
|
-
// src/entrypoints/support.ts
|
|
13
|
-
import { existsSync, statSync } from "node:fs";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
var OBS_CONFIG = coreFacade.observability.DEFAULT_OBS;
|
|
16
|
-
var OBS_CONFIG_AUDIT = { ...OBS_CONFIG, debugEnabled: true };
|
|
17
|
-
function obsConfigFor(policy, base = OBS_CONFIG) {
|
|
18
|
-
return {
|
|
19
|
-
...base,
|
|
20
|
-
globalSpool: policy.obs.globalSpool,
|
|
21
|
-
includePayloads: policy.obs.includePayloads,
|
|
22
|
-
maxAttrChars: policy.obs.maxAttrChars,
|
|
23
|
-
sessionCostAlertUsd: policy.obs.sessionCostAlertUsd,
|
|
24
|
-
retentionDays: policy.obs.retentionDays
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function sizeOf(path) {
|
|
28
|
-
try {
|
|
29
|
-
return statSync(path).size;
|
|
30
|
-
} catch {
|
|
31
|
-
return 0;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function sessionIdFromKey(event) {
|
|
35
|
-
const prefix = `${event.provider}-`;
|
|
36
|
-
return event.sessionKey.startsWith(prefix) ? event.sessionKey.slice(prefix.length) : event.sessionKey;
|
|
37
|
-
}
|
|
38
|
-
async function currentGitBranch(root) {
|
|
39
|
-
if (!existsSync(join(root, ".git"))) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
const result = await runProcess({ command: ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd: root });
|
|
43
|
-
if (result.exitCode !== 0) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
const branch = result.stdout.trim();
|
|
47
|
-
return branch.length > 0 ? branch : null;
|
|
48
|
-
}
|
|
49
|
-
async function currentGitSha(root) {
|
|
50
|
-
if (!existsSync(join(root, ".git"))) {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
const result = await runProcess({ command: ["git", "rev-parse", "--short", "HEAD"], cwd: root });
|
|
54
|
-
if (result.exitCode !== 0) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
const sha = result.stdout.trim();
|
|
58
|
-
return sha.length > 0 ? sha : null;
|
|
59
|
-
}
|
|
60
|
-
function effectiveAllowedModels(configured, provider) {
|
|
61
|
-
return coreFacade.policy.forProvider(configured, provider.name) ?? [];
|
|
62
|
-
}
|
|
63
|
-
function effectiveBlockedPatterns(configured, provider) {
|
|
64
|
-
const fromConfig = coreFacade.policy.forProvider(configured, provider.name) ?? [];
|
|
65
|
-
return [...fromConfig, ...provider.policyDefaults().blockedPatterns];
|
|
66
|
-
}
|
|
67
|
-
function effectiveMinEffort(configured, provider) {
|
|
68
|
-
return configured ?? provider.policyDefaults().minEffort;
|
|
69
|
-
}
|
|
70
|
-
function subagentSpawnInput(event, policy, provider, model) {
|
|
71
|
-
return {
|
|
72
|
-
provider: provider.name,
|
|
73
|
-
sessionKey: event.sessionKey,
|
|
74
|
-
projectDir: event.projectDir,
|
|
75
|
-
model,
|
|
76
|
-
effort: event.effort,
|
|
77
|
-
allowedModels: effectiveAllowedModels(policy.subagents.allowedModels, provider),
|
|
78
|
-
blockedPatterns: effectiveBlockedPatterns(policy.subagents.blockedPatterns, provider),
|
|
79
|
-
minEffort: effectiveMinEffort(policy.subagents.minEffort, provider),
|
|
80
|
-
requireModel: policy.subagents.requireModel,
|
|
81
|
-
enforceAllowlist: policy.subagents.enforceAllowlist,
|
|
82
|
-
blockParentFast: policy.subagents.blockParentFast,
|
|
83
|
-
blockMode: policy.subagents.blockMode
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function readModelFromToolInput(toolInput) {
|
|
87
|
-
if (!toolInput) {
|
|
88
|
-
return "";
|
|
89
|
-
}
|
|
90
|
-
const model = toolInput.model ?? toolInput.Model;
|
|
91
|
-
return typeof model === "string" ? model : "";
|
|
92
|
-
}
|
|
93
|
-
function renderLessonLine(lesson) {
|
|
94
|
-
return coreFacade.lesson.renderLessonBlock(lesson);
|
|
95
|
-
}
|
|
96
|
-
function renderProviderLessonsView(providerName, root) {
|
|
97
|
-
if (providerName === "cursor") {
|
|
98
|
-
return renderCursorLessonsView(root);
|
|
99
|
-
}
|
|
100
|
-
if (providerName === "claude") {
|
|
101
|
-
return renderClaudeLessonsView(root);
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
function formatLessonsBlock(lessons, title, omitted = 0) {
|
|
106
|
-
if (lessons.length === 0) {
|
|
107
|
-
return "";
|
|
108
|
-
}
|
|
109
|
-
const lines = [title, ...lessons.map(renderLessonLine)];
|
|
110
|
-
if (omitted > 0) {
|
|
111
|
-
const noun = omitted === 1 ? "lesson" : "lessons";
|
|
112
|
-
lines.push(` (${omitted} more eligible ${noun} omitted under the char budget — raise maxCharsSession to see them)`);
|
|
113
|
-
}
|
|
114
|
-
return lines.join(`
|
|
115
|
-
`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export { OBS_CONFIG, OBS_CONFIG_AUDIT, obsConfigFor, sizeOf, sessionIdFromKey, currentGitBranch, currentGitSha, effectiveAllowedModels, effectiveBlockedPatterns, effectiveMinEffort, subagentSpawnInput, readModelFromToolInput, renderLessonLine, renderProviderLessonsView, formatLessonsBlock };
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
effectiveBlockedPatterns,
|
|
3
|
-
obsConfigFor,
|
|
4
|
-
sessionIdFromKey
|
|
5
|
-
} from "./compact-before-pk86tqx2.mjs";
|
|
6
|
-
import {
|
|
7
|
-
degrade,
|
|
8
|
-
providers,
|
|
9
|
-
resolveFromRegistry
|
|
10
|
-
} from "./compact-before-j9y4jgn4.mjs";
|
|
11
|
-
import {
|
|
12
|
-
coreFacade
|
|
13
|
-
} from "./compact-before-2hpbfxm5.mjs";
|
|
14
|
-
import {
|
|
15
|
-
appendRecord,
|
|
16
|
-
readStdinText
|
|
17
|
-
} from "./compact-before-49j320yp.mjs";
|
|
18
|
-
import {
|
|
19
|
-
projectStateDir
|
|
20
|
-
} from "./compact-before-4jrq0sqs.mjs";
|
|
21
|
-
|
|
22
|
-
// src/entrypoints/run.ts
|
|
23
|
-
import { join } from "node:path";
|
|
24
|
-
var CONTEXT_BUDGET_CHARS = 6000;
|
|
25
|
-
function errorMessage(error) {
|
|
26
|
-
return error instanceof Error ? error.message : String(error);
|
|
27
|
-
}
|
|
28
|
-
function recordAdapterEvent(root, kind, attrs) {
|
|
29
|
-
try {
|
|
30
|
-
appendRecord(join(projectStateDir(root), "obs.jsonl"), {
|
|
31
|
-
schema: "harness.observability.v1",
|
|
32
|
-
provider: "unknown",
|
|
33
|
-
kind,
|
|
34
|
-
level: "signal",
|
|
35
|
-
ts: new Date().toISOString(),
|
|
36
|
-
attrs
|
|
37
|
-
});
|
|
38
|
-
} catch {}
|
|
39
|
-
}
|
|
40
|
-
function recordRefusal(event, policy, decision) {
|
|
41
|
-
if (decision.kind !== "deny" && decision.kind !== "ask") {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (event.event === "shell.before") {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
coreFacade.observability.recordObs(event.projectDir, obsConfigFor(policy), {
|
|
48
|
-
provider: event.provider,
|
|
49
|
-
kind: "policy.deny",
|
|
50
|
-
sessionKey: event.sessionKey,
|
|
51
|
-
attrs: {
|
|
52
|
-
event: event.event,
|
|
53
|
-
tool_name: event.toolName,
|
|
54
|
-
permission: decision.kind,
|
|
55
|
-
rule: decision.rule ?? "none"
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
function asRecord(value) {
|
|
60
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
61
|
-
}
|
|
62
|
-
async function runHandler(handler, io = {}) {
|
|
63
|
-
const readStdin = io.readStdin ?? readStdinText;
|
|
64
|
-
const now = io.now ? io.now() : new Date;
|
|
65
|
-
const abstainRendered = { stdout: null, exitCode: 0 };
|
|
66
|
-
const text = await readStdin();
|
|
67
|
-
const trimmed = text.trim();
|
|
68
|
-
if (!trimmed) {
|
|
69
|
-
recordAdapterEvent(process.cwd(), "adapter.unrecognized", { reason: "empty-stdin" });
|
|
70
|
-
return { event: null, decision: { kind: "abstain" }, rendered: abstainRendered };
|
|
71
|
-
}
|
|
72
|
-
let parsed;
|
|
73
|
-
try {
|
|
74
|
-
parsed = JSON.parse(trimmed);
|
|
75
|
-
} catch {
|
|
76
|
-
recordAdapterEvent(process.cwd(), "adapter.unrecognized", { reason: "invalid-json" });
|
|
77
|
-
return { event: null, decision: { kind: "abstain" }, rendered: abstainRendered };
|
|
78
|
-
}
|
|
79
|
-
const resolved = resolveFromRegistry(parsed, providers);
|
|
80
|
-
if (!resolved.provider) {
|
|
81
|
-
recordAdapterEvent(process.cwd(), "adapter.unrecognized", { reason: "no-provider-match" });
|
|
82
|
-
return { event: null, decision: { kind: "abstain" }, rendered: abstainRendered };
|
|
83
|
-
}
|
|
84
|
-
if (resolved.ambiguous) {
|
|
85
|
-
recordAdapterEvent(process.cwd(), "adapter.ambiguous", { matched: resolved.matchedNames });
|
|
86
|
-
}
|
|
87
|
-
const provider = resolved.provider;
|
|
88
|
-
const event = provider.toEvent(asRecord(parsed));
|
|
89
|
-
if (!event) {
|
|
90
|
-
recordAdapterEvent(process.cwd(), "adapter.unrecognized", {
|
|
91
|
-
reason: "unrecognized-event",
|
|
92
|
-
provider: provider.name
|
|
93
|
-
});
|
|
94
|
-
return { event: null, decision: { kind: "abstain" }, rendered: abstainRendered };
|
|
95
|
-
}
|
|
96
|
-
const capabilities = provider.capabilities();
|
|
97
|
-
try {
|
|
98
|
-
const policy = coreFacade.policy.loadPolicy(event.projectDir);
|
|
99
|
-
if (event.model) {
|
|
100
|
-
coreFacade.subagentPolicy.upsertParentModelState(event.projectDir, event.sessionKey, { model: event.model }, effectiveBlockedPatterns(policy.subagents.blockedPatterns, provider));
|
|
101
|
-
}
|
|
102
|
-
coreFacade.presence.heartbeat(event.projectDir, {
|
|
103
|
-
provider: event.provider,
|
|
104
|
-
session: sessionIdFromKey(event),
|
|
105
|
-
file: event.filePath,
|
|
106
|
-
now
|
|
107
|
-
});
|
|
108
|
-
const context = { policy, capabilities, provider, now };
|
|
109
|
-
const decision = await handler(event, context);
|
|
110
|
-
const degraded = degrade(decision, event, capabilities, {
|
|
111
|
-
contextBudgetChars: CONTEXT_BUDGET_CHARS
|
|
112
|
-
});
|
|
113
|
-
recordRefusal(event, policy, degraded);
|
|
114
|
-
const rendered = provider.render(degraded, event);
|
|
115
|
-
return { event, decision: degraded, rendered };
|
|
116
|
-
} catch (error) {
|
|
117
|
-
recordAdapterEvent(event.projectDir, "adapter.error", {
|
|
118
|
-
provider: event.provider,
|
|
119
|
-
event: event.event,
|
|
120
|
-
message: errorMessage(error)
|
|
121
|
-
});
|
|
122
|
-
const abstain = { kind: "abstain" };
|
|
123
|
-
return { event, decision: abstain, rendered: provider.render(abstain, event) };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
async function main(handler) {
|
|
127
|
-
const outcome = await runHandler(handler);
|
|
128
|
-
if (outcome.rendered.stdout !== null) {
|
|
129
|
-
const text = outcome.rendered.stdout;
|
|
130
|
-
process.stdout.write(text.endsWith(`
|
|
131
|
-
`) ? text : `${text}
|
|
132
|
-
`);
|
|
133
|
-
}
|
|
134
|
-
process.exit(outcome.rendered.exitCode);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export { CONTEXT_BUDGET_CHARS, runHandler, main };
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
claudeConfigDir,
|
|
3
|
-
cursorConfigDir
|
|
4
|
-
} from "./compact-before-4jrq0sqs.mjs";
|
|
5
|
-
|
|
6
|
-
// src/providers/claude/claude.wiring.ts
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
9
|
-
var ENTRY_SPECS = [
|
|
10
|
-
{ hookEvent: "SessionStart", handler: "session-start", timeoutSeconds: 10 },
|
|
11
|
-
{ hookEvent: "SessionEnd", handler: "session-end", timeoutSeconds: 10 },
|
|
12
|
-
{ hookEvent: "UserPromptSubmit", handler: "prompt-submit", timeoutSeconds: 5 },
|
|
13
|
-
{ hookEvent: "PreToolUse", handler: "tool-before", timeoutSeconds: 10, failClosed: true },
|
|
14
|
-
{ hookEvent: "PostToolUse", handler: "tool-after", timeoutSeconds: 10 },
|
|
15
|
-
{ hookEvent: "PostToolUseFailure", handler: "tool-failure", timeoutSeconds: 5 },
|
|
16
|
-
{ hookEvent: "SubagentStart", handler: "subagent-start", timeoutSeconds: 5, failClosed: true },
|
|
17
|
-
{ hookEvent: "SubagentStop", handler: "subagent-stop", timeoutSeconds: 5 },
|
|
18
|
-
{ hookEvent: "Stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
19
|
-
{ hookEvent: "PreCompact", handler: "compact-before", timeoutSeconds: 5 },
|
|
20
|
-
{ hookEvent: "MessageDisplay", handler: "response-after", timeoutSeconds: 5 }
|
|
21
|
-
];
|
|
22
|
-
function claudeSettingsPath() {
|
|
23
|
-
return join(claudeConfigDir(), "settings.json");
|
|
24
|
-
}
|
|
25
|
-
function claudeWiring(runtime) {
|
|
26
|
-
const entries = ENTRY_SPECS.map((spec) => ({
|
|
27
|
-
hookEvent: spec.hookEvent,
|
|
28
|
-
handler: spec.handler,
|
|
29
|
-
command: "node",
|
|
30
|
-
args: [runtime.launcherPath, spec.handler],
|
|
31
|
-
timeoutSeconds: spec.timeoutSeconds,
|
|
32
|
-
...spec.failClosed !== undefined ? { failClosed: spec.failClosed } : {},
|
|
33
|
-
...spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}
|
|
34
|
-
}));
|
|
35
|
-
return {
|
|
36
|
-
target: claudeSettingsPath(),
|
|
37
|
-
strategy: "merge",
|
|
38
|
-
entries
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function isPlainRecord(value) {
|
|
42
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
43
|
-
}
|
|
44
|
-
function isHooksRecord(value) {
|
|
45
|
-
return isPlainRecord(value);
|
|
46
|
-
}
|
|
47
|
-
function deepEqual(a, b) {
|
|
48
|
-
if (a === b) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
52
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return a.every((item, index) => deepEqual(item, b[index]));
|
|
56
|
-
}
|
|
57
|
-
if (isPlainRecord(a) && isPlainRecord(b)) {
|
|
58
|
-
const aKeys = Object.keys(a);
|
|
59
|
-
const bKeys = Object.keys(b);
|
|
60
|
-
if (aKeys.length !== bKeys.length) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
return aKeys.every((key) => bKeys.includes(key) && deepEqual(a[key], b[key]));
|
|
64
|
-
}
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
function desiredHooksFor(entries) {
|
|
68
|
-
const hooks = {};
|
|
69
|
-
for (const entry of entries) {
|
|
70
|
-
const group = {
|
|
71
|
-
hooks: [{ type: "command", command: entry.command, args: entry.args }]
|
|
72
|
-
};
|
|
73
|
-
hooks[entry.hookEvent] = [...hooks[entry.hookEvent] ?? [], group];
|
|
74
|
-
}
|
|
75
|
-
return hooks;
|
|
76
|
-
}
|
|
77
|
-
var LAUNCHER_MARKER = "tlc-exec.mjs";
|
|
78
|
-
function isHarnessGroup(group) {
|
|
79
|
-
return JSON.stringify(group ?? null).includes(LAUNCHER_MARKER);
|
|
80
|
-
}
|
|
81
|
-
function canonicalLauncherPath(path, resolve = realpathSync) {
|
|
82
|
-
try {
|
|
83
|
-
return resolve(path);
|
|
84
|
-
} catch {
|
|
85
|
-
return path;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
function canonicalizeGroups(groups, resolve) {
|
|
89
|
-
return JSON.parse(JSON.stringify(groups ?? null, (_key, value) => typeof value === "string" && value.includes(LAUNCHER_MARKER) ? canonicalLauncherPath(value, resolve) : value));
|
|
90
|
-
}
|
|
91
|
-
function mergeClaudeSettings(existingText, entries) {
|
|
92
|
-
const desired = desiredHooksFor(entries);
|
|
93
|
-
let settings = {};
|
|
94
|
-
if (existingText !== null && existingText.trim() !== "") {
|
|
95
|
-
let parsed;
|
|
96
|
-
try {
|
|
97
|
-
parsed = JSON.parse(existingText);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
-
return { ok: false, error: message, block: JSON.stringify({ hooks: desired }, null, 2) };
|
|
101
|
-
}
|
|
102
|
-
if (!isPlainRecord(parsed)) {
|
|
103
|
-
return {
|
|
104
|
-
ok: false,
|
|
105
|
-
error: "settings.json root is not a JSON object",
|
|
106
|
-
block: JSON.stringify({ hooks: desired }, null, 2)
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
settings = parsed;
|
|
110
|
-
}
|
|
111
|
-
const currentHooks = isHooksRecord(settings.hooks) ? settings.hooks : {};
|
|
112
|
-
const mergedHooks = { ...currentHooks };
|
|
113
|
-
let changed = false;
|
|
114
|
-
for (const [hookEvent, groups] of Object.entries(desired)) {
|
|
115
|
-
const existingGroups = mergedHooks[hookEvent] ?? [];
|
|
116
|
-
const foreign = existingGroups.filter((group) => !isHarnessGroup(group));
|
|
117
|
-
const nextGroups = [...foreign, ...groups];
|
|
118
|
-
if (!deepEqual(canonicalizeGroups(existingGroups), canonicalizeGroups(nextGroups))) {
|
|
119
|
-
changed = true;
|
|
120
|
-
}
|
|
121
|
-
mergedHooks[hookEvent] = nextGroups;
|
|
122
|
-
}
|
|
123
|
-
const mergedSettings = { ...settings, hooks: mergedHooks };
|
|
124
|
-
return { ok: true, settingsText: JSON.stringify(mergedSettings, null, 2), changed };
|
|
125
|
-
}
|
|
126
|
-
function unmergeClaudeSettings(existingText) {
|
|
127
|
-
if (existingText === null || existingText.trim() === "") {
|
|
128
|
-
return { ok: true, settingsText: "", changed: false };
|
|
129
|
-
}
|
|
130
|
-
let parsed;
|
|
131
|
-
try {
|
|
132
|
-
parsed = JSON.parse(existingText);
|
|
133
|
-
} catch (error) {
|
|
134
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
-
return { ok: false, error: message, block: "" };
|
|
136
|
-
}
|
|
137
|
-
if (!isPlainRecord(parsed)) {
|
|
138
|
-
return { ok: false, error: "settings.json root is not a JSON object", block: "" };
|
|
139
|
-
}
|
|
140
|
-
const currentHooks = isHooksRecord(parsed.hooks) ? parsed.hooks : {};
|
|
141
|
-
const remainingHooks = {};
|
|
142
|
-
let changed = false;
|
|
143
|
-
for (const [hookEvent, groups] of Object.entries(currentHooks)) {
|
|
144
|
-
const foreign = groups.filter((group) => !isHarnessGroup(group));
|
|
145
|
-
if (foreign.length !== groups.length) {
|
|
146
|
-
changed = true;
|
|
147
|
-
}
|
|
148
|
-
if (foreign.length > 0) {
|
|
149
|
-
remainingHooks[hookEvent] = foreign;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const next = {};
|
|
153
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
154
|
-
if (key !== "hooks") {
|
|
155
|
-
next[key] = value;
|
|
156
|
-
} else if (Object.keys(remainingHooks).length > 0) {
|
|
157
|
-
next.hooks = remainingHooks;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return { ok: true, settingsText: JSON.stringify(next, null, 2), changed };
|
|
161
|
-
}
|
|
162
|
-
function removeClaudeWiring(settingsPath) {
|
|
163
|
-
if (!existsSync(settingsPath)) {
|
|
164
|
-
return { ok: true, settingsText: "", changed: false };
|
|
165
|
-
}
|
|
166
|
-
const result = unmergeClaudeSettings(readFileSync(settingsPath, "utf8"));
|
|
167
|
-
if (result.ok && result.changed) {
|
|
168
|
-
writeFileSync(settingsPath, result.settingsText, "utf8");
|
|
169
|
-
}
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
function applyClaudeWiring(settingsPath, entries) {
|
|
173
|
-
const existingText = existsSync(settingsPath) ? readFileSync(settingsPath, "utf8") : null;
|
|
174
|
-
const result = mergeClaudeSettings(existingText, entries);
|
|
175
|
-
if (result.ok && result.changed) {
|
|
176
|
-
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
177
|
-
writeFileSync(settingsPath, result.settingsText, "utf8");
|
|
178
|
-
}
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// src/providers/cursor/cursor.wiring.ts
|
|
183
|
-
import { join as join2 } from "node:path";
|
|
184
|
-
var ENTRY_SPECS2 = [
|
|
185
|
-
{ hookEvent: "sessionStart", handler: "session-start", timeoutSeconds: 10 },
|
|
186
|
-
{ hookEvent: "sessionEnd", handler: "session-end", timeoutSeconds: 10 },
|
|
187
|
-
{ hookEvent: "beforeSubmitPrompt", handler: "prompt-submit", timeoutSeconds: 5 },
|
|
188
|
-
{ hookEvent: "afterAgentThought", handler: "tool-after", timeoutSeconds: 5 },
|
|
189
|
-
{ hookEvent: "preCompact", handler: "compact-before", timeoutSeconds: 5 },
|
|
190
|
-
{ hookEvent: "subagentStart", handler: "subagent-start", timeoutSeconds: 5, failClosed: true },
|
|
191
|
-
{ hookEvent: "subagentStop", handler: "subagent-stop", timeoutSeconds: 5 },
|
|
192
|
-
{ hookEvent: "preToolUse", handler: "tool-before", timeoutSeconds: 5, failClosed: true },
|
|
193
|
-
{ hookEvent: "postToolUse", handler: "tool-after", timeoutSeconds: 5 },
|
|
194
|
-
{ hookEvent: "postToolUseFailure", handler: "tool-failure", timeoutSeconds: 5 },
|
|
195
|
-
{ hookEvent: "beforeShellExecution", handler: "tool-before", timeoutSeconds: 10, failClosed: true },
|
|
196
|
-
{ hookEvent: "afterShellExecution", handler: "tool-after", timeoutSeconds: 10 },
|
|
197
|
-
{ hookEvent: "beforeMCPExecution", handler: "tool-before", timeoutSeconds: 10 },
|
|
198
|
-
{ hookEvent: "afterMCPExecution", handler: "tool-after", timeoutSeconds: 5 },
|
|
199
|
-
{ hookEvent: "beforeReadFile", handler: "tool-before", timeoutSeconds: 5 },
|
|
200
|
-
{ hookEvent: "afterFileEdit", handler: "tool-after", timeoutSeconds: 30, matcher: "Write" },
|
|
201
|
-
{ hookEvent: "stop", handler: "stop", timeoutSeconds: 120, loopLimit: 5 },
|
|
202
|
-
{ hookEvent: "afterAgentResponse", handler: "response-after", timeoutSeconds: 5, matcher: "AgentResponse" }
|
|
203
|
-
];
|
|
204
|
-
function cursorWiring(runtime) {
|
|
205
|
-
const command = "node";
|
|
206
|
-
const argsPrefix = [runtime.launcherPath];
|
|
207
|
-
const entries = ENTRY_SPECS2.map((spec) => ({
|
|
208
|
-
hookEvent: spec.hookEvent,
|
|
209
|
-
handler: spec.handler,
|
|
210
|
-
command,
|
|
211
|
-
args: [...argsPrefix, spec.handler],
|
|
212
|
-
timeoutSeconds: spec.timeoutSeconds,
|
|
213
|
-
...spec.failClosed !== undefined ? { failClosed: spec.failClosed } : {},
|
|
214
|
-
...spec.matcher !== undefined ? { matcher: spec.matcher } : {},
|
|
215
|
-
...spec.loopLimit !== undefined ? { loopLimit: spec.loopLimit } : {}
|
|
216
|
-
}));
|
|
217
|
-
return {
|
|
218
|
-
target: join2(cursorConfigDir(), "hooks.json"),
|
|
219
|
-
strategy: "replace",
|
|
220
|
-
entries
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
function commandTokens(command) {
|
|
224
|
-
return [...command.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => match[1] ?? match[2] ?? "");
|
|
225
|
-
}
|
|
226
|
-
function cursorWiringProblems(text, runtime, fileExists) {
|
|
227
|
-
if (text === null) {
|
|
228
|
-
return [{ hookEvent: "(file)", reason: "no hooks file at the expected path" }];
|
|
229
|
-
}
|
|
230
|
-
let parsed;
|
|
231
|
-
try {
|
|
232
|
-
parsed = JSON.parse(text);
|
|
233
|
-
} catch {
|
|
234
|
-
return [{ hookEvent: "(file)", reason: "the hooks file is not valid JSON" }];
|
|
235
|
-
}
|
|
236
|
-
const hooks = parsed !== null && typeof parsed === "object" ? parsed.hooks ?? {} : {};
|
|
237
|
-
const problems = [];
|
|
238
|
-
for (const spec of ENTRY_SPECS2) {
|
|
239
|
-
const list = Array.isArray(hooks[spec.hookEvent]) ? hooks[spec.hookEvent] : [];
|
|
240
|
-
const commands = list.map((row) => row !== null && typeof row === "object" ? String(row.command ?? "") : "").filter((command) => command.includes(runtime.launcherPath));
|
|
241
|
-
if (commands.length === 0) {
|
|
242
|
-
problems.push({ hookEvent: spec.hookEvent, reason: "no harness entry — run: tlc harness update" });
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
for (const command of commands) {
|
|
246
|
-
const tokens = commandTokens(command);
|
|
247
|
-
const scriptAt = tokens.indexOf(runtime.launcherPath);
|
|
248
|
-
if (scriptAt < 1) {
|
|
249
|
-
problems.push({
|
|
250
|
-
hookEvent: spec.hookEvent,
|
|
251
|
-
reason: `no executable before the script: \`${command}\``
|
|
252
|
-
});
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (!fileExists(runtime.launcherPath)) {
|
|
256
|
-
problems.push({
|
|
257
|
-
hookEvent: spec.hookEvent,
|
|
258
|
-
reason: `the script does not exist: ${runtime.launcherPath}`
|
|
259
|
-
});
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
if (tokens[scriptAt + 1] === undefined || tokens[scriptAt + 1] === "") {
|
|
263
|
-
problems.push({
|
|
264
|
-
hookEvent: spec.hookEvent,
|
|
265
|
-
reason: `no handler after the script: \`${command}\``
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return problems;
|
|
271
|
-
}
|
|
272
|
-
function formatWiringProblems(problems, max = 3) {
|
|
273
|
-
const shown = problems.slice(0, max).map((problem) => `${problem.hookEvent}: ${problem.reason}`).join("; ");
|
|
274
|
-
const rest = problems.length - Math.min(problems.length, max);
|
|
275
|
-
return rest > 0 ? `${shown}; and ${rest} more` : shown;
|
|
276
|
-
}
|
|
277
|
-
function unwireCursorHooks(text, marker = "tlc-exec.mjs") {
|
|
278
|
-
if (text === null || text.trim() === "") {
|
|
279
|
-
return { kind: "absent" };
|
|
280
|
-
}
|
|
281
|
-
let parsed;
|
|
282
|
-
try {
|
|
283
|
-
parsed = JSON.parse(text);
|
|
284
|
-
} catch {
|
|
285
|
-
return { kind: "unparsed" };
|
|
286
|
-
}
|
|
287
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
288
|
-
return { kind: "unparsed" };
|
|
289
|
-
}
|
|
290
|
-
const document = parsed;
|
|
291
|
-
const hooks = document.hooks !== null && typeof document.hooks === "object" && !Array.isArray(document.hooks) ? document.hooks : {};
|
|
292
|
-
const remaining = {};
|
|
293
|
-
let removed = 0;
|
|
294
|
-
let kept = 0;
|
|
295
|
-
for (const [hookEvent, value] of Object.entries(hooks)) {
|
|
296
|
-
const list = Array.isArray(value) ? value : [];
|
|
297
|
-
const foreign = list.filter((row) => !JSON.stringify(row ?? null).includes(marker));
|
|
298
|
-
removed += list.length - foreign.length;
|
|
299
|
-
if (foreign.length > 0) {
|
|
300
|
-
remaining[hookEvent] = foreign;
|
|
301
|
-
kept += foreign.length;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
if (kept === 0) {
|
|
305
|
-
return { kind: "empty", removed };
|
|
306
|
-
}
|
|
307
|
-
return {
|
|
308
|
-
kind: "rewritten",
|
|
309
|
-
removed,
|
|
310
|
-
text: `${JSON.stringify({ ...document, hooks: remaining }, null, 2)}
|
|
311
|
-
`
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export { claudeWiring, mergeClaudeSettings, unmergeClaudeSettings, removeClaudeWiring, applyClaudeWiring, cursorWiring, cursorWiringProblems, formatWiringProblems, unwireCursorHooks };
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// src/platform/cli-output.ts
|
|
2
|
-
var JSON_FLAG = "--json";
|
|
3
|
-
function takeJsonFlag(args) {
|
|
4
|
-
const rest = [];
|
|
5
|
-
let json = false;
|
|
6
|
-
for (const arg of args) {
|
|
7
|
-
if (arg === JSON_FLAG) {
|
|
8
|
-
json = true;
|
|
9
|
-
continue;
|
|
10
|
-
}
|
|
11
|
-
rest.push(arg);
|
|
12
|
-
}
|
|
13
|
-
return { json, rest };
|
|
14
|
-
}
|
|
15
|
-
function emitJson(value, write = writeStdout) {
|
|
16
|
-
write(`${JSON.stringify(value)}
|
|
17
|
-
`);
|
|
18
|
-
}
|
|
19
|
-
function writeStdout(text) {
|
|
20
|
-
process.stdout.write(text);
|
|
21
|
-
}
|
|
22
|
-
function unknownFlags(args) {
|
|
23
|
-
return args.filter((arg) => arg.startsWith("--"));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export { JSON_FLAG, takeJsonFlag, emitJson, unknownFlags };
|