@statforge/claudestat 1.0.1

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 (80) hide show
  1. package/README.md +437 -0
  2. package/dashboard/dist/assets/AnalyticsView-BApcOGsD.js +8 -0
  3. package/dashboard/dist/assets/HistoryView-B331k5oL.js +1 -0
  4. package/dashboard/dist/assets/ProjectsView-DUleaXsP.js +6 -0
  5. package/dashboard/dist/assets/SystemView-BGe__vl1.js +1 -0
  6. package/dashboard/dist/assets/TopView-CXggyydU.js +1 -0
  7. package/dashboard/dist/assets/index-CB01c5lb.js +84 -0
  8. package/dashboard/dist/assets/vendor-lucide-Cym0q5l_.js +344 -0
  9. package/dashboard/dist/assets/vendor-react-B_Jzs0gY.js +24 -0
  10. package/dashboard/dist/index.html +21 -0
  11. package/dist/cache/projects-cache.d.ts +9 -0
  12. package/dist/cache/projects-cache.js +51 -0
  13. package/dist/claude-auth.d.ts +38 -0
  14. package/dist/claude-auth.js +133 -0
  15. package/dist/claude-stats.d.ts +32 -0
  16. package/dist/claude-stats.js +98 -0
  17. package/dist/config.d.ts +43 -0
  18. package/dist/config.js +110 -0
  19. package/dist/daemon.d.ts +15 -0
  20. package/dist/daemon.js +247 -0
  21. package/dist/db.d.ts +134 -0
  22. package/dist/db.js +546 -0
  23. package/dist/doctor.d.ts +1 -0
  24. package/dist/doctor.js +191 -0
  25. package/dist/enricher.d.ts +34 -0
  26. package/dist/enricher.js +394 -0
  27. package/dist/export.d.ts +8 -0
  28. package/dist/export.js +82 -0
  29. package/dist/git.d.ts +22 -0
  30. package/dist/git.js +57 -0
  31. package/dist/github.d.ts +27 -0
  32. package/dist/github.js +62 -0
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.js +319 -0
  35. package/dist/install.d.ts +14 -0
  36. package/dist/install.js +202 -0
  37. package/dist/intelligence.d.ts +45 -0
  38. package/dist/intelligence.js +105 -0
  39. package/dist/meta-stats.d.ts +28 -0
  40. package/dist/meta-stats.js +137 -0
  41. package/dist/middleware/rate-limiter.d.ts +2 -0
  42. package/dist/middleware/rate-limiter.js +30 -0
  43. package/dist/notifier.d.ts +1 -0
  44. package/dist/notifier.js +22 -0
  45. package/dist/paths.d.ts +79 -0
  46. package/dist/paths.js +134 -0
  47. package/dist/pattern-analyzer.d.ts +35 -0
  48. package/dist/pattern-analyzer.js +123 -0
  49. package/dist/project-scanner.d.ts +71 -0
  50. package/dist/project-scanner.js +619 -0
  51. package/dist/quota-tracker.d.ts +45 -0
  52. package/dist/quota-tracker.js +320 -0
  53. package/dist/render.d.ts +55 -0
  54. package/dist/render.js +229 -0
  55. package/dist/routes/events.d.ts +18 -0
  56. package/dist/routes/events.js +272 -0
  57. package/dist/routes/history.d.ts +1 -0
  58. package/dist/routes/history.js +65 -0
  59. package/dist/routes/misc.d.ts +1 -0
  60. package/dist/routes/misc.js +280 -0
  61. package/dist/routes/projects.d.ts +15 -0
  62. package/dist/routes/projects.js +153 -0
  63. package/dist/routes/reports.d.ts +11 -0
  64. package/dist/routes/reports.js +205 -0
  65. package/dist/routes/stream.d.ts +8 -0
  66. package/dist/routes/stream.js +70 -0
  67. package/dist/routes/top.d.ts +1 -0
  68. package/dist/routes/top.js +30 -0
  69. package/dist/session-state.d.ts +35 -0
  70. package/dist/session-state.js +50 -0
  71. package/dist/summarizer.d.ts +18 -0
  72. package/dist/summarizer.js +137 -0
  73. package/dist/watch.d.ts +8 -0
  74. package/dist/watch.js +157 -0
  75. package/dist/watchdog.d.ts +11 -0
  76. package/dist/watchdog.js +75 -0
  77. package/dist/weekly.d.ts +13 -0
  78. package/dist/weekly.js +39 -0
  79. package/hooks/event.js +80 -0
  80. package/package.json +78 -0
package/dist/doctor.js ADDED
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runDoctor = runDoctor;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const paths_1 = require("./paths");
11
+ async function runDoctor() {
12
+ const checks = [];
13
+ const G = '\x1b[32m✓\x1b[0m';
14
+ const R = '\x1b[31m✗\x1b[0m';
15
+ const W = '\x1b[33m⚠\x1b[0m';
16
+ // 1. Node.js version
17
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
18
+ checks.push({
19
+ label: `Node.js version (${process.versions.node})`,
20
+ ok: nodeMajor >= 18,
21
+ note: nodeMajor >= 22 ? 'node:sqlite supported ✓'
22
+ : nodeMajor >= 18 ? 'Works — Node 22+ recommended for native node:sqlite'
23
+ : undefined,
24
+ fix: nodeMajor < 18 ? 'Install Node.js 18 or later: https://nodejs.org' : undefined,
25
+ });
26
+ // 2. Claude Code installed
27
+ const claudeOk = (() => { try {
28
+ (0, child_process_1.execSync)('claude --version', { stdio: 'pipe' });
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ } })();
34
+ checks.push({
35
+ label: 'Claude Code installed',
36
+ ok: claudeOk,
37
+ fix: claudeOk ? undefined : 'npm install -g @anthropic-ai/claude-code',
38
+ });
39
+ // 3. Hooks wired into Claude Code settings
40
+ const settingsPath = path_1.default.join((0, paths_1.getClaudeDir)(), 'settings.json');
41
+ let hooksOk = false;
42
+ try {
43
+ const settings = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf8'));
44
+ const required = ['PreToolUse', 'PostToolUse', 'SessionStart', 'Stop'];
45
+ hooksOk = required.every(type => settings.hooks?.[type]?.some((entry) => entry.hooks?.some((h) => typeof h.command === 'string' && h.command.includes('claudestat'))));
46
+ }
47
+ catch { }
48
+ checks.push({
49
+ label: 'Hooks installed in Claude Code',
50
+ ok: hooksOk,
51
+ note: hooksOk ? undefined : `Expected hooks in ${settingsPath}`,
52
+ fix: hooksOk ? undefined : 'claudestat install',
53
+ });
54
+ // 4. Data directory
55
+ const dataDir = (0, paths_1.getClaudestatDir)();
56
+ const dataDirOk = fs_1.default.existsSync(dataDir);
57
+ checks.push({
58
+ label: '~/.claudestat/ data directory exists',
59
+ ok: dataDirOk,
60
+ fix: dataDirOk ? undefined : 'Run "claudestat start" once to create it automatically',
61
+ });
62
+ // 5. Hook script deployed
63
+ const hookFile = path_1.default.join(dataDir, 'hooks', 'event.js');
64
+ const hookOk = fs_1.default.existsSync(hookFile);
65
+ checks.push({
66
+ label: 'Hook script deployed (~/.claudestat/hooks/event.js)',
67
+ ok: hookOk,
68
+ fix: hookOk ? undefined : 'claudestat install',
69
+ });
70
+ // 6. Daemon reachable
71
+ const daemonOk = await (async () => { try {
72
+ const res = await fetch('http://localhost:7337/health');
73
+ return res.ok;
74
+ }
75
+ catch {
76
+ return false;
77
+ } })();
78
+ checks.push({
79
+ label: 'Daemon running (localhost:7337)',
80
+ ok: daemonOk,
81
+ fix: daemonOk ? undefined : 'claudestat start',
82
+ });
83
+ // 7. Global CLI symlink valid (no stale link from old installs)
84
+ let symlinkOk = false;
85
+ let symlinkNote;
86
+ let activeBinary = '';
87
+ try {
88
+ activeBinary = (0, child_process_1.execSync)((0, paths_1.whichCmd)('claudestat'), { stdio: 'pipe' }).toString().trim();
89
+ const realPath = fs_1.default.realpathSync(activeBinary);
90
+ symlinkOk = fs_1.default.existsSync(realPath);
91
+ if (!symlinkOk)
92
+ symlinkNote = `Symlink points to missing file: ${realPath}`;
93
+ }
94
+ catch {
95
+ symlinkNote = 'claudestat not found in PATH';
96
+ }
97
+ checks.push({
98
+ label: 'Global CLI symlink valid',
99
+ ok: symlinkOk,
100
+ note: symlinkNote,
101
+ fix: symlinkOk ? undefined : 'npm install -g @deibygs/claudestat',
102
+ });
103
+ // 8. No duplicate claudestat binaries in PATH
104
+ let duplicatesOk = true;
105
+ let duplicatesNote;
106
+ try {
107
+ const allBinaries = (0, child_process_1.execSync)((0, paths_1.whichAllCmd)('claudestat'), { stdio: 'pipe' })
108
+ .toString().trim().split('\n').filter(Boolean);
109
+ if (allBinaries.length > 1) {
110
+ duplicatesOk = false;
111
+ duplicatesNote = `Found ${allBinaries.length} binaries:\n${allBinaries.map(p => ` ${p}`).join('\n')}`;
112
+ }
113
+ }
114
+ catch { }
115
+ checks.push({
116
+ label: 'No duplicate claudestat binaries in PATH',
117
+ ok: duplicatesOk,
118
+ note: duplicatesNote,
119
+ fix: duplicatesOk ? undefined :
120
+ `npm uninstall -g @deibygs/claudestat && npm install -g @deibygs/claudestat\n Then restart your terminal or run: ${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'}`,
121
+ });
122
+ // 9. Active binary version matches installed package
123
+ let versionOk = true;
124
+ let versionNote;
125
+ const installedVersion = (() => {
126
+ try {
127
+ return JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'package.json'), 'utf8')).version;
128
+ }
129
+ catch {
130
+ return 'unknown';
131
+ }
132
+ })();
133
+ if (activeBinary) {
134
+ try {
135
+ const runningVersion = (0, child_process_1.execSync)(`${activeBinary} --version`, { stdio: 'pipe' })
136
+ .toString().trim().replace(/^v?/, '');
137
+ versionOk = runningVersion === installedVersion;
138
+ if (!versionOk) {
139
+ versionNote = `Active binary reports v${runningVersion}, installed package is v${installedVersion}`;
140
+ }
141
+ }
142
+ catch { }
143
+ }
144
+ checks.push({
145
+ label: `Version match (installed: v${installedVersion})`,
146
+ ok: versionOk,
147
+ note: versionNote,
148
+ fix: versionOk ? undefined :
149
+ `${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'} (or restart terminal)\n If persists: npm uninstall -g @deibygs/claudestat && npm install -g @deibygs/claudestat`,
150
+ });
151
+ // 10. NVM prefix sanity (only when NVM is active)
152
+ if ((process.env.NVM_DIR || process.env.NVM_HOME) && activeBinary) {
153
+ let nvmOk = true;
154
+ let nvmNote;
155
+ try {
156
+ const npmPrefix = (0, child_process_1.execSync)('npm prefix -g', { stdio: 'pipe' }).toString().trim();
157
+ if (!activeBinary.startsWith(npmPrefix)) {
158
+ nvmOk = false;
159
+ nvmNote = `Binary at ${activeBinary}\n Expected under: ${npmPrefix}/bin/`;
160
+ }
161
+ }
162
+ catch { }
163
+ checks.push({
164
+ label: 'NVM prefix matches active binary',
165
+ ok: nvmOk,
166
+ note: nvmNote,
167
+ fix: nvmOk ? undefined :
168
+ `nvm use default && npm install -g @deibygs/claudestat\n Then restart terminal`,
169
+ });
170
+ }
171
+ // ── Print results ───────────────────────────────────────────
172
+ console.log('\n🩺 claudestat doctor\n' + '─'.repeat(46));
173
+ for (const c of checks) {
174
+ console.log(` ${c.ok ? G : R} ${c.label}`);
175
+ if (!c.ok) {
176
+ if (c.note)
177
+ console.log(` ${W} ${c.note}`);
178
+ if (c.fix)
179
+ console.log(` \x1b[36mfix:\x1b[0m ${c.fix}`);
180
+ }
181
+ }
182
+ console.log('─'.repeat(46));
183
+ const failed = checks.filter(c => !c.ok).length;
184
+ if (failed === 0) {
185
+ console.log(' \x1b[32mAll checks passed — claudestat is healthy!\x1b[0m\n');
186
+ }
187
+ else {
188
+ console.log(` \x1b[31m${failed} check(s) failed — see fixes above\x1b[0m\n`);
189
+ process.exit(1);
190
+ }
191
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * enricher.ts — Enriquecedor de coste desde JSONL de Claude Code
3
+ *
4
+ * Claude Code escribe los tokens de cada respuesta en:
5
+ * ~/.claude/projects/{project-hash}/{session-id}.jsonl
6
+ *
7
+ * Cada línea de tipo "assistant" contiene:
8
+ * message.usage.input_tokens
9
+ * message.usage.output_tokens
10
+ * message.usage.cache_read_input_tokens
11
+ * message.usage.cache_creation_input_tokens
12
+ * message.model
13
+ *
14
+ * El enricher observa cambios en esos archivos (con chokidar),
15
+ * calcula el coste acumulado por sesión y llama al callback
16
+ * para que el daemon actualice la DB y haga broadcast via SSE.
17
+ */
18
+ import type { CostUpdate } from './db';
19
+ export declare function getContextWindow(model: string): number;
20
+ import type { BlockCostEntry } from './db';
21
+ export declare function getAllBlockCostsForSession(sessionId: string): Promise<BlockCostEntry[]>;
22
+ export interface SessionPrompt {
23
+ index: number;
24
+ ts: number;
25
+ text: string;
26
+ }
27
+ export declare function getSessionPrompts(sessionId: string): Promise<SessionPrompt[]>;
28
+ export type CostUpdateCallback = (sessionId: string, cost: CostUpdate) => void;
29
+ export type CompactDetectedCallback = (sessionId: string) => void;
30
+ export type SessionEndCallback = (sessionId: string) => void;
31
+ export declare function startEnricher(onUpdate: CostUpdateCallback, onCompact?: CompactDetectedCallback, onSessionEnd?: SessionEndCallback): void;
32
+ export declare function stopEnricher(): void;
33
+ export declare function cleanupSession(sessionId: string): void;
34
+ export declare function processLatestForSession(sessionId: string, onUpdate: CostUpdateCallback): Promise<void>;
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ /**
3
+ * enricher.ts — Enriquecedor de coste desde JSONL de Claude Code
4
+ *
5
+ * Claude Code escribe los tokens de cada respuesta en:
6
+ * ~/.claude/projects/{project-hash}/{session-id}.jsonl
7
+ *
8
+ * Cada línea de tipo "assistant" contiene:
9
+ * message.usage.input_tokens
10
+ * message.usage.output_tokens
11
+ * message.usage.cache_read_input_tokens
12
+ * message.usage.cache_creation_input_tokens
13
+ * message.model
14
+ *
15
+ * El enricher observa cambios en esos archivos (con chokidar),
16
+ * calcula el coste acumulado por sesión y llama al callback
17
+ * para que el daemon actualice la DB y haga broadcast via SSE.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.getContextWindow = getContextWindow;
24
+ exports.getAllBlockCostsForSession = getAllBlockCostsForSession;
25
+ exports.getSessionPrompts = getSessionPrompts;
26
+ exports.startEnricher = startEnricher;
27
+ exports.stopEnricher = stopEnricher;
28
+ exports.cleanupSession = cleanupSession;
29
+ exports.processLatestForSession = processLatestForSession;
30
+ const promises_1 = __importDefault(require("fs/promises"));
31
+ const fs_1 = __importDefault(require("fs"));
32
+ const path_1 = __importDefault(require("path"));
33
+ const chokidar_1 = __importDefault(require("chokidar"));
34
+ const paths_1 = require("./paths");
35
+ const PRICING = {
36
+ 'claude-opus-4-6': { input: 15, output: 75, cacheRead: 1.50, cacheCreate: 18.75 },
37
+ 'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreate: 3.75 },
38
+ 'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
39
+ 'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
40
+ };
41
+ const DEFAULT_PRICING = PRICING['claude-sonnet-4-6'];
42
+ // ─── Context window dinámico ──────────────────────────────────────────────────
43
+ const KNOWN_CONTEXT_WINDOWS = {
44
+ 'claude-opus-4-6': 200000,
45
+ 'claude-sonnet-4-6': 200000,
46
+ 'claude-haiku-4-5': 200000,
47
+ };
48
+ function getContextWindow(model) {
49
+ return KNOWN_CONTEXT_WINDOWS[model] ?? 200000;
50
+ }
51
+ // ─── Calculo de coste ─────────────────────────────────────────────────────────
52
+ function calcCost(model, usage) {
53
+ const price = PRICING[model] ?? DEFAULT_PRICING;
54
+ const M = 1000000;
55
+ return ((usage.input_tokens * price.input) / M +
56
+ (usage.output_tokens * price.output) / M +
57
+ (usage.cache_read_input_tokens * price.cacheRead) / M +
58
+ (usage.cache_creation_input_tokens * price.cacheCreate) / M);
59
+ }
60
+ const fileOffsets = new Map();
61
+ const FILE_OFFSET_TTL = 30 * 60000; // 30 minutos
62
+ function cleanupStaleOffsets() {
63
+ const now = Date.now();
64
+ for (const [key, entry] of fileOffsets) {
65
+ if (now - entry.lastAccess > FILE_OFFSET_TTL)
66
+ fileOffsets.delete(key);
67
+ }
68
+ }
69
+ async function processJSONL(filePath) {
70
+ let fileContent;
71
+ try {
72
+ fileContent = await promises_1.default.readFile(filePath, 'utf8');
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ const currentSize = Buffer.byteLength(fileContent, 'utf8');
78
+ const knownEntry = fileOffsets.get(filePath);
79
+ const knownOffset = knownEntry?.offset ?? 0;
80
+ if (currentSize < knownOffset)
81
+ fileOffsets.set(filePath, { offset: 0, lastAccess: Date.now() });
82
+ const totals = {
83
+ input_tokens: 0, output_tokens: 0,
84
+ cache_read: 0, cache_creation: 0, cost_usd: 0,
85
+ context_used: 0, context_window: 200000
86
+ };
87
+ let lastInputUsd = 0;
88
+ let lastOutputUsd = 0;
89
+ let lastInputTokens = 0;
90
+ let lastOutputTokens = 0;
91
+ let lastModel = undefined;
92
+ let firstTs = undefined;
93
+ for (const raw of fileContent.split('\n')) {
94
+ const line = raw.trim();
95
+ if (!line)
96
+ continue;
97
+ try {
98
+ const obj = JSON.parse(line);
99
+ if (obj.type !== 'assistant')
100
+ continue;
101
+ const msg = obj.message;
102
+ const usage = msg?.usage;
103
+ const model = msg?.model ?? undefined;
104
+ if (!usage)
105
+ continue;
106
+ if (firstTs === undefined && obj.timestamp) {
107
+ try {
108
+ firstTs = new Date(obj.timestamp).getTime();
109
+ }
110
+ catch { }
111
+ }
112
+ totals.input_tokens += usage.input_tokens ?? 0;
113
+ totals.output_tokens += usage.output_tokens ?? 0;
114
+ totals.cache_read += usage.cache_read_input_tokens ?? 0;
115
+ totals.cache_creation += usage.cache_creation_input_tokens ?? 0;
116
+ const resolvedModel = model ?? 'claude-sonnet-4-6';
117
+ totals.cost_usd += calcCost(resolvedModel, usage);
118
+ totals.context_used = (usage.input_tokens ?? 0)
119
+ + (usage.cache_read_input_tokens ?? 0)
120
+ + (usage.cache_creation_input_tokens ?? 0);
121
+ totals.context_window = getContextWindow(resolvedModel);
122
+ const price = PRICING[resolvedModel] ?? DEFAULT_PRICING;
123
+ const M = 1000000;
124
+ lastInputUsd = ((usage.input_tokens ?? 0) * price.input +
125
+ (usage.cache_read_input_tokens ?? 0) * price.cacheRead +
126
+ (usage.cache_creation_input_tokens ?? 0) * price.cacheCreate) / M;
127
+ lastOutputUsd = ((usage.output_tokens ?? 0) * price.output) / M;
128
+ lastInputTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
129
+ lastOutputTokens = usage.output_tokens ?? 0;
130
+ lastModel = model ?? lastModel;
131
+ }
132
+ catch { }
133
+ }
134
+ if (lastInputUsd + lastOutputUsd > 0) {
135
+ totals.lastEntry = {
136
+ inputUsd: lastInputUsd,
137
+ outputUsd: lastOutputUsd,
138
+ totalUsd: lastInputUsd + lastOutputUsd,
139
+ inputTokens: lastInputTokens,
140
+ outputTokens: lastOutputTokens,
141
+ };
142
+ }
143
+ totals.lastModel = lastModel;
144
+ totals.firstTs = firstTs;
145
+ fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now() });
146
+ return totals;
147
+ }
148
+ const blockCostCache = new Map();
149
+ const BLOCK_COST_TTL = 5 * 60000;
150
+ async function getAllBlockCostsForSession(sessionId) {
151
+ const cached = blockCostCache.get(sessionId);
152
+ if (cached && Date.now() - cached.ts < BLOCK_COST_TTL)
153
+ return cached.data;
154
+ try {
155
+ if (!fs_1.default.existsSync(PROJECTS_DIR))
156
+ return [];
157
+ const dirs = await promises_1.default.readdir(PROJECTS_DIR);
158
+ for (const dir of dirs) {
159
+ const dirPath = path_1.default.join(PROJECTS_DIR, dir);
160
+ try {
161
+ const stat = await promises_1.default.stat(dirPath);
162
+ if (!stat.isDirectory())
163
+ continue;
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
169
+ try {
170
+ await promises_1.default.access(filePath);
171
+ }
172
+ catch {
173
+ continue;
174
+ }
175
+ const result = [];
176
+ let current = null;
177
+ const content = await promises_1.default.readFile(filePath, 'utf8');
178
+ for (const raw of content.split('\n')) {
179
+ const line = raw.trim();
180
+ if (!line)
181
+ continue;
182
+ try {
183
+ const obj = JSON.parse(line);
184
+ if (obj.type === 'human' || obj.type === 'user') {
185
+ const msgContent = obj.message?.content;
186
+ if (Array.isArray(msgContent) && msgContent[0]?.type === 'tool_result')
187
+ continue;
188
+ const text = typeof msgContent === 'string' ? msgContent
189
+ : Array.isArray(msgContent)
190
+ ? (msgContent.find((c) => c?.type === 'text')?.text ?? '')
191
+ : '';
192
+ if (text.includes('<system-reminder>') || text.includes('<command-name>'))
193
+ continue;
194
+ current = { inputUsd: 0, outputUsd: 0, totalUsd: 0, inputTokens: 0, outputTokens: 0 };
195
+ result.push(current);
196
+ }
197
+ if (obj.type === 'assistant' && current) {
198
+ const usage = obj.message?.usage;
199
+ const model = obj.message?.model ?? 'claude-sonnet-4-6';
200
+ if (!usage)
201
+ continue;
202
+ const price = PRICING[model] ?? DEFAULT_PRICING;
203
+ const M = 1000000;
204
+ const inUsd = ((usage.input_tokens ?? 0) * price.input +
205
+ (usage.cache_read_input_tokens ?? 0) * price.cacheRead +
206
+ (usage.cache_creation_input_tokens ?? 0) * price.cacheCreate) / M;
207
+ const outUsd = ((usage.output_tokens ?? 0) * price.output) / M;
208
+ const inTok = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
209
+ const outTok = usage.output_tokens ?? 0;
210
+ current.inputUsd += inUsd;
211
+ current.outputUsd += outUsd;
212
+ current.totalUsd += inUsd + outUsd;
213
+ current.inputTokens += inTok;
214
+ current.outputTokens += outTok;
215
+ }
216
+ }
217
+ catch { }
218
+ }
219
+ const filtered = result.filter(b => b.totalUsd > 0);
220
+ blockCostCache.set(sessionId, { data: filtered, ts: Date.now() });
221
+ return filtered;
222
+ }
223
+ }
224
+ catch { }
225
+ return [];
226
+ }
227
+ async function getSessionPrompts(sessionId) {
228
+ try {
229
+ if (!fs_1.default.existsSync(PROJECTS_DIR))
230
+ return [];
231
+ const dirs = await promises_1.default.readdir(PROJECTS_DIR);
232
+ for (const dir of dirs) {
233
+ const dirPath = path_1.default.join(PROJECTS_DIR, dir);
234
+ try {
235
+ const stat = await promises_1.default.stat(dirPath);
236
+ if (!stat.isDirectory())
237
+ continue;
238
+ }
239
+ catch {
240
+ continue;
241
+ }
242
+ const candidates = [
243
+ path_1.default.join(dirPath, `${sessionId}.jsonl`),
244
+ ];
245
+ for (const file of candidates) {
246
+ try {
247
+ await promises_1.default.access(file);
248
+ }
249
+ catch {
250
+ continue;
251
+ }
252
+ const results = [];
253
+ const content = await promises_1.default.readFile(file, 'utf8');
254
+ let index = 0;
255
+ for (const raw of content.split('\n')) {
256
+ const line = raw.trim();
257
+ if (!line)
258
+ continue;
259
+ try {
260
+ const obj = JSON.parse(line);
261
+ if (obj.type !== 'human' && obj.type !== 'user')
262
+ continue;
263
+ const ts = obj.timestamp ? new Date(obj.timestamp).getTime() : 0;
264
+ if (!ts || isNaN(ts))
265
+ continue;
266
+ const msgContent = obj.message?.content;
267
+ let text = '';
268
+ if (typeof msgContent === 'string') {
269
+ text = msgContent;
270
+ }
271
+ else if (Array.isArray(msgContent)) {
272
+ const textBlocks = msgContent.filter(c => c?.type === 'text');
273
+ if (textBlocks.length === 0)
274
+ continue;
275
+ text = textBlocks.map((c) => c.text ?? '').join('\n').trim();
276
+ }
277
+ if (text.includes('<command-name>') ||
278
+ text.includes('<local-command-stdout>') ||
279
+ text.includes('<system-reminder>') ||
280
+ text.length === 0)
281
+ continue;
282
+ index++;
283
+ results.push({ index, ts, text });
284
+ }
285
+ catch { }
286
+ }
287
+ return results;
288
+ }
289
+ }
290
+ }
291
+ catch { }
292
+ return [];
293
+ }
294
+ // ─── Watcher ─────────────────────────────────────────────────────────────────
295
+ const PROJECTS_DIR = path_1.default.join((0, paths_1.getClaudeDir)(), 'projects');
296
+ const prevContextBySession = new Map();
297
+ let watcher = null;
298
+ const pendingFiles = new Map();
299
+ let offsetCleanupInterval = null;
300
+ function startEnricher(onUpdate, onCompact, onSessionEnd) {
301
+ if (!fs_1.default.existsSync(PROJECTS_DIR)) {
302
+ console.warn(`[enricher] Directory not found: ${PROJECTS_DIR}`);
303
+ return;
304
+ }
305
+ watcher = chokidar_1.default.watch(`${PROJECTS_DIR}/**/*.jsonl`, {
306
+ persistent: true,
307
+ ignoreInitial: true,
308
+ awaitWriteFinish: {
309
+ stabilityThreshold: 200,
310
+ pollInterval: 100
311
+ }
312
+ });
313
+ const handleFile = (filePath) => {
314
+ const sessionId = path_1.default.basename(filePath, '.jsonl');
315
+ if (!sessionId.includes('-') || sessionId.length < 10)
316
+ return;
317
+ const existing = pendingFiles.get(filePath);
318
+ if (existing)
319
+ clearTimeout(existing);
320
+ const timer = setTimeout(() => {
321
+ pendingFiles.delete(filePath);
322
+ processJSONL(filePath).then(cost => {
323
+ if (cost && cost.cost_usd >= 0) {
324
+ const prev = prevContextBySession.get(sessionId);
325
+ if (onCompact && prev !== undefined && prev > 140000 && cost.context_used < prev * 0.5) {
326
+ onCompact(sessionId);
327
+ }
328
+ prevContextBySession.set(sessionId, cost.context_used);
329
+ onUpdate(sessionId, cost);
330
+ }
331
+ }).catch(err => console.error('[enricher] Error processing JSONL:', err));
332
+ }, 100);
333
+ pendingFiles.set(filePath, timer);
334
+ };
335
+ watcher.on('change', handleFile);
336
+ watcher.on('add', handleFile);
337
+ offsetCleanupInterval = setInterval(cleanupStaleOffsets, 5 * 60000);
338
+ console.log(`[enricher] Watching ${PROJECTS_DIR}`);
339
+ }
340
+ function stopEnricher() {
341
+ if (watcher) {
342
+ watcher.close();
343
+ watcher = null;
344
+ }
345
+ if (offsetCleanupInterval) {
346
+ clearInterval(offsetCleanupInterval);
347
+ offsetCleanupInterval = null;
348
+ }
349
+ for (const [, timer] of pendingFiles)
350
+ clearTimeout(timer);
351
+ pendingFiles.clear();
352
+ fileOffsets.clear();
353
+ prevContextBySession.clear();
354
+ blockCostCache.clear();
355
+ console.log('[enricher] Stopped');
356
+ }
357
+ function cleanupSession(sessionId) {
358
+ blockCostCache.delete(sessionId);
359
+ prevContextBySession.delete(sessionId);
360
+ for (const [key, entry] of fileOffsets) {
361
+ if (key.includes(sessionId))
362
+ fileOffsets.delete(key);
363
+ }
364
+ }
365
+ async function processLatestForSession(sessionId, onUpdate) {
366
+ try {
367
+ if (!fs_1.default.existsSync(PROJECTS_DIR))
368
+ return;
369
+ const dirs = await promises_1.default.readdir(PROJECTS_DIR);
370
+ for (const dir of dirs) {
371
+ const dirPath = path_1.default.join(PROJECTS_DIR, dir);
372
+ try {
373
+ const stat = await promises_1.default.stat(dirPath);
374
+ if (!stat.isDirectory())
375
+ continue;
376
+ }
377
+ catch {
378
+ continue;
379
+ }
380
+ const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
381
+ try {
382
+ await promises_1.default.access(filePath);
383
+ }
384
+ catch {
385
+ continue;
386
+ }
387
+ const cost = await processJSONL(filePath);
388
+ if (cost && cost.cost_usd >= 0)
389
+ onUpdate(sessionId, cost);
390
+ return;
391
+ }
392
+ }
393
+ catch { }
394
+ }
@@ -0,0 +1,8 @@
1
+ export interface ExportOpts {
2
+ format: 'json' | 'csv';
3
+ from?: string;
4
+ to?: string;
5
+ project?: string;
6
+ output?: string;
7
+ }
8
+ export declare function runExport(opts: ExportOpts): void;