@thehammer/danx-session-telemetry 0.1.4 → 0.1.8
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/browser.d.ts +6 -3
- package/dist/browser.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/pricing.d.ts +64 -0
- package/dist/pricing.js +107 -0
- package/dist/session-log-watcher.d.ts +13 -0
- package/dist/session-log-watcher.js +28 -4
- package/dist/usage-ledger.d.ts +129 -0
- package/dist/usage-ledger.js +276 -0
- package/package.json +3 -1
package/dist/browser.d.ts
CHANGED
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
* dashboard SPA, a future browser-side usage display) can depend on this
|
|
15
15
|
* subpath with zero risk of pulling in a Node builtin.
|
|
16
16
|
*
|
|
17
|
-
* DX-2963 adds the first real runtime
|
|
18
|
-
* and the pricing function, both pure
|
|
19
|
-
*
|
|
17
|
+
* DX-2963 adds the first real runtime exports here: the one usage counter
|
|
18
|
+
* (`usage-ledger.ts`) and the one pricing function (`pricing.ts`), both pure
|
|
19
|
+
* arithmetic with no I/O — `usage-snapshot.ts` in danxbot root (bundled by the
|
|
20
|
+
* frozen Vue SPA) imports through THIS subpath, never the main entry.
|
|
20
21
|
*/
|
|
21
22
|
export type { AgentLogEntry, AgentLineage } from "./types.js";
|
|
22
23
|
export { truncStr } from "./tool-summary.js";
|
|
24
|
+
export * from "./usage-ledger.js";
|
|
25
|
+
export * from "./pricing.js";
|
package/dist/browser.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
// `truncStr` is pure string arithmetic (no `node:` imports, no I/O) — safe
|
|
2
2
|
// to carry on the browser subpath alongside the two types above.
|
|
3
3
|
export { truncStr } from "./tool-summary.js";
|
|
4
|
+
// DX-2963 — counter + pricing (pure arithmetic, no `node:` imports).
|
|
5
|
+
export * from "./usage-ledger.js";
|
|
6
|
+
export * from "./pricing.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -18,3 +18,5 @@ export { SessionLogWatcher, DISPATCH_TAG_PREFIX, encodeClaudeProjectsCwd, derive
|
|
|
18
18
|
export type { EntryConsumer, SessionLogWatcherOptions, } from "./session-log-watcher.js";
|
|
19
19
|
export type { AgentLogEntry, AgentLineage } from "./types.js";
|
|
20
20
|
export { truncStr, summarizeToolInput, buildAssistantSummary, buildToolResultSummary, } from "./tool-summary.js";
|
|
21
|
+
export * from "./usage-ledger.js";
|
|
22
|
+
export * from "./pricing.js";
|
package/dist/index.js
CHANGED
|
@@ -21,3 +21,7 @@ export { SessionLogWatcher, DISPATCH_TAG_PREFIX, encodeClaudeProjectsCwd, derive
|
|
|
21
21
|
// truncate a string for display but have nothing else to do with the
|
|
22
22
|
// transcript collector, e.g. `src/dashboard/dispatch-scout.ts`.
|
|
23
23
|
export { truncStr, summarizeToolInput, buildAssistantSummary, buildToolResultSummary, } from "./tool-summary.js";
|
|
24
|
+
// DX-2963 — the one usage counter and the one pricing function (also on the
|
|
25
|
+
// browser-safe `./browser` subpath; both are pure arithmetic).
|
|
26
|
+
export * from "./usage-ledger.js";
|
|
27
|
+
export * from "./pricing.js";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2963 — the ONE pricing function, shared by every consumer that turns
|
|
3
|
+
* token counts into dollars: the danxbot server's usage-ledger writer (which
|
|
4
|
+
* prices each credit once, when it is credited), the router's Haiku calls and
|
|
5
|
+
* the skill-eval cost report. Moved here from danxbot root `src/agent/pricing.ts`
|
|
6
|
+
* so the counter and the price list live side by side, and exported through
|
|
7
|
+
* the browser-safe `./browser` subpath (pure arithmetic, no `node:` imports).
|
|
8
|
+
*
|
|
9
|
+
* Source of every row: Anthropic's official pricing page,
|
|
10
|
+
* https://platform.claude.com/docs/en/about-claude/pricing — read 2026-09-23
|
|
11
|
+
* (DX-2963 re-checked every row against it; the previous list had drifted:
|
|
12
|
+
* Sonnet 5 at $3/$15 and Haiku 4.5 at $0.80/$4 where the page says $2/$10 and
|
|
13
|
+
* $1/$5, and Opus 5 / Fable 5.1 / Opus 5.5 were missing).
|
|
14
|
+
*
|
|
15
|
+
* DX-2963 — cache writes are TWO prices, never one: a 5-minute write is
|
|
16
|
+
* 1.25x base input and a 1-hour write is 2x base input. Claude Code reports
|
|
17
|
+
* each lifetime separately (`usage.cache_creation.ephemeral_5m_input_tokens` /
|
|
18
|
+
* `ephemeral_1h_input_tokens`), and both are material (top-level transcripts
|
|
19
|
+
* write almost only 1-hour, sub-agent transcripts mostly 5-minute), so each
|
|
20
|
+
* field is priced with its own price.
|
|
21
|
+
*/
|
|
22
|
+
/** Price per MILLION tokens, USD. */
|
|
23
|
+
export interface ModelPrice {
|
|
24
|
+
input: number;
|
|
25
|
+
output: number;
|
|
26
|
+
cacheWrite5m: number;
|
|
27
|
+
cacheWrite1h: number;
|
|
28
|
+
cacheRead: number;
|
|
29
|
+
}
|
|
30
|
+
/** Token counts for one priced unit (a ledger credit, a model bucket, a call). */
|
|
31
|
+
export interface PricedTokens {
|
|
32
|
+
input: number;
|
|
33
|
+
output: number;
|
|
34
|
+
cacheRead: number;
|
|
35
|
+
cacheWrite5m: number;
|
|
36
|
+
cacheWrite1h: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Claude Code's literal model tag on an API-error entry (rate limit, context
|
|
40
|
+
* overflow). Its tokens count in a dispatch's flat totals but it is never a
|
|
41
|
+
* real priced model, so it never enters `usage_by_model` or cost.
|
|
42
|
+
*/
|
|
43
|
+
export declare const SYNTHETIC_MODEL = "<synthetic>";
|
|
44
|
+
/** The `usage.speed` value a fast-mode reply carries. */
|
|
45
|
+
export declare const FAST_SPEED = "fast";
|
|
46
|
+
export type PriceResult = {
|
|
47
|
+
kind: "priced";
|
|
48
|
+
costUsd: number;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "unpriced";
|
|
51
|
+
model: string;
|
|
52
|
+
speed: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Price one unit of usage. NEVER throws (DX-2963 replaces DX-1720's throw):
|
|
56
|
+
* an unknown model — or a fast reply on a model with no fast price — returns
|
|
57
|
+
* `{kind: "unpriced"}` so the caller counts the tokens, leaves the cost out
|
|
58
|
+
* and records a visible `unpriced_model` event, instead of one new model id
|
|
59
|
+
* failing every write (the live tick used to fail every 30 seconds on it).
|
|
60
|
+
* `<synthetic>` is never passed here: callers exclude it before pricing.
|
|
61
|
+
*/
|
|
62
|
+
export declare function priceUsage(model: string, speed: string, tokens: PricedTokens): PriceResult;
|
|
63
|
+
/** True when `model` has a standard-speed price (used by tests + guards). */
|
|
64
|
+
export declare function isPricedModel(model: string): boolean;
|
package/dist/pricing.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2963 — the ONE pricing function, shared by every consumer that turns
|
|
3
|
+
* token counts into dollars: the danxbot server's usage-ledger writer (which
|
|
4
|
+
* prices each credit once, when it is credited), the router's Haiku calls and
|
|
5
|
+
* the skill-eval cost report. Moved here from danxbot root `src/agent/pricing.ts`
|
|
6
|
+
* so the counter and the price list live side by side, and exported through
|
|
7
|
+
* the browser-safe `./browser` subpath (pure arithmetic, no `node:` imports).
|
|
8
|
+
*
|
|
9
|
+
* Source of every row: Anthropic's official pricing page,
|
|
10
|
+
* https://platform.claude.com/docs/en/about-claude/pricing — read 2026-09-23
|
|
11
|
+
* (DX-2963 re-checked every row against it; the previous list had drifted:
|
|
12
|
+
* Sonnet 5 at $3/$15 and Haiku 4.5 at $0.80/$4 where the page says $2/$10 and
|
|
13
|
+
* $1/$5, and Opus 5 / Fable 5.1 / Opus 5.5 were missing).
|
|
14
|
+
*
|
|
15
|
+
* DX-2963 — cache writes are TWO prices, never one: a 5-minute write is
|
|
16
|
+
* 1.25x base input and a 1-hour write is 2x base input. Claude Code reports
|
|
17
|
+
* each lifetime separately (`usage.cache_creation.ephemeral_5m_input_tokens` /
|
|
18
|
+
* `ephemeral_1h_input_tokens`), and both are material (top-level transcripts
|
|
19
|
+
* write almost only 1-hour, sub-agent transcripts mostly 5-minute), so each
|
|
20
|
+
* field is priced with its own price.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Claude Code's literal model tag on an API-error entry (rate limit, context
|
|
24
|
+
* overflow). Its tokens count in a dispatch's flat totals but it is never a
|
|
25
|
+
* real priced model, so it never enters `usage_by_model` or cost.
|
|
26
|
+
*/
|
|
27
|
+
export const SYNTHETIC_MODEL = "<synthetic>";
|
|
28
|
+
/** The `usage.speed` value a fast-mode reply carries. */
|
|
29
|
+
export const FAST_SPEED = "fast";
|
|
30
|
+
/**
|
|
31
|
+
* Build a row from base input/output with the page's caching multipliers
|
|
32
|
+
* (5m write 1.25x, 1h write 2x) and an explicit cache-hit price — the hit
|
|
33
|
+
* multiplier differs per model (0.1x standard, 0.025x Fable 5.1, 0.05x
|
|
34
|
+
* Opus 5.5), so it is stated per row rather than derived.
|
|
35
|
+
*/
|
|
36
|
+
function price(input, output, cacheRead) {
|
|
37
|
+
return {
|
|
38
|
+
input,
|
|
39
|
+
output,
|
|
40
|
+
cacheWrite5m: input * 1.25,
|
|
41
|
+
cacheWrite1h: input * 2,
|
|
42
|
+
cacheRead,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Standard-speed prices, keyed by the model id Claude Code writes in the JSONL. */
|
|
46
|
+
const PRICING = {
|
|
47
|
+
// Claude Fable 5.1 — cache hits at 0.025x input ($0.25).
|
|
48
|
+
"claude-fable-5-1": price(10, 50, 0.25),
|
|
49
|
+
"claude-fable-5": price(10, 50, 1),
|
|
50
|
+
// Claude Opus 5.5 — cache hits at 0.05x input ($0.20).
|
|
51
|
+
"claude-opus-5-5": price(4, 20, 0.2),
|
|
52
|
+
"claude-opus-5": price(5, 25, 0.5),
|
|
53
|
+
"claude-opus-4-8": price(5, 25, 0.5),
|
|
54
|
+
"claude-opus-4-6": price(5, 25, 0.5),
|
|
55
|
+
"claude-opus-4-6-20250916": price(5, 25, 0.5),
|
|
56
|
+
"claude-opus-4-20250514": price(15, 75, 1.5),
|
|
57
|
+
"claude-sonnet-5": price(2, 10, 0.2),
|
|
58
|
+
"claude-sonnet-4-6": price(3, 15, 0.3),
|
|
59
|
+
"claude-sonnet-4-5-20250929": price(3, 15, 0.3),
|
|
60
|
+
"claude-sonnet-4-20250514": price(3, 15, 0.3),
|
|
61
|
+
"claude-haiku-4-5": price(1, 5, 0.1),
|
|
62
|
+
"claude-haiku-4-5-20251001": price(1, 5, 0.1),
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Fast-mode prices (the page's "Fast mode pricing" section): premium
|
|
66
|
+
* input/output with the prompt-caching multipliers applied ON TOP (so a 5m
|
|
67
|
+
* write is 1.25x and a 1h write 2x of the fast input price, a hit 0.1x — the
|
|
68
|
+
* page states Opus 5.5's own 0.05x hit multiplier only for standard speed and
|
|
69
|
+
* says caching multipliers stack, so its fast hit is 0.05x of $8).
|
|
70
|
+
*
|
|
71
|
+
* DX-2963: the card names Opus 5 and Opus 4.8; the page re-read on
|
|
72
|
+
* 2026-09-23 also lists Opus 5.5 ($8 / $40), so it is priced too. A fast
|
|
73
|
+
* reply on any model NOT in this table is unpriced (tokens count, cost left
|
|
74
|
+
* out, a visible `unpriced_model` event) — never priced at the standard rate.
|
|
75
|
+
*/
|
|
76
|
+
const FAST_PRICING = {
|
|
77
|
+
"claude-opus-5-5": price(8, 40, 0.4),
|
|
78
|
+
"claude-opus-5": price(10, 50, 1),
|
|
79
|
+
"claude-opus-4-8": price(10, 50, 1),
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Price one unit of usage. NEVER throws (DX-2963 replaces DX-1720's throw):
|
|
83
|
+
* an unknown model — or a fast reply on a model with no fast price — returns
|
|
84
|
+
* `{kind: "unpriced"}` so the caller counts the tokens, leaves the cost out
|
|
85
|
+
* and records a visible `unpriced_model` event, instead of one new model id
|
|
86
|
+
* failing every write (the live tick used to fail every 30 seconds on it).
|
|
87
|
+
* `<synthetic>` is never passed here: callers exclude it before pricing.
|
|
88
|
+
*/
|
|
89
|
+
export function priceUsage(model, speed, tokens) {
|
|
90
|
+
const table = speed === FAST_SPEED ? FAST_PRICING : PRICING;
|
|
91
|
+
const p = table[model];
|
|
92
|
+
if (!p)
|
|
93
|
+
return { kind: "unpriced", model, speed };
|
|
94
|
+
return {
|
|
95
|
+
kind: "priced",
|
|
96
|
+
costUsd: (tokens.input * p.input +
|
|
97
|
+
tokens.output * p.output +
|
|
98
|
+
tokens.cacheRead * p.cacheRead +
|
|
99
|
+
tokens.cacheWrite5m * p.cacheWrite5m +
|
|
100
|
+
tokens.cacheWrite1h * p.cacheWrite1h) /
|
|
101
|
+
1_000_000,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** True when `model` has a standard-speed price (used by tests + guards). */
|
|
105
|
+
export function isPricedModel(model) {
|
|
106
|
+
return model in PRICING;
|
|
107
|
+
}
|
|
@@ -61,6 +61,17 @@ export interface SessionLogWatcherOptions {
|
|
|
61
61
|
* EOF is 0, so it ingests its own full file from the start.
|
|
62
62
|
*/
|
|
63
63
|
fromEof?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* DX-2963 — the end-of-file rule for PRE-CARD lineage. When set, every
|
|
66
|
+
* sub-agent file that ALREADY EXISTS on the first sub-agent discovery pass
|
|
67
|
+
* is opened at its end (like the main file under `fromEof`); a sub-agent
|
|
68
|
+
* file that appears later is read from byte 0. Set by the worker when its
|
|
69
|
+
* own dispatch row, or its predecessor's, is pre-card (stored totals with
|
|
70
|
+
* no ledger rows to dedupe against), so replies those totals already count
|
|
71
|
+
* are never credited again. DX-2954 replaces this with its cursor start
|
|
72
|
+
* order and deletes it.
|
|
73
|
+
*/
|
|
74
|
+
existingSubagentFilesFromEof?: boolean;
|
|
64
75
|
}
|
|
65
76
|
/**
|
|
66
77
|
* Encode a working directory to the subdir name Claude Code writes JSONL
|
|
@@ -186,6 +197,8 @@ export declare class SessionLogWatcher {
|
|
|
186
197
|
private dispatchId;
|
|
187
198
|
private pollIntervalMs;
|
|
188
199
|
private fromEof;
|
|
200
|
+
private existingSubagentFilesFromEof;
|
|
201
|
+
private subagentDiscoveryDone;
|
|
189
202
|
private running;
|
|
190
203
|
private polling;
|
|
191
204
|
private initSynthesized;
|
|
@@ -424,6 +424,8 @@ export class SessionLogWatcher {
|
|
|
424
424
|
dispatchId;
|
|
425
425
|
pollIntervalMs;
|
|
426
426
|
fromEof;
|
|
427
|
+
existingSubagentFilesFromEof;
|
|
428
|
+
subagentDiscoveryDone = false;
|
|
427
429
|
running = false;
|
|
428
430
|
polling = false;
|
|
429
431
|
// DX-2962: entries are NOT accumulated here — see `emitEntry`. Only
|
|
@@ -439,6 +441,8 @@ export class SessionLogWatcher {
|
|
|
439
441
|
this.dispatchId = options.dispatchId;
|
|
440
442
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
441
443
|
this.fromEof = options.fromEof ?? false;
|
|
444
|
+
this.existingSubagentFilesFromEof =
|
|
445
|
+
options.existingSubagentFilesFromEof ?? false;
|
|
442
446
|
}
|
|
443
447
|
/** Register a consumer callback that receives each new AgentLogEntry. */
|
|
444
448
|
onEntry(consumer) {
|
|
@@ -577,6 +581,7 @@ export class SessionLogWatcher {
|
|
|
577
581
|
filePath,
|
|
578
582
|
byteOffset,
|
|
579
583
|
lineBuffer: "",
|
|
584
|
+
lineBufferStartOffset: byteOffset,
|
|
580
585
|
lastTimestamp: 0,
|
|
581
586
|
};
|
|
582
587
|
this.subagentsDir = join(dirname(filePath), basename(filePath, ".jsonl"), "subagents");
|
|
@@ -680,6 +685,10 @@ export class SessionLogWatcher {
|
|
|
680
685
|
log.error("Failed to enumerate sub-agent directory:", err);
|
|
681
686
|
return;
|
|
682
687
|
}
|
|
688
|
+
// DX-2963 — the FIRST discovery pass sees exactly the sub-agent files
|
|
689
|
+
// that existed when this watcher attached; only those may open at EOF.
|
|
690
|
+
const openExistingAtEof = this.existingSubagentFilesFromEof && !this.subagentDiscoveryDone;
|
|
691
|
+
this.subagentDiscoveryDone = true;
|
|
683
692
|
for (const file of files) {
|
|
684
693
|
if (!file.endsWith(".jsonl"))
|
|
685
694
|
continue;
|
|
@@ -688,9 +697,19 @@ export class SessionLogWatcher {
|
|
|
688
697
|
continue;
|
|
689
698
|
const subagentId = basename(file, ".jsonl");
|
|
690
699
|
const agentType = await this.readAgentType(subagentId);
|
|
700
|
+
let startOffset = 0;
|
|
701
|
+
if (openExistingAtEof) {
|
|
702
|
+
try {
|
|
703
|
+
startOffset = (await stat(fullPath)).size;
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
log.warn(`existingSubagentFilesFromEof: stat failed for ${fullPath}; tailing from 0: ${String(err)}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
691
709
|
this.subagentTails.set(fullPath, {
|
|
692
710
|
filePath: fullPath,
|
|
693
|
-
byteOffset:
|
|
711
|
+
byteOffset: startOffset,
|
|
712
|
+
lineBufferStartOffset: startOffset,
|
|
694
713
|
lineBuffer: "",
|
|
695
714
|
lastTimestamp: 0,
|
|
696
715
|
lineage: {
|
|
@@ -759,13 +778,16 @@ export class SessionLogWatcher {
|
|
|
759
778
|
state.lineBuffer += text;
|
|
760
779
|
let newlineIdx;
|
|
761
780
|
while ((newlineIdx = state.lineBuffer.indexOf("\n")) !== -1) {
|
|
762
|
-
const
|
|
781
|
+
const rawLine = state.lineBuffer.substring(0, newlineIdx);
|
|
782
|
+
const lineStart = state.lineBufferStartOffset;
|
|
783
|
+
state.lineBufferStartOffset += Buffer.byteLength(rawLine, "utf-8") + 1;
|
|
784
|
+
const line = rawLine.trim();
|
|
763
785
|
state.lineBuffer = state.lineBuffer.substring(newlineIdx + 1);
|
|
764
786
|
if (!line)
|
|
765
787
|
continue;
|
|
766
788
|
try {
|
|
767
789
|
const raw = JSON.parse(line);
|
|
768
|
-
this.handleRawEntry(raw, state);
|
|
790
|
+
this.handleRawEntry(raw, state, lineStart);
|
|
769
791
|
}
|
|
770
792
|
catch {
|
|
771
793
|
log.warn(`Skipping malformed JSONL line: ${line.substring(0, 100)}`);
|
|
@@ -795,7 +817,7 @@ export class SessionLogWatcher {
|
|
|
795
817
|
* skill/agent set. The pre-launch probe in `mcp-server-probe.ts` is the
|
|
796
818
|
* tool-registration check that works across both runtimes.
|
|
797
819
|
*/
|
|
798
|
-
handleRawEntry(raw, state) {
|
|
820
|
+
handleRawEntry(raw, state, lineStart) {
|
|
799
821
|
const isSubagent = state.lineage !== undefined;
|
|
800
822
|
if (!isSubagent && !this.initSynthesized && raw.type === "assistant") {
|
|
801
823
|
const message = raw.message;
|
|
@@ -835,6 +857,8 @@ export class SessionLogWatcher {
|
|
|
835
857
|
this.parentSessionId = sid;
|
|
836
858
|
}
|
|
837
859
|
state.lastTimestamp = result.timestamp;
|
|
860
|
+
// DX-2963 — the line's own byte offset (see `TailState.lineBufferStartOffset`).
|
|
861
|
+
result.entry.data = { ...result.entry.data, byte_offset: lineStart };
|
|
838
862
|
this.emitEntry(result.entry, state);
|
|
839
863
|
}
|
|
840
864
|
/**
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DX-2963 — the ONE usage counter. Turns Claude Code transcript lines into
|
|
3
|
+
* per-reply usage-ledger entries; every counter in danxbot (the worker's
|
|
4
|
+
* accumulator, the skill-eval probe, the stall sweep's productivity signal,
|
|
5
|
+
* the cross-consumer tests) uses it instead of its own summing.
|
|
6
|
+
*
|
|
7
|
+
* The rule it implements, measured on real transcripts (DX-2963 card,
|
|
8
|
+
* 2026-09-18): Claude Code writes one assistant reply as SEVERAL lines that
|
|
9
|
+
* share a `message.id`, and the later lines carry the real (larger) usage —
|
|
10
|
+
* the last line held the maximum in 28,121 of 28,124 multi-line replies. A
|
|
11
|
+
* reply's usage is therefore the per-FIELD MAXIMUM over its lines, never its
|
|
12
|
+
* first line (the old rule every counter shared, which undercounted output
|
|
13
|
+
* 3.17x on sub-agent transcripts) and never a sum (which over-counts).
|
|
14
|
+
* Far-later repeats of a `message.id` are replays of re-written history and
|
|
15
|
+
* never raise usage, so a max-based counter credits them nothing for free.
|
|
16
|
+
*
|
|
17
|
+
* Pure arithmetic — no `node:` imports — so it is exported through the
|
|
18
|
+
* browser-safe `./browser` subpath as well as the main entry.
|
|
19
|
+
*/
|
|
20
|
+
import type { PricedTokens } from "./pricing.js";
|
|
21
|
+
import type { AgentLogEntry } from "./types.js";
|
|
22
|
+
/** Per-reply usage, both cache-write lifetimes kept apart (priced apart). */
|
|
23
|
+
export type LedgerUsage = PricedTokens;
|
|
24
|
+
/**
|
|
25
|
+
* One reply's ledger entry as the collector reports it: the per-field
|
|
26
|
+
* MAXIMUM usage seen over the reply's lines so far, plus every `tool_use`
|
|
27
|
+
* id seen in it.
|
|
28
|
+
*/
|
|
29
|
+
export interface LedgerEntry {
|
|
30
|
+
/** `message.id`, or the synthetic `nomsg:<source>:<byteOffset>` key. */
|
|
31
|
+
messageId: string;
|
|
32
|
+
/** `"main"` for the dispatch's own transcript, else the sub-agent id. */
|
|
33
|
+
source: string;
|
|
34
|
+
/** First non-empty `message.model` seen for the reply; `null` if none. */
|
|
35
|
+
model: string | null;
|
|
36
|
+
/** `usage.speed` (`standard` / `fast`); `"standard"` when the line has none. */
|
|
37
|
+
speed: string;
|
|
38
|
+
usage: LedgerUsage;
|
|
39
|
+
/** The reply's own transcript timestamp (its FIRST line's), epoch ms. */
|
|
40
|
+
repliedAt: number;
|
|
41
|
+
toolUseIds: string[];
|
|
42
|
+
}
|
|
43
|
+
/** The fields of one assistant line the counter needs. */
|
|
44
|
+
export interface LedgerLine {
|
|
45
|
+
messageId: string | undefined;
|
|
46
|
+
source: string;
|
|
47
|
+
/** Byte offset of the line's first byte in its file — keys a line with no id. */
|
|
48
|
+
byteOffset: number;
|
|
49
|
+
model: string | undefined;
|
|
50
|
+
/** The line's raw `message.usage` block, if it carries one. */
|
|
51
|
+
usage: Record<string, unknown> | undefined;
|
|
52
|
+
/** The line's transcript timestamp, epoch ms. */
|
|
53
|
+
timestamp: number;
|
|
54
|
+
/** `tool_use` block ids on this line. */
|
|
55
|
+
toolUseIds: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
export declare const MAIN_SOURCE = "main";
|
|
58
|
+
/**
|
|
59
|
+
* DX-2963 — a usage line with no `message.id` (never observed in the 22,087
|
|
60
|
+
* usage lines of the 20 newest local transcripts, 2026-09-18) is keyed by its
|
|
61
|
+
* source and the byte offset of its line, so it is credited exactly once
|
|
62
|
+
* however often the file is re-read, instead of once per read.
|
|
63
|
+
*/
|
|
64
|
+
export declare function syntheticMessageKey(source: string, byteOffset: number): string;
|
|
65
|
+
export declare function isSyntheticMessageKey(key: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Read a raw `message.usage` block into {@link LedgerUsage}. The two
|
|
68
|
+
* cache-write lifetimes come from `usage.cache_creation.ephemeral_5m_input_tokens`
|
|
69
|
+
* / `ephemeral_1h_input_tokens`. A line that carries only the flat
|
|
70
|
+
* `cache_creation_input_tokens` (no split object) wrote with the API's
|
|
71
|
+
* default 5-minute lifetime, so the whole amount is a 5-minute write — that
|
|
72
|
+
* is what the flat field meant before the split existed.
|
|
73
|
+
*/
|
|
74
|
+
export declare function parseUsageBlock(raw: Record<string, unknown>): LedgerUsage;
|
|
75
|
+
export declare function zeroUsage(): LedgerUsage;
|
|
76
|
+
export declare const LEDGER_USAGE_FIELDS: readonly ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1h"];
|
|
77
|
+
export declare function maxUsage(a: LedgerUsage, b: LedgerUsage): LedgerUsage;
|
|
78
|
+
export declare function addUsage(a: LedgerUsage, b: LedgerUsage): LedgerUsage;
|
|
79
|
+
/** Per-field `max(0, a - b)`. */
|
|
80
|
+
export declare function excessUsage(a: LedgerUsage, b: LedgerUsage): LedgerUsage;
|
|
81
|
+
export declare function isZeroUsage(u: LedgerUsage): boolean;
|
|
82
|
+
/** Window tokens: input + output + both cache writes (the account-window unit). */
|
|
83
|
+
export declare function windowTokensOfUsage(u: LedgerUsage): number;
|
|
84
|
+
/** All five fields summed. */
|
|
85
|
+
export declare function totalTokensOfUsage(u: LedgerUsage): number;
|
|
86
|
+
/**
|
|
87
|
+
* The per-process counter: one entry per reply, updated line by line.
|
|
88
|
+
* `observe` returns true when the line CHANGED the reply's entry (a new
|
|
89
|
+
* reply, a raised field, or a new tool-use id) — a replay returns false —
|
|
90
|
+
* and the entry is queued for {@link takeChanged}. Holds one small entry per
|
|
91
|
+
* reply for the life of the process; no transcript text is kept.
|
|
92
|
+
*/
|
|
93
|
+
export declare class UsageLedgerCounter {
|
|
94
|
+
private readonly entries;
|
|
95
|
+
private readonly changed;
|
|
96
|
+
observe(line: LedgerLine): boolean;
|
|
97
|
+
/** Entries changed since the last call, as snapshots; clears the queue. */
|
|
98
|
+
takeChanged(): LedgerEntry[];
|
|
99
|
+
/** Put entries back on the changed queue (a write that failed). */
|
|
100
|
+
requeue(messageIds: readonly string[]): void;
|
|
101
|
+
get(messageId: string): LedgerEntry | undefined;
|
|
102
|
+
all(): LedgerEntry[];
|
|
103
|
+
/** Per-field sum of every reply's maximum — the transcript's true total. */
|
|
104
|
+
totals(): LedgerUsage;
|
|
105
|
+
/** Distinct tool-use ids across all replies. */
|
|
106
|
+
toolUseCount(): number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Pull the counter's {@link LedgerLine} fields out of one raw transcript
|
|
110
|
+
* line (a parsed JSONL object). Returns `null` for anything that is not an
|
|
111
|
+
* assistant line. Shared by every counter that reads a transcript from disk
|
|
112
|
+
* (the dashboard's reader, the skill-eval probe, the stall sweep).
|
|
113
|
+
*/
|
|
114
|
+
export declare function ledgerLineFromRaw(raw: Record<string, unknown>, source: string, byteOffset: number): LedgerLine | null;
|
|
115
|
+
/**
|
|
116
|
+
* Count a whole transcript text (one file) into a counter. Byte offsets are
|
|
117
|
+
* computed on UTF-8 bytes so a no-id line keys identically to the live
|
|
118
|
+
* watcher's own offset for the same line.
|
|
119
|
+
*/
|
|
120
|
+
export declare function observeTranscriptText(counter: UsageLedgerCounter, text: string, source: string, startByteOffset?: number): void;
|
|
121
|
+
/**
|
|
122
|
+
* The counter's {@link LedgerLine} for one converted watcher entry
|
|
123
|
+
* (`AgentLogEntry`, as `SessionLogWatcher` emits it: `data.messageId`,
|
|
124
|
+
* `data.model`, `data.usage`, `data.content`, `data.subagent_id`,
|
|
125
|
+
* `data.byte_offset`). `null` for anything that is not an assistant entry.
|
|
126
|
+
* Shared by every counter fed from watcher entries (the worker's dispatch
|
|
127
|
+
* usage, the stall sweep's productivity signal).
|
|
128
|
+
*/
|
|
129
|
+
export declare function ledgerLineFromLogEntry(entry: AgentLogEntry): LedgerLine | null;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
export const MAIN_SOURCE = "main";
|
|
2
|
+
/**
|
|
3
|
+
* DX-2963 — a usage line with no `message.id` (never observed in the 22,087
|
|
4
|
+
* usage lines of the 20 newest local transcripts, 2026-09-18) is keyed by its
|
|
5
|
+
* source and the byte offset of its line, so it is credited exactly once
|
|
6
|
+
* however often the file is re-read, instead of once per read.
|
|
7
|
+
*/
|
|
8
|
+
export function syntheticMessageKey(source, byteOffset) {
|
|
9
|
+
return `nomsg:${source}:${byteOffset}`;
|
|
10
|
+
}
|
|
11
|
+
export function isSyntheticMessageKey(key) {
|
|
12
|
+
return key.startsWith("nomsg:");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A usage field as a non-negative count. A numeric STRING is read as its
|
|
16
|
+
* number (drift-safe: a transcript that ever quotes a count is still counted,
|
|
17
|
+
* never silently zeroed); anything else absent or non-numeric is 0.
|
|
18
|
+
*/
|
|
19
|
+
function num(v) {
|
|
20
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
21
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read a raw `message.usage` block into {@link LedgerUsage}. The two
|
|
25
|
+
* cache-write lifetimes come from `usage.cache_creation.ephemeral_5m_input_tokens`
|
|
26
|
+
* / `ephemeral_1h_input_tokens`. A line that carries only the flat
|
|
27
|
+
* `cache_creation_input_tokens` (no split object) wrote with the API's
|
|
28
|
+
* default 5-minute lifetime, so the whole amount is a 5-minute write — that
|
|
29
|
+
* is what the flat field meant before the split existed.
|
|
30
|
+
*/
|
|
31
|
+
export function parseUsageBlock(raw) {
|
|
32
|
+
const split = raw.cache_creation;
|
|
33
|
+
const hasSplit = split !== null && typeof split === "object";
|
|
34
|
+
return {
|
|
35
|
+
input: num(raw.input_tokens),
|
|
36
|
+
output: num(raw.output_tokens),
|
|
37
|
+
cacheRead: num(raw.cache_read_input_tokens),
|
|
38
|
+
cacheWrite5m: hasSplit
|
|
39
|
+
? num(split.ephemeral_5m_input_tokens)
|
|
40
|
+
: num(raw.cache_creation_input_tokens),
|
|
41
|
+
cacheWrite1h: hasSplit ? num(split.ephemeral_1h_input_tokens) : 0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function zeroUsage() {
|
|
45
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite5m: 0, cacheWrite1h: 0 };
|
|
46
|
+
}
|
|
47
|
+
export const LEDGER_USAGE_FIELDS = [
|
|
48
|
+
"input",
|
|
49
|
+
"output",
|
|
50
|
+
"cacheRead",
|
|
51
|
+
"cacheWrite5m",
|
|
52
|
+
"cacheWrite1h",
|
|
53
|
+
];
|
|
54
|
+
export function maxUsage(a, b) {
|
|
55
|
+
const out = zeroUsage();
|
|
56
|
+
for (const f of LEDGER_USAGE_FIELDS)
|
|
57
|
+
out[f] = Math.max(a[f], b[f]);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
export function addUsage(a, b) {
|
|
61
|
+
const out = zeroUsage();
|
|
62
|
+
for (const f of LEDGER_USAGE_FIELDS)
|
|
63
|
+
out[f] = a[f] + b[f];
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/** Per-field `max(0, a - b)`. */
|
|
67
|
+
export function excessUsage(a, b) {
|
|
68
|
+
const out = zeroUsage();
|
|
69
|
+
for (const f of LEDGER_USAGE_FIELDS)
|
|
70
|
+
out[f] = Math.max(0, a[f] - b[f]);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
export function isZeroUsage(u) {
|
|
74
|
+
return LEDGER_USAGE_FIELDS.every((f) => u[f] === 0);
|
|
75
|
+
}
|
|
76
|
+
/** Window tokens: input + output + both cache writes (the account-window unit). */
|
|
77
|
+
export function windowTokensOfUsage(u) {
|
|
78
|
+
return u.input + u.output + u.cacheWrite5m + u.cacheWrite1h;
|
|
79
|
+
}
|
|
80
|
+
/** All five fields summed. */
|
|
81
|
+
export function totalTokensOfUsage(u) {
|
|
82
|
+
return u.input + u.output + u.cacheRead + u.cacheWrite5m + u.cacheWrite1h;
|
|
83
|
+
}
|
|
84
|
+
function readSpeed(usage) {
|
|
85
|
+
const s = usage?.speed;
|
|
86
|
+
return typeof s === "string" && s.length > 0 ? s : "standard";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The per-process counter: one entry per reply, updated line by line.
|
|
90
|
+
* `observe` returns true when the line CHANGED the reply's entry (a new
|
|
91
|
+
* reply, a raised field, or a new tool-use id) — a replay returns false —
|
|
92
|
+
* and the entry is queued for {@link takeChanged}. Holds one small entry per
|
|
93
|
+
* reply for the life of the process; no transcript text is kept.
|
|
94
|
+
*/
|
|
95
|
+
export class UsageLedgerCounter {
|
|
96
|
+
entries = new Map();
|
|
97
|
+
changed = new Set();
|
|
98
|
+
observe(line) {
|
|
99
|
+
const hasUsage = line.usage !== undefined;
|
|
100
|
+
if (!hasUsage && line.toolUseIds.length === 0)
|
|
101
|
+
return false;
|
|
102
|
+
// A tool-use-only line with no id cannot be tied to a reply; it has no
|
|
103
|
+
// usage to credit, and its tool ids would be counted on a key no other
|
|
104
|
+
// line of the reply shares — skip it rather than invent a reply.
|
|
105
|
+
if (line.messageId === undefined && !hasUsage)
|
|
106
|
+
return false;
|
|
107
|
+
const key = line.messageId ?? syntheticMessageKey(line.source, line.byteOffset);
|
|
108
|
+
const usage = hasUsage ? parseUsageBlock(line.usage) : zeroUsage();
|
|
109
|
+
const prior = this.entries.get(key);
|
|
110
|
+
if (!prior) {
|
|
111
|
+
this.entries.set(key, {
|
|
112
|
+
messageId: key,
|
|
113
|
+
source: line.source,
|
|
114
|
+
model: line.model ?? null,
|
|
115
|
+
speed: readSpeed(line.usage),
|
|
116
|
+
usage,
|
|
117
|
+
repliedAt: line.timestamp,
|
|
118
|
+
toolUseIds: [...new Set(line.toolUseIds)],
|
|
119
|
+
});
|
|
120
|
+
this.changed.add(key);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
let didChange = false;
|
|
124
|
+
const next = maxUsage(prior.usage, usage);
|
|
125
|
+
if (LEDGER_USAGE_FIELDS.some((f) => next[f] !== prior.usage[f])) {
|
|
126
|
+
prior.usage = next;
|
|
127
|
+
didChange = true;
|
|
128
|
+
}
|
|
129
|
+
for (const id of line.toolUseIds) {
|
|
130
|
+
if (!prior.toolUseIds.includes(id)) {
|
|
131
|
+
prior.toolUseIds.push(id);
|
|
132
|
+
didChange = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (prior.model === null && line.model) {
|
|
136
|
+
prior.model = line.model;
|
|
137
|
+
didChange = true;
|
|
138
|
+
}
|
|
139
|
+
if (hasUsage && prior.speed !== readSpeed(line.usage) && readSpeed(line.usage) !== "standard") {
|
|
140
|
+
prior.speed = readSpeed(line.usage);
|
|
141
|
+
didChange = true;
|
|
142
|
+
}
|
|
143
|
+
if (didChange)
|
|
144
|
+
this.changed.add(key);
|
|
145
|
+
return didChange;
|
|
146
|
+
}
|
|
147
|
+
/** Entries changed since the last call, as snapshots; clears the queue. */
|
|
148
|
+
takeChanged() {
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const key of this.changed) {
|
|
151
|
+
const e = this.entries.get(key);
|
|
152
|
+
if (e)
|
|
153
|
+
out.push(snapshot(e));
|
|
154
|
+
}
|
|
155
|
+
this.changed.clear();
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
/** Put entries back on the changed queue (a write that failed). */
|
|
159
|
+
requeue(messageIds) {
|
|
160
|
+
for (const id of messageIds)
|
|
161
|
+
if (this.entries.has(id))
|
|
162
|
+
this.changed.add(id);
|
|
163
|
+
}
|
|
164
|
+
get(messageId) {
|
|
165
|
+
const e = this.entries.get(messageId);
|
|
166
|
+
return e ? snapshot(e) : undefined;
|
|
167
|
+
}
|
|
168
|
+
all() {
|
|
169
|
+
return [...this.entries.values()].map(snapshot);
|
|
170
|
+
}
|
|
171
|
+
/** Per-field sum of every reply's maximum — the transcript's true total. */
|
|
172
|
+
totals() {
|
|
173
|
+
let t = zeroUsage();
|
|
174
|
+
for (const e of this.entries.values())
|
|
175
|
+
t = addUsage(t, e.usage);
|
|
176
|
+
return t;
|
|
177
|
+
}
|
|
178
|
+
/** Distinct tool-use ids across all replies. */
|
|
179
|
+
toolUseCount() {
|
|
180
|
+
let n = 0;
|
|
181
|
+
for (const e of this.entries.values())
|
|
182
|
+
n += e.toolUseIds.length;
|
|
183
|
+
return n;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function snapshot(e) {
|
|
187
|
+
return { ...e, usage: { ...e.usage }, toolUseIds: [...e.toolUseIds] };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Pull the counter's {@link LedgerLine} fields out of one raw transcript
|
|
191
|
+
* line (a parsed JSONL object). Returns `null` for anything that is not an
|
|
192
|
+
* assistant line. Shared by every counter that reads a transcript from disk
|
|
193
|
+
* (the dashboard's reader, the skill-eval probe, the stall sweep).
|
|
194
|
+
*/
|
|
195
|
+
export function ledgerLineFromRaw(raw, source, byteOffset) {
|
|
196
|
+
if (raw.type !== "assistant")
|
|
197
|
+
return null;
|
|
198
|
+
const message = raw.message;
|
|
199
|
+
if (!message)
|
|
200
|
+
return null;
|
|
201
|
+
const content = Array.isArray(message.content)
|
|
202
|
+
? message.content
|
|
203
|
+
: [];
|
|
204
|
+
const ts = typeof raw.timestamp === "string" ? Date.parse(raw.timestamp) : NaN;
|
|
205
|
+
return {
|
|
206
|
+
messageId: typeof message.id === "string" && message.id.length > 0 ? message.id : undefined,
|
|
207
|
+
source,
|
|
208
|
+
byteOffset,
|
|
209
|
+
model: typeof message.model === "string" && message.model.length > 0
|
|
210
|
+
? message.model
|
|
211
|
+
: undefined,
|
|
212
|
+
usage: message.usage && typeof message.usage === "object"
|
|
213
|
+
? message.usage
|
|
214
|
+
: undefined,
|
|
215
|
+
timestamp: Number.isFinite(ts) ? ts : 0,
|
|
216
|
+
toolUseIds: content
|
|
217
|
+
.filter((b) => b.type === "tool_use" && typeof b.id === "string")
|
|
218
|
+
.map((b) => b.id),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Count a whole transcript text (one file) into a counter. Byte offsets are
|
|
223
|
+
* computed on UTF-8 bytes so a no-id line keys identically to the live
|
|
224
|
+
* watcher's own offset for the same line.
|
|
225
|
+
*/
|
|
226
|
+
export function observeTranscriptText(counter, text, source, startByteOffset = 0) {
|
|
227
|
+
const encoder = new TextEncoder();
|
|
228
|
+
let offset = startByteOffset;
|
|
229
|
+
for (const rawLine of text.split("\n")) {
|
|
230
|
+
const lineBytes = encoder.encode(rawLine).length;
|
|
231
|
+
const lineOffset = offset;
|
|
232
|
+
offset += lineBytes + 1;
|
|
233
|
+
const trimmed = rawLine.trim();
|
|
234
|
+
if (!trimmed)
|
|
235
|
+
continue;
|
|
236
|
+
let parsed;
|
|
237
|
+
try {
|
|
238
|
+
parsed = JSON.parse(trimmed);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const line = ledgerLineFromRaw(parsed, source, lineOffset);
|
|
244
|
+
if (line)
|
|
245
|
+
counter.observe(line);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The counter's {@link LedgerLine} for one converted watcher entry
|
|
250
|
+
* (`AgentLogEntry`, as `SessionLogWatcher` emits it: `data.messageId`,
|
|
251
|
+
* `data.model`, `data.usage`, `data.content`, `data.subagent_id`,
|
|
252
|
+
* `data.byte_offset`). `null` for anything that is not an assistant entry.
|
|
253
|
+
* Shared by every counter fed from watcher entries (the worker's dispatch
|
|
254
|
+
* usage, the stall sweep's productivity signal).
|
|
255
|
+
*/
|
|
256
|
+
export function ledgerLineFromLogEntry(entry) {
|
|
257
|
+
if (entry.type !== "assistant")
|
|
258
|
+
return null;
|
|
259
|
+
const d = entry.data;
|
|
260
|
+
const content = Array.isArray(d.content)
|
|
261
|
+
? d.content
|
|
262
|
+
: [];
|
|
263
|
+
return {
|
|
264
|
+
messageId: typeof d.messageId === "string" && d.messageId.length > 0 ? d.messageId : undefined,
|
|
265
|
+
source: typeof d.subagent_id === "string" ? d.subagent_id : MAIN_SOURCE,
|
|
266
|
+
byteOffset: typeof d.byte_offset === "number" ? d.byte_offset : 0,
|
|
267
|
+
model: typeof d.model === "string" && d.model.length > 0 ? d.model : undefined,
|
|
268
|
+
usage: d.usage && typeof d.usage === "object"
|
|
269
|
+
? d.usage
|
|
270
|
+
: undefined,
|
|
271
|
+
timestamp: entry.timestamp,
|
|
272
|
+
toolUseIds: content
|
|
273
|
+
.filter((b) => b.type === "tool_use" && typeof b.id === "string")
|
|
274
|
+
.map((b) => b.id),
|
|
275
|
+
};
|
|
276
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-session-telemetry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Shared Claude Code transcript collector (DX-2962) — incremental JSONL tailing for the danxbot worker and the danx-dashboard-mcp desktop bridge, so both run identical collector code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
|
+
"source": "./src/index.ts",
|
|
12
13
|
"default": "./dist/index.js"
|
|
13
14
|
},
|
|
14
15
|
"./browser": {
|
|
15
16
|
"types": "./dist/browser.d.ts",
|
|
17
|
+
"source": "./src/browser.ts",
|
|
16
18
|
"default": "./dist/browser.js"
|
|
17
19
|
}
|
|
18
20
|
},
|