@kennykeni/agent-trace 0.1.0
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 +124 -0
- package/package.json +37 -0
- package/src/cli.ts +119 -0
- package/src/core/schemas.ts +138 -0
- package/src/core/trace-hook.ts +230 -0
- package/src/core/trace-store.ts +170 -0
- package/src/core/types.ts +93 -0
- package/src/core/utils.ts +85 -0
- package/src/extensions/diffs.ts +89 -0
- package/src/extensions/helpers.ts +19 -0
- package/src/extensions/index.ts +16 -0
- package/src/extensions/line-hashes.ts +67 -0
- package/src/extensions/messages.ts +48 -0
- package/src/extensions/raw-events.ts +30 -0
- package/src/install/args.ts +71 -0
- package/src/install/claude.ts +78 -0
- package/src/install/cursor.ts +50 -0
- package/src/install/index.ts +49 -0
- package/src/install/opencode.ts +18 -0
- package/src/install/templates/opencode-plugin.ts +207 -0
- package/src/install/types.ts +14 -0
- package/src/install/utils.ts +78 -0
- package/src/providers/claude.ts +130 -0
- package/src/providers/cursor.ts +127 -0
- package/src/providers/index.ts +19 -0
- package/src/providers/opencode.ts +239 -0
- package/src/providers/types.ts +6 -0
- package/src/providers/utils.ts +27 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { ChangeSummary } from "./types";
|
|
3
|
+
import { hookCommand, readJson, writeJson } from "./utils";
|
|
4
|
+
|
|
5
|
+
function ensureClaudeCommandGroup(
|
|
6
|
+
hooks: Record<string, unknown>,
|
|
7
|
+
event: string,
|
|
8
|
+
command: string,
|
|
9
|
+
matcher?: string,
|
|
10
|
+
): void {
|
|
11
|
+
const groups = Array.isArray(hooks[event])
|
|
12
|
+
? [...(hooks[event] as unknown[])]
|
|
13
|
+
: [];
|
|
14
|
+
let target: Record<string, unknown> | undefined;
|
|
15
|
+
|
|
16
|
+
for (const group of groups) {
|
|
17
|
+
if (!group || typeof group !== "object") continue;
|
|
18
|
+
const candidate = group as Record<string, unknown>;
|
|
19
|
+
const candidateMatcher =
|
|
20
|
+
typeof candidate.matcher === "string" ? candidate.matcher : undefined;
|
|
21
|
+
if ((matcher ?? undefined) === candidateMatcher) {
|
|
22
|
+
target = candidate;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!target) {
|
|
28
|
+
target = {};
|
|
29
|
+
if (matcher) target.matcher = matcher;
|
|
30
|
+
target.hooks = [];
|
|
31
|
+
groups.push(target);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const inner = Array.isArray(target.hooks)
|
|
35
|
+
? [...(target.hooks as unknown[])]
|
|
36
|
+
: [];
|
|
37
|
+
const filtered = inner.filter(
|
|
38
|
+
(entry) =>
|
|
39
|
+
!(
|
|
40
|
+
entry &&
|
|
41
|
+
typeof entry === "object" &&
|
|
42
|
+
(entry as Record<string, unknown>).type === "command" &&
|
|
43
|
+
typeof (entry as Record<string, unknown>).command === "string" &&
|
|
44
|
+
((entry as Record<string, unknown>).command as string).includes(
|
|
45
|
+
"agent-trace",
|
|
46
|
+
)
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
filtered.push({ type: "command", command });
|
|
50
|
+
target.hooks = filtered;
|
|
51
|
+
hooks[event] = groups;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function installClaude(
|
|
55
|
+
targetRoot: string,
|
|
56
|
+
dryRun: boolean,
|
|
57
|
+
pinVersion = true,
|
|
58
|
+
): ChangeSummary {
|
|
59
|
+
const path = join(targetRoot, ".claude", "settings.json");
|
|
60
|
+
const command = hookCommand("claude", { pinVersion });
|
|
61
|
+
|
|
62
|
+
const config = readJson(path);
|
|
63
|
+
const hooks = (config.hooks ?? {}) as Record<string, unknown>;
|
|
64
|
+
|
|
65
|
+
ensureClaudeCommandGroup(hooks, "SessionStart", command);
|
|
66
|
+
ensureClaudeCommandGroup(hooks, "SessionEnd", command);
|
|
67
|
+
ensureClaudeCommandGroup(hooks, "UserPromptSubmit", command);
|
|
68
|
+
ensureClaudeCommandGroup(hooks, "PostToolUse", command, "Write|Edit");
|
|
69
|
+
ensureClaudeCommandGroup(hooks, "PostToolUse", command, "Bash");
|
|
70
|
+
ensureClaudeCommandGroup(
|
|
71
|
+
hooks,
|
|
72
|
+
"PostToolUseFailure",
|
|
73
|
+
command,
|
|
74
|
+
"Write|Edit|Bash",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return writeJson(path, { ...config, hooks }, dryRun);
|
|
78
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { ChangeSummary } from "./types";
|
|
3
|
+
import { hookCommand, readJson, writeJson } from "./utils";
|
|
4
|
+
|
|
5
|
+
export function installCursor(
|
|
6
|
+
targetRoot: string,
|
|
7
|
+
dryRun: boolean,
|
|
8
|
+
pinVersion = true,
|
|
9
|
+
): ChangeSummary {
|
|
10
|
+
const path = join(targetRoot, ".cursor", "hooks.json");
|
|
11
|
+
const command = hookCommand("cursor", { pinVersion });
|
|
12
|
+
|
|
13
|
+
const config = readJson(path);
|
|
14
|
+
const hooks = (config.hooks ?? {}) as Record<string, unknown>;
|
|
15
|
+
const events = [
|
|
16
|
+
"sessionStart",
|
|
17
|
+
"sessionEnd",
|
|
18
|
+
"beforeSubmitPrompt",
|
|
19
|
+
"afterFileEdit",
|
|
20
|
+
"afterTabFileEdit",
|
|
21
|
+
"afterShellExecution",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
for (const event of events) {
|
|
25
|
+
const entries = Array.isArray(hooks[event])
|
|
26
|
+
? [...(hooks[event] as unknown[])]
|
|
27
|
+
: [];
|
|
28
|
+
const filtered = entries.filter(
|
|
29
|
+
(entry) =>
|
|
30
|
+
!(
|
|
31
|
+
entry &&
|
|
32
|
+
typeof entry === "object" &&
|
|
33
|
+
typeof (entry as Record<string, unknown>).command === "string" &&
|
|
34
|
+
((entry as Record<string, unknown>).command as string).includes(
|
|
35
|
+
"agent-trace",
|
|
36
|
+
)
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
filtered.push({ command });
|
|
40
|
+
hooks[event] = filtered;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const next = {
|
|
44
|
+
...config,
|
|
45
|
+
version: Number.isInteger(config.version) ? config.version : 1,
|
|
46
|
+
hooks,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return writeJson(path, next, dryRun);
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { installClaude } from "./claude";
|
|
2
|
+
import { installCursor } from "./cursor";
|
|
3
|
+
import { installOpenCode } from "./opencode";
|
|
4
|
+
import type { ChangeSummary, InstallOptions } from "./types";
|
|
5
|
+
|
|
6
|
+
export { InstallError, parseArgs } from "./args";
|
|
7
|
+
export type { ChangeSummary, InstallOptions } from "./types";
|
|
8
|
+
|
|
9
|
+
export function install(options: InstallOptions): ChangeSummary[] {
|
|
10
|
+
const changes: ChangeSummary[] = [];
|
|
11
|
+
|
|
12
|
+
for (const targetRoot of options.targetRoots) {
|
|
13
|
+
if (options.providers.includes("cursor")) {
|
|
14
|
+
changes.push(
|
|
15
|
+
installCursor(targetRoot, options.dryRun, options.pinVersion),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (options.providers.includes("claude")) {
|
|
19
|
+
changes.push(
|
|
20
|
+
installClaude(targetRoot, options.dryRun, options.pinVersion),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (options.providers.includes("opencode")) {
|
|
24
|
+
changes.push(
|
|
25
|
+
installOpenCode(targetRoot, options.dryRun, options.pinVersion),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return changes;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function printInstallSummary(
|
|
34
|
+
changes: ChangeSummary[],
|
|
35
|
+
targetRoots: string[],
|
|
36
|
+
): void {
|
|
37
|
+
const summary = changes
|
|
38
|
+
.map((change) =>
|
|
39
|
+
change.note
|
|
40
|
+
? `${change.status.toUpperCase()}: ${change.file} (${change.note})`
|
|
41
|
+
: `${change.status.toUpperCase()}: ${change.file}`,
|
|
42
|
+
)
|
|
43
|
+
.join("\n");
|
|
44
|
+
|
|
45
|
+
console.log(summary);
|
|
46
|
+
console.log("\nTargets:");
|
|
47
|
+
for (const target of targetRoots) console.log(` ${target}`);
|
|
48
|
+
console.log("\nTrace output: .agent-trace/");
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ChangeSummary } from "./types";
|
|
4
|
+
import { getPackageName, getPackageVersion, writeTextFile } from "./utils";
|
|
5
|
+
|
|
6
|
+
export function installOpenCode(
|
|
7
|
+
targetRoot: string,
|
|
8
|
+
dryRun: boolean,
|
|
9
|
+
pinVersion = true,
|
|
10
|
+
): ChangeSummary {
|
|
11
|
+
const templatePath = join(import.meta.dir, "templates", "opencode-plugin.ts");
|
|
12
|
+
const raw = readFileSync(templatePath, "utf-8");
|
|
13
|
+
const name = getPackageName();
|
|
14
|
+
const pkg = pinVersion ? `${name}@${getPackageVersion()}` : name;
|
|
15
|
+
const content = raw.replace("__AGENT_TRACE_PKG__", pkg);
|
|
16
|
+
const path = join(targetRoot, ".opencode", "plugins", "agent-trace.ts");
|
|
17
|
+
return writeTextFile(path, content, dryRun);
|
|
18
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
async function emitToAgentTrace(root: string, payload: unknown): Promise<void> {
|
|
4
|
+
await new Promise<void>((resolve) => {
|
|
5
|
+
const child = spawn(
|
|
6
|
+
"bunx",
|
|
7
|
+
["__AGENT_TRACE_PKG__", "hook", "--provider", "opencode"],
|
|
8
|
+
{
|
|
9
|
+
cwd: root,
|
|
10
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
child.on("error", () => resolve());
|
|
14
|
+
child.on("exit", () => resolve());
|
|
15
|
+
child.stdin.end(JSON.stringify(payload));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const AgentTracePlugin = async ({
|
|
20
|
+
worktree,
|
|
21
|
+
directory,
|
|
22
|
+
}: {
|
|
23
|
+
worktree?: string;
|
|
24
|
+
directory?: string;
|
|
25
|
+
}) => {
|
|
26
|
+
const root = worktree || directory || process.cwd();
|
|
27
|
+
const pendingCommands = new Map<string, string>();
|
|
28
|
+
return {
|
|
29
|
+
event: async ({
|
|
30
|
+
event,
|
|
31
|
+
}: {
|
|
32
|
+
event: { type: string; properties: Record<string, unknown> };
|
|
33
|
+
}) => {
|
|
34
|
+
const props = event.properties ?? {};
|
|
35
|
+
const nested = (key: string) => {
|
|
36
|
+
const v = props[key];
|
|
37
|
+
return typeof v === "object" && v !== null
|
|
38
|
+
? (v as Record<string, unknown>)
|
|
39
|
+
: undefined;
|
|
40
|
+
};
|
|
41
|
+
const info = nested("info");
|
|
42
|
+
const part = nested("part");
|
|
43
|
+
|
|
44
|
+
const str = (v: unknown): string | undefined =>
|
|
45
|
+
typeof v === "string" && v ? v : undefined;
|
|
46
|
+
|
|
47
|
+
const session =
|
|
48
|
+
str(props.sessionID) ??
|
|
49
|
+
str(info?.sessionID) ??
|
|
50
|
+
str(info?.id) ??
|
|
51
|
+
str(part?.sessionID);
|
|
52
|
+
|
|
53
|
+
const filterDiffs = (arr: unknown[]) =>
|
|
54
|
+
arr.filter((d: unknown) => {
|
|
55
|
+
if (!d || typeof d !== "object") return true;
|
|
56
|
+
const file = (d as Record<string, unknown>).file;
|
|
57
|
+
return typeof file !== "string" || !file.startsWith(".agent-trace/");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let filtered = props;
|
|
61
|
+
if (Array.isArray(props.diff)) {
|
|
62
|
+
filtered = { ...filtered, diff: filterDiffs(props.diff) };
|
|
63
|
+
}
|
|
64
|
+
if (info?.summary && typeof info.summary === "object") {
|
|
65
|
+
const summary = info.summary as Record<string, unknown>;
|
|
66
|
+
if (Array.isArray(summary.diffs)) {
|
|
67
|
+
filtered = {
|
|
68
|
+
...filtered,
|
|
69
|
+
info: {
|
|
70
|
+
...info,
|
|
71
|
+
summary: { ...summary, diffs: filterDiffs(summary.diffs) },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await emitToAgentTrace(root, {
|
|
78
|
+
hook_event_name: event.type ?? "event",
|
|
79
|
+
provider: "opencode",
|
|
80
|
+
session_id: session,
|
|
81
|
+
cwd: root,
|
|
82
|
+
event: filtered,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
"chat.message": async (
|
|
87
|
+
input: {
|
|
88
|
+
sessionID: string;
|
|
89
|
+
agent?: string;
|
|
90
|
+
model?: { providerID: string; modelID: string };
|
|
91
|
+
messageID?: string;
|
|
92
|
+
},
|
|
93
|
+
output: {
|
|
94
|
+
parts?: Array<{ type?: string; text?: string; synthetic?: boolean }>;
|
|
95
|
+
},
|
|
96
|
+
) => {
|
|
97
|
+
const parts = output?.parts ?? [];
|
|
98
|
+
const content = parts
|
|
99
|
+
.filter(
|
|
100
|
+
(p) =>
|
|
101
|
+
!p.synthetic && p.type === "text" && typeof p.text === "string",
|
|
102
|
+
)
|
|
103
|
+
.map((p) => p.text as string)
|
|
104
|
+
.join("\n");
|
|
105
|
+
if (!content) return;
|
|
106
|
+
|
|
107
|
+
const model =
|
|
108
|
+
input.model?.providerID && input.model?.modelID
|
|
109
|
+
? `${input.model.providerID}/${input.model.modelID}`
|
|
110
|
+
: input.model?.modelID;
|
|
111
|
+
|
|
112
|
+
await emitToAgentTrace(root, {
|
|
113
|
+
hook_event_name: "hook:chat.message",
|
|
114
|
+
provider: "opencode",
|
|
115
|
+
session_id: input.sessionID,
|
|
116
|
+
cwd: root,
|
|
117
|
+
content,
|
|
118
|
+
role: "user",
|
|
119
|
+
model,
|
|
120
|
+
message_id: input.messageID,
|
|
121
|
+
agent: input.agent,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
"tool.execute.before": async (
|
|
126
|
+
input: { tool: string; sessionID: string; callID: string },
|
|
127
|
+
output: { args: Record<string, unknown> },
|
|
128
|
+
) => {
|
|
129
|
+
const shellTools = ["bash", "shell"];
|
|
130
|
+
if (
|
|
131
|
+
shellTools.includes(input.tool) &&
|
|
132
|
+
typeof output?.args?.command === "string"
|
|
133
|
+
) {
|
|
134
|
+
pendingCommands.set(input.callID, output.args.command);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
"tool.execute.after": async (
|
|
139
|
+
input: {
|
|
140
|
+
tool: string;
|
|
141
|
+
sessionID: string;
|
|
142
|
+
callID: string;
|
|
143
|
+
},
|
|
144
|
+
output: {
|
|
145
|
+
title?: string;
|
|
146
|
+
output?: string;
|
|
147
|
+
metadata?: {
|
|
148
|
+
files?: Array<{
|
|
149
|
+
filePath?: string;
|
|
150
|
+
before?: string;
|
|
151
|
+
after?: string;
|
|
152
|
+
additions?: number;
|
|
153
|
+
deletions?: number;
|
|
154
|
+
}>;
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
) => {
|
|
158
|
+
const toolName = input?.tool ?? "";
|
|
159
|
+
const shellTools = ["bash", "shell"];
|
|
160
|
+
|
|
161
|
+
if (shellTools.includes(toolName)) {
|
|
162
|
+
const command = pendingCommands.get(input.callID);
|
|
163
|
+
pendingCommands.delete(input.callID);
|
|
164
|
+
await emitToAgentTrace(root, {
|
|
165
|
+
hook_event_name: "hook:tool.execute.after",
|
|
166
|
+
provider: "opencode",
|
|
167
|
+
session_id: input.sessionID,
|
|
168
|
+
cwd: root,
|
|
169
|
+
tool_name: toolName,
|
|
170
|
+
command,
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const editTools = ["edit", "apply_patch", "write", "patch"];
|
|
176
|
+
if (!editTools.includes(toolName)) return;
|
|
177
|
+
|
|
178
|
+
const metadata = output?.metadata;
|
|
179
|
+
const files: Array<{ file: string; before?: string; after?: string }> =
|
|
180
|
+
[];
|
|
181
|
+
|
|
182
|
+
if (metadata?.files) {
|
|
183
|
+
for (const f of metadata.files) {
|
|
184
|
+
const fp = f.filePath ?? "";
|
|
185
|
+
if (!fp || fp.startsWith(".agent-trace/")) continue;
|
|
186
|
+
files.push({
|
|
187
|
+
file: fp,
|
|
188
|
+
before: f.before,
|
|
189
|
+
after: f.after,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (files.length === 0) return;
|
|
195
|
+
|
|
196
|
+
await emitToAgentTrace(root, {
|
|
197
|
+
hook_event_name: "hook:tool.execute.after",
|
|
198
|
+
provider: "opencode",
|
|
199
|
+
session_id: input.sessionID,
|
|
200
|
+
cwd: root,
|
|
201
|
+
tool_name: toolName,
|
|
202
|
+
call_id: input.callID,
|
|
203
|
+
files,
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Provider } from "../providers/types";
|
|
2
|
+
|
|
3
|
+
export interface ChangeSummary {
|
|
4
|
+
file: string;
|
|
5
|
+
status: "created" | "updated" | "unchanged" | "skipped";
|
|
6
|
+
note?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface InstallOptions {
|
|
10
|
+
providers: Provider[];
|
|
11
|
+
dryRun: boolean;
|
|
12
|
+
pinVersion: boolean;
|
|
13
|
+
targetRoots: string[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { ensureDir } from "../core/utils";
|
|
5
|
+
import type { ChangeSummary } from "./types";
|
|
6
|
+
|
|
7
|
+
export function readJson(path: string): Record<string, unknown> {
|
|
8
|
+
if (!existsSync(path)) return {};
|
|
9
|
+
const raw = readFileSync(path, "utf-8");
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error(`Failed to parse JSON at ${path}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writeJson(
|
|
18
|
+
path: string,
|
|
19
|
+
value: unknown,
|
|
20
|
+
dryRun: boolean,
|
|
21
|
+
): ChangeSummary {
|
|
22
|
+
const next = `${JSON.stringify(value, null, 2)}\n`;
|
|
23
|
+
return writeTextFile(path, next, dryRun);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeTextFile(
|
|
27
|
+
path: string,
|
|
28
|
+
content: string,
|
|
29
|
+
dryRun: boolean,
|
|
30
|
+
): ChangeSummary {
|
|
31
|
+
let previous = "";
|
|
32
|
+
if (existsSync(path)) previous = readFileSync(path, "utf-8");
|
|
33
|
+
const status: ChangeSummary["status"] = existsSync(path)
|
|
34
|
+
? previous === content
|
|
35
|
+
? "unchanged"
|
|
36
|
+
: "updated"
|
|
37
|
+
: "created";
|
|
38
|
+
if (!dryRun && status !== "unchanged") {
|
|
39
|
+
ensureDir(dirname(path));
|
|
40
|
+
writeFileSync(path, content, "utf-8");
|
|
41
|
+
}
|
|
42
|
+
return { file: path, status };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function findPackageRoot(startDir: string): string {
|
|
46
|
+
let dir = startDir;
|
|
47
|
+
while (true) {
|
|
48
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
49
|
+
const parent = dirname(dir);
|
|
50
|
+
if (parent === dir) break;
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
return startDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readPkg(): { name: string; version: string } {
|
|
57
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
const root = findPackageRoot(dir);
|
|
59
|
+
return JSON.parse(readFileSync(join(root, "package.json"), "utf-8"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getPackageName(): string {
|
|
63
|
+
return readPkg().name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getPackageVersion(): string {
|
|
67
|
+
return readPkg().version;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function hookCommand(
|
|
71
|
+
provider: string,
|
|
72
|
+
options?: { pinVersion?: boolean },
|
|
73
|
+
): string {
|
|
74
|
+
const pkg = getPackageName();
|
|
75
|
+
const pinVersion = options?.pinVersion ?? true;
|
|
76
|
+
const target = pinVersion ? `${pkg}@${getPackageVersion()}` : pkg;
|
|
77
|
+
return `bunx ${target} hook --provider ${provider}`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { HookInput, TraceEvent } from "../core/types";
|
|
2
|
+
import { textFromUnknown } from "../core/utils";
|
|
3
|
+
import { normalizeModelId, sessionIdFor } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface ClaudeHookInput extends HookInput {
|
|
6
|
+
tool_name?: string;
|
|
7
|
+
tool_input?: {
|
|
8
|
+
file_path?: string;
|
|
9
|
+
new_string?: string;
|
|
10
|
+
old_string?: string;
|
|
11
|
+
content?: string;
|
|
12
|
+
command?: string;
|
|
13
|
+
};
|
|
14
|
+
tool_response?: {
|
|
15
|
+
originalFile?: string;
|
|
16
|
+
};
|
|
17
|
+
tool_use_id?: string;
|
|
18
|
+
source?: string;
|
|
19
|
+
reason?: string;
|
|
20
|
+
prompt?: unknown;
|
|
21
|
+
message?: unknown;
|
|
22
|
+
content?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { sessionIdFor } from "./utils";
|
|
26
|
+
|
|
27
|
+
export function adapt(input: HookInput): TraceEvent | TraceEvent[] | undefined {
|
|
28
|
+
const ci = input as ClaudeHookInput;
|
|
29
|
+
const sessionId = sessionIdFor(input);
|
|
30
|
+
const model = normalizeModelId(input.model);
|
|
31
|
+
|
|
32
|
+
switch (input.hook_event_name) {
|
|
33
|
+
case "PostToolUse": {
|
|
34
|
+
const toolName = ci.tool_name ?? "";
|
|
35
|
+
const isFileEdit = toolName === "Write" || toolName === "Edit";
|
|
36
|
+
const isBash = toolName === "Bash";
|
|
37
|
+
if (!isFileEdit && !isBash) return undefined;
|
|
38
|
+
|
|
39
|
+
if (isBash) {
|
|
40
|
+
return {
|
|
41
|
+
kind: "shell",
|
|
42
|
+
provider: "claude",
|
|
43
|
+
sessionId,
|
|
44
|
+
model,
|
|
45
|
+
transcript: input.transcript_path,
|
|
46
|
+
meta: {
|
|
47
|
+
session_id: input.session_id,
|
|
48
|
+
tool_name: toolName,
|
|
49
|
+
tool_use_id: ci.tool_use_id,
|
|
50
|
+
command: ci.tool_input?.command,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const file = ci.tool_input?.file_path ?? ".unknown";
|
|
56
|
+
const newContent = ci.tool_input?.new_string ?? ci.tool_input?.content;
|
|
57
|
+
const edits = newContent
|
|
58
|
+
? [
|
|
59
|
+
{
|
|
60
|
+
old_string:
|
|
61
|
+
ci.tool_input?.old_string ??
|
|
62
|
+
ci.tool_response?.originalFile ??
|
|
63
|
+
"",
|
|
64
|
+
new_string: newContent,
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
: [];
|
|
68
|
+
return {
|
|
69
|
+
kind: "file_edit",
|
|
70
|
+
provider: "claude",
|
|
71
|
+
sessionId,
|
|
72
|
+
filePath: file,
|
|
73
|
+
edits,
|
|
74
|
+
model,
|
|
75
|
+
readContent: !!ci.tool_input?.file_path,
|
|
76
|
+
transcript: input.transcript_path,
|
|
77
|
+
eventName: "PostToolUse",
|
|
78
|
+
meta: {
|
|
79
|
+
session_id: input.session_id,
|
|
80
|
+
tool_name: toolName,
|
|
81
|
+
tool_use_id: ci.tool_use_id,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "UserPromptSubmit": {
|
|
87
|
+
const text = textFromUnknown(ci.prompt ?? ci.message ?? ci.content);
|
|
88
|
+
if (!text) return undefined;
|
|
89
|
+
return {
|
|
90
|
+
kind: "message",
|
|
91
|
+
provider: "claude",
|
|
92
|
+
sessionId,
|
|
93
|
+
role: "user",
|
|
94
|
+
content: text,
|
|
95
|
+
eventName: "UserPromptSubmit",
|
|
96
|
+
model,
|
|
97
|
+
meta: {},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case "SessionStart": {
|
|
102
|
+
return {
|
|
103
|
+
kind: "session_start",
|
|
104
|
+
provider: "claude",
|
|
105
|
+
sessionId,
|
|
106
|
+
model,
|
|
107
|
+
meta: {
|
|
108
|
+
session_id: input.session_id,
|
|
109
|
+
source: ci.source,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case "SessionEnd": {
|
|
115
|
+
return {
|
|
116
|
+
kind: "session_end",
|
|
117
|
+
provider: "claude",
|
|
118
|
+
sessionId,
|
|
119
|
+
model,
|
|
120
|
+
meta: {
|
|
121
|
+
session_id: input.session_id,
|
|
122
|
+
reason: ci.reason,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
default:
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|