Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ jobs:
- name: Resolve target packages
id: targets
run: |
# Dependency order: reader → ledger → analyze → mcp → cli → relayburn.
# `relayburn` is a thin wrapper that depends on `@relayburn/cli`,
# so cli must publish first.
echo "packages=reader ledger analyze mcp cli relayburn" >> "$GITHUB_OUTPUT"
# Dependency order: reader → ledger → analyze → mcp → cli → sdk → relayburn.
# `sdk` depends on cli/ledger/analyze; `relayburn` is a thin wrapper
# that depends on `@relayburn/cli`, so cli must publish before both.
echo "packages=reader ledger analyze mcp cli sdk relayburn" >> "$GITHUB_OUTPUT"

# Catch the failure mode that bit us on 2026-04-23: a previous publish
# run shipped @relayburn/*@0.3.0 to npm but failed at the Tag + push
Expand Down Expand Up @@ -646,7 +646,7 @@ jobs:
const canonicalPkg = process.env.CANONICAL_PKG;
const canonicalVersion = process.env.CANONICAL_VERSION;

const packageOrder = ['reader', 'ledger', 'analyze', 'mcp', 'cli', 'relayburn'];
const packageOrder = ['reader', 'ledger', 'analyze', 'mcp', 'cli', 'sdk', 'relayburn'];
const entries = versionsRaw.trim().split(/\s+/).filter(Boolean).map((entry) => {
const idx = entry.indexOf(':');
return { pkg: entry.slice(0, idx), ver: entry.slice(idx + 1) };
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/verify-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ on:
- '@relayburn/reader'
- '@relayburn/ledger'
- '@relayburn/analyze'
- '@relayburn/sdk'
version:
description: 'Version to verify (defaults to the "latest" dist-tag)'
required: false
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ Pairs with [`README.md`](./README.md) — README is what burn does, this file is

## Layout

pnpm workspace, six published packages in dependency order:
pnpm workspace, seven published packages in dependency order:

```
@relayburn/reader — pure parsers (Claude Code / Codex / OpenCode session logs → TurnRecord)
@relayburn/ledger — append-only JSONL ledger + content sidecar at ~/.relayburn/
@relayburn/analyze — pricing + per-record cost derivation + comparison aggregator
@relayburn/mcp — stdio MCP server exposing read-only ledger queries for in-session self-query
@relayburn/cli — `burn` binary (summary, hotspots, overhead, compare, `burn run <harness>` wrapper, mcp-server, …)
@relayburn/sdk — embeddable Node API (`ingest`, `summary`, `hotspots`) for in-process use
relayburn — thin install-wrapper so `npm i -g relayburn` exposes the same `burn` bin as `@relayburn/cli`
```

`reader → ledger → analyze → mcp → cli → relayburn`. Always build the whole workspace; never touch a single package's `tsconfig.tsbuildinfo` to "skip" a dep.
`reader → ledger → analyze → mcp → cli → sdk → relayburn`. Always build the whole workspace; never touch a single package's `tsconfig.tsbuildinfo` to "skip" a dep.

## Common commands

Expand Down
11 changes: 11 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

All notable changes to `@relayburn/sdk`.

## [Unreleased]

## [1.5.0] - 2026-05-01

### Added

- Initial release with embedded `Ledger.open()`, `ingest()`, `summary()`, and `hotspots()` helpers.
12 changes: 12 additions & 0 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @relayburn/sdk

Embeddable Relayburn SDK for in-process ingestion and analysis.

```ts
import { Ledger, ingest, summary, hotspots } from '@relayburn/sdk';

await Ledger.open({ home: '/tmp/relayburn-home' });
await ingest({ ledgerHome: '/tmp/relayburn-home' });
const stats = await summary({ session: 'session-id', ledgerHome: '/tmp/relayburn-home' });
const findings = await hotspots({ session: 'session-id', patterns: ['retry-loop'] });
```
11 changes: 11 additions & 0 deletions packages/sdk/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface LedgerOpenOptions { home?: string }
export declare class Ledger { static open(opts?: LedgerOpenOptions): Promise<Ledger> }

export interface IngestOptions { sessionId?: string; harness?: 'claude-code'|'codex'|'opencode'; ledgerHome?: string }
export declare function ingest(opts?: IngestOptions): Promise<unknown>

export interface SummaryOptions { session?: string; project?: string; since?: string; ledgerHome?: string }
export declare function summary(opts?: SummaryOptions): Promise<{ totalTokens: number; totalCost: number; byTool: Array<{tool:string;tokens:number;cost:number;count:number}>; byModel: Array<{model:string;tokens:number;cost:number}> }>

export interface HotspotsOptions { session?: string; patterns?: string[]; ledgerHome?: string }
export declare function hotspots(opts?: HotspotsOptions): Promise<unknown>
81 changes: 81 additions & 0 deletions packages/sdk/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { queryAll, queryUserTurns } from '@relayburn/ledger';
import { loadPricing, costForTurn, attributeHotspots, detectPatterns, findingsFromPatterns } from '@relayburn/analyze';
import { ingestAll } from '@relayburn/cli';

function withHome(home, fn) {
const prev = process.env.RELAYBURN_HOME;
if (home) process.env.RELAYBURN_HOME = home;
return Promise.resolve(fn()).finally(() => {
if (home) {
if (prev === undefined) delete process.env.RELAYBURN_HOME;
else process.env.RELAYBURN_HOME = prev;
}
});
}

export class Ledger {
static async open(opts = {}) {
return new Ledger(opts.home);
}

constructor(home) {
this.home = home;
}
}

export async function ingest(opts = {}) {
return withHome(opts.ledgerHome, async () => ingestAll());
}

export async function summary(opts = {}) {
return withHome(opts.ledgerHome, async () => {
const q = { sessionId: opts.session, project: opts.project, since: opts.since };
const turns = await queryAll(q);
const pricing = await loadPricing();
const byTool = new Map();
const byModel = new Map();
let totalTokens = 0;
let totalCost = 0;

for (const t of turns) {
const c = costForTurn(t, pricing)?.total ?? 0;
const usage =
t.usage.input +
t.usage.output +
t.usage.reasoning +
t.usage.cacheRead +
t.usage.cacheCreate5m +
t.usage.cacheCreate1h;
totalTokens += usage;
totalCost += c;

const model = byModel.get(t.model) ?? { model: t.model, tokens: 0, cost: 0 };
model.tokens += usage;
model.cost += c;
byModel.set(t.model, model);

for (const call of t.toolCalls) {
const tool = byTool.get(call.name) ?? { tool: call.name, tokens: 0, cost: 0, count: 0 };
tool.tokens += usage;
tool.cost += c;
tool.count += 1;
byTool.set(call.name, tool);
}
}

return { totalTokens, totalCost, byTool: [...byTool.values()], byModel: [...byModel.values()] };
});
}

export async function hotspots(opts = {}) {
return withHome(opts.ledgerHome, async () => {
const turns = await queryAll({ sessionId: opts.session });
const userTurns = await queryUserTurns({ sessionId: opts.session });
const attribution = attributeHotspots({ turns, userTurns });

if (!opts.patterns || opts.patterns.length === 0) return attribution;

const detected = detectPatterns({ turns, userTurns, hotspots: attribution });
return findingsFromPatterns(detected).filter((f) => opts.patterns.includes(f.kind));
});
}
34 changes: 34 additions & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@relayburn/sdk",
"version": "1.5.0",
"description": "Embeddable Relayburn SDK for in-process ingest, summary, and hotspots queries",
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",
"files": [
"index.js",
"index.d.ts",
"README.md",
"CHANGELOG.md",
"package.json"
],
"scripts": {
"build": "node -e \"process.exit(0)\""
},
"engines": {
"node": ">=22"
},
"dependencies": {
"@relayburn/analyze": "workspace:*",
"@relayburn/cli": "workspace:*",
"@relayburn/ledger": "workspace:*"
},
"repository": {
"type": "git",
"url": "https://github.com/AgentWorkforce/burn",
"directory": "packages/sdk"
},
"publishConfig": {
"access": "public"
}
}
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.