@relayburn/sdk 1.10.0 → 2.0.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
@@ -1,46 +1,31 @@
1
- # Changelog
2
-
3
- All notable changes to `@relayburn/sdk`.
1
+ # @relayburn/sdk (2.x)
4
2
 
5
3
  ## [Unreleased]
6
4
 
7
- ## [1.10.0] - 2026-05-03
8
-
9
- ### Breaking Changes
10
-
11
- - `hotspots()` now returns a discriminated union (`{ kind: 'attribution' | 'bash' | 'bash-verb' | 'file' | 'subagent' | 'findings' }`) instead of either an attribution blob or a raw findings array. Callers must branch on `kind`. The default (no `groupBy`, no `patterns`) returns the `attribution` shape that mirrors `burn hotspots --json`. Pass `patterns` to get `findings`. Pass `groupBy` to narrow attribution to one axis (`bash` / `bash-verb` / `file` / `subagent`). Warrants the SDK major bump.
12
-
13
- ### Added
14
-
15
- - `hotspots({ groupBy })` narrows attribution to one aggregation axis: `bash`, `bash-verb`, `file`, or `subagent`. Useful for MCP tools and embedders that only want a single per-axis cut.
16
- - `hotspots({ project, since })` accept the same forwarded options the other SDK queries do (project filter + ISO/relative `since` window normalization).
17
- - `hotspots()` reads through the SQLite archive by default with transparent fallback to the JSONL ledger walk on archive failure. Pass `onLog` to capture the fallback reason.
18
- - `hotspots()` surfaces a coverage-refusal shape (`refused: true` + `refusalReason`) when every matched turn lacks the tool-call/tool-result coverage `attributeHotspots` needs, so presenters can map it to the user-facing exit-2 + stderr message.
19
-
20
- ## [1.9.0] - 2026-05-03
21
-
22
- ### Added
23
-
24
- - `compare({ models, … })` returns the per-(model, activity) `CompareResult` shape (`analyzedTurns`, `models`, `categories`, `totals`, flat `cells[]`, `fidelity { minimum, excluded, summary }`) — the JSON object `burn compare --json` now emits. Mirrors the CLI's archive-vs-ledger branching: archive when `minFidelity === 'partial'` and no provider filter, ledger walk otherwise. Falls back transparently to the ledger walk when the archive read fails.
25
- - `sessionCost({ session })` returns the compact per-session cost shape (`totalUSD`, `totalTokens`, `turnCount`, `models`) the MCP `burn__sessionCost` tool now wraps directly.
26
- - `summary()` result now includes `turnCount`.
27
- - `summary()` and `sessionCost()` read through the SQLite archive by default with transparent fallback to the JSONL ledger walk on archive failure. Pass `onLog` to capture the fallback reason.
28
- - `overhead({ project, since?, kind? })` returns per-file + per-section overhead cost attribution (the JSON shape `burn overhead --json` now consumes).
29
- - `overheadTrim({ project, since?, kind?, top?, includeDiff? })` returns trim recommendations with projected savings and (by default) embedded unified diffs (the JSON shape `burn overhead trim --json` now consumes). Pass `includeDiff: false` to skip the per-file disk reads.
30
- - `summary({ since })` and `overhead({ since })` / `overheadTrim({ since })` now accept either an ISO timestamp or a relative range (`24h`, `7d`, `4w`, `2m`); the SDK normalizes both forms before querying the ledger so direct SDK callers get the same forgiving input shape CLI users have. Previously a raw relative string would silently filter out every turn.
31
-
32
- ## [1.7.0] - 2026-05-02
33
-
34
- ### Added
35
-
36
- - `hotspots({ patterns })` now also surfaces `tool-output-bloat`, `ghost-surface`, and `tool-call-pattern` findings (previously only the core `detectPatterns` set). Each side-channel detector loads its own inputs (Claude settings, tool-result events, on-disk surface) lazily based on the requested patterns.
37
-
38
- ### Changed
39
-
40
- - SDK no longer depends on `@relayburn/cli`. `ingest()` now imports from the new `@relayburn/ingest` package, and `buildGhostSurfaceInputs` lives in `@relayburn/analyze`. The SDK's public surface is unchanged.
41
-
42
- ## [1.5.0] - 2026-05-01
43
-
44
- ### Added
5
+ ## [2.0.0] - 2026-05-07
6
+
7
+ - Initial scaffolding: umbrella package layout (`@relayburn/sdk`) +
8
+ per-platform packages (`@relayburn/sdk-{darwin-arm64,darwin-x64,linux-arm64-gnu,linux-x64-gnu}`)
9
+ resolved via `optionalDependencies`, TS facade re-exporting the napi-rs
10
+ binding, conformance scaffold against the TS 1.x SDK, esbuild bundle
11
+ smoke test. (#247 part b)
12
+ - Shape conformance with TS `@relayburn/sdk@1.x`: `Ledger.open()` returns
13
+ a `Promise<Ledger>` instance, `sessionCost()` emits `totalUSD`
14
+ (screaming USD), every read verb is `async` (`Promise<T>`),
15
+ `IngestOptions` is `{ sessionId, harness, ledgerHome }`, `top` and
16
+ `minSample` accept plain `number`, and `onLog` callbacks are accepted
17
+ on every read verb's options (silently dropped at the napi boundary
18
+ until the SDK wires fallback logging). Adds `search`, `exportLedger`,
19
+ `exportStamps`, `BurnErrorCode`, `OverheadFileKind`, and
20
+ `HotspotsGroupBy` as 2.x extensions over the 1.x surface. (#247 part c)
21
+ - Umbrella facade now coerces napi-rs `BigInt` return values to `Number`
22
+ for safe-range integers (`[Number.MIN_SAFE_INTEGER,
23
+ Number.MAX_SAFE_INTEGER]`), matching the TS 1.x runtime shape; values
24
+ outside that range stay `BigInt` to avoid silent precision loss.
25
+ - Conformance suite is now wired into CI: `napi build` writes its outputs
26
+ (`.node`, `binding.cjs`, `binding.d.ts`) into `src/` so the generated
27
+ loader's local-file branch resolves; the suite seeds a deterministic
28
+ ledger via `tests/fixtures/cli-golden/scripts/build-ledger.mjs` and
29
+ flips `RELAYBURN_SDK_NAPI_BUILT=1` to enable the `deepStrictEqual` gate
30
+ against TS `@relayburn/sdk@1.x`. (#247 part d)
45
31
 
46
- - Initial release with embedded `Ledger.open()`, `ingest()`, `summary()`, and `hotspots()` helpers.
package/README.md CHANGED
@@ -1,50 +1,36 @@
1
- # @relayburn/sdk
2
-
3
- Embeddable Relayburn SDK for in-process ingestion and analysis. This package is the **source of truth** for the in-process query/compute surface — `@relayburn/mcp` and `@relayburn/cli` consume the SDK rather than duplicating its logic.
4
-
5
- ```ts
6
- import {
7
- Ledger,
8
- ingest,
9
- summary,
10
- sessionCost,
11
- compare,
12
- overhead,
13
- overheadTrim,
14
- hotspots,
15
- } from '@relayburn/sdk';
16
-
17
- await Ledger.open({ home: '/tmp/relayburn-home' });
18
- await ingest({ ledgerHome: '/tmp/relayburn-home' });
19
-
20
- // Slice-wide rollup: turnCount + per-model + per-tool aggregates.
21
- const stats = await summary({ session: 'session-id', ledgerHome: '/tmp/relayburn-home' });
22
-
23
- // Compact session-scoped cost shape (totalUSD/totalTokens/turnCount/models).
24
- // Powers the MCP `burn__sessionCost` tool.
25
- const cost = await sessionCost({ session: 'session-id' });
26
-
27
- // Per-(model, activity) comparison shape the JSON object `burn compare --json` emits.
28
- const cmp = await compare({
29
- models: ['claude-sonnet-4-6', 'claude-haiku-4-5'],
30
- since: '30d',
31
- minFidelity: 'usage-only',
32
- });
33
-
34
- // Overhead-file (CLAUDE.md / AGENTS.md / .claude/CLAUDE.md) cost attribution.
35
- const oh = await overhead({ project: '/path/to/repo', since: '30d' });
36
- const trim = await overheadTrim({ project: '/path/to/repo', top: 3 });
37
-
38
- // Per-axis hotspot attribution + pattern findings. Returns a discriminated
39
- // union — branch on `kind`:
40
- // { kind: 'attribution', files, bashVerbs, bash, subagents, sessions, … }
41
- // { kind: 'bash' | 'bash-verb' | 'file' | 'subagent', rows: [...] }
42
- // { kind: 'findings', findings: WasteFinding[], summary }
43
- const attribution = await hotspots({ session: 'session-id' });
44
- const fileRows = await hotspots({ session: 'session-id', groupBy: 'file' });
45
- const findings = await hotspots({ session: 'session-id', patterns: ['retry-loop'] });
46
- ```
47
-
48
- `summary`, `sessionCost`, `compare`, `overhead`, `overheadTrim`, and `hotspots` read through the SQLite archive when available, transparently falling back to the JSONL ledger walk if the archive can't be opened. Pass `onLog` to surface fallback messages in your host's log channel.
49
-
50
- `overheadTrim` includes a unified-diff string per recommendation by default (matches `burn overhead trim --json`); pass `includeDiff: false` to skip the per-file disk reads when you only need the recommendation rows.
1
+ # @relayburn/sdk (2.x)
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/`.
6
+
7
+ The 2.x umbrella resolves the right native binary for your platform via
8
+ `optionalDependencies`:
9
+
10
+ | Platform | Package |
11
+ |---|---|
12
+ | darwin-arm64 (Apple Silicon) | `@relayburn/sdk-darwin-arm64` |
13
+ | darwin-x64 (Intel Mac) | `@relayburn/sdk-darwin-x64` |
14
+ | linux-arm64-gnu | `@relayburn/sdk-linux-arm64-gnu` |
15
+ | linux-x64-gnu | `@relayburn/sdk-linux-x64-gnu` |
16
+
17
+ Windows (`win32-x64-msvc`) is not yet shipped — see #247 follow-up.
18
+
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.
package/package.json CHANGED
@@ -1,35 +1,64 @@
1
1
  {
2
2
  "name": "@relayburn/sdk",
3
- "version": "1.10.0",
4
- "description": "Embeddable Relayburn SDK for in-process ingest, summary, and hotspots queries",
3
+ "version": "2.0.0",
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
- "main": "./index.js",
7
- "types": "./index.d.ts",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js",
12
+ "require": "./src/index.cjs"
13
+ }
14
+ },
8
15
  "files": [
9
- "index.js",
10
- "index.d.ts",
16
+ "src/index.js",
17
+ "src/index.cjs",
18
+ "src/index.d.ts",
19
+ "src/binding.cjs",
20
+ "src/binding.d.ts",
11
21
  "README.md",
12
22
  "CHANGELOG.md",
13
23
  "package.json"
14
24
  ],
25
+ "scripts": {
26
+ "build": "node -e \"process.exit(0)\"",
27
+ "build:napi": "napi build --platform --release --cargo-cwd ../../crates/relayburn-sdk-node --js binding.cjs --dts binding.d.ts src",
28
+ "build:napi:debug": "napi build --platform --cargo-cwd ../../crates/relayburn-sdk-node --js binding.cjs --dts binding.d.ts src",
29
+ "test": "node --test 'test/**/*.test.js'",
30
+ "test:bundle": "node test/esbuild-smoke.test.js"
31
+ },
15
32
  "engines": {
16
33
  "node": ">=22"
17
34
  },
18
- "dependencies": {
19
- "@relayburn/analyze": "1.10.0",
20
- "@relayburn/ingest": "1.10.0",
21
- "@relayburn/ledger": "1.10.0",
22
- "@relayburn/reader": "1.10.0"
35
+ "napi": {
36
+ "binaryName": "relayburn-sdk",
37
+ "packageName": "@relayburn/sdk",
38
+ "targets": [
39
+ "x86_64-apple-darwin",
40
+ "aarch64-apple-darwin",
41
+ "x86_64-unknown-linux-gnu",
42
+ "aarch64-unknown-linux-gnu"
43
+ ]
44
+ },
45
+ "optionalDependencies": {
46
+ "@relayburn/sdk-darwin-arm64": "2.0.0",
47
+ "@relayburn/sdk-darwin-x64": "2.0.0",
48
+ "@relayburn/sdk-linux-arm64-gnu": "2.0.0",
49
+ "@relayburn/sdk-linux-x64-gnu": "2.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@napi-rs/cli": "^2.18.4",
53
+ "esbuild": "^0.25.0"
23
54
  },
24
55
  "repository": {
25
56
  "type": "git",
26
57
  "url": "https://github.com/AgentWorkforce/burn",
27
- "directory": "packages/sdk"
58
+ "directory": "packages/sdk-node"
28
59
  },
29
60
  "publishConfig": {
30
- "access": "public"
31
- },
32
- "scripts": {
33
- "build": "node -e \"process.exit(0)\""
61
+ "access": "public",
62
+ "tag": "next"
34
63
  }
35
- }
64
+ }
@@ -0,0 +1,91 @@
1
+ // Native-binding loader. At publish time, `napi build` (via `@napi-rs/cli`)
2
+ // regenerates this file to dispatch to the right per-platform package
3
+ // (`@relayburn/sdk-darwin-arm64`, `@relayburn/sdk-linux-x64-gnu`, etc.) based
4
+ // on `process.platform` + `process.arch` + libc detection. The generated
5
+ // version pulls the prebuilt `.node` file out of `optionalDependencies` so
6
+ // installs don't need a Rust toolchain.
7
+ //
8
+ // **File extension note:** this file is `.cjs` (not `.js`) because the
9
+ // umbrella package is `"type": "module"`, which would make Node treat a
10
+ // bare `.js` as ESM and reject the `module.exports` below at load time.
11
+ // `napi build` is invoked with `--js src/binding.cjs` (see
12
+ // `package.json` scripts + `.github/workflows/napi-build.yml`) so the
13
+ // regeneration writes back to the `.cjs` path; both `src/index.js`
14
+ // (ESM facade) and `src/index.cjs` (CJS facade) `require('./binding.cjs')`.
15
+ //
16
+ // This stub matches the napi-rs-generated dispatcher *shape* so the umbrella
17
+ // package's TS facade (`src/index.js`) can import from it during local dev /
18
+ // CI conformance scaffolding before the prebuilt binaries exist. While
19
+ // #247-a is in flight, we throw a clear "binding not built" error instead of
20
+ // requiring `*.node` artifacts that don't exist yet.
21
+ //
22
+ // Once `napi build` runs in CI for the first time, this file is overwritten;
23
+ // see `.github/workflows/napi-build.yml`.
24
+
25
+ const { existsSync, readFileSync } = require('node:fs');
26
+ const { join } = require('node:path');
27
+ const { platform, arch } = process;
28
+
29
+ // Detect glibc vs musl on Linux. napi-rs generates this with `detect-libc`
30
+ // at build time; we keep a minimal fallback so `require('./binding.cjs')`
31
+ // doesn't crash when run before the binary build.
32
+ function isMusl() {
33
+ if (!process.report) return false;
34
+ try {
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const { glibcVersionRuntime } = (process.report.getReport() || {}).header || {};
37
+ return !glibcVersionRuntime;
38
+ } catch (_) {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ let nativeBinding = null;
44
+ let loadError = null;
45
+
46
+ function tryRequire(specifier, localFile) {
47
+ // Prefer the optional-dep platform package; fall back to a sibling .node
48
+ // that `napi build --release` drops next to this loader during local dev.
49
+ const localPath = localFile ? join(__dirname, '..', localFile) : null;
50
+ if (localPath && existsSync(localPath)) {
51
+ try {
52
+ return require(localPath);
53
+ } catch (e) {
54
+ loadError = e;
55
+ }
56
+ }
57
+ try {
58
+ return require(specifier);
59
+ } catch (e) {
60
+ loadError = e;
61
+ return null;
62
+ }
63
+ }
64
+
65
+ if (platform === 'darwin' && arch === 'arm64') {
66
+ nativeBinding = tryRequire('@relayburn/sdk-darwin-arm64', 'relayburn-sdk.darwin-arm64.node');
67
+ } else if (platform === 'darwin' && arch === 'x64') {
68
+ nativeBinding = tryRequire('@relayburn/sdk-darwin-x64', 'relayburn-sdk.darwin-x64.node');
69
+ } else if (platform === 'linux' && arch === 'arm64' && !isMusl()) {
70
+ nativeBinding = tryRequire('@relayburn/sdk-linux-arm64-gnu', 'relayburn-sdk.linux-arm64-gnu.node');
71
+ } else if (platform === 'linux' && arch === 'x64' && !isMusl()) {
72
+ nativeBinding = tryRequire('@relayburn/sdk-linux-x64-gnu', 'relayburn-sdk.linux-x64-gnu.node');
73
+ }
74
+
75
+ if (!nativeBinding) {
76
+ // Surface a clear actionable error. While #247-a is still merging, this is
77
+ // the failure mode CI / dev machines will hit; the conformance test
78
+ // `test/conformance.test.js` checks for it and skips so the suite stays
79
+ // green until bindings land.
80
+ const detail = loadError
81
+ ? `\nUnderlying error: ${loadError.message}`
82
+ : '';
83
+ throw new Error(
84
+ `@relayburn/sdk: native binding not found for ${platform}-${arch}.\n` +
85
+ `Expected one of @relayburn/sdk-{darwin-arm64,darwin-x64,linux-arm64-gnu,linux-x64-gnu} ` +
86
+ `to be installed via optionalDependencies, or a sibling .node prebuilt by ` +
87
+ `\`pnpm --filter @relayburn/sdk run build:napi\`.${detail}`,
88
+ );
89
+ }
90
+
91
+ module.exports = nativeBinding;
@@ -0,0 +1,21 @@
1
+ // Generated by `napi build --dts` at publish time. This stub matches the
2
+ // expected exported function names from `crates/relayburn-sdk-node` (#247-a).
3
+ // Once the bindings land, `napi build` overwrites this file with the real
4
+ // generated declarations.
5
+ //
6
+ // The umbrella's `src/index.js` re-exports these directly. The runtime
7
+ // shape is the discriminated unions / option objects defined in
8
+ // `src/index.d.ts`; this file just declares "the binding exposes these
9
+ // names as functions."
10
+
11
+ export declare class Ledger {
12
+ static open(opts?: { home?: string }): Promise<Ledger>;
13
+ }
14
+
15
+ export declare function ingest(opts?: unknown): Promise<unknown>;
16
+ export declare function summary(opts?: unknown): Promise<unknown>;
17
+ export declare function sessionCost(opts?: unknown): Promise<unknown>;
18
+ export declare function overhead(opts?: unknown): Promise<unknown>;
19
+ export declare function overheadTrim(opts?: unknown): Promise<unknown>;
20
+ export declare function hotspots(opts?: unknown): Promise<unknown>;
21
+ export declare function compare(opts: unknown): Promise<unknown>;
package/src/index.cjs ADDED
@@ -0,0 +1,68 @@
1
+ // CommonJS variant of the umbrella facade. Required for tools that resolve
2
+ // `require('@relayburn/sdk')` through CJS — the ESM `src/index.js` is the
3
+ // canonical entry point, but Node falls back to this when a consumer's
4
+ // package is `"type": "commonjs"` and the `exports.require` map is honored.
5
+ //
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);
8
+ // see `src/index.js` for the rationale.
9
+
10
+ 'use strict';
11
+
12
+ const binding = require('./binding.cjs');
13
+
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.
18
+ const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
19
+ const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
20
+
21
+ function coerceBigInts(value) {
22
+ if (typeof value === 'bigint') {
23
+ return value >= MIN_SAFE && value <= MAX_SAFE ? Number(value) : value;
24
+ }
25
+ if (Array.isArray(value)) {
26
+ for (let i = 0; i < value.length; i++) {
27
+ value[i] = coerceBigInts(value[i]);
28
+ }
29
+ return value;
30
+ }
31
+ if (value !== null && typeof value === 'object') {
32
+ const proto = Object.getPrototypeOf(value);
33
+ if (proto === null || proto === Object.prototype) {
34
+ for (const key of Object.keys(value)) {
35
+ value[key] = coerceBigInts(value[key]);
36
+ }
37
+ }
38
+ return value;
39
+ }
40
+ return value;
41
+ }
42
+
43
+ class Ledger {
44
+ constructor(home) {
45
+ this.home = home;
46
+ }
47
+ static async open(opts) {
48
+ const home = binding.ledgerOpen(opts);
49
+ return new Ledger(home);
50
+ }
51
+ }
52
+
53
+ module.exports = {
54
+ Ledger,
55
+ ingest: async (opts) => coerceBigInts(await binding.ingest(opts)),
56
+ summary: async (opts) => coerceBigInts(await binding.summary(opts)),
57
+ sessionCost: async (opts) => coerceBigInts(await binding.sessionCost(opts)),
58
+ overhead: async (opts) => coerceBigInts(await binding.overhead(opts)),
59
+ overheadTrim: async (opts) => coerceBigInts(await binding.overheadTrim(opts)),
60
+ hotspots: async (opts) => coerceBigInts(await binding.hotspots(opts)),
61
+ compare: async (opts) => coerceBigInts(await binding.compare(opts)),
62
+ search: async (opts) => coerceBigInts(await binding.search(opts)),
63
+ exportLedger: async (opts) => coerceBigInts(await binding.exportLedger(opts)),
64
+ exportStamps: async (opts) => coerceBigInts(await binding.exportStamps(opts)),
65
+ BurnErrorCode: binding.BurnErrorCode,
66
+ OverheadFileKind: binding.OverheadFileKind,
67
+ HotspotsGroupBy: binding.HotspotsGroupBy,
68
+ };