@replayio/app-building 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,92 +1,175 @@
1
- # app-building
1
+ # @replayio/app-building
2
2
 
3
- Simple and extensible platform for dark factory agentic app building: creating apps
4
- according to a spec without human involvement along the way. Example use cases:
3
+ 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.
5
4
 
6
- * `npm run agent -- -p "Build me an app XYZ based on this spec: ..."`
7
- * `npm run agent -- -p "Continue maintaining app XYZ and fix these bugs: ..."`
8
- * `npm run agent -- -i` for interactive access to the agent.
5
+ ## Install
9
6
 
10
- Core ideas:
7
+ ```bash
8
+ npm install @replayio/app-building
9
+ ```
11
10
 
12
- * The agent runs within a docker container that clones the target repo and exposes an HTTP server for control.
13
- * The host communicates with the container via HTTP — sending prompts, polling events/logs, and managing lifecycle.
14
- * The agent builds by following a set of skill documents with guides
15
- for breaking its work down into jobs and directives for performing those tasks.
16
- * The agent commits logs for it to review later and improve its skills.
17
- * All code changes are committed and pushed back to the remote from inside the container.
11
+ ## Usage
12
+
13
+ ```ts
14
+ import {
15
+ loadDotEnv,
16
+ FileContainerRegistry,
17
+ type ContainerConfig,
18
+ type RepoOptions,
19
+ startRemoteContainer,
20
+ stopRemoteContainer,
21
+ httpGet,
22
+ httpPost,
23
+ httpOptsFor,
24
+ } from "@replayio/app-building";
25
+
26
+ // Assemble config once at startup
27
+ const envVars = loadDotEnv("/path/to/project");
28
+ const config: ContainerConfig = {
29
+ projectRoot: "/path/to/project", // optional — only needed for local Docker operations
30
+ envVars,
31
+ registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
32
+ flyToken: envVars.FLY_API_TOKEN,
33
+ flyApp: envVars.FLY_APP_NAME,
34
+ };
35
+
36
+ // Start a remote container
37
+ const repo: RepoOptions = { repoUrl: "https://github.com/...", cloneBranch: "main", pushBranch: "main" };
38
+ const state = await startRemoteContainer(config, repo);
39
+
40
+ // Send a prompt and wait for completion
41
+ const httpOpts = httpOptsFor(state);
42
+ const { id } = await httpPost(`${state.baseUrl}/message`, { prompt: "Build the app" }, httpOpts);
43
+
44
+ // Check status
45
+ const status = await httpGet(`${state.baseUrl}/status`, httpOpts);
46
+
47
+ // Query the registry
48
+ const alive = await config.registry.findAlive();
49
+
50
+ // Clean up
51
+ await stopRemoteContainer(config, state);
52
+ ```
18
53
 
19
- Containers can run locally or remotely.
54
+ ## Exported API
20
55
 
21
- ## Setup
56
+ ### Domain objects
22
57
 
23
- ```bash
24
- npm install
25
- ```
58
+ | Export | Description |
59
+ |---|---|
60
+ | `ContainerConfig` | Interface bundling all external state: optional `projectRoot` (only needed for local Docker operations), `envVars`, `registry`, optional `flyToken`/`flyApp`/`imageRef`/`webhookUrl`. See [Webhooks](#webhooks) below. |
61
+ | `RepoOptions` | Per-invocation git settings: `repoUrl`, `cloneBranch`, `pushBranch`. |
62
+ | `ContainerRegistry` | Interface for container registry storage. Methods: `log`, `markStopped`, `clearStopped`, `getRecent`, `find`, `findAlive`. |
63
+ | `FileContainerRegistry` | Built-in file-backed implementation of `ContainerRegistry`, backed by a `.jsonl` file. |
26
64
 
27
- Copy `.env.example` to `.env` and fill in all required API keys.
65
+ ### Container lifecycle
28
66
 
29
- ## Running the Agent
67
+ | Export | Description |
68
+ |---|---|
69
+ | `startContainer(config, repo)` | Build the Docker image locally and start a container with `--network host`. Returns `AgentState`. |
70
+ | `startRemoteContainer(config, repo)` | Create a Fly.io machine using the GHCR image. Requires `config.flyToken` and `config.flyApp`. Returns `AgentState`. |
71
+ | `stopContainer(config, containerName)` | Stop a local Docker container by name. |
72
+ | `stopRemoteContainer(config, state)` | Destroy the Fly.io machine for a remote container. Requires `config.flyToken`. |
73
+ | `buildImage(config)` | Build the Docker image locally (called automatically by `startContainer`). |
74
+ | `spawnTestContainer(config)` | Start an interactive (`-it`) container with the repo mounted at `/repo`. |
75
+ | `loadDotEnv(projectRoot)` | Parse a `.env` file and return key-value pairs. |
30
76
 
31
- `npm run agent` starts a new container with the running agent. By default the container is local, add `--remote` to spawn the container remotely. This requires FLY_API_TOKEN in .env
77
+ **Types:** `AgentState`, `ContainerConfig`, `RepoOptions`
32
78
 
33
- ### Detached mode
79
+ ### Container registry (`ContainerRegistry` interface / `FileContainerRegistry` class)
34
80
 
35
- ```bash
36
- npm run agent
37
- npm run agent -- -p "<prompt>"
38
- npm run agent -- --branch dev --push-branch feature/xyz -p "<prompt>"
39
- ```
81
+ | Method | Description |
82
+ |---|---|
83
+ | `log(state)` | Append a new entry to the registry. |
84
+ | `markStopped(name?)` | Mark a container as stopped. |
85
+ | `clearStopped(name)` | Clear the stopped flag (container came back alive). |
86
+ | `getRecent(limit?)` | Read the most recent registry entries. |
87
+ | `find(name)` | Find a specific container by name. |
88
+ | `findAlive()` | Probe recent entries and return those that are alive. Reconciles stopped flags. |
40
89
 
41
- Starts a container, optionally queues a prompt, then detaches. The container processes the prompt followed by any pending tasks, commits and pushes results, then exits.
90
+ **Types:** `RegistryEntry`
42
91
 
43
- ### Interactive mode
92
+ ### HTTP client
44
93
 
45
- ```bash
46
- npm run agent -- -i
47
- ```
94
+ | Export | Description |
95
+ |---|---|
96
+ | `httpGet(url, opts?)` | GET with retries and timeout. Returns parsed JSON. |
97
+ | `httpPost(url, body?, opts?)` | POST with retries and timeout. Returns parsed JSON. |
48
98
 
49
- Chat with the agent inside a container. Output is streamed via event polling. Press ESC to interrupt the current message. On exit, the container is detached and finishes any remaining work.
99
+ **Types:** `HttpOptions`
50
100
 
51
- ### Checking status
101
+ ### Container utilities
52
102
 
53
- ```bash
54
- npm run status
55
- ```
103
+ | Export | Description |
104
+ |---|---|
105
+ | `httpOptsFor(state)` | Return `HttpOptions` for a container (adds `fly-force-instance-id` header for remote containers). |
106
+ | `probeAlive(entry)` | Check if a container is responding to `/status`. |
56
107
 
57
- Connects to the running container's HTTP API and shows state, revision, queue depth, cost, and recent log output. Tails logs in real-time (Ctrl+C to stop). Errors if no agent is running.
108
+ ### Fly.io machines
58
109
 
59
- ### Stopping the agent
110
+ | Export | Description |
111
+ |---|---|
112
+ | `createApp(token, name, org?)` | Create a Fly app and allocate IPs. |
113
+ | `createMachine(app, token, image, env, name)` | Create a Fly machine. Returns machine ID. |
114
+ | `waitForMachine(app, token, machineId, timeout?)` | Wait for a machine to reach `started` state. |
115
+ | `destroyMachine(app, token, machineId)` | Force-destroy a machine. |
116
+ | `listMachines(app, token)` | List all machines for an app. |
60
117
 
61
- ```bash
62
- npm run stop
63
- npm run stop -- <containerName>
64
- ```
118
+ **Types:** `FlyMachineInfo`
65
119
 
66
- Sends an HTTP stop signal. Without arguments, finds and stops all running containers. Pass a container name to stop a specific one.
120
+ ### Image ref
67
121
 
68
- ## Skills
122
+ | Export | Description |
123
+ |---|---|
124
+ | `getImageRef()` | Returns `CONTAINER_IMAGE_REF` env var, or `ghcr.io/replayio/app-building:latest` by default. Used by `startRemoteContainer`. |
69
125
 
70
- The provided skill documents emphasize a structured approach for autonomously building
71
- high quality, well tested apps from the initial spec. During the initial app build
72
- it does the following:
126
+ ## Webhooks
73
127
 
74
- 1. Designs a comprehensive test specification based on the initial spec.
75
- 2. Builds the app and writes tests to match the spec.
76
- 3. Gets all the tests to pass, deploys to production, and does further testing.
128
+ Set `webhookUrl` on `ContainerConfig` to receive real-time notifications of container activity. The container POSTs fire-and-forget JSON to that URL on key events (no retries, failures are silently ignored).
77
129
 
78
- The initial build will not come out perfect. The agent can followup with maintenance passes
79
- where it checks to make sure it closely followed the spec and skill directives and fixes
80
- any issues it finds. It will also fix reported bugs and update the skills to avoid
81
- similar problems in the future.
130
+ ### Payload format
82
131
 
83
- As long as each individual step the agent takes is within its capabilities (it can usually
84
- do it but not always) the agent will converge on an app that follows the initial spec
85
- and skill directives.
132
+ Every POST body has this shape:
86
133
 
87
- Key things to watch out for:
134
+ ```json
135
+ {
136
+ "type": "container.started",
137
+ "containerName": "app-building-abc123",
138
+ "timestamp": "2026-02-28T12:00:00.000Z",
139
+ "data": { ... }
140
+ }
141
+ ```
88
142
 
89
- * Best suited for CRUD and API-calling apps up to a medium level of complexity.
90
- Overly complicated or specialized apps will not work as well yet.
91
- * Make sure to get a Replay API key and configure it. The agent will use Replay to identify
92
- and debug problems it encounters in tests or the deployed app.
143
+ | Field | Type | Description |
144
+ |---|---|---|
145
+ | `type` | `string` | Event type (see table below). |
146
+ | `containerName` | `string` | Name of the container that emitted the event. |
147
+ | `timestamp` | `string` | ISO-8601 timestamp. |
148
+ | `data` | `object` | Event-specific payload. Present on all events; contents vary by type. |
149
+
150
+ ### Events
151
+
152
+ | Type | When | `data` fields |
153
+ |---|---|---|
154
+ | `container.started` | HTTP server is listening | `pushBranch`, `revision` |
155
+ | `container.idle` | State transitions to idle | `pendingTasks`, `queueLength` |
156
+ | `container.processing` | State transitions to processing | `iteration` |
157
+ | `container.stopping` | State transitions to stopping | _(empty)_ |
158
+ | `container.stopped` | State transitions to stopped | _(empty)_ |
159
+ | `message.queued` | `POST /message` received | `messageId`, `prompt` |
160
+ | `message.done` | Message processing complete | `messageId`, `cost_usd`, `duration_ms`, `num_turns` |
161
+ | `message.error` | Message processing failed | `messageId`, `error` |
162
+ | `task.started` | Task processing begins | `pendingTasks` |
163
+ | `task.done` | Task processing complete | `tasksProcessed`, `totalCost` |
164
+ | `log` | Each log line | `line` |
165
+
166
+ ### Example
167
+
168
+ ```ts
169
+ const config: ContainerConfig = {
170
+ projectRoot: "/path/to/project",
171
+ envVars: loadDotEnv("/path/to/project"),
172
+ registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
173
+ webhookUrl: "https://example.com/hooks/container-events",
174
+ };
175
+ ```
@@ -3,7 +3,15 @@ export interface RegistryEntry extends AgentState {
3
3
  startedAt: string;
4
4
  stoppedAt?: string;
5
5
  }
6
- export declare class ContainerRegistry {
6
+ export interface ContainerRegistry {
7
+ log(state: AgentState): void;
8
+ markStopped(containerName?: string): void;
9
+ clearStopped(containerName: string): void;
10
+ getRecent(limit?: number): RegistryEntry[];
11
+ find(containerName: string): RegistryEntry | null;
12
+ findAlive(): Promise<RegistryEntry[]>;
13
+ }
14
+ export declare class FileContainerRegistry implements ContainerRegistry {
7
15
  private filePath;
8
16
  constructor(filePath: string);
9
17
  private readRegistry;
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync, appendFileSync, existsSync } from "fs";
2
2
  import { probeAlive } from "./container-utils";
3
- export class ContainerRegistry {
3
+ export class FileContainerRegistry {
4
4
  filePath;
5
5
  constructor(filePath) {
6
6
  this.filePath = filePath;
@@ -8,12 +8,13 @@ export interface AgentState {
8
8
  flyMachineId?: string;
9
9
  }
10
10
  export interface ContainerConfig {
11
- projectRoot: string;
11
+ projectRoot?: string;
12
12
  envVars: Record<string, string>;
13
13
  registry: ContainerRegistry;
14
14
  flyToken?: string;
15
15
  flyApp?: string;
16
16
  imageRef?: string;
17
+ webhookUrl?: string;
17
18
  }
18
19
  export interface RepoOptions {
19
20
  repoUrl: string;
@@ -28,6 +28,8 @@ export function loadDotEnv(projectRoot) {
28
28
  return vars;
29
29
  }
30
30
  export function buildImage(config) {
31
+ if (!config.projectRoot)
32
+ throw new Error("projectRoot is required for local Docker operations");
31
33
  console.log("Building Docker image...");
32
34
  execFileSync("docker", ["build", "--network", "host", "-t", IMAGE_NAME, config.projectRoot], {
33
35
  stdio: "inherit",
@@ -92,10 +94,13 @@ export async function startContainer(config, repo) {
92
94
  const uniqueId = Math.random().toString(36).slice(2, 8);
93
95
  const containerName = `app-building-${uniqueId}`;
94
96
  const hostPort = findFreePort();
95
- const containerEnv = buildContainerEnv(repo, config.envVars, {
97
+ const extra = {
96
98
  PORT: String(hostPort),
97
99
  CONTAINER_NAME: containerName,
98
- });
100
+ };
101
+ if (config.webhookUrl)
102
+ extra.WEBHOOK_URL = config.webhookUrl;
103
+ const containerEnv = buildContainerEnv(repo, config.envVars, extra);
99
104
  // Build docker run args
100
105
  const args = ["run", "-d", "--rm", "--name", containerName];
101
106
  // --network host: container shares host network stack (no -p needed)
@@ -163,10 +168,13 @@ export async function startRemoteContainer(config, repo) {
163
168
  const uniqueId = Math.random().toString(36).slice(2, 8);
164
169
  const machineName = `app-building-${uniqueId}`;
165
170
  // Build env vars for the machine
166
- const containerEnv = buildContainerEnv(repo, config.envVars, {
171
+ const remoteExtra = {
167
172
  PORT: "3000",
168
173
  CONTAINER_NAME: machineName,
169
- });
174
+ };
175
+ if (config.webhookUrl)
176
+ remoteExtra.WEBHOOK_URL = config.webhookUrl;
177
+ const containerEnv = buildContainerEnv(repo, config.envVars, remoteExtra);
170
178
  // Remove Fly-specific vars from container env (not needed inside)
171
179
  delete containerEnv.FLY_API_TOKEN;
172
180
  delete containerEnv.FLY_APP_NAME;
@@ -260,6 +268,8 @@ export function stopContainer(config, containerName) {
260
268
  config.registry.markStopped(containerName);
261
269
  }
262
270
  export function spawnTestContainer(config) {
271
+ if (!config.projectRoot)
272
+ throw new Error("projectRoot is required for local Docker operations");
263
273
  ensureImageExists(config.projectRoot);
264
274
  const uniqueId = Math.random().toString(36).slice(2, 8);
265
275
  const containerName = `app-building-test-${uniqueId}`;
package/package.json CHANGED
@@ -1,35 +1,18 @@
1
1
  {
2
2
  "name": "@replayio/app-building",
3
- "version": "1.0.2",
4
- "description": "Simple agent loop for building apps with Claude Code CLI",
3
+ "version": "1.1.0",
4
+ "description": "Library for managing agentic app-building containers",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "import": "./dist/package/index.js",
9
- "types": "./dist/package/index.d.ts"
8
+ "import": "./dist/index.js",
9
+ "types": "./dist/index.d.ts"
10
10
  }
11
11
  },
12
12
  "files": [
13
- "dist/package"
13
+ "dist"
14
14
  ],
15
- "readme": "src/package/README.md",
16
15
  "scripts": {
17
- "agent": "tsx src/agent.ts",
18
- "status": "tsx src/status.ts",
19
- "read-log": "tsx src/read-log.ts",
20
- "reset-app": "tsx src/reset-app.ts",
21
- "docker:build": "docker build --no-cache --network=host -t app-building .",
22
- "test-container": "tsx src/test-container.ts",
23
- "stop": "tsx src/stop.ts",
24
- "fly-machines": "tsx src/fly-machines.ts",
25
- "add-task": "tsx scripts/add-task.ts"
26
- },
27
- "devDependencies": {
28
- "@types/node": "^25.3.0",
29
- "tsx": "^4.19.0",
30
- "typescript": "^5.5.0"
31
- },
32
- "dependencies": {
33
- "commander": "^14.0.3"
16
+ "prepublishOnly": "tsc -p tsconfig.json"
34
17
  }
35
18
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes