@juspay/neurolink 11.18.5 → 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
+ });
@@ -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 {};
@@ -487,82 +487,6 @@ export async function buildMessagesArray(options) {
487
487
  else if ("input" in options && options.input?.text) {
488
488
  currentPrompt = options.input.text;
489
489
  }
490
- // Process CSV files if present and inject into prompt using proper CSV parser
491
- if ("input" in options && options.input) {
492
- const input = options.input;
493
- let csvContent = "";
494
- const csvOptions = "csvOptions" in options ? options.csvOptions : undefined;
495
- // Process explicit csvFiles array
496
- if (input.csvFiles && input.csvFiles.length > 0) {
497
- for (let i = 0; i < input.csvFiles.length; i++) {
498
- const csvFile = input.csvFiles[i];
499
- const filename = extractFilename(csvFile, i);
500
- const filePath = typeof csvFile === "string" ? csvFile : filename;
501
- try {
502
- const result = await FileDetector.detectAndProcess(csvFile, {
503
- allowedTypes: ["csv"],
504
- csvOptions: csvOptions,
505
- });
506
- let csvSection = `\n\n## CSV Data from "${filename}":\n`;
507
- // Add metadata from csv-parser library
508
- if (result.metadata) {
509
- const metadataText = formatCSVMetadata(result.metadata);
510
- if (metadataText) {
511
- csvSection += metadataText + `\n\n`;
512
- }
513
- }
514
- // Put the actual CSV content BEFORE the tool instructions —
515
- // buildCSVToolInstructions references "the CSV data shown above"
516
- // and the trailing position keeps that reference accurate.
517
- // Vertex Gemini misreads CSV-only prompts as "no files attached"
518
- // when the NOTE-then-data order makes the reference dangle.
519
- csvSection += result.content;
520
- csvSection += buildCSVToolInstructions(filePath);
521
- csvContent += csvSection;
522
- logger.info(`[CSV] ✅ Processed: ${filename}`, result.metadata);
523
- }
524
- catch (error) {
525
- logger.error(`[CSV] ❌ Failed to process ${filename}:`, error);
526
- csvContent += `\n\n## CSV Data Error: Failed to process "${filename}"\nReason: ${error instanceof Error ? error.message : "Unknown error"}`;
527
- }
528
- }
529
- }
530
- // Process unified files array (auto-detect CSV)
531
- if (input.files && input.files.length > 0) {
532
- for (const file of input.files) {
533
- const filename = extractFilename(file);
534
- try {
535
- const result = await FileDetector.detectAndProcess(file, {
536
- maxSize: 50 * 1024 * 1024,
537
- allowedTypes: ["csv"],
538
- csvOptions: csvOptions,
539
- mimetypeHint: isFileWithMetadata(file) ? file.mimetype : undefined,
540
- });
541
- if (result.type === "csv") {
542
- let csvSection = `\n\n## CSV Data from "${filename}":\n`;
543
- // Add metadata from csv-parser library
544
- if (result.metadata) {
545
- const metadataText = formatCSVMetadata(result.metadata);
546
- if (metadataText) {
547
- csvSection += metadataText + `\n\n`;
548
- }
549
- }
550
- csvSection += result.content;
551
- csvContent += csvSection;
552
- logger.info(`[FileDetector] ✅ CSV: ${filename}`, result.metadata);
553
- }
554
- }
555
- catch (error) {
556
- // Silently skip non-CSV files in auto-detect mode
557
- logger.debug(`[FileDetector] Skipped ${filename}: ${error instanceof Error ? error.message : String(error)}`);
558
- }
559
- }
560
- }
561
- // Prepend CSV content to current prompt
562
- if (csvContent) {
563
- currentPrompt = csvContent + (currentPrompt || "");
564
- }
565
- }
566
490
  if (currentPrompt?.trim()) {
567
491
  messages.push({
568
492
  role: "user",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.18.5",
3
+ "version": "11.19.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -123,6 +123,7 @@
123
123
  "test:music:unit": "npx tsx test/continuous-test-suite-music-unit.ts",
124
124
  "test:image-gen": "npx tsx test/continuous-test-suite-image-gen-extras.ts",
125
125
  "test:credentials": "npx tsx test/continuous-test-suite-credentials.ts",
126
+ "test:local-usage": "npx tsx test/continuous-test-suite-local-usage.ts",
126
127
  "test:dynamic": "npx tsx test/continuous-test-suite-dynamic.ts",
127
128
  "test:proxy": "npx tsx test/continuous-test-suite-proxy.ts",
128
129
  "test:codex": "npx tsx test/continuous-test-suite-codex.ts",