panopticon-cli 0.5.3 → 0.5.4

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.
@@ -149663,6 +149663,8 @@ async function resumeAgent(agentId, message) {
149663
149663
  createSession(normalizedId, agentState.workspace, claudeCmd, {
149664
149664
  env: {
149665
149665
  PANOPTICON_AGENT_ID: normalizedId,
149666
+ PANOPTICON_ISSUE_ID: agentState.issueId || "",
149667
+ PANOPTICON_SESSION_TYPE: agentState.phase || "implementation",
149666
149668
  CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION: "false",
149667
149669
  ...providerEnv
149668
149670
  }
@@ -149738,6 +149740,8 @@ function recoverAgent(agentId) {
149738
149740
  createSession(normalizedId, state.workspace, claudeCmd, {
149739
149741
  env: {
149740
149742
  PANOPTICON_AGENT_ID: normalizedId,
149743
+ PANOPTICON_ISSUE_ID: state.issueId || "",
149744
+ PANOPTICON_SESSION_TYPE: state.phase || "implementation",
149741
149745
  CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION: "false",
149742
149746
  ...providerEnv
149743
149747
  }
@@ -172206,6 +172210,10 @@ var MERGE_STUCK_STALENESS_MS = 2 * 60 * 1e3;
172206
172210
  var MERGE_STUCK_COOLDOWN_MS = 10 * 60 * 1e3;
172207
172211
  var MERGE_STUCK_MAX_ATTEMPTS = 3;
172208
172212
  var mergeStuckCooldowns = /* @__PURE__ */ new Map();
172213
+ var mergeReadyNotifier = null;
172214
+ function setMergeReadyNotifier(fn) {
172215
+ mergeReadyNotifier = fn;
172216
+ }
172209
172217
  async function checkReadyForMergeStuck() {
172210
172218
  const actions = [];
172211
172219
  try {
@@ -172215,7 +172223,6 @@ async function checkReadyForMergeStuck() {
172215
172223
  const content = readFileSync33(REVIEW_STATUS_FILE, "utf-8");
172216
172224
  const statuses = JSON.parse(content);
172217
172225
  const now = Date.now();
172218
- const apiPort = process.env.API_PORT || process.env.PORT || "3011";
172219
172226
  const state = loadState();
172220
172227
  const attemptCounts = state.mergeStuckAttempts ?? {};
172221
172228
  let stateModified = false;
@@ -172238,10 +172245,19 @@ async function checkReadyForMergeStuck() {
172238
172245
  continue;
172239
172246
  }
172240
172247
  const ageMin = Math.round((now - new Date(status.updatedAt).getTime()) / 6e4);
172241
- const msg = `${key} is readyForMerge (age: ${ageMin}m) \u2014 waiting for human approval`;
172242
- actions.push(msg);
172243
- console.log(`[deacon] ${msg}`);
172248
+ console.warn(`[deacon] readyForMerge stuck for ${key} (age: ${ageMin}m, attempts: ${attempts}) \u2014 merge requires manual action via MERGE button`);
172244
172249
  mergeStuckCooldowns.set(key, now);
172250
+ attemptCounts[key] = attempts + 1;
172251
+ stateModified = true;
172252
+ const msg = `Stuck-merge: ${key} has been readyForMerge for ${ageMin}m \u2014 click MERGE to proceed`;
172253
+ if (mergeReadyNotifier) {
172254
+ mergeReadyNotifier(status.issueId ?? key);
172255
+ actions.push(msg);
172256
+ console.log(`[deacon] merge:ready notification sent for ${key}`);
172257
+ } else {
172258
+ actions.push(msg);
172259
+ console.warn(`[deacon] No mergeReadyNotifier registered \u2014 dashboard will not be notified for ${key}`);
172260
+ }
172245
172261
  }
172246
172262
  if (stateModified) {
172247
172263
  state.mergeStuckAttempts = attemptCounts;
@@ -174344,6 +174360,41 @@ function insertCostEvents(events, sourceFile) {
174344
174360
  insertMany(events);
174345
174361
  return { inserted, duplicates };
174346
174362
  }
174363
+ function getCostsByIssueFromDb() {
174364
+ const db = getDatabase();
174365
+ const rows = db.prepare(`
174366
+ SELECT
174367
+ UPPER(issue_id) as issue_id,
174368
+ SUM(cost) as total_cost,
174369
+ SUM(input) as input_tokens,
174370
+ SUM(output) as output_tokens,
174371
+ SUM(cache_read) as cache_read_tokens,
174372
+ SUM(cache_write) as cache_write_tokens,
174373
+ MAX(ts) as last_updated
174374
+ FROM cost_events
174375
+ GROUP BY UPPER(issue_id)
174376
+ ORDER BY total_cost DESC
174377
+ `).all();
174378
+ const result = {};
174379
+ for (const row of rows) {
174380
+ const models = getModelBreakdownForIssue(db, row.issue_id);
174381
+ const stages = getStageBreakdownForIssue(db, row.issue_id);
174382
+ result[row.issue_id] = {
174383
+ issueId: row.issue_id,
174384
+ totalCost: row.total_cost,
174385
+ inputTokens: row.input_tokens,
174386
+ outputTokens: row.output_tokens,
174387
+ cacheReadTokens: row.cache_read_tokens,
174388
+ cacheWriteTokens: row.cache_write_tokens,
174389
+ lastUpdated: row.last_updated,
174390
+ budgetWarning: false,
174391
+ // Set externally
174392
+ models,
174393
+ stages
174394
+ };
174395
+ }
174396
+ return result;
174397
+ }
174347
174398
  function getCostForIssueFromDb(issueId) {
174348
174399
  const db = getDatabase();
174349
174400
  const row = db.prepare(`
@@ -174694,9 +174745,6 @@ function deduplicateEvents() {
174694
174745
  }
174695
174746
  return removed;
174696
174747
  }
174697
- function eventsFileExists() {
174698
- return existsSync49(getEventsFile());
174699
- }
174700
174748
 
174701
174749
  // ../../lib/costs/aggregator.ts
174702
174750
  import { existsSync as existsSync50, mkdirSync as mkdirSync33, readFileSync as readFileSync42, writeFileSync as writeFileSync34, renameSync as renameSync3 } from "fs";
@@ -174866,17 +174914,6 @@ function getCostsForIssue(issueId) {
174866
174914
  const issueKey = issueId.toUpperCase();
174867
174915
  return cache.issues[issueKey] || null;
174868
174916
  }
174869
- function getCacheStatus() {
174870
- const cache = loadCache();
174871
- const metadata = getLastEventMetadata();
174872
- return {
174873
- status: cache.status,
174874
- lastEventTs: cache.lastEventTs,
174875
- eventCount: cache.lastEventLine,
174876
- issueCount: Object.keys(cache.issues).length,
174877
- needsSync: metadata.lastEventLine !== cache.lastEventLine
174878
- };
174879
- }
174880
174917
 
174881
174918
  // ../../lib/costs/migration.ts
174882
174919
  import { existsSync as existsSync51, readdirSync as readdirSync17, readFileSync as readFileSync43 } from "fs";
@@ -175183,23 +175220,6 @@ function migrateAllSessions() {
175183
175220
  console.log(` Warnings: ${stats.warnings.length}`);
175184
175221
  return stats;
175185
175222
  }
175186
- function needsMigration() {
175187
- if (!eventsFileExists()) {
175188
- return true;
175189
- }
175190
- const metadata = getLastEventMetadata();
175191
- if (metadata.totalEvents === 0) {
175192
- return true;
175193
- }
175194
- return false;
175195
- }
175196
- function migrateIfNeeded() {
175197
- if (!needsMigration()) {
175198
- console.log("Migration not needed - events file already exists with data");
175199
- return null;
175200
- }
175201
- return migrateAllSessions();
175202
- }
175203
175223
 
175204
175224
  // ../../lib/costs/sync-wal.ts
175205
175225
  init_projects();
@@ -175642,7 +175662,8 @@ function setReviewStatus2(issueId, update) {
175642
175662
  updateLinearIssueStatus(issueId, "In Review").catch((err) => {
175643
175663
  console.error(`[status] Error updating Linear to In Review for ${issueId}:`, err);
175644
175664
  });
175645
- console.log(`[merge] ${issueId} is ready for merge \u2014 waiting for human approval via MERGE button`);
175665
+ console.log(`[merge] ${issueId} is ready for merge \u2014 emitting merge:ready notification`);
175666
+ socketIo.emit("merge:ready", { issueId });
175646
175667
  }
175647
175668
  return updated;
175648
175669
  }
@@ -180767,6 +180788,33 @@ app.post("/api/workspaces/:issueId/request-review", async (req, res) => {
180767
180788
  });
180768
180789
  }
180769
180790
  if (existingStatus?.reviewStatus === "passed") {
180791
+ if (existingStatus.testStatus === "failed") {
180792
+ console.log(`[request-review] ${issueId}: review passed but tests failed \u2014 re-queuing test specialist`);
180793
+ setReviewStatus2(issueId, { testStatus: "pending" });
180794
+ try {
180795
+ const teamPrefix = extractTeamPrefix(issueId);
180796
+ const projectConfig = teamPrefix ? findProjectByTeam(teamPrefix) : null;
180797
+ const projectPath2 = projectConfig?.path || "";
180798
+ const workspacesDir = projectConfig?.workspace?.workspaces_dir || "workspaces";
180799
+ const workspacePath2 = join60(projectPath2, workspacesDir, `feature-${issueId.toLowerCase()}`);
180800
+ const branchName2 = `feature/${issueId.toLowerCase()}`;
180801
+ setReviewStatus2(issueId, { testStatus: "testing" });
180802
+ const { wakeSpecialistOrQueue: wakeSpecialistOrQueue2 } = await Promise.resolve().then(() => (init_specialists(), specialists_exports));
180803
+ const wakeResult = await wakeSpecialistOrQueue2("test-agent", {
180804
+ issueId,
180805
+ workspace: workspacePath2,
180806
+ branch: branchName2
180807
+ });
180808
+ console.log(`[request-review] Test specialist ${wakeResult.success ? "woken" : "failed"} for ${issueId}`);
180809
+ } catch (err) {
180810
+ console.warn(`[request-review] Failed to queue test specialist for ${issueId}: ${err.message}`);
180811
+ }
180812
+ return res.json({
180813
+ success: true,
180814
+ requeued: true,
180815
+ message: `Tests re-queued for ${issueId} (review already passed)`
180816
+ });
180817
+ }
180770
180818
  console.log(`[request-review] ${issueId}: review already passed \u2014 returning success no-op`);
180771
180819
  return res.json({
180772
180820
  success: true,
@@ -184584,16 +184632,8 @@ app.get("/api/costs/summary", (_req, res) => {
184584
184632
  });
184585
184633
  app.get("/api/costs/by-issue", async (_req, res) => {
184586
184634
  try {
184587
- if (needsMigration()) {
184588
- console.log("Running cost migration on first request...");
184589
- const stats = migrateIfNeeded();
184590
- if (stats) {
184591
- console.log(`Migration complete: ${stats.eventsCreated} events created, ${stats.errors.length} errors`);
184592
- }
184593
- }
184594
- const cache = syncCache();
184595
- const cacheStatus = getCacheStatus();
184596
- const issues = Object.entries(cache.issues).map(([issueId, data]) => ({
184635
+ const dbIssues = getCostsByIssueFromDb();
184636
+ const issues = Object.entries(dbIssues).map(([issueId, data]) => ({
184597
184637
  issueId,
184598
184638
  totalCost: data.totalCost,
184599
184639
  tokenCount: data.inputTokens + data.outputTokens + data.cacheReadTokens + data.cacheWriteTokens,
@@ -184601,32 +184641,28 @@ app.get("/api/costs/by-issue", async (_req, res) => {
184601
184641
  outputTokens: data.outputTokens,
184602
184642
  cacheReadTokens: data.cacheReadTokens,
184603
184643
  cacheWriteTokens: data.cacheWriteTokens,
184604
- // Legacy fields (keep for backward compatibility)
184644
+ // Per-model breakdown
184605
184645
  models: data.models,
184606
- providers: data.providers,
184607
- // New per-model breakdown (PAN-105)
184608
184646
  byModel: Object.fromEntries(
184609
184647
  Object.entries(data.models).map(([model, stats]) => [
184610
184648
  model,
184611
184649
  { cost: stats.cost, tokens: stats.tokens }
184612
184650
  ])
184613
184651
  ),
184614
- // New per-stage breakdown (PAN-105)
184652
+ // Per-stage breakdown
184615
184653
  byStage: Object.fromEntries(
184616
184654
  Object.entries(data.stages || {}).map(([stage, stats]) => [
184617
184655
  stage,
184618
184656
  { cost: stats.cost, tokens: stats.tokens }
184619
184657
  ])
184620
184658
  ),
184621
- budget: data.budget,
184622
184659
  budgetWarning: data.budgetWarning,
184623
184660
  lastUpdated: data.lastUpdated
184624
184661
  }));
184625
184662
  issues.sort((a, b) => b.totalCost - a.totalCost);
184626
184663
  res.json({
184627
- status: cacheStatus.status,
184628
- lastEventTs: cacheStatus.lastEventTs,
184629
- eventCount: cacheStatus.eventCount,
184664
+ status: "live",
184665
+ eventCount: issues.length,
184630
184666
  issues
184631
184667
  });
184632
184668
  } catch (error) {
@@ -184888,6 +184924,7 @@ var socketIo = new Server(server, {
184888
184924
  path: "/socket.io",
184889
184925
  cors: { origin: "*" }
184890
184926
  });
184927
+ setMergeReadyNotifier((issueId) => socketIo.emit("merge:ready", { issueId }));
184891
184928
  server.on("upgrade", (request2, socket, head) => {
184892
184929
  const pathname = new URL(request2.url || "", `http://${request2.headers.host}`).pathname;
184893
184930
  if (pathname === "/ws/terminal") {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getRecentRunLogs,
3
3
  init_specialist_logs
4
- } from "./chunk-2V2DQ3IX.js";
4
+ } from "./chunk-ID4OYXVH.js";
5
5
  import {
6
6
  getModelId,
7
7
  init_work_type_router
@@ -255,4 +255,4 @@ export {
255
255
  regenerateContextDigest,
256
256
  scheduleDigestGeneration
257
257
  };
258
- //# sourceMappingURL=specialist-context-53AWO6AE.js.map
258
+ //# sourceMappingURL=specialist-context-C66TEMXS.js.map
@@ -16,7 +16,7 @@ import {
16
16
  isRunLogActive,
17
17
  listRunLogs,
18
18
  parseLogMetadata
19
- } from "./chunk-2V2DQ3IX.js";
19
+ } from "./chunk-ID4OYXVH.js";
20
20
  import "./chunk-HRU7S4TA.js";
21
21
  import "./chunk-JQBV3Q2W.js";
22
22
  import "./chunk-USYP2SBE.js";
@@ -43,4 +43,4 @@ export {
43
43
  listRunLogs,
44
44
  parseLogMetadata
45
45
  };
46
- //# sourceMappingURL=specialist-logs-QREUJ4HN.js.map
46
+ //# sourceMappingURL=specialist-logs-CJKXM3SR.js.map
@@ -55,7 +55,7 @@ import {
55
55
  wakeSpecialist,
56
56
  wakeSpecialistOrQueue,
57
57
  wakeSpecialistWithTask
58
- } from "./chunk-2V2DQ3IX.js";
58
+ } from "./chunk-ID4OYXVH.js";
59
59
  import "./chunk-HRU7S4TA.js";
60
60
  import "./chunk-JQBV3Q2W.js";
61
61
  import "./chunk-USYP2SBE.js";
@@ -121,4 +121,4 @@ export {
121
121
  wakeSpecialistOrQueue,
122
122
  wakeSpecialistWithTask
123
123
  };
124
- //# sourceMappingURL=specialists-2DBBXRCK.js.map
124
+ //# sourceMappingURL=specialists-NXYD4Z62.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panopticon-cli",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Multi-agent orchestration for AI coding assistants (Claude Code, Codex, Cursor, Gemini CLI)",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deep cost recovery: scans ALL Claude transcripts, including non-workspace ones.
4
+ * For transcripts not in a workspace dir, infers issue ID from conversation content.
5
+ */
6
+
7
+ import { readdirSync, readFileSync, statSync } from 'fs';
8
+ import { join, basename } from 'path';
9
+ import { homedir } from 'os';
10
+ import Database from 'better-sqlite3';
11
+
12
+ const CLAUDE_PROJECTS = join(homedir(), '.claude', 'projects');
13
+ const DB_PATH = join(homedir(), '.panopticon', 'panopticon.db');
14
+
15
+ const PRICING = [
16
+ { provider: 'anthropic', model: 'claude-opus-4.6', inputPer1k: 5e-3, outputPer1k: 0.025, cacheReadPer1k: 5e-4, cacheWrite5mPer1k: 625e-5 },
17
+ { provider: 'anthropic', model: 'claude-opus-4-6', inputPer1k: 5e-3, outputPer1k: 0.025, cacheReadPer1k: 5e-4, cacheWrite5mPer1k: 625e-5 },
18
+ { provider: 'anthropic', model: 'claude-opus-4-1', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 15e-4, cacheWrite5mPer1k: 0.01875 },
19
+ { provider: 'anthropic', model: 'claude-opus-4', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 15e-4, cacheWrite5mPer1k: 0.01875 },
20
+ { provider: 'anthropic', model: 'claude-sonnet-4.5', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5 },
21
+ { provider: 'anthropic', model: 'claude-sonnet-4-6', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5 },
22
+ { provider: 'anthropic', model: 'claude-sonnet-4', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5 },
23
+ { provider: 'anthropic', model: 'claude-haiku-4.5', inputPer1k: 1e-3, outputPer1k: 5e-3, cacheReadPer1k: 1e-4, cacheWrite5mPer1k: 125e-5 },
24
+ { provider: 'anthropic', model: 'claude-haiku-4', inputPer1k: 1e-3, outputPer1k: 5e-3, cacheReadPer1k: 1e-4, cacheWrite5mPer1k: 125e-5 },
25
+ { provider: 'anthropic', model: 'claude-haiku-3', inputPer1k: 25e-5, outputPer1k: 125e-5, cacheReadPer1k: 3e-5, cacheWrite5mPer1k: 3e-4 },
26
+ { provider: 'custom', model: 'kimi-k2.5', inputPer1k: 6e-4, outputPer1k: 2e-3, cacheReadPer1k: 6e-5, cacheWrite5mPer1k: 75e-5 },
27
+ { provider: 'custom', model: 'kimi-for-coding', inputPer1k: 6e-4, outputPer1k: 2e-3, cacheReadPer1k: 6e-5, cacheWrite5mPer1k: 75e-5 },
28
+ ];
29
+
30
+ function getPricing(model) {
31
+ return PRICING.find(p => model.startsWith(p.model)) || PRICING.find(p => model.includes(p.model)) || null;
32
+ }
33
+
34
+ function calculateCost(usage, pricing) {
35
+ let cost = 0;
36
+ let inputMul = 1, outputMul = 1;
37
+ const totalInput = usage.input + (usage.cacheRead || 0) + (usage.cacheWrite || 0);
38
+ if (pricing.model.includes('sonnet-4') && totalInput > 200000) { inputMul = 2; outputMul = 1.5; }
39
+ cost += usage.input / 1000 * pricing.inputPer1k * inputMul;
40
+ cost += usage.output / 1000 * pricing.outputPer1k * outputMul;
41
+ if (usage.cacheRead && pricing.cacheReadPer1k) cost += usage.cacheRead / 1000 * pricing.cacheReadPer1k;
42
+ if (usage.cacheWrite && pricing.cacheWrite5mPer1k) cost += usage.cacheWrite / 1000 * pricing.cacheWrite5mPer1k;
43
+ return Math.round(cost * 1e6) / 1e6;
44
+ }
45
+
46
+ // Issue ID pattern: PAN-123, MIN-456, KRUX-1, CLI-1, AUR-1, etc.
47
+ const ISSUE_RE = /\b(PAN|MIN|AUR|KRUX|CLI)-(\d+)\b/gi;
48
+
49
+ function inferIssueFromPath(dirName) {
50
+ const match = dirName.match(/(pan|min|aud|krux|cli)[-](\d+)/i);
51
+ if (match) return `${match[1].toUpperCase()}-${match[2]}`;
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * Infer the primary issue from transcript content by counting mentions.
57
+ * Only considers user and assistant messages, not system/tool content.
58
+ */
59
+ function inferIssueFromContent(lines) {
60
+ const counts = {};
61
+ for (const line of lines) {
62
+ if (!line.trim()) continue;
63
+ try {
64
+ const entry = JSON.parse(line);
65
+ // Only look at human and assistant messages for issue mentions
66
+ if (entry.type !== 'human' && entry.type !== 'assistant') continue;
67
+ const text = JSON.stringify(entry.message || '');
68
+ let match;
69
+ const re = new RegExp(ISSUE_RE.source, 'gi');
70
+ while ((match = re.exec(text)) !== null) {
71
+ const id = `${match[1].toUpperCase()}-${match[2]}`;
72
+ counts[id] = (counts[id] || 0) + 1;
73
+ }
74
+ } catch {}
75
+ }
76
+
77
+ // Return the most-mentioned issue (if any has 2+ mentions)
78
+ const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
79
+ if (sorted.length > 0 && sorted[0][1] >= 2) {
80
+ return sorted[0][0];
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function findTranscriptFiles(dir) {
86
+ const files = [];
87
+ try {
88
+ for (const entry of readdirSync(dir)) {
89
+ if (entry.endsWith('.jsonl')) {
90
+ const full = join(dir, entry);
91
+ try { if (statSync(full).isFile()) files.push(full); } catch {}
92
+ }
93
+ }
94
+ } catch {}
95
+ return files;
96
+ }
97
+
98
+ // Main
99
+ const db = new Database(DB_PATH);
100
+ db.pragma('journal_mode = WAL');
101
+
102
+ const insert = db.prepare(`
103
+ INSERT OR IGNORE INTO cost_events (
104
+ ts, agent_id, issue_id, session_type, provider, model,
105
+ input, output, cache_read, cache_write, cost, request_id, source_file
106
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
107
+ `);
108
+
109
+ let totalInserted = 0;
110
+ let totalDuplicates = 0;
111
+ let totalUnattributed = 0;
112
+ const issueStats = {};
113
+
114
+ const projectDirs = readdirSync(CLAUDE_PROJECTS);
115
+
116
+ for (const dirName of projectDirs) {
117
+ const projectDir = join(CLAUDE_PROJECTS, dirName);
118
+ try { if (!statSync(projectDir).isDirectory()) continue; } catch { continue; }
119
+
120
+ // Try to get issue from path first
121
+ const pathIssueId = inferIssueFromPath(dirName);
122
+
123
+ const transcripts = findTranscriptFiles(projectDir);
124
+ if (transcripts.length === 0) continue;
125
+
126
+ for (const transcript of transcripts) {
127
+ let content;
128
+ try { content = readFileSync(transcript, 'utf-8'); } catch { continue; }
129
+ const lines = content.split('\n');
130
+
131
+ // Determine issue ID: path first, then content inference
132
+ let issueId = pathIssueId;
133
+ if (!issueId) {
134
+ issueId = inferIssueFromContent(lines);
135
+ }
136
+
137
+ if (!issueId) {
138
+ // Count usage events we're skipping
139
+ for (const line of lines) {
140
+ try {
141
+ const entry = JSON.parse(line);
142
+ if (entry.type === 'assistant' && entry.message?.usage) {
143
+ const u = entry.message.usage;
144
+ if ((u.input_tokens || 0) + (u.output_tokens || 0) > 0) totalUnattributed++;
145
+ }
146
+ } catch {}
147
+ }
148
+ continue;
149
+ }
150
+
151
+ for (const line of lines) {
152
+ if (!line.trim()) continue;
153
+ try {
154
+ const entry = JSON.parse(line);
155
+ if (entry.type !== 'assistant' || !entry.message?.usage) continue;
156
+
157
+ const usage = entry.message.usage;
158
+ const model = entry.message.model || 'claude-sonnet-4';
159
+ const requestId = entry.requestId;
160
+ if (!requestId) continue;
161
+
162
+ const input = usage.input_tokens || 0;
163
+ const output = usage.output_tokens || 0;
164
+ const cacheRead = usage.cache_read_input_tokens || 0;
165
+ const cacheWrite = usage.cache_creation_input_tokens || 0;
166
+ if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) continue;
167
+
168
+ let provider = 'anthropic';
169
+ if (model.includes('gpt')) provider = 'openai';
170
+ else if (model.includes('gemini')) provider = 'google';
171
+ else if (model.includes('kimi')) provider = 'custom';
172
+
173
+ const pricing = getPricing(model);
174
+ if (!pricing) continue;
175
+
176
+ const cost = calculateCost({ input, output, cacheRead, cacheWrite }, pricing);
177
+ const ts = entry.timestamp || new Date(statSync(transcript).mtime).toISOString();
178
+
179
+ const result = insert.run(
180
+ ts, 'recovered-deep', issueId, 'interactive', provider, model,
181
+ input, output, cacheRead, cacheWrite, cost, requestId, basename(transcript)
182
+ );
183
+
184
+ if (result.changes > 0) {
185
+ totalInserted++;
186
+ if (!issueStats[issueId]) issueStats[issueId] = { inserted: 0, cost: 0 };
187
+ issueStats[issueId].inserted++;
188
+ issueStats[issueId].cost += cost;
189
+ } else {
190
+ totalDuplicates++;
191
+ }
192
+ } catch {}
193
+ }
194
+ }
195
+ }
196
+
197
+ db.close();
198
+
199
+ console.log(`\nDeep Cost Recovery Complete`);
200
+ console.log(` NEW events inserted: ${totalInserted}`);
201
+ console.log(` Duplicates skipped: ${totalDuplicates}`);
202
+ console.log(` Unattributable events: ${totalUnattributed}`);
203
+ console.log(`\nNewly recovered costs by issue:`);
204
+ const sorted = Object.entries(issueStats).sort((a, b) => b[1].cost - a[1].cost);
205
+ for (const [id, stats] of sorted) {
206
+ console.log(` ${id.padEnd(12)} ${String(stats.inserted).padStart(5)} events $${stats.cost.toFixed(2)}`);
207
+ }
208
+ const totalCost = sorted.reduce((sum, [, s]) => sum + s.cost, 0);
209
+ console.log(`\n TOTAL NEWLY RECOVERED: $${totalCost.toFixed(2)}`);