@replayio/app-building 1.0.3 → 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
@@ -13,10 +13,9 @@ npm install @replayio/app-building
13
13
  ```ts
14
14
  import {
15
15
  loadDotEnv,
16
- ContainerRegistry,
16
+ FileContainerRegistry,
17
17
  type ContainerConfig,
18
18
  type RepoOptions,
19
- startContainer,
20
19
  startRemoteContainer,
21
20
  stopRemoteContainer,
22
21
  httpGet,
@@ -27,9 +26,9 @@ import {
27
26
  // Assemble config once at startup
28
27
  const envVars = loadDotEnv("/path/to/project");
29
28
  const config: ContainerConfig = {
30
- projectRoot: "/path/to/project",
29
+ projectRoot: "/path/to/project", // optional — only needed for local Docker operations
31
30
  envVars,
32
- registry: new ContainerRegistry("/path/to/.container-registry.jsonl"),
31
+ registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
33
32
  flyToken: envVars.FLY_API_TOKEN,
34
33
  flyApp: envVars.FLY_APP_NAME,
35
34
  };
@@ -58,9 +57,10 @@ await stopRemoteContainer(config, state);
58
57
 
59
58
  | Export | Description |
60
59
  |---|---|
61
- | `ContainerConfig` | Interface bundling all external state: `projectRoot`, `envVars`, `registry`, optional `flyToken`/`flyApp`/`imageRef`. |
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. |
62
61
  | `RepoOptions` | Per-invocation git settings: `repoUrl`, `cloneBranch`, `pushBranch`. |
63
- | `ContainerRegistry` | Class encapsulating `.container-registry.jsonl` persistence. Methods: `log`, `markStopped`, `clearStopped`, `getRecent`, `find`, `findAlive`. |
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. |
64
64
 
65
65
  ### Container lifecycle
66
66
 
@@ -76,7 +76,7 @@ await stopRemoteContainer(config, state);
76
76
 
77
77
  **Types:** `AgentState`, `ContainerConfig`, `RepoOptions`
78
78
 
79
- ### Container registry (`ContainerRegistry` class)
79
+ ### Container registry (`ContainerRegistry` interface / `FileContainerRegistry` class)
80
80
 
81
81
  | Method | Description |
82
82
  |---|---|
@@ -122,3 +122,54 @@ await stopRemoteContainer(config, state);
122
122
  | Export | Description |
123
123
  |---|---|
124
124
  | `getImageRef()` | Returns `CONTAINER_IMAGE_REF` env var, or `ghcr.io/replayio/app-building:latest` by default. Used by `startRemoteContainer`. |
125
+
126
+ ## Webhooks
127
+
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).
129
+
130
+ ### Payload format
131
+
132
+ Every POST body has this shape:
133
+
134
+ ```json
135
+ {
136
+ "type": "container.started",
137
+ "containerName": "app-building-abc123",
138
+ "timestamp": "2026-02-28T12:00:00.000Z",
139
+ "data": { ... }
140
+ }
141
+ ```
142
+
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;
package/dist/container.js CHANGED
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio/app-building",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Library for managing agentic app-building containers",
5
5
  "type": "module",
6
6
  "exports": {