@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 +117 -21
- package/dist/api.js +18 -9
- package/dist/args.js +2 -0
- package/dist/cli.js +206 -151
- package/dist/output.js +4 -1
- package/dist/runs.js +7 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
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.
|
|
19
|
-
|
|
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"`.
|
|
85
|
-
|
|
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,
|
|
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
|
|
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.
|
|
184
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
204
|
-
sanbox
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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,
|
|
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
|
|
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
|
|
71
|
-
Ed25519 key, authorizes it for one connection, verifies the microVM host key, and tunnels
|
|
72
|
-
the authenticated private management path. Run sanbox login first. The
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
244
|
-
|
|
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
|
-
"
|
|
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.
|
|
467
|
-
|
|
468
|
-
|
|
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", "
|
|
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
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
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
|
|
1150
|
-
throw new CliError("
|
|
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
|
-
|
|
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([
|
|
1190
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
const
|
|
1389
|
-
if (
|
|
1390
|
-
throw new
|
|
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
|
|
1398
|
-
if (
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
|
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(
|
|
1455
|
-
|
|
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
|
-
|
|
1463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
1520
|
+
throw new CliError("opencode_connections_action_required", "opencode connections requires an action: list or revoke.");
|
|
1480
1521
|
}
|
|
1481
|
-
throw new
|
|
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.
|
|
1
|
+
export const version = "0.0.11";
|