@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.
@@ -0,0 +1,127 @@
1
+ import type { FileEdit, HookInput, TraceEvent } from "../core/types";
2
+ import { textFromUnknown } from "../core/utils";
3
+ import { normalizeModelId, sessionIdFor } from "./utils";
4
+
5
+ export interface CursorHookInput extends HookInput {
6
+ file_path?: string;
7
+ edits?: FileEdit[];
8
+ command?: string;
9
+ duration?: number;
10
+ duration_ms?: number;
11
+ is_background_agent?: boolean;
12
+ composer_mode?: string;
13
+ reason?: string;
14
+ prompt?: unknown;
15
+ message?: unknown;
16
+ content?: unknown;
17
+ }
18
+
19
+ export { sessionIdFor } from "./utils";
20
+
21
+ export function adapt(input: HookInput): TraceEvent | TraceEvent[] | undefined {
22
+ const ci = input as CursorHookInput;
23
+ const sessionId = sessionIdFor(input);
24
+ const model = normalizeModelId(input.model);
25
+
26
+ switch (input.hook_event_name) {
27
+ case "afterFileEdit": {
28
+ if (!ci.file_path) return undefined;
29
+ return {
30
+ kind: "file_edit",
31
+ provider: "cursor",
32
+ sessionId,
33
+ filePath: ci.file_path,
34
+ edits: ci.edits ?? [],
35
+ model,
36
+ transcript: input.transcript_path,
37
+ readContent: true,
38
+ eventName: "afterFileEdit",
39
+ meta: {
40
+ conversation_id: input.conversation_id,
41
+ generation_id: input.generation_id,
42
+ },
43
+ };
44
+ }
45
+
46
+ case "afterTabFileEdit": {
47
+ if (!ci.file_path) return undefined;
48
+ return {
49
+ kind: "file_edit",
50
+ provider: "cursor",
51
+ sessionId,
52
+ filePath: ci.file_path,
53
+ edits: ci.edits ?? [],
54
+ model,
55
+ eventName: "afterTabFileEdit",
56
+ meta: {
57
+ conversation_id: input.conversation_id,
58
+ generation_id: input.generation_id,
59
+ },
60
+ };
61
+ }
62
+
63
+ case "afterShellExecution": {
64
+ return {
65
+ kind: "shell",
66
+ provider: "cursor",
67
+ sessionId,
68
+ model,
69
+ transcript: input.transcript_path,
70
+ meta: {
71
+ conversation_id: input.conversation_id,
72
+ generation_id: input.generation_id,
73
+ command: ci.command,
74
+ duration_ms: ci.duration_ms ?? ci.duration,
75
+ },
76
+ };
77
+ }
78
+
79
+ case "sessionStart": {
80
+ return {
81
+ kind: "session_start",
82
+ provider: "cursor",
83
+ sessionId,
84
+ model,
85
+ meta: {
86
+ session_id: input.session_id,
87
+ conversation_id: input.conversation_id,
88
+ is_background_agent: ci.is_background_agent,
89
+ composer_mode: ci.composer_mode,
90
+ },
91
+ };
92
+ }
93
+
94
+ case "sessionEnd": {
95
+ return {
96
+ kind: "session_end",
97
+ provider: "cursor",
98
+ sessionId,
99
+ model,
100
+ meta: {
101
+ session_id: input.session_id,
102
+ conversation_id: input.conversation_id,
103
+ reason: ci.reason,
104
+ duration_ms: ci.duration_ms,
105
+ },
106
+ };
107
+ }
108
+
109
+ case "beforeSubmitPrompt": {
110
+ const text = textFromUnknown(ci.prompt ?? ci.message ?? ci.content);
111
+ if (!text) return undefined;
112
+ return {
113
+ kind: "message",
114
+ provider: "cursor",
115
+ sessionId,
116
+ role: "user",
117
+ content: text,
118
+ eventName: "beforeSubmitPrompt",
119
+ model,
120
+ meta: {},
121
+ };
122
+ }
123
+
124
+ default:
125
+ return undefined;
126
+ }
127
+ }
@@ -0,0 +1,19 @@
1
+ import { registerProvider } from "../core/trace-hook";
2
+ import * as claude from "./claude";
3
+ import * as cursor from "./cursor";
4
+ import * as opencode from "./opencode";
5
+
6
+ registerProvider("claude", {
7
+ ...claude,
8
+ toolInfo: () => ({ name: "claude-code" }),
9
+ });
10
+
11
+ registerProvider("cursor", {
12
+ ...cursor,
13
+ toolInfo: () => ({ name: "cursor", version: process.env.CURSOR_VERSION }),
14
+ });
15
+
16
+ registerProvider("opencode", {
17
+ ...opencode,
18
+ toolInfo: () => ({ name: "opencode" }),
19
+ });
@@ -0,0 +1,239 @@
1
+ import type { FileEdit, HookInput, TraceEvent } from "../core/types";
2
+ import { maybeString, safeRecord, textFromUnknown } from "../core/utils";
3
+ import { normalizeModelId } from "./utils";
4
+
5
+ export interface OpenCodeHookInput extends HookInput {
6
+ event?: unknown;
7
+ file_path?: string;
8
+ content?: unknown;
9
+ tool_name?: string;
10
+ command?: string;
11
+ message_id?: string;
12
+ agent?: string;
13
+ call_id?: string;
14
+ files?: Array<{
15
+ file: string;
16
+ before?: string;
17
+ after?: string;
18
+ additions?: number;
19
+ deletions?: number;
20
+ }>;
21
+ }
22
+
23
+ function extractEventField(
24
+ event: Record<string, unknown>,
25
+ keys: string[],
26
+ ): unknown {
27
+ for (const key of keys) {
28
+ if (Object.hasOwn(event, key)) return event[key];
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ export function sessionIdFor(input: HookInput): string | undefined {
34
+ const oi = input as OpenCodeHookInput;
35
+ const event = safeRecord(oi.event);
36
+ const info = safeRecord(event?.info);
37
+ const part = safeRecord(event?.part);
38
+ return (
39
+ maybeString(input.session_id) ??
40
+ maybeString(event?.sessionID) ??
41
+ maybeString(info?.sessionID) ??
42
+ maybeString(info?.id) ??
43
+ maybeString(part?.sessionID)
44
+ );
45
+ }
46
+
47
+ export function adapt(input: HookInput): TraceEvent | TraceEvent[] | undefined {
48
+ const oi = input as OpenCodeHookInput;
49
+ const sessionId = sessionIdFor(input);
50
+ const model = normalizeModelId(input.model);
51
+
52
+ switch (input.hook_event_name) {
53
+ case "session.created": {
54
+ return {
55
+ kind: "session_start",
56
+ provider: "opencode",
57
+ sessionId,
58
+ model,
59
+ meta: {
60
+ session_id: sessionId,
61
+ source: "opencode",
62
+ },
63
+ };
64
+ }
65
+
66
+ case "session.deleted": {
67
+ return {
68
+ kind: "session_end",
69
+ provider: "opencode",
70
+ sessionId,
71
+ model,
72
+ meta: {
73
+ session_id: sessionId,
74
+ source: "opencode",
75
+ reason: "session.deleted",
76
+ },
77
+ };
78
+ }
79
+
80
+ case "session.idle": {
81
+ return {
82
+ kind: "session_end",
83
+ provider: "opencode",
84
+ sessionId,
85
+ model,
86
+ meta: {
87
+ session_id: sessionId,
88
+ source: "opencode",
89
+ reason: "session.idle",
90
+ },
91
+ };
92
+ }
93
+
94
+ case "message.updated": {
95
+ const event = safeRecord(oi.event);
96
+ if (!event) return undefined;
97
+ const info = safeRecord(event.info);
98
+ if (!info) return undefined;
99
+ const roleRaw = maybeString(info.role);
100
+ const role =
101
+ roleRaw === "assistant" || roleRaw === "system" ? roleRaw : "user";
102
+ const content =
103
+ textFromUnknown(info.content) ??
104
+ textFromUnknown(info.text) ??
105
+ textFromUnknown(info.parts);
106
+ if (!content) return undefined;
107
+ return {
108
+ kind: "message",
109
+ provider: "opencode",
110
+ sessionId,
111
+ role,
112
+ content,
113
+ eventName: "message.updated",
114
+ model: normalizeModelId(maybeString(info.modelID)) ?? model,
115
+ meta: { source: "opencode.event" },
116
+ };
117
+ }
118
+
119
+ case "command.executed": {
120
+ const event = safeRecord(oi.event);
121
+ if (!event) return undefined;
122
+ return {
123
+ kind: "shell",
124
+ provider: "opencode",
125
+ sessionId,
126
+ model,
127
+ transcript: input.transcript_path,
128
+ meta: {
129
+ event: "command.executed",
130
+ session_id: sessionId,
131
+ source: "opencode",
132
+ command: extractEventField(event, ["name", "command", "cmd"]),
133
+ arguments: extractEventField(event, ["arguments"]),
134
+ messageID: extractEventField(event, ["messageID"]),
135
+ },
136
+ };
137
+ }
138
+
139
+ case "file.edited": {
140
+ const event = safeRecord(oi.event);
141
+ if (!event) return undefined;
142
+ const path =
143
+ maybeString(
144
+ extractEventField(event, ["file", "file_path", "filePath", "path"]),
145
+ ) ?? maybeString(oi.file_path);
146
+ if (!path) return undefined;
147
+ return {
148
+ kind: "file_edit",
149
+ provider: "opencode",
150
+ sessionId,
151
+ filePath: path,
152
+ edits: [],
153
+ model,
154
+ diffs: false,
155
+ eventName: "file.edited",
156
+ meta: {
157
+ event: "file.edited",
158
+ session_id: sessionId,
159
+ source: "opencode",
160
+ },
161
+ };
162
+ }
163
+
164
+ case "hook:chat.message": {
165
+ const content = typeof oi.content === "string" ? oi.content : undefined;
166
+ if (!content) return undefined;
167
+ return {
168
+ kind: "message",
169
+ provider: "opencode",
170
+ sessionId,
171
+ role: "user",
172
+ content,
173
+ eventName: "hook:chat.message",
174
+ model,
175
+ meta: {
176
+ source: "opencode.hook",
177
+ message_id: oi.message_id,
178
+ agent: oi.agent,
179
+ },
180
+ };
181
+ }
182
+
183
+ case "hook:tool.execute.after": {
184
+ const toolName = oi.tool_name ?? "";
185
+ const shellTools = ["bash", "shell"];
186
+
187
+ if (shellTools.includes(toolName)) {
188
+ return {
189
+ kind: "shell",
190
+ provider: "opencode",
191
+ sessionId,
192
+ model,
193
+ meta: {
194
+ event: "hook:tool.execute.after",
195
+ session_id: sessionId,
196
+ source: "opencode.hook",
197
+ tool_name: toolName,
198
+ command: oi.command,
199
+ },
200
+ };
201
+ }
202
+
203
+ const files = oi.files;
204
+ if (!files || files.length === 0) return undefined;
205
+
206
+ const events: TraceEvent[] = [];
207
+ for (const f of files) {
208
+ const edits: FileEdit[] = [
209
+ {
210
+ old_string: f.before ?? "",
211
+ new_string: f.after ?? "",
212
+ },
213
+ ];
214
+ events.push({
215
+ kind: "file_edit",
216
+ provider: "opencode",
217
+ sessionId,
218
+ filePath: f.file,
219
+ edits,
220
+ diffs: true,
221
+ eventName: "hook:tool.execute.after",
222
+ model,
223
+ meta: {
224
+ event: "hook:tool.execute.after",
225
+ session_id: sessionId,
226
+ source: "opencode.hook",
227
+ tool_name: toolName,
228
+ call_id: oi.call_id,
229
+ },
230
+ });
231
+ }
232
+
233
+ return events.length === 1 ? events[0] : events;
234
+ }
235
+
236
+ default:
237
+ return undefined;
238
+ }
239
+ }
@@ -0,0 +1,6 @@
1
+ export const PROVIDERS = ["cursor", "claude", "opencode"] as const;
2
+ export type Provider = (typeof PROVIDERS)[number];
3
+
4
+ export function isProvider(value: string): value is Provider {
5
+ return PROVIDERS.includes(value as Provider);
6
+ }
@@ -0,0 +1,27 @@
1
+ import type { HookInput } from "../core/types";
2
+ import { maybeString } from "../core/utils";
3
+
4
+ export function sessionIdFor(input: HookInput): string | undefined {
5
+ return (
6
+ maybeString(input.session_id) ??
7
+ maybeString(input.conversation_id) ??
8
+ maybeString(input.generation_id)
9
+ );
10
+ }
11
+
12
+ export function normalizeModelId(model?: string): string | undefined {
13
+ if (!model) return undefined;
14
+ if (model.includes("/")) return model;
15
+ const prefixes: Record<string, string> = {
16
+ "claude-": "anthropic",
17
+ "gpt-": "openai",
18
+ o1: "openai",
19
+ o3: "openai",
20
+ o4: "openai",
21
+ "gemini-": "google",
22
+ };
23
+ for (const [prefix, provider] of Object.entries(prefixes)) {
24
+ if (model.startsWith(prefix)) return `${provider}/${model}`;
25
+ }
26
+ return model;
27
+ }