letmecode 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/ink-app/dist/index.js +263 -133
- package/ink-app/dist/providers/antigravity.js +290 -0
- package/ink-app/dist/providers/claude.js +539 -56
- package/ink-app/dist/providers/codex.js +111 -18
- package/ink-app/dist/providers/contract.js +14 -46
- package/ink-app/dist/providers/copilot.js +31 -46
- package/ink-app/dist/providers/index.js +14 -1
- package/ink-app/dist/providers/limits.js +1 -1
- package/ink-app/dist/providers/pricing.js +18 -0
- package/ink-app/dist/reporting.js +115 -0
- package/package.json +5 -2
|
@@ -1,31 +1,67 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
1
3
|
import fs from "node:fs";
|
|
2
4
|
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import readline from "node:readline";
|
|
7
|
+
import { promisify } from "node:util";
|
|
5
8
|
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
|
|
6
9
|
import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
|
|
7
10
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
11
|
+
import { resolveUsageRate } from "./pricing.js";
|
|
8
12
|
const RATE_CARD = {
|
|
9
|
-
"claude-opus-4-8": { input: 5,
|
|
10
|
-
"claude-opus-4-7": { input: 5,
|
|
11
|
-
"claude-opus-4-6": { input: 5,
|
|
12
|
-
"claude-opus-4-5": { input: 5,
|
|
13
|
-
"claude-opus-4-1": { input: 15,
|
|
14
|
-
"claude-opus-4": { input: 15,
|
|
15
|
-
"claude-sonnet-4-6": { input: 3,
|
|
16
|
-
"claude-sonnet-4-5": { input: 3,
|
|
17
|
-
"claude-sonnet-4": { input: 3,
|
|
18
|
-
"claude-haiku-4-5": { input: 1,
|
|
19
|
-
"claude-haiku-3-5": { input: 0.8,
|
|
13
|
+
"claude-opus-4-8": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
14
|
+
"claude-opus-4-7": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
15
|
+
"claude-opus-4-6": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
16
|
+
"claude-opus-4-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
|
|
17
|
+
"claude-opus-4-1": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
|
|
18
|
+
"claude-opus-4": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
|
|
19
|
+
"claude-sonnet-4-6": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
20
|
+
"claude-sonnet-4-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
21
|
+
"claude-sonnet-4": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
|
|
22
|
+
"claude-haiku-4-5": { input: 1, cacheRead: 0.1, cacheWrite: 1.25, cacheWrite5m: 1.25, cacheWrite1h: 2, output: 5 },
|
|
23
|
+
"claude-haiku-3-5": { input: 0.8, cacheRead: 0.08, cacheWrite: 1, cacheWrite5m: 1, cacheWrite1h: 1.6, output: 4 }
|
|
20
24
|
};
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
21
26
|
const USD_TO_CREDITS = 100;
|
|
27
|
+
const VSCODE_CLAUDE_EXTENSION_PREFIX = "anthropic.claude-code-";
|
|
28
|
+
const CLAUDE_SESSION_WINDOW_MINUTES = 5 * 60;
|
|
29
|
+
const CLAUDE_WEEK_WINDOW_MINUTES = 7 * 24 * 60;
|
|
30
|
+
const ANSI_ESCAPE_SEQUENCE = /\u001B\[[0-9;]*[A-Za-z]/g;
|
|
31
|
+
const DEFAULT_CLAUDE_ENTRYPOINTS = ["sdk-cli", "claude"];
|
|
32
|
+
const MONTH_INDEX_BY_LABEL = {
|
|
33
|
+
jan: 0,
|
|
34
|
+
feb: 1,
|
|
35
|
+
mar: 2,
|
|
36
|
+
apr: 3,
|
|
37
|
+
may: 4,
|
|
38
|
+
jun: 5,
|
|
39
|
+
jul: 6,
|
|
40
|
+
aug: 7,
|
|
41
|
+
sep: 8,
|
|
42
|
+
oct: 9,
|
|
43
|
+
nov: 10,
|
|
44
|
+
dec: 11
|
|
45
|
+
};
|
|
46
|
+
const parsedClaudeSessionFilesCache = new Map();
|
|
47
|
+
const claudeBinaryPathCache = new Map();
|
|
48
|
+
const claudeUsageOutputCache = new Map();
|
|
49
|
+
const claudeAuthStatusOutputCache = new Map();
|
|
22
50
|
export class ClaudeUsageProvider extends UsageProviderBase {
|
|
23
51
|
constructor(options = {}) {
|
|
24
|
-
super("claude", "Claude");
|
|
52
|
+
super(options.id ?? "claude", options.label ?? "Claude");
|
|
25
53
|
this.root = path.resolve(options.root ?? os.homedir());
|
|
54
|
+
this.entrypoints = new Set(options.entrypoints ?? DEFAULT_CLAUDE_ENTRYPOINTS);
|
|
55
|
+
this.usageCommandKind = options.usageCommandKind ?? "cli";
|
|
56
|
+
this.readUsageCommandOutput = options.readUsageCommandOutput;
|
|
57
|
+
this.readAuthStatusOutput = options.readAuthStatusOutput;
|
|
58
|
+
this.now = options.now ?? (() => new Date());
|
|
26
59
|
}
|
|
27
60
|
async getStats(options = {}) {
|
|
28
|
-
const
|
|
61
|
+
const resolvedSessionsRoot = await resolveClaudeSessionsRoot(this.root);
|
|
62
|
+
const sessionsRoot = resolvedSessionsRoot.rootPath;
|
|
63
|
+
const agentName = normalizeAnalyticsAgentName(this.label);
|
|
64
|
+
const userIdHash = await readClaudeUserIdHash(this.root, this.usageCommandKind, this.readAuthStatusOutput, agentName);
|
|
29
65
|
const byModel = new Map();
|
|
30
66
|
const byDay = createDailyUsageAggregates();
|
|
31
67
|
const windows = createLimitWindowAggregates();
|
|
@@ -38,11 +74,18 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
38
74
|
tokenEvents: 0,
|
|
39
75
|
malformedLines: 0
|
|
40
76
|
};
|
|
41
|
-
|
|
77
|
+
const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot);
|
|
78
|
+
for (const file of parsedSessionFiles) {
|
|
79
|
+
const matchingEvents = file.events.filter((event) => this.entrypoints.has(event.entrypoint));
|
|
80
|
+
if (matchingEvents.length === 0) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
42
83
|
parseTotals.filesScanned += 1;
|
|
43
|
-
|
|
44
|
-
parseTotals.
|
|
45
|
-
|
|
84
|
+
parseTotals.linesRead += file.linesRead;
|
|
85
|
+
parseTotals.malformedLines += file.malformedLines;
|
|
86
|
+
for (const event of matchingEvents) {
|
|
87
|
+
recordParsedUsageEvent(parsedEvents, event);
|
|
88
|
+
}
|
|
46
89
|
}
|
|
47
90
|
const selectedEvents = [
|
|
48
91
|
...parsedEvents.keyedEvents.values(),
|
|
@@ -77,18 +120,26 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
77
120
|
if (unknownPricedModels.length > 0) {
|
|
78
121
|
warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
79
122
|
}
|
|
80
|
-
if (
|
|
123
|
+
if (parsedSessionFiles.length === 0) {
|
|
81
124
|
warnings.push(`No Claude session files found under ${sessionsRoot}.`);
|
|
82
125
|
}
|
|
83
126
|
const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
|
|
84
127
|
const dayUsage = buildDailyUsageRows(byDay);
|
|
85
|
-
const [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
128
|
+
const [fallbackPrimaryLimitWindows, fallbackSecondaryLimitWindows] = buildWindowLists(windows);
|
|
129
|
+
const liveLimitWindows = await buildLiveLimitWindows({
|
|
130
|
+
root: this.root,
|
|
131
|
+
usageCommandKind: this.usageCommandKind,
|
|
132
|
+
readUsageCommandOutput: this.readUsageCommandOutput,
|
|
133
|
+
readAuthStatusOutput: this.readAuthStatusOutput,
|
|
134
|
+
now: this.now(),
|
|
135
|
+
selectedEvents
|
|
136
|
+
});
|
|
137
|
+
const primaryLimitWindows = liveLimitWindows.primaryLimitWindows.length > 0
|
|
138
|
+
? liveLimitWindows.primaryLimitWindows
|
|
139
|
+
: fallbackPrimaryLimitWindows;
|
|
140
|
+
const secondaryLimitWindows = liveLimitWindows.secondaryLimitWindows.length > 0
|
|
141
|
+
? liveLimitWindows.secondaryLimitWindows
|
|
142
|
+
: fallbackSecondaryLimitWindows;
|
|
92
143
|
return {
|
|
93
144
|
providerId: this.id,
|
|
94
145
|
providerLabel: this.label,
|
|
@@ -99,14 +150,18 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
99
150
|
totals: summaryTotals,
|
|
100
151
|
distinctModels: modelUsage.map((row) => row.modelId),
|
|
101
152
|
distinctPlanTypes: [...planTypes].sort(),
|
|
102
|
-
rootLabel:
|
|
153
|
+
rootLabel: resolvedSessionsRoot.rootLabel,
|
|
103
154
|
rootPath: sessionsRoot
|
|
104
155
|
},
|
|
105
156
|
modelUsage,
|
|
106
157
|
dayUsage,
|
|
107
158
|
primaryLimitWindows,
|
|
108
159
|
secondaryLimitWindows,
|
|
109
|
-
warnings
|
|
160
|
+
warnings,
|
|
161
|
+
analytics: {
|
|
162
|
+
agentName,
|
|
163
|
+
userIdHash
|
|
164
|
+
}
|
|
110
165
|
};
|
|
111
166
|
}
|
|
112
167
|
}
|
|
@@ -127,13 +182,7 @@ function normalizeUsage(value) {
|
|
|
127
182
|
};
|
|
128
183
|
}
|
|
129
184
|
function resolveRate(modelId) {
|
|
130
|
-
|
|
131
|
-
for (const candidate of candidates) {
|
|
132
|
-
if (modelId === candidate || modelId.startsWith(`${candidate}-`)) {
|
|
133
|
-
return RATE_CARD[candidate];
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return undefined;
|
|
185
|
+
return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true });
|
|
137
186
|
}
|
|
138
187
|
function isInternalClaudeModel(modelId) {
|
|
139
188
|
return modelId === "<synthetic>";
|
|
@@ -155,30 +204,27 @@ function creditsFor(modelId, usage) {
|
|
|
155
204
|
}
|
|
156
205
|
function usageToTotals(modelId, usage) {
|
|
157
206
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
158
|
-
const
|
|
159
|
-
cacheWriteBreakdown.
|
|
160
|
-
cacheWriteBreakdown.cacheWrite1hInputTokens +
|
|
161
|
-
usage.cacheReadInputTokens;
|
|
207
|
+
const cacheWriteInputTokens = cacheWriteBreakdown.cacheWrite5mInputTokens +
|
|
208
|
+
cacheWriteBreakdown.cacheWrite1hInputTokens;
|
|
162
209
|
return {
|
|
163
|
-
|
|
210
|
+
inputTokens: usage.inputTokens,
|
|
164
211
|
outputTokens: usage.outputTokens,
|
|
212
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
213
|
+
cacheWriteInputTokens,
|
|
214
|
+
cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
|
|
215
|
+
cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens,
|
|
165
216
|
reasoningOutputTokens: 0,
|
|
166
|
-
totalTokens:
|
|
217
|
+
totalTokens: usage.inputTokens +
|
|
218
|
+
usage.cacheReadInputTokens +
|
|
219
|
+
cacheWriteInputTokens +
|
|
220
|
+
usage.outputTokens,
|
|
167
221
|
estimatedCredits: creditsFor(modelId, usage),
|
|
168
|
-
eventCount: 1
|
|
169
|
-
tokenBreakdown: {
|
|
170
|
-
schema: "anthropic",
|
|
171
|
-
inputTokens: usage.inputTokens,
|
|
172
|
-
cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
|
|
173
|
-
cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens,
|
|
174
|
-
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
175
|
-
outputTokens: usage.outputTokens
|
|
176
|
-
}
|
|
222
|
+
eventCount: 1
|
|
177
223
|
};
|
|
178
224
|
}
|
|
179
225
|
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
180
226
|
const resolvedModelId = modelId || "unknown";
|
|
181
|
-
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals(
|
|
227
|
+
const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
|
|
182
228
|
addUsageTotals(totals, deltaTotals);
|
|
183
229
|
byModel.set(resolvedModelId, totals);
|
|
184
230
|
}
|
|
@@ -191,7 +237,87 @@ function resolveClaudeCacheWriteBreakdown(usage) {
|
|
|
191
237
|
};
|
|
192
238
|
}
|
|
193
239
|
function isSessionFile(filePath) {
|
|
194
|
-
return filePath.endsWith(".jsonl")
|
|
240
|
+
return filePath.endsWith(".jsonl");
|
|
241
|
+
}
|
|
242
|
+
async function resolveClaudeSessionsRoot(root) {
|
|
243
|
+
const candidates = buildClaudeSessionsRootCandidates(root);
|
|
244
|
+
for (const candidate of candidates) {
|
|
245
|
+
if (await isDirectory(candidate.rootPath)) {
|
|
246
|
+
return candidate;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return candidates[0] ?? {
|
|
250
|
+
rootLabel: "~/.claude/projects",
|
|
251
|
+
rootPath: path.join(path.resolve(root), ".claude", "projects")
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function buildClaudeSessionsRootCandidates(root) {
|
|
255
|
+
const resolvedRoot = path.resolve(root);
|
|
256
|
+
const baseName = path.basename(resolvedRoot);
|
|
257
|
+
const parentBaseName = path.basename(path.dirname(resolvedRoot));
|
|
258
|
+
const candidates = [];
|
|
259
|
+
if (baseName === "projects") {
|
|
260
|
+
if (parentBaseName === ".claude") {
|
|
261
|
+
candidates.push({
|
|
262
|
+
rootLabel: "~/.claude/projects",
|
|
263
|
+
rootPath: resolvedRoot
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else if (parentBaseName === "claude" || parentBaseName === "Claude") {
|
|
267
|
+
candidates.push({
|
|
268
|
+
rootLabel: `~/.config/${parentBaseName}/projects`,
|
|
269
|
+
rootPath: resolvedRoot
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
candidates.push({
|
|
274
|
+
rootLabel: "projects",
|
|
275
|
+
rootPath: resolvedRoot
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (baseName === ".claude") {
|
|
280
|
+
candidates.push({
|
|
281
|
+
rootLabel: "~/.claude/projects",
|
|
282
|
+
rootPath: path.join(resolvedRoot, "projects")
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (parentBaseName === ".config" && (baseName === "claude" || baseName === "Claude")) {
|
|
286
|
+
candidates.push({
|
|
287
|
+
rootLabel: `~/.config/${baseName}/projects`,
|
|
288
|
+
rootPath: path.join(resolvedRoot, "projects")
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
candidates.push({
|
|
292
|
+
rootLabel: "~/.claude/projects",
|
|
293
|
+
rootPath: path.join(resolvedRoot, ".claude", "projects")
|
|
294
|
+
}, {
|
|
295
|
+
rootLabel: "~/.config/claude/projects",
|
|
296
|
+
rootPath: path.join(resolvedRoot, ".config", "claude", "projects")
|
|
297
|
+
}, {
|
|
298
|
+
rootLabel: "~/.config/Claude/projects",
|
|
299
|
+
rootPath: path.join(resolvedRoot, ".config", "Claude", "projects")
|
|
300
|
+
});
|
|
301
|
+
const dedupedCandidates = new Map();
|
|
302
|
+
for (const candidate of candidates) {
|
|
303
|
+
const normalizedPath = path.resolve(candidate.rootPath);
|
|
304
|
+
if (!dedupedCandidates.has(normalizedPath)) {
|
|
305
|
+
dedupedCandidates.set(normalizedPath, {
|
|
306
|
+
rootLabel: candidate.rootLabel,
|
|
307
|
+
rootPath: normalizedPath
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return [...dedupedCandidates.values()];
|
|
312
|
+
}
|
|
313
|
+
async function isDirectory(directoryPath) {
|
|
314
|
+
try {
|
|
315
|
+
const stats = await fs.promises.stat(directoryPath);
|
|
316
|
+
return stats.isDirectory();
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
195
321
|
}
|
|
196
322
|
async function* walkSessionFiles(directory) {
|
|
197
323
|
let entries;
|
|
@@ -211,11 +337,28 @@ async function* walkSessionFiles(directory) {
|
|
|
211
337
|
}
|
|
212
338
|
}
|
|
213
339
|
}
|
|
214
|
-
async function
|
|
340
|
+
async function loadParsedClaudeSessionFiles(sessionsRoot) {
|
|
341
|
+
const cacheKey = path.resolve(sessionsRoot);
|
|
342
|
+
const cached = parsedClaudeSessionFilesCache.get(cacheKey);
|
|
343
|
+
if (cached) {
|
|
344
|
+
return cached;
|
|
345
|
+
}
|
|
346
|
+
const pending = (async () => {
|
|
347
|
+
const files = [];
|
|
348
|
+
for await (const filePath of walkSessionFiles(sessionsRoot)) {
|
|
349
|
+
files.push(await parseSessionFile(filePath));
|
|
350
|
+
}
|
|
351
|
+
return files;
|
|
352
|
+
})();
|
|
353
|
+
parsedClaudeSessionFilesCache.set(cacheKey, pending);
|
|
354
|
+
return pending;
|
|
355
|
+
}
|
|
356
|
+
async function parseSessionFile(filePath) {
|
|
215
357
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
216
358
|
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
217
359
|
let linesRead = 0;
|
|
218
360
|
let malformedLines = 0;
|
|
361
|
+
const events = [];
|
|
219
362
|
for await (const line of lineReader) {
|
|
220
363
|
linesRead += 1;
|
|
221
364
|
if (!line.trim()) {
|
|
@@ -243,17 +386,17 @@ async function parseSessionFile(filePath, parsedEvents) {
|
|
|
243
386
|
const normalizedUsage = normalizeUsage(usage);
|
|
244
387
|
const usageKey = buildUsageEventKey(payloadObject, message);
|
|
245
388
|
const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
|
|
246
|
-
|
|
389
|
+
events.push({
|
|
390
|
+
entrypoint: typeof payloadObject.entrypoint === "string" ? payloadObject.entrypoint : "",
|
|
247
391
|
usageKey,
|
|
248
392
|
usageSignature,
|
|
249
393
|
timestampMs: eventTimeMs,
|
|
250
394
|
modelId,
|
|
251
395
|
totals: usageToTotals(modelId, normalizedUsage),
|
|
252
396
|
rateLimits
|
|
253
|
-
};
|
|
254
|
-
recordParsedUsageEvent(parsedEvents, parsedEvent);
|
|
397
|
+
});
|
|
255
398
|
}
|
|
256
|
-
return { linesRead, malformedLines };
|
|
399
|
+
return { linesRead, malformedLines, events };
|
|
257
400
|
}
|
|
258
401
|
function buildUsageEventKey(payloadObject, message) {
|
|
259
402
|
const sessionId = String(payloadObject.sessionId ?? "");
|
|
@@ -327,3 +470,343 @@ function normalizeTimestamp(value) {
|
|
|
327
470
|
function extractRateLimits(payloadObject, message) {
|
|
328
471
|
return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
|
|
329
472
|
}
|
|
473
|
+
async function buildLiveLimitWindows(options) {
|
|
474
|
+
const [usageOutput, subscriptionType] = await Promise.all([
|
|
475
|
+
readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput),
|
|
476
|
+
readClaudeSubscriptionType(options.root, options.usageCommandKind, options.readAuthStatusOutput)
|
|
477
|
+
]);
|
|
478
|
+
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
479
|
+
const resolvedPlanType = subscriptionType || "live";
|
|
480
|
+
return {
|
|
481
|
+
primaryLimitWindows: snapshots
|
|
482
|
+
.filter((snapshot) => snapshot.scope === "primary")
|
|
483
|
+
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now)),
|
|
484
|
+
secondaryLimitWindows: snapshots
|
|
485
|
+
.filter((snapshot) => snapshot.scope === "secondary")
|
|
486
|
+
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now))
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
async function readClaudeSubscriptionType(root, usageCommandKind, override) {
|
|
490
|
+
const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
|
|
491
|
+
return parseClaudeSubscriptionType(output);
|
|
492
|
+
}
|
|
493
|
+
async function readClaudeAuthStatusOutput(root, usageCommandKind, override) {
|
|
494
|
+
if (override) {
|
|
495
|
+
try {
|
|
496
|
+
return await override();
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
503
|
+
const cached = claudeAuthStatusOutputCache.get(cacheKey);
|
|
504
|
+
if (cached) {
|
|
505
|
+
return cached;
|
|
506
|
+
}
|
|
507
|
+
const pending = (async () => {
|
|
508
|
+
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
|
|
509
|
+
if (!binaryPath) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const { stdout, stderr } = await execFileAsync(binaryPath, ["auth", "status"], {
|
|
514
|
+
encoding: "utf8",
|
|
515
|
+
maxBuffer: 1024 * 1024,
|
|
516
|
+
timeout: 15000,
|
|
517
|
+
windowsHide: true
|
|
518
|
+
});
|
|
519
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
520
|
+
return combined || null;
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
const combined = extractExecOutput(error);
|
|
524
|
+
return combined || null;
|
|
525
|
+
}
|
|
526
|
+
})();
|
|
527
|
+
claudeAuthStatusOutputCache.set(cacheKey, pending);
|
|
528
|
+
return pending;
|
|
529
|
+
}
|
|
530
|
+
function parseClaudeSubscriptionType(output) {
|
|
531
|
+
const snapshot = parseClaudeAuthStatusSnapshot(output);
|
|
532
|
+
return snapshot?.subscriptionType ?? null;
|
|
533
|
+
}
|
|
534
|
+
function parseClaudeAuthStatusSnapshot(output) {
|
|
535
|
+
if (!output) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const normalizedOutput = output.replace(ANSI_ESCAPE_SEQUENCE, "").trim();
|
|
539
|
+
const firstBraceIndex = normalizedOutput.indexOf("{");
|
|
540
|
+
const lastBraceIndex = normalizedOutput.lastIndexOf("}");
|
|
541
|
+
if (firstBraceIndex < 0 || lastBraceIndex <= firstBraceIndex) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const payload = JSON.parse(normalizedOutput.slice(firstBraceIndex, lastBraceIndex + 1));
|
|
546
|
+
const record = asRecord(payload);
|
|
547
|
+
const email = typeof record?.email === "string" ? record.email.trim() : "";
|
|
548
|
+
const orgId = typeof record?.orgId === "string" ? record.orgId.trim() : "";
|
|
549
|
+
const orgName = typeof record?.orgName === "string" ? record.orgName.trim() : "";
|
|
550
|
+
const subscriptionType = typeof record?.subscriptionType === "string" ? record.subscriptionType.trim() : "";
|
|
551
|
+
if (!email || !orgId || !orgName || !subscriptionType) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
return { email, orgId, orgName, subscriptionType };
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function readClaudeUserIdHash(root, usageCommandKind, override, agentName) {
|
|
561
|
+
const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
|
|
562
|
+
const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
|
|
563
|
+
if (!snapshot) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
|
|
567
|
+
}
|
|
568
|
+
async function readClaudeUsageCommandOutput(root, usageCommandKind, override) {
|
|
569
|
+
if (override) {
|
|
570
|
+
try {
|
|
571
|
+
return await override();
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
578
|
+
const cached = claudeUsageOutputCache.get(cacheKey);
|
|
579
|
+
if (cached) {
|
|
580
|
+
return cached;
|
|
581
|
+
}
|
|
582
|
+
const pending = (async () => {
|
|
583
|
+
const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
|
|
584
|
+
if (!binaryPath) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const { stdout, stderr } = await execFileAsync(binaryPath, ["-p", "/usage"], {
|
|
589
|
+
encoding: "utf8",
|
|
590
|
+
maxBuffer: 1024 * 1024,
|
|
591
|
+
timeout: 15000,
|
|
592
|
+
windowsHide: true
|
|
593
|
+
});
|
|
594
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
595
|
+
return combined || null;
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
const combined = extractExecOutput(error);
|
|
599
|
+
return combined || null;
|
|
600
|
+
}
|
|
601
|
+
})();
|
|
602
|
+
claudeUsageOutputCache.set(cacheKey, pending);
|
|
603
|
+
return pending;
|
|
604
|
+
}
|
|
605
|
+
function extractExecOutput(error) {
|
|
606
|
+
if (!error || typeof error !== "object") {
|
|
607
|
+
return "";
|
|
608
|
+
}
|
|
609
|
+
const stdout = typeof error.stdout === "string" ? error.stdout : "";
|
|
610
|
+
const stderr = typeof error.stderr === "string" ? error.stderr : "";
|
|
611
|
+
return [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
612
|
+
}
|
|
613
|
+
async function resolveClaudeBinaryPath(root, usageCommandKind) {
|
|
614
|
+
const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
|
|
615
|
+
const cached = claudeBinaryPathCache.get(cacheKey);
|
|
616
|
+
if (cached) {
|
|
617
|
+
return cached;
|
|
618
|
+
}
|
|
619
|
+
const pending = usageCommandKind === "vscode"
|
|
620
|
+
? resolveVsCodeClaudeBinaryPath(root)
|
|
621
|
+
: resolveCliClaudeBinaryPath(root);
|
|
622
|
+
claudeBinaryPathCache.set(cacheKey, pending);
|
|
623
|
+
return pending;
|
|
624
|
+
}
|
|
625
|
+
async function resolveVsCodeClaudeBinaryPath(root) {
|
|
626
|
+
const boosterDirectories = [
|
|
627
|
+
path.join(root, ".vscode", "extensions"),
|
|
628
|
+
path.join(root, ".vscode-server", "extensions"),
|
|
629
|
+
path.join(root, ".vscode-server-insiders", "extensions")
|
|
630
|
+
];
|
|
631
|
+
for (const directory of boosterDirectories) {
|
|
632
|
+
const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory);
|
|
633
|
+
if (binaryPath) {
|
|
634
|
+
return binaryPath;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
async function resolveClaudeBinaryFromExtensionDirectory(directory) {
|
|
640
|
+
let entries;
|
|
641
|
+
try {
|
|
642
|
+
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const candidates = entries
|
|
648
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX))
|
|
649
|
+
.map((entry) => entry.name)
|
|
650
|
+
.sort(compareClaudeExtensionDirectoryNames);
|
|
651
|
+
for (const candidate of candidates) {
|
|
652
|
+
const binaryPath = path.join(directory, candidate, "resources", "native-binary", "claude");
|
|
653
|
+
if (await isReadableExecutableFile(binaryPath)) {
|
|
654
|
+
return binaryPath;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
function compareClaudeExtensionDirectoryNames(left, right) {
|
|
660
|
+
const leftVersion = extractClaudeExtensionVersion(left);
|
|
661
|
+
const rightVersion = extractClaudeExtensionVersion(right);
|
|
662
|
+
const length = Math.max(leftVersion.length, rightVersion.length);
|
|
663
|
+
for (let index = 0; index < length; index += 1) {
|
|
664
|
+
const difference = (rightVersion[index] ?? 0) - (leftVersion[index] ?? 0);
|
|
665
|
+
if (difference !== 0) {
|
|
666
|
+
return difference;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return right.localeCompare(left);
|
|
670
|
+
}
|
|
671
|
+
function extractClaudeExtensionVersion(directoryName) {
|
|
672
|
+
if (!directoryName.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX)) {
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
const versionLabel = directoryName.slice(VSCODE_CLAUDE_EXTENSION_PREFIX.length).split("-")[0] ?? "";
|
|
676
|
+
return versionLabel
|
|
677
|
+
.split(".")
|
|
678
|
+
.map((part) => Number(part))
|
|
679
|
+
.filter((part) => Number.isFinite(part));
|
|
680
|
+
}
|
|
681
|
+
async function resolveCliClaudeBinaryPath(root) {
|
|
682
|
+
const directCandidates = [
|
|
683
|
+
path.join(root, ".local", "bin", "claude"),
|
|
684
|
+
path.join(root, "bin", "claude")
|
|
685
|
+
];
|
|
686
|
+
for (const candidate of directCandidates) {
|
|
687
|
+
if (await isReadableExecutableFile(candidate)) {
|
|
688
|
+
return candidate;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
async function isReadableExecutableFile(filePath) {
|
|
694
|
+
try {
|
|
695
|
+
await fs.promises.access(filePath, fs.constants.R_OK | fs.constants.X_OK);
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function parseLiveUsageWindowSnapshots(usageOutput, now) {
|
|
703
|
+
if (!usageOutput) {
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
const snapshots = new Map();
|
|
707
|
+
const normalizedOutput = usageOutput.replace(ANSI_ESCAPE_SEQUENCE, "");
|
|
708
|
+
for (const line of normalizedOutput.split(/\r?\n/)) {
|
|
709
|
+
const match = line
|
|
710
|
+
.trim()
|
|
711
|
+
.match(/^Current\s+(session|week)(?:\s+\([^)]+\))?:\s+(\d+)%\s+used\b.*?\bresets\s+(.+)$/i);
|
|
712
|
+
if (!match) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const label = match[1].toLowerCase() === "session" ? "session" : "week";
|
|
716
|
+
const usedPercent = Number(match[2]);
|
|
717
|
+
const windowMinutes = label === "session" ? CLAUDE_SESSION_WINDOW_MINUTES : CLAUDE_WEEK_WINDOW_MINUTES;
|
|
718
|
+
const resetsAtMs = parseResetTimestampUtc(match[3], now.getTime(), windowMinutes);
|
|
719
|
+
if (!Number.isFinite(usedPercent) || !resetsAtMs) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
snapshots.set(label, {
|
|
723
|
+
scope: label === "session" ? "primary" : "secondary",
|
|
724
|
+
label,
|
|
725
|
+
usedPercent,
|
|
726
|
+
resetsAtMs,
|
|
727
|
+
windowMinutes
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
return [...snapshots.values()].sort((left, right) => left.windowMinutes - right.windowMinutes);
|
|
731
|
+
}
|
|
732
|
+
function parseResetTimestampUtc(value, nowMs, windowMinutes) {
|
|
733
|
+
const match = value
|
|
734
|
+
.trim()
|
|
735
|
+
.match(/^([A-Za-z]{3})\s+(\d{1,2}),\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s+\(UTC\)$/i);
|
|
736
|
+
if (!match) {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
const monthIndex = MONTH_INDEX_BY_LABEL[match[1].slice(0, 3).toLowerCase()];
|
|
740
|
+
const day = Number(match[2]);
|
|
741
|
+
const hour12 = Number(match[3]);
|
|
742
|
+
const minute = Number(match[4] ?? "0");
|
|
743
|
+
const meridiem = match[5].toLowerCase();
|
|
744
|
+
if (monthIndex === undefined ||
|
|
745
|
+
!Number.isFinite(day) ||
|
|
746
|
+
!Number.isFinite(hour12) ||
|
|
747
|
+
!Number.isFinite(minute) ||
|
|
748
|
+
day < 1 ||
|
|
749
|
+
day > 31 ||
|
|
750
|
+
hour12 < 1 ||
|
|
751
|
+
hour12 > 12 ||
|
|
752
|
+
minute < 0 ||
|
|
753
|
+
minute > 59) {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
const hour24 = resolveHour24(hour12, meridiem);
|
|
757
|
+
const currentYear = new Date(nowMs).getUTCFullYear();
|
|
758
|
+
const candidates = [currentYear - 1, currentYear, currentYear + 1]
|
|
759
|
+
.map((year) => Date.UTC(year, monthIndex, day, hour24, minute))
|
|
760
|
+
.filter((candidate) => Number.isFinite(candidate));
|
|
761
|
+
const maxFutureMs = nowMs + windowMinutes * 60000 + 24 * 60 * 60 * 1000;
|
|
762
|
+
const plausibleFutureCandidate = candidates
|
|
763
|
+
.filter((candidate) => candidate >= nowMs - 60000 && candidate <= maxFutureMs)
|
|
764
|
+
.sort((left, right) => left - right)[0];
|
|
765
|
+
if (plausibleFutureCandidate !== undefined) {
|
|
766
|
+
return plausibleFutureCandidate;
|
|
767
|
+
}
|
|
768
|
+
const nearestCandidate = candidates.sort((left, right) => Math.abs(left - nowMs) - Math.abs(right - nowMs))[0];
|
|
769
|
+
return nearestCandidate ?? null;
|
|
770
|
+
}
|
|
771
|
+
function resolveHour24(hour12, meridiem) {
|
|
772
|
+
if (hour12 === 12) {
|
|
773
|
+
return meridiem === "am" ? 0 : 12;
|
|
774
|
+
}
|
|
775
|
+
return meridiem === "pm" ? hour12 + 12 : hour12;
|
|
776
|
+
}
|
|
777
|
+
function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
|
|
778
|
+
const startTimeMs = snapshot.resetsAtMs - snapshot.windowMinutes * 60000;
|
|
779
|
+
const inWindowEvents = selectedEvents.filter((event) => Number.isFinite(event.timestampMs) &&
|
|
780
|
+
event.timestampMs >= startTimeMs &&
|
|
781
|
+
event.timestampMs < snapshot.resetsAtMs);
|
|
782
|
+
const totals = sumUsageTotals(inWindowEvents.map((event) => event.totals));
|
|
783
|
+
const fallbackLastSeenMs = Math.min(now.getTime(), snapshot.resetsAtMs);
|
|
784
|
+
const firstSeenMs = inWindowEvents.reduce((minimum, event) => Math.min(minimum, event.timestampMs), Number.POSITIVE_INFINITY);
|
|
785
|
+
const lastSeenMs = inWindowEvents.reduce((maximum, event) => Math.max(maximum, event.timestampMs), Number.NEGATIVE_INFINITY);
|
|
786
|
+
return {
|
|
787
|
+
scope: snapshot.scope,
|
|
788
|
+
planType,
|
|
789
|
+
limitId: `current-${snapshot.label}`,
|
|
790
|
+
windowMinutes: snapshot.windowMinutes,
|
|
791
|
+
startTimeUtcIso: toUtcIso(startTimeMs),
|
|
792
|
+
endTimeUtcIso: toUtcIso(snapshot.resetsAtMs),
|
|
793
|
+
firstSeenUtcIso: toUtcIso(Number.isFinite(firstSeenMs) ? firstSeenMs : startTimeMs),
|
|
794
|
+
lastSeenUtcIso: toUtcIso(Number.isFinite(lastSeenMs) ? lastSeenMs : fallbackLastSeenMs),
|
|
795
|
+
minUsedPercent: snapshot.usedPercent,
|
|
796
|
+
maxUsedPercent: snapshot.usedPercent,
|
|
797
|
+
totals,
|
|
798
|
+
eventCount: totals.eventCount
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function toUtcIso(value) {
|
|
802
|
+
return new Date(value).toISOString().replace(".000Z", "Z");
|
|
803
|
+
}
|
|
804
|
+
function buildUserIdHash(parts) {
|
|
805
|
+
if (parts.some((part) => !part)) {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
return createHash("md5").update(parts.join("-")).digest("hex");
|
|
809
|
+
}
|
|
810
|
+
function normalizeAnalyticsAgentName(label) {
|
|
811
|
+
return label.replace(/\s+/g, "");
|
|
812
|
+
}
|