@mutagent/cli 0.1.260 → 0.1.262

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/dist/bin/cli.js CHANGED
@@ -68,8 +68,8 @@ function loadConfig() {
68
68
  ...credentials,
69
69
  apiKey: process.env.MUTAGENT_API_KEY ?? credentials?.apiKey ?? rcConfig?.apiKey,
70
70
  endpoint: process.env.MUTAGENT_ENDPOINT ?? credentials?.endpoint ?? rcConfig?.endpoint,
71
- defaultWorkspace: credentials?.defaultWorkspace ?? rcConfig?.defaultWorkspace,
72
- defaultWorkspaceSource: credentials?.defaultWorkspace ? credentials.defaultWorkspaceSource ?? "login-inferred" : rcConfig?.defaultWorkspace ? "login-inferred" : undefined,
71
+ defaultWorkspace: process.env.MUTAGENT_WORKSPACE_ID ?? credentials?.defaultWorkspace ?? rcConfig?.defaultWorkspace,
72
+ defaultWorkspaceSource: process.env.MUTAGENT_WORKSPACE_ID ? "user" : credentials?.defaultWorkspace ? credentials.defaultWorkspaceSource ?? "login-inferred" : rcConfig?.defaultWorkspace ? "login-inferred" : undefined,
73
73
  defaultOrganization: credentials?.defaultOrganization ?? rcConfig?.defaultOrganization
74
74
  };
75
75
  return configSchema.parse(merged);
@@ -269,7 +269,7 @@ function handleError(error, isJson) {
269
269
  }
270
270
  process.exit(1);
271
271
  }
272
- var MutagentError, AUTH_REMEDIATION_MESSAGE, AuthenticationError, ApiError, WORKSPACE_REMEDIATION_MESSAGE, WorkspaceContextError, ValidationError;
272
+ var MutagentError, AUTH_REMEDIATION_MESSAGE, AuthenticationError, ApiError, WORKSPACE_REMEDIATION_MESSAGE, WorkspaceContextError, ValidationError, NotFoundError;
273
273
  var init_errors = __esm(() => {
274
274
  MutagentError = class MutagentError extends Error {
275
275
  code;
@@ -335,6 +335,11 @@ var init_errors = __esm(() => {
335
335
  super("VALIDATION_ERROR", message, "Check the command syntax with: mutagent <command> --help", 1);
336
336
  }
337
337
  };
338
+ NotFoundError = class NotFoundError extends MutagentError {
339
+ constructor(resource, id) {
340
+ super("NOT_FOUND", `${resource} not found: ${id}`, `Check the ID and try again. List available ${resource.toLowerCase()}s with the appropriate list command.`, 1);
341
+ }
342
+ };
338
343
  });
339
344
 
340
345
  // src/lib/provider-request.ts
@@ -878,10 +883,10 @@ var init_sdk_client = __esm(() => {
878
883
  });
879
884
 
880
885
  // src/bin/cli.ts
881
- import { Command as Command13 } from "commander";
882
- import chalk19 from "chalk";
883
- import { readFileSync as readFileSync10 } from "fs";
884
- import { join as join14, dirname as dirname3 } from "path";
886
+ import { Command as Command14 } from "commander";
887
+ import chalk25 from "chalk";
888
+ import { readFileSync as readFileSync12 } from "fs";
889
+ import { join as join15, dirname as dirname4 } from "path";
885
890
  import { fileURLToPath as fileURLToPath2 } from "url";
886
891
 
887
892
  // src/commands/auth.ts
@@ -4369,8 +4374,149 @@ Examples:
4369
4374
  }
4370
4375
 
4371
4376
  // src/commands/hooks/index.ts
4377
+ import { readFileSync as readFileSync8 } from "node:fs";
4378
+ import { basename } from "node:path";
4372
4379
  import { Command as Command9 } from "commander";
4373
4380
 
4381
+ // src/commands/hooks/import-transcript.ts
4382
+ var MAX_SPAN_TEXT = 4000;
4383
+ function contentToText(content) {
4384
+ if (typeof content === "string")
4385
+ return content.slice(0, MAX_SPAN_TEXT);
4386
+ if (Array.isArray(content)) {
4387
+ return content.map((part) => part !== null && typeof part === "object" && ("text" in part) ? String(part.text) : "").filter(Boolean).join(" ").slice(0, MAX_SPAN_TEXT);
4388
+ }
4389
+ return "";
4390
+ }
4391
+ function convertTranscript(records, opts) {
4392
+ const maxSpans = opts.maxSpans ?? 200;
4393
+ const session = records.find((r) => r.type === "session");
4394
+ if (session === undefined || typeof session.id !== "string" || session.id === "") {
4395
+ return { trace: null, skipped: opts.skipped ?? 0, ignored: records.length };
4396
+ }
4397
+ const spans = [];
4398
+ let ignored = 0;
4399
+ let lastTs = session.timestamp ?? new Date().toISOString();
4400
+ for (const r of records) {
4401
+ if (r.type !== "message" && r.type !== "custom_message") {
4402
+ ignored += 1;
4403
+ continue;
4404
+ }
4405
+ if (spans.length >= maxSpans) {
4406
+ ignored += 1;
4407
+ continue;
4408
+ }
4409
+ const role = r.message?.role ?? r.role ?? r.type;
4410
+ const startTime = r.timestamp ?? lastTs;
4411
+ lastTs = startTime;
4412
+ const text = contentToText(r.message?.content);
4413
+ const span = {
4414
+ spanId: String(r.id ?? r.uuid ?? `span-${String(spans.length)}`).slice(0, 128),
4415
+ name: `helix.${role}`,
4416
+ kind: role === "assistant" ? "llm" : "message",
4417
+ startTime,
4418
+ status: "ok"
4419
+ };
4420
+ if (text !== "")
4421
+ span.output = text;
4422
+ spans.push(span);
4423
+ }
4424
+ return {
4425
+ trace: {
4426
+ traceId: `helix-${session.id}`.slice(0, 128),
4427
+ sessionId: session.id.slice(0, 256),
4428
+ name: "helix.session",
4429
+ source: "helix",
4430
+ startTime: session.timestamp ?? lastTs,
4431
+ endTime: lastTs,
4432
+ status: "ok",
4433
+ spans,
4434
+ metadata: {
4435
+ harness: "helix",
4436
+ transcript: opts.fileName,
4437
+ ...session.cwd !== undefined ? { cwd: session.cwd } : {},
4438
+ ...session.version !== undefined ? { version: session.version } : {}
4439
+ }
4440
+ },
4441
+ skipped: opts.skipped ?? 0,
4442
+ ignored
4443
+ };
4444
+ }
4445
+ function parseTranscript(raw) {
4446
+ const records = [];
4447
+ let skipped = 0;
4448
+ for (const line of raw.split(`
4449
+ `)) {
4450
+ const t = line.trim();
4451
+ if (t === "")
4452
+ continue;
4453
+ try {
4454
+ records.push(JSON.parse(t));
4455
+ } catch {
4456
+ skipped += 1;
4457
+ }
4458
+ }
4459
+ return { records, skipped };
4460
+ }
4461
+
4462
+ // src/commands/hooks/api.ts
4463
+ init_config();
4464
+ var API_TIMEOUT_MS = 5000;
4465
+ function resolveTenancyHeaders(config) {
4466
+ if (config.defaultWorkspace && !config.defaultOrganization) {
4467
+ return {
4468
+ ok: false,
4469
+ warning: "[mutagent hooks] Warning: a default workspace is set but no default organization, " + "so the server will reject every trace batch. " + `Fix with: mutagent config set organization <org-id>
4470
+ `
4471
+ };
4472
+ }
4473
+ const headers = {};
4474
+ if (config.defaultWorkspace)
4475
+ headers["x-workspace-id"] = config.defaultWorkspace;
4476
+ if (config.defaultOrganization)
4477
+ headers["x-organization-id"] = config.defaultOrganization;
4478
+ return { ok: true, headers };
4479
+ }
4480
+ async function sendBatchTrace(traces) {
4481
+ const apiKey = getApiKey();
4482
+ if (!apiKey) {
4483
+ process.stderr.write(`[mutagent hooks] Warning: Not authenticated. Run: mutagent auth login
4484
+ `);
4485
+ return;
4486
+ }
4487
+ const config = loadConfig();
4488
+ const endpoint = config.endpoint ?? "http://localhost:3003";
4489
+ const headers = {
4490
+ "x-api-key": apiKey,
4491
+ "Content-Type": "application/json"
4492
+ };
4493
+ const tenancy = resolveTenancyHeaders(config);
4494
+ if (!tenancy.ok) {
4495
+ process.stderr.write(tenancy.warning);
4496
+ return;
4497
+ }
4498
+ Object.assign(headers, tenancy.headers);
4499
+ const controller = new AbortController;
4500
+ const timeout = setTimeout(() => {
4501
+ controller.abort();
4502
+ }, API_TIMEOUT_MS);
4503
+ try {
4504
+ const response = await fetch(`${endpoint}/api/traces/batch`, {
4505
+ method: "POST",
4506
+ headers,
4507
+ body: JSON.stringify({ traces }),
4508
+ signal: controller.signal
4509
+ });
4510
+ if (!response.ok) {
4511
+ const body = await response.text().catch(() => "");
4512
+ process.stderr.write(`[mutagent hooks] Warning: API returned ${String(response.status)}: ${body.slice(0, 200)}
4513
+ `);
4514
+ }
4515
+ } finally {
4516
+ clearTimeout(timeout);
4517
+ }
4518
+ }
4519
+
4374
4520
  // src/commands/hooks/handlers-core.ts
4375
4521
  import { randomUUID } from "crypto";
4376
4522
 
@@ -4434,49 +4580,6 @@ function peekParentKind(state) {
4434
4580
  return state.parentStackKinds[state.parentStackKinds.length - 1] ?? null;
4435
4581
  }
4436
4582
 
4437
- // src/commands/hooks/api.ts
4438
- init_config();
4439
- var API_TIMEOUT_MS = 5000;
4440
- async function sendBatchTrace(traces) {
4441
- const apiKey = getApiKey();
4442
- if (!apiKey) {
4443
- process.stderr.write(`[mutagent hooks] Warning: Not authenticated. Run: mutagent auth login
4444
- `);
4445
- return;
4446
- }
4447
- const config = loadConfig();
4448
- const endpoint = config.endpoint ?? "http://localhost:3003";
4449
- const headers = {
4450
- "x-api-key": apiKey,
4451
- "Content-Type": "application/json"
4452
- };
4453
- if (config.defaultWorkspace) {
4454
- headers["x-workspace-id"] = config.defaultWorkspace;
4455
- }
4456
- if (config.defaultOrganization) {
4457
- headers["x-organization-id"] = config.defaultOrganization;
4458
- }
4459
- const controller = new AbortController;
4460
- const timeout = setTimeout(() => {
4461
- controller.abort();
4462
- }, API_TIMEOUT_MS);
4463
- try {
4464
- const response = await fetch(`${endpoint}/api/traces/batch`, {
4465
- method: "POST",
4466
- headers,
4467
- body: JSON.stringify({ traces }),
4468
- signal: controller.signal
4469
- });
4470
- if (!response.ok) {
4471
- const body = await response.text().catch(() => "");
4472
- process.stderr.write(`[mutagent hooks] Warning: API returned ${String(response.status)}: ${body.slice(0, 200)}
4473
- `);
4474
- }
4475
- } finally {
4476
- clearTimeout(timeout);
4477
- }
4478
- }
4479
-
4480
4583
  // src/commands/hooks/handlers-core.ts
4481
4584
  async function readStdin() {
4482
4585
  const chunks = [];
@@ -5200,6 +5303,69 @@ Claude Code Session Telemetry:
5200
5303
 
5201
5304
  Or run: mutagent hooks install
5202
5305
  `);
5306
+ hooks.command("import").argument("<files...>", "Session transcript files (.jsonl)").description("Import harness session transcripts as traces").option("--max-spans <n>", "Cap spans per session", "200").option("--dry-run", "Convert and report, but do not send").addHelpText("after", `
5307
+ Sources traces from transcripts already on disk, rather than capturing them
5308
+ live. mutagent-pi discovers these same files for its own /trace viewer; this
5309
+ ships them to the backend instead.
5310
+
5311
+ Where they live:
5312
+ ~/.mutagent/sessions* Helix
5313
+ ~/Library/Caches/mutagent/helix Helix (cache)
5314
+
5315
+ Examples:
5316
+ mutagent hooks import ~/.mutagent/sessions-rc/*.jsonl
5317
+ mutagent hooks import session.jsonl --dry-run
5318
+
5319
+ AI Agent Directive: use --dry-run first; it reports span counts and any
5320
+ unparseable lines without sending anything.
5321
+ `).action(async (files, opts) => {
5322
+ const maxSpans = Number.parseInt(opts.maxSpans ?? "200", 10);
5323
+ if (!Number.isFinite(maxSpans) || maxSpans < 1) {
5324
+ process.stderr.write(`[mutagent hooks] --max-spans must be a positive integer
5325
+ `);
5326
+ process.exitCode = 2;
5327
+ return;
5328
+ }
5329
+ const batch = [];
5330
+ for (const file of files) {
5331
+ let raw;
5332
+ try {
5333
+ raw = readFileSync8(file, "utf8");
5334
+ } catch (err) {
5335
+ process.stderr.write(`[mutagent hooks] skipped ${file}: ${err instanceof Error ? err.message : String(err)}
5336
+ `);
5337
+ continue;
5338
+ }
5339
+ const { records, skipped } = parseTranscript(raw);
5340
+ const { trace, ignored } = convertTranscript(records, {
5341
+ fileName: basename(file),
5342
+ maxSpans,
5343
+ skipped
5344
+ });
5345
+ if (trace === null) {
5346
+ process.stderr.write(`[mutagent hooks] ${basename(file)}: no session record, skipped
5347
+ `);
5348
+ continue;
5349
+ }
5350
+ process.stderr.write(`[mutagent hooks] ${basename(file)}: ${String(trace.spans.length)} spans` + `${skipped > 0 ? `, ${String(skipped)} unparseable lines` : ""}` + `${ignored > 0 ? `, ${String(ignored)} records ignored` : ""}
5351
+ `);
5352
+ batch.push(trace);
5353
+ }
5354
+ if (batch.length === 0) {
5355
+ process.stderr.write(`[mutagent hooks] nothing to import
5356
+ `);
5357
+ process.exitCode = 1;
5358
+ return;
5359
+ }
5360
+ if (opts.dryRun === true) {
5361
+ process.stderr.write(`[mutagent hooks] --dry-run: ${String(batch.length)} trace(s) not sent
5362
+ `);
5363
+ return;
5364
+ }
5365
+ await sendBatchTrace(batch);
5366
+ process.stderr.write(`[mutagent hooks] imported ${String(batch.length)} session(s)
5367
+ `);
5368
+ });
5203
5369
  hooks.command("install").description("Install Mutagent hooks into .claude/settings.local.json (safe merge — never overwrites existing hooks)").option("--cwd <dir>", "Target directory (defaults to cwd)", process.cwd()).addHelpText("after", `
5204
5370
  Reads existing .claude/settings.local.json (if present) and deep-merges
5205
5371
  Mutagent telemetry hooks into each event array (all 10 events). Skips any
@@ -5305,7 +5471,7 @@ times is safe.
5305
5471
  import { Command as Command10 } from "commander";
5306
5472
  import chalk17 from "chalk";
5307
5473
  import { type as osType, release as osRelease } from "os";
5308
- import { readFileSync as readFileSync9 } from "fs";
5474
+ import { readFileSync as readFileSync10 } from "fs";
5309
5475
  import { join as join13, dirname as dirname2 } from "path";
5310
5476
  import { fileURLToPath } from "url";
5311
5477
  init_errors();
@@ -5318,7 +5484,7 @@ import { join as join12 } from "path";
5318
5484
  import {
5319
5485
  existsSync as fsExistsSync,
5320
5486
  statSync as fsStatSync,
5321
- readFileSync as readFileSync8,
5487
+ readFileSync as readFileSync9,
5322
5488
  readdirSync as readdirSync2,
5323
5489
  openSync,
5324
5490
  readSync,
@@ -5329,7 +5495,7 @@ var TAIL_BYTES = 200000;
5329
5495
  function defaultReadTail(path, tailBytes) {
5330
5496
  const { size } = fsStatSync(path);
5331
5497
  if (size <= tailBytes) {
5332
- return { content: readFileSync8(path, "utf-8"), truncated: false };
5498
+ return { content: readFileSync9(path, "utf-8"), truncated: false };
5333
5499
  }
5334
5500
  const fd = openSync(path, "r");
5335
5501
  try {
@@ -5692,7 +5858,7 @@ function getCliVersion() {
5692
5858
  try {
5693
5859
  const __dirname2 = dirname2(fileURLToPath(import.meta.url));
5694
5860
  const pkgPath = join13(__dirname2, "..", "..", "package.json");
5695
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
5861
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
5696
5862
  return pkg.version ?? "0.1.1";
5697
5863
  } catch {
5698
5864
  return "0.1.1";
@@ -5986,85 +6152,1422 @@ If mutagent-cli is not installed: mutagent install diagnostics`), isJson);
5986
6152
  return trace;
5987
6153
  }
5988
6154
 
5989
- // src/bin/cli.ts
6155
+ // src/commands/helix/index.ts
6156
+ import { Command as Command13 } from "commander";
6157
+ import chalk24 from "chalk";
6158
+
6159
+ // src/commands/helix/spawn.ts
6160
+ import chalk20 from "chalk";
6161
+ init_errors();
6162
+
6163
+ // src/lib/sandbox-api.ts
5990
6164
  init_config();
6165
+ init_errors();
5991
6166
 
5992
- // src/lib/brand.ts
5993
- var BRAND = {
5994
- name: "MUTAGENT",
5995
- primary: "#7E47D7",
5996
- accent: "#45b8cc",
5997
- gradientFrom: "#45b8cc",
5998
- gradientTo: "#7E47D7"
5999
- };
6000
- var WORDMARK = [
6001
- " ███╗ ███╗ ██╗ ██╗ ████████╗ █████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗ ",
6002
- " ████╗ ████║ ██║ ██║ ╚══██╔══╝ ██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ",
6003
- " ██╔████╔██║ ██║ ██║ ██║ ███████║ ██║ ███╗ █████╗ ██╔██╗ ██║ ██║ ",
6004
- " ██║╚██╔╝██║ ██║ ██║ ██║ ██╔══██║ ██║ ██║ ██╔══╝ ██║╚██╗██║ ██║ ",
6005
- " ██║ ╚═╝ ██║ ╚██████╔╝ ██║ ██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚████║ ██║ ",
6006
- " ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═══╝ ╚═╝ "
6007
- ];
6008
- var WORDMARK_MIN_COLUMNS = 84;
6009
- function hexToRgb(hex) {
6010
- const n = parseInt(hex.slice(1), 16);
6011
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
6167
+ // src/lib/sandbox-token.ts
6168
+ init_secure_file();
6169
+ init_errors();
6170
+ import { existsSync as existsSync9, readFileSync as readFileSync11, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
6171
+ import { homedir as homedir4 } from "os";
6172
+ import { join as join14, dirname as dirname3 } from "path";
6173
+ function sandboxTokenFile() {
6174
+ const override = process.env.MUTAGENT_SANDBOX_TOKEN_FILE;
6175
+ return override !== undefined && override !== "" ? override : join14(homedir4(), ".config", "mutagent", "sandbox-token.json");
6012
6176
  }
6013
- var lerp = (a, b, t) => Math.round(a + (b - a) * t);
6014
- function mix(from, to, t) {
6015
- return [lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)];
6177
+ var EXPIRY_SKEW_MS = 60000;
6178
+ function isCacheShape(value) {
6179
+ if (typeof value !== "object" || value === null)
6180
+ return false;
6181
+ const r = value;
6182
+ return typeof r.token === "string" && typeof r.expiresAt === "number" && Number.isFinite(r.expiresAt) && typeof r.endpoint === "string" && typeof r.workspaceId === "string";
6016
6183
  }
6017
- var ansi = (r, g, b) => `\x1B[38;2;${r};${g};${b}m`;
6018
- var RESET = "\x1B[0m";
6019
- function gradientLine(line, from, to) {
6020
- const chars = Array.from(line);
6021
- const n = chars.length;
6022
- let out = "";
6023
- chars.forEach((ch, i) => {
6024
- if (ch === " ") {
6025
- out += ch;
6026
- return;
6184
+ function createFileTokenStore(path) {
6185
+ return {
6186
+ read() {
6187
+ if (!existsSync9(path))
6188
+ return null;
6189
+ try {
6190
+ const parsed = JSON.parse(readFileSync11(path, "utf-8"));
6191
+ return isCacheShape(parsed) ? parsed : null;
6192
+ } catch {
6193
+ return null;
6194
+ }
6195
+ },
6196
+ write(entry) {
6197
+ const dir = dirname3(path);
6198
+ if (!existsSync9(dir))
6199
+ mkdirSync6(dir, { recursive: true });
6200
+ writeSecureFile(path, JSON.stringify(entry, null, 2));
6201
+ },
6202
+ clear() {
6203
+ try {
6204
+ rmSync2(path, { force: true });
6205
+ } catch {}
6027
6206
  }
6028
- const [r, g, b] = mix(from, to, n > 1 ? i / (n - 1) : 0);
6029
- out += ansi(r, g, b) + ch;
6030
- });
6031
- return out + RESET;
6207
+ };
6032
6208
  }
6033
- function hasTruecolor(env = process.env) {
6034
- return /truecolor|24bit/i.test(env.COLORTERM ?? "");
6209
+ var fileTokenStore = {
6210
+ read: () => createFileTokenStore(sandboxTokenFile()).read(),
6211
+ write: (entry) => {
6212
+ createFileTokenStore(sandboxTokenFile()).write(entry);
6213
+ },
6214
+ clear: () => {
6215
+ createFileTokenStore(sandboxTokenFile()).clear();
6216
+ }
6217
+ };
6218
+ function isUsable(entry, endpoint, workspaceId, now) {
6219
+ return entry !== null && entry.endpoint === endpoint && entry.workspaceId === workspaceId && entry.expiresAt - EXPIRY_SKEW_MS > now;
6035
6220
  }
6036
- function detectBannerEnv(stream = process.stdout, env = process.env) {
6221
+ function messageFrom(body) {
6222
+ try {
6223
+ const parsed = JSON.parse(body);
6224
+ if (parsed && typeof parsed === "object") {
6225
+ const { message: value } = parsed;
6226
+ if (typeof value === "string" && value !== "")
6227
+ return value;
6228
+ }
6229
+ } catch {}
6230
+ return "";
6231
+ }
6232
+ async function exchangeToken(input, deps = {}) {
6233
+ const doFetch = deps.fetchImpl ?? fetch;
6234
+ const now = (deps.now ?? Date.now)();
6235
+ let response;
6236
+ try {
6237
+ response = await doFetch(`${input.endpoint}/api/sandbox/token`, {
6238
+ method: "POST",
6239
+ headers: {
6240
+ "x-api-key": input.apiKey,
6241
+ "Content-Type": "application/json"
6242
+ },
6243
+ body: JSON.stringify({ workspaceId: input.workspaceId })
6244
+ });
6245
+ } catch (error) {
6246
+ const detail = error instanceof Error ? error.message : String(error);
6247
+ throw new MutagentError("SANDBOX_UNREACHABLE", `Could not reach the MutagenT API at ${input.endpoint}: ${detail}`, "Check your connection, then verify the endpoint with: mutagent config list", 5);
6248
+ }
6249
+ if (response.status === 401) {
6250
+ throw new AuthenticationError("Your MutagenT API key was rejected. It may have been revoked or expired.", { suggestions: ["mutagent auth login --browser", "mutagent auth status"] });
6251
+ }
6252
+ if (response.status === 403) {
6253
+ const detail = messageFrom(await response.text().catch(() => ""));
6254
+ throw new MutagentError("SANDBOX_WORKSPACE_FORBIDDEN", detail !== "" ? detail : `Your API key is valid, but this account cannot use workspace "${input.workspaceId}".`, `Pick a workspace you belong to: mutagent workspaces list, then mutagent config set workspace <id>`, 3);
6255
+ }
6256
+ if (!response.ok) {
6257
+ const detail = messageFrom(await response.text().catch(() => ""));
6258
+ throw new MutagentError("SANDBOX_TOKEN_EXCHANGE_FAILED", detail !== "" ? detail : `The MutagenT API returned ${String(response.status)} when issuing a sandbox token.`, 'Retry, and if it persists report it with: mutagent feedback send "<what happened>" --category cli');
6259
+ }
6260
+ let payload;
6261
+ try {
6262
+ payload = await response.json();
6263
+ } catch {
6264
+ throw new MutagentError("INVALID_RESPONSE", "The server returned a non-JSON response when issuing a sandbox token.", 'Retry, and if it persists report it with: mutagent feedback send "<what happened>" --category cli');
6265
+ }
6266
+ if (typeof payload.token !== "string" || payload.token === "") {
6267
+ throw new MutagentError("INVALID_RESPONSE", "The server issued a sandbox token response with no token in it.", 'Retry, and if it persists report it with: mutagent feedback send "<what happened>" --category cli');
6268
+ }
6269
+ const lifetimeMs = typeof payload.expiresIn === "number" && Number.isFinite(payload.expiresIn) && payload.expiresIn > 0 ? payload.expiresIn * 1000 : 0;
6037
6270
  return {
6038
- isTTY: stream.isTTY === true,
6039
- columns: stream.columns ?? 80,
6040
- truecolor: hasTruecolor(env),
6041
- disabled: env.MUTAGENT_NO_BANNER === "1"
6271
+ token: payload.token,
6272
+ expiresAt: now + lifetimeMs,
6273
+ endpoint: input.endpoint,
6274
+ workspaceId: input.workspaceId
6042
6275
  };
6043
6276
  }
6044
- function renderBanner(env = detectBannerEnv()) {
6045
- if (env.disabled || !env.isTTY)
6046
- return "";
6047
- const from = hexToRgb(BRAND.gradientFrom);
6048
- const to = hexToRgb(BRAND.gradientTo);
6049
- const wide = env.columns >= WORDMARK_MIN_COLUMNS;
6050
- const art = wide ? WORDMARK : [` ${BRAND.name}`];
6051
- const lines = env.truecolor ? art.map((l) => gradientLine(l, from, to)) : art.map((l) => `${ansi(...from)}${l}${RESET}`);
6052
- return `
6053
- ${lines.join(`
6054
- `)}
6055
- `;
6277
+ async function getOperatorToken(input, options = {}, deps = {}) {
6278
+ const store = deps.store ?? fileTokenStore;
6279
+ const now = (deps.now ?? Date.now)();
6280
+ if (options.forceRefresh !== true) {
6281
+ const cached = store.read();
6282
+ if (isUsable(cached, input.endpoint, input.workspaceId, now))
6283
+ return cached.token;
6284
+ }
6285
+ const fresh = await exchangeToken(input, deps);
6286
+ store.write(fresh);
6287
+ return fresh.token;
6288
+ }
6289
+ function clearCachedToken(deps = {}) {
6290
+ (deps.store ?? fileTokenStore).clear();
6056
6291
  }
6057
6292
 
6058
- // src/lib/global-version-flag.ts
6059
- var GLOBAL_VALUE_OPTIONS = new Set(["--api-key", "--endpoint"]);
6060
- function findSubcommandIndex(rawArgs) {
6061
- for (let i = 0;i < rawArgs.length; i++) {
6062
- const arg = rawArgs[i];
6063
- if (arg === undefined)
6064
- continue;
6065
- if (arg === "--")
6066
- return i + 1 < rawArgs.length ? i + 1 : -1;
6067
- if (arg.startsWith("-")) {
6293
+ // src/lib/sandbox-api.ts
6294
+ function resolveConnection() {
6295
+ const apiKey = getApiKey();
6296
+ if (!apiKey)
6297
+ throw new AuthenticationError;
6298
+ const config = loadConfig();
6299
+ const configured = config.endpoint?.trim();
6300
+ const endpoint = (configured === undefined || configured === "" ? "https://api.mutagent.io" : configured).replace(/\/+$/, "");
6301
+ const workspaceId = requireWorkspace(config.defaultWorkspace);
6302
+ const headers = { "x-workspace-id": workspaceId };
6303
+ if (config.defaultOrganization)
6304
+ headers["x-organization-id"] = config.defaultOrganization;
6305
+ return { endpoint, apiKey, workspaceId, headers };
6306
+ }
6307
+ function requireWorkspace(workspaceId) {
6308
+ if (workspaceId === undefined || workspaceId.trim() === "") {
6309
+ throw new WorkspaceContextError("A workspace is required: sandbox access is minted per workspace, not per account.");
6310
+ }
6311
+ return workspaceId.trim();
6312
+ }
6313
+ async function fetchWithTokenRetry(url, init, baseHeaders, transport) {
6314
+ const attempt = async (forceRefresh) => transport.doFetch(url, {
6315
+ ...init,
6316
+ headers: { ...baseHeaders, Authorization: await transport.authorization(forceRefresh) }
6317
+ });
6318
+ const first = await attempt(false);
6319
+ if (first.status !== 401)
6320
+ return first;
6321
+ transport.onTokenRejected();
6322
+ return attempt(true);
6323
+ }
6324
+ async function sandboxFetch(path, init, deps = {}) {
6325
+ const connection = resolveConnection();
6326
+ const { endpoint } = connection;
6327
+ try {
6328
+ const response = await fetchWithTokenRetry(`${endpoint}${path}`, init, { ...connection.headers, ...toHeaderRecord(init.headers) }, {
6329
+ doFetch: fetch,
6330
+ authorization: async (forceRefresh) => `Bearer ${await getOperatorToken({ endpoint, apiKey: connection.apiKey, workspaceId: connection.workspaceId }, { forceRefresh }, deps)}`,
6331
+ onTokenRejected: () => {
6332
+ clearCachedToken(deps);
6333
+ }
6334
+ });
6335
+ return { response, endpoint };
6336
+ } catch (error) {
6337
+ if (error instanceof MutagentError)
6338
+ throw error;
6339
+ throw toNetworkError(error, endpoint);
6340
+ }
6341
+ }
6342
+ async function toCliError(response, sandboxId) {
6343
+ const body = await response.text().catch(() => "");
6344
+ const message = extractMessage(body) || response.statusText || "Request failed";
6345
+ if (response.status === 401)
6346
+ return new AuthenticationError;
6347
+ if (response.status === 404 && sandboxId)
6348
+ return new NotFoundError("Sandbox", sandboxId);
6349
+ return new ApiError(response.status, message);
6350
+ }
6351
+ function extractMessage(body) {
6352
+ const outer = parseRecord(body);
6353
+ if (!outer)
6354
+ return body.slice(0, 300);
6355
+ const code = typeof outer.error === "string" ? outer.error : "";
6356
+ const detail = typeof outer.message === "string" ? outer.message : "";
6357
+ const nested = parseRecord(detail);
6358
+ const summary = nested && typeof nested.summary === "string" ? nested.summary : "";
6359
+ if (summary !== "") {
6360
+ const property = nested && typeof nested.property === "string" ? nested.property : "";
6361
+ const where = property === "" ? "" : ` (at ${property})`;
6362
+ return code === "" ? `${summary}${where}` : `${code}: ${summary}${where}`;
6363
+ }
6364
+ if (detail !== "" && detail.length <= 400)
6365
+ return detail;
6366
+ if (code !== "")
6367
+ return code;
6368
+ return body.slice(0, 300);
6369
+ }
6370
+ function toHeaderRecord(init) {
6371
+ if (!init)
6372
+ return {};
6373
+ if (init instanceof Headers) {
6374
+ const record = {};
6375
+ init.forEach((value, key) => {
6376
+ record[key] = value;
6377
+ });
6378
+ return record;
6379
+ }
6380
+ if (Array.isArray(init))
6381
+ return Object.fromEntries(init);
6382
+ return init;
6383
+ }
6384
+ function toNetworkError(error, endpoint) {
6385
+ const detail = error instanceof Error ? error.message : String(error);
6386
+ return new MutagentError("SANDBOX_UNREACHABLE", `Could not reach the MutagenT API at ${endpoint}: ${detail}`, "Check your connection, then verify the endpoint with: mutagent config list", 5);
6387
+ }
6388
+ async function requestJson(path, init = {}) {
6389
+ const { sandboxId, ...requestInit } = init;
6390
+ const { response } = await sandboxFetch(path, {
6391
+ ...requestInit,
6392
+ headers: { "Content-Type": "application/json", ...toHeaderRecord(requestInit.headers) }
6393
+ });
6394
+ if (!response.ok)
6395
+ throw await toCliError(response, sandboxId);
6396
+ const text = await response.text();
6397
+ if (text.trim() === "")
6398
+ return {};
6399
+ try {
6400
+ return JSON.parse(text);
6401
+ } catch {
6402
+ throw new MutagentError("INVALID_RESPONSE", `The server returned a non-JSON response for ${path}.`, 'Retry, and if it persists report it with: mutagent feedback send "<what happened>" --category cli');
6403
+ }
6404
+ }
6405
+ async function spawnSandbox(definition) {
6406
+ return requestJson("/api/sandbox", {
6407
+ method: "POST",
6408
+ body: JSON.stringify({ definition })
6409
+ });
6410
+ }
6411
+ async function listSandboxes() {
6412
+ const body = await requestJson("/api/sandbox");
6413
+ return unwrapList(body);
6414
+ }
6415
+ function unwrapList(body) {
6416
+ if (Array.isArray(body))
6417
+ return body;
6418
+ if (body && typeof body === "object") {
6419
+ for (const key of ["sandboxes", "spans", "traces", "presets", "data", "items"]) {
6420
+ const value = body[key];
6421
+ if (Array.isArray(value))
6422
+ return value;
6423
+ }
6424
+ }
6425
+ return [];
6426
+ }
6427
+ async function destroySandbox(id) {
6428
+ const { response } = await sandboxFetch(`/api/sandbox/${encodeURIComponent(id)}`, {
6429
+ method: "DELETE"
6430
+ });
6431
+ if (response.status === 404)
6432
+ return;
6433
+ if (!response.ok)
6434
+ throw await toCliError(response, id);
6435
+ }
6436
+ async function execSandbox(id, body) {
6437
+ return requestJson(`/api/sandbox/${encodeURIComponent(id)}/exec`, {
6438
+ method: "POST",
6439
+ body: JSON.stringify(body),
6440
+ sandboxId: id
6441
+ });
6442
+ }
6443
+ async function oneShotRun(body) {
6444
+ return requestJson("/api/sandbox/run", {
6445
+ method: "POST",
6446
+ body: JSON.stringify(body)
6447
+ });
6448
+ }
6449
+ async function agentRun(body) {
6450
+ return requestJson("/api/sandbox/agent/run", {
6451
+ method: "POST",
6452
+ body: JSON.stringify(body)
6453
+ });
6454
+ }
6455
+ async function listPresets() {
6456
+ return requestJson("/api/sandbox/presets");
6457
+ }
6458
+ async function fetchSandboxTraces(id) {
6459
+ return requestJson(`/api/sandbox/${encodeURIComponent(id)}/traces`, { sandboxId: id });
6460
+ }
6461
+ async function openAttachStream(id, options = {}) {
6462
+ const query = options.since === undefined ? "" : `?since=${String(options.since)}`;
6463
+ return openStream(`/api/sandbox/${encodeURIComponent(id)}/stream${query}`, id, options);
6464
+ }
6465
+ async function openExecStream(id, body, options = {}) {
6466
+ return openStream(`/api/sandbox/${encodeURIComponent(id)}/exec?stream=1`, id, options, {
6467
+ method: "POST",
6468
+ body: JSON.stringify(body),
6469
+ headers: { "Content-Type": "application/json" }
6470
+ });
6471
+ }
6472
+ async function openStream(path, sandboxId, options, init = {}) {
6473
+ const { response } = await sandboxFetch(path, {
6474
+ ...init,
6475
+ headers: { Accept: "text/event-stream", ...toHeaderRecord(init.headers) },
6476
+ signal: options.signal
6477
+ });
6478
+ if (!response.ok)
6479
+ throw await toCliError(response, sandboxId);
6480
+ if (!response.body) {
6481
+ throw new MutagentError("EMPTY_STREAM", "The server accepted the stream request but sent no body.", `Retry with: mutagent helix attach ${sandboxId}`);
6482
+ }
6483
+ return response.body;
6484
+ }
6485
+ function sandboxLinks(id) {
6486
+ const base = `/api/sandbox/${encodeURIComponent(id)}`;
6487
+ return { api: base, stream: `${base}/stream`, traces: `${base}/traces` };
6488
+ }
6489
+ function parseOutputPayload(data) {
6490
+ const record = parseRecord(data);
6491
+ if (!record)
6492
+ return null;
6493
+ const { seq, ts, text } = record;
6494
+ if (typeof seq !== "number" || typeof text !== "string")
6495
+ return null;
6496
+ return { seq, ts: typeof ts === "string" ? ts : "", text };
6497
+ }
6498
+ function parseStatusPayload(data) {
6499
+ const record = parseRecord(data);
6500
+ if (!record)
6501
+ return null;
6502
+ if (typeof record.state !== "string")
6503
+ return null;
6504
+ return {
6505
+ state: record.state,
6506
+ exitCode: typeof record.exitCode === "number" ? record.exitCode : null
6507
+ };
6508
+ }
6509
+ function parseErrorPayload(data) {
6510
+ const record = parseRecord(data);
6511
+ const message = record && typeof record.message === "string" ? record.message : data.trim();
6512
+ return { message: message === "" ? "The server reported an unspecified stream error." : message };
6513
+ }
6514
+ function parseRecord(data) {
6515
+ try {
6516
+ const parsed = JSON.parse(data);
6517
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
6518
+ return parsed;
6519
+ }
6520
+ } catch {}
6521
+ return null;
6522
+ }
6523
+ function hostArch() {
6524
+ if (process.arch === "arm64")
6525
+ return "arm64";
6526
+ if (process.arch === "x64")
6527
+ return "x86_64";
6528
+ return null;
6529
+ }
6530
+
6531
+ // src/lib/tty.ts
6532
+ function isPlainOutput() {
6533
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "")
6534
+ return true;
6535
+ if (process.env.MUTAGENT_NON_INTERACTIVE === "true")
6536
+ return true;
6537
+ if (process.env.CI === "true")
6538
+ return true;
6539
+ return !process.stdout.isTTY;
6540
+ }
6541
+ function canAnimate(isJson) {
6542
+ return !isJson && !isPlainOutput();
6543
+ }
6544
+
6545
+ // src/commands/helix/session.ts
6546
+ import chalk19 from "chalk";
6547
+
6548
+ // src/lib/sse.ts
6549
+ var DEFAULT_EVENT_NAME = "message";
6550
+
6551
+ class SseDecoder {
6552
+ buffer = "";
6553
+ eventName = null;
6554
+ dataLines = [];
6555
+ push(chunk) {
6556
+ this.buffer += chunk;
6557
+ const dispatched = [];
6558
+ let newlineAt = this.buffer.indexOf(`
6559
+ `);
6560
+ while (newlineAt !== -1) {
6561
+ let line = this.buffer.slice(0, newlineAt);
6562
+ this.buffer = this.buffer.slice(newlineAt + 1);
6563
+ if (line.endsWith("\r"))
6564
+ line = line.slice(0, -1);
6565
+ const event = this.consumeLine(line);
6566
+ if (event)
6567
+ dispatched.push(event);
6568
+ newlineAt = this.buffer.indexOf(`
6569
+ `);
6570
+ }
6571
+ return dispatched;
6572
+ }
6573
+ consumeLine(line) {
6574
+ if (line === "")
6575
+ return this.dispatch();
6576
+ if (line.startsWith(":"))
6577
+ return null;
6578
+ const colonAt = line.indexOf(":");
6579
+ const field = colonAt === -1 ? line : line.slice(0, colonAt);
6580
+ let value = colonAt === -1 ? "" : line.slice(colonAt + 1);
6581
+ if (value.startsWith(" "))
6582
+ value = value.slice(1);
6583
+ if (field === "event") {
6584
+ this.eventName = value;
6585
+ } else if (field === "data") {
6586
+ this.dataLines.push(value);
6587
+ }
6588
+ return null;
6589
+ }
6590
+ dispatch() {
6591
+ const name = this.eventName;
6592
+ const data = this.dataLines;
6593
+ this.eventName = null;
6594
+ this.dataLines = [];
6595
+ if (data.length === 0)
6596
+ return null;
6597
+ return { event: name ?? DEFAULT_EVENT_NAME, data: data.join(`
6598
+ `) };
6599
+ }
6600
+ }
6601
+ async function* readSseEvents(body) {
6602
+ const reader = body.getReader();
6603
+ const decoder = new TextDecoder;
6604
+ const framer = new SseDecoder;
6605
+ try {
6606
+ for (;; ) {
6607
+ const { done, value } = await reader.read();
6608
+ if (done)
6609
+ break;
6610
+ for (const event of framer.push(decoder.decode(value, { stream: true }))) {
6611
+ yield event;
6612
+ }
6613
+ }
6614
+ } finally {
6615
+ reader.releaseLock();
6616
+ }
6617
+ }
6618
+
6619
+ // src/lib/sandbox-stream.ts
6620
+ init_errors();
6621
+ var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 500 };
6622
+ async function runStreamSession(options) {
6623
+ const retry = options.retry ?? DEFAULT_RETRY;
6624
+ const sleep2 = options.sleep ?? delay;
6625
+ const { sink, signal } = options;
6626
+ let lastSeq = options.since ?? 0;
6627
+ let exitCode = null;
6628
+ let reconnects = 0;
6629
+ let gaps = 0;
6630
+ let attempt = 0;
6631
+ const detached = () => ({ end: "detached", lastSeq, exitCode, reconnects, gaps });
6632
+ const isAborted = () => signal.aborted;
6633
+ for (;; ) {
6634
+ if (isAborted())
6635
+ return detached();
6636
+ let stream;
6637
+ try {
6638
+ stream = await options.open({ since: lastSeq > 0 ? lastSeq : undefined, signal });
6639
+ } catch (error) {
6640
+ if (isAborted())
6641
+ return detached();
6642
+ if (reconnects === 0 && attempt === 0)
6643
+ throw error;
6644
+ attempt += 1;
6645
+ if (attempt > retry.attempts)
6646
+ throw exhausted(lastSeq, error);
6647
+ reconnects += 1;
6648
+ sink.notice(reconnectNotice(attempt, retry.attempts, lastSeq));
6649
+ await sleep2(backoffMs(retry.baseDelayMs, attempt), signal);
6650
+ continue;
6651
+ }
6652
+ let exited = false;
6653
+ try {
6654
+ for await (const event of readSseEvents(stream)) {
6655
+ attempt = 0;
6656
+ if (event.event === "stdout" || event.event === "stderr") {
6657
+ const payload = parseOutputPayload(event.data);
6658
+ if (!payload)
6659
+ continue;
6660
+ if (payload.seq > lastSeq + 1 && lastSeq > 0) {
6661
+ gaps += 1;
6662
+ sink.notice(gapNotice(lastSeq, payload.seq));
6663
+ }
6664
+ lastSeq = Math.max(lastSeq, payload.seq);
6665
+ if (event.event === "stdout")
6666
+ sink.stdout(payload.text, payload.seq);
6667
+ else
6668
+ sink.stderr(payload.text, payload.seq);
6669
+ continue;
6670
+ }
6671
+ if (event.event === "status") {
6672
+ const payload = parseStatusPayload(event.data);
6673
+ if (payload?.state === "exited") {
6674
+ exitCode = payload.exitCode;
6675
+ exited = true;
6676
+ break;
6677
+ }
6678
+ continue;
6679
+ }
6680
+ if (event.event === "error") {
6681
+ const { message } = parseErrorPayload(event.data);
6682
+ if (mentionsLostBuffer(message)) {
6683
+ gaps += 1;
6684
+ sink.notice(`Output before this point is unavailable: ${message}. ` + "What follows is not continuous with what came before.");
6685
+ } else {
6686
+ sink.notice(`Stream error: ${message}`);
6687
+ }
6688
+ continue;
6689
+ }
6690
+ }
6691
+ } catch (error) {
6692
+ if (isAborted())
6693
+ return detached();
6694
+ attempt += 1;
6695
+ if (attempt > retry.attempts)
6696
+ throw exhausted(lastSeq, error);
6697
+ reconnects += 1;
6698
+ sink.notice(reconnectNotice(attempt, retry.attempts, lastSeq));
6699
+ await sleep2(backoffMs(retry.baseDelayMs, attempt), signal);
6700
+ continue;
6701
+ }
6702
+ if (exited)
6703
+ return { end: "exited", lastSeq, exitCode, reconnects, gaps };
6704
+ if (isAborted())
6705
+ return detached();
6706
+ attempt += 1;
6707
+ if (attempt > retry.attempts)
6708
+ throw exhausted(lastSeq, null);
6709
+ reconnects += 1;
6710
+ sink.notice(reconnectNotice(attempt, retry.attempts, lastSeq));
6711
+ await sleep2(backoffMs(retry.baseDelayMs, attempt), signal);
6712
+ }
6713
+ }
6714
+ function backoffMs(base, attempt) {
6715
+ return base * 2 ** (attempt - 1);
6716
+ }
6717
+ function reconnectNotice(attempt, max, lastSeq) {
6718
+ return `Connection lost. Reconnecting (${String(attempt)}/${String(max)}) from seq ${String(lastSeq)}...`;
6719
+ }
6720
+ function gapNotice(lastSeq, nextSeq) {
6721
+ const missing = nextSeq - lastSeq - 1;
6722
+ return `Output gap: ${String(missing)} event(s) between seq ${String(lastSeq)} and ` + `${String(nextSeq)} are unavailable — the server buffer rolled past the resume point. ` + "What follows is not continuous with what came before.";
6723
+ }
6724
+ function exhausted(lastSeq, cause) {
6725
+ const detail = cause instanceof Error ? ` (${cause.message})` : "";
6726
+ return new MutagentError("STREAM_DISCONNECTED", `Lost the sandbox stream and could not reconnect${detail}.`, `The sandbox is still running — nothing was destroyed. Resume with: mutagent helix attach <id> --since ${String(lastSeq)}`, 5);
6727
+ }
6728
+ function mentionsLostBuffer(message) {
6729
+ return /buffer|rolled|expired|evicted|no longer available|too old/i.test(message);
6730
+ }
6731
+ function delay(ms, signal) {
6732
+ return new Promise((resolve) => {
6733
+ if (signal.aborted) {
6734
+ resolve();
6735
+ return;
6736
+ }
6737
+ const timer = setTimeout(() => {
6738
+ signal.removeEventListener("abort", onAbort);
6739
+ resolve();
6740
+ }, ms);
6741
+ function onAbort() {
6742
+ clearTimeout(timer);
6743
+ resolve();
6744
+ }
6745
+ signal.addEventListener("abort", onAbort, { once: true });
6746
+ });
6747
+ }
6748
+
6749
+ // src/commands/helix/session.ts
6750
+ init_errors();
6751
+ function dim(text) {
6752
+ return isPlainOutput() ? text : chalk19.dim(text);
6753
+ }
6754
+ function reattachHint(id, lastSeq) {
6755
+ const since = lastSeq > 0 ? ` --since ${String(lastSeq)}` : "";
6756
+ return `mutagent helix attach ${id}${since}`;
6757
+ }
6758
+ function createSessionSink(isJson) {
6759
+ if (isJson) {
6760
+ return {
6761
+ stdout: (text, seq) => writeEvent("stdout", { seq, text }),
6762
+ stderr: (text, seq) => writeEvent("stderr", { seq, text }),
6763
+ notice: (message) => {
6764
+ process.stderr.write(`${JSON.stringify({ type: "notice", message })}
6765
+ `);
6766
+ }
6767
+ };
6768
+ }
6769
+ return {
6770
+ stdout: (text) => {
6771
+ process.stdout.write(text);
6772
+ },
6773
+ stderr: (text) => {
6774
+ process.stderr.write(text);
6775
+ },
6776
+ notice: (message) => {
6777
+ process.stderr.write(`${dim(`[mutagent] ${message}`)}
6778
+ `);
6779
+ }
6780
+ };
6781
+ }
6782
+ function writeEvent(type, fields) {
6783
+ process.stdout.write(`${JSON.stringify({ type, ...fields })}
6784
+ `);
6785
+ }
6786
+ async function attachToSandbox(options) {
6787
+ const sink = createSessionSink(options.isJson);
6788
+ const controller = new AbortController;
6789
+ const onSigint = () => {
6790
+ controller.abort();
6791
+ };
6792
+ process.on("SIGINT", onSigint);
6793
+ try {
6794
+ if (options.announce)
6795
+ sink.notice(options.announce);
6796
+ const result = await runStreamSession({
6797
+ open: options.open,
6798
+ sink,
6799
+ since: options.since,
6800
+ signal: controller.signal
6801
+ });
6802
+ reportOutcome(options.id, result, sink);
6803
+ return result;
6804
+ } finally {
6805
+ process.removeListener("SIGINT", onSigint);
6806
+ }
6807
+ }
6808
+ function reportOutcome(id, result, sink) {
6809
+ if (result.end === "detached") {
6810
+ sink.notice(`Detached. Sandbox ${id} is still running — nothing was destroyed.`);
6811
+ sink.notice(`Re-attach with: ${reattachHint(id, result.lastSeq)}`);
6812
+ sink.notice(`Destroy it with: mutagent helix rm ${id} --force`);
6813
+ return;
6814
+ }
6815
+ if (result.gaps > 0) {
6816
+ sink.notice(`Session ended, but ${String(result.gaps)} gap(s) occurred — the output above is not complete.`);
6817
+ }
6818
+ if (result.exitCode === null) {
6819
+ sink.notice(`Sandbox ${id} exited (no exit code reported).`);
6820
+ return;
6821
+ }
6822
+ sink.notice(`Sandbox ${id} exited with code ${String(result.exitCode)}.`);
6823
+ if (result.exitCode !== 0) {
6824
+ process.exitCode = result.exitCode;
6825
+ }
6826
+ }
6827
+ function parseSince(raw) {
6828
+ if (raw === undefined)
6829
+ return;
6830
+ const value = Number(raw);
6831
+ if (!Number.isInteger(value) || value < 0) {
6832
+ throw new MutagentError("INVALID_ARGUMENTS", `--since must be a non-negative sequence number, got "${raw}".`, "Sequence numbers start at 1 and are printed when a session detaches.");
6833
+ }
6834
+ return value;
6835
+ }
6836
+
6837
+ // src/commands/helix/spawn.ts
6838
+ var ARCHES = ["x86_64", "arm64"];
6839
+ function registerSpawnCommand(parent) {
6840
+ parent.command("spawn").description("Spawn a Helix session in a cloud sandbox and attach to it").requiredOption("--image <ref>", "Image reference (repository:tag or @digest)").option("--arch <arch>", `Image architecture: ${ARCHES.join(" | ")} (default: this host's)`).option("--name <name>", "Definition name (default: derived from the image reference)").option("--provider <name>", "Provider registry key (default: the backend default)").option("--env <name>", "Environment (secrets + variables) to load at spawn").option("--detach", "Spawn without attaching — print the ID and return").addHelpText("after", `
6841
+ Examples:
6842
+ ${chalk20.dim("$")} mutagent helix spawn --image alpine:3.20
6843
+ ${chalk20.dim("$")} mutagent helix spawn --image ghcr.io/example/helix:latest --arch x86_64
6844
+ ${chalk20.dim("$")} mutagent helix spawn --image alpine:3.20 --detach --json
6845
+ ${chalk20.dim("$")} mutagent helix spawn --image alpine:3.20 --provider docker --env staging
6846
+
6847
+ --image is required. A sandbox is spawned from a DEFINITION, and the definition
6848
+ names its own image — there is no default the CLI could invent for you. To run
6849
+ something without knowing an image reference, use 'mutagent helix run' or
6850
+ 'mutagent helix exec', which spawn from a named preset instead.
6851
+
6852
+ --arch defaults to this host's architecture, on the assumption that you are
6853
+ running an image you just built. A definition whose arch does not match its
6854
+ image is rejected at preflight rather than emulated, so set it explicitly when
6855
+ the image was built elsewhere.
6856
+
6857
+ --env names an Environment (a stored bundle of secrets and variables). It is a
6858
+ DIFFERENT axis from --provider, which selects the backend. Secrets never travel
6859
+ on the command line.
6860
+
6861
+ Without --detach this spawns and immediately attaches, so the common path is a
6862
+ single command. Ctrl-C then DETACHES — the sandbox keeps running and the
6863
+ re-attach command is printed. Use 'mutagent helix rm <id> --force' to destroy it.
6864
+
6865
+ AI Agent Directive:
6866
+ Prefer --detach --json for automation: it returns one JSON object with the
6867
+ sandbox id, so you can decide what to run before committing to a live stream.
6868
+ Without --detach, --json output is NDJSON and the final line is the summary.
6869
+ A spawned sandbox costs resources until 'mutagent helix rm <id>' — confirm
6870
+ with the user before spawning, and tell them the ID afterwards.
6871
+ `).action(async (options) => {
6872
+ const isJson = getJsonFlag(parent);
6873
+ const output = new OutputFormatter(isJson ? "json" : "table");
6874
+ try {
6875
+ const definition = buildDefinition(options);
6876
+ const sandbox = await createSandbox(definition, isJson);
6877
+ const id = requireId(sandbox);
6878
+ if (options.detach) {
6879
+ reportDetached(sandbox, id, output, isJson);
6880
+ return;
6881
+ }
6882
+ if (isJson) {
6883
+ process.stdout.write(`${JSON.stringify({ type: "spawned", sandbox })}
6884
+ `);
6885
+ }
6886
+ const result = await attachToSandbox({
6887
+ id,
6888
+ isJson,
6889
+ announce: `Spawned sandbox ${id}. Ctrl-C detaches without destroying it.`,
6890
+ open: ({ since, signal }) => openAttachStream(id, { since, signal })
6891
+ });
6892
+ if (isJson) {
6893
+ console.log(JSON.stringify({
6894
+ success: true,
6895
+ sandbox,
6896
+ end: result.end,
6897
+ exitCode: result.exitCode,
6898
+ lastSeq: result.lastSeq,
6899
+ reconnects: result.reconnects,
6900
+ gaps: result.gaps,
6901
+ _links: sandboxLinks(id)
6902
+ }));
6903
+ }
6904
+ } catch (error) {
6905
+ handleError(error, isJson);
6906
+ }
6907
+ });
6908
+ }
6909
+ function buildDefinition(options) {
6910
+ const image = options.image?.trim() ?? "";
6911
+ if (image === "") {
6912
+ throw new MutagentError("MISSING_ARGUMENTS", "--image is required and cannot be empty.", "Example: mutagent helix spawn --image alpine:3.20");
6913
+ }
6914
+ const arch = resolveArch(options.arch);
6915
+ const definition = {
6916
+ name: options.name?.trim() || deriveName(image),
6917
+ image,
6918
+ arch
6919
+ };
6920
+ if (options.provider)
6921
+ definition.provider = options.provider;
6922
+ if (options.env)
6923
+ definition.environment = options.env;
6924
+ return definition;
6925
+ }
6926
+ function resolveArch(requested) {
6927
+ if (requested !== undefined) {
6928
+ const value = requested.trim();
6929
+ if (!ARCHES.includes(value)) {
6930
+ throw new MutagentError("INVALID_ARGUMENTS", `--arch must be one of ${ARCHES.join(", ")}, got "${requested}".`, "Use --arch arm64 for Apple Silicon images, --arch x86_64 for Intel/AMD.");
6931
+ }
6932
+ return value;
6933
+ }
6934
+ const detected = hostArch();
6935
+ if (detected === null) {
6936
+ throw new MutagentError("MISSING_ARGUMENTS", `This host reports architecture "${process.arch}", which has no sandbox equivalent.`, `Name it explicitly: --arch ${ARCHES.join(" | --arch ")}`);
6937
+ }
6938
+ return detected;
6939
+ }
6940
+ function deriveName(image) {
6941
+ const slug = image.replace(/^.*\//, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120);
6942
+ return slug === "" ? "mutagent-cli" : slug;
6943
+ }
6944
+ async function createSandbox(definition, isJson) {
6945
+ if (!canAnimate(isJson))
6946
+ return spawnSandbox(definition);
6947
+ const { default: ora2 } = await import("ora");
6948
+ const spinner = ora2(`Spawning ${definition.image} (${definition.arch})...`).start();
6949
+ try {
6950
+ const sandbox = await spawnSandbox(definition);
6951
+ spinner.succeed(chalk20.green(`Sandbox ${String(sandbox.id)} spawned`));
6952
+ return sandbox;
6953
+ } catch (error) {
6954
+ spinner.fail(chalk20.red("Spawn failed"));
6955
+ throw error;
6956
+ }
6957
+ }
6958
+ function requireId(sandbox) {
6959
+ if (typeof sandbox.id === "string" && sandbox.id !== "")
6960
+ return sandbox.id;
6961
+ throw new MutagentError("INVALID_RESPONSE", "The server created a sandbox but did not return an id.", "The sandbox may still be running — check with: mutagent helix ls");
6962
+ }
6963
+ function reportDetached(sandbox, id, output, isJson) {
6964
+ if (isJson) {
6965
+ output.output({
6966
+ success: true,
6967
+ sandbox,
6968
+ _links: sandboxLinks(id),
6969
+ _directive: {
6970
+ instruction: "The sandbox is running and detached. Attach to stream its output, or destroy it when finished — a detached sandbox consumes resources until removed.",
6971
+ next: [`mutagent helix attach ${id} --json`, `mutagent helix rm ${id} --json`]
6972
+ }
6973
+ });
6974
+ return;
6975
+ }
6976
+ output.output({
6977
+ id,
6978
+ status: sandbox.status ?? "unknown",
6979
+ provider: sandbox.provider ?? "unknown",
6980
+ arch: sandbox.arch ?? "unknown",
6981
+ definition: sandbox.definitionName ?? "unknown"
6982
+ });
6983
+ console.log("");
6984
+ console.log(` Attach: ${reattachHint(id, 0)}`);
6985
+ console.log(` Destroy: mutagent helix rm ${id} --force`);
6986
+ }
6987
+
6988
+ // src/commands/helix/attach.ts
6989
+ import chalk21 from "chalk";
6990
+ init_errors();
6991
+ function registerAttachCommand(parent) {
6992
+ parent.command("attach").description("Attach to a running sandbox and stream its output").argument("<id>", "Sandbox ID").option("--since <seq>", "Resume after this sequence number instead of from the start").addHelpText("after", `
6993
+ Examples:
6994
+ ${chalk21.dim("$")} mutagent helix attach sbx_123
6995
+ ${chalk21.dim("$")} mutagent helix attach sbx_123 --since 412
6996
+ ${chalk21.dim("$")} mutagent helix attach sbx_123 --json | tail -1
6997
+ ${chalk21.dim("$")} mutagent helix attach sbx_123 > session.log
6998
+
6999
+ Ctrl-C detaches. The sandbox keeps running and the exact re-attach command,
7000
+ with the resume point filled in, is printed to stderr. Only 'mutagent helix rm <id> --force'
7001
+ destroys a sandbox.
7002
+
7003
+ Sandbox stdout goes to stdout and sandbox stderr goes to stderr, so redirects
7004
+ and pipes behave as they would for a local process. Everything this CLI says
7005
+ about the session goes to stderr.
7006
+
7007
+ If the connection drops, attach reconnects from the last sequence number seen.
7008
+ If the server's buffer has rolled past that point, the missing range is stated
7009
+ plainly rather than being papered over.
7010
+
7011
+ AI Agent Directive:
7012
+ Use --json. Output is NDJSON — one object per line, {"type":"stdout"|"stderr"|
7013
+ "notice", ...} — because a live stream cannot be one JSON document until it is
7014
+ over. The final line is a complete summary object; read it with 'tail -1' if
7015
+ you only need the result.
7016
+ Run 'mutagent helix ls --json' first to confirm the sandbox is running.
7017
+ `).action(async (id, options) => {
7018
+ const isJson = getJsonFlag(parent);
7019
+ try {
7020
+ const since = parseSince(options.since);
7021
+ const result = await attachToSandbox({
7022
+ id,
7023
+ isJson,
7024
+ since,
7025
+ announce: `Attaching to sandbox ${id}. Ctrl-C detaches without destroying it.`,
7026
+ open: ({ since: resumeFrom, signal }) => openAttachStream(id, { since: resumeFrom, signal })
7027
+ });
7028
+ if (isJson) {
7029
+ console.log(JSON.stringify({
7030
+ success: true,
7031
+ sandboxId: id,
7032
+ end: result.end,
7033
+ exitCode: result.exitCode,
7034
+ lastSeq: result.lastSeq,
7035
+ reconnects: result.reconnects,
7036
+ gaps: result.gaps,
7037
+ _links: sandboxLinks(id)
7038
+ }));
7039
+ }
7040
+ } catch (error) {
7041
+ handleError(error, isJson);
7042
+ }
7043
+ });
7044
+ }
7045
+
7046
+ // src/commands/helix/run.ts
7047
+ import chalk22 from "chalk";
7048
+ init_errors();
7049
+ function registerRunCommand(parent) {
7050
+ parent.command("run").description("Run one Helix agent turn in a fresh sandbox").argument("<task...>", "WHAT to do this turn").requiredOption("--prompt <definition>", "WHO the agent is — the agent definition").option("--preset <name>", "Preset to run on (default: the backend default)").option("--keep", "Keep the sandbox afterwards instead of tearing it down").option("--timeout-ms <n>", "Per-command timeout in milliseconds").addHelpText("after", `
7051
+ Examples:
7052
+ ${chalk22.dim("$")} mutagent helix run "summarise the failing tests" --prompt "You are a test triage agent."
7053
+ ${chalk22.dim("$")} mutagent helix run fix the flaky login spec --prompt "$(cat agent.md)"
7054
+ ${chalk22.dim("$")} mutagent helix run "list open PRs" --prompt "You are a release assistant." --json
7055
+ ${chalk22.dim("$")} mutagent helix run "explore the repo" --prompt "$(cat agent.md)" --keep
7056
+
7057
+ TWO PROMPTS, AND THEY ARE NOT THE SAME THING:
7058
+ --prompt WHO the agent is — its definition, its standing instructions.
7059
+ <task...> WHAT to do this turn.
7060
+ They are never concatenated. Passing the task as --prompt produces a run that
7061
+ starts, looks plausible, and answers a different question.
7062
+
7063
+ This is a ONE-SHOT call: it spawns a fresh sandbox from a preset, takes the turn
7064
+ and tears the sandbox down. There is no sandbox id argument because there is no
7065
+ endpoint that runs an agent turn inside an existing box. Pass --keep to hold the
7066
+ sandbox open; the id is reported so you can attach or exec against it.
7067
+
7068
+ The turn is blocking and does not stream — the server returns the whole result.
7069
+ Its exit code becomes this command's exit code. See 'mutagent helix presets' for
7070
+ what --preset accepts.
7071
+
7072
+ AI Agent Directive:
7073
+ --json returns ONE object: { success, result: { sandboxId, exitCode, stdout,
7074
+ stderr, timedOut, tornDown }, _links }. No NDJSON here — the call is blocking.
7075
+ Both --prompt and the task are required; do not fold one into the other.
7076
+ `).action(async (taskParts, options) => {
7077
+ const isJson = getJsonFlag(parent);
7078
+ const output = new OutputFormatter(isJson ? "json" : "table");
7079
+ try {
7080
+ const task = taskParts.join(" ").trim();
7081
+ if (task === "") {
7082
+ throw new MutagentError("MISSING_ARGUMENTS", "The task is required — it is WHAT the agent should do this turn.", 'Run: mutagent helix run "<what to do>" --prompt "<who the agent is>"');
7083
+ }
7084
+ const prompt = options.prompt?.trim() ?? "";
7085
+ if (prompt === "") {
7086
+ throw new MutagentError("MISSING_ARGUMENTS", "--prompt is required — it is WHO the agent is, not what it should do.", `The task you already passed is the WHAT. Add the agent definition:
7087
+ --prompt "You are a ..." (or --prompt "$(cat agent.md)")`);
7088
+ }
7089
+ const result = await agentRun({
7090
+ prompt,
7091
+ task,
7092
+ ...oneShotFields(options.preset, options.keep, options.timeoutMs)
7093
+ });
7094
+ reportResult(result, output, isJson, { prompt, task });
7095
+ } catch (error) {
7096
+ handleError(error, isJson);
7097
+ }
7098
+ });
7099
+ }
7100
+ function registerExecCommand(parent) {
7101
+ parent.command("exec").description("Run a command in a sandbox — an existing one, or a fresh one").argument("<argv...>", "Command and arguments (argv, not a shell string)").option("--sandbox <id>", "Run in this existing sandbox instead of a fresh one").option("--preset <name>", "Preset for the fresh sandbox (ignored with --sandbox)").option("--keep", "Keep the fresh sandbox afterwards (ignored with --sandbox)").option("--cwd <dir>", "Working directory inside the sandbox").option("--timeout-ms <n>", "Command timeout in milliseconds").option("--no-stream", "Wait for the full result instead of streaming it").addHelpText("after", `
7102
+ Examples:
7103
+ ${chalk22.dim("$")} mutagent helix exec -- ls -la /workspace
7104
+ ${chalk22.dim("$")} mutagent helix exec --sandbox sbx_123 -- bun test
7105
+ ${chalk22.dim("$")} mutagent helix exec --sandbox sbx_123 --cwd /src -- git status
7106
+ ${chalk22.dim("$")} mutagent helix exec --preset default -- uname -m --json
7107
+
7108
+ Put ${chalk22.bold("--")} before the command. Everything after it is argv and is passed through
7109
+ untouched, so flags meant for your command are not read as flags for this one.
7110
+
7111
+ ARGV, NOT A SHELL STRING. There is no shell in the sandbox unless you name one:
7112
+ ${chalk22.dim("$")} mutagent helix exec -- sh -c 'ls | wc -l' ${chalk22.dim("a shell, explicitly")}
7113
+ ${chalk22.dim("$")} mutagent helix exec -- 'ls | wc -l' ${chalk22.dim("looks for a file called 'ls | wc -l'")}
7114
+
7115
+ WITH --sandbox runs in that box and streams as output is produced.
7116
+ WITHOUT --sandbox spawns a fresh box from a preset, runs, tears it down. That
7117
+ path is blocking, so --no-stream is redundant there and
7118
+ --preset / --keep apply only there.
7119
+
7120
+ The command's exit code becomes this command's exit code.
7121
+
7122
+ AI Agent Directive:
7123
+ Use --no-stream --json (or the one-shot form, which is always blocking) for a
7124
+ result you intend to parse: one object, no NDJSON to reassemble.
7125
+ Run 'mutagent helix ls --json' first to confirm a --sandbox id is running.
7126
+ `).action(async (argv, options) => {
7127
+ const isJson = getJsonFlag(parent);
7128
+ const output = new OutputFormatter(isJson ? "json" : "table");
7129
+ try {
7130
+ const command = argv.filter((part) => part !== "");
7131
+ if (command.length === 0) {
7132
+ throw new MutagentError("MISSING_ARGUMENTS", "A command is required.", "Run: mutagent helix exec -- <command> [args...]");
7133
+ }
7134
+ const timeoutMs = parseTimeout(options.timeoutMs);
7135
+ const id = options.sandbox?.trim();
7136
+ if (id === undefined || id === "") {
7137
+ const result = await oneShotRun({
7138
+ command,
7139
+ ...options.cwd === undefined ? {} : { cwd: options.cwd },
7140
+ ...oneShotFields(options.preset, options.keep, options.timeoutMs)
7141
+ });
7142
+ reportResult(result, output, isJson, { command });
7143
+ return;
7144
+ }
7145
+ rejectOneShotOnlyFlags(options);
7146
+ const body = {
7147
+ command,
7148
+ ...options.cwd === undefined ? {} : { cwd: options.cwd },
7149
+ ...timeoutMs === undefined ? {} : { timeoutMs }
7150
+ };
7151
+ if (options.stream === false) {
7152
+ const result = await execSandbox(id, body);
7153
+ reportResult(result, output, isJson, { command, sandboxId: id });
7154
+ return;
7155
+ }
7156
+ const session = await attachToSandbox({
7157
+ id,
7158
+ isJson,
7159
+ announce: `Running in sandbox ${id}. Ctrl-C detaches without stopping it.`,
7160
+ open: createExecOpener(id, body)
7161
+ });
7162
+ if (isJson) {
7163
+ console.log(JSON.stringify({
7164
+ success: true,
7165
+ sandboxId: id,
7166
+ command,
7167
+ end: session.end,
7168
+ exitCode: session.exitCode,
7169
+ lastSeq: session.lastSeq,
7170
+ reconnects: session.reconnects,
7171
+ gaps: session.gaps,
7172
+ _links: sandboxLinks(id)
7173
+ }));
7174
+ }
7175
+ } catch (error) {
7176
+ handleError(error, isJson);
7177
+ }
7178
+ });
7179
+ }
7180
+ function rejectOneShotOnlyFlags(options) {
7181
+ const offenders = [];
7182
+ if (options.preset !== undefined)
7183
+ offenders.push("--preset");
7184
+ if (options.keep === true)
7185
+ offenders.push("--keep");
7186
+ if (offenders.length === 0)
7187
+ return;
7188
+ throw new MutagentError("INVALID_ARGUMENTS", `${offenders.join(" and ")} ${offenders.length === 1 ? "applies" : "apply"} only to a fresh sandbox, but --sandbox names an existing one.`, "Drop --sandbox to run one-shot in a fresh box, or drop these flags to run in the existing one.");
7189
+ }
7190
+ function createExecOpener(id, body) {
7191
+ let submitted = false;
7192
+ return ({ since, signal }) => {
7193
+ if (!submitted) {
7194
+ submitted = true;
7195
+ return openExecStream(id, body, { signal });
7196
+ }
7197
+ return openAttachStream(id, { since, signal });
7198
+ };
7199
+ }
7200
+ function oneShotFields(preset, keep, timeoutMs) {
7201
+ const parsed = parseTimeout(timeoutMs);
7202
+ return {
7203
+ ...preset === undefined ? {} : { preset },
7204
+ ...keep === true ? { keep: true } : {},
7205
+ ...parsed === undefined ? {} : { timeoutMs: parsed }
7206
+ };
7207
+ }
7208
+ function parseTimeout(raw) {
7209
+ if (raw === undefined)
7210
+ return;
7211
+ const value = Number(raw);
7212
+ if (!Number.isInteger(value) || value <= 0) {
7213
+ throw new MutagentError("INVALID_ARGUMENTS", `--timeout-ms must be a positive whole number of milliseconds, got "${raw}".`, "Example: --timeout-ms 30000 for thirty seconds.");
7214
+ }
7215
+ return value;
7216
+ }
7217
+ function reportResult(result, output, isJson, context) {
7218
+ if (isJson) {
7219
+ const links = typeof result.sandboxId === "string" ? sandboxLinks(result.sandboxId) : undefined;
7220
+ output.output({
7221
+ success: true,
7222
+ ...context,
7223
+ result,
7224
+ ...links === undefined ? {} : { _links: links }
7225
+ });
7226
+ } else {
7227
+ if (result.stdout)
7228
+ process.stdout.write(result.stdout);
7229
+ if (result.stderr)
7230
+ process.stderr.write(result.stderr);
7231
+ if (result.timedOut)
7232
+ process.stderr.write(`[mutagent] The command timed out.
7233
+ `);
7234
+ if (typeof result.sandboxId === "string" && result.tornDown === false) {
7235
+ process.stderr.write(`[mutagent] Sandbox ${result.sandboxId} is still running. Destroy it with: mutagent helix rm ${result.sandboxId} --force
7236
+ `);
7237
+ }
7238
+ }
7239
+ if (typeof result.exitCode === "number" && result.exitCode !== 0) {
7240
+ process.exitCode = result.exitCode;
7241
+ }
7242
+ }
7243
+
7244
+ // src/commands/helix/inventory.ts
7245
+ import chalk23 from "chalk";
7246
+ init_errors();
7247
+ function registerLsCommand(parent) {
7248
+ parent.command("ls").description("List your sandboxes").addHelpText("after", `
7249
+ Examples:
7250
+ ${chalk23.dim("$")} mutagent helix ls
7251
+ ${chalk23.dim("$")} mutagent helix ls --json
7252
+
7253
+ Listing is always workspace-scoped — these are the sandboxes in the workspace
7254
+ you are configured for, not every sandbox on the account. Change it with:
7255
+ ${chalk23.dim("$")} mutagent config set workspace <workspace-id>
7256
+
7257
+ An empty list is a normal result, not an error: it means no sandbox is running.
7258
+
7259
+ AI Agent Directive:
7260
+ Run this before attach, exec or rm to confirm the sandbox exists and is
7261
+ running. --json returns { sandboxes: [...], count: N }.
7262
+ `).action(async () => {
7263
+ const isJson = getJsonFlag(parent);
7264
+ const output = new OutputFormatter(isJson ? "json" : "table");
7265
+ try {
7266
+ const sandboxes = await listSandboxes();
7267
+ if (isJson) {
7268
+ output.output({
7269
+ sandboxes: sandboxes.map((s) => ({ ...s, _links: sandboxLinks(String(s.id)) })),
7270
+ count: sandboxes.length
7271
+ });
7272
+ return;
7273
+ }
7274
+ if (sandboxes.length === 0) {
7275
+ console.log(chalk23.gray("No sandboxes running."));
7276
+ console.log("");
7277
+ console.log(" Start one: mutagent helix spawn --image <ref>");
7278
+ return;
7279
+ }
7280
+ output.output(sandboxes.map(toRow));
7281
+ } catch (error) {
7282
+ handleError(error, isJson);
7283
+ }
7284
+ });
7285
+ }
7286
+ function toRow(sandbox) {
7287
+ return {
7288
+ id: sandbox.id,
7289
+ status: sandbox.status ?? "unknown",
7290
+ provider: sandbox.provider ?? "",
7291
+ arch: sandbox.arch ?? "",
7292
+ definition: sandbox.definitionName ?? "",
7293
+ created: typeof sandbox.createdAt === "string" ? new Date(sandbox.createdAt).toLocaleString() : ""
7294
+ };
7295
+ }
7296
+ function registerPresetsCommand(parent) {
7297
+ parent.command("presets").description("List the presets a one-shot run can name").addHelpText("after", `
7298
+ Examples:
7299
+ ${chalk23.dim("$")} mutagent helix presets
7300
+ ${chalk23.dim("$")} mutagent helix presets --json
7301
+
7302
+ A preset is a named definition on the backend. It is what lets 'helix run' and
7303
+ 'helix exec' start something without you knowing an image reference, a registry
7304
+ or a region. Image references are deliberately not returned — if you need
7305
+ image-level detail you are looking for 'helix spawn --image'.
7306
+
7307
+ AI Agent Directive:
7308
+ --json returns { presets: [...], default: <name|null>, count: N }. Read this
7309
+ before passing --preset; an unknown preset is rejected by the server.
7310
+ `).action(async () => {
7311
+ const isJson = getJsonFlag(parent);
7312
+ const output = new OutputFormatter(isJson ? "json" : "table");
7313
+ try {
7314
+ const list = await listPresets();
7315
+ const presets = list.presets;
7316
+ if (isJson) {
7317
+ output.output({ presets, default: list.default, count: presets.length });
7318
+ return;
7319
+ }
7320
+ if (presets.length === 0) {
7321
+ console.log(chalk23.gray("No presets configured on this backend."));
7322
+ console.log("");
7323
+ console.log(" Spawn with an explicit image: mutagent helix spawn --image <ref>");
7324
+ return;
7325
+ }
7326
+ output.output(presets.map((p) => ({
7327
+ name: p.name,
7328
+ default: p.name === list.default ? "yes" : "",
7329
+ arch: p.arch ?? "",
7330
+ provider: p.provider ?? "",
7331
+ description: p.description ?? ""
7332
+ })));
7333
+ } catch (error) {
7334
+ handleError(error, isJson);
7335
+ }
7336
+ });
7337
+ }
7338
+ function registerRmCommand(parent) {
7339
+ parent.command("rm").description("Destroy a sandbox").argument("<id>", "Sandbox ID").option("-f, --force", "Skip confirmation").addHelpText("after", `
7340
+ Examples:
7341
+ ${chalk23.dim("$")} mutagent helix rm sbx_123 --force
7342
+ ${chalk23.dim("$")} mutagent helix rm sbx_123 --json
7343
+
7344
+ This is the ONLY command that destroys a sandbox — detaching, Ctrl-C and a
7345
+ closed terminal all leave it running. Removing a sandbox that is already gone
7346
+ succeeds, so a retried teardown is safe.
7347
+
7348
+ ${chalk23.dim("Note: --force is required. The CLI is non-interactive — confirm with the user via your native flow, then pass --force. --json auto-confirms.")}
7349
+ ${chalk23.dim("Warning: any session running in the sandbox ends immediately and cannot be recovered.")}
7350
+
7351
+ AI Agent Directive:
7352
+ Destroying a sandbox ends any session running in it and cannot be undone.
7353
+ Confirm with the user before running it, then pass --force (or --json).
7354
+ `).action(async (id, options) => {
7355
+ const isJson = getJsonFlag(parent);
7356
+ const output = new OutputFormatter(isJson ? "json" : "table");
7357
+ try {
7358
+ if (!options.force && !isJson) {
7359
+ throw new MutagentError("CONFIRMATION_REQUIRED", `Destroying sandbox ${id} requires confirmation. Any session running in it ends immediately.`, `[Agent: confirm with the user first]
7360
+ Use --force to confirm: mutagent helix rm ${id} --force`);
7361
+ }
7362
+ await destroySandbox(id);
7363
+ if (isJson) {
7364
+ output.output({
7365
+ success: true,
7366
+ deletedId: id,
7367
+ _links: sandboxLinks(id),
7368
+ _directive: {
7369
+ instruction: "The sandbox is destroyed. Any session it was running has ended. Spawn a new one to continue.",
7370
+ next: ["mutagent helix ls --json", "mutagent helix spawn --image <ref> --detach --json"]
7371
+ }
7372
+ });
7373
+ return;
7374
+ }
7375
+ output.success(`Sandbox ${id} destroyed`);
7376
+ } catch (error) {
7377
+ handleError(error, isJson);
7378
+ }
7379
+ });
7380
+ }
7381
+ function registerTracesCommand(parent) {
7382
+ parent.command("traces").description("Show the spans captured for a sandbox").argument("<id>", "Sandbox ID").addHelpText("after", `
7383
+ Examples:
7384
+ ${chalk23.dim("$")} mutagent helix traces sbx_123
7385
+ ${chalk23.dim("$")} mutagent helix traces sbx_123 --json
7386
+
7387
+ Spans are what the session actually did — the record to read when the output
7388
+ alone does not explain the result. They are called spans, not traces, because
7389
+ that is what the server returns; the command keeps the familiar name.
7390
+
7391
+ An empty result means no spans were captured. That is not the same as the box
7392
+ having done nothing: telemetry ingest has to be configured for spans to exist.
7393
+
7394
+ AI Agent Directive:
7395
+ --json returns { spans: [...], count: N, sandboxId }. Read spans before
7396
+ concluding why a session behaved the way it did; the streamed output is only
7397
+ what was printed, not what was attempted.
7398
+ `).action(async (id) => {
7399
+ const isJson = getJsonFlag(parent);
7400
+ const output = new OutputFormatter(isJson ? "json" : "table");
7401
+ try {
7402
+ const body = await fetchSandboxTraces(id);
7403
+ const spans = unwrapList(body);
7404
+ if (isJson) {
7405
+ output.output({ spans, count: spans.length, sandboxId: id, _links: sandboxLinks(id) });
7406
+ return;
7407
+ }
7408
+ if (spans.length === 0) {
7409
+ console.log(chalk23.gray(`No spans captured for sandbox ${id}.`));
7410
+ return;
7411
+ }
7412
+ output.output(spans.map((span) => {
7413
+ const {
7414
+ workspaceId: _workspaceId,
7415
+ sandboxId: _sandboxId,
7416
+ receivedAt: _receivedAt,
7417
+ ...rest
7418
+ } = span;
7419
+ return rest;
7420
+ }));
7421
+ } catch (error) {
7422
+ handleError(error, isJson);
7423
+ }
7424
+ });
7425
+ }
7426
+
7427
+ // src/commands/helix/index.ts
7428
+ function createHelixCommand() {
7429
+ const helix = new Command13("helix").description("Run Helix sessions in a cloud sandbox").addHelpText("after", `
7430
+ Examples:
7431
+ ${chalk24.dim("$")} mutagent helix run "fix the failing test" --prompt "$(cat agent.md)"
7432
+ ${chalk24.dim("$")} mutagent helix exec -- uname -m ${chalk24.dim("One command, fresh box")}
7433
+ ${chalk24.dim("$")} mutagent helix presets ${chalk24.dim("What a one-shot can run on")}
7434
+ ${chalk24.dim("$")} mutagent helix spawn --image alpine:3.20 ${chalk24.dim("Persistent box, then attach")}
7435
+ ${chalk24.dim("$")} mutagent helix ls ${chalk24.dim("What is running")}
7436
+ ${chalk24.dim("$")} mutagent helix exec --sandbox sbx_1 -- bun test ${chalk24.dim("Command in that box")}
7437
+ ${chalk24.dim("$")} mutagent helix attach sbx_1 --since 412 ${chalk24.dim("Resume after a sequence number")}
7438
+ ${chalk24.dim("$")} mutagent helix traces sbx_1 --json ${chalk24.dim("What the session actually did")}
7439
+ ${chalk24.dim("$")} mutagent helix rm sbx_1 --force ${chalk24.dim("Destroy it")}
7440
+
7441
+ Subcommands:
7442
+ run, exec, spawn, ls, presets, attach, traces, rm
7443
+
7444
+ One-shot vs persistent:
7445
+ ${chalk24.bold("run")} and ${chalk24.bold("exec")} (without --sandbox) start from a named PRESET, do one
7446
+ thing and tear the sandbox down. Nothing to clean up, no image to know.
7447
+ ${chalk24.bold("spawn")} takes an explicit --image and leaves a box running for you to
7448
+ ${chalk24.bold("attach")} and ${chalk24.bold("exec --sandbox")} against until you ${chalk24.bold("rm")} it.
7449
+
7450
+ Lifecycle:
7451
+ A spawned sandbox outlives your terminal. Ctrl-C, a dropped connection and a
7452
+ closed terminal all DETACH — the session keeps running and can be re-attached.
7453
+ Only 'mutagent helix rm <id>' destroys a sandbox, so remember to run it when
7454
+ you are finished.
7455
+
7456
+ Access:
7457
+ Sandbox access is minted per WORKSPACE, so a workspace must be configured:
7458
+ ${chalk24.dim("$")} mutagent config set workspace <workspace-id>
7459
+
7460
+ Output:
7461
+ Sandbox stdout goes to stdout, sandbox stderr goes to stderr, and anything
7462
+ this CLI says about the session goes to stderr. Redirects and pipes therefore
7463
+ behave as they would for a locally running process. Spinners and colour are
7464
+ suppressed when the output is not a terminal or NO_COLOR is set.
7465
+
7466
+ Not to be confused with:
7467
+ 'mutagent install helix' — that installs the Helix tool on THIS machine.
7468
+ This group runs a session on a REMOTE sandbox.
7469
+
7470
+ AI Agent Directive:
7471
+ Run 'mutagent helix ls --json' before attach, exec --sandbox, or rm.
7472
+ ONLY the streaming paths ('attach', 'spawn' without --detach, 'exec --sandbox'
7473
+ without --no-stream) emit NDJSON in --json mode — one object per line, with a
7474
+ complete summary object as the final line — because a live stream is not a
7475
+ single JSON document until it ends. Everything else returns one JSON object.
7476
+ 'run' is always blocking: it takes TWO strings, --prompt (WHO the agent is)
7477
+ and the task (WHAT to do). Never fold one into the other.
7478
+ Spawning consumes resources until removed — confirm with the user before
7479
+ 'spawn' and before 'rm'.
7480
+ `);
7481
+ registerRunCommand(helix);
7482
+ registerExecCommand(helix);
7483
+ registerSpawnCommand(helix);
7484
+ registerLsCommand(helix);
7485
+ registerPresetsCommand(helix);
7486
+ registerAttachCommand(helix);
7487
+ registerTracesCommand(helix);
7488
+ registerRmCommand(helix);
7489
+ return helix;
7490
+ }
7491
+
7492
+ // src/bin/cli.ts
7493
+ init_config();
7494
+
7495
+ // src/lib/brand.ts
7496
+ var BRAND = {
7497
+ name: "MUTAGENT",
7498
+ primary: "#7E47D7",
7499
+ accent: "#45b8cc",
7500
+ gradientFrom: "#45b8cc",
7501
+ gradientTo: "#7E47D7"
7502
+ };
7503
+ var WORDMARK = [
7504
+ " ███╗ ███╗ ██╗ ██╗ ████████╗ █████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗ ",
7505
+ " ████╗ ████║ ██║ ██║ ╚══██╔══╝ ██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ",
7506
+ " ██╔████╔██║ ██║ ██║ ██║ ███████║ ██║ ███╗ █████╗ ██╔██╗ ██║ ██║ ",
7507
+ " ██║╚██╔╝██║ ██║ ██║ ██║ ██╔══██║ ██║ ██║ ██╔══╝ ██║╚██╗██║ ██║ ",
7508
+ " ██║ ╚═╝ ██║ ╚██████╔╝ ██║ ██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚████║ ██║ ",
7509
+ " ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═══╝ ╚═╝ "
7510
+ ];
7511
+ var WORDMARK_MIN_COLUMNS = 84;
7512
+ function hexToRgb(hex) {
7513
+ const n = parseInt(hex.slice(1), 16);
7514
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
7515
+ }
7516
+ var lerp = (a, b, t) => Math.round(a + (b - a) * t);
7517
+ function mix(from, to, t) {
7518
+ return [lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)];
7519
+ }
7520
+ var ansi = (r, g, b) => `\x1B[38;2;${r};${g};${b}m`;
7521
+ var RESET = "\x1B[0m";
7522
+ function gradientLine(line, from, to) {
7523
+ const chars = Array.from(line);
7524
+ const n = chars.length;
7525
+ let out = "";
7526
+ chars.forEach((ch, i) => {
7527
+ if (ch === " ") {
7528
+ out += ch;
7529
+ return;
7530
+ }
7531
+ const [r, g, b] = mix(from, to, n > 1 ? i / (n - 1) : 0);
7532
+ out += ansi(r, g, b) + ch;
7533
+ });
7534
+ return out + RESET;
7535
+ }
7536
+ function hasTruecolor(env = process.env) {
7537
+ return /truecolor|24bit/i.test(env.COLORTERM ?? "");
7538
+ }
7539
+ function detectBannerEnv(stream = process.stdout, env = process.env) {
7540
+ return {
7541
+ isTTY: stream.isTTY === true,
7542
+ columns: stream.columns ?? 80,
7543
+ truecolor: hasTruecolor(env),
7544
+ disabled: env.MUTAGENT_NO_BANNER === "1"
7545
+ };
7546
+ }
7547
+ function renderBanner(env = detectBannerEnv()) {
7548
+ if (env.disabled || !env.isTTY)
7549
+ return "";
7550
+ const from = hexToRgb(BRAND.gradientFrom);
7551
+ const to = hexToRgb(BRAND.gradientTo);
7552
+ const wide = env.columns >= WORDMARK_MIN_COLUMNS;
7553
+ const art = wide ? WORDMARK : [` ${BRAND.name}`];
7554
+ const lines = env.truecolor ? art.map((l) => gradientLine(l, from, to)) : art.map((l) => `${ansi(...from)}${l}${RESET}`);
7555
+ return `
7556
+ ${lines.join(`
7557
+ `)}
7558
+ `;
7559
+ }
7560
+
7561
+ // src/lib/global-version-flag.ts
7562
+ var GLOBAL_VALUE_OPTIONS = new Set(["--api-key", "--endpoint"]);
7563
+ function findSubcommandIndex(rawArgs) {
7564
+ for (let i = 0;i < rawArgs.length; i++) {
7565
+ const arg = rawArgs[i];
7566
+ if (arg === undefined)
7567
+ continue;
7568
+ if (arg === "--")
7569
+ return i + 1 < rawArgs.length ? i + 1 : -1;
7570
+ if (arg.startsWith("-")) {
6068
7571
  if (GLOBAL_VALUE_OPTIONS.has(arg))
6069
7572
  i++;
6070
7573
  continue;
@@ -6087,14 +7590,14 @@ if (process.env.CLI_VERSION) {
6087
7590
  cliVersion = process.env.CLI_VERSION;
6088
7591
  } else {
6089
7592
  try {
6090
- const __dirname2 = dirname3(fileURLToPath2(import.meta.url));
6091
- const pkgPath = join14(__dirname2, "..", "..", "package.json");
6092
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf-8"));
7593
+ const __dirname2 = dirname4(fileURLToPath2(import.meta.url));
7594
+ const pkgPath = join15(__dirname2, "..", "..", "package.json");
7595
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
6093
7596
  cliVersion = pkg.version ?? cliVersion;
6094
7597
  } catch {}
6095
7598
  }
6096
7599
  setCliVersion(cliVersion);
6097
- var program = new Command13;
7600
+ var program = new Command14;
6098
7601
  program.name("mutagent").description(`Mutagent CLI - command-line client for the Mutagent platform
6099
7602
 
6100
7603
  Documentation: https://docs.mutagent.io/cli
@@ -6104,56 +7607,61 @@ program.name("mutagent").description(`Mutagent CLI - command-line client for the
6104
7607
  });
6105
7608
  program.addHelpText("beforeAll", () => renderBanner());
6106
7609
  program.addHelpText("after", `
6107
- ${chalk19.bold.cyan("WORKFLOWS:")}
6108
- ${chalk19.bold("Setup")} mutagent login → mutagent init
6109
- ${chalk19.bold("Lifecycle Tools")} mutagent install <helix|diagnostics|evaluator> ${chalk19.dim("(login-gated)")}
6110
- ${chalk19.bold("Feedback")} mutagent feedback send "<what happened>" --category <cli|helix|stage:<x>> ${chalk19.dim("[--session <id>] [--attach-transcript]")}
7610
+ ${chalk25.bold.cyan("WORKFLOWS:")}
7611
+ ${chalk25.bold("Setup")} mutagent login → mutagent init
7612
+ ${chalk25.bold("Lifecycle Tools")} mutagent install <helix|diagnostics|evaluator> ${chalk25.dim("(login-gated)")}
7613
+ ${chalk25.bold("Feedback")} mutagent feedback send "<what happened>" --category <cli|helix|stage:<x>> ${chalk25.dim("[--session <id>] [--attach-transcript]")}
6111
7614
 
6112
- ${chalk19.dim("For CLI usage guidance for AI agents, see the Skill at")}
6113
- ${chalk19.cyan(".claude/skills/mutagent-cli/SKILL.md")}
7615
+ ${chalk25.dim("For CLI usage guidance for AI agents, see the Skill at")}
7616
+ ${chalk25.cyan(".claude/skills/mutagent-cli/SKILL.md")}
6114
7617
 
6115
- ${chalk19.yellow("Global flags:")}
6116
- -v, --version ${chalk19.dim("Print the CLI version")} ${chalk19.dim("(before a subcommand; after one it is the subcommand's)")}
7618
+ ${chalk25.yellow("Global flags:")}
7619
+ -v, --version ${chalk25.dim("Print the CLI version")} ${chalk25.dim("(before a subcommand; after one it is the subcommand's)")}
6117
7620
  --json --api-key <k> --endpoint <url> --non-interactive
6118
7621
 
6119
- ${chalk19.yellow("Non-Interactive Mode (CI/CD & Coding Agents):")}
6120
- export MUTAGENT_API_KEY=mt_... ${chalk19.dim("or")} --api-key mt_...
6121
- --json ${chalk19.dim("for structured output")} --non-interactive ${chalk19.dim("to disable prompts")}
7622
+ ${chalk25.yellow("Non-Interactive Mode (CI/CD & Coding Agents):")}
7623
+ export MUTAGENT_API_KEY=mt_... ${chalk25.dim("or")} --api-key mt_...
7624
+ --json ${chalk25.dim("for structured output")} --non-interactive ${chalk25.dim("to disable prompts")}
7625
+
7626
+ ${chalk25.yellow("Command Navigation:")}
7627
+ mutagent login ${chalk25.dim("Login (browser OAuth — recommended)")}
7628
+ mutagent auth status ${chalk25.dim("Check auth + workspace")}
7629
+ mutagent init ${chalk25.dim("Initialize project (.mutagentrc.json)")}
7630
+ mutagent workspaces list --json ${chalk25.dim("List workspaces (verify ID)")}
7631
+ mutagent config set workspace <id> ${chalk25.dim("Set active workspace")}
7632
+ mutagent usage --json ${chalk25.dim("Show account usage + provider status")}
6122
7633
 
6123
- ${chalk19.yellow("Command Navigation:")}
6124
- mutagent login ${chalk19.dim("Login (browser OAuth recommended)")}
6125
- mutagent auth status ${chalk19.dim("Check auth + workspace")}
6126
- mutagent init ${chalk19.dim("Initialize project (.mutagentrc.json)")}
6127
- mutagent workspaces list --json ${chalk19.dim("List workspaces (verify ID)")}
6128
- mutagent config set workspace <id> ${chalk19.dim("Set active workspace")}
6129
- mutagent usage --json ${chalk19.dim("Show account usage + provider status")}
7634
+ mutagent providers list --json ${chalk25.dim("List configured BYOK providers")}
7635
+ mutagent providers list --models ${chalk25.dim("See available models per provider")}
6130
7636
 
6131
- mutagent providers list --json ${chalk19.dim("List configured BYOK providers")}
6132
- mutagent providers list --models ${chalk19.dim("See available models per provider")}
7637
+ mutagent helix run "<task>" --prompt "<who>" ${chalk25.dim("One Helix agent turn in a cloud sandbox")}
7638
+ mutagent helix ls --json ${chalk25.dim("List running sandboxes")}
7639
+ mutagent helix spawn --image <ref> ${chalk25.dim("Persistent sandbox you attach to")}
7640
+ mutagent helix rm <id> --force ${chalk25.dim("Destroy a sandbox (the only thing that does)")}
6133
7641
 
6134
- mutagent install helix ${chalk19.dim("Install the ADL lifecycle conductor (login-gated)")}
6135
- mutagent install evaluator --version 1.2.3 ${chalk19.dim("Pin a version")}
6136
- mutagent install --help ${chalk19.dim("helix | diagnostics | evaluator")}
7642
+ mutagent install helix ${chalk25.dim("Install the ADL lifecycle conductor locally (login-gated)")}
7643
+ mutagent install evaluator --version 1.2.3 ${chalk25.dim("Pin a version")}
7644
+ mutagent install --help ${chalk25.dim("helix | diagnostics | evaluator")}
6137
7645
 
6138
- mutagent hooks --help ${chalk19.dim("Hook setup for Claude Code session telemetry upload")}
7646
+ mutagent hooks --help ${chalk25.dim("Hook setup for Claude Code session telemetry upload")}
6139
7647
 
6140
- ${chalk19.bold.red("Report Issues:")}
6141
- Hit a bug? Run: ${chalk19.cyan('mutagent feedback send "describe what went wrong" --category cli')}
6142
- Lifecycle-stage feedback: ${chalk19.cyan('mutagent feedback send "eval gate unclear" --category stage:evaluate')}
6143
- Link to a session: ${chalk19.cyan('mutagent feedback send "..." --session <session-id>')}
6144
- Attach your coding-agent session transcript: ${chalk19.cyan('mutagent feedback send "..." --attach-transcript')}
6145
- ${chalk19.dim("--category accepts: cli | helix | stage:<spec|build|evaluate|diagnose|optimize>")}
7648
+ ${chalk25.bold.red("Report Issues:")}
7649
+ Hit a bug? Run: ${chalk25.cyan('mutagent feedback send "describe what went wrong" --category cli')}
7650
+ Lifecycle-stage feedback: ${chalk25.cyan('mutagent feedback send "eval gate unclear" --category stage:evaluate')}
7651
+ Link to a session: ${chalk25.cyan('mutagent feedback send "..." --session <session-id>')}
7652
+ Attach your coding-agent session transcript: ${chalk25.cyan('mutagent feedback send "..." --attach-transcript')}
7653
+ ${chalk25.dim("--category accepts: cli | helix | stage:<spec|build|evaluate|diagnose|optimize>")}
6146
7654
 
6147
- ${chalk19.yellow("Directive System:")}
7655
+ ${chalk25.yellow("Directive System:")}
6148
7656
  Every --json response may include:
6149
- ${chalk19.bold("_directive.display")} Type tag — 'status_card' for card-kind directives (drives test/docs guards)
6150
- ${chalk19.bold("_directive.renderedCard")} Pre-formatted card ${chalk19.red("(MUST echo verbatim in chat whenever this field exists — see SKILL.md Verbatim Card Display Protocol)")}
6151
- ${chalk19.bold("_directive.instruction")} Next step for the agent (self-sufficient, no Skill required)
6152
- ${chalk19.bold("_directive.next")} Array of suggested follow-up commands
6153
- ${chalk19.bold("_links")} Dashboard/API URLs (format as markdown links)
6154
- ${chalk19.bold("_compat")} Compat metadata: cliVersion, skillVersion, skillMinCliVersion
6155
-
6156
- ${chalk19.yellow("AI Agent Rules (MANDATORY for coding agents):")}
7657
+ ${chalk25.bold("_directive.display")} Type tag — 'status_card' for card-kind directives (drives test/docs guards)
7658
+ ${chalk25.bold("_directive.renderedCard")} Pre-formatted card ${chalk25.red("(MUST echo verbatim in chat whenever this field exists — see SKILL.md Verbatim Card Display Protocol)")}
7659
+ ${chalk25.bold("_directive.instruction")} Next step for the agent (self-sufficient, no Skill required)
7660
+ ${chalk25.bold("_directive.next")} Array of suggested follow-up commands
7661
+ ${chalk25.bold("_links")} Dashboard/API URLs (format as markdown links)
7662
+ ${chalk25.bold("_compat")} Compat metadata: cliVersion, skillVersion, skillMinCliVersion
7663
+
7664
+ ${chalk25.yellow("AI Agent Rules (MANDATORY for coding agents):")}
6157
7665
  1. Login (two paths):
6158
7666
  - CI / fully automated: export MUTAGENT_API_KEY=mt_... then mutagent login --json
6159
7667
  - Helping a user onboard: mutagent login --browser --json
@@ -6163,13 +7671,13 @@ ${chalk19.yellow("AI Agent Rules (MANDATORY for coding agents):")}
6163
7671
  2. EVERY command MUST include --json (no exceptions)
6164
7672
  3. Run <command> --help BEFORE first use of any command
6165
7673
  4. Parse _directive.renderedCard and copy it into your CHAT RESPONSE verbatim
6166
- ${chalk19.red("HARD STOP")}: do NOT run further commands until the card is rendered in chat
7674
+ ${chalk25.red("HARD STOP")}: do NOT run further commands until the card is rendered in chat
6167
7675
  5. After mutagent init, verify workspace: mutagent workspaces list --json
6168
7676
  6. ALL user interaction via AskUserQuestion — CLI is non-interactive
6169
7677
  ${!hasCredentials() ? `
6170
- ` + chalk19.yellow(" Warning: Not authenticated. Run: mutagent login") + `
7678
+ ` + chalk25.yellow(" Warning: Not authenticated. Run: mutagent login") + `
6171
7679
  ` : ""}${!hasRcConfig() ? `
6172
- ` + chalk19.green(" Get started: mutagent init") + `
7680
+ ` + chalk25.green(" Get started: mutagent init") + `
6173
7681
  ` : ""}`);
6174
7682
  var rawArgs = process.argv.slice(2);
6175
7683
  if (isGlobalVersionFlag(rawArgs)) {
@@ -6205,9 +7713,10 @@ program.addCommand(createSkillsCommand());
6205
7713
  program.addCommand(createUsageCommand());
6206
7714
  program.addCommand(createHooksCommand());
6207
7715
  program.addCommand(createInstallCommand());
7716
+ program.addCommand(createHelixCommand());
6208
7717
  program.addCommand(createFeedbackCommand());
6209
7718
  program.addCommand(createTraceCommand());
6210
7719
  program.parse();
6211
7720
 
6212
- //# debugId=B0E8F30FEF98BABD64756E2164756E21
7721
+ //# debugId=4276BD959C14332264756E2164756E21
6213
7722
  //# sourceMappingURL=cli.js.map