@cloudflare/sandbox 0.13.0-next.651.1 → 0.13.0-next.709.1

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.
Files changed (67) hide show
  1. package/Dockerfile +12 -17
  2. package/README.md +48 -2
  3. package/dist/bridge/index.d.ts.map +1 -1
  4. package/dist/bridge/index.js +1365 -1054
  5. package/dist/bridge/index.js.map +1 -1
  6. package/dist/{contexts-BS0Bs6IU.d.ts → contexts-1EsLHByO.d.ts} +240 -15
  7. package/dist/contexts-1EsLHByO.d.ts.map +1 -0
  8. package/dist/{dist-DF8sudAg.js → dist-Duor5GbS.js} +38 -147
  9. package/dist/dist-Duor5GbS.js.map +1 -0
  10. package/dist/errors/index.d.ts +4 -0
  11. package/dist/errors/index.js +4 -0
  12. package/dist/{errors-BG6NZiPD.js → errors-CXR0xBpw.js} +82 -13
  13. package/dist/errors-CXR0xBpw.js.map +1 -0
  14. package/dist/errors-QYlSkVGz.js +893 -0
  15. package/dist/errors-QYlSkVGz.js.map +1 -0
  16. package/dist/extensions/index.d.ts +4 -74
  17. package/dist/extensions/index.js +5 -152
  18. package/dist/extensions-CFB2xHqY.js +1023 -0
  19. package/dist/extensions-CFB2xHqY.js.map +1 -0
  20. package/dist/filesystem-BWAZCZER.d.ts +732 -0
  21. package/dist/filesystem-BWAZCZER.d.ts.map +1 -0
  22. package/dist/git/index.d.ts +63 -0
  23. package/dist/git/index.d.ts.map +1 -0
  24. package/dist/git/index.js +338 -0
  25. package/dist/git/index.js.map +1 -0
  26. package/dist/index-Bs4bqXDR.d.ts +438 -0
  27. package/dist/index-Bs4bqXDR.d.ts.map +1 -0
  28. package/dist/index-HNYBk-az.d.ts +444 -0
  29. package/dist/index-HNYBk-az.d.ts.map +1 -0
  30. package/dist/index.d.ts +487 -181
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +12 -43
  33. package/dist/index.js.map +1 -1
  34. package/dist/interpreter/index.d.ts +311 -0
  35. package/dist/interpreter/index.d.ts.map +1 -0
  36. package/dist/interpreter/index.js +292 -0
  37. package/dist/interpreter/index.js.map +1 -0
  38. package/dist/openai/index.d.ts +5 -4
  39. package/dist/openai/index.d.ts.map +1 -1
  40. package/dist/openai/index.js +11 -6
  41. package/dist/openai/index.js.map +1 -1
  42. package/dist/opencode/index.d.ts +133 -161
  43. package/dist/opencode/index.d.ts.map +1 -1
  44. package/dist/opencode/index.js +284 -203
  45. package/dist/opencode/index.js.map +1 -1
  46. package/dist/process-types-GStiZ8f8.d.ts +73 -0
  47. package/dist/process-types-GStiZ8f8.d.ts.map +1 -0
  48. package/dist/sandbox-Auuwfnur.js +10010 -0
  49. package/dist/sandbox-Auuwfnur.js.map +1 -0
  50. package/dist/sandbox-BbAabq93.d.ts +42 -0
  51. package/dist/sandbox-BbAabq93.d.ts.map +1 -0
  52. package/dist/xterm/index.d.ts +11 -7
  53. package/dist/xterm/index.d.ts.map +1 -1
  54. package/dist/xterm/index.js +61 -18
  55. package/dist/xterm/index.js.map +1 -1
  56. package/package.json +28 -4
  57. package/dist/contexts-BS0Bs6IU.d.ts.map +0 -1
  58. package/dist/dist-DF8sudAg.js.map +0 -1
  59. package/dist/errors-BG6NZiPD.js.map +0 -1
  60. package/dist/extensions/index.d.ts.map +0 -1
  61. package/dist/extensions/index.js.map +0 -1
  62. package/dist/rpc-types-PBUY-xXM.d.ts +0 -1679
  63. package/dist/rpc-types-PBUY-xXM.d.ts.map +0 -1
  64. package/dist/sandbox-BgwMBQ7S.js +0 -8273
  65. package/dist/sandbox-BgwMBQ7S.js.map +0 -1
  66. package/dist/sandbox-D_MMqExx.d.ts +0 -1077
  67. package/dist/sandbox-D_MMqExx.d.ts.map +0 -1
@@ -1,60 +1,103 @@
1
- import { s as createLogger } from "../dist-DF8sudAg.js";
2
- import { r as ErrorCode } from "../errors-BG6NZiPD.js";
1
+ import { i as ErrorCode } from "../errors-CXR0xBpw.js";
2
+ import "../errors-QYlSkVGz.js";
3
+ import { r as createLogger } from "../dist-Duor5GbS.js";
4
+ import { n as createExtensionProcessSandbox, t as SandboxExtension } from "../extensions-CFB2xHqY.js";
3
5
 
4
- //#region src/opencode/types.ts
6
+ //#region ../../extensions/opencode/src/client.ts
7
+ let createSDKClient;
8
+ async function loadSDK() {
9
+ if (createSDKClient) return createSDKClient;
10
+ try {
11
+ createSDKClient = (await import("@opencode-ai/sdk/v2/client")).createOpencodeClient;
12
+ return createSDKClient;
13
+ } catch {
14
+ throw new Error("@opencode-ai/sdk is required for OpenCode integration. Install it with: npm install @opencode-ai/sdk");
15
+ }
16
+ }
17
+ /**
18
+ * Build a typed OpenCode SDK client from a lifecycle handle.
19
+ *
20
+ * Works against either the Worker stub (`sandbox.opencode`) or the in-DO object
21
+ * (`this.opencode`): it ensures the server through the handle, reads the stored
22
+ * config, and routes every request through `handle.fetch`. Because the handle
23
+ * owns `containerFetch`, this helper never touches sandbox transport directly.
24
+ *
25
+ * The SDK is imported lazily so the peer dependency is only required when an
26
+ * OpenCode client is actually built.
27
+ */
28
+ async function createOpenCodeClient(handle, options) {
29
+ const server = await handle.start(options);
30
+ const config = await handle.config();
31
+ const directory = options?.directory ?? config.directory;
32
+ return (await loadSDK())({
33
+ baseUrl: server.url,
34
+ fetch: (input, init) => handle.fetch(new Request(input, init)),
35
+ directory
36
+ });
37
+ }
38
+
39
+ //#endregion
40
+ //#region ../../extensions/opencode/src/types.ts
5
41
  /**
6
42
  * Error thrown when OpenCode server fails to start
7
43
  */
8
- var OpencodeStartupError = class extends Error {
44
+ var OpenCodeStartupError = class extends Error {
9
45
  code = ErrorCode.OPENCODE_STARTUP_FAILED;
10
46
  context;
11
47
  constructor(message, context, options) {
12
48
  super(message, options);
13
- this.name = "OpencodeStartupError";
49
+ this.name = "OpenCodeStartupError";
14
50
  this.context = context;
15
51
  }
16
52
  };
17
53
 
18
54
  //#endregion
19
- //#region src/opencode/opencode.ts
55
+ //#region ../../extensions/opencode/src/opencode.ts
20
56
  function getLogger() {
21
57
  return createLogger({
22
58
  component: "sandbox-do",
23
59
  operation: "opencode"
24
60
  });
25
61
  }
26
- const DEFAULT_PORT = 4096;
62
+ const DEFAULT_PORT$1 = 4096;
27
63
  const OPENCODE_STARTUP_TIMEOUT_MS = 18e4;
28
- const OPENCODE_SERVE = (port) => `opencode serve --port ${port} --hostname 0.0.0.0`;
29
- /**
30
- * Build the full command, optionally with a directory prefix.
31
- * If directory is provided, we cd to it first so OpenCode uses it as cwd.
32
- */
33
- function buildOpencodeCommand(port, directory) {
34
- const serve = OPENCODE_SERVE(port);
35
- return directory ? `cd ${directory} && ${serve}` : serve;
64
+ function openCodeCommand(port) {
65
+ return [
66
+ "opencode",
67
+ "serve",
68
+ "--port",
69
+ String(port),
70
+ "--hostname",
71
+ "0.0.0.0"
72
+ ];
36
73
  }
37
- let createOpencodeClient;
38
- async function ensureSdkLoaded() {
39
- if (createOpencodeClient) return;
40
- try {
41
- createOpencodeClient = (await import("@opencode-ai/sdk/v2/client")).createOpencodeClient;
42
- } catch {
43
- throw new Error("@opencode-ai/sdk is required for OpenCode integration. Install it with: npm install @opencode-ai/sdk");
44
- }
74
+ function commandsEqual(a, b) {
75
+ return a.length === b.length && a.every((value, index) => value === b[index]);
45
76
  }
46
- /**
47
- * Find an existing OpenCode server process running on the specified port.
48
- * Returns the process if found and still active, null otherwise.
49
- * Matches by the serve command pattern since directory prefix may vary.
50
- */
51
- async function findExistingOpencodeProcess(sandbox, port) {
52
- const processes = await sandbox.listProcesses();
53
- const serveCommand = OPENCODE_SERVE(port);
54
- for (const proc of processes) if (proc.command.includes(serveCommand)) {
55
- if (proc.status === "starting" || proc.status === "running") return proc;
77
+ /** Recover a running OpenCode process from runtime-local process status. */
78
+ async function findExistingOpenCodeProcess(sandbox, port, directory) {
79
+ const command = openCodeCommand(port);
80
+ const processes = createExtensionProcessSandbox(sandbox);
81
+ const status = (await processes.listProcesses()).find((candidate) => candidate.state === "running" && candidate.cwd === directory && commandsEqual(candidate.command, command));
82
+ return status ? processes.getProcess(status.id) : null;
83
+ }
84
+ async function collectStderr(process) {
85
+ const reader = (await process.logs({
86
+ replay: true,
87
+ follow: false
88
+ })).getReader();
89
+ const decoder = new TextDecoder();
90
+ let stderr = "";
91
+ try {
92
+ while (true) {
93
+ const { done, value } = await reader.read();
94
+ if (done) break;
95
+ if (value.type === "stderr") stderr += decoder.decode(value.data, { stream: true });
96
+ }
97
+ return stderr + decoder.decode();
98
+ } finally {
99
+ reader.releaseLock();
56
100
  }
57
- return null;
58
101
  }
59
102
  /**
60
103
  * Ensures OpenCode server is running in the container.
@@ -62,30 +105,9 @@ async function findExistingOpencodeProcess(sandbox, port) {
62
105
  * Handles concurrent startup attempts gracefully by retrying on failure.
63
106
  * Returns the process handle.
64
107
  */
65
- async function ensureOpencodeServer(sandbox, port, directory, config, customEnv) {
66
- const existingProcess = await findExistingOpencodeProcess(sandbox, port);
108
+ async function ensureOpenCodeServer(sandbox, port, directory, config, customEnv) {
109
+ const existingProcess = await findExistingOpenCodeProcess(sandbox, port, directory);
67
110
  if (existingProcess) {
68
- if (existingProcess.status === "starting") {
69
- getLogger().debug("Found starting OpenCode process, waiting for ready", {
70
- port,
71
- processId: existingProcess.id
72
- });
73
- try {
74
- await existingProcess.waitForPort(port, {
75
- mode: "http",
76
- path: "/path",
77
- status: 200,
78
- timeout: OPENCODE_STARTUP_TIMEOUT_MS
79
- });
80
- } catch (e) {
81
- const logs = await existingProcess.getLogs();
82
- throw new OpencodeStartupError(`OpenCode server failed to start. Stderr: ${logs.stderr || "(empty)"}`, {
83
- port,
84
- stderr: logs.stderr,
85
- command: existingProcess.command
86
- }, { cause: e });
87
- }
88
- }
89
111
  getLogger().debug("Reusing existing OpenCode process", {
90
112
  port,
91
113
  processId: existingProcess.id
@@ -93,29 +115,14 @@ async function ensureOpencodeServer(sandbox, port, directory, config, customEnv)
93
115
  return existingProcess;
94
116
  }
95
117
  try {
96
- return await startOpencodeServer(sandbox, port, directory, config, customEnv);
118
+ return await startOpenCodeServer(sandbox, port, directory, config, customEnv);
97
119
  } catch (startupError) {
98
- const retryProcess = await findExistingOpencodeProcess(sandbox, port);
120
+ const retryProcess = await findExistingOpenCodeProcess(sandbox, port, directory);
99
121
  if (retryProcess) {
100
122
  getLogger().debug("Startup failed but found concurrent process, reusing", {
101
123
  port,
102
124
  processId: retryProcess.id
103
125
  });
104
- if (retryProcess.status === "starting") try {
105
- await retryProcess.waitForPort(port, {
106
- mode: "http",
107
- path: "/path",
108
- status: 200,
109
- timeout: OPENCODE_STARTUP_TIMEOUT_MS
110
- });
111
- } catch (e) {
112
- const logs = await retryProcess.getLogs();
113
- throw new OpencodeStartupError(`OpenCode server failed to start. Stderr: ${logs.stderr || "(empty)"}`, {
114
- port,
115
- stderr: logs.stderr,
116
- command: retryProcess.command
117
- }, { cause: e });
118
- }
119
126
  return retryProcess;
120
127
  }
121
128
  throw startupError;
@@ -124,7 +131,7 @@ async function ensureOpencodeServer(sandbox, port, directory, config, customEnv)
124
131
  /**
125
132
  * Internal function to start a new OpenCode server process.
126
133
  */
127
- async function startOpencodeServer(sandbox, port, directory, config, customEnv) {
134
+ async function startOpenCodeServer(sandbox, port, directory, config, customEnv) {
128
135
  getLogger().info("Starting OpenCode server", {
129
136
  port,
130
137
  directory
@@ -152,8 +159,11 @@ async function startOpencodeServer(sandbox, port, directory, config, customEnv)
152
159
  }
153
160
  }
154
161
  if (customEnv) Object.assign(env, customEnv);
155
- const command = buildOpencodeCommand(port, directory);
156
- const process = await sandbox.startProcess(command, { env: Object.keys(env).length > 0 ? env : void 0 });
162
+ const command = openCodeCommand(port);
163
+ const process = await createExtensionProcessSandbox(sandbox).exec(command, {
164
+ cwd: directory,
165
+ env: Object.keys(env).length > 0 ? env : void 0
166
+ });
157
167
  try {
158
168
  await process.waitForPort(port, {
159
169
  mode: "http",
@@ -166,16 +176,16 @@ async function startOpencodeServer(sandbox, port, directory, config, customEnv)
166
176
  processId: process.id
167
177
  });
168
178
  } catch (e) {
169
- const logs = await process.getLogs();
179
+ const stderr = await collectStderr(process);
170
180
  const error = e instanceof Error ? e : void 0;
171
181
  getLogger().error("OpenCode server failed to start", error, {
172
182
  port,
173
- stderr: logs.stderr
183
+ stderr
174
184
  });
175
- throw new OpencodeStartupError(`OpenCode server failed to start. Stderr: ${logs.stderr || "(empty)"}`, {
185
+ throw new OpenCodeStartupError(`OpenCode server failed to start. Stderr: ${stderr || "(empty)"}`, {
176
186
  port,
177
- stderr: logs.stderr,
178
- command
187
+ stderr,
188
+ command: command.join(" ")
179
189
  }, { cause: e });
180
190
  }
181
191
  return process;
@@ -183,7 +193,7 @@ async function startOpencodeServer(sandbox, port, directory, config, customEnv)
183
193
  /**
184
194
  * Starts an OpenCode server inside a Sandbox container.
185
195
  *
186
- * This function manages the server lifecycle only - use `createOpencode()` if you
196
+ * This function manages the server lifecycle only - use `createOpenCodeClient()` if you
187
197
  * also need a typed SDK client for programmatic access.
188
198
  *
189
199
  * If an OpenCode server is already running on the specified port, this function
@@ -196,10 +206,10 @@ async function startOpencodeServer(sandbox, port, directory, config, customEnv)
196
206
  * @example
197
207
  * ```typescript
198
208
  * import { getSandbox } from '@cloudflare/sandbox'
199
- * import { createOpencodeServer } from '@cloudflare/sandbox/opencode'
209
+ * import { createOpenCodeServer } from '@cloudflare/sandbox/opencode'
200
210
  *
201
211
  * const sandbox = getSandbox(env.Sandbox, 'my-agent')
202
- * const server = await createOpencodeServer(sandbox, {
212
+ * const server = await createOpenCodeServer(sandbox, {
203
213
  * directory: '/home/user/my-project',
204
214
  * config: {
205
215
  * provider: {
@@ -226,148 +236,219 @@ async function startOpencodeServer(sandbox, port, directory, config, customEnv)
226
236
  * await server.close()
227
237
  * ```
228
238
  */
229
- async function createOpencodeServer(sandbox, options) {
230
- const port = options?.port ?? DEFAULT_PORT;
231
- const process = await ensureOpencodeServer(sandbox, port, options?.directory, options?.config, options?.env);
239
+ async function createOpenCodeServer(sandbox, options) {
240
+ const port = options?.port ?? DEFAULT_PORT$1;
241
+ const process = await ensureOpenCodeServer(sandbox, port, options?.directory, options?.config, options?.env);
232
242
  return {
233
243
  port,
234
244
  url: `http://localhost:${port}`,
235
- close: () => process.kill("SIGTERM")
245
+ close: async () => {
246
+ await process.kill(15);
247
+ await process.waitForExit().catch(() => {});
248
+ }
236
249
  };
237
250
  }
251
+
252
+ //#endregion
253
+ //#region ../../extensions/opencode/src/lifecycle.ts
254
+ const DEFAULT_PORT = 4096;
255
+ const STATE_KEY_PREFIX = "opencode:desired-state:";
256
+ /** Recognise the retryable container-unavailable error across RPC. */
257
+ function isContainerUnavailable(error) {
258
+ if (typeof error !== "object" || error === null) return false;
259
+ return error.name === "ContainerUnavailableError";
260
+ }
238
261
  /**
239
- * Creates an OpenCode server inside a Sandbox container and returns a typed SDK client.
262
+ * DO-resident lifecycle handle for an OpenCode server. Owns the durable
263
+ * `opencode serve` process: start/reuse, stop, status, and request proxying.
240
264
  *
241
- * This function is API-compatible with OpenCode's own createOpencode(), but uses
242
- * Sandbox process management instead of Node.js spawn. The returned client uses
243
- * a custom fetch adapter to route requests through the Sandbox container.
265
+ * It extends {@link SandboxExtension}, so the DO stub exposes it as
266
+ * `sandbox.opencode` and method calls dispatch through `callExtension` the
267
+ * same path the interpreter extension uses. `createOpenCodeClient` consumes the
268
+ * same handle from either the Worker stub or the in-DO object.
244
269
  *
245
- * If an OpenCode server is already running on the specified port, this function
246
- * will reuse it instead of starting a new one.
247
- *
248
- * @param sandbox - The Sandbox instance to run OpenCode in
249
- * @param options - Configuration options
250
- * @returns Promise resolving to { client, server }
251
- *
252
- * @example
253
- * ```typescript
254
- * import { getSandbox } from '@cloudflare/sandbox'
255
- * import { createOpencode } from '@cloudflare/sandbox/opencode'
256
- *
257
- * const sandbox = getSandbox(env.Sandbox, 'my-agent')
258
- * const { client, server } = await createOpencode(sandbox, {
259
- * directory: '/home/user/my-project',
260
- * config: {
261
- * provider: {
262
- * anthropic: {
263
- * options: { apiKey: env.ANTHROPIC_KEY }
264
- * },
265
- * // Or use Cloudflare AI Gateway (with unified billing, no provider keys needed).
266
- * // 'cloudflare-ai-gateway': {
267
- * // options: {
268
- * // accountId: env.CF_ACCOUNT_ID,
269
- * // gatewayId: env.CF_GATEWAY_ID,
270
- * // apiToken: env.CF_API_TOKEN
271
- * // },
272
- * // models: { 'anthropic/claude-sonnet-4-5-20250929': {} }
273
- * // }
274
- * }
275
- * },
276
- * // Optional: Pass additional environment variables (e.g., for OTEL telemetry)
277
- * env: {
278
- * OTEL_EXPORTER_OTLP_ENDPOINT: 'http://127.0.0.1:4318',
279
- * TRACEPARENT: '00-abc123-def456-01'
280
- * }
281
- * })
282
- *
283
- * // Use the SDK client for programmatic access
284
- * const session = await client.session.create()
270
+ * The server starts lazily: nothing runs until `start()` (or `fetch()`, which
271
+ * calls it) is invoked. To start optimistically, call `opencode.start()` from
272
+ * your Sandbox subclass's `onStart`.
273
+ */
274
+ var OpenCodeHandle = class extends SandboxExtension {
275
+ #sandbox;
276
+ #defaults;
277
+ #storage;
278
+ #stateKey;
279
+ #server;
280
+ #lastOptions;
281
+ constructor(sandbox, defaults = {}, storage, stateIndex = 0) {
282
+ super(sandbox);
283
+ this.#sandbox = sandbox;
284
+ this.#defaults = defaults;
285
+ this.#storage = storage;
286
+ this.#stateKey = `${STATE_KEY_PREFIX}${stateIndex}`;
287
+ }
288
+ /**
289
+ * Start or reuse the OpenCode server, returning RPC-safe metadata. With no
290
+ * options it reuses the last-used config, recovering persisted desired-state
291
+ * after a cold start. Retries once on a transient `CONTAINER_UNAVAILABLE`
292
+ * (e.g. a rollout in flight).
293
+ */
294
+ async start(options) {
295
+ const recovered = options ?? this.#lastOptions ?? await this.#loadPersisted();
296
+ const resolved = {
297
+ ...this.#defaults,
298
+ ...recovered
299
+ };
300
+ this.#lastOptions = resolved;
301
+ await this.#persist(resolved);
302
+ let server;
303
+ try {
304
+ server = await createOpenCodeServer(this.#sandbox, resolved);
305
+ } catch (error) {
306
+ if (!isContainerUnavailable(error)) throw error;
307
+ server = await createOpenCodeServer(this.#sandbox, resolved);
308
+ }
309
+ this.#server = server;
310
+ return {
311
+ port: server.port,
312
+ url: server.url
313
+ };
314
+ }
315
+ /** Stop the running server, if one was started through this handle. */
316
+ async stop() {
317
+ if (!this.#server) return;
318
+ await this.#server.close();
319
+ this.#server = void 0;
320
+ }
321
+ /** Report whether the named OpenCode server is currently running. */
322
+ async status() {
323
+ const resolved = {
324
+ ...this.#defaults,
325
+ ...this.#lastOptions
326
+ };
327
+ const port = resolved.port ?? DEFAULT_PORT;
328
+ return {
329
+ running: await findExistingOpenCodeProcess(this.#sandbox, port, resolved.directory) !== null,
330
+ port,
331
+ url: `http://localhost:${port}`
332
+ };
333
+ }
334
+ /** Snapshot of the resolved configuration the client builder reads. */
335
+ async config() {
336
+ const resolved = {
337
+ ...this.#defaults,
338
+ ...this.#lastOptions
339
+ };
340
+ return {
341
+ port: resolved.port ?? DEFAULT_PORT,
342
+ directory: resolved.directory
343
+ };
344
+ }
345
+ /**
346
+ * Start the server then route a request into the container. This is the
347
+ * transport the SDK client's `fetch` adapter and the Worker proxy use, so it
348
+ * works identically from the Worker stub (one RPC hop) or in-DO (local).
349
+ */
350
+ async fetch(request) {
351
+ const server = await this.start();
352
+ return this.#sandbox.containerFetch(request, server.port);
353
+ }
354
+ /**
355
+ * Persist resolved desired-state so a cold DO can recover it. Best-effort.
356
+ *
357
+ * Secret-bearing fields (`config`, `env`) are never written to storage. They
358
+ * are sourced fresh from the handle's defaults — which the Sandbox rebuilds
359
+ * from the environment on every construction — so a cold start always uses
360
+ * current credentials rather than a durable copy.
361
+ */
362
+ async #persist(options) {
363
+ if (!this.#storage) return;
364
+ const { config: _config, env: _env, ...safe } = options;
365
+ await this.#storage.put(this.#stateKey, safe);
366
+ }
367
+ /** Read persisted desired-state, if any, after a DO eviction. */
368
+ async #loadPersisted() {
369
+ if (!this.#storage) return void 0;
370
+ return this.#storage.get(this.#stateKey);
371
+ }
372
+ };
373
+ /**
374
+ * Assigns each handle a stable per-sandbox index so its persisted state key is
375
+ * deterministic across DO reconstruction (field initializers run in the same
376
+ * order each time). Keyed weakly so it is collected with the sandbox.
377
+ */
378
+ const stateIndexCounter = /* @__PURE__ */ new WeakMap();
379
+ /**
380
+ * Factory — attach as a field on a Sandbox subclass:
381
+ * `opencode = withOpenCode(this, { directory, config, storage: this.ctx.storage })`.
285
382
  *
286
- * // When done
287
- * await server.close()
288
- * ```
383
+ * Pass `storage` to persist desired-state so the server is recovered after a DO
384
+ * eviction (cold start). The server starts lazily on first use.
289
385
  */
290
- async function createOpencode(sandbox, options) {
291
- await ensureSdkLoaded();
292
- const server = await createOpencodeServer(sandbox, options);
293
- const clientFactory = createOpencodeClient;
294
- if (!clientFactory) throw new Error("OpenCode SDK client unavailable.");
295
- return {
296
- client: clientFactory({
297
- baseUrl: server.url,
298
- fetch: (input, init) => sandbox.containerFetch(new Request(input, init), server.port)
299
- }),
300
- server
301
- };
386
+ function withOpenCode(sandbox, options = {}) {
387
+ const { storage, ...defaults } = options;
388
+ const index = stateIndexCounter.get(sandbox) ?? 0;
389
+ stateIndexCounter.set(sandbox, index + 1);
390
+ return new OpenCodeHandle(sandbox, defaults, storage, index);
302
391
  }
392
+
393
+ //#endregion
394
+ //#region ../../extensions/opencode/src/proxy.ts
303
395
  /**
304
- * Proxy a request directly to the OpenCode server.
396
+ * Proxy a request to the OpenCode web UI through a lifecycle handle.
305
397
  *
306
- * Unlike `proxyToOpencode()`, this helper does not apply any web UI redirects
307
- * or query parameter rewrites. Use it for API/CLI traffic where raw request
308
- * forwarding is preferred.
398
+ * For an initial HTML page load that still lacks the `?url=` parameter, returns
399
+ * a redirect that adds it (so the OpenCode frontend calls back through the
400
+ * proxy instead of `127.0.0.1:4096`). Every other request is routed through
401
+ * `handle.fetch`, which ensures the server is running before forwarding into the
402
+ * container.
309
403
  */
310
- function proxyToOpencodeServer(request, sandbox, server) {
311
- return sandbox.containerFetch(request, server.port);
404
+ function proxyToOpenCodeUI(request, handle, options) {
405
+ const url = new URL(request.url);
406
+ if (!url.searchParams.has("url") && request.method === "GET") {
407
+ if ((request.headers.get("accept") || "").includes("text/html") || url.pathname === "/") {
408
+ url.searchParams.set("url", options?.callbackOrigin ?? url.origin);
409
+ return Response.redirect(url.toString(), 302);
410
+ }
411
+ }
412
+ return handle.fetch(request);
312
413
  }
313
414
  /**
314
- * Proxy a request to the OpenCode web UI.
315
- *
316
- * This function handles the redirect and proxying only - you must start the
317
- * server separately using `createOpencodeServer()`.
415
+ * Curried Worker fetch wrapper for OpenCode.
318
416
  *
319
- * Specifically handles:
320
- * 1. Ensuring the `?url=` parameter is set (required for OpenCode's frontend to
321
- * make API calls through the proxy instead of directly to localhost:4096)
322
- * 2. Proxying the request to the container
417
+ * `createOpenCodeProxy(resolve, options?)` captures a lazy per-request resolver
418
+ * for the OpenCode lifecycle handle (`sandbox.opencode`) and returns a function
419
+ * that wraps the user's worker entrypoint.
323
420
  *
324
- * @param request - The incoming HTTP request
325
- * @param sandbox - The Sandbox instance running OpenCode
326
- * @param server - The OpenCode server handle from createOpencodeServer()
327
- * @returns Response from OpenCode or a redirect response
421
+ * The wrapped handler runs first. If it returns a 404 (or has no `fetch`), the
422
+ * request falls through to the OpenCode web-UI proxy the redirect handshake
423
+ * for HTML loads and an ensure-then-forward into the container for everything
424
+ * else. A 404 from the user handler is the "not mine, proxy it" signal, so the
425
+ * handler only needs to own its own routes and `return new Response('Not
426
+ * found', { status: 404 })` for the rest.
328
427
  *
329
- * @example
330
- * ```typescript
331
- * import { getSandbox } from '@cloudflare/sandbox'
332
- * import { createOpencodeServer, proxyToOpencode } from '@cloudflare/sandbox/opencode'
333
- *
334
- * export default {
335
- * async fetch(request: Request, env: Env) {
336
- * const sandbox = getSandbox(env.Sandbox, 'opencode')
337
- * const server = await createOpencodeServer(sandbox, {
338
- * directory: '/home/user/project',
339
- * config: {
340
- * provider: {
341
- * anthropic: {
342
- * options: { apiKey: env.ANTHROPIC_KEY }
343
- * },
344
- * // Optional: Route all providers through Cloudflare AI Gateway
345
- * 'cloudflare-ai-gateway': {
346
- * options: {
347
- * accountId: env.CF_ACCOUNT_ID,
348
- * gatewayId: env.CF_GATEWAY_ID,
349
- * apiToken: env.CF_API_TOKEN
350
- * }
351
- * }
352
- * }
353
- * }
354
- * })
355
- * return proxyToOpencode(request, sandbox, server)
428
+ * ```ts
429
+ * export default createOpenCodeProxy(
430
+ * (env) => getSandbox(env.Sandbox, 'my-sandbox').opencode
431
+ * )({
432
+ * async fetch(request, env) {
433
+ * // handle your own routes, else:
434
+ * return new Response('Not found', { status: 404 });
356
435
  * }
357
- * }
436
+ * });
358
437
  * ```
359
438
  */
360
- function proxyToOpencode(request, sandbox, server) {
361
- const url = new URL(request.url);
362
- if (!url.searchParams.has("url") && request.method === "GET") {
363
- if ((request.headers.get("accept") || "").includes("text/html") || url.pathname === "/") {
364
- url.searchParams.set("url", url.origin);
365
- return Response.redirect(url.toString(), 302);
439
+ function createOpenCodeProxy(resolve, options) {
440
+ return (handler) => ({
441
+ ...handler,
442
+ async fetch(request, env, ctx) {
443
+ if (handler.fetch) {
444
+ const response = await handler.fetch(request, env, ctx);
445
+ if (response.status !== 404) return response;
446
+ }
447
+ return proxyToOpenCodeUI(request, resolve(env), options);
366
448
  }
367
- }
368
- return proxyToOpencodeServer(request, sandbox, server);
449
+ });
369
450
  }
370
451
 
371
452
  //#endregion
372
- export { OpencodeStartupError, createOpencode, createOpencodeServer, proxyToOpencode, proxyToOpencodeServer };
453
+ export { OpenCodeHandle, OpenCodeStartupError, createOpenCodeClient, createOpenCodeProxy, proxyToOpenCodeUI, withOpenCode };
373
454
  //# sourceMappingURL=index.js.map