agent-profiler 0.1.0 → 1.0.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 +66 -0
- package/dist/cli.js +5 -4
- package/dist/commands/init.js +4 -0
- package/dist/commands/status.js +36 -9
- package/dist/core/db.js +164 -28
- package/dist/core/gitWorkspace.js +46 -5
- package/dist/core/packageMeta.js +20 -0
- package/dist/core/schema.sql +8 -0
- package/package.json +57 -3
- package/agent-profiler-0.1.0.tgz +0 -0
- package/docs/agent-profiler-mvp-handoff.md +0 -980
- package/google-home.png +0 -0
- package/src/adapters/codex.ts +0 -131
- package/src/adapters/cursor.ts +0 -115
- package/src/cli.ts +0 -109
- package/src/commands/auditContext.ts +0 -62
- package/src/commands/hook.ts +0 -104
- package/src/commands/init.ts +0 -324
- package/src/commands/last.ts +0 -326
- package/src/commands/status.ts +0 -345
- package/src/core/contextAudit.ts +0 -102
- package/src/core/db.ts +0 -491
- package/src/core/eventMetadata.ts +0 -184
- package/src/core/gitWorkspace.ts +0 -92
- package/src/core/normalize.ts +0 -29
- package/src/core/profile.ts +0 -35
- package/src/core/schema.sql +0 -56
- package/src/core/tokens.ts +0 -4
- package/src/types/better-sqlite3.d.ts +0 -26
- package/tsconfig.json +0 -15
package/src/core/db.ts
DELETED
|
@@ -1,491 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import Database from "better-sqlite3";
|
|
6
|
-
import { getConfiguredDatabasePath } from "./profile.js";
|
|
7
|
-
import type { DerivedIngestFields } from "./eventMetadata.js";
|
|
8
|
-
import type { NormalizedAgentEvent } from "./normalize.js";
|
|
9
|
-
import type { WorkspaceGitMeta } from "./gitWorkspace.js";
|
|
10
|
-
|
|
11
|
-
const HOME_DIR = path.join(os.homedir(), ".agent-profiler");
|
|
12
|
-
const WORKSPACE_DIR = path.join(process.cwd(), ".agent-profiler");
|
|
13
|
-
/** Same directory as this module: `src/core` when using tsx, `dist/core` when using build + copied schema. */
|
|
14
|
-
const SCHEMA_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "schema.sql");
|
|
15
|
-
|
|
16
|
-
export function getDefaultDbPath(): string {
|
|
17
|
-
const fromEnv = process.env.AGENT_PROFILER_DB_PATH;
|
|
18
|
-
if (fromEnv && fromEnv.trim().length > 0) return fromEnv;
|
|
19
|
-
const configured = getConfiguredDatabasePath(process.cwd());
|
|
20
|
-
if (configured && configured.trim().length > 0) return configured;
|
|
21
|
-
return path.join(HOME_DIR, "events.sqlite");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type SqliteDatabase = {
|
|
25
|
-
pragma(source: string): unknown;
|
|
26
|
-
exec(sql: string): unknown;
|
|
27
|
-
prepare(sql: string): {
|
|
28
|
-
run(...params: unknown[]): unknown;
|
|
29
|
-
get(...params: unknown[]): unknown;
|
|
30
|
-
all(...params: unknown[]): unknown[];
|
|
31
|
-
};
|
|
32
|
-
close(): void;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export function resolveWritableDbPath(dbPath: string): string {
|
|
36
|
-
try {
|
|
37
|
-
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
38
|
-
return dbPath;
|
|
39
|
-
} catch {
|
|
40
|
-
const fallbackPath = path.join(WORKSPACE_DIR, "events.sqlite");
|
|
41
|
-
fs.mkdirSync(path.dirname(fallbackPath), { recursive: true });
|
|
42
|
-
return fallbackPath;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function openDb(dbPath = getDefaultDbPath()): SqliteDatabase {
|
|
47
|
-
const writableDbPath = resolveWritableDbPath(dbPath);
|
|
48
|
-
const db = new Database(writableDbPath) as unknown as SqliteDatabase;
|
|
49
|
-
db.pragma("journal_mode = WAL");
|
|
50
|
-
applySchema(db);
|
|
51
|
-
return db;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function resolveUsableDbPath(preferredDbPath: string): string {
|
|
55
|
-
const primary = resolveWritableDbPath(preferredDbPath);
|
|
56
|
-
try {
|
|
57
|
-
const db = new Database(primary);
|
|
58
|
-
db.pragma("journal_mode = WAL");
|
|
59
|
-
db.close();
|
|
60
|
-
return primary;
|
|
61
|
-
} catch {
|
|
62
|
-
const fallbackPath = path.join(WORKSPACE_DIR, "events.sqlite");
|
|
63
|
-
fs.mkdirSync(path.dirname(fallbackPath), { recursive: true });
|
|
64
|
-
const db = new Database(fallbackPath);
|
|
65
|
-
db.pragma("journal_mode = WAL");
|
|
66
|
-
db.close();
|
|
67
|
-
return fallbackPath;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function applySchema(db: SqliteDatabase): void {
|
|
72
|
-
const schemaSql = fs.readFileSync(SCHEMA_PATH, "utf8");
|
|
73
|
-
db.exec(schemaSql);
|
|
74
|
-
migrateEventsSchema(db);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function migrateEventsSchema(db: SqliteDatabase): void {
|
|
78
|
-
const columns = db.prepare(`PRAGMA table_info(events)`).all() as { name: string }[];
|
|
79
|
-
const names = new Set(columns.map((c) => c.name));
|
|
80
|
-
|
|
81
|
-
const add = (sql: string, colName: string) => {
|
|
82
|
-
if (!names.has(colName)) {
|
|
83
|
-
db.exec(sql);
|
|
84
|
-
names.add(colName);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
add(`ALTER TABLE events ADD COLUMN workspace_path TEXT`, "workspace_path");
|
|
89
|
-
add(`ALTER TABLE events ADD COLUMN git_repo_root TEXT`, "git_repo_root");
|
|
90
|
-
add(`ALTER TABLE events ADD COLUMN git_repo_name TEXT`, "git_repo_name");
|
|
91
|
-
add(`ALTER TABLE events ADD COLUMN git_branch TEXT`, "git_branch");
|
|
92
|
-
add(`ALTER TABLE events ADD COLUMN interaction_kind TEXT`, "interaction_kind");
|
|
93
|
-
add(`ALTER TABLE events ADD COLUMN correlation_id TEXT`, "correlation_id");
|
|
94
|
-
add(`ALTER TABLE events ADD COLUMN tool_canonical_name TEXT`, "tool_canonical_name");
|
|
95
|
-
add(`ALTER TABLE events ADD COLUMN mcp_server TEXT`, "mcp_server");
|
|
96
|
-
add(`ALTER TABLE events ADD COLUMN mcp_tool TEXT`, "mcp_tool");
|
|
97
|
-
add(`ALTER TABLE events ADD COLUMN payload_byte_length INTEGER`, "payload_byte_length");
|
|
98
|
-
add(`ALTER TABLE events ADD COLUMN prompt_fingerprint TEXT`, "prompt_fingerprint");
|
|
99
|
-
|
|
100
|
-
db.exec(
|
|
101
|
-
`CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace_path, created_at)`,
|
|
102
|
-
);
|
|
103
|
-
db.exec(
|
|
104
|
-
`CREATE INDEX IF NOT EXISTS idx_events_interaction_kind ON events(interaction_kind, created_at)`,
|
|
105
|
-
);
|
|
106
|
-
db.exec(
|
|
107
|
-
`CREATE INDEX IF NOT EXISTS idx_events_correlation ON events(correlation_id, session_id)`,
|
|
108
|
-
);
|
|
109
|
-
db.exec(
|
|
110
|
-
`CREATE INDEX IF NOT EXISTS idx_events_prompt_fingerprint ON events(prompt_fingerprint, created_at)`,
|
|
111
|
-
);
|
|
112
|
-
db.exec(
|
|
113
|
-
`CREATE INDEX IF NOT EXISTS idx_spans_mcp ON interaction_spans(mcp_server, mcp_tool, started_at)`,
|
|
114
|
-
);
|
|
115
|
-
db.exec(
|
|
116
|
-
`CREATE INDEX IF NOT EXISTS idx_spans_session ON interaction_spans(session_key, started_at)`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function insertEvent(
|
|
121
|
-
db: SqliteDatabase,
|
|
122
|
-
event: NormalizedAgentEvent,
|
|
123
|
-
payloadHash: string,
|
|
124
|
-
workspaceGit: WorkspaceGitMeta,
|
|
125
|
-
derived: DerivedIngestFields,
|
|
126
|
-
): number {
|
|
127
|
-
const stmt = db.prepare(`
|
|
128
|
-
INSERT INTO events (
|
|
129
|
-
created_at,
|
|
130
|
-
source,
|
|
131
|
-
source_event,
|
|
132
|
-
repo_path,
|
|
133
|
-
session_id,
|
|
134
|
-
turn_id,
|
|
135
|
-
model,
|
|
136
|
-
role,
|
|
137
|
-
estimated_input_tokens,
|
|
138
|
-
estimated_output_tokens,
|
|
139
|
-
estimated_total_tokens,
|
|
140
|
-
payload_hash,
|
|
141
|
-
raw_payload,
|
|
142
|
-
workspace_path,
|
|
143
|
-
git_repo_root,
|
|
144
|
-
git_repo_name,
|
|
145
|
-
git_branch,
|
|
146
|
-
interaction_kind,
|
|
147
|
-
correlation_id,
|
|
148
|
-
tool_canonical_name,
|
|
149
|
-
mcp_server,
|
|
150
|
-
mcp_tool,
|
|
151
|
-
payload_byte_length,
|
|
152
|
-
prompt_fingerprint
|
|
153
|
-
)
|
|
154
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
155
|
-
`);
|
|
156
|
-
|
|
157
|
-
const info = stmt.run(
|
|
158
|
-
new Date().toISOString(),
|
|
159
|
-
event.source,
|
|
160
|
-
event.sourceEvent,
|
|
161
|
-
event.repoPath ?? null,
|
|
162
|
-
event.sessionId ?? null,
|
|
163
|
-
event.turnId ?? null,
|
|
164
|
-
event.model ?? null,
|
|
165
|
-
event.role,
|
|
166
|
-
event.estimatedInputTokens,
|
|
167
|
-
event.estimatedOutputTokens,
|
|
168
|
-
event.estimatedTotalTokens,
|
|
169
|
-
payloadHash,
|
|
170
|
-
JSON.stringify(event.rawPayload),
|
|
171
|
-
workspaceGit.workspacePath,
|
|
172
|
-
workspaceGit.gitRepoRoot,
|
|
173
|
-
workspaceGit.gitRepoName,
|
|
174
|
-
workspaceGit.gitBranch,
|
|
175
|
-
derived.interactionKind,
|
|
176
|
-
derived.correlationId,
|
|
177
|
-
derived.toolCanonicalName,
|
|
178
|
-
derived.mcpServer,
|
|
179
|
-
derived.mcpTool,
|
|
180
|
-
derived.payloadByteLength,
|
|
181
|
-
derived.promptFingerprint,
|
|
182
|
-
) as { lastInsertRowid: number | bigint };
|
|
183
|
-
|
|
184
|
-
return Number(info.lastInsertRowid);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
type SpanRow = {
|
|
188
|
-
id: number;
|
|
189
|
-
arg_token_estimate: number;
|
|
190
|
-
result_token_estimate: number;
|
|
191
|
-
pre_event_id: number | null;
|
|
192
|
-
post_event_id: number | null;
|
|
193
|
-
failure_event_id: number | null;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Links pre/post/failure hook rows for the same logical tool or MCP call via correlation_id.
|
|
198
|
-
*/
|
|
199
|
-
export function mergeInteractionSpan(
|
|
200
|
-
db: SqliteDatabase,
|
|
201
|
-
eventId: number,
|
|
202
|
-
normalized: NormalizedAgentEvent,
|
|
203
|
-
workspaceGit: WorkspaceGitMeta,
|
|
204
|
-
derived: DerivedIngestFields,
|
|
205
|
-
): void {
|
|
206
|
-
if (!derived.correlationId || !derived.toolPhase) return;
|
|
207
|
-
|
|
208
|
-
const sessionKey = normalized.sessionId ?? "";
|
|
209
|
-
const source = normalized.source;
|
|
210
|
-
const now = new Date().toISOString();
|
|
211
|
-
|
|
212
|
-
const existing = db
|
|
213
|
-
.prepare(
|
|
214
|
-
`SELECT id, arg_token_estimate, result_token_estimate, pre_event_id, post_event_id, failure_event_id
|
|
215
|
-
FROM interaction_spans
|
|
216
|
-
WHERE session_key = ? AND source = ? AND correlation_id = ?`,
|
|
217
|
-
)
|
|
218
|
-
.get(sessionKey, source, derived.correlationId) as SpanRow | undefined;
|
|
219
|
-
|
|
220
|
-
const argTok = Math.max(normalized.estimatedInputTokens, 0);
|
|
221
|
-
const resTok = Math.max(normalized.estimatedOutputTokens, 0);
|
|
222
|
-
|
|
223
|
-
const toolName = derived.toolCanonicalName;
|
|
224
|
-
const mcpS = derived.mcpServer;
|
|
225
|
-
const mcpT = derived.mcpTool;
|
|
226
|
-
|
|
227
|
-
if (!existing) {
|
|
228
|
-
const ins = db.prepare(`
|
|
229
|
-
INSERT INTO interaction_spans (
|
|
230
|
-
session_key, source, correlation_id, turn_id,
|
|
231
|
-
tool_canonical_name, mcp_server, mcp_tool,
|
|
232
|
-
pre_event_id, post_event_id, failure_event_id,
|
|
233
|
-
arg_token_estimate, result_token_estimate,
|
|
234
|
-
workspace_path, git_repo_root, git_repo_name, git_branch,
|
|
235
|
-
started_at, completed_at
|
|
236
|
-
)
|
|
237
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
238
|
-
`);
|
|
239
|
-
if (derived.toolPhase === "pre") {
|
|
240
|
-
ins.run(
|
|
241
|
-
sessionKey,
|
|
242
|
-
source,
|
|
243
|
-
derived.correlationId,
|
|
244
|
-
normalized.turnId ?? null,
|
|
245
|
-
toolName,
|
|
246
|
-
mcpS,
|
|
247
|
-
mcpT,
|
|
248
|
-
eventId,
|
|
249
|
-
null,
|
|
250
|
-
null,
|
|
251
|
-
argTok,
|
|
252
|
-
0,
|
|
253
|
-
workspaceGit.workspacePath,
|
|
254
|
-
workspaceGit.gitRepoRoot,
|
|
255
|
-
workspaceGit.gitRepoName,
|
|
256
|
-
workspaceGit.gitBranch,
|
|
257
|
-
now,
|
|
258
|
-
null,
|
|
259
|
-
);
|
|
260
|
-
} else if (derived.toolPhase === "post") {
|
|
261
|
-
ins.run(
|
|
262
|
-
sessionKey,
|
|
263
|
-
source,
|
|
264
|
-
derived.correlationId,
|
|
265
|
-
normalized.turnId ?? null,
|
|
266
|
-
toolName,
|
|
267
|
-
mcpS,
|
|
268
|
-
mcpT,
|
|
269
|
-
null,
|
|
270
|
-
eventId,
|
|
271
|
-
null,
|
|
272
|
-
0,
|
|
273
|
-
resTok,
|
|
274
|
-
workspaceGit.workspacePath,
|
|
275
|
-
workspaceGit.gitRepoRoot,
|
|
276
|
-
workspaceGit.gitRepoName,
|
|
277
|
-
workspaceGit.gitBranch,
|
|
278
|
-
null,
|
|
279
|
-
now,
|
|
280
|
-
);
|
|
281
|
-
} else {
|
|
282
|
-
ins.run(
|
|
283
|
-
sessionKey,
|
|
284
|
-
source,
|
|
285
|
-
derived.correlationId,
|
|
286
|
-
normalized.turnId ?? null,
|
|
287
|
-
toolName,
|
|
288
|
-
mcpS,
|
|
289
|
-
mcpT,
|
|
290
|
-
null,
|
|
291
|
-
null,
|
|
292
|
-
eventId,
|
|
293
|
-
0,
|
|
294
|
-
resTok,
|
|
295
|
-
workspaceGit.workspacePath,
|
|
296
|
-
workspaceGit.gitRepoRoot,
|
|
297
|
-
workspaceGit.gitRepoName,
|
|
298
|
-
workspaceGit.gitBranch,
|
|
299
|
-
null,
|
|
300
|
-
now,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (derived.toolPhase === "pre") {
|
|
307
|
-
db.prepare(
|
|
308
|
-
`UPDATE interaction_spans SET
|
|
309
|
-
pre_event_id = COALESCE(pre_event_id, ?),
|
|
310
|
-
started_at = COALESCE(started_at, ?),
|
|
311
|
-
arg_token_estimate = ?,
|
|
312
|
-
tool_canonical_name = COALESCE(tool_canonical_name, ?),
|
|
313
|
-
mcp_server = COALESCE(mcp_server, ?),
|
|
314
|
-
mcp_tool = COALESCE(mcp_tool, ?),
|
|
315
|
-
turn_id = COALESCE(turn_id, ?)
|
|
316
|
-
WHERE id = ?`,
|
|
317
|
-
).run(
|
|
318
|
-
eventId,
|
|
319
|
-
now,
|
|
320
|
-
Math.max(existing.arg_token_estimate, argTok),
|
|
321
|
-
toolName,
|
|
322
|
-
mcpS,
|
|
323
|
-
mcpT,
|
|
324
|
-
normalized.turnId ?? null,
|
|
325
|
-
existing.id,
|
|
326
|
-
);
|
|
327
|
-
} else if (derived.toolPhase === "post") {
|
|
328
|
-
db.prepare(
|
|
329
|
-
`UPDATE interaction_spans SET
|
|
330
|
-
post_event_id = COALESCE(post_event_id, ?),
|
|
331
|
-
completed_at = COALESCE(completed_at, ?),
|
|
332
|
-
result_token_estimate = ?,
|
|
333
|
-
tool_canonical_name = COALESCE(tool_canonical_name, ?),
|
|
334
|
-
mcp_server = COALESCE(mcp_server, ?),
|
|
335
|
-
mcp_tool = COALESCE(mcp_tool, ?),
|
|
336
|
-
turn_id = COALESCE(turn_id, ?)
|
|
337
|
-
WHERE id = ?`,
|
|
338
|
-
).run(
|
|
339
|
-
eventId,
|
|
340
|
-
now,
|
|
341
|
-
Math.max(existing.result_token_estimate, resTok),
|
|
342
|
-
toolName,
|
|
343
|
-
mcpS,
|
|
344
|
-
mcpT,
|
|
345
|
-
normalized.turnId ?? null,
|
|
346
|
-
existing.id,
|
|
347
|
-
);
|
|
348
|
-
} else {
|
|
349
|
-
db.prepare(
|
|
350
|
-
`UPDATE interaction_spans SET
|
|
351
|
-
failure_event_id = COALESCE(failure_event_id, ?),
|
|
352
|
-
completed_at = COALESCE(completed_at, ?),
|
|
353
|
-
result_token_estimate = ?,
|
|
354
|
-
tool_canonical_name = COALESCE(tool_canonical_name, ?),
|
|
355
|
-
mcp_server = COALESCE(mcp_server, ?),
|
|
356
|
-
mcp_tool = COALESCE(mcp_tool, ?),
|
|
357
|
-
turn_id = COALESCE(turn_id, ?)
|
|
358
|
-
WHERE id = ?`,
|
|
359
|
-
).run(
|
|
360
|
-
eventId,
|
|
361
|
-
now,
|
|
362
|
-
Math.max(existing.result_token_estimate, resTok),
|
|
363
|
-
toolName,
|
|
364
|
-
mcpS,
|
|
365
|
-
mcpT,
|
|
366
|
-
normalized.turnId ?? null,
|
|
367
|
-
existing.id,
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export type StoredEventSummary = {
|
|
373
|
-
createdAt: string;
|
|
374
|
-
source: string;
|
|
375
|
-
sourceEvent: string;
|
|
376
|
-
estimatedTotalTokens: number;
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
export function getLastEventSummary(db: SqliteDatabase): StoredEventSummary | null {
|
|
380
|
-
const row = db
|
|
381
|
-
.prepare(
|
|
382
|
-
`
|
|
383
|
-
SELECT
|
|
384
|
-
created_at AS createdAt,
|
|
385
|
-
source,
|
|
386
|
-
source_event AS sourceEvent,
|
|
387
|
-
estimated_total_tokens AS estimatedTotalTokens
|
|
388
|
-
FROM events
|
|
389
|
-
ORDER BY created_at DESC
|
|
390
|
-
LIMIT 1
|
|
391
|
-
`,
|
|
392
|
-
)
|
|
393
|
-
.get() as StoredEventSummary | undefined;
|
|
394
|
-
|
|
395
|
-
return row ?? null;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export type StoredEvent = {
|
|
399
|
-
id: number;
|
|
400
|
-
createdAt: string;
|
|
401
|
-
source: string;
|
|
402
|
-
sourceEvent: string;
|
|
403
|
-
repoPath: string | null;
|
|
404
|
-
sessionId: string | null;
|
|
405
|
-
turnId: string | null;
|
|
406
|
-
model: string | null;
|
|
407
|
-
role: string;
|
|
408
|
-
estimatedInputTokens: number;
|
|
409
|
-
estimatedOutputTokens: number;
|
|
410
|
-
estimatedTotalTokens: number;
|
|
411
|
-
rawPayload: string;
|
|
412
|
-
workspacePath: string | null;
|
|
413
|
-
gitRepoRoot: string | null;
|
|
414
|
-
gitRepoName: string | null;
|
|
415
|
-
gitBranch: string | null;
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
export function getEventsForLatestSession(db: SqliteDatabase): StoredEvent[] {
|
|
419
|
-
const latest = db
|
|
420
|
-
.prepare(
|
|
421
|
-
`
|
|
422
|
-
SELECT source, session_id AS sessionId, repo_path AS repoPath
|
|
423
|
-
FROM events
|
|
424
|
-
ORDER BY created_at DESC
|
|
425
|
-
LIMIT 1
|
|
426
|
-
`,
|
|
427
|
-
)
|
|
428
|
-
.get() as { source: string; sessionId: string | null; repoPath: string | null } | undefined;
|
|
429
|
-
|
|
430
|
-
if (!latest) return [];
|
|
431
|
-
|
|
432
|
-
const rows = latest.sessionId
|
|
433
|
-
? db
|
|
434
|
-
.prepare(
|
|
435
|
-
`
|
|
436
|
-
SELECT
|
|
437
|
-
id,
|
|
438
|
-
created_at AS createdAt,
|
|
439
|
-
source,
|
|
440
|
-
source_event AS sourceEvent,
|
|
441
|
-
repo_path AS repoPath,
|
|
442
|
-
session_id AS sessionId,
|
|
443
|
-
turn_id AS turnId,
|
|
444
|
-
model,
|
|
445
|
-
role,
|
|
446
|
-
estimated_input_tokens AS estimatedInputTokens,
|
|
447
|
-
estimated_output_tokens AS estimatedOutputTokens,
|
|
448
|
-
estimated_total_tokens AS estimatedTotalTokens,
|
|
449
|
-
raw_payload AS rawPayload,
|
|
450
|
-
workspace_path AS workspacePath,
|
|
451
|
-
git_repo_root AS gitRepoRoot,
|
|
452
|
-
git_repo_name AS gitRepoName,
|
|
453
|
-
git_branch AS gitBranch
|
|
454
|
-
FROM events
|
|
455
|
-
WHERE source = ? AND session_id = ?
|
|
456
|
-
ORDER BY created_at ASC, id ASC
|
|
457
|
-
`,
|
|
458
|
-
)
|
|
459
|
-
.all(latest.source, latest.sessionId)
|
|
460
|
-
: db
|
|
461
|
-
.prepare(
|
|
462
|
-
`
|
|
463
|
-
SELECT
|
|
464
|
-
id,
|
|
465
|
-
created_at AS createdAt,
|
|
466
|
-
source,
|
|
467
|
-
source_event AS sourceEvent,
|
|
468
|
-
repo_path AS repoPath,
|
|
469
|
-
session_id AS sessionId,
|
|
470
|
-
turn_id AS turnId,
|
|
471
|
-
model,
|
|
472
|
-
role,
|
|
473
|
-
estimated_input_tokens AS estimatedInputTokens,
|
|
474
|
-
estimated_output_tokens AS estimatedOutputTokens,
|
|
475
|
-
estimated_total_tokens AS estimatedTotalTokens,
|
|
476
|
-
raw_payload AS rawPayload,
|
|
477
|
-
workspace_path AS workspacePath,
|
|
478
|
-
git_repo_root AS gitRepoRoot,
|
|
479
|
-
git_repo_name AS gitRepoName,
|
|
480
|
-
git_branch AS gitBranch
|
|
481
|
-
FROM events
|
|
482
|
-
WHERE source = ? AND repo_path IS ?
|
|
483
|
-
ORDER BY created_at DESC, id DESC
|
|
484
|
-
LIMIT 200
|
|
485
|
-
`,
|
|
486
|
-
)
|
|
487
|
-
.all(latest.source, latest.repoPath ?? null)
|
|
488
|
-
.reverse();
|
|
489
|
-
|
|
490
|
-
return rows as StoredEvent[];
|
|
491
|
-
}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import type { NormalizedAgentEvent } from "./normalize.js";
|
|
3
|
-
|
|
4
|
-
/** Hook adapters that emit structured telemetry (matches CLI init sources). */
|
|
5
|
-
export type TelemetryHookSource = "cursor" | "codex";
|
|
6
|
-
|
|
7
|
-
export type ToolSpanPhase = "pre" | "post" | "failure";
|
|
8
|
-
|
|
9
|
-
export type DerivedIngestFields = {
|
|
10
|
-
/** Normalized category for analytics / future intent models. */
|
|
11
|
-
interactionKind: string;
|
|
12
|
-
correlationId: string | null;
|
|
13
|
-
toolCanonicalName: string | null;
|
|
14
|
-
mcpServer: string | null;
|
|
15
|
-
mcpTool: string | null;
|
|
16
|
-
payloadByteLength: number;
|
|
17
|
-
/** SHA-256 of trimmed prompt text when this row is a user submission. */
|
|
18
|
-
promptFingerprint: string | null;
|
|
19
|
-
/** When set, row participates in `interaction_spans` merge. */
|
|
20
|
-
toolPhase: ToolSpanPhase | null;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
24
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
25
|
-
return value as Record<string, unknown>;
|
|
26
|
-
}
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function pickFirstString(values: unknown[]): string | undefined {
|
|
31
|
-
for (const value of values) {
|
|
32
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Split Codex-style `mcp__server__tool` or loose `MCP: server — tool`. */
|
|
40
|
-
export function parseMcpToolName(canonical: string): {
|
|
41
|
-
mcpServer: string | null;
|
|
42
|
-
mcpTool: string | null;
|
|
43
|
-
} {
|
|
44
|
-
const t = canonical.trim();
|
|
45
|
-
if (!t) return { mcpServer: null, mcpTool: null };
|
|
46
|
-
|
|
47
|
-
if (t.startsWith("mcp__")) {
|
|
48
|
-
const parts = t.split("__").filter(Boolean);
|
|
49
|
-
if (parts.length >= 3) {
|
|
50
|
-
return { mcpServer: parts[1] ?? null, mcpTool: parts.slice(2).join("__") || null };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (t.toLowerCase().startsWith("mcp:")) {
|
|
55
|
-
const rest = t.slice(4).trim();
|
|
56
|
-
const sep = rest.indexOf(":");
|
|
57
|
-
if (sep > 0) {
|
|
58
|
-
return {
|
|
59
|
-
mcpServer: rest.slice(0, sep).trim() || null,
|
|
60
|
-
mcpTool: rest.slice(sep + 1).trim() || null,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return { mcpServer: null, mcpTool: null };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function fingerprintPrompt(text: string): string | null {
|
|
69
|
-
const trimmed = text.trim();
|
|
70
|
-
if (!trimmed) return null;
|
|
71
|
-
return crypto.createHash("sha256").update(trimmed, "utf8").digest("hex");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function hookToInteractionKind(
|
|
75
|
-
source: TelemetryHookSource,
|
|
76
|
-
hookEventName: string,
|
|
77
|
-
): { kind: string; toolPhase: ToolSpanPhase | null } {
|
|
78
|
-
const cursorMap: Record<string, { kind: string; toolPhase: ToolSpanPhase | null }> = {
|
|
79
|
-
beforeSubmitPrompt: { kind: "user_prompt_submit", toolPhase: null },
|
|
80
|
-
afterAgentResponse: { kind: "model_output", toolPhase: null },
|
|
81
|
-
afterAgentThought: { kind: "model_thought", toolPhase: null },
|
|
82
|
-
preToolUse: { kind: "tool_request", toolPhase: "pre" },
|
|
83
|
-
postToolUse: { kind: "tool_result_event", toolPhase: "post" },
|
|
84
|
-
postToolUseFailure: { kind: "tool_failure_event", toolPhase: "failure" },
|
|
85
|
-
beforeMCPExecution: { kind: "mcp_request", toolPhase: "pre" },
|
|
86
|
-
afterMCPExecution: { kind: "mcp_result_event", toolPhase: "post" },
|
|
87
|
-
beforeShellExecution: { kind: "shell_command_request", toolPhase: "pre" },
|
|
88
|
-
afterShellExecution: { kind: "shell_output", toolPhase: null },
|
|
89
|
-
afterFileEdit: { kind: "file_edit", toolPhase: null },
|
|
90
|
-
beforeReadFile: { kind: "file_read_request", toolPhase: "pre" },
|
|
91
|
-
start: { kind: "session_start", toolPhase: null },
|
|
92
|
-
sessionStart: { kind: "session_start", toolPhase: null },
|
|
93
|
-
stop: { kind: "session_stop", toolPhase: null },
|
|
94
|
-
sessionEnd: { kind: "session_end", toolPhase: null },
|
|
95
|
-
preCompact: { kind: "context_compact", toolPhase: null },
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const codexMap: Record<string, { kind: string; toolPhase: ToolSpanPhase | null }> = {
|
|
99
|
-
SessionStart: { kind: "session_start", toolPhase: null },
|
|
100
|
-
UserPromptSubmit: { kind: "user_prompt_submit", toolPhase: null },
|
|
101
|
-
PreToolUse: { kind: "tool_request", toolPhase: "pre" },
|
|
102
|
-
PostToolUse: { kind: "tool_result_event", toolPhase: "post" },
|
|
103
|
-
Stop: { kind: "session_stop", toolPhase: null },
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const mapped =
|
|
107
|
-
source === "codex" ? codexMap[hookEventName] : cursorMap[hookEventName];
|
|
108
|
-
if (mapped) return mapped;
|
|
109
|
-
|
|
110
|
-
return { kind: `other:${hookEventName}`, toolPhase: null };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function extractCorrelationId(payload: Record<string, unknown>): string | null {
|
|
114
|
-
return (
|
|
115
|
-
pickFirstString([
|
|
116
|
-
payload.tool_use_id,
|
|
117
|
-
payload.toolUseId,
|
|
118
|
-
payload.toolCallId,
|
|
119
|
-
payload.callId,
|
|
120
|
-
payload.id,
|
|
121
|
-
payload.invocationId,
|
|
122
|
-
]) ?? null
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function extractToolCanonicalName(payload: Record<string, unknown>): string | null {
|
|
127
|
-
const name =
|
|
128
|
-
pickFirstString([
|
|
129
|
-
payload.tool_name,
|
|
130
|
-
payload.toolName,
|
|
131
|
-
payload.tool,
|
|
132
|
-
payload.name,
|
|
133
|
-
payload.type,
|
|
134
|
-
]) ?? null;
|
|
135
|
-
return name?.trim() || null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Derives query-friendly fields + MCP split + span phase for tool correlation.
|
|
140
|
-
*/
|
|
141
|
-
export function deriveIngestFields(
|
|
142
|
-
source: TelemetryHookSource,
|
|
143
|
-
hookEventName: string,
|
|
144
|
-
rawPayload: unknown,
|
|
145
|
-
rawJsonText: string,
|
|
146
|
-
normalized: NormalizedAgentEvent,
|
|
147
|
-
): DerivedIngestFields {
|
|
148
|
-
const { kind, toolPhase } = hookToInteractionKind(source, hookEventName);
|
|
149
|
-
const payload = asRecord(rawPayload);
|
|
150
|
-
const correlationId = extractCorrelationId(payload);
|
|
151
|
-
let toolCanonicalName = extractToolCanonicalName(payload);
|
|
152
|
-
|
|
153
|
-
if (!toolCanonicalName && payload.tool_input && typeof payload.tool_input === "object") {
|
|
154
|
-
const ti = payload.tool_input as Record<string, unknown>;
|
|
155
|
-
toolCanonicalName =
|
|
156
|
-
pickFirstString([ti.command, ti.tool, ti.name])?.trim() ||
|
|
157
|
-
toolCanonicalName;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const { mcpServer, mcpTool } = toolCanonicalName
|
|
161
|
-
? parseMcpToolName(toolCanonicalName)
|
|
162
|
-
: { mcpServer: null, mcpTool: null };
|
|
163
|
-
|
|
164
|
-
let promptFingerprint: string | null = null;
|
|
165
|
-
if (kind === "user_prompt_submit") {
|
|
166
|
-
const prompt =
|
|
167
|
-
pickFirstString([payload.prompt, payload.text, payload.message]) ??
|
|
168
|
-
normalized.observableText;
|
|
169
|
-
promptFingerprint = fingerprintPrompt(prompt);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const payloadByteLength = Buffer.byteLength(rawJsonText, "utf8");
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
interactionKind: kind,
|
|
176
|
-
correlationId,
|
|
177
|
-
toolCanonicalName,
|
|
178
|
-
mcpServer,
|
|
179
|
-
mcpTool,
|
|
180
|
-
payloadByteLength,
|
|
181
|
-
promptFingerprint,
|
|
182
|
-
toolPhase,
|
|
183
|
-
};
|
|
184
|
-
}
|