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 +239 -0
- package/dist/cli.js +108 -0
- package/dist/consoleUi.js +74 -0
- package/dist/db.js +158 -0
- package/dist/otlpReceiver.js +415 -0
- package/dist/proxy.js +231 -0
- package/dist/setup.js +216 -0
- package/dist/types.js +1 -0
- package/dist/webServer.js +57 -0
- package/package.json +63 -0
- package/web/index.html +407 -0
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
|
+
}
|