@mutagent/cli 0.1.385 → 0.1.387

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.
Files changed (3) hide show
  1. package/dist/bin/cli.js +2511 -189
  2. package/dist/index.js +30 -2
  3. package/package.json +2 -2
package/dist/bin/cli.js CHANGED
@@ -16520,6 +16520,18 @@ var SECURE_FILE_MODE = 384;
16520
16520
  var init_secure_file = () => {};
16521
16521
 
16522
16522
  // src/lib/config.ts
16523
+ var exports_config = {};
16524
+ __export(exports_config, {
16525
+ setWorkspaceSelection: () => setWorkspaceSelection,
16526
+ saveFullCredentials: () => saveFullCredentials,
16527
+ saveCredentials: () => saveCredentials,
16528
+ recordKeyScope: () => recordKeyScope,
16529
+ loadConfig: () => loadConfig,
16530
+ hasCredentials: () => hasCredentials,
16531
+ getApiKey: () => getApiKey,
16532
+ envKeyReplacesFile: () => envKeyReplacesFile,
16533
+ clearCredentials: () => clearCredentials
16534
+ });
16523
16535
  import { cosmiconfigSync } from "cosmiconfig";
16524
16536
  import { z } from "zod";
16525
16537
  import { homedir as homedir2 } from "os";
@@ -16686,6 +16698,7 @@ __export(exports_organization_scope, {
16686
16698
  setWorkspaceFlag: () => setWorkspaceFlag,
16687
16699
  setOrgAssertion: () => setOrgAssertion,
16688
16700
  resolveKeyTenancy: () => resolveKeyTenancy,
16701
+ resetKeyScopeCache: () => resetKeyScopeCache,
16689
16702
  otherOrganizationHint: () => otherOrganizationHint,
16690
16703
  orgMismatch: () => orgMismatch,
16691
16704
  noWorkspaceSelected: () => noWorkspaceSelected,
@@ -16848,6 +16861,20 @@ async function probeKeyScope(apiKey, endpoint, workspaceNamed) {
16848
16861
  const fromFile = !envKeyReplacesFile();
16849
16862
  if (!fromFile && workspaceNamed && orgAssertion === undefined)
16850
16863
  return;
16864
+ const cacheKey = `${endpoint}
16865
+ ${apiKey}`;
16866
+ const cached = keySelfProbes.get(cacheKey);
16867
+ if (cached)
16868
+ return cached;
16869
+ const probe = readKeyScope(apiKey, endpoint, fromFile);
16870
+ keySelfProbes.set(cacheKey, probe);
16871
+ probe.catch(() => keySelfProbes.delete(cacheKey));
16872
+ return probe;
16873
+ }
16874
+ function resetKeyScopeCache() {
16875
+ keySelfProbes.clear();
16876
+ }
16877
+ async function readKeyScope(apiKey, endpoint, fromFile) {
16851
16878
  try {
16852
16879
  const self = await fetchKeySelf(apiKey, endpoint);
16853
16880
  if (fromFile)
@@ -16931,7 +16958,7 @@ async function findWorkspace(wanted, t) {
16931
16958
  const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
16932
16959
  throw otherOrganizationHint(wanted, t.organization, orgs);
16933
16960
  }
16934
- var TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag;
16961
+ var TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes;
16935
16962
  var init_organization_scope = __esm(() => {
16936
16963
  init_errors();
16937
16964
  init_config();
@@ -16942,6 +16969,7 @@ var init_organization_scope = __esm(() => {
16942
16969
  this.name = "TenancyError";
16943
16970
  }
16944
16971
  };
16972
+ keySelfProbes = new Map;
16945
16973
  });
16946
16974
 
16947
16975
  // src/lib/provider-request.ts
@@ -17728,7 +17756,7 @@ var init_service = __esm(() => {
17728
17756
  });
17729
17757
 
17730
17758
  // src/bin/cli.ts
17731
- import { Command as Command18 } from "commander";
17759
+ import { Command as Command23 } from "commander";
17732
17760
 
17733
17761
  // src/commands/agent/index.ts
17734
17762
  import { existsSync as existsSync3 } from "node:fs";
@@ -18332,9 +18360,9 @@ function handleAgentParseError(error3, json) {
18332
18360
  }
18333
18361
 
18334
18362
  // src/bin/cli.ts
18335
- import chalk38 from "chalk";
18336
- import { readFileSync as readFileSync14 } from "fs";
18337
- import { join as join21, dirname as dirname7 } from "path";
18363
+ import chalk47 from "chalk";
18364
+ import { readFileSync as readFileSync18 } from "fs";
18365
+ import { join as join22, dirname as dirname8 } from "path";
18338
18366
  import { fileURLToPath as fileURLToPath4 } from "url";
18339
18367
 
18340
18368
  // src/commands/auth.ts
@@ -18812,6 +18840,8 @@ async function performBrowserAuth(options, onStatusUpdate) {
18812
18840
  throw error3;
18813
18841
  }
18814
18842
  }
18843
+ } else if (process.env.MUTAGENT_TEST_MODE === "true") {
18844
+ console.log(`AUTH_URL:${authUrl}`);
18815
18845
  }
18816
18846
  console.log("");
18817
18847
  console.log(" Open this URL to authenticate:");
@@ -18917,10 +18947,13 @@ async function performLoginAction(opts) {
18917
18947
  useBrowserAuth = method === "browser";
18918
18948
  }
18919
18949
  if (useBrowserAuth) {
18920
- return loginWithBrowser(endpoint, output, wasFirstLogin, opts.org);
18950
+ return loginWithBrowser(endpoint, output, wasFirstLogin, shouldSkipBrowserOpen(isJson), opts.org);
18921
18951
  }
18922
18952
  return loginWithPastedKey(endpoint, output, wasFirstLogin);
18923
18953
  }
18954
+ function shouldSkipBrowserOpen(isJson) {
18955
+ return isJson || !process.stdout.isTTY;
18956
+ }
18924
18957
  function buildLoginJsonResponse(result) {
18925
18958
  return {
18926
18959
  success: true,
@@ -18984,11 +19017,14 @@ async function loginWithExistingKey(apiKey, endpoint, output, wasFirstLogin) {
18984
19017
  saveFullCredentials({ apiKey, endpoint, ...described });
18985
19018
  return loginResult(apiKey, endpoint, described, wasFirstLogin);
18986
19019
  }
18987
- async function loginWithBrowser(endpoint, output, wasFirstLogin, org) {
18988
- const spinner = ora2({ text: "Opening browser for authentication...", spinner: "dots" });
19020
+ async function loginWithBrowser(endpoint, output, wasFirstLogin, skipBrowserOpen, org) {
19021
+ const spinner = ora2({
19022
+ text: skipBrowserOpen ? "Waiting for the URL to be opened..." : "Opening browser for authentication...",
19023
+ spinner: "dots"
19024
+ });
18989
19025
  try {
18990
19026
  spinner.start();
18991
- const result = await performBrowserAuth({ endpoint, timeout: 300000, pollInterval: 2000, org }, (message) => {
19027
+ const result = await performBrowserAuth({ endpoint, timeout: 300000, pollInterval: 2000, skipBrowserOpen, org }, (message) => {
18992
19028
  spinner.text = message;
18993
19029
  });
18994
19030
  spinner.succeed("Authenticated successfully!");
@@ -19191,16 +19227,24 @@ function createLoginCommand() {
19191
19227
  const login = new Command3("login").description("Sign in to Mutagent and store the API key on this machine").option("--browser", "Sign in with the browser (skip the method prompt)").option("--non-interactive", "Skip the method prompt and use browser sign-in (with --json it also needs --browser)").option("--endpoint <url>", "API endpoint", "https://api.mutagent.io").addHelpText("after", `
19192
19228
  Examples:
19193
19229
  ${chalk5.dim("$")} mutagent login ${chalk5.dim("# In a terminal, choose browser or API key")}
19194
- ${chalk5.dim("$")} mutagent login --browser ${chalk5.dim("# Sign in with the browser")}
19230
+ ${chalk5.dim("$")} mutagent login --browser ${chalk5.dim("# Opens a real browser here; sign in or sign up")}
19195
19231
  ${chalk5.dim("$")} mutagent login --non-interactive ${chalk5.dim("# Browser sign-in without the method prompt (a person approves in the browser)")}
19232
+ ${chalk5.dim("$")} mutagent login --browser --non-interactive --json ${chalk5.dim("# Agent path: no browser opens here — see below")}
19196
19233
  ${chalk5.dim("$")} mutagent login --org <slug> ${chalk5.dim("# Sign in for another organization (preselected on the approval page)")}
19197
19234
  ${chalk5.dim("$")} MUTAGENT_API_KEY=mt_... mutagent login --json ${chalk5.dim("# Sign in with a key (CI)")}
19198
19235
 
19199
- Browser sign-in opens app.mutagent.io, where you sign in or sign up, finish onboarding
19236
+ Browser sign-in: app.mutagent.io is where you sign in or sign up, finish onboarding
19200
19237
  and approve this CLI. The CLI waits up to 5 minutes and saves the credentials.
19201
19238
  The key works in every workspace you are a member of in the approved organization, for
19202
19239
  30 days. The approved workspace is selected; switch with mutagent workspaces use <name>.
19203
19240
 
19241
+ At a real terminal (no --json, stdout is a TTY), --browser opens that page for you
19242
+ automatically. Under --json, or whenever stdout is not a TTY (a coding agent's
19243
+ captured output, a pipe, CI), this CLI never opens a browser on this machine — it
19244
+ only PRINTS the URL and keeps waiting. An agent must open that URL in ITS OWN
19245
+ browser (Playwright, browser-use, or handing it to the person) for the sign-in to
19246
+ complete.
19247
+
19204
19248
  When MUTAGENT_API_KEY is set, login skips the browser and validates the key.
19205
19249
 
19206
19250
  Environment:
@@ -28950,163 +28994,2412 @@ function printEntries(environment) {
28950
28994
  }
28951
28995
  }
28952
28996
 
28953
- // src/bin/cli.ts
28997
+ // src/commands/gateway/index.ts
28998
+ init_config();
28999
+ import { Command as Command22 } from "commander";
29000
+ import chalk46 from "chalk";
29001
+
29002
+ // src/commands/gateway/status.ts
29003
+ import chalk39 from "chalk";
29004
+
29005
+ // src/lib/gateway-api.ts
28954
29006
  init_config();
29007
+ init_known_secrets();
28955
29008
  init_organization_scope();
29009
+ class GatewayApiError extends Error {
29010
+ status;
29011
+ code;
29012
+ reason;
29013
+ fix;
29014
+ constructor(status, code, reason, fix) {
29015
+ super(reason);
29016
+ this.status = status;
29017
+ this.code = code;
29018
+ this.reason = reason;
29019
+ this.fix = fix;
29020
+ this.name = "GatewayApiError";
29021
+ }
29022
+ }
28956
29023
 
28957
- // src/lib/brand.ts
28958
- var BRAND = {
28959
- name: "MUTAGENT",
28960
- primary: "#7E47D7",
28961
- accent: "#45b8cc",
28962
- gradientFrom: "#45b8cc",
28963
- gradientTo: "#7E47D7"
28964
- };
28965
- var WORDMARK = [
28966
- " ███╗ ███╗ ██╗ ██╗ ████████╗ █████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗ ",
28967
- " ████╗ ████║ ██║ ██║ ╚══██╔══╝ ██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ",
28968
- " ██╔████╔██║ ██║ ██║ ██║ ███████║ ██║ ███╗ █████╗ ██╔██╗ ██║ ██║ ",
28969
- " ██║╚██╔╝██║ ██║ ██║ ██║ ██╔══██║ ██║ ██║ ██╔══╝ ██║╚██╗██║ ██║ ",
28970
- " ██║ ╚═╝ ██║ ╚██████╔╝ ██║ ██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚████║ ██║ ",
28971
- " ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═══╝ ╚═╝ "
28972
- ];
28973
- var WORDMARK_MIN_COLUMNS = 84;
28974
- function hexToRgb(hex) {
28975
- const n = parseInt(hex.slice(1), 16);
28976
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
29024
+ class GatewayNetworkError extends Error {
29025
+ endpoint;
29026
+ constructor(endpoint, cause) {
29027
+ super(`Could not reach the MutagenT API at ${endpoint}: ${cause instanceof Error ? cause.message : String(cause)}`);
29028
+ this.endpoint = endpoint;
29029
+ this.name = "GatewayNetworkError";
29030
+ }
28977
29031
  }
28978
- var lerp = (a, b, t) => Math.round(a + (b - a) * t);
28979
- function mix(from, to, t) {
28980
- return [lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)];
29032
+
29033
+ class GatewayConfigError extends Error {
29034
+ code;
29035
+ constructor(code) {
29036
+ super("No MutagenT API key configured.");
29037
+ this.code = code;
29038
+ this.name = "GatewayConfigError";
29039
+ }
28981
29040
  }
28982
- var ansi = (r, g, b) => `\x1B[38;2;${String(r)};${String(g)};${String(b)}m`;
28983
- var RESET = "\x1B[0m";
28984
- function gradientLine(line, from, to) {
28985
- const chars = Array.from(line);
28986
- const n = chars.length;
28987
- let out = "";
28988
- chars.forEach((ch, i) => {
28989
- if (ch === " ") {
28990
- out += ch;
28991
- return;
29041
+ function parseGatewayErrorBody(status, bodyText, statusText) {
29042
+ let parsed;
29043
+ try {
29044
+ parsed = JSON.parse(bodyText);
29045
+ } catch {
29046
+ parsed = undefined;
29047
+ }
29048
+ if (parsed && typeof parsed === "object") {
29049
+ const record3 = parsed;
29050
+ if (typeof record3.code === "string" && typeof record3.reason === "string") {
29051
+ if ("fix" in record3) {
29052
+ const rawFix = record3.fix;
29053
+ const fix = rawFix && typeof rawFix === "object" && typeof rawFix.action === "string" ? rawFix : null;
29054
+ return { code: record3.code, reason: record3.reason, fix };
29055
+ }
29056
+ return { code: record3.code, reason: record3.reason };
28992
29057
  }
28993
- const [r, g, b] = mix(from, to, n > 1 ? i / (n - 1) : 0);
28994
- out += ansi(r, g, b) + ch;
28995
- });
28996
- return out + RESET;
28997
- }
28998
- function hasTruecolor(env = process.env) {
28999
- return /truecolor|24bit/i.test(env.COLORTERM ?? "");
29058
+ if (typeof record3.error === "string" && typeof record3.message === "string") {
29059
+ return { code: record3.error.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "server_fault", reason: record3.message };
29060
+ }
29061
+ }
29062
+ return { code: "server_fault", reason: bodyText.slice(0, 300) || statusText || `Request failed with status ${String(status)}` };
29000
29063
  }
29001
- function detectBannerEnv(stream = process.stdout, env = process.env) {
29064
+ async function resolveGatewayConnection() {
29065
+ const apiKey = getApiKey();
29066
+ if (apiKey === undefined || apiKey === "")
29067
+ throw new GatewayConfigError("not_signed_in");
29068
+ const tenancy = await resolveKeyTenancy(apiKey);
29069
+ const workspaceId = tenancy.workspaceId?.trim();
29070
+ if (workspaceId === undefined || workspaceId === "")
29071
+ throw noWorkspaceSelected();
29072
+ const headers = { "x-api-key": apiKey, "Content-Type": "application/json" };
29073
+ if (tenancy.keyScope !== "workspace")
29074
+ headers["x-workspace-id"] = workspaceId;
29002
29075
  return {
29003
- isTTY: stream.isTTY === true,
29004
- columns: stream.columns ?? 80,
29005
- truecolor: hasTruecolor(env),
29006
- disabled: env.MUTAGENT_NO_BANNER === "1"
29076
+ endpoint: tenancy.endpoint,
29077
+ apiKey,
29078
+ workspaceId,
29079
+ headers,
29080
+ tenancy: {
29081
+ keyScope: tenancy.keyScope,
29082
+ workspaceId,
29083
+ workspaceName: tenancy.workspaceName,
29084
+ workspaceOrigin: tenancy.workspaceOrigin,
29085
+ organizationName: tenancy.organization?.name
29086
+ }
29007
29087
  };
29008
29088
  }
29009
- function renderBanner(env = detectBannerEnv()) {
29010
- if (env.disabled || !env.isTTY)
29011
- return "";
29012
- const from = hexToRgb(BRAND.gradientFrom);
29013
- const to = hexToRgb(BRAND.gradientTo);
29014
- const wide = env.columns >= WORDMARK_MIN_COLUMNS;
29015
- const art = wide ? WORDMARK : [` ${BRAND.name}`];
29016
- const lines = env.truecolor ? art.map((l) => gradientLine(l, from, to)) : art.map((l) => `${ansi(...from)}${l}${RESET}`);
29017
- return `
29018
- ${lines.join(`
29019
- `)}
29020
- `;
29021
- }
29022
-
29023
- // src/lib/global-version-flag.ts
29024
- var GLOBAL_VALUE_OPTIONS = new Set(["--api-key", "--endpoint", "--workspace", "--org"]);
29025
- function findSubcommandIndex(rawArgs) {
29026
- for (let i = 0;i < rawArgs.length; i++) {
29027
- const arg = rawArgs[i];
29028
- if (arg === undefined)
29029
- continue;
29030
- if (arg === "--")
29031
- return i + 1 < rawArgs.length ? i + 1 : -1;
29032
- if (arg.startsWith("-")) {
29033
- if (GLOBAL_VALUE_OPTIONS.has(arg))
29034
- i++;
29035
- continue;
29089
+ async function gatewayRequest(path, init = {}) {
29090
+ const connection = await resolveGatewayConnection();
29091
+ let response;
29092
+ try {
29093
+ response = await fetch(`${connection.endpoint}${path}`, {
29094
+ ...init,
29095
+ headers: { ...connection.headers, ...init.headers }
29096
+ });
29097
+ } catch (error3) {
29098
+ throw new GatewayNetworkError(connection.endpoint, error3);
29099
+ }
29100
+ if (response.status === 401)
29101
+ throw deadKeyError();
29102
+ if (!response.ok) {
29103
+ const bodyText = await response.text().catch(() => "");
29104
+ if (response.status === 403 && tenancyCodeOf(bodyText) === TENANCY_DENIED) {
29105
+ const denied = tenancyFailure(response.status, bodyText, connection.tenancy);
29106
+ if (denied)
29107
+ throw denied;
29036
29108
  }
29037
- return i;
29109
+ const { code, reason, fix } = parseGatewayErrorBody(response.status, bodyText, response.statusText);
29110
+ throw new GatewayApiError(response.status, code, reason, fix);
29111
+ }
29112
+ const text = await response.text();
29113
+ if (text.trim() === "")
29114
+ return {};
29115
+ try {
29116
+ return scrubKnownSuccessDeep(JSON.parse(text));
29117
+ } catch {
29118
+ throw new GatewayApiError(response.status, "server_fault", `The server returned a non-JSON response for ${path}.`);
29038
29119
  }
29039
- return -1;
29040
29120
  }
29041
- function isGlobalVersionFlag(rawArgs) {
29042
- const versionAt = rawArgs.findIndex((a) => a === "-v" || a === "--version");
29043
- if (versionAt === -1)
29044
- return false;
29045
- const subcommandAt = findSubcommandIndex(rawArgs);
29046
- return subcommandAt === -1 || versionAt < subcommandAt;
29121
+ function tenancyCodeOf(bodyText) {
29122
+ try {
29123
+ const parsed = JSON.parse(bodyText);
29124
+ if (parsed && typeof parsed === "object") {
29125
+ const code = parsed.code;
29126
+ return typeof code === "string" ? code : undefined;
29127
+ }
29128
+ } catch {}
29129
+ return;
29047
29130
  }
29048
- var ROOT_BOOLEAN_OPTIONS = new Set(["--json", "--non-interactive"]);
29049
- function commandPathIndexes(rawArgs, depth) {
29050
- const indexes = [];
29051
- let from = 0;
29052
- while (indexes.length < depth) {
29053
- const at = findSubcommandIndex(rawArgs.slice(from));
29054
- if (at === -1)
29131
+ function qs(params) {
29132
+ const entries = Object.entries(params).filter((entry) => entry[1] !== undefined && entry[1] !== "");
29133
+ if (entries.length === 0)
29134
+ return "";
29135
+ return `?${new URLSearchParams(entries).toString()}`;
29136
+ }
29137
+ async function listIntegrations() {
29138
+ const { workspaceId } = await resolveGatewayConnection();
29139
+ return gatewayRequest(`/api/gateway/integrations${qs({ workspaceId })}`);
29140
+ }
29141
+ async function startIntegration(provider, mode) {
29142
+ const { workspaceId } = await resolveGatewayConnection();
29143
+ const body = { workspaceId, returnTo: "close" };
29144
+ return gatewayRequest(`/api/gateway/integrations/${provider}/${mode}`, {
29145
+ method: "POST",
29146
+ body: JSON.stringify(body)
29147
+ });
29148
+ }
29149
+ async function getIntegrationAttempt(state) {
29150
+ return gatewayRequest(`/api/gateway/integrations/attempts/${encodeURIComponent(state)}`);
29151
+ }
29152
+ async function disconnectIntegration(provider) {
29153
+ const { workspaceId } = await resolveGatewayConnection();
29154
+ return gatewayRequest(`/api/gateway/integrations/${provider}${qs({ workspaceId })}`, { method: "DELETE" });
29155
+ }
29156
+ async function listRepositories(cursor) {
29157
+ const { workspaceId } = await resolveGatewayConnection();
29158
+ return gatewayRequest(`/api/gateway/integrations/github/repositories${qs({ workspaceId, cursor })}`);
29159
+ }
29160
+ async function listAllRepositories(maxPages = 20) {
29161
+ const items = [];
29162
+ let cursor;
29163
+ for (let page = 0;page < maxPages; page += 1) {
29164
+ const response = await listRepositories(cursor);
29165
+ items.push(...response.items);
29166
+ if (response.nextCursor === null)
29055
29167
  break;
29056
- indexes.push(from + at);
29057
- from += at + 1;
29168
+ cursor = response.nextCursor;
29058
29169
  }
29059
- return indexes;
29170
+ return items;
29171
+ }
29172
+ async function getDefaultRepository() {
29173
+ const { workspaceId } = await resolveGatewayConnection();
29174
+ return gatewayRequest(`/api/gateway/integrations/github/default-repository${qs({ workspaceId })}`);
29175
+ }
29176
+ async function setDefaultRepository(repositoryId) {
29177
+ const { workspaceId } = await resolveGatewayConnection();
29178
+ return gatewayRequest("/api/gateway/integrations/github/default-repository", {
29179
+ method: "PUT",
29180
+ body: JSON.stringify({ workspaceId, repositoryId })
29181
+ });
29182
+ }
29183
+ async function startTestRun(key, repositoryId) {
29184
+ return gatewayRequest("/api/gateway/test-run", {
29185
+ method: "POST",
29186
+ body: JSON.stringify({ key, repositoryId })
29187
+ });
29188
+ }
29189
+ async function getEventRun(eventId) {
29190
+ return gatewayRequest(`/api/gateway/events/${encodeURIComponent(eventId)}/run`);
29191
+ }
29192
+ async function emitEvent(key, kind, payload) {
29193
+ return gatewayRequest("/api/gateway/emit", {
29194
+ method: "POST",
29195
+ body: JSON.stringify({ key, kind, ...payload === undefined ? {} : { payload } })
29196
+ });
29197
+ }
29198
+ async function findRepositoryByName(name) {
29199
+ const items = await listAllRepositories();
29200
+ return items.find((item) => item.name === name);
29060
29201
  }
29061
29202
 
29062
- // src/bin/cli.ts
29203
+ // src/lib/gateway-envelope.ts
29204
+ import chalk38 from "chalk";
29205
+ init_known_secrets();
29063
29206
  init_errors();
29064
29207
 
29065
- // src/commands/helix/api-key-guard.ts
29066
- init_errors();
29067
- var FORWARDING_VERBS = [
29068
- { path: ["helix"], label: "mutagent helix" },
29069
- { path: ["sandbox", "exec"], label: "mutagent sandbox exec" }
29070
- ];
29071
- function rootValueOption(arg) {
29072
- const eq = arg.indexOf("=");
29073
- const name = eq === -1 ? arg : arg.slice(0, eq);
29074
- return GLOBAL_VALUE_OPTIONS.has(name) ? name : undefined;
29075
- }
29076
- function commanderBindable(tokens) {
29077
- const bindable = [];
29078
- for (let i = 0;i < tokens.length; i++) {
29079
- const token = tokens[i] ?? "";
29080
- if (token === "--")
29081
- break;
29082
- bindable.push(token);
29083
- if (GLOBAL_VALUE_OPTIONS.has(token) && i + 1 < tokens.length) {
29084
- bindable.push(tokens[i + 1] ?? "");
29085
- i++;
29208
+ class GatewayCliError extends Error {
29209
+ code;
29210
+ fix;
29211
+ exitCode;
29212
+ notes;
29213
+ constructor(code, reason, fix, exitCode = 1, notes = []) {
29214
+ super(reason);
29215
+ this.code = code;
29216
+ this.fix = fix;
29217
+ this.exitCode = exitCode;
29218
+ this.notes = notes;
29219
+ this.name = "GatewayCliError";
29220
+ }
29221
+ }
29222
+ var OPERATOR_HANDOFF_COMMAND = 'mutagent feedback send "<what happened>" --category cli';
29223
+ var OPERATOR_HANDOFF_NOTE = 'The gateway is not configured on the server. Only a Mutagent operator can fix that (it needs "mutagent gateway admin preflight" from a server checkout), so report it rather than retrying.';
29224
+ var RECOVERY = {
29225
+ not_signed_in: { exitCode: 2, fix: () => ["mutagent login --browser"] },
29226
+ github_not_connected: { exitCode: 2, fix: () => ["mutagent gateway connect github --wait"] },
29227
+ state_user_mismatch: {
29228
+ exitCode: 2,
29229
+ fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`],
29230
+ notes: () => ["Open the printed signInUrl in a browser already signed in to Mutagent as you, then open the printed url in that SAME browser."]
29231
+ },
29232
+ installation_not_authorized: {
29233
+ exitCode: 2,
29234
+ fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`],
29235
+ notes: () => ["Reinstall the GitHub App while signed in to GitHub as its owner."]
29236
+ },
29237
+ user_authorization_failed: {
29238
+ exitCode: 2,
29239
+ fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`]
29240
+ },
29241
+ installation_not_found: {
29242
+ exitCode: 2,
29243
+ fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`]
29244
+ },
29245
+ installation_not_recent: {
29246
+ exitCode: 2,
29247
+ fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`],
29248
+ notes: () => ["Finish the install on GitHub during the NEW attempt — an install that predates it is not accepted."]
29249
+ },
29250
+ installation_suspended: {
29251
+ exitCode: 2,
29252
+ fix: (provider) => [`mutagent gateway reconnect ${provider ?? "github"}`],
29253
+ notes: () => ["First unsuspend the MutagenT app for this account in GitHub settings: github.com -> Settings -> Applications."]
29254
+ },
29255
+ installation_owned_elsewhere: {
29256
+ exitCode: 4,
29257
+ fix: (provider) => [`mutagent gateway disconnect ${provider ?? "<provider>"} --yes`],
29258
+ notes: () => ["Run that from the OTHER workspace — the one this account is already connected to."]
29259
+ },
29260
+ installation_inactive: {
29261
+ exitCode: 3,
29262
+ fix: (provider) => [`mutagent gateway reconnect ${provider ?? "github"}`]
29263
+ },
29264
+ provider_denied: {
29265
+ exitCode: 2,
29266
+ fix: (provider) => [`mutagent gateway connect ${provider ?? "slack"}`]
29267
+ },
29268
+ slack_oauth_refused: {
29269
+ exitCode: 2,
29270
+ fix: () => ["mutagent gateway connect slack"]
29271
+ },
29272
+ enterprise_install_unsupported: {
29273
+ exitCode: 2,
29274
+ fix: () => ["mutagent gateway connect slack"],
29275
+ notes: () => ["Install the Slack app to a SINGLE workspace, not organization-wide — Enterprise Grid org installs are not supported yet."]
29276
+ },
29277
+ provider_unavailable: {
29278
+ exitCode: 3,
29279
+ fix: (provider) => [`mutagent gateway connect ${provider ?? "<provider>"}`],
29280
+ notes: () => ["The provider did not answer. Nothing is misconfigured — start the attempt over."]
29281
+ },
29282
+ test_run_repository_inaccessible: {
29283
+ exitCode: 3,
29284
+ fix: () => ["mutagent gateway repos grant-more", "mutagent gateway repos link owner/name"]
29285
+ },
29286
+ repository_inaccessible: {
29287
+ exitCode: 3,
29288
+ fix: () => ["mutagent gateway repos grant-more", "mutagent gateway repos link owner/name"]
29289
+ },
29290
+ repository_not_found: {
29291
+ exitCode: 3,
29292
+ fix: () => ["mutagent gateway repos --json", "mutagent gateway repos grant-more"],
29293
+ notes: () => ["The first command lists every repository this installation can reach."]
29294
+ },
29295
+ no_default_repository: { exitCode: 3, fix: () => ["mutagent gateway repos default owner/name"] },
29296
+ connected_to_multiple_workspaces: {
29297
+ exitCode: 4,
29298
+ fix: (provider) => [`mutagent gateway disconnect ${provider ?? "<provider>"} --yes`],
29299
+ notes: () => ["Run that from the OTHER workspace — the one this account is already connected to."]
29300
+ },
29301
+ run_principal_unavailable: { exitCode: 3, fix: (provider) => [`mutagent gateway reconnect ${provider ?? "github"}`] },
29302
+ builtin_run_unconfigured: { exitCode: 2, fix: () => [OPERATOR_HANDOFF_COMMAND], notes: () => [OPERATOR_HANDOFF_NOTE] },
29303
+ integration_unconfigured: { exitCode: 2, fix: () => [OPERATOR_HANDOFF_COMMAND], notes: () => [OPERATOR_HANDOFF_NOTE] },
29304
+ test_run_unconfigured: { exitCode: 2, fix: () => [OPERATOR_HANDOFF_COMMAND], notes: () => [OPERATOR_HANDOFF_NOTE] },
29305
+ test_run_key_conflict: {
29306
+ exitCode: 4,
29307
+ fix: () => ["mutagent gateway test-run --key <a-different-key>"],
29308
+ notes: () => ["Or omit --key entirely and a fresh one is generated."]
29309
+ },
29310
+ test_run_target_unavailable: { exitCode: 1, fix: () => ["mutagent gateway test-run --wait"], notes: () => ["Transient — retry shortly."] },
29311
+ key_invalid: { exitCode: 3, fix: () => [], notes: () => ["--key must be 1 to 256 characters. Re-run the same command with a valid --key."] },
29312
+ cursor_invalid: { exitCode: 1, fix: () => ["mutagent gateway repos --json"], notes: () => ["That starts the listing over from the first page."] },
29313
+ not_found: { exitCode: 1, fix: () => [], notes: () => ["Check the id and retry."] },
29314
+ state_unknown: { exitCode: 3, fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`] },
29315
+ state_replayed: { exitCode: 3, fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`] },
29316
+ state_expired: { exitCode: 3, fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`] },
29317
+ callback_invalid: { exitCode: 1, fix: (provider) => [`mutagent gateway connect ${provider ?? "github"}`] },
29318
+ server_fault: {
29319
+ exitCode: 1,
29320
+ fix: () => [OPERATOR_HANDOFF_COMMAND],
29321
+ notes: () => ["Retry shortly first — this may be transient. Report it if it persists."]
29322
+ }
29323
+ };
29324
+ var GATEWAY_RECOVERY_CODES = Object.keys(RECOVERY);
29325
+ function recoveryEntry(code) {
29326
+ return RECOVERY[code];
29327
+ }
29328
+ var GATEWAY_FIX_ACTION_EXIT_CODES = {
29329
+ sign_in: 2,
29330
+ connect_github: 2,
29331
+ reconnect_github: 2,
29332
+ ask_workspace_admin: 2,
29333
+ contact_operator: 2,
29334
+ wait_for_event_source: 4,
29335
+ retry: 1,
29336
+ read_run_logs: 3,
29337
+ grant_more_repositories: 3,
29338
+ link_repository: 3,
29339
+ set_default_repository: 3,
29340
+ pass_timezone: 3,
29341
+ fix_schedule: 3,
29342
+ choose_other_name: 3,
29343
+ use_other_key: 3,
29344
+ recreate_as_member: 3,
29345
+ fix_limit: 3,
29346
+ use_own_emit_kind: 3,
29347
+ create_own_trigger: 3,
29348
+ edit_existing_trigger: 3,
29349
+ use_other_trigger_kind: 3
29350
+ };
29351
+ var UNKNOWN_FIX_ACTION_EXIT_CODE = 1;
29352
+ function exitCodeForFixAction(action) {
29353
+ const known2 = GATEWAY_FIX_ACTION_EXIT_CODES;
29354
+ return known2[action] ?? UNKNOWN_FIX_ACTION_EXIT_CODE;
29355
+ }
29356
+ function unknownFixActionNote(action) {
29357
+ return `This CLI does not know the remedy the server sent ('${action}'), so it cannot name a command for it. Update the CLI to a newer version; until then, act on the reason above.`;
29358
+ }
29359
+ function renderWorkspaceConfigFix(fix, ctx = {}) {
29360
+ const p = fix.params;
29361
+ const resource = ctx.resource ?? "triggers";
29362
+ switch (fix.action) {
29363
+ case "sign_in":
29364
+ return { commands: ["mutagent login --browser"], notes: [] };
29365
+ case "connect_github":
29366
+ return { commands: ["mutagent gateway connect github --wait"], notes: [] };
29367
+ case "reconnect_github":
29368
+ return { commands: ["mutagent gateway reconnect github"], notes: [] };
29369
+ case "grant_more_repositories": {
29370
+ const commands = ["mutagent gateway repos grant-more"];
29371
+ if (p.repository)
29372
+ commands.push(`mutagent gateway repos link ${p.repository}`);
29373
+ return { commands, notes: p.url ? [`Or grant access directly at: ${p.url}`] : [] };
29374
+ }
29375
+ case "link_repository":
29376
+ return { commands: [`mutagent gateway repos link ${p.repository ?? "owner/name"}`], notes: [] };
29377
+ case "set_default_repository":
29378
+ return { commands: [`mutagent gateway repos default ${p.repository ?? "owner/name"}`], notes: [] };
29379
+ case "pass_timezone":
29380
+ return { commands: [], notes: [`Re-run the same command with --tz ${p.example ?? "<IANA zone>"}.`] };
29381
+ case "fix_schedule":
29382
+ return {
29383
+ commands: [],
29384
+ notes: [
29385
+ p.grammar ? `Re-run with a schedule matching: ${p.grammar}` : "Re-run with a corrected --cron or --every.",
29386
+ ...p.example ? [`For example: --every "${p.example}"`] : []
29387
+ ]
29388
+ };
29389
+ case "choose_other_name":
29390
+ return { commands: [], notes: ["Re-run the same command with a different --name."] };
29391
+ case "use_other_key":
29392
+ return { commands: [], notes: ["Re-run the same command with a different --key."] };
29393
+ case "recreate_as_member":
29394
+ return {
29395
+ commands: [`mutagent gateway ${resource} delete <id> --yes`],
29396
+ notes: [`Then create it again as a current workspace member: mutagent gateway ${resource} create --help lists the flags.`]
29397
+ };
29398
+ case "wait_for_event_source":
29399
+ return { commands: [], notes: [`The '${p.kind ?? "this"}' event source is not live yet — retry once it ships.`] };
29400
+ case "contact_operator":
29401
+ return { commands: [OPERATOR_HANDOFF_COMMAND], notes: [OPERATOR_HANDOFF_NOTE] };
29402
+ case "retry":
29403
+ return { commands: [], notes: ["Retry the same command — this was transient."] };
29404
+ case "edit_existing_trigger":
29405
+ return {
29406
+ commands: [`mutagent gateway triggers show ${p.triggerId ?? "<id>"}`, `mutagent gateway triggers delete ${p.triggerId ?? "<id>"} --yes`],
29407
+ notes: ["Delete it only if you would rather replace the existing trigger than keep it."]
29408
+ };
29409
+ case "ask_workspace_admin":
29410
+ return { commands: [], notes: ["Ask a workspace admin to run this, or to grant you admin in the dashboard."] };
29411
+ case "fix_limit":
29412
+ return {
29413
+ commands: [`mutagent gateway admin limits set --${p.field ?? "concurrent"} <n>`],
29414
+ notes: [`--${p.field ?? "concurrent|daily"} must be between ${p.min ?? "?"} and ${p.max ?? "?"}.`]
29415
+ };
29416
+ case "use_own_emit_kind":
29417
+ return {
29418
+ commands: ["mutagent gateway routines run-now <routine-id>"],
29419
+ notes: [`'${p.kind ?? "this"}' is reserved for the platform — emit your own kind instead, or run the routine directly.`]
29420
+ };
29421
+ case "create_own_trigger":
29422
+ return { commands: ["mutagent gateway triggers create --help", "mutagent gateway routines create --help"], notes: [] };
29423
+ case "read_run_logs":
29424
+ return { commands: [`mutagent gateway runs logs ${p.runId ?? "<runId>"}`], notes: [] };
29425
+ case "use_other_trigger_kind":
29426
+ return {
29427
+ commands: [
29428
+ `mutagent gateway triggers create --on ${p.github ?? "github.issue.opened"} --repo owner/name --instruction "<what the agent should do>"`,
29429
+ `mutagent gateway triggers create --on ${p.slack ?? "slack.mention"} --channel <channel-id> --instruction "<what the agent should do>"`
29430
+ ],
29431
+ notes: [`'${p.kind ?? "github.mention"}' is answered by the built-in @mention behaviour on every repository GitHub can reach, so it cannot be a trigger of your own.`]
29432
+ };
29433
+ default: {
29434
+ const unknownAction = fix.action;
29435
+ return { commands: [], notes: [unknownFixActionNote(unknownAction)] };
29086
29436
  }
29087
29437
  }
29088
- return bindable;
29089
29438
  }
29090
- function forwardedRootOptionRefusal(rawArgs) {
29091
- for (const verb of FORWARDING_VERBS) {
29092
- const at = commandPathIndexes(rawArgs, verb.path.length);
29093
- if (at.length !== verb.path.length || at.some((index, k) => rawArgs[index] !== verb.path[k]))
29094
- continue;
29095
- const after = rawArgs.slice((at[at.length - 1] ?? -1) + 1);
29096
- const bindable = commanderBindable(after);
29097
- const isHelix = verb.path[0] === "helix";
29098
- const keyScope = isHelix ? after : bindable;
29099
- if (keyScope.some((arg) => rootValueOption(arg) === "--api-key")) {
29100
- return isHelix ? helixKeyRefusal() : sandboxExecKeyRefusal();
29439
+ function exitCodeForStatus(status) {
29440
+ if (status === undefined)
29441
+ return 1;
29442
+ if (status === 409)
29443
+ return 4;
29444
+ if (status === 400 || status === 422)
29445
+ return 3;
29446
+ if (status === 503)
29447
+ return 3;
29448
+ return 1;
29449
+ }
29450
+ function gatewayErrorFromCode(code, reason, provider, status) {
29451
+ const entry = recoveryEntry(code);
29452
+ if (entry)
29453
+ return new GatewayCliError(code, reason, entry.fix(provider), entry.exitCode, entry.notes?.(provider) ?? []);
29454
+ return new GatewayCliError(code, reason, [], exitCodeForStatus(status));
29455
+ }
29456
+ function gatewayErrorFromRunError(error3, ctx = {}) {
29457
+ if (error3.fix) {
29458
+ const rendered = renderWorkspaceConfigFix(error3.fix, ctx);
29459
+ return new GatewayCliError(error3.code, error3.reason, rendered.commands, exitCodeForFixAction(error3.fix.action), rendered.notes);
29460
+ }
29461
+ if (error3.fix === null) {
29462
+ return new GatewayCliError(error3.code, error3.reason, [], 4, ["The server has already determined that nothing resolves this."]);
29463
+ }
29464
+ return gatewayErrorFromCode(error3.code, error3.reason, ctx.provider);
29465
+ }
29466
+ function renderRunErrorForDisplay(error3, ctx = {}) {
29467
+ if (error3 === null)
29468
+ return null;
29469
+ const withNotes = (fix, notes) => notes.length > 0 ? { code: error3.code, reason: error3.reason, fix, notes } : { code: error3.code, reason: error3.reason, fix };
29470
+ if (error3.fix) {
29471
+ const rendered = renderWorkspaceConfigFix(error3.fix, ctx);
29472
+ return withNotes(rendered.commands, rendered.notes);
29473
+ }
29474
+ if (error3.fix === null)
29475
+ return withNotes([], []);
29476
+ const fallback = gatewayErrorFromCode(error3.code, error3.reason, ctx.provider);
29477
+ return withNotes(fallback.fix, fallback.notes);
29478
+ }
29479
+ function toGatewayCliError(error3, provider, resource) {
29480
+ if (error3 instanceof GatewayCliError)
29481
+ return error3;
29482
+ if (error3 instanceof GatewayConfigError) {
29483
+ return gatewayErrorFromCode(error3.code, error3.message, provider);
29484
+ }
29485
+ if (error3 instanceof GatewayApiError) {
29486
+ if (error3.fix) {
29487
+ const rendered = renderWorkspaceConfigFix(error3.fix, { provider, resource });
29488
+ return new GatewayCliError(error3.code, error3.reason, rendered.commands, exitCodeForFixAction(error3.fix.action), rendered.notes);
29101
29489
  }
29102
- const other = bindable.map(rootValueOption).find((option) => option !== undefined);
29103
- if (other !== undefined)
29104
- return rootValueOptionRefusal(other, verb.label);
29105
- return;
29490
+ return gatewayErrorFromCode(error3.code, error3.reason, provider, error3.status);
29106
29491
  }
29107
- return;
29492
+ if (error3 instanceof GatewayNetworkError) {
29493
+ return new GatewayCliError("network_error", error3.message, ["mutagent gateway doctor"], 1, ["Check your network connection first — the request never reached the server."]);
29494
+ }
29495
+ if (error3 instanceof MutagentError) {
29496
+ const exitCode = error3.exitCode >= 1 && error3.exitCode <= 4 ? error3.exitCode : 1;
29497
+ return new GatewayCliError(error3.code, error3.message, error3.suggestion ? [error3.suggestion] : [], exitCode);
29498
+ }
29499
+ const message = error3 instanceof Error ? error3.message : String(error3);
29500
+ return new GatewayCliError("unexpected_error", message, [], 1);
29108
29501
  }
29109
- function keyAlternative() {
29502
+ function gatewayIsJson(command) {
29503
+ return getJsonFlag(command);
29504
+ }
29505
+ function printGatewayOk(isJson, data, opts) {
29506
+ if (isJson) {
29507
+ const envelope = { ok: true, data: scrubKnownSuccessDeep(data) };
29508
+ if (opts.next && opts.next.length > 0)
29509
+ envelope.next = opts.next;
29510
+ console.log(JSON.stringify(envelope, null, 2));
29511
+ return;
29512
+ }
29513
+ opts.human();
29514
+ if (opts.next && opts.next.length > 0) {
29515
+ console.log("");
29516
+ console.log(chalk38.dim("Next:"));
29517
+ for (const step of opts.next)
29518
+ console.log(chalk38.dim(` ${step}`));
29519
+ }
29520
+ }
29521
+ function handleGatewayError(error3, isJson, provider, resource) {
29522
+ const gwError = toGatewayCliError(error3, provider, resource);
29523
+ if (isJson) {
29524
+ const envelope = {
29525
+ ok: false,
29526
+ error: { code: gwError.code, reason: scrubKnownSuccess(gwError.message), fix: gwError.fix }
29527
+ };
29528
+ if (gwError.notes.length > 0 && envelope.error)
29529
+ envelope.error.notes = gwError.notes.map(scrubKnownSuccess);
29530
+ console.log(JSON.stringify(envelope, null, 2));
29531
+ } else {
29532
+ console.error(chalk38.red(`✗ ${scrubKnownSuccess(gwError.message)}`) + chalk38.dim(` [${gwError.code}]`));
29533
+ if (gwError.fix.length > 0) {
29534
+ console.error("");
29535
+ console.error(chalk38.yellow("Fix:"));
29536
+ for (const step of gwError.fix)
29537
+ console.error(chalk38.yellow(` ${step}`));
29538
+ }
29539
+ if (gwError.notes.length > 0) {
29540
+ console.error("");
29541
+ for (const note of gwError.notes)
29542
+ console.error(chalk38.dim(` ${scrubKnownSuccess(note)}`));
29543
+ }
29544
+ }
29545
+ process.exit(gwError.exitCode);
29546
+ }
29547
+ async function runGatewayAction(isJson, provider, fn, resource) {
29548
+ let exitCode = undefined;
29549
+ let caught;
29550
+ try {
29551
+ exitCode = await fn();
29552
+ } catch (error3) {
29553
+ caught = { error: error3 };
29554
+ }
29555
+ if (caught) {
29556
+ handleGatewayError(caught.error, isJson, provider, resource);
29557
+ }
29558
+ if (typeof exitCode === "number" && exitCode !== 0)
29559
+ process.exit(exitCode);
29560
+ }
29561
+
29562
+ // src/commands/gateway/status.ts
29563
+ async function computeGatewayStatus() {
29564
+ const [integrations, defaultRepo] = await Promise.all([listIntegrations(), getDefaultRepository()]);
29565
+ const github = integrations.integrations.find((i) => i.provider === "github") ?? null;
29566
+ const slack = integrations.integrations.find((i) => i.provider === "slack") ?? null;
29567
+ return {
29568
+ workspaceId: integrations.workspaceId,
29569
+ connections: { github, slack },
29570
+ defaultRepository: defaultRepo.defaultRepository
29571
+ };
29572
+ }
29573
+ function nextStepFor(status) {
29574
+ if (status.connections.github?.state !== "connected")
29575
+ return ["mutagent gateway connect github --wait"];
29576
+ if (status.defaultRepository === null)
29577
+ return ["mutagent gateway repos default owner/name"];
29578
+ if (status.connections.slack?.state !== "connected")
29579
+ return ["mutagent gateway connect slack --wait (optional)"];
29580
+ return ["mutagent gateway test-run --wait"];
29581
+ }
29582
+ function printStatusHuman(status) {
29583
+ console.log(chalk39.bold("Gateway status") + chalk39.dim(` workspace ${status.workspaceId}`));
29584
+ for (const provider of ["github", "slack"]) {
29585
+ const conn = status.connections[provider];
29586
+ const connected = conn?.state === "connected";
29587
+ const mark = connected ? chalk39.green("✓") : chalk39.yellow("·");
29588
+ const detail = conn === null ? "unknown" : connected ? conn.accountName ?? conn.accountId ?? "connected" : conn.state;
29589
+ console.log(` ${mark} ${provider.padEnd(6)} ${detail}`);
29590
+ }
29591
+ console.log(` ${status.defaultRepository ? chalk39.green("✓") : chalk39.yellow("·")} default repo ${status.defaultRepository?.name ?? "(none)"}`);
29592
+ }
29593
+ function registerGatewayStatusCommands(gateway) {
29594
+ gateway.action(async () => {
29595
+ const isJson = gatewayIsJson(gateway);
29596
+ await runGatewayAction(isJson, undefined, async () => {
29597
+ const status = await computeGatewayStatus();
29598
+ printGatewayOk(isJson, status, { next: nextStepFor(status), human: () => {
29599
+ printStatusHuman(status);
29600
+ } });
29601
+ return;
29602
+ });
29603
+ });
29604
+ gateway.command("status").description("What's connected, what's missing, and the next command to run").addHelpText("after", `
29605
+ Examples:
29606
+ ${chalk39.dim("$")} mutagent gateway status
29607
+ ${chalk39.dim("$")} mutagent gateway status --json
29608
+
29609
+ Same as bare 'mutagent gateway' — the one command that always tells you what's next.
29610
+
29611
+ AI Agent Directive:
29612
+ --json returns { ok, data: { workspaceId, connections: { github, slack }, defaultRepository },
29613
+ next: ["<the one command to run next>"] }. Always run this first; follow 'next' verbatim.
29614
+ `).action(async () => {
29615
+ const isJson = gatewayIsJson(gateway);
29616
+ await runGatewayAction(isJson, undefined, async () => {
29617
+ const status = await computeGatewayStatus();
29618
+ printGatewayOk(isJson, status, { next: nextStepFor(status), human: () => {
29619
+ printStatusHuman(status);
29620
+ } });
29621
+ return;
29622
+ });
29623
+ });
29624
+ gateway.command("doctor").description("End-to-end check of the gateway setup, with fixes for anything broken").addHelpText("after", `
29625
+ Examples:
29626
+ ${chalk39.dim("$")} mutagent gateway doctor
29627
+ ${chalk39.dim("$")} mutagent gateway doctor --json
29628
+
29629
+ Checks: GitHub/Slack connection state, a default repository is set, and — the one thing
29630
+ 'status' does not check — that the default repository is still reachable through the
29631
+ GitHub installation (access can be revoked after it was chosen).
29632
+
29633
+ AI Agent Directive:
29634
+ --json returns { ok, data: { ...status, checks: [{ name, pass, detail }] } }.
29635
+ ok is false when any check fails; each failing check's 'detail' names the fix.
29636
+ `).action(async () => {
29637
+ const isJson = gatewayIsJson(gateway);
29638
+ await runGatewayAction(isJson, undefined, async () => {
29639
+ const status = await computeGatewayStatus();
29640
+ const checks = [
29641
+ {
29642
+ name: "github_connected",
29643
+ pass: status.connections.github?.state === "connected",
29644
+ detail: status.connections.github?.state === "connected" ? "connected" : "mutagent gateway connect github --wait"
29645
+ },
29646
+ {
29647
+ name: "default_repository_set",
29648
+ pass: status.defaultRepository !== null,
29649
+ detail: status.defaultRepository ? status.defaultRepository.name : "mutagent gateway repos default owner/name"
29650
+ }
29651
+ ];
29652
+ if (status.defaultRepository !== null && status.connections.github?.state === "connected") {
29653
+ const reachable = await listAllRepositories();
29654
+ const stillReachable = reachable.some((repo) => repo.id === status.defaultRepository?.repositoryId);
29655
+ checks.push({
29656
+ name: "default_repository_reachable",
29657
+ pass: stillReachable,
29658
+ detail: stillReachable ? "reachable" : "mutagent gateway repos grant-more"
29659
+ });
29660
+ }
29661
+ const allPass = checks.every((c) => c.pass);
29662
+ const data = { ...status, checks };
29663
+ if (isJson) {
29664
+ console.log(JSON.stringify({ ok: allPass, data }, null, 2));
29665
+ } else {
29666
+ printStatusHuman(status);
29667
+ console.log("");
29668
+ console.log(chalk39.bold("Checks:"));
29669
+ for (const check3 of checks) {
29670
+ console.log(` ${check3.pass ? chalk39.green("✓") : chalk39.red("✗")} ${check3.name} ${chalk39.dim(check3.detail)}`);
29671
+ }
29672
+ }
29673
+ return allPass ? undefined : 3;
29674
+ });
29675
+ });
29676
+ }
29677
+
29678
+ // src/commands/gateway/connect.ts
29679
+ import chalk40 from "chalk";
29680
+
29681
+ // src/lib/gateway-poll.ts
29682
+ function sleep2(ms) {
29683
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
29684
+ }
29685
+ function defaultIntervalMs() {
29686
+ return envMsOverride("MUTAGENT_TEST_POLL_INTERVAL_MS") ?? 2000;
29687
+ }
29688
+ function defaultTimeoutMs() {
29689
+ return envMsOverride("MUTAGENT_TEST_POLL_TIMEOUT_MS") ?? 300000;
29690
+ }
29691
+ function envMsOverride(name) {
29692
+ const override2 = process.env[name];
29693
+ if (override2 === undefined)
29694
+ return;
29695
+ const parsed = Number(override2);
29696
+ if (Number.isFinite(parsed) && parsed >= 0)
29697
+ return parsed;
29698
+ return;
29699
+ }
29700
+ async function pollUntil(fetchOnce, isDone, opts = {}) {
29701
+ const timeoutMs = opts.timeoutMs ?? defaultTimeoutMs();
29702
+ const intervalMs = opts.intervalMs ?? defaultIntervalMs();
29703
+ const start = Date.now();
29704
+ let value2 = await fetchOnce();
29705
+ if (isDone(value2))
29706
+ return { value: value2, settled: true };
29707
+ while (Date.now() - start < timeoutMs) {
29708
+ await sleep2(intervalMs);
29709
+ value2 = await fetchOnce();
29710
+ if (isDone(value2))
29711
+ return { value: value2, settled: true };
29712
+ }
29713
+ return { value: value2, settled: false };
29714
+ }
29715
+
29716
+ // src/commands/gateway/connect.ts
29717
+ var PROVIDERS = ["github", "slack"];
29718
+ function parseProvider(raw) {
29719
+ if (raw === "github" || raw === "slack")
29720
+ return raw;
29721
+ throw new GatewayCliError("validation_error", `Unknown provider: ${raw}`, [], 3, [`Re-run with one of: ${PROVIDERS.join(", ")}.`]);
29722
+ }
29723
+ function parseProviderOrExit(raw, isJson) {
29724
+ try {
29725
+ return parseProvider(raw);
29726
+ } catch (error3) {
29727
+ handleGatewayError(error3, isJson);
29728
+ }
29729
+ }
29730
+ async function openInBrowser(url) {
29731
+ try {
29732
+ const { default: open2 } = await import("open");
29733
+ await open2(url);
29734
+ } catch {}
29735
+ }
29736
+ async function waitForAttempt(state) {
29737
+ const { value: value2 } = await pollUntil(() => getIntegrationAttempt(state), (attempt) => attempt.phase !== "pending");
29738
+ return value2;
29739
+ }
29740
+ function reportAttempt(provider, attempt, isJson) {
29741
+ if (attempt.phase === "completed") {
29742
+ printGatewayOk(isJson, { provider, status: attempt.status }, {
29743
+ next: provider === "github" ? ["mutagent gateway repos default owner/name"] : [],
29744
+ human: () => {
29745
+ console.log(chalk40.green(`✓ ${provider} connected`) + chalk40.dim(` ${attempt.status?.accountName ?? attempt.status?.accountId ?? ""}`));
29746
+ }
29747
+ });
29748
+ return;
29749
+ }
29750
+ if (attempt.phase === "pending") {
29751
+ throw new GatewayCliError("connect_pending", "Still waiting for the browser step to complete.", [`mutagent gateway connect ${provider} --wait --attempt <id>`], 2);
29752
+ }
29753
+ const errorCode = attempt.status?.error?.code ?? (attempt.phase === "expired" ? "state_expired" : "callback_invalid");
29754
+ const reason = attempt.status?.error?.reason ?? `The connection attempt ${attempt.phase}.`;
29755
+ throw gatewayErrorFromCode(errorCode, reason, provider);
29756
+ }
29757
+ function registerConnect(gateway) {
29758
+ gateway.command("connect").description("Connect GitHub or Slack (prints a URL to open, or resumes an attempt)").argument("<gateway-provider>", "github | slack").option("--wait", "Poll until the attempt settles instead of returning immediately").option("--open", "Also open the URL in a local browser (for a human at a desktop)").option("--attempt <id>", "Resume an existing attempt instead of starting a new one").addHelpText("after", `
29759
+ Examples:
29760
+ ${chalk40.dim("$")} mutagent gateway connect github --json
29761
+ ${chalk40.dim("$")} mutagent gateway connect github --wait --attempt att_abc123
29762
+ ${chalk40.dim("$")} mutagent gateway connect slack --open
29763
+
29764
+ Already connected is success, not an error: re-running this once github or slack is
29765
+ already connected exits 0 and does nothing — safe to retry.
29766
+
29767
+ Without --wait: prints { signInUrl, url, attempt } and exits 2 (a person or browser
29768
+ is needed). Open signInUrl and sign in FIRST, then open url in that SAME browser —
29769
+ the authorization only completes in a browser signed in to Mutagent as you.
29770
+ With --wait: polls until the attempt completes, fails or expires.
29771
+
29772
+ AI Agent Directive:
29773
+ 1. mutagent gateway connect <provider> --json -> { ok:true, data:{signInUrl,url,attempt}, ... } exit 2
29774
+ 2. Open 'data.signInUrl' and sign in (Playwright/browser-use).
29775
+ 3. In that SAME browser, open 'data.url' and authorize.
29776
+ 4. mutagent gateway connect <provider> --wait --attempt <data.attempt> --json
29777
+ `).action(async (providerArg, options) => {
29778
+ const isJson = gatewayIsJson(gateway);
29779
+ const provider = parseProviderOrExit(providerArg, isJson);
29780
+ await runGatewayAction(isJson, provider, async () => {
29781
+ if (options.attempt) {
29782
+ const attempt2 = options.wait ? await waitForAttempt(options.attempt) : await getIntegrationAttempt(options.attempt);
29783
+ return reportAttempt(provider, attempt2, isJson);
29784
+ }
29785
+ const existing = (await listIntegrations()).integrations.find((i) => i.provider === provider);
29786
+ if (existing?.state === "connected") {
29787
+ printGatewayOk(isJson, { provider, status: existing, alreadyConnected: true }, {
29788
+ human: () => {
29789
+ console.log(chalk40.green(`✓ ${provider} already connected`) + chalk40.dim(` ${existing.accountName ?? existing.accountId ?? ""}`));
29790
+ }
29791
+ });
29792
+ return;
29793
+ }
29794
+ const start = await startIntegration(provider, "connect");
29795
+ if (options.open === true)
29796
+ await openInBrowser(start.url);
29797
+ const signInUrl = getAppBaseUrl();
29798
+ if (options.wait !== true) {
29799
+ printGatewayOk(isJson, { provider, signInUrl, url: start.url, attempt: start.state }, {
29800
+ next: [
29801
+ `Sign in at ${signInUrl}`,
29802
+ `Then, in that SAME browser, open ${start.url}`,
29803
+ `mutagent gateway connect ${provider} --wait --attempt ${start.state}`
29804
+ ],
29805
+ human: () => {
29806
+ console.log(`1. Sign in (in a browser): ${chalk40.cyan(signInUrl)}`);
29807
+ console.log(`2. Then, in that SAME browser, authorize: ${chalk40.cyan(start.url)}`);
29808
+ console.log(chalk40.dim(`attempt: ${start.state}`));
29809
+ }
29810
+ });
29811
+ return 2;
29812
+ }
29813
+ const attempt = await waitForAttempt(start.state);
29814
+ return reportAttempt(provider, attempt, isJson);
29815
+ });
29816
+ });
29817
+ }
29818
+ function registerReconnect(gateway) {
29819
+ gateway.command("reconnect").description("Re-authorize GitHub or Slack — also recovers run_principal_unavailable").argument("<gateway-provider>", "github | slack").addHelpText("after", `
29820
+ Examples:
29821
+ ${chalk40.dim("$")} mutagent gateway reconnect github --json
29822
+
29823
+ Starts a re-auth attempt. Sign in at signInUrl first, then open url in that SAME
29824
+ browser, then finish it the same way as 'connect':
29825
+ mutagent gateway connect <provider> --wait --attempt <id>
29826
+
29827
+ AI Agent Directive:
29828
+ --json returns { ok:true, data:{ signInUrl, url, attempt },
29829
+ next:["Sign in at ...", "Then, in that SAME browser, open ...", "mutagent gateway connect <provider> --wait --attempt <id>"] }, exit 2.
29830
+ `).action(async (providerArg) => {
29831
+ const isJson = gatewayIsJson(gateway);
29832
+ const provider = parseProviderOrExit(providerArg, isJson);
29833
+ await runGatewayAction(isJson, provider, async () => {
29834
+ const start = await startIntegration(provider, "reauth");
29835
+ const signInUrl = getAppBaseUrl();
29836
+ printGatewayOk(isJson, { provider, signInUrl, url: start.url, attempt: start.state }, {
29837
+ next: [
29838
+ `Sign in at ${signInUrl}`,
29839
+ `Then, in that SAME browser, open ${start.url}`,
29840
+ `mutagent gateway connect ${provider} --wait --attempt ${start.state}`
29841
+ ],
29842
+ human: () => {
29843
+ console.log(`1. Sign in (in a browser): ${chalk40.cyan(signInUrl)}`);
29844
+ console.log(`2. Then, in that SAME browser, authorize: ${chalk40.cyan(start.url)}`);
29845
+ console.log(chalk40.dim(`attempt: ${start.state}`));
29846
+ }
29847
+ });
29848
+ return 2;
29849
+ });
29850
+ });
29851
+ }
29852
+ function registerDisconnect(gateway) {
29853
+ gateway.command("disconnect").description("Disconnect GitHub or Slack from this workspace").argument("<gateway-provider>", "github | slack").option("--yes", "Confirm — required, never prompted for").addHelpText("after", `
29854
+ Examples:
29855
+ ${chalk40.dim("$")} mutagent gateway disconnect slack --yes
29856
+
29857
+ Idempotent: disconnecting when github or slack is already disconnected is success, not an error.
29858
+ Never prompts — pass --yes or the command refuses (exit 4).
29859
+
29860
+ AI Agent Directive:
29861
+ Confirm with the user before running this. --json returns { ok:true, data:{ provider, status } }.
29862
+ `).action(async (providerArg, options) => {
29863
+ const isJson = gatewayIsJson(gateway);
29864
+ const provider = parseProviderOrExit(providerArg, isJson);
29865
+ await runGatewayAction(isJson, provider, async () => {
29866
+ if (options.yes !== true) {
29867
+ throw new GatewayCliError("confirmation_required", `Disconnecting ${provider} requires confirmation.`, [`mutagent gateway disconnect ${provider} --yes`], 4);
29868
+ }
29869
+ const status = await disconnectIntegration(provider);
29870
+ printGatewayOk(isJson, { provider, status }, {
29871
+ human: () => {
29872
+ console.log(chalk40.green(`✓ ${provider} disconnected`));
29873
+ }
29874
+ });
29875
+ return;
29876
+ });
29877
+ });
29878
+ }
29879
+ function registerGatewayConnectCommands(gateway) {
29880
+ registerConnect(gateway);
29881
+ registerReconnect(gateway);
29882
+ registerDisconnect(gateway);
29883
+ }
29884
+
29885
+ // src/commands/gateway/repos.ts
29886
+ import chalk41 from "chalk";
29887
+
29888
+ // src/lib/gateway-api-repos.ts
29889
+ async function listLinkedRepositories() {
29890
+ const { workspaceId } = await resolveGatewayConnection();
29891
+ return gatewayRequest(`/api/gateway/repositories${qs({ workspaceId })}`);
29892
+ }
29893
+ async function linkRepositories(repositories) {
29894
+ const { workspaceId } = await resolveGatewayConnection();
29895
+ return gatewayRequest("/api/gateway/repositories/link", {
29896
+ method: "POST",
29897
+ body: JSON.stringify({ workspaceId, repositories })
29898
+ });
29899
+ }
29900
+ async function unlinkRepository(repository) {
29901
+ const { workspaceId } = await resolveGatewayConnection();
29902
+ return gatewayRequest("/api/gateway/repositories/unlink", {
29903
+ method: "POST",
29904
+ body: JSON.stringify({ workspaceId, repository })
29905
+ });
29906
+ }
29907
+ async function getGrantMoreRepositoriesUrl() {
29908
+ const { workspaceId } = await resolveGatewayConnection();
29909
+ return gatewayRequest(`/api/gateway/repositories/grant-more${qs({ workspaceId })}`);
29910
+ }
29911
+
29912
+ // src/commands/gateway/repos.ts
29913
+ function printLinkedRepo(repo) {
29914
+ const mark = repo.state === "linked" ? chalk41.green("✓") : chalk41.yellow("·");
29915
+ const def = repo.isDefault ? chalk41.dim(" (default)") : "";
29916
+ const dependents = repo.dependents.triggers + repo.dependents.routines > 0 ? chalk41.dim(` ${String(repo.dependents.triggers)} trigger(s), ${String(repo.dependents.routines)} routine(s)`) : "";
29917
+ console.log(` ${mark} ${repo.name}${def}${dependents}`);
29918
+ }
29919
+ function registerList(gateway) {
29920
+ gateway.command("repos").description("Repositories the workspace's GitHub installation can reach, or has linked").option("--available", "Every reachable repository (same as no flag)").option("--linked", "Only repositories explicitly linked to this workspace").addHelpText("after", `
29921
+ Examples:
29922
+ ${chalk41.dim("$")} mutagent gateway repos --json
29923
+ ${chalk41.dim("$")} mutagent gateway repos --linked --json
29924
+
29925
+ No flag or --available: every repository the GitHub App installation can reach
29926
+ (repos still bind lazily on first use for those not explicitly linked).
29927
+ --linked: only repositories 'repos link' has explicitly linked — shows dependents
29928
+ (triggers/routines) so an unlink's blast radius is visible before you run it.
29929
+
29930
+ AI Agent Directive:
29931
+ --available (default) --json: { ok, data: { items: [{ id, name, defaultBranch }], count } }.
29932
+ --linked --json: { ok, data: { items: [{ repositoryId, name, state, isDefault, isPrivate,
29933
+ dependents: { triggers, routines } }], count } }.
29934
+ An empty --available list with GitHub connected means no repository access —
29935
+ fix: mutagent gateway repos grant-more.
29936
+ `).action(async (options) => {
29937
+ const isJson = gatewayIsJson(gateway);
29938
+ await runGatewayAction(isJson, undefined, async () => {
29939
+ if (options.linked === true) {
29940
+ const { items: items2 } = await listLinkedRepositories();
29941
+ printGatewayOk(isJson, { items: items2, count: items2.length }, {
29942
+ human: () => {
29943
+ if (items2.length === 0) {
29944
+ console.log(chalk41.gray("No repositories linked. Fix: mutagent gateway repos link owner/name"));
29945
+ return;
29946
+ }
29947
+ for (const repo of items2)
29948
+ printLinkedRepo(repo);
29949
+ }
29950
+ });
29951
+ return;
29952
+ }
29953
+ const items = await listAllRepositories();
29954
+ printGatewayOk(isJson, { items, count: items.length }, {
29955
+ human: () => {
29956
+ if (items.length === 0) {
29957
+ console.log(chalk41.gray("No repositories reachable. Fix: mutagent gateway repos grant-more"));
29958
+ return;
29959
+ }
29960
+ for (const repo of items)
29961
+ console.log(` ${repo.name} ${chalk41.dim(repo.defaultBranch)}`);
29962
+ }
29963
+ });
29964
+ return;
29965
+ });
29966
+ });
29967
+ }
29968
+ function registerLink(repos, gateway) {
29969
+ repos.command("link").description("Link repositories to this workspace so triggers/routines can target them").argument("<repositories...>", "owner/name (or GitHub repository id), 1–25").addHelpText("after", `
29970
+ Examples:
29971
+ ${chalk41.dim("$")} mutagent gateway repos link owner/name
29972
+ ${chalk41.dim("$")} mutagent gateway repos link owner/one owner/two
29973
+
29974
+ Idempotent: an already-linked repository is returned unchanged. All-or-nothing —
29975
+ if one of several repositories can't be linked, none are.
29976
+
29977
+ AI Agent Directive:
29978
+ --json returns { ok, data: { items: LinkedRepository[] } }. An unreachable repository
29979
+ fails with 'repository_inaccessible', fix: mutagent gateway repos grant-more.
29980
+ `).action(async (repositories) => {
29981
+ const isJson = gatewayIsJson(gateway);
29982
+ await runGatewayAction(isJson, undefined, async () => {
29983
+ const { items } = await linkRepositories(repositories);
29984
+ printGatewayOk(isJson, { items }, {
29985
+ human: () => {
29986
+ for (const repo of items)
29987
+ printLinkedRepo(repo);
29988
+ }
29989
+ });
29990
+ return;
29991
+ });
29992
+ });
29993
+ }
29994
+ function registerUnlink(repos, gateway) {
29995
+ repos.command("unlink").description("Unlink a repository — its triggers and routines pause, they are not deleted").argument("<owner/name>", "The repository to unlink").option("--yes", "Confirm — required, never prompted for").addHelpText("after", `
29996
+ Examples:
29997
+ ${chalk41.dim("$")} mutagent gateway repos unlink owner/name --yes
29998
+
29999
+ Dependent triggers and routines PAUSE (visibly — 'repository_unlinked'), never delete.
30000
+ Refused for the workspace default repository — set another default first.
30001
+ Never prompts — pass --yes or the command refuses (exit 4).
30002
+
30003
+ AI Agent Directive:
30004
+ --json returns { ok, data: { repository, paused: { triggers: id[], routines: id[] } } }.
30005
+ `).action(async (repository, options) => {
30006
+ const isJson = gatewayIsJson(gateway);
30007
+ await runGatewayAction(isJson, undefined, async () => {
30008
+ if (options.yes !== true) {
30009
+ throw new GatewayCliError("confirmation_required", `Unlinking ${repository} requires confirmation.`, [`mutagent gateway repos unlink ${repository} --yes`], 4);
30010
+ }
30011
+ const result = await unlinkRepository(repository);
30012
+ printGatewayOk(isJson, result, {
30013
+ human: () => {
30014
+ console.log(chalk41.green(`✓ Unlinked ${result.repository.name}`));
30015
+ if (result.paused.triggers.length > 0)
30016
+ console.log(chalk41.yellow(` paused triggers: ${result.paused.triggers.join(", ")}`));
30017
+ if (result.paused.routines.length > 0)
30018
+ console.log(chalk41.yellow(` paused routines: ${result.paused.routines.join(", ")}`));
30019
+ }
30020
+ });
30021
+ return;
30022
+ });
30023
+ });
30024
+ }
30025
+ function registerGrantMore(repos, gateway) {
30026
+ repos.command("grant-more").description("Print the GitHub page for granting the app access to more repositories").addHelpText("after", `
30027
+ Examples:
30028
+ ${chalk41.dim("$")} mutagent gateway repos grant-more --json
30029
+
30030
+ Opens no browser itself — prints the URL. Open it in the SAME browser you signed
30031
+ in to Mutagent with, then 'repos link owner/name'.
30032
+
30033
+ AI Agent Directive:
30034
+ --json returns { ok, data: { url } }, next: ["mutagent gateway repos link owner/name"].
30035
+ `).action(async () => {
30036
+ const isJson = gatewayIsJson(gateway);
30037
+ await runGatewayAction(isJson, undefined, async () => {
30038
+ const result = await getGrantMoreRepositoriesUrl();
30039
+ printGatewayOk(isJson, result, {
30040
+ next: ["mutagent gateway repos link owner/name"],
30041
+ human: () => {
30042
+ console.log(`Open this URL (in the browser you signed in with): ${chalk41.cyan(result.url)}`);
30043
+ }
30044
+ });
30045
+ return;
30046
+ });
30047
+ });
30048
+ }
30049
+ function registerDefault(repos, gateway) {
30050
+ repos.command("default").description("Show or set the workspace default repository").argument("[owner/name]", "Set the default to this repository (must be reachable)").addHelpText("after", `
30051
+ Examples:
30052
+ ${chalk41.dim("$")} mutagent gateway repos default
30053
+ ${chalk41.dim("$")} mutagent gateway repos default owner/name
30054
+
30055
+ No argument: shows the current default (or null). With an argument: sets it — the
30056
+ repository must already be reachable through the GitHub installation (see 'repos').
30057
+
30058
+ AI Agent Directive:
30059
+ --json returns { ok, data: { defaultRepository: { repositoryId, name, setAt } | null } }.
30060
+ Setting an unreachable repository fails with 'repository_not_found', fix:
30061
+ mutagent gateway repos --json (to see what IS reachable), then repos grant-more.
30062
+ `).action(async (ownerName) => {
30063
+ const isJson = gatewayIsJson(gateway);
30064
+ await runGatewayAction(isJson, undefined, async () => {
30065
+ if (ownerName === undefined) {
30066
+ const current = await getDefaultRepository();
30067
+ printGatewayOk(isJson, { defaultRepository: current.defaultRepository }, {
30068
+ human: () => {
30069
+ console.log(current.defaultRepository ? current.defaultRepository.name : chalk41.gray("(no default repository set)"));
30070
+ }
30071
+ });
30072
+ return;
30073
+ }
30074
+ const repo = await findRepositoryByName(ownerName);
30075
+ if (repo === undefined) {
30076
+ throw gatewayErrorFromCode("repository_not_found", `${ownerName} is not reachable through this workspace's GitHub installation.`);
30077
+ }
30078
+ const result = await setDefaultRepository(repo.id);
30079
+ printGatewayOk(isJson, { defaultRepository: result.defaultRepository }, {
30080
+ next: ["mutagent gateway test-run --wait"],
30081
+ human: () => {
30082
+ console.log(chalk41.green(`✓ Default repository set to ${result.defaultRepository?.name ?? ownerName}`));
30083
+ }
30084
+ });
30085
+ return;
30086
+ });
30087
+ });
30088
+ }
30089
+ function registerGatewayReposCommands(gateway) {
30090
+ registerList(gateway);
30091
+ const reposCommand = gateway.commands.find((c) => c.name() === "repos");
30092
+ if (reposCommand) {
30093
+ registerDefault(reposCommand, gateway);
30094
+ registerLink(reposCommand, gateway);
30095
+ registerUnlink(reposCommand, gateway);
30096
+ registerGrantMore(reposCommand, gateway);
30097
+ }
30098
+ }
30099
+
30100
+ // src/commands/gateway/triggers.ts
30101
+ import { readFileSync as readFileSync14 } from "node:fs";
30102
+ import { Command as Command18 } from "commander";
30103
+ import chalk42 from "chalk";
30104
+
30105
+ // src/lib/gateway-api-triggers.ts
30106
+ var TRIGGER_KINDS = ["github.issue.opened", "slack.mention", "webhook", "github.ci-failed", "trace.threshold"];
30107
+ var BUILTIN_ONLY_TRIGGER_KINDS = ["github.mention"];
30108
+ async function listTriggers() {
30109
+ const { workspaceId } = await resolveGatewayConnection();
30110
+ return gatewayRequest(`/api/gateway/triggers?workspaceId=${encodeURIComponent(workspaceId)}`);
30111
+ }
30112
+ async function createTrigger(body) {
30113
+ const { workspaceId } = await resolveGatewayConnection();
30114
+ return gatewayRequest("/api/gateway/triggers", { method: "POST", body: JSON.stringify({ workspaceId, ...body }) });
30115
+ }
30116
+ async function getTrigger(id) {
30117
+ const { workspaceId } = await resolveGatewayConnection();
30118
+ return gatewayRequest(`/api/gateway/triggers/${encodeURIComponent(id)}?workspaceId=${encodeURIComponent(workspaceId)}`);
30119
+ }
30120
+ async function setTriggerEnabled(id, enabled) {
30121
+ const { workspaceId } = await resolveGatewayConnection();
30122
+ return gatewayRequest(`/api/gateway/triggers/${encodeURIComponent(id)}/${enabled ? "enable" : "disable"}`, {
30123
+ method: "POST",
30124
+ body: JSON.stringify({ workspaceId })
30125
+ });
30126
+ }
30127
+ var enableTrigger = (id) => setTriggerEnabled(id, true);
30128
+ var disableTrigger = (id) => setTriggerEnabled(id, false);
30129
+ async function deleteTrigger(id) {
30130
+ const { workspaceId } = await resolveGatewayConnection();
30131
+ await gatewayRequest(`/api/gateway/triggers/${encodeURIComponent(id)}?workspaceId=${encodeURIComponent(workspaceId)}`, {
30132
+ method: "DELETE"
30133
+ });
30134
+ }
30135
+ async function testTrigger(id, event) {
30136
+ const { workspaceId } = await resolveGatewayConnection();
30137
+ return gatewayRequest(`/api/gateway/triggers/${encodeURIComponent(id)}/test`, {
30138
+ method: "POST",
30139
+ body: JSON.stringify({ workspaceId, event })
30140
+ });
30141
+ }
30142
+
30143
+ // src/commands/gateway/triggers.ts
30144
+ var RESOURCE = "triggers";
30145
+ function printTrigger(t) {
30146
+ const mark = t.status === "enabled" ? chalk42.green("✓") : t.status === "paused" ? chalk42.yellow("⏸") : chalk42.dim("○");
30147
+ const builtin = t.builtin ? chalk42.dim(" [built-in]") : "";
30148
+ console.log(` ${mark} ${t.id} ${t.name}${builtin} ${chalk42.dim(t.kind)} ${t.repository?.name ?? chalk42.dim("(default repo)")}`);
30149
+ if (t.pausedReason)
30150
+ console.log(chalk42.yellow(` paused: ${t.pausedReason.reason}`));
30151
+ }
30152
+ function assertCreatableKind(kind) {
30153
+ if (!BUILTIN_ONLY_TRIGGER_KINDS.includes(kind))
30154
+ return;
30155
+ throw gatewayErrorFromRunError({
30156
+ code: "trigger_kind_builtin_only",
30157
+ reason: `${kind} is answered by the built-in @mention behaviour on every repository GitHub can reach, so it cannot be a trigger of your own. Use github.issue.opened for repository events, or slack.mention with a dedicated Slack channel.`,
30158
+ fix: { action: "use_other_trigger_kind", params: { kind, github: "github.issue.opened", slack: "slack.mention" } }
30159
+ }, { resource: RESOURCE });
30160
+ }
30161
+ function registerList2(gateway) {
30162
+ const triggers = new Command18("triggers").description("Event → agent run: create, list, show, enable, disable, delete, test");
30163
+ gateway.addCommand(triggers);
30164
+ triggers.description("List triggers — user-created first, then built-ins").addHelpText("after", `
30165
+ Examples:
30166
+ ${chalk42.dim("$")} mutagent gateway triggers --json
30167
+
30168
+ Built-in triggers (the default @mention behaviour) are listed, marked 'builtin: true',
30169
+ and are read-only — 'update'/'enable'/'disable'/'delete' on one refuse (409). A built-in
30170
+ always lists as enabled: no user trigger displaces it.
30171
+
30172
+ AI Agent Directive:
30173
+ --json returns { ok, data: { items: Trigger[] } }. status is enabled | disabled | paused
30174
+ (paused = the SYSTEM paused it — see pausedReason; disabled = a user did). The
30175
+ built-ins (builtin: true) are always enabled.
30176
+ `).action(async () => {
30177
+ const isJson = gatewayIsJson(gateway);
30178
+ await runGatewayAction(isJson, undefined, async () => {
30179
+ const { items } = await listTriggers();
30180
+ printGatewayOk(isJson, { items }, {
30181
+ human: () => {
30182
+ if (items.length === 0) {
30183
+ console.log(chalk42.gray("No triggers."));
30184
+ return;
30185
+ }
30186
+ for (const t of items)
30187
+ printTrigger(t);
30188
+ }
30189
+ });
30190
+ return;
30191
+ }, RESOURCE);
30192
+ });
30193
+ return triggers;
30194
+ }
30195
+ function normalizeInstructionText(text) {
30196
+ return text.replace(/\r\n/g, `
30197
+ `).replace(/\r/g, `
30198
+ `);
30199
+ }
30200
+ function readInstruction(options) {
30201
+ if (options.instructionFile) {
30202
+ let text;
30203
+ try {
30204
+ text = readFileSync14(options.instructionFile, "utf8");
30205
+ } catch {
30206
+ throw new GatewayCliError("validation_error", `Could not read --instruction-file: ${options.instructionFile}`, [], 3, ["Check the path — --instruction-file takes a readable file."]);
30207
+ }
30208
+ return normalizeInstructionText(text);
30209
+ }
30210
+ if (options.instruction)
30211
+ return normalizeInstructionText(options.instruction);
30212
+ throw new GatewayCliError("validation_error", 'Pass --instruction "…" or --instruction-file f.md', ['mutagent gateway triggers create --on github.issue.opened --repo owner/name --instruction "<what the agent should do>"'], 3, ["Or read it from a file: --instruction-file <path>."]);
30213
+ }
30214
+ var WINDOW_UNIT_MINUTES = { m: 1, h: 60, d: 1440 };
30215
+ function parseWindowMinutes(raw) {
30216
+ const match = /^(\d+)(m|h|d)$/.exec(raw.trim());
30217
+ if (!match) {
30218
+ throw new GatewayCliError("validation_error", `--window must look like 60m, 24h or 7d, got: ${raw}`, [], 3, ["Re-run with --window 60m, --window 24h or --window 7d."]);
30219
+ }
30220
+ const amount = match[1];
30221
+ const unit = match[2];
30222
+ if (amount === undefined || unit === undefined) {
30223
+ throw new GatewayCliError("validation_error", `--window must look like 60m, 24h or 7d, got: ${raw}`, [], 3, ["Re-run with --window 60m, --window 24h or --window 7d."]);
30224
+ }
30225
+ return Number(amount) * (WINDOW_UNIT_MINUTES[unit] ?? 0);
30226
+ }
30227
+ function buildFilter(options) {
30228
+ if (options.on === "trace.threshold") {
30229
+ if (options.threshold === undefined || options.window === undefined) {
30230
+ throw new GatewayCliError("trigger_filter_invalid", "trace.threshold needs --threshold and --window.", ['mutagent gateway triggers create --on trace.threshold --threshold 100 --window 60m --repo owner/name --instruction "<what the agent should do>"'], 3);
30231
+ }
30232
+ return {
30233
+ traceThreshold: {
30234
+ threshold: Number(options.threshold),
30235
+ windowMinutes: parseWindowMinutes(options.window),
30236
+ ...options.service ? { serviceName: options.service } : {},
30237
+ ...options.session ? { sessionId: options.session } : {}
30238
+ }
30239
+ };
30240
+ }
30241
+ return options.channel ? { channelIds: options.channel } : undefined;
30242
+ }
30243
+ function registerCreate(triggers, gateway) {
30244
+ triggers.command("create").description("Create a trigger").option("--on <kind>", `REQUIRED — trigger kind: ${TRIGGER_KINDS.join(" | ")} | emit:<kind>`).option("--repo <owner/name>", "Repository (default: the workspace default repository)").option("--instruction <text>", "The instruction Helix runs").option("--instruction-file <path>", "Read the instruction from a file").option("--name <name>", "Name (default: derived from kind + repository)").option("--key <key>", "Idempotency key — a retry with the same key returns the same trigger").option("--channel <id...>", "slack.mention only: limit to these Slack channel ids").option("--threshold <n>", "trace.threshold only: fire at or above this many traces (1–1,000,000)").option("--window <duration>", "trace.threshold only: the counting window, e.g. 60m, 24h, 7d").option("--service <name>", "trace.threshold only: limit to this service name").option("--session <id>", "trace.threshold only: limit to this session id").addHelpText("after", `
30245
+ Examples:
30246
+ ${chalk42.dim("$")} mutagent gateway triggers create --on github.issue.opened --repo owner/name --instruction "Triage: label, estimate, ask one clarifying question if needed"
30247
+ ${chalk42.dim("$")} mutagent gateway triggers create --on slack.mention --channel C0123 --instruction "…"
30248
+ ${chalk42.dim("$")} mutagent gateway triggers create --on trace.threshold --threshold 100 --window 60m --repo owner/name --instruction "Investigate the volume spike"
30249
+
30250
+ The built-in @mention behaviour already answers every @mention, and no trigger displaces it:
30251
+ - github.mention is built-in only: the built-in answers every GitHub @mention on every
30252
+ repository the installation can reach, so --on github.mention is refused
30253
+ (trigger_kind_builtin_only). Use github.issue.opened for repository events, or a
30254
+ slack.mention trigger on a dedicated channel.
30255
+ - slack.mention needs --channel: the built-in answers in every channel the bot was invited
30256
+ to, and a trigger takes dedicated channels of its own. A channel another ENABLED trigger
30257
+ already holds is refused (trigger_conflict) — edit or delete that one first. Two
30258
+ triggers on different channels may run on the same repository.
30259
+
30260
+ A webhook trigger fires once per distinct delivery: two deliveries carrying identical
30261
+ bytes and no 'x-gateway-delivery-id' header are ONE event, however far apart they
30262
+ arrive. A trigger starts a real agent run, and with nothing to tell a retry from a new
30263
+ occurrence apart, the safe reading is a retry. To have a second delivery run again,
30264
+ send a fresh 'x-gateway-delivery-id' header, or put something distinguishing in the
30265
+ body such as a timestamp or a sequence number.
30266
+
30267
+ The 'x-gateway-delivery-id' header is COVERED BY THE SIGNATURE: sign
30268
+ 'v1:<delivery-id>:' followed by the raw body (empty id when you send no such
30269
+ header). An id the signature was not computed over is refused 401, so a captured
30270
+ body cannot be replayed under a fresh id by anyone who does not hold the secret.
30271
+
30272
+ Idempotent: the same --key (or the same --name) retried with the SAME request
30273
+ returns the same trigger; a DIFFERENT request under it is refused (key_conflict /
30274
+ name_taken) — pick a new --key/--name rather than silently overwriting.
30275
+
30276
+ AI Agent Directive:
30277
+ --json returns { ok, data: Trigger }, next: ["mutagent gateway triggers test <id> --event @sample.json"].
30278
+ Never pass --on github.mention: it is refused with code trigger_kind_builtin_only, and
30279
+ error.fix lists the runnable alternatives. For slack.mention always pass --channel.
30280
+ `).action(async (options) => {
30281
+ const isJson = gatewayIsJson(gateway);
30282
+ await runGatewayAction(isJson, undefined, async () => {
30283
+ if (options.on === undefined || options.on.trim() === "") {
30284
+ throw new GatewayCliError("validation_error", "A trigger needs a kind.", [], 3, [`Re-run with --on <kind>, one of: ${TRIGGER_KINDS.join(" | ")} | emit:<kind>.`]);
30285
+ }
30286
+ assertCreatableKind(options.on);
30287
+ const instruction = readInstruction(options);
30288
+ const trigger = await createTrigger({
30289
+ kind: options.on,
30290
+ repository: options.repo,
30291
+ instruction,
30292
+ name: options.name,
30293
+ key: options.key,
30294
+ filter: buildFilter(options)
30295
+ });
30296
+ printGatewayOk(isJson, trigger, {
30297
+ next: [`mutagent gateway triggers test ${trigger.id} --event @sample.json`],
30298
+ human: () => {
30299
+ console.log(chalk42.green(`✓ Created trigger ${trigger.id}`));
30300
+ printTrigger(trigger);
30301
+ }
30302
+ });
30303
+ return;
30304
+ }, RESOURCE);
30305
+ });
30306
+ }
30307
+ function registerShow2(triggers, gateway) {
30308
+ triggers.command("show").description("Show one trigger").argument("<id>", "Trigger id").action(async (id) => {
30309
+ const isJson = gatewayIsJson(gateway);
30310
+ await runGatewayAction(isJson, undefined, async () => {
30311
+ const trigger = await getTrigger(id);
30312
+ printGatewayOk(isJson, trigger, { human: () => {
30313
+ printTrigger(trigger);
30314
+ } });
30315
+ return;
30316
+ }, RESOURCE);
30317
+ });
30318
+ }
30319
+ function registerEnableDisable(triggers, gateway) {
30320
+ for (const verb of ["enable", "disable"]) {
30321
+ triggers.command(verb).description(`${verb === "enable" ? "Enable" : "Disable"} a trigger`).argument("<id>", "Trigger id").addHelpText("after", `
30322
+ Built-in triggers refuse this (409 builtin_trigger_read_only).
30323
+ `).action(async (id) => {
30324
+ const isJson = gatewayIsJson(gateway);
30325
+ await runGatewayAction(isJson, undefined, async () => {
30326
+ const trigger = await (verb === "enable" ? enableTrigger(id) : disableTrigger(id));
30327
+ printGatewayOk(isJson, trigger, { human: () => {
30328
+ printTrigger(trigger);
30329
+ } });
30330
+ return;
30331
+ }, RESOURCE);
30332
+ });
30333
+ }
30334
+ }
30335
+ function registerDelete(triggers, gateway) {
30336
+ triggers.command("delete").description("Delete a trigger").argument("<id>", "Trigger id").option("--yes", "Confirm — required, never prompted for").addHelpText("after", `
30337
+ Built-in triggers refuse this. Never prompts — pass --yes or exit 4.
30338
+ `).action(async (id, options) => {
30339
+ const isJson = gatewayIsJson(gateway);
30340
+ await runGatewayAction(isJson, undefined, async () => {
30341
+ if (options.yes !== true) {
30342
+ throw new GatewayCliError("confirmation_required", `Deleting trigger ${id} requires confirmation.`, [`mutagent gateway triggers delete ${id} --yes`], 4);
30343
+ }
30344
+ await deleteTrigger(id);
30345
+ printGatewayOk(isJson, { id, deleted: true }, { human: () => {
30346
+ console.log(chalk42.green(`✓ Deleted trigger ${id}`));
30347
+ } });
30348
+ return;
30349
+ }, RESOURCE);
30350
+ });
30351
+ }
30352
+ function registerTest(triggers, gateway) {
30353
+ triggers.command("test").description("Dry run: would a sample event match this trigger?").argument("<id>", "Trigger id").option("--event <file>", "REQUIRED — JSON sample event, as @file.json").addHelpText("after", `
30354
+ Examples:
30355
+ ${chalk42.dim("$")} mutagent gateway triggers test trg_abc --event @sample.json
30356
+
30357
+ sample.json: { "source": "github", "kind": "github.issues.opened", "repositoryId": "123" }
30358
+ Zero side effects — this never fires the trigger.
30359
+
30360
+ AI Agent Directive:
30361
+ --json returns { ok, data: { matches, wouldRun, checks: [{ check, passed, detail }] } }.
30362
+ `).action(async (id, options) => {
30363
+ const isJson = gatewayIsJson(gateway);
30364
+ await runGatewayAction(isJson, undefined, async () => {
30365
+ if (!options.event?.startsWith("@")) {
30366
+ throw new GatewayCliError("validation_error", "--event must be an @file.json reference", [], 3, ["Re-run with --event @sample.json — an @ reference to a JSON file."]);
30367
+ }
30368
+ let event;
30369
+ try {
30370
+ event = JSON.parse(readFileSync14(options.event.slice(1), "utf8"));
30371
+ } catch {
30372
+ throw new GatewayCliError("validation_error", `Could not read/parse ${options.event}`, [], 3);
30373
+ }
30374
+ const result = await testTrigger(id, event);
30375
+ printGatewayOk(isJson, result, {
30376
+ human: () => {
30377
+ console.log(`matches: ${String(result.matches)} wouldRun: ${String(result.wouldRun)}`);
30378
+ for (const c of result.checks)
30379
+ console.log(` ${c.passed ? chalk42.green("✓") : chalk42.red("✗")} ${c.check}: ${c.detail}`);
30380
+ }
30381
+ });
30382
+ return;
30383
+ }, RESOURCE);
30384
+ });
30385
+ }
30386
+ function registerGatewayTriggersCommands(gateway) {
30387
+ const triggers = registerList2(gateway);
30388
+ registerCreate(triggers, gateway);
30389
+ registerShow2(triggers, gateway);
30390
+ registerEnableDisable(triggers, gateway);
30391
+ registerDelete(triggers, gateway);
30392
+ registerTest(triggers, gateway);
30393
+ }
30394
+
30395
+ // src/commands/gateway/routines.ts
30396
+ import { readFileSync as readFileSync15 } from "node:fs";
30397
+ import { Command as Command19 } from "commander";
30398
+ import chalk43 from "chalk";
30399
+
30400
+ // src/lib/gateway-api-routines.ts
30401
+ var ROUTINE_EVERY_GRAMMAR = [
30402
+ "every N minutes (N: 1,2,3,4,5,6,10,12,15,20,30)",
30403
+ "every N hours (N: 1,2,3,4,6,8,12)",
30404
+ "hourly",
30405
+ "daily HH:MM",
30406
+ "weekdays HH:MM",
30407
+ "weekends HH:MM",
30408
+ "mon,wed,fri HH:MM"
30409
+ ];
30410
+ async function listRoutines() {
30411
+ const { workspaceId } = await resolveGatewayConnection();
30412
+ return gatewayRequest(`/api/gateway/routines${qs({ workspaceId })}`);
30413
+ }
30414
+ async function createRoutine(body) {
30415
+ const { workspaceId } = await resolveGatewayConnection();
30416
+ return gatewayRequest("/api/gateway/routines", { method: "POST", body: JSON.stringify({ workspaceId, ...body }) });
30417
+ }
30418
+ async function getRoutine(id) {
30419
+ const { workspaceId } = await resolveGatewayConnection();
30420
+ return gatewayRequest(`/api/gateway/routines/${encodeURIComponent(id)}${qs({ workspaceId })}`);
30421
+ }
30422
+ async function setRoutinePaused(id, paused) {
30423
+ const { workspaceId } = await resolveGatewayConnection();
30424
+ return gatewayRequest(`/api/gateway/routines/${encodeURIComponent(id)}/${paused ? "pause" : "resume"}`, {
30425
+ method: "POST",
30426
+ body: JSON.stringify({ workspaceId })
30427
+ });
30428
+ }
30429
+ var pauseRoutine = (id) => setRoutinePaused(id, true);
30430
+ var resumeRoutine = (id) => setRoutinePaused(id, false);
30431
+ async function deleteRoutine(id) {
30432
+ const { workspaceId } = await resolveGatewayConnection();
30433
+ await gatewayRequest(`/api/gateway/routines/${encodeURIComponent(id)}${qs({ workspaceId })}`, { method: "DELETE" });
30434
+ }
30435
+ async function runRoutineNow(id, key) {
30436
+ const { workspaceId } = await resolveGatewayConnection();
30437
+ return gatewayRequest(`/api/gateway/routines/${encodeURIComponent(id)}/run-now`, {
30438
+ method: "POST",
30439
+ body: JSON.stringify({ workspaceId, key })
30440
+ });
30441
+ }
30442
+
30443
+ // src/lib/gateway-run-wait.ts
30444
+ var TERMINAL_RUN_PHASES = new Set(["succeeded", "failed", "no-match"]);
30445
+ function withDisplayError(run) {
30446
+ return { ...run, error: renderRunErrorForDisplay(run.error) };
30447
+ }
30448
+ function runWaitTimeout(eventId, phase) {
30449
+ return new GatewayCliError("run_wait_timeout", `--wait stopped before the run settled: it is still ${phase}.`, [`mutagent gateway runs show ${eventId} --json`], 2, ["The run itself was not cancelled — it is still going. Nothing failed and nothing succeeded yet."]);
30450
+ }
30451
+ async function waitForRun(eventId) {
30452
+ const { value: value2, settled } = await pollUntil(() => getEventRun(eventId), (r) => TERMINAL_RUN_PHASES.has(r.phase));
30453
+ if (!settled)
30454
+ throw runWaitTimeout(eventId, value2.phase);
30455
+ return value2;
30456
+ }
30457
+
30458
+ // src/commands/gateway/routines.ts
30459
+ var RESOURCE2 = "routines";
30460
+ function printRoutine(r) {
30461
+ const mark = r.status === "active" ? chalk43.green("✓") : chalk43.yellow("⏸");
30462
+ console.log(` ${mark} ${r.id} ${r.name} ${chalk43.dim(`${r.schedule.cron} ${r.schedule.timezone}`)} ${r.repository.name}`);
30463
+ if (r.schedule.next.length > 0)
30464
+ console.log(chalk43.dim(` next: ${r.schedule.next.join(", ")}`));
30465
+ if (r.pausedReason)
30466
+ console.log(chalk43.yellow(` paused: ${r.pausedReason.reason}`));
30467
+ }
30468
+ function registerList3(gateway) {
30469
+ const routines = new Command19("routines").description("Schedule → agent run: create, list, show, pause, resume, run-now, delete");
30470
+ gateway.addCommand(routines);
30471
+ routines.description("List routines, with their next three fire times").addHelpText("after", `
30472
+ Examples:
30473
+ ${chalk43.dim("$")} mutagent gateway routines --json
30474
+
30475
+ AI Agent Directive:
30476
+ --json returns { ok, data: { items: Routine[] } }. schedule.next is 3 ISO-8601 UTC
30477
+ fire times — always present, computed from schedule.cron + schedule.timezone.
30478
+ `).action(async () => {
30479
+ const isJson = gatewayIsJson(gateway);
30480
+ await runGatewayAction(isJson, undefined, async () => {
30481
+ const { items } = await listRoutines();
30482
+ printGatewayOk(isJson, { items }, {
30483
+ human: () => {
30484
+ if (items.length === 0) {
30485
+ console.log(chalk43.gray("No routines."));
30486
+ return;
30487
+ }
30488
+ for (const r of items)
30489
+ printRoutine(r);
30490
+ }
30491
+ });
30492
+ return;
30493
+ }, RESOURCE2);
30494
+ });
30495
+ return routines;
30496
+ }
30497
+ function normalizeInstructionText2(text) {
30498
+ return text.replace(/\r\n/g, `
30499
+ `).replace(/\r/g, `
30500
+ `);
30501
+ }
30502
+ function readInstruction2(options) {
30503
+ if (options.instructionFile) {
30504
+ let text;
30505
+ try {
30506
+ text = readFileSync15(options.instructionFile, "utf8");
30507
+ } catch {
30508
+ throw new GatewayCliError("validation_error", `Could not read --instruction-file: ${options.instructionFile}`, [], 3, ["Check the path — --instruction-file takes a readable file."]);
30509
+ }
30510
+ return normalizeInstructionText2(text);
30511
+ }
30512
+ if (options.instruction)
30513
+ return normalizeInstructionText2(options.instruction);
30514
+ throw new GatewayCliError("validation_error", 'Pass --instruction "…" or --instruction-file f.md', [], 3);
30515
+ }
30516
+ function registerCreate2(routines, gateway) {
30517
+ routines.command("create").description("Create a routine").option("--every <schedule>", "Plain-language schedule — see --help for the grammar").option("--cron <expr>", "5-field cron expression (minute hour day-of-month month day-of-week)").option("--tz <IANA timezone>", "REQUIRED — e.g. Europe/Berlin").option("--repo <owner/name>", "REQUIRED — the repository the routine runs on").option("--instruction <text>", "The instruction Helix runs").option("--instruction-file <path>", "Read the instruction from a file").option("--name <name>", "Name (default: derived)").option("--key <key>", "Idempotency key").addHelpText("after", `
30518
+ Examples:
30519
+ ${chalk43.dim("$")} mutagent gateway routines create --every "weekdays 09:00" --tz Europe/Berlin --repo owner/name --instruction "Check main's failing CI and open an issue for each new failure"
30520
+ ${chalk43.dim("$")} mutagent gateway routines create --cron "0 9 * * 1-5" --tz America/New_York --repo owner/name --instruction "…"
30521
+
30522
+ Exactly one of --every / --cron. --tz is REQUIRED — omitting it is refused locally,
30523
+ before any request:
30524
+ mutagent gateway routines create --every "..." --repo owner/name --instruction "..."
30525
+ ✗ timezone_required [exit 3] Fix: pass --tz <IANA zone>, e.g. --tz Europe/Berlin
30526
+
30527
+ --every grammar:
30528
+ ${ROUTINE_EVERY_GRAMMAR.map((g) => ` ${g}`).join(`
30529
+ `)}
30530
+
30531
+ AI Agent Directive:
30532
+ --json returns { ok, data: Routine }. Output always shows the normalized cron,
30533
+ timezone and next three fire times — read data.schedule.
30534
+ `).action(async (options) => {
30535
+ const isJson = gatewayIsJson(gateway);
30536
+ await runGatewayAction(isJson, undefined, async () => {
30537
+ if (options.tz === undefined || options.tz.trim() === "") {
30538
+ throw new GatewayCliError("timezone_required", "A routine needs a timezone.", [], 3, ["Re-run the same command with --tz <IANA zone>, e.g. --tz Europe/Berlin."]);
30539
+ }
30540
+ if (options.repo === undefined || options.repo.trim() === "") {
30541
+ throw new GatewayCliError("validation_error", "A routine needs a repository.", [], 3, ["Re-run the same command with --repo owner/name."]);
30542
+ }
30543
+ if (options.every === undefined === (options.cron === undefined)) {
30544
+ throw new GatewayCliError("schedule_required", "Pass exactly one of --every or --cron.", [], 3, ['Re-run with exactly one of --every "weekdays 09:00" or --cron "0 9 * * 1-5".']);
30545
+ }
30546
+ const instruction = readInstruction2(options);
30547
+ const routine = await createRoutine({
30548
+ every: options.every,
30549
+ cron: options.cron,
30550
+ timezone: options.tz,
30551
+ repository: options.repo,
30552
+ instruction,
30553
+ name: options.name,
30554
+ key: options.key
30555
+ });
30556
+ printGatewayOk(isJson, routine, {
30557
+ next: [`mutagent gateway routines run-now ${routine.id} --wait`],
30558
+ human: () => {
30559
+ console.log(chalk43.green(`✓ Created routine ${routine.id}`));
30560
+ printRoutine(routine);
30561
+ }
30562
+ });
30563
+ return;
30564
+ }, RESOURCE2);
30565
+ });
30566
+ }
30567
+ function registerShow3(routines, gateway) {
30568
+ routines.command("show").argument("<id>", "Routine id").description("Show one routine").action(async (id) => {
30569
+ const isJson = gatewayIsJson(gateway);
30570
+ await runGatewayAction(isJson, undefined, async () => {
30571
+ const routine = await getRoutine(id);
30572
+ printGatewayOk(isJson, routine, { human: () => {
30573
+ printRoutine(routine);
30574
+ } });
30575
+ return;
30576
+ }, RESOURCE2);
30577
+ });
30578
+ }
30579
+ function registerPauseResume(routines, gateway) {
30580
+ for (const verb of ["pause", "resume"]) {
30581
+ routines.command(verb).argument("<id>", "Routine id").description(`${verb === "pause" ? "Pause" : "Resume"} a routine`).action(async (id) => {
30582
+ const isJson = gatewayIsJson(gateway);
30583
+ await runGatewayAction(isJson, undefined, async () => {
30584
+ const routine = await (verb === "pause" ? pauseRoutine(id) : resumeRoutine(id));
30585
+ printGatewayOk(isJson, routine, { human: () => {
30586
+ printRoutine(routine);
30587
+ } });
30588
+ return;
30589
+ }, RESOURCE2);
30590
+ });
30591
+ }
30592
+ }
30593
+ function registerDelete2(routines, gateway) {
30594
+ routines.command("delete").argument("<id>", "Routine id").option("--yes", "Confirm — required, never prompted for").description("Delete a routine").action(async (id, options) => {
30595
+ const isJson = gatewayIsJson(gateway);
30596
+ await runGatewayAction(isJson, undefined, async () => {
30597
+ if (options.yes !== true) {
30598
+ throw new GatewayCliError("confirmation_required", `Deleting routine ${id} requires confirmation.`, [`mutagent gateway routines delete ${id} --yes`], 4);
30599
+ }
30600
+ await deleteRoutine(id);
30601
+ printGatewayOk(isJson, { id, deleted: true }, { human: () => {
30602
+ console.log(chalk43.green(`✓ Deleted routine ${id}`));
30603
+ } });
30604
+ return;
30605
+ }, RESOURCE2);
30606
+ });
30607
+ }
30608
+ function registerRunNow(routines, gateway) {
30609
+ routines.command("run-now").argument("<id>", "Routine id").option("--wait", "Poll until the run settles instead of returning the event id immediately").option("--key <key>", "Idempotency key (default: a fresh key each call — same key is the same run)").description("Run a routine once, right now").addHelpText("after", `
30610
+ Examples:
30611
+ ${chalk43.dim("$")} mutagent gateway routines run-now rtn_abc --wait
30612
+
30613
+ Idempotent per key: the same --key returns the SAME run, not a new one.
30614
+
30615
+ AI Agent Directive:
30616
+ --json without --wait: { ok, data: { eventId, created } }. created:false means this
30617
+ key already ran — data.eventId is that run's event, not a new one.
30618
+ `).action(async (id, options) => {
30619
+ const isJson = gatewayIsJson(gateway);
30620
+ await runGatewayAction(isJson, undefined, async () => {
30621
+ const key = options.key ?? `cli-run-now-${id}-${String(Date.now())}`;
30622
+ const result = await runRoutineNow(id, key);
30623
+ if (options.wait !== true) {
30624
+ printGatewayOk(isJson, result, {
30625
+ next: [`mutagent gateway runs show ${result.eventId} --json`],
30626
+ human: () => {
30627
+ console.log(chalk43.dim(`event: ${result.eventId}${result.created ? "" : " (existing run for this key)"}`));
30628
+ }
30629
+ });
30630
+ return;
30631
+ }
30632
+ const run = await waitForRun(result.eventId);
30633
+ if (run.phase === "failed") {
30634
+ throw gatewayErrorFromRunError({ code: run.error?.code ?? "server_fault", reason: run.error?.reason ?? "The run failed.", fix: run.error?.fix }, { resource: RESOURCE2 });
30635
+ }
30636
+ const display = withDisplayError(run);
30637
+ printGatewayOk(isJson, display, { human: () => {
30638
+ console.log(`${display.eventId} ${display.phase}`);
30639
+ } });
30640
+ return;
30641
+ }, RESOURCE2);
30642
+ });
30643
+ }
30644
+ function registerGatewayRoutinesCommands(gateway) {
30645
+ const routines = registerList3(gateway);
30646
+ registerCreate2(routines, gateway);
30647
+ registerShow3(routines, gateway);
30648
+ registerPauseResume(routines, gateway);
30649
+ registerDelete2(routines, gateway);
30650
+ registerRunNow(routines, gateway);
30651
+ }
30652
+
30653
+ // src/commands/gateway/runs.ts
30654
+ import { randomUUID as randomUUID5 } from "node:crypto";
30655
+ import { readFileSync as readFileSync16 } from "node:fs";
30656
+ import { Command as Command20 } from "commander";
30657
+ import chalk44 from "chalk";
30658
+
30659
+ // src/lib/gateway-api-runs.ts
30660
+ async function listRuns(opts) {
30661
+ const { workspaceId, status, since, triggerId, routineId, cursor, limit } = opts;
30662
+ return gatewayRequest(`/api/gateway/runs${qs({ workspaceId, status, since, triggerId, routineId, cursor, limit: limit === undefined ? undefined : String(limit) })}`);
30663
+ }
30664
+ async function getRun(runId, workspaceId) {
30665
+ return gatewayRequest(`/api/gateway/runs/${encodeURIComponent(runId)}${qs({ workspaceId })}`);
30666
+ }
30667
+ async function getRunLogs(runId, workspaceId, after, limit) {
30668
+ return gatewayRequest(`/api/gateway/runs/${encodeURIComponent(runId)}/logs${qs({
30669
+ workspaceId,
30670
+ after: after === undefined ? undefined : String(after),
30671
+ limit: limit === undefined ? undefined : String(limit)
30672
+ })}`);
30673
+ }
30674
+
30675
+ // src/commands/gateway/runs.ts
30676
+ async function resolveRepositoryId(repoArg) {
30677
+ if (repoArg !== undefined) {
30678
+ const repo = await findRepositoryByName(repoArg);
30679
+ if (repo === undefined) {
30680
+ throw gatewayErrorFromCode("repository_not_found", `${repoArg} is not reachable through this workspace's GitHub installation.`);
30681
+ }
30682
+ return repo.id;
30683
+ }
30684
+ const current = await getDefaultRepository();
30685
+ if (current.defaultRepository === null) {
30686
+ throw gatewayErrorFromCode("no_default_repository", "No repository given and no workspace default repository set.");
30687
+ }
30688
+ return current.defaultRepository.repositoryId;
30689
+ }
30690
+ function printRunStatus(run) {
30691
+ console.log(`${chalk44.bold(run.eventId)} ${run.phase}`);
30692
+ if (run.sandbox)
30693
+ console.log(chalk44.dim(` sandbox: ${run.sandbox.kind} (${run.sandbox.environment})`));
30694
+ if (run.startedAt)
30695
+ console.log(chalk44.dim(` started: ${run.startedAt}`));
30696
+ if (run.finishedAt)
30697
+ console.log(chalk44.dim(` finished: ${run.finishedAt}`));
30698
+ if (run.error) {
30699
+ console.log(chalk44.red(` ${run.error.code}: ${run.error.reason}`));
30700
+ for (const step of run.error.fix)
30701
+ console.log(chalk44.yellow(` Fix: ${step}`));
30702
+ }
30703
+ }
30704
+ function registerTestRun(gateway) {
30705
+ gateway.command("test-run").description("Run the built-in onboarding test agent on a repository").option("--repo <owner/name>", "Repository to run on (default: the workspace default repository)").option("--wait", "Poll until the run settles instead of returning the event id immediately").option("--key <key>", "Idempotency key — the same key is one run (default: a fresh key each call)").addHelpText("after", `
30706
+ Examples:
30707
+ ${chalk44.dim("$")} mutagent gateway test-run --wait
30708
+ ${chalk44.dim("$")} mutagent gateway test-run --repo owner/name --key onboarding-check-1
30709
+
30710
+ Proves the whole pipeline runs: GitHub/Slack connection, repository access, and a
30711
+ Helix agent turn in a sandbox, latest default-branch commit.
30712
+
30713
+ AI Agent Directive:
30714
+ --json without --wait returns { ok, data: { eventId } } immediately — follow with
30715
+ 'mutagent gateway runs show <eventId> --json' or pass --wait to block until it settles.
30716
+ With --wait: { ok, data: RunStatus }. ok is false and exit is non-zero when the run's
30717
+ terminal phase is 'failed' — read data.error for the code and reason.
30718
+ `).action(async (options) => {
30719
+ const isJson = gatewayIsJson(gateway);
30720
+ await runGatewayAction(isJson, undefined, async () => {
30721
+ const repositoryId = await resolveRepositoryId(options.repo);
30722
+ const key = options.key ?? `cli-test-run-${randomUUID5()}`;
30723
+ const { eventId } = await startTestRun(key, repositoryId);
30724
+ if (options.wait !== true) {
30725
+ printGatewayOk(isJson, { eventId }, {
30726
+ next: [`mutagent gateway runs show ${eventId} --json`],
30727
+ human: () => {
30728
+ console.log(chalk44.dim(`event: ${eventId}`));
30729
+ }
30730
+ });
30731
+ return;
30732
+ }
30733
+ const run = await waitForRun(eventId);
30734
+ if (run.phase === "failed") {
30735
+ throw gatewayErrorFromRunError(run.error ?? { code: "server_fault", reason: "The test run failed.", fix: null });
30736
+ }
30737
+ const display = withDisplayError(run);
30738
+ printGatewayOk(isJson, display, { human: () => {
30739
+ printRunStatus(display);
30740
+ } });
30741
+ return;
30742
+ });
30743
+ });
30744
+ }
30745
+ function printRunSummary(r) {
30746
+ console.log(` ${r.id} ${chalk44.dim(r.state)} ${r.origin.kind}${r.origin.id ? `:${r.origin.id}` : ""} ${r.repository?.name ?? ""}`);
30747
+ if (r.error) {
30748
+ console.log(chalk44.red(` ${r.error.code}: ${r.error.reason}`));
30749
+ for (const step of r.error.fix)
30750
+ console.log(chalk44.yellow(` Fix: ${step}`));
30751
+ }
30752
+ }
30753
+ function registerRunsList(runs, gateway) {
30754
+ runs.description("Runs from triggers, routines, the test run and emit — newest first").option("--status <s>", "Comma-separated states to filter to").option("--since <duration>", "ISO-8601 timestamp, or a duration like 30m / 24h / 7d").option("--trigger <id>", "Only runs launched by this trigger").option("--routine <id>", "Only runs launched by this routine").addHelpText("after", `
30755
+ Examples:
30756
+ ${chalk44.dim("$")} mutagent gateway runs --since 24h --json
30757
+ ${chalk44.dim("$")} mutagent gateway runs --status failed --json
30758
+
30759
+ AI Agent Directive:
30760
+ --json returns { ok, data: { items: RunSummary[] } }. state is one of queued |
30761
+ allocating | checkout_ready | grant_ready | running | succeeded | failed |
30762
+ cancelled | unknown.
30763
+ `).action(async (options) => {
30764
+ const isJson = gatewayIsJson(gateway);
30765
+ await runGatewayAction(isJson, undefined, async () => {
30766
+ const { workspaceId } = await resolveGatewayConnection();
30767
+ const { items } = await listRuns({
30768
+ workspaceId,
30769
+ status: options.status,
30770
+ since: options.since,
30771
+ triggerId: options.trigger,
30772
+ routineId: options.routine
30773
+ });
30774
+ const display = items.map(withDisplayError);
30775
+ printGatewayOk(isJson, { items: display }, {
30776
+ human: () => {
30777
+ if (display.length === 0) {
30778
+ console.log(chalk44.gray("No runs."));
30779
+ return;
30780
+ }
30781
+ for (const r of display)
30782
+ printRunSummary(r);
30783
+ }
30784
+ });
30785
+ return;
30786
+ });
30787
+ });
30788
+ }
30789
+ function registerRunsShow(runs, gateway) {
30790
+ runs.command("show").description("Run status — by event id or run id").argument("<id>", "The event id (from test-run/emit) or the run id (from runs list)").addHelpText("after", `
30791
+ Examples:
30792
+ ${chalk44.dim("$")} mutagent gateway runs show evt_abc123 --json
30793
+ ${chalk44.dim("$")} mutagent gateway runs show run_abc123 --json
30794
+
30795
+ Tries the event-id route first ('GET events/:eventId/run', unchanged since test-run
30796
+ and emit have always used it); an id that route doesn't recognise (not_found) is
30797
+ retried as a run id ('GET runs/:runId'). An id neither route recognises reports the
30798
+ run-id lookup's not_found, since that is the more specific check.
30799
+
30800
+ AI Agent Directive:
30801
+ --json returns { ok, data }. An event-id hit has 'phase'; a run-id hit has 'state'
30802
+ — branch on whichever key is present.
30803
+ `).action(async (id) => {
30804
+ const isJson = gatewayIsJson(gateway);
30805
+ await runGatewayAction(isJson, undefined, async () => {
30806
+ try {
30807
+ const run2 = await getEventRun(id);
30808
+ const display2 = withDisplayError(run2);
30809
+ printGatewayOk(isJson, display2, { human: () => {
30810
+ printRunStatus(display2);
30811
+ } });
30812
+ return;
30813
+ } catch (error3) {
30814
+ if (!(error3 instanceof GatewayApiError) || error3.code !== "not_found")
30815
+ throw error3;
30816
+ }
30817
+ const { workspaceId } = await resolveGatewayConnection();
30818
+ const run = await getRun(id, workspaceId);
30819
+ const display = withDisplayError(run);
30820
+ printGatewayOk(isJson, display, { human: () => {
30821
+ printRunSummary(display);
30822
+ } });
30823
+ return;
30824
+ });
30825
+ });
30826
+ }
30827
+ function registerRunsLogs(runs, gateway) {
30828
+ runs.command("logs").description("A run's logs, from its Helix session").argument("<runId>", "The run id (from runs list / runs show)").option("--follow", "Poll and print new entries until the run is done").addHelpText("after", `
30829
+ Examples:
30830
+ ${chalk44.dim("$")} mutagent gateway runs logs run_abc123 --json
30831
+ ${chalk44.dim("$")} mutagent gateway runs logs run_abc123 --follow
30832
+
30833
+ --follow polls with 'after = nextAfter' until 'done'. archivedBefore, when non-null,
30834
+ names the seq below which entries were sealed into archived segments and are not
30835
+ served here — a stated gap, not a silent skip.
30836
+
30837
+ AI Agent Directive:
30838
+ --json (no --follow) returns ONE page: { ok, data: { items, nextAfter, done,
30839
+ archivedBefore } }. --follow --json prints one such envelope per page until done.
30840
+ `).action(async (runId, options) => {
30841
+ const isJson = gatewayIsJson(gateway);
30842
+ await runGatewayAction(isJson, undefined, async () => {
30843
+ const { workspaceId } = await resolveGatewayConnection();
30844
+ let after;
30845
+ for (;; ) {
30846
+ const page = await getRunLogs(runId, workspaceId, after);
30847
+ printGatewayOk(isJson, page, {
30848
+ human: () => {
30849
+ for (const entry of page.items)
30850
+ console.log(`${chalk44.dim(entry.at)} ${entry.text}`);
30851
+ }
30852
+ });
30853
+ if (options.follow !== true || page.done)
30854
+ return;
30855
+ after = page.nextAfter;
30856
+ await new Promise((resolve9) => {
30857
+ setTimeout(resolve9, Number(process.env.MUTAGENT_TEST_POLL_INTERVAL_MS ?? 2000));
30858
+ });
30859
+ }
30860
+ });
30861
+ });
30862
+ }
30863
+ function readPayloadFile(path) {
30864
+ let text;
30865
+ try {
30866
+ text = readFileSync16(path, "utf8");
30867
+ } catch {
30868
+ throw new GatewayCliError("validation_error", `Could not read payload file: ${path}`, [], 3, ["--payload takes an @file.json reference to a readable file — check the path."]);
30869
+ }
30870
+ try {
30871
+ return JSON.parse(text);
30872
+ } catch {
30873
+ throw new GatewayCliError("validation_error", `Payload file is not valid JSON: ${path}`, [], 3);
30874
+ }
30875
+ }
30876
+ function registerEmit(gateway) {
30877
+ gateway.command("emit").description("Emit a user-defined event into the gateway").argument("<kind>", "Event kind — matched against emit:<kind> triggers").option("--key <key>", "Idempotency key (default: a fresh key each call)").option("--payload <file>", "JSON payload, as @file.json").option("--wait", "Poll every resulting run to a terminal phase before returning").addHelpText("after", `
30878
+ Examples:
30879
+ ${chalk44.dim("$")} mutagent gateway emit deploy.finished --payload @payload.json --wait
30880
+ ${chalk44.dim("$")} mutagent gateway emit heartbeat --key daily-2026-09-22
30881
+
30882
+ --payload takes an '@' file reference, e.g. --payload @payload.json — never inline JSON
30883
+ (shells mangle quoting on long payloads).
30884
+
30885
+ Some kinds are reserved for the platform's own use (routine.*, onboarding.*,
30886
+ trace.threshold, github.ci-failed) and are refused — use your own kind, or for a
30887
+ routine: mutagent gateway routines run-now <id>.
30888
+
30889
+ AI Agent Directive:
30890
+ --json returns { ok, data: { kind, eventIds } } (or the awaited RunStatus array with
30891
+ --wait). No matching 'emit:<kind>' trigger is not an error: eventIds may be empty.
30892
+ `).action(async (kind, options) => {
30893
+ const isJson = gatewayIsJson(gateway);
30894
+ await runGatewayAction(isJson, undefined, async () => {
30895
+ const key = options.key ?? `cli-emit-${randomUUID5()}`;
30896
+ const payload = options.payload?.startsWith("@") ? readPayloadFile(options.payload.slice(1)) : undefined;
30897
+ const result = await emitEvent(key, kind, payload);
30898
+ if (options.wait !== true || result.eventIds.length === 0) {
30899
+ printGatewayOk(isJson, result, {
30900
+ human: () => {
30901
+ console.log(`${result.kind} ${chalk44.dim(`${String(result.eventIds.length)} event(s)`)}`);
30902
+ for (const id of result.eventIds)
30903
+ console.log(` ${id}`);
30904
+ }
30905
+ });
30906
+ return;
30907
+ }
30908
+ const runs = [];
30909
+ for (const eventId of result.eventIds) {
30910
+ runs.push(await waitForRun(eventId));
30911
+ }
30912
+ const failed = runs.find((r) => r.phase === "failed");
30913
+ if (failed) {
30914
+ const failedIds = runs.filter((r) => r.phase === "failed").map((r) => r.eventId);
30915
+ throw gatewayErrorFromRunError({
30916
+ code: failed.error?.code ?? "server_fault",
30917
+ reason: `${failed.error?.reason ?? "A run failed."} (${String(failedIds.length)}/${String(runs.length)} failed: ${failedIds.join(", ")})`,
30918
+ fix: failed.error?.fix
30919
+ });
30920
+ }
30921
+ const displayRuns = runs.map(withDisplayError);
30922
+ printGatewayOk(isJson, { ...result, runs: displayRuns }, { human: () => {
30923
+ for (const run of displayRuns)
30924
+ printRunStatus(run);
30925
+ } });
30926
+ return;
30927
+ });
30928
+ });
30929
+ }
30930
+ function registerGatewayRunCommands(gateway) {
30931
+ registerTestRun(gateway);
30932
+ const runs = new Command20("runs");
30933
+ gateway.addCommand(runs);
30934
+ registerRunsList(runs, gateway);
30935
+ registerRunsShow(runs, gateway);
30936
+ registerRunsLogs(runs, gateway);
30937
+ registerEmit(gateway);
30938
+ }
30939
+
30940
+ // src/commands/gateway/admin.ts
30941
+ import { Command as Command21 } from "commander";
30942
+ import chalk45 from "chalk";
30943
+
30944
+ // src/lib/gateway-admin.ts
30945
+ import { execFile } from "node:child_process";
30946
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "node:fs";
30947
+ import { dirname as dirname7, join as join21 } from "node:path";
30948
+ var SERVER_PACKAGE_NAME = "@mutagent/server";
30949
+ function isServerDir(dir) {
30950
+ const pkgPath = join21(dir, "package.json");
30951
+ if (!existsSync13(pkgPath))
30952
+ return false;
30953
+ try {
30954
+ const pkg = JSON.parse(readFileSync17(pkgPath, "utf8"));
30955
+ return pkg.name === SERVER_PACKAGE_NAME;
30956
+ } catch {
30957
+ return false;
30958
+ }
30959
+ }
30960
+ function resolveMutagentServerDir() {
30961
+ const envDir = process.env.MUTAGENT_SERVER_DIR;
30962
+ if (envDir !== undefined && envDir !== "" && isServerDir(envDir))
30963
+ return envDir;
30964
+ let dir = process.cwd();
30965
+ for (let depth = 0;depth < 8; depth += 1) {
30966
+ const candidate = join21(dir, "mutagent");
30967
+ if (isServerDir(candidate))
30968
+ return candidate;
30969
+ const parent = dirname7(dir);
30970
+ if (parent === dir)
30971
+ break;
30972
+ dir = parent;
30973
+ }
30974
+ return;
30975
+ }
30976
+ function requireMutagentServerDir() {
30977
+ const dir = resolveMutagentServerDir();
30978
+ if (dir === undefined) {
30979
+ throw new GatewayCliError("server_checkout_not_found", "This command needs the mutagent/ server checkout (it wraps an operator script, not an HTTP route).", [], 3, [
30980
+ "Run this from inside the mutagent-monorepo checkout — a directory with a mutagent/ sibling.",
30981
+ "Or set MUTAGENT_SERVER_DIR=/path/to/mutagent and re-run."
30982
+ ]);
30983
+ }
30984
+ return dir;
30985
+ }
30986
+ function runServerScript(script, args = []) {
30987
+ const serverDir = requireMutagentServerDir();
30988
+ return new Promise((resolve9) => {
30989
+ execFile("bun", ["run", script, ...args], { cwd: serverDir, env: process.env, maxBuffer: 10 * 1024 * 1024 }, (error3, stdout, stderr) => {
30990
+ const exitCode = error3 === null ? 0 : typeof error3.code === "number" ? error3.code : 1;
30991
+ resolve9({ exitCode, stdout, stderr });
30992
+ });
30993
+ });
30994
+ }
30995
+
30996
+ // src/lib/gateway-api-limits.ts
30997
+ var WORKSPACE_LIMIT_BOUNDS = {
30998
+ concurrent: { min: 1, max: 1000 },
30999
+ daily: { min: 1, max: 1e5 }
31000
+ };
31001
+ async function getWorkspaceLimits() {
31002
+ const { workspaceId } = await resolveGatewayConnection();
31003
+ return gatewayRequest(`/api/gateway/limits${qs({ workspaceId })}`);
31004
+ }
31005
+ async function setWorkspaceLimits(body) {
31006
+ const { workspaceId } = await resolveGatewayConnection();
31007
+ return gatewayRequest("/api/gateway/limits", {
31008
+ method: "PUT",
31009
+ body: JSON.stringify({ workspaceId, ...body })
31010
+ });
31011
+ }
31012
+
31013
+ // src/commands/gateway/admin.ts
31014
+ init_known_secrets();
31015
+ function registerPreflight(admin, gateway) {
31016
+ admin.command("preflight").description("End-to-end check: could this deployment serve a gateway delivery right now?").option("--workspace <id>", "Scope repository/channel/target checks to one workspace (default: the configured workspace)").addHelpText("after", `
31017
+ Examples:
31018
+ ${chalk45.dim("$")} mutagent gateway admin preflight --json
31019
+
31020
+ Wraps 'bun run gateway:preflight' in the mutagent/ server checkout. Operator-only —
31021
+ see 'mutagent gateway admin --help' for what that requires (checkout + DATABASE_URL).
31022
+
31023
+ AI Agent Directive:
31024
+ --json returns { ok, data: { verdict, checked, findings } }. ok is (verdict === 'PASS').
31025
+ Each finding names the broken item and the fix in its own 'detail'.
31026
+ `).action(async (options) => {
31027
+ const isJson = gatewayIsJson(gateway);
31028
+ await runGatewayAction(isJson, undefined, async () => {
31029
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
31030
+ const workspace = options.workspace ?? loadConfig2().defaultWorkspace;
31031
+ const args = ["--json", ...workspace ? ["--workspace", workspace] : []];
31032
+ const result = await runServerScript("gateway:preflight", args);
31033
+ let report;
31034
+ try {
31035
+ report = JSON.parse(result.stdout);
31036
+ } catch {
31037
+ throw new GatewayCliError("server_fault", `gateway:preflight did not print JSON (exit ${String(result.exitCode)}): ${scrubKnownSuccess(result.stderr || result.stdout).slice(0, 500)}`, [], 1);
31038
+ }
31039
+ const ok = report.verdict === "PASS";
31040
+ if (isJson) {
31041
+ console.log(JSON.stringify({ ok, data: report }, null, 2));
31042
+ } else {
31043
+ console.log(`${ok ? chalk45.green("PASS") : chalk45.red("BLOCKED")} ${chalk45.dim(`${String(report.checked.length)} checked`)}`);
31044
+ for (const finding of report.findings)
31045
+ console.log(` ${chalk45.red("✗")} ${finding.item}: ${finding.detail}`);
31046
+ }
31047
+ return ok ? undefined : 3;
31048
+ });
31049
+ });
31050
+ }
31051
+ function registerSeedApps(admin, gateway) {
31052
+ admin.command("seed-apps").description("Load a GitHub App's / Slack app's credentials from the environment").addHelpText("after", `
31053
+ Examples:
31054
+ ${chalk45.dim("$")} mutagent gateway admin seed-apps --json
31055
+
31056
+ Wraps 'bun run gateway:seed-apps' in the mutagent/ server checkout. Operator-only —
31057
+ see 'mutagent gateway admin --help'. Credentials come from the environment ONLY
31058
+ (GATEWAY_GITHUB_APP_*, GATEWAY_SLACK_APP_*, or mutagent/.env) and go straight to the
31059
+ database — never over the API, and never from a CLI argument, so nothing here can
31060
+ land a secret in shell history. Idempotent: a second run with the same input reports
31061
+ 'unchanged'.
31062
+
31063
+ AI Agent Directive:
31064
+ --json returns { ok, data: { exitCode, output } }. ok is (exitCode === 0). The script
31065
+ never prints a secret; 'output' is safe to show the user verbatim.
31066
+ `).action(async () => {
31067
+ const isJson = gatewayIsJson(gateway);
31068
+ await runGatewayAction(isJson, undefined, async () => {
31069
+ const result = await runServerScript("gateway:seed-apps");
31070
+ const output = scrubKnownSuccess(`${result.stdout}${result.stderr}`.trim());
31071
+ if (result.exitCode !== 0) {
31072
+ throw new GatewayCliError("seed_apps_failed", output || `gateway:seed-apps exited ${String(result.exitCode)}`, [], result.exitCode === 2 ? 3 : 1, result.exitCode === 2 ? ["Set the missing GATEWAY_GITHUB_APP_* / GATEWAY_SLACK_APP_* environment variables, then re-run this command."] : []);
31073
+ }
31074
+ printGatewayOk(isJson, { exitCode: result.exitCode, output }, {
31075
+ human: () => {
31076
+ console.log(output);
31077
+ }
31078
+ });
31079
+ return;
31080
+ });
31081
+ });
31082
+ }
31083
+ function registerLimits(admin, gateway) {
31084
+ const limits = new Command21("limits").description("Show or set the workspace's run-concurrency and daily-run caps").addHelpText("after", `
31085
+ Examples:
31086
+ ${chalk45.dim("$")} mutagent gateway admin limits --json
31087
+ ${chalk45.dim("$")} mutagent gateway admin limits set --concurrent 5 --daily 100 --json
31088
+
31089
+ Bare (or 'admin limits'): shows the current caps. 'set': changes them — admins only.
31090
+
31091
+ AI Agent Directive:
31092
+ --json returns { ok, data: { workspaceId, concurrent, daily, source, usedToday, day } }.
31093
+ source is 'workspace' (an explicit override) or 'default' (the server-wide default,
31094
+ not yet overridden for this workspace). day/usedToday reset at UTC midnight.
31095
+ `);
31096
+ admin.addCommand(limits);
31097
+ limits.action(async () => {
31098
+ const isJson = gatewayIsJson(gateway);
31099
+ await runGatewayAction(isJson, undefined, async () => {
31100
+ const current = await getWorkspaceLimits();
31101
+ printGatewayOk(isJson, current, {
31102
+ human: () => {
31103
+ console.log(`concurrent: ${String(current.concurrent)} daily: ${String(current.daily)} ${chalk45.dim(`(${current.source})`)}`);
31104
+ console.log(chalk45.dim(`used today (${current.day}): ${String(current.usedToday)} / ${String(current.daily)}`));
31105
+ }
31106
+ });
31107
+ return;
31108
+ });
31109
+ });
31110
+ limits.command("set").description("Set the workspace's run-concurrency and/or daily-run caps").option("--concurrent <n>", `Runs in flight at once (${String(WORKSPACE_LIMIT_BOUNDS.concurrent.min)}–${String(WORKSPACE_LIMIT_BOUNDS.concurrent.max)})`).option("--daily <n>", `Runs started per UTC day (${String(WORKSPACE_LIMIT_BOUNDS.daily.min)}–${String(WORKSPACE_LIMIT_BOUNDS.daily.max)})`).addHelpText("after", `
31111
+ Examples:
31112
+ ${chalk45.dim("$")} mutagent gateway admin limits set --concurrent 5 --daily 100
31113
+
31114
+ Admins only (workspace admin, or org owner/admin) — anyone else is refused.
31115
+ Pass one or both of --concurrent / --daily; the other stays unchanged.
31116
+
31117
+ AI Agent Directive:
31118
+ --json returns { ok, data: WorkspaceLimits } — the shape 'admin limits' shows.
31119
+ An out-of-range value fails locally (exit 3) before any request.
31120
+ `).action(async (options) => {
31121
+ const isJson = gatewayIsJson(gateway);
31122
+ await runGatewayAction(isJson, undefined, async () => {
31123
+ if (options.concurrent === undefined && options.daily === undefined) {
31124
+ throw new GatewayCliError("validation_error", "Pass --concurrent and/or --daily.", ["mutagent gateway admin limits set --concurrent 5 --daily 100"], 3);
31125
+ }
31126
+ const body = {};
31127
+ for (const [key, bounds] of Object.entries(WORKSPACE_LIMIT_BOUNDS)) {
31128
+ const raw = key === "concurrent" ? options.concurrent : options.daily;
31129
+ if (raw === undefined)
31130
+ continue;
31131
+ const value2 = Number(raw);
31132
+ if (!Number.isInteger(value2) || value2 < bounds.min || value2 > bounds.max) {
31133
+ throw new GatewayCliError("limit_out_of_range", `--${key} must be an integer between ${String(bounds.min)} and ${String(bounds.max)}, got: ${raw}`, [`mutagent gateway admin limits set --${key} <n>`], 3, [`--${key} must be between ${String(bounds.min)} and ${String(bounds.max)}.`]);
31134
+ }
31135
+ body[key] = value2;
31136
+ }
31137
+ const updated = await setWorkspaceLimits(body);
31138
+ printGatewayOk(isJson, updated, {
31139
+ human: () => {
31140
+ console.log(chalk45.green(`✓ concurrent: ${String(updated.concurrent)} daily: ${String(updated.daily)}`));
31141
+ }
31142
+ });
31143
+ return;
31144
+ });
31145
+ });
31146
+ }
31147
+ function registerGatewayAdminCommands(gateway) {
31148
+ const admin = new Command21("admin").description("Operator setup, and the workspace run-caps").addHelpText("after", `
31149
+ 'preflight' and 'seed-apps' are OPERATOR commands: run them from a Mutagent server
31150
+ checkout (mutagent-monorepo's mutagent/) with DATABASE_URL set. Neither wraps an
31151
+ HTTP route; both shell out to the server's own scripts (bun run gateway:preflight /
31152
+ gateway:seed-apps). Environment variables pass through untouched and are never
31153
+ echoed.
31154
+
31155
+ 'limits' and 'limits set' are ordinary API calls — any workspace admin can run
31156
+ them from anywhere, no checkout needed.
31157
+ `);
31158
+ gateway.addCommand(admin);
31159
+ registerPreflight(admin, gateway);
31160
+ registerSeedApps(admin, gateway);
31161
+ registerLimits(admin, gateway);
31162
+ }
31163
+
31164
+ // src/commands/gateway/index.ts
31165
+ function createGatewayCommand() {
31166
+ const gateway = new Command22("gateway").description("Connect GitHub/Slack, run and inspect the Helix gateway");
31167
+ registerGatewayStatusCommands(gateway);
31168
+ registerGatewayConnectCommands(gateway);
31169
+ registerGatewayReposCommands(gateway);
31170
+ registerGatewayTriggersCommands(gateway);
31171
+ registerGatewayRoutinesCommands(gateway);
31172
+ registerGatewayRunCommands(gateway);
31173
+ registerGatewayAdminCommands(gateway);
31174
+ gateway.addHelpText("after", `
31175
+ ${chalk46.yellow("Start here:")}
31176
+ mutagent gateway ${chalk46.dim("= status: connected, missing, next command")}
31177
+ mutagent gateway status [--json]
31178
+ mutagent gateway doctor ${chalk46.dim("End-to-end check, with fixes")}
31179
+
31180
+ ${chalk46.yellow("Configure:")}
31181
+ mutagent gateway connect github|slack [--wait] [--open] [--attempt <id>]
31182
+ mutagent gateway reconnect github|slack
31183
+ mutagent gateway disconnect github|slack --yes
31184
+ mutagent gateway repos [--available|--linked]
31185
+ mutagent gateway repos default [owner/name]
31186
+ mutagent gateway repos link owner/name…
31187
+ mutagent gateway repos unlink owner/name --yes
31188
+ mutagent gateway repos grant-more
31189
+
31190
+ ${chalk46.yellow("Configure — triggers (event → agent run):")}
31191
+ mutagent gateway triggers
31192
+ mutagent gateway triggers create --on <kind> [--repo owner/name] (--instruction "…" | --instruction-file f.md) [--name n]
31193
+ mutagent gateway triggers show|enable|disable <id>
31194
+ mutagent gateway triggers delete <id> --yes
31195
+ mutagent gateway triggers test <id> --event @sample.json
31196
+
31197
+ ${chalk46.yellow("Configure — routines (schedule → agent run):")}
31198
+ mutagent gateway routines
31199
+ mutagent gateway routines create (--every "weekdays 09:00" | --cron "0 9 * * 1-5") --tz Europe/Berlin --repo owner/name (--instruction "…" | --instruction-file f.md) [--name n]
31200
+ mutagent gateway routines show|pause|resume <id>
31201
+ mutagent gateway routines run-now <id> [--wait]
31202
+ mutagent gateway routines delete <id> --yes
31203
+
31204
+ ${chalk46.yellow("Operate:")}
31205
+ mutagent gateway test-run [--repo owner/name] [--wait] [--key k]
31206
+ mutagent gateway runs [--status s] [--since 24h] [--trigger id|--routine id]
31207
+ mutagent gateway runs show <eventId|runId>
31208
+ mutagent gateway runs logs <runId> [--follow]
31209
+ mutagent gateway emit <kind> [--key k] [--payload @file] [--wait]
31210
+
31211
+ ${chalk46.yellow("Admin:")}
31212
+ mutagent gateway admin preflight [--workspace id]
31213
+ mutagent gateway admin seed-apps
31214
+ mutagent gateway admin limits
31215
+ mutagent gateway admin limits set --concurrent N --daily N
31216
+
31217
+ ${chalk46.yellow("The --json envelope (every command):")}
31218
+ ${chalk46.bold("{ ok, data, next, error: { code, reason, fix, notes } }")}
31219
+ Exit codes: 0 ok · 1 unexpected · 2 needs a person or browser · 3 fixable configuration · 4 refused.
31220
+ 'next' names the next command to run; 'error.fix' names the command(s) that resolve a failure.
31221
+ Every 'fix' entry is a command line, never prose — but a command line may carry a
31222
+ <placeholder> you must replace with a real value first, such as the <workspace-id>
31223
+ in 'mutagent config set workspace <workspace-id>'. Replace every <...> before
31224
+ running; a line with none runs as it stands. The same holds for 'next'.
31225
+ 'error.notes' is prose — what you need to know, never something to execute — and is
31226
+ absent when there is none. A failure no command can resolve has 'fix: []' and
31227
+ explains itself in 'notes'.
31228
+
31229
+ ${chalk46.yellow("AI Agent Directive:")}
31230
+ 1. Run 'mutagent gateway --json' first, every time, and run what 'next' says.
31231
+ 2. Exit 2 means a person or browser is needed. On 'connect', open 'data.signInUrl'
31232
+ first and sign in, then open 'data.url' in that SAME browser — the callback only
31233
+ completes in a browser signed in to Mutagent as you.
31234
+ 3. Never retry a failed command blind — read 'error.fix', replace any <placeholder>
31235
+ in it with the real value, and run that. If 'error.fix' is empty, read
31236
+ 'error.notes': no command resolves it, so act on what it says instead of retrying.
31237
+ 4. Destructive commands (disconnect, repos unlink, triggers/routines delete) need
31238
+ --yes; this CLI never prompts.
31239
+ 5. Routines REQUIRE --tz — a missing one is refused locally before any request.
31240
+ ${!hasCredentials() ? `
31241
+ ` + chalk46.yellow(" Warning: Not authenticated. Run: mutagent login --browser") + `
31242
+ ` : ""}`);
31243
+ return gateway;
31244
+ }
31245
+
31246
+ // src/bin/cli.ts
31247
+ init_config();
31248
+ init_organization_scope();
31249
+
31250
+ // src/lib/brand.ts
31251
+ var BRAND = {
31252
+ name: "MUTAGENT",
31253
+ primary: "#7E47D7",
31254
+ accent: "#45b8cc",
31255
+ gradientFrom: "#45b8cc",
31256
+ gradientTo: "#7E47D7"
31257
+ };
31258
+ var WORDMARK = [
31259
+ " ███╗ ███╗ ██╗ ██╗ ████████╗ █████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗ ",
31260
+ " ████╗ ████║ ██║ ██║ ╚══██╔══╝ ██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ",
31261
+ " ██╔████╔██║ ██║ ██║ ██║ ███████║ ██║ ███╗ █████╗ ██╔██╗ ██║ ██║ ",
31262
+ " ██║╚██╔╝██║ ██║ ██║ ██║ ██╔══██║ ██║ ██║ ██╔══╝ ██║╚██╗██║ ██║ ",
31263
+ " ██║ ╚═╝ ██║ ╚██████╔╝ ██║ ██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚████║ ██║ ",
31264
+ " ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═══╝ ╚═╝ "
31265
+ ];
31266
+ var WORDMARK_MIN_COLUMNS = 84;
31267
+ function hexToRgb(hex) {
31268
+ const n = parseInt(hex.slice(1), 16);
31269
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
31270
+ }
31271
+ var lerp = (a, b, t) => Math.round(a + (b - a) * t);
31272
+ function mix(from, to, t) {
31273
+ return [lerp(from[0], to[0], t), lerp(from[1], to[1], t), lerp(from[2], to[2], t)];
31274
+ }
31275
+ var ansi = (r, g, b) => `\x1B[38;2;${String(r)};${String(g)};${String(b)}m`;
31276
+ var RESET = "\x1B[0m";
31277
+ function gradientLine(line, from, to) {
31278
+ const chars = Array.from(line);
31279
+ const n = chars.length;
31280
+ let out = "";
31281
+ chars.forEach((ch, i) => {
31282
+ if (ch === " ") {
31283
+ out += ch;
31284
+ return;
31285
+ }
31286
+ const [r, g, b] = mix(from, to, n > 1 ? i / (n - 1) : 0);
31287
+ out += ansi(r, g, b) + ch;
31288
+ });
31289
+ return out + RESET;
31290
+ }
31291
+ function hasTruecolor(env = process.env) {
31292
+ return /truecolor|24bit/i.test(env.COLORTERM ?? "");
31293
+ }
31294
+ function detectBannerEnv(stream = process.stdout, env = process.env) {
31295
+ return {
31296
+ isTTY: stream.isTTY === true,
31297
+ columns: stream.columns ?? 80,
31298
+ truecolor: hasTruecolor(env),
31299
+ disabled: env.MUTAGENT_NO_BANNER === "1"
31300
+ };
31301
+ }
31302
+ function renderBanner(env = detectBannerEnv()) {
31303
+ if (env.disabled || !env.isTTY)
31304
+ return "";
31305
+ const from = hexToRgb(BRAND.gradientFrom);
31306
+ const to = hexToRgb(BRAND.gradientTo);
31307
+ const wide = env.columns >= WORDMARK_MIN_COLUMNS;
31308
+ const art = wide ? WORDMARK : [` ${BRAND.name}`];
31309
+ const lines = env.truecolor ? art.map((l) => gradientLine(l, from, to)) : art.map((l) => `${ansi(...from)}${l}${RESET}`);
31310
+ return `
31311
+ ${lines.join(`
31312
+ `)}
31313
+ `;
31314
+ }
31315
+
31316
+ // src/lib/global-version-flag.ts
31317
+ var GLOBAL_VALUE_OPTIONS = new Set(["--api-key", "--endpoint", "--workspace", "--org"]);
31318
+ function findSubcommandIndex(rawArgs) {
31319
+ for (let i = 0;i < rawArgs.length; i++) {
31320
+ const arg = rawArgs[i];
31321
+ if (arg === undefined)
31322
+ continue;
31323
+ if (arg === "--")
31324
+ return i + 1 < rawArgs.length ? i + 1 : -1;
31325
+ if (arg.startsWith("-")) {
31326
+ if (GLOBAL_VALUE_OPTIONS.has(arg))
31327
+ i++;
31328
+ continue;
31329
+ }
31330
+ return i;
31331
+ }
31332
+ return -1;
31333
+ }
31334
+ function isGlobalVersionFlag(rawArgs) {
31335
+ const versionAt = rawArgs.findIndex((a) => a === "-v" || a === "--version");
31336
+ if (versionAt === -1)
31337
+ return false;
31338
+ const subcommandAt = findSubcommandIndex(rawArgs);
31339
+ return subcommandAt === -1 || versionAt < subcommandAt;
31340
+ }
31341
+ var ROOT_BOOLEAN_OPTIONS = new Set(["--json", "--non-interactive"]);
31342
+ function commandPathIndexes(rawArgs, depth) {
31343
+ const indexes = [];
31344
+ let from = 0;
31345
+ while (indexes.length < depth) {
31346
+ const at = findSubcommandIndex(rawArgs.slice(from));
31347
+ if (at === -1)
31348
+ break;
31349
+ indexes.push(from + at);
31350
+ from += at + 1;
31351
+ }
31352
+ return indexes;
31353
+ }
31354
+
31355
+ // src/bin/cli.ts
31356
+ init_errors();
31357
+
31358
+ // src/commands/helix/api-key-guard.ts
31359
+ init_errors();
31360
+ var FORWARDING_VERBS = [
31361
+ { path: ["helix"], label: "mutagent helix" },
31362
+ { path: ["sandbox", "exec"], label: "mutagent sandbox exec" }
31363
+ ];
31364
+ function rootValueOption(arg) {
31365
+ const eq = arg.indexOf("=");
31366
+ const name = eq === -1 ? arg : arg.slice(0, eq);
31367
+ return GLOBAL_VALUE_OPTIONS.has(name) ? name : undefined;
31368
+ }
31369
+ function commanderBindable(tokens) {
31370
+ const bindable = [];
31371
+ for (let i = 0;i < tokens.length; i++) {
31372
+ const token = tokens[i] ?? "";
31373
+ if (token === "--")
31374
+ break;
31375
+ bindable.push(token);
31376
+ if (GLOBAL_VALUE_OPTIONS.has(token) && i + 1 < tokens.length) {
31377
+ bindable.push(tokens[i + 1] ?? "");
31378
+ i++;
31379
+ }
31380
+ }
31381
+ return bindable;
31382
+ }
31383
+ function forwardedRootOptionRefusal(rawArgs) {
31384
+ for (const verb of FORWARDING_VERBS) {
31385
+ const at = commandPathIndexes(rawArgs, verb.path.length);
31386
+ if (at.length !== verb.path.length || at.some((index, k) => rawArgs[index] !== verb.path[k]))
31387
+ continue;
31388
+ const after = rawArgs.slice((at[at.length - 1] ?? -1) + 1);
31389
+ const bindable = commanderBindable(after);
31390
+ const isHelix = verb.path[0] === "helix";
31391
+ const keyScope = isHelix ? after : bindable;
31392
+ if (keyScope.some((arg) => rootValueOption(arg) === "--api-key")) {
31393
+ return isHelix ? helixKeyRefusal() : sandboxExecKeyRefusal();
31394
+ }
31395
+ const other = bindable.map(rootValueOption).find((option) => option !== undefined);
31396
+ if (other !== undefined)
31397
+ return rootValueOptionRefusal(other, verb.label);
31398
+ return;
31399
+ }
31400
+ return;
31401
+ }
31402
+ function keyAlternative() {
29110
31403
  return transportFor("--api-key")?.alternative ?? "";
29111
31404
  }
29112
31405
  function helixKeyRefusal() {
@@ -29125,6 +31418,34 @@ function rootValueOptionRefusal(option, label) {
29125
31418
  return new MutagentError("NOT_SUPPORTED", `${option} after '${label}' and before a -- is refused: the mutagent CLI would read it as ${what}, and the command it forwards would never see it.`, `For the mutagent CLI, put it before the command (mutagent ${option} <value> ${command} …)${env}.
29126
31419
  For the forwarded command, put it after --: ${label} … -- … ${option} <value>.`, 2);
29127
31420
  }
31421
+
31422
+ // src/commands/providers/api-key-scope.ts
31423
+ var API_KEY_FLAG = "--api-key";
31424
+ var API_KEY_FLAG_EQUALS = `${API_KEY_FLAG}=`;
31425
+ var RESCOPED_COMMAND_PATHS = [
31426
+ ["providers", "add"],
31427
+ ["providers", "update"]
31428
+ ];
31429
+ function rescopeProviderApiKeyFlag(rawArgs) {
31430
+ for (const path of RESCOPED_COMMAND_PATHS) {
31431
+ const at = commandPathIndexes(rawArgs, path.length);
31432
+ if (at.length !== path.length)
31433
+ continue;
31434
+ if (at.some((index, i) => rawArgs[index] !== path[i]))
31435
+ continue;
31436
+ const spanStart = (at[at.length - 1] ?? -1) + 1;
31437
+ const before = rawArgs.slice(0, spanStart);
31438
+ const span = rawArgs.slice(spanStart).map((token) => {
31439
+ if (token === API_KEY_FLAG)
31440
+ return "-k";
31441
+ if (token.startsWith(API_KEY_FLAG_EQUALS))
31442
+ return `-k${token.slice(API_KEY_FLAG_EQUALS.length)}`;
31443
+ return token;
31444
+ });
31445
+ return [...before, ...span];
31446
+ }
31447
+ return [...rawArgs];
31448
+ }
29128
31449
  // src/commands/helix/helix-surface.json
29129
31450
  var helix_surface_default = {
29130
31451
  helixVersion: "0.0.33-m0",
@@ -29502,14 +31823,14 @@ if (process.env.CLI_VERSION) {
29502
31823
  cliVersion = process.env.CLI_VERSION;
29503
31824
  } else {
29504
31825
  try {
29505
- const __dirname2 = dirname7(fileURLToPath4(import.meta.url));
29506
- const pkgPath = join21(__dirname2, "..", "..", "package.json");
29507
- const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
31826
+ const __dirname2 = dirname8(fileURLToPath4(import.meta.url));
31827
+ const pkgPath = join22(__dirname2, "..", "..", "package.json");
31828
+ const pkg = JSON.parse(readFileSync18(pkgPath, "utf-8"));
29508
31829
  cliVersion = pkg.version ?? cliVersion;
29509
31830
  } catch {}
29510
31831
  }
29511
31832
  setCliVersion(cliVersion);
29512
- var program = new Command18;
31833
+ var program = new Command23;
29513
31834
  program.name("mutagent").description(`Mutagent CLI - command-line client for the Mutagent platform
29514
31835
 
29515
31836
  Documentation: https://docs.mutagent.io/cli
@@ -29519,50 +31840,50 @@ program.name("mutagent").description(`Mutagent CLI - command-line client for the
29519
31840
  });
29520
31841
  program.addHelpText("beforeAll", () => renderBanner());
29521
31842
  program.addHelpText("after", `
29522
- ${chalk38.yellow("Get started:")}
29523
- mutagent login ${chalk38.dim("Sign in (browser or API key)")}
29524
- mutagent providers mirror ${chalk38.dim("Copy local Helix LLM provider keys into the workspace")}
29525
- mutagent helix -p "<task>" ${chalk38.dim("Run Helix in the cloud")}
29526
- mutagent install helix ${chalk38.dim("Install the standalone Helix binary on this machine")}
29527
-
29528
- ${chalk38.yellow("Managed agents:")}
29529
- mutagent agent deploy <agent.md> ${chalk38.dim("Check, upload and activate an agent.md folder (--env names the slot)")}
29530
- mutagent helix agent @<slug> -p "<task>" ${chalk38.dim("Run a managed agent in the cloud")}
29531
- mutagent agent --help ${chalk38.dim("check, pack, run, deploy, activate, list, inspect, retire")}
29532
-
29533
- ${chalk38.yellow("Workspace:")}
29534
- mutagent workspaces use <name> ${chalk38.dim("Choose the workspace every later command works in")}
29535
- mutagent workspaces current ${chalk38.dim("The key's scope, organization and the verified workspace")}
29536
- mutagent workspaces list ${chalk38.dim("Your workspaces in the key's organization")}
29537
- --workspace <name|id> ${chalk38.dim("One command in another workspace (then MUTAGENT_WORKSPACE_ID, then the selection)")}
29538
- mutagent login --org <slug> ${chalk38.dim("Sign in for another organization")}
29539
- mutagent providers list ${chalk38.dim("LLM providers in the workspace")}
29540
- mutagent helix models ${chalk38.dim("Models Helix can use in the cloud, and the workspace default model")}
29541
- mutagent env ls ${chalk38.dim("Environments: variables and secrets for tools")}
29542
- mutagent usage ${chalk38.dim("Count the workspace's LLM providers and your workspaces")}
29543
-
29544
- ${chalk38.yellow("Automation:")}
29545
- MUTAGENT_API_KEY=mt_... ${chalk38.dim("Sign in without a browser (or --api-key <key>)")}
29546
- --json ${chalk38.dim("Structured output")}
29547
- --non-interactive ${chalk38.dim("No sign-in method or consent prompts (also when CI=true or stdin is not a terminal); a first sign-in in a terminal ends with a menu unless --json")}
29548
- -v, --version ${chalk38.dim("CLI version (only before a subcommand)")}
29549
-
29550
- ${chalk38.yellow("Report a problem:")}
31843
+ ${chalk47.yellow("Get started:")}
31844
+ mutagent login ${chalk47.dim("Sign in (browser or API key)")}
31845
+ mutagent providers mirror ${chalk47.dim("Copy local Helix LLM provider keys into the workspace")}
31846
+ mutagent helix -p "<task>" ${chalk47.dim("Run Helix in the cloud")}
31847
+ mutagent install helix ${chalk47.dim("Install the standalone Helix binary on this machine")}
31848
+
31849
+ ${chalk47.yellow("Managed agents:")}
31850
+ mutagent agent deploy <agent.md> ${chalk47.dim("Check, upload and activate an agent.md folder (--env names the slot)")}
31851
+ mutagent helix agent @<slug> -p "<task>" ${chalk47.dim("Run a managed agent in the cloud")}
31852
+ mutagent agent --help ${chalk47.dim("check, pack, run, deploy, activate, list, inspect, retire")}
31853
+
31854
+ ${chalk47.yellow("Workspace:")}
31855
+ mutagent workspaces use <name> ${chalk47.dim("Choose the workspace every later command works in")}
31856
+ mutagent workspaces current ${chalk47.dim("The key's scope, organization and the verified workspace")}
31857
+ mutagent workspaces list ${chalk47.dim("Your workspaces in the key's organization")}
31858
+ --workspace <name|id> ${chalk47.dim("One command in another workspace (then MUTAGENT_WORKSPACE_ID, then the selection)")}
31859
+ mutagent login --org <slug> ${chalk47.dim("Sign in for another organization")}
31860
+ mutagent providers list ${chalk47.dim("LLM providers in the workspace")}
31861
+ mutagent helix models ${chalk47.dim("Models Helix can use in the cloud, and the workspace default model")}
31862
+ mutagent env ls ${chalk47.dim("Environments: variables and secrets for tools")}
31863
+ mutagent usage ${chalk47.dim("Count the workspace's LLM providers and your workspaces")}
31864
+
31865
+ ${chalk47.yellow("Automation:")}
31866
+ MUTAGENT_API_KEY=mt_... ${chalk47.dim("Sign in without a browser (or --api-key <key>)")}
31867
+ --json ${chalk47.dim("Structured output")}
31868
+ --non-interactive ${chalk47.dim("No sign-in method or consent prompts (also when CI=true or stdin is not a terminal); a first sign-in in a terminal ends with a menu unless --json")}
31869
+ -v, --version ${chalk47.dim("CLI version (only before a subcommand)")}
31870
+
31871
+ ${chalk47.yellow("Report a problem:")}
29551
31872
  mutagent feedback send "<what happened>" --category cli
29552
- ${chalk38.dim("--category accepts: cli | helix | stage:<spec|build|evaluate|diagnose|optimize>")}
31873
+ ${chalk47.dim("--category accepts: cli | helix | stage:<spec|build|evaluate|diagnose|optimize>")}
29553
31874
 
29554
- ${chalk38.dim("For CLI usage guidance for AI agents, see the Skill at")}
29555
- ${chalk38.cyan(".claude/skills/mutagent-cli/SKILL.md")}
31875
+ ${chalk47.dim("For CLI usage guidance for AI agents, see the Skill at")}
31876
+ ${chalk47.cyan(".claude/skills/mutagent-cli/SKILL.md")}
29556
31877
 
29557
- ${chalk38.yellow("Directive System:")}
31878
+ ${chalk47.yellow("Directive System:")}
29558
31879
  A --json response may include:
29559
- ${chalk38.bold("_directive.renderedCard")} A status card: copy it into your chat reply verbatim before running another command
29560
- ${chalk38.bold("_directive.instruction")} What to do next
29561
- ${chalk38.bold("_directive.next")} Suggested follow-up commands
29562
- ${chalk38.bold("_links")} Dashboard and API URLs
29563
- ${chalk38.bold("_compat")} cliVersion, skillVersion, skillMinCliVersion
31880
+ ${chalk47.bold("_directive.renderedCard")} A status card: copy it into your chat reply verbatim before running another command
31881
+ ${chalk47.bold("_directive.instruction")} What to do next
31882
+ ${chalk47.bold("_directive.next")} Suggested follow-up commands
31883
+ ${chalk47.bold("_links")} Dashboard and API URLs
31884
+ ${chalk47.bold("_compat")} cliVersion, skillVersion, skillMinCliVersion
29564
31885
 
29565
- ${chalk38.yellow("AI Agent Rules (MANDATORY for coding agents):")}
31886
+ ${chalk47.yellow("AI Agent Rules (MANDATORY for coding agents):")}
29566
31887
  1. Sign in: with MUTAGENT_API_KEY set, run mutagent login --json. To help a user sign in,
29567
31888
  run mutagent login --browser --json, show the printed URL to the user verbatim, and
29568
31889
  wait: the CLI polls for up to 5 minutes.
@@ -29573,11 +31894,11 @@ ${chalk38.yellow("AI Agent Rules (MANDATORY for coding agents):")}
29573
31894
  5. After mutagent init, check the workspace: mutagent workspaces list --json
29574
31895
  6. Ask the user through your own question tool. With --json only mutagent login can prompt, in a terminal; pass --browser or set MUTAGENT_API_KEY.
29575
31896
  ${!hasCredentials() ? `
29576
- ` + chalk38.yellow(" Warning: Not authenticated. Run: mutagent login") + `
31897
+ ` + chalk47.yellow(" Warning: Not authenticated. Run: mutagent login") + `
29577
31898
  ` : ""}${!hasRcConfig() ? `
29578
- ` + chalk38.green(" Get started: mutagent init") + `
31899
+ ` + chalk47.green(" Get started: mutagent init") + `
29579
31900
  ` : ""}`);
29580
- var rawArgs = process.argv.slice(2);
31901
+ var rawArgs = rescopeProviderApiKeyFlag(process.argv.slice(2));
29581
31902
  if (isGlobalVersionFlag(rawArgs)) {
29582
31903
  if (rawArgs.includes("--json")) {
29583
31904
  console.log(JSON.stringify({
@@ -29629,6 +31950,7 @@ program.addCommand(createHooksCommand());
29629
31950
  program.addCommand(createInstallCommand());
29630
31951
  program.addCommand(createSandboxCommand());
29631
31952
  program.addCommand(createEnvCommand());
31953
+ program.addCommand(createGatewayCommand());
29632
31954
  program.addCommand(createHelixCommand());
29633
31955
  program.addCommand(createFeedbackCommand());
29634
31956
  program.addCommand(createTraceCommand());
@@ -29656,4 +31978,4 @@ if (isAgentInvocation(rawArgs)) {
29656
31978
  program.parse(argvForCommander, { from: "user" });
29657
31979
  }
29658
31980
 
29659
- //# debugId=E046D4F47E19C00364756E2164756E21
31981
+ //# debugId=208B56617BAAF0E964756E2164756E21