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/dist/setup.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* copilot-tracer setup — auto-detect copilot CLI + VS Code and inject OTLP env config
|
|
3
|
+
*
|
|
4
|
+
* What it does:
|
|
5
|
+
* 1. Detect copilot CLI (which copilot)
|
|
6
|
+
* 2. Detect VS Code installation + built-in copilot (v1.99+)
|
|
7
|
+
* 3. Inject OTEL env vars into:
|
|
8
|
+
* - Shell profile (~/.zshrc / ~/.bashrc / ~/.zprofile)
|
|
9
|
+
* - VS Code settings.json (terminal.integrated.env.osx)
|
|
10
|
+
* 4. Print a summary and next steps
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
const OTEL_ENDPOINT_KEY = 'OTEL_EXPORTER_OTLP_ENDPOINT';
|
|
17
|
+
const OTEL_CONTENT_KEY = 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT';
|
|
18
|
+
const OTEL_ENABLED_KEY = 'COPILOT_OTEL_ENABLED';
|
|
19
|
+
function otelEnvBlock(port) {
|
|
20
|
+
return [
|
|
21
|
+
`# >>> copilot-tracer OTLP config (auto-added) >>>`,
|
|
22
|
+
`export ${OTEL_ENDPOINT_KEY}=http://localhost:${port}`,
|
|
23
|
+
`export ${OTEL_CONTENT_KEY}=true`,
|
|
24
|
+
`export ${OTEL_ENABLED_KEY}=true`,
|
|
25
|
+
`# <<< copilot-tracer <<<`,
|
|
26
|
+
].join('\n');
|
|
27
|
+
}
|
|
28
|
+
function vscodeEnvBlock(port) {
|
|
29
|
+
return {
|
|
30
|
+
[OTEL_ENDPOINT_KEY]: `http://localhost:${port}`,
|
|
31
|
+
[OTEL_CONTENT_KEY]: 'true',
|
|
32
|
+
[OTEL_ENABLED_KEY]: 'true',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// ── Detection helpers ─────────────────────────────────────────────────────────
|
|
36
|
+
function detectCopilotCli() {
|
|
37
|
+
try {
|
|
38
|
+
const p = execSync('which copilot', { encoding: 'utf8' }).trim();
|
|
39
|
+
const v = execSync('copilot --version 2>/dev/null || true', { encoding: 'utf8' }).trim();
|
|
40
|
+
return { found: true, path: p, version: v.split('\n')[0] };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { found: false };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function detectVSCode() {
|
|
47
|
+
try {
|
|
48
|
+
const v = execSync('code --version 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
49
|
+
const lines = v.split('\n');
|
|
50
|
+
const version = lines[0];
|
|
51
|
+
const major = parseInt(version.split('.')[0], 10);
|
|
52
|
+
const minor = parseInt(version.split('.')[1], 10);
|
|
53
|
+
// Copilot built-in since VS Code 1.99
|
|
54
|
+
const hasBuiltinCopilot = major > 1 || (major === 1 && minor >= 99);
|
|
55
|
+
return { found: true, version, hasBuiltinCopilot };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Try app bundle directly
|
|
59
|
+
const appPath = '/Applications/Visual Studio Code.app';
|
|
60
|
+
if (fs.existsSync(appPath)) {
|
|
61
|
+
return { found: true, hasBuiltinCopilot: true, version: 'unknown (app found)' };
|
|
62
|
+
}
|
|
63
|
+
return { found: false, hasBuiltinCopilot: false };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function detectShellProfile() {
|
|
67
|
+
const candidates = [
|
|
68
|
+
path.join(os.homedir(), '.zshrc'),
|
|
69
|
+
path.join(os.homedir(), '.zprofile'),
|
|
70
|
+
path.join(os.homedir(), '.bash_profile'),
|
|
71
|
+
path.join(os.homedir(), '.bashrc'),
|
|
72
|
+
];
|
|
73
|
+
for (const p of candidates) {
|
|
74
|
+
if (fs.existsSync(p))
|
|
75
|
+
return p;
|
|
76
|
+
}
|
|
77
|
+
// Default to .zshrc (create it)
|
|
78
|
+
return path.join(os.homedir(), '.zshrc');
|
|
79
|
+
}
|
|
80
|
+
function getVSCodeSettingsPath() {
|
|
81
|
+
return path.join(os.homedir(), 'Library/Application Support/Code/User/settings.json');
|
|
82
|
+
}
|
|
83
|
+
// ── Patchers ──────────────────────────────────────────────────────────────────
|
|
84
|
+
function patchShellProfile(profilePath, port) {
|
|
85
|
+
const content = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
|
|
86
|
+
const block = otelEnvBlock(port);
|
|
87
|
+
// Already has our block?
|
|
88
|
+
if (content.includes('copilot-tracer OTLP config')) {
|
|
89
|
+
// Check if port matches
|
|
90
|
+
if (content.includes(`http://localhost:${port}`)) {
|
|
91
|
+
return { action: 'already_set' };
|
|
92
|
+
}
|
|
93
|
+
// Port changed — update
|
|
94
|
+
const updated = content.replace(/# >>> copilot-tracer OTLP config[\s\S]*?# <<< copilot-tracer <<</, block);
|
|
95
|
+
fs.writeFileSync(profilePath, updated, 'utf8');
|
|
96
|
+
return { action: 'updated' };
|
|
97
|
+
}
|
|
98
|
+
// Append
|
|
99
|
+
const newContent = content.trimEnd() + '\n\n' + block + '\n';
|
|
100
|
+
fs.writeFileSync(profilePath, newContent, 'utf8');
|
|
101
|
+
return { action: 'added' };
|
|
102
|
+
}
|
|
103
|
+
function patchVSCodeSettings(settingsPath, port) {
|
|
104
|
+
if (!fs.existsSync(settingsPath)) {
|
|
105
|
+
return { action: 'skipped', reason: 'settings.json not found' };
|
|
106
|
+
}
|
|
107
|
+
let settings;
|
|
108
|
+
try {
|
|
109
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { action: 'skipped', reason: 'could not parse settings.json' };
|
|
113
|
+
}
|
|
114
|
+
const envKey = 'terminal.integrated.env.osx';
|
|
115
|
+
const existing = (settings[envKey] ?? {});
|
|
116
|
+
const newEnv = vscodeEnvBlock(port);
|
|
117
|
+
// Check if already set correctly
|
|
118
|
+
if (existing[OTEL_ENDPOINT_KEY] === `http://localhost:${port}` &&
|
|
119
|
+
existing[OTEL_CONTENT_KEY] === 'true' &&
|
|
120
|
+
existing[OTEL_ENABLED_KEY] === 'true') {
|
|
121
|
+
return { action: 'already_set' };
|
|
122
|
+
}
|
|
123
|
+
const wasSet = !!existing[OTEL_ENDPOINT_KEY];
|
|
124
|
+
settings[envKey] = { ...existing, ...newEnv };
|
|
125
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
126
|
+
return { action: wasSet ? 'updated' : 'added' };
|
|
127
|
+
}
|
|
128
|
+
// ── Main setup ────────────────────────────────────────────────────────────────
|
|
129
|
+
export function runSetup(port) {
|
|
130
|
+
const CHECK = '✅';
|
|
131
|
+
const WARN = '⚠️ ';
|
|
132
|
+
const INFO = '📍';
|
|
133
|
+
const ARROW = '→';
|
|
134
|
+
console.log('\n╔════════════════════════════════════════════════╗');
|
|
135
|
+
console.log('║ Copilot Tracer — Auto Setup ║');
|
|
136
|
+
console.log('╚════════════════════════════════════════════════╝\n');
|
|
137
|
+
// 1. Detect copilot CLI
|
|
138
|
+
const cli = detectCopilotCli();
|
|
139
|
+
if (cli.found) {
|
|
140
|
+
console.log(`${CHECK} GitHub Copilot CLI detected`);
|
|
141
|
+
console.log(` ${INFO} Path : ${cli.path}`);
|
|
142
|
+
if (cli.version)
|
|
143
|
+
console.log(` ${INFO} Version: ${cli.version}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log(`${WARN} GitHub Copilot CLI not found`);
|
|
147
|
+
console.log(` Install: npm install -g @github/copilot`);
|
|
148
|
+
}
|
|
149
|
+
// 2. Detect VS Code
|
|
150
|
+
const vscode = detectVSCode();
|
|
151
|
+
if (vscode.found) {
|
|
152
|
+
console.log(`\n${CHECK} Visual Studio Code detected`);
|
|
153
|
+
if (vscode.version)
|
|
154
|
+
console.log(` ${INFO} Version: ${vscode.version}`);
|
|
155
|
+
if (vscode.hasBuiltinCopilot) {
|
|
156
|
+
console.log(` ${CHECK} Built-in Copilot (v1.99+) — will be configured`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log(` ${WARN} VS Code version may not have built-in Copilot`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(`\n${WARN} Visual Studio Code not found — skipping VS Code config`);
|
|
164
|
+
}
|
|
165
|
+
// 3. Patch shell profile
|
|
166
|
+
const profilePath = detectShellProfile();
|
|
167
|
+
if (profilePath) {
|
|
168
|
+
const result = patchShellProfile(profilePath, port);
|
|
169
|
+
const label = path.basename(profilePath);
|
|
170
|
+
if (result.action === 'added') {
|
|
171
|
+
console.log(`\n${CHECK} Shell profile patched: ${label}`);
|
|
172
|
+
console.log(` ${ARROW} Added OTEL env vars (endpoint, content capture)`);
|
|
173
|
+
console.log(` ${ARROW} Run: source ${profilePath}`);
|
|
174
|
+
}
|
|
175
|
+
else if (result.action === 'updated') {
|
|
176
|
+
console.log(`\n${CHECK} Shell profile updated: ${label}`);
|
|
177
|
+
console.log(` ${ARROW} Updated port to ${port}`);
|
|
178
|
+
console.log(` ${ARROW} Run: source ${profilePath}`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(`\n${CHECK} Shell profile: already configured (${label})`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// 4. Patch VS Code settings
|
|
185
|
+
if (vscode.found) {
|
|
186
|
+
const settingsPath = getVSCodeSettingsPath();
|
|
187
|
+
const result = patchVSCodeSettings(settingsPath, port);
|
|
188
|
+
if (result.action === 'added') {
|
|
189
|
+
console.log(`\n${CHECK} VS Code settings patched`);
|
|
190
|
+
console.log(` ${ARROW} Added terminal.integrated.env.osx with OTEL vars`);
|
|
191
|
+
console.log(` ${ARROW} Restart VS Code to apply`);
|
|
192
|
+
}
|
|
193
|
+
else if (result.action === 'updated') {
|
|
194
|
+
console.log(`\n${CHECK} VS Code settings updated`);
|
|
195
|
+
console.log(` ${ARROW} Updated port to ${port}`);
|
|
196
|
+
console.log(` ${ARROW} Restart VS Code to apply`);
|
|
197
|
+
}
|
|
198
|
+
else if (result.action === 'already_set') {
|
|
199
|
+
console.log(`\n${CHECK} VS Code settings: already configured`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.log(`\n${WARN} VS Code settings: ${result.reason}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// 5. Summary
|
|
206
|
+
console.log('\n────────────────────────────────────────────────');
|
|
207
|
+
console.log(' Next steps:\n');
|
|
208
|
+
console.log(` 1. source ${profilePath ?? '~/.zshrc'} (apply to current terminal)`);
|
|
209
|
+
if (vscode.found)
|
|
210
|
+
console.log(' 2. Restart VS Code');
|
|
211
|
+
console.log(` 3. node dist/cli.js --ui web --port ${port} --no-proxy`);
|
|
212
|
+
console.log(` 4. Open http://localhost:${port}`);
|
|
213
|
+
console.log(` 5. Use copilot normally — traces appear automatically`);
|
|
214
|
+
console.log('\n Tracer endpoint: http://localhost:' + port + '/v1/traces');
|
|
215
|
+
console.log('────────────────────────────────────────────────\n');
|
|
216
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { Server } from 'socket.io';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { getTraces, getTrace, getSessionSummary } from './db.js';
|
|
7
|
+
import { traceEvents } from './proxy.js';
|
|
8
|
+
import { registerOtlpRoutes } from './otlpReceiver.js';
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
export function startWebServer(port = 4747, sessionId) {
|
|
11
|
+
const app = express();
|
|
12
|
+
const httpServer = createServer(app);
|
|
13
|
+
const io = new Server(httpServer, { cors: { origin: '*' } });
|
|
14
|
+
// Serve static web UI
|
|
15
|
+
app.use(express.static(path.join(__dirname, '../web')));
|
|
16
|
+
// Register OTLP receiver routes
|
|
17
|
+
app.use(express.json({ limit: '10mb' }));
|
|
18
|
+
registerOtlpRoutes(app, sessionId ?? 'default');
|
|
19
|
+
// API
|
|
20
|
+
app.get('/api/traces', (req, res) => {
|
|
21
|
+
const sid = req.query.sessionId || undefined; // undefined = all sessions
|
|
22
|
+
const traces = getTraces(sid, 200);
|
|
23
|
+
res.json(traces);
|
|
24
|
+
});
|
|
25
|
+
app.get('/api/traces/:id', (req, res) => {
|
|
26
|
+
const trace = getTrace(req.params.id);
|
|
27
|
+
if (!trace)
|
|
28
|
+
return res.status(404).json({ error: 'Not found' });
|
|
29
|
+
res.json(trace);
|
|
30
|
+
});
|
|
31
|
+
app.get('/api/summary', (req, res) => {
|
|
32
|
+
const sid = req.query.sessionId || sessionId;
|
|
33
|
+
if (!sid)
|
|
34
|
+
return res.json(null);
|
|
35
|
+
res.json(getSessionSummary(sid));
|
|
36
|
+
});
|
|
37
|
+
// Socket.io — push real-time updates
|
|
38
|
+
io.on('connection', (socket) => {
|
|
39
|
+
// Send current state on connect
|
|
40
|
+
const sid = sessionId;
|
|
41
|
+
socket.emit('init', {
|
|
42
|
+
traces: getTraces(sid, 200),
|
|
43
|
+
summary: sid ? getSessionSummary(sid) : null,
|
|
44
|
+
});
|
|
45
|
+
const onUpdate = (entry) => socket.emit('trace:update', entry);
|
|
46
|
+
const onDone = (entry) => socket.emit('trace:done', entry);
|
|
47
|
+
traceEvents.on('trace:update', onUpdate);
|
|
48
|
+
traceEvents.on('trace:done', onDone);
|
|
49
|
+
socket.on('disconnect', () => {
|
|
50
|
+
traceEvents.off('trace:update', onUpdate);
|
|
51
|
+
traceEvents.off('trace:done', onDone);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
httpServer.listen(port, () => {
|
|
55
|
+
console.log(`\n 🌐 Copilot Tracer Web UI: http://localhost:${port}\n`);
|
|
56
|
+
});
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "copilot-tracer",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Real-time monitor and tracing tool for GitHub Copilot CLI — tracks tokens, AI credits, MCP/skill/agent calls with console and web UI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"copilot",
|
|
7
|
+
"github-copilot",
|
|
8
|
+
"tracing",
|
|
9
|
+
"observability",
|
|
10
|
+
"opentelemetry",
|
|
11
|
+
"tokens",
|
|
12
|
+
"ai-credits"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/chuongnd/copilot-tracer.git"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/",
|
|
24
|
+
"web/",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"bin": {
|
|
29
|
+
"copilot-tracer": "dist/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"prepare": "tsc",
|
|
34
|
+
"dev": "tsx src/cli.ts",
|
|
35
|
+
"console": "tsx src/cli.ts --ui console",
|
|
36
|
+
"web": "tsx src/cli.ts --ui web",
|
|
37
|
+
"start": "node dist/cli.js",
|
|
38
|
+
"publish:npm": "bash scripts/publish.sh"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@opentelemetry/otlp-transformer": "^0.221.0",
|
|
42
|
+
"better-sqlite3": "^13.0.2",
|
|
43
|
+
"chalk": "^5.3.0",
|
|
44
|
+
"cli-table3": "^0.6.4",
|
|
45
|
+
"commander": "^12.0.0",
|
|
46
|
+
"date-fns": "^3.3.1",
|
|
47
|
+
"express": "^4.18.2",
|
|
48
|
+
"ink": "^5.0.1",
|
|
49
|
+
"ink-table": "^3.0.0",
|
|
50
|
+
"open": "^10.1.0",
|
|
51
|
+
"react": "^18.2.0",
|
|
52
|
+
"socket.io": "^4.7.4"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/better-sqlite3": "^7.6.8",
|
|
56
|
+
"@types/express": "^4.17.21",
|
|
57
|
+
"@types/node": "^20.0.0",
|
|
58
|
+
"@types/react": "^18.2.0",
|
|
59
|
+
"tsx": "^4.7.0",
|
|
60
|
+
"typescript": "^5.4.0"
|
|
61
|
+
},
|
|
62
|
+
"type": "module"
|
|
63
|
+
}
|