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

@replayio/app-building

v1.24.1

Published

Library for managing agentic app-building containers

Readme

@replayio/app-building

Library for managing agentic app-building containers. Start and stop containers locally (Docker) or remotely (Fly.io), communicate with the in-container HTTP server, and track container state via a local registry.

Install

npm install @replayio/app-building

Usage

import {
  loadDotEnv,
  FileContainerRegistry,
  getInfisicalConfig,
  startContainer,
  stopContainer,
  type ContainerConfig,
  type RepoOptions,
  httpGet,
  httpOptsFor,
} from "@replayio/app-building";

// Load orchestration vars from .env, then get Infisical credentials
const orchestrationVars = loadDotEnv("/path/to/project");
const infisicalConfig = await getInfisicalConfig(orchestrationVars);

// Local container (no flyToken/flyApp)
const config: ContainerConfig = {
  projectRoot: "/path/to/project",
  infisical: infisicalConfig,
  registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
};

// Remote container (set flyToken + flyApp)
const remoteConfig: ContainerConfig = {
  ...config,
  flyToken: orchestrationVars.FLY_API_TOKEN,
  flyApp: orchestrationVars.FLY_APP_NAME,
};

// Start — automatically chooses local or remote based on config
const repo: RepoOptions = { repoUrl: "https://...", cloneBranch: "main", pushBranch: "feature/x" };
const state = await startContainer(config, repo);

// Check status
const status = await httpGet(`${state.baseUrl}/status`, httpOptsFor(state));

// Query the registry
const alive = await config.registry.findAlive();

// Stop — handles both local and remote
await stopContainer(config, state);

Secrets architecture

Secrets are never passed directly to the container or agent. Instead:

  1. The orchestration host passes Infisical credentials (InfisicalConfig) to the container via ContainerConfig.infisical.
  2. At startup, the container fetches global secrets from Infisical for internal use (clone token, agent API key).
  3. A secrets server (127.0.0.1:9119) runs inside the container, accessible only locally. It fetches secrets live from Infisical on every request — no caching.
  4. The agent process runs with a restricted environment — only ANTHROPIC_API_KEY (required for the Claude CLI) is present.
  5. When the agent needs to run a command that requires secrets, it uses exec-secrets:
exec-secrets NEON_API_KEY -- curl -s -H "Authorization: Bearer $NEON_API_KEY" https://...
exec-secrets NETLIFY_AUTH_TOKEN NETLIFY_ACCOUNT_SLUG -- netlify deploy --prod

The secrets server spawns the command with the requested secrets in its environment and redacts requested secret values from the output.

The agent can also run list-secrets to see which secrets are available, and set-branch-secret to store new branch-level secrets (e.g., DATABASE_URL created at deploy time). The server rejects credential values that have already appeared in logs.

Exported API

Domain objects

| Export | Description | |---|---| | ContainerConfig | infisical (required InfisicalConfig), optional projectRoot (local Docker only), registry, flyToken/flyApp (set both for remote Fly.io), imageRef, webhookUrl/webhookSecret, taskWebhookUrl (GET endpoint for external task queue), detached, initialPrompt, localPort, absorbTasks, namePrefix (default: "app-building"). | | RepoOptions | Per-invocation git settings: repoUrl, cloneBranch, pushBranch. | | AgentState | Returned by startContainer. Contains type, containerName, port, baseUrl, and Fly-specific fields for remote containers. | | ContainerRegistry | Interface for container registry storage. Methods: log, markStopped, clearStopped, getRecent, find, findAlive. | | FileContainerRegistry | Built-in file-backed implementation of ContainerRegistry, backed by a .jsonl file. |

Container lifecycle

| Export | Description | |---|---| | startContainer(config, repo) | Start a container. Uses local Docker if flyToken/flyApp are not set, Fly.io if they are. Returns AgentState. | | stopContainer(config, state) | Stop a container by its AgentState or RegistryEntry. Handles both local and remote. | | buildImage(config) | Build the Docker image locally (called automatically by startContainer for local containers). | | spawnTestContainer(config) | Start an interactive (-it) local container with the repo mounted at /repo. | | loadDotEnv(projectRoot) | Parse a .env file and return key-value pairs. |

Container registry (ContainerRegistry interface / FileContainerRegistry class)

| Method | Description | |---|---| | log(state) | Append a new entry to the registry. | | markStopped(name?) | Mark a container as stopped. | | clearStopped(name) | Clear the stopped flag (container came back alive). | | getRecent(limit?) | Read the most recent registry entries. | | find(name) | Find a specific container by name. | | findAlive() | Probe recent entries and return those that are alive. Reconciles stopped flags. |

Types: RegistryEntry

HTTP client

| Export | Description | |---|---| | httpGet(url, opts?) | GET with retries and timeout. Returns parsed JSON. | | httpPost(url, body?, opts?) | POST with retries and timeout. Returns parsed JSON. |

Types: HttpOptions

Container utilities

| Export | Description | |---|---| | httpOptsFor(state) | Return HttpOptions for a container (adds fly-force-instance-id header for remote containers). | | probeAlive(entry) | Check if a container is responding to /status. |

Secrets (Infisical)

| Export | Description | |---|---| | infisicalLogin(clientId, clientSecret) | Log in via Universal Auth, returns a short-lived access token. | | getInfisicalConfig(envVars) | Extract InfisicalConfig from env vars and log in. Requires INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID, INFISICAL_ENVIRONMENT. | | fetchGlobalSecrets(config) | Fetch secrets from the /global/ path. | | fetchBranchSecrets(config, branch) | Fetch secrets from /branches/<branch>/. | | createBranchSecret(config, branch, name, value) | Create or update a secret in /branches/<branch>/. Creates the folder if needed. | | fetchInfisicalSecrets(config, path) | Raw fetch from any Infisical folder path. |

Types: InfisicalConfig

Image ref

| Export | Description | |---|---| | getImageRef() | Returns CONTAINER_IMAGE_REF env var, or ghcr.io/replayio/app-building:latest by default. |

Container HTTP API

Each container runs an HTTP server that accepts the following requests:

| Method | Path | Description | |---|---|---| | POST /message | { prompt: string } | Add a prompt as a task in the persistent task queue. Returns { ok: true }. | | POST /detach | | Signal the container to exit once all tasks are done. | | POST /stop | | Force-stop the container immediately. Interrupts any running work, commits remaining changes, then exits. | | POST /interrupt | | Kill the currently running Claude process without stopping the container. | | GET /status | | Container state, queue depth, iteration count, cost, revision, etc. | | GET /events?offset=N | | Stream of Claude events (JSON lines) since offset. | | GET /logs?offset=N | | Stream of log lines since offset. |

Container lifecycle

A container stays running and accepts messages until it receives a detach or stop signal:

  • Detached at startup (config.detached = true): Set detached on ContainerConfig to start the container in detached mode. Use config.initialPrompt to provide a prompt that is queued before the HTTP server starts accepting requests. The container processes the initial prompt and any queued tasks, then exits cleanly. This is the preferred way to run fire-and-forget jobs — no race between container startup and a subsequent POST /message or POST /detach.
  • Detach (POST /detach): Signal a running container to exit once all in-flight and queued work is done. In the CLI, interactive mode (npm run agent -- -i) sends /detach automatically when the user disconnects (Ctrl+C/D).
  • Stop (POST /stop): The container exits immediately, interrupting any running Claude process. It commits any remaining work before shutting down. This is the forced shutdown path.

Without either signal, the container waits indefinitely for new messages — this is intentional so that interactive users can send follow-up messages at any time.

Task absorption

Set config.absorbTasks = true to have the container absorb task files from other containers at startup. This is off by default. When enabled, the container scans tasks/ for task files belonging to other containers, merges their tasks into its own queue, and deletes the foreign files.

Task structure

Each task in the queue (local or webhook) has the following fields:

| Field | Type | Required | Description | |---|---|---|---| | id | string | Auto-assigned | UUID for coordination. Auto-assigned by add-task if not provided. Included in task.started/task.done webhook events. | | skill | string | Yes | Path to the skill file (e.g. skills/tasks/build/writeApp.md). | | subtasks | string[] | Yes | List of subtask descriptions. | | app | string | No | App name (directory under apps/). | | prompt | string | No | Raw prompt for message-derived tasks (no skill file). | | command | string | No | Custom command to run instead of the default claude CLI. | | maxAttempts | number | No | Maximum attempts before giving up. Default: 5. | | timeoutMinutes | number | No | Per-attempt time limit in minutes. Agent is killed if exceeded. |

External task queue

Set config.taskWebhookUrl to a GET endpoint that returns tasks to process. When the local task queue is empty, the container GETs this URL (with the same Bearer token from webhookSecret) and expects a JSON response:

  • { "tasks": [{ "skill": "...", "subtasks": [...], ... }, ...] } — process these tasks
  • {} or { "tasks": [] } — no tasks available, go idle

The first task is processed immediately; any remaining tasks are added to the local queue. Tasks use the same structure documented above.

Webhooks

Set webhookUrl on ContainerConfig to receive real-time notifications of container activity. The container POSTs JSON to that URL on key events (no retries; failures are logged to stderr). Set webhookSecret to include a Bearer token in the Authorization header for authenticating webhook requests.

Payload format

Every POST body has this shape:

{
  "type": "container.started",
  "containerName": "app-building-abc123",
  "timestamp": "2026-02-28T12:00:00.000Z",
  "data": { ... }
}

| Field | Type | Description | |---|---|---| | type | string | Event type (see table below). | | containerName | string | Name of the container that emitted the event. | | timestamp | string | ISO-8601 timestamp. | | data | object | Event-specific payload. Present on all events; contents vary by type. |

Events

| Type | When | data fields | |---|---|---| | container.started | HTTP server is listening | pushBranch, revision | | container.idle | Container is waiting for work | pendingTasks, queueLength | | container.stopping | Container is shutting down | (empty) | | container.stopped | Container has stopped | (empty) | | message.queued | POST /message received | messageId, prompt | | message.started | Message processing begins | iteration, prompt | | message.done | Message processing complete | messageId, cost_usd, duration_ms, num_turns | | message.error | Message processing failed | messageId, error | | task.started | Task processing begins | id, iteration, skill, subtasks, prompt | | task.done | Task processing complete | id, skill, subtasks, prompt, cost, totalCost, failed, pendingTasks, duration_ms | | log | Each log line | line |

Example

const orchestrationVars = loadDotEnv("/path/to/project");
const infisicalConfig = await getInfisicalConfig(orchestrationVars);

const config: ContainerConfig = {
  projectRoot: "/path/to/project",
  infisical: infisicalConfig,
  registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
  webhookUrl: "https://example.com/hooks/container-events",
  webhookSecret: "your-webhook-secret",
};