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