@mutagent/cli 0.1.279 → 0.1.281

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
@@ -883,8 +883,8 @@ var init_sdk_client = __esm(() => {
883
883
  });
884
884
 
885
885
  // src/bin/cli.ts
886
- import { Command as Command14 } from "commander";
887
- import chalk25 from "chalk";
886
+ import { Command as Command15 } from "commander";
887
+ import chalk28 from "chalk";
888
888
  import { readFileSync as readFileSync12 } from "fs";
889
889
  import { join as join14, dirname as dirname4 } from "path";
890
890
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1139,7 +1139,7 @@ init_errors();
1139
1139
  import { spawn } from "child_process";
1140
1140
  import { createHash } from "crypto";
1141
1141
  import { homedir as homedir2 } from "os";
1142
- import { join as join5 } from "path";
1142
+ import { join as join5, posix as posixPath, win32 as win32Path, resolve as resolvePath } from "path";
1143
1143
  import {
1144
1144
  existsSync as existsSync4,
1145
1145
  mkdirSync as mkdirSync2,
@@ -1266,9 +1266,18 @@ async function defaultDownload(url, destPath) {
1266
1266
  async function defaultSha256(filePath) {
1267
1267
  return Promise.resolve(createHash("sha256").update(readFileSync4(filePath)).digest("hex"));
1268
1268
  }
1269
+ function buildTarExtractInvocation(tgzPath, destDir, platform = process.platform) {
1270
+ const p = platform === "win32" ? win32Path : posixPath;
1271
+ const cwd = p.dirname(tgzPath) || ".";
1272
+ const archive = p.basename(tgzPath);
1273
+ const dest = p.isAbsolute(destDir) ? destDir : resolvePath(destDir);
1274
+ return { args: ["-xzf", archive, "-C", dest], cwd };
1275
+ }
1269
1276
  function defaultExtract(tgzPath, destDir) {
1277
+ const invocation = buildTarExtractInvocation(tgzPath, destDir);
1270
1278
  return new Promise((resolve, reject) => {
1271
- const child = spawn("tar", ["-xzf", tgzPath, "-C", destDir], {
1279
+ const child = spawn("tar", invocation.args, {
1280
+ cwd: invocation.cwd,
1272
1281
  stdio: ["ignore", "ignore", "pipe"]
1273
1282
  });
1274
1283
  let stderr = "";
@@ -6112,11 +6121,11 @@ If mutagent-cli is not installed: mutagent install helix`), isJson);
6112
6121
  }
6113
6122
 
6114
6123
  // src/commands/helix/index.ts
6115
- import { Command as Command13 } from "commander";
6116
- import chalk24 from "chalk";
6124
+ import { Command as Command14 } from "commander";
6125
+ import chalk27 from "chalk";
6117
6126
 
6118
6127
  // src/commands/helix/spawn.ts
6119
- import chalk20 from "chalk";
6128
+ import chalk21 from "chalk";
6120
6129
  init_errors();
6121
6130
 
6122
6131
  // src/lib/sandbox-api.ts
@@ -6399,6 +6408,20 @@ async function execSandbox(id, body) {
6399
6408
  sandboxId: id
6400
6409
  });
6401
6410
  }
6411
+ async function postInput(id, body) {
6412
+ return requestJson(`/api/sandbox/${encodeURIComponent(id)}/input`, {
6413
+ method: "POST",
6414
+ body: JSON.stringify(body),
6415
+ sandboxId: id
6416
+ });
6417
+ }
6418
+ async function startSession(id, body) {
6419
+ return requestJson(`/api/sandbox/${encodeURIComponent(id)}/session`, {
6420
+ method: "POST",
6421
+ body: JSON.stringify(body),
6422
+ sandboxId: id
6423
+ });
6424
+ }
6402
6425
  async function oneShotRun(body) {
6403
6426
  return requestJson("/api/sandbox/run", {
6404
6427
  method: "POST",
@@ -6534,7 +6557,7 @@ function canAnimate(isJson) {
6534
6557
  }
6535
6558
 
6536
6559
  // src/commands/helix/session.ts
6537
- import chalk19 from "chalk";
6560
+ import chalk20 from "chalk";
6538
6561
 
6539
6562
  // src/lib/sse.ts
6540
6563
  var DEFAULT_EVENT_NAME = "message";
@@ -6633,6 +6656,13 @@ async function runStreamSession(options) {
6633
6656
  });
6634
6657
  const detached = () => outcome("detached");
6635
6658
  const isAborted = () => signal.aborted;
6659
+ const noteSeq = (seq) => {
6660
+ if (seq > lastSeq + 1 && lastSeq > 0) {
6661
+ gaps += 1;
6662
+ sink.notice(gapNotice(lastSeq, seq));
6663
+ }
6664
+ lastSeq = Math.max(lastSeq, seq);
6665
+ };
6636
6666
  for (;; ) {
6637
6667
  if (isAborted())
6638
6668
  return detached();
@@ -6657,38 +6687,44 @@ async function runStreamSession(options) {
6657
6687
  for await (const event of readSseEvents(stream)) {
6658
6688
  attempt = 0;
6659
6689
  if (event.event === "stdout" || event.event === "stderr") {
6660
- const payload = parseOutputPayload(event.data);
6661
- if (!payload)
6690
+ const payload2 = parseOutputPayload(event.data);
6691
+ if (!payload2)
6662
6692
  continue;
6663
- if (payload.seq > lastSeq + 1 && lastSeq > 0) {
6664
- gaps += 1;
6665
- sink.notice(gapNotice(lastSeq, payload.seq));
6666
- }
6667
- lastSeq = Math.max(lastSeq, payload.seq);
6693
+ noteSeq(payload2.seq);
6668
6694
  if (event.event === "stdout")
6669
- sink.stdout(payload.text, payload.seq);
6695
+ sink.stdout(payload2.text, payload2.seq);
6670
6696
  else
6671
- sink.stderr(payload.text, payload.seq);
6697
+ sink.stderr(payload2.text, payload2.seq);
6672
6698
  continue;
6673
6699
  }
6674
6700
  if (event.event === "sandbox") {
6675
- const payload = parseSandboxRunPayload(event.data);
6676
- if (payload) {
6677
- sandbox = payload;
6678
- options.onSandbox?.(payload);
6701
+ const payload2 = parseSandboxRunPayload(event.data);
6702
+ if (payload2) {
6703
+ sandbox = payload2;
6704
+ options.onSandbox?.(payload2);
6679
6705
  }
6680
6706
  continue;
6681
6707
  }
6682
6708
  if (event.event === "result") {
6683
- const payload = parseSandboxResultPayload(event.data);
6684
- if (payload)
6685
- result = payload;
6709
+ const payload2 = parseSandboxResultPayload(event.data);
6710
+ if (payload2)
6711
+ result = payload2;
6712
+ continue;
6713
+ }
6714
+ if (event.event === "rpc") {
6715
+ const payload2 = parseOutputPayload(event.data);
6716
+ if (!payload2) {
6717
+ sink.unrecognised(event.data, 0);
6718
+ continue;
6719
+ }
6720
+ noteSeq(payload2.seq);
6721
+ sink.rpc(payload2.text, payload2.seq);
6686
6722
  continue;
6687
6723
  }
6688
6724
  if (event.event === "status") {
6689
- const payload = parseStatusPayload(event.data);
6690
- if (payload?.state === "exited") {
6691
- exitCode = payload.exitCode;
6725
+ const payload2 = parseStatusPayload(event.data);
6726
+ if (payload2?.state === "exited") {
6727
+ exitCode = payload2.exitCode;
6692
6728
  exited = true;
6693
6729
  break;
6694
6730
  }
@@ -6704,6 +6740,15 @@ async function runStreamSession(options) {
6704
6740
  }
6705
6741
  continue;
6706
6742
  }
6743
+ if (event.event === "ping")
6744
+ continue;
6745
+ const payload = parseOutputPayload(event.data);
6746
+ if (payload) {
6747
+ noteSeq(payload.seq);
6748
+ sink.unrecognised(payload.text, payload.seq);
6749
+ } else {
6750
+ sink.unrecognised(event.data, 0);
6751
+ }
6707
6752
  }
6708
6753
  } catch (error) {
6709
6754
  if (isAborted())
@@ -6766,8 +6811,226 @@ function delay(ms, signal) {
6766
6811
 
6767
6812
  // src/commands/helix/session.ts
6768
6813
  init_errors();
6814
+
6815
+ // src/commands/helix/frames.ts
6816
+ import chalk19 from "chalk";
6817
+ var SILENT = new Set([
6818
+ "message_start",
6819
+ "message_update",
6820
+ "message_delta",
6821
+ "turn_end",
6822
+ "agent_start",
6823
+ "agent_end"
6824
+ ]);
6825
+ var IDENTIFYING_ARG = {
6826
+ read: ["path", "file_path", "filePath"],
6827
+ write: ["path", "file_path", "filePath"],
6828
+ edit: ["path", "file_path", "filePath"],
6829
+ ls: ["path", "dir"],
6830
+ glob: ["pattern", "path"],
6831
+ grep: ["pattern", "query"],
6832
+ search: ["pattern", "query"],
6833
+ bash: ["command", "cmd", "script"],
6834
+ shell: ["command", "cmd"],
6835
+ exec: ["command", "cmd"],
6836
+ fetch: ["url"],
6837
+ webfetch: ["url"],
6838
+ task: ["description", "prompt"],
6839
+ agent: ["description", "prompt"]
6840
+ };
6841
+ var GENERIC_ARG = ["path", "file_path", "command", "pattern", "url", "query", "name", "id"];
6842
+ var TOOL_COL = 9;
6843
+ var DURATION_COL = 44;
6844
+ var MAX_ARG = 56;
6845
+ var MAX_TEXT = 400;
6846
+
6847
+ class FrameFeed {
6848
+ #plain;
6849
+ #pending = new Map;
6850
+ #turn = 0;
6851
+ #now;
6852
+ constructor(options = {}) {
6853
+ this.#plain = options.plain ?? isPlainOutput();
6854
+ this.#now = options.now ?? (() => Date.now());
6855
+ }
6856
+ render(frameText, seq) {
6857
+ const frame = asRecord(frameText);
6858
+ if (!frame)
6859
+ return this.renderUnrecognised(frameText, seq);
6860
+ const type = typeof frame.type === "string" ? frame.type : "";
6861
+ switch (type) {
6862
+ case "tool_execution_start":
6863
+ this.#startCall(frame);
6864
+ return null;
6865
+ case "tool_execution_end":
6866
+ return this.#endCall(frame);
6867
+ case "turn_start":
6868
+ this.#turn += 1;
6869
+ return this.#rule(`turn ${String(this.#turn)}`);
6870
+ case "message_end":
6871
+ return this.#assistantText(frame);
6872
+ case "agent_settled":
6873
+ return this.#line(this.#sym("●", "*"), this.#dim("agent settled — waiting for input"));
6874
+ case "extension_ui_request":
6875
+ return this.#hitl(frame);
6876
+ case "response":
6877
+ return this.#response(frame);
6878
+ default:
6879
+ if (type === "" || SILENT.has(type))
6880
+ return null;
6881
+ return this.#line(this.#sym("·", "."), this.#dim(type));
6882
+ }
6883
+ }
6884
+ renderUnrecognised(raw, seq) {
6885
+ const where = seq > 0 ? ` (seq ${String(seq)})` : "";
6886
+ const body = `unrecognised${where}: ${oneLine(raw, MAX_ARG * 2)}`;
6887
+ return this.#line(this.#sym("·", "."), this.#dim(body));
6888
+ }
6889
+ #startCall(frame) {
6890
+ const id = typeof frame.toolCallId === "string" ? frame.toolCallId : "";
6891
+ if (id === "")
6892
+ return;
6893
+ this.#pending.set(id, {
6894
+ toolName: typeof frame.toolName === "string" ? frame.toolName : "tool",
6895
+ arg: identifyingArg(frame.toolName, frame.args),
6896
+ startedAt: this.#now()
6897
+ });
6898
+ }
6899
+ #endCall(frame) {
6900
+ const id = typeof frame.toolCallId === "string" ? frame.toolCallId : "";
6901
+ const started = id === "" ? undefined : this.#pending.get(id);
6902
+ if (id !== "")
6903
+ this.#pending.delete(id);
6904
+ const toolName = started?.toolName ?? (typeof frame.toolName === "string" ? frame.toolName : "tool");
6905
+ const arg = started?.arg ?? identifyingArg(frame.toolName, frame.args);
6906
+ const failed = isFailure(frame.result);
6907
+ const elapsed = started === undefined ? "" : formatDuration(this.#now() - started.startedAt);
6908
+ const name = this.#plain ? toolName : failed ? chalk19.red(toolName) : chalk19.cyan(toolName);
6909
+ const visible = arg === "" ? toolName.padEnd(TOOL_COL) : `${toolName.padEnd(TOOL_COL)} ${arg}`;
6910
+ const head = arg === "" ? pad(name, toolName, TOOL_COL) : `${pad(name, toolName, TOOL_COL)} ${arg}`;
6911
+ const gap = " ".repeat(Math.max(2, DURATION_COL - visible.length));
6912
+ const tail = elapsed === "" ? "" : gap + this.#dim(elapsed);
6913
+ return this.#line(failed ? this.#sym("✗", "x") : this.#sym("▸", ">"), head + tail);
6914
+ }
6915
+ #assistantText(frame) {
6916
+ const text = extractText(frame);
6917
+ if (text === "")
6918
+ return null;
6919
+ return this.#line(this.#sym("❯", "|"), oneLine(text, MAX_TEXT));
6920
+ }
6921
+ #hitl(frame) {
6922
+ const method = typeof frame.method === "string" ? frame.method : "request";
6923
+ const key = typeof frame.statusKey === "string" ? ` ${frame.statusKey}` : "";
6924
+ const body = `hitl ${method}${key}`;
6925
+ return this.#line(this.#sym("◆", "?"), this.#plain ? body : chalk19.yellow(body));
6926
+ }
6927
+ #response(frame) {
6928
+ const command = typeof frame.command === "string" ? frame.command : "command";
6929
+ const id = typeof frame.id === "string" ? ` ${frame.id}` : "";
6930
+ const ok = frame.success !== false;
6931
+ const body = `${command}${id} ${ok ? "accepted" : "rejected"}`;
6932
+ if (this.#plain)
6933
+ return this.#line(ok ? "+" : "x", body);
6934
+ return this.#line(ok ? "✓" : "✗", ok ? chalk19.green(body) : chalk19.red(body));
6935
+ }
6936
+ #line(symbol, body) {
6937
+ return ` ${symbol} ${body}`;
6938
+ }
6939
+ #sym(styled, plain) {
6940
+ return this.#plain ? plain : styled;
6941
+ }
6942
+ #dim(text) {
6943
+ return this.#plain ? text : chalk19.dim(text);
6944
+ }
6945
+ #rule(label) {
6946
+ const dash = this.#plain ? "-" : "─";
6947
+ const body = `${dash.repeat(2)} ${label} ${dash.repeat(Math.max(2, 40 - label.length))}`;
6948
+ return ` ${this.#dim(body)}`;
6949
+ }
6950
+ }
6951
+ function asRecord(text) {
6952
+ try {
6953
+ const parsed = JSON.parse(text);
6954
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
6955
+ return parsed;
6956
+ }
6957
+ } catch {}
6958
+ return null;
6959
+ }
6960
+ function pad(styled, visible, width) {
6961
+ const missing = Math.max(0, width - visible.length);
6962
+ return styled + " ".repeat(missing);
6963
+ }
6964
+ function identifyingArg(toolName, args) {
6965
+ if (!args || typeof args !== "object" || Array.isArray(args))
6966
+ return "";
6967
+ const record = args;
6968
+ const name = typeof toolName === "string" ? toolName.toLowerCase() : "";
6969
+ for (const key of IDENTIFYING_ARG[name] ?? GENERIC_ARG) {
6970
+ const value = record[key];
6971
+ if (typeof value === "string" && value.trim() !== "")
6972
+ return oneLine(value, MAX_ARG);
6973
+ if (typeof value === "number")
6974
+ return String(value);
6975
+ }
6976
+ for (const value of Object.values(record)) {
6977
+ if (typeof value === "string" && value.trim() !== "")
6978
+ return oneLine(value, MAX_ARG);
6979
+ }
6980
+ return "";
6981
+ }
6982
+ function isFailure(result) {
6983
+ if (!result || typeof result !== "object" || Array.isArray(result))
6984
+ return false;
6985
+ const record = result;
6986
+ if (record.isError === true || record.error != null)
6987
+ return true;
6988
+ return record.success === false;
6989
+ }
6990
+ function extractText(frame) {
6991
+ const candidates = [
6992
+ frame.text,
6993
+ frame.content,
6994
+ frame.message?.content,
6995
+ frame.message?.text,
6996
+ frame.message
6997
+ ];
6998
+ for (const candidate of candidates) {
6999
+ if (typeof candidate === "string" && candidate.trim() !== "")
7000
+ return candidate.trim();
7001
+ if (Array.isArray(candidate)) {
7002
+ const joined = candidate.map((part) => {
7003
+ if (typeof part === "string")
7004
+ return part;
7005
+ if (part && typeof part === "object" && typeof part.text === "string") {
7006
+ return part.text;
7007
+ }
7008
+ return "";
7009
+ }).join("").trim();
7010
+ if (joined !== "")
7011
+ return joined;
7012
+ }
7013
+ }
7014
+ return "";
7015
+ }
7016
+ function oneLine(text, max) {
7017
+ const flat = text.replace(/\s+/g, " ").trim();
7018
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
7019
+ }
7020
+ function formatDuration(ms) {
7021
+ const safe = Math.max(0, Math.round(ms));
7022
+ if (safe < 1000)
7023
+ return `${String(safe)}ms`;
7024
+ if (safe < 60000)
7025
+ return `${(safe / 1000).toFixed(1)}s`;
7026
+ const minutes = Math.floor(safe / 60000);
7027
+ const seconds = Math.round(safe % 60000 / 1000);
7028
+ return `${String(minutes)}m ${String(seconds).padStart(2, "0")}s`;
7029
+ }
7030
+
7031
+ // src/commands/helix/session.ts
6769
7032
  function dim(text) {
6770
- return isPlainOutput() ? text : chalk19.dim(text);
7033
+ return isPlainOutput() ? text : chalk20.dim(text);
6771
7034
  }
6772
7035
  function reattachHint(id, lastSeq) {
6773
7036
  const since = lastSeq > 0 ? ` --since ${String(lastSeq)}` : "";
@@ -6782,12 +7045,23 @@ function createSessionSink(isJson) {
6782
7045
  stderr: (text, seq) => {
6783
7046
  writeEvent("stderr", { seq, text });
6784
7047
  },
7048
+ rpc: (frame, seq) => {
7049
+ const parsed = parseFrame(frame);
7050
+ if (parsed === null)
7051
+ writeEvent("unrecognised", { seq, raw: frame });
7052
+ else
7053
+ writeEvent("rpc", { seq, frame: parsed });
7054
+ },
7055
+ unrecognised: (raw, seq) => {
7056
+ writeEvent("unrecognised", { seq, raw });
7057
+ },
6785
7058
  notice: (message) => {
6786
7059
  process.stderr.write(`${JSON.stringify({ type: "notice", message })}
6787
7060
  `);
6788
7061
  }
6789
7062
  };
6790
7063
  }
7064
+ const feed = new FrameFeed;
6791
7065
  return {
6792
7066
  stdout: (text) => {
6793
7067
  process.stdout.write(text);
@@ -6795,12 +7069,33 @@ function createSessionSink(isJson) {
6795
7069
  stderr: (text) => {
6796
7070
  process.stderr.write(text);
6797
7071
  },
7072
+ rpc: (frame, seq) => {
7073
+ const line = feed.render(frame, seq);
7074
+ if (line !== null)
7075
+ process.stderr.write(`${line}
7076
+ `);
7077
+ },
7078
+ unrecognised: (raw, seq) => {
7079
+ const line = feed.renderUnrecognised(raw, seq);
7080
+ if (line !== null)
7081
+ process.stderr.write(`${line}
7082
+ `);
7083
+ },
6798
7084
  notice: (message) => {
6799
7085
  process.stderr.write(`${dim(`[mutagent] ${message}`)}
6800
7086
  `);
6801
7087
  }
6802
7088
  };
6803
7089
  }
7090
+ function parseFrame(text) {
7091
+ try {
7092
+ const parsed = JSON.parse(text);
7093
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
7094
+ return parsed;
7095
+ }
7096
+ } catch {}
7097
+ return null;
7098
+ }
6804
7099
  function writeEvent(type, fields) {
6805
7100
  process.stdout.write(`${JSON.stringify({ type, ...fields })}
6806
7101
  `);
@@ -6874,13 +7169,17 @@ function parseSince(raw) {
6874
7169
 
6875
7170
  // src/commands/helix/spawn.ts
6876
7171
  var ARCHES = ["x86_64", "arm64"];
7172
+ var EGRESS_POLICIES = ["deny", "allowlist", "allow"];
7173
+ var EGRESS_KINDS = ["port", "cidr", "domain"];
6877
7174
  function registerSpawnCommand(parent) {
6878
- 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", `
7175
+ 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("--egress <policy>", `Outbound network policy: ${EGRESS_POLICIES.join(" | ")} (default: deny)`).option("--egress-allow <rule>", "Allowance for --egress allowlist, as kind:value (repeatable)", collectRule, []).option("--detach", "Spawn without attaching — print the ID and return").addHelpText("after", `
6879
7176
  Examples:
6880
- ${chalk20.dim("$")} mutagent helix spawn --image alpine:3.20
6881
- ${chalk20.dim("$")} mutagent helix spawn --image ghcr.io/example/helix:latest --arch x86_64
6882
- ${chalk20.dim("$")} mutagent helix spawn --image alpine:3.20 --detach --json
6883
- ${chalk20.dim("$")} mutagent helix spawn --image alpine:3.20 --provider docker --env staging
7177
+ ${chalk21.dim("$")} mutagent helix spawn --image alpine:3.20
7178
+ ${chalk21.dim("$")} mutagent helix spawn --image ghcr.io/example/helix:latest --arch x86_64
7179
+ ${chalk21.dim("$")} mutagent helix spawn --image alpine:3.20 --detach --json
7180
+ ${chalk21.dim("$")} mutagent helix spawn --image alpine:3.20 --provider docker --env staging
7181
+ ${chalk21.dim("$")} mutagent helix spawn --image <helix-image> --egress allow ${chalk21.dim("Required for a Helix session")}
7182
+ ${chalk21.dim("$")} mutagent helix spawn --image <ref> --egress allowlist --egress-allow domain:api.anthropic.com
6884
7183
 
6885
7184
  --image is required. A sandbox is spawned from a DEFINITION, and the definition
6886
7185
  names its own image — there is no default the CLI could invent for you. To run
@@ -6896,11 +7195,37 @@ the image was built elsewhere.
6896
7195
  DIFFERENT axis from --provider, which selects the backend. Secrets never travel
6897
7196
  on the command line.
6898
7197
 
7198
+ NETWORK EGRESS IS DENIED BY DEFAULT, AND THAT BREAKS HELIX SESSIONS:
7199
+ A sandbox runs code we did not write, so the safe default is the default:
7200
+ nothing reaches out. That is right for a plain 'exec' box, which needs no
7201
+ network at all.
7202
+
7203
+ It is WRONG for a Helix session, for two independent reasons — either alone
7204
+ is enough to sink it:
7205
+ 1. Under deny the container runs with --network none. Docker accepts -P and
7206
+ publishes nothing, so the in-box server on :8080 is unreachable and
7207
+ 'mutagent helix session start' fails with "Helix is not reachable".
7208
+ 2. An agent turn has to reach a model provider regardless.
7209
+
7210
+ So: ${chalk21.bold("--egress allow")} before 'helix session start'. Without it the failure
7211
+ presents as an unreachable box, which reads like a broken image rather than a
7212
+ policy you chose.
7213
+
7214
+ ${chalk21.bold("--egress allowlist")} narrows that to named allowances, each given explicitly
7215
+ as kind:value — 'domain:api.anthropic.com', 'port:443', 'cidr:10.0.0.0/8'. The
7216
+ kind is never inferred from the value. An allowlist with no rules denies
7217
+ everything, so it is refused rather than silently walling the box off; it also
7218
+ needs a provider with egress control, which not every backend has.
7219
+
6899
7220
  Without --detach this spawns and immediately attaches, so the common path is a
6900
7221
  single command. Ctrl-C then DETACHES — the sandbox keeps running and the
6901
7222
  re-attach command is printed. Use 'mutagent helix rm <id> --force' to destroy it.
6902
7223
 
6903
7224
  AI Agent Directive:
7225
+ Pass --egress allow whenever the box is for a Helix session — 'mutagent helix
7226
+ session start' CANNOT reach the in-box server under the default deny policy,
7227
+ and the error it raises names the box, not the policy. A box you only 'exec'
7228
+ in needs no egress.
6904
7229
  Prefer --detach --json for automation: it returns one JSON object with the
6905
7230
  sandbox id, so you can decide what to run before committing to a live stream.
6906
7231
  Without --detach, --json output is NDJSON and the final line is the summary.
@@ -6959,8 +7284,49 @@ function buildDefinition(options) {
6959
7284
  definition.provider = options.provider;
6960
7285
  if (options.env)
6961
7286
  definition.environment = options.env;
7287
+ const network = resolveNetwork(options.egress, options.egressAllow ?? []);
7288
+ if (network)
7289
+ definition.network = network;
6962
7290
  return definition;
6963
7291
  }
7292
+ function resolveNetwork(requested, rules) {
7293
+ if (requested === undefined) {
7294
+ if (rules.length > 0) {
7295
+ throw new MutagentError("INVALID_ARGUMENTS", "--egress-allow only means something with --egress allowlist.", "Add --egress allowlist, or drop the allowances and use --egress allow.");
7296
+ }
7297
+ return;
7298
+ }
7299
+ const policy = requested.trim();
7300
+ if (!EGRESS_POLICIES.includes(policy)) {
7301
+ throw new MutagentError("INVALID_ARGUMENTS", `--egress must be one of ${EGRESS_POLICIES.join(", ")}, got "${requested}".`, "Use --egress allow to drive a Helix session; the default, deny, leaves the box with no network at all.");
7302
+ }
7303
+ if (policy !== "allowlist") {
7304
+ if (rules.length > 0) {
7305
+ throw new MutagentError("INVALID_ARGUMENTS", `--egress-allow is only read under "allowlist", so under "${policy}" the allowances would be silently discarded.`, `Use --egress allowlist to apply them, or drop them to keep --egress ${policy}.`);
7306
+ }
7307
+ return { egress: policy };
7308
+ }
7309
+ if (rules.length === 0) {
7310
+ throw new MutagentError("INVALID_ARGUMENTS", "An allowlist with no allowances denies everything — the same wall as --egress deny, under a name that says otherwise.", `Name what may be reached: --egress-allow domain:api.anthropic.com
7311
+ Or open it fully: --egress allow`);
7312
+ }
7313
+ return { egress: "allowlist", allow: rules.map(parseRule) };
7314
+ }
7315
+ function collectRule(value, previous) {
7316
+ return [...previous, value];
7317
+ }
7318
+ function parseRule(raw) {
7319
+ const separator = raw.indexOf(":");
7320
+ const kind = separator === -1 ? "" : raw.slice(0, separator).trim();
7321
+ const value = separator === -1 ? "" : raw.slice(separator + 1).trim();
7322
+ if (!EGRESS_KINDS.includes(kind) || value === "") {
7323
+ throw new MutagentError("INVALID_ARGUMENTS", `--egress-allow must be <kind>:<value>, got "${raw}".`, `kind is one of ${EGRESS_KINDS.join(", ")}. Examples:
7324
+ --egress-allow domain:api.anthropic.com
7325
+ --egress-allow port:443
7326
+ --egress-allow cidr:10.0.0.0/8`);
7327
+ }
7328
+ return { kind, value };
7329
+ }
6964
7330
  function resolveArch(requested) {
6965
7331
  if (requested !== undefined) {
6966
7332
  const value = requested.trim();
@@ -6986,10 +7352,10 @@ async function createSandbox(definition, isJson) {
6986
7352
  const spinner = ora2(`Spawning ${definition.image} (${definition.arch})...`).start();
6987
7353
  try {
6988
7354
  const sandbox = await spawnSandbox(definition);
6989
- spinner.succeed(chalk20.green(`Sandbox ${sandbox.id} spawned`));
7355
+ spinner.succeed(chalk21.green(`Sandbox ${sandbox.id} spawned`));
6990
7356
  return sandbox;
6991
7357
  } catch (error) {
6992
- spinner.fail(chalk20.red("Spawn failed"));
7358
+ spinner.fail(chalk21.red("Spawn failed"));
6993
7359
  throw error;
6994
7360
  }
6995
7361
  }
@@ -7024,15 +7390,15 @@ function reportDetached(sandbox, id, output, isJson) {
7024
7390
  }
7025
7391
 
7026
7392
  // src/commands/helix/attach.ts
7027
- import chalk21 from "chalk";
7393
+ import chalk22 from "chalk";
7028
7394
  init_errors();
7029
7395
  function registerAttachCommand(parent) {
7030
7396
  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", `
7031
7397
  Examples:
7032
- ${chalk21.dim("$")} mutagent helix attach sbx_123
7033
- ${chalk21.dim("$")} mutagent helix attach sbx_123 --since 412
7034
- ${chalk21.dim("$")} mutagent helix attach sbx_123 --json | tail -1
7035
- ${chalk21.dim("$")} mutagent helix attach sbx_123 > session.log
7398
+ ${chalk22.dim("$")} mutagent helix attach sbx_123
7399
+ ${chalk22.dim("$")} mutagent helix attach sbx_123 --since 412
7400
+ ${chalk22.dim("$")} mutagent helix attach sbx_123 --json | tail -1
7401
+ ${chalk22.dim("$")} mutagent helix attach sbx_123 > session.log
7036
7402
 
7037
7403
  Ctrl-C detaches. The sandbox keeps running and the exact re-attach command,
7038
7404
  with the resume point filled in, is printed to stderr. Only 'mutagent helix rm <id> --force'
@@ -7081,16 +7447,244 @@ AI Agent Directive:
7081
7447
  });
7082
7448
  }
7083
7449
 
7450
+ // src/commands/helix/send.ts
7451
+ import chalk23 from "chalk";
7452
+ import { randomUUID as randomUUID3 } from "crypto";
7453
+ init_errors();
7454
+ var COMMAND_TYPES = ["prompt", "steer", "follow_up", "abort"];
7455
+ var NEEDS_MESSAGE = new Set(["prompt", "steer", "follow_up"]);
7456
+ function registerSendCommand(parent) {
7457
+ parent.command("send").description("Send a command line into a running session").argument("<id>", "Sandbox ID").argument("[message...]", "What to say to the agent").requiredOption("--session <id>", "Session ID inside the sandbox").option("--type <type>", `Command verb: ${COMMAND_TYPES.join(" | ")} (default: prompt)`).option("--line <json>", "Send this exact JSON line instead of building one").option("--message-id <id>", "Correlation id to use instead of a generated one").addHelpText("after", `
7458
+ Examples:
7459
+ ${chalk23.dim("$")} mutagent helix send sbx_123 --session s1 "also check the refresh path"
7460
+ ${chalk23.dim("$")} mutagent helix send sbx_123 --session s1 --type steer "stop and re-read the spec"
7461
+ ${chalk23.dim("$")} mutagent helix send sbx_123 --session s1 --type abort
7462
+ ${chalk23.dim("$")} mutagent helix send sbx_123 --session s1 --line '{"type":"prompt","id":"p7","message":"go"}'
7463
+ ${chalk23.dim("$")} mutagent helix send sbx_123 --session s1 "run the tests" --json
7464
+
7465
+ TWO FORMS:
7466
+ ${chalk23.bold("The friendly form")} — a --type and a message. This command builds the JSON:
7467
+ {"type":"prompt","id":"<generated>","message":"<your message>"}. Use it for
7468
+ everything the four verbs cover.
7469
+ ${chalk23.bold("--line")} — one raw JSON object, passed through byte for byte. It exists for
7470
+ a command shape this CLI does not know about yet. It is validated as a single
7471
+ JSON object before anything is sent, but its CONTENTS are the harness's
7472
+ business, not this CLI's. Do not combine it with --type or a message.
7473
+
7474
+ THE VERBS:
7475
+ prompt A new instruction to a settled agent.
7476
+ steer Redirect the turn already running.
7477
+ follow_up Continue from what the agent just said.
7478
+ abort Stop the current turn. Takes no message.
7479
+
7480
+ Sending is not watching. A 200 means the line was written to the session; the
7481
+ agent's reply arrives on the event stream as a 'response' frame carrying the id
7482
+ printed here. Attach in another terminal to see it:
7483
+ ${chalk23.dim("$")} mutagent helix attach sbx_123
7484
+
7485
+ A session that is no longer running is refused rather than silently accepted —
7486
+ 'mutagent helix ls' shows what is still up.
7487
+
7488
+ AI Agent Directive:
7489
+ --json returns ONE object: { success, sandboxId, sessionId, type, id, line,
7490
+ accepted, _links }. 'accepted' means DELIVERED, not answered — the answer is a
7491
+ 'response' frame with the same id on 'mutagent helix attach <id> --json'.
7492
+ Attach BEFORE sending if you need the reply; this call does not wait for one.
7493
+ Use the friendly form (--type + message). Reserve --line for a command shape
7494
+ the four verbs cannot express.
7495
+ `).action(async (id, messageParts, options) => {
7496
+ const isJson = getJsonFlag(parent);
7497
+ const output = new OutputFormatter(isJson ? "json" : "table");
7498
+ try {
7499
+ const sessionId = options.session?.trim() ?? "";
7500
+ if (sessionId === "") {
7501
+ throw new MutagentError("MISSING_ARGUMENTS", "--session is required — a sandbox can hold more than one session.", `Run: mutagent helix send ${id} --session <session-id> "<message>"`);
7502
+ }
7503
+ const built = buildLine(messageParts.join(" ").trim(), options);
7504
+ const result = await postInput(id, { sessionId, line: built.line });
7505
+ if (isJson) {
7506
+ output.output({
7507
+ success: true,
7508
+ sandboxId: id,
7509
+ sessionId,
7510
+ type: built.type,
7511
+ id: built.id,
7512
+ line: built.line,
7513
+ accepted: result.accepted,
7514
+ _links: sandboxLinks(id),
7515
+ _directive: {
7516
+ instruction: "The line was delivered, not answered. The reply is a `response` frame with this id on the event stream.",
7517
+ next: [`mutagent helix attach ${id} --json`]
7518
+ }
7519
+ });
7520
+ return;
7521
+ }
7522
+ output.success(`Sent ${built.type} to session ${sessionId}`);
7523
+ console.error(chalk23.dim(` ${built.line}`));
7524
+ console.error(chalk23.dim(` The reply arrives on the stream, not here: mutagent helix attach ${id}`));
7525
+ } catch (error) {
7526
+ handleError(error, isJson);
7527
+ }
7528
+ });
7529
+ }
7530
+ function buildLine(message, options) {
7531
+ const raw = options.line;
7532
+ if (raw !== undefined) {
7533
+ if (message !== "" || options.type !== undefined) {
7534
+ throw new MutagentError("INVALID_ARGUMENTS", "--line is the whole command already — it cannot be combined with --type or a message.", `Send the raw line on its own, or drop --line and use the friendly form:
7535
+ --type prompt "<message>"`);
7536
+ }
7537
+ return { line: requireOneJsonObject(raw), type: readType(raw), id: null };
7538
+ }
7539
+ const type = options.type ?? "prompt";
7540
+ if (!COMMAND_TYPES.includes(type)) {
7541
+ throw new MutagentError("INVALID_ARGUMENTS", `Unknown command type "${options.type ?? ""}".`, `Use one of: ${COMMAND_TYPES.join(", ")}. For anything else, pass the raw JSON with --line.`);
7542
+ }
7543
+ if (message === "" && NEEDS_MESSAGE.has(type)) {
7544
+ throw new MutagentError("MISSING_ARGUMENTS", `A "${type}" carries a message — there is nothing to send without one.`, `Run: mutagent helix send <id> --session <session-id> --type ${type} "<message>"`);
7545
+ }
7546
+ if (message !== "" && !NEEDS_MESSAGE.has(type)) {
7547
+ throw new MutagentError("INVALID_ARGUMENTS", `A "${type}" takes no message, so the text would be silently discarded.`, `Drop the message, or send it separately as a prompt.`);
7548
+ }
7549
+ const id = options.messageId?.trim() ?? "";
7550
+ const correlationId = id === "" ? `cli_${randomUUID3().slice(0, 8)}` : id;
7551
+ const body = { type, id: correlationId };
7552
+ if (message !== "")
7553
+ body.message = message;
7554
+ return { line: JSON.stringify(body), type, id: correlationId };
7555
+ }
7556
+ function requireOneJsonObject(raw) {
7557
+ const trimmed = raw.trim();
7558
+ if (trimmed.includes(`
7559
+ `)) {
7560
+ throw new MutagentError("INVALID_ARGUMENTS", "--line must be ONE line — a newline inside it would be read as two commands.", "Send one command per call.");
7561
+ }
7562
+ let parsed;
7563
+ try {
7564
+ parsed = JSON.parse(trimmed);
7565
+ } catch {
7566
+ throw new MutagentError("INVALID_ARGUMENTS", "--line is not valid JSON.", `The harness reads one JSON object per line. Example:
7567
+ --line '{"type":"prompt","id":"p1","message":"go"}'`);
7568
+ }
7569
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7570
+ throw new MutagentError("INVALID_ARGUMENTS", "--line must be a JSON object, not an array or a bare value.", `Example:
7571
+ --line '{"type":"prompt","id":"p1","message":"go"}'`);
7572
+ }
7573
+ return trimmed;
7574
+ }
7575
+ function readType(raw) {
7576
+ try {
7577
+ const parsed = JSON.parse(raw);
7578
+ if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
7579
+ return parsed.type;
7580
+ }
7581
+ } catch {}
7582
+ return "raw";
7583
+ }
7584
+
7585
+ // src/commands/helix/session-start.ts
7586
+ import { Command as Command13 } from "commander";
7587
+ import chalk24 from "chalk";
7588
+ init_errors();
7589
+ function registerSessionCommand(parent) {
7590
+ const session = new Command13("session").description("Start and inspect Helix sessions inside a sandbox").addHelpText("after", `
7591
+ Examples:
7592
+ ${chalk24.dim("$")} mutagent helix session start sbx_123 --rpc
7593
+ ${chalk24.dim("$")} mutagent helix session start sbx_123
7594
+ ${chalk24.dim("$")} mutagent helix session start sbx_123 --rpc --json
7595
+
7596
+ A sandbox and a session are not the same thing:
7597
+ ${chalk24.bold("spawn")} gives you a BOX — a machine with nothing running in it.
7598
+ ${chalk24.bold("session start")} gives you an AGENT inside that box.
7599
+ ${chalk24.bold("attach")} WATCHES that agent.
7600
+ ${chalk24.bold("send")} TYPES INTO it.
7601
+ A box with no session is a normal state — that is what 'exec' runs against.
7602
+
7603
+ AI Agent Directive:
7604
+ Run 'mutagent helix session start <sandbox-id> --rpc --json' before 'send':
7605
+ a session id is required and there is no default one to guess.
7606
+ `);
7607
+ registerStart(session, parent);
7608
+ parent.addCommand(session);
7609
+ }
7610
+ function registerStart(session, root) {
7611
+ session.command("start").description("Start a Helix session inside an existing sandbox").argument("<id>", "Sandbox ID").option("--rpc", "Long-lived session you can send into (default: one-shot)").addHelpText("after", `
7612
+ Examples:
7613
+ ${chalk24.dim("$")} mutagent helix session start sbx_123 --rpc
7614
+ ${chalk24.dim("$")} mutagent helix session start sbx_123
7615
+ ${chalk24.dim("$")} mutagent helix session start sbx_123 --rpc --json
7616
+
7617
+ TWO MODES, AND ONLY ONE OF THEM LISTENS:
7618
+ ${chalk24.bold("--rpc")} A long-lived agent reading commands on its stdin. This is
7619
+ the mode 'mutagent helix send' writes into; without it there
7620
+ is nothing to write to and every send is refused.
7621
+ ${chalk24.dim("(default)")} One-shot. The session takes its turn and ends. Watchable
7622
+ with 'attach', not steerable.
7623
+
7624
+ The box must already exist — 'mutagent helix spawn --image <ref>' makes one, and
7625
+ 'mutagent helix ls' shows which are running. One sandbox can host several
7626
+ sessions, so starting one never replaces another.
7627
+
7628
+ The session id printed here is the handle for everything after it. Nothing else
7629
+ can produce it, so a session started and then forgotten can only be found again
7630
+ by attaching to the box and reading the stream.
7631
+
7632
+ AI Agent Directive:
7633
+ --json returns ONE object: { success, sandboxId, sessionId, status, mode,
7634
+ _links }. Capture 'sessionId' — 'mutagent helix send' requires it and there is
7635
+ no default. Use --rpc whenever you intend to send into the session; a one-shot
7636
+ session refuses input with a 409.
7637
+ `).action(async (id, options) => {
7638
+ const isJson = getJsonFlag(root);
7639
+ const output = new OutputFormatter(isJson ? "json" : "table");
7640
+ try {
7641
+ const mode = options.rpc === true ? "rpc" : "oneshot";
7642
+ const result = await startSession(id, { mode });
7643
+ const sessionId = requireSessionId(result, id);
7644
+ if (isJson) {
7645
+ output.output({
7646
+ success: true,
7647
+ sandboxId: id,
7648
+ sessionId,
7649
+ status: result.status,
7650
+ mode,
7651
+ _links: sandboxLinks(id),
7652
+ _directive: {
7653
+ instruction: mode === "rpc" ? "The session is live and reading its stdin. Attach to watch it; send to steer it." : "The session takes one turn and ends. Attach to watch it; it does not accept input.",
7654
+ next: mode === "rpc" ? [
7655
+ `mutagent helix attach ${id} --json`,
7656
+ `mutagent helix send ${id} --session ${sessionId} "<message>" --json`
7657
+ ] : [`mutagent helix attach ${id} --json`]
7658
+ }
7659
+ });
7660
+ return;
7661
+ }
7662
+ output.success(`Session ${sessionId} started in ${id} (${mode})`);
7663
+ console.error(chalk24.dim(` Watch it: mutagent helix attach ${id}`));
7664
+ if (mode === "rpc") {
7665
+ console.error(chalk24.dim(` Talk to it: mutagent helix send ${id} --session ${sessionId} "<message>"`));
7666
+ }
7667
+ } catch (error) {
7668
+ handleError(error, isJson);
7669
+ }
7670
+ });
7671
+ }
7672
+ function requireSessionId(result, sandboxId) {
7673
+ if (typeof result.sessionId === "string" && result.sessionId !== "")
7674
+ return result.sessionId;
7675
+ throw new MutagentError("INVALID_RESPONSE", "The server started a session but did not return a session id.", `The session may be running — watch it with: mutagent helix attach ${sandboxId}`);
7676
+ }
7677
+
7084
7678
  // src/commands/helix/run.ts
7085
- import chalk22 from "chalk";
7679
+ import chalk25 from "chalk";
7086
7680
  init_errors();
7087
7681
  function registerRunCommand(parent) {
7088
7682
  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").option("--no-stream", "Wait for the whole turn instead of watching it happen").addHelpText("after", `
7089
7683
  Examples:
7090
- ${chalk22.dim("$")} mutagent helix run "summarise the failing tests" --prompt "You are a test triage agent."
7091
- ${chalk22.dim("$")} mutagent helix run fix the flaky login spec --prompt "$(cat agent.md)"
7092
- ${chalk22.dim("$")} mutagent helix run "list open PRs" --prompt "You are a release assistant." --json
7093
- ${chalk22.dim("$")} mutagent helix run "explore the repo" --prompt "$(cat agent.md)" --keep
7684
+ ${chalk25.dim("$")} mutagent helix run "summarise the failing tests" --prompt "You are a test triage agent."
7685
+ ${chalk25.dim("$")} mutagent helix run fix the flaky login spec --prompt "$(cat agent.md)"
7686
+ ${chalk25.dim("$")} mutagent helix run "list open PRs" --prompt "You are a release assistant." --json
7687
+ ${chalk25.dim("$")} mutagent helix run "explore the repo" --prompt "$(cat agent.md)" --keep
7094
7688
 
7095
7689
  TWO PROMPTS, AND THEY ARE NOT THE SAME THING:
7096
7690
  --prompt WHO the agent is — its definition, its standing instructions.
@@ -7197,17 +7791,17 @@ async function streamAgentTurn(body, isJson, keep) {
7197
7791
  function registerExecCommand(parent) {
7198
7792
  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", `
7199
7793
  Examples:
7200
- ${chalk22.dim("$")} mutagent helix exec -- ls -la /workspace
7201
- ${chalk22.dim("$")} mutagent helix exec --sandbox sbx_123 -- bun test
7202
- ${chalk22.dim("$")} mutagent helix exec --sandbox sbx_123 --cwd /src -- git status
7203
- ${chalk22.dim("$")} mutagent helix exec --preset default -- uname -m --json
7794
+ ${chalk25.dim("$")} mutagent helix exec -- ls -la /workspace
7795
+ ${chalk25.dim("$")} mutagent helix exec --sandbox sbx_123 -- bun test
7796
+ ${chalk25.dim("$")} mutagent helix exec --sandbox sbx_123 --cwd /src -- git status
7797
+ ${chalk25.dim("$")} mutagent helix exec --preset default -- uname -m --json
7204
7798
 
7205
- Put ${chalk22.bold("--")} before the command. Everything after it is argv and is passed through
7799
+ Put ${chalk25.bold("--")} before the command. Everything after it is argv and is passed through
7206
7800
  untouched, so flags meant for your command are not read as flags for this one.
7207
7801
 
7208
7802
  ARGV, NOT A SHELL STRING. There is no shell in the sandbox unless you name one:
7209
- ${chalk22.dim("$")} mutagent helix exec -- sh -c 'ls | wc -l' ${chalk22.dim("a shell, explicitly")}
7210
- ${chalk22.dim("$")} mutagent helix exec -- 'ls | wc -l' ${chalk22.dim("looks for a file called 'ls | wc -l'")}
7803
+ ${chalk25.dim("$")} mutagent helix exec -- sh -c 'ls | wc -l' ${chalk25.dim("a shell, explicitly")}
7804
+ ${chalk25.dim("$")} mutagent helix exec -- 'ls | wc -l' ${chalk25.dim("looks for a file called 'ls | wc -l'")}
7211
7805
 
7212
7806
  WITH --sandbox runs in that box and streams as output is produced.
7213
7807
  WITHOUT --sandbox spawns a fresh box from a preset, runs, tears it down. That
@@ -7339,17 +7933,17 @@ function reportResult(result, output, isJson, context) {
7339
7933
  }
7340
7934
 
7341
7935
  // src/commands/helix/inventory.ts
7342
- import chalk23 from "chalk";
7936
+ import chalk26 from "chalk";
7343
7937
  init_errors();
7344
7938
  function registerLsCommand(parent) {
7345
7939
  parent.command("ls").description("List your sandboxes").addHelpText("after", `
7346
7940
  Examples:
7347
- ${chalk23.dim("$")} mutagent helix ls
7348
- ${chalk23.dim("$")} mutagent helix ls --json
7941
+ ${chalk26.dim("$")} mutagent helix ls
7942
+ ${chalk26.dim("$")} mutagent helix ls --json
7349
7943
 
7350
7944
  Listing is always workspace-scoped — these are the sandboxes in the workspace
7351
7945
  you are configured for, not every sandbox on the account. Change it with:
7352
- ${chalk23.dim("$")} mutagent config set workspace <workspace-id>
7946
+ ${chalk26.dim("$")} mutagent config set workspace <workspace-id>
7353
7947
 
7354
7948
  An empty list is a normal result, not an error: it means no sandbox is running.
7355
7949
 
@@ -7369,7 +7963,7 @@ AI Agent Directive:
7369
7963
  return;
7370
7964
  }
7371
7965
  if (sandboxes.length === 0) {
7372
- console.log(chalk23.gray("No sandboxes running."));
7966
+ console.log(chalk26.gray("No sandboxes running."));
7373
7967
  console.log("");
7374
7968
  console.log(" Start one: mutagent helix spawn --image <ref>");
7375
7969
  return;
@@ -7393,8 +7987,8 @@ function toRow(sandbox) {
7393
7987
  function registerPresetsCommand(parent) {
7394
7988
  parent.command("presets").description("List the presets a one-shot run can name").addHelpText("after", `
7395
7989
  Examples:
7396
- ${chalk23.dim("$")} mutagent helix presets
7397
- ${chalk23.dim("$")} mutagent helix presets --json
7990
+ ${chalk26.dim("$")} mutagent helix presets
7991
+ ${chalk26.dim("$")} mutagent helix presets --json
7398
7992
 
7399
7993
  A preset is a named definition on the backend. It is what lets 'helix run' and
7400
7994
  'helix exec' start something without you knowing an image reference, a registry
@@ -7415,7 +8009,7 @@ AI Agent Directive:
7415
8009
  return;
7416
8010
  }
7417
8011
  if (presets.length === 0) {
7418
- console.log(chalk23.gray("No presets configured on this backend."));
8012
+ console.log(chalk26.gray("No presets configured on this backend."));
7419
8013
  console.log("");
7420
8014
  console.log(" Spawn with an explicit image: mutagent helix spawn --image <ref>");
7421
8015
  return;
@@ -7435,15 +8029,15 @@ AI Agent Directive:
7435
8029
  function registerRmCommand(parent) {
7436
8030
  parent.command("rm").description("Destroy a sandbox").argument("<id>", "Sandbox ID").option("-f, --force", "Skip confirmation").addHelpText("after", `
7437
8031
  Examples:
7438
- ${chalk23.dim("$")} mutagent helix rm sbx_123 --force
7439
- ${chalk23.dim("$")} mutagent helix rm sbx_123 --json
8032
+ ${chalk26.dim("$")} mutagent helix rm sbx_123 --force
8033
+ ${chalk26.dim("$")} mutagent helix rm sbx_123 --json
7440
8034
 
7441
8035
  This is the ONLY command that destroys a sandbox — detaching, Ctrl-C and a
7442
8036
  closed terminal all leave it running. Removing a sandbox that is already gone
7443
8037
  succeeds, so a retried teardown is safe.
7444
8038
 
7445
- ${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.")}
7446
- ${chalk23.dim("Warning: any session running in the sandbox ends immediately and cannot be recovered.")}
8039
+ ${chalk26.dim("Note: --force is required. The CLI is non-interactive — confirm with the user via your native flow, then pass --force. --json auto-confirms.")}
8040
+ ${chalk26.dim("Warning: any session running in the sandbox ends immediately and cannot be recovered.")}
7447
8041
 
7448
8042
  AI Agent Directive:
7449
8043
  Destroying a sandbox ends any session running in it and cannot be undone.
@@ -7478,8 +8072,8 @@ Use --force to confirm: mutagent helix rm ${id} --force`);
7478
8072
  function registerTracesCommand(parent) {
7479
8073
  parent.command("traces").description("Show the spans captured for a sandbox").argument("<id>", "Sandbox ID").addHelpText("after", `
7480
8074
  Examples:
7481
- ${chalk23.dim("$")} mutagent helix traces sbx_123
7482
- ${chalk23.dim("$")} mutagent helix traces sbx_123 --json
8075
+ ${chalk26.dim("$")} mutagent helix traces sbx_123
8076
+ ${chalk26.dim("$")} mutagent helix traces sbx_123 --json
7483
8077
 
7484
8078
  Spans are what the session actually did — the record to read when the output
7485
8079
  alone does not explain the result. They are called spans, not traces, because
@@ -7503,7 +8097,7 @@ AI Agent Directive:
7503
8097
  return;
7504
8098
  }
7505
8099
  if (spans.length === 0) {
7506
- console.log(chalk23.gray(`No spans captured for sandbox ${id}.`));
8100
+ console.log(chalk26.gray(`No spans captured for sandbox ${id}.`));
7507
8101
  return;
7508
8102
  }
7509
8103
  output.output(spans.map((span) => {
@@ -7523,26 +8117,35 @@ AI Agent Directive:
7523
8117
 
7524
8118
  // src/commands/helix/index.ts
7525
8119
  function createHelixCommand() {
7526
- const helix = new Command13("helix").description("Run Helix sessions in a cloud sandbox").addHelpText("after", `
8120
+ const helix = new Command14("helix").description("Run Helix sessions in a cloud sandbox").addHelpText("after", `
7527
8121
  Examples:
7528
- ${chalk24.dim("$")} mutagent helix run "fix the failing test" --prompt "$(cat agent.md)"
7529
- ${chalk24.dim("$")} mutagent helix exec -- uname -m ${chalk24.dim("One command, fresh box")}
7530
- ${chalk24.dim("$")} mutagent helix presets ${chalk24.dim("What a one-shot can run on")}
7531
- ${chalk24.dim("$")} mutagent helix spawn --image alpine:3.20 ${chalk24.dim("Persistent box, then attach")}
7532
- ${chalk24.dim("$")} mutagent helix ls ${chalk24.dim("What is running")}
7533
- ${chalk24.dim("$")} mutagent helix exec --sandbox sbx_1 -- bun test ${chalk24.dim("Command in that box")}
7534
- ${chalk24.dim("$")} mutagent helix attach sbx_1 --since 412 ${chalk24.dim("Resume after a sequence number")}
7535
- ${chalk24.dim("$")} mutagent helix traces sbx_1 --json ${chalk24.dim("What the session actually did")}
7536
- ${chalk24.dim("$")} mutagent helix rm sbx_1 --force ${chalk24.dim("Destroy it")}
8122
+ ${chalk27.dim("$")} mutagent helix run "fix the failing test" --prompt "$(cat agent.md)"
8123
+ ${chalk27.dim("$")} mutagent helix exec -- uname -m ${chalk27.dim("One command, fresh box")}
8124
+ ${chalk27.dim("$")} mutagent helix presets ${chalk27.dim("What a one-shot can run on")}
8125
+ ${chalk27.dim("$")} mutagent helix spawn --image alpine:3.20 ${chalk27.dim("Persistent box, then attach")}
8126
+ ${chalk27.dim("$")} mutagent helix session start sbx_1 --rpc ${chalk27.dim("Start an agent inside that box")}
8127
+ ${chalk27.dim("$")} mutagent helix ls ${chalk27.dim("What is running")}
8128
+ ${chalk27.dim("$")} mutagent helix exec --sandbox sbx_1 -- bun test ${chalk27.dim("Command in that box")}
8129
+ ${chalk27.dim("$")} mutagent helix attach sbx_1 --since 412 ${chalk27.dim("Resume after a sequence number")}
8130
+ ${chalk27.dim("$")} mutagent helix send sbx_1 --session s1 "and now" ${chalk27.dim("Talk to a running session")}
8131
+ ${chalk27.dim("$")} mutagent helix traces sbx_1 --json ${chalk27.dim("What the session actually did")}
8132
+ ${chalk27.dim("$")} mutagent helix rm sbx_1 --force ${chalk27.dim("Destroy it")}
7537
8133
 
7538
8134
  Subcommands:
7539
- run, exec, spawn, ls, presets, attach, traces, rm
8135
+ run, exec, spawn, session, ls, presets, attach, send, traces, rm
7540
8136
 
7541
8137
  One-shot vs persistent:
7542
- ${chalk24.bold("run")} and ${chalk24.bold("exec")} (without --sandbox) start from a named PRESET, do one
8138
+ ${chalk27.bold("run")} and ${chalk27.bold("exec")} (without --sandbox) start from a named PRESET, do one
7543
8139
  thing and tear the sandbox down. Nothing to clean up, no image to know.
7544
- ${chalk24.bold("spawn")} takes an explicit --image and leaves a box running for you to
7545
- ${chalk24.bold("attach")} and ${chalk24.bold("exec --sandbox")} against until you ${chalk24.bold("rm")} it.
8140
+ ${chalk27.bold("spawn")} takes an explicit --image and leaves a box running for you to
8141
+ ${chalk27.bold("attach")} and ${chalk27.bold("exec --sandbox")} against until you ${chalk27.bold("rm")} it.
8142
+
8143
+ Boxes vs sessions vs watching vs steering:
8144
+ ${chalk27.bold("spawn")} gives you a BOX. ${chalk27.bold("session start")} gives you an AGENT inside that box.
8145
+ ${chalk27.bold("attach")} WATCHES that agent; it does not type into one. ${chalk27.bold("send")} TYPES INTO it,
8146
+ from any terminal holding the id — so a session can be redirected without the
8147
+ terminal that started it. A box with no session is normal: that is what
8148
+ ${chalk27.bold("exec")} runs against.
7546
8149
 
7547
8150
  Lifecycle:
7548
8151
  A spawned sandbox outlives your terminal. Ctrl-C, a dropped connection and a
@@ -7552,7 +8155,7 @@ Lifecycle:
7552
8155
 
7553
8156
  Access:
7554
8157
  Sandbox access is minted per WORKSPACE, so a workspace must be configured:
7555
- ${chalk24.dim("$")} mutagent config set workspace <workspace-id>
8158
+ ${chalk27.dim("$")} mutagent config set workspace <workspace-id>
7556
8159
 
7557
8160
  Output:
7558
8161
  Sandbox stdout goes to stdout, sandbox stderr goes to stderr, and anything
@@ -7581,6 +8184,8 @@ AI Agent Directive:
7581
8184
  registerLsCommand(helix);
7582
8185
  registerPresetsCommand(helix);
7583
8186
  registerAttachCommand(helix);
8187
+ registerSendCommand(helix);
8188
+ registerSessionCommand(helix);
7584
8189
  registerTracesCommand(helix);
7585
8190
  registerRmCommand(helix);
7586
8191
  return helix;
@@ -7694,7 +8299,7 @@ if (process.env.CLI_VERSION) {
7694
8299
  } catch {}
7695
8300
  }
7696
8301
  setCliVersion(cliVersion);
7697
- var program = new Command14;
8302
+ var program = new Command15;
7698
8303
  program.name("mutagent").description(`Mutagent CLI - command-line client for the Mutagent platform
7699
8304
 
7700
8305
  Documentation: https://docs.mutagent.io/cli
@@ -7704,61 +8309,61 @@ program.name("mutagent").description(`Mutagent CLI - command-line client for the
7704
8309
  });
7705
8310
  program.addHelpText("beforeAll", () => renderBanner());
7706
8311
  program.addHelpText("after", `
7707
- ${chalk25.bold.cyan("WORKFLOWS:")}
7708
- ${chalk25.bold("Setup")} mutagent login → mutagent init
7709
- ${chalk25.bold("Lifecycle Tools")} mutagent install helix ${chalk25.dim("(login-gated)")}
7710
- ${chalk25.bold("Feedback")} mutagent feedback send "<what happened>" --category <cli|helix|stage:<x>> ${chalk25.dim("[--session <id>] [--attach-transcript]")}
8312
+ ${chalk28.bold.cyan("WORKFLOWS:")}
8313
+ ${chalk28.bold("Setup")} mutagent login → mutagent init
8314
+ ${chalk28.bold("Lifecycle Tools")} mutagent install helix ${chalk28.dim("(login-gated)")}
8315
+ ${chalk28.bold("Feedback")} mutagent feedback send "<what happened>" --category <cli|helix|stage:<x>> ${chalk28.dim("[--session <id>] [--attach-transcript]")}
7711
8316
 
7712
- ${chalk25.dim("For CLI usage guidance for AI agents, see the Skill at")}
7713
- ${chalk25.cyan(".claude/skills/mutagent-cli/SKILL.md")}
8317
+ ${chalk28.dim("For CLI usage guidance for AI agents, see the Skill at")}
8318
+ ${chalk28.cyan(".claude/skills/mutagent-cli/SKILL.md")}
7714
8319
 
7715
- ${chalk25.yellow("Global flags:")}
7716
- -v, --version ${chalk25.dim("Print the CLI version")} ${chalk25.dim("(before a subcommand; after one it is the subcommand's)")}
8320
+ ${chalk28.yellow("Global flags:")}
8321
+ -v, --version ${chalk28.dim("Print the CLI version")} ${chalk28.dim("(before a subcommand; after one it is the subcommand's)")}
7717
8322
  --json --api-key <k> --endpoint <url> --non-interactive
7718
8323
 
7719
- ${chalk25.yellow("Non-Interactive Mode (CI/CD & Coding Agents):")}
7720
- export MUTAGENT_API_KEY=mt_... ${chalk25.dim("or")} --api-key mt_...
7721
- --json ${chalk25.dim("for structured output")} --non-interactive ${chalk25.dim("to disable prompts")}
8324
+ ${chalk28.yellow("Non-Interactive Mode (CI/CD & Coding Agents):")}
8325
+ export MUTAGENT_API_KEY=mt_... ${chalk28.dim("or")} --api-key mt_...
8326
+ --json ${chalk28.dim("for structured output")} --non-interactive ${chalk28.dim("to disable prompts")}
7722
8327
 
7723
- ${chalk25.yellow("Command Navigation:")}
7724
- mutagent login ${chalk25.dim("Login (browser OAuth — recommended)")}
7725
- mutagent auth status ${chalk25.dim("Check auth + workspace")}
7726
- mutagent init ${chalk25.dim("Initialize project (.mutagentrc.json)")}
7727
- mutagent workspaces list --json ${chalk25.dim("List workspaces (verify ID)")}
7728
- mutagent config set workspace <id> ${chalk25.dim("Set active workspace")}
7729
- mutagent usage --json ${chalk25.dim("Show account usage + provider status")}
8328
+ ${chalk28.yellow("Command Navigation:")}
8329
+ mutagent login ${chalk28.dim("Login (browser OAuth — recommended)")}
8330
+ mutagent auth status ${chalk28.dim("Check auth + workspace")}
8331
+ mutagent init ${chalk28.dim("Initialize project (.mutagentrc.json)")}
8332
+ mutagent workspaces list --json ${chalk28.dim("List workspaces (verify ID)")}
8333
+ mutagent config set workspace <id> ${chalk28.dim("Set active workspace")}
8334
+ mutagent usage --json ${chalk28.dim("Show account usage + provider status")}
7730
8335
 
7731
- mutagent providers list --json ${chalk25.dim("List configured BYOK providers")}
7732
- mutagent providers list --models ${chalk25.dim("See available models per provider")}
8336
+ mutagent providers list --json ${chalk28.dim("List configured BYOK providers")}
8337
+ mutagent providers list --models ${chalk28.dim("See available models per provider")}
7733
8338
 
7734
- mutagent helix run "<task>" --prompt "<who>" ${chalk25.dim("One Helix agent turn in a cloud sandbox")}
7735
- mutagent helix ls --json ${chalk25.dim("List running sandboxes")}
7736
- mutagent helix spawn --image <ref> ${chalk25.dim("Persistent sandbox you attach to")}
7737
- mutagent helix rm <id> --force ${chalk25.dim("Destroy a sandbox (the only thing that does)")}
8339
+ mutagent helix run "<task>" --prompt "<who>" ${chalk28.dim("One Helix agent turn in a cloud sandbox")}
8340
+ mutagent helix ls --json ${chalk28.dim("List running sandboxes")}
8341
+ mutagent helix spawn --image <ref> ${chalk28.dim("Persistent sandbox you attach to")}
8342
+ mutagent helix rm <id> --force ${chalk28.dim("Destroy a sandbox (the only thing that does)")}
7738
8343
 
7739
- mutagent install helix ${chalk25.dim("Install the ADL lifecycle conductor locally (login-gated)")}
7740
- mutagent install helix --version 1.2.3 ${chalk25.dim("Pin a version")}
7741
- mutagent install --help ${chalk25.dim("helix")}
8344
+ mutagent install helix ${chalk28.dim("Install the ADL lifecycle conductor locally (login-gated)")}
8345
+ mutagent install helix --version 1.2.3 ${chalk28.dim("Pin a version")}
8346
+ mutagent install --help ${chalk28.dim("helix")}
7742
8347
 
7743
- mutagent hooks --help ${chalk25.dim("Hook setup for Claude Code session telemetry upload")}
8348
+ mutagent hooks --help ${chalk28.dim("Hook setup for Claude Code session telemetry upload")}
7744
8349
 
7745
- ${chalk25.bold.red("Report Issues:")}
7746
- Hit a bug? Run: ${chalk25.cyan('mutagent feedback send "describe what went wrong" --category cli')}
7747
- Lifecycle-stage feedback: ${chalk25.cyan('mutagent feedback send "eval gate unclear" --category stage:evaluate')}
7748
- Link to a session: ${chalk25.cyan('mutagent feedback send "..." --session <session-id>')}
7749
- Attach your coding-agent session transcript: ${chalk25.cyan('mutagent feedback send "..." --attach-transcript')}
7750
- ${chalk25.dim("--category accepts: cli | helix | stage:<spec|build|evaluate|diagnose|optimize>")}
8350
+ ${chalk28.bold.red("Report Issues:")}
8351
+ Hit a bug? Run: ${chalk28.cyan('mutagent feedback send "describe what went wrong" --category cli')}
8352
+ Lifecycle-stage feedback: ${chalk28.cyan('mutagent feedback send "eval gate unclear" --category stage:evaluate')}
8353
+ Link to a session: ${chalk28.cyan('mutagent feedback send "..." --session <session-id>')}
8354
+ Attach your coding-agent session transcript: ${chalk28.cyan('mutagent feedback send "..." --attach-transcript')}
8355
+ ${chalk28.dim("--category accepts: cli | helix | stage:<spec|build|evaluate|diagnose|optimize>")}
7751
8356
 
7752
- ${chalk25.yellow("Directive System:")}
8357
+ ${chalk28.yellow("Directive System:")}
7753
8358
  Every --json response may include:
7754
- ${chalk25.bold("_directive.display")} Type tag — 'status_card' for card-kind directives (drives test/docs guards)
7755
- ${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)")}
7756
- ${chalk25.bold("_directive.instruction")} Next step for the agent (self-sufficient, no Skill required)
7757
- ${chalk25.bold("_directive.next")} Array of suggested follow-up commands
7758
- ${chalk25.bold("_links")} Dashboard/API URLs (format as markdown links)
7759
- ${chalk25.bold("_compat")} Compat metadata: cliVersion, skillVersion, skillMinCliVersion
7760
-
7761
- ${chalk25.yellow("AI Agent Rules (MANDATORY for coding agents):")}
8359
+ ${chalk28.bold("_directive.display")} Type tag — 'status_card' for card-kind directives (drives test/docs guards)
8360
+ ${chalk28.bold("_directive.renderedCard")} Pre-formatted card ${chalk28.red("(MUST echo verbatim in chat whenever this field exists — see SKILL.md Verbatim Card Display Protocol)")}
8361
+ ${chalk28.bold("_directive.instruction")} Next step for the agent (self-sufficient, no Skill required)
8362
+ ${chalk28.bold("_directive.next")} Array of suggested follow-up commands
8363
+ ${chalk28.bold("_links")} Dashboard/API URLs (format as markdown links)
8364
+ ${chalk28.bold("_compat")} Compat metadata: cliVersion, skillVersion, skillMinCliVersion
8365
+
8366
+ ${chalk28.yellow("AI Agent Rules (MANDATORY for coding agents):")}
7762
8367
  1. Login (two paths):
7763
8368
  - CI / fully automated: export MUTAGENT_API_KEY=mt_... then mutagent login --json
7764
8369
  - Helping a user onboard: mutagent login --browser --json
@@ -7768,13 +8373,13 @@ ${chalk25.yellow("AI Agent Rules (MANDATORY for coding agents):")}
7768
8373
  2. EVERY command MUST include --json (no exceptions)
7769
8374
  3. Run <command> --help BEFORE first use of any command
7770
8375
  4. Parse _directive.renderedCard and copy it into your CHAT RESPONSE verbatim
7771
- ${chalk25.red("HARD STOP")}: do NOT run further commands until the card is rendered in chat
8376
+ ${chalk28.red("HARD STOP")}: do NOT run further commands until the card is rendered in chat
7772
8377
  5. After mutagent init, verify workspace: mutagent workspaces list --json
7773
8378
  6. ALL user interaction via AskUserQuestion — CLI is non-interactive
7774
8379
  ${!hasCredentials() ? `
7775
- ` + chalk25.yellow(" Warning: Not authenticated. Run: mutagent login") + `
8380
+ ` + chalk28.yellow(" Warning: Not authenticated. Run: mutagent login") + `
7776
8381
  ` : ""}${!hasRcConfig() ? `
7777
- ` + chalk25.green(" Get started: mutagent init") + `
8382
+ ` + chalk28.green(" Get started: mutagent init") + `
7778
8383
  ` : ""}`);
7779
8384
  var rawArgs = process.argv.slice(2);
7780
8385
  if (isGlobalVersionFlag(rawArgs)) {
@@ -7815,5 +8420,5 @@ program.addCommand(createFeedbackCommand());
7815
8420
  program.addCommand(createTraceCommand());
7816
8421
  program.parse();
7817
8422
 
7818
- //# debugId=0D5785F3AE94E3BA64756E2164756E21
8423
+ //# debugId=9CA63CD3787CB08B64756E2164756E21
7819
8424
  //# sourceMappingURL=cli.js.map