@sanlabs/sanbox-cli 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,28 +2,33 @@
2
2
 
3
3
  Run isolated Sanbox agent tasks from a terminal, CI job, or autonomous coding agent.
4
4
 
5
+ Sanbox packages a task and selected inputs, starts an isolated runner, streams events, and keeps the sandbox and outputs indefinitely.
6
+
5
7
  The full machine operating protocol is available at https://console.sanbox.cloud/agent.md and in [`web/public/agent.md`](../web/public/agent.md).
6
8
 
7
9
  ## Install
8
10
 
9
11
  ```bash
10
12
  npm install -g @sanlabs/sanbox-cli@latest
11
- sanbox --version
13
+ latest_cli_version="$(npm view @sanlabs/sanbox-cli version)"
14
+ installed_cli_version="$(sanbox --version)"
15
+ test "$installed_cli_version" = "$latest_cli_version"
12
16
  ```
13
17
 
14
- CLI 0.0.4 adds strict argument validation, stable JSON envelopes, existing-resource discovery, resumable watches, verified artifact downloads, and correlated post-run follow-ups.
18
+ Always use the latest published CLI. CLI 0.0.5 adds organization discovery from the API key,
19
+ Hermes-aware template metadata, permanent paused sandboxes, and same-sandbox follow-ups.
15
20
 
16
21
  ## Configure
17
22
 
18
23
  ```bash
19
24
  export SANBOX_API_URL=https://console.sanbox.cloud
20
25
  export SANBOX_API_KEY=sbx_live_...
21
- sanbox orgs list --json
22
- export SANBOX_ORG=<returned-org-slug>
23
26
  export SANBOX_TEMPLATE=<template-id-or-slug>
24
27
  ```
25
28
 
26
29
  `SANBOX_API_URL` is optional for the hosted service. `SANBOX_TEMPLATE` can instead come from `--template` or `.sanbox/config.json` `default_template`.
30
+ The CLI derives the organization automatically from `SANBOX_API_KEY`; use an organization-scoped
31
+ key that has access to exactly one organization.
27
32
 
28
33
  An org admin configures provider credentials and templates in the console. Do not pass provider keys to the CLI or a runner.
29
34
 
@@ -37,7 +42,9 @@ sanbox templates validate "$SANBOX_TEMPLATE" --json
37
42
  sanbox doctor --json
38
43
  ```
39
44
 
40
- Model IDs are provider-scoped. The CLI never guesses or silently substitutes an organization, provider, model, or template.
45
+ Model IDs are provider-scoped. The CLI never guesses or silently substitutes a provider, model, or template.
46
+ For waited task runs, choose a template with `runnable: true`, `template_type: "runner"`, and
47
+ `runner_config.harness: "opencode"`. Hermes service templates are always-on.
41
48
 
42
49
  ## Run Idempotently
43
50
 
@@ -93,6 +100,13 @@ sanbox runs message <run-id> "Summarize the retained output" --wait --json
93
100
 
94
101
  The JSON response includes the user `message`, submitted `chat_job`, matching terminal `followup_event`, and conversation messages. The waiter matches `payload.chat_job_id`, so another concurrent follow-up cannot complete the wrong command.
95
102
 
103
+ Completed runs pause into a durable Firecracker snapshot. `sanbox runs message <run-id> --message "..."`
104
+ resumes the same writable sandbox and OpenCode session, then pauses it again. There is no retention
105
+ TTL; paused sandboxes and their artifacts remain available until explicitly deleted.
106
+
107
+ Check `sanbox runs get <run-id> --json` before a follow-up and require `sandbox_state: "paused"` with
108
+ a positive `snapshot_generation`. Do not submit concurrent follow-ups to one run.
109
+
96
110
  ## Batch Work
97
111
 
98
112
  ```json
package/dist/api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { CliError } from "./errors.js";
1
2
  const errorRecord = (body) => {
2
3
  if (!body || typeof body !== "object" || Array.isArray(body))
3
4
  return null;
@@ -38,6 +39,9 @@ export class SanboxApiError extends Error {
38
39
  }
39
40
  export class SanboxClient {
40
41
  config;
42
+ mePromise = null;
43
+ organizationPromise = null;
44
+ organizationSlug = null;
41
45
  constructor(config) {
42
46
  this.config = config;
43
47
  }
@@ -80,22 +84,50 @@ export class SanboxClient {
80
84
  return res;
81
85
  }
82
86
  me() {
83
- return this.request("/v1/me");
87
+ this.mePromise ||= this.request("/v1/me");
88
+ return this.mePromise;
89
+ }
90
+ organization() {
91
+ this.organizationPromise ||= this.resolveOrganization();
92
+ return this.organizationPromise;
93
+ }
94
+ resolvedOrganizationSlug() {
95
+ return this.organizationSlug;
96
+ }
97
+ async resolveOrganization() {
98
+ const me = await this.me();
99
+ const organizations = Array.isArray(me.organizations)
100
+ ? me.organizations.filter((item) => Boolean(item)
101
+ && typeof item === "object"
102
+ && !Array.isArray(item)
103
+ && typeof item.id === "string"
104
+ && typeof item.slug === "string"
105
+ && Boolean(String(item.slug).trim()))
106
+ : [];
107
+ if (organizations.length === 0) {
108
+ throw new CliError("organization_not_found", "The API key is not associated with an organization.");
109
+ }
110
+ if (organizations.length > 1) {
111
+ throw new CliError("organization_ambiguous", "The API key can access multiple organizations. Use an organization-scoped API key.");
112
+ }
113
+ this.organizationSlug = organizations[0].slug;
114
+ return organizations[0];
84
115
  }
85
- listOrganizations() {
86
- return this.request("/v1/orgs");
116
+ async orgPath(path) {
117
+ const organization = await this.organization();
118
+ return `/v1/orgs/${encodeURIComponent(organization.slug)}${path}`;
87
119
  }
88
- listRuns(limit = 50) {
89
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs?limit=${limit}`);
120
+ async listRuns(limit = 50) {
121
+ return this.request(`${await this.orgPath("/runs")}?limit=${limit}`);
90
122
  }
91
- createRun(body) {
92
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs`, {
123
+ async createRun(body) {
124
+ return this.request(await this.orgPath("/runs"), {
93
125
  method: "POST",
94
126
  body: JSON.stringify(body)
95
127
  });
96
128
  }
97
- uploadInputCollection(input) {
98
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/input-collections`, {
129
+ async uploadInputCollection(input) {
130
+ return this.request(await this.orgPath("/input-collections"), {
99
131
  method: "POST",
100
132
  headers: {
101
133
  "Content-Type": "application/vnd.sanbox.input+zip",
@@ -105,8 +137,8 @@ export class SanboxClient {
105
137
  body: Uint8Array.from(input.buffer).buffer
106
138
  });
107
139
  }
108
- listModelProviders() {
109
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/model-providers`);
140
+ async listModelProviders() {
141
+ return this.request(await this.orgPath("/model-providers"));
110
142
  }
111
143
  async getModelProvider(providerId) {
112
144
  const payload = await this.listModelProviders();
@@ -115,49 +147,49 @@ export class SanboxClient {
115
147
  throw new SanboxApiError(`Model provider ${providerId} was not found.`, 404, { code: "model_provider_not_found" });
116
148
  return { provider };
117
149
  }
118
- listProviderModels(providerId) {
119
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/model-providers/${encodeURIComponent(providerId)}/models`);
150
+ async listProviderModels(providerId) {
151
+ return this.request(await this.orgPath(`/model-providers/${encodeURIComponent(providerId)}/models`));
120
152
  }
121
- listTemplates() {
122
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/templates`);
153
+ async listTemplates() {
154
+ return this.request(await this.orgPath("/templates"));
123
155
  }
124
- getTemplate(templateId) {
125
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/templates/${encodeURIComponent(templateId)}`);
156
+ async getTemplate(templateId) {
157
+ return this.request(await this.orgPath(`/templates/${encodeURIComponent(templateId)}`));
126
158
  }
127
- validateTemplate(templateId) {
128
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/templates/${encodeURIComponent(templateId)}/validate`);
159
+ async validateTemplate(templateId) {
160
+ return this.request(await this.orgPath(`/templates/${encodeURIComponent(templateId)}/validate`));
129
161
  }
130
- createTemplate(body) {
131
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/templates`, {
162
+ async createTemplate(body) {
163
+ return this.request(await this.orgPath("/templates"), {
132
164
  method: "POST",
133
165
  body: JSON.stringify(body)
134
166
  });
135
167
  }
136
- getRun(runId, signal) {
137
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs/${encodeURIComponent(runId)}`, { signal });
168
+ async getRun(runId, signal) {
169
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}`), { signal });
138
170
  }
139
- listEvents(runId, afterEventId = 0, limit = 200, signal) {
140
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs/${encodeURIComponent(runId)}/events?after_event_id=${afterEventId}&limit=${limit}`, { signal });
171
+ async listEvents(runId, afterEventId = 0, limit = 200, signal) {
172
+ return this.request(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/events`)}?after_event_id=${afterEventId}&limit=${limit}`, { signal });
141
173
  }
142
- cancelRun(runId) {
143
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs/${encodeURIComponent(runId)}/cancel`, {
174
+ async cancelRun(runId) {
175
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/cancel`), {
144
176
  method: "POST",
145
177
  body: "{}"
146
178
  });
147
179
  }
148
- sendMessage(runId, message, payload = {}) {
149
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs/${encodeURIComponent(runId)}/messages`, {
180
+ async sendMessage(runId, message, payload = {}) {
181
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/messages`), {
150
182
  method: "POST",
151
183
  body: JSON.stringify({ message, payload })
152
184
  });
153
185
  }
154
- listMessages(runId) {
155
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs/${encodeURIComponent(runId)}/messages`);
186
+ async listMessages(runId) {
187
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/messages`));
156
188
  }
157
- listArtifacts(runId) {
158
- return this.request(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs/${encodeURIComponent(runId)}/artifacts`);
189
+ async listArtifacts(runId) {
190
+ return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/artifacts`));
159
191
  }
160
- downloadArtifact(runId, artifactPath) {
161
- return this.rawRequest(`/v1/orgs/${encodeURIComponent(this.config.org)}/runs/${encodeURIComponent(runId)}/artifacts?path=${encodeURIComponent(artifactPath)}`);
192
+ async downloadArtifact(runId, artifactPath) {
193
+ return this.rawRequest(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/artifacts`)}?path=${encodeURIComponent(artifactPath)}`);
162
194
  }
163
195
  }
package/dist/args.js CHANGED
@@ -1,4 +1,4 @@
1
- const multiFlags = new Set(["input", "include", "artifact"]);
1
+ const multiFlags = new Set(["input", "include", "artifact", "telegram-allowed-user"]);
2
2
  export const booleanFlags = new Set([
3
3
  "help",
4
4
  "version",
package/dist/cli.js CHANGED
@@ -16,14 +16,12 @@ const help = `Sanbox CLI
16
16
 
17
17
  Environment:
18
18
  SANBOX_API_URL Sanbox API base URL
19
- SANBOX_ORG Organization slug
20
19
  SANBOX_API_KEY Org API key
21
20
  SANBOX_TEMPLATE Explicit template id or slug for run and batch
22
21
 
23
22
  Commands:
24
23
  sanbox --version
25
24
  sanbox auth check [--json]
26
- sanbox orgs list [--json]
27
25
  sanbox context [--json]
28
26
  sanbox doctor [--json]
29
27
  sanbox model-providers list [--json]
@@ -32,7 +30,7 @@ Commands:
32
30
  sanbox templates list [--json]
33
31
  sanbox templates get <template-id> [--json]
34
32
  sanbox templates validate <template-id> [--json]
35
- sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--llm-budget-usd <amount>] [--web-access] [--json]
33
+ sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes] [--llm-budget-usd <amount>] [--json]
36
34
  sanbox run "task" --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
37
35
  sanbox run --task "..." --template <template-id> [--input <path>] [--wait | --watch] [--json | --jsonl]
38
36
  sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
@@ -58,7 +56,6 @@ Options:
58
56
  --input <path> File, directory, or glob to upload. Repeatable.
59
57
  --template <id> Template id or slug. Required unless SANBOX_TEMPLATE or project config sets it.
60
58
  --external-run-id <id> Idempotency key for retries.
61
- --retention-ttl-seconds <n> Workspace retention TTL. Default: 86400.
62
59
  --dry-run Preview included files without creating a run.
63
60
  --wait Poll until terminal status.
64
61
  --watch Stream activity until terminal status.
@@ -73,7 +70,7 @@ const doctorHelp = `Sanbox doctor
73
70
  Usage:
74
71
  sanbox doctor [--json]
75
72
 
76
- Checks local configuration, API health, auth, org visibility, and template access.
73
+ Checks local configuration, API health, auth, organization discovery, and template access.
77
74
  SANBOX_API_KEY is still read only from the environment.
78
75
  `;
79
76
  const modelProvidersHelp = `Sanbox model providers
@@ -92,26 +89,34 @@ Usage:
92
89
  sanbox templates list [--json]
93
90
  sanbox templates get <template-id> [--json]
94
91
  sanbox templates validate <template-id> [--json]
95
- sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--llm-budget-usd <amount>] [--web-access] [--json]
92
+ sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes] [--llm-budget-usd <amount>] [--json]
96
93
 
97
94
  Template creation requires an exact provider id and that provider's exact model id.
98
95
  LiteLLM budgets are optional USD amounts and apply separately to each run.
96
+ Hermes templates are always-on Telegram gateways. Set SANBOX_TELEGRAM_BOT_TOKEN
97
+ and repeat --telegram-allowed-user <numeric-id> for every permitted user.
99
98
  `;
100
99
  const agentInstructions = `# Operate Sanbox Autonomously
101
100
 
102
101
  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.
102
+ protocol is https://console.sanbox.cloud/agent.md.
103
+
104
+ Always use the latest published CLI. Install and verify it at the start of every operating session:
105
+ \`\`\`bash
106
+ npm install -g @sanlabs/sanbox-cli@latest
107
+ latest_cli_version="$(npm view @sanlabs/sanbox-cli version)"
108
+ installed_cli_version="$(sanbox --version)"
109
+ test "$installed_cli_version" = "$latest_cli_version"
110
+ \`\`\`
104
111
 
105
112
  Human bootstrap:
106
- - A human/admin supplies SANBOX_API_KEY and configures provider credentials plus a runnable template.
113
+ - A human/admin supplies an organization-scoped SANBOX_API_KEY and configures provider credentials plus a runnable template.
107
114
  - Never print, persist, prompt with, or upload API keys or provider credentials.
108
115
  - If the key or a runnable template is unavailable, stop with the exact human action required.
109
116
 
110
117
  Deterministic startup:
111
118
  \`\`\`bash
112
119
  sanbox --version
113
- sanbox orgs list --json
114
- export SANBOX_ORG=<returned-org-slug>
115
120
  sanbox auth check --json
116
121
  sanbox context --json
117
122
  sanbox templates list --json
@@ -120,8 +125,11 @@ sanbox templates validate "$SANBOX_TEMPLATE" --json
120
125
  sanbox doctor --json
121
126
  \`\`\`
122
127
 
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
128
+ Never guess opaque IDs. The CLI derives the organization from the API key and requires the key to
129
+ resolve to exactly one organization. For task execution, select only a template with runnable: true,
130
+ template_type: "runner", and runner_config.harness: "opencode". Never select a Hermes service
131
+ template for a waited task because it is always-on. Select automatically only when exactly one task
132
+ template qualifies; otherwise ask the user. Provider credentials and template administration are
125
133
  console-only.
126
134
 
127
135
  Use --json for request/response commands and --jsonl for streams. Parse the versioned envelope:
@@ -139,7 +147,7 @@ sanbox run "Investigate one focused task and write output/report.md" \\
139
147
 
140
148
  Reuse the same --external-run-id after ambiguous failures. Retry network errors, HTTP 429, HTTP 5xx,
141
149
  and workspace_busy with bounded backoff. Do not retry other 4xx errors unless next_actions directs
142
- recovery.
150
+ recovery. Terminal statuses are completed, failed, and canceled.
143
151
 
144
152
  Recover and retrieve results:
145
153
  \`\`\`bash
@@ -154,33 +162,38 @@ sanbox runs download <run-id> --output .sanbox/output/<run-id> --json
154
162
  Tasks must put durable files under /workspace/output. Downloads preserve relative paths and report
155
163
  byte counts plus SHA-256 digests; existing files require explicit --overwrite.
156
164
 
157
- Continue through the retained workspace:
165
+ Continue through the same paused writable sandbox:
158
166
  \`\`\`bash
159
167
  sanbox runs messages <run-id> --json
168
+ sanbox runs get <run-id> --json
160
169
  sanbox runs message <run-id> "Summarize the retained output" --wait --json
161
170
  \`\`\`
162
171
 
163
- Follow-up waits are correlated to the returned chat_job.id. Keep dependent follow-ups sequential.
172
+ Before a follow-up, require sandbox_state: "paused" and a positive snapshot_generation. The message
173
+ resumes the same sandbox and OpenCode session, then pauses it as a new generation. Follow-up waits
174
+ are correlated to the returned chat_job.id. Never submit concurrent follow-ups to one run. Retry
175
+ sandbox_not_paused only after the run returns to paused; treat sandbox_not_resumable,
176
+ sandbox_snapshot_missing, sandbox_worker_missing, and hermes_uses_channel as blockers.
177
+
164
178
  For independent fan-out, use \`sanbox batch\` with a stable external_run_id per task and keep the
165
179
  client alive until submission completes.
166
180
 
167
181
  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.
182
+ and required follow-ups succeeded. Report run/external/template IDs, status, sandbox state, snapshot
183
+ generation, artifact paths/digests, and blockers. The CLI excludes common secrets by default; add
184
+ .sanboxignore for project rules.
170
185
  `;
171
186
  const cwd = () => process.cwd();
172
187
  const makeClient = (flags) => new SanboxClient(readConfig(flags));
173
- const makeAuthClient = (flags) => new SanboxClient(readConfig(flags, { requireOrg: false }));
174
188
  const jsonContext = (client) => ({
175
189
  api_url: client.config.apiUrl,
176
- org: client.config.org
190
+ ...(client.resolvedOrganizationSlug() ? { org: client.resolvedOrganizationSlug() } : {})
177
191
  });
178
192
  const localJsonContext = (flags) => {
179
193
  try {
180
194
  const local = readLocalConfig();
181
195
  const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || local.api_url || defaultApiUrl).replace(/\/+$/, "");
182
- const org = String(flags.org || process.env.SANBOX_ORG || local.org || "").trim();
183
- return { api_url: apiUrl, ...(org ? { org } : {}) };
196
+ return { api_url: apiUrl };
184
197
  }
185
198
  catch {
186
199
  return {};
@@ -191,7 +204,7 @@ const consoleUrl = (client) => consolePathUrl(client, "/model-providers");
191
204
  const providerConsoleAction = (client) => consoleAction(consoleUrl(client), "Ask an organization admin to configure or repair the model provider in the Sanbox console.");
192
205
  const templateAdminConsoleAction = (client) => consoleAction(consolePathUrl(client, "/templates"), "Ask an organization admin to create a template in the Sanbox console.");
193
206
  const templateActions = () => [
194
- commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the selected organization."),
207
+ commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization."),
195
208
  commandAction(["sanbox", "templates", "validate", "<template-id>", "--json"], "Check whether a template is runnable.")
196
209
  ];
197
210
  const runReadinessCodes = new Set([
@@ -273,10 +286,9 @@ const formatBytes = (bytes) => {
273
286
  };
274
287
  const positionalText = (command, startIndex) => command.slice(startIndex).join(" ").trim();
275
288
  const runTask = (command, flags) => flagString(flags, "task") || positionalText(command, 1);
276
- const commonFlags = ["api-url", "org", "json", "help"];
289
+ const commonFlags = ["api-url", "json", "help"];
277
290
  const flagSets = {
278
291
  "auth.check": commonFlags,
279
- "orgs.list": commonFlags,
280
292
  context: [...commonFlags, "template"],
281
293
  doctor: [...commonFlags, "template"],
282
294
  "model-providers.list": commonFlags,
@@ -285,15 +297,24 @@ const flagSets = {
285
297
  "templates.list": commonFlags,
286
298
  "templates.get": commonFlags,
287
299
  "templates.validate": commonFlags,
288
- "templates.create": [...commonFlags, "name", "model-provider", "model", "llm-budget-usd", "web-access"],
300
+ "templates.create": [
301
+ ...commonFlags,
302
+ "name",
303
+ "model-provider",
304
+ "model",
305
+ "harness",
306
+ "llm-budget-usd",
307
+ "web-access",
308
+ "telegram-allowed-user"
309
+ ],
289
310
  run: [
290
- ...commonFlags, "task", "input", "template", "external-run-id", "retention-ttl-seconds",
311
+ ...commonFlags, "task", "input", "template", "external-run-id",
291
312
  "dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "cancel-on-interrupt",
292
313
  "poll-interval-ms", "event-page-size", "timeout-seconds", "verbose"
293
314
  ],
294
315
  batch: [
295
316
  ...commonFlags, "tasks", "template", "input", "max-parallel", "wait", "batch-id",
296
- "retention-ttl-seconds", "poll-interval-ms", "timeout-seconds"
317
+ "poll-interval-ms", "timeout-seconds"
297
318
  ],
298
319
  "runs.list": [...commonFlags, "limit"],
299
320
  "runs.get": commonFlags,
@@ -321,8 +342,6 @@ const commandKey = (command) => {
321
342
  return "version";
322
343
  if (command[0] === "auth")
323
344
  return `auth.${command[1] || ""}`;
324
- if (command[0] === "orgs")
325
- return `orgs.${command[1] || ""}`;
326
345
  if (command[0] === "model-providers" || command[0] === "templates" || command[0] === "runs") {
327
346
  return `${command[0]}.${command[1] || ""}`;
328
347
  }
@@ -331,10 +350,11 @@ const commandKey = (command) => {
331
350
  return command[0];
332
351
  };
333
352
  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",
353
+ "api-url", "template", "task", "input", "include", "external-run-id",
354
+ "tasks", "max-parallel", "batch-id", "poll-interval-ms",
336
355
  "event-page-size", "timeout-seconds", "view", "after-event-id", "limit", "name",
337
- "model-provider", "model", "llm-budget-usd", "message", "output", "artifact"
356
+ "model-provider", "model", "harness", "llm-budget-usd", "telegram-allowed-user",
357
+ "message", "output", "artifact"
338
358
  ]);
339
359
  const validateFlagValues = (flags) => {
340
360
  for (const flag of requiredValueFlags) {
@@ -372,7 +392,7 @@ const validateFlags = (command, flags) => {
372
392
  return;
373
393
  }
374
394
  const knownRoots = new Set([
375
- "auth", "orgs", "context", "doctor", "model-providers", "templates",
395
+ "auth", "context", "doctor", "model-providers", "templates",
376
396
  "run", "batch", "runs", "init", "version"
377
397
  ]);
378
398
  const allowed = flagSets[key] ?? (knownRoots.has(command[0] || "") ? commonFlags : null);
@@ -388,7 +408,6 @@ const validatePositionals = (command, flags) => {
388
408
  return;
389
409
  const maximums = {
390
410
  "auth.check": 2,
391
- "orgs.list": 2,
392
411
  context: 1,
393
412
  doctor: 1,
394
413
  "model-providers.list": 2,
@@ -533,8 +552,7 @@ const commandRun = async (command, flags) => {
533
552
  instruction: task,
534
553
  inputs: flagList(flags, "input"),
535
554
  externalRunId: flagString(flags, "external-run-id") || undefined,
536
- templateId: template.id,
537
- retentionTtlSeconds: integerFlag(flags, "retention-ttl-seconds", 86400, 0, Number.MAX_SAFE_INTEGER)
555
+ templateId: template.id
538
556
  });
539
557
  }
540
558
  catch (error) {
@@ -587,8 +605,7 @@ const commandBatch = async (flags) => {
587
605
  instruction: task.task,
588
606
  inputs: task.input || inputs,
589
607
  externalRunId: task.external_run_id || `sanbox-batch-${batchId}-${index + 1}`,
590
- templateId: template.id,
591
- retentionTtlSeconds: integerFlag(flags, "retention-ttl-seconds", 86400, 0, Number.MAX_SAFE_INTEGER)
608
+ templateId: template.id
592
609
  });
593
610
  }
594
611
  catch (error) {
@@ -617,50 +634,28 @@ const commandBatch = async (flags) => {
617
634
  };
618
635
  const commandAuthCheck = async (flags) => {
619
636
  const client = makeClient(flags);
620
- const me = await client.me();
621
- const org = client.config.org;
622
- const organizations = Array.isArray(me.organizations)
623
- ? me.organizations
624
- : [];
625
- const selected = organizations.find((item) => item.slug === org) || null;
626
- const output = { ok: Boolean(selected), org, selected, me };
637
+ const [me, selected] = await Promise.all([client.me(), client.organization()]);
638
+ const org = selected.slug;
639
+ const output = { ok: true, org, selected, me };
627
640
  if (hasFlag(flags, "json"))
628
641
  printSuccess("auth.check", output, jsonContext(client));
629
642
  else
630
- process.stdout.write(selected ? `ok org=${org}\n` : `authenticated, but org ${org} is not visible\n`);
631
- if (!selected)
632
- process.exitCode = 2;
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
- }
643
+ process.stdout.write(`ok org=${org}\n`);
646
644
  };
647
645
  const commandContext = async (flags) => {
648
646
  const client = makeClient(flags);
649
647
  const selection = readTemplateSelection(flags, { required: false });
650
- const [me, providerPayload, templatePayload] = await Promise.all([
648
+ const [organization, me, providerPayload, templatePayload] = await Promise.all([
649
+ client.organization(),
651
650
  client.me(),
652
651
  client.listModelProviders(),
653
652
  client.listTemplates()
654
653
  ]);
655
- const organizations = Array.isArray(me.organizations)
656
- ? me.organizations
657
- : [];
658
- const selectedOrganization = organizations.find((item) => item.slug === client.config.org) || null;
659
654
  const selectedTemplate = selection
660
655
  ? templatePayload.templates.find((item) => item.id === selection.id || item.template_slug === selection.id) || null
661
656
  : null;
662
657
  const output = {
663
- organization: selectedOrganization,
658
+ organization,
664
659
  authentication: me.auth || null,
665
660
  template_selection: selection
666
661
  ? { id: selection.id, source: selection.source, found: Boolean(selectedTemplate), template: selectedTemplate }
@@ -669,22 +664,17 @@ const commandContext = async (flags) => {
669
664
  templates: templatePayload.templates
670
665
  };
671
666
  const nextActions = [];
672
- if (!selectedOrganization) {
673
- nextActions.push(commandAction(["sanbox", "auth", "check", "--json"], "Verify that the API key can access the selected organization."));
674
- }
675
667
  if (!selection) {
676
668
  nextActions.push(commandAction(["sanbox", "templates", "list", "--json"], "List available templates."), commandAction(["sanbox", "context", "--json"], "Select a template for this shell and inspect the resolved context.", { SANBOX_TEMPLATE: "<template-id>" }));
677
669
  }
678
670
  else if (!selectedTemplate) {
679
671
  nextActions.push(commandAction(["sanbox", "templates", "list", "--json"], "Choose a template that exists in this organization."));
680
672
  }
681
- if (!selectedOrganization)
682
- process.exitCode = 2;
683
673
  if (hasFlag(flags, "json")) {
684
674
  printSuccess("context.get", output, jsonContext(client), nextActions);
685
675
  return;
686
676
  }
687
- process.stdout.write(`org=${client.config.org}\n`);
677
+ process.stdout.write(`org=${organization.slug}\n`);
688
678
  process.stdout.write(`template=${selection ? `${selection.id} source=${selection.source}${selectedTemplate ? "" : " missing"}` : "not selected"}\n`);
689
679
  process.stdout.write(`model_providers=${providerPayload.providers.map((item) => providerId(item)).filter(Boolean).join(",") || "none"}\n`);
690
680
  process.stdout.write(`templates=${templatePayload.templates.map((item) => item.template_slug || item.id).join(",") || "none"}\n`);
@@ -767,7 +757,7 @@ const commandTemplates = async (command, flags) => {
767
757
  const payload = await client.listTemplates();
768
758
  const nextActions = payload.templates.length === 0
769
759
  ? [
770
- commandAction(["sanbox", "context", "--json"], "Inspect the selected organization and current authentication role."),
760
+ commandAction(["sanbox", "context", "--json"], "Inspect the API key's organization and current authentication role."),
771
761
  templateAdminConsoleAction(client)
772
762
  ]
773
763
  : [];
@@ -791,7 +781,7 @@ const commandTemplates = async (command, flags) => {
791
781
  throw new CliError("template_not_found", error.message, {
792
782
  status: error.status,
793
783
  details: { template_id: id },
794
- nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the selected organization.")]
784
+ nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization.")]
795
785
  });
796
786
  }
797
787
  throw error;
@@ -814,7 +804,7 @@ const commandTemplates = async (command, flags) => {
814
804
  throw new CliError("template_validation_failed", error.message, {
815
805
  status: error.status,
816
806
  details: { template_id: id },
817
- nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the selected organization.")]
807
+ nextActions: [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization.")]
818
808
  });
819
809
  }
820
810
  throw error;
@@ -835,6 +825,10 @@ const commandTemplates = async (command, flags) => {
835
825
  const name = requiredPositional(flagString(flags, "name"), "template_name_required", "templates create requires --name.");
836
826
  const modelProvider = requiredPositional(flagString(flags, "model-provider"), "model_provider_required", "templates create requires --model-provider.");
837
827
  const model = requiredPositional(flagString(flags, "model"), "model_required", "templates create requires --model.");
828
+ const harness = flagString(flags, "harness", "opencode");
829
+ if (harness !== "opencode" && harness !== "hermes") {
830
+ throw new CliError("invalid_harness", "templates create --harness must be opencode or hermes.");
831
+ }
838
832
  const budgetRaw = flagString(flags, "llm-budget-usd");
839
833
  const parsedBudgetUsd = budgetRaw ? Number(budgetRaw) : undefined;
840
834
  const llmBudgetUsd = parsedBudgetUsd === undefined
@@ -844,14 +838,29 @@ const commandTemplates = async (command, flags) => {
844
838
  (!Number.isFinite(parsedBudgetUsd) || llmBudgetUsd <= 0 || llmBudgetUsd > 100_000)) {
845
839
  throw new CliError("invalid_llm_budget", "templates create --llm-budget-usd must be at least 0.000001 and no more than 100000.");
846
840
  }
841
+ if (harness === "hermes" && llmBudgetUsd !== undefined) {
842
+ throw new CliError("hermes_budget_unsupported", "Always-on Hermes templates do not support --llm-budget-usd yet.");
843
+ }
844
+ const telegramBotToken = process.env.SANBOX_TELEGRAM_BOT_TOKEN?.trim() || "";
845
+ const telegramAllowedUsers = flagList(flags, "telegram-allowed-user").map((value) => value.trim()).filter(Boolean);
846
+ if (harness === "hermes" && !telegramBotToken) {
847
+ throw new CliError("telegram_bot_token_required", "Hermes template creation requires SANBOX_TELEGRAM_BOT_TOKEN.");
848
+ }
849
+ if (harness === "hermes" && telegramAllowedUsers.length === 0) {
850
+ throw new CliError("telegram_allowed_user_required", "Hermes template creation requires at least one --telegram-allowed-user.");
851
+ }
847
852
  let payload;
848
853
  try {
849
854
  payload = await client.createTemplate({
850
855
  name,
851
856
  provider_id: modelProvider,
852
857
  model_id: model,
853
- web_access: hasFlag(flags, "web-access"),
854
- ...(llmBudgetUsd === undefined ? {} : { llm_budget_usd: llmBudgetUsd })
858
+ ...(llmBudgetUsd === undefined ? {} : { llm_budget_usd: llmBudgetUsd }),
859
+ ...(harness === "hermes" ? {
860
+ harness,
861
+ telegram_bot_token: telegramBotToken,
862
+ telegram_allowed_users: telegramAllowedUsers
863
+ } : {})
855
864
  });
856
865
  }
857
866
  catch (error) {
@@ -860,7 +869,9 @@ const commandTemplates = async (command, flags) => {
860
869
  if (hasFlag(flags, "json")) {
861
870
  printSuccess("templates.create", payload, jsonContext(client), [
862
871
  commandAction(["sanbox", "templates", "validate", payload.template.id, "--json"], "Validate the new template before running it."),
863
- commandAction(["sanbox", "run", "<task>", "--template", payload.template.id], "Create a run with the new template.")
872
+ commandAction(["sanbox", "run", harness === "hermes" ? "<agent role and instructions>" : "<task>", "--template", payload.template.id], harness === "hermes"
873
+ ? "Start the always-on gateway; interact with it through the configured Telegram bot."
874
+ : "Create a run with the new template.")
864
875
  ]);
865
876
  return;
866
877
  }
@@ -1096,9 +1107,9 @@ const commandRuns = async (command, flags) => {
1096
1107
  const commandDoctor = async (flags) => {
1097
1108
  const localConfig = readLocalConfig();
1098
1109
  const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || localConfig.api_url || defaultApiUrl).replace(/\/+$/, "");
1099
- const org = String(flags.org || process.env.SANBOX_ORG || localConfig.org || "").trim();
1100
1110
  const apiKey = process.env.SANBOX_API_KEY || "";
1101
1111
  const selection = readTemplateSelection(flags, { required: false });
1112
+ let resolvedOrg = "";
1102
1113
  const checks = [];
1103
1114
  const nextActions = [];
1104
1115
  const add = (name, ok, detail, data) => checks.push({
@@ -1109,11 +1120,7 @@ const commandDoctor = async (flags) => {
1109
1120
  });
1110
1121
  add("node", Number(process.versions.node.split(".")[0]) >= 20, process.version);
1111
1122
  add("api_url", Boolean(apiUrl), apiUrl);
1112
- add("org", Boolean(org), org || "missing SANBOX_ORG, --org, or .sanbox/config.json org");
1113
1123
  add("api_key", Boolean(apiKey), apiKey ? "present in environment" : "missing SANBOX_API_KEY");
1114
- if (!org) {
1115
- nextActions.push(commandAction(["sanbox", "doctor", "--json"], "Select an organization and run the checks again.", { SANBOX_ORG: "<org-slug>" }));
1116
- }
1117
1124
  if (!apiKey) {
1118
1125
  nextActions.push(commandAction(["sanbox", "doctor", "--json"], "Set a Sanbox control-plane API key and run the checks again.", { SANBOX_API_KEY: "<sanbox-api-key>" }));
1119
1126
  }
@@ -1124,50 +1131,50 @@ const commandDoctor = async (flags) => {
1124
1131
  catch (error) {
1125
1132
  add("api_health", false, error instanceof Error ? error.message : String(error));
1126
1133
  }
1127
- if (org && apiKey) {
1128
- const client = new SanboxClient({ apiUrl, org, apiKey });
1134
+ if (apiKey) {
1135
+ const client = new SanboxClient({ apiUrl, apiKey });
1129
1136
  try {
1130
- const me = await client.me();
1131
- const organizations = Array.isArray(me.organizations)
1132
- ? me.organizations
1133
- : [];
1137
+ const organization = await client.organization();
1138
+ resolvedOrg = organization.slug;
1134
1139
  add("auth", true, "authenticated");
1135
- add("org_visible", organizations.some((item) => item.slug === org), org);
1140
+ add("organization", true, organization.slug, organization);
1136
1141
  }
1137
1142
  catch (error) {
1138
1143
  add("auth", false, error instanceof Error ? error.message : String(error));
1139
1144
  }
1140
- try {
1141
- const providers = await client.listModelProviders();
1142
- add("model_providers", true, providers.providers.map((item) => `${providerId(item)}:${item.status || (item.configured ? "configured" : "not_configured")}`).join(", ") || "none", providers.providers);
1143
- }
1144
- catch (error) {
1145
- add("model_providers", false, error instanceof Error ? error.message : String(error));
1146
- }
1147
- if (!selection) {
1148
- add("template", false, "not selected; use --template, SANBOX_TEMPLATE, or .sanbox/config.json default_template");
1149
- nextActions.push(...templateActions());
1150
- }
1151
- else {
1145
+ if (resolvedOrg) {
1152
1146
  try {
1153
- const template = await client.getTemplate(selection.id);
1154
- add("template", true, `${template.template.id}; source=${selection.source}`, template.template);
1147
+ const providers = await client.listModelProviders();
1148
+ add("model_providers", true, providers.providers.map((item) => `${providerId(item)}:${item.status || (item.configured ? "configured" : "not_configured")}`).join(", ") || "none", providers.providers);
1149
+ }
1150
+ catch (error) {
1151
+ add("model_providers", false, error instanceof Error ? error.message : String(error));
1152
+ }
1153
+ if (!selection) {
1154
+ add("template", false, "not selected; use --template, SANBOX_TEMPLATE, or .sanbox/config.json default_template");
1155
+ nextActions.push(...templateActions());
1156
+ }
1157
+ else {
1155
1158
  try {
1156
- const validation = await client.validateTemplate(selection.id);
1157
- const runnable = validationRunnable(validation);
1158
- add("template_readiness", runnable, runnable ? "ready" : "blocked", validation);
1159
- if (!runnable) {
1160
- nextActions.push(commandAction(["sanbox", "model-providers", "list", "--json"], "Inspect model-provider status."), providerConsoleAction(client));
1159
+ const template = await client.getTemplate(selection.id);
1160
+ add("template", true, `${template.template.id}; source=${selection.source}`, template.template);
1161
+ try {
1162
+ const validation = await client.validateTemplate(selection.id);
1163
+ const runnable = validationRunnable(validation);
1164
+ add("template_readiness", runnable, runnable ? "ready" : "blocked", validation);
1165
+ if (!runnable) {
1166
+ nextActions.push(commandAction(["sanbox", "model-providers", "list", "--json"], "Inspect model-provider status."), providerConsoleAction(client));
1167
+ }
1168
+ }
1169
+ catch (error) {
1170
+ add("template_readiness", false, error instanceof Error ? error.message : String(error));
1161
1171
  }
1162
1172
  }
1163
1173
  catch (error) {
1164
- add("template_readiness", false, error instanceof Error ? error.message : String(error));
1174
+ add("template", false, error instanceof Error ? error.message : String(error));
1175
+ nextActions.push(...templateActions());
1165
1176
  }
1166
1177
  }
1167
- catch (error) {
1168
- add("template", false, error instanceof Error ? error.message : String(error));
1169
- nextActions.push(...templateActions());
1170
- }
1171
1178
  }
1172
1179
  }
1173
1180
  try {
@@ -1183,7 +1190,7 @@ const commandDoctor = async (flags) => {
1183
1190
  checks
1184
1191
  };
1185
1192
  if (hasFlag(flags, "json")) {
1186
- printSuccess("doctor", output, { api_url: apiUrl, ...(org ? { org } : {}) }, nextActions);
1193
+ printSuccess("doctor", output, { api_url: apiUrl, ...(resolvedOrg ? { org: resolvedOrg } : {}) }, nextActions);
1187
1194
  }
1188
1195
  else {
1189
1196
  for (const check of checks) {
@@ -1220,11 +1227,9 @@ const commandInit = async (command, flags) => {
1220
1227
  const force = hasFlag(flags, "force");
1221
1228
  const localConfig = readLocalConfig();
1222
1229
  const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || localConfig.api_url || defaultApiUrl).replace(/\/+$/, "");
1223
- const org = String(flags.org || process.env.SANBOX_ORG || localConfig.org || "").trim();
1224
1230
  const template = readTemplateSelection(flags, { required: false });
1225
1231
  const config = {
1226
1232
  api_url: apiUrl,
1227
- org,
1228
1233
  ...(template ? { default_template: template.id } : {})
1229
1234
  };
1230
1235
  const sanboxIgnore = [
@@ -1245,7 +1250,7 @@ const commandInit = async (command, flags) => {
1245
1250
  printSuccess("init", {
1246
1251
  files: results.map(([file, status]) => ({ file, status })),
1247
1252
  template_selection: template
1248
- }, { api_url: apiUrl, ...(org ? { org } : {}) }, template ? [] : templateActions());
1253
+ }, { api_url: apiUrl }, template ? [] : templateActions());
1249
1254
  }
1250
1255
  else {
1251
1256
  for (const [file, status] of results)
@@ -1268,8 +1273,6 @@ const commandId = (command) => {
1268
1273
  return "version";
1269
1274
  if (command[0] === "auth")
1270
1275
  return "auth.check";
1271
- if (command[0] === "orgs")
1272
- return `orgs.${command[1] || "unknown"}`;
1273
1276
  if (command[0] === "context")
1274
1277
  return "context.get";
1275
1278
  if (command[0] === "model-providers")
@@ -1306,8 +1309,6 @@ const main = async (command, flags) => {
1306
1309
  }
1307
1310
  if (command[0] === "auth" && command[1] === "check")
1308
1311
  return commandAuthCheck(flags);
1309
- if (command[0] === "orgs")
1310
- return commandOrganizations(command, flags);
1311
1312
  if (command[0] === "context")
1312
1313
  return commandContext(flags);
1313
1314
  if (command[0] === "doctor")
package/dist/config.js CHANGED
@@ -11,7 +11,6 @@ export const readLocalConfig = (cwd = process.cwd()) => {
11
11
  const record = parsed;
12
12
  return {
13
13
  api_url: typeof record.api_url === "string" ? record.api_url : undefined,
14
- org: typeof record.org === "string" ? record.org : undefined,
15
14
  default_template: typeof record.default_template === "string" ? record.default_template : undefined
16
15
  };
17
16
  }
@@ -21,22 +20,16 @@ export const readLocalConfig = (cwd = process.cwd()) => {
21
20
  throw error;
22
21
  }
23
22
  };
24
- export const readConfig = (flags = {}, options = {}) => {
23
+ export const readConfig = (flags = {}) => {
25
24
  const localConfig = readLocalConfig();
26
25
  const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || localConfig.api_url || defaultApiUrl).replace(/\/+$/, "");
27
- const org = String(flags.org || process.env.SANBOX_ORG || localConfig.org || "").trim();
28
26
  const apiKey = process.env.SANBOX_API_KEY || "";
29
- if (options.requireOrg !== false && !org) {
30
- throw new CliError("org_required", "Organization context is required.", {
31
- nextActions: [commandAction(["sanbox", "context", "--json"], "Select an organization and retry with SANBOX_ORG set.", { SANBOX_ORG: "<org-slug>" })]
32
- });
33
- }
34
27
  if (!apiKey) {
35
28
  throw new CliError("api_key_required", "SANBOX_API_KEY is required.", {
36
29
  nextActions: [commandAction(["sanbox", "auth", "check", "--json"], "Set a Sanbox control-plane API key and check access.", { SANBOX_API_KEY: "<sanbox-api-key>" })]
37
30
  });
38
31
  }
39
- return { apiUrl, org, apiKey };
32
+ return { apiUrl, apiKey };
40
33
  };
41
34
  export function readTemplateSelection(flags = {}, options = {}) {
42
35
  const flagValue = flags.template;
@@ -52,7 +45,7 @@ export function readTemplateSelection(flags = {}, options = {}) {
52
45
  return null;
53
46
  throw new CliError("template_required", "A template must be selected explicitly.", {
54
47
  nextActions: [
55
- commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the selected organization."),
48
+ commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization."),
56
49
  commandAction(["sanbox", "run", "<task>", "--template", "<template-id>"], "Run with an explicit template."),
57
50
  commandAction(["sanbox", "context", "--json"], "Select a template for this shell and inspect the resolved context.", { SANBOX_TEMPLATE: "<template-id>" })
58
51
  ]
package/dist/output.js CHANGED
@@ -77,7 +77,9 @@ export const summarizeRun = (payload) => {
77
77
  const selection = [
78
78
  templateId ? `template=${templateId}` : "",
79
79
  run.provider_id ? `provider=${run.provider_id}` : "",
80
- run.model_id ? `model=${run.model_id}` : ""
80
+ run.model_id ? `model=${run.model_id}` : "",
81
+ run.sandbox_state ? `sandbox=${run.sandbox_state}` : "",
82
+ run.snapshot_generation ? `snapshot=${run.snapshot_generation}` : ""
81
83
  ].filter(Boolean).join(" ");
82
84
  return `${run.id} ${run.status}${selection ? ` ${selection}` : ""}${run.exit_code === null ? "" : ` exit=${run.exit_code}`}${run.error ? ` error=${run.error}` : ""}`;
83
85
  };
package/dist/runs.js CHANGED
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { buildInputBundle } from "./inputs.js";
5
- const terminalStatuses = new Set(["completed", "failed", "canceled", "expired"]);
5
+ const terminalStatuses = new Set(["completed", "failed", "canceled"]);
6
6
  export const isTerminalRun = (run) => terminalStatuses.has(run.status);
7
7
  export const waitForRun = async (client, runId, options = {}) => {
8
8
  const pollIntervalMs = options.pollIntervalMs ?? 2000;
@@ -31,8 +31,7 @@ export const createRun = async (client, options) => {
31
31
  external_run_id: options.externalRunId,
32
32
  workload_id: options.templateId,
33
33
  instruction: options.instruction,
34
- ...(inputCollectionId ? { input_collection_id: inputCollectionId } : {}),
35
- retention_ttl_seconds: options.retentionTtlSeconds
34
+ ...(inputCollectionId ? { input_collection_id: inputCollectionId } : {})
36
35
  });
37
36
  };
38
37
  export const readTasks = async (tasksPath) => {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.0.4";
1
+ export const version = "0.0.5";
package/dist/watch.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SanboxApiError } from "./api.js";
2
- const terminalStatuses = new Set(["completed", "failed", "canceled", "expired"]);
3
- const terminalEventKinds = new Set(["run.completed", "run.failed", "run.canceled", "run.expired"]);
2
+ const terminalStatuses = new Set(["completed", "failed", "canceled"]);
3
+ const terminalEventKinds = new Set(["run.completed", "run.failed", "run.canceled"]);
4
4
  const retryableNetworkCodes = new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH"]);
5
5
  export class WatchInterruptedError extends Error {
6
6
  constructor() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanlabs/sanbox-cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",