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.
- package/dist/{agents-DMPT32H7.js → agents-HNMF52RM.js} +2 -2
- package/dist/{chunk-2V2DQ3IX.js → chunk-ID4OYXVH.js} +16 -16
- package/dist/{chunk-D67AQTHF.js → chunk-KY2E2Q3T.js} +5 -1
- package/dist/{chunk-D67AQTHF.js.map → chunk-KY2E2Q3T.js.map} +1 -1
- package/dist/cli/index.js +25 -16
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/public/assets/{index-CgJjqjAV.js → index-DA6pnizT.js} +91 -91
- package/dist/dashboard/public/assets/{index-BJKEp64j.css → index-DSvt5pPn.css} +1 -1
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/server.js +91 -54
- package/dist/{specialist-context-53AWO6AE.js → specialist-context-C66TEMXS.js} +2 -2
- package/dist/{specialist-logs-QREUJ4HN.js → specialist-logs-CJKXM3SR.js} +2 -2
- package/dist/{specialists-2DBBXRCK.js → specialists-NXYD4Z62.js} +2 -2
- package/package.json +1 -1
- package/scripts/recover-costs-deep.mjs +209 -0
- package/scripts/recover-costs-proportional.mjs +206 -0
- package/scripts/recover-costs.mjs +169 -0
- /package/dist/{agents-DMPT32H7.js.map → agents-HNMF52RM.js.map} +0 -0
- /package/dist/{chunk-2V2DQ3IX.js.map → chunk-ID4OYXVH.js.map} +0 -0
- /package/dist/{specialist-context-53AWO6AE.js.map → specialist-context-C66TEMXS.js.map} +0 -0
- /package/dist/{specialist-logs-QREUJ4HN.js.map → specialist-logs-CJKXM3SR.js.map} +0 -0
- /package/dist/{specialists-2DBBXRCK.js.map → specialists-NXYD4Z62.js.map} +0 -0
package/dist/dashboard/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
184588
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
184628
|
-
|
|
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-
|
|
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-
|
|
258
|
+
//# sourceMappingURL=specialist-context-C66TEMXS.js.map
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
isRunLogActive,
|
|
17
17
|
listRunLogs,
|
|
18
18
|
parseLogMetadata
|
|
19
|
-
} from "./chunk-
|
|
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-
|
|
46
|
+
//# sourceMappingURL=specialist-logs-CJKXM3SR.js.map
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
wakeSpecialist,
|
|
56
56
|
wakeSpecialistOrQueue,
|
|
57
57
|
wakeSpecialistWithTask
|
|
58
|
-
} from "./chunk-
|
|
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-
|
|
124
|
+
//# sourceMappingURL=specialists-NXYD4Z62.js.map
|
package/package.json
CHANGED
|
@@ -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)}`);
|