@mutagent/cli 0.1.280 → 0.1.282

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";
@@ -6121,11 +6121,11 @@ If mutagent-cli is not installed: mutagent install helix`), isJson);
6121
6121
  }
6122
6122
 
6123
6123
  // src/commands/helix/index.ts
6124
- import { Command as Command13 } from "commander";
6125
- import chalk24 from "chalk";
6124
+ import { Command as Command14 } from "commander";
6125
+ import chalk27 from "chalk";
6126
6126
 
6127
6127
  // src/commands/helix/spawn.ts
6128
- import chalk20 from "chalk";
6128
+ import chalk21 from "chalk";
6129
6129
  init_errors();
6130
6130
 
6131
6131
  // src/lib/sandbox-api.ts
@@ -6408,6 +6408,20 @@ async function execSandbox(id, body) {
6408
6408
  sandboxId: id
6409
6409
  });
6410
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
+ }
6411
6425
  async function oneShotRun(body) {
6412
6426
  return requestJson("/api/sandbox/run", {
6413
6427
  method: "POST",
@@ -6543,7 +6557,7 @@ function canAnimate(isJson) {
6543
6557
  }
6544
6558
 
6545
6559
  // src/commands/helix/session.ts
6546
- import chalk19 from "chalk";
6560
+ import chalk20 from "chalk";
6547
6561
 
6548
6562
  // src/lib/sse.ts
6549
6563
  var DEFAULT_EVENT_NAME = "message";
@@ -6642,6 +6656,13 @@ async function runStreamSession(options) {
6642
6656
  });
6643
6657
  const detached = () => outcome("detached");
6644
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
+ };
6645
6666
  for (;; ) {
6646
6667
  if (isAborted())
6647
6668
  return detached();
@@ -6666,38 +6687,44 @@ async function runStreamSession(options) {
6666
6687
  for await (const event of readSseEvents(stream)) {
6667
6688
  attempt = 0;
6668
6689
  if (event.event === "stdout" || event.event === "stderr") {
6669
- const payload = parseOutputPayload(event.data);
6670
- if (!payload)
6690
+ const payload2 = parseOutputPayload(event.data);
6691
+ if (!payload2)
6671
6692
  continue;
6672
- if (payload.seq > lastSeq + 1 && lastSeq > 0) {
6673
- gaps += 1;
6674
- sink.notice(gapNotice(lastSeq, payload.seq));
6675
- }
6676
- lastSeq = Math.max(lastSeq, payload.seq);
6693
+ noteSeq(payload2.seq);
6677
6694
  if (event.event === "stdout")
6678
- sink.stdout(payload.text, payload.seq);
6695
+ sink.stdout(payload2.text, payload2.seq);
6679
6696
  else
6680
- sink.stderr(payload.text, payload.seq);
6697
+ sink.stderr(payload2.text, payload2.seq);
6681
6698
  continue;
6682
6699
  }
6683
6700
  if (event.event === "sandbox") {
6684
- const payload = parseSandboxRunPayload(event.data);
6685
- if (payload) {
6686
- sandbox = payload;
6687
- options.onSandbox?.(payload);
6701
+ const payload2 = parseSandboxRunPayload(event.data);
6702
+ if (payload2) {
6703
+ sandbox = payload2;
6704
+ options.onSandbox?.(payload2);
6688
6705
  }
6689
6706
  continue;
6690
6707
  }
6691
6708
  if (event.event === "result") {
6692
- const payload = parseSandboxResultPayload(event.data);
6693
- if (payload)
6694
- 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);
6695
6722
  continue;
6696
6723
  }
6697
6724
  if (event.event === "status") {
6698
- const payload = parseStatusPayload(event.data);
6699
- if (payload?.state === "exited") {
6700
- exitCode = payload.exitCode;
6725
+ const payload2 = parseStatusPayload(event.data);
6726
+ if (payload2?.state === "exited") {
6727
+ exitCode = payload2.exitCode;
6701
6728
  exited = true;
6702
6729
  break;
6703
6730
  }
@@ -6713,6 +6740,15 @@ async function runStreamSession(options) {
6713
6740
  }
6714
6741
  continue;
6715
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
+ }
6716
6752
  }
6717
6753
  } catch (error) {
6718
6754
  if (isAborted())
@@ -6775,8 +6811,226 @@ function delay(ms, signal) {
6775
6811
 
6776
6812
  // src/commands/helix/session.ts
6777
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
6778
7032
  function dim(text) {
6779
- return isPlainOutput() ? text : chalk19.dim(text);
7033
+ return isPlainOutput() ? text : chalk20.dim(text);
6780
7034
  }
6781
7035
  function reattachHint(id, lastSeq) {
6782
7036
  const since = lastSeq > 0 ? ` --since ${String(lastSeq)}` : "";
@@ -6791,12 +7045,23 @@ function createSessionSink(isJson) {
6791
7045
  stderr: (text, seq) => {
6792
7046
  writeEvent("stderr", { seq, text });
6793
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
+ },
6794
7058
  notice: (message) => {
6795
7059
  process.stderr.write(`${JSON.stringify({ type: "notice", message })}
6796
7060
  `);
6797
7061
  }
6798
7062
  };
6799
7063
  }
7064
+ const feed = new FrameFeed;
6800
7065
  return {
6801
7066
  stdout: (text) => {
6802
7067
  process.stdout.write(text);
@@ -6804,12 +7069,33 @@ function createSessionSink(isJson) {
6804
7069
  stderr: (text) => {
6805
7070
  process.stderr.write(text);
6806
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
+ },
6807
7084
  notice: (message) => {
6808
7085
  process.stderr.write(`${dim(`[mutagent] ${message}`)}
6809
7086
  `);
6810
7087
  }
6811
7088
  };
6812
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
+ }
6813
7099
  function writeEvent(type, fields) {
6814
7100
  process.stdout.write(`${JSON.stringify({ type, ...fields })}
6815
7101
  `);
@@ -6883,13 +7169,17 @@ function parseSince(raw) {
6883
7169
 
6884
7170
  // src/commands/helix/spawn.ts
6885
7171
  var ARCHES = ["x86_64", "arm64"];
7172
+ var EGRESS_POLICIES = ["deny", "allowlist", "allow"];
7173
+ var EGRESS_KINDS = ["port", "cidr", "domain"];
6886
7174
  function registerSpawnCommand(parent) {
6887
- 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", `
6888
7176
  Examples:
6889
- ${chalk20.dim("$")} mutagent helix spawn --image alpine:3.20
6890
- ${chalk20.dim("$")} mutagent helix spawn --image ghcr.io/example/helix:latest --arch x86_64
6891
- ${chalk20.dim("$")} mutagent helix spawn --image alpine:3.20 --detach --json
6892
- ${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
6893
7183
 
6894
7184
  --image is required. A sandbox is spawned from a DEFINITION, and the definition
6895
7185
  names its own image — there is no default the CLI could invent for you. To run
@@ -6905,11 +7195,37 @@ the image was built elsewhere.
6905
7195
  DIFFERENT axis from --provider, which selects the backend. Secrets never travel
6906
7196
  on the command line.
6907
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
+
6908
7220
  Without --detach this spawns and immediately attaches, so the common path is a
6909
7221
  single command. Ctrl-C then DETACHES — the sandbox keeps running and the
6910
7222
  re-attach command is printed. Use 'mutagent helix rm <id> --force' to destroy it.
6911
7223
 
6912
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.
6913
7229
  Prefer --detach --json for automation: it returns one JSON object with the
6914
7230
  sandbox id, so you can decide what to run before committing to a live stream.
6915
7231
  Without --detach, --json output is NDJSON and the final line is the summary.
@@ -6968,8 +7284,49 @@ function buildDefinition(options) {
6968
7284
  definition.provider = options.provider;
6969
7285
  if (options.env)
6970
7286
  definition.environment = options.env;
7287
+ const network = resolveNetwork(options.egress, options.egressAllow ?? []);
7288
+ if (network)
7289
+ definition.network = network;
6971
7290
  return definition;
6972
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
+ }
6973
7330
  function resolveArch(requested) {
6974
7331
  if (requested !== undefined) {
6975
7332
  const value = requested.trim();
@@ -6995,10 +7352,10 @@ async function createSandbox(definition, isJson) {
6995
7352
  const spinner = ora2(`Spawning ${definition.image} (${definition.arch})...`).start();
6996
7353
  try {
6997
7354
  const sandbox = await spawnSandbox(definition);
6998
- spinner.succeed(chalk20.green(`Sandbox ${sandbox.id} spawned`));
7355
+ spinner.succeed(chalk21.green(`Sandbox ${sandbox.id} spawned`));
6999
7356
  return sandbox;
7000
7357
  } catch (error) {
7001
- spinner.fail(chalk20.red("Spawn failed"));
7358
+ spinner.fail(chalk21.red("Spawn failed"));
7002
7359
  throw error;
7003
7360
  }
7004
7361
  }
@@ -7033,15 +7390,15 @@ function reportDetached(sandbox, id, output, isJson) {
7033
7390
  }
7034
7391
 
7035
7392
  // src/commands/helix/attach.ts
7036
- import chalk21 from "chalk";
7393
+ import chalk22 from "chalk";
7037
7394
  init_errors();
7038
7395
  function registerAttachCommand(parent) {
7039
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", `
7040
7397
  Examples:
7041
- ${chalk21.dim("$")} mutagent helix attach sbx_123
7042
- ${chalk21.dim("$")} mutagent helix attach sbx_123 --since 412
7043
- ${chalk21.dim("$")} mutagent helix attach sbx_123 --json | tail -1
7044
- ${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
7045
7402
 
7046
7403
  Ctrl-C detaches. The sandbox keeps running and the exact re-attach command,
7047
7404
  with the resume point filled in, is printed to stderr. Only 'mutagent helix rm <id> --force'
@@ -7090,16 +7447,244 @@ AI Agent Directive:
7090
7447
  });
7091
7448
  }
7092
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
+
7093
7678
  // src/commands/helix/run.ts
7094
- import chalk22 from "chalk";
7679
+ import chalk25 from "chalk";
7095
7680
  init_errors();
7096
7681
  function registerRunCommand(parent) {
7097
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", `
7098
7683
  Examples:
7099
- ${chalk22.dim("$")} mutagent helix run "summarise the failing tests" --prompt "You are a test triage agent."
7100
- ${chalk22.dim("$")} mutagent helix run fix the flaky login spec --prompt "$(cat agent.md)"
7101
- ${chalk22.dim("$")} mutagent helix run "list open PRs" --prompt "You are a release assistant." --json
7102
- ${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
7103
7688
 
7104
7689
  TWO PROMPTS, AND THEY ARE NOT THE SAME THING:
7105
7690
  --prompt WHO the agent is — its definition, its standing instructions.
@@ -7206,17 +7791,17 @@ async function streamAgentTurn(body, isJson, keep) {
7206
7791
  function registerExecCommand(parent) {
7207
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", `
7208
7793
  Examples:
7209
- ${chalk22.dim("$")} mutagent helix exec -- ls -la /workspace
7210
- ${chalk22.dim("$")} mutagent helix exec --sandbox sbx_123 -- bun test
7211
- ${chalk22.dim("$")} mutagent helix exec --sandbox sbx_123 --cwd /src -- git status
7212
- ${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
7213
7798
 
7214
- 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
7215
7800
  untouched, so flags meant for your command are not read as flags for this one.
7216
7801
 
7217
7802
  ARGV, NOT A SHELL STRING. There is no shell in the sandbox unless you name one:
7218
- ${chalk22.dim("$")} mutagent helix exec -- sh -c 'ls | wc -l' ${chalk22.dim("a shell, explicitly")}
7219
- ${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'")}
7220
7805
 
7221
7806
  WITH --sandbox runs in that box and streams as output is produced.
7222
7807
  WITHOUT --sandbox spawns a fresh box from a preset, runs, tears it down. That
@@ -7348,17 +7933,17 @@ function reportResult(result, output, isJson, context) {
7348
7933
  }
7349
7934
 
7350
7935
  // src/commands/helix/inventory.ts
7351
- import chalk23 from "chalk";
7936
+ import chalk26 from "chalk";
7352
7937
  init_errors();
7353
7938
  function registerLsCommand(parent) {
7354
7939
  parent.command("ls").description("List your sandboxes").addHelpText("after", `
7355
7940
  Examples:
7356
- ${chalk23.dim("$")} mutagent helix ls
7357
- ${chalk23.dim("$")} mutagent helix ls --json
7941
+ ${chalk26.dim("$")} mutagent helix ls
7942
+ ${chalk26.dim("$")} mutagent helix ls --json
7358
7943
 
7359
7944
  Listing is always workspace-scoped — these are the sandboxes in the workspace
7360
7945
  you are configured for, not every sandbox on the account. Change it with:
7361
- ${chalk23.dim("$")} mutagent config set workspace <workspace-id>
7946
+ ${chalk26.dim("$")} mutagent config set workspace <workspace-id>
7362
7947
 
7363
7948
  An empty list is a normal result, not an error: it means no sandbox is running.
7364
7949
 
@@ -7378,7 +7963,7 @@ AI Agent Directive:
7378
7963
  return;
7379
7964
  }
7380
7965
  if (sandboxes.length === 0) {
7381
- console.log(chalk23.gray("No sandboxes running."));
7966
+ console.log(chalk26.gray("No sandboxes running."));
7382
7967
  console.log("");
7383
7968
  console.log(" Start one: mutagent helix spawn --image <ref>");
7384
7969
  return;
@@ -7402,8 +7987,8 @@ function toRow(sandbox) {
7402
7987
  function registerPresetsCommand(parent) {
7403
7988
  parent.command("presets").description("List the presets a one-shot run can name").addHelpText("after", `
7404
7989
  Examples:
7405
- ${chalk23.dim("$")} mutagent helix presets
7406
- ${chalk23.dim("$")} mutagent helix presets --json
7990
+ ${chalk26.dim("$")} mutagent helix presets
7991
+ ${chalk26.dim("$")} mutagent helix presets --json
7407
7992
 
7408
7993
  A preset is a named definition on the backend. It is what lets 'helix run' and
7409
7994
  'helix exec' start something without you knowing an image reference, a registry
@@ -7424,7 +8009,7 @@ AI Agent Directive:
7424
8009
  return;
7425
8010
  }
7426
8011
  if (presets.length === 0) {
7427
- console.log(chalk23.gray("No presets configured on this backend."));
8012
+ console.log(chalk26.gray("No presets configured on this backend."));
7428
8013
  console.log("");
7429
8014
  console.log(" Spawn with an explicit image: mutagent helix spawn --image <ref>");
7430
8015
  return;
@@ -7444,15 +8029,15 @@ AI Agent Directive:
7444
8029
  function registerRmCommand(parent) {
7445
8030
  parent.command("rm").description("Destroy a sandbox").argument("<id>", "Sandbox ID").option("-f, --force", "Skip confirmation").addHelpText("after", `
7446
8031
  Examples:
7447
- ${chalk23.dim("$")} mutagent helix rm sbx_123 --force
7448
- ${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
7449
8034
 
7450
8035
  This is the ONLY command that destroys a sandbox — detaching, Ctrl-C and a
7451
8036
  closed terminal all leave it running. Removing a sandbox that is already gone
7452
8037
  succeeds, so a retried teardown is safe.
7453
8038
 
7454
- ${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.")}
7455
- ${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.")}
7456
8041
 
7457
8042
  AI Agent Directive:
7458
8043
  Destroying a sandbox ends any session running in it and cannot be undone.
@@ -7487,8 +8072,8 @@ Use --force to confirm: mutagent helix rm ${id} --force`);
7487
8072
  function registerTracesCommand(parent) {
7488
8073
  parent.command("traces").description("Show the spans captured for a sandbox").argument("<id>", "Sandbox ID").addHelpText("after", `
7489
8074
  Examples:
7490
- ${chalk23.dim("$")} mutagent helix traces sbx_123
7491
- ${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
7492
8077
 
7493
8078
  Spans are what the session actually did — the record to read when the output
7494
8079
  alone does not explain the result. They are called spans, not traces, because
@@ -7512,7 +8097,7 @@ AI Agent Directive:
7512
8097
  return;
7513
8098
  }
7514
8099
  if (spans.length === 0) {
7515
- console.log(chalk23.gray(`No spans captured for sandbox ${id}.`));
8100
+ console.log(chalk26.gray(`No spans captured for sandbox ${id}.`));
7516
8101
  return;
7517
8102
  }
7518
8103
  output.output(spans.map((span) => {
@@ -7532,26 +8117,35 @@ AI Agent Directive:
7532
8117
 
7533
8118
  // src/commands/helix/index.ts
7534
8119
  function createHelixCommand() {
7535
- 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", `
7536
8121
  Examples:
7537
- ${chalk24.dim("$")} mutagent helix run "fix the failing test" --prompt "$(cat agent.md)"
7538
- ${chalk24.dim("$")} mutagent helix exec -- uname -m ${chalk24.dim("One command, fresh box")}
7539
- ${chalk24.dim("$")} mutagent helix presets ${chalk24.dim("What a one-shot can run on")}
7540
- ${chalk24.dim("$")} mutagent helix spawn --image alpine:3.20 ${chalk24.dim("Persistent box, then attach")}
7541
- ${chalk24.dim("$")} mutagent helix ls ${chalk24.dim("What is running")}
7542
- ${chalk24.dim("$")} mutagent helix exec --sandbox sbx_1 -- bun test ${chalk24.dim("Command in that box")}
7543
- ${chalk24.dim("$")} mutagent helix attach sbx_1 --since 412 ${chalk24.dim("Resume after a sequence number")}
7544
- ${chalk24.dim("$")} mutagent helix traces sbx_1 --json ${chalk24.dim("What the session actually did")}
7545
- ${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")}
7546
8133
 
7547
8134
  Subcommands:
7548
- run, exec, spawn, ls, presets, attach, traces, rm
8135
+ run, exec, spawn, session, ls, presets, attach, send, traces, rm
7549
8136
 
7550
8137
  One-shot vs persistent:
7551
- ${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
7552
8139
  thing and tear the sandbox down. Nothing to clean up, no image to know.
7553
- ${chalk24.bold("spawn")} takes an explicit --image and leaves a box running for you to
7554
- ${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.
7555
8149
 
7556
8150
  Lifecycle:
7557
8151
  A spawned sandbox outlives your terminal. Ctrl-C, a dropped connection and a
@@ -7561,7 +8155,7 @@ Lifecycle:
7561
8155
 
7562
8156
  Access:
7563
8157
  Sandbox access is minted per WORKSPACE, so a workspace must be configured:
7564
- ${chalk24.dim("$")} mutagent config set workspace <workspace-id>
8158
+ ${chalk27.dim("$")} mutagent config set workspace <workspace-id>
7565
8159
 
7566
8160
  Output:
7567
8161
  Sandbox stdout goes to stdout, sandbox stderr goes to stderr, and anything
@@ -7590,6 +8184,8 @@ AI Agent Directive:
7590
8184
  registerLsCommand(helix);
7591
8185
  registerPresetsCommand(helix);
7592
8186
  registerAttachCommand(helix);
8187
+ registerSendCommand(helix);
8188
+ registerSessionCommand(helix);
7593
8189
  registerTracesCommand(helix);
7594
8190
  registerRmCommand(helix);
7595
8191
  return helix;
@@ -7703,7 +8299,7 @@ if (process.env.CLI_VERSION) {
7703
8299
  } catch {}
7704
8300
  }
7705
8301
  setCliVersion(cliVersion);
7706
- var program = new Command14;
8302
+ var program = new Command15;
7707
8303
  program.name("mutagent").description(`Mutagent CLI - command-line client for the Mutagent platform
7708
8304
 
7709
8305
  Documentation: https://docs.mutagent.io/cli
@@ -7713,61 +8309,61 @@ program.name("mutagent").description(`Mutagent CLI - command-line client for the
7713
8309
  });
7714
8310
  program.addHelpText("beforeAll", () => renderBanner());
7715
8311
  program.addHelpText("after", `
7716
- ${chalk25.bold.cyan("WORKFLOWS:")}
7717
- ${chalk25.bold("Setup")} mutagent login → mutagent init
7718
- ${chalk25.bold("Lifecycle Tools")} mutagent install helix ${chalk25.dim("(login-gated)")}
7719
- ${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]")}
7720
8316
 
7721
- ${chalk25.dim("For CLI usage guidance for AI agents, see the Skill at")}
7722
- ${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")}
7723
8319
 
7724
- ${chalk25.yellow("Global flags:")}
7725
- -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)")}
7726
8322
  --json --api-key <k> --endpoint <url> --non-interactive
7727
8323
 
7728
- ${chalk25.yellow("Non-Interactive Mode (CI/CD & Coding Agents):")}
7729
- export MUTAGENT_API_KEY=mt_... ${chalk25.dim("or")} --api-key mt_...
7730
- --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")}
7731
8327
 
7732
- ${chalk25.yellow("Command Navigation:")}
7733
- mutagent login ${chalk25.dim("Login (browser OAuth — recommended)")}
7734
- mutagent auth status ${chalk25.dim("Check auth + workspace")}
7735
- mutagent init ${chalk25.dim("Initialize project (.mutagentrc.json)")}
7736
- mutagent workspaces list --json ${chalk25.dim("List workspaces (verify ID)")}
7737
- mutagent config set workspace <id> ${chalk25.dim("Set active workspace")}
7738
- 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")}
7739
8335
 
7740
- mutagent providers list --json ${chalk25.dim("List configured BYOK providers")}
7741
- 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")}
7742
8338
 
7743
- mutagent helix run "<task>" --prompt "<who>" ${chalk25.dim("One Helix agent turn in a cloud sandbox")}
7744
- mutagent helix ls --json ${chalk25.dim("List running sandboxes")}
7745
- mutagent helix spawn --image <ref> ${chalk25.dim("Persistent sandbox you attach to")}
7746
- 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)")}
7747
8343
 
7748
- mutagent install helix ${chalk25.dim("Install the ADL lifecycle conductor locally (login-gated)")}
7749
- mutagent install helix --version 1.2.3 ${chalk25.dim("Pin a version")}
7750
- 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")}
7751
8347
 
7752
- 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")}
7753
8349
 
7754
- ${chalk25.bold.red("Report Issues:")}
7755
- Hit a bug? Run: ${chalk25.cyan('mutagent feedback send "describe what went wrong" --category cli')}
7756
- Lifecycle-stage feedback: ${chalk25.cyan('mutagent feedback send "eval gate unclear" --category stage:evaluate')}
7757
- Link to a session: ${chalk25.cyan('mutagent feedback send "..." --session <session-id>')}
7758
- Attach your coding-agent session transcript: ${chalk25.cyan('mutagent feedback send "..." --attach-transcript')}
7759
- ${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>")}
7760
8356
 
7761
- ${chalk25.yellow("Directive System:")}
8357
+ ${chalk28.yellow("Directive System:")}
7762
8358
  Every --json response may include:
7763
- ${chalk25.bold("_directive.display")} Type tag — 'status_card' for card-kind directives (drives test/docs guards)
7764
- ${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)")}
7765
- ${chalk25.bold("_directive.instruction")} Next step for the agent (self-sufficient, no Skill required)
7766
- ${chalk25.bold("_directive.next")} Array of suggested follow-up commands
7767
- ${chalk25.bold("_links")} Dashboard/API URLs (format as markdown links)
7768
- ${chalk25.bold("_compat")} Compat metadata: cliVersion, skillVersion, skillMinCliVersion
7769
-
7770
- ${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):")}
7771
8367
  1. Login (two paths):
7772
8368
  - CI / fully automated: export MUTAGENT_API_KEY=mt_... then mutagent login --json
7773
8369
  - Helping a user onboard: mutagent login --browser --json
@@ -7777,13 +8373,13 @@ ${chalk25.yellow("AI Agent Rules (MANDATORY for coding agents):")}
7777
8373
  2. EVERY command MUST include --json (no exceptions)
7778
8374
  3. Run <command> --help BEFORE first use of any command
7779
8375
  4. Parse _directive.renderedCard and copy it into your CHAT RESPONSE verbatim
7780
- ${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
7781
8377
  5. After mutagent init, verify workspace: mutagent workspaces list --json
7782
8378
  6. ALL user interaction via AskUserQuestion — CLI is non-interactive
7783
8379
  ${!hasCredentials() ? `
7784
- ` + chalk25.yellow(" Warning: Not authenticated. Run: mutagent login") + `
8380
+ ` + chalk28.yellow(" Warning: Not authenticated. Run: mutagent login") + `
7785
8381
  ` : ""}${!hasRcConfig() ? `
7786
- ` + chalk25.green(" Get started: mutagent init") + `
8382
+ ` + chalk28.green(" Get started: mutagent init") + `
7787
8383
  ` : ""}`);
7788
8384
  var rawArgs = process.argv.slice(2);
7789
8385
  if (isGlobalVersionFlag(rawArgs)) {
@@ -7824,5 +8420,5 @@ program.addCommand(createFeedbackCommand());
7824
8420
  program.addCommand(createTraceCommand());
7825
8421
  program.parse();
7826
8422
 
7827
- //# debugId=651725607524CF1464756E2164756E21
8423
+ //# debugId=9CA63CD3787CB08B64756E2164756E21
7828
8424
  //# sourceMappingURL=cli.js.map