@vibecook/spaghetti-sdk

API reference

Index Claude Code and Codex data into one SQLite store, query with source-scoped reads, run full-text search, and subscribe to live updates. Same surface the CLI uses — version 0.5.17.

Install

Installs a platform-specific native binary via optional dependencies. If the binary is missing, the TypeScript engine takes over automatically — no flags required.

shell
npm install @vibecook/spaghetti-sdk
# or
pnpm add @vibecook/spaghetti-sdk
Peer deps: React 19 is optional and only required for @vibecook/spaghetti-sdk/react. Runtime needs Node ≥ 18 and better-sqlite3 (prebuilds for common platforms).

Quickstart

Create a service, initialize once (cold or warm start), query, then dispose.

app.ts
import {
  createSpaghettiService,
  createCodexSource,
} from '@vibecook/spaghetti-sdk';

const api = createSpaghettiService({
  additionalSources: [createCodexSource()],
});

api.onProgress((p) => {
  console.log(p.phase, p.message, p.current, p.total);
});

await api.initialize();

const projects = api.getProjectList();
const p = projects[0];
const sessions = api.getSessionList(p);
const page = api.getSessionMessages(
  sessions[0].projectSlug,
  sessions[0].sessionId,
  50,
  0,
  { sourceId: sessions[0].sourceId },
);
const hits = api.search({ text: 'refactor parser', limit: 20 });

await api.dispose(); // preferred over shutdown()

createSpaghettiService

Factory that wires FileService, shared SqliteService, parsers, ingest, and the public app service.

signature
function createSpaghettiService(
  options?: SpaghettiServiceOptions
): SpaghettiAPI
Option Type Default Description
claudeDir string ~/.claude Claude Code data root. Ignored when source is set.
source AgentSource Claude Code Primary agent adapter. Defaults to createClaudeCodeSource().
additionalSources AgentSource[] [] Extra agents into the same store (e.g. createCodexSource()). CLI auto-detects ~/.codex/sessions.
dbPath string ~/.spaghetti/cache/spaghetti-{rs,ts}.db SQLite index path. Per-engine default so switching engines does not force a re-ingest.
engine 'rs' | 'ts' resolved Pin ingest engine for this instance. Wins over env and config. Codex always uses the TS path.
live boolean false Start filesystem watchers and expose api.live (Claude + Codex when present).
dataService ClaudeCodeAgentDataService Inject a custom/mock implementation for tests.
errorSink ErrorSink console.warn Unified sink for live-pipeline and internal errors.

Multi-source (RFC 006)

One index, source_id column — not one DB per agent. Project PK is composite (source_id, slug). The project list aggregates agents that worked in the same directory; pass its stable project locator when drilling into sessions.

multi-source.ts
import {
  createSpaghettiService,
  createCodexSource,
  defaultCodexDir,
} from '@vibecook/spaghetti-sdk';

const api = createSpaghettiService({
  additionalSources: [createCodexSource({ rootDir: defaultCodexDir() })],
});
await api.initialize();

api.getSourceIds();                        // ['claude-code', 'codex']
api.getProjectList({ sourceId: 'codex' }); // Codex only

const p = api.getProjectList().find((x) => x.sourceIds.includes('codex'))!;
api.getSessionList(p); // sessions from every agent in this workspace
Codex tokens: official usage from token_count events; when missing, tiktoken estimates visible text and sets tokensEstimated: true (UI shows ~N).

Engine selection

Resolution order (first match wins). Per-instance engine on the factory is independent of process-wide resolution used when the option is omitted.

resolution order
1. createSpaghettiService({ engine: 'rs' | 'ts' })
2. SPAG_ENGINE=ts|rs
3. SPAG_NATIVE_INGEST=0|1   // legacy
4. ~/.spaghetti/config.json → { "engine": "rs" }
5. Default: rs
Two instances, one DB: avoid opening the same dbPath from two services in one process — risks SQLITE_BUSY. Use separate paths for parallel indexes.

Lifecycle

Initialize runs cold start (empty DB) or warm start (fingerprint diff). Always call dispose/shutdown when done.

Methods

initialize(): Promise<void>
async

Parse source data, write/update the SQLite index, build FTS. Emits progress via onProgress and fires onReady when complete.

shutdown(): void
sync

Fire-and-forget teardown: stop watchers, close the database. Prefer dispose() when you can await.

dispose(): Promise<void>
async

Awaitable teardown. Stops the live pipeline, drains in-flight writes, disposes subscribers, closes SQLite.

rebuildIndex(): Promise<{ durationMs: number }>
async

Force a full cold rebuild. Closes the DB, deletes its files, re-ingests from scratch via native Rust (with TS fallback). Use after schema bumps or when the index looks out of sync.

isReady(): boolean
sync

Whether the service finished initialize and can accept queries.

Projects & sessions

Primary navigation surface. Lists are sorted by recency.

Methods

getSourceIds(): string[]
sync

Distinct agent sources present in the index (e.g. ['claude-code', 'codex']).

getProjectList(options?): ProjectListItem[]
sync

Unique workspaces sorted by last active date. Each item includes its stable projectId, exact source/slug members, sourceIds, session counts, token totals, and tokensEstimated.

ParamTypeDescription
options.sourceIdstring?Optional filter to one agent source.
getSessionList(project, options?): SessionListItem[]
sync

Sessions for a project sorted by last update. Pass a ProjectListItem (or other ProjectLocator) for exact multi-agent lookup. A slug string remains available for legacy callers.

ParamTypeDescription
projectProjectReferenceStable project locator, or a legacy project slug string.
options.sourceIdstring?Optional agent scope for legacy slug calls.
getSessionMessages(projectSlug, sessionId, limit?, offset?, options?): MessagePage
sync

Paginated messages. Payloads preserve the source-native record (Claude JSONL or Codex rollout line). Optional sourceId for defense in depth.

ParamTypeDescription
projectSlugstringProject slug.
sessionIdstringSession UUID.
limitnumber?Page size (default 30).
offsetnumber?Row offset.
options.sourceIdstring?Optional agent scope.
getProjectMemory(project, options?): string | null
sync

Project MEMORY.md. Claude-only today — returns null when sourceId is not claude-code.

Artifacts

Plans, todos, memory, subagents, workflows, tool results, and teams.

Methods

getProjectMemory(project): string | null
sync

Raw MEMORY.md content for a project, or null if absent.

getSessionTodos(projectSlug, sessionId): unknown[]
sync

Todo items for a session (agent-scoped todo files under ~/.claude/todos/).

getSessionPlan(projectSlug, sessionId): unknown | null
sync

Plan linked to the session, if any.

getSessionTask(projectSlug, sessionId): unknown | null
sync

Task metadata (highwatermark / lock presence). Task item bodies are not fully ingested.

getToolResult(projectSlug, sessionId, toolUseId): string | null
sync

Persisted tool result text for a tool_use id.

getSessionSubagents(projectSlug, sessionId, options?): SubagentListItem[]
sync

Subagent metadata with normalized message counts and parent Task/Agent linkage. Pass { includeNested: true } to include workflow-nested agents.

getSubagentTimeline(projectSlug, sessionId, agentId, request): SubagentTimelinePage
sync

DB-filtered, independently paginated display rows for an inline subagent branch. Raw sidecar messages remain available through getSubagentMessages.

getSubagentMessages(projectSlug, sessionId, agentId, limit?, offset?): SubagentMessagePage
sync

Paginated messages from a subagent transcript.

getSessionWorkflows(projectSlug, sessionId): WorkflowListItem[]
sync

Agent-orchestration workflow runs for a session (schema v4+).

getWorkflowSubagents(projectSlug, sessionId, workflowId): SubagentListItem[]
sync

Subagents that ran under a specific workflow.

getTeams(): TeamDirectory[]
sync

Agent teams from ~/.claude/teams/. Empty when the feature is unused. config may be null for orphaned team dirs.

Events

Subscribe to init progress, ready, and coarse data-change batches. For fine-grained live events, use api.live.

Methods

onProgress(cb): () => void
sync

Subscribe to init progress. Callback receives InitProgress with phase, message, and optional current/total. Returns unsubscribe.

onReady(cb): () => void
sync

Fires once when initialize completes with { durationMs }.

onChange(cb): () => void
sync

Coarse segment change batches during ingest/reconcile. For live file appends, prefer api.live.onChange.

Live updates

Present only when constructed with { live: true }. Watchers keep SQLite warm; typed Change events fan out after each commit.

live.ts
const api = createSpaghettiService({ live: true });
await api.initialize();

const dispose = api.live!.onChange(
  { type: 'session', slug, sessionId },
  (e) => {
    if (e.type === 'session.message.added') {
      console.log(e.message);
    }
  },
);

// Async iterable form
for await (const e of api.live!.events()) {
  console.log(e.type, e.seq);
}

dispose();

api.live surface

onChange(listener | topic, listener, options?): Dispose
live

Firehose or scoped subscribe. Prewarms the relevant watcher scope. Returns a dispose handle. Supports single-event or coalesced batch delivery via options.

events(options?): AsyncIterable<Change>
live

Async iterable over changes. Bounded ring buffer with drop-oldest on overflow. Options: bufferSize, onDrop.

prewarm(topic): Dispose
live

Explicit watcher attachment without a subscriber. Ref-counted; dispose detaches when last ref drops.

Change

union

Discriminated by type. Each event carries seq (in-memory monotonic) and ts.

'session.message.added'   // new message append
'session.created'          // new session file
'session.rewritten'        // truncate / rewrite
'subagent.updated'
'tool-result.added'
'file-history.added'
'todo.updated'
'task.updated'
'plan.upserted'
'settings.changed'

Runtime (Plane 3)

api.runtime exposes hooks streams and channel session discovery. Always attached on the default factory path; watchers start lazily on first subscribe. Runtime events do not write the SQLite index — the durable store is a pure function of files on disk.

runtime.ts
const unsub = api.runtime?.onHookEvent((ev) => {
  console.log(ev.hookName, ev.sessionId);
});
const channels = api.runtime?.listChannelSessions() ?? [];

List & page types

Return shapes from the primary list and pagination methods.

ProjectListItem

interface
{
  projectId: string                // stable workspace identity
  members: ProjectMember[]         // exact { sourceId, slug } pairs
  slug: string                     // representative legacy slug
  sourceIds: string[]              // agents active in the workspace
  folderName: string
  absolutePath: string
  sessionCount: number
  messageCount: number
  tokenUsage: TokenUsageSummary
  tokensEstimated: boolean        // true → show "~" (tiktoken fallback)
  lastActiveAt: string
  firstActiveAt: string
  latestGitBranch: string
  hasMemory: boolean
}

SessionListItem

interface
{
  sessionId: string
  sourceId: string
  projectSlug: string
  startTime: string
  lastUpdate: string
  lifespanMs: number
  tokenUsage: TokenUsageSummary
  tokensEstimated: boolean
  messageCount: number
  fullPath: string
  summary: string
  firstPrompt: string
  gitBranch: string
  todoCount: number
  planSlug: string | null
  hasTask: boolean
  isSidechain: boolean
}

SourceFilter

interface
{ sourceId?: string }

MessagePage

interface
{
  messages: SessionMessage[]
  total: number
  offset: number
  hasMore: boolean
}

SubagentListItem · WorkflowListItem

interface
// SubagentListItem
{
  sourceId: string
  agentId: string
  agentType: string
  messageCount: number
  workflowId: string
  spawnToolId: string | null
  linkMethod: 'tool_result' | 'ordinal' | 'unlinked'
}

// WorkflowListItem
{
  workflowId: string
  name: string
  status: string
  agentCount: number
  totalTokens: number
  totalToolCalls: number
  durationMs: number
  subagentCount: number
}

Service options & settings

Helpers exported from the package root for engine preferences.

settings API
import {
  resolveEngine,
  readSettings,
  writeSettings,
  defaultDbPathForEngine,
  type IngestEngine,       // 'ts' | 'rs'
} from '@vibecook/spaghetti-sdk';

writeSettings({ engine: 'ts' });
const engine = resolveEngine();
const db = defaultDbPathForEngine(engine);

React exports

Subpath @vibecook/spaghetti-sdk/react. Requires React 19 peer dependency.

App.tsx
import {
  SpaghettiProvider,
  AgentDataPlayground,
  ProjectCard,
  SessionCard,
  MessageEntry,
  useSpaghettiAPI,
  formatTokenCount,
  formatRelativeTime,
} from '@vibecook/spaghetti-sdk/react';

export default function App({ api }: { api: SpaghettiAPI }) {
  return (
    <SpaghettiProvider api={api}>
      <AgentDataPlayground />
    </SpaghettiProvider>
  );
}
ExportKindDescription
SpaghettiProvidercomponentProvides API context to the tree.
useSpaghettiAPIhookRead the API from context.
AgentDataPlaygroundcomponentFull browse UI.
ProjectCard · SessionCardcomponentList cards.
MessageEntrycomponentRendered session message.
DetailOverlay · MetaRow · BadgecomponentUI primitives.
formatTokenCount · formatRelativeTime · formatDuration · formatBytesutilDisplay formatters.

React live hooks

Require a service created with { live: true }.

HookDescription
useLiveSessionMessages Live-updating message list for a session.
useLiveSessionList Live-updating session list for a project.
useLiveSettings Subscribe to settings file changes.
useLiveChanges Low-level change stream subscription.

Default paths

PathPurpose
~/.claude Claude Code source data (read-only for Spaghetti).
~/.codex OpenAI Codex CLI data; rollouts under sessions/YYYY/MM/DD/.
~/.spaghetti/cache/spaghetti-rs.db Default SQLite index for Rust engine (schema v7+).
~/.spaghetti/cache/spaghetti-ts.db Default SQLite index for TypeScript engine.
~/.spaghetti/config.json Persisted settings (engine preference).
~/.spaghetti/channel/ Live chat channel discovery (CLI chat / plugins).
~/.spaghetti/hooks/ Hook event stream from spaghetti-hooks plugin.

Errors & disposal

Initialize throws if the primary source root is missing or unreadable. Codex failures are non-fatal (additive source). Live pipeline errors route through errorSink rather than crashing the process.

Best practice: always pair initialize() with await dispose() (or shutdown() in sync contexts). Leaving the process without teardown can keep SQLite WAL files and watchers alive.
cleanup pattern
const api = createSpaghettiService();
try {
  await api.initialize();
  // … work …
} finally {
  await api.dispose();
}

Ready to index?

Install the CLI or drop the SDK into your next tool.