copilot-tracer 1.0.5 → 1.0.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # copilot-tracer
2
2
 
3
- Real-time tracing and prompt-refinement companion for **GitHub Copilot CLI** and **VS Code Copilot extension**.
3
+ Real-time tracing and prompt-refinement companion for **GitHub Copilot CLI**, **Claude Code**, and **VS Code Copilot extension**.
4
4
 
5
5
  Captures every prompt, response, token usage, AI credits, tool calls, and duration — all in one place. Runs as a background daemon that collects data from all your projects automatically. Includes a web dashboard with project overview and per-project live tracing.
6
6
 
@@ -11,7 +11,7 @@ Captures every prompt, response, token usage, AI credits, tool calls, and durati
11
11
  - **Daemon mode** — install once, run forever. Collects traces from all projects automatically
12
12
  - **Auto project detection** — detects project from `github.copilot.git.repository` in OTLP spans
13
13
  - **Zero-intrusion capture** — uses Copilot's built-in OTel support. Set env vars, done.
14
- - **Works everywhere** — captures both Copilot CLI and VS Code Copilot Chat
14
+ - **Works everywhere** — captures GitHub Copilot CLI, Claude Code, and VS Code Copilot Chat
15
15
  - **Dashboard** — overview of all projects with token usage, credits, and session counts
16
16
  - **Live tracer** — real-time trace table per project with detail panel
17
17
  - **Prompt refinement** — rewrites prompts with stronger instructions and less noise
@@ -31,7 +31,8 @@ This will:
31
31
  1. Detect your Copilot CLI and VS Code installation
32
32
  2. Patch `~/.zshrc` with OTEL env vars
33
33
  3. Patch VS Code `settings.json` with terminal env vars
34
- 4. Start the daemon on port 4747
34
+ 4. Enable Claude Code OTLP logs/events and enhanced beta traces
35
+ 5. Start the daemon on port 4747
35
36
 
36
37
  Then apply env vars in your current shell:
37
38
 
@@ -39,7 +40,9 @@ Then apply env vars in your current shell:
39
40
  source ~/.zshrc
40
41
  ```
41
42
 
42
- Restart VS Code once. After that, the daemon collects traces from all your Copilot sessions automatically.
43
+ Restart VS Code once. After that, the daemon collects traces from all your Copilot
44
+ and Claude Code sessions automatically. Claude Code content flags are enabled by
45
+ setup so prompts and responses can be displayed in the local dashboard.
43
46
 
44
47
  Open **http://localhost:4747** to see the dashboard.
45
48
 
@@ -51,20 +54,20 @@ Open **http://localhost:4747** to see the dashboard.
51
54
  ┌─────────────────────────────────────────────────────────┐
52
55
  │ copilot-tracer --daemon (runs once, stays running) │
53
56
  │ │
54
- │ OTLP Receiver ← Copilot CLI + VS Code │
57
+ │ OTLP Receiver ← Copilot CLI + Claude Code + VS Code │
55
58
  │ (auto-detects project from github.copilot.git.repository)│
56
59
  │ │
57
60
  │ SQLite DB → Dashboard + Live Tracer (Socket.io) │
58
61
  └─────────────────────────────────────────────────────────┘
59
62
 
60
- Copilot CLI / VS Code Copilot Chat
63
+ Copilot CLI / Claude Code / VS Code Copilot Chat
61
64
  │
62
65
  │ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4747
63
66
  ↓
64
- POST /v1/traces (OpenTelemetry OTLP JSON)
67
+ POST /v1/traces and /v1/logs (OpenTelemetry OTLP JSON)
65
68
  │
66
69
  ↓
67
- copilot-tracer parses spans → tokens, credits, tool calls
70
+ copilot-tracer parses spans and events → tokens, credits, tool calls
68
71
  │
69
72
  ↓
70
73
  SQLite DB (~/.copilot-tracer/traces.db)
@@ -121,6 +124,7 @@ Open http://localhost:4747 after starting the daemon.
121
124
  - **Summary cards** — total projects, sessions, tokens, credits
122
125
  - **Project cards** — each project shows path, session count, tokens, credits, last active
123
126
  - **Click a project** → opens live tracer filtered to that project
127
+ <img width="737" height="410" alt="image" src="https://github.com/user-attachments/assets/dc20653b-8774-46b3-9a42-6e7bb934aded" />
124
128
 
125
129
  ---
126
130
 
@@ -138,6 +142,10 @@ Real-time trace table for a specific project.
138
142
  - Click Skills / Agents / MCPs → filtered call list
139
143
  - Real-time updates via Socket.io
140
144
 
145
+ <img width="1504" height="742" alt="image" src="https://github.com/user-attachments/assets/f334108f-c587-4228-8120-7dac2f85f90b" />
146
+
147
+ <img width="1061" height="696" alt="image" src="https://github.com/user-attachments/assets/1555d5a2-dbca-4f1d-b102-3d2865dcee68" />
148
+
141
149
  ---
142
150
 
143
151
  ## Prompt Refinement
@@ -173,8 +181,21 @@ Add to `~/.zshrc`:
173
181
  export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4747
174
182
  export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
175
183
  export COPILOT_OTEL_ENABLED=true
184
+
185
+ # Claude Code telemetry
186
+ export CLAUDE_CODE_ENABLE_TELEMETRY=1
187
+ export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
188
+ export OTEL_LOGS_EXPORTER=otlp
189
+ export OTEL_TRACES_EXPORTER=otlp
190
+ export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
191
+ export OTEL_LOG_USER_PROMPTS=1
192
+ export OTEL_LOG_ASSISTANT_RESPONSES=1
176
193
  ```
177
194
 
195
+ Claude Code exports standard OTLP logs/events and optional beta traces. See
196
+ [Anthropic's monitoring documentation](https://code.claude.com/docs/en/monitoring-usage)
197
+ for protocol and content controls.
198
+
178
199
  For VS Code, add to `~/Library/Application Support/Code/User/settings.json`:
179
200
 
180
201
  ```json
package/dist/consoleUi.js CHANGED
@@ -10,7 +10,7 @@ function fmtDuration(ms) {
10
10
  return `${(ms / 1000).toFixed(1)}s`;
11
11
  }
12
12
  function fmtCredits(c) {
13
- return `${c.toFixed(2)} cr | $${(c * 0.01).toFixed(4)}`;
13
+ return `$${(c * 0.01).toFixed(4)}`;
14
14
  }
15
15
  function statusColor(status, text) {
16
16
  if (status === 'running')
@@ -25,7 +25,7 @@ export function renderConsoleTable(entries, summary) {
25
25
  head: [
26
26
  chalk.cyan('Date / Time'),
27
27
  chalk.cyan('Prompt'),
28
- chalk.cyan('AI Credits'),
28
+ chalk.cyan('Est Cost'),
29
29
  chalk.cyan('Duration'),
30
30
  chalk.cyan('Tokens\nCached|Written|Reason'),
31
31
  chalk.cyan('Skills'),
@@ -14,6 +14,7 @@
14
14
  * chat <model> span attrs: same as above
15
15
  * User message event: github.copilot.user.message
16
16
  */
17
+ import { randomUUID } from 'crypto';
17
18
  import { upsertTrace, createSession, deleteTrace, ensureProject, ensureProjectByRepo } from './db.js';
18
19
  import { traceEvents } from './proxy.js';
19
20
  // ── Helpers ───────────────────────────────────────────────────────────────────
@@ -36,7 +37,53 @@ function nanoToMs(nano) {
36
37
  function nanoToIso(nano) {
37
38
  return new Date(nanoToMs(nano)).toISOString();
38
39
  }
40
+ function getStringAttr(attrs, ...keys) {
41
+ for (const key of keys) {
42
+ const value = getAttr(attrs, key);
43
+ if (value !== undefined && String(value).trim())
44
+ return String(value);
45
+ }
46
+ return undefined;
47
+ }
48
+ function getBodyText(body) {
49
+ if (!body)
50
+ return undefined;
51
+ if (body.stringValue !== undefined)
52
+ return body.stringValue;
53
+ if (body.intValue !== undefined)
54
+ return String(body.intValue);
55
+ if (body.doubleValue !== undefined)
56
+ return String(body.doubleValue);
57
+ if (body.boolValue !== undefined)
58
+ return String(body.boolValue);
59
+ return undefined;
60
+ }
39
61
  const inFlight = new Map(); // traceId → InFlight
62
+ // Anthropic API pricing per 1K tokens (USD), converted to GitHub "AI credit" units
63
+ // (1 credit = $0.01) so aiCredits stays one unit across Copilot and Claude entries.
64
+ // Rates are Anthropic's current published per-model prices; a model id that doesn't
65
+ // match a specific entry falls back to its tier's (opus/sonnet/haiku) latest rate.
66
+ const ANTHROPIC_USD_PER_1K = {
67
+ 'claude-fable-5': { input: 0.010, output: 0.050 },
68
+ 'claude-mythos-5': { input: 0.010, output: 0.050 },
69
+ 'claude-opus-5': { input: 0.005, output: 0.025 },
70
+ 'claude-opus-4-8': { input: 0.005, output: 0.025 },
71
+ 'claude-opus-4-7': { input: 0.005, output: 0.025 },
72
+ 'claude-opus-4-6': { input: 0.005, output: 0.025 },
73
+ 'claude-sonnet-5': { input: 0.002, output: 0.010 },
74
+ 'claude-sonnet-4-6': { input: 0.003, output: 0.015 },
75
+ 'claude-haiku-4-5': { input: 0.001, output: 0.005 },
76
+ 'opus': { input: 0.005, output: 0.025 },
77
+ 'sonnet': { input: 0.003, output: 0.015 },
78
+ 'haiku': { input: 0.001, output: 0.005 },
79
+ 'default': { input: 0.003, output: 0.015 },
80
+ };
81
+ export function calcClaudeCredits(tokens, model) {
82
+ const key = Object.keys(ANTHROPIC_USD_PER_1K).find(k => (model ?? '').toLowerCase().includes(k)) ?? 'default';
83
+ const rate = ANTHROPIC_USD_PER_1K[key];
84
+ const usd = (tokens.input / 1000) * rate.input + (tokens.output / 1000) * rate.output;
85
+ return usd * 100; // USD → credits (1 credit = $0.01)
86
+ }
40
87
  // Sessions are created lazily. Always upsert so project_id gets backfilled
41
88
  // when the session was created earlier without a project.
42
89
  function ensureSession(sessionId, projectId) {
@@ -53,7 +100,7 @@ function resolveProjectId(repoUrl, workingDir, defaultProjectId) {
53
100
  return defaultProjectId;
54
101
  }
55
102
  function detectWorkingDir(attrs) {
56
- for (const key of ['process.working_directory', 'github.copilot.working_dir']) {
103
+ for (const key of ['process.working_directory', 'github.copilot.working_dir', 'claude_code.working_dir']) {
57
104
  const v = getAttr(attrs, key);
58
105
  if (v && String(v).trim())
59
106
  return String(v).trim();
@@ -65,6 +112,120 @@ function detectWorkingDir(attrs) {
65
112
  const pendingChatIds = new Map(); // `chat:${traceId}` → list of entryIds
66
113
  // Buffer tool calls that arrive before invoke_agent
67
114
  const pendingToolCalls = new Map(); // traceId → tool calls
115
+ const claudePromptEntries = new Map(); // prompt.id → trace entry
116
+ const claudeInteractionEntries = new Map(); // traceId → interaction entry
117
+ // Real Claude Code telemetry sends the child claude_code.llm_request span before its
118
+ // parent claude_code.interaction span within the same batch, so buffer llm_request's
119
+ // token/cost delta here until the interaction entry shows up to receive it.
120
+ const pendingClaudeLlmDeltas = new Map();
121
+ const CLAUDE_STATE_LIMIT = 1000;
122
+ function rememberClaudeEntry(map, key, entry) {
123
+ map.set(key, entry);
124
+ if (map.size > CLAUDE_STATE_LIMIT) {
125
+ const oldest = map.keys().next().value;
126
+ if (oldest)
127
+ map.delete(oldest);
128
+ }
129
+ }
130
+ function claudeEventId(record, attrs, eventName) {
131
+ const messageId = getStringAttr(attrs, 'message.uuid', 'tool_use_id');
132
+ const promptId = getStringAttr(attrs, 'prompt.id');
133
+ return `claude:${messageId ?? promptId ?? record.traceId ?? randomUUID()}:${eventName}`;
134
+ }
135
+ function processClaudeLogRecord(record, resourceAttrs, sessionId, projectId, workingDir) {
136
+ const attrs = record.attributes ?? [];
137
+ const eventName = getStringAttr(attrs, 'event.name') ?? getBodyText(record.body);
138
+ if (!eventName || !eventName.startsWith('claude_code.'))
139
+ return;
140
+ const eventTime = getStringAttr(attrs, 'event.timestamp');
141
+ const dateTime = eventTime ?? (record.timeUnixNano ? nanoToIso(record.timeUnixNano) : new Date().toISOString());
142
+ const promptId = getStringAttr(attrs, 'prompt.id');
143
+ const resolvedProjectId = resolveProjectId(getStringAttr(resourceAttrs, 'github.copilot.git.repository', 'vcs.repository.url'), getStringAttr(attrs, 'process.working_directory', 'github.copilot.working_dir', 'claude_code.working_dir') ?? workingDir, projectId);
144
+ const eventSessionId = getStringAttr(attrs, 'session.id') ?? sessionId;
145
+ if (eventName === 'claude_code.user_prompt') {
146
+ const entry = {
147
+ id: claudeEventId(record, attrs, 'prompt'),
148
+ sessionId: eventSessionId,
149
+ dateTime,
150
+ prompt: getStringAttr(attrs, 'prompt') ?? '[Claude Code prompt]',
151
+ tokens: { input: 0, output: 0, cached: 0, reasoning: 0, written: 0, total: 0 },
152
+ aiCredits: 0,
153
+ durationMs: 0,
154
+ toolCalls: [],
155
+ skillCount: 0,
156
+ agentCount: 0,
157
+ mcpCount: 0,
158
+ status: 'running',
159
+ };
160
+ if (promptId)
161
+ rememberClaudeEntry(claudePromptEntries, promptId, entry);
162
+ ensureSession(eventSessionId, resolvedProjectId);
163
+ upsertTrace(entry);
164
+ traceEvents.emit('trace:update', entry);
165
+ return;
166
+ }
167
+ if (eventName === 'claude_code.assistant_response') {
168
+ const existingEntry = promptId ? claudePromptEntries.get(promptId) : undefined;
169
+ const entry = existingEntry ?? {
170
+ id: claudeEventId(record, attrs, 'response'),
171
+ sessionId: eventSessionId,
172
+ dateTime,
173
+ prompt: '[Claude Code response]',
174
+ tokens: { input: 0, output: 0, cached: 0, reasoning: 0, written: 0, total: 0 },
175
+ aiCredits: 0,
176
+ durationMs: 0,
177
+ toolCalls: [],
178
+ skillCount: 0,
179
+ agentCount: 0,
180
+ mcpCount: 0,
181
+ status: 'running',
182
+ };
183
+ entry.response = getStringAttr(attrs, 'response');
184
+ entry.status = 'done';
185
+ entry.durationMs = Number(getStringAttr(attrs, 'duration_ms') ?? 0);
186
+ ensureSession(eventSessionId, resolvedProjectId);
187
+ upsertTrace(entry);
188
+ traceEvents.emit('trace:update', entry);
189
+ traceEvents.emit('trace:done', entry);
190
+ return;
191
+ }
192
+ if (eventName === 'claude_code.tool_result') {
193
+ const entry = promptId ? claudePromptEntries.get(promptId) : undefined;
194
+ if (!entry)
195
+ return;
196
+ const toolName = getStringAttr(attrs, 'tool_name') ?? 'Claude Code tool';
197
+ const toolId = getStringAttr(attrs, 'tool_use_id') ?? randomUUID();
198
+ if (entry.toolCalls.some(call => call.id === toolId))
199
+ return;
200
+ entry.toolCalls.push({
201
+ id: toolId,
202
+ name: toolName,
203
+ type: detectToolType(toolName),
204
+ input: {},
205
+ startedAt: Date.parse(dateTime),
206
+ endedAt: Date.parse(dateTime),
207
+ durationMs: Number(getStringAttr(attrs, 'duration_ms') ?? 0),
208
+ error: getStringAttr(attrs, 'error'),
209
+ });
210
+ upsertTrace(entry);
211
+ traceEvents.emit('trace:update', entry);
212
+ }
213
+ }
214
+ function processClaudeLogs(payload, defaultSessionId, projectId) {
215
+ if (!payload || !Array.isArray(payload.resourceLogs)) {
216
+ throw new Error('OTLP logs payload must contain resourceLogs');
217
+ }
218
+ for (const resourceLogs of payload.resourceLogs ?? []) {
219
+ const resourceAttrs = resourceLogs.resource?.attributes ?? [];
220
+ const sessionId = getStringAttr(resourceAttrs, 'session.id') ?? defaultSessionId;
221
+ const workingDir = detectWorkingDir(resourceAttrs);
222
+ for (const scopeLogs of resourceLogs.scopeLogs ?? []) {
223
+ for (const record of scopeLogs.logRecords ?? []) {
224
+ processClaudeLogRecord(record, resourceAttrs, sessionId, projectId, workingDir);
225
+ }
226
+ }
227
+ }
228
+ }
68
229
  function processSpans(spans, sessionId, projectId, workingDir) {
69
230
  for (const span of spans) {
70
231
  // DEBUG — log raw span to stderr when COPILOT_TRACER_DEBUG=1
@@ -75,6 +236,78 @@ function processSpans(spans, sessionId, projectId, workingDir) {
75
236
  const spanName = span.name;
76
237
  const traceId = span.traceId;
77
238
  const spanId = span.spanId;
239
+ if (spanName === 'claude_code.interaction') {
240
+ const attrs = span.attributes ?? [];
241
+ const inputTokens = Number(getAttr(attrs, 'input_tokens') ?? 0);
242
+ const outputTokens = Number(getAttr(attrs, 'output_tokens') ?? 0);
243
+ const model = getStringAttr(attrs, 'model', 'gen_ai.request.model');
244
+ const entry = {
245
+ id: spanId,
246
+ sessionId: getStringAttr(attrs, 'session.id') ?? sessionId,
247
+ dateTime: nanoToIso(span.startTimeUnixNano),
248
+ prompt: getStringAttr(attrs, 'user_prompt') ?? '[Claude Code interaction]',
249
+ tokens: { input: inputTokens, output: outputTokens, cached: Number(getAttr(attrs, 'cache_read_tokens') ?? 0), reasoning: 0, written: outputTokens, total: inputTokens + outputTokens },
250
+ aiCredits: calcClaudeCredits({ input: inputTokens, output: outputTokens }, model),
251
+ durationMs: Number(getAttr(attrs, 'interaction.duration_ms')
252
+ ?? (nanoToMs(span.endTimeUnixNano) - nanoToMs(span.startTimeUnixNano))),
253
+ toolCalls: [],
254
+ skillCount: 0,
255
+ agentCount: 0,
256
+ mcpCount: 0,
257
+ status: (span.status?.code ?? 0) === 2 ? 'error' : 'done',
258
+ error: span.status?.message,
259
+ };
260
+ // Apply any llm_request delta that arrived before this interaction span.
261
+ const pendingDelta = pendingClaudeLlmDeltas.get(traceId);
262
+ if (pendingDelta) {
263
+ entry.tokens = {
264
+ input: entry.tokens.input + pendingDelta.input,
265
+ output: entry.tokens.output + pendingDelta.output,
266
+ cached: entry.tokens.cached + pendingDelta.cached,
267
+ reasoning: 0,
268
+ written: entry.tokens.written + pendingDelta.output,
269
+ total: entry.tokens.total + pendingDelta.input + pendingDelta.output,
270
+ };
271
+ entry.aiCredits += pendingDelta.credits;
272
+ pendingClaudeLlmDeltas.delete(traceId);
273
+ }
274
+ ensureSession(entry.sessionId, resolveProjectId(getAttr(attrs, 'vcs.repository.url'), detectWorkingDir(attrs) ?? workingDir, projectId));
275
+ upsertTrace(entry);
276
+ rememberClaudeEntry(claudeInteractionEntries, traceId, entry);
277
+ traceEvents.emit('trace:update', entry);
278
+ traceEvents.emit('trace:done', entry);
279
+ continue;
280
+ }
281
+ if (spanName === 'claude_code.llm_request') {
282
+ const inputTokens = Number(getAttr(attrs, 'input_tokens') ?? 0);
283
+ const outputTokens = Number(getAttr(attrs, 'output_tokens') ?? 0);
284
+ const cachedTokens = Number(getAttr(attrs, 'cache_read_tokens') ?? 0);
285
+ const model = getStringAttr(attrs, 'model', 'gen_ai.request.model');
286
+ const entry = claudeInteractionEntries.get(traceId);
287
+ if (entry) {
288
+ entry.tokens = {
289
+ input: entry.tokens.input + inputTokens,
290
+ output: entry.tokens.output + outputTokens,
291
+ cached: entry.tokens.cached + cachedTokens,
292
+ reasoning: 0,
293
+ written: entry.tokens.written + outputTokens,
294
+ total: entry.tokens.total + inputTokens + outputTokens,
295
+ };
296
+ entry.aiCredits += calcClaudeCredits({ input: inputTokens, output: outputTokens }, model);
297
+ upsertTrace(entry);
298
+ traceEvents.emit('trace:update', entry);
299
+ }
300
+ else {
301
+ // Parent claude_code.interaction span hasn't arrived yet — buffer the delta.
302
+ const pending = pendingClaudeLlmDeltas.get(traceId) ?? { input: 0, output: 0, cached: 0, credits: 0 };
303
+ pending.input += inputTokens;
304
+ pending.output += outputTokens;
305
+ pending.cached += cachedTokens;
306
+ pending.credits += calcClaudeCredits({ input: inputTokens, output: outputTokens }, model);
307
+ rememberClaudeEntry(pendingClaudeLlmDeltas, traceId, pending);
308
+ }
309
+ continue;
310
+ }
78
311
  // ── invoke_agent span = top-level agent turn ──────────────────────────
79
312
  if (spanName === 'invoke_agent') {
80
313
  const startMs = nanoToMs(span.startTimeUnixNano);
@@ -426,8 +659,17 @@ export function registerOtlpRoutes(app, defaultSessionId, projectId) {
426
659
  res.status(400).json({ error: 'invalid payload' });
427
660
  }
428
661
  });
429
- // Metrics + logs — accept and ignore
662
+ app.post('/v1/logs', (req, res) => {
663
+ try {
664
+ processClaudeLogs(req.body, defaultSessionId, projectId);
665
+ res.status(200).json({ partialSuccess: {} });
666
+ }
667
+ catch (e) {
668
+ console.error('[OTLP] log parse error:', e);
669
+ res.status(400).json({ error: 'invalid payload' });
670
+ }
671
+ });
672
+ // Metrics remain accepted for Claude Code dashboards but are not trace entries.
430
673
  app.post('/v1/metrics', (_req, res) => res.status(200).json({ partialSuccess: {} }));
431
- app.post('/v1/logs', (_req, res) => res.status(200).json({ partialSuccess: {} }));
432
- console.log(' 📡 OTLP receiver ready on /v1/traces');
674
+ console.log(' 📡 OTLP receiver ready on /v1/traces and /v1/logs');
433
675
  }
package/dist/setup.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * copilot-tracer setup — auto-detect copilot CLI + VS Code and inject OTLP env config
2
+ * copilot-tracer setup — auto-detect Copilot/VS Code and inject OTLP env config
3
3
  *
4
4
  * What it does:
5
5
  * 1. Detect copilot CLI (which copilot)
@@ -16,12 +16,26 @@ import os from 'os';
16
16
  const OTEL_ENDPOINT_KEY = 'OTEL_EXPORTER_OTLP_ENDPOINT';
17
17
  const OTEL_CONTENT_KEY = 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT';
18
18
  const OTEL_ENABLED_KEY = 'COPILOT_OTEL_ENABLED';
19
+ const CLAUDE_TELEMETRY_KEY = 'CLAUDE_CODE_ENABLE_TELEMETRY';
20
+ const CLAUDE_TRACES_KEY = 'CLAUDE_CODE_ENHANCED_TELEMETRY_BETA';
21
+ const OTEL_LOGS_EXPORTER_KEY = 'OTEL_LOGS_EXPORTER';
22
+ const OTEL_TRACES_EXPORTER_KEY = 'OTEL_TRACES_EXPORTER';
23
+ const OTEL_PROTOCOL_KEY = 'OTEL_EXPORTER_OTLP_PROTOCOL';
24
+ const OTEL_LOG_PROMPTS_KEY = 'OTEL_LOG_USER_PROMPTS';
25
+ const OTEL_LOG_RESPONSES_KEY = 'OTEL_LOG_ASSISTANT_RESPONSES';
19
26
  function otelEnvBlock(port) {
20
27
  return [
21
28
  `# >>> copilot-tracer OTLP config (auto-added) >>>`,
22
29
  `export ${OTEL_ENDPOINT_KEY}=http://localhost:${port}`,
23
30
  `export ${OTEL_CONTENT_KEY}=true`,
24
31
  `export ${OTEL_ENABLED_KEY}=true`,
32
+ `export ${CLAUDE_TELEMETRY_KEY}=1`,
33
+ `export ${CLAUDE_TRACES_KEY}=1`,
34
+ `export ${OTEL_LOGS_EXPORTER_KEY}=otlp`,
35
+ `export ${OTEL_TRACES_EXPORTER_KEY}=otlp`,
36
+ `export ${OTEL_PROTOCOL_KEY}=http/json`,
37
+ `export ${OTEL_LOG_PROMPTS_KEY}=1`,
38
+ `export ${OTEL_LOG_RESPONSES_KEY}=1`,
25
39
  ``,
26
40
  `# Tag every copilot prompt with the terminal folder it ran from, so the`,
27
41
  `# tracer can attribute it to the right project. OTLP carries no working-dir,`,
@@ -42,6 +56,22 @@ function otelEnvBlock(port) {
42
56
  ` export OTEL_RESOURCE_ATTRIBUTES`,
43
57
  ` command copilot "\$@"`,
44
58
  `}`,
59
+ ``,
60
+ `claude() {`,
61
+ ` local _wd`,
62
+ ` if command -v python3 >/dev/null 2>&1; then`,
63
+ ` _wd="$(pwd | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read().strip(), safe="/"))')"`,
64
+ ` else`,
65
+ ` _wd="$(pwd)"`,
66
+ ` fi`,
67
+ ` if [ -n "\${OTEL_RESOURCE_ATTRIBUTES:-}" ]; then`,
68
+ ` OTEL_RESOURCE_ATTRIBUTES="\${OTEL_RESOURCE_ATTRIBUTES},claude_code.working_dir=\${_wd}"`,
69
+ ` else`,
70
+ ` OTEL_RESOURCE_ATTRIBUTES="claude_code.working_dir=\${_wd}"`,
71
+ ` fi`,
72
+ ` export OTEL_RESOURCE_ATTRIBUTES`,
73
+ ` command claude "\$@"`,
74
+ `}`,
45
75
  `# <<< copilot-tracer <<<`,
46
76
  ].join('\n');
47
77
  }
@@ -50,6 +80,13 @@ function vscodeEnvBlock(port) {
50
80
  [OTEL_ENDPOINT_KEY]: `http://localhost:${port}`,
51
81
  [OTEL_CONTENT_KEY]: 'true',
52
82
  [OTEL_ENABLED_KEY]: 'true',
83
+ [CLAUDE_TELEMETRY_KEY]: '1',
84
+ [CLAUDE_TRACES_KEY]: '1',
85
+ [OTEL_LOGS_EXPORTER_KEY]: 'otlp',
86
+ [OTEL_TRACES_EXPORTER_KEY]: 'otlp',
87
+ [OTEL_PROTOCOL_KEY]: 'http/json',
88
+ [OTEL_LOG_PROMPTS_KEY]: '1',
89
+ [OTEL_LOG_RESPONSES_KEY]: '1',
53
90
  };
54
91
  }
55
92
  // ── Detection helpers ─────────────────────────────────────────────────────────
@@ -110,11 +147,13 @@ function patchShellProfile(profilePath, port) {
110
147
  const block = otelEnvBlock(port);
111
148
  // Already has our block?
112
149
  if (content.includes('copilot-tracer OTLP config')) {
113
- // Check if port matches
114
- if (content.includes(`http://localhost:${port}`)) {
150
+ // Only skip if the embedded block is byte-identical to what we'd generate now —
151
+ // a marker + matching port isn't enough, since the block's env vars can gain new
152
+ // keys (e.g. Claude Code support) between tracer versions without the port changing.
153
+ if (content.includes(block)) {
115
154
  return { action: 'already_set' };
116
155
  }
117
- // Port changed — update
156
+ // Block is stale (port changed, or vars were added/changed) — replace it in place
118
157
  const updated = content.replace(/# >>> copilot-tracer OTLP config[\s\S]*?# <<< copilot-tracer <<</, block);
119
158
  fs.writeFileSync(profilePath, updated, 'utf8');
120
159
  return { action: 'updated' };
@@ -138,10 +177,9 @@ function patchVSCodeSettings(settingsPath, port) {
138
177
  const envKey = 'terminal.integrated.env.osx';
139
178
  const existing = (settings[envKey] ?? {});
140
179
  const newEnv = vscodeEnvBlock(port);
141
- // Check if already set correctly
142
- if (existing[OTEL_ENDPOINT_KEY] === `http://localhost:${port}` &&
143
- existing[OTEL_CONTENT_KEY] === 'true' &&
144
- existing[OTEL_ENABLED_KEY] === 'true') {
180
+ // Already set correctly only if every current key/value is present — checking a
181
+ // hardcoded subset let newer keys (e.g. Claude Code support) silently go unset.
182
+ if (Object.entries(newEnv).every(([k, v]) => existing[k] === v)) {
145
183
  return { action: 'already_set' };
146
184
  }
147
185
  const wasSet = !!existing[OTEL_ENDPOINT_KEY];
@@ -193,13 +231,13 @@ export function runSetup(port, silent = false) {
193
231
  const label = path.basename(profilePath);
194
232
  if (result.action === 'added') {
195
233
  console.log(`\n${CHECK} Shell profile patched: ${label}`);
196
- console.log(` ${ARROW} Added OTEL env vars (endpoint, content capture)`);
234
+ console.log(` ${ARROW} Added Copilot + Claude Code OTLP env vars`);
197
235
  console.log(` ${ARROW} Added copilot() wrapper — tags each prompt with the terminal folder`);
198
236
  console.log(` ${ARROW} Run: source ${profilePath}`);
199
237
  }
200
238
  else if (result.action === 'updated') {
201
239
  console.log(`\n${CHECK} Shell profile updated: ${label}`);
202
- console.log(` ${ARROW} Updated port to ${port} + copilot() wrapper`);
240
+ console.log(` ${ARROW} Updated port to ${port} + Claude Code/Copilot OTLP config`);
203
241
  console.log(` ${ARROW} Run: source ${profilePath}`);
204
242
  }
205
243
  else {
@@ -231,6 +269,13 @@ export function runSetup(port, silent = false) {
231
269
  process.env[OTEL_ENDPOINT_KEY] = `http://localhost:${port}`;
232
270
  process.env[OTEL_CONTENT_KEY] = 'true';
233
271
  process.env[OTEL_ENABLED_KEY] = 'true';
272
+ process.env[CLAUDE_TELEMETRY_KEY] = '1';
273
+ process.env[CLAUDE_TRACES_KEY] = '1';
274
+ process.env[OTEL_LOGS_EXPORTER_KEY] = 'otlp';
275
+ process.env[OTEL_TRACES_EXPORTER_KEY] = 'otlp';
276
+ process.env[OTEL_PROTOCOL_KEY] = 'http/json';
277
+ process.env[OTEL_LOG_PROMPTS_KEY] = '1';
278
+ process.env[OTEL_LOG_RESPONSES_KEY] = '1';
234
279
  // 6. Summary
235
280
  if (!silent) {
236
281
  const profileBase = profilePath ? path.basename(profilePath) : '.zshrc';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-tracer",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
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",
@@ -14,7 +14,7 @@
14
14
  "license": "MIT",
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "https://github.com/chuongnd/copilot-tracer.git"
17
+ "url": "https://github.com/chuongxl/copilot-tracer.git"
18
18
  },
19
19
  "engines": {
20
20
  "node": ">=18"
package/web/index.html CHANGED
@@ -321,7 +321,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
321
321
 
322
322
  <div class="stats-bar">
323
323
  <div class="stat"><span class="stat-label">Total Prompts</span><span class="stat-value" id="s-prompts">0</span></div>
324
- <div class="stat"><span class="stat-label">AI Credits</span><span class="stat-value credits" id="s-credits">0.00 cr | $0.0000</span></div>
324
+ <div class="stat"><span class="stat-label">Est Cost</span><span class="stat-value credits" id="s-credits">$0.0000</span></div>
325
325
  <div class="stat"><span class="stat-label">Total Tokens</span><span class="stat-value tokens" id="s-tokens">0</span></div>
326
326
  <div class="stat"><span class="stat-label">Cached</span><span class="stat-value tokens" id="s-cached">0</span></div>
327
327
  <div class="stat"><span class="stat-label">Reasoning</span><span class="stat-value" style="color:#d2a8ff" id="s-reasoning">0</span></div>
@@ -338,7 +338,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
338
338
  <tr>
339
339
  <th>Date / Time</th>
340
340
  <th>Prompt</th>
341
- <th>AI Credits</th>
341
+ <th>Est Cost</th>
342
342
  <th>Duration</th>
343
343
  <th>Cached Tokens</th>
344
344
  <th>Written Tokens</th>
@@ -488,7 +488,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
488
488
  <div class="dash-total-value tokens">${data.totals.tokens.toLocaleString()}</div>
489
489
  </div>
490
490
  <div class="dash-total-card">
491
- <div class="dash-total-label">Total AI Credits</div>
491
+ <div class="dash-total-label">Total Est Cost</div>
492
492
  <div class="dash-total-value credits">${fmtCredits(data.totals.credits)}</div>
493
493
  </div>
494
494
  `;
@@ -525,7 +525,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
525
525
  <span class="dash-project-stat-value tokens">${p.totalTokens.toLocaleString()}</span>
526
526
  </div>
527
527
  <div class="dash-project-stat">
528
- <span class="dash-project-stat-label">AI Credits</span>
528
+ <span class="dash-project-stat-label">Est Cost</span>
529
529
  <span class="dash-project-stat-value credits">${fmtCredits(p.totalCredits)}</span>
530
530
  </div>
531
531
  </div>
@@ -559,7 +559,7 @@ Reply with ONLY the rewritten prompt. No explanation, no preamble.`;
559
559
  return ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1) + 's';
560
560
  }
561
561
 
562
- function fmtCredits(c) { return (c||0).toFixed(2) + ' cr | $' + ((c||0)*0.01).toFixed(4); }
562
+ function fmtCredits(c) { return '$' + ((c||0)*0.01).toFixed(4); }
563
563
 
564
564
  function fmtDateTime(dt) {
565
565
  if (!dt) return '—';
@@ -650,7 +650,7 @@ Written: ${(t.tokens?.written||0).toLocaleString()}
650
650
  Reasoning: ${(t.tokens?.reasoning||0).toLocaleString()}
651
651
  Output: ${(t.tokens?.output||0).toLocaleString()}
652
652
  Total: ${(t.tokens?.total||0).toLocaleString()}
653
- AI Credits: ${fmtCredits(t.aiCredits)}
653
+ Est Cost: ${fmtCredits(t.aiCredits)}
654
654
  </div>
655
655
  </div>
656
656
  ${t.toolCalls?.length ? `
@@ -725,14 +725,14 @@ AI Credits: ${fmtCredits(t.aiCredits)}
725
725
  function showCreditDetail(id) {
726
726
  const t = traces[id];
727
727
  if (!t) return;
728
- document.getElementById('modal-title').textContent = `AI Credits: ${fmtCredits(t.aiCredits)}`;
728
+ document.getElementById('modal-title').textContent = `Est Cost: ${fmtCredits(t.aiCredits)}`;
729
729
  document.getElementById('modal-body').innerHTML = `
730
730
  <div class="detail-box" style="line-height:2">
731
731
  Input tokens: ${(t.tokens?.input||0).toLocaleString()} × $0.003/1k = $${((t.tokens?.input||0)/1000*0.003).toFixed(5)}
732
732
  Written tokens: ${(t.tokens?.written||0).toLocaleString()} × $0.006/1k = $${((t.tokens?.written||0)/1000*0.006).toFixed(5)}
733
733
  Reasoning tokens: ${(t.tokens?.reasoning||0).toLocaleString()} × $0.009/1k = $${((t.tokens?.reasoning||0)/1000*0.009).toFixed(5)}
734
734
  ─────────────────────────────────────────
735
- Total AI Credits: ${fmtCredits(t.aiCredits)}
735
+ Total Est Cost: ${fmtCredits(t.aiCredits)}
736
736
  Duration: ${fmtDuration(t.durationMs)}
737
737
  </div>
738
738
  `;