copilot-tracer 1.0.1

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,239 @@
1
+ # copilot-tracer
2
+
3
+ Real-time tracing and monitoring tool for **GitHub Copilot CLI** and **VS Code Copilot extension**.
4
+
5
+ Captures every prompt, response, token usage, AI credits, tool calls, skill invocations and duration — all in one place. Works via native **OpenTelemetry (OTLP)** integration built into GitHub Copilot. No wrapper, no binary replacement, no ACP proxy needed.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Zero-intrusion capture** — uses Copilot's built-in OTel support. Set 2 env vars, done.
12
+ - **Works everywhere** — captures both Copilot CLI (`copilot -p "..."`) and VS Code Copilot Chat
13
+ - **Real-time web UI** — live dashboard at `http://localhost:4747` with dark theme
14
+ - **Full prompt & response** — see exactly what you sent and what Copilot replied
15
+ - **Token breakdown** — input, output, cached, reasoning, written tokens per request
16
+ - **AI Credits tracking** — matches exactly what Copilot terminal reports (e.g. `2.59 cr`)
17
+ - **Tool call visibility** — see every tool/skill/MCP invoked during a session
18
+ - **Persistent storage** — SQLite at `~/.copilot-tracer/traces.db`, survives restarts
19
+ - **Console + Web UI** — CLI table view or browser dashboard, your choice
20
+
21
+ ---
22
+
23
+ ## Prerequisites
24
+
25
+ - Node.js v18+ (tested on v24)
26
+ - GitHub Copilot CLI (`copilot` command available in terminal)
27
+ - VS Code 1.99+ with built-in Copilot (no extension install needed)
28
+
29
+ ---
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install -g copilot-tracer
35
+ ```
36
+
37
+ Requires Node.js v18+. The `copilot-tracer` command is available globally after install.
38
+
39
+ > **Building from source**
40
+ > ```bash
41
+ > git clone https://github.com/chuongnd/copilot-tracer
42
+ > cd copilot-tracer && npm install && npx tsc
43
+ > npm link # registers global command from local build
44
+ > ```
45
+
46
+ ---
47
+
48
+ ## Setup (one-time)
49
+
50
+ Run the auto-setup command. It detects your Copilot CLI and VS Code installation and injects the required config automatically:
51
+
52
+ ```bash
53
+ copilot-tracer --setup
54
+ ```
55
+
56
+ What it does:
57
+ - Detects `copilot` CLI path and version
58
+ - Detects VS Code version and confirms built-in Copilot
59
+ - Patches `~/.zshrc` (or `~/.bashrc`) with OTEL env vars
60
+ - Patches VS Code `settings.json` with `terminal.integrated.env.osx` block
61
+
62
+ Example output:
63
+ ```
64
+ ✅ GitHub Copilot CLI detected — /usr/bin/copilot v1.0.77
65
+ ✅ Visual Studio Code detected — 1.131.0 (Built-in Copilot)
66
+ ✅ Shell profile patched: .zshrc
67
+ ✅ VS Code settings patched
68
+ ```
69
+
70
+ Then apply the env vars:
71
+
72
+ ```bash
73
+ source ~/.zshrc
74
+ ```
75
+
76
+ Restart VS Code completely (Cmd+Q, then reopen).
77
+
78
+ ---
79
+
80
+ ## Manual Setup (alternative)
81
+
82
+ If you prefer to configure manually, add these to `~/.zshrc`:
83
+
84
+ ```bash
85
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4747
86
+ export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
87
+ export COPILOT_OTEL_ENABLED=true
88
+ ```
89
+
90
+ For VS Code, add to `~/Library/Application Support/Code/User/settings.json`:
91
+
92
+ ```json
93
+ "terminal.integrated.env.osx": {
94
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4747",
95
+ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true",
96
+ "COPILOT_OTEL_ENABLED": "true"
97
+ }
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Start Tracer
103
+
104
+ ```bash
105
+ copilot-tracer --ui web --port 4747 --no-proxy
106
+ ```
107
+
108
+ Open **http://localhost:4747** — shows "waiting for copilot CLI activity".
109
+
110
+ ---
111
+
112
+ ## Use Copilot Normally
113
+
114
+ No change to how you use Copilot. Just run as usual:
115
+
116
+ ```bash
117
+ # Copilot CLI
118
+ copilot -p "how to convert microservice to modular" --allow-all-tools
119
+
120
+ # Or use Copilot Chat in VS Code
121
+ ```
122
+
123
+ Traces appear instantly in the web UI as each request completes.
124
+
125
+ ---
126
+
127
+ ## How It Works
128
+
129
+ ```
130
+ copilot CLI / VS Code Copilot Chat
131
+ |
132
+ | reads OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4747
133
+ |
134
+ ↓ POST /v1/traces (OpenTelemetry OTLP JSON)
135
+ copilot-tracer OTLP receiver
136
+ |
137
+
138
+ Parse spans → extract prompt, response, tokens, credits, tool calls
139
+ |
140
+
141
+ SQLite DB (~/.copilot-tracer/traces.db)
142
+ |
143
+
144
+ Web UI (Socket.io real-time) + Console table
145
+ ```
146
+
147
+ Copilot has built-in OpenTelemetry instrumentation. When `OTEL_EXPORTER_OTLP_ENDPOINT` is set, it pushes all trace data to that endpoint automatically — both CLI and VS Code extension.
148
+
149
+ ---
150
+
151
+ ## Web UI
152
+
153
+ Open http://localhost:4747 after starting the tracer.
154
+
155
+ **Table columns:**
156
+ | Date/Time | Prompt | AI Credits | Duration | Cached | Written | Reasoning | Skills | Agents | MCPs |
157
+
158
+ **Interactive features:**
159
+ - Click any row → detail panel: full prompt, full response, reasoning text, call graph
160
+ - Click AI Credits → cost breakdown per token type
161
+ - Click Reasoning count → full reasoning text
162
+ - Click Skills / Agents / MCPs pill → filtered call list with input/output/duration
163
+ - Real-time updates via Socket.io — no page refresh needed
164
+ - Dark theme
165
+
166
+ ---
167
+
168
+ ## Console UI
169
+
170
+ ```bash
171
+ copilot-tracer --ui console --no-proxy
172
+ ```
173
+
174
+ Live updating table in terminal. Same columns as web UI. TOTALS row pinned at top.
175
+
176
+ ---
177
+
178
+ ## CLI Flags
179
+
180
+ | Flag | Description |
181
+ |------|-------------|
182
+ | `--ui web` | Start web UI (default) |
183
+ | `--ui console` | Start console table UI |
184
+ | `--ui both` | Both web + console |
185
+ | `--port 4747` | Web UI port (default: 4747) |
186
+ | `--no-proxy` | Web/console only, no ACP proxy |
187
+ | `--session <id>` | Custom session ID |
188
+ | `--setup` | Auto-detect and configure env vars |
189
+ | `--debug` | Verbose logging |
190
+
191
+ ---
192
+
193
+ ## Storage
194
+
195
+ Traces persist to `~/.copilot-tracer/traces.db` (SQLite). Safe to keep across sessions.
196
+
197
+ To test the web UI without running a live Copilot session:
198
+
199
+ ```bash
200
+ node test-seed.mjs # seeds 4 sample traces (source build only)
201
+ copilot-tracer --ui web --no-proxy --session test-session-001
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Build & Publish
207
+
208
+ ```bash
209
+ npm install
210
+ npx tsc # compile to dist/
211
+ ```
212
+
213
+ To publish a new release to npm:
214
+
215
+ ```bash
216
+ # Bump patch version (1.0.0 → 1.0.1), build, publish, git-tag
217
+ bash scripts/publish.sh
218
+
219
+ # Bump minor version
220
+ bash scripts/publish.sh minor
221
+
222
+ # Publish a beta pre-release
223
+ bash scripts/publish.sh --tag beta --pre beta
224
+ ```
225
+
226
+ The script will:
227
+ 1. Check npm authentication (`npm login` required first)
228
+ 2. Verify git working tree is clean
229
+ 3. Type-check + build
230
+ 4. Verify `better-sqlite3` native module loads
231
+ 5. Show files that will be published (dry-run preview)
232
+ 6. Prompt for confirmation
233
+ 7. `npm publish`, commit the version bump, and create a git tag
234
+
235
+ After publishing:
236
+
237
+ ```bash
238
+ git push && git push origin v<new-version>
239
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import { spawn } from 'child_process';
4
+ import { randomUUID } from 'crypto';
5
+ import readline from 'readline';
6
+ import fs from 'fs';
7
+ import { createSession, getTraces, getSessionSummary } from './db.js';
8
+ import { handleAcpMessage, traceEvents } from './proxy.js';
9
+ import { renderConsoleTable } from './consoleUi.js';
10
+ import { startWebServer } from './webServer.js';
11
+ import { runSetup } from './setup.js';
12
+ import open from 'open';
13
+ program
14
+ .name('copilot-tracer')
15
+ .description('Real-time monitor and tracer for GitHub Copilot CLI')
16
+ .option('--ui <mode>', 'UI mode: console | web | both', 'both')
17
+ .option('--port <port>', 'Web UI port', '4747')
18
+ .option('--cmd <command>', 'Copilot CLI command to wrap', 'copilot')
19
+ .option('--no-proxy', 'Run UI only (no ACP proxy, read from DB)')
20
+ .option('--session <id>', 'Filter by session ID')
21
+ .option('--debug', 'Dump ALL raw ACP messages to stderr (use to discover real method names)')
22
+ .option('--setup', 'Auto-detect copilot CLI + VS Code and configure OTLP env vars')
23
+ .allowUnknownOption()
24
+ .parse();
25
+ const opts = program.opts();
26
+ const port = parseInt(opts.port);
27
+ // Handle --setup before anything else
28
+ if (opts.setup) {
29
+ runSetup(port);
30
+ process.exit(0);
31
+ }
32
+ const sessionId = opts.session || randomUUID();
33
+ createSession(sessionId);
34
+ console.log(`\n 🤖 Copilot Tracer | Session: ${sessionId}\n`);
35
+ // Start Web UI
36
+ if (opts.ui === 'web' || opts.ui === 'both') {
37
+ startWebServer(port, sessionId);
38
+ setTimeout(() => open(`http://localhost:${port}`), 1500);
39
+ }
40
+ // Start Console UI refresh loop
41
+ if (opts.ui === 'console' || opts.ui === 'both') {
42
+ const refreshConsole = () => {
43
+ const entries = getTraces(sessionId, 50);
44
+ const summary = getSessionSummary(sessionId);
45
+ renderConsoleTable(entries, summary ?? undefined);
46
+ };
47
+ traceEvents.on('trace:update', refreshConsole);
48
+ traceEvents.on('trace:done', refreshConsole);
49
+ refreshConsole();
50
+ }
51
+ // ACP Proxy — wrap copilot CLI
52
+ if (opts.proxy !== false) {
53
+ const copilotArgs = ['--acp', '--stdio', ...program.args];
54
+ const child = spawn(opts.cmd, copilotArgs, {
55
+ stdio: ['pipe', 'pipe', 'inherit'],
56
+ });
57
+ if (!child.pid) {
58
+ console.error(`\n ❌ Failed to start: ${opts.cmd} ${copilotArgs.join(' ')}`);
59
+ console.error(' Make sure GitHub Copilot CLI is installed: npm install -g @github/copilot-cli\n');
60
+ process.exit(1);
61
+ }
62
+ // Parse newline-delimited JSON (NDJSON) from copilot
63
+ const stdinRl = readline.createInterface({ input: process.stdin });
64
+ const stdoutRl = readline.createInterface({ input: child.stdout });
65
+ const debug = opts.debug === true;
66
+ const logFile = debug ? fs.createWriteStream(`/tmp/copilot-tracer-${sessionId.slice(0, 8)}.ndjson`, { flags: 'a' }) : null;
67
+ function debugLog(direction, raw, parsed) {
68
+ if (!debug)
69
+ return;
70
+ const entry = JSON.stringify({ ts: new Date().toISOString(), dir: direction, raw, parsed });
71
+ process.stderr.write('[TRACER] ' + entry + '\n');
72
+ logFile?.write(entry + '\n');
73
+ }
74
+ // stdin → copilot (user → copilot = 'out' direction from user's perspective)
75
+ stdinRl.on('line', (line) => {
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(line);
79
+ handleAcpMessage(sessionId, parsed, 'out'); // outbound = user sending to copilot
80
+ }
81
+ catch (e) {
82
+ // Not JSON — plain text from terminal, not ACP
83
+ if (debug)
84
+ process.stderr.write(`[TRACER] stdin non-JSON: ${line}\n`);
85
+ }
86
+ debugLog('→ copilot', line, parsed);
87
+ child.stdin.write(line + '\n');
88
+ });
89
+ // copilot → stdout (copilot → user = 'in' direction from user's perspective)
90
+ stdoutRl.on('line', (line) => {
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(line);
94
+ handleAcpMessage(sessionId, parsed, 'in'); // inbound = copilot sending to user
95
+ }
96
+ catch (e) {
97
+ if (debug)
98
+ process.stderr.write(`[TRACER] stdout non-JSON: ${line}\n`);
99
+ }
100
+ debugLog('← copilot', line, parsed);
101
+ process.stdout.write(line + '\n');
102
+ });
103
+ child.on('exit', (code) => {
104
+ console.log(`\n Copilot CLI exited (code ${code})\n`);
105
+ process.exit(code ?? 0);
106
+ });
107
+ process.on('SIGINT', () => { child.kill(); process.exit(0); });
108
+ }
@@ -0,0 +1,74 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { format } from 'date-fns';
4
+ function truncate(str, len) {
5
+ return str.length > len ? str.slice(0, len - 1) + '…' : str;
6
+ }
7
+ function fmtDuration(ms) {
8
+ if (ms < 1000)
9
+ return `${ms}ms`;
10
+ return `${(ms / 1000).toFixed(1)}s`;
11
+ }
12
+ function fmtCredits(c) {
13
+ return `${c.toFixed(2)} cr | $${(c * 0.01).toFixed(4)}`;
14
+ }
15
+ function statusColor(status, text) {
16
+ if (status === 'running')
17
+ return chalk.yellow(text);
18
+ if (status === 'error')
19
+ return chalk.red(text);
20
+ return chalk.green(text);
21
+ }
22
+ export function renderConsoleTable(entries, summary) {
23
+ console.clear();
24
+ const table = new Table({
25
+ head: [
26
+ chalk.cyan('Date / Time'),
27
+ chalk.cyan('Prompt'),
28
+ chalk.cyan('AI Credits'),
29
+ chalk.cyan('Duration'),
30
+ chalk.cyan('Tokens\nCached|Written|Reason'),
31
+ chalk.cyan('Skills'),
32
+ chalk.cyan('Agents'),
33
+ chalk.cyan('MCPs'),
34
+ ],
35
+ colWidths: [20, 40, 12, 10, 26, 8, 8, 8],
36
+ style: { head: [], border: ['grey'] },
37
+ wordWrap: true,
38
+ });
39
+ // TOTALS row (right after header)
40
+ if (summary) {
41
+ const t = summary.totalTokens;
42
+ table.push([
43
+ chalk.bold.white('TOTALS'),
44
+ chalk.bold.white(`${summary.totalEntries} prompts`),
45
+ chalk.bold.yellow(fmtCredits(summary.totalCredits)),
46
+ chalk.bold.white(fmtDuration(summary.totalDurationMs)),
47
+ chalk.bold.white(`${t.cached} | ${t.written} | ${t.reasoning}`),
48
+ chalk.bold.magenta(String(summary.totalSkillCalls)),
49
+ chalk.bold.blue(String(summary.totalAgentCalls)),
50
+ chalk.bold.cyan(String(summary.totalMcpCalls)),
51
+ ]);
52
+ // divider
53
+ table.push([{ colSpan: 8, content: chalk.grey('─'.repeat(130)) }]);
54
+ }
55
+ // Data rows
56
+ for (const e of entries) {
57
+ const dt = format(new Date(e.dateTime), 'MM-dd HH:mm:ss');
58
+ const tokenStr = `${e.tokens.cached} | ${e.tokens.written} | ${e.tokens.reasoning}`;
59
+ const tools = e.toolCalls.map(c => c.name).join(', ');
60
+ table.push([
61
+ statusColor(e.status, dt),
62
+ truncate(e.prompt, 38),
63
+ chalk.yellow(fmtCredits(e.aiCredits)),
64
+ fmtDuration(e.durationMs),
65
+ tokenStr + (tools ? chalk.grey(`\n[${truncate(tools, 22)}]`) : ''),
66
+ chalk.magenta(String(e.skillCount)),
67
+ chalk.blue(String(e.agentCount)),
68
+ chalk.cyan(String(e.mcpCount)),
69
+ ]);
70
+ }
71
+ console.log(chalk.bold.white('\n 🤖 COPILOT TRACER — Real-time Monitor\n'));
72
+ console.log(table.toString());
73
+ console.log(chalk.grey(` Last updated: ${format(new Date(), 'HH:mm:ss')} | DB: ~/.copilot-tracer/traces.db\n`));
74
+ }
package/dist/db.js ADDED
@@ -0,0 +1,158 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fs from 'fs';
5
+ const DB_DIR = path.join(os.homedir(), '.copilot-tracer');
6
+ const DB_PATH = path.join(DB_DIR, 'traces.db');
7
+ if (!fs.existsSync(DB_DIR))
8
+ fs.mkdirSync(DB_DIR, { recursive: true });
9
+ const db = new Database(DB_PATH);
10
+ db.exec(`
11
+ CREATE TABLE IF NOT EXISTS sessions (
12
+ id TEXT PRIMARY KEY,
13
+ started_at TEXT NOT NULL,
14
+ ended_at TEXT
15
+ );
16
+
17
+ CREATE TABLE IF NOT EXISTS traces (
18
+ id TEXT PRIMARY KEY,
19
+ session_id TEXT NOT NULL,
20
+ date_time TEXT NOT NULL,
21
+ prompt TEXT NOT NULL,
22
+ response TEXT,
23
+ reasoning TEXT,
24
+ tokens_input INTEGER DEFAULT 0,
25
+ tokens_output INTEGER DEFAULT 0,
26
+ tokens_cached INTEGER DEFAULT 0,
27
+ tokens_reasoning INTEGER DEFAULT 0,
28
+ tokens_written INTEGER DEFAULT 0,
29
+ tokens_total INTEGER DEFAULT 0,
30
+ ai_credits REAL DEFAULT 0,
31
+ duration_ms INTEGER DEFAULT 0,
32
+ tool_calls TEXT DEFAULT '[]',
33
+ skill_count INTEGER DEFAULT 0,
34
+ agent_count INTEGER DEFAULT 0,
35
+ mcp_count INTEGER DEFAULT 0,
36
+ status TEXT DEFAULT 'running',
37
+ error TEXT,
38
+ FOREIGN KEY(session_id) REFERENCES sessions(id)
39
+ );
40
+ `);
41
+ export function createSession(id) {
42
+ db.prepare('INSERT OR REPLACE INTO sessions (id, started_at) VALUES (?, ?)').run(id, new Date().toISOString());
43
+ }
44
+ export function upsertTrace(entry) {
45
+ db.prepare(`
46
+ INSERT OR REPLACE INTO traces (
47
+ id, session_id, date_time, prompt, response, reasoning,
48
+ tokens_input, tokens_output, tokens_cached, tokens_reasoning, tokens_written, tokens_total,
49
+ ai_credits, duration_ms, tool_calls,
50
+ skill_count, agent_count, mcp_count, status, error
51
+ ) VALUES (
52
+ @id, @sessionId, @dateTime, @prompt, @response, @reasoning,
53
+ @tokensInput, @tokensOutput, @tokensCached, @tokensReasoning, @tokensWritten, @tokensTotal,
54
+ @aiCredits, @durationMs, @toolCalls,
55
+ @skillCount, @agentCount, @mcpCount, @status, @error
56
+ )
57
+ `).run({
58
+ id: entry.id,
59
+ sessionId: entry.sessionId,
60
+ dateTime: entry.dateTime,
61
+ prompt: entry.prompt,
62
+ response: entry.response ?? null,
63
+ reasoning: entry.reasoning ?? null,
64
+ tokensInput: entry.tokens.input,
65
+ tokensOutput: entry.tokens.output,
66
+ tokensCached: entry.tokens.cached,
67
+ tokensReasoning: entry.tokens.reasoning,
68
+ tokensWritten: entry.tokens.written,
69
+ tokensTotal: entry.tokens.total,
70
+ aiCredits: entry.aiCredits,
71
+ durationMs: entry.durationMs,
72
+ toolCalls: JSON.stringify(entry.toolCalls),
73
+ skillCount: entry.skillCount,
74
+ agentCount: entry.agentCount,
75
+ mcpCount: entry.mcpCount,
76
+ status: entry.status,
77
+ error: entry.error ?? null,
78
+ });
79
+ }
80
+ export function getTraces(sessionId, limit = 100) {
81
+ const rows = sessionId
82
+ ? db.prepare('SELECT * FROM traces WHERE session_id = ? ORDER BY date_time DESC LIMIT ?').all(sessionId, limit)
83
+ : db.prepare('SELECT * FROM traces ORDER BY date_time DESC LIMIT ?').all(limit);
84
+ return rows.map((r) => rowToEntry(r));
85
+ }
86
+ export function getTrace(id) {
87
+ const row = db.prepare('SELECT * FROM traces WHERE id = ?').get(id);
88
+ return row ? rowToEntry(row) : null;
89
+ }
90
+ export function getSessionSummary(sessionId) {
91
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
92
+ if (!session)
93
+ return null;
94
+ const stats = db.prepare(`
95
+ SELECT
96
+ COUNT(*) as entries,
97
+ SUM(tokens_input) as input,
98
+ SUM(tokens_output) as output,
99
+ SUM(tokens_cached) as cached,
100
+ SUM(tokens_reasoning) as reasoning,
101
+ SUM(tokens_written) as written,
102
+ SUM(tokens_total) as total,
103
+ SUM(ai_credits) as credits,
104
+ SUM(duration_ms) as duration,
105
+ SUM(skill_count) as skills,
106
+ SUM(agent_count) as agents,
107
+ SUM(mcp_count) as mcps
108
+ FROM traces WHERE session_id = ?
109
+ `).get(sessionId);
110
+ const tokens = {
111
+ input: stats.input || 0,
112
+ output: stats.output || 0,
113
+ cached: stats.cached || 0,
114
+ reasoning: stats.reasoning || 0,
115
+ written: stats.written || 0,
116
+ total: stats.total || 0,
117
+ };
118
+ return {
119
+ sessionId,
120
+ startedAt: session.started_at,
121
+ totalEntries: stats.entries || 0,
122
+ totalTokens: tokens,
123
+ totalCredits: stats.credits || 0,
124
+ totalDurationMs: stats.duration || 0,
125
+ totalSkillCalls: stats.skills || 0,
126
+ totalAgentCalls: stats.agents || 0,
127
+ totalMcpCalls: stats.mcps || 0,
128
+ };
129
+ }
130
+ function rowToEntry(row) {
131
+ return {
132
+ id: row.id,
133
+ sessionId: row.session_id,
134
+ dateTime: row.date_time,
135
+ prompt: row.prompt,
136
+ response: row.response,
137
+ reasoning: row.reasoning,
138
+ tokens: {
139
+ input: row.tokens_input || 0,
140
+ output: row.tokens_output || 0,
141
+ cached: row.tokens_cached || 0,
142
+ reasoning: row.tokens_reasoning || 0,
143
+ written: row.tokens_written || 0,
144
+ total: row.tokens_total || 0,
145
+ },
146
+ aiCredits: row.ai_credits || 0,
147
+ durationMs: row.duration_ms || 0,
148
+ toolCalls: JSON.parse(row.tool_calls || '[]'),
149
+ skillCount: row.skill_count || 0,
150
+ agentCount: row.agent_count || 0,
151
+ mcpCount: row.mcp_count || 0,
152
+ status: row.status,
153
+ error: row.error,
154
+ };
155
+ }
156
+ export function deleteTrace(id) {
157
+ db.prepare('DELETE FROM traces WHERE id = ?').run(id);
158
+ }