panopticon-cli 0.5.1 → 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.
Files changed (120) hide show
  1. package/dist/{agents-5OPQKM5K.js → agents-HNMF52RM.js} +7 -6
  2. package/dist/{chunk-F5555J3A.js → chunk-4HST45MO.js} +13 -27
  3. package/dist/chunk-4HST45MO.js.map +1 -0
  4. package/dist/{chunk-YLPSQAM2.js → chunk-565HZ6VV.js} +2 -2
  5. package/dist/chunk-6N2KBSJA.js +452 -0
  6. package/dist/chunk-6N2KBSJA.js.map +1 -0
  7. package/dist/{chunk-4YSYJ4HM.js → chunk-DFNVHK3N.js} +2 -2
  8. package/dist/{chunk-7SN4L4PH.js → chunk-HOGYHJ2G.js} +2 -2
  9. package/dist/{chunk-FTCPTHIJ.js → chunk-HRU7S4TA.js} +24 -7
  10. package/dist/chunk-HRU7S4TA.js.map +1 -0
  11. package/dist/{chunk-OWHXCGVO.js → chunk-ID4OYXVH.js} +378 -101
  12. package/dist/chunk-ID4OYXVH.js.map +1 -0
  13. package/dist/{chunk-NLQRED36.js → chunk-KBHRXV5T.js} +3 -3
  14. package/dist/chunk-KBHRXV5T.js.map +1 -0
  15. package/dist/{chunk-VHKSS7QX.js → chunk-KY2E2Q3T.js} +25 -19
  16. package/dist/chunk-KY2E2Q3T.js.map +1 -0
  17. package/dist/{chunk-CWELWPWQ.js → chunk-MOPGR3CL.js} +1 -1
  18. package/dist/chunk-MOPGR3CL.js.map +1 -0
  19. package/dist/{chunk-2V4NF7J2.js → chunk-RLZQB7HS.js} +2 -2
  20. package/dist/chunk-RLZQB7HS.js.map +1 -0
  21. package/dist/{chunk-76F6DSVS.js → chunk-T7BBPDEJ.js} +11 -7
  22. package/dist/chunk-T7BBPDEJ.js.map +1 -0
  23. package/dist/chunk-USYP2SBE.js +317 -0
  24. package/dist/chunk-USYP2SBE.js.map +1 -0
  25. package/dist/{chunk-JM6V62LT.js → chunk-ZDNQFWR5.js} +2 -2
  26. package/dist/{chunk-JM6V62LT.js.map → chunk-ZDNQFWR5.js.map} +1 -1
  27. package/dist/{chunk-HJSM6E6U.js → chunk-ZP6EWSZV.js} +29 -322
  28. package/dist/chunk-ZP6EWSZV.js.map +1 -0
  29. package/dist/{chunk-PELXV435.js → chunk-ZTYHZMEC.js} +2 -2
  30. package/dist/chunk-ZTYHZMEC.js.map +1 -0
  31. package/dist/cli/index.js +1390 -777
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/config-yaml-OVZLKFMA.js +18 -0
  34. package/dist/dashboard/prompts/merge-agent.md +7 -5
  35. package/dist/dashboard/prompts/review-agent.md +12 -1
  36. package/dist/dashboard/prompts/test-agent.md +3 -1
  37. package/dist/dashboard/public/assets/index-DA6pnizT.js +767 -0
  38. package/dist/dashboard/public/assets/index-DSvt5pPn.css +32 -0
  39. package/dist/dashboard/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  40. package/dist/dashboard/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  41. package/dist/dashboard/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  42. package/dist/dashboard/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  43. package/dist/dashboard/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  44. package/dist/dashboard/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  45. package/dist/dashboard/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  46. package/dist/dashboard/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  47. package/dist/dashboard/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  48. package/dist/dashboard/public/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
  49. package/dist/dashboard/public/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
  50. package/dist/dashboard/public/assets/space-grotesk-latin-600-normal-BflQw4A9.woff +0 -0
  51. package/dist/dashboard/public/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
  52. package/dist/dashboard/public/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  53. package/dist/dashboard/public/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  54. package/dist/dashboard/public/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
  55. package/dist/dashboard/public/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
  56. package/dist/dashboard/public/assets/space-grotesk-latin-ext-600-normal-DxxdqCpr.woff2 +0 -0
  57. package/dist/dashboard/public/assets/space-grotesk-latin-ext-600-normal-VcznFIpX.woff +0 -0
  58. package/dist/dashboard/public/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  59. package/dist/dashboard/public/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  60. package/dist/dashboard/public/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
  61. package/dist/dashboard/public/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
  62. package/dist/dashboard/public/assets/space-grotesk-vietnamese-600-normal-D6zpsUhD.woff +0 -0
  63. package/dist/dashboard/public/assets/space-grotesk-vietnamese-600-normal-DUi7WF5p.woff2 +0 -0
  64. package/dist/dashboard/public/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  65. package/dist/dashboard/public/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  66. package/dist/dashboard/public/index.html +5 -3
  67. package/dist/dashboard/server.js +4728 -2767
  68. package/dist/{feedback-writer-VRMMWWTW.js → feedback-writer-T43PI5S2.js} +2 -2
  69. package/dist/{hume-WMAUBBV2.js → hume-CKJJ3OUU.js} +3 -3
  70. package/dist/index.js +4 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/{projects-CFX3RTDL.js → projects-KVM3MN3Y.js} +2 -2
  73. package/dist/{remote-agents-TFSMW7GN.js → remote-agents-ULPD6C5U.js} +3 -3
  74. package/dist/{remote-workspace-7FPGF2RM.js → remote-workspace-XX6ARE6I.js} +3 -3
  75. package/dist/{review-status-TDPSOU5J.js → review-status-XKUKZF6J.js} +3 -2
  76. package/dist/{specialist-context-WGUUYDWY.js → specialist-context-C66TEMXS.js} +6 -5
  77. package/dist/{specialist-context-WGUUYDWY.js.map → specialist-context-C66TEMXS.js.map} +1 -1
  78. package/dist/{specialist-logs-XJB5TCKJ.js → specialist-logs-CJKXM3SR.js} +6 -5
  79. package/dist/{specialists-5LBRHYFA.js → specialists-NXYD4Z62.js} +6 -5
  80. package/dist/{traefik-WFMQX2LY.js → traefik-5GL3Q7DJ.js} +3 -3
  81. package/dist/{tunnel-W2GZBLEV.js → tunnel-BKC7KLBX.js} +3 -3
  82. package/dist/{workspace-manager-E434Z45T.js → workspace-manager-ALBR62AS.js} +5 -5
  83. package/dist/workspace-manager-ALBR62AS.js.map +1 -0
  84. package/package.json +1 -1
  85. package/scripts/record-cost-event.js +8424 -42
  86. package/scripts/recover-costs-deep.mjs +209 -0
  87. package/scripts/recover-costs-proportional.mjs +206 -0
  88. package/scripts/recover-costs.mjs +169 -0
  89. package/scripts/work-agent-stop-hook +221 -24
  90. package/dist/chunk-2V4NF7J2.js.map +0 -1
  91. package/dist/chunk-76F6DSVS.js.map +0 -1
  92. package/dist/chunk-CWELWPWQ.js.map +0 -1
  93. package/dist/chunk-F5555J3A.js.map +0 -1
  94. package/dist/chunk-FTCPTHIJ.js.map +0 -1
  95. package/dist/chunk-HJSM6E6U.js.map +0 -1
  96. package/dist/chunk-NLQRED36.js.map +0 -1
  97. package/dist/chunk-OWHXCGVO.js.map +0 -1
  98. package/dist/chunk-PELXV435.js.map +0 -1
  99. package/dist/chunk-VHKSS7QX.js.map +0 -1
  100. package/dist/chunk-YGJ54GW2.js +0 -96
  101. package/dist/chunk-YGJ54GW2.js.map +0 -1
  102. package/dist/dashboard/public/assets/index-Ce6q21Fm.js +0 -743
  103. package/dist/dashboard/public/assets/index-NzpI0ItZ.css +0 -32
  104. package/dist/git-utils-I2UDKNZH.js +0 -131
  105. package/dist/git-utils-I2UDKNZH.js.map +0 -1
  106. /package/dist/{agents-5OPQKM5K.js.map → agents-HNMF52RM.js.map} +0 -0
  107. /package/dist/{chunk-YLPSQAM2.js.map → chunk-565HZ6VV.js.map} +0 -0
  108. /package/dist/{chunk-4YSYJ4HM.js.map → chunk-DFNVHK3N.js.map} +0 -0
  109. /package/dist/{chunk-7SN4L4PH.js.map → chunk-HOGYHJ2G.js.map} +0 -0
  110. /package/dist/{hume-WMAUBBV2.js.map → config-yaml-OVZLKFMA.js.map} +0 -0
  111. /package/dist/{feedback-writer-VRMMWWTW.js.map → feedback-writer-T43PI5S2.js.map} +0 -0
  112. /package/dist/{projects-CFX3RTDL.js.map → hume-CKJJ3OUU.js.map} +0 -0
  113. /package/dist/{remote-agents-TFSMW7GN.js.map → projects-KVM3MN3Y.js.map} +0 -0
  114. /package/dist/{review-status-TDPSOU5J.js.map → remote-agents-ULPD6C5U.js.map} +0 -0
  115. /package/dist/{remote-workspace-7FPGF2RM.js.map → remote-workspace-XX6ARE6I.js.map} +0 -0
  116. /package/dist/{specialist-logs-XJB5TCKJ.js.map → review-status-XKUKZF6J.js.map} +0 -0
  117. /package/dist/{specialists-5LBRHYFA.js.map → specialist-logs-CJKXM3SR.js.map} +0 -0
  118. /package/dist/{traefik-WFMQX2LY.js.map → specialists-NXYD4Z62.js.map} +0 -0
  119. /package/dist/{tunnel-W2GZBLEV.js.map → traefik-5GL3Q7DJ.js.map} +0 -0
  120. /package/dist/{workspace-manager-E434Z45T.js.map → tunnel-BKC7KLBX.js.map} +0 -0
@@ -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)}`);
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Proportional cost recovery: for multi-issue sessions, splits costs
4
+ * based on which issue was being discussed at each point in the conversation.
5
+ *
6
+ * Strategy: for each assistant response with usage, look at the surrounding
7
+ * context (the human message before it and assistant message itself) to determine
8
+ * which issue is being worked on at that moment.
9
+ */
10
+
11
+ import { readdirSync, readFileSync, statSync } from 'fs';
12
+ import { join, basename } from 'path';
13
+ import { homedir } from 'os';
14
+ import Database from 'better-sqlite3';
15
+
16
+ const CLAUDE_PROJECTS = join(homedir(), '.claude', 'projects');
17
+ const DB_PATH = join(homedir(), '.panopticon', 'panopticon.db');
18
+
19
+ const PRICING = [
20
+ { provider: 'anthropic', model: 'claude-opus-4.6', inputPer1k: 5e-3, outputPer1k: 0.025, cacheReadPer1k: 5e-4, cacheWrite5mPer1k: 625e-5 },
21
+ { provider: 'anthropic', model: 'claude-opus-4-6', inputPer1k: 5e-3, outputPer1k: 0.025, cacheReadPer1k: 5e-4, cacheWrite5mPer1k: 625e-5 },
22
+ { provider: 'anthropic', model: 'claude-opus-4-1', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 15e-4, cacheWrite5mPer1k: 0.01875 },
23
+ { provider: 'anthropic', model: 'claude-opus-4', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 15e-4, cacheWrite5mPer1k: 0.01875 },
24
+ { provider: 'anthropic', model: 'claude-sonnet-4.5', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5 },
25
+ { provider: 'anthropic', model: 'claude-sonnet-4-6', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5 },
26
+ { provider: 'anthropic', model: 'claude-sonnet-4', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5 },
27
+ { provider: 'anthropic', model: 'claude-haiku-4.5', inputPer1k: 1e-3, outputPer1k: 5e-3, cacheReadPer1k: 1e-4, cacheWrite5mPer1k: 125e-5 },
28
+ { provider: 'anthropic', model: 'claude-haiku-4', inputPer1k: 1e-3, outputPer1k: 5e-3, cacheReadPer1k: 1e-4, cacheWrite5mPer1k: 125e-5 },
29
+ { provider: 'anthropic', model: 'claude-haiku-3', inputPer1k: 25e-5, outputPer1k: 125e-5, cacheReadPer1k: 3e-5, cacheWrite5mPer1k: 3e-4 },
30
+ { provider: 'custom', model: 'kimi-k2.5', inputPer1k: 6e-4, outputPer1k: 2e-3, cacheReadPer1k: 6e-5, cacheWrite5mPer1k: 75e-5 },
31
+ { provider: 'custom', model: 'kimi-for-coding', inputPer1k: 6e-4, outputPer1k: 2e-3, cacheReadPer1k: 6e-5, cacheWrite5mPer1k: 75e-5 },
32
+ ];
33
+
34
+ function getPricing(model) {
35
+ return PRICING.find(p => model.startsWith(p.model)) || PRICING.find(p => model.includes(p.model)) || null;
36
+ }
37
+
38
+ function calculateCost(usage, pricing) {
39
+ let cost = 0;
40
+ let inputMul = 1, outputMul = 1;
41
+ const totalInput = usage.input + (usage.cacheRead || 0) + (usage.cacheWrite || 0);
42
+ if (pricing.model.includes('sonnet-4') && totalInput > 200000) { inputMul = 2; outputMul = 1.5; }
43
+ cost += usage.input / 1000 * pricing.inputPer1k * inputMul;
44
+ cost += usage.output / 1000 * pricing.outputPer1k * outputMul;
45
+ if (usage.cacheRead && pricing.cacheReadPer1k) cost += usage.cacheRead / 1000 * pricing.cacheReadPer1k;
46
+ if (usage.cacheWrite && pricing.cacheWrite5mPer1k) cost += usage.cacheWrite / 1000 * pricing.cacheWrite5mPer1k;
47
+ return Math.round(cost * 1e6) / 1e6;
48
+ }
49
+
50
+ const ISSUE_RE = /\b(PAN|MIN|AUR|KRUX|CLI)-(\d+)\b/gi;
51
+
52
+ function extractIssues(text) {
53
+ const counts = {};
54
+ let match;
55
+ const re = new RegExp(ISSUE_RE.source, 'gi');
56
+ while ((match = re.exec(text)) !== null) {
57
+ const id = `${match[1].toUpperCase()}-${match[2]}`;
58
+ counts[id] = (counts[id] || 0) + 1;
59
+ }
60
+ return counts;
61
+ }
62
+
63
+ function inferIssueFromPath(dirName) {
64
+ const match = dirName.match(/(pan|min|aud|krux|cli)[-](\d+)/i);
65
+ if (match) return `${match[1].toUpperCase()}-${match[2]}`;
66
+ return null;
67
+ }
68
+
69
+ // Main
70
+ const db = new Database(DB_PATH);
71
+ db.pragma('journal_mode = WAL');
72
+
73
+ const insert = db.prepare(`
74
+ INSERT OR IGNORE INTO cost_events (
75
+ ts, agent_id, issue_id, session_type, provider, model,
76
+ input, output, cache_read, cache_write, cost, request_id, source_file
77
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
78
+ `);
79
+
80
+ let totalInserted = 0;
81
+ let totalDuplicates = 0;
82
+ let totalUnattributed = 0;
83
+ const issueStats = {};
84
+
85
+ const projectDirs = readdirSync(CLAUDE_PROJECTS);
86
+
87
+ for (const dirName of projectDirs) {
88
+ const projectDir = join(CLAUDE_PROJECTS, dirName);
89
+ try { if (!statSync(projectDir).isDirectory()) continue; } catch { continue; }
90
+
91
+ const pathIssueId = inferIssueFromPath(dirName);
92
+
93
+ let transcripts;
94
+ try {
95
+ transcripts = readdirSync(projectDir)
96
+ .filter(f => f.endsWith('.jsonl'))
97
+ .map(f => join(projectDir, f))
98
+ .filter(f => { try { return statSync(f).isFile(); } catch { return false; } });
99
+ } catch { continue; }
100
+
101
+ for (const transcript of transcripts) {
102
+ let content;
103
+ try { content = readFileSync(transcript, 'utf-8'); } catch { continue; }
104
+ const lines = content.split('\n').filter(l => l.trim());
105
+
106
+ // Parse all entries
107
+ const entries = [];
108
+ for (const line of lines) {
109
+ try { entries.push(JSON.parse(line)); } catch {}
110
+ }
111
+
112
+ // Track the "current issue context" as we walk through the conversation
113
+ let currentIssue = pathIssueId || null;
114
+ let lastHumanText = '';
115
+
116
+ for (let i = 0; i < entries.length; i++) {
117
+ const entry = entries[i];
118
+
119
+ // Track human messages to build context window
120
+ if (entry.type === 'human') {
121
+ const text = JSON.stringify(entry.message || '');
122
+ lastHumanText = text;
123
+ // Update current issue if this human message mentions issues
124
+ const issues = extractIssues(text);
125
+ const sorted = Object.entries(issues).sort((a, b) => b[1] - a[1]);
126
+ if (sorted.length > 0) {
127
+ currentIssue = sorted[0][0];
128
+ }
129
+ continue;
130
+ }
131
+
132
+ if (entry.type !== 'assistant' || !entry.message?.usage) continue;
133
+
134
+ const usage = entry.message.usage;
135
+ const model = entry.message.model || 'claude-sonnet-4';
136
+ const requestId = entry.requestId;
137
+ if (!requestId) continue;
138
+
139
+ const input = usage.input_tokens || 0;
140
+ const output = usage.output_tokens || 0;
141
+ const cacheRead = usage.cache_read_input_tokens || 0;
142
+ const cacheWrite = usage.cache_creation_input_tokens || 0;
143
+ if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) continue;
144
+
145
+ // Check this assistant message for issue mentions too
146
+ const assistantText = JSON.stringify(entry.message?.content || '');
147
+ const assistantIssues = extractIssues(assistantText);
148
+ const combinedText = lastHumanText + ' ' + assistantText;
149
+ const contextIssues = extractIssues(combinedText);
150
+ const sorted = Object.entries(contextIssues).sort((a, b) => b[1] - a[1]);
151
+
152
+ // Use the most-mentioned issue in the immediate context, falling back to running context
153
+ let issueId = null;
154
+ if (sorted.length > 0) {
155
+ issueId = sorted[0][0];
156
+ currentIssue = issueId; // Update running context
157
+ } else {
158
+ issueId = currentIssue;
159
+ }
160
+
161
+ if (!issueId) {
162
+ totalUnattributed++;
163
+ continue;
164
+ }
165
+
166
+ let provider = 'anthropic';
167
+ if (model.includes('gpt')) provider = 'openai';
168
+ else if (model.includes('gemini')) provider = 'google';
169
+ else if (model.includes('kimi')) provider = 'custom';
170
+
171
+ const pricing = getPricing(model);
172
+ if (!pricing) continue;
173
+
174
+ const cost = calculateCost({ input, output, cacheRead, cacheWrite }, pricing);
175
+ const ts = entry.timestamp || new Date(statSync(transcript).mtime).toISOString();
176
+
177
+ const result = insert.run(
178
+ ts, 'recovered-proportional', issueId, 'interactive', provider, model,
179
+ input, output, cacheRead, cacheWrite, cost, requestId, basename(transcript)
180
+ );
181
+
182
+ if (result.changes > 0) {
183
+ totalInserted++;
184
+ if (!issueStats[issueId]) issueStats[issueId] = { inserted: 0, cost: 0 };
185
+ issueStats[issueId].inserted++;
186
+ issueStats[issueId].cost += cost;
187
+ } else {
188
+ totalDuplicates++;
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ db.close();
195
+
196
+ console.log(`\nProportional Cost Recovery Complete`);
197
+ console.log(` NEW events inserted: ${totalInserted}`);
198
+ console.log(` Duplicates skipped: ${totalDuplicates}`);
199
+ console.log(` Unattributable: ${totalUnattributed}`);
200
+ console.log(`\nNewly recovered costs by issue:`);
201
+ const sorted = Object.entries(issueStats).sort((a, b) => b[1].cost - a[1].cost);
202
+ for (const [id, stats] of sorted) {
203
+ console.log(` ${id.padEnd(12)} ${String(stats.inserted).padStart(5)} events $${stats.cost.toFixed(2)}`);
204
+ }
205
+ const totalCost = sorted.reduce((sum, [, s]) => sum + s.cost, 0);
206
+ console.log(`\n TOTAL NEWLY RECOVERED: $${totalCost.toFixed(2)}`);
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Recover cost events from Claude Code transcript files.
4
+ *
5
+ * Scans ~/.claude/projects/ for transcript JSONL files,
6
+ * infers issue ID from directory path, extracts usage data,
7
+ * and inserts into the panopticon SQLite database.
8
+ *
9
+ * Deduplication is handled by the UNIQUE index on request_id.
10
+ */
11
+
12
+ import { readdirSync, readFileSync, existsSync, statSync } from 'fs';
13
+ import { join, basename } from 'path';
14
+ import { homedir } from 'os';
15
+ import Database from 'better-sqlite3';
16
+
17
+ const CLAUDE_PROJECTS = join(homedir(), '.claude', 'projects');
18
+ const DB_PATH = join(homedir(), '.panopticon', 'panopticon.db');
19
+
20
+ // Pricing table (same as record-cost-event.js)
21
+ const PRICING = [
22
+ { provider: 'anthropic', model: 'claude-opus-4.6', inputPer1k: 5e-3, outputPer1k: 0.025, cacheReadPer1k: 5e-4, cacheWrite5mPer1k: 625e-5, cacheWrite1hPer1k: 0.01 },
23
+ { provider: 'anthropic', model: 'claude-opus-4-6', inputPer1k: 5e-3, outputPer1k: 0.025, cacheReadPer1k: 5e-4, cacheWrite5mPer1k: 625e-5, cacheWrite1hPer1k: 0.01 },
24
+ { provider: 'anthropic', model: 'claude-opus-4-1', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 15e-4, cacheWrite5mPer1k: 0.01875, cacheWrite1hPer1k: 0.03 },
25
+ { provider: 'anthropic', model: 'claude-opus-4', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 15e-4, cacheWrite5mPer1k: 0.01875, cacheWrite1hPer1k: 0.03 },
26
+ { provider: 'anthropic', model: 'claude-sonnet-4.5', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5, cacheWrite1hPer1k: 6e-3 },
27
+ { provider: 'anthropic', model: 'claude-sonnet-4-6', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5, cacheWrite1hPer1k: 6e-3 },
28
+ { provider: 'anthropic', model: 'claude-sonnet-4', inputPer1k: 3e-3, outputPer1k: 0.015, cacheReadPer1k: 3e-4, cacheWrite5mPer1k: 375e-5, cacheWrite1hPer1k: 6e-3 },
29
+ { provider: 'anthropic', model: 'claude-haiku-4.5', inputPer1k: 1e-3, outputPer1k: 5e-3, cacheReadPer1k: 1e-4, cacheWrite5mPer1k: 125e-5, cacheWrite1hPer1k: 2e-3 },
30
+ { provider: 'anthropic', model: 'claude-haiku-4', inputPer1k: 1e-3, outputPer1k: 5e-3, cacheReadPer1k: 1e-4, cacheWrite5mPer1k: 125e-5, cacheWrite1hPer1k: 2e-3 },
31
+ { provider: 'anthropic', model: 'claude-haiku-3', inputPer1k: 25e-5, outputPer1k: 125e-5, cacheReadPer1k: 3e-5, cacheWrite5mPer1k: 3e-4, cacheWrite1hPer1k: 5e-4 },
32
+ { provider: 'custom', model: 'kimi-k2.5', inputPer1k: 6e-4, outputPer1k: 2e-3, cacheReadPer1k: 6e-5, cacheWrite5mPer1k: 75e-5 },
33
+ { provider: 'custom', model: 'kimi-for-coding', inputPer1k: 6e-4, outputPer1k: 2e-3, cacheReadPer1k: 6e-5, cacheWrite5mPer1k: 75e-5 },
34
+ ];
35
+
36
+ function getPricing(model) {
37
+ return PRICING.find(p => model.startsWith(p.model)) || PRICING.find(p => model.includes(p.model)) || null;
38
+ }
39
+
40
+ function calculateCost(usage, pricing) {
41
+ let cost = 0;
42
+ let inputMul = 1, outputMul = 1;
43
+ const totalInput = usage.input + (usage.cacheRead || 0) + (usage.cacheWrite || 0);
44
+ if ((pricing.model.includes('sonnet-4')) && totalInput > 200000) {
45
+ inputMul = 2; outputMul = 1.5;
46
+ }
47
+ cost += usage.input / 1000 * pricing.inputPer1k * inputMul;
48
+ cost += usage.output / 1000 * pricing.outputPer1k * outputMul;
49
+ if (usage.cacheRead && pricing.cacheReadPer1k) cost += usage.cacheRead / 1000 * pricing.cacheReadPer1k;
50
+ if (usage.cacheWrite && pricing.cacheWrite5mPer1k) cost += usage.cacheWrite / 1000 * pricing.cacheWrite5mPer1k;
51
+ return Math.round(cost * 1e6) / 1e6;
52
+ }
53
+
54
+ function inferIssueId(dirName) {
55
+ const match = dirName.match(/(pan|min|aud|krux|cli)[-](\d+)/i);
56
+ if (match) return `${match[1].toUpperCase()}-${match[2]}`;
57
+ return null;
58
+ }
59
+
60
+ function findTranscripts(projectDir) {
61
+ const transcripts = [];
62
+ try {
63
+ const entries = readdirSync(projectDir, { recursive: true });
64
+ for (const entry of entries) {
65
+ if (entry.endsWith('.jsonl')) {
66
+ const full = join(projectDir, entry);
67
+ try { if (statSync(full).isFile()) transcripts.push(full); } catch {}
68
+ }
69
+ }
70
+ } catch {}
71
+ return transcripts;
72
+ }
73
+
74
+ // Main
75
+ const db = new Database(DB_PATH);
76
+ db.pragma('journal_mode = WAL');
77
+
78
+ const insert = db.prepare(`
79
+ INSERT OR IGNORE INTO cost_events (
80
+ ts, agent_id, issue_id, session_type, provider, model,
81
+ input, output, cache_read, cache_write, cost, request_id, source_file
82
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
83
+ `);
84
+
85
+ let totalInserted = 0;
86
+ let totalDuplicates = 0;
87
+ let totalErrors = 0;
88
+ const issueStats = {};
89
+
90
+ const projectDirs = readdirSync(CLAUDE_PROJECTS);
91
+ for (const dirName of projectDirs) {
92
+ const issueId = inferIssueId(dirName);
93
+ if (!issueId) continue;
94
+
95
+ const projectDir = join(CLAUDE_PROJECTS, dirName);
96
+ if (!statSync(projectDir).isDirectory()) continue;
97
+
98
+ const transcripts = findTranscripts(projectDir);
99
+ if (transcripts.length === 0) continue;
100
+
101
+ for (const transcript of transcripts) {
102
+ let content;
103
+ try { content = readFileSync(transcript, 'utf-8'); } catch { continue; }
104
+
105
+ const lines = content.split('\n');
106
+ for (const line of lines) {
107
+ if (!line.trim()) continue;
108
+ try {
109
+ const entry = JSON.parse(line);
110
+ if (entry.type !== 'assistant' || !entry.message?.usage) continue;
111
+
112
+ const usage = entry.message.usage;
113
+ const model = entry.message.model || 'claude-sonnet-4';
114
+ const requestId = entry.requestId;
115
+ if (!requestId) continue;
116
+
117
+ const input = usage.input_tokens || 0;
118
+ const output = usage.output_tokens || 0;
119
+ const cacheRead = usage.cache_read_input_tokens || 0;
120
+ const cacheWrite = usage.cache_creation_input_tokens || 0;
121
+ if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) continue;
122
+
123
+ let provider = 'anthropic';
124
+ if (model.includes('gpt')) provider = 'openai';
125
+ else if (model.includes('gemini')) provider = 'google';
126
+ else if (model.includes('kimi')) provider = 'custom';
127
+
128
+ const pricing = getPricing(model);
129
+ if (!pricing) continue;
130
+
131
+ const cost = calculateCost({ input, output, cacheRead, cacheWrite }, pricing);
132
+
133
+ // Use timestamp from the entry if available, otherwise from transcript modification time
134
+ const ts = entry.timestamp || new Date(statSync(transcript).mtime).toISOString();
135
+
136
+ const result = insert.run(
137
+ ts, 'recovered', issueId, 'interactive', provider, model,
138
+ input, output, cacheRead, cacheWrite, cost, requestId, basename(transcript)
139
+ );
140
+
141
+ if (result.changes > 0) {
142
+ totalInserted++;
143
+ if (!issueStats[issueId]) issueStats[issueId] = { inserted: 0, cost: 0 };
144
+ issueStats[issueId].inserted++;
145
+ issueStats[issueId].cost += cost;
146
+ } else {
147
+ totalDuplicates++;
148
+ }
149
+ } catch {
150
+ totalErrors++;
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ db.close();
157
+
158
+ // Report
159
+ console.log(`\nCost Recovery Complete`);
160
+ console.log(` Inserted: ${totalInserted} new events`);
161
+ console.log(` Duplicates skipped: ${totalDuplicates}`);
162
+ console.log(` Errors: ${totalErrors}`);
163
+ console.log(`\nRecovered costs by issue:`);
164
+ const sorted = Object.entries(issueStats).sort((a, b) => b[1].cost - a[1].cost);
165
+ for (const [id, stats] of sorted) {
166
+ console.log(` ${id.padEnd(12)} ${stats.inserted} events $${stats.cost.toFixed(2)}`);
167
+ }
168
+ const totalCost = sorted.reduce((sum, [, s]) => sum + s.cost, 0);
169
+ console.log(`\n TOTAL RECOVERED: $${totalCost.toFixed(2)}`);