@juspay/neurolink 11.18.4 → 11.19.0

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/index.d.ts CHANGED
@@ -445,3 +445,4 @@ export { TripwireEvaluator, createDefaultTripwireEvaluator, commonTripwires, } f
445
445
  export { Agent } from "./agent/agent.js";
446
446
  export { AgentNetwork } from "./agent/agentNetwork.js";
447
447
  export { AgentCoordinator, TaskDistributor, MessageBus, NetworkOrchestrator, NetworkTopology, TopologyBuilder, } from "./agent/index.js";
448
+ export { createLocalUsageReader, getLocalUsageDescriptors, getRegisteredLocalUsageCliIds, readAllLocalUsage, registerLocalUsageReader, } from "./localUsage/index.js";
package/dist/index.js CHANGED
@@ -745,3 +745,10 @@ AgentCoordinator, TaskDistributor,
745
745
  MessageBus,
746
746
  // Orchestration
747
747
  NetworkOrchestrator, NetworkTopology, TopologyBuilder, } from "./agent/index.js";
748
+ // ============================================================================
749
+ // Local usage exports
750
+ // ============================================================================
751
+ // Token spend read from each CLI's own session logs, rather than from proxy
752
+ // traffic. Exported because the proxy's ledger only sees CLIs that route
753
+ // through it; this covers the rest, and covers history predating the proxy.
754
+ export { createLocalUsageReader, getLocalUsageDescriptors, getRegisteredLocalUsageCliIds, readAllLocalUsage, registerLocalUsageReader, } from "./localUsage/index.js";
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Reads token usage out of Claude Code's own session transcripts.
3
+ *
4
+ * Layout, confirmed on a real machine rather than from documentation:
5
+ * `~/.claude/projects/<sanitized-cwd>/<sessionId>.jsonl`, where the sanitized
6
+ * cwd is the absolute working-directory path with every `/` replaced by `-`.
7
+ *
8
+ * The load-bearing detail is that top-level session transcripts are a tiny
9
+ * minority of the files. On the machine this was written against, 17,439
10
+ * transcripts totalling 9.7 GB broke down as roughly 103 top-level sessions
11
+ * against 17,116 subagent-task transcripts nested under
12
+ * `<sessionId>/subagents/`. A reader that globbed only `<project>/*.jsonl`
13
+ * would miss essentially all of a subagent-heavy user's real spend, so this
14
+ * one recurses.
15
+ *
16
+ * The files are not homogeneous message streams. A single 14,747-line
17
+ * transcript carried eleven distinct `type` values — `assistant`, `user`,
18
+ * `attachment`, `pr-link`, `last-prompt`, `mode`, `permission-mode`,
19
+ * `queue-operation`, `system`, `file-history-delta`, `file-history-snapshot` —
20
+ * several of which have no `message` key at all. Every one of its 6,028
21
+ * `type: "assistant"` lines carried a `message.usage` object, so that filter is
22
+ * safe, but everything else must be skipped rather than treated as malformed.
23
+ */
24
+ import type { LocalUsageReader } from "../types/index.js";
25
+ export declare function createClaudeCodeReader(): Promise<LocalUsageReader>;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Reads token usage out of Claude Code's own session transcripts.
3
+ *
4
+ * Layout, confirmed on a real machine rather than from documentation:
5
+ * `~/.claude/projects/<sanitized-cwd>/<sessionId>.jsonl`, where the sanitized
6
+ * cwd is the absolute working-directory path with every `/` replaced by `-`.
7
+ *
8
+ * The load-bearing detail is that top-level session transcripts are a tiny
9
+ * minority of the files. On the machine this was written against, 17,439
10
+ * transcripts totalling 9.7 GB broke down as roughly 103 top-level sessions
11
+ * against 17,116 subagent-task transcripts nested under
12
+ * `<sessionId>/subagents/`. A reader that globbed only `<project>/*.jsonl`
13
+ * would miss essentially all of a subagent-heavy user's real spend, so this
14
+ * one recurses.
15
+ *
16
+ * The files are not homogeneous message streams. A single 14,747-line
17
+ * transcript carried eleven distinct `type` values — `assistant`, `user`,
18
+ * `attachment`, `pr-link`, `last-prompt`, `mode`, `permission-mode`,
19
+ * `queue-operation`, `system`, `file-history-delta`, `file-history-snapshot` —
20
+ * several of which have no `message` key at all. Every one of its 6,028
21
+ * `type: "assistant"` lines carried a `message.usage` object, so that filter is
22
+ * safe, but everything else must be skipped rather than treated as malformed.
23
+ */
24
+ import { createReadStream } from "fs";
25
+ import { createInterface } from "readline";
26
+ import { readdir, stat } from "fs/promises";
27
+ import { homedir } from "os";
28
+ import { join } from "path";
29
+ import { calculateCost, hasPricing } from "../utils/pricing.js";
30
+ const CLI_ID = "claude-code";
31
+ const DEFAULT_SINCE_DAYS = 30;
32
+ const PROVIDER = "anthropic";
33
+ function projectsRoot() {
34
+ return join(homedir(), ".claude", "projects");
35
+ }
36
+ function emptyTotals() {
37
+ return {
38
+ requests: 0,
39
+ inputTokens: 0,
40
+ outputTokens: 0,
41
+ cacheReadTokens: 0,
42
+ cacheCreationTokens: 0,
43
+ costUsd: 0,
44
+ costConfidence: "modeled",
45
+ unpricedRequests: 0,
46
+ unpricedModels: [],
47
+ };
48
+ }
49
+ /**
50
+ * Every `.jsonl` under the projects root, at any depth.
51
+ *
52
+ * Depth matters here — see the module header. `withFileTypes` avoids a stat
53
+ * per entry, and a directory that cannot be read is skipped rather than
54
+ * aborting the walk: one unreadable project must not cost the other eleven
55
+ * thousand files.
56
+ */
57
+ async function collectTranscripts(dir, out) {
58
+ let entries;
59
+ try {
60
+ entries = await readdir(dir, { withFileTypes: true });
61
+ }
62
+ catch {
63
+ return;
64
+ }
65
+ for (const entry of entries) {
66
+ const full = join(dir, entry.name);
67
+ if (entry.isDirectory()) {
68
+ await collectTranscripts(full, out);
69
+ }
70
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
71
+ out.push(full);
72
+ }
73
+ }
74
+ }
75
+ function num(value) {
76
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
77
+ }
78
+ /**
79
+ * Fold one transcript into `totals`.
80
+ *
81
+ * Read line by line rather than into a string: these files reach tens of
82
+ * megabytes, and a full-history scan opens thousands of them.
83
+ *
84
+ * Dedup is by `message.id`, keeping the LARGEST output count seen for that id.
85
+ * A resumed session re-logs turns it has already written, and the re-logged
86
+ * copy can carry a higher output count than the first — taking the max rather
87
+ * than the first or last is what makes a resumed session total correct instead
88
+ * of either under- or double-counted. The map is per-file and discarded after,
89
+ * so it stays bounded no matter how many files a scan covers.
90
+ */
91
+ async function foldTranscript(filePath, totals, unpriced) {
92
+ const seen = new Map();
93
+ const rl = createInterface({
94
+ input: createReadStream(filePath, { encoding: "utf8" }),
95
+ crlfDelay: Infinity,
96
+ });
97
+ try {
98
+ for await (const line of rl) {
99
+ if (!line || line.charCodeAt(0) !== 123 /* '{' */) {
100
+ continue;
101
+ }
102
+ let parsed;
103
+ try {
104
+ parsed = JSON.parse(line);
105
+ }
106
+ catch {
107
+ // A transcript being appended to while we read it ends in a partial
108
+ // line. That is normal, not corruption — skip it.
109
+ continue;
110
+ }
111
+ const record = parsed;
112
+ if (record.type !== "assistant" || !record.message?.usage) {
113
+ continue;
114
+ }
115
+ const usage = record.message.usage;
116
+ const id = record.message.id;
117
+ if (typeof id !== "string" || id.length === 0) {
118
+ continue;
119
+ }
120
+ const candidate = {
121
+ model: record.message.model ?? "unknown",
122
+ input: num(usage.input_tokens),
123
+ output: num(usage.output_tokens),
124
+ read: num(usage.cache_read_input_tokens),
125
+ create: num(usage.cache_creation_input_tokens),
126
+ };
127
+ const existing = seen.get(id);
128
+ if (!existing || candidate.output > existing.output) {
129
+ seen.set(id, candidate);
130
+ }
131
+ }
132
+ }
133
+ finally {
134
+ rl.close();
135
+ }
136
+ for (const turn of seen.values()) {
137
+ totals.requests += 1;
138
+ totals.inputTokens += turn.input;
139
+ totals.outputTokens += turn.output;
140
+ totals.cacheReadTokens += turn.read;
141
+ totals.cacheCreationTokens += turn.create;
142
+ if (hasPricing(PROVIDER, turn.model)) {
143
+ totals.costUsd += calculateCost(PROVIDER, turn.model, {
144
+ input: turn.input,
145
+ output: turn.output,
146
+ total: turn.input + turn.output,
147
+ cacheReadTokens: turn.read,
148
+ cacheCreationTokens: turn.create,
149
+ });
150
+ }
151
+ else {
152
+ totals.unpricedRequests += 1;
153
+ unpriced.add(turn.model);
154
+ }
155
+ }
156
+ }
157
+ export async function createClaudeCodeReader() {
158
+ return {
159
+ descriptor: {
160
+ id: CLI_ID,
161
+ displayName: "Claude Code",
162
+ verified: true,
163
+ dedupStrategy: "message-id-keep-max",
164
+ costConfidence: "modeled",
165
+ requiresSqlite: false,
166
+ },
167
+ detect: async () => {
168
+ try {
169
+ const info = await stat(projectsRoot());
170
+ return info.isDirectory();
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ },
176
+ scan: async (options) => {
177
+ const totals = emptyTotals();
178
+ const errors = [];
179
+ const unpriced = new Set();
180
+ const files = [];
181
+ await collectTranscripts(projectsRoot(), files);
182
+ const sinceDays = options?.sinceDays ?? DEFAULT_SINCE_DAYS;
183
+ const cutoff = Number.isFinite(sinceDays) && sinceDays > 0
184
+ ? Date.now() - sinceDays * 86_400_000
185
+ : undefined;
186
+ let filesScanned = 0;
187
+ for (const file of files) {
188
+ try {
189
+ if (cutoff !== undefined) {
190
+ const info = await stat(file);
191
+ if (info.mtimeMs < cutoff) {
192
+ continue;
193
+ }
194
+ }
195
+ await foldTranscript(file, totals, unpriced);
196
+ filesScanned += 1;
197
+ }
198
+ catch (error) {
199
+ // One unreadable transcript is a reportable fact, not a reason to
200
+ // lose the totals from every other file in the scan.
201
+ errors.push({
202
+ cliId: CLI_ID,
203
+ filePath: file,
204
+ message: error instanceof Error ? error.message : String(error),
205
+ });
206
+ }
207
+ }
208
+ totals.unpricedModels = [...unpriced].sort();
209
+ // Confidence stays "modeled" even when some turns went unpriced, and the
210
+ // distinction matters. "heuristic" means the number itself was estimated
211
+ // — Kiro's byte-count figure, say. Here every dollar in `costUsd` came
212
+ // from a real rate table; what is missing is turns, not accuracy. That
213
+ // incompleteness is already reported precisely by `unpricedRequests` and
214
+ // `unpricedModels` (on this machine: Claude Code's internal
215
+ // "<synthetic>" model), so downgrading the whole row would overstate the
216
+ // doubt and make a genuinely estimated total indistinguishable from this
217
+ // one.
218
+ totals.costUsd = Math.round(totals.costUsd * 1_000_000) / 1_000_000;
219
+ return { cliId: CLI_ID, totals, filesScanned, errors };
220
+ },
221
+ };
222
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Local usage: token spend read from each CLI's own session logs.
3
+ *
4
+ * The proxy's ledger can only account for traffic that went through it, which
5
+ * caps coverage at the CLIs that expose a base-URL override. Every CLI writes
6
+ * a local transcript regardless, so reading those covers the rest — and covers
7
+ * history from before the proxy existed.
8
+ */
9
+ import type { LocalUsageAggregateReport, LocalUsageScanOptions } from "../types/index.js";
10
+ export { createLocalUsageReader, getLocalUsageDescriptors, getRegisteredLocalUsageCliIds, registerLocalUsageReader, } from "./localUsageReaderRegistry.js";
11
+ /**
12
+ * Scan every registered reader whose CLI is actually present on this machine.
13
+ *
14
+ * "Not installed" and "failed" are reported separately and deliberately: a CLI
15
+ * the user never installed is not an error, and collapsing the two would make
16
+ * a broken reader indistinguishable from an absent one.
17
+ */
18
+ export declare function readAllLocalUsage(options?: LocalUsageScanOptions): Promise<LocalUsageAggregateReport>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Local usage: token spend read from each CLI's own session logs.
3
+ *
4
+ * The proxy's ledger can only account for traffic that went through it, which
5
+ * caps coverage at the CLIs that expose a base-URL override. Every CLI writes
6
+ * a local transcript regardless, so reading those covers the rest — and covers
7
+ * history from before the proxy existed.
8
+ */
9
+ import { createLocalUsageReader, getRegisteredLocalUsageCliIds, } from "./localUsageReaderRegistry.js";
10
+ export { createLocalUsageReader, getLocalUsageDescriptors, getRegisteredLocalUsageCliIds, registerLocalUsageReader, } from "./localUsageReaderRegistry.js";
11
+ /**
12
+ * Scan every registered reader whose CLI is actually present on this machine.
13
+ *
14
+ * "Not installed" and "failed" are reported separately and deliberately: a CLI
15
+ * the user never installed is not an error, and collapsing the two would make
16
+ * a broken reader indistinguishable from an absent one.
17
+ */
18
+ export async function readAllLocalUsage(options) {
19
+ const totals = {};
20
+ const failures = [];
21
+ const notInstalled = [];
22
+ for (const cliId of getRegisteredLocalUsageCliIds()) {
23
+ try {
24
+ const reader = await createLocalUsageReader(cliId);
25
+ if (!(await reader.detect())) {
26
+ notInstalled.push(cliId);
27
+ continue;
28
+ }
29
+ const result = await reader.scan(options);
30
+ totals[cliId] = result.totals;
31
+ }
32
+ catch (error) {
33
+ // One reader throwing must not lose the others' results.
34
+ failures.push({
35
+ cliId,
36
+ message: error instanceof Error ? error.message : String(error),
37
+ });
38
+ }
39
+ }
40
+ return {
41
+ generatedAt: new Date().toISOString(),
42
+ totals,
43
+ failures,
44
+ notInstalled,
45
+ };
46
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Registry of local-usage readers, one per CLI.
3
+ *
4
+ * Mirrors `providerRegistry.ts`: descriptors are static and always available,
5
+ * while the reader itself arrives through a dynamic import inside its factory.
6
+ * Same reason as the provider registry — a static import graph across a dozen
7
+ * readers is how circular dependencies start, and it would also force every
8
+ * reader's cost of loading onto a caller who only wanted one of them.
9
+ */
10
+ import type { LocalUsageCliId, LocalUsageReader, LocalUsageReaderDescriptor, LocalUsageReaderRegistration } from "../types/index.js";
11
+ export declare function registerLocalUsageReader(registration: LocalUsageReaderRegistration): void;
12
+ export declare function getLocalUsageDescriptors(): LocalUsageReaderDescriptor[];
13
+ export declare function getRegisteredLocalUsageCliIds(): LocalUsageCliId[];
14
+ export declare function createLocalUsageReader(cliId: LocalUsageCliId): Promise<LocalUsageReader>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Registry of local-usage readers, one per CLI.
3
+ *
4
+ * Mirrors `providerRegistry.ts`: descriptors are static and always available,
5
+ * while the reader itself arrives through a dynamic import inside its factory.
6
+ * Same reason as the provider registry — a static import graph across a dozen
7
+ * readers is how circular dependencies start, and it would also force every
8
+ * reader's cost of loading onto a caller who only wanted one of them.
9
+ */
10
+ const registry = new Map();
11
+ export function registerLocalUsageReader(registration) {
12
+ registry.set(registration.descriptor.id, registration);
13
+ }
14
+ export function getLocalUsageDescriptors() {
15
+ return [...registry.values()].map((entry) => entry.descriptor);
16
+ }
17
+ export function getRegisteredLocalUsageCliIds() {
18
+ return [...registry.keys()];
19
+ }
20
+ export async function createLocalUsageReader(cliId) {
21
+ const registration = registry.get(cliId);
22
+ if (!registration) {
23
+ throw new Error(`No local-usage reader registered for "${cliId}". Registered: ${getRegisteredLocalUsageCliIds().join(", ") || "(none)"}`);
24
+ }
25
+ return registration.factory();
26
+ }
27
+ registerLocalUsageReader({
28
+ descriptor: {
29
+ id: "claude-code",
30
+ displayName: "Claude Code",
31
+ verified: true,
32
+ dedupStrategy: "message-id-keep-max",
33
+ costConfidence: "modeled",
34
+ requiresSqlite: false,
35
+ },
36
+ factory: async () => {
37
+ const { createClaudeCodeReader } = await import("./claudeCodeReader.js");
38
+ return createClaudeCodeReader();
39
+ },
40
+ });
@@ -20,6 +20,14 @@
20
20
  */
21
21
  /** Google's role for assistant turns. */
22
22
  const MODEL_ROLE = "model";
23
+ /**
24
+ * Sent as `input.text` when a request's final turn is a model turn.
25
+ *
26
+ * Google lets a client continue generation from the assistant's own last turn;
27
+ * the chat-completions wire format the engine targets has no way to say that,
28
+ * and an empty prompt is rejected before any provider is reached.
29
+ */
30
+ const CONTINUATION_PROMPT = "Continue.";
23
31
  function partsToText(parts) {
24
32
  if (!Array.isArray(parts)) {
25
33
  return "";
@@ -91,11 +99,24 @@ export function parseGeminiRequest(model, body, stream) {
91
99
  // it, which is the same lost-turn bug one case further along.
92
100
  //
93
101
  // A terminal placeholder restores the invariant: the slice removes this
94
- // instead of the model turn. It is never sent anywhere — `prompt` is
95
- // independently "" in exactly this case, so the placeholder only exists to be
96
- // consumed by the slice.
102
+ // instead of the model turn.
103
+ //
104
+ // The prompt needs its own answer, and leaving it "" was a 500. A
105
+ // model-final request carries no user turn to send as `input.text`, and
106
+ // NeuroLink's stream() rejects an empty one outright — "Stream options must
107
+ // include either input.text, input.audio, or stt.audio" — so every continue
108
+ // from a model turn failed at the door rather than reaching a provider. The
109
+ // placeholder above fixed the history slice but never exercised this path,
110
+ // because nothing had sent the request.
111
+ //
112
+ // Google's semantics for a model-final `contents` are "keep going", and the
113
+ // chat-completions shape the engine translates into has no assistant-prefill
114
+ // to express that. An explicit continuation instruction is the closest
115
+ // faithful equivalent: the full conversation still arrives as history, and
116
+ // the model is told to continue it rather than being handed an empty turn.
97
117
  if (turns.length > 0 && turns[turns.length - 1].role !== "user") {
98
- conversationMessages.push({ role: "user", content: "" });
118
+ conversationMessages.push({ role: "user", content: CONTINUATION_PROMPT });
119
+ prompt = CONTINUATION_PROMPT;
99
120
  }
100
121
  const numeric = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
101
122
  const stops = generationConfig.stopSequences;
@@ -84,3 +84,4 @@ export * from "./modelPool.js";
84
84
  export * from "./requestRouter.js";
85
85
  export * from "./classifierRouter.js";
86
86
  export * from "./agentNetwork.js";
87
+ export * from "./localUsage.js";
@@ -95,3 +95,4 @@ export * from "./requestRouter.js";
95
95
  export * from "./classifierRouter.js";
96
96
  // Multi-Agent orchestration types
97
97
  export * from "./agentNetwork.js";
98
+ export * from "./localUsage.js";
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Types for `src/lib/localUsage/` — reading token usage out of each AI CLI's
3
+ * own local session logs.
4
+ *
5
+ * Why this exists alongside the proxy's ledger: the proxy can only account for
6
+ * traffic that went through it, which caps coverage at the CLIs that expose a
7
+ * base-URL override. Every CLI writes its own local transcript regardless, so
8
+ * reading those recovers the rest — and recovers history from before the proxy
9
+ * was ever installed. It is also an independent source of truth: a pricing
10
+ * defect in the proxy's own accounting cannot hide from a reader that derives
11
+ * cost from a different input.
12
+ */
13
+ /**
14
+ * Stable identifier for one CLI this subsystem can read local usage from.
15
+ * Kebab-case, matching `CliProxyClientConfigurator.id`'s convention — a
16
+ * different registry, but the same repo-wide convention for CLI identifiers.
17
+ */
18
+ export type LocalUsageCliId = "claude-code" | "codex" | "gemini-cli" | "opencode" | "qwen-code" | "copilot-cli" | "cursor" | "amp" | "hermes" | "kiro" | "antigravity" | "grok";
19
+ /**
20
+ * How much to trust a computed cost figure.
21
+ *
22
+ * Not decoration: some CLIs are flat-rate subscriptions where a per-request
23
+ * cost is meaningless, and at least one publishes a byte heuristic rather than
24
+ * a real number. A caller must never render "heuristic" with the same
25
+ * confidence as "modeled", so the distinction travels with the number.
26
+ */
27
+ export type LocalUsageCostConfidence = "modeled" | "unavailable" | "heuristic";
28
+ /**
29
+ * How a reader avoids counting the same turn twice.
30
+ *
31
+ * Metadata on the descriptor, for introspection and for the person writing the
32
+ * next reader — the aggregator does not branch on it.
33
+ */
34
+ export type LocalUsageDedupStrategy = "message-id-keep-max" | "last-write-wins" | "rowid-high-water-mark" | "session-dag";
35
+ /** Aggregated totals for one CLI, one scan. */
36
+ export type LocalUsageTotals = {
37
+ requests: number;
38
+ inputTokens: number;
39
+ outputTokens: number;
40
+ cacheReadTokens: number;
41
+ cacheCreationTokens: number;
42
+ costUsd: number;
43
+ /**
44
+ * The weakest confidence contributing to `costUsd`. A totals row mixing
45
+ * modeled and heuristic entries must report the weaker one, otherwise the
46
+ * aggregate looks better-sourced than its worst input.
47
+ */
48
+ costConfidence: LocalUsageCostConfidence;
49
+ /** Turns whose model had no pricing entry, so contributed 0 to costUsd. */
50
+ unpricedRequests: number;
51
+ /** Distinct model ids behind `unpricedRequests`, for diagnosis. */
52
+ unpricedModels: string[];
53
+ };
54
+ /** A non-fatal per-file problem, surfaced instead of aborting the scan. */
55
+ export type LocalUsageScanError = {
56
+ cliId: LocalUsageCliId;
57
+ filePath: string;
58
+ message: string;
59
+ };
60
+ /** What one reader's `scan()` returns. */
61
+ export type LocalUsageScanResult = {
62
+ cliId: LocalUsageCliId;
63
+ totals: LocalUsageTotals;
64
+ /** Files opened during this scan, after any time filter. */
65
+ filesScanned: number;
66
+ errors: LocalUsageScanError[];
67
+ };
68
+ /** Static metadata, available without constructing a reader. */
69
+ export type LocalUsageReaderDescriptor = {
70
+ id: LocalUsageCliId;
71
+ displayName: string;
72
+ /**
73
+ * True only for readers checked against real data on a real machine. An
74
+ * honesty marker, not a completeness claim — an unverified reader may still
75
+ * be correct, it just has not been shown to be.
76
+ */
77
+ verified: boolean;
78
+ dedupStrategy: LocalUsageDedupStrategy;
79
+ costConfidence: LocalUsageCostConfidence;
80
+ /** Whether reading this CLI's store needs a SQLite binding. */
81
+ requiresSqlite: boolean;
82
+ };
83
+ /** Options accepted by every reader's `scan()` and by the aggregator. */
84
+ export type LocalUsageScanOptions = {
85
+ /**
86
+ * Only read files modified within this many days. Defaults to 30.
87
+ *
88
+ * This is a real constraint rather than a convenience: one developer machine
89
+ * held 17,439 transcripts totalling 9.7 GB, and an unbounded scan reads all
90
+ * of it on every call. Pass `Infinity` for a deliberate full history sweep.
91
+ */
92
+ sinceDays?: number;
93
+ };
94
+ /** The contract every reader implements — one per CLI. */
95
+ export type LocalUsageReader = {
96
+ descriptor: LocalUsageReaderDescriptor;
97
+ /**
98
+ * Whether this CLI's local store appears to exist on this machine at all —
99
+ * the same "do not report on something never installed" discipline the proxy
100
+ * client configurators use before writing a config.
101
+ */
102
+ detect: () => Promise<boolean>;
103
+ scan: (options?: LocalUsageScanOptions) => Promise<LocalUsageScanResult>;
104
+ };
105
+ /** Async factory stored in the registry — a reader needs no credentials, only
106
+ * the filesystem, so this takes no arguments. */
107
+ export type LocalUsageReaderFactoryFn = () => Promise<LocalUsageReader>;
108
+ /** One entry in the registry map. */
109
+ export type LocalUsageReaderRegistration = {
110
+ descriptor: LocalUsageReaderDescriptor;
111
+ factory: LocalUsageReaderFactoryFn;
112
+ };
113
+ /** A whole reader failing — not installed, or threw — so the aggregate report
114
+ * can carry successes and failures side by side rather than losing both. */
115
+ export type LocalUsageReaderFailure = {
116
+ cliId: LocalUsageCliId;
117
+ message: string;
118
+ };
119
+ /** Top-level output of scanning every registered, detected reader. */
120
+ export type LocalUsageAggregateReport = {
121
+ generatedAt: string;
122
+ /** Only CLIs whose store was detected AND scanned appear here. */
123
+ totals: Partial<Record<LocalUsageCliId, LocalUsageTotals>>;
124
+ /** CLIs that were registered but produced nothing, and why. */
125
+ failures: LocalUsageReaderFailure[];
126
+ /** CLIs with no local store on this machine — absent, not failed. */
127
+ notInstalled: LocalUsageCliId[];
128
+ };
129
+ /**
130
+ * The `message.usage` object exactly as Claude Code writes it into a
131
+ * transcript line — snake_case, and every field optional because older
132
+ * transcripts predate some of them.
133
+ */
134
+ export type LocalUsageClaudeRawUsage = {
135
+ input_tokens?: number;
136
+ output_tokens?: number;
137
+ cache_read_input_tokens?: number;
138
+ cache_creation_input_tokens?: number;
139
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Types for `src/lib/localUsage/` — reading token usage out of each AI CLI's
3
+ * own local session logs.
4
+ *
5
+ * Why this exists alongside the proxy's ledger: the proxy can only account for
6
+ * traffic that went through it, which caps coverage at the CLIs that expose a
7
+ * base-URL override. Every CLI writes its own local transcript regardless, so
8
+ * reading those recovers the rest — and recovers history from before the proxy
9
+ * was ever installed. It is also an independent source of truth: a pricing
10
+ * defect in the proxy's own accounting cannot hide from a reader that derives
11
+ * cost from a different input.
12
+ */
13
+ export {};