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

background-agents

v0.1.1

Published

A TypeScript SDK for interacting with various AI coding agents (Claude, Codex, OpenCode, Gemini)

Downloads

207

Readme

Coding Agents SDK

A unified TypeScript interface for AI coding agents—Claude, Codex, Gemini, and OpenCode. Commands run in secure Daytona sandboxes by default, with real-time PTY streaming.

import { Daytona } from "@daytonaio/sdk"
import { createSession } from "background-agents"

const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY })
const sandbox = await daytona.create({ envVars: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY } })
const session = await createSession("claude", { sandbox })

for await (const event of session.run("Hello!")) {
  if (event.type === "token") process.stdout.write(event.text)
  if (event.type === "end") break
}

await sandbox.delete()

Same pattern for any provider: create a sandbox, create a session, stream events, then tear down. Swap the provider name and env keys as needed.


Features

  • Secure by default — Execution runs in isolated Daytona sandboxes
  • Real-time streaming — PTY-based streaming for live token output
  • Unified API — One interface for Claude, Codex, Gemini, and OpenCode
  • Zero-friction setup — Provider CLI is installed when you create a session (skipInstall: true to skip). Env and Codex login run on every run().
  • Session persistence — Resume conversations across runs

Provider support

| Provider | Status | Auth | |----------|--------|------| | Claude | ✅ | ANTHROPIC_API_KEY | | Codex | ✅ | OPENAI_API_KEY | | OpenCode | ✅ | Provider-specific (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY) | | Gemini | 🚧 | GOOGLE_API_KEY |


Prerequisites

A Daytona API key (or run locally without a sandbox).

export DAYTONA_API_KEY=dtn_your_api_key

Installation

npm install background-agents

For sandboxed execution, also install the Daytona SDK:

npm install @daytonaio/sdk

Next.js: Merge the SDK's Next config so native deps (e.g. ssh2 / cpu-features) are not bundled:

// next.config.js or next.config.mjs
import codeagentsdk from 'background-agents/next.config'
export default { ...codeagentsdk, ...yourConfig }

Quick start

1. Create a sandbox — Pass provider API keys via the sandbox; the SDK does not read your host env.

import { Daytona } from "@daytonaio/sdk"
import { createSession } from "background-agents"

const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY })
const sandbox = await daytona.create({
  envVars: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
})

2. Create a session — The provider CLI is installed in the sandbox (unless skipInstall: true).

const session = await createSession("claude", {
  sandbox,
  model: "sonnet",
  timeout: 120,
  systemPrompt: "You are a helpful coding assistant.",
})

3. Stream responses

for await (const event of session.run("Hello!")) {
  if (event.type === "token") process.stdout.write(event.text)
  if (event.type === "tool_start") console.log(`\n[Tool: ${event.name}]`)
  if (event.type === "end") break
}

4. Cleanup

await sandbox.delete()

Optional: Git workflow — Use the Daytona Git SDK to clone before and push after:

const repoPath = "workspace/repo"
await sandbox.git.clone("https://github.com/user/repo.git", repoPath)
// ... run session ...
await sandbox.git.push(repoPath)

Full example

End-to-end example with event handling and cleanup:

import { Daytona } from "@daytonaio/sdk"
import { createSession } from "background-agents"

async function main() {
  const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY })
  const sandbox = await daytona.create({
    envVars: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
  })

  try {
    const session = await createSession("claude", { sandbox })

    for await (const event of session.run("List /tmp then write /tmp/out.txt with 'done'")) {
      switch (event.type) {
        case "token":
          process.stdout.write(event.text)
          break
        case "tool_start":
          console.log("\n🛠️", event.name, event.input ?? "")
          break
        case "end":
          console.log("\nDone.")
          break
      }
    }
  } finally {
    await sandbox.delete()
  }
}

main()

CLI commands (reference)

Each provider is invoked via its CLI. Optional flags in brackets.

| Provider | Command | |----------|---------| | Claude | claude -p --output-format stream-json --verbose --dangerously-skip-permissions [--model <m>] [--resume <id>] <prompt> | | Codex | codex exec --json --skip-git-repo-check --yolo [--model <m>] [resume <id>] <prompt> | | OpenCode | opencode run --format json --variant medium -m <model> [-s <id>] <prompt> (via bash -lc "…") | | Gemini | gemini -p --output-format stream-json --yolo [--model <m>] [--resume <id>] <prompt> |


API reference

createSession(provider, options)

Creates a session with the given provider and options (e.g. sandbox, model, timeout). Installs the provider CLI in the sandbox before returning unless skipInstall: true. Codex login runs automatically on each run() when needed.

const session = await createSession("claude", {
  sandbox,
  model: "sonnet",
  timeout: 120,
})

session.run(prompt)

Returns an async iterable of events. Stream and handle them uniformly across providers.

for await (const event of session.run("Hello")) {
  // event.type: "session" | "token" | "tool_start" | "tool_delta" | "tool_end" | "end" | "agent_crashed"
}

Event stream

| Event | Description | Fields | |-------|-------------|--------| | session | Session started (for resumption) | id: string | | token | Streamed assistant text | text: string | | tool_start | Tool invoked | name: string, input?: unknown | | tool_delta | Streaming tool input | text: string | | tool_end | Tool finished | output?: string | | end | Turn complete | — | | agent_crashed | Process exited without completing (crash/kill) | message?: string, output?: string (raw tail of stdout/stderr; often not JSONL) |

type Event =
  | { type: "session"; id: string }
  | { type: "token"; text: string }
  | { type: "tool_start"; name: string; input?: unknown }
  | { type: "tool_delta"; text: string }
  | { type: "tool_end"; output?: string }
  | { type: "end" }
  | { type: "agent_crashed"; message?: string; output?: string }

Normalized tool names

Tool names are normalized across providers. Each has a defined tool_start input and tool_end output.

| Tool | tool_start input | Claude | Codex | OpenCode | |------|--------------------|:------:|:-----:|:--------:| | write | { file_path, content?, kind } | ✅ | ✅ | ✅ | | read | { file_path } | ✅ | — | ✅ | | edit | { file_path, ... } | ✅ | — | ✅ | | glob | { pattern } | ✅ | — | ✅ | | grep | { pattern, path? } | ✅ | — | ✅ | | shell | { command, description? } | ✅ | ✅ | ✅ |


Model selection

Set model when creating the session.

| Provider | Example | Docs | |----------|---------|------| | Claude | model: "sonnet" or "opus", "haiku" | Claude Code models | | Codex | model: "gpt-4o" or "o1", "o3" | Codex CLI models | | OpenCode | model: "openai/gpt-4o" (provider/model) | OpenCode models | | Gemini | model: "gemini-2.0-flash" or "gemini-1.5-pro" | Gemini CLI model |


Sandboxed background sessions

For long-running or restart-tolerant flows: start the agent in the sandbox, write the event stream to log files there, and poll with getEvents. All state except the session ID lives in the sandbox.

  • Session ID — One UUID per background session; host stores only this.
  • start() — Returns immediately with { executionId, pid, outputFile }; the agent runs in the background.
  • isRunning() — True while the turn is in progress, false after.
  • Crash detection — If the process exits without completing, getEvents returns an agent_crashed event. You can treat it like end to stop polling and show a warning.

Example: start, persist sandboxId and backgroundSessionId, then reattach after a restart.

import { Daytona } from "@daytonaio/sdk"
import { createBackgroundSession, getBackgroundSession } from "background-agents"

const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY! })
const sandbox = await daytona.create({
  envVars: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
})

const bgSession = await createBackgroundSession("claude", {
  sandbox,
  model: "sonnet",
  // Optional: per-session system prompt (applied once, persisted across turns).
  systemPrompt: "You are a helpful coding assistant.",
})
await bgSession.start("Do a long-running refactor...")
// Persist sandbox.id and bgSession.id, then exit.

// --- After restart ---
const sandboxAgain = await daytona.get(sandboxId)
const bgAgain = await getBackgroundSession({
  sandbox: sandboxAgain,
  backgroundSessionId,
  // Re-apply session options so the provider is recreated with the same model
  // and system prompt when reattaching.
  model: "sonnet",
  systemPrompt: "You are a helpful coding assistant.",
})

async function poll() {
  const { events } = await bgAgain.getEvents()
  for (const e of events) {
    if (e.type === "token") process.stdout.write(e.text)
    else if (e.type === "tool_start") console.log("[Tool]", e.name)
  }
  if (!(await bgAgain.isRunning())) return
  setTimeout(poll, 2000)
}
poll()

await bgAgain.cancel() // kill agent in sandbox (no-op if stopped)

Local mode (dangerous)

Runs the provider CLI on your machine instead of a sandbox. Only use when you fully trust the code.

const session = await createSession("claude", { dangerouslyAllowLocalExecution: true })
for await (const event of session.run("Hello")) {
  if (event.type === "token") process.stdout.write(event.text)
}

Interactive REPL

# Claude (default)
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npx tsx scripts/repl.ts

# Other providers
npx tsx scripts/repl.ts --provider codex   # OPENAI_API_KEY
npx tsx scripts/repl.ts --provider opencode
npx tsx scripts/repl.ts --provider gemini  # GEMINI_API_KEY (or GOOGLE_API_KEY)

# Polling-based (background session)
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npx tsx scripts/repl-polling.ts
npx tsx scripts/repl.ts -h   # help; providers: claude, codex, opencode, gemini

How it works

  1. Sandbox — You create a Daytona sandbox and pass it to createSession.
  2. CLI — Provider CLI is installed in the sandbox at session creation (unless skipInstall: true). Each run() sets env and, for Codex, runs codex login --with-api-key.
  3. PTY — Commands run in a PTY for real-time streaming.
  4. Events — JSON from the CLI is parsed into typed events.
  5. Cleanup — You call sandbox.delete() when done.
┌─────────────┐     ┌──────────────────────────────────────┐
│   Your App  │────▶│          Daytona Sandbox             │
│             │◀────│  ┌─────────────┐    ┌─────────────┐  │
│             │     │  │  PTY Stream │◀──▶│  Agent CLI  │  │
│             │     │  └─────────────┘    └─────────────┘  │
└─────────────┘     └──────────────────────────────────────┘

Debug mode

Set CODING_AGENTS_DEBUG=1 (or any non-empty value) to log debugging information to stderr:

  • Agent lifecycle — when sessions and background sessions are created, when runs start and end
  • Background agents — when a turn starts (session dir, turn number, output file), when the background process is started (pid), and each time events are polled (cursor, event count)
  • Unparsed output — any CLI line that didn’t parse as an event (helps spot hangs where the agent prints something the SDK doesn’t recognize)
CODING_AGENTS_DEBUG=1 npx tsx scripts/repl-polling.ts

Development

npm install
npm run build
npm test                    # unit tests (integration/sandbox-background skipped without keys)
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npm run test -- tests/integration/sandbox-background.test.ts   # real sandbox background test
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npx tsx scripts/test-sdk-full.ts   # integration
DAYTONA_API_KEY=... ANTHROPIC_API_KEY=... npx tsx scripts/repl.ts            # REPL

Resources

SandboxDaytona Docs · Daytona GitHub

AgentsClaude Code · Codex CLI · Gemini CLI · OpenCode


License

MIT