@sanlabs/sanbox-cli 0.0.9 → 0.0.11

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
@@ -15,8 +15,11 @@ installed_cli_version="$(sanbox --version)"
15
15
  test "$installed_cli_version" = "$latest_cli_version"
16
16
  ```
17
17
 
18
- Always use the latest published CLI. CLI 0.0.9 adds user login for private SSH access to running
19
- Hermes Computers.
18
+ Always use the latest published CLI. CLI 0.0.11 adds OpenCode Computer templates and short-lived
19
+ native SDK connections for HTTP, SSE streaming, and interactive steering.
20
+ CLI 0.0.10 added per-run Hermes email and Telegram channels, Supabase user authorization, and
21
+ removed the retired run-chat commands.
22
+ CLI 0.0.9 added user login for private SSH access to supported running sandboxes.
20
23
  CLI 0.0.6 added Anthropic self-hosted environment inspection and administration.
21
24
 
22
25
  ## Configure
@@ -81,13 +84,39 @@ sanbox doctor --json
81
84
 
82
85
  Model IDs are provider-scoped. The CLI never guesses or silently substitutes a provider, model, or template.
83
86
  For waited task runs, choose a template with `runnable: true`, `template_type: "runner"`, and
84
- `runner_config.harness: "opencode"` or `"browser-use"`. Browser Use is for one-shot web tasks whose
85
- target domains are already approved. Hermes service templates are always-on.
87
+ `runner_config.harness: "opencode"` or `"browser-use"`. OpenCode remains the default one-shot mode,
88
+ while OpenCode Computer runs a retained private OpenCode server:
89
+
90
+ ```bash
91
+ sanbox templates create \
92
+ --name "OpenCode Computer" \
93
+ --harness opencode \
94
+ --mode computer \
95
+ --model-provider openai \
96
+ --model '<model-id>'
97
+ ```
98
+
99
+ Existing templates keep their current names, IDs, slugs, and task behavior. Browser Use is for one-shot
100
+ web tasks whose target domains are already approved. Hermes service templates are always-on. A Hermes template
101
+ declares which optional channels its runs may activate:
102
+
103
+ ```bash
104
+ sanbox templates create \
105
+ --name "Customer assistant" \
106
+ --harness hermes \
107
+ --model-provider openai \
108
+ --model '<model-id>' \
109
+ --channel email \
110
+ --channel telegram
111
+ ```
112
+
113
+ Omit both `--channel` flags for a WebUI-only computer. Channel credentials never belong to the
114
+ template.
86
115
 
87
116
  ## Create A Browser Use Template
88
117
 
89
118
  Browser Use runs local headless Chromium inside the Firecracker sandbox. It requires OpenAI,
90
- Anthropic, or Google Gemini and at least one explicit browser target:
119
+ Anthropic, Google Gemini, or Hetzner Inference and at least one explicit browser target:
91
120
 
92
121
  ```bash
93
122
  sanbox templates create \
@@ -106,7 +135,7 @@ sanbox templates create \
106
135
 
107
136
  Targets are limited to hostnames and leading wildcards on ports 80 and 443. The remaining browser
108
137
  security controls are fixed. Runs return `browser-use-response.md`, a sanitized run ledger, and any
109
- allowed downloads. Browser Use templates do not support retained follow-up messages.
138
+ allowed downloads. Browser Use templates are one-shot agent executions.
110
139
 
111
140
  ## Run Idempotently
112
141
 
@@ -126,6 +155,51 @@ sanbox run "Review this repo and write output/report.md" \
126
155
 
127
156
  Repeat `--input` for files, directories, or globs. The CLI excludes common secrets and applies `.sanboxignore`. `--include` is a deprecated compatibility alias.
128
157
 
158
+ For a Hermes template that allows `email`, optionally request an address from one of the AgentMail
159
+ domains listed in the Sanbox console:
160
+
161
+ ```bash
162
+ sanbox run "Handle customer email for this project" \
163
+ --template "$SANBOX_TEMPLATE" \
164
+ --email-address research-agent@agentmail.to \
165
+ --json
166
+ ```
167
+
168
+ `--email-address` is optional. When supplied, AgentMail must be connected and Sanbox does not
169
+ generate a fallback address; use `agentmail.to` or one of the domains listed for the connected Pod.
170
+
171
+ For a Hermes template that allows `telegram`, pass the bot token and at least one numeric user ID on
172
+ the run. Both values are optional unless the run needs Telegram:
173
+
174
+ ```bash
175
+ sanbox run "Handle Telegram messages for this project" \
176
+ --template "$SANBOX_TEMPLATE" \
177
+ --telegram-bot-token '<telegram-bot-token>' \
178
+ --telegram-allowed-user '<numeric-telegram-user-id>' \
179
+ --json
180
+ ```
181
+
182
+ You can set `SANBOX_TELEGRAM_BOT_TOKEN` instead of passing `--telegram-bot-token`. Do not use both.
183
+
184
+ For a long-running Hermes Computer that should act as one Supabase user, provide that user's
185
+ Supabase Auth UUID. Sanbox automatically uses the organization's connected Supabase project:
186
+
187
+ ```bash
188
+ sanbox run "Act as this user's procurement assistant" \
189
+ --template "$SANBOX_TEMPLATE" \
190
+ --external-run-id "hermes:<tenant-id>:<user-id>" \
191
+ --supabase-user-id "<supabase-auth-user-uuid>" \
192
+ --json
193
+
194
+ sanbox runs supabase authorize <run-id> --open
195
+ ```
196
+
197
+ The run stays in `awaiting_grant` until the expected Supabase user approves the browser consent
198
+ flow. Without `--open`, the authorize command prints the URL for copying into any browser. The
199
+ callback lands on a Sanbox-hosted completion page, so a CLI demo does not need a customer web app.
200
+ Customer integrations may use `--return-url <https-url>` when its origin matches the return origin
201
+ configured on the organization Supabase connection.
202
+
129
203
  Reuse the same external ID when retrying an ambiguous submission. To stream activity, replace `--wait --json` with `--jsonl`. Ctrl-C detaches without canceling unless `--cancel-on-interrupt` is supplied.
130
204
 
131
205
  ## Inspect And Recover
@@ -180,39 +254,61 @@ sanbox runs pause <run-id> --wait --json
180
254
 
181
255
  Resume restores the latest writable Firecracker snapshot without starting the configured agent
182
256
  harness. It acquires an exclusive manual lease and leaves the sandbox running for live filesystem
183
- access until Pause creates the next snapshot generation. Do not submit agent work while that lease is
184
- active. Paused state has no retention TTL and remains available until explicitly deleted.
257
+ access until Pause creates the next snapshot generation. Before resuming, require
258
+ `sandbox_state: "paused"` and a positive `snapshot_generation`. Do not submit agent work while that
259
+ lease is active. Paused state has no retention TTL and remains available until explicitly deleted.
185
260
 
186
- ## SSH Into A Hermes Computer
261
+ ## SSH Into A Running Sandbox
187
262
 
188
263
  ```bash
189
264
  sanbox login
190
265
  sanbox ssh <run-id>
191
266
  ```
192
267
 
193
- The run must be a currently running Hermes Computer created from the SSH-capable template artifact.
268
+ The run must be a currently running OpenCode run or Hermes Computer created from an SSH-capable
269
+ artifact.
194
270
  The signed-in user must own the run or be an organization admin. The command uses your local OpenSSH
195
271
  client, but it does not expose the microVM on a public IP or port. The CLI generates a temporary
196
272
  Ed25519 identity, pins the microVM's runtime host key, and tunnels the SSH stream over a one-time
197
273
  authenticated WebSocket. The temporary key is deleted when the connection closes, and the saved
198
274
  user token is not passed to the OpenSSH child process.
199
275
 
200
- ## Continue An OpenCode Run
276
+ OpenCode and Browser Use runs are one-shot agent executions. OpenCode Computer keeps its private
277
+ server running after the initial task so it can accept later steering. A persisted task sandbox can
278
+ still be resumed manually for inspection when a snapshot exists.
279
+
280
+ ## Connect The OpenCode SDK
281
+
282
+ Create a short-lived connection for a currently running OpenCode Computer:
201
283
 
202
284
  ```bash
203
- sanbox runs messages <run-id> --json
204
- sanbox runs message <run-id> "Summarize the retained output" --wait --json
285
+ sanbox opencode connect <run-id> --expires 1h --json
286
+ sanbox opencode connections list <run-id> --json
287
+ sanbox opencode connections revoke <run-id> <connection-id> --json
205
288
  ```
206
289
 
207
- The JSON response includes the user `message`, submitted `chat_job`, matching terminal
208
- `followup_event`, and conversation messages. The waiter matches `payload.chat_job_id`, so another
209
- concurrent follow-up cannot complete the wrong command. The follow-up resumes OpenCode for one turn,
210
- then pauses the sandbox again.
290
+ The create response contains `url`, `access_token`, and connection metadata. The plaintext token is
291
+ returned only once, but it can authenticate all OpenCode SDK requests and SSE reconnects until it
292
+ expires, is revoked, or the runtime session ends. Multiple connections can coexist. Organization API
293
+ keys use their existing organization role and require member access or higher; no additional API-key
294
+ or connection-token scopes are introduced.
295
+
296
+ ```ts
297
+ import { createOpencodeClient } from "@opencode-ai/sdk"
298
+
299
+ const client = createOpencodeClient({
300
+ baseUrl: connection.url,
301
+ headers: { Authorization: `Bearer ${connection.access_token}` },
302
+ })
303
+
304
+ const events = await client.event.subscribe()
305
+ ```
211
306
 
212
- Check `sanbox runs get <run-id> --json` before a follow-up and require `sandbox_state: "paused"` with
213
- a positive `snapshot_generation`. Do not submit concurrent follow-ups to one run.
214
- Treat `browser_use_followup_unsupported` as a signal to create a new browser agent run; its persisted
215
- sandbox can still be resumed manually when a snapshot exists.
307
+ Sanbox proxies the native HTTP and SSE API through its private Firecracker management path. The
308
+ microVM never exposes an inbound port. Only security-sensitive OpenCode configuration,
309
+ authentication, credential mutation, server-administration, raw PTY, and public-share routes are
310
+ denied; core session, event, file, command, and session-shell APIs remain native. See
311
+ [OpenCode SDK access](../docs/opencode-sdk-access.md) for the API and lifecycle contract.
216
312
 
217
313
  ## Batch Work
218
314
 
package/dist/api.js CHANGED
@@ -183,6 +183,12 @@ export class SanboxClient {
183
183
  async getRun(runId, signal) {
184
184
  return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}`), { signal });
185
185
  }
186
+ async authorizeRunSupabase(runId, returnUrl) {
187
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/supabase/authorize`), {
188
+ method: "POST",
189
+ body: JSON.stringify(returnUrl ? { return_url: returnUrl } : {})
190
+ });
191
+ }
186
192
  async createSSHSession(runId, publicKey) {
187
193
  return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/ssh-sessions`), {
188
194
  method: "POST",
@@ -210,15 +216,6 @@ export class SanboxClient {
210
216
  body: "{}"
211
217
  });
212
218
  }
213
- async sendMessage(runId, message, payload = {}) {
214
- return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/messages`), {
215
- method: "POST",
216
- body: JSON.stringify({ message, payload })
217
- });
218
- }
219
- async listMessages(runId) {
220
- return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/messages`));
221
- }
222
219
  async listArtifacts(runId) {
223
220
  return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/artifacts`));
224
221
  }
@@ -234,6 +231,18 @@ export class SanboxClient {
234
231
  async revokeFileAccessPoint(runId, accessPointId) {
235
232
  return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/file-access-points/${encodeURIComponent(accessPointId)}`), { method: "DELETE" });
236
233
  }
234
+ async listOpenCodeConnections(runId) {
235
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/opencode-connections`));
236
+ }
237
+ async createOpenCodeConnection(runId, expiresInSeconds) {
238
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/opencode-connections`), {
239
+ method: "POST",
240
+ body: JSON.stringify({ expires_in_seconds: expiresInSeconds })
241
+ });
242
+ }
243
+ async revokeOpenCodeConnection(runId, connectionId) {
244
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/opencode-connections/${encodeURIComponent(connectionId)}`), { method: "DELETE" });
245
+ }
237
246
  async downloadArtifact(runId, artifactPath) {
238
247
  return this.rawRequest(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/artifacts`)}?path=${encodeURIComponent(artifactPath)}`);
239
248
  }
package/dist/args.js CHANGED
@@ -2,6 +2,7 @@ const multiFlags = new Set([
2
2
  "input",
3
3
  "include",
4
4
  "artifact",
5
+ "channel",
5
6
  "telegram-allowed-user",
6
7
  "browser-domain"
7
8
  ]);
@@ -19,6 +20,7 @@ export const booleanFlags = new Set([
19
20
  "force",
20
21
  "write",
21
22
  "overwrite",
23
+ "open",
22
24
  "no-browser"
23
25
  ]);
24
26
  export const parseArgs = (argv) => {
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { previewInputs } from "./inputs.js";
13
13
  import { printError, printJsonlError, printRun, printSuccess, publicRun, publicRunPayload } from "./output.js";
14
14
  import { createRun, isTerminalRun, readTasks, runPool, stableBatchId, waitForRun, waitForSandboxState } from "./runs.js";
15
15
  import { version } from "./version.js";
16
- import { WatchInterruptedError, watchEventsUntil, watchRun } from "./watch.js";
16
+ import { WatchInterruptedError, watchRun } from "./watch.js";
17
17
  import { openSSH, runSSHProxy } from "./ssh.js";
18
18
  import { userSessionStore } from "./userSession.js";
19
19
  const help = `Sanbox CLI
@@ -40,14 +40,13 @@ Commands:
40
40
  sanbox templates list [--json]
41
41
  sanbox templates get <template-id> [--json]
42
42
  sanbox templates validate <template-id> [--json]
43
- sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
44
- sanbox run "task" --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
45
- sanbox run --task "..." --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
43
+ sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--channel email|telegram] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
44
+ sanbox run "task" --template <template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
45
+ sanbox run --task "..." --template <template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
46
46
  sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
47
47
  sanbox runs list [--limit 50] [--json]
48
48
  sanbox runs get <run-id> [--json]
49
49
  sanbox runs events <run-id> [--after-event-id 0] [--json]
50
- sanbox runs messages <run-id> [--json]
51
50
  sanbox runs artifacts <run-id> [--json]
52
51
  sanbox runs share <run-id> [--expires 1h] [--name "Debug access"] [--json]
53
52
  sanbox runs shares <run-id> [--json]
@@ -57,7 +56,10 @@ Commands:
57
56
  sanbox runs cancel <run-id> [--json]
58
57
  sanbox runs resume <run-id> [--wait] [--json]
59
58
  sanbox runs pause <run-id> [--wait] [--json]
60
- sanbox runs message <run-id> "..." [--wait | --watch] [--json | --jsonl]
59
+ sanbox runs supabase authorize <run-id> [--open] [--return-url <https-url>] [--json]
60
+ sanbox opencode connect <run-id> [--expires 1h] [--json]
61
+ sanbox opencode connections list <run-id> [--json]
62
+ sanbox opencode connections revoke <run-id> <connection-id> [--json]
61
63
  sanbox ssh <run-id>
62
64
  sanbox init [--force]
63
65
  sanbox init agent [--write]
@@ -67,12 +69,24 @@ const sshHelp = `Sanbox SSH
67
69
  Usage:
68
70
  sanbox ssh <run-id>
69
71
 
70
- Opens the running Hermes Computer with your local OpenSSH client. Sanbox creates an ephemeral
71
- Ed25519 key, authorizes it for one connection, verifies the microVM host key, and tunnels SSH over
72
- the authenticated private management path. Run sanbox login first. The computer owner and
72
+ Opens a running OpenCode run or Hermes Computer with your local OpenSSH client. Sanbox creates an
73
+ ephemeral Ed25519 key, authorizes it for one connection, verifies the microVM host key, and tunnels
74
+ SSH over the authenticated private management path. Run sanbox login first. The run owner and
73
75
  organization admins can connect; organization API keys cannot open interactive sessions. The guest
74
76
  does not expose port 22 publicly.
75
77
  `;
78
+ const openCodeHelp = `Sanbox OpenCode SDK access
79
+
80
+ Usage:
81
+ sanbox opencode connect <run-id> [--expires 1h] [--json]
82
+ sanbox opencode connections list <run-id> [--json]
83
+ sanbox opencode connections revoke <run-id> <connection-id> [--json]
84
+
85
+ Creates short-lived bearer credentials for the native OpenCode SDK. The token is bound to one
86
+ running OpenCode Computer and its current runtime session. Its plaintext value is returned only
87
+ when created. Multiple connections may be active at once; each expires and can be revoked
88
+ independently. Organization API keys require member access or higher.
89
+ `;
76
90
  const runHelp = `Sanbox run
77
91
 
78
92
  Usage:
@@ -83,6 +97,10 @@ Options:
83
97
  --input <path> File, directory, or glob to upload. Repeatable.
84
98
  --template <id> Template id or slug. Required unless SANBOX_TEMPLATE or project config sets it.
85
99
  --external-run-id <id> Idempotency key for retries.
100
+ --supabase-user-id <id> Supabase Auth user UUID expected during OAuth consent.
101
+ --email-address <address> Optional for Hermes computers; must use a listed AgentMail domain.
102
+ --telegram-bot-token <token> Optional Telegram bot token; SANBOX_TELEGRAM_BOT_TOKEN is also supported.
103
+ --telegram-allowed-user <id> Numeric Telegram user allowed to reach this run. Repeatable.
86
104
  --dry-run Preview included files without creating a run.
87
105
  --wait Poll until terminal status.
88
106
  --watch Stream activity until terminal status.
@@ -92,6 +110,17 @@ Options:
92
110
  --cancel-on-interrupt Request run cancellation when Ctrl-C is pressed.
93
111
  --json Print JSON.
94
112
  `;
113
+ const runsSupabaseHelp = `Sanbox Supabase run authorization
114
+
115
+ Usage:
116
+ sanbox runs supabase authorize <run-id>
117
+ sanbox runs supabase authorize <run-id> --open
118
+
119
+ Options:
120
+ --open Open the consent URL in the default browser.
121
+ --return-url <https-url> Override the built-in Sanbox completion page with an allowlisted customer URL.
122
+ --json Print JSON.
123
+ `;
95
124
  const doctorHelp = `Sanbox doctor
96
125
 
97
126
  Usage:
@@ -130,12 +159,14 @@ Usage:
130
159
  sanbox templates list [--json]
131
160
  sanbox templates get <template-id> [--json]
132
161
  sanbox templates validate <template-id> [--json]
133
- sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
162
+ sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
134
163
 
135
164
  Template creation requires an exact provider id and that provider's exact model id.
136
165
  LiteLLM budgets are optional USD amounts and apply separately to each run.
137
- Hermes templates are always-on Telegram gateways. Set SANBOX_TELEGRAM_BOT_TOKEN
138
- and repeat --telegram-allowed-user <numeric-id> for every permitted user.
166
+ OpenCode defaults to --mode task. Use --mode computer for a retained private OpenCode server;
167
+ per-run LiteLLM budgets are not available for that retained mode yet.
168
+ Hermes templates are always-on computers. Use repeatable --channel email|telegram to select which
169
+ channels runs may activate. Email and Telegram credentials are supplied only when creating a run.
139
170
  Browser Use templates run local headless Chromium inside a one-shot sandbox. They require OpenAI
140
171
  or Anthropic and at least one repeatable --browser-domain hostname or leading wildcard.
141
172
  Optional controls: --browser-max-steps, --browser-step-timeout-seconds,
@@ -231,17 +262,11 @@ exclusive manual lease and leaves the sandbox running until pause snapshots it a
231
262
  agent work while the manual lease is active. Before resume, require sandbox_state: "paused" and a
232
263
  positive snapshot_generation.
233
264
 
234
- For an OpenCode follow-up, use \`sanbox runs message <run-id> "..." --wait --json\`. Follow-up waits
235
- are correlated to the returned chat_job.id. Never submit concurrent follow-ups to one run. Retry
236
- sandbox_not_paused only after the run returns to paused; treat sandbox_not_resumable,
237
- sandbox_snapshot_missing, sandbox_worker_missing, hermes_uses_channel, and
238
- browser_use_followup_unsupported as blockers.
239
-
240
265
  For independent fan-out, use \`sanbox batch\` with a stable external_run_id per task and keep the
241
266
  client alive until submission completes.
242
267
 
243
- Do not claim completion until the run is completed, required artifacts are downloaded and verified,
244
- and required follow-ups succeeded. Report run/external/template IDs, status, sandbox state, snapshot
268
+ Do not claim completion until the run is completed and required artifacts are downloaded and verified.
269
+ Report run/external/template IDs, status, sandbox state, snapshot
245
270
  generation, artifact paths/digests, and blockers. The CLI excludes common secrets by default; add
246
271
  .sanboxignore for project rules.
247
272
  `;
@@ -427,9 +452,10 @@ const flagSets = {
427
452
  "model-provider",
428
453
  "model",
429
454
  "harness",
455
+ "mode",
430
456
  "llm-budget-usd",
431
457
  "web-access",
432
- "telegram-allowed-user",
458
+ "channel",
433
459
  "browser-domain",
434
460
  "browser-max-steps",
435
461
  "browser-step-timeout-seconds",
@@ -439,7 +465,9 @@ const flagSets = {
439
465
  "browser-additional-instructions"
440
466
  ],
441
467
  run: [
442
- ...commonFlags, "task", "input", "template", "external-run-id",
468
+ ...commonFlags, "task", "input", "template", "external-run-id", "email-address",
469
+ "supabase-user-id",
470
+ "telegram-bot-token", "telegram-allowed-user",
443
471
  "dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "cancel-on-interrupt",
444
472
  "poll-interval-ms", "event-page-size", "timeout-seconds", "verbose"
445
473
  ],
@@ -450,7 +478,6 @@ const flagSets = {
450
478
  "runs.list": [...commonFlags, "limit"],
451
479
  "runs.get": commonFlags,
452
480
  "runs.events": [...commonFlags, "after-event-id"],
453
- "runs.messages": commonFlags,
454
481
  "runs.artifacts": commonFlags,
455
482
  "runs.share": [...commonFlags, "expires", "name"],
456
483
  "runs.shares": commonFlags,
@@ -463,10 +490,9 @@ const flagSets = {
463
490
  "runs.cancel": commonFlags,
464
491
  "runs.resume": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
465
492
  "runs.pause": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
466
- "runs.message": [
467
- ...commonFlags, "message", "wait", "watch", "jsonl", "view",
468
- "poll-interval-ms", "event-page-size", "timeout-seconds"
469
- ],
493
+ "runs.supabase": [...commonFlags, "open", "return-url"],
494
+ "opencode.connect": [...commonFlags, "expires"],
495
+ "opencode.connections": commonFlags,
470
496
  ssh: ["api-url", "help"],
471
497
  "ssh-proxy": [],
472
498
  login: ["api-url", "json", "no-browser", "help"],
@@ -482,10 +508,15 @@ const commandKey = (command) => {
482
508
  return "version";
483
509
  if (command[0] === "auth")
484
510
  return `auth.${command[1] || ""}`;
511
+ if (command[0] === "runs" && command[1] === "supabase")
512
+ return "runs.supabase";
513
+ if (command[0] === "opencode" && command[1] === "connections")
514
+ return "opencode.connections";
485
515
  if (command[0] === "model-providers" ||
486
516
  command[0] === "anthropic-environments" ||
487
517
  command[0] === "templates" ||
488
- command[0] === "runs") {
518
+ command[0] === "runs" ||
519
+ command[0] === "opencode") {
489
520
  return `${command[0]}.${command[1] || ""}`;
490
521
  }
491
522
  if (command[0] === "init" && command[1] === "agent")
@@ -493,14 +524,15 @@ const commandKey = (command) => {
493
524
  return command[0];
494
525
  };
495
526
  const requiredValueFlags = new Set([
496
- "api-url", "template", "task", "input", "include", "external-run-id",
527
+ "api-url", "template", "task", "input", "include", "external-run-id", "email-address",
528
+ "supabase-user-id", "return-url",
529
+ "telegram-bot-token", "telegram-allowed-user", "channel",
497
530
  "tasks", "max-parallel", "batch-id", "poll-interval-ms",
498
531
  "event-page-size", "timeout-seconds", "view", "after-event-id", "limit", "name",
499
- "model-provider", "model", "harness", "llm-budget-usd", "telegram-allowed-user",
532
+ "model-provider", "model", "harness", "mode", "llm-budget-usd",
500
533
  "browser-domain", "browser-max-steps", "browser-step-timeout-seconds",
501
534
  "browser-vision-mode", "browser-viewport", "browser-download-policy",
502
- "browser-additional-instructions", "expires",
503
- "message", "output", "artifact"
535
+ "browser-additional-instructions", "expires", "output", "artifact"
504
536
  ]);
505
537
  const validateFlagValues = (flags) => {
506
538
  for (const flag of requiredValueFlags) {
@@ -539,7 +571,7 @@ const validateFlags = (command, flags) => {
539
571
  }
540
572
  const knownRoots = new Set([
541
573
  "auth", "context", "doctor", "model-providers", "anthropic-environments", "templates",
542
- "run", "batch", "runs", "ssh", "ssh-proxy", "login", "logout", "init", "version"
574
+ "run", "batch", "runs", "opencode", "ssh", "ssh-proxy", "login", "logout", "init", "version"
543
575
  ]);
544
576
  const allowed = flagSets[key] ?? (knownRoots.has(command[0] || "") ? commonFlags : null);
545
577
  if (!allowed)
@@ -571,7 +603,6 @@ const validatePositionals = (command, flags) => {
571
603
  "runs.list": 2,
572
604
  "runs.get": 3,
573
605
  "runs.events": 3,
574
- "runs.messages": 3,
575
606
  "runs.artifacts": 3,
576
607
  "runs.share": 3,
577
608
  "runs.shares": 3,
@@ -581,6 +612,9 @@ const validatePositionals = (command, flags) => {
581
612
  "runs.cancel": 3,
582
613
  "runs.resume": 3,
583
614
  "runs.pause": 3,
615
+ "runs.supabase": 4,
616
+ "opencode.connect": 3,
617
+ "opencode.connections": 5,
584
618
  ssh: 2,
585
619
  "ssh-proxy": 1,
586
620
  login: 1,
@@ -692,6 +726,11 @@ const commandRun = async (command, flags) => {
692
726
  throw new Error("--wait cannot be combined with --watch or --jsonl.");
693
727
  }
694
728
  validateWatchFlags(flags);
729
+ const supabaseUserId = flagString(flags, "supabase-user-id").trim();
730
+ if (supabaseUserId &&
731
+ !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(supabaseUserId)) {
732
+ throw new CliError("invalid_supabase_user_id", "--supabase-user-id must be a UUID.");
733
+ }
695
734
  if (hasFlag(flags, "dry-run")) {
696
735
  if (wantsWatch(flags))
697
736
  throw new Error("--dry-run cannot be combined with --watch or --jsonl.");
@@ -702,6 +741,24 @@ const commandRun = async (command, flags) => {
702
741
  printPreview(preview, hasFlag(flags, "verbose"));
703
742
  return;
704
743
  }
744
+ const tokenFlag = flagString(flags, "telegram-bot-token").trim();
745
+ const telegramAllowedUsers = [...new Set(flagList(flags, "telegram-allowed-user")
746
+ .map((value) => value.trim())
747
+ .filter(Boolean))];
748
+ const telegramRequested = Boolean(tokenFlag || telegramAllowedUsers.length > 0);
749
+ const tokenEnvironment = telegramRequested
750
+ ? process.env.SANBOX_TELEGRAM_BOT_TOKEN?.trim() || ""
751
+ : "";
752
+ if (tokenFlag && tokenEnvironment) {
753
+ throw new CliError("telegram_token_ambiguous", "Use either --telegram-bot-token or SANBOX_TELEGRAM_BOT_TOKEN, not both.");
754
+ }
755
+ const telegramBotToken = tokenFlag || tokenEnvironment;
756
+ if (!telegramBotToken && telegramAllowedUsers.length > 0) {
757
+ throw new CliError("telegram_bot_token_required", "--telegram-allowed-user requires --telegram-bot-token or SANBOX_TELEGRAM_BOT_TOKEN.");
758
+ }
759
+ if (telegramBotToken && telegramAllowedUsers.length === 0) {
760
+ throw new CliError("telegram_allowed_user_required", "Telegram requires at least one --telegram-allowed-user.");
761
+ }
705
762
  const client = makeClient(flags);
706
763
  const template = readTemplateSelection(flags);
707
764
  let payload;
@@ -711,6 +768,10 @@ const commandRun = async (command, flags) => {
711
768
  instruction: task,
712
769
  inputs: flagList(flags, "input"),
713
770
  externalRunId: flagString(flags, "external-run-id") || undefined,
771
+ supabaseUserId: supabaseUserId || undefined,
772
+ emailAddress: flagString(flags, "email-address") || undefined,
773
+ telegramBotToken: telegramBotToken || undefined,
774
+ telegramAllowedUsers,
714
775
  templateId: template.id
715
776
  });
716
777
  }
@@ -764,6 +825,7 @@ const commandBatch = async (flags) => {
764
825
  instruction: task.task,
765
826
  inputs: task.input || inputs,
766
827
  externalRunId: task.external_run_id || `sanbox-batch-${batchId}-${index + 1}`,
828
+ emailAddress: task.email_address,
767
829
  templateId: template.id
768
830
  });
769
831
  }
@@ -874,8 +936,7 @@ const commandModelProviders = async (command, flags) => {
874
936
  printSuccess("model_providers.get", payload, jsonContext(client), nextActions);
875
937
  return;
876
938
  }
877
- process.stdout.write(`${providerId(payload.provider)}${payload.provider.status ? ` ${payload.provider.status}` : ""}` +
878
- `${payload.provider.base_url ? ` base_url=${payload.provider.base_url}` : ""}\n`);
939
+ process.stdout.write(`${providerId(payload.provider)}${payload.provider.status ? ` ${payload.provider.status}` : ""}\n`);
879
940
  return;
880
941
  }
881
942
  if (action === "models") {
@@ -1108,6 +1169,14 @@ const commandTemplates = async (command, flags) => {
1108
1169
  if (harness !== "opencode" && harness !== "hermes" && harness !== "browser-use") {
1109
1170
  throw new CliError("invalid_harness", "templates create --harness must be opencode, hermes, or browser-use.");
1110
1171
  }
1172
+ const modeFlag = flagString(flags, "mode");
1173
+ const mode = modeFlag || "task";
1174
+ if (mode !== "task" && mode !== "computer") {
1175
+ throw new CliError("invalid_opencode_mode", "templates create --mode must be task or computer.");
1176
+ }
1177
+ if (harness !== "opencode" && modeFlag) {
1178
+ throw new CliError("opencode_mode_not_supported", "templates create --mode requires --harness opencode.");
1179
+ }
1111
1180
  const budgetRaw = flagString(flags, "llm-budget-usd");
1112
1181
  const parsedBudgetUsd = budgetRaw ? Number(budgetRaw) : undefined;
1113
1182
  const llmBudgetUsd = parsedBudgetUsd === undefined
@@ -1120,8 +1189,13 @@ const commandTemplates = async (command, flags) => {
1120
1189
  if (harness === "hermes" && llmBudgetUsd !== undefined) {
1121
1190
  throw new CliError("hermes_budget_unsupported", "Always-on Hermes templates do not support --llm-budget-usd yet.");
1122
1191
  }
1123
- const telegramBotToken = process.env.SANBOX_TELEGRAM_BOT_TOKEN?.trim() || "";
1124
- const telegramAllowedUsers = flagList(flags, "telegram-allowed-user").map((value) => value.trim()).filter(Boolean);
1192
+ if (harness === "opencode" && mode === "computer" && llmBudgetUsd !== undefined) {
1193
+ throw new CliError("opencode_computer_budget_unsupported", "OpenCode Computer templates do not support --llm-budget-usd yet.");
1194
+ }
1195
+ const channels = [...new Set(flagList(flags, "channel").map((value) => value.trim()).filter(Boolean))];
1196
+ if (channels.some((channel) => channel !== "email" && channel !== "telegram")) {
1197
+ throw new CliError("invalid_hermes_channels", "--channel must be email or telegram.");
1198
+ }
1125
1199
  const browserFlagNames = [
1126
1200
  "browser-domain",
1127
1201
  "browser-max-steps",
@@ -1137,8 +1211,9 @@ const commandTemplates = async (command, flags) => {
1137
1211
  if (harness === "browser-use" &&
1138
1212
  modelProvider !== "openai" &&
1139
1213
  modelProvider !== "anthropic" &&
1140
- modelProvider !== "google") {
1141
- throw new CliError("browser_use_provider_unsupported", "Browser Use templates require --model-provider openai, anthropic, or google.");
1214
+ modelProvider !== "google" &&
1215
+ modelProvider !== "hetzner") {
1216
+ throw new CliError("browser_use_provider_unsupported", "Browser Use templates require --model-provider openai, anthropic, google, or hetzner.");
1142
1217
  }
1143
1218
  const browserDomains = harness === "browser-use"
1144
1219
  ? parseBrowserDomains(flagList(flags, "browser-domain"))
@@ -1146,11 +1221,8 @@ const commandTemplates = async (command, flags) => {
1146
1221
  const browserUse = harness === "browser-use"
1147
1222
  ? parseBrowserUseConfig(flags)
1148
1223
  : undefined;
1149
- if (harness === "hermes" && !telegramBotToken) {
1150
- throw new CliError("telegram_bot_token_required", "Hermes template creation requires SANBOX_TELEGRAM_BOT_TOKEN.");
1151
- }
1152
- if (harness === "hermes" && telegramAllowedUsers.length === 0) {
1153
- throw new CliError("telegram_allowed_user_required", "Hermes template creation requires at least one --telegram-allowed-user.");
1224
+ if (harness !== "hermes" && channels.length > 0) {
1225
+ throw new CliError("hermes_channels_not_supported", "--channel requires --harness hermes.");
1154
1226
  }
1155
1227
  let payload;
1156
1228
  try {
@@ -1158,6 +1230,7 @@ const commandTemplates = async (command, flags) => {
1158
1230
  name,
1159
1231
  provider_id: modelProvider,
1160
1232
  model_id: model,
1233
+ ...(harness === "opencode" && modeFlag ? { mode: mode } : {}),
1161
1234
  ...(llmBudgetUsd === undefined ? {} : { llm_budget_usd: llmBudgetUsd }),
1162
1235
  ...(harness === "browser-use" ? {
1163
1236
  harness,
@@ -1175,8 +1248,7 @@ const commandTemplates = async (command, flags) => {
1175
1248
  } : {}),
1176
1249
  ...(harness === "hermes" ? {
1177
1250
  harness,
1178
- telegram_bot_token: telegramBotToken,
1179
- telegram_allowed_users: telegramAllowedUsers
1251
+ channels: channels
1180
1252
  } : {})
1181
1253
  });
1182
1254
  }
@@ -1186,8 +1258,20 @@ const commandTemplates = async (command, flags) => {
1186
1258
  if (hasFlag(flags, "json")) {
1187
1259
  printSuccess("templates.create", payload, jsonContext(client), [
1188
1260
  commandAction(["sanbox", "templates", "validate", payload.template.id, "--json"], "Validate the new template before running it."),
1189
- commandAction(["sanbox", "run", harness === "hermes" ? "<agent role and instructions>" : "<task>", "--template", payload.template.id], harness === "hermes"
1190
- ? "Start the always-on gateway; interact with it through the configured Telegram bot."
1261
+ commandAction([
1262
+ "sanbox",
1263
+ "run",
1264
+ harness === "hermes"
1265
+ ? "<agent role and instructions>"
1266
+ : harness === "opencode" && mode === "computer"
1267
+ ? "<computer role and instructions>"
1268
+ : "<task>",
1269
+ "--template",
1270
+ payload.template.id
1271
+ ], harness === "hermes" || (harness === "opencode" && mode === "computer")
1272
+ ? harness === "hermes"
1273
+ ? "Start the always-on computer and activate only the channels this run needs."
1274
+ : "Start the retained OpenCode computer."
1191
1275
  : "Create a run with the new template.")
1192
1276
  ]);
1193
1277
  return;
@@ -1215,6 +1299,23 @@ const commandRuns = async (command, flags) => {
1215
1299
  }
1216
1300
  return;
1217
1301
  }
1302
+ if (action === "supabase") {
1303
+ if (command[2] !== "authorize") {
1304
+ throw new CliError("supabase_action_required", "runs supabase requires the action: authorize.");
1305
+ }
1306
+ const runId = requiredPositional(command[3], "run_id_required", "runs supabase authorize requires a run id.");
1307
+ const payload = await client.authorizeRunSupabase(runId, flagString(flags, "return-url").trim() || undefined);
1308
+ if (hasFlag(flags, "open") && !openBrowser(payload.authorization_url)) {
1309
+ process.stderr.write("Could not open a browser. Open the printed consent URL manually.\n");
1310
+ }
1311
+ if (hasFlag(flags, "json")) {
1312
+ printSuccess("runs.supabase.authorize", payload, jsonContext(client));
1313
+ }
1314
+ else {
1315
+ process.stdout.write(`${payload.authorization_url}\n`);
1316
+ }
1317
+ return;
1318
+ }
1218
1319
  const runId = requiredPositional(command[2], "run_id_required", `runs ${action} requires a run id.`);
1219
1320
  if (action === "get") {
1220
1321
  const payload = await client.getRun(runId);
@@ -1232,17 +1333,6 @@ const commandRuns = async (command, flags) => {
1232
1333
  payload.events.forEach((event) => process.stdout.write(`${event.id} ${event.level} ${event.kind} ${event.message}\n`));
1233
1334
  return;
1234
1335
  }
1235
- if (action === "messages") {
1236
- const payload = await client.listMessages(runId);
1237
- if (hasFlag(flags, "json")) {
1238
- printSuccess("runs.messages", payload, jsonContext(client));
1239
- return;
1240
- }
1241
- for (const message of payload.messages) {
1242
- process.stdout.write(`${message.role}\t${message.created_at}\t${message.message}\n`);
1243
- }
1244
- return;
1245
- }
1246
1336
  if (action === "artifacts") {
1247
1337
  const payload = await client.listArtifacts(runId);
1248
1338
  if (hasFlag(flags, "json")) {
@@ -1378,107 +1468,58 @@ const commandRuns = async (command, flags) => {
1378
1468
  }
1379
1469
  return;
1380
1470
  }
1381
- if (action === "message") {
1382
- const commandName = "runs.message";
1383
- const flaggedMessage = flagString(flags, "message");
1384
- const positionalMessage = positionalText(command, 3);
1385
- if (flaggedMessage && positionalMessage) {
1386
- throw new CliError("conflicting_arguments", "Use either --message or a positional message, not both.");
1387
- }
1388
- const message = flaggedMessage || positionalMessage;
1389
- if (!message)
1390
- throw new Error(`runs ${action} requires an instruction as positional text or --message.`);
1391
- if (wantsWatch(flags) && hasFlag(flags, "json")) {
1392
- throw new Error("--json cannot be combined with --watch or --jsonl.");
1393
- }
1394
- if (hasFlag(flags, "wait") && wantsWatch(flags)) {
1395
- throw new Error("--wait cannot be combined with --watch or --jsonl.");
1471
+ throw new Error(`Unknown runs action: ${action}`);
1472
+ };
1473
+ const commandOpenCode = async (command, flags) => {
1474
+ const client = makeClient(flags);
1475
+ const action = command[1];
1476
+ if (action === "connect") {
1477
+ const runId = requiredPositional(command[2], "run_id_required", "opencode connect requires a run id.");
1478
+ const expiresInSeconds = parseFileAccessExpiry(flagString(flags, "expires", "1h"));
1479
+ if (expiresInSeconds > 24 * 60 * 60) {
1480
+ throw new CliError("invalid_opencode_connection_expiry", "OpenCode connection expiry must be between 1 minute and 1 day.");
1396
1481
  }
1397
- const submitted = await client.sendMessage(runId, message);
1398
- if (!hasFlag(flags, "wait") && !wantsWatch(flags)) {
1399
- if (hasFlag(flags, "json")) {
1400
- printSuccess(commandName, {
1401
- ...publicRunPayload(submitted),
1402
- message: submitted.message,
1403
- chat_job: submitted.chat_job
1404
- }, jsonContext(client));
1405
- }
1406
- else
1407
- process.stdout.write(`queued OpenCode follow-up for run ${runId}\n`);
1482
+ const payload = await client.createOpenCodeConnection(runId, expiresInSeconds);
1483
+ if (hasFlag(flags, "json")) {
1484
+ printSuccess("opencode.connect", payload, jsonContext(client), [
1485
+ commandAction(["sanbox", "opencode", "connections", "revoke", runId, payload.connection.id, "--json"], "Revoke this OpenCode SDK connection.")
1486
+ ]);
1408
1487
  return;
1409
1488
  }
1410
- const chatJobId = submitted.chat_job.id;
1411
- const belongsToFollowup = (event) => event.payload.chat_job_id === chatJobId;
1412
- const queuedEventId = submitted.events.reduce((latest, event) => event.kind === "chat.queued" && belongsToFollowup(event) ? Math.max(latest, event.id) : latest, 0);
1413
- const receivedEventId = [...submitted.events]
1414
- .reverse()
1415
- .find((event) => event.kind === "message.received" && belongsToFollowup(event))?.id ?? queuedEventId;
1416
- const cursor = submitted.events.reduce((latest, event) => Math.max(latest, event.id), 0);
1417
- const completedInResponse = [...submitted.events]
1418
- .reverse()
1419
- .find((event) => (event.kind === "chat.completed" || event.kind === "chat.failed") && belongsToFollowup(event));
1420
- const view = parseActivityView(flagString(flags, "view", "activity"));
1421
- const jsonl = hasFlag(flags, "jsonl");
1422
- const controller = new AbortController();
1423
- const onInterrupt = () => controller.abort();
1424
- process.once("SIGINT", onInterrupt);
1425
- if (wantsWatch(flags) && !jsonl)
1426
- process.stdout.write(`Watching follow-up for run ${runId}. Ctrl-C detaches.\n`);
1427
- try {
1428
- const renderFollowupEvent = (event) => {
1429
- if (!belongsToFollowup(event))
1430
- return;
1431
- if (!wantsWatch(flags) || !shouldRenderEvent(event, view))
1432
- return;
1433
- process.stdout.write(`${jsonl ? formatActivityJsonl(event) : formatActivityLine(event, submitted.run.created_at)}\n`);
1434
- };
1435
- for (const event of submitted.events) {
1436
- if (event.id >= receivedEventId)
1437
- renderFollowupEvent(event);
1489
+ process.stdout.write(`URL ${payload.url}\nTOKEN ${payload.access_token}\nEXPIRES ${payload.connection.expires_at}\n`);
1490
+ return;
1491
+ }
1492
+ if (action === "connections") {
1493
+ const connectionAction = command[2];
1494
+ if (connectionAction === "list") {
1495
+ const runId = requiredPositional(command[3], "run_id_required", "opencode connections list requires a run id.");
1496
+ if (command[4]) {
1497
+ throw new CliError("unexpected_argument", `Unexpected positional argument: ${command[4]}`);
1438
1498
  }
1439
- const terminalEvent = completedInResponse ?? await watchEventsUntil(client, runId, {
1440
- afterEventId: cursor,
1441
- pageSize: integerFlag(flags, "event-page-size", 200, 1, 500),
1442
- pollIntervalMs: integerFlag(flags, "poll-interval-ms", 2000, 250, 60_000),
1443
- timeoutSeconds: integerFlag(flags, "timeout-seconds", 1800, 1, 604_800),
1444
- signal: controller.signal,
1445
- stopWhen: (event) => (event.kind === "chat.completed" || event.kind === "chat.failed") && belongsToFollowup(event),
1446
- onRetry: ({ error, attempt, delayMs }) => {
1447
- const detail = error instanceof Error ? error.message : String(error);
1448
- process.stderr.write(`Follow-up connection lost (${detail}); retry ${attempt} in ${delayMs}ms.\n`);
1449
- },
1450
- onEvent: renderFollowupEvent
1451
- });
1452
- const messages = await client.listMessages(runId);
1499
+ const payload = await client.listOpenCodeConnections(runId);
1453
1500
  if (hasFlag(flags, "json")) {
1454
- printSuccess(commandName, {
1455
- ...publicRunPayload(submitted),
1456
- message: submitted.message,
1457
- chat_job: submitted.chat_job,
1458
- messages: messages.messages,
1459
- followup_event: terminalEvent
1460
- }, jsonContext(client));
1501
+ printSuccess("opencode.connections.list", payload, jsonContext(client));
1502
+ return;
1461
1503
  }
1462
- else if (!jsonl) {
1463
- const assistant = [...messages.messages].reverse().find((item) => item.role === "assistant" && item.payload.chat_job_id === chatJobId);
1464
- if (assistant)
1465
- process.stdout.write(`${assistant.message}\n`);
1504
+ for (const connection of payload.connections) {
1505
+ process.stdout.write(`${connection.id}\t${connection.status}\t${connection.expires_at}\t${connection.token_prefix}\n`);
1466
1506
  }
1467
- if (terminalEvent.kind === "chat.failed")
1468
- process.exitCode = 2;
1469
- }
1470
- catch (error) {
1471
- if (!(error instanceof WatchInterruptedError))
1472
- throw error;
1473
- process.stderr.write(`Detached from follow-up for run ${runId}; processing continues.\n`);
1474
- process.exitCode = 130;
1507
+ return;
1475
1508
  }
1476
- finally {
1477
- process.off("SIGINT", onInterrupt);
1509
+ if (connectionAction === "revoke") {
1510
+ const runId = requiredPositional(command[3], "run_id_required", "opencode connections revoke requires a run id.");
1511
+ const connectionId = requiredPositional(command[4], "opencode_connection_id_required", "opencode connections revoke requires a connection id.");
1512
+ const payload = await client.revokeOpenCodeConnection(runId, connectionId);
1513
+ if (hasFlag(flags, "json")) {
1514
+ printSuccess("opencode.connections.revoke", payload, jsonContext(client));
1515
+ return;
1516
+ }
1517
+ process.stdout.write(`revoked ${payload.connection.id}\n`);
1518
+ return;
1478
1519
  }
1479
- return;
1520
+ throw new CliError("opencode_connections_action_required", "opencode connections requires an action: list or revoke.");
1480
1521
  }
1481
- throw new Error(`Unknown runs action: ${action}`);
1522
+ throw new CliError("opencode_action_required", "opencode requires an action: connect or connections.");
1482
1523
  };
1483
1524
  const commandDoctor = async (flags) => {
1484
1525
  const localConfig = readLocalConfig();
@@ -1717,6 +1758,8 @@ const commandInit = async (command, flags) => {
1717
1758
  const helpFor = (command) => {
1718
1759
  if (command[0] === "run")
1719
1760
  return runHelp;
1761
+ if (command[0] === "runs" && command[1] === "supabase")
1762
+ return runsSupabaseHelp;
1720
1763
  if (command[0] === "doctor")
1721
1764
  return doctorHelp;
1722
1765
  if (command[0] === "model-providers")
@@ -1725,6 +1768,8 @@ const helpFor = (command) => {
1725
1768
  return anthropicEnvironmentsHelp;
1726
1769
  if (command[0] === "templates")
1727
1770
  return templatesHelp;
1771
+ if (command[0] === "opencode")
1772
+ return openCodeHelp;
1728
1773
  if (command[0] === "ssh")
1729
1774
  return sshHelp;
1730
1775
  return help;
@@ -1747,8 +1792,16 @@ const commandId = (command) => {
1747
1792
  return "run.create";
1748
1793
  if (command[0] === "batch")
1749
1794
  return "batch.create";
1795
+ if (command[0] === "runs" && command[1] === "supabase") {
1796
+ return `runs.supabase.${command[2] || "unknown"}`;
1797
+ }
1750
1798
  if (command[0] === "runs")
1751
1799
  return `runs.${command[1] || "unknown"}`;
1800
+ if (command[0] === "opencode" && command[1] === "connections") {
1801
+ return `opencode.connections.${command[2] || "unknown"}`;
1802
+ }
1803
+ if (command[0] === "opencode")
1804
+ return `opencode.${command[1] || "unknown"}`;
1752
1805
  if (command[0] === "ssh" || command[0] === "ssh-proxy")
1753
1806
  return command[0];
1754
1807
  return command[0] || "help";
@@ -1796,6 +1849,8 @@ const main = async (command, flags) => {
1796
1849
  return commandBatch(flags);
1797
1850
  if (command[0] === "runs")
1798
1851
  return commandRuns(command, flags);
1852
+ if (command[0] === "opencode")
1853
+ return commandOpenCode(command, flags);
1799
1854
  if (command[0] === "ssh")
1800
1855
  return commandSSH(command, flags);
1801
1856
  if (command[0] === "ssh-proxy") {
package/dist/output.js CHANGED
@@ -79,7 +79,10 @@ export const summarizeRun = (payload) => {
79
79
  run.provider_id ? `provider=${run.provider_id}` : "",
80
80
  run.model_id ? `model=${run.model_id}` : "",
81
81
  run.sandbox_state ? `sandbox=${run.sandbox_state}` : "",
82
- run.snapshot_generation ? `snapshot=${run.snapshot_generation}` : ""
82
+ run.snapshot_generation ? `snapshot=${run.snapshot_generation}` : "",
83
+ run.email?.address ? `email=${run.email.address}` : run.email ? `email=${run.email.status}` : "",
84
+ run.channels?.telegram.enabled ? "telegram=enabled" : "",
85
+ run.supabase ? `supabase=${run.supabase.status}` : ""
83
86
  ].filter(Boolean).join(" ");
84
87
  return `${run.id} ${run.status}${selection ? ` ${selection}` : ""}${run.exit_code === null ? "" : ` exit=${run.exit_code}`}${run.error ? ` error=${run.error}` : ""}`;
85
88
  };
package/dist/runs.js CHANGED
@@ -50,6 +50,12 @@ export const createRun = async (client, options) => {
50
50
  external_run_id: options.externalRunId,
51
51
  workload_id: options.templateId,
52
52
  instruction: options.instruction,
53
+ ...(options.supabaseUserId ? { supabase_user_id: options.supabaseUserId } : {}),
54
+ ...(options.emailAddress ? { email_address: options.emailAddress } : {}),
55
+ ...(options.telegramBotToken ? {
56
+ telegram_bot_token: options.telegramBotToken,
57
+ telegram_allowed_users: options.telegramAllowedUsers || []
58
+ } : {}),
53
59
  ...(inputCollectionId ? { input_collection_id: inputCollectionId } : {})
54
60
  });
55
61
  };
@@ -71,6 +77,7 @@ export const readTasks = async (tasksPath) => {
71
77
  return {
72
78
  task,
73
79
  external_run_id: record.external_run_id ? String(record.external_run_id) : undefined,
80
+ email_address: record.email_address ? String(record.email_address) : undefined,
74
81
  input: Array.isArray(record.input) ? record.input.map(String) : undefined
75
82
  };
76
83
  });
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.0.9";
1
+ export const version = "0.0.11";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanlabs/sanbox-cli",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",