@relayburn/sdk 2.3.0 → 2.4.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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.4.0] - 2026-05-08
6
+
7
+ ### Breaking Changes
8
+
9
+ - Removed the `onLog` option from `summary`, `sessionCost`, `overhead`, `overheadTrim`, `hotspots`, and `compare` option types. The 2.x stack is SQLite-native and has no archive-fallback path to surface, so the callback was already a no-op at the napi boundary. (#374)
10
+
11
+ ### Added
12
+
13
+ - Exported `writePendingStamp()` so Node launchers can write generic
14
+ enrichment tags before spawning Claude, Codex, or OpenCode directly.
15
+ - `summary()` options now accept `tags` and `groupByTag` for generic
16
+ enrichment filtering and cost/token grouping.
17
+ - Exported `computeCompareExcluded()` from the Node facade for callers that
18
+ need the same fidelity-exclusion breakdown used by `compare()`.
19
+
20
+ ### Changed
21
+
22
+ - Replaced the TypeScript 1.x deep-conformance test with native 2.x smoke
23
+ coverage against the committed fixture ledger.
24
+
25
+ ### Fixed
26
+
27
+ - `search()` now accepts a numeric `limit` in the Node facade and normalizes it
28
+ before calling the napi-rs binding.
29
+
5
30
  ## [2.1.0] - 2026-05-07
6
31
 
7
32
  ### Added
@@ -30,8 +55,5 @@
30
55
  outside that range stay `BigInt` to avoid silent precision loss.
31
56
  - Conformance suite is now wired into CI: `napi build` writes its outputs
32
57
  (`.node`, `binding.cjs`, `binding.d.ts`) into `src/` so the generated
33
- loader's local-file branch resolves; the suite seeds a deterministic
34
- ledger via `tests/fixtures/cli-golden/scripts/build-ledger.mjs` and
35
- flips `RELAYBURN_SDK_NAPI_BUILT=1` to enable the `deepStrictEqual` gate
36
- against TS `@relayburn/sdk@1.x`. (#247 part d)
37
-
58
+ loader's local-file branch resolves; the suite was originally wired as a
59
+ deep-equality gate against TS `@relayburn/sdk@1.x`. (#247 part d)
package/README.md CHANGED
@@ -1,10 +1,9 @@
1
- # @relayburn/sdk (2.x)
1
+ # @relayburn/sdk
2
2
 
3
- Embeddable Relayburn SDK napi-rs bindings over the Rust `relayburn-sdk`
4
- crate. Drop-in replacement for the TS `@relayburn/sdk@1.x` published from
5
- `packages/sdk/`.
3
+ Embeddable Relayburn SDK for Node.js. The package is a napi-rs facade over the
4
+ Rust `relayburn-sdk` crate.
6
5
 
7
- The 2.x umbrella resolves the right native binary for your platform via
6
+ The package resolves the native binding for your platform via
8
7
  `optionalDependencies`:
9
8
 
10
9
  | Platform | Package |
@@ -16,21 +15,15 @@ The 2.x umbrella resolves the right native binary for your platform via
16
15
 
17
16
  Windows (`win32-x64-msvc`) is not yet shipped — see #247 follow-up.
18
17
 
19
- ## Migration from 1.x
20
-
21
- Same imports, same option shapes, same return shapes except:
22
-
23
- - **u64 token counts are `bigint`.** napi-rs maps Rust `u64` to JavaScript
24
- `BigInt`. Code that does arithmetic on `summary().totalTokens` (and
25
- similar fields on `hotspots`, `overhead`, `sessionCost`) needs to either
26
- use `BigInt` literals (`100n`) or coerce with `Number(x)`. The TS
27
- declarations widen these fields to `number | bigint` to keep existing
28
- callers compiling.
29
- - Otherwise byte-for-byte compatible. Run your test suite the conformance
30
- test in `test/conformance.test.js` is what we use to validate.
31
-
32
- ## Status
33
-
34
- This is a `2.0.0-pre` build published to npm under the `next` tag while
35
- the rest of the Rust port lands. Until the lockstep 2.0 cutover ships, the
36
- 1.x TS SDK at `packages/sdk/` is still the source of truth.
18
+ ## API Notes
19
+
20
+ - Large u64 token counts may be `bigint`. napi-rs maps Rust `u64` to
21
+ JavaScript `BigInt`; the facade downcasts safe-range integers to `number`
22
+ and leaves larger values as `bigint`. The declarations widen these fields
23
+ to `number | bigint`.
24
+ - The SDK exposes read verbs such as `summary()`, `sessionCost()`,
25
+ `hotspots()`, `compare()`, `search()`, `exportLedger()`, and
26
+ `exportStamps()`.
27
+ - Launchers can call `writePendingStamp({ harness, cwd, enrichment })`
28
+ before spawning Claude, Codex, or OpenCode, then run `ingest()` to fold
29
+ those generic enrichment tags onto the discovered turns.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayburn/sdk",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Embeddable Relayburn SDK — napi-rs bindings over the Rust relayburn-sdk crate (2.x). Drop-in replacement for the TS @relayburn/sdk@1.x.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -43,10 +43,10 @@
43
43
  ]
44
44
  },
45
45
  "optionalDependencies": {
46
- "@relayburn/sdk-darwin-arm64": "2.3.0",
47
- "@relayburn/sdk-darwin-x64": "2.3.0",
48
- "@relayburn/sdk-linux-arm64-gnu": "2.3.0",
49
- "@relayburn/sdk-linux-x64-gnu": "2.3.0"
46
+ "@relayburn/sdk-darwin-arm64": "2.4.0",
47
+ "@relayburn/sdk-darwin-x64": "2.4.0",
48
+ "@relayburn/sdk-linux-arm64-gnu": "2.4.0",
49
+ "@relayburn/sdk-linux-x64-gnu": "2.4.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@napi-rs/cli": "^2.18.4",
package/src/binding.d.ts CHANGED
@@ -19,3 +19,4 @@ export declare function overhead(opts?: unknown): Promise<unknown>;
19
19
  export declare function overheadTrim(opts?: unknown): Promise<unknown>;
20
20
  export declare function hotspots(opts?: unknown): Promise<unknown>;
21
21
  export declare function compare(opts: unknown): Promise<unknown>;
22
+ export declare function writePendingStamp(opts: unknown): unknown;
package/src/index.cjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // package is `"type": "commonjs"` and the `exports.require` map is honored.
5
5
  //
6
6
  // Mirrors the ESM facade verb-for-verb. The sync binding verbs are wrapped
7
- // in `async` so callers see `Promise<T>` (matching the 1.x TS contract);
7
+ // in `async` so callers see `Promise<T>` (matching the Node facade contract);
8
8
  // see `src/index.js` for the rationale.
9
9
 
10
10
  'use strict';
@@ -12,9 +12,8 @@
12
12
  const binding = require('./binding.cjs');
13
13
 
14
14
  // See `src/index.js` for the rationale: napi-rs serializes Rust `u64` /
15
- // `i64` as JS `BigInt`, while TS 1.x `@relayburn/sdk` emits plain
16
- // `Number`. We downcast safe-range BigInts to keep `deepStrictEqual`
17
- // passing in conformance and to match user expectations from 1.x.
15
+ // `i64` as JS `BigInt`. We downcast safe-range BigInts to match common
16
+ // JavaScript caller expectations while keeping larger values precise.
18
17
  const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
19
18
  const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
20
19
 
@@ -40,6 +39,16 @@ function coerceBigInts(value) {
40
39
  return value;
41
40
  }
42
41
 
42
+ function normalizeSearchOptions(opts) {
43
+ if (!opts || typeof opts !== 'object' || typeof opts.limit !== 'number') {
44
+ return opts;
45
+ }
46
+ if (!Number.isSafeInteger(opts.limit) || opts.limit < 0) {
47
+ throw new RangeError('search limit must be a non-negative safe integer');
48
+ }
49
+ return { ...opts, limit: BigInt(opts.limit) };
50
+ }
51
+
43
52
  class Ledger {
44
53
  constructor(home) {
45
54
  this.home = home;
@@ -50,6 +59,30 @@ class Ledger {
50
59
  }
51
60
  }
52
61
 
62
+ function computeCompareExcluded(summary, minimum) {
63
+ const out = { total: 0, aggregateOnly: 0, costOnly: 0, partial: 0, usageOnly: 0 };
64
+ if (minimum === 'partial') return out;
65
+ const order = ['cost-only', 'aggregate-only', 'partial', 'usage-only', 'full'];
66
+ const need = order.indexOf(minimum);
67
+ if (need < 0) {
68
+ throw new Error(
69
+ `invalid minimum fidelity: ${minimum} (expected one of ${order.join(', ')})`,
70
+ );
71
+ }
72
+ const byClass = summary?.byClass ?? {};
73
+ for (const cls of order) {
74
+ if (order.indexOf(cls) >= need) continue;
75
+ const n = Number(byClass[cls] ?? 0);
76
+ if (!n) continue;
77
+ out.total += n;
78
+ if (cls === 'aggregate-only') out.aggregateOnly += n;
79
+ else if (cls === 'cost-only') out.costOnly += n;
80
+ else if (cls === 'partial') out.partial += n;
81
+ else if (cls === 'usage-only') out.usageOnly += n;
82
+ }
83
+ return out;
84
+ }
85
+
53
86
  module.exports = {
54
87
  Ledger,
55
88
  ingest: async (opts) => coerceBigInts(await binding.ingest(opts)),
@@ -59,7 +92,9 @@ module.exports = {
59
92
  overheadTrim: async (opts) => coerceBigInts(await binding.overheadTrim(opts)),
60
93
  hotspots: async (opts) => coerceBigInts(await binding.hotspots(opts)),
61
94
  compare: async (opts) => coerceBigInts(await binding.compare(opts)),
62
- search: async (opts) => coerceBigInts(await binding.search(opts)),
95
+ writePendingStamp: async (opts) => coerceBigInts(await binding.writePendingStamp(opts)),
96
+ computeCompareExcluded,
97
+ search: async (opts) => coerceBigInts(await binding.search(normalizeSearchOptions(opts))),
63
98
  exportLedger: async (opts) => coerceBigInts(await binding.exportLedger(opts)),
64
99
  exportStamps: async (opts) => coerceBigInts(await binding.exportStamps(opts)),
65
100
  BurnErrorCode: binding.BurnErrorCode,
package/src/index.d.ts CHANGED
@@ -1,23 +1,20 @@
1
1
  // Type surface for `@relayburn/sdk@2.x`.
2
2
  //
3
- // Mirrors `packages/sdk/index.d.ts` (the TS 1.x SDK) byte-for-byte modulo:
3
+ // Mirrors the Rust SDK verb surface through the napi-rs facade, with
4
+ // compatibility affordances for callers migrating from the 1.x JS SDK:
4
5
  // - `bigint` is allowed alongside `number` for u64-typed token counts (the
5
- // napi-rs binding emits `BigInt` for `u64`; the TS shape is widened so
6
- // existing callers that pass through `number` keep type-checking once
7
- // bound to the Rust impl).
6
+ // napi-rs binding emits `BigInt` for `u64`; the facade downcasts safe-range
7
+ // values at runtime).
8
8
  // - Async fns return `Promise<T>` — the napi-rs binding uses `async fn`
9
9
  // where the Rust SDK does, which is everywhere except the `Ledger.open`
10
10
  // constructor.
11
- //
12
- // Source-of-truth comment: track `packages/sdk/index.d.ts`. Whenever a verb
13
- // shape changes in TS, mirror it here AND in the Rust napi-rs binding (#247-a).
14
11
 
15
12
  export interface LedgerOpenOptions { home?: string; contentHome?: string }
16
13
  /**
17
- * Stateful ledger handle. The 1.x TS class only exposes the static
18
- * `open()` constructor; instances are placeholders today, with `home`
19
- * exposed for callers that want to confirm which ledger they attached
20
- * to. Verb methods are a future PR.
14
+ * Stateful ledger handle. The Node facade exposes the static `open()`
15
+ * constructor; instances carry the resolved home for callers that want
16
+ * to confirm which ledger they attached to. Verb methods are reserved
17
+ * for a future facade expansion.
21
18
  */
22
19
  export declare class Ledger {
23
20
  readonly home: string;
@@ -29,17 +26,48 @@ export interface IngestReport {
29
26
  scannedSessions: number | bigint;
30
27
  ingestedSessions: number | bigint;
31
28
  appendedTurns: number | bigint;
29
+ appliedPendingStamps: number | bigint;
32
30
  }
33
31
  export declare function ingest(opts?: IngestOptions): Promise<IngestReport>
34
32
 
33
+ export type PendingStampHarness = 'claude' | 'codex' | 'opencode';
34
+ export interface WritePendingStampOptions {
35
+ harness: PendingStampHarness;
36
+ cwd: string;
37
+ enrichment: Record<string, string>;
38
+ sessionDirHint?: string;
39
+ /** ISO timestamp, e.g. `2026-04-23T00:00:00.000Z`. Defaults to now. */
40
+ spawnStartTs?: string;
41
+ spawnerPid?: number;
42
+ ledgerHome?: string;
43
+ }
44
+ export interface PendingStamp {
45
+ v: number;
46
+ harness: PendingStampHarness;
47
+ spawnerPid: number;
48
+ spawnStartTs: string;
49
+ cwd: string;
50
+ enrichment: Record<string, string>;
51
+ sessionDirHint?: string;
52
+ }
53
+ export interface PendingStampWriteResult {
54
+ file: string;
55
+ stamp: PendingStamp;
56
+ }
57
+ export declare function writePendingStamp(
58
+ opts: WritePendingStampOptions,
59
+ ): Promise<PendingStampWriteResult>
60
+
35
61
  export interface SummaryOptions {
36
62
  session?: string;
37
63
  project?: string;
38
64
  /** ISO timestamp (e.g. `2026-04-01T00:00:00Z`) or relative range (`24h`, `7d`, `4w`, `2m`). */
39
65
  since?: string;
66
+ /** Folded enrichment tag filters; every key/value pair must match. */
67
+ tags?: Record<string, string>;
68
+ /** Group summary costs/tokens by this folded enrichment tag key. */
69
+ groupByTag?: string;
40
70
  ledgerHome?: string;
41
- /** Optional logger invoked when the SQLite archive read fails and the SDK falls back to a full ledger walk. */
42
- onLog?: (msg: string) => void;
43
71
  }
44
72
  export declare function summary(opts?: SummaryOptions): Promise<{
45
73
  totalTokens: number | bigint;
@@ -47,6 +75,13 @@ export declare function summary(opts?: SummaryOptions): Promise<{
47
75
  turnCount: number;
48
76
  byTool: Array<{ tool: string; tokens: number | bigint; cost: number; count: number }>;
49
77
  byModel: Array<{ model: string; tokens: number | bigint; cost: number }>;
78
+ byTag?: Array<{
79
+ tag: string;
80
+ value?: string;
81
+ tokens: number | bigint;
82
+ cost: number;
83
+ turnCount: number | bigint;
84
+ }>;
50
85
  replacementSavings?: {
51
86
  calls: number | bigint;
52
87
  collapsedCalls: number | bigint;
@@ -64,7 +99,6 @@ export interface SessionCostOptions {
64
99
  /** Session id to total. Omit for `{ note: 'no session id provided' }`. */
65
100
  session?: string;
66
101
  ledgerHome?: string;
67
- onLog?: (msg: string) => void;
68
102
  }
69
103
  export interface SessionCostResult {
70
104
  sessionId: string | null;
@@ -85,7 +119,6 @@ export interface OverheadOptions {
85
119
  since?: string;
86
120
  kind?: OverheadFileKind;
87
121
  ledgerHome?: string;
88
- onLog?: (msg: string) => void;
89
122
  }
90
123
 
91
124
  export interface OverheadSection {
@@ -183,7 +216,6 @@ export interface HotspotsOptions {
183
216
  groupBy?: HotspotsGroupBy;
184
217
  patterns?: string[];
185
218
  ledgerHome?: string;
186
- onLog?: (msg: string) => void;
187
219
  }
188
220
 
189
221
  export interface HotspotsFileRow {
@@ -333,7 +365,6 @@ export interface CompareOptions {
333
365
  minSample?: number;
334
366
  minFidelity?: FidelityClass;
335
367
  ledgerHome?: string;
336
- onLog?: (msg: string) => void;
337
368
  }
338
369
 
339
370
  export interface CompareResult {
@@ -352,13 +383,18 @@ export interface CompareResult {
352
383
 
353
384
  /** Per-(model, activity) comparison shape. Powers `burn compare`. */
354
385
  export declare function compare(opts: CompareOptions): Promise<CompareResult>
386
+ export declare function computeCompareExcluded(
387
+ summary: FidelitySummaryShape,
388
+ minimum: FidelityClass
389
+ ): CompareExcludedBreakdown
355
390
 
356
391
  // ---------------------------------------------------------------------------
357
392
  // 2.x extensions — surfaces present in `relayburn-sdk` (the Rust crate)
358
393
  // but not in the TS 1.x `packages/sdk/index.d.ts`. Pre-1.0 widening per
359
- // the SDK shape rule; embedders that pinned to 1.x won't see these names
360
- // (the missing `onLog` etc. plus the absence of these is the only TS-vs-
361
- // napi gap the conformance gate measures).
394
+ // the SDK shape rule; embedders that pinned to 1.x won't see these names.
395
+ // The 1.x `onLog` callback is intentionally omitted: it surfaced the
396
+ // archive-fallback path that no longer exists in the SQLite-native 2.x
397
+ // stack (see issue #374), so there is nothing to log.
362
398
  // ---------------------------------------------------------------------------
363
399
 
364
400
  export interface SearchQueryOptions {
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Thin ESM facade over the napi-rs binding. The verbs here re-export the
2
2
  // matching `#[napi]` exports from the platform package resolved by
3
- // `./binding.cjs`, with two adjustments to match the TS 1.x contract at
4
- // `packages/sdk/index.d.ts`:
3
+ // `./binding.cjs`, with two compatibility adjustments carried forward from
4
+ // the 1.x SDK contract:
5
5
  //
6
6
  // 1. The sync `#[napi]` verbs (`summary`, `sessionCost`, `overhead`,
7
7
  // `overheadTrim`, `hotspots`, `compare`) are re-exported as `async`
@@ -23,18 +23,13 @@ import { createRequire } from 'node:module';
23
23
  const require = createRequire(import.meta.url);
24
24
  const binding = require('./binding.cjs');
25
25
 
26
- // napi-rs serializes Rust `u64` / `i64` as JS `BigInt`, but the TS 1.x
27
- // `@relayburn/sdk` shape (mirrored in `src/index.d.ts`) emits plain
28
- // `Number` for the same fields. To keep the conformance gate's
29
- // `deepStrictEqual` checks honest and to match the runtime shape that
30
- // 1.x callers expect (e.g. `result.turnCount === 0`, not `=== 0n`) — we
31
- // downcast every `BigInt` in a verb's return value to `Number` when it
32
- // fits in `[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]`. Values
33
- // outside that range are left as `BigInt`: realistic burn ledgers won't
34
- // hit 2^53 tokens, but if one ever does, leaking a `BigInt` that crashes
35
- // a `===` check is strictly safer than silently rounding to the nearest
36
- // 1024. The TS shape declares `number | bigint` everywhere this matters
37
- // so the type stays sound either way.
26
+ // napi-rs serializes Rust `u64` / `i64` as JS `BigInt`, while 1.x emitted
27
+ // plain `Number` for the same fields. To keep common caller expectations
28
+ // intact (e.g. `result.turnCount === 0`, not `=== 0n`), downcast every
29
+ // `BigInt` in a verb's return value to `Number` when it fits in
30
+ // `[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]`. Values outside that
31
+ // range are left as `BigInt`; leaking a `BigInt` is safer than silently
32
+ // rounding a very large ledger.
38
33
  const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
39
34
  const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
40
35
 
@@ -63,12 +58,20 @@ function coerceBigInts(value) {
63
58
  return value;
64
59
  }
65
60
 
61
+ function normalizeSearchOptions(opts) {
62
+ if (!opts || typeof opts !== 'object' || typeof opts.limit !== 'number') {
63
+ return opts;
64
+ }
65
+ if (!Number.isSafeInteger(opts.limit) || opts.limit < 0) {
66
+ throw new RangeError('search limit must be a non-negative safe integer');
67
+ }
68
+ return { ...opts, limit: BigInt(opts.limit) };
69
+ }
70
+
66
71
  /**
67
- * Stateful ledger handle. Mirrors the TS 1.x `Ledger` class shape from
68
- * `packages/sdk/index.d.ts`. The 1.x version only exposes the static
69
- * `open(opts)` constructor instance methods are reserved for a future
70
- * PR — so we replicate that surface and stash the resolved home for
71
- * introspection.
72
+ * Stateful ledger handle. The 1.x SDK only exposed the static `open(opts)`
73
+ * constructor; instance methods are reserved for a future PR. Keep that shape
74
+ * and stash the resolved home for introspection.
72
75
  */
73
76
  export class Ledger {
74
77
  constructor(home) {
@@ -117,13 +120,39 @@ export async function compare(opts) {
117
120
  return coerceBigInts(await binding.compare(opts));
118
121
  }
119
122
 
123
+ export async function writePendingStamp(opts) {
124
+ return coerceBigInts(await binding.writePendingStamp(opts));
125
+ }
126
+
127
+ export function computeCompareExcluded(summary, minimum) {
128
+ const out = { total: 0, aggregateOnly: 0, costOnly: 0, partial: 0, usageOnly: 0 };
129
+ if (minimum === 'partial') return out;
130
+ const order = ['cost-only', 'aggregate-only', 'partial', 'usage-only', 'full'];
131
+ const need = order.indexOf(minimum);
132
+ if (need < 0) {
133
+ throw new Error(
134
+ `invalid minimum fidelity: ${minimum} (expected one of ${order.join(', ')})`,
135
+ );
136
+ }
137
+ const byClass = summary?.byClass ?? {};
138
+ for (const cls of order) {
139
+ if (order.indexOf(cls) >= need) continue;
140
+ const n = Number(byClass[cls] ?? 0);
141
+ if (!n) continue;
142
+ out.total += n;
143
+ if (cls === 'aggregate-only') out.aggregateOnly += n;
144
+ else if (cls === 'cost-only') out.costOnly += n;
145
+ else if (cls === 'partial') out.partial += n;
146
+ else if (cls === 'usage-only') out.usageOnly += n;
147
+ }
148
+ return out;
149
+ }
150
+
120
151
  // 2.x extensions — exposed by the Rust SDK but not declared in
121
- // `packages/sdk/index.d.ts` (the 1.x TS surface). Per the SDK shape rule,
122
- // pre-1.0 widening is allowed; these are surfaced here so embedders can
123
- // reach the FTS5 search index and the JSONL export iterators without
124
- // dropping into the binding directly.
152
+ // the 1.x SDK surface. These let embedders reach the FTS5 search index and
153
+ // JSONL export iterators without dropping into the binding directly.
125
154
  export async function search(opts) {
126
- return coerceBigInts(await binding.search(opts));
155
+ return coerceBigInts(await binding.search(normalizeSearchOptions(opts)));
127
156
  }
128
157
 
129
158
  export async function exportLedger(opts) {