@sanlabs/sanbox-cli 0.0.3 → 0.0.4
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 +58 -79
- package/dist/api.js +47 -0
- package/dist/args.js +23 -7
- package/dist/artifacts.js +124 -0
- package/dist/cli.js +472 -147
- package/dist/config.js +2 -2
- package/dist/inputs.js +150 -0
- package/dist/runs.js +12 -23
- package/dist/version.js +1 -1
- package/dist/watch.js +59 -0
- package/package.json +1 -1
- package/dist/dossier.js +0 -187
package/dist/cli.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { formatActivityJsonl, formatActivityLine, parseActivityView, shouldRenderEvent } from "./activity.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ArtifactExistsError, downloadArtifacts } from "./artifacts.js";
|
|
6
|
+
import { booleanFlags, flagList, flagString, hasFlag, parseArgs } from "./args.js";
|
|
6
7
|
import { SanboxApiError, SanboxClient } from "./api.js";
|
|
7
8
|
import { defaultApiUrl, readConfig, readLocalConfig, readTemplateSelection } from "./config.js";
|
|
8
|
-
import { inspectDossierFile, previewTaskDossier, writeTaskDossier } from "./dossier.js";
|
|
9
9
|
import { CliError, commandAction, consoleAction } from "./errors.js";
|
|
10
|
+
import { previewInputs } from "./inputs.js";
|
|
10
11
|
import { printError, printJsonlError, printRun, printSuccess, publicRun, publicRunPayload } from "./output.js";
|
|
11
12
|
import { createRun, isTerminalRun, readTasks, runPool, stableBatchId, waitForRun } from "./runs.js";
|
|
12
13
|
import { version } from "./version.js";
|
|
13
|
-
import { WatchInterruptedError, watchRun } from "./watch.js";
|
|
14
|
+
import { WatchInterruptedError, watchEventsUntil, watchRun } from "./watch.js";
|
|
14
15
|
const help = `Sanbox CLI
|
|
15
16
|
|
|
16
17
|
Environment:
|
|
@@ -20,7 +21,9 @@ Environment:
|
|
|
20
21
|
SANBOX_TEMPLATE Explicit template id or slug for run and batch
|
|
21
22
|
|
|
22
23
|
Commands:
|
|
24
|
+
sanbox --version
|
|
23
25
|
sanbox auth check [--json]
|
|
26
|
+
sanbox orgs list [--json]
|
|
24
27
|
sanbox context [--json]
|
|
25
28
|
sanbox doctor [--json]
|
|
26
29
|
sanbox model-providers list [--json]
|
|
@@ -29,32 +32,30 @@ Commands:
|
|
|
29
32
|
sanbox templates list [--json]
|
|
30
33
|
sanbox templates get <template-id> [--json]
|
|
31
34
|
sanbox templates validate <template-id> [--json]
|
|
32
|
-
sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--web-access] [--json]
|
|
33
|
-
sanbox run "task" --template <template-id> [--
|
|
34
|
-
sanbox run --task "..." --template <template-id> [--
|
|
35
|
-
sanbox
|
|
36
|
-
sanbox
|
|
37
|
-
sanbox bundle preview [--include "app/**"] [--json]
|
|
38
|
-
sanbox bundle create "task" --out ./run.zip [--include "app/**"]
|
|
39
|
-
sanbox bundle inspect ./run.zip [--json]
|
|
35
|
+
sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--llm-budget-usd <amount>] [--web-access] [--json]
|
|
36
|
+
sanbox run "task" --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
37
|
+
sanbox run --task "..." --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
38
|
+
sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
|
|
39
|
+
sanbox runs list [--limit 50] [--json]
|
|
40
40
|
sanbox runs get <run-id> [--json]
|
|
41
41
|
sanbox runs events <run-id> [--after-event-id 0] [--json]
|
|
42
|
+
sanbox runs messages <run-id> [--json]
|
|
43
|
+
sanbox runs artifacts <run-id> [--json]
|
|
44
|
+
sanbox runs download <run-id> --output <directory> [--artifact <path>] [--overwrite] [--json]
|
|
42
45
|
sanbox runs watch <run-id> [--after-event-id 0] [--view activity|logs|compact] [--jsonl]
|
|
43
46
|
sanbox runs cancel <run-id> [--json]
|
|
44
|
-
sanbox runs message <run-id>
|
|
47
|
+
sanbox runs message <run-id> "..." [--wait | --watch] [--json | --jsonl]
|
|
45
48
|
sanbox init [--force]
|
|
46
49
|
sanbox init agent [--write]
|
|
47
50
|
`;
|
|
48
51
|
const runHelp = `Sanbox run
|
|
49
52
|
|
|
50
53
|
Usage:
|
|
51
|
-
sanbox run "Review
|
|
52
|
-
sanbox run --task "Review
|
|
53
|
-
sanbox run --dossier ./run.zip --template <template-id> --wait
|
|
54
|
+
sanbox run "Review these files" --template <template-id> --input report.pdf --input data/ --wait
|
|
55
|
+
sanbox run --task "Review these files" --template <template-id> --input report.pdf --wait --json
|
|
54
56
|
|
|
55
57
|
Options:
|
|
56
|
-
--
|
|
57
|
-
--dossier <path> Submit an existing dossier/run-bundle ZIP.
|
|
58
|
+
--input <path> File, directory, or glob to upload. Repeatable.
|
|
58
59
|
--template <id> Template id or slug. Required unless SANBOX_TEMPLATE or project config sets it.
|
|
59
60
|
--external-run-id <id> Idempotency key for retries.
|
|
60
61
|
--retention-ttl-seconds <n> Workspace retention TTL. Default: 86400.
|
|
@@ -67,16 +68,6 @@ Options:
|
|
|
67
68
|
--cancel-on-interrupt Request run cancellation when Ctrl-C is pressed.
|
|
68
69
|
--json Print JSON.
|
|
69
70
|
`;
|
|
70
|
-
const bundleHelp = `Sanbox bundle
|
|
71
|
-
|
|
72
|
-
Usage:
|
|
73
|
-
sanbox bundle preview --include "app/**"
|
|
74
|
-
sanbox bundle create "Review this repo" --include "app/**" --out ./run.zip
|
|
75
|
-
sanbox bundle inspect ./run.zip
|
|
76
|
-
|
|
77
|
-
Bundle is the public name for the portable ZIP contract. The runner still accepts
|
|
78
|
-
the same ZIP dossier shape: RUNBOOK.md, manifest.json, and input/.
|
|
79
|
-
`;
|
|
80
71
|
const doctorHelp = `Sanbox doctor
|
|
81
72
|
|
|
82
73
|
Usage:
|
|
@@ -101,42 +92,85 @@ Usage:
|
|
|
101
92
|
sanbox templates list [--json]
|
|
102
93
|
sanbox templates get <template-id> [--json]
|
|
103
94
|
sanbox templates validate <template-id> [--json]
|
|
104
|
-
sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--web-access] [--json]
|
|
95
|
+
sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--llm-budget-usd <amount>] [--web-access] [--json]
|
|
105
96
|
|
|
106
97
|
Template creation requires an exact provider id and that provider's exact model id.
|
|
98
|
+
LiteLLM budgets are optional USD amounts and apply separately to each run.
|
|
107
99
|
`;
|
|
108
|
-
const agentInstructions = `# Sanbox
|
|
100
|
+
const agentInstructions = `# Operate Sanbox Autonomously
|
|
109
101
|
|
|
110
|
-
Use
|
|
102
|
+
Use the \`sanbox\` CLI for focused, isolated, long-running, risky, or parallel work. The canonical
|
|
103
|
+
protocol is https://console.sanbox.cloud/agent.md. This workflow requires CLI 0.0.4 or newer.
|
|
111
104
|
|
|
112
|
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
- SANBOX_TEMPLATE, or an explicit --template flag/project default
|
|
105
|
+
Human bootstrap:
|
|
106
|
+
- A human/admin supplies SANBOX_API_KEY and configures provider credentials plus a runnable template.
|
|
107
|
+
- Never print, persist, prompt with, or upload API keys or provider credentials.
|
|
108
|
+
- If the key or a runnable template is unavailable, stop with the exact human action required.
|
|
117
109
|
|
|
118
|
-
|
|
110
|
+
Deterministic startup:
|
|
119
111
|
\`\`\`bash
|
|
120
|
-
sanbox
|
|
121
|
-
sanbox
|
|
112
|
+
sanbox --version
|
|
113
|
+
sanbox orgs list --json
|
|
114
|
+
export SANBOX_ORG=<returned-org-slug>
|
|
115
|
+
sanbox auth check --json
|
|
122
116
|
sanbox context --json
|
|
123
|
-
sanbox model-providers list --json
|
|
124
|
-
sanbox model-providers models <provider-id> --json
|
|
125
117
|
sanbox templates list --json
|
|
126
|
-
|
|
127
|
-
sanbox
|
|
128
|
-
sanbox
|
|
118
|
+
export SANBOX_TEMPLATE=<returned-template-id-or-slug>
|
|
119
|
+
sanbox templates validate "$SANBOX_TEMPLATE" --json
|
|
120
|
+
sanbox doctor --json
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
Never guess opaque IDs. Select a discovered org/template automatically only when exactly one valid
|
|
124
|
+
choice exists; otherwise ask the user. Provider credentials and template administration are
|
|
125
|
+
console-only.
|
|
126
|
+
|
|
127
|
+
Use --json for request/response commands and --jsonl for streams. Parse the versioned envelope:
|
|
128
|
+
schema_version, ok, command, context, data or error, and next_actions. Execute command actions as
|
|
129
|
+
argv arrays, never shell strings. Exit 0 means command success, 1 local/API failure, 2 readiness or
|
|
130
|
+
waited remote failure, and 130 detached while remote work continues.
|
|
131
|
+
|
|
132
|
+
Preview and submit each logical task with a stable idempotency key:
|
|
133
|
+
\`\`\`bash
|
|
134
|
+
sanbox run "Investigate one focused task and write output/report.md" --input app/ --dry-run --json
|
|
135
|
+
sanbox run "Investigate one focused task and write output/report.md" \\
|
|
136
|
+
--template "$SANBOX_TEMPLATE" --external-run-id "<stable-project-task-id>" \\
|
|
137
|
+
--input app/ --wait --json
|
|
138
|
+
\`\`\`
|
|
139
|
+
|
|
140
|
+
Reuse the same --external-run-id after ambiguous failures. Retry network errors, HTTP 429, HTTP 5xx,
|
|
141
|
+
and workspace_busy with bounded backoff. Do not retry other 4xx errors unless next_actions directs
|
|
142
|
+
recovery.
|
|
143
|
+
|
|
144
|
+
Recover and retrieve results:
|
|
145
|
+
\`\`\`bash
|
|
146
|
+
sanbox runs list --limit 50 --json
|
|
129
147
|
sanbox runs get <run-id> --json
|
|
130
|
-
sanbox runs
|
|
131
|
-
sanbox runs
|
|
132
|
-
sanbox runs
|
|
148
|
+
sanbox runs events <run-id> --after-event-id <cursor> --json
|
|
149
|
+
sanbox runs watch <run-id> --after-event-id <cursor> --jsonl
|
|
150
|
+
sanbox runs artifacts <run-id> --json
|
|
151
|
+
sanbox runs download <run-id> --output .sanbox/output/<run-id> --json
|
|
152
|
+
\`\`\`
|
|
153
|
+
|
|
154
|
+
Tasks must put durable files under /workspace/output. Downloads preserve relative paths and report
|
|
155
|
+
byte counts plus SHA-256 digests; existing files require explicit --overwrite.
|
|
156
|
+
|
|
157
|
+
Continue through the retained workspace:
|
|
158
|
+
\`\`\`bash
|
|
159
|
+
sanbox runs messages <run-id> --json
|
|
160
|
+
sanbox runs message <run-id> "Summarize the retained output" --wait --json
|
|
133
161
|
\`\`\`
|
|
134
162
|
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
Follow-up waits are correlated to the returned chat_job.id. Keep dependent follow-ups sequential.
|
|
164
|
+
For independent fan-out, use \`sanbox batch\` with a stable external_run_id per task and keep the
|
|
165
|
+
client alive until submission completes.
|
|
166
|
+
|
|
167
|
+
Do not claim completion until the run is completed, required artifacts are downloaded and verified,
|
|
168
|
+
and required follow-ups succeeded. Report run/external/template IDs, status, artifact paths/digests,
|
|
169
|
+
and blockers. The CLI excludes common secrets by default; add .sanboxignore for project rules.
|
|
137
170
|
`;
|
|
138
171
|
const cwd = () => process.cwd();
|
|
139
172
|
const makeClient = (flags) => new SanboxClient(readConfig(flags));
|
|
173
|
+
const makeAuthClient = (flags) => new SanboxClient(readConfig(flags, { requireOrg: false }));
|
|
140
174
|
const jsonContext = (client) => ({
|
|
141
175
|
api_url: client.config.apiUrl,
|
|
142
176
|
org: client.config.org
|
|
@@ -239,7 +273,149 @@ const formatBytes = (bytes) => {
|
|
|
239
273
|
};
|
|
240
274
|
const positionalText = (command, startIndex) => command.slice(startIndex).join(" ").trim();
|
|
241
275
|
const runTask = (command, flags) => flagString(flags, "task") || positionalText(command, 1);
|
|
242
|
-
const
|
|
276
|
+
const commonFlags = ["api-url", "org", "json", "help"];
|
|
277
|
+
const flagSets = {
|
|
278
|
+
"auth.check": commonFlags,
|
|
279
|
+
"orgs.list": commonFlags,
|
|
280
|
+
context: [...commonFlags, "template"],
|
|
281
|
+
doctor: [...commonFlags, "template"],
|
|
282
|
+
"model-providers.list": commonFlags,
|
|
283
|
+
"model-providers.get": commonFlags,
|
|
284
|
+
"model-providers.models": commonFlags,
|
|
285
|
+
"templates.list": commonFlags,
|
|
286
|
+
"templates.get": commonFlags,
|
|
287
|
+
"templates.validate": commonFlags,
|
|
288
|
+
"templates.create": [...commonFlags, "name", "model-provider", "model", "llm-budget-usd", "web-access"],
|
|
289
|
+
run: [
|
|
290
|
+
...commonFlags, "task", "input", "template", "external-run-id", "retention-ttl-seconds",
|
|
291
|
+
"dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "cancel-on-interrupt",
|
|
292
|
+
"poll-interval-ms", "event-page-size", "timeout-seconds", "verbose"
|
|
293
|
+
],
|
|
294
|
+
batch: [
|
|
295
|
+
...commonFlags, "tasks", "template", "input", "max-parallel", "wait", "batch-id",
|
|
296
|
+
"retention-ttl-seconds", "poll-interval-ms", "timeout-seconds"
|
|
297
|
+
],
|
|
298
|
+
"runs.list": [...commonFlags, "limit"],
|
|
299
|
+
"runs.get": commonFlags,
|
|
300
|
+
"runs.events": [...commonFlags, "after-event-id"],
|
|
301
|
+
"runs.messages": commonFlags,
|
|
302
|
+
"runs.artifacts": commonFlags,
|
|
303
|
+
"runs.download": [...commonFlags, "output", "artifact", "overwrite"],
|
|
304
|
+
"runs.watch": [
|
|
305
|
+
...commonFlags, "after-event-id", "view", "jsonl", "cancel-on-interrupt",
|
|
306
|
+
"poll-interval-ms", "event-page-size", "timeout-seconds"
|
|
307
|
+
],
|
|
308
|
+
"runs.cancel": commonFlags,
|
|
309
|
+
"runs.message": [
|
|
310
|
+
...commonFlags, "message", "wait", "watch", "jsonl", "view",
|
|
311
|
+
"poll-interval-ms", "event-page-size", "timeout-seconds"
|
|
312
|
+
],
|
|
313
|
+
init: [...commonFlags, "force", "template"],
|
|
314
|
+
"init.agent": [...commonFlags, "write"],
|
|
315
|
+
version: ["json", "help"]
|
|
316
|
+
};
|
|
317
|
+
const commandKey = (command) => {
|
|
318
|
+
if (command.length === 0)
|
|
319
|
+
return "help";
|
|
320
|
+
if (command[0] === "version")
|
|
321
|
+
return "version";
|
|
322
|
+
if (command[0] === "auth")
|
|
323
|
+
return `auth.${command[1] || ""}`;
|
|
324
|
+
if (command[0] === "orgs")
|
|
325
|
+
return `orgs.${command[1] || ""}`;
|
|
326
|
+
if (command[0] === "model-providers" || command[0] === "templates" || command[0] === "runs") {
|
|
327
|
+
return `${command[0]}.${command[1] || ""}`;
|
|
328
|
+
}
|
|
329
|
+
if (command[0] === "init" && command[1] === "agent")
|
|
330
|
+
return "init.agent";
|
|
331
|
+
return command[0];
|
|
332
|
+
};
|
|
333
|
+
const requiredValueFlags = new Set([
|
|
334
|
+
"api-url", "org", "template", "task", "input", "include", "external-run-id",
|
|
335
|
+
"retention-ttl-seconds", "tasks", "max-parallel", "batch-id", "poll-interval-ms",
|
|
336
|
+
"event-page-size", "timeout-seconds", "view", "after-event-id", "limit", "name",
|
|
337
|
+
"model-provider", "model", "llm-budget-usd", "message", "output", "artifact"
|
|
338
|
+
]);
|
|
339
|
+
const validateFlagValues = (flags) => {
|
|
340
|
+
for (const flag of requiredValueFlags) {
|
|
341
|
+
const value = flags[flag];
|
|
342
|
+
const missing = value === true
|
|
343
|
+
|| (typeof value === "string" && value.trim() === "")
|
|
344
|
+
|| (Array.isArray(value) && value.some((item) => item.trim() === ""));
|
|
345
|
+
if (missing)
|
|
346
|
+
throw new CliError("flag_value_required", `--${flag} requires a value.`);
|
|
347
|
+
}
|
|
348
|
+
for (const flag of booleanFlags) {
|
|
349
|
+
if (flags[flag] !== undefined && flags[flag] !== true) {
|
|
350
|
+
throw new CliError("unexpected_flag_value", `--${flag} does not accept a value.`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const normalizeInputAlias = (command, flags) => {
|
|
355
|
+
if (command[0] !== "run" && command[0] !== "batch")
|
|
356
|
+
return;
|
|
357
|
+
const legacyInputs = flagList(flags, "include");
|
|
358
|
+
if (legacyInputs.length === 0)
|
|
359
|
+
return;
|
|
360
|
+
flags.input = [...flagList(flags, "input"), ...legacyInputs];
|
|
361
|
+
delete flags.include;
|
|
362
|
+
if (!hasFlag(flags, "json") && !hasFlag(flags, "jsonl")) {
|
|
363
|
+
process.stderr.write("Warning: --include is deprecated; use --input.\n");
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const validateFlags = (command, flags) => {
|
|
367
|
+
const key = commandKey(command);
|
|
368
|
+
if (key === "help") {
|
|
369
|
+
const unknown = Object.keys(flags).filter((flag) => !["help", "version", "json"].includes(flag));
|
|
370
|
+
if (unknown.length > 0)
|
|
371
|
+
throw new CliError("unknown_flag", `Unknown flag: --${unknown[0]}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const knownRoots = new Set([
|
|
375
|
+
"auth", "orgs", "context", "doctor", "model-providers", "templates",
|
|
376
|
+
"run", "batch", "runs", "init", "version"
|
|
377
|
+
]);
|
|
378
|
+
const allowed = flagSets[key] ?? (knownRoots.has(command[0] || "") ? commonFlags : null);
|
|
379
|
+
if (!allowed)
|
|
380
|
+
return;
|
|
381
|
+
const unknown = Object.keys(flags).filter((flag) => !allowed.includes(flag));
|
|
382
|
+
if (unknown.length > 0) {
|
|
383
|
+
throw new CliError("unknown_flag", `Unknown flag for ${command.join(" ")}: --${unknown[0]}`);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const validatePositionals = (command, flags) => {
|
|
387
|
+
if (hasFlag(flags, "help"))
|
|
388
|
+
return;
|
|
389
|
+
const maximums = {
|
|
390
|
+
"auth.check": 2,
|
|
391
|
+
"orgs.list": 2,
|
|
392
|
+
context: 1,
|
|
393
|
+
doctor: 1,
|
|
394
|
+
"model-providers.list": 2,
|
|
395
|
+
"model-providers.get": 3,
|
|
396
|
+
"model-providers.models": 3,
|
|
397
|
+
"templates.list": 2,
|
|
398
|
+
"templates.get": 3,
|
|
399
|
+
"templates.validate": 3,
|
|
400
|
+
"templates.create": 2,
|
|
401
|
+
batch: 1,
|
|
402
|
+
"runs.list": 2,
|
|
403
|
+
"runs.get": 3,
|
|
404
|
+
"runs.events": 3,
|
|
405
|
+
"runs.messages": 3,
|
|
406
|
+
"runs.artifacts": 3,
|
|
407
|
+
"runs.download": 3,
|
|
408
|
+
"runs.watch": 3,
|
|
409
|
+
"runs.cancel": 3,
|
|
410
|
+
init: 1,
|
|
411
|
+
"init.agent": 2,
|
|
412
|
+
version: 1
|
|
413
|
+
};
|
|
414
|
+
const maximum = maximums[commandKey(command)];
|
|
415
|
+
if (maximum !== undefined && command.length > maximum) {
|
|
416
|
+
throw new CliError("unexpected_argument", `Unexpected positional argument: ${command[maximum]}`);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
243
419
|
const wantsWatch = (flags) => hasFlag(flags, "watch") || hasFlag(flags, "jsonl");
|
|
244
420
|
const integerFlag = (flags, key, fallback, minimum, maximum) => {
|
|
245
421
|
if (flags[key] === undefined)
|
|
@@ -328,23 +504,20 @@ const writeIfNeeded = async (filePath, content, force) => {
|
|
|
328
504
|
}
|
|
329
505
|
};
|
|
330
506
|
const commandRun = async (command, flags) => {
|
|
507
|
+
if (flagString(flags, "task") && positionalText(command, 1)) {
|
|
508
|
+
throw new CliError("conflicting_arguments", "Use either --task or a positional task, not both.");
|
|
509
|
+
}
|
|
331
510
|
const task = runTask(command, flags);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
511
|
+
if (!task)
|
|
512
|
+
throw new Error("--task or a positional task is required.");
|
|
513
|
+
if (hasFlag(flags, "wait") && wantsWatch(flags)) {
|
|
514
|
+
throw new Error("--wait cannot be combined with --watch or --jsonl.");
|
|
515
|
+
}
|
|
335
516
|
validateWatchFlags(flags);
|
|
336
517
|
if (hasFlag(flags, "dry-run")) {
|
|
337
518
|
if (wantsWatch(flags))
|
|
338
519
|
throw new Error("--dry-run cannot be combined with --watch or --jsonl.");
|
|
339
|
-
|
|
340
|
-
const inspection = await inspectDossierFile(path.resolve(cwd(), dossierPath));
|
|
341
|
-
if (hasFlag(flags, "json"))
|
|
342
|
-
printSuccess("run.preview", inspection, localJsonContext(flags));
|
|
343
|
-
else
|
|
344
|
-
process.stdout.write(`Bundle has ${inspection.entries.length} entries\n`);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const preview = await previewTaskDossier({ cwd: cwd(), include: flagList(flags, "include") });
|
|
520
|
+
const preview = await previewInputs({ cwd: cwd(), inputs: flagList(flags, "input") });
|
|
348
521
|
if (hasFlag(flags, "json"))
|
|
349
522
|
printSuccess("run.preview", preview, localJsonContext(flags));
|
|
350
523
|
else
|
|
@@ -357,13 +530,11 @@ const commandRun = async (command, flags) => {
|
|
|
357
530
|
try {
|
|
358
531
|
payload = await createRun(client, {
|
|
359
532
|
cwd: cwd(),
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
dossierPath,
|
|
363
|
-
include: flagList(flags, "include"),
|
|
533
|
+
instruction: task,
|
|
534
|
+
inputs: flagList(flags, "input"),
|
|
364
535
|
externalRunId: flagString(flags, "external-run-id") || undefined,
|
|
365
536
|
templateId: template.id,
|
|
366
|
-
retentionTtlSeconds:
|
|
537
|
+
retentionTtlSeconds: integerFlag(flags, "retention-ttl-seconds", 86400, 0, Number.MAX_SAFE_INTEGER)
|
|
367
538
|
});
|
|
368
539
|
}
|
|
369
540
|
catch (error) {
|
|
@@ -382,8 +553,8 @@ const commandRun = async (command, flags) => {
|
|
|
382
553
|
}
|
|
383
554
|
if (hasFlag(flags, "wait")) {
|
|
384
555
|
payload = await waitForRun(client, payload.run.id, {
|
|
385
|
-
pollIntervalMs:
|
|
386
|
-
timeoutSeconds:
|
|
556
|
+
pollIntervalMs: integerFlag(flags, "poll-interval-ms", 2000, 250, 60_000),
|
|
557
|
+
timeoutSeconds: integerFlag(flags, "timeout-seconds", 1800, 1, 604_800)
|
|
387
558
|
});
|
|
388
559
|
}
|
|
389
560
|
if (hasFlag(flags, "json")) {
|
|
@@ -405,20 +576,19 @@ const commandBatch = async (flags) => {
|
|
|
405
576
|
const template = readTemplateSelection(flags);
|
|
406
577
|
const tasks = await readTasks(tasksPath);
|
|
407
578
|
const batchId = flagString(flags, "batch-id") || await stableBatchId(tasksPath);
|
|
408
|
-
const
|
|
409
|
-
const maxParallel =
|
|
579
|
+
const inputs = flagList(flags, "input");
|
|
580
|
+
const maxParallel = integerFlag(flags, "max-parallel", 5, 1, 100);
|
|
410
581
|
const wait = hasFlag(flags, "wait");
|
|
411
582
|
const results = await runPool(tasks, maxParallel, async (task, index) => {
|
|
412
583
|
let payload;
|
|
413
584
|
try {
|
|
414
585
|
payload = await createRun(client, {
|
|
415
586
|
cwd: cwd(),
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
include: task.include || include,
|
|
587
|
+
instruction: task.task,
|
|
588
|
+
inputs: task.input || inputs,
|
|
419
589
|
externalRunId: task.external_run_id || `sanbox-batch-${batchId}-${index + 1}`,
|
|
420
590
|
templateId: template.id,
|
|
421
|
-
retentionTtlSeconds:
|
|
591
|
+
retentionTtlSeconds: integerFlag(flags, "retention-ttl-seconds", 86400, 0, Number.MAX_SAFE_INTEGER)
|
|
422
592
|
});
|
|
423
593
|
}
|
|
424
594
|
catch (error) {
|
|
@@ -426,8 +596,8 @@ const commandBatch = async (flags) => {
|
|
|
426
596
|
}
|
|
427
597
|
if (wait) {
|
|
428
598
|
payload = await waitForRun(client, payload.run.id, {
|
|
429
|
-
pollIntervalMs:
|
|
430
|
-
timeoutSeconds:
|
|
599
|
+
pollIntervalMs: integerFlag(flags, "poll-interval-ms", 2000, 250, 60_000),
|
|
600
|
+
timeoutSeconds: integerFlag(flags, "timeout-seconds", 1800, 1, 604_800)
|
|
431
601
|
});
|
|
432
602
|
}
|
|
433
603
|
return payload;
|
|
@@ -461,6 +631,19 @@ const commandAuthCheck = async (flags) => {
|
|
|
461
631
|
if (!selected)
|
|
462
632
|
process.exitCode = 2;
|
|
463
633
|
};
|
|
634
|
+
const commandOrganizations = async (command, flags) => {
|
|
635
|
+
if (command[1] !== "list")
|
|
636
|
+
throw new CliError("orgs_action_required", "orgs requires the list action.");
|
|
637
|
+
const client = makeAuthClient(flags);
|
|
638
|
+
const payload = await client.listOrganizations();
|
|
639
|
+
if (hasFlag(flags, "json")) {
|
|
640
|
+
printSuccess("orgs.list", payload, { api_url: client.config.apiUrl });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
for (const organization of payload.organizations) {
|
|
644
|
+
process.stdout.write(`${organization.slug}${organization.name ? `\t${organization.name}` : ""}${organization.membership_role ? `\t${organization.membership_role}` : ""}\n`);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
464
647
|
const commandContext = async (flags) => {
|
|
465
648
|
const client = makeClient(flags);
|
|
466
649
|
const selection = readTemplateSelection(flags, { required: false });
|
|
@@ -652,13 +835,23 @@ const commandTemplates = async (command, flags) => {
|
|
|
652
835
|
const name = requiredPositional(flagString(flags, "name"), "template_name_required", "templates create requires --name.");
|
|
653
836
|
const modelProvider = requiredPositional(flagString(flags, "model-provider"), "model_provider_required", "templates create requires --model-provider.");
|
|
654
837
|
const model = requiredPositional(flagString(flags, "model"), "model_required", "templates create requires --model.");
|
|
838
|
+
const budgetRaw = flagString(flags, "llm-budget-usd");
|
|
839
|
+
const parsedBudgetUsd = budgetRaw ? Number(budgetRaw) : undefined;
|
|
840
|
+
const llmBudgetUsd = parsedBudgetUsd === undefined
|
|
841
|
+
? undefined
|
|
842
|
+
: Math.round(parsedBudgetUsd * 1_000_000) / 1_000_000;
|
|
843
|
+
if (budgetRaw &&
|
|
844
|
+
(!Number.isFinite(parsedBudgetUsd) || llmBudgetUsd <= 0 || llmBudgetUsd > 100_000)) {
|
|
845
|
+
throw new CliError("invalid_llm_budget", "templates create --llm-budget-usd must be at least 0.000001 and no more than 100000.");
|
|
846
|
+
}
|
|
655
847
|
let payload;
|
|
656
848
|
try {
|
|
657
849
|
payload = await client.createTemplate({
|
|
658
850
|
name,
|
|
659
851
|
provider_id: modelProvider,
|
|
660
852
|
model_id: model,
|
|
661
|
-
web_access: hasFlag(flags, "web-access")
|
|
853
|
+
web_access: hasFlag(flags, "web-access"),
|
|
854
|
+
...(llmBudgetUsd === undefined ? {} : { llm_budget_usd: llmBudgetUsd })
|
|
662
855
|
});
|
|
663
856
|
}
|
|
664
857
|
catch (error) {
|
|
@@ -671,7 +864,8 @@ const commandTemplates = async (command, flags) => {
|
|
|
671
864
|
]);
|
|
672
865
|
return;
|
|
673
866
|
}
|
|
674
|
-
process.stdout.write(`created ${payload.template.id} provider=${payload.template.provider_id || modelProvider} model=${payload.template.model_id || model}
|
|
867
|
+
process.stdout.write(`created ${payload.template.id} provider=${payload.template.provider_id || modelProvider} model=${payload.template.model_id || model}` +
|
|
868
|
+
`${payload.template.llm_budget_usd ? ` budget=$${payload.template.llm_budget_usd}/run` : ""}\n`);
|
|
675
869
|
return;
|
|
676
870
|
}
|
|
677
871
|
throw new CliError("templates_action_required", "templates requires an action: list, get, validate, or create.");
|
|
@@ -679,9 +873,21 @@ const commandTemplates = async (command, flags) => {
|
|
|
679
873
|
const commandRuns = async (command, flags) => {
|
|
680
874
|
const client = makeClient(flags);
|
|
681
875
|
const action = command[1];
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
876
|
+
if (!action)
|
|
877
|
+
throw new Error("runs command requires an action.");
|
|
878
|
+
if (action === "list") {
|
|
879
|
+
const limit = integerFlag(flags, "limit", 50, 1, 200);
|
|
880
|
+
const payload = await client.listRuns(limit);
|
|
881
|
+
if (hasFlag(flags, "json")) {
|
|
882
|
+
printSuccess("runs.list", { ...payload, runs: payload.runs.map(publicRun) }, jsonContext(client));
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
for (const run of payload.runs) {
|
|
886
|
+
process.stdout.write(`${run.id}\t${run.status}\t${run.created_at}\t${run.instruction.replace(/\s+/g, " ").slice(0, 100)}\n`);
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const runId = requiredPositional(command[2], "run_id_required", `runs ${action} requires a run id.`);
|
|
685
891
|
if (action === "get") {
|
|
686
892
|
const payload = await client.getRun(runId);
|
|
687
893
|
if (hasFlag(flags, "json"))
|
|
@@ -691,13 +897,82 @@ const commandRuns = async (command, flags) => {
|
|
|
691
897
|
return;
|
|
692
898
|
}
|
|
693
899
|
if (action === "events") {
|
|
694
|
-
const payload = await client.listEvents(runId,
|
|
900
|
+
const payload = await client.listEvents(runId, integerFlag(flags, "after-event-id", 0, 0, Number.MAX_SAFE_INTEGER));
|
|
695
901
|
if (hasFlag(flags, "json"))
|
|
696
902
|
printSuccess("runs.events", payload, jsonContext(client));
|
|
697
903
|
else
|
|
698
904
|
payload.events.forEach((event) => process.stdout.write(`${event.id} ${event.level} ${event.kind} ${event.message}\n`));
|
|
699
905
|
return;
|
|
700
906
|
}
|
|
907
|
+
if (action === "messages") {
|
|
908
|
+
const payload = await client.listMessages(runId);
|
|
909
|
+
if (hasFlag(flags, "json")) {
|
|
910
|
+
printSuccess("runs.messages", payload, jsonContext(client));
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
for (const message of payload.messages) {
|
|
914
|
+
process.stdout.write(`${message.role}\t${message.created_at}\t${message.message}\n`);
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (action === "artifacts") {
|
|
919
|
+
const payload = await client.listArtifacts(runId);
|
|
920
|
+
if (hasFlag(flags, "json")) {
|
|
921
|
+
printSuccess("runs.artifacts", payload, jsonContext(client));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
for (const artifact of payload.artifacts) {
|
|
925
|
+
process.stdout.write(`${artifact.path}\t${artifact.size_bytes}\n`);
|
|
926
|
+
}
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (action === "download") {
|
|
930
|
+
const outputDirectory = requiredPositional(flagString(flags, "output"), "output_directory_required", "runs download requires --output.");
|
|
931
|
+
const payload = await client.listArtifacts(runId);
|
|
932
|
+
const requested = flagList(flags, "artifact");
|
|
933
|
+
const selected = requested.length === 0
|
|
934
|
+
? payload.artifacts
|
|
935
|
+
: requested.map((artifactPath) => {
|
|
936
|
+
const artifact = payload.artifacts.find((item) => item.path === artifactPath);
|
|
937
|
+
if (!artifact) {
|
|
938
|
+
throw new CliError("artifact_not_found", `Run ${runId} has no artifact named ${artifactPath}.`, {
|
|
939
|
+
details: { run_id: runId, artifact_path: artifactPath },
|
|
940
|
+
nextActions: [commandAction(["sanbox", "runs", "artifacts", runId, "--json"], "List exact artifact paths for this run.")]
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
return artifact;
|
|
944
|
+
});
|
|
945
|
+
let downloads;
|
|
946
|
+
try {
|
|
947
|
+
downloads = await downloadArtifacts(client, runId, selected, outputDirectory, hasFlag(flags, "overwrite"));
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
if (error instanceof ArtifactExistsError) {
|
|
951
|
+
throw new CliError("artifact_exists", error.message, {
|
|
952
|
+
details: {
|
|
953
|
+
run_id: runId,
|
|
954
|
+
artifact_path: error.artifactPath,
|
|
955
|
+
local_path: error.destination
|
|
956
|
+
},
|
|
957
|
+
nextActions: [commandAction(["sanbox", "runs", "download", runId, "--output", "<new-output-directory>", "--json"], "Download into a new directory without replacing an existing file.")]
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
const result = {
|
|
963
|
+
run_id: runId,
|
|
964
|
+
output_directory: path.resolve(outputDirectory),
|
|
965
|
+
downloads
|
|
966
|
+
};
|
|
967
|
+
if (hasFlag(flags, "json")) {
|
|
968
|
+
printSuccess("runs.download", result, jsonContext(client));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
for (const download of downloads) {
|
|
972
|
+
process.stdout.write(`${download.path}\t${download.local_path}\t${download.size_bytes}\t${download.sha256}\n`);
|
|
973
|
+
}
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
701
976
|
if (action === "watch") {
|
|
702
977
|
validateWatchFlags({ ...flags, watch: true });
|
|
703
978
|
const payload = await watchRunWithOutput(client, runId, { ...flags, watch: true });
|
|
@@ -718,14 +993,102 @@ const commandRuns = async (command, flags) => {
|
|
|
718
993
|
return;
|
|
719
994
|
}
|
|
720
995
|
if (action === "message") {
|
|
721
|
-
const
|
|
996
|
+
const flaggedMessage = flagString(flags, "message");
|
|
997
|
+
const positionalMessage = positionalText(command, 3);
|
|
998
|
+
if (flaggedMessage && positionalMessage) {
|
|
999
|
+
throw new CliError("conflicting_arguments", "Use either --message or a positional message, not both.");
|
|
1000
|
+
}
|
|
1001
|
+
const message = flaggedMessage || positionalMessage;
|
|
722
1002
|
if (!message)
|
|
723
|
-
throw new Error("--message is required.");
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1003
|
+
throw new Error("--message or a positional message is required.");
|
|
1004
|
+
if (wantsWatch(flags) && hasFlag(flags, "json")) {
|
|
1005
|
+
throw new Error("--json cannot be combined with --watch or --jsonl.");
|
|
1006
|
+
}
|
|
1007
|
+
if (hasFlag(flags, "wait") && wantsWatch(flags)) {
|
|
1008
|
+
throw new Error("--wait cannot be combined with --watch or --jsonl.");
|
|
1009
|
+
}
|
|
1010
|
+
const submitted = await client.sendMessage(runId, message);
|
|
1011
|
+
if (!hasFlag(flags, "wait") && !wantsWatch(flags)) {
|
|
1012
|
+
if (hasFlag(flags, "json")) {
|
|
1013
|
+
printSuccess("runs.message", {
|
|
1014
|
+
...publicRunPayload(submitted),
|
|
1015
|
+
message: submitted.message,
|
|
1016
|
+
chat_job: submitted.chat_job
|
|
1017
|
+
}, jsonContext(client));
|
|
1018
|
+
}
|
|
1019
|
+
else
|
|
1020
|
+
process.stdout.write(`queued follow-up for run ${runId}\n`);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const chatJobId = submitted.chat_job.id;
|
|
1024
|
+
const belongsToFollowup = (event) => event.payload.chat_job_id === chatJobId;
|
|
1025
|
+
const queuedEventId = submitted.events.reduce((latest, event) => event.kind === "chat.queued" && belongsToFollowup(event) ? Math.max(latest, event.id) : latest, 0);
|
|
1026
|
+
const receivedEventId = [...submitted.events]
|
|
1027
|
+
.reverse()
|
|
1028
|
+
.find((event) => event.kind === "message.received" && belongsToFollowup(event))?.id ?? queuedEventId;
|
|
1029
|
+
const cursor = submitted.events.reduce((latest, event) => Math.max(latest, event.id), 0);
|
|
1030
|
+
const completedInResponse = [...submitted.events]
|
|
1031
|
+
.reverse()
|
|
1032
|
+
.find((event) => (event.kind === "chat.completed" || event.kind === "chat.failed") && belongsToFollowup(event));
|
|
1033
|
+
const view = parseActivityView(flagString(flags, "view", "activity"));
|
|
1034
|
+
const jsonl = hasFlag(flags, "jsonl");
|
|
1035
|
+
const controller = new AbortController();
|
|
1036
|
+
const onInterrupt = () => controller.abort();
|
|
1037
|
+
process.once("SIGINT", onInterrupt);
|
|
1038
|
+
if (wantsWatch(flags) && !jsonl)
|
|
1039
|
+
process.stdout.write(`Watching follow-up for run ${runId}. Ctrl-C detaches.\n`);
|
|
1040
|
+
try {
|
|
1041
|
+
const renderFollowupEvent = (event) => {
|
|
1042
|
+
if (!belongsToFollowup(event))
|
|
1043
|
+
return;
|
|
1044
|
+
if (!wantsWatch(flags) || !shouldRenderEvent(event, view))
|
|
1045
|
+
return;
|
|
1046
|
+
process.stdout.write(`${jsonl ? formatActivityJsonl(event) : formatActivityLine(event, submitted.run.created_at)}\n`);
|
|
1047
|
+
};
|
|
1048
|
+
for (const event of submitted.events) {
|
|
1049
|
+
if (event.id >= receivedEventId)
|
|
1050
|
+
renderFollowupEvent(event);
|
|
1051
|
+
}
|
|
1052
|
+
const terminalEvent = completedInResponse ?? await watchEventsUntil(client, runId, {
|
|
1053
|
+
afterEventId: cursor,
|
|
1054
|
+
pageSize: integerFlag(flags, "event-page-size", 200, 1, 500),
|
|
1055
|
+
pollIntervalMs: integerFlag(flags, "poll-interval-ms", 2000, 250, 60_000),
|
|
1056
|
+
timeoutSeconds: integerFlag(flags, "timeout-seconds", 1800, 1, 604_800),
|
|
1057
|
+
signal: controller.signal,
|
|
1058
|
+
stopWhen: (event) => (event.kind === "chat.completed" || event.kind === "chat.failed") && belongsToFollowup(event),
|
|
1059
|
+
onRetry: ({ error, attempt, delayMs }) => {
|
|
1060
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1061
|
+
process.stderr.write(`Follow-up connection lost (${detail}); retry ${attempt} in ${delayMs}ms.\n`);
|
|
1062
|
+
},
|
|
1063
|
+
onEvent: renderFollowupEvent
|
|
1064
|
+
});
|
|
1065
|
+
const messages = await client.listMessages(runId);
|
|
1066
|
+
if (hasFlag(flags, "json")) {
|
|
1067
|
+
printSuccess("runs.message", {
|
|
1068
|
+
...publicRunPayload(submitted),
|
|
1069
|
+
message: submitted.message,
|
|
1070
|
+
chat_job: submitted.chat_job,
|
|
1071
|
+
messages: messages.messages,
|
|
1072
|
+
followup_event: terminalEvent
|
|
1073
|
+
}, jsonContext(client));
|
|
1074
|
+
}
|
|
1075
|
+
else if (!jsonl) {
|
|
1076
|
+
const assistant = [...messages.messages].reverse().find((item) => item.role === "assistant" && item.payload.chat_job_id === chatJobId);
|
|
1077
|
+
if (assistant)
|
|
1078
|
+
process.stdout.write(`${assistant.message}\n`);
|
|
1079
|
+
}
|
|
1080
|
+
if (terminalEvent.kind === "chat.failed")
|
|
1081
|
+
process.exitCode = 2;
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
if (!(error instanceof WatchInterruptedError))
|
|
1085
|
+
throw error;
|
|
1086
|
+
process.stderr.write(`Detached from follow-up for run ${runId}; processing continues.\n`);
|
|
1087
|
+
process.exitCode = 130;
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
process.off("SIGINT", onInterrupt);
|
|
1091
|
+
}
|
|
729
1092
|
return;
|
|
730
1093
|
}
|
|
731
1094
|
throw new Error(`Unknown runs action: ${action}`);
|
|
@@ -808,11 +1171,11 @@ const commandDoctor = async (flags) => {
|
|
|
808
1171
|
}
|
|
809
1172
|
}
|
|
810
1173
|
try {
|
|
811
|
-
const preview = await
|
|
812
|
-
add("
|
|
1174
|
+
const preview = await previewInputs({ cwd: cwd(), inputs: ["README.md"] });
|
|
1175
|
+
add("input_preview", true, `${preview.files.length} file(s), ${formatBytes(preview.totalBytes)}`);
|
|
813
1176
|
}
|
|
814
1177
|
catch (error) {
|
|
815
|
-
add("
|
|
1178
|
+
add("input_preview", false, error instanceof Error ? error.message : String(error));
|
|
816
1179
|
}
|
|
817
1180
|
const output = {
|
|
818
1181
|
ok: checks.every((check) => check.ok),
|
|
@@ -830,55 +1193,6 @@ const commandDoctor = async (flags) => {
|
|
|
830
1193
|
if (!output.ok)
|
|
831
1194
|
process.exitCode = 2;
|
|
832
1195
|
};
|
|
833
|
-
const commandBundle = async (command, flags) => {
|
|
834
|
-
const action = command[1];
|
|
835
|
-
if (action === "preview") {
|
|
836
|
-
const preview = await previewTaskDossier({ cwd: cwd(), include: flagList(flags, "include") });
|
|
837
|
-
if (hasFlag(flags, "json"))
|
|
838
|
-
printSuccess("bundle.preview", preview, localJsonContext(flags));
|
|
839
|
-
else
|
|
840
|
-
printPreview(preview, hasFlag(flags, "verbose"));
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
if (action === "create") {
|
|
844
|
-
const task = bundleTask(command, flags);
|
|
845
|
-
const outPath = flagString(flags, "out");
|
|
846
|
-
if (!task)
|
|
847
|
-
throw new Error("bundle create requires a positional task or --task.");
|
|
848
|
-
if (!outPath)
|
|
849
|
-
throw new Error("bundle create requires --out.");
|
|
850
|
-
const dossier = await writeTaskDossier({
|
|
851
|
-
cwd: cwd(),
|
|
852
|
-
task,
|
|
853
|
-
include: flagList(flags, "include"),
|
|
854
|
-
cliVersion: version,
|
|
855
|
-
outPath: path.resolve(cwd(), outPath)
|
|
856
|
-
});
|
|
857
|
-
const output = { path: outPath, sha256: dossier.sha256, files: dossier.files, bytes: dossier.buffer.byteLength };
|
|
858
|
-
if (hasFlag(flags, "json"))
|
|
859
|
-
printSuccess("bundle.create", output, localJsonContext(flags));
|
|
860
|
-
else
|
|
861
|
-
process.stdout.write(`Wrote ${outPath} with ${dossier.files.length} files (${formatBytes(dossier.buffer.byteLength)})\n`);
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
if (action === "inspect") {
|
|
865
|
-
const bundlePath = command[2] || flagString(flags, "dossier") || flagString(flags, "bundle");
|
|
866
|
-
if (!bundlePath)
|
|
867
|
-
throw new Error("bundle inspect requires a ZIP path.");
|
|
868
|
-
const inspection = await inspectDossierFile(path.resolve(cwd(), bundlePath));
|
|
869
|
-
if (hasFlag(flags, "json")) {
|
|
870
|
-
printSuccess("bundle.inspect", inspection, localJsonContext(flags));
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
process.stdout.write(`Entries: ${inspection.entries.length}\n`);
|
|
874
|
-
if (inspection.manifest)
|
|
875
|
-
process.stdout.write("manifest.json: present\n");
|
|
876
|
-
if (inspection.runbook)
|
|
877
|
-
process.stdout.write(`RUNBOOK.md: ${inspection.runbook.split(/\r?\n/)[0] || "present"}\n`);
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
throw new Error("bundle requires action: preview, create, or inspect.");
|
|
881
|
-
};
|
|
882
1196
|
const commandInit = async (command, flags) => {
|
|
883
1197
|
const dir = path.join(cwd(), ".sanbox");
|
|
884
1198
|
if (command[1] === "agent") {
|
|
@@ -941,8 +1255,6 @@ const commandInit = async (command, flags) => {
|
|
|
941
1255
|
const helpFor = (command) => {
|
|
942
1256
|
if (command[0] === "run")
|
|
943
1257
|
return runHelp;
|
|
944
|
-
if (command[0] === "bundle")
|
|
945
|
-
return bundleHelp;
|
|
946
1258
|
if (command[0] === "doctor")
|
|
947
1259
|
return doctorHelp;
|
|
948
1260
|
if (command[0] === "model-providers")
|
|
@@ -952,8 +1264,12 @@ const helpFor = (command) => {
|
|
|
952
1264
|
return help;
|
|
953
1265
|
};
|
|
954
1266
|
const commandId = (command) => {
|
|
1267
|
+
if (command[0] === "version" || command.length === 0)
|
|
1268
|
+
return "version";
|
|
955
1269
|
if (command[0] === "auth")
|
|
956
1270
|
return "auth.check";
|
|
1271
|
+
if (command[0] === "orgs")
|
|
1272
|
+
return `orgs.${command[1] || "unknown"}`;
|
|
957
1273
|
if (command[0] === "context")
|
|
958
1274
|
return "context.get";
|
|
959
1275
|
if (command[0] === "model-providers")
|
|
@@ -964,13 +1280,22 @@ const commandId = (command) => {
|
|
|
964
1280
|
return "run.create";
|
|
965
1281
|
if (command[0] === "batch")
|
|
966
1282
|
return "batch.create";
|
|
967
|
-
if (command[0] === "bundle")
|
|
968
|
-
return `bundle.${command[1] || "unknown"}`;
|
|
969
1283
|
if (command[0] === "runs")
|
|
970
1284
|
return `runs.${command[1] || "unknown"}`;
|
|
971
1285
|
return command[0] || "help";
|
|
972
1286
|
};
|
|
973
1287
|
const main = async (command, flags) => {
|
|
1288
|
+
validateFlagValues(flags);
|
|
1289
|
+
normalizeInputAlias(command, flags);
|
|
1290
|
+
validateFlags(command, flags);
|
|
1291
|
+
validatePositionals(command, flags);
|
|
1292
|
+
if (command[0] === "version" || (command.length === 0 && hasFlag(flags, "version"))) {
|
|
1293
|
+
if (hasFlag(flags, "json"))
|
|
1294
|
+
printSuccess("version", { version });
|
|
1295
|
+
else
|
|
1296
|
+
process.stdout.write(`${version}\n`);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
974
1299
|
if (command.length === 0) {
|
|
975
1300
|
process.stdout.write(help);
|
|
976
1301
|
return;
|
|
@@ -981,6 +1306,8 @@ const main = async (command, flags) => {
|
|
|
981
1306
|
}
|
|
982
1307
|
if (command[0] === "auth" && command[1] === "check")
|
|
983
1308
|
return commandAuthCheck(flags);
|
|
1309
|
+
if (command[0] === "orgs")
|
|
1310
|
+
return commandOrganizations(command, flags);
|
|
984
1311
|
if (command[0] === "context")
|
|
985
1312
|
return commandContext(flags);
|
|
986
1313
|
if (command[0] === "doctor")
|
|
@@ -993,8 +1320,6 @@ const main = async (command, flags) => {
|
|
|
993
1320
|
return commandRun(command, flags);
|
|
994
1321
|
if (command[0] === "batch")
|
|
995
1322
|
return commandBatch(flags);
|
|
996
|
-
if (command[0] === "bundle")
|
|
997
|
-
return commandBundle(command, flags);
|
|
998
1323
|
if (command[0] === "runs")
|
|
999
1324
|
return commandRuns(command, flags);
|
|
1000
1325
|
if (command[0] === "init")
|