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 ADDED
@@ -0,0 +1,66 @@
1
+ # agent-profiler
2
+
3
+ Local-first profiling for AI coding agents.
4
+
5
+ `agent-profiler` captures local Cursor and Codex hook events into SQLite so you can inspect recent sessions, setup state, and always-on context overhead without sending data to a remote service.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 22 or newer
10
+ - macOS, Linux, or another environment supported by `better-sqlite3`
11
+
12
+ ## Install
13
+
14
+ For one-off commands, use `npx`:
15
+
16
+ ```bash
17
+ npx agent-profiler --help
18
+ npx agent-profiler last
19
+ ```
20
+
21
+ For persistent hook installation, install the package so `agent-profiler` is available on `PATH`:
22
+
23
+ ```bash
24
+ npm install -g agent-profiler
25
+ agent-profiler --help
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ Initialize hooks for Cursor:
31
+
32
+ ```bash
33
+ agent-profiler init cursor --mode prod
34
+ ```
35
+
36
+ Initialize hooks for Codex:
37
+
38
+ ```bash
39
+ agent-profiler init codex --mode prod
40
+ ```
41
+
42
+ Inspect the resulting setup and the latest captured session:
43
+
44
+ ```bash
45
+ agent-profiler status
46
+ agent-profiler last
47
+ agent-profiler audit context
48
+ ```
49
+
50
+ ## `npx` vs `--mode prod`
51
+
52
+ - `npx agent-profiler ...` is great for one-off inspection commands.
53
+ - `agent-profiler init <source> --mode prod` writes persistent hook commands that expect `agent-profiler` to be installed on `PATH`.
54
+ - `--mode dev` is meant for local development in this repository and writes absolute `node <repo>/dist/cli.js ...` hook commands instead.
55
+
56
+ ## Commands
57
+
58
+ - `agent-profiler init <cursor|codex>`: install hook wiring for a supported source
59
+ - `agent-profiler hook <source> <eventName>`: ingest one hook payload from stdin
60
+ - `agent-profiler status`: inspect local setup and ingest state
61
+ - `agent-profiler last`: summarize the most recent observed session
62
+ - `agent-profiler audit context`: estimate always-on context token footprint
63
+
64
+ ## Releases
65
+
66
+ Releases are automated with semantic-release. Pull requests run CI plus canary publishing, and pushes to `main` publish to npm and create a GitHub release.
package/dist/cli.js CHANGED
@@ -1,20 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { getAuditContextReport, runAuditContext } from "./commands/auditContext.js";
3
+ import { getAuditContextReport, runAuditContext, } from "./commands/auditContext.js";
4
4
  import { runHook } from "./commands/hook.js";
5
5
  import { runInit } from "./commands/init.js";
6
6
  import { getLastReport, runLast } from "./commands/last.js";
7
7
  import { getStatusReport, runStatus } from "./commands/status.js";
8
+ import { getPackageVersion } from "./core/packageMeta.js";
8
9
  const program = new Command();
9
10
  program
10
11
  .name("agent-profiler")
11
12
  .description("Local-first profiling for AI coding agents.")
12
- .version("0.1.0");
13
+ .version(getPackageVersion());
13
14
  program
14
15
  .command("init")
15
16
  .description("Initialize Agent Profiler for a supported source.")
16
17
  .argument("<source>", "supported: cursor | codex")
17
- .option("--mode <mode>", "init mode: dev or prod", "dev")
18
+ .option("--mode <mode>", "init mode: dev or prod (prod requires `agent-profiler` on PATH)", "dev")
18
19
  .action((source, options) => {
19
20
  const allowed = ["cursor", "codex"];
20
21
  if (!allowed.includes(source)) {
@@ -60,7 +61,7 @@ program
60
61
  program
61
62
  .command("status")
62
63
  .description("Show local Agent Profiler setup and ingest status.")
63
- .option("--mode <mode>", "status mode: dev or prod", "dev")
64
+ .option("--mode <mode>", "status mode: dev or prod (prod expects a global install on PATH)", "dev")
64
65
  .option("--json", "Output status as JSON")
65
66
  .action((options) => {
66
67
  if (options.mode !== "dev" && options.mode !== "prod") {
@@ -250,6 +250,10 @@ export function runInit(source, mode = "dev") {
250
250
  console.log(`Config: ${configPath}`);
251
251
  console.log(`Database: ${resolvedDbPath}`);
252
252
  console.log(`Hooks: ${hooksPath}`);
253
+ if (mode === "prod") {
254
+ console.log("Note: Prod mode requires a real `agent-profiler` install on PATH.");
255
+ console.log(" Use `npx agent-profiler ...` for one-off commands only.");
256
+ }
253
257
  if (usedFallbackConfigPath) {
254
258
  console.log("Note: Could not write home config in this environment; wrote local config instead.");
255
259
  }
@@ -105,19 +105,31 @@ function getCursorSetupStatus(configPath, mode) {
105
105
  const sampleCommand = hookEntryCommand(hooks.beforeSubmitPrompt);
106
106
  if (mode === "dev") {
107
107
  if (!sampleCommand.startsWith("node ")) {
108
- return { state: "partial", note: "dev mode expects hooks to use `node <abs>/dist/cli.js ...`" };
108
+ return {
109
+ state: "partial",
110
+ note: "dev mode expects hooks to use `node <abs>/dist/cli.js ...`",
111
+ };
109
112
  }
110
113
  const cliPath = sampleCommand.split(" ")[1];
111
114
  if (!cliPath || !fs.existsSync(cliPath)) {
112
- return { state: "partial", note: "dev hook CLI path is missing or invalid" };
115
+ return {
116
+ state: "partial",
117
+ note: "dev hook CLI path is missing or invalid",
118
+ };
113
119
  }
114
120
  }
115
121
  else {
116
122
  if (!sampleCommand.startsWith("agent-profiler ")) {
117
- return { state: "partial", note: "prod mode expects hooks to use `agent-profiler ...`" };
123
+ return {
124
+ state: "partial",
125
+ note: "prod mode expects hooks to use `agent-profiler ...` from a real install on PATH",
126
+ };
118
127
  }
119
128
  if (!commandExistsInPath("agent-profiler")) {
120
- return { state: "partial", note: "`agent-profiler` is not on PATH" };
129
+ return {
130
+ state: "partial",
131
+ note: "`agent-profiler` is not on PATH; `npx` only covers one-off commands",
132
+ };
121
133
  }
122
134
  }
123
135
  return { state: "yes", note: "configured via init" };
@@ -148,23 +160,38 @@ function getCodexSetupStatus(configPath, mode) {
148
160
  }
149
161
  }
150
162
  if (!sampleCommand) {
151
- return { state: "partial", note: "could not find agent-profiler command in Codex hooks" };
163
+ return {
164
+ state: "partial",
165
+ note: "could not find agent-profiler command in Codex hooks",
166
+ };
152
167
  }
153
168
  if (mode === "dev") {
154
169
  if (!sampleCommand.startsWith("node ")) {
155
- return { state: "partial", note: "dev mode expects hooks to use `node <abs>/dist/cli.js ...`" };
170
+ return {
171
+ state: "partial",
172
+ note: "dev mode expects hooks to use `node <abs>/dist/cli.js ...`",
173
+ };
156
174
  }
157
175
  const cliPath = sampleCommand.split(" ")[1];
158
176
  if (!cliPath || !fs.existsSync(cliPath)) {
159
- return { state: "partial", note: "dev hook CLI path is missing or invalid" };
177
+ return {
178
+ state: "partial",
179
+ note: "dev hook CLI path is missing or invalid",
180
+ };
160
181
  }
161
182
  }
162
183
  else {
163
184
  if (!sampleCommand.startsWith("agent-profiler ")) {
164
- return { state: "partial", note: "prod mode expects hooks to use `agent-profiler ...`" };
185
+ return {
186
+ state: "partial",
187
+ note: "prod mode expects hooks to use `agent-profiler ...` from a real install on PATH",
188
+ };
165
189
  }
166
190
  if (!commandExistsInPath("agent-profiler")) {
167
- return { state: "partial", note: "`agent-profiler` is not on PATH" };
191
+ return {
192
+ state: "partial",
193
+ note: "`agent-profiler` is not on PATH; `npx` only covers one-off commands",
194
+ };
168
195
  }
169
196
  }
170
197
  return { state: "yes", note: "configured via init" };
package/dist/core/db.js CHANGED
@@ -56,27 +56,122 @@ export function applySchema(db) {
56
56
  const schemaSql = fs.readFileSync(SCHEMA_PATH, "utf8");
57
57
  db.exec(schemaSql);
58
58
  migrateEventsSchema(db);
59
+ migrateInteractionSpansSchema(db);
59
60
  }
60
- function migrateEventsSchema(db) {
61
- const columns = db.prepare(`PRAGMA table_info(events)`).all();
61
+ function migrateTableColumns(db, tableName, columnsToAdd) {
62
+ const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
62
63
  const names = new Set(columns.map((c) => c.name));
63
- const add = (sql, colName) => {
64
- if (!names.has(colName)) {
65
- db.exec(sql);
66
- names.add(colName);
64
+ for (const column of columnsToAdd) {
65
+ if (!names.has(column.name)) {
66
+ db.exec(column.sql);
67
+ names.add(column.name);
67
68
  }
68
- };
69
- add(`ALTER TABLE events ADD COLUMN workspace_path TEXT`, "workspace_path");
70
- add(`ALTER TABLE events ADD COLUMN git_repo_root TEXT`, "git_repo_root");
71
- add(`ALTER TABLE events ADD COLUMN git_repo_name TEXT`, "git_repo_name");
72
- add(`ALTER TABLE events ADD COLUMN git_branch TEXT`, "git_branch");
73
- add(`ALTER TABLE events ADD COLUMN interaction_kind TEXT`, "interaction_kind");
74
- add(`ALTER TABLE events ADD COLUMN correlation_id TEXT`, "correlation_id");
75
- add(`ALTER TABLE events ADD COLUMN tool_canonical_name TEXT`, "tool_canonical_name");
76
- add(`ALTER TABLE events ADD COLUMN mcp_server TEXT`, "mcp_server");
77
- add(`ALTER TABLE events ADD COLUMN mcp_tool TEXT`, "mcp_tool");
78
- add(`ALTER TABLE events ADD COLUMN payload_byte_length INTEGER`, "payload_byte_length");
79
- add(`ALTER TABLE events ADD COLUMN prompt_fingerprint TEXT`, "prompt_fingerprint");
69
+ }
70
+ }
71
+ function migrateEventsSchema(db) {
72
+ migrateTableColumns(db, "events", [
73
+ { name: "workspace_path", sql: `ALTER TABLE events ADD COLUMN workspace_path TEXT` },
74
+ {
75
+ name: "workspace_home_rel_path",
76
+ sql: `ALTER TABLE events ADD COLUMN workspace_home_rel_path TEXT`,
77
+ },
78
+ {
79
+ name: "workspace_display_path",
80
+ sql: `ALTER TABLE events ADD COLUMN workspace_display_path TEXT`,
81
+ },
82
+ { name: "git_repo_root", sql: `ALTER TABLE events ADD COLUMN git_repo_root TEXT` },
83
+ {
84
+ name: "git_repo_root_home_rel_path",
85
+ sql: `ALTER TABLE events ADD COLUMN git_repo_root_home_rel_path TEXT`,
86
+ },
87
+ {
88
+ name: "git_repo_root_display_path",
89
+ sql: `ALTER TABLE events ADD COLUMN git_repo_root_display_path TEXT`,
90
+ },
91
+ { name: "git_repo_name", sql: `ALTER TABLE events ADD COLUMN git_repo_name TEXT` },
92
+ { name: "git_branch", sql: `ALTER TABLE events ADD COLUMN git_branch TEXT` },
93
+ {
94
+ name: "interaction_kind",
95
+ sql: `ALTER TABLE events ADD COLUMN interaction_kind TEXT`,
96
+ },
97
+ { name: "correlation_id", sql: `ALTER TABLE events ADD COLUMN correlation_id TEXT` },
98
+ {
99
+ name: "tool_canonical_name",
100
+ sql: `ALTER TABLE events ADD COLUMN tool_canonical_name TEXT`,
101
+ },
102
+ { name: "mcp_server", sql: `ALTER TABLE events ADD COLUMN mcp_server TEXT` },
103
+ { name: "mcp_tool", sql: `ALTER TABLE events ADD COLUMN mcp_tool TEXT` },
104
+ {
105
+ name: "payload_byte_length",
106
+ sql: `ALTER TABLE events ADD COLUMN payload_byte_length INTEGER`,
107
+ },
108
+ {
109
+ name: "prompt_fingerprint",
110
+ sql: `ALTER TABLE events ADD COLUMN prompt_fingerprint TEXT`,
111
+ },
112
+ ]);
113
+ }
114
+ function migrateInteractionSpansSchema(db) {
115
+ migrateTableColumns(db, "interaction_spans", [
116
+ { name: "turn_id", sql: `ALTER TABLE interaction_spans ADD COLUMN turn_id TEXT` },
117
+ {
118
+ name: "tool_canonical_name",
119
+ sql: `ALTER TABLE interaction_spans ADD COLUMN tool_canonical_name TEXT`,
120
+ },
121
+ { name: "mcp_server", sql: `ALTER TABLE interaction_spans ADD COLUMN mcp_server TEXT` },
122
+ { name: "mcp_tool", sql: `ALTER TABLE interaction_spans ADD COLUMN mcp_tool TEXT` },
123
+ {
124
+ name: "pre_event_id",
125
+ sql: `ALTER TABLE interaction_spans ADD COLUMN pre_event_id INTEGER`,
126
+ },
127
+ {
128
+ name: "post_event_id",
129
+ sql: `ALTER TABLE interaction_spans ADD COLUMN post_event_id INTEGER`,
130
+ },
131
+ {
132
+ name: "failure_event_id",
133
+ sql: `ALTER TABLE interaction_spans ADD COLUMN failure_event_id INTEGER`,
134
+ },
135
+ {
136
+ name: "arg_token_estimate",
137
+ sql: `ALTER TABLE interaction_spans ADD COLUMN arg_token_estimate INTEGER DEFAULT 0`,
138
+ },
139
+ {
140
+ name: "result_token_estimate",
141
+ sql: `ALTER TABLE interaction_spans ADD COLUMN result_token_estimate INTEGER DEFAULT 0`,
142
+ },
143
+ {
144
+ name: "workspace_path",
145
+ sql: `ALTER TABLE interaction_spans ADD COLUMN workspace_path TEXT`,
146
+ },
147
+ {
148
+ name: "workspace_home_rel_path",
149
+ sql: `ALTER TABLE interaction_spans ADD COLUMN workspace_home_rel_path TEXT`,
150
+ },
151
+ {
152
+ name: "workspace_display_path",
153
+ sql: `ALTER TABLE interaction_spans ADD COLUMN workspace_display_path TEXT`,
154
+ },
155
+ {
156
+ name: "git_repo_root",
157
+ sql: `ALTER TABLE interaction_spans ADD COLUMN git_repo_root TEXT`,
158
+ },
159
+ {
160
+ name: "git_repo_root_home_rel_path",
161
+ sql: `ALTER TABLE interaction_spans ADD COLUMN git_repo_root_home_rel_path TEXT`,
162
+ },
163
+ {
164
+ name: "git_repo_root_display_path",
165
+ sql: `ALTER TABLE interaction_spans ADD COLUMN git_repo_root_display_path TEXT`,
166
+ },
167
+ {
168
+ name: "git_repo_name",
169
+ sql: `ALTER TABLE interaction_spans ADD COLUMN git_repo_name TEXT`,
170
+ },
171
+ { name: "git_branch", sql: `ALTER TABLE interaction_spans ADD COLUMN git_branch TEXT` },
172
+ { name: "started_at", sql: `ALTER TABLE interaction_spans ADD COLUMN started_at TEXT` },
173
+ { name: "completed_at", sql: `ALTER TABLE interaction_spans ADD COLUMN completed_at TEXT` },
174
+ ]);
80
175
  db.exec(`CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace_path, created_at)`);
81
176
  db.exec(`CREATE INDEX IF NOT EXISTS idx_events_interaction_kind ON events(interaction_kind, created_at)`);
82
177
  db.exec(`CREATE INDEX IF NOT EXISTS idx_events_correlation ON events(correlation_id, session_id)`);
@@ -85,6 +180,9 @@ function migrateEventsSchema(db) {
85
180
  db.exec(`CREATE INDEX IF NOT EXISTS idx_spans_session ON interaction_spans(session_key, started_at)`);
86
181
  }
87
182
  export function insertEvent(db, event, payloadHash, workspaceGit, derived) {
183
+ // Keep raw payloads verbatim for forensics and stable hashes; path redaction is a
184
+ // separate privacy decision from the derived `~/...` metadata stored alongside them.
185
+ const rawPayloadJson = JSON.stringify(event.rawPayload);
88
186
  const stmt = db.prepare(`
89
187
  INSERT INTO events (
90
188
  created_at,
@@ -101,7 +199,11 @@ export function insertEvent(db, event, payloadHash, workspaceGit, derived) {
101
199
  payload_hash,
102
200
  raw_payload,
103
201
  workspace_path,
202
+ workspace_home_rel_path,
203
+ workspace_display_path,
104
204
  git_repo_root,
205
+ git_repo_root_home_rel_path,
206
+ git_repo_root_display_path,
105
207
  git_repo_name,
106
208
  git_branch,
107
209
  interaction_kind,
@@ -112,9 +214,9 @@ export function insertEvent(db, event, payloadHash, workspaceGit, derived) {
112
214
  payload_byte_length,
113
215
  prompt_fingerprint
114
216
  )
115
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
217
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
116
218
  `);
117
- const info = stmt.run(new Date().toISOString(), event.source, event.sourceEvent, event.repoPath ?? null, event.sessionId ?? null, event.turnId ?? null, event.model ?? null, event.role, event.estimatedInputTokens, event.estimatedOutputTokens, event.estimatedTotalTokens, payloadHash, JSON.stringify(event.rawPayload), workspaceGit.workspacePath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoName, workspaceGit.gitBranch, derived.interactionKind, derived.correlationId, derived.toolCanonicalName, derived.mcpServer, derived.mcpTool, derived.payloadByteLength, derived.promptFingerprint);
219
+ const info = stmt.run(new Date().toISOString(), event.source, event.sourceEvent, event.repoPath ?? null, event.sessionId ?? null, event.turnId ?? null, event.model ?? null, event.role, event.estimatedInputTokens, event.estimatedOutputTokens, event.estimatedTotalTokens, payloadHash, rawPayloadJson, workspaceGit.workspacePath, workspaceGit.workspaceHomeRelPath, workspaceGit.workspaceDisplayPath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoRootHomeRelPath, workspaceGit.gitRepoRootDisplayPath, workspaceGit.gitRepoName, workspaceGit.gitBranch, derived.interactionKind, derived.correlationId, derived.toolCanonicalName, derived.mcpServer, derived.mcpTool, derived.payloadByteLength, derived.promptFingerprint);
118
220
  return Number(info.lastInsertRowid);
119
221
  }
120
222
  /**
@@ -143,19 +245,21 @@ export function mergeInteractionSpan(db, eventId, normalized, workspaceGit, deri
143
245
  tool_canonical_name, mcp_server, mcp_tool,
144
246
  pre_event_id, post_event_id, failure_event_id,
145
247
  arg_token_estimate, result_token_estimate,
146
- workspace_path, git_repo_root, git_repo_name, git_branch,
248
+ workspace_path, workspace_home_rel_path, workspace_display_path,
249
+ git_repo_root, git_repo_root_home_rel_path, git_repo_root_display_path,
250
+ git_repo_name, git_branch,
147
251
  started_at, completed_at
148
252
  )
149
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
253
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
150
254
  `);
151
255
  if (derived.toolPhase === "pre") {
152
- ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, eventId, null, null, argTok, 0, workspaceGit.workspacePath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoName, workspaceGit.gitBranch, now, null);
256
+ ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, eventId, null, null, argTok, 0, workspaceGit.workspacePath, workspaceGit.workspaceHomeRelPath, workspaceGit.workspaceDisplayPath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoRootHomeRelPath, workspaceGit.gitRepoRootDisplayPath, workspaceGit.gitRepoName, workspaceGit.gitBranch, now, null);
153
257
  }
154
258
  else if (derived.toolPhase === "post") {
155
- ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, null, eventId, null, 0, resTok, workspaceGit.workspacePath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoName, workspaceGit.gitBranch, null, now);
259
+ ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, null, eventId, null, 0, resTok, workspaceGit.workspacePath, workspaceGit.workspaceHomeRelPath, workspaceGit.workspaceDisplayPath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoRootHomeRelPath, workspaceGit.gitRepoRootDisplayPath, workspaceGit.gitRepoName, workspaceGit.gitBranch, null, now);
156
260
  }
157
261
  else {
158
- ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, null, null, eventId, 0, resTok, workspaceGit.workspacePath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoName, workspaceGit.gitBranch, null, now);
262
+ ins.run(sessionKey, source, derived.correlationId, normalized.turnId ?? null, toolName, mcpS, mcpT, null, null, eventId, 0, resTok, workspaceGit.workspacePath, workspaceGit.workspaceHomeRelPath, workspaceGit.workspaceDisplayPath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoRootHomeRelPath, workspaceGit.gitRepoRootDisplayPath, workspaceGit.gitRepoName, workspaceGit.gitBranch, null, now);
159
263
  }
160
264
  return;
161
265
  }
@@ -164,33 +268,57 @@ export function mergeInteractionSpan(db, eventId, normalized, workspaceGit, deri
164
268
  pre_event_id = COALESCE(pre_event_id, ?),
165
269
  started_at = COALESCE(started_at, ?),
166
270
  arg_token_estimate = ?,
271
+ workspace_path = COALESCE(workspace_path, ?),
272
+ workspace_home_rel_path = COALESCE(workspace_home_rel_path, ?),
273
+ workspace_display_path = COALESCE(workspace_display_path, ?),
274
+ git_repo_root = COALESCE(git_repo_root, ?),
275
+ git_repo_root_home_rel_path = COALESCE(git_repo_root_home_rel_path, ?),
276
+ git_repo_root_display_path = COALESCE(git_repo_root_display_path, ?),
277
+ git_repo_name = COALESCE(git_repo_name, ?),
278
+ git_branch = COALESCE(git_branch, ?),
167
279
  tool_canonical_name = COALESCE(tool_canonical_name, ?),
168
280
  mcp_server = COALESCE(mcp_server, ?),
169
281
  mcp_tool = COALESCE(mcp_tool, ?),
170
282
  turn_id = COALESCE(turn_id, ?)
171
- WHERE id = ?`).run(eventId, now, Math.max(existing.arg_token_estimate, argTok), toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
283
+ WHERE id = ?`).run(eventId, now, Math.max(existing.arg_token_estimate, argTok), workspaceGit.workspacePath, workspaceGit.workspaceHomeRelPath, workspaceGit.workspaceDisplayPath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoRootHomeRelPath, workspaceGit.gitRepoRootDisplayPath, workspaceGit.gitRepoName, workspaceGit.gitBranch, toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
172
284
  }
173
285
  else if (derived.toolPhase === "post") {
174
286
  db.prepare(`UPDATE interaction_spans SET
175
287
  post_event_id = COALESCE(post_event_id, ?),
176
288
  completed_at = COALESCE(completed_at, ?),
177
289
  result_token_estimate = ?,
290
+ workspace_path = COALESCE(workspace_path, ?),
291
+ workspace_home_rel_path = COALESCE(workspace_home_rel_path, ?),
292
+ workspace_display_path = COALESCE(workspace_display_path, ?),
293
+ git_repo_root = COALESCE(git_repo_root, ?),
294
+ git_repo_root_home_rel_path = COALESCE(git_repo_root_home_rel_path, ?),
295
+ git_repo_root_display_path = COALESCE(git_repo_root_display_path, ?),
296
+ git_repo_name = COALESCE(git_repo_name, ?),
297
+ git_branch = COALESCE(git_branch, ?),
178
298
  tool_canonical_name = COALESCE(tool_canonical_name, ?),
179
299
  mcp_server = COALESCE(mcp_server, ?),
180
300
  mcp_tool = COALESCE(mcp_tool, ?),
181
301
  turn_id = COALESCE(turn_id, ?)
182
- WHERE id = ?`).run(eventId, now, Math.max(existing.result_token_estimate, resTok), toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
302
+ WHERE id = ?`).run(eventId, now, Math.max(existing.result_token_estimate, resTok), workspaceGit.workspacePath, workspaceGit.workspaceHomeRelPath, workspaceGit.workspaceDisplayPath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoRootHomeRelPath, workspaceGit.gitRepoRootDisplayPath, workspaceGit.gitRepoName, workspaceGit.gitBranch, toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
183
303
  }
184
304
  else {
185
305
  db.prepare(`UPDATE interaction_spans SET
186
306
  failure_event_id = COALESCE(failure_event_id, ?),
187
307
  completed_at = COALESCE(completed_at, ?),
188
308
  result_token_estimate = ?,
309
+ workspace_path = COALESCE(workspace_path, ?),
310
+ workspace_home_rel_path = COALESCE(workspace_home_rel_path, ?),
311
+ workspace_display_path = COALESCE(workspace_display_path, ?),
312
+ git_repo_root = COALESCE(git_repo_root, ?),
313
+ git_repo_root_home_rel_path = COALESCE(git_repo_root_home_rel_path, ?),
314
+ git_repo_root_display_path = COALESCE(git_repo_root_display_path, ?),
315
+ git_repo_name = COALESCE(git_repo_name, ?),
316
+ git_branch = COALESCE(git_branch, ?),
189
317
  tool_canonical_name = COALESCE(tool_canonical_name, ?),
190
318
  mcp_server = COALESCE(mcp_server, ?),
191
319
  mcp_tool = COALESCE(mcp_tool, ?),
192
320
  turn_id = COALESCE(turn_id, ?)
193
- WHERE id = ?`).run(eventId, now, Math.max(existing.result_token_estimate, resTok), toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
321
+ WHERE id = ?`).run(eventId, now, Math.max(existing.result_token_estimate, resTok), workspaceGit.workspacePath, workspaceGit.workspaceHomeRelPath, workspaceGit.workspaceDisplayPath, workspaceGit.gitRepoRoot, workspaceGit.gitRepoRootHomeRelPath, workspaceGit.gitRepoRootDisplayPath, workspaceGit.gitRepoName, workspaceGit.gitBranch, toolName, mcpS, mcpT, normalized.turnId ?? null, existing.id);
194
322
  }
195
323
  }
196
324
  export function getLastEventSummary(db) {
@@ -237,7 +365,11 @@ export function getEventsForLatestSession(db) {
237
365
  estimated_total_tokens AS estimatedTotalTokens,
238
366
  raw_payload AS rawPayload,
239
367
  workspace_path AS workspacePath,
368
+ workspace_home_rel_path AS workspaceHomeRelPath,
369
+ workspace_display_path AS workspaceDisplayPath,
240
370
  git_repo_root AS gitRepoRoot,
371
+ git_repo_root_home_rel_path AS gitRepoRootHomeRelPath,
372
+ git_repo_root_display_path AS gitRepoRootDisplayPath,
241
373
  git_repo_name AS gitRepoName,
242
374
  git_branch AS gitBranch
243
375
  FROM events
@@ -262,7 +394,11 @@ export function getEventsForLatestSession(db) {
262
394
  estimated_total_tokens AS estimatedTotalTokens,
263
395
  raw_payload AS rawPayload,
264
396
  workspace_path AS workspacePath,
397
+ workspace_home_rel_path AS workspaceHomeRelPath,
398
+ workspace_display_path AS workspaceDisplayPath,
265
399
  git_repo_root AS gitRepoRoot,
400
+ git_repo_root_home_rel_path AS gitRepoRootHomeRelPath,
401
+ git_repo_root_display_path AS gitRepoRootDisplayPath,
266
402
  git_repo_name AS gitRepoName,
267
403
  git_branch AS gitBranch
268
404
  FROM events
@@ -1,3 +1,4 @@
1
+ import os from "node:os";
1
2
  import path from "node:path";
2
3
  import { spawnSync } from "node:child_process";
3
4
  function trimmedOrNull(value) {
@@ -12,6 +13,35 @@ function asRecord(value) {
12
13
  }
13
14
  return {};
14
15
  }
16
+ function toSlashPath(relativePath) {
17
+ return relativePath.split(path.sep).join("/");
18
+ }
19
+ function deriveHomePath(absolutePath) {
20
+ if (!absolutePath) {
21
+ return { homeRelPath: null, displayPath: null };
22
+ }
23
+ const homeDir = trimmedOrNull(os.homedir());
24
+ if (!homeDir) {
25
+ return { homeRelPath: null, displayPath: null };
26
+ }
27
+ const normalizedHome = path.normalize(homeDir);
28
+ const normalizedPath = path.normalize(absolutePath);
29
+ const relativePath = path.relative(normalizedHome, normalizedPath);
30
+ if (!relativePath) {
31
+ return { homeRelPath: ".", displayPath: "~" };
32
+ }
33
+ if (path.isAbsolute(relativePath)) {
34
+ return { homeRelPath: null, displayPath: null };
35
+ }
36
+ if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) {
37
+ return { homeRelPath: null, displayPath: null };
38
+ }
39
+ const slashPath = toSlashPath(relativePath);
40
+ return {
41
+ homeRelPath: slashPath,
42
+ displayPath: `~/${slashPath}`,
43
+ };
44
+ }
15
45
  /**
16
46
  * Absolute path for “where this event ran”: adapter repoPath, then common payload cwd keys, then process.cwd().
17
47
  */
@@ -52,22 +82,33 @@ function gitOutput(workspacePath, args) {
52
82
  * Best-effort git context for `workspacePath`. Non-repo → git* fields null; `workspacePath` still set.
53
83
  */
54
84
  export function resolveWorkspaceGitMeta(workspacePath) {
55
- const inside = gitOutput(workspacePath, ["rev-parse", "--is-inside-work-tree"]);
85
+ const normalizedWorkspacePath = path.normalize(workspacePath);
86
+ const workspaceHomePath = deriveHomePath(normalizedWorkspacePath);
87
+ const inside = gitOutput(normalizedWorkspacePath, ["rev-parse", "--is-inside-work-tree"]);
56
88
  if (inside !== "true") {
57
89
  return {
58
- workspacePath,
90
+ workspacePath: normalizedWorkspacePath,
91
+ workspaceHomeRelPath: workspaceHomePath.homeRelPath,
92
+ workspaceDisplayPath: workspaceHomePath.displayPath,
59
93
  gitRepoRoot: null,
94
+ gitRepoRootHomeRelPath: null,
95
+ gitRepoRootDisplayPath: null,
60
96
  gitRepoName: null,
61
97
  gitBranch: null,
62
98
  };
63
99
  }
64
- const root = gitOutput(workspacePath, ["rev-parse", "--show-toplevel"]);
65
- const branch = gitOutput(workspacePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
100
+ const root = gitOutput(normalizedWorkspacePath, ["rev-parse", "--show-toplevel"]);
101
+ const branch = gitOutput(normalizedWorkspacePath, ["rev-parse", "--abbrev-ref", "HEAD"]);
66
102
  const gitRepoRoot = root ? path.normalize(root) : null;
103
+ const gitRepoRootHomePath = deriveHomePath(gitRepoRoot);
67
104
  const gitRepoName = gitRepoRoot ? path.basename(gitRepoRoot) : null;
68
105
  return {
69
- workspacePath,
106
+ workspacePath: normalizedWorkspacePath,
107
+ workspaceHomeRelPath: workspaceHomePath.homeRelPath,
108
+ workspaceDisplayPath: workspaceHomePath.displayPath,
70
109
  gitRepoRoot,
110
+ gitRepoRootHomeRelPath: gitRepoRootHomePath.homeRelPath,
111
+ gitRepoRootDisplayPath: gitRepoRootHomePath.displayPath,
71
112
  gitRepoName,
72
113
  gitBranch: branch,
73
114
  };
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ let cachedPackageMeta = null;
5
+ function readPackageMeta() {
6
+ if (cachedPackageMeta)
7
+ return cachedPackageMeta;
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
9
+ const packageJsonPath = path.join(packageRoot, "package.json");
10
+ try {
11
+ cachedPackageMeta = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
12
+ }
13
+ catch {
14
+ cachedPackageMeta = {};
15
+ }
16
+ return cachedPackageMeta;
17
+ }
18
+ export function getPackageVersion() {
19
+ return readPackageMeta().version ?? "0.0.0";
20
+ }
@@ -14,7 +14,11 @@ CREATE TABLE IF NOT EXISTS events (
14
14
  payload_hash TEXT NOT NULL,
15
15
  raw_payload TEXT NOT NULL,
16
16
  workspace_path TEXT,
17
+ workspace_home_rel_path TEXT,
18
+ workspace_display_path TEXT,
17
19
  git_repo_root TEXT,
20
+ git_repo_root_home_rel_path TEXT,
21
+ git_repo_root_display_path TEXT,
18
22
  git_repo_name TEXT,
19
23
  git_branch TEXT,
20
24
  interaction_kind TEXT,
@@ -41,7 +45,11 @@ CREATE TABLE IF NOT EXISTS interaction_spans (
41
45
  arg_token_estimate INTEGER DEFAULT 0,
42
46
  result_token_estimate INTEGER DEFAULT 0,
43
47
  workspace_path TEXT,
48
+ workspace_home_rel_path TEXT,
49
+ workspace_display_path TEXT,
44
50
  git_repo_root TEXT,
51
+ git_repo_root_home_rel_path TEXT,
52
+ git_repo_root_display_path TEXT,
45
53
  git_repo_name TEXT,
46
54
  git_branch TEXT,
47
55
  started_at TEXT,