npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sovara/runner

v0.3.3

Published

TypeScript runner for Sovara — wrap a script in a single function to record every LLM call, tool invocation, and log line into a structured run graph.

Downloads

476

Readme

@sovara/runner

TypeScript runner for Sovara — wrap your script in a single function and Sovara records every LLM call, tool invocation, and log line into a structured run graph.

The runner is the TS counterpart to the Python sovara runner. It targets the same Sovara server, produces the same node shapes, and is safe to mix with Python runs in the same project.

Requirements

  • Node 18 or newer (built-in fetch).
  • A reachable Sovara backend. For local use, open the Sovara desktop app; for enterprise use, point SOVARA_SERVER_URL at the remote endpoint.
  • A Sovara user and project, configured once with sovara init.

Install

npm install @sovara/runner
# or
pnpm add @sovara/runner
# or
yarn add @sovara/runner

If you use the Claude Agent SDK, install it as a peer dependency:

npm install @anthropic-ai/claude-agent-sdk

Quick start

import { withSovaraRun } from "@sovara/runner";
import OpenAI from "openai";

const openai = new OpenAI();

await withSovaraRun("hello-world", async () => {
  const response = await openai.responses.create({
    model: "gpt-5",
    input: "Say hello.",
  });

  console.log(response.output_text);
});

That's the full integration. The call to OpenAI is captured automatically; console.log output is captured as run logs; the run shows up in the Sovara UI.

How it works

Importing @sovara/runner patches two transports at module load:

  • globalThis.fetch — wraps every fetch call.
  • node:http and node:https — wraps http.request / http.get and their HTTPS counterparts. This covers axios and any library that uses Node's HTTP modules directly.

Inside withSovaraRun(...), calls to whitelisted endpoints are observed: the runner builds a request envelope, posts it to the Sovara server's /internal/runner/llm/prepare endpoint (which optionally applies runtime preparation such as priors injection), executes the prepared request on the wire, and records the response or exception. Outside the run scope, both patches are no-ops.

Endpoints captured by default

| Provider | Path pattern | | ---------------- | ----------------------------------------------------------------------------- | | Anthropic | /v1/messages | | OpenAI Responses | /v1/responses | | OpenAI Chat | /v1/chat/completions | | AWS Bedrock | bedrock-runtime.*.amazonaws.com/model/.../{converse,invoke} | | Google Gemini | models/...:generateContent, :streamGenerateContent | | Ollama | /api/chat, /api/generate, /api/embed, /api/embeddings | | Tooling | Serper, Brave Search, Jina Reader, BrightData, Patronus, Contextual, Parallel |

Streaming responses (SSE, NDJSON, JSONL, chunked text) are tee'd: the user gets the bytes unchanged and the runner finalizes after the stream completes.

API

withSovaraRun(name | options, fn)

export type WithSovaraRunOptions = {
  name: string; // required run name
  url?: string; // server URL (default: SOVARA_SERVER_URL or http://127.0.0.1:5959)
  captureLogs?: boolean; // default: true
  user?: { fullName?: string; email?: string }; // overrides only
  project?: { name?: string; description?: string };
};

export function withSovaraRun<T>(
  input: string | WithSovaraRunOptions,
  fn: () => Promise<T> | T,
): Promise<T>;

The string form is shorthand for { name: input }. The runner returns whatever fn returns.

Top-level call (no parent run on the stack):

  1. Health-checks the configured server and fails fast if it is unreachable.
  2. Resolves the active user. Fails hard with a sovara init hint if user setup is missing.
  3. Resolves the project for process.cwd(). Fails hard with a sovara init hint if the cwd isn't part of a known Sovara project.
  4. Registers the run, opens an SSE shutdown listener, runs fn inside an AsyncLocalStorage scope, and deregisters in finally.

Nested call (called from inside another withSovaraRun):

The nested call is registered as a subrun via /runner/subrun, inheriting the parent's client and url settings. It deregisters its own subrun when fn returns.

fn exceptions propagate untouched. The run is deregistered in either case.

Subruns

await withSovaraRun("pipeline", async () => {
  const docs = await withSovaraRun("retrieve", async () => loadDocs());
  await withSovaraRun("summarize", async () => summarize(docs));
});

Every nested withSovaraRun becomes a subrun under the active scope.

Logs

Standard console.log, console.error, and any direct writes to process.stdout / process.stderr are captured into the run log buffer and flushed to the server every 250ms.

Disable with captureLogs: false:

await withSovaraRun({ name: "noisy", captureLogs: false }, async () => {
  // stdout/stderr will not be tee'd to the server
});

User and project overrides

await withSovaraRun(
  {
    name: "billing-job",
    user: { fullName: "CI Bot", email: "[email protected]" },
    project: { name: "Billing", description: "Nightly invoice run" },
  },
  async () => doWork(),
);

Overrides are only applied if user/project are present. They don't bypass the sovara init requirement — they just refine the values.

trace(fn, options?)

Use trace when an important custom function or tool does not make an LLM HTTP call on its own, but should still appear as a tool node in the Sovara run graph.

import { trace, withSovaraRun } from "@sovara/runner";

const lookupCustomer = trace(
  async function lookupCustomer(customerId: string) {
    return { customerId, tier: "enterprise" };
  },
  { meta: { system: "crm" } },
);

await withSovaraRun("support-agent", async () => {
  const customer = await lookupCustomer("cust_123");
  // Continue your agent flow with customer.
});

trace records function arguments as the node input and the return value as the node output. If the wrapped function throws, the error type and message are recorded and the original exception is re-thrown. Outside an active withSovaraRun(...) scope, the wrapper calls the function normally without recording anything.

export type TraceOptions = {
  name?: string; // display name for the tool node, default: function/method name
  meta?: Record<string, unknown>; // optional metadata stored with input/output
};

The same helper can be used as a standard Stage 3 method decorator when your TypeScript configuration supports decorators:

class WeatherService {
  @trace({ name: "weather_lookup" })
  async lookup(city: string) {
    return { city, forecast: "sunny" };
  }
}

Claude Agent SDK integration

ESM module namespaces in Node are immutable, so the runner cannot monkey-patch @anthropic-ai/claude-agent-sdk from the outside. Instead, @sovara/runner ships a drop-in wrapper at @sovara/runner/claude:

// before
import { query, startup } from "@anthropic-ai/claude-agent-sdk";

// after
import { query, startup } from "@sovara/runner/claude";

That single import change is the entire integration. The wrapper:

  • Routes the spawned Claude CLI subprocess through Sovara's local Claude proxy by setting ANTHROPIC_BASE_URL in the subprocess env, so every LLM request the SDK makes shows up in your run graph.
  • Registers a unique proxy client with the server before each query() / startup() so concurrent SDK calls don't collide.
  • Sets CLAUDE_CODE_ENTRYPOINT=sdk-ts and SOVARA_CLAUDE_PROXY_ACTIVE=1 to mark the subprocess as running under Sovara.
  • Wraps the returned AsyncIterable so each yielded message is parsed for tool_use / tool_result blocks and recorded as nodes on the run.
  • Throws if options.transport (custom transport) is supplied — this matches the Python runner's behavior.
  • Throws if options.env.ANTHROPIC_BASE_URL already points at the Sovara proxy, to prevent infinite proxy loops.
  • Outside a withSovaraRun scope, query() and startup() are pass-throughs to the real SDK with zero overhead.

Example

import { withSovaraRun } from "@sovara/runner";
import { query } from "@sovara/runner/claude";

await withSovaraRun("agent-task", async () => {
  for await (const message of query({
    prompt: "List the largest files in the current directory.",
  })) {
    if (message.type === "assistant") {
      // tool_use blocks here are also recorded as Sovara nodes.
    }
  }
});

startup()

import { startup } from "@sovara/runner/claude";

const handle = await startup({ options: {} });
for await (const message of handle.query("What did I install last?")) {
  // ...
}

startup() returns a warm handle whose query() is also instrumented.

Configuration

Environment variables

| Variable | Purpose | Default | | ---------------------------- | -------------------------------------------------------------------------- | ----------------------- | | SOVARA_SERVER_URL | Override the Sovara server URL | http://127.0.0.1:5959 | | HOST | Server host (used to compute the default URL) | 127.0.0.1 | | PYTHON_PORT | Server port (used to compute the default URL) | 5959 | | SOVARA_CLAUDE_PROXY_ACTIVE | Set automatically by @sovara/runner/claude to prevent nested proxy loops | unset |

Server URL

Either pass url per call:

await withSovaraRun({ name: "demo", url: "http://localhost:6000" }, fn);

Or set SOVARA_SERVER_URL once for the process.

Error messages

The runner fails fast for misconfiguration. Common ones:

  • Sovara backend is not reachable at <url>. Open the Sovara desktop app or configure SOVARA_SERVER_URL to a reachable enterprise Sovara backend endpoint. Open the desktop app for local runs, or point SOVARA_SERVER_URL at the enterprise backend.

  • Sovara user setup is missing. Run `sovara init --user-name <full-name> --user-email <email>` and retry. Run the suggested sovara init command.

  • Sovara project setup is missing for <cwd>. Run `sovara init --project-root "<cwd>" --project-name <project-name>` and retry. Run the suggested sovara init command.

  • Custom Claude Agent SDK transports are not supported yet under sovara. Remove options.transport from your query() / startup() call.

  • Claude Agent SDK ANTHROPIC_BASE_URL points at the Sovara Claude proxy itself. Nested Claude proxy configuration is not supported. Don't manually set ANTHROPIC_BASE_URL to the Sovara proxy URL — the wrapper sets it for you.

Limitations

  • Direct import { request, fetch } from "undici" calls are not intercepted (the runner patches globalThis.fetch and node:http(s), which covers all common LLM clients today). Open an issue if you encounter an SDK that bypasses both.
  • The runner is Node-only. Browser, edge runtimes, and bundled environments without Node's node:http/async_hooks are out of scope.
  • If you import @anthropic-ai/claude-agent-sdk directly anywhere in your project, those call sites are not instrumented. Use @sovara/runner/claude everywhere you want Sovara observability.

Development

npm install
npm test         # vitest run, fully hermetic, no real network or server
npm run typecheck
npm run build    # emit dist/ via tsc -p tsconfig.build.json

Releasing

Publishing is automated via GitHub Actions using npm trusted publishing (OIDC). There is no NPM_TOKEN secret — the runner-ts-release.yaml workflow authenticates to npm via OIDC.

To cut a release:

  1. Bump version in runner_ts/package.json on a release PR. Land it on main.
  2. From main, create and push a tag matching the new version:
    git tag runner-ts-v0.2.0
    git push origin runner-ts-v0.2.0
  3. The Runner (TypeScript) — Release workflow verifies the tag matches package.json, runs typecheck + tests + build, then publishes with npm publish --access public.

The tag must match the package version exactly; mismatches fail the workflow.