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

@titan-design/agent

v0.4.1

Published

Headless agent triggering over the Claude Agent SDK with env-scrub, failure taxonomy, and hard budgets

Readme

@titan-design/agent

Run one headless Claude Code session and get back either a typed answer or a typed failure. A thin wrapper over the Claude Agent SDK's query() that adds the three things a production caller always has to add itself: a scrubbed child environment, mandatory circuit breakers, and a failure taxonomy you can branch on.

Tier 1 of the titan-platform DAG (TP-11). Depends on @titan-design/agent-protocol, @titan-design/agent-lifecycle, and @anthropic-ai/claude-agent-sdk; zod is a peer (v4).

Run an agent

import { z } from "zod";
import { runAgent } from "@titan-design/agent";

const result = await runAgent({
  prompt: "Summarise the failing tests in this repo.",
  cwd: "/path/to/worktree",
  maxTurns: 12,
  maxBudgetUsd: 2,
  outputSchema: z.object({ failing: z.array(z.string()), likelyCause: z.string() }),
});

if (result.ok) {
  console.log(result.output.likelyCause, result.usage.totalCostUsd);
} else if (result.failure.kind === "rate_limited") {
  scheduleRetry(result.failure.retryAt);
}

runAgent never throws for anything the agent does. The only throws are caller mistakes caught before the session starts: a missing or non-positive budget.

Budgets are required, not defaulted

The SDK leaves maxTurns and maxBudgetUsd unlimited. Rather than pick a default nobody would notice was wrong, both are required fields on AgentRunConfig and runAgent throws a TypeError before it calls query() if either is missing, zero, negative, or not finite.

The environment scrub

prepareEnv(env, { allowApiKeyBilling }) copies the environment and hands the result to options.env. It never mutates its argument and never reads process.env except as the default first argument.

| Variable | Action | Why | |---|---|---| | CLAUDECODE, CLAUDE_CODE_SSE_PORT, CLAUDE_CODE_ENTRYPOINT | strip | a child CLI that inherits these refuses to start inside a parent session | | every other CLAUDE_CODE_* (e.g. CLAUDE_CODE_EXECPATH) | strip | describes the parent session and confuses the child's config inference | | CLAUDE_CODE_OAUTH_TOKEN | keep | the subscription credential | | CLAUDE_CODE_USE_BEDROCK / _VERTEX / _FOUNDRY | keep | a deliberate provider choice | | CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS | keep | a deliberate opt-out | | CLAUDE_CODE_MAX_RETRIES | keep, default 3 | the built-in default of 10 turns an outage into a long silent hang | | ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN | strip unless allowApiKeyBilling | either one silently outranks the OAuth token and bills the API account | | HTTP_PROXY, HTTPS_PROXY, ALL_PROXY (both cases) | strip | leak from the parent shell and re-route the child's API traffic | | everything else | keep | ordinary process environment |

Two guards sit either side of the run. assertAuthEnvOk is the pre-flight: it refuses to start when the OAuth token is missing, or when a metered credential survived the scrub. The claude-print harness passes requireOAuthToken: false, because the CLI's keychain login is its credential; the metered-credential check still applies. The post-flight reads apiKeySource off the first system/init message and ends the run as auth_misconfigured if it names the env API-key path. That check is a blacklist, not a whitelist: the SDK's ApiKeySource union grows over time, and an unknown new member is far more likely to be another benign route than a billing one.

Failures

One kind per recovery strategy:

| kind | Means | |---|---| | rate_limited | usage or burst limit; carries retryAt when the stream or the message said when | | budget_exceeded | maxBudgetUsd was reached | | max_turns | maxTurns was reached | | schema_invalid | the SDK exhausted its structured-output retries, or the answer failed the caller's zod schema | | refusal | the model declined | | auth_misconfigured | the pre-flight or the apiKeySource check failed | | runtime_error | anything else, including a dead subprocess | | aborted | the caller's AbortSignal fired | | inactivity_timeout | no message arrived for inactivityTimeoutMs (default 600 000) |

classifyResult(resultMessage, options?) is exported so a product can map a result message it captured itself.

Structured output

Pass outputSchema and the SDK is asked for outputFormat: { type: "json_schema" }. The SDK validates and retries on its own; this package re-parses structured_output with the same zod schema so the value you receive is the one TypeScript promises. result.output is the final assistant text when no schema was given.

Cost

result.usage is { totalCostUsd, modelUsage, turns, durationMs }. These are the SDK's client-side estimates, priced from a bundled table. Use them as a budget signal, never as a billing statement. The SDK's cost fields are already cumulative across a query() call, so this package reads the latest result rather than summing; modelUsage is the fallback when a crash result arrives with the total zeroed.

Subagents and permissions

agents, mcpServers, hooks, allowedTools, disallowedTools, model, resumeSessionId and settingSources pass straight through. So do tools, the built-in tool set ([] runs with no tools, for a pure judgement call), and systemPrompt, which replaces the Claude Code default prompt. Leaving either unset keeps the SDK default. Two defaults are chosen for headless safety:

  • permissionMode defaults to "dontAsk", so nothing runs that was not pre-approved.
  • settingSources defaults to [], so no CLAUDE.md, settings file, or .mcp.json is picked up off the filesystem unless you ask for it.

Gotcha: a subagent inherits bypassPermissions, acceptEdits and auto from the parent and cannot narrow them per-subagent. A parent running wide open gives every subagent that same reach regardless of what its AgentDefinition says. Prefer allowedTools wildcards over a permissive mode.

This package runs one session. It does not pool, schedule, or retry; that belongs to the workflow tier.

The claude-print harness

harness: "claude-print" makes runAgent spawn the claude CLI in print mode instead of calling the SDK. Use it for one-turn structured answers, such as a triage or judgement step, on a machine where the CLI is already logged in. The CLI's own keychain login is the credential, so CLAUDE_CODE_OAUTH_TOKEN is not required. harness defaults to "claude-code", and the SDK path is unchanged.

const result = await runAgent({
  harness: "claude-print",
  model: "sonnet",
  prompt: "Is this diff a refactor? Answer as JSON.",
  outputSchema: z.object({ refactor: z.boolean() }),
  cwd: process.cwd(),
  maxTurns: 1,
  maxBudgetUsd: 0.5,
});

| Aspect | claude-print behaviour | |---|---| | Binary | CLAUDE_BIN, else the first executable file named claude on the scrubbed env's PATH; never a shell function or alias | | Fixed flags | -p --output-format json --tools "" --strict-mcp-config --mcp-config '{"mcpServers":{}}' --setting-sources "" --max-budget-usd <maxBudgetUsd> | | maxTurns | --max-turns <maxTurns>, raised to at least 2 when outputSchema is set, because the CLI can need a second turn to emit the structured answer | | Prompt | written to stdin, so large prompts avoid the argv size cap | | model | --model <model> | | systemPrompt | --system-prompt <text>, which replaces the default Claude Code prompt | | outputSchema | --json-schema <schema> without the $schema header, which the CLI rejects; structured_output is then re-parsed with the zod schema and a mismatch is schema_invalid | | inactivityTimeoutMs | a wall deadline, because JSON output arrives only at the end; SIGTERM to the process group, then SIGKILL after CLAUDE_PRINT_KILL_GRACE_MS | | signal | kills the process group the same way and returns aborted | | allowApiKeyBilling | same scrub as the SDK path: ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN are stripped unless it is set | | Result | the JSON result has the SDK result-message shape, so classifyResult and usageFromResult apply unchanged; onMessage receives that one message | | init | only model is known, taken from modelUsage; the JSON output carries no init message, so there is no apiKeySource post-flight | | Logged-out CLI | an error result that says to log in becomes auth_misconfigured | | error_max_turns | runtime_error, which callers such as the workflow agentRunner treat as retryable; with no tools the cap is hit only on a structured-output retry, which a fresh call usually clears | | Rejected options | tools (other than []), allowedTools, disallowedTools, permissionMode, resumeSessionId, agents, mcpServers, hooks and a non-empty settingSources throw a TypeError before anything spawns |

claudePrintCapabilities() reports the same limits in the shared capability vocabulary: fresh runs, structured output, external cancellation and token reporting are supported; resume, tools and every interactive capability are not. There is no HarnessAdapter<"claude-print"> yet, so dispatchHarnessRun and the durable dispatcher cannot use it; claude-print is selectable only through runAgent. Without systemPrompt the default Claude Code prompt costs about 7,600 input tokens per call. A short systemPrompt brings that down to about 1,000.

usage.input_tokens in the CLI's JSON result counts only the non-cached input tokens. A live triage run saw input_tokens: 2 beside 2,100 to 37,000 cache_creation_input_tokens per call. modelUsage splits input the same way. Read the cache fields, or use totalCostUsd, when judging what a call cost.

Explicit multi-harness contracts

dispatchHarnessRun(request, adapter) is the new, explicit contract for Claude Code and Codex adapters. It does not replace or reroute runAgent(). Every request has a discriminating harness, a fresh or resume target, and a required positive finite wallTimeMs. Claude's Anthropic SDK fields live only under the claude-code branch; Codex model, reasoning, sandbox, approval, and native JSON Schema fields live only under the codex branch.

Optional limits name their unit (usd, model_requests, agent_iterations, or tokens), scope (execution or conversation), and enforcement (hard or advisory). These are not interchangeable: an agent iteration is not a model request, and an estimated SDK dollar stop is not a hard financial cap.

Adapters supply a HarnessCapabilityDescriptor that marks every operation and declared limit supported, unsupported, or unverified, with evidence or a reason. Core declares no adapter capabilities itself. Before invoking an adapter, the dispatcher checks the operation, resume identity, structured-output and cancellation needs, caller requirements, mandatory hard execution deadline, and all optional limits. Unsupported and unverified requirements return an unsupported_requirement result with no adapter call.

Support for the hard milliseconds execution limit means the adapter can stop the local execution at its deadline. It does not claim that a remote model request has stopped unless the adapter separately reports verified cancellation support. Normalized progress, results, usage measurements, execution identity, conversation identity, and transcript source hints contain no harness-native event types.

Supervised Codex exec adapter

createCodexExecAdapter({ auth: "cached-cli" }) runs the pinned ChatGPT desktop Codex binary through codex exec --json. The caller must select a model and an absolute working directory. Fresh runs persist a native thread; resumes always use the supplied native thread ID and never --last. The adapter ignores user config, strips API-key credentials from the child environment, accepts only the noninteractive never approval policy, and leaves persistence enabled so @titan-design/session-read can discover the rollout by thread ID and namespace.

The adapter checks codex --version before every launch. The mandatory wall deadline covers that check, temporary output-schema setup, and the run itself. Timeout or caller abort terminates the owned process group, then sends SIGKILL after the configured grace period. JSONL output becomes normalized progress, conversation identity, final text or locally validated structured output, and token usage with Codex event provenance. Hard dollar, request, iteration, and token caps remain unsupported and fail preflight before either version probing or process spawn.

A zero exit is successful only after Codex emits turn.completed. Its usage is a turn snapshot: the adapter uses Codex's native turn ID when one is present and an execution-correlated synthetic turn ID otherwise. It is never labeled as a conversation total. If an abort or wall deadline stops the OS process without a native terminal turn event, the result is cancelled_unknown and retains the requested cause and process exit evidence. Temporary schema cleanup completes before the terminal progress event, so cleanup failure cannot follow a reported successful finish.

Ending a run

Every run ends cleanly. The inactivity watchdog resets on each streamed message and, like the caller's AbortSignal, ends the run through the SDK's abortController and then query.close(), so no CLI subprocess is left behind.

Bounded Claude adapter

createClaudeCodeAdapter({ maxTurns, maxBudgetUsd, inactivityTimeoutMs?, deps? }) wraps the existing runAgent in the common HarnessAdapter<"claude-code"> contract. Both native circuit breakers remain required. Every request also supplies wallTimeMs; expiration aborts and closes the owned SDK query. A local stop cannot prove that native execution was cancelled, so the durable dispatcher records an unknown cancellation unless terminal evidence is available.

Fresh and resumed requests keep invocation identity separate from native session identity. Native options, permission settings, MCP servers, schema validation, and cached-auth handling flow through runAgent. Structured output remains validated by the caller's Zod schema. SDK query usage is aggregated across its main, subagent, and internal model calls into one turn-scoped snapshot with estimated cost. Multiple models yield a null model label so their totals do not overwrite each other. Total input includes cache reads and cache creation; those subset counters must not be added again.

The adapter does not translate SDK maxTurns into a generic model-request or agent-iteration limit, or treat the SDK budget estimate as an exact monetary cap. Generic optional limits remain unsupported until their semantics are verified. No process reattachment, visible terminal, or semantic-interrupt capability is claimed by this wrapper. Legacy runAgent callers retain their existing API.

Durable harness dispatch

createDurableHarnessDispatcher(adapter, { ledger, supervisorId, leaseMs }) wraps either harness adapter with an early durable acknowledgment and a completion promise. Apply executionLedgerMigration(version) from @titan-design/agent-lifecycle to the authoritative database before constructing the ledger. Each dispatch requires caller-generated executionId and requestKey values. The dispatcher commits prepare and begin_dispatch before calling the adapter, renews its captured owner lease while the call is live, and commits the terminal result before resolving completion.

reconcile(executionId) returns a live handle only in the same dispatcher instance while its exact owner generation and lease remain valid. After restart it can read back a durable terminal result. An expired record that never committed begin_dispatch is closed as a retryable failure because the ledger proves the adapter was not called. A missing record or any post-dispatch record without an attachment handle returns unknown or recovery-required evidence and never authorizes resubmission. The dispatcher does not infer liveness from a PID or a transcript.

Cancellation intent is stored before the local abort signal fires. Without a native terminal acknowledgment after a deadline or abort, the ledger records cancellation_unknown. Lease-renewal, stale-owner, and local persistence failures settle completion promptly even when the adapter ignores abort; they cannot write a terminal result through a newer owner's fence. Durable results must be JSON-safe.