@sanlabs/sanbox-cli 0.0.9 → 0.0.10
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 +70 -23
- package/dist/api.js +6 -9
- package/dist/args.js +2 -0
- package/dist/cli.js +106 -154
- 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,9 @@ 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.10 adds per-run Hermes email and Telegram channels,
|
|
19
|
+
Supabase user authorization, and removes the retired run-chat commands.
|
|
20
|
+
CLI 0.0.9 added user login for private SSH access to running Hermes Computers.
|
|
20
21
|
CLI 0.0.6 added Anthropic self-hosted environment inspection and administration.
|
|
21
22
|
|
|
22
23
|
## Configure
|
|
@@ -82,12 +83,26 @@ sanbox doctor --json
|
|
|
82
83
|
Model IDs are provider-scoped. The CLI never guesses or silently substitutes a provider, model, or template.
|
|
83
84
|
For waited task runs, choose a template with `runnable: true`, `template_type: "runner"`, and
|
|
84
85
|
`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.
|
|
86
|
+
target domains are already approved. Hermes service templates are always-on. A Hermes template
|
|
87
|
+
declares which optional channels its runs may activate:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
sanbox templates create \
|
|
91
|
+
--name "Customer assistant" \
|
|
92
|
+
--harness hermes \
|
|
93
|
+
--model-provider openai \
|
|
94
|
+
--model '<model-id>' \
|
|
95
|
+
--channel email \
|
|
96
|
+
--channel telegram
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Omit both `--channel` flags for a WebUI-only computer. Channel credentials never belong to the
|
|
100
|
+
template.
|
|
86
101
|
|
|
87
102
|
## Create A Browser Use Template
|
|
88
103
|
|
|
89
104
|
Browser Use runs local headless Chromium inside the Firecracker sandbox. It requires OpenAI,
|
|
90
|
-
Anthropic,
|
|
105
|
+
Anthropic, Google Gemini, or Hetzner Inference and at least one explicit browser target:
|
|
91
106
|
|
|
92
107
|
```bash
|
|
93
108
|
sanbox templates create \
|
|
@@ -106,7 +121,7 @@ sanbox templates create \
|
|
|
106
121
|
|
|
107
122
|
Targets are limited to hostnames and leading wildcards on ports 80 and 443. The remaining browser
|
|
108
123
|
security controls are fixed. Runs return `browser-use-response.md`, a sanitized run ledger, and any
|
|
109
|
-
allowed downloads. Browser Use templates
|
|
124
|
+
allowed downloads. Browser Use templates are one-shot agent executions.
|
|
110
125
|
|
|
111
126
|
## Run Idempotently
|
|
112
127
|
|
|
@@ -126,6 +141,51 @@ sanbox run "Review this repo and write output/report.md" \
|
|
|
126
141
|
|
|
127
142
|
Repeat `--input` for files, directories, or globs. The CLI excludes common secrets and applies `.sanboxignore`. `--include` is a deprecated compatibility alias.
|
|
128
143
|
|
|
144
|
+
For a Hermes template that allows `email`, optionally request an address from one of the AgentMail
|
|
145
|
+
domains listed in the Sanbox console:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
sanbox run "Handle customer email for this project" \
|
|
149
|
+
--template "$SANBOX_TEMPLATE" \
|
|
150
|
+
--email-address research-agent@agentmail.to \
|
|
151
|
+
--json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`--email-address` is optional. When supplied, AgentMail must be connected and Sanbox does not
|
|
155
|
+
generate a fallback address; use `agentmail.to` or one of the domains listed for the connected Pod.
|
|
156
|
+
|
|
157
|
+
For a Hermes template that allows `telegram`, pass the bot token and at least one numeric user ID on
|
|
158
|
+
the run. Both values are optional unless the run needs Telegram:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
sanbox run "Handle Telegram messages for this project" \
|
|
162
|
+
--template "$SANBOX_TEMPLATE" \
|
|
163
|
+
--telegram-bot-token '<telegram-bot-token>' \
|
|
164
|
+
--telegram-allowed-user '<numeric-telegram-user-id>' \
|
|
165
|
+
--json
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
You can set `SANBOX_TELEGRAM_BOT_TOKEN` instead of passing `--telegram-bot-token`. Do not use both.
|
|
169
|
+
|
|
170
|
+
For a long-running Hermes Computer that should act as one Supabase user, provide that user's
|
|
171
|
+
Supabase Auth UUID. Sanbox automatically uses the organization's connected Supabase project:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
sanbox run "Act as this user's procurement assistant" \
|
|
175
|
+
--template "$SANBOX_TEMPLATE" \
|
|
176
|
+
--external-run-id "hermes:<tenant-id>:<user-id>" \
|
|
177
|
+
--supabase-user-id "<supabase-auth-user-uuid>" \
|
|
178
|
+
--json
|
|
179
|
+
|
|
180
|
+
sanbox runs supabase authorize <run-id> --open
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The run stays in `awaiting_grant` until the expected Supabase user approves the browser consent
|
|
184
|
+
flow. Without `--open`, the authorize command prints the URL for copying into any browser. The
|
|
185
|
+
callback lands on a Sanbox-hosted completion page, so a CLI demo does not need a customer web app.
|
|
186
|
+
Customer integrations may use `--return-url <https-url>` when its origin matches the return origin
|
|
187
|
+
configured on the organization Supabase connection.
|
|
188
|
+
|
|
129
189
|
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
190
|
|
|
131
191
|
## Inspect And Recover
|
|
@@ -180,8 +240,9 @@ sanbox runs pause <run-id> --wait --json
|
|
|
180
240
|
|
|
181
241
|
Resume restores the latest writable Firecracker snapshot without starting the configured agent
|
|
182
242
|
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
|
-
|
|
243
|
+
access until Pause creates the next snapshot generation. Before resuming, require
|
|
244
|
+
`sandbox_state: "paused"` and a positive `snapshot_generation`. Do not submit agent work while that
|
|
245
|
+
lease is active. Paused state has no retention TTL and remains available until explicitly deleted.
|
|
185
246
|
|
|
186
247
|
## SSH Into A Hermes Computer
|
|
187
248
|
|
|
@@ -197,22 +258,8 @@ Ed25519 identity, pins the microVM's runtime host key, and tunnels the SSH strea
|
|
|
197
258
|
authenticated WebSocket. The temporary key is deleted when the connection closes, and the saved
|
|
198
259
|
user token is not passed to the OpenSSH child process.
|
|
199
260
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
```bash
|
|
203
|
-
sanbox runs messages <run-id> --json
|
|
204
|
-
sanbox runs message <run-id> "Summarize the retained output" --wait --json
|
|
205
|
-
```
|
|
206
|
-
|
|
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.
|
|
211
|
-
|
|
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.
|
|
261
|
+
OpenCode and Browser Use runs are one-shot agent executions. Create a new run for additional agent
|
|
262
|
+
work. A persisted sandbox can still be resumed manually for inspection when a snapshot exists.
|
|
216
263
|
|
|
217
264
|
## Batch Work
|
|
218
265
|
|
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
|
}
|
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] [--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,7 @@ 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]
|
|
61
60
|
sanbox ssh <run-id>
|
|
62
61
|
sanbox init [--force]
|
|
63
62
|
sanbox init agent [--write]
|
|
@@ -83,6 +82,10 @@ Options:
|
|
|
83
82
|
--input <path> File, directory, or glob to upload. Repeatable.
|
|
84
83
|
--template <id> Template id or slug. Required unless SANBOX_TEMPLATE or project config sets it.
|
|
85
84
|
--external-run-id <id> Idempotency key for retries.
|
|
85
|
+
--supabase-user-id <id> Supabase Auth user UUID expected during OAuth consent.
|
|
86
|
+
--email-address <address> Optional for Hermes computers; must use a listed AgentMail domain.
|
|
87
|
+
--telegram-bot-token <token> Optional Telegram bot token; SANBOX_TELEGRAM_BOT_TOKEN is also supported.
|
|
88
|
+
--telegram-allowed-user <id> Numeric Telegram user allowed to reach this run. Repeatable.
|
|
86
89
|
--dry-run Preview included files without creating a run.
|
|
87
90
|
--wait Poll until terminal status.
|
|
88
91
|
--watch Stream activity until terminal status.
|
|
@@ -92,6 +95,17 @@ Options:
|
|
|
92
95
|
--cancel-on-interrupt Request run cancellation when Ctrl-C is pressed.
|
|
93
96
|
--json Print JSON.
|
|
94
97
|
`;
|
|
98
|
+
const runsSupabaseHelp = `Sanbox Supabase run authorization
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
sanbox runs supabase authorize <run-id>
|
|
102
|
+
sanbox runs supabase authorize <run-id> --open
|
|
103
|
+
|
|
104
|
+
Options:
|
|
105
|
+
--open Open the consent URL in the default browser.
|
|
106
|
+
--return-url <https-url> Override the built-in Sanbox completion page with an allowlisted customer URL.
|
|
107
|
+
--json Print JSON.
|
|
108
|
+
`;
|
|
95
109
|
const doctorHelp = `Sanbox doctor
|
|
96
110
|
|
|
97
111
|
Usage:
|
|
@@ -134,8 +148,8 @@ Usage:
|
|
|
134
148
|
|
|
135
149
|
Template creation requires an exact provider id and that provider's exact model id.
|
|
136
150
|
LiteLLM budgets are optional USD amounts and apply separately to each run.
|
|
137
|
-
Hermes templates are always-on
|
|
138
|
-
and
|
|
151
|
+
Hermes templates are always-on computers. Use repeatable --channel email|telegram to select which
|
|
152
|
+
channels runs may activate. Email and Telegram credentials are supplied only when creating a run.
|
|
139
153
|
Browser Use templates run local headless Chromium inside a one-shot sandbox. They require OpenAI
|
|
140
154
|
or Anthropic and at least one repeatable --browser-domain hostname or leading wildcard.
|
|
141
155
|
Optional controls: --browser-max-steps, --browser-step-timeout-seconds,
|
|
@@ -231,17 +245,11 @@ exclusive manual lease and leaves the sandbox running until pause snapshots it a
|
|
|
231
245
|
agent work while the manual lease is active. Before resume, require sandbox_state: "paused" and a
|
|
232
246
|
positive snapshot_generation.
|
|
233
247
|
|
|
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
248
|
For independent fan-out, use \`sanbox batch\` with a stable external_run_id per task and keep the
|
|
241
249
|
client alive until submission completes.
|
|
242
250
|
|
|
243
|
-
Do not claim completion until the run is completed
|
|
244
|
-
|
|
251
|
+
Do not claim completion until the run is completed and required artifacts are downloaded and verified.
|
|
252
|
+
Report run/external/template IDs, status, sandbox state, snapshot
|
|
245
253
|
generation, artifact paths/digests, and blockers. The CLI excludes common secrets by default; add
|
|
246
254
|
.sanboxignore for project rules.
|
|
247
255
|
`;
|
|
@@ -429,7 +437,7 @@ const flagSets = {
|
|
|
429
437
|
"harness",
|
|
430
438
|
"llm-budget-usd",
|
|
431
439
|
"web-access",
|
|
432
|
-
"
|
|
440
|
+
"channel",
|
|
433
441
|
"browser-domain",
|
|
434
442
|
"browser-max-steps",
|
|
435
443
|
"browser-step-timeout-seconds",
|
|
@@ -439,7 +447,9 @@ const flagSets = {
|
|
|
439
447
|
"browser-additional-instructions"
|
|
440
448
|
],
|
|
441
449
|
run: [
|
|
442
|
-
...commonFlags, "task", "input", "template", "external-run-id",
|
|
450
|
+
...commonFlags, "task", "input", "template", "external-run-id", "email-address",
|
|
451
|
+
"supabase-user-id",
|
|
452
|
+
"telegram-bot-token", "telegram-allowed-user",
|
|
443
453
|
"dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "cancel-on-interrupt",
|
|
444
454
|
"poll-interval-ms", "event-page-size", "timeout-seconds", "verbose"
|
|
445
455
|
],
|
|
@@ -450,7 +460,6 @@ const flagSets = {
|
|
|
450
460
|
"runs.list": [...commonFlags, "limit"],
|
|
451
461
|
"runs.get": commonFlags,
|
|
452
462
|
"runs.events": [...commonFlags, "after-event-id"],
|
|
453
|
-
"runs.messages": commonFlags,
|
|
454
463
|
"runs.artifacts": commonFlags,
|
|
455
464
|
"runs.share": [...commonFlags, "expires", "name"],
|
|
456
465
|
"runs.shares": commonFlags,
|
|
@@ -463,10 +472,7 @@ const flagSets = {
|
|
|
463
472
|
"runs.cancel": commonFlags,
|
|
464
473
|
"runs.resume": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
465
474
|
"runs.pause": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
466
|
-
"runs.
|
|
467
|
-
...commonFlags, "message", "wait", "watch", "jsonl", "view",
|
|
468
|
-
"poll-interval-ms", "event-page-size", "timeout-seconds"
|
|
469
|
-
],
|
|
475
|
+
"runs.supabase": [...commonFlags, "open", "return-url"],
|
|
470
476
|
ssh: ["api-url", "help"],
|
|
471
477
|
"ssh-proxy": [],
|
|
472
478
|
login: ["api-url", "json", "no-browser", "help"],
|
|
@@ -482,6 +488,8 @@ const commandKey = (command) => {
|
|
|
482
488
|
return "version";
|
|
483
489
|
if (command[0] === "auth")
|
|
484
490
|
return `auth.${command[1] || ""}`;
|
|
491
|
+
if (command[0] === "runs" && command[1] === "supabase")
|
|
492
|
+
return "runs.supabase";
|
|
485
493
|
if (command[0] === "model-providers" ||
|
|
486
494
|
command[0] === "anthropic-environments" ||
|
|
487
495
|
command[0] === "templates" ||
|
|
@@ -493,14 +501,15 @@ const commandKey = (command) => {
|
|
|
493
501
|
return command[0];
|
|
494
502
|
};
|
|
495
503
|
const requiredValueFlags = new Set([
|
|
496
|
-
"api-url", "template", "task", "input", "include", "external-run-id",
|
|
504
|
+
"api-url", "template", "task", "input", "include", "external-run-id", "email-address",
|
|
505
|
+
"supabase-user-id", "return-url",
|
|
506
|
+
"telegram-bot-token", "telegram-allowed-user", "channel",
|
|
497
507
|
"tasks", "max-parallel", "batch-id", "poll-interval-ms",
|
|
498
508
|
"event-page-size", "timeout-seconds", "view", "after-event-id", "limit", "name",
|
|
499
|
-
"model-provider", "model", "harness", "llm-budget-usd",
|
|
509
|
+
"model-provider", "model", "harness", "llm-budget-usd",
|
|
500
510
|
"browser-domain", "browser-max-steps", "browser-step-timeout-seconds",
|
|
501
511
|
"browser-vision-mode", "browser-viewport", "browser-download-policy",
|
|
502
|
-
"browser-additional-instructions", "expires",
|
|
503
|
-
"message", "output", "artifact"
|
|
512
|
+
"browser-additional-instructions", "expires", "output", "artifact"
|
|
504
513
|
]);
|
|
505
514
|
const validateFlagValues = (flags) => {
|
|
506
515
|
for (const flag of requiredValueFlags) {
|
|
@@ -571,7 +580,6 @@ const validatePositionals = (command, flags) => {
|
|
|
571
580
|
"runs.list": 2,
|
|
572
581
|
"runs.get": 3,
|
|
573
582
|
"runs.events": 3,
|
|
574
|
-
"runs.messages": 3,
|
|
575
583
|
"runs.artifacts": 3,
|
|
576
584
|
"runs.share": 3,
|
|
577
585
|
"runs.shares": 3,
|
|
@@ -581,6 +589,7 @@ const validatePositionals = (command, flags) => {
|
|
|
581
589
|
"runs.cancel": 3,
|
|
582
590
|
"runs.resume": 3,
|
|
583
591
|
"runs.pause": 3,
|
|
592
|
+
"runs.supabase": 4,
|
|
584
593
|
ssh: 2,
|
|
585
594
|
"ssh-proxy": 1,
|
|
586
595
|
login: 1,
|
|
@@ -692,6 +701,11 @@ const commandRun = async (command, flags) => {
|
|
|
692
701
|
throw new Error("--wait cannot be combined with --watch or --jsonl.");
|
|
693
702
|
}
|
|
694
703
|
validateWatchFlags(flags);
|
|
704
|
+
const supabaseUserId = flagString(flags, "supabase-user-id").trim();
|
|
705
|
+
if (supabaseUserId &&
|
|
706
|
+
!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(supabaseUserId)) {
|
|
707
|
+
throw new CliError("invalid_supabase_user_id", "--supabase-user-id must be a UUID.");
|
|
708
|
+
}
|
|
695
709
|
if (hasFlag(flags, "dry-run")) {
|
|
696
710
|
if (wantsWatch(flags))
|
|
697
711
|
throw new Error("--dry-run cannot be combined with --watch or --jsonl.");
|
|
@@ -702,6 +716,24 @@ const commandRun = async (command, flags) => {
|
|
|
702
716
|
printPreview(preview, hasFlag(flags, "verbose"));
|
|
703
717
|
return;
|
|
704
718
|
}
|
|
719
|
+
const tokenFlag = flagString(flags, "telegram-bot-token").trim();
|
|
720
|
+
const telegramAllowedUsers = [...new Set(flagList(flags, "telegram-allowed-user")
|
|
721
|
+
.map((value) => value.trim())
|
|
722
|
+
.filter(Boolean))];
|
|
723
|
+
const telegramRequested = Boolean(tokenFlag || telegramAllowedUsers.length > 0);
|
|
724
|
+
const tokenEnvironment = telegramRequested
|
|
725
|
+
? process.env.SANBOX_TELEGRAM_BOT_TOKEN?.trim() || ""
|
|
726
|
+
: "";
|
|
727
|
+
if (tokenFlag && tokenEnvironment) {
|
|
728
|
+
throw new CliError("telegram_token_ambiguous", "Use either --telegram-bot-token or SANBOX_TELEGRAM_BOT_TOKEN, not both.");
|
|
729
|
+
}
|
|
730
|
+
const telegramBotToken = tokenFlag || tokenEnvironment;
|
|
731
|
+
if (!telegramBotToken && telegramAllowedUsers.length > 0) {
|
|
732
|
+
throw new CliError("telegram_bot_token_required", "--telegram-allowed-user requires --telegram-bot-token or SANBOX_TELEGRAM_BOT_TOKEN.");
|
|
733
|
+
}
|
|
734
|
+
if (telegramBotToken && telegramAllowedUsers.length === 0) {
|
|
735
|
+
throw new CliError("telegram_allowed_user_required", "Telegram requires at least one --telegram-allowed-user.");
|
|
736
|
+
}
|
|
705
737
|
const client = makeClient(flags);
|
|
706
738
|
const template = readTemplateSelection(flags);
|
|
707
739
|
let payload;
|
|
@@ -711,6 +743,10 @@ const commandRun = async (command, flags) => {
|
|
|
711
743
|
instruction: task,
|
|
712
744
|
inputs: flagList(flags, "input"),
|
|
713
745
|
externalRunId: flagString(flags, "external-run-id") || undefined,
|
|
746
|
+
supabaseUserId: supabaseUserId || undefined,
|
|
747
|
+
emailAddress: flagString(flags, "email-address") || undefined,
|
|
748
|
+
telegramBotToken: telegramBotToken || undefined,
|
|
749
|
+
telegramAllowedUsers,
|
|
714
750
|
templateId: template.id
|
|
715
751
|
});
|
|
716
752
|
}
|
|
@@ -764,6 +800,7 @@ const commandBatch = async (flags) => {
|
|
|
764
800
|
instruction: task.task,
|
|
765
801
|
inputs: task.input || inputs,
|
|
766
802
|
externalRunId: task.external_run_id || `sanbox-batch-${batchId}-${index + 1}`,
|
|
803
|
+
emailAddress: task.email_address,
|
|
767
804
|
templateId: template.id
|
|
768
805
|
});
|
|
769
806
|
}
|
|
@@ -874,8 +911,7 @@ const commandModelProviders = async (command, flags) => {
|
|
|
874
911
|
printSuccess("model_providers.get", payload, jsonContext(client), nextActions);
|
|
875
912
|
return;
|
|
876
913
|
}
|
|
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`);
|
|
914
|
+
process.stdout.write(`${providerId(payload.provider)}${payload.provider.status ? ` ${payload.provider.status}` : ""}\n`);
|
|
879
915
|
return;
|
|
880
916
|
}
|
|
881
917
|
if (action === "models") {
|
|
@@ -1120,8 +1156,10 @@ const commandTemplates = async (command, flags) => {
|
|
|
1120
1156
|
if (harness === "hermes" && llmBudgetUsd !== undefined) {
|
|
1121
1157
|
throw new CliError("hermes_budget_unsupported", "Always-on Hermes templates do not support --llm-budget-usd yet.");
|
|
1122
1158
|
}
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1159
|
+
const channels = [...new Set(flagList(flags, "channel").map((value) => value.trim()).filter(Boolean))];
|
|
1160
|
+
if (channels.some((channel) => channel !== "email" && channel !== "telegram")) {
|
|
1161
|
+
throw new CliError("invalid_hermes_channels", "--channel must be email or telegram.");
|
|
1162
|
+
}
|
|
1125
1163
|
const browserFlagNames = [
|
|
1126
1164
|
"browser-domain",
|
|
1127
1165
|
"browser-max-steps",
|
|
@@ -1137,8 +1175,9 @@ const commandTemplates = async (command, flags) => {
|
|
|
1137
1175
|
if (harness === "browser-use" &&
|
|
1138
1176
|
modelProvider !== "openai" &&
|
|
1139
1177
|
modelProvider !== "anthropic" &&
|
|
1140
|
-
modelProvider !== "google"
|
|
1141
|
-
|
|
1178
|
+
modelProvider !== "google" &&
|
|
1179
|
+
modelProvider !== "hetzner") {
|
|
1180
|
+
throw new CliError("browser_use_provider_unsupported", "Browser Use templates require --model-provider openai, anthropic, google, or hetzner.");
|
|
1142
1181
|
}
|
|
1143
1182
|
const browserDomains = harness === "browser-use"
|
|
1144
1183
|
? parseBrowserDomains(flagList(flags, "browser-domain"))
|
|
@@ -1146,11 +1185,8 @@ const commandTemplates = async (command, flags) => {
|
|
|
1146
1185
|
const browserUse = harness === "browser-use"
|
|
1147
1186
|
? parseBrowserUseConfig(flags)
|
|
1148
1187
|
: 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.");
|
|
1188
|
+
if (harness !== "hermes" && channels.length > 0) {
|
|
1189
|
+
throw new CliError("hermes_channels_not_supported", "--channel requires --harness hermes.");
|
|
1154
1190
|
}
|
|
1155
1191
|
let payload;
|
|
1156
1192
|
try {
|
|
@@ -1175,8 +1211,7 @@ const commandTemplates = async (command, flags) => {
|
|
|
1175
1211
|
} : {}),
|
|
1176
1212
|
...(harness === "hermes" ? {
|
|
1177
1213
|
harness,
|
|
1178
|
-
|
|
1179
|
-
telegram_allowed_users: telegramAllowedUsers
|
|
1214
|
+
channels: channels
|
|
1180
1215
|
} : {})
|
|
1181
1216
|
});
|
|
1182
1217
|
}
|
|
@@ -1186,8 +1221,14 @@ const commandTemplates = async (command, flags) => {
|
|
|
1186
1221
|
if (hasFlag(flags, "json")) {
|
|
1187
1222
|
printSuccess("templates.create", payload, jsonContext(client), [
|
|
1188
1223
|
commandAction(["sanbox", "templates", "validate", payload.template.id, "--json"], "Validate the new template before running it."),
|
|
1189
|
-
commandAction([
|
|
1190
|
-
|
|
1224
|
+
commandAction([
|
|
1225
|
+
"sanbox",
|
|
1226
|
+
"run",
|
|
1227
|
+
harness === "hermes" ? "<agent role and instructions>" : "<task>",
|
|
1228
|
+
"--template",
|
|
1229
|
+
payload.template.id
|
|
1230
|
+
], harness === "hermes"
|
|
1231
|
+
? "Start the always-on computer and activate only the channels this run needs."
|
|
1191
1232
|
: "Create a run with the new template.")
|
|
1192
1233
|
]);
|
|
1193
1234
|
return;
|
|
@@ -1215,6 +1256,23 @@ const commandRuns = async (command, flags) => {
|
|
|
1215
1256
|
}
|
|
1216
1257
|
return;
|
|
1217
1258
|
}
|
|
1259
|
+
if (action === "supabase") {
|
|
1260
|
+
if (command[2] !== "authorize") {
|
|
1261
|
+
throw new CliError("supabase_action_required", "runs supabase requires the action: authorize.");
|
|
1262
|
+
}
|
|
1263
|
+
const runId = requiredPositional(command[3], "run_id_required", "runs supabase authorize requires a run id.");
|
|
1264
|
+
const payload = await client.authorizeRunSupabase(runId, flagString(flags, "return-url").trim() || undefined);
|
|
1265
|
+
if (hasFlag(flags, "open") && !openBrowser(payload.authorization_url)) {
|
|
1266
|
+
process.stderr.write("Could not open a browser. Open the printed consent URL manually.\n");
|
|
1267
|
+
}
|
|
1268
|
+
if (hasFlag(flags, "json")) {
|
|
1269
|
+
printSuccess("runs.supabase.authorize", payload, jsonContext(client));
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
process.stdout.write(`${payload.authorization_url}\n`);
|
|
1273
|
+
}
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1218
1276
|
const runId = requiredPositional(command[2], "run_id_required", `runs ${action} requires a run id.`);
|
|
1219
1277
|
if (action === "get") {
|
|
1220
1278
|
const payload = await client.getRun(runId);
|
|
@@ -1232,17 +1290,6 @@ const commandRuns = async (command, flags) => {
|
|
|
1232
1290
|
payload.events.forEach((event) => process.stdout.write(`${event.id} ${event.level} ${event.kind} ${event.message}\n`));
|
|
1233
1291
|
return;
|
|
1234
1292
|
}
|
|
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
1293
|
if (action === "artifacts") {
|
|
1247
1294
|
const payload = await client.listArtifacts(runId);
|
|
1248
1295
|
if (hasFlag(flags, "json")) {
|
|
@@ -1378,106 +1425,6 @@ const commandRuns = async (command, flags) => {
|
|
|
1378
1425
|
}
|
|
1379
1426
|
return;
|
|
1380
1427
|
}
|
|
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.");
|
|
1396
|
-
}
|
|
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`);
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
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);
|
|
1438
|
-
}
|
|
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);
|
|
1453
|
-
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));
|
|
1461
|
-
}
|
|
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`);
|
|
1466
|
-
}
|
|
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;
|
|
1475
|
-
}
|
|
1476
|
-
finally {
|
|
1477
|
-
process.off("SIGINT", onInterrupt);
|
|
1478
|
-
}
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
1428
|
throw new Error(`Unknown runs action: ${action}`);
|
|
1482
1429
|
};
|
|
1483
1430
|
const commandDoctor = async (flags) => {
|
|
@@ -1717,6 +1664,8 @@ const commandInit = async (command, flags) => {
|
|
|
1717
1664
|
const helpFor = (command) => {
|
|
1718
1665
|
if (command[0] === "run")
|
|
1719
1666
|
return runHelp;
|
|
1667
|
+
if (command[0] === "runs" && command[1] === "supabase")
|
|
1668
|
+
return runsSupabaseHelp;
|
|
1720
1669
|
if (command[0] === "doctor")
|
|
1721
1670
|
return doctorHelp;
|
|
1722
1671
|
if (command[0] === "model-providers")
|
|
@@ -1747,6 +1696,9 @@ const commandId = (command) => {
|
|
|
1747
1696
|
return "run.create";
|
|
1748
1697
|
if (command[0] === "batch")
|
|
1749
1698
|
return "batch.create";
|
|
1699
|
+
if (command[0] === "runs" && command[1] === "supabase") {
|
|
1700
|
+
return `runs.supabase.${command[2] || "unknown"}`;
|
|
1701
|
+
}
|
|
1750
1702
|
if (command[0] === "runs")
|
|
1751
1703
|
return `runs.${command[1] || "unknown"}`;
|
|
1752
1704
|
if (command[0] === "ssh" || 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.10";
|