🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@breadcrumb-sh/core

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@breadcrumb-sh/core - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+249
dist/index-S95nStiR.d.mts
import { S as TraceSummary, _ as SpanKind, a as DatabaseAdapter, b as Stats, f as Page, g as SessionSummary, h as SchemaState, i as CostSummary, m as RunSummary, o as Dialect, r as CostQueryOptions, s as ListOptions, u as MigrationPlan, v as SpanRecord, x as TraceFilter } from "./types-BL3Zics_.mjs";
import { Tracer } from "@opentelemetry/api";
import { ReadableSpan, SpanProcessor } from "@opentelemetry/sdk-trace-base";
//#region src/otel/pipeline.d.ts
/** Matches the AI SDK's telemetry metadata constraint (OTel AttributeValue). */
type TelemetryMetadataValue = string | number | boolean | string[] | number[] | boolean[];
interface TelemetryOptions {
/** Names the operation. Every span of the call carries it, so a run reads as
* what it does rather than as `ai.streamText`, wherever it sits in a trace. */
functionId?: string;
/** Your app's end-user id — groups and filters traces by who ran them. */
userId?: string;
/** Groups related runs into one conversation/session. */
sessionId?: string;
metadata?: Record<string, TelemetryMetadataValue>;
recordInputs?: boolean;
recordOutputs?: boolean;
}
/** The settings object the Vercel AI SDK expects for experimental_telemetry. */
interface TelemetrySettings {
isEnabled: true;
tracer: Tracer;
functionId?: string;
metadata?: Record<string, TelemetryMetadataValue>;
recordInputs?: boolean;
recordOutputs?: boolean;
}
//#endregion
//#region src/pricing.d.ts
/** USD per 1M tokens, as declared by your app. breadcrumb ships no prices. */
interface ModelPrice {
input: number;
output: number;
/** Price per 1M cache-read tokens. Defaults to `input` (no discount assumed). */
cachedInput?: number;
/** Price per 1M cache-write tokens. Defaults to `input` (no premium assumed). */
cacheWrite?: number;
}
/**
* Your model prices, keyed by model name. Keys match as lowercase substrings
* of the span's model, longest key wins (so "claude-opus" beats "claude").
*/
type PricingTable = Record<string, ModelPrice>;
type Pricing = PricingTable | false;
//#endregion
//#region src/retention.d.ts
interface RetentionOptions {
/** Window for environments without an explicit entry. Default: "90d". */
default?: string;
/** Per-environment overrides, e.g. { development: "7d" }. */
environments?: Record<string, string>;
/** "auto" (default): bounded sweeps piggyback on ingest. "manual": only bc.api.runRetention(). */
sweep?: "auto" | "manual";
}
//#endregion
//#region src/mcp/config.d.ts
interface McpOptions {
/**
* What MCP clients register this server as. Defaults to "breadcrumb", or
* "breadcrumb-local" when reached over a loopback address, so a developer can
* connect their local and deployed instances at once without the second
* `mcp add` overwriting the first. Set it explicitly when you run more than
* two, e.g. "breadcrumb-staging".
*/
name?: string;
/**
* Hide the captured prompt/completion payloads from the agent. Everything else
* — timings, tokens, cost, model, status, errors — stays queryable, so an
* agent can still diagnose a failing run without reading user content.
*/
hidePayloads?: boolean;
}
//#endregion
//#region src/router.d.ts
type AuthorizeFn = (request: Request) => boolean | Response | undefined | null | Promise<boolean | Response | undefined | null>;
//#endregion
//#region src/trace.d.ts
interface SpanAttrs {
/** The operation this span belongs to, for attributing cost to a function
* the way `bc.telemetry({ functionId })` does. Defaults to nothing: a manual
* span is attributed to its run's root name. */
functionId?: string;
kind?: SpanKind;
model?: string;
provider?: string;
inputTokens?: number;
outputTokens?: number;
/** Cache-read tokens (subset of inputTokens). */
cachedInputTokens?: number;
/** Cache-write tokens (subset of inputTokens). */
cacheWriteTokens?: number;
/** Reasoning/thinking tokens (subset of outputTokens). */
reasoningTokens?: number;
cost?: number;
input?: unknown;
output?: unknown;
metadata?: Record<string, unknown>;
}
interface TraceAttrs {
functionId?: string;
userId?: string;
sessionId?: string;
metadata?: Record<string, unknown>;
}
interface SpanContext {
/** Attach/override attributes on the current span (tokens, model, io, ...). */
set(attrs: SpanAttrs): void;
/** Run a nested child span. A thrown error marks the span failed and rethrows. */
span<T>(name: string, fn: (s: SpanContext) => T | Promise<T>): Promise<T>;
span<T>(name: string, attrs: SpanAttrs, fn: (s: SpanContext) => T | Promise<T>): Promise<T>;
}
type TraceFn = {
<T>(name: string, fn: (t: SpanContext) => T | Promise<T>): Promise<T>;
<T>(name: string, attrs: TraceAttrs, fn: (t: SpanContext) => T | Promise<T>): Promise<T>;
};
//#endregion
//#region src/db/ddl.d.ts
/**
* Diff the current schema against a live database's state and return the DDL to
* reconcile it. The single source of truth for both `migrate()` (which executes
* the statements) and `generate` (which writes them to a file).
*/
declare function planMigration(dialect: Dialect, state: SchemaState): MigrationPlan;
/** The empty state — plans a full, fresh schema without a database connection. */
declare const EMPTY_SCHEMA_STATE: SchemaState;
/** Render a plan as a reviewable `.sql` migration file. */
declare function renderMigrationSql(dialect: Dialect, plan: MigrationPlan, generatedAt: string): string;
//#endregion
//#region src/index.d.ts
interface BreadcrumbOptions {
/** Database adapter, e.g. sqlite(".breadcrumb/dev.db") from @breadcrumb-sh/core/adapters */
database: DatabaseAdapter;
/** Where the handler is mounted, e.g. "/admin/traces". Default: "/breadcrumb" */
basePath?: string;
/** Stamped on every span. Default: VERCEL_ENV ?? NODE_ENV ?? "development" */
environment?: string;
/**
* Enables the HTTP ingest endpoints for external services (OTLP later).
* Omit entirely if only this app writes traces — the endpoints then 404.
*/
ingest?: {
apiKey: string;
};
/**
* Retention windows per environment. Defaults: 90d, development 7d.
* Sweeps are bounded and piggyback on ingest — no cron needed.
*/
retention?: RetentionOptions;
/**
* Guards the UI/query routes (ingest routes use the API key instead).
* Return true to allow, false to 401, or a Response (e.g. a redirect).
* Alternative to wrapping the mount in middleware.
*/
authorize?: AuthorizeFn;
/**
* Your model prices (USD per 1M tokens), keyed by model name. When a span
* has tokens + model but no explicit cost, breadcrumb computes it from these.
* Omit to store only costs you set yourself — breadcrumb assumes no prices.
*/
pricing?: Pricing;
/**
* Scrub PII or trim payloads before storage. Runs on every span from every
* path (manual, AI SDK, external ingest) just before it's written. Mutate the
* span in place, or return a replacement.
*/
redact?: (span: SpanRecord) => SpanRecord | void;
/**
* Max characters kept for a span's captured input/output; longer payloads are
* truncated with a marker so one call can't write megabytes. Default 16384;
* set 0 to disable.
*/
maxPayloadChars?: number;
/**
* "batch" (default): buffer spans and export every ~2s — best for a
* long-running server. "sync": export each span as it ends, for serverless or
* edge where a final flush isn't guaranteed. Either way, `await bc.flush()`
* (or `waitUntil(bc.flush())`) before a serverless function returns.
*/
flushMode?: "batch" | "sync";
/**
* Filters what `bc.spanProcessor` keeps when it's registered on a provider you
* own. By default only spans breadcrumb can read are stored (`ai.*`,
* `gen_ai.*`, `breadcrumb.*`); return true for more to keep your own
* instrumentation's spans alongside them.
*/
shouldExport?: (span: ReadableSpan) => boolean;
/**
* "auto" (default): create/upgrade the schema on first use — great for local
* development. "manual": never run DDL at runtime; you own migrations, applied
* with `breadcrumb migrate` or `breadcrumb generate` plus your tooling.
*/
migrations?: "auto" | "manual";
/**
* Tunes the MCP endpoint at `basePath + /api/mcp`, where a coding agent reads
* your traces. Not a switch: the endpoint is always mounted, but it is
* unreachable until someone mints a key from the dashboard's MCP tab, which
* `authorize` already guards. The agent gets policy-gated read access to the
* span table only — no other table in your database, and no writes.
*/
mcp?: McpOptions;
}
interface Breadcrumb {
/** Fetch-native handler: mount it, wrapped in your own auth. */
handler: (request: Request) => Promise<Response>;
/** Manual tracing: bc.trace("name", { userId }, async (t) => { ... t.span(...) }) */
trace: TraceFn;
/** Preconfigured experimental_telemetry settings for the Vercel AI SDK. */
telemetry: (options?: TelemetryOptions) => TelemetrySettings;
/**
* An OpenTelemetry span processor for apps that already own a TracerProvider
* (@vercel/otel, NodeSDK, Sentry). Register it and model spans reach breadcrumb
* without threading `bc.telemetry()` through every call.
*/
readonly spanProcessor: SpanProcessor;
/** Flush buffered spans (serverless: call before the runtime freezes). */
flush: () => Promise<void>;
/**
* Programmatic queries + ingest, callable server-side without HTTP — the
* headless surface for building a custom admin UI over your own data.
*/
api: {
/** Traces (one per run), newest first, filtered + keyset-paginated. */
listTraces(options?: ListOptions): Promise<Page<TraceSummary>>;
/** Sessions (traces grouped by sessionId), by last activity, paginated. */
listSessions(options?: ListOptions): Promise<Page<SessionSummary>>;
listRuns(options: {
sessionKey: string;
}): Promise<RunSummary[]>;
getTrace(options: {
id: string;
}): Promise<SpanRecord[]>;
getSpan(options: {
id: string;
}): Promise<SpanRecord | null>;
listEnvironments(): Promise<string[]>;
costSummary(options?: CostQueryOptions): Promise<CostSummary>;
/** Headline numbers (runs, error rate, cost, latency) over a filter. */
stats(options?: TraceFilter): Promise<Stats>;
ingestSpans(options: {
spans: SpanRecord[];
}): Promise<void>;
/** One bounded retention batch (for cron/manual sweeping); returns rows deleted. */
runRetention(): Promise<number>;
};
options: Required<Pick<BreadcrumbOptions, "basePath" | "environment">> & BreadcrumbOptions;
}
declare function breadcrumb(options: BreadcrumbOptions): Breadcrumb;
//#endregion
export { TelemetrySettings as _, planMigration as a, SpanContext as c, AuthorizeFn as d, RetentionOptions as f, TelemetryOptions as g, PricingTable as h, EMPTY_SCHEMA_STATE as i, TraceAttrs as l, Pricing as m, BreadcrumbOptions as n, renderMigrationSql as o, ModelPrice as p, breadcrumb as r, SpanAttrs as s, Breadcrumb as t, TraceFn as u };
+1
-1
import { S as TraceSummary, _ as SpanKind, a as DatabaseAdapter, b as Stats, c as ListTracesOptions, d as MigrationResult, f as Page, g as SessionSummary, h as SchemaState, i as CostSummary, m as RunSummary, n as CostGroup, o as Dialect, p as RetentionRule, r as CostQueryOptions, s as ListOptions, t as CostDatum, u as MigrationPlan, v as SpanRecord, x as TraceFilter, y as SpanStatus } from "./types-BL3Zics_.mjs";
import { _ as TelemetrySettings, a as planMigration, c as SpanContext, d as AuthorizeFn, f as RetentionOptions, g as TelemetryOptions, h as PricingTable, i as EMPTY_SCHEMA_STATE, l as TraceAttrs, m as Pricing, n as BreadcrumbOptions, o as renderMigrationSql, p as ModelPrice, r as breadcrumb, s as SpanAttrs, t as Breadcrumb, u as TraceFn } from "./index-Bl8Kc3Jd.mjs";
import { _ as TelemetrySettings, a as planMigration, c as SpanContext, d as AuthorizeFn, f as RetentionOptions, g as TelemetryOptions, h as PricingTable, i as EMPTY_SCHEMA_STATE, l as TraceAttrs, m as Pricing, n as BreadcrumbOptions, o as renderMigrationSql, p as ModelPrice, r as breadcrumb, s as SpanAttrs, t as Breadcrumb, u as TraceFn } from "./index-S95nStiR.mjs";
export { type AuthorizeFn, Breadcrumb, BreadcrumbOptions, type CostDatum, type CostGroup, type CostQueryOptions, type CostSummary, type DatabaseAdapter, type Dialect, EMPTY_SCHEMA_STATE, type ListOptions, type ListTracesOptions, type MigrationPlan, type MigrationResult, type ModelPrice, type Page, type Pricing, type PricingTable, type RetentionOptions, type RetentionRule, type RunSummary, type SchemaState, type SessionSummary, type SpanAttrs, type SpanContext, type SpanKind, type SpanRecord, type SpanStatus, type Stats, type TelemetryOptions, type TelemetrySettings, type TraceAttrs, type TraceFilter, type TraceFn, type TraceSummary, breadcrumb, planMigration, renderMigrationSql };

@@ -12,12 +12,48 @@ import { c as clampLimit, f as pageOf, n as planMigration, r as renderMigrationSql, t as EMPTY_SCHEMA_STATE } from "./ddl-C4JrQPLV.mjs";

* results can be large; without a cap a single span could write megabytes into
* the user's database. Returns the value unchanged when small, or a truncated
* marker string when it exceeds `maxChars`. `maxChars <= 0` disables the cap.
* the user's database. `maxChars <= 0` disables the cap.
*
* The budget is spent on the long strings inside the payload rather than on the
* payload as a whole, so a capped message array is still an array of messages
* with shortened text. Flattening it to one truncated string saves the same
* bytes and costs the reader a conversation they can no longer read.
*/
function capPayload(value, maxChars) {
if (value == null || maxChars <= 0) return value;
const s = typeof value === "string" ? value : safeStringify(value);
if (s.length <= maxChars) return value;
return `${s.slice(0, maxChars)}…[breadcrumb: truncated ${s.length - maxChars} more chars]`;
if (typeof value === "string") return capString(value, maxChars);
if (measure(value) <= maxChars) return value;
const floor = Math.min(MIN_ALLOWANCE, maxChars);
let allowance = maxChars;
while (allowance > floor) {
allowance = Math.max(Math.floor(allowance / 2), floor);
const capped = capStrings(value, allowance);
if (measure(capped) <= maxChars) return capped;
}
return capString(stringify(value), maxChars);
}
function safeStringify(value) {
const MIN_ALLOWANCE = 80;
const marker = (dropped) => `…[breadcrumb: truncated ${dropped} more chars]`;
/**
* The marker comes out of the budget rather than being added on top, so capping
* a string never returns more than `maxChars` — and never returns more than it
* was handed, which the naive version did for anything just over the line.
*/
function capString(value, maxChars) {
if (value.length <= maxChars) return value;
const room = Math.max(maxChars - marker(value.length).length, 0);
return value.slice(0, room) + marker(value.length - room);
}
function capStrings(value, allowance) {
if (typeof value === "string") return capString(value, allowance);
if (Array.isArray(value)) return value.map((v) => capStrings(v, allowance));
if (typeof value === "object" && value !== null) {
const out = {};
for (const [key, v] of Object.entries(value)) out[key] = capStrings(v, allowance);
return out;
}
return value;
}
function measure(value) {
return stringify(value).length;
}
function stringify(value) {
try {

@@ -228,9 +264,46 @@ return JSON.stringify(value) ?? String(value);

};
const DIALECTS = [
"ai.",
"gen_ai.",
"breadcrumb."
];
/** On a shared provider the processor sees every span in the app — HTTP, database,
* filesystem. Default to the ones breadcrumb can actually read, so registering it
* doesn't turn the trace table into a general-purpose span dump. */
function isModelSpan(span) {
for (const key of Object.keys(span.attributes)) if (DIALECTS.some((prefix) => key.startsWith(prefix))) return true;
return false;
}
var FilteredSpanProcessor = class {
inner;
shouldExport;
constructor(inner, shouldExport) {
this.inner = inner;
this.shouldExport = shouldExport;
}
onStart(span, parentContext) {
this.inner.onStart(span, parentContext);
}
onEnd(span) {
if (this.shouldExport(span)) this.inner.onEnd(span);
}
forceFlush() {
return this.inner.forceFlush();
}
shutdown() {
return this.inner.shutdown();
}
};
function createTelemetryPipeline(deps) {
ensureContextManager();
const exporter = new AdapterSpanExporter(deps.write, deps.environment);
const provider = new BasicTracerProvider({ spanProcessors: [deps.flushMode === "sync" ? new SimpleSpanProcessor(exporter) : new BatchSpanProcessor(exporter, { scheduledDelayMillis: 2e3 })] });
const newProcessor = () => deps.flushMode === "sync" ? new SimpleSpanProcessor(exporter) : new BatchSpanProcessor(exporter, { scheduledDelayMillis: 2e3 });
const provider = new BasicTracerProvider({ spanProcessors: [newProcessor()] });
const tracer = provider.getTracer("breadcrumb");
let external = null;
return {
tracer,
get spanProcessor() {
return external ??= new FilteredSpanProcessor(newProcessor(), deps.shouldExport ?? isModelSpan);
},
telemetry({ userId, sessionId, metadata, ...rest } = {}) {

@@ -247,4 +320,8 @@ const merged = { ...metadata };

},
flush: () => provider.forceFlush(),
shutdown: () => provider.shutdown()
async flush() {
await Promise.all([provider.forceFlush(), external?.forceFlush()]);
},
async shutdown() {
await Promise.all([provider.shutdown(), external?.shutdown()]);
}
};

@@ -805,3 +882,4 @@ }

write: (spans) => api.ingestSpans({ spans }),
flushMode: options.flushMode
flushMode: options.flushMode,
shouldExport: options.shouldExport
});

@@ -832,2 +910,5 @@ const trace = createTraceFn(pipeline.tracer);

telemetry: pipeline.telemetry,
get spanProcessor() {
return pipeline.spanProcessor;
},
flush: pipeline.flush,

@@ -834,0 +915,0 @@ api,

@@ -15,2 +15,9 @@ import { v as SpanRecord } from "../types-BL3Zics_.mjs";

declare function displayName(span: SpanRecord): string;
/**
* When the run last did anything, as a wall-clock timestamp. A trace that ended
* seconds ago is probably still being written to — spans are only stored once
* they end, so an in-flight run arrives a piece at a time — which is what tells
* a UI whether it's worth polling for more.
*/
declare function lastActivity(spans: SpanRecord[]): number;
/** A row in the denoised flow view: a span, or a tucked-away run of trivia. */

@@ -42,2 +49,9 @@ type FlowRow = {

/**
* Flat rows in the order the work actually happened. A tree groups a span with
* its parent, which buries the fact that two branches ran at the same time;
* sorting the denoised set by start time puts concurrent steps side by side,
* which is the whole point of reading a run on a timeline.
*/
declare function timelineRows(spans: SpanRecord[]): FlowRow[];
/**
* The three steps worth opening a run on, derived from the spans already

@@ -68,3 +82,3 @@ * loaded: where it broke, where the time went, where the money went.

*/
type TraceViewMode = "flow" | "full";
type TraceViewMode = "flow" | "full" | "timeline";
declare const extent: (span: SpanRecord) => number;

@@ -77,2 +91,7 @@ /** A row as it appears on screen, with expanded minor groups already spliced in. */

children: SpanRecord[];
/** Whether this row has rows nested under it — i.e. it can be collapsed. */
hasChildren: boolean;
collapsed: boolean;
/** Rows hidden underneath, at any depth. 0 unless collapsed. */
hiddenCount: number;
} | {

@@ -96,2 +115,8 @@ type: "minor";

roots: SpanRecord[];
/**
* Wall-clock zero for the run's bars. Not the root's start: an orphan whose
* parent never arrived can begin before it, and measuring from the root would
* push that span off the left of the track.
*/
origin: number;
/** The run's extent, floored at 1 so it is always safe to divide by. */

@@ -110,5 +135,13 @@ total: number;

}
/**
* Ids to collapse when a trace first opens: every row below the top level that
* has rows under it. A run then reads as its roots and the steps they ran, with
* depth one click away rather than fifty rows deep on arrival.
*/
declare function defaultCollapsed(spans: SpanRecord[], mode?: TraceViewMode): Set<string>;
declare function traceModel(spans: SpanRecord[], options?: {
mode?: TraceViewMode;
openMinor?: ReadonlySet<string>;
/** Span ids whose descendants are hidden. See `defaultCollapsed`. */
collapsed?: ReadonlySet<string>;
}): TraceModel;

@@ -152,2 +185,2 @@ /** Open a run on its worst moment rather than on a collapsed root. */

//#endregion
export { type ChatMessage, type FlowRow, type HeatLevel, type Hotspots, type TraceModel, type TraceRow, type TraceTotals, type TraceViewMode, asMessages, defaultSelection, displayName, extent, flowRows, fmtAgo, fmtCompact, fmtCost, fmtInt, fmtMoney, fmtMs, fmtTime, fmtTokens, fullRows, heatLevel, hotspots, keyboardTarget, preview, selfIntervals, selfTime, traceModel };
export { type ChatMessage, type FlowRow, type HeatLevel, type Hotspots, type TraceModel, type TraceRow, type TraceTotals, type TraceViewMode, asMessages, defaultCollapsed, defaultSelection, displayName, extent, flowRows, fmtAgo, fmtCompact, fmtCost, fmtInt, fmtMoney, fmtMs, fmtTime, fmtTokens, fullRows, heatLevel, hotspots, keyboardTarget, lastActivity, preview, selfIntervals, selfTime, timelineRows, traceModel };

@@ -60,2 +60,13 @@ //#region src/kit/tree.ts

/**
* When the run last did anything, as a wall-clock timestamp. A trace that ended
* seconds ago is probably still being written to — spans are only stored once
* they end, so an in-flight run arrives a piece at a time — which is what tells
* a UI whether it's worth polling for more.
*/
function lastActivity(spans) {
let last = 0;
for (const s of spans) last = Math.max(last, s.endTime ?? s.startTime);
return last;
}
/**
* Denoised view of a trace. Instrumentation wraps every model call in a

@@ -138,2 +149,20 @@ * pass-through span (`ai.streamText` around `ai.streamText.doStream`), which

}
/**
* Flat rows in the order the work actually happened. A tree groups a span with
* its parent, which buries the fact that two branches ran at the same time;
* sorting the denoised set by start time puts concurrent steps side by side,
* which is the whole point of reading a run on a timeline.
*/
function timelineRows(spans) {
const byParent = groupByParent(spans);
const flat = [];
for (const row of flowRows(spans)) if (row.type === "span") flat.push(row.span);
else flat.push(...row.spans);
return flat.sort((a, b) => a.startTime - b.startTime || (b.endTime ?? 0) - (a.endTime ?? 0)).map((span) => ({
type: "span",
span,
depth: 0,
children: byParent.get(span.id) ?? []
}));
}
function hotspots(spans) {

@@ -235,5 +264,26 @@ const ids = new Set(spans.map((s) => s.id));

const NO_OPEN = /* @__PURE__ */ new Set();
const ROWS_FOR = {
flow: flowRows,
full: fullRows,
timeline: timelineRows
};
/**
* Ids to collapse when a trace first opens: every row below the top level that
* has rows under it. A run then reads as its roots and the steps they ran, with
* depth one click away rather than fifty rows deep on arrival.
*/
function defaultCollapsed(spans, mode = "flow") {
const source = ROWS_FOR[mode](spans);
const out = /* @__PURE__ */ new Set();
for (const [i, row] of source.entries()) {
if (row.type !== "span" || row.depth < 1) continue;
const next = source[i + 1];
if (next !== void 0 && next.depth > row.depth) out.add(row.span.id);
}
return out;
}
function traceModel(spans, options = {}) {
const mode = options.mode ?? "flow";
const openMinor = options.openMinor ?? NO_OPEN;
const collapsed = options.collapsed ?? NO_OPEN;
const byId = new Map(spans.map((s) => [s.id, s]));

@@ -250,2 +300,5 @@ const childrenById = /* @__PURE__ */ new Map();

const root = roots[0] ?? null;
let origin = Infinity;
for (const s of spans) origin = Math.min(origin, s.startTime);
if (!Number.isFinite(origin)) origin = 0;
const total = spans.length > 0 ? traceExtent(spans) : 1;

@@ -259,18 +312,16 @@ const spots = spans.length > 0 ? hotspots(spans) : null;

}
const source = spans.length === 0 ? [] : mode === "flow" ? flowRows(spans) : fullRows(spans);
const rows = [];
const order = [];
const source = spans.length === 0 ? [] : ROWS_FOR[mode](spans);
const expanded = [];
for (const row of source) {
if (row.type === "span") {
rows.push({
type: "span",
span: row.span,
depth: row.depth,
children: row.children
expanded.push({
...row,
hasChildren: false,
collapsed: false,
hiddenCount: 0
});
order.push(row.span.id);
continue;
}
const open = openMinor.has(row.parentId);
rows.push({
expanded.push({
type: "minor",

@@ -283,11 +334,37 @@ parentId: row.parentId,

if (!open) continue;
for (const s of row.spans) {
rows.push({
type: "span",
span: s,
depth: row.depth,
children: []
});
order.push(s.id);
for (const s of row.spans) expanded.push({
type: "span",
span: s,
depth: row.depth,
children: [],
hasChildren: false,
collapsed: false,
hiddenCount: 0
});
}
const rows = [];
const order = [];
let hidingUnder = null;
for (const [i, row] of expanded.entries()) {
if (hidingUnder && row.depth > hidingUnder.depth) {
hidingUnder.row.hiddenCount++;
continue;
}
hidingUnder = null;
if (row.type === "minor") {
rows.push(row);
continue;
}
const next = expanded[i + 1];
const visible = {
...row,
hasChildren: next !== void 0 && next.depth > row.depth
};
visible.collapsed = visible.hasChildren && collapsed.has(row.span.id);
rows.push(visible);
order.push(row.span.id);
if (visible.collapsed) hidingUnder = {
depth: row.depth,
row: visible
};
}

@@ -311,2 +388,3 @@ let cost = 0;

roots,
origin,
total,

@@ -405,2 +483,2 @@ maxSelf,

//#endregion
export { asMessages, defaultSelection, displayName, extent, flowRows, fmtAgo, fmtCompact, fmtCost, fmtInt, fmtMoney, fmtMs, fmtTime, fmtTokens, fullRows, heatLevel, hotspots, keyboardTarget, preview, selfIntervals, selfTime, traceModel };
export { asMessages, defaultCollapsed, defaultSelection, displayName, extent, flowRows, fmtAgo, fmtCompact, fmtCost, fmtInt, fmtMoney, fmtMs, fmtTime, fmtTokens, fullRows, heatLevel, hotspots, keyboardTarget, lastActivity, preview, selfIntervals, selfTime, timelineRows, traceModel };

@@ -1,2 +0,2 @@

import { t as Breadcrumb } from "./index-Bl8Kc3Jd.mjs";
import { t as Breadcrumb } from "./index-S95nStiR.mjs";
//#region src/next.d.ts

@@ -3,0 +3,0 @@ type Handler = (request: Request) => Promise<Response>;

@@ -1,2 +0,2 @@

import { t as Breadcrumb } from "./index-Bl8Kc3Jd.mjs";
import { t as Breadcrumb } from "./index-S95nStiR.mjs";
import { IncomingMessage, ServerResponse } from "node:http";

@@ -3,0 +3,0 @@ //#region src/node.d.ts

{
"name": "@breadcrumb-sh/core",
"version": "0.1.0",
"version": "0.2.0",
"description": "Embeddable LLM tracing for TypeScript apps. Your database, your deployment, your UI.",

@@ -5,0 +5,0 @@ "license": "MIT",

@@ -78,2 +78,14 @@ # @breadcrumb-sh/core

**Your own OpenTelemetry setup.** If the app already has a tracer provider
(`@vercel/otel`, `NodeSDK`, Sentry), register `bc.spanProcessor` on it and model
spans reach breadcrumb without threading `bc.telemetry()` through every call:
```ts
registerOTel({ serviceName: "app", spanProcessors: [bc.spanProcessor] });
```
It stores only spans breadcrumb can read (`ai.*`, `gen_ai.*`, `breadcrumb.*`)
unless `shouldExport` says otherwise, so a shared provider's HTTP and filesystem
spans don't land in the trace table.
**Manual tracing.** `bc.trace(name, attrs?, fn)`, with nested `t.span(...)`:

@@ -80,0 +92,0 @@

import { S as TraceSummary, _ as SpanKind, a as DatabaseAdapter, b as Stats, f as Page, g as SessionSummary, h as SchemaState, i as CostSummary, m as RunSummary, o as Dialect, r as CostQueryOptions, s as ListOptions, u as MigrationPlan, v as SpanRecord, x as TraceFilter } from "./types-BL3Zics_.mjs";
import { Tracer } from "@opentelemetry/api";
//#region src/otel/pipeline.d.ts
/** Matches the AI SDK's telemetry metadata constraint (OTel AttributeValue). */
type TelemetryMetadataValue = string | number | boolean | string[] | number[] | boolean[];
interface TelemetryOptions {
/** Names the operation. Every span of the call carries it, so a run reads as
* what it does rather than as `ai.streamText`, wherever it sits in a trace. */
functionId?: string;
/** Your app's end-user id — groups and filters traces by who ran them. */
userId?: string;
/** Groups related runs into one conversation/session. */
sessionId?: string;
metadata?: Record<string, TelemetryMetadataValue>;
recordInputs?: boolean;
recordOutputs?: boolean;
}
/** The settings object the Vercel AI SDK expects for experimental_telemetry. */
interface TelemetrySettings {
isEnabled: true;
tracer: Tracer;
functionId?: string;
metadata?: Record<string, TelemetryMetadataValue>;
recordInputs?: boolean;
recordOutputs?: boolean;
}
//#endregion
//#region src/pricing.d.ts
/** USD per 1M tokens, as declared by your app. breadcrumb ships no prices. */
interface ModelPrice {
input: number;
output: number;
/** Price per 1M cache-read tokens. Defaults to `input` (no discount assumed). */
cachedInput?: number;
/** Price per 1M cache-write tokens. Defaults to `input` (no premium assumed). */
cacheWrite?: number;
}
/**
* Your model prices, keyed by model name. Keys match as lowercase substrings
* of the span's model, longest key wins (so "claude-opus" beats "claude").
*/
type PricingTable = Record<string, ModelPrice>;
type Pricing = PricingTable | false;
//#endregion
//#region src/retention.d.ts
interface RetentionOptions {
/** Window for environments without an explicit entry. Default: "90d". */
default?: string;
/** Per-environment overrides, e.g. { development: "7d" }. */
environments?: Record<string, string>;
/** "auto" (default): bounded sweeps piggyback on ingest. "manual": only bc.api.runRetention(). */
sweep?: "auto" | "manual";
}
//#endregion
//#region src/mcp/config.d.ts
interface McpOptions {
/**
* What MCP clients register this server as. Defaults to "breadcrumb", or
* "breadcrumb-local" when reached over a loopback address, so a developer can
* connect their local and deployed instances at once without the second
* `mcp add` overwriting the first. Set it explicitly when you run more than
* two, e.g. "breadcrumb-staging".
*/
name?: string;
/**
* Hide the captured prompt/completion payloads from the agent. Everything else
* — timings, tokens, cost, model, status, errors — stays queryable, so an
* agent can still diagnose a failing run without reading user content.
*/
hidePayloads?: boolean;
}
//#endregion
//#region src/router.d.ts
type AuthorizeFn = (request: Request) => boolean | Response | undefined | null | Promise<boolean | Response | undefined | null>;
//#endregion
//#region src/trace.d.ts
interface SpanAttrs {
/** The operation this span belongs to, for attributing cost to a function
* the way `bc.telemetry({ functionId })` does. Defaults to nothing: a manual
* span is attributed to its run's root name. */
functionId?: string;
kind?: SpanKind;
model?: string;
provider?: string;
inputTokens?: number;
outputTokens?: number;
/** Cache-read tokens (subset of inputTokens). */
cachedInputTokens?: number;
/** Cache-write tokens (subset of inputTokens). */
cacheWriteTokens?: number;
/** Reasoning/thinking tokens (subset of outputTokens). */
reasoningTokens?: number;
cost?: number;
input?: unknown;
output?: unknown;
metadata?: Record<string, unknown>;
}
interface TraceAttrs {
functionId?: string;
userId?: string;
sessionId?: string;
metadata?: Record<string, unknown>;
}
interface SpanContext {
/** Attach/override attributes on the current span (tokens, model, io, ...). */
set(attrs: SpanAttrs): void;
/** Run a nested child span. A thrown error marks the span failed and rethrows. */
span<T>(name: string, fn: (s: SpanContext) => T | Promise<T>): Promise<T>;
span<T>(name: string, attrs: SpanAttrs, fn: (s: SpanContext) => T | Promise<T>): Promise<T>;
}
type TraceFn = {
<T>(name: string, fn: (t: SpanContext) => T | Promise<T>): Promise<T>;
<T>(name: string, attrs: TraceAttrs, fn: (t: SpanContext) => T | Promise<T>): Promise<T>;
};
//#endregion
//#region src/db/ddl.d.ts
/**
* Diff the current schema against a live database's state and return the DDL to
* reconcile it. The single source of truth for both `migrate()` (which executes
* the statements) and `generate` (which writes them to a file).
*/
declare function planMigration(dialect: Dialect, state: SchemaState): MigrationPlan;
/** The empty state — plans a full, fresh schema without a database connection. */
declare const EMPTY_SCHEMA_STATE: SchemaState;
/** Render a plan as a reviewable `.sql` migration file. */
declare function renderMigrationSql(dialect: Dialect, plan: MigrationPlan, generatedAt: string): string;
//#endregion
//#region src/index.d.ts
interface BreadcrumbOptions {
/** Database adapter, e.g. sqlite(".breadcrumb/dev.db") from @breadcrumb-sh/core/adapters */
database: DatabaseAdapter;
/** Where the handler is mounted, e.g. "/admin/traces". Default: "/breadcrumb" */
basePath?: string;
/** Stamped on every span. Default: VERCEL_ENV ?? NODE_ENV ?? "development" */
environment?: string;
/**
* Enables the HTTP ingest endpoints for external services (OTLP later).
* Omit entirely if only this app writes traces — the endpoints then 404.
*/
ingest?: {
apiKey: string;
};
/**
* Retention windows per environment. Defaults: 90d, development 7d.
* Sweeps are bounded and piggyback on ingest — no cron needed.
*/
retention?: RetentionOptions;
/**
* Guards the UI/query routes (ingest routes use the API key instead).
* Return true to allow, false to 401, or a Response (e.g. a redirect).
* Alternative to wrapping the mount in middleware.
*/
authorize?: AuthorizeFn;
/**
* Your model prices (USD per 1M tokens), keyed by model name. When a span
* has tokens + model but no explicit cost, breadcrumb computes it from these.
* Omit to store only costs you set yourself — breadcrumb assumes no prices.
*/
pricing?: Pricing;
/**
* Scrub PII or trim payloads before storage. Runs on every span from every
* path (manual, AI SDK, external ingest) just before it's written. Mutate the
* span in place, or return a replacement.
*/
redact?: (span: SpanRecord) => SpanRecord | void;
/**
* Max characters kept for a span's captured input/output; longer payloads are
* truncated with a marker so one call can't write megabytes. Default 16384;
* set 0 to disable.
*/
maxPayloadChars?: number;
/**
* "batch" (default): buffer spans and export every ~2s — best for a
* long-running server. "sync": export each span as it ends, for serverless or
* edge where a final flush isn't guaranteed. Either way, `await bc.flush()`
* (or `waitUntil(bc.flush())`) before a serverless function returns.
*/
flushMode?: "batch" | "sync";
/**
* "auto" (default): create/upgrade the schema on first use — great for local
* development. "manual": never run DDL at runtime; you own migrations, applied
* with `breadcrumb migrate` or `breadcrumb generate` plus your tooling.
*/
migrations?: "auto" | "manual";
/**
* Tunes the MCP endpoint at `basePath + /api/mcp`, where a coding agent reads
* your traces. Not a switch: the endpoint is always mounted, but it is
* unreachable until someone mints a key from the dashboard's MCP tab, which
* `authorize` already guards. The agent gets policy-gated read access to the
* span table only — no other table in your database, and no writes.
*/
mcp?: McpOptions;
}
interface Breadcrumb {
/** Fetch-native handler: mount it, wrapped in your own auth. */
handler: (request: Request) => Promise<Response>;
/** Manual tracing: bc.trace("name", { userId }, async (t) => { ... t.span(...) }) */
trace: TraceFn;
/** Preconfigured experimental_telemetry settings for the Vercel AI SDK. */
telemetry: (options?: TelemetryOptions) => TelemetrySettings;
/** Flush buffered spans (serverless: call before the runtime freezes). */
flush: () => Promise<void>;
/**
* Programmatic queries + ingest, callable server-side without HTTP — the
* headless surface for building a custom admin UI over your own data.
*/
api: {
/** Traces (one per run), newest first, filtered + keyset-paginated. */
listTraces(options?: ListOptions): Promise<Page<TraceSummary>>;
/** Sessions (traces grouped by sessionId), by last activity, paginated. */
listSessions(options?: ListOptions): Promise<Page<SessionSummary>>;
listRuns(options: {
sessionKey: string;
}): Promise<RunSummary[]>;
getTrace(options: {
id: string;
}): Promise<SpanRecord[]>;
getSpan(options: {
id: string;
}): Promise<SpanRecord | null>;
listEnvironments(): Promise<string[]>;
costSummary(options?: CostQueryOptions): Promise<CostSummary>;
/** Headline numbers (runs, error rate, cost, latency) over a filter. */
stats(options?: TraceFilter): Promise<Stats>;
ingestSpans(options: {
spans: SpanRecord[];
}): Promise<void>;
/** One bounded retention batch (for cron/manual sweeping); returns rows deleted. */
runRetention(): Promise<number>;
};
options: Required<Pick<BreadcrumbOptions, "basePath" | "environment">> & BreadcrumbOptions;
}
declare function breadcrumb(options: BreadcrumbOptions): Breadcrumb;
//#endregion
export { TelemetrySettings as _, planMigration as a, SpanContext as c, AuthorizeFn as d, RetentionOptions as f, TelemetryOptions as g, PricingTable as h, EMPTY_SCHEMA_STATE as i, TraceAttrs as l, Pricing as m, BreadcrumbOptions as n, renderMigrationSql as o, ModelPrice as p, breadcrumb as r, SpanAttrs as s, Breadcrumb as t, TraceFn as u };