copilot-tracer 1.0.3 → 1.0.5

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/dist/db.js CHANGED
@@ -8,10 +8,20 @@ if (!fs.existsSync(DB_DIR))
8
8
  fs.mkdirSync(DB_DIR, { recursive: true });
9
9
  const db = new Database(DB_PATH);
10
10
  db.exec(`
11
+ CREATE TABLE IF NOT EXISTS projects (
12
+ id TEXT PRIMARY KEY,
13
+ path TEXT NOT NULL UNIQUE,
14
+ repo_url TEXT,
15
+ local_path TEXT,
16
+ created_at TEXT NOT NULL,
17
+ updated_at TEXT NOT NULL
18
+ );
19
+
11
20
  CREATE TABLE IF NOT EXISTS sessions (
12
21
  id TEXT PRIMARY KEY,
13
22
  started_at TEXT NOT NULL,
14
- ended_at TEXT
23
+ ended_at TEXT,
24
+ project_id TEXT REFERENCES projects(id)
15
25
  );
16
26
 
17
27
  CREATE TABLE IF NOT EXISTS traces (
@@ -38,8 +48,113 @@ db.exec(`
38
48
  FOREIGN KEY(session_id) REFERENCES sessions(id)
39
49
  );
40
50
  `);
41
- export function createSession(id) {
42
- db.prepare('INSERT OR REPLACE INTO sessions (id, started_at) VALUES (?, ?)').run(id, new Date().toISOString());
51
+ // Migrate existing DBs
52
+ try {
53
+ db.prepare('ALTER TABLE projects ADD COLUMN repo_url TEXT').run();
54
+ }
55
+ catch { }
56
+ try {
57
+ db.prepare('ALTER TABLE projects ADD COLUMN local_path TEXT').run();
58
+ }
59
+ catch { }
60
+ try {
61
+ db.prepare('ALTER TABLE sessions ADD COLUMN project_id TEXT REFERENCES projects(id)').run();
62
+ }
63
+ catch { }
64
+ export function ensureProject(projectPath, repoUrl) {
65
+ const id = 'project:' + projectPath;
66
+ const now = new Date().toISOString();
67
+ db.prepare('INSERT OR IGNORE INTO projects (id, path, repo_url, local_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, projectPath, repoUrl ?? null, projectPath, now, now);
68
+ if (repoUrl) {
69
+ db.prepare('UPDATE projects SET repo_url = ?, updated_at = ? WHERE id = ?').run(repoUrl, now, id);
70
+ }
71
+ db.prepare('UPDATE projects SET updated_at = ? WHERE id = ?').run(now, id);
72
+ return id;
73
+ }
74
+ export function ensureProjectByRepo(repoUrl) {
75
+ // Try to find existing project by repo URL
76
+ const existing = db.prepare('SELECT id FROM projects WHERE repo_url = ?').get(repoUrl);
77
+ if (existing) {
78
+ db.prepare('UPDATE projects SET updated_at = ? WHERE id = ?').run(new Date().toISOString(), existing.id);
79
+ return existing.id;
80
+ }
81
+ // Create new project with repo URL as path (local_path set later)
82
+ const id = 'project:' + repoUrl;
83
+ const now = new Date().toISOString();
84
+ db.prepare('INSERT OR IGNORE INTO projects (id, path, repo_url, local_path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, repoUrl, repoUrl, null, now, now);
85
+ return id;
86
+ }
87
+ export function updateProjectLocalPath(projectId, localPath) {
88
+ db.prepare('UPDATE projects SET local_path = ?, updated_at = ? WHERE id = ?').run(localPath, new Date().toISOString(), projectId);
89
+ }
90
+ export function findProjectByRepo(repoUrl) {
91
+ const row = db.prepare('SELECT id, path, local_path FROM projects WHERE repo_url = ?').get(repoUrl);
92
+ return row ? { id: row.id, path: row.path, local_path: row.local_path } : null;
93
+ }
94
+ export function createSession(id, projectId) {
95
+ // Backfill-safe upsert: insert if missing, set project_id only when a project is
96
+ // provided AND the session currently has none (never null out an existing link).
97
+ db.prepare(`
98
+ INSERT INTO sessions (id, started_at, project_id) VALUES (?, ?, ?)
99
+ ON CONFLICT(id) DO UPDATE SET
100
+ project_id = CASE
101
+ WHEN excluded.project_id IS NOT NULL THEN COALESCE(sessions.project_id, excluded.project_id)
102
+ ELSE sessions.project_id
103
+ END
104
+ `).run(id, new Date().toISOString(), projectId ?? null);
105
+ }
106
+ export function getDashboard() {
107
+ const projects = db.prepare(`
108
+ SELECT
109
+ p.id, p.path, p.repo_url, p.local_path,
110
+ COUNT(DISTINCT s.id) as session_count,
111
+ COALESCE(SUM(t.tokens_total), 0) as total_tokens,
112
+ COALESCE(SUM(t.ai_credits), 0) as total_credits,
113
+ MAX(s.started_at) as last_active_at
114
+ FROM projects p
115
+ LEFT JOIN sessions s ON s.project_id = p.id
116
+ LEFT JOIN traces t ON t.session_id = s.id
117
+ GROUP BY p.id
118
+ ORDER BY last_active_at DESC
119
+ `).all();
120
+ const totals = db.prepare(`
121
+ SELECT
122
+ (SELECT COUNT(*) FROM projects) as projects,
123
+ (SELECT COUNT(*) FROM sessions) as sessions,
124
+ COALESCE((SELECT SUM(tokens_total) FROM traces), 0) as tokens,
125
+ COALESCE((SELECT SUM(ai_credits) FROM traces), 0) as credits
126
+ `).get();
127
+ const enriched = projects.map(p => {
128
+ const lastSession = db.prepare(`
129
+ SELECT s.id,
130
+ COALESCE((SELECT SUM(tokens_total) FROM traces WHERE session_id = s.id), 0) as tokens,
131
+ COALESCE((SELECT SUM(ai_credits) FROM traces WHERE session_id = s.id), 0) as credits
132
+ FROM sessions s
133
+ WHERE s.project_id = ?
134
+ ORDER BY s.started_at DESC
135
+ LIMIT 1
136
+ `).get(p.id);
137
+ return {
138
+ id: p.id,
139
+ path: p.path,
140
+ repoUrl: p.repo_url,
141
+ localPath: p.local_path,
142
+ sessionCount: p.session_count || 0,
143
+ totalTokens: p.total_tokens || 0,
144
+ totalCredits: p.total_credits || 0,
145
+ lastActiveAt: p.last_active_at,
146
+ lastSession: lastSession ?? null,
147
+ };
148
+ });
149
+ return {
150
+ projects: enriched,
151
+ totals: {
152
+ projects: totals.projects,
153
+ sessions: totals.sessions,
154
+ tokens: totals.tokens,
155
+ credits: totals.credits,
156
+ },
157
+ };
43
158
  }
44
159
  export function upsertTrace(entry) {
45
160
  db.prepare(`
@@ -156,3 +271,51 @@ function rowToEntry(row) {
156
271
  export function deleteTrace(id) {
157
272
  db.prepare('DELETE FROM traces WHERE id = ?').run(id);
158
273
  }
274
+ export function getProjectTraces(projectId, limit = 200) {
275
+ const rows = db.prepare(`
276
+ SELECT t.* FROM traces t
277
+ JOIN sessions s ON s.id = t.session_id
278
+ WHERE s.project_id = ?
279
+ ORDER BY t.date_time DESC
280
+ LIMIT ?
281
+ `).all(projectId, limit);
282
+ return rows.map((r) => rowToEntry(r));
283
+ }
284
+ export function getProjectSessionSummary(projectId) {
285
+ const stats = db.prepare(`
286
+ SELECT
287
+ COUNT(*) as entries,
288
+ SUM(t.tokens_input) as input,
289
+ SUM(t.tokens_output) as output,
290
+ SUM(t.tokens_cached) as cached,
291
+ SUM(t.tokens_reasoning) as reasoning,
292
+ SUM(t.tokens_written) as written,
293
+ SUM(t.tokens_total) as total,
294
+ SUM(t.ai_credits) as credits,
295
+ SUM(t.duration_ms) as duration,
296
+ SUM(t.skill_count) as skills,
297
+ SUM(t.agent_count) as agents,
298
+ SUM(t.mcp_count) as mcps
299
+ FROM traces t
300
+ JOIN sessions s ON s.id = t.session_id
301
+ WHERE s.project_id = ?
302
+ `).get(projectId);
303
+ return {
304
+ sessionId: projectId,
305
+ startedAt: '',
306
+ totalEntries: stats.entries || 0,
307
+ totalTokens: {
308
+ input: stats.input || 0,
309
+ output: stats.output || 0,
310
+ cached: stats.cached || 0,
311
+ reasoning: stats.reasoning || 0,
312
+ written: stats.written || 0,
313
+ total: stats.total || 0,
314
+ },
315
+ totalCredits: stats.credits || 0,
316
+ totalDurationMs: stats.duration || 0,
317
+ totalSkillCalls: stats.skills || 0,
318
+ totalAgentCalls: stats.agents || 0,
319
+ totalMcpCalls: stats.mcps || 0,
320
+ };
321
+ }
@@ -14,7 +14,7 @@
14
14
  * chat <model> span attrs: same as above
15
15
  * User message event: github.copilot.user.message
16
16
  */
17
- import { upsertTrace, createSession, deleteTrace } from './db.js';
17
+ import { upsertTrace, createSession, deleteTrace, ensureProject, ensureProjectByRepo } from './db.js';
18
18
  import { traceEvents } from './proxy.js';
19
19
  // ── Helpers ───────────────────────────────────────────────────────────────────
20
20
  function getAttr(attrs, key) {
@@ -37,20 +37,35 @@ function nanoToIso(nano) {
37
37
  return new Date(nanoToMs(nano)).toISOString();
38
38
  }
39
39
  const inFlight = new Map(); // traceId → InFlight
40
- // Sessions we've created in DB (avoid duplicate createSession calls)
41
- const knownSessions = new Set();
42
- function ensureSession(sessionId) {
43
- if (!knownSessions.has(sessionId)) {
44
- createSession(sessionId);
45
- knownSessions.add(sessionId);
40
+ // Sessions are created lazily. Always upsert so project_id gets backfilled
41
+ // when the session was created earlier without a project.
42
+ function ensureSession(sessionId, projectId) {
43
+ createSession(sessionId, projectId);
44
+ }
45
+ // ── Project resolution ────────────────────────────────────────────────────────
46
+ // Precedence: repo URL > working dir > default (CLI) project.
47
+ // If none match, the session keeps no project (traces still appear on live page).
48
+ function resolveProjectId(repoUrl, workingDir, defaultProjectId) {
49
+ if (repoUrl)
50
+ return ensureProjectByRepo(String(repoUrl));
51
+ if (workingDir)
52
+ return ensureProject(workingDir);
53
+ return defaultProjectId;
54
+ }
55
+ function detectWorkingDir(attrs) {
56
+ for (const key of ['process.working_directory', 'github.copilot.working_dir']) {
57
+ const v = getAttr(attrs, key);
58
+ if (v && String(v).trim())
59
+ return String(v).trim();
46
60
  }
61
+ return undefined;
47
62
  }
48
63
  // ── Process one batch of spans ────────────────────────────────────────────────
49
64
  // Track standalone chat entries so invoke_agent can replace them (multiple chat spans per traceId)
50
65
  const pendingChatIds = new Map(); // `chat:${traceId}` → list of entryIds
51
66
  // Buffer tool calls that arrive before invoke_agent
52
67
  const pendingToolCalls = new Map(); // traceId → tool calls
53
- function processSpans(spans, sessionId) {
68
+ function processSpans(spans, sessionId, projectId, workingDir) {
54
69
  for (const span of spans) {
55
70
  // DEBUG — log raw span to stderr when COPILOT_TRACER_DEBUG=1
56
71
  if (process.env.COPILOT_TRACER_DEBUG === '1') {
@@ -65,6 +80,9 @@ function processSpans(spans, sessionId) {
65
80
  const startMs = nanoToMs(span.startTimeUnixNano);
66
81
  const endMs = nanoToMs(span.endTimeUnixNano);
67
82
  const durationMs = endMs - startMs;
83
+ // Extract repo URL for auto project detection
84
+ const repoUrl = getAttr(attrs, 'github.copilot.git.repository');
85
+ const resolvedProjectId = resolveProjectId(repoUrl, workingDir, projectId);
68
86
  // Extract prompt from gen_ai.input.messages (real attr name from copilot)
69
87
  let promptText = '';
70
88
  let responseText = '';
@@ -108,15 +126,14 @@ function processSpans(spans, sessionId) {
108
126
  return text.trim();
109
127
  })
110
128
  .join(' ')
111
- .trim()
112
- .slice(0, 300);
129
+ .trim();
113
130
  }
114
131
  else {
115
- promptText = (userMsg?.content ?? String(raw)).slice(0, 300);
132
+ promptText = (userMsg?.content ?? String(raw)).trim();
116
133
  }
117
134
  }
118
135
  catch {
119
- promptText = String(raw).slice(0, 300);
136
+ promptText = String(raw).trim();
120
137
  }
121
138
  }
122
139
  }
@@ -214,7 +231,7 @@ function processSpans(spans, sessionId) {
214
231
  inf.entry.mcpCount = mcps;
215
232
  pendingToolCalls.delete(traceId);
216
233
  }
217
- ensureSession(sessionId);
234
+ ensureSession(sessionId, resolvedProjectId);
218
235
  upsertTrace(entry);
219
236
  traceEvents.emit('trace:update', entry);
220
237
  traceEvents.emit('trace:done', entry);
@@ -277,7 +294,7 @@ function processSpans(spans, sessionId) {
277
294
  skillCount: 0, agentCount: 0, mcpCount: 0,
278
295
  status: 'done',
279
296
  };
280
- ensureSession(sessionId);
297
+ ensureSession(sessionId, resolveProjectId(getAttr(attrs, 'github.copilot.git.repository'), workingDir, projectId));
281
298
  upsertTrace(entry);
282
299
  const chatKey = `chat:${traceId}`;
283
300
  const existing = pendingChatIds.get(chatKey) ?? [];
@@ -384,7 +401,7 @@ function detectToolType(name) {
384
401
  return 'builtin';
385
402
  }
386
403
  // ── Register OTLP HTTP routes on the Express app ──────────────────────────────
387
- export function registerOtlpRoutes(app, defaultSessionId) {
404
+ export function registerOtlpRoutes(app, defaultSessionId, projectId) {
388
405
  // OTLP traces — copilot sends JSON (http/json protocol)
389
406
  // body already parsed by express.json() middleware registered before this call
390
407
  app.post('/v1/traces', (req, res) => {
@@ -397,8 +414,9 @@ export function registerOtlpRoutes(app, defaultSessionId) {
397
414
  ?? getAttr(resAttrs, 'session.id')
398
415
  ?? getAttr(resAttrs, 'github.copilot.conversation_id');
399
416
  const sessionId = String(sessionFromOtel ?? defaultSessionId);
417
+ const workingDir = detectWorkingDir(resAttrs);
400
418
  for (const ss of rs.scopeSpans ?? []) {
401
- processSpans(ss.spans ?? [], sessionId);
419
+ processSpans(ss.spans ?? [], sessionId, projectId, workingDir);
402
420
  }
403
421
  }
404
422
  res.status(200).json({ partialSuccess: {} });
package/dist/setup.js CHANGED
@@ -22,6 +22,26 @@ function otelEnvBlock(port) {
22
22
  `export ${OTEL_ENDPOINT_KEY}=http://localhost:${port}`,
23
23
  `export ${OTEL_CONTENT_KEY}=true`,
24
24
  `export ${OTEL_ENABLED_KEY}=true`,
25
+ ``,
26
+ `# Tag every copilot prompt with the terminal folder it ran from, so the`,
27
+ `# tracer can attribute it to the right project. OTLP carries no working-dir,`,
28
+ `# so we inject it via OTEL_RESOURCE_ATTRIBUTES (percent-encoded).`,
29
+ `copilot() {`,
30
+ ` local _wd`,
31
+ ` if command -v python3 >/dev/null 2>&1; then`,
32
+ ` _wd="$(pwd | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read().strip(), safe="/"))')"`,
33
+ ` else`,
34
+ ` _wd="$(pwd)"`,
35
+ ` fi`,
36
+ ` if [ -n "\${OTEL_RESOURCE_ATTRIBUTES:-}" ]; then`,
37
+ ` # Drop any stale working_dir entry, then append the current folder`,
38
+ ` OTEL_RESOURCE_ATTRIBUTES="$(printf '%s' "\$OTEL_RESOURCE_ATTRIBUTES" | sed -E 's/(^|,)github\.copilot\.working_dir=[^,]*/\\1/g; s/^,//')"`,
39
+ ` [ -z "\$OTEL_RESOURCE_ATTRIBUTES" ] || OTEL_RESOURCE_ATTRIBUTES="\${OTEL_RESOURCE_ATTRIBUTES},"`,
40
+ ` fi`,
41
+ ` OTEL_RESOURCE_ATTRIBUTES="\${OTEL_RESOURCE_ATTRIBUTES}github.copilot.working_dir=\${_wd}"`,
42
+ ` export OTEL_RESOURCE_ATTRIBUTES`,
43
+ ` command copilot "\$@"`,
44
+ `}`,
25
45
  `# <<< copilot-tracer <<<`,
26
46
  ].join('\n');
27
47
  }
@@ -130,7 +150,7 @@ function patchVSCodeSettings(settingsPath, port) {
130
150
  return { action: wasSet ? 'updated' : 'added' };
131
151
  }
132
152
  // ── Main setup ────────────────────────────────────────────────────────────────
133
- export function runSetup(port) {
153
+ export function runSetup(port, silent = false) {
134
154
  const CHECK = '✅';
135
155
  const WARN = '⚠️ ';
136
156
  const INFO = '📍';
@@ -174,11 +194,12 @@ export function runSetup(port) {
174
194
  if (result.action === 'added') {
175
195
  console.log(`\n${CHECK} Shell profile patched: ${label}`);
176
196
  console.log(` ${ARROW} Added OTEL env vars (endpoint, content capture)`);
197
+ console.log(` ${ARROW} Added copilot() wrapper — tags each prompt with the terminal folder`);
177
198
  console.log(` ${ARROW} Run: source ${profilePath}`);
178
199
  }
179
200
  else if (result.action === 'updated') {
180
201
  console.log(`\n${CHECK} Shell profile updated: ${label}`);
181
- console.log(` ${ARROW} Updated port to ${port}`);
202
+ console.log(` ${ARROW} Updated port to ${port} + copilot() wrapper`);
182
203
  console.log(` ${ARROW} Run: source ${profilePath}`);
183
204
  }
184
205
  else {
@@ -211,14 +232,16 @@ export function runSetup(port) {
211
232
  process.env[OTEL_CONTENT_KEY] = 'true';
212
233
  process.env[OTEL_ENABLED_KEY] = 'true';
213
234
  // 6. Summary
214
- const profileBase = profilePath ? path.basename(profilePath) : '.zshrc';
215
- console.log('\n────────────────────────────────────────────────');
216
- console.log(' One manual step required:\n');
217
- console.log(` source ~/${profileBase}`);
218
- console.log(` (opens a new terminal already? — env is already active there)`);
219
- if (vscode.found)
220
- console.log('\n Restart VS Code once to pick up the new terminal env.');
221
- console.log('\n ✨ Starting tracer web UI now...');
222
- console.log(` Open: http://localhost:${port}/`);
223
- console.log('────────────────────────────────────────────────\n');
235
+ if (!silent) {
236
+ const profileBase = profilePath ? path.basename(profilePath) : '.zshrc';
237
+ console.log('\n────────────────────────────────────────────────');
238
+ console.log(' One manual step required:\n');
239
+ console.log(` source ~/${profileBase}`);
240
+ console.log(` (opens a new terminal already? — env is already active there)`);
241
+ if (vscode.found)
242
+ console.log('\n Restart VS Code once to pick up the new terminal env.');
243
+ console.log('\n ✨ Starting tracer web UI now...');
244
+ console.log(` Open: http://localhost:${port}/`);
245
+ console.log('────────────────────────────────────────────────\n');
246
+ }
224
247
  }
package/dist/webServer.js CHANGED
@@ -3,11 +3,12 @@ import { createServer } from 'http';
3
3
  import { Server } from 'socket.io';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { getTraces, getTrace, getSessionSummary } from './db.js';
6
+ import { execSync } from 'child_process';
7
+ import { getTraces, getTrace, getSessionSummary, getDashboard, updateProjectLocalPath, getProjectTraces, getProjectSessionSummary } from './db.js';
7
8
  import { traceEvents } from './proxy.js';
8
9
  import { registerOtlpRoutes } from './otlpReceiver.js';
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
- export function startWebServer(port = 4747, sessionId) {
11
+ export function startWebServer(port = 4747, sessionId, projectId) {
11
12
  const app = express();
12
13
  const httpServer = createServer(app);
13
14
  const io = new Server(httpServer, { cors: { origin: '*' } });
@@ -15,13 +16,59 @@ export function startWebServer(port = 4747, sessionId) {
15
16
  app.use(express.static(path.join(__dirname, '../web')));
16
17
  // Register OTLP receiver routes
17
18
  app.use(express.json({ limit: '10mb' }));
18
- registerOtlpRoutes(app, sessionId ?? 'default');
19
+ registerOtlpRoutes(app, sessionId ?? 'default', projectId);
19
20
  // API
20
21
  app.get('/api/traces', (req, res) => {
21
22
  const sid = req.query.sessionId || undefined; // undefined = all sessions
22
23
  const traces = getTraces(sid, 200);
23
24
  res.json(traces);
24
25
  });
26
+ app.post('/api/refine', async (req, res) => {
27
+ const { prompt } = req.body;
28
+ if (!prompt?.trim()) {
29
+ res.status(400).json({ error: 'prompt required' });
30
+ return;
31
+ }
32
+ const origTok = Math.ceil(prompt.trim().length / 4);
33
+ // Meta-prompt grounded in prompt engineering best practices (promptingguide.ai)
34
+ const metaPrompt = `You are a world-class prompt engineering expert. Rewrite the user prompt below using these techniques where applicable:
35
+ 1. Role grounding — prepend "You are a <expert role>" if no persona is set
36
+ 2. Imperative clarity — replace indirect/hedging phrases with direct imperatives (Explain / List / Generate / Analyze)
37
+ 3. Output format — specify format (JSON, markdown, numbered steps, bullet list) when missing
38
+ 4. Chain-of-thought — add "Think step by step." for multi-step reasoning or debugging tasks
39
+ 5. Remove noise — remove filler (please, could you, I want you to, thank you, if you don't mind, maybe, I think, kind of, sort of)
40
+ 6. Add constraints — specify language, length, audience, tone if not present
41
+ 7. Redundancy — collapse repeated or contradictory instructions
42
+
43
+ Respond ONLY with a valid JSON object (no markdown, no code fences):
44
+ {"optimized":"<rewritten prompt>","issues":[{"type":"warn","msg":"<what was wrong>"}],"techniques":["<applied>"]}
45
+
46
+ Prompt to optimize:
47
+ ${prompt.trim()}`;
48
+ try {
49
+ const raw = execSync(`copilot -p ${JSON.stringify(metaPrompt)} --model claude-sonnet-4.6`, { timeout: 45000, encoding: 'utf8', maxBuffer: 2 * 1024 * 1024 });
50
+ // Strip copilot CLI chrome (trailing "Changes +0 -0" line, ANSI codes)
51
+ const cleaned = raw
52
+ .replace(/\x1b\[[0-9;]*m/g, '') // ANSI
53
+ .replace(/\r/g, '')
54
+ .split('\n')
55
+ .filter(l => !/^Changes\s+\+\d/.test(l.trim()))
56
+ .join('\n')
57
+ .trim();
58
+ // Extract JSON — copilot may wrap with prose
59
+ const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
60
+ if (!jsonMatch)
61
+ throw new Error('No JSON in response: ' + cleaned.slice(0, 200));
62
+ const result = JSON.parse(jsonMatch[0]);
63
+ const newTok = Math.ceil((result.optimized ?? '').length / 4);
64
+ const inputSavingPct = origTok > 0 ? Math.max(0, Math.round((1 - newTok / origTok) * 100)) : 0;
65
+ res.json({ ok: true, ...result, origTok, newTok, inputSavingPct });
66
+ }
67
+ catch (err) {
68
+ const msg = err instanceof Error ? err.message : String(err);
69
+ res.status(500).json({ ok: false, error: msg });
70
+ }
71
+ });
25
72
  app.get('/api/traces/:id', (req, res) => {
26
73
  const trace = getTrace(req.params.id);
27
74
  if (!trace)
@@ -34,14 +81,45 @@ export function startWebServer(port = 4747, sessionId) {
34
81
  return res.json(null);
35
82
  res.json(getSessionSummary(sid));
36
83
  });
84
+ app.get('/api/dashboard', (_req, res) => {
85
+ res.json(getDashboard());
86
+ });
87
+ // Project registration — CLI registers its local path for a project
88
+ app.post('/api/projects', (req, res) => {
89
+ const { projectId, localPath } = req.body;
90
+ if (!projectId || !localPath) {
91
+ res.status(400).json({ error: 'projectId and localPath required' });
92
+ return;
93
+ }
94
+ updateProjectLocalPath(projectId, localPath);
95
+ res.json({ ok: true });
96
+ });
97
+ // Project-scoped traces
98
+ app.get('/api/projects/:id/traces', (req, res) => {
99
+ const traces = getProjectTraces(req.params.id, 200);
100
+ res.json(traces);
101
+ });
102
+ // Project-scoped summary
103
+ app.get('/api/projects/:id/summary', (req, res) => {
104
+ res.json(getProjectSessionSummary(req.params.id));
105
+ });
37
106
  // Socket.io — push real-time updates
38
107
  io.on('connection', (socket) => {
108
+ const projectId = socket.handshake.query.projectId;
39
109
  // 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
- });
110
+ if (projectId) {
111
+ socket.emit('init', {
112
+ traces: getProjectTraces(projectId, 200),
113
+ summary: getProjectSessionSummary(projectId),
114
+ });
115
+ }
116
+ else {
117
+ const sid = sessionId;
118
+ socket.emit('init', {
119
+ traces: getTraces(sid, 200),
120
+ summary: sid ? getSessionSummary(sid) : null,
121
+ });
122
+ }
45
123
  const onUpdate = (entry) => socket.emit('trace:update', entry);
46
124
  const onDone = (entry) => socket.emit('trace:done', entry);
47
125
  traceEvents.on('trace:update', onUpdate);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "copilot-tracer",
3
- "version": "1.0.3",
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",
3
+ "version": "1.0.5",
4
+ "description": "Real-time tracing and prompt-refinement companion for GitHub Copilot CLI and VS Code Copilot — tracks tokens, AI credits, tool calls, and refined prompts with console and web UI",
5
5
  "keywords": [
6
6
  "copilot",
7
7
  "github-copilot",