@mutagent/cli 0.1.198 → 0.1.200

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -728,7 +728,7 @@ var init_sdk_client = __esm(() => {
728
728
  // src/bin/cli.ts
729
729
  import { Command as Command12 } from "commander";
730
730
  import chalk18 from "chalk";
731
- import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
731
+ import { readFileSync as readFileSync9 } from "fs";
732
732
  import { join as join12, dirname as dirname3 } from "path";
733
733
  import { fileURLToPath as fileURLToPath2 } from "url";
734
734
 
@@ -737,8 +737,8 @@ init_config();
737
737
  init_sdk_client();
738
738
  import { Command } from "commander";
739
739
  import chalk4 from "chalk";
740
- import { existsSync as existsSync3 } from "fs";
741
- import { join as join3 } from "path";
740
+ import { existsSync as existsSync5 } from "fs";
741
+ import { join as join6 } from "path";
742
742
 
743
743
  // src/lib/output.ts
744
744
  import chalk from "chalk";
@@ -937,478 +937,923 @@ init_errors();
937
937
 
938
938
  // src/commands/onboarding.ts
939
939
  import chalk2 from "chalk";
940
- async function runPostOnboarding() {
941
- const inquirer = (await import("inquirer")).default;
942
- console.log("");
943
- console.log(chalk2.bold.cyan(" You are authenticated. What would you like to do next?"));
944
- console.log("");
945
- const { path: selectedPath } = await inquirer.prompt([{
946
- type: "list",
947
- name: "path",
948
- message: "Choose your path:",
949
- choices: [
950
- {
951
- name: `${chalk2.green("A")} Initialize this project — create .mutagentrc.json + install the CLI skill`,
952
- value: "init"
953
- },
954
- {
955
- name: `${chalk2.green("B")} Install a lifecycle tool — helix / diagnostics / evaluator`,
956
- value: "install"
957
- },
958
- {
959
- name: `${chalk2.green("C")} Exit — explore the CLI on your own`,
960
- value: "exit"
961
- }
962
- ]
963
- }]);
964
- console.log("");
965
- if (selectedPath === "init") {
966
- console.log(chalk2.bold(" Project setup:"));
967
- console.log("");
968
- console.log(` 1. ${chalk2.green("mutagent init")}`);
969
- console.log(" Scaffold .mutagentrc.json and select your workspace");
970
- console.log("");
971
- console.log(` 2. ${chalk2.green("mutagent skills install")}`);
972
- console.log(" Install the MutagenT CLI skill for AI coding agents");
973
- console.log("");
974
- console.log(` 3. ${chalk2.green("mutagent providers list")}`);
975
- console.log(" Confirm an LLM provider is configured");
976
- console.log("");
977
- } else if (selectedPath === "install") {
978
- console.log(chalk2.bold(" Install a lifecycle tool:"));
979
- console.log("");
980
- console.log(` ${chalk2.green("mutagent install diagnostics")} ${chalk2.dim("Root-cause analysis from your traces")}`);
981
- console.log(` ${chalk2.green("mutagent install evaluator")} ${chalk2.dim("Build trustworthy eval suites")}`);
982
- console.log(` ${chalk2.green("mutagent install helix")} ${chalk2.dim("The ADL lifecycle conductor")}`);
983
- console.log("");
984
- console.log(chalk2.dim(" All installs are login-gated. Run `mutagent install --help` for flags."));
985
- console.log("");
986
- } else {
987
- console.log(chalk2.dim(" You can run `mutagent --help` anytime to see available commands."));
988
- console.log(chalk2.dim(' Hit a snag? Send feedback: mutagent feedback send "what happened"'));
989
- console.log("");
990
- }
991
- }
992
940
 
993
- // src/lib/auth-flow.ts
941
+ // src/lib/rc-config.ts
994
942
  init_config();
995
- init_sdk_client();
996
- init_errors();
997
- import inquirer from "inquirer";
998
- import chalk3 from "chalk";
999
- import ora from "ora";
943
+ import { join as join4 } from "path";
944
+ import { writeFileSync as writeFileSync2 } from "fs";
1000
945
 
1001
- // src/lib/browser-auth.ts
1002
- import { hostname, platform } from "os";
1003
- function generateCliToken() {
1004
- return crypto.randomUUID();
1005
- }
1006
- async function initBrowserAuth(endpoint, cliToken) {
1007
- const response = await fetch(`${endpoint}/api/auth/cli/init`, {
1008
- method: "POST",
1009
- headers: { "Content-Type": "application/json" },
1010
- body: JSON.stringify({
1011
- cliToken,
1012
- hostname: hostname(),
1013
- platform: platform()
1014
- })
1015
- });
1016
- if (!response.ok) {
1017
- const errorText = await response.text();
1018
- throw new BrowserAuthError("INIT_FAILED", "Failed to initialize browser auth: " + String(response.status) + " " + errorText);
946
+ // src/lib/framework-detection.ts
947
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
948
+ import { execSync } from "child_process";
949
+ import { join as join3 } from "path";
950
+ var FRAMEWORK_DETECTION_MAP = {
951
+ "@mastra/core": {
952
+ name: "mastra",
953
+ displayName: "Mastra",
954
+ npmPackage: "@mastra/core"
955
+ },
956
+ "@langchain/langgraph": {
957
+ name: "langgraph",
958
+ displayName: "LangGraph",
959
+ npmPackage: "@langchain/langgraph",
960
+ mutagentPackage: "@mutagent/langgraph"
961
+ },
962
+ langchain: {
963
+ name: "langchain",
964
+ displayName: "LangChain",
965
+ npmPackage: "langchain",
966
+ mutagentPackage: "@mutagent/langchain"
967
+ },
968
+ "@langchain/core": {
969
+ name: "langchain",
970
+ displayName: "LangChain",
971
+ npmPackage: "@langchain/core",
972
+ mutagentPackage: "@mutagent/langchain"
973
+ },
974
+ "@ai-sdk/core": {
975
+ name: "vercel-ai",
976
+ displayName: "Vercel AI SDK",
977
+ npmPackage: "@ai-sdk/core",
978
+ mutagentPackage: "@mutagent/vercel-ai"
979
+ },
980
+ ai: {
981
+ name: "vercel-ai",
982
+ displayName: "Vercel AI SDK",
983
+ npmPackage: "ai",
984
+ mutagentPackage: "@mutagent/vercel-ai"
985
+ },
986
+ "@google/genai": {
987
+ name: "generic",
988
+ displayName: "Google GenAI",
989
+ npmPackage: "@google/genai"
990
+ },
991
+ openai: {
992
+ name: "openai",
993
+ displayName: "OpenAI SDK",
994
+ npmPackage: "openai",
995
+ mutagentPackage: "@mutagent/openai"
1019
996
  }
1020
- const data = await response.json();
1021
- return data;
1022
- }
1023
- async function pollAuthStatus(endpoint, cliToken) {
1024
- const response = await fetch(`${endpoint}/api/auth/cli/status?token=${encodeURIComponent(cliToken)}`);
1025
- if (!response.ok) {
1026
- const errorText = await response.text();
1027
- throw new BrowserAuthError("POLL_FAILED", "Failed to poll auth status: " + String(response.status) + " " + errorText);
997
+ };
998
+ function detectPackageManager(cwd = process.cwd()) {
999
+ if (existsSync3(join3(cwd, "bun.lockb")) || existsSync3(join3(cwd, "bun.lock"))) {
1000
+ return "bun";
1028
1001
  }
1029
- const data = await response.json();
1030
- return data;
1031
- }
1032
- async function openBrowser(url) {
1033
- if (process.env.MUTAGENT_TEST_MODE === "true") {
1034
- console.log(`AUTH_URL:${url}`);
1035
- return;
1002
+ if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) {
1003
+ return "pnpm";
1004
+ }
1005
+ if (existsSync3(join3(cwd, "yarn.lock"))) {
1006
+ return "yarn";
1007
+ }
1008
+ if (existsSync3(join3(cwd, "package-lock.json"))) {
1009
+ return "npm";
1036
1010
  }
1037
1011
  try {
1038
- const { default: open } = await import("open");
1039
- await open(url);
1012
+ execSync("bun --version", { stdio: "ignore" });
1013
+ return "bun";
1040
1014
  } catch {
1041
- throw new BrowserAuthError("BROWSER_OPEN_FAILED", `Could not open browser automatically. Please visit: ${url}`);
1015
+ return "npm";
1042
1016
  }
1043
1017
  }
1044
- function sleep(ms) {
1045
- return new Promise((resolve) => setTimeout(resolve, ms));
1018
+ function getInstallCommand(pm, packages) {
1019
+ const pkgList = packages.join(" ");
1020
+ const commands = {
1021
+ bun: `bun add ${pkgList}`,
1022
+ npm: `npm install ${pkgList}`,
1023
+ yarn: `yarn add ${pkgList}`,
1024
+ pnpm: `pnpm add ${pkgList}`
1025
+ };
1026
+ return commands[pm];
1046
1027
  }
1047
- async function performBrowserAuth(options, onStatusUpdate) {
1048
- const {
1049
- endpoint,
1050
- timeout = 300000,
1051
- pollInterval = 2000,
1052
- skipBrowserOpen = false
1053
- } = options;
1054
- const cliToken = generateCliToken();
1055
- onStatusUpdate?.("Initializing browser authentication...");
1056
- const initResponse = await initBrowserAuth(endpoint, cliToken);
1057
- const { authUrl } = initResponse;
1058
- if (!skipBrowserOpen) {
1059
- onStatusUpdate?.("Opening browser for authentication...");
1060
- try {
1061
- await openBrowser(authUrl);
1062
- } catch (error) {
1063
- if (error instanceof BrowserAuthError && error.code === "BROWSER_OPEN_FAILED") {
1064
- onStatusUpdate?.(error.message);
1065
- } else {
1066
- throw error;
1067
- }
1068
- }
1028
+ function detectFrameworkFromPackageJson(cwd = process.cwd()) {
1029
+ const pkgPath = join3(cwd, "package.json");
1030
+ if (!existsSync3(pkgPath)) {
1031
+ return null;
1069
1032
  }
1070
- console.log("");
1071
- console.log(" Open this URL to authenticate:");
1072
- console.log("");
1073
- console.log(" " + authUrl);
1074
- console.log("");
1075
- onStatusUpdate?.("Waiting for browser authentication...");
1076
- const startTime = Date.now();
1077
- while (Date.now() - startTime < timeout) {
1078
- await sleep(pollInterval);
1079
- const status = await pollAuthStatus(endpoint, cliToken);
1080
- switch (status.status) {
1081
- case "pending":
1082
- continue;
1083
- case "completed":
1084
- if (!status.apiKey || !status.workspaceId || !status.workspaceName || !status.organizationId || !status.organizationName) {
1085
- throw new BrowserAuthError("INCOMPLETE_RESPONSE", "Server returned incomplete auth response");
1086
- }
1087
- return {
1088
- apiKey: status.apiKey,
1089
- workspaceId: status.workspaceId,
1090
- workspaceName: status.workspaceName,
1091
- organizationId: status.organizationId,
1092
- organizationName: status.organizationName,
1093
- expiresAt: status.expiresAt
1094
- };
1095
- case "expired":
1096
- throw new BrowserAuthError("AUTH_EXPIRED", "Browser authentication expired. Please try again.");
1097
- case "denied":
1098
- throw new BrowserAuthError("AUTH_DENIED", status.error ?? "Browser authentication was denied.");
1099
- case "not_found":
1100
- throw new BrowserAuthError("TOKEN_NOT_FOUND", "Authentication token not found. Please try again.");
1101
- default:
1102
- throw new BrowserAuthError("UNKNOWN_STATUS", "Unknown auth status: " + String(status.status));
1033
+ let pkg;
1034
+ try {
1035
+ pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1036
+ } catch {
1037
+ return null;
1038
+ }
1039
+ const allDeps = {
1040
+ ...pkg.dependencies,
1041
+ ...pkg.devDependencies
1042
+ };
1043
+ for (const [depName, framework] of Object.entries(FRAMEWORK_DETECTION_MAP)) {
1044
+ if (depName in allDeps) {
1045
+ return framework;
1103
1046
  }
1104
1047
  }
1105
- throw new BrowserAuthError("AUTH_TIMEOUT", "Browser authentication timed out after 5 minutes. Please try again.");
1048
+ return null;
1049
+ }
1050
+ function hasRcConfig(cwd = process.cwd()) {
1051
+ return existsSync3(join3(cwd, ".mutagentrc.json"));
1106
1052
  }
1107
1053
 
1108
- class BrowserAuthError extends Error {
1109
- code;
1110
- constructor(code, message) {
1111
- super(message);
1112
- this.name = "BrowserAuthError";
1113
- this.code = code;
1114
- }
1115
- getSuggestion() {
1116
- switch (this.code) {
1117
- case "INIT_FAILED":
1118
- return "Check your endpoint configuration and network connection.";
1119
- case "POLL_FAILED":
1120
- return "Check your network connection and try again.";
1121
- case "BROWSER_OPEN_FAILED":
1122
- return "Copy the URL above and open it manually in your browser.";
1123
- case "AUTH_EXPIRED":
1124
- case "AUTH_TIMEOUT":
1125
- return 'Run "mutagent auth login" again to restart authentication.';
1126
- case "AUTH_DENIED":
1127
- return "Ensure you have access to the workspace and try again.";
1128
- case "TOKEN_NOT_FOUND":
1129
- case "INCOMPLETE_RESPONSE":
1130
- case "UNKNOWN_STATUS":
1131
- return "Please try again. If the issue persists, contact support.";
1132
- default:
1133
- return "Please try again.";
1134
- }
1054
+ // src/lib/rc-config.ts
1055
+ function writeRcConfig(config, cwd = process.cwd()) {
1056
+ const rcPath = join4(cwd, ".mutagentrc.json");
1057
+ writeFileSync2(rcPath, JSON.stringify(config, null, 2) + `
1058
+ `);
1059
+ }
1060
+ function scaffoldRcConfig(cwd = process.cwd(), deps = {}) {
1061
+ const load = deps.loadConfig ?? loadConfig;
1062
+ const hasRc = deps.hasRcConfig ?? hasRcConfig;
1063
+ const write = deps.writeRcConfig ?? writeRcConfig;
1064
+ const cfg = load();
1065
+ const rcConfig = {
1066
+ endpoint: cfg.endpoint ?? "https://api.mutagent.io",
1067
+ ...cfg.defaultWorkspace ? { defaultWorkspace: cfg.defaultWorkspace } : {},
1068
+ ...cfg.defaultOrganization ? { defaultOrganization: cfg.defaultOrganization } : {}
1069
+ };
1070
+ if (hasRc(cwd)) {
1071
+ return { created: false, alreadyPresent: true, config: rcConfig };
1135
1072
  }
1073
+ write(rcConfig, cwd);
1074
+ return { created: true, alreadyPresent: false, config: rcConfig };
1136
1075
  }
1137
1076
 
1138
- // src/lib/auth-flow.ts
1139
- async function performLoginAction(opts) {
1140
- const { isJson, output } = opts;
1141
- const wasFirstLogin = !hasCredentials();
1142
- const envApiKey = process.env.MUTAGENT_API_KEY;
1143
- const endpoint = process.env.MUTAGENT_ENDPOINT ?? opts.endpoint;
1144
- if (envApiKey) {
1145
- return loginWithExistingKey(envApiKey, endpoint, output, wasFirstLogin);
1146
- }
1147
- const isNonInteractive = opts.nonInteractive === true || process.env.MUTAGENT_NON_INTERACTIVE === "true" || process.env.CI === "true" || !process.stdin.isTTY;
1148
- if (isJson && !opts.browser && isNonInteractive) {
1149
- throw new MutagentError("INTERACTIVE_REQUIRED", "No API key provided. Set MUTAGENT_API_KEY env var or add --browser for browser auth.", "Run: export MUTAGENT_API_KEY=<key> or mutagent login --browser --non-interactive");
1077
+ // src/lib/installer.ts
1078
+ init_errors();
1079
+ init_config();
1080
+ import { spawn as spawn2 } from "child_process";
1081
+
1082
+ // src/lib/installer-helix.ts
1083
+ init_errors();
1084
+ init_config();
1085
+ import { spawn } from "child_process";
1086
+ import { createHash } from "crypto";
1087
+ import { homedir as homedir2 } from "os";
1088
+ import { join as join5 } from "path";
1089
+ import {
1090
+ existsSync as existsSync4,
1091
+ mkdirSync as mkdirSync2,
1092
+ readFileSync as readFileSync4,
1093
+ renameSync,
1094
+ rmSync,
1095
+ writeFileSync as writeFileSync3
1096
+ } from "fs";
1097
+ async function installHelix(opts, deps = {}) {
1098
+ const auth = (deps.resolveAuth ?? defaultResolveAuth)();
1099
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
1100
+ const descriptor = await fetchDescriptor(fetchImpl, auth, opts.version);
1101
+ const baseDir = deps.homeDir ?? join5(homedir2(), ".mutagent", "helix");
1102
+ const versionDir = join5(baseDir, descriptor.version);
1103
+ mkdirSync2(versionDir, { recursive: true });
1104
+ const tgzPath = join5(versionDir, `helix-plugin-${descriptor.version}.tgz`);
1105
+ const tmpPath = `${tgzPath}.${String(process.pid)}.${String(Date.now())}.part`;
1106
+ const download = deps.download ?? defaultDownload;
1107
+ await download(descriptor.url, tmpPath);
1108
+ const sha256 = deps.sha256 ?? defaultSha256;
1109
+ const actual = (await sha256(tmpPath)).toLowerCase();
1110
+ const expected = descriptor.sha256.toLowerCase();
1111
+ if (actual !== expected) {
1112
+ safeRm(tmpPath);
1113
+ throw new MutagentError("INTEGRITY_ERROR", `Downloaded helix plugin failed sha256 verification (expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…).`, "The download may be corrupt or tampered with. Retry: mutagent install helix");
1150
1114
  }
1151
- if (!isNonInteractive && wasFirstLogin) {
1152
- console.log(`
1153
- ` + chalk3.bold.cyan(" Welcome to MutagenT CLI!") + `
1154
- `);
1155
- console.log(` No credentials found. Please authenticate to continue.
1156
- `);
1115
+ renameSync(tmpPath, tgzPath);
1116
+ const extract = deps.extract ?? defaultExtract;
1117
+ await extract(tgzPath, versionDir);
1118
+ const locateInitBin = deps.locateInitBin ?? defaultLocateInitBin;
1119
+ const binPath = await locateInitBin(versionDir);
1120
+ const initArgs = ["init", "--harness", opts.harness];
1121
+ if (opts.global)
1122
+ initArgs.push("--global");
1123
+ const runInit = deps.runInit ?? defaultRunInit;
1124
+ const code = await runInit(binPath, initArgs, process.cwd());
1125
+ if (code !== 0) {
1126
+ throw new MutagentError("INSTALL_FAILED", `helix plugin init exited with code ${String(code)}.`, "Re-run: mutagent install helix — or run the plugin init manually and report the error.");
1157
1127
  }
1158
- let useBrowserAuth = opts.browser === true;
1159
- if (!useBrowserAuth && isNonInteractive) {
1160
- output.info("Non-interactive environment detected. Using browser authentication.");
1161
- useBrowserAuth = true;
1128
+ await postTelemetry(fetchImpl, auth, {
1129
+ pkg: "helix",
1130
+ version: descriptor.version,
1131
+ harness: opts.harness,
1132
+ cliVersion: deps.cliVersion ?? readCliVersion()
1133
+ });
1134
+ return { version: descriptor.version };
1135
+ }
1136
+ async function fetchDescriptor(fetchImpl, auth, version) {
1137
+ if (!auth.apiKey) {
1138
+ throw new MutagentError("AUTH_REQUIRED", "Authentication required to install helix.", "Run: mutagent login");
1162
1139
  }
1163
- if (!useBrowserAuth) {
1164
- const methodAnswer = await inquirer.prompt([
1165
- {
1166
- type: "list",
1167
- name: "method",
1168
- message: "How would you like to authenticate?",
1169
- choices: [
1170
- { name: "Login (opens browser)", value: "browser" },
1171
- { name: "API Key (paste existing key)", value: "apiKey" }
1172
- ]
1173
- }
1174
- ]);
1175
- useBrowserAuth = methodAnswer.method === "browser";
1140
+ const url = `${auth.apiBase}/api/helix/plugin/download?version=${encodeURIComponent(version)}`;
1141
+ let res;
1142
+ try {
1143
+ res = await fetchImpl(url, {
1144
+ method: "GET",
1145
+ headers: { "x-api-key": auth.apiKey, ...auth.headers }
1146
+ });
1147
+ } catch {
1148
+ throw new MutagentError("SERVER_UNAVAILABLE", "Could not reach the MutagenT plugin broker.", "Check your network connection or verify the endpoint with: mutagent config show");
1176
1149
  }
1177
- if (useBrowserAuth) {
1178
- return loginWithBrowser(endpoint, output, wasFirstLogin);
1150
+ if (res.status === 401) {
1151
+ throw new MutagentError("AUTH_REQUIRED", "The plugin broker rejected your credentials.", "Re-authenticate: mutagent login");
1179
1152
  }
1180
- return loginWithPastedKey(endpoint, output, wasFirstLogin);
1153
+ if (!res.ok) {
1154
+ const detail = await readErrorMessage(res);
1155
+ throw new MutagentError("INSTALL_FAILED", `Plugin broker returned ${String(res.status)}.${detail ? ` ${detail}` : ""}`, "Verify the requested version exists, then retry: mutagent install helix");
1156
+ }
1157
+ const raw = await res.json();
1158
+ if (!isBrokerResponse(raw)) {
1159
+ throw new MutagentError("INSTALL_FAILED", "Plugin broker returned a malformed response (missing version/sha256/url).", "Retry: mutagent install helix — if it persists, report it.");
1160
+ }
1161
+ return raw;
1181
1162
  }
1182
- function buildLoginJsonResponse(result) {
1183
- return {
1184
- success: true,
1185
- authenticated: result.authenticated,
1186
- endpoint: result.endpoint,
1187
- workspace: result.workspace,
1188
- organization: result.organization,
1189
- _directive: {
1190
- instruction: "Verify workspace. Run: mutagent workspaces list --json",
1191
- next: ["mutagent workspaces list --json", "mutagent usage --json"]
1192
- }
1193
- };
1163
+ async function postTelemetry(fetchImpl, auth, event) {
1164
+ if (!auth.apiKey)
1165
+ return;
1166
+ try {
1167
+ await fetchImpl(`${auth.apiBase}/api/helix/installs`, {
1168
+ method: "POST",
1169
+ headers: {
1170
+ "Content-Type": "application/json",
1171
+ "x-api-key": auth.apiKey,
1172
+ ...auth.headers
1173
+ },
1174
+ body: JSON.stringify(event)
1175
+ });
1176
+ } catch {}
1194
1177
  }
1195
- async function loginWithExistingKey(apiKey, endpoint, output, wasFirstLogin) {
1196
- output.info("Validating API key...");
1197
- const isValid = await validateApiKey(apiKey, endpoint);
1198
- if (!isValid) {
1199
- throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
1178
+ function defaultResolveAuth() {
1179
+ const apiKey = getApiKey();
1180
+ const config = loadConfig();
1181
+ const apiBase = config.endpoint ?? "https://api.mutagent.io";
1182
+ const headers = {};
1183
+ if (config.defaultWorkspace)
1184
+ headers["x-workspace-id"] = config.defaultWorkspace;
1185
+ if (config.defaultOrganization)
1186
+ headers["x-organization-id"] = config.defaultOrganization;
1187
+ return { apiBase, apiKey, headers };
1188
+ }
1189
+ async function defaultDownload(url, destPath) {
1190
+ let res;
1191
+ try {
1192
+ res = await globalThis.fetch(url);
1193
+ } catch {
1194
+ throw new MutagentError("SERVER_UNAVAILABLE", "Failed to download the helix plugin from storage.", "Check your network connection and retry: mutagent install helix");
1200
1195
  }
1201
- const orgs = await fetchOrganizations(apiKey, endpoint);
1202
- let orgId;
1203
- let orgName;
1204
- let wsId;
1205
- let wsName;
1206
- if (orgs.length >= 1 && orgs[0]) {
1207
- orgId = orgs[0].id;
1208
- orgName = orgs[0].name;
1209
- const workspaces = await fetchWorkspaces(apiKey, endpoint, orgId);
1210
- const defaultWs = workspaces.find((w) => w.isDefault) ?? workspaces[0];
1211
- if (defaultWs) {
1212
- wsId = defaultWs.id;
1213
- wsName = defaultWs.name;
1214
- }
1196
+ if (!res.ok) {
1197
+ throw new MutagentError("INSTALL_FAILED", `Plugin download failed with status ${String(res.status)}.`, "The signed URL may have expired — retry: mutagent install helix");
1215
1198
  }
1216
- saveFullCredentials({
1217
- apiKey,
1218
- endpoint,
1219
- workspaceId: wsId,
1220
- organizationId: orgId
1221
- });
1222
- return {
1223
- authenticated: true,
1224
- apiKey,
1225
- endpoint,
1226
- workspace: wsId ? { id: wsId, name: wsName } : null,
1227
- organization: orgId ? { id: orgId, name: orgName } : null,
1228
- wasFirstLogin
1229
- };
1199
+ const bytes = Buffer.from(await res.arrayBuffer());
1200
+ writeFileSync3(destPath, bytes);
1230
1201
  }
1231
- async function loginWithBrowser(endpoint, output, wasFirstLogin) {
1232
- const spinner = ora({ text: "Opening browser for authentication...", spinner: "dots" });
1233
- try {
1234
- spinner.start();
1235
- const result = await performBrowserAuth({ endpoint, timeout: 300000, pollInterval: 2000 }, (message) => {
1236
- spinner.text = message;
1202
+ async function defaultSha256(filePath) {
1203
+ return Promise.resolve(createHash("sha256").update(readFileSync4(filePath)).digest("hex"));
1204
+ }
1205
+ function defaultExtract(tgzPath, destDir) {
1206
+ return new Promise((resolve, reject) => {
1207
+ const child = spawn("tar", ["-xzf", tgzPath, "-C", destDir], {
1208
+ stdio: ["ignore", "ignore", "pipe"]
1237
1209
  });
1238
- spinner.succeed("Authenticated successfully!");
1239
- saveFullCredentials({
1240
- apiKey: result.apiKey,
1241
- endpoint,
1242
- workspaceId: result.workspaceId,
1243
- organizationId: result.organizationId,
1244
- expiresAt: result.expiresAt
1210
+ let stderr = "";
1211
+ child.stderr.on("data", (chunk) => {
1212
+ stderr += chunk.toString("utf-8");
1245
1213
  });
1246
- if (result.workspaceName)
1247
- output.info(`Workspace: ${result.workspaceName}`);
1248
- if (result.organizationName)
1249
- output.info(`Organization: ${result.organizationName}`);
1250
- return {
1251
- authenticated: true,
1252
- apiKey: result.apiKey,
1253
- endpoint,
1254
- workspace: result.workspaceId ? { id: result.workspaceId, name: result.workspaceName } : null,
1255
- organization: result.organizationId ? { id: result.organizationId, name: result.organizationName } : null,
1256
- wasFirstLogin
1257
- };
1258
- } catch (error) {
1259
- spinner.fail("Authentication failed");
1260
- if (error instanceof BrowserAuthError) {
1261
- throw new MutagentError(error.code, error.message, error.getSuggestion());
1262
- }
1263
- throw error;
1264
- }
1214
+ child.on("error", (err) => {
1215
+ reject(new MutagentError("INSTALL_FAILED", `Failed to extract the helix plugin: ${err.message}`, 'Ensure "tar" is installed and available on your PATH.'));
1216
+ });
1217
+ child.on("close", (code) => {
1218
+ if (code === 0) {
1219
+ resolve();
1220
+ } else {
1221
+ reject(new MutagentError("INSTALL_FAILED", `Extracting the helix plugin failed (tar exit ${String(code ?? 1)}).${stderr ? ` ${stderr.trim().slice(0, 200)}` : ""}`, "The downloaded archive may be corrupt — retry: mutagent install helix"));
1222
+ }
1223
+ });
1224
+ });
1265
1225
  }
1266
- async function loginWithPastedKey(initialEndpoint, output, wasFirstLogin) {
1267
- const answers = await inquirer.prompt([
1268
- {
1269
- type: "input",
1270
- name: "endpoint",
1271
- message: "MutagenT endpoint:",
1272
- default: initialEndpoint
1273
- },
1274
- {
1275
- type: "password",
1276
- name: "apiKey",
1277
- message: "API Key:",
1278
- mask: "*",
1279
- validate: (input) => input.length > 0 || "API key is required"
1280
- }
1281
- ]);
1282
- const apiKey = answers.apiKey;
1283
- const endpoint = answers.endpoint;
1284
- output.info("Validating API key...");
1285
- const isValid = await validateApiKey(apiKey, endpoint);
1286
- if (!isValid) {
1287
- throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
1226
+ function defaultLocateInitBin(extractedDir) {
1227
+ const pkgDir = existsSync4(join5(extractedDir, "package", "package.json")) ? join5(extractedDir, "package") : extractedDir;
1228
+ const pkgJsonPath = join5(pkgDir, "package.json");
1229
+ if (!existsSync4(pkgJsonPath)) {
1230
+ return Promise.reject(new MutagentError("INSTALL_FAILED", "Could not find package.json in the extracted helix plugin.", "The archive layout is unexpected — retry or report: mutagent install helix"));
1288
1231
  }
1289
- let selectedOrgId;
1290
- let selectedOrgName;
1291
- let selectedWsId;
1292
- let selectedWsName;
1293
- const orgs = await fetchOrganizations(apiKey, endpoint);
1294
- if (orgs.length === 1 && orgs[0]) {
1295
- selectedOrgId = orgs[0].id;
1296
- selectedOrgName = orgs[0].name;
1297
- } else if (orgs.length > 1) {
1298
- const orgAnswer = await inquirer.prompt([
1299
- {
1300
- type: "list",
1301
- name: "orgId",
1302
- message: "Select organization:",
1303
- choices: orgs.map((o) => ({ name: o.name, value: o.id }))
1304
- }
1305
- ]);
1306
- selectedOrgId = orgAnswer.orgId;
1307
- selectedOrgName = orgs.find((o) => o.id === selectedOrgId)?.name;
1232
+ const raw = JSON.parse(readFileSync4(pkgJsonPath, "utf-8"));
1233
+ const binRel = resolveBinField(raw);
1234
+ if (!binRel) {
1235
+ return Promise.reject(new MutagentError("INSTALL_FAILED", "The helix plugin package.json declares no runnable bin.", "The plugin package is malformed — report: mutagent install helix"));
1308
1236
  }
1309
- if (selectedOrgId) {
1310
- const workspaces = await fetchWorkspaces(apiKey, endpoint, selectedOrgId);
1311
- const defaultWs = workspaces.find((w) => w.isDefault);
1312
- if (workspaces.length === 1 && workspaces[0]) {
1313
- selectedWsId = workspaces[0].id;
1314
- selectedWsName = workspaces[0].name;
1315
- } else if (defaultWs) {
1316
- selectedWsId = defaultWs.id;
1317
- selectedWsName = defaultWs.name;
1318
- } else if (workspaces.length > 1) {
1319
- const wsAnswer = await inquirer.prompt([
1320
- {
1321
- type: "list",
1322
- name: "wsId",
1323
- message: "Select workspace:",
1324
- choices: workspaces.map((w) => ({
1325
- name: w.name + (w.isDefault ? " (default)" : ""),
1326
- value: w.id
1327
- }))
1328
- }
1329
- ]);
1330
- selectedWsId = wsAnswer.wsId;
1331
- selectedWsName = workspaces.find((w) => w.id === selectedWsId)?.name;
1237
+ const binPath = join5(pkgDir, binRel);
1238
+ if (!existsSync4(binPath)) {
1239
+ return Promise.reject(new MutagentError("INSTALL_FAILED", `The helix plugin bin was not found at ${binRel}.`, "The plugin package is incomplete — report: mutagent install helix"));
1240
+ }
1241
+ return Promise.resolve(binPath);
1242
+ }
1243
+ function defaultRunInit(binPath, args, cwd) {
1244
+ return new Promise((resolve, reject) => {
1245
+ const child = spawn("node", [binPath, ...args], { cwd, stdio: "inherit" });
1246
+ child.on("error", (err) => {
1247
+ reject(new MutagentError("INSTALL_FAILED", `Failed to run the helix plugin init: ${err.message}`, 'Ensure "node" is installed and available on your PATH.'));
1248
+ });
1249
+ child.on("close", (code) => {
1250
+ resolve(code ?? 1);
1251
+ });
1252
+ });
1253
+ }
1254
+ function resolveBinField(pkg) {
1255
+ if (!pkg || typeof pkg !== "object")
1256
+ return;
1257
+ const bin = pkg.bin;
1258
+ if (typeof bin === "string")
1259
+ return bin;
1260
+ if (bin && typeof bin === "object") {
1261
+ const entries = bin;
1262
+ const preferred = entries["mutagent-helix"];
1263
+ if (typeof preferred === "string")
1264
+ return preferred;
1265
+ for (const value of Object.values(entries)) {
1266
+ if (typeof value === "string")
1267
+ return value;
1332
1268
  }
1333
1269
  }
1334
- saveFullCredentials({
1335
- apiKey,
1336
- endpoint,
1337
- workspaceId: selectedWsId,
1338
- organizationId: selectedOrgId
1270
+ return;
1271
+ }
1272
+ function isBrokerResponse(value) {
1273
+ if (!value || typeof value !== "object")
1274
+ return false;
1275
+ const v = value;
1276
+ return typeof v.version === "string" && typeof v.sha256 === "string" && typeof v.url === "string";
1277
+ }
1278
+ async function readErrorMessage(res) {
1279
+ try {
1280
+ const body = await res.json();
1281
+ if (body && typeof body === "object") {
1282
+ const b = body;
1283
+ if (typeof b.message === "string")
1284
+ return b.message;
1285
+ if (typeof b.error === "string")
1286
+ return b.error;
1287
+ }
1288
+ } catch {}
1289
+ return;
1290
+ }
1291
+ function readCliVersion() {
1292
+ if (process.env.CLI_VERSION)
1293
+ return process.env.CLI_VERSION;
1294
+ return "unknown";
1295
+ }
1296
+ function safeRm(path) {
1297
+ try {
1298
+ rmSync(path, { force: true });
1299
+ } catch {}
1300
+ }
1301
+
1302
+ // src/lib/installer.ts
1303
+ var VALID_PACKAGES = ["helix", "diagnostics", "evaluator"];
1304
+ var VALID_HARNESSES = ["claude-code", "codex", "omp"];
1305
+ var VERSION_MATRIX = {
1306
+ helix: "latest",
1307
+ diagnostics: "latest",
1308
+ evaluator: "latest"
1309
+ };
1310
+ var NPM_PACKAGES = {
1311
+ diagnostics: "@mutagent/diagnostics",
1312
+ evaluator: "@mutagent/evaluator"
1313
+ };
1314
+ var defaultRunner = (cmd, args) => new Promise((resolve, reject) => {
1315
+ const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1316
+ let stdout = "";
1317
+ let stderr = "";
1318
+ child.stdout.on("data", (chunk) => {
1319
+ stdout += chunk.toString("utf-8");
1320
+ });
1321
+ child.stderr.on("data", (chunk) => {
1322
+ stderr += chunk.toString("utf-8");
1339
1323
  });
1324
+ child.on("error", (err) => {
1325
+ reject(new MutagentError("INSTALL_FAILED", `Failed to run ${cmd}: ${err.message}`, `Ensure "${cmd}" is installed and available on your PATH.`));
1326
+ });
1327
+ child.on("close", (code) => {
1328
+ resolve({ code: code ?? 1, stdout, stderr });
1329
+ });
1330
+ });
1331
+ function isValidPackage(pkg) {
1332
+ return VALID_PACKAGES.includes(pkg);
1333
+ }
1334
+ function isValidHarness(harness) {
1335
+ return VALID_HARNESSES.includes(harness);
1336
+ }
1337
+ async function installPackage(pkg, opts, deps = {}) {
1338
+ const isAuthed = deps.isAuthed ?? hasCredentials;
1339
+ if (!isAuthed()) {
1340
+ throw new MutagentError("AUTH_REQUIRED", `Authentication required to install ${pkg}.`, "Run: mutagent login");
1341
+ }
1342
+ if (!isValidPackage(pkg)) {
1343
+ throw new MutagentError("INVALID_ARGUMENTS", `Unknown package "${pkg}". Valid: ${VALID_PACKAGES.join(", ")}`, "Run: mutagent install --help");
1344
+ }
1345
+ if (!isValidHarness(opts.harness)) {
1346
+ throw new MutagentError("INVALID_ARGUMENTS", `Unknown harness "${opts.harness}". Valid: ${VALID_HARNESSES.join(", ")}`, "Run: mutagent install --help");
1347
+ }
1348
+ const version = opts.version ?? VERSION_MATRIX[pkg];
1349
+ if (pkg === "helix") {
1350
+ const { version: resolved } = await installHelix({ harness: opts.harness, global: opts.global, version }, deps.helix ?? {});
1351
+ return {
1352
+ package: pkg,
1353
+ version: resolved,
1354
+ harness: opts.harness,
1355
+ global: opts.global
1356
+ };
1357
+ }
1358
+ const npmPackage = NPM_PACKAGES[pkg];
1359
+ const args = ["install", "-g", `${npmPackage}@${version}`];
1360
+ const runner = deps.runner ?? defaultRunner;
1361
+ const result = await runner("npm", args);
1362
+ if (result.code !== 0) {
1363
+ const detail = result.stderr.trim();
1364
+ throw new MutagentError("INSTALL_FAILED", `npm failed to install ${npmPackage}@${version} (exit ${String(result.code)}).${detail ? ` ${detail.slice(0, 200)}` : ""}`, "Verify the package and version exist and that you have permission for a global npm install.");
1365
+ }
1340
1366
  return {
1341
- authenticated: true,
1342
- apiKey,
1343
- endpoint,
1344
- workspace: selectedWsId ? { id: selectedWsId, name: selectedWsName } : null,
1345
- organization: selectedOrgId ? { id: selectedOrgId, name: selectedOrgName } : null,
1346
- wasFirstLogin
1367
+ package: pkg,
1368
+ version,
1369
+ harness: opts.harness,
1370
+ global: opts.global,
1371
+ command: `npm ${args.join(" ")}`
1347
1372
  };
1348
1373
  }
1349
1374
 
1350
- // src/commands/auth.ts
1351
- function createAuthCommand() {
1352
- const auth = new Command("auth").description("Authenticate with MutagenT platform").addHelpText("after", `
1353
- Examples:
1354
- ${chalk4.dim("$")} mutagent auth login
1355
- ${chalk4.dim("$")} mutagent auth login --browser
1356
- ${chalk4.dim("$")} mutagent auth status
1357
- ${chalk4.dim("$")} mutagent auth logout
1358
- `);
1359
- auth.command("login").description("Authenticate and store API key").option("--browser", "Force browser-based authentication").option("--non-interactive", "Disable interactive prompts (auto-selects browser auth)").option("--endpoint <url>", "API endpoint", "https://api.mutagent.io").addHelpText("after", `
1360
- Examples:
1361
- ${chalk4.dim("$")} mutagent auth login ${chalk4.dim("# Interactive (choose method)")}
1362
- ${chalk4.dim("$")} mutagent auth login --browser ${chalk4.dim("# Browser OAuth flow")}
1363
- ${chalk4.dim("$")} mutagent auth login --non-interactive ${chalk4.dim("# Auto browser flow (AI agents)")}
1364
-
1365
- ${chalk4.dim("Note: this command is an alias for `mutagent login`.")}
1366
- ${chalk4.dim("See `mutagent login --help` for full documentation.")}
1367
- `).action(async (options) => {
1368
- const isJson = getJsonFlag(auth);
1369
- const output = new OutputFormatter(isJson ? "json" : "table");
1375
+ // src/commands/onboarding.ts
1376
+ init_errors();
1377
+ var ONBOARDING_CHOICES = [
1378
+ { name: "1) Install Helix — the Agent Development Life Cycle Orchestrator (ADLC)", value: "helix" },
1379
+ { name: "2) Exit explore the CLI on your own", value: "exit" }
1380
+ ];
1381
+ async function defaultSelectAction(choices) {
1382
+ const inquirer = (await import("inquirer")).default;
1383
+ const { action } = await inquirer.prompt([{
1384
+ type: "list",
1385
+ name: "action",
1386
+ message: "What would you like to do next?",
1387
+ choices,
1388
+ default: "helix"
1389
+ }]);
1390
+ return action;
1391
+ }
1392
+ async function runPostOnboarding(deps = {}) {
1393
+ const cwd = deps.cwd ?? process.cwd();
1394
+ const scaffold = deps.scaffoldRcConfig ?? scaffoldRcConfig;
1395
+ const select = deps.selectAction ?? defaultSelectAction;
1396
+ const install = deps.installPackage ?? installPackage;
1397
+ console.log("");
1398
+ console.log(chalk2.bold.cyan(" You are authenticated."));
1399
+ console.log("");
1400
+ const rc = scaffold(cwd);
1401
+ if (rc.alreadyPresent) {
1402
+ console.log(` ${chalk2.green("✓")} .mutagentrc.json already present`);
1403
+ } else {
1404
+ console.log(` ${chalk2.green("✓")} Created .mutagentrc.json`);
1405
+ }
1406
+ console.log("");
1407
+ const action = await select(ONBOARDING_CHOICES);
1408
+ console.log("");
1409
+ if (action === "helix") {
1410
+ console.log(chalk2.bold(" Installing Helix…"));
1411
+ console.log(chalk2.dim(" Downloading + verifying the plugin, then writing .claude/ + .codex/"));
1412
+ console.log("");
1370
1413
  try {
1371
- const result = await performLoginAction({
1372
- endpoint: options.endpoint,
1373
- browser: options.browser,
1374
- nonInteractive: options.nonInteractive,
1375
- isJson,
1376
- output
1377
- });
1378
- if (isJson) {
1379
- output.output(buildLoginJsonResponse(result));
1380
- } else {
1381
- output.success("Authenticated successfully");
1382
- if (result.organization?.name)
1383
- output.info(`Organization: ${result.organization.name}`);
1384
- if (result.workspace?.name)
1385
- output.info(`Workspace: ${result.workspace.name}`);
1386
- output.info(`Endpoint: ${result.endpoint}`);
1387
- output.info("Next: mutagent workspaces list --json");
1388
- }
1389
- if (result.wasFirstLogin && process.stdin.isTTY && !isJson) {
1390
- await runPostOnboarding();
1391
- }
1414
+ await install("helix", { harness: "claude-code", global: true });
1415
+ console.log("");
1416
+ console.log(` ${chalk2.green("✓")} Helix ready — run ${chalk2.cyan("*help")} inside your agent`);
1417
+ console.log("");
1392
1418
  } catch (error) {
1419
+ console.log("");
1393
1420
  if (error instanceof MutagentError) {
1394
- output.error(error.message);
1395
- process.exit(error.exitCode);
1421
+ console.error(` ${chalk2.red("✗")} Helix install failed: ${error.message}`);
1422
+ if (error.suggestion)
1423
+ console.error(` ${chalk2.yellow("→")} ${error.suggestion}`);
1424
+ } else {
1425
+ const message = error instanceof Error ? error.message : String(error);
1426
+ console.error(` ${chalk2.red("✗")} Helix install failed: ${message}`);
1396
1427
  }
1397
- throw error;
1428
+ console.error("");
1429
+ process.exitCode = 1;
1398
1430
  }
1399
- });
1400
- auth.command("status").description("Check authentication status").addHelpText("after", `
1401
- Examples:
1402
- ${chalk4.dim("$")} mutagent auth status
1403
- ${chalk4.dim("$")} mutagent auth status --json
1404
- `).action(async () => {
1405
- const isJson = getJsonFlag(auth);
1406
- const output = new OutputFormatter(isJson ? "json" : "table");
1407
- const apiKey = getApiKey();
1408
- const config = loadConfig();
1409
- const endpoint = config.endpoint ?? "https://api.mutagent.io";
1410
- if (!apiKey) {
1411
- if (isJson) {
1431
+ } else {
1432
+ console.log(chalk2.dim(` Run ${chalk2.cyan("mutagent --help")} to explore the CLI.`));
1433
+ console.log(chalk2.dim(' Hit a snag? Send feedback: mutagent feedback send "what happened"'));
1434
+ console.log("");
1435
+ }
1436
+ }
1437
+
1438
+ // src/lib/auth-flow.ts
1439
+ init_config();
1440
+ init_sdk_client();
1441
+ init_errors();
1442
+ import inquirer from "inquirer";
1443
+ import chalk3 from "chalk";
1444
+ import ora from "ora";
1445
+
1446
+ // src/lib/browser-auth.ts
1447
+ import { hostname, platform } from "os";
1448
+ function generateCliToken() {
1449
+ return crypto.randomUUID();
1450
+ }
1451
+ async function initBrowserAuth(endpoint, cliToken) {
1452
+ const response = await fetch(`${endpoint}/api/auth/cli/init`, {
1453
+ method: "POST",
1454
+ headers: { "Content-Type": "application/json" },
1455
+ body: JSON.stringify({
1456
+ cliToken,
1457
+ hostname: hostname(),
1458
+ platform: platform()
1459
+ })
1460
+ });
1461
+ if (!response.ok) {
1462
+ const errorText = await response.text();
1463
+ throw new BrowserAuthError("INIT_FAILED", "Failed to initialize browser auth: " + String(response.status) + " " + errorText);
1464
+ }
1465
+ const data = await response.json();
1466
+ return data;
1467
+ }
1468
+ async function pollAuthStatus(endpoint, cliToken) {
1469
+ const response = await fetch(`${endpoint}/api/auth/cli/status?token=${encodeURIComponent(cliToken)}`);
1470
+ if (!response.ok) {
1471
+ const errorText = await response.text();
1472
+ throw new BrowserAuthError("POLL_FAILED", "Failed to poll auth status: " + String(response.status) + " " + errorText);
1473
+ }
1474
+ const data = await response.json();
1475
+ return data;
1476
+ }
1477
+ async function openBrowser(url) {
1478
+ if (process.env.MUTAGENT_TEST_MODE === "true") {
1479
+ console.log(`AUTH_URL:${url}`);
1480
+ return;
1481
+ }
1482
+ try {
1483
+ const { default: open } = await import("open");
1484
+ await open(url);
1485
+ } catch {
1486
+ throw new BrowserAuthError("BROWSER_OPEN_FAILED", `Could not open browser automatically. Please visit: ${url}`);
1487
+ }
1488
+ }
1489
+ function sleep(ms) {
1490
+ return new Promise((resolve) => setTimeout(resolve, ms));
1491
+ }
1492
+ async function performBrowserAuth(options, onStatusUpdate) {
1493
+ const {
1494
+ endpoint,
1495
+ timeout = 300000,
1496
+ pollInterval = 2000,
1497
+ skipBrowserOpen = false
1498
+ } = options;
1499
+ const cliToken = generateCliToken();
1500
+ onStatusUpdate?.("Initializing browser authentication...");
1501
+ const initResponse = await initBrowserAuth(endpoint, cliToken);
1502
+ const { authUrl } = initResponse;
1503
+ if (!skipBrowserOpen) {
1504
+ onStatusUpdate?.("Opening browser for authentication...");
1505
+ try {
1506
+ await openBrowser(authUrl);
1507
+ } catch (error) {
1508
+ if (error instanceof BrowserAuthError && error.code === "BROWSER_OPEN_FAILED") {
1509
+ onStatusUpdate?.(error.message);
1510
+ } else {
1511
+ throw error;
1512
+ }
1513
+ }
1514
+ }
1515
+ console.log("");
1516
+ console.log(" Open this URL to authenticate:");
1517
+ console.log("");
1518
+ console.log(" " + authUrl);
1519
+ console.log("");
1520
+ onStatusUpdate?.("Waiting for browser authentication...");
1521
+ const startTime = Date.now();
1522
+ while (Date.now() - startTime < timeout) {
1523
+ await sleep(pollInterval);
1524
+ const status = await pollAuthStatus(endpoint, cliToken);
1525
+ switch (status.status) {
1526
+ case "pending":
1527
+ continue;
1528
+ case "completed":
1529
+ if (!status.apiKey || !status.workspaceId || !status.workspaceName || !status.organizationId || !status.organizationName) {
1530
+ throw new BrowserAuthError("INCOMPLETE_RESPONSE", "Server returned incomplete auth response");
1531
+ }
1532
+ return {
1533
+ apiKey: status.apiKey,
1534
+ workspaceId: status.workspaceId,
1535
+ workspaceName: status.workspaceName,
1536
+ organizationId: status.organizationId,
1537
+ organizationName: status.organizationName,
1538
+ expiresAt: status.expiresAt
1539
+ };
1540
+ case "expired":
1541
+ throw new BrowserAuthError("AUTH_EXPIRED", "Browser authentication expired. Please try again.");
1542
+ case "denied":
1543
+ throw new BrowserAuthError("AUTH_DENIED", status.error ?? "Browser authentication was denied.");
1544
+ case "not_found":
1545
+ throw new BrowserAuthError("TOKEN_NOT_FOUND", "Authentication token not found. Please try again.");
1546
+ default:
1547
+ throw new BrowserAuthError("UNKNOWN_STATUS", "Unknown auth status: " + String(status.status));
1548
+ }
1549
+ }
1550
+ throw new BrowserAuthError("AUTH_TIMEOUT", "Browser authentication timed out after 5 minutes. Please try again.");
1551
+ }
1552
+
1553
+ class BrowserAuthError extends Error {
1554
+ code;
1555
+ constructor(code, message) {
1556
+ super(message);
1557
+ this.name = "BrowserAuthError";
1558
+ this.code = code;
1559
+ }
1560
+ getSuggestion() {
1561
+ switch (this.code) {
1562
+ case "INIT_FAILED":
1563
+ return "Check your endpoint configuration and network connection.";
1564
+ case "POLL_FAILED":
1565
+ return "Check your network connection and try again.";
1566
+ case "BROWSER_OPEN_FAILED":
1567
+ return "Copy the URL above and open it manually in your browser.";
1568
+ case "AUTH_EXPIRED":
1569
+ case "AUTH_TIMEOUT":
1570
+ return 'Run "mutagent auth login" again to restart authentication.';
1571
+ case "AUTH_DENIED":
1572
+ return "Ensure you have access to the workspace and try again.";
1573
+ case "TOKEN_NOT_FOUND":
1574
+ case "INCOMPLETE_RESPONSE":
1575
+ case "UNKNOWN_STATUS":
1576
+ return "Please try again. If the issue persists, contact support.";
1577
+ default:
1578
+ return "Please try again.";
1579
+ }
1580
+ }
1581
+ }
1582
+
1583
+ // src/lib/auth-flow.ts
1584
+ async function performLoginAction(opts) {
1585
+ const { isJson, output } = opts;
1586
+ const wasFirstLogin = !hasCredentials();
1587
+ const envApiKey = process.env.MUTAGENT_API_KEY;
1588
+ const endpoint = process.env.MUTAGENT_ENDPOINT ?? opts.endpoint;
1589
+ if (envApiKey) {
1590
+ return loginWithExistingKey(envApiKey, endpoint, output, wasFirstLogin);
1591
+ }
1592
+ const isNonInteractive = opts.nonInteractive === true || process.env.MUTAGENT_NON_INTERACTIVE === "true" || process.env.CI === "true" || !process.stdin.isTTY;
1593
+ if (isJson && !opts.browser && isNonInteractive) {
1594
+ throw new MutagentError("INTERACTIVE_REQUIRED", "No API key provided. Set MUTAGENT_API_KEY env var or add --browser for browser auth.", "Run: export MUTAGENT_API_KEY=<key> or mutagent login --browser --non-interactive");
1595
+ }
1596
+ if (!isNonInteractive && wasFirstLogin) {
1597
+ console.log(`
1598
+ ` + chalk3.bold.cyan(" Welcome to MutagenT CLI!") + `
1599
+ `);
1600
+ console.log(` No credentials found. Please authenticate to continue.
1601
+ `);
1602
+ }
1603
+ let useBrowserAuth = opts.browser === true;
1604
+ if (!useBrowserAuth && isNonInteractive) {
1605
+ output.info("Non-interactive environment detected. Using browser authentication.");
1606
+ useBrowserAuth = true;
1607
+ }
1608
+ if (!useBrowserAuth) {
1609
+ const methodAnswer = await inquirer.prompt([
1610
+ {
1611
+ type: "list",
1612
+ name: "method",
1613
+ message: "How would you like to authenticate?",
1614
+ choices: [
1615
+ { name: "Login (opens browser)", value: "browser" },
1616
+ { name: "API Key (paste existing key)", value: "apiKey" }
1617
+ ]
1618
+ }
1619
+ ]);
1620
+ useBrowserAuth = methodAnswer.method === "browser";
1621
+ }
1622
+ if (useBrowserAuth) {
1623
+ return loginWithBrowser(endpoint, output, wasFirstLogin);
1624
+ }
1625
+ return loginWithPastedKey(endpoint, output, wasFirstLogin);
1626
+ }
1627
+ function buildLoginJsonResponse(result) {
1628
+ return {
1629
+ success: true,
1630
+ authenticated: result.authenticated,
1631
+ endpoint: result.endpoint,
1632
+ workspace: result.workspace,
1633
+ organization: result.organization,
1634
+ _directive: {
1635
+ instruction: "Verify workspace. Run: mutagent workspaces list --json",
1636
+ next: ["mutagent workspaces list --json", "mutagent usage --json"]
1637
+ }
1638
+ };
1639
+ }
1640
+ async function loginWithExistingKey(apiKey, endpoint, output, wasFirstLogin) {
1641
+ output.info("Validating API key...");
1642
+ const isValid = await validateApiKey(apiKey, endpoint);
1643
+ if (!isValid) {
1644
+ throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
1645
+ }
1646
+ const orgs = await fetchOrganizations(apiKey, endpoint);
1647
+ let orgId;
1648
+ let orgName;
1649
+ let wsId;
1650
+ let wsName;
1651
+ if (orgs.length >= 1 && orgs[0]) {
1652
+ orgId = orgs[0].id;
1653
+ orgName = orgs[0].name;
1654
+ const workspaces = await fetchWorkspaces(apiKey, endpoint, orgId);
1655
+ const defaultWs = workspaces.find((w) => w.isDefault) ?? workspaces[0];
1656
+ if (defaultWs) {
1657
+ wsId = defaultWs.id;
1658
+ wsName = defaultWs.name;
1659
+ }
1660
+ }
1661
+ saveFullCredentials({
1662
+ apiKey,
1663
+ endpoint,
1664
+ workspaceId: wsId,
1665
+ organizationId: orgId
1666
+ });
1667
+ return {
1668
+ authenticated: true,
1669
+ apiKey,
1670
+ endpoint,
1671
+ workspace: wsId ? { id: wsId, name: wsName } : null,
1672
+ organization: orgId ? { id: orgId, name: orgName } : null,
1673
+ wasFirstLogin
1674
+ };
1675
+ }
1676
+ async function loginWithBrowser(endpoint, output, wasFirstLogin) {
1677
+ const spinner = ora({ text: "Opening browser for authentication...", spinner: "dots" });
1678
+ try {
1679
+ spinner.start();
1680
+ const result = await performBrowserAuth({ endpoint, timeout: 300000, pollInterval: 2000 }, (message) => {
1681
+ spinner.text = message;
1682
+ });
1683
+ spinner.succeed("Authenticated successfully!");
1684
+ saveFullCredentials({
1685
+ apiKey: result.apiKey,
1686
+ endpoint,
1687
+ workspaceId: result.workspaceId,
1688
+ organizationId: result.organizationId,
1689
+ expiresAt: result.expiresAt
1690
+ });
1691
+ if (result.workspaceName)
1692
+ output.info(`Workspace: ${result.workspaceName}`);
1693
+ if (result.organizationName)
1694
+ output.info(`Organization: ${result.organizationName}`);
1695
+ return {
1696
+ authenticated: true,
1697
+ apiKey: result.apiKey,
1698
+ endpoint,
1699
+ workspace: result.workspaceId ? { id: result.workspaceId, name: result.workspaceName } : null,
1700
+ organization: result.organizationId ? { id: result.organizationId, name: result.organizationName } : null,
1701
+ wasFirstLogin
1702
+ };
1703
+ } catch (error) {
1704
+ spinner.fail("Authentication failed");
1705
+ if (error instanceof BrowserAuthError) {
1706
+ throw new MutagentError(error.code, error.message, error.getSuggestion());
1707
+ }
1708
+ throw error;
1709
+ }
1710
+ }
1711
+ async function loginWithPastedKey(initialEndpoint, output, wasFirstLogin) {
1712
+ const answers = await inquirer.prompt([
1713
+ {
1714
+ type: "input",
1715
+ name: "endpoint",
1716
+ message: "MutagenT endpoint:",
1717
+ default: initialEndpoint
1718
+ },
1719
+ {
1720
+ type: "password",
1721
+ name: "apiKey",
1722
+ message: "API Key:",
1723
+ mask: "*",
1724
+ validate: (input) => input.length > 0 || "API key is required"
1725
+ }
1726
+ ]);
1727
+ const apiKey = answers.apiKey;
1728
+ const endpoint = answers.endpoint;
1729
+ output.info("Validating API key...");
1730
+ const isValid = await validateApiKey(apiKey, endpoint);
1731
+ if (!isValid) {
1732
+ throw new MutagentError("INVALID_API_KEY", "Invalid API key or endpoint", "Check your API key and try again");
1733
+ }
1734
+ let selectedOrgId;
1735
+ let selectedOrgName;
1736
+ let selectedWsId;
1737
+ let selectedWsName;
1738
+ const orgs = await fetchOrganizations(apiKey, endpoint);
1739
+ if (orgs.length === 1 && orgs[0]) {
1740
+ selectedOrgId = orgs[0].id;
1741
+ selectedOrgName = orgs[0].name;
1742
+ } else if (orgs.length > 1) {
1743
+ const orgAnswer = await inquirer.prompt([
1744
+ {
1745
+ type: "list",
1746
+ name: "orgId",
1747
+ message: "Select organization:",
1748
+ choices: orgs.map((o) => ({ name: o.name, value: o.id }))
1749
+ }
1750
+ ]);
1751
+ selectedOrgId = orgAnswer.orgId;
1752
+ selectedOrgName = orgs.find((o) => o.id === selectedOrgId)?.name;
1753
+ }
1754
+ if (selectedOrgId) {
1755
+ const workspaces = await fetchWorkspaces(apiKey, endpoint, selectedOrgId);
1756
+ const defaultWs = workspaces.find((w) => w.isDefault);
1757
+ if (workspaces.length === 1 && workspaces[0]) {
1758
+ selectedWsId = workspaces[0].id;
1759
+ selectedWsName = workspaces[0].name;
1760
+ } else if (defaultWs) {
1761
+ selectedWsId = defaultWs.id;
1762
+ selectedWsName = defaultWs.name;
1763
+ } else if (workspaces.length > 1) {
1764
+ const wsAnswer = await inquirer.prompt([
1765
+ {
1766
+ type: "list",
1767
+ name: "wsId",
1768
+ message: "Select workspace:",
1769
+ choices: workspaces.map((w) => ({
1770
+ name: w.name + (w.isDefault ? " (default)" : ""),
1771
+ value: w.id
1772
+ }))
1773
+ }
1774
+ ]);
1775
+ selectedWsId = wsAnswer.wsId;
1776
+ selectedWsName = workspaces.find((w) => w.id === selectedWsId)?.name;
1777
+ }
1778
+ }
1779
+ saveFullCredentials({
1780
+ apiKey,
1781
+ endpoint,
1782
+ workspaceId: selectedWsId,
1783
+ organizationId: selectedOrgId
1784
+ });
1785
+ return {
1786
+ authenticated: true,
1787
+ apiKey,
1788
+ endpoint,
1789
+ workspace: selectedWsId ? { id: selectedWsId, name: selectedWsName } : null,
1790
+ organization: selectedOrgId ? { id: selectedOrgId, name: selectedOrgName } : null,
1791
+ wasFirstLogin
1792
+ };
1793
+ }
1794
+
1795
+ // src/commands/auth.ts
1796
+ function createAuthCommand() {
1797
+ const auth = new Command("auth").description("Authenticate with MutagenT platform").addHelpText("after", `
1798
+ Examples:
1799
+ ${chalk4.dim("$")} mutagent auth login
1800
+ ${chalk4.dim("$")} mutagent auth login --browser
1801
+ ${chalk4.dim("$")} mutagent auth status
1802
+ ${chalk4.dim("$")} mutagent auth logout
1803
+ `);
1804
+ auth.command("login").description("Authenticate and store API key").option("--browser", "Force browser-based authentication").option("--non-interactive", "Disable interactive prompts (auto-selects browser auth)").option("--endpoint <url>", "API endpoint", "https://api.mutagent.io").addHelpText("after", `
1805
+ Examples:
1806
+ ${chalk4.dim("$")} mutagent auth login ${chalk4.dim("# Interactive (choose method)")}
1807
+ ${chalk4.dim("$")} mutagent auth login --browser ${chalk4.dim("# Browser OAuth flow")}
1808
+ ${chalk4.dim("$")} mutagent auth login --non-interactive ${chalk4.dim("# Auto browser flow (AI agents)")}
1809
+
1810
+ ${chalk4.dim("Note: this command is an alias for `mutagent login`.")}
1811
+ ${chalk4.dim("See `mutagent login --help` for full documentation.")}
1812
+ `).action(async (options) => {
1813
+ const isJson = getJsonFlag(auth);
1814
+ const output = new OutputFormatter(isJson ? "json" : "table");
1815
+ try {
1816
+ const result = await performLoginAction({
1817
+ endpoint: options.endpoint,
1818
+ browser: options.browser,
1819
+ nonInteractive: options.nonInteractive,
1820
+ isJson,
1821
+ output
1822
+ });
1823
+ if (isJson) {
1824
+ output.output(buildLoginJsonResponse(result));
1825
+ } else {
1826
+ output.success("Authenticated successfully");
1827
+ if (result.organization?.name)
1828
+ output.info(`Organization: ${result.organization.name}`);
1829
+ if (result.workspace?.name)
1830
+ output.info(`Workspace: ${result.workspace.name}`);
1831
+ output.info(`Endpoint: ${result.endpoint}`);
1832
+ output.info("Next: mutagent workspaces list --json");
1833
+ }
1834
+ if (result.wasFirstLogin && process.stdin.isTTY && !isJson) {
1835
+ await runPostOnboarding();
1836
+ }
1837
+ } catch (error) {
1838
+ if (error instanceof MutagentError) {
1839
+ output.error(error.message);
1840
+ process.exit(error.exitCode);
1841
+ }
1842
+ throw error;
1843
+ }
1844
+ });
1845
+ auth.command("status").description("Check authentication status").addHelpText("after", `
1846
+ Examples:
1847
+ ${chalk4.dim("$")} mutagent auth status
1848
+ ${chalk4.dim("$")} mutagent auth status --json
1849
+ `).action(async () => {
1850
+ const isJson = getJsonFlag(auth);
1851
+ const output = new OutputFormatter(isJson ? "json" : "table");
1852
+ const apiKey = getApiKey();
1853
+ const config = loadConfig();
1854
+ const endpoint = config.endpoint ?? "https://api.mutagent.io";
1855
+ if (!apiKey) {
1856
+ if (isJson) {
1412
1857
  output.output({ authenticated: false, message: "Not authenticated" });
1413
1858
  } else {
1414
1859
  output.error("Not authenticated");
@@ -1418,7 +1863,7 @@ Examples:
1418
1863
  }
1419
1864
  const isValid = await validateApiKey(apiKey, endpoint);
1420
1865
  const cwd = process.cwd();
1421
- const hasOnboarding = existsSync3(join3(cwd, ".mutagentrc.json"));
1866
+ const hasOnboarding = existsSync5(join6(cwd, ".mutagentrc.json"));
1422
1867
  if (isJson) {
1423
1868
  const statusResult = {
1424
1869
  authenticated: isValid,
@@ -2305,142 +2750,25 @@ Available Models:`));
2305
2750
  if (result.error) {
2306
2751
  console.log(chalk11.red(`Error: ${result.error}`));
2307
2752
  }
2308
- }
2309
- }
2310
- } catch (error) {
2311
- handleError(error, isJson);
2312
- }
2313
- });
2314
- registerAddCommand(providers);
2315
- registerUpdateCommand(providers);
2316
- registerDeleteCommand(providers);
2317
- return providers;
2318
- }
2319
-
2320
- // src/commands/init.ts
2321
- init_config();
2322
- import { Command as Command6 } from "commander";
2323
- import inquirer2 from "inquirer";
2324
- import chalk12 from "chalk";
2325
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2326
- import { execSync as execSync2 } from "child_process";
2327
- import { join as join5 } from "path";
2328
- init_errors();
2329
-
2330
- // src/lib/framework-detection.ts
2331
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2332
- import { execSync } from "child_process";
2333
- import { join as join4 } from "path";
2334
- var FRAMEWORK_DETECTION_MAP = {
2335
- "@mastra/core": {
2336
- name: "mastra",
2337
- displayName: "Mastra",
2338
- npmPackage: "@mastra/core"
2339
- },
2340
- "@langchain/langgraph": {
2341
- name: "langgraph",
2342
- displayName: "LangGraph",
2343
- npmPackage: "@langchain/langgraph",
2344
- mutagentPackage: "@mutagent/langgraph"
2345
- },
2346
- langchain: {
2347
- name: "langchain",
2348
- displayName: "LangChain",
2349
- npmPackage: "langchain",
2350
- mutagentPackage: "@mutagent/langchain"
2351
- },
2352
- "@langchain/core": {
2353
- name: "langchain",
2354
- displayName: "LangChain",
2355
- npmPackage: "@langchain/core",
2356
- mutagentPackage: "@mutagent/langchain"
2357
- },
2358
- "@ai-sdk/core": {
2359
- name: "vercel-ai",
2360
- displayName: "Vercel AI SDK",
2361
- npmPackage: "@ai-sdk/core",
2362
- mutagentPackage: "@mutagent/vercel-ai"
2363
- },
2364
- ai: {
2365
- name: "vercel-ai",
2366
- displayName: "Vercel AI SDK",
2367
- npmPackage: "ai",
2368
- mutagentPackage: "@mutagent/vercel-ai"
2369
- },
2370
- "@google/genai": {
2371
- name: "generic",
2372
- displayName: "Google GenAI",
2373
- npmPackage: "@google/genai"
2374
- },
2375
- openai: {
2376
- name: "openai",
2377
- displayName: "OpenAI SDK",
2378
- npmPackage: "openai",
2379
- mutagentPackage: "@mutagent/openai"
2380
- }
2381
- };
2382
- function detectPackageManager(cwd = process.cwd()) {
2383
- if (existsSync4(join4(cwd, "bun.lockb")) || existsSync4(join4(cwd, "bun.lock"))) {
2384
- return "bun";
2385
- }
2386
- if (existsSync4(join4(cwd, "pnpm-lock.yaml"))) {
2387
- return "pnpm";
2388
- }
2389
- if (existsSync4(join4(cwd, "yarn.lock"))) {
2390
- return "yarn";
2391
- }
2392
- if (existsSync4(join4(cwd, "package-lock.json"))) {
2393
- return "npm";
2394
- }
2395
- try {
2396
- execSync("bun --version", { stdio: "ignore" });
2397
- return "bun";
2398
- } catch {
2399
- return "npm";
2400
- }
2401
- }
2402
- function getInstallCommand(pm, packages) {
2403
- const pkgList = packages.join(" ");
2404
- const commands = {
2405
- bun: `bun add ${pkgList}`,
2406
- npm: `npm install ${pkgList}`,
2407
- yarn: `yarn add ${pkgList}`,
2408
- pnpm: `pnpm add ${pkgList}`
2409
- };
2410
- return commands[pm];
2411
- }
2412
- function detectFrameworkFromPackageJson(cwd = process.cwd()) {
2413
- const pkgPath = join4(cwd, "package.json");
2414
- if (!existsSync4(pkgPath)) {
2415
- return null;
2416
- }
2417
- let pkg;
2418
- try {
2419
- pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
2420
- } catch {
2421
- return null;
2422
- }
2423
- const allDeps = {
2424
- ...pkg.dependencies,
2425
- ...pkg.devDependencies
2426
- };
2427
- for (const [depName, framework] of Object.entries(FRAMEWORK_DETECTION_MAP)) {
2428
- if (depName in allDeps) {
2429
- return framework;
2753
+ }
2754
+ }
2755
+ } catch (error) {
2756
+ handleError(error, isJson);
2430
2757
  }
2431
- }
2432
- return null;
2433
- }
2434
- function hasRcConfig(cwd = process.cwd()) {
2435
- return existsSync4(join4(cwd, ".mutagentrc.json"));
2758
+ });
2759
+ registerAddCommand(providers);
2760
+ registerUpdateCommand(providers);
2761
+ registerDeleteCommand(providers);
2762
+ return providers;
2436
2763
  }
2437
2764
 
2438
2765
  // src/commands/init.ts
2439
- function writeRcConfig(config, cwd = process.cwd()) {
2440
- const rcPath = join5(cwd, ".mutagentrc.json");
2441
- writeFileSync2(rcPath, JSON.stringify(config, null, 2) + `
2442
- `);
2443
- }
2766
+ init_config();
2767
+ import { Command as Command6 } from "commander";
2768
+ import inquirer2 from "inquirer";
2769
+ import chalk12 from "chalk";
2770
+ import { execSync as execSync2 } from "child_process";
2771
+ init_errors();
2444
2772
  function createInitCommand() {
2445
2773
  const init = new Command6("init").description("Initialize MutagenT in your project").option("--non-interactive", "Skip interactive prompts (defaults to CLI-only mode)").addHelpText("after", `
2446
2774
  Examples:
@@ -2631,9 +2959,8 @@ Modes:
2631
2959
  }
2632
2960
  console.log("");
2633
2961
  output.info("Next steps:");
2634
- output.info(" 1. Run: mutagent skills install (install the CLI skill for AI agents)");
2635
- output.info(` 2. Add MutagenT tracing to your ${confirmedFramework.name} code via ${confirmedFramework.mutagentPackage}`);
2636
- output.info(" 3. Run: mutagent providers list (confirm an LLM provider is configured)");
2962
+ output.info(` 1. Add MutagenT tracing to your ${confirmedFramework.name} code via ${confirmedFramework.mutagentPackage}`);
2963
+ output.info(" 2. Run: mutagent providers list (confirm an LLM provider is configured)");
2637
2964
  } else {
2638
2965
  writeRcConfig(rcConfig, cwd);
2639
2966
  output.success("Created .mutagentrc.json");
@@ -2653,60 +2980,9 @@ Modes:
2653
2980
  }
2654
2981
  }
2655
2982
  }
2656
- const skillPath = join5(cwd, ".claude/skills/mutagent-cli/SKILL.md");
2657
- const skillInstalled = existsSync5(skillPath);
2658
- if (!isNonInteractive && !skillInstalled) {
2659
- const { installSkill } = await inquirer2.prompt([{
2660
- type: "confirm",
2661
- name: "installSkill",
2662
- message: "Install MutagenT skill for Claude Code? (Teaches AI agents how to use the CLI)",
2663
- default: true
2664
- }]);
2665
- if (installSkill) {
2666
- try {
2667
- const skillDir = join5(cwd, ".claude/skills/mutagent-cli");
2668
- if (!existsSync5(skillDir)) {
2669
- mkdirSync2(skillDir, { recursive: true });
2670
- }
2671
- execSync2("node " + join5(cwd, "node_modules/.bin/mutagent") + " skills install", {
2672
- cwd,
2673
- stdio: "ignore"
2674
- });
2675
- output.success("Installed MutagenT CLI skill for Claude Code");
2676
- } catch {
2677
- output.info("Install skill manually: mutagent skills install");
2678
- }
2679
- }
2680
- }
2681
2983
  const initDirective = {
2682
- instruction: skillInstalled ? "Verify workspace. Run: mutagent workspaces list --json" : [
2683
- "MANDATORY: Ask the user whether they want to install the MutagenT CLI skill and Claude Code hooks.",
2684
- "Use your AskUserQuestion tool to present the following choices:",
2685
- "1. Install skill + hooks (Recommended) — Installs the CLI skill AND configures Claude Code session telemetry hooks",
2686
- "2. Install skill only — Installs the CLI skill (teaches AI agents to use MutagenT CLI)",
2687
- "3. Skip — Do not install",
2688
- "",
2689
- "If the user picks option 1, run BOTH commands:",
2690
- " mutagent skills install --json",
2691
- " mutagent hooks install --json",
2692
- "",
2693
- "If the user picks option 2, run only: mutagent skills install --json",
2694
- "",
2695
- "After skill/hooks install, verify workspace: mutagent workspaces list --json"
2696
- ].join(`
2697
- `),
2698
- next: ["mutagent workspaces list --json", "mutagent providers list --json"],
2699
- ...!skillInstalled ? {
2700
- askUserQuestions: [{
2701
- question: "Would you like to install the MutagenT CLI skill and Claude Code hooks?",
2702
- options: [
2703
- { label: "Install skill + hooks (Recommended)", description: "Installs the CLI skill AND configures Claude Code session telemetry hooks" },
2704
- { label: "Install skill only", description: "Installs the CLI skill (teaches AI agents to use MutagenT CLI)" },
2705
- { label: "Skip", description: "Do not install" }
2706
- ],
2707
- multiSelect: false
2708
- }]
2709
- } : {}
2984
+ instruction: "Verify workspace. Run: mutagent workspaces list --json",
2985
+ next: ["mutagent workspaces list --json", "mutagent providers list --json"]
2710
2986
  };
2711
2987
  const summary = {
2712
2988
  success: true,
@@ -2716,7 +2992,6 @@ Modes:
2716
2992
  framework: confirmedFramework?.name ?? null,
2717
2993
  authenticated,
2718
2994
  workspaceValidation: workspaceValidation ?? null,
2719
- skillInstalled: skillInstalled || existsSync5(skillPath),
2720
2995
  _directive: initDirective
2721
2996
  };
2722
2997
  output.output(summary);
@@ -2730,8 +3005,8 @@ Modes:
2730
3005
  // src/commands/skills.ts
2731
3006
  import { Command as Command7 } from "commander";
2732
3007
  import chalk13 from "chalk";
2733
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
2734
- import { dirname, join as join6 } from "path";
3008
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
3009
+ import { dirname, join as join7 } from "path";
2735
3010
  import { execSync as execSync3 } from "child_process";
2736
3011
 
2737
3012
  // src/generated/skill-content.ts
@@ -3386,7 +3661,7 @@ that teaches coding agents how to use the MutagenT CLI effectively.
3386
3661
  const isJson = parentCmd ? getJsonFlag(parentCmd) : false;
3387
3662
  const output = new OutputFormatter(isJson ? "json" : "table");
3388
3663
  const repoRoot = findRepoRoot();
3389
- const skillDir = join6(repoRoot, SKILL_DIR);
3664
+ const skillDir = join7(repoRoot, SKILL_DIR);
3390
3665
  const files = getSkillFiles();
3391
3666
  const writtenFiles = [];
3392
3667
  let totalBytes = 0;
@@ -3398,7 +3673,7 @@ that teaches coding agents how to use the MutagenT CLI effectively.
3398
3673
  return a.localeCompare(b);
3399
3674
  });
3400
3675
  for (const relPath of sortedKeys) {
3401
- const destPath = join6(skillDir, relPath);
3676
+ const destPath = join7(skillDir, relPath);
3402
3677
  const parentDir = dirname(destPath);
3403
3678
  if (!existsSync6(parentDir)) {
3404
3679
  mkdirSync3(parentDir, { recursive: true });
@@ -3407,7 +3682,7 @@ that teaches coding agents how to use the MutagenT CLI effectively.
3407
3682
  const finalContent = raw.endsWith(`
3408
3683
  `) ? raw : `${raw}
3409
3684
  `;
3410
- writeFileSync3(destPath, finalContent, "utf-8");
3685
+ writeFileSync4(destPath, finalContent, "utf-8");
3411
3686
  writtenFiles.push({ path: destPath, bytes: finalContent.length });
3412
3687
  totalBytes += finalContent.length;
3413
3688
  }
@@ -3510,18 +3785,18 @@ import { Command as Command9 } from "commander";
3510
3785
  import { randomUUID } from "crypto";
3511
3786
 
3512
3787
  // src/commands/hooks/state.ts
3513
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync, unlinkSync, existsSync as existsSync7 } from "fs";
3514
- import { join as join7 } from "path";
3788
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, renameSync as renameSync2, unlinkSync, existsSync as existsSync7 } from "fs";
3789
+ import { join as join8 } from "path";
3515
3790
  import { tmpdir } from "os";
3516
3791
  function stateFilePath(sessionId) {
3517
- return join7(tmpdir(), `mutagent-hook-${sessionId}.json`);
3792
+ return join8(tmpdir(), `mutagent-hook-${sessionId}.json`);
3518
3793
  }
3519
3794
  function readState(sessionId) {
3520
3795
  const path = stateFilePath(sessionId);
3521
3796
  if (!existsSync7(path))
3522
3797
  return null;
3523
3798
  try {
3524
- const raw = JSON.parse(readFileSync4(path, "utf-8"));
3799
+ const raw = JSON.parse(readFileSync5(path, "utf-8"));
3525
3800
  if (!Array.isArray(raw.parentStack)) {
3526
3801
  raw.parentStack = [];
3527
3802
  }
@@ -3536,8 +3811,8 @@ function readState(sessionId) {
3536
3811
  function writeState(sessionId, state) {
3537
3812
  const path = stateFilePath(sessionId);
3538
3813
  const tmpPath = `${path}.${process.pid.toString()}.tmp`;
3539
- writeFileSync4(tmpPath, JSON.stringify(state), "utf-8");
3540
- renameSync(tmpPath, path);
3814
+ writeFileSync5(tmpPath, JSON.stringify(state), "utf-8");
3815
+ renameSync2(tmpPath, path);
3541
3816
  }
3542
3817
  function deleteState(sessionId) {
3543
3818
  const path = stateFilePath(sessionId);
@@ -4165,863 +4440,563 @@ async function handlePostToolUseFailure() {
4165
4440
  }
4166
4441
  const errorPayload = serializePayload(reason);
4167
4442
  await sendBatchTrace([
4168
- {
4169
- traceId,
4170
- sessionId,
4171
- name: "Claude Code Session",
4172
- source: "sdk",
4173
- startTime,
4174
- status: "running",
4175
- spans: [
4176
- {
4177
- spanId: matchedSpanId,
4178
- name: toolName,
4179
- kind: "tool",
4180
- startTime: matchedSpanStart,
4181
- endTime: now,
4182
- status: "error",
4183
- ...errorPayload !== undefined ? { output: errorPayload } : {}
4184
- }
4185
- ]
4186
- }
4187
- ]);
4188
- }
4189
-
4190
- // src/commands/hooks/install.ts
4191
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
4192
- import { join as join8 } from "path";
4193
-
4194
- class SettingsParseError extends Error {
4195
- settingsPath;
4196
- backupPath;
4197
- constructor(settingsPath, backupPath, cause) {
4198
- super(`settings.local.json contains unparseable JSON.
4199
- ` + ` Original file backed up to: ${backupPath}
4200
- ` + ` To recover: restore from backup, fix JSON syntax, then re-run ` + `\`mutagent hooks install\``);
4201
- this.settingsPath = settingsPath;
4202
- this.backupPath = backupPath;
4203
- this.name = "SettingsParseError";
4204
- if (cause instanceof Error)
4205
- this.cause = cause;
4206
- }
4207
- }
4208
- var V1_MIGRATIONS = {
4209
- Stop: ["mutagent hooks claude-code session-end"]
4210
- };
4211
- var MUTAGENT_HOOKS = {
4212
- SessionStart: [{ matcher: "startup", hooks: [{ type: "command", command: "mutagent hooks claude-code session-start" }] }],
4213
- PreToolUse: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code pre-tool-use" }] }],
4214
- PostToolUse: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code post-tool-use" }] }],
4215
- Stop: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code stop" }] }],
4216
- SessionEnd: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code session-end" }] }],
4217
- UserPromptSubmit: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code user-prompt-submit" }] }],
4218
- SubagentStart: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code subagent-start" }] }],
4219
- SubagentStop: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code subagent-stop" }] }],
4220
- PreCompact: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code pre-compact" }] }],
4221
- PostCompact: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code post-compact" }] }],
4222
- PostToolUseFailure: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code post-tool-use-failure" }] }]
4223
- };
4224
- function hasCommand(matchers, command) {
4225
- return matchers.some((m) => m.hooks.some((h) => h.command === command));
4226
- }
4227
- function migrateV1Hooks(settings) {
4228
- const migrated = [];
4229
- if (!settings.hooks)
4230
- return migrated;
4231
- for (const [event, commandsToRemove] of Object.entries(V1_MIGRATIONS)) {
4232
- const matchers = settings.hooks[event];
4233
- if (!matchers)
4234
- continue;
4235
- const filtered = matchers.map((matcher) => {
4236
- const keptHooks = matcher.hooks.filter((h) => {
4237
- if (commandsToRemove.includes(h.command)) {
4238
- migrated.push({ event, command: h.command });
4239
- return false;
4240
- }
4241
- return true;
4242
- });
4243
- return keptHooks.length > 0 ? { ...matcher, hooks: keptHooks } : null;
4244
- }).filter(Boolean);
4245
- if (filtered.length === 0) {
4246
- delete settings.hooks[event];
4247
- } else {
4248
- settings.hooks[event] = filtered;
4249
- }
4250
- }
4251
- return migrated;
4252
- }
4253
- function installHooks(cwd) {
4254
- const claudeDir = join8(cwd, ".claude");
4255
- const settingsPath = join8(claudeDir, "settings.local.json");
4256
- const existed = existsSync8(settingsPath);
4257
- let settings = {};
4258
- if (existed) {
4259
- const raw = readFileSync5(settingsPath, "utf-8");
4260
- try {
4261
- settings = JSON.parse(raw);
4262
- } catch (err) {
4263
- const backupPath = `${settingsPath}.bak.${new Date().toISOString().replace(/:/g, "-")}`;
4264
- writeFileSync5(backupPath, raw, "utf-8");
4265
- throw new SettingsParseError(settingsPath, backupPath, err);
4266
- }
4267
- }
4268
- const added = [];
4269
- const alreadyPresent = [];
4270
- const migrated = migrateV1Hooks(settings);
4271
- for (const [event, newMatchers] of Object.entries(MUTAGENT_HOOKS)) {
4272
- if (!newMatchers)
4273
- continue;
4274
- settings.hooks ??= {};
4275
- settings.hooks[event] ??= [];
4276
- const existing = settings.hooks[event];
4277
- for (const matcher of newMatchers) {
4278
- for (const hook of matcher.hooks) {
4279
- if (hasCommand(existing, hook.command)) {
4280
- alreadyPresent.push(hook.command);
4281
- } else {
4282
- const newMatcher = {
4283
- ...matcher.matcher !== undefined ? { matcher: matcher.matcher } : {},
4284
- hooks: [hook]
4285
- };
4286
- existing.push(newMatcher);
4287
- added.push(hook.command);
4288
- }
4289
- }
4290
- }
4291
- }
4292
- let userWarning;
4293
- if (added.length > 0 || migrated.length > 0) {
4294
- if (!existsSync8(claudeDir)) {
4295
- mkdirSync4(claudeDir, { recursive: true });
4296
- }
4297
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + `
4298
- `, "utf-8");
4299
- }
4300
- if (added.length > 0) {
4301
- const addedList = added.map((cmd) => {
4302
- const parts = cmd.split(" ");
4303
- return parts[parts.length - 1] ?? cmd;
4304
- }).join(", ");
4305
- userWarning = `MutagenT hooks installed into .claude/settings.local.json
4306
- ` + ` Added: ${addedList}
4307
- ` + ` This file was modified. Review with: git diff .claude/settings.local.json
4308
- ` + ` (To remove hooks, edit .claude/settings.local.json and delete the mutagent entries)`;
4309
- }
4310
- return { settingsPath, existed, added, alreadyPresent, migrated, userWarning };
4311
- }
4312
-
4313
- // src/commands/hooks/index.ts
4314
- function createHooksCommand() {
4315
- const hooks = new Command9("hooks").description("Hook handlers for AI coding assistants").addHelpText("after", `
4316
- Claude Code Session Telemetry:
4317
- Sends lightweight session activity to the MutagenT traces API for observability.
4318
-
4319
- Install by adding to .claude/settings.local.json:
4320
-
4321
- {
4322
- "hooks": {
4323
- "SessionStart": [{"matcher": "startup", "hooks": [{"type": "command", "command": "mutagent hooks claude-code session-start"}]}],
4324
- "Stop": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code session-end"}]}],
4325
- "PreToolUse": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code pre-tool-use"}]}],
4326
- "PostToolUse": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code post-tool-use"}]}],
4327
- "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code user-prompt-submit"}]}],
4328
- "SubagentStart": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code subagent-start"}]}],
4329
- "SubagentStop": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code subagent-stop"}]}],
4330
- "PreCompact": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code pre-compact"}]}],
4331
- "PostCompact": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code post-compact"}]}],
4332
- "PostToolUseFailure": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code post-tool-use-failure"}]}]
4333
- }
4334
- }
4335
-
4336
- Or run: mutagent hooks install
4337
- `);
4338
- hooks.command("install").description("Install MutagenT hooks into .claude/settings.local.json (safe merge — never overwrites existing hooks)").option("--cwd <dir>", "Target directory (defaults to cwd)", process.cwd()).addHelpText("after", `
4339
- Reads existing .claude/settings.local.json (if present) and deep-merges
4340
- MutagenT telemetry hooks into each event array (all 10 events). Skips any
4341
- hook already present (checked by command string) so running this multiple
4342
- times is safe.
4343
- `).action((opts) => {
4344
- const targetDir = opts.cwd ?? process.cwd();
4345
- const isJson = Boolean(opts.json);
4346
- let result;
4347
- try {
4348
- result = installHooks(targetDir);
4349
- } catch (err) {
4350
- if (err instanceof SettingsParseError) {
4351
- if (isJson) {
4352
- process.stdout.write(JSON.stringify({
4353
- success: false,
4354
- error: err.message,
4355
- backupPath: err.backupPath,
4356
- settingsPath: err.settingsPath
4357
- }) + `
4358
- `);
4359
- } else {
4360
- process.stderr.write(`[mutagent hooks install] ERROR: ${err.message}
4361
- `);
4362
- }
4363
- process.exit(1);
4364
- }
4365
- throw err;
4366
- }
4367
- for (const { event, command } of result.migrated) {
4368
- process.stderr.write(`[mutagent hooks install] ⚠️ Migrated v1 hook: removed '${event} → ${command}' (v2 wires this as 'SessionEnd')
4369
- `);
4370
- }
4371
- if (result.added.length === 0 && result.alreadyPresent.length === 0) {
4372
- process.stdout.write(JSON.stringify({
4373
- success: true,
4374
- settingsPath: result.settingsPath,
4375
- added: [],
4376
- alreadyPresent: [],
4377
- message: "No hooks to install."
4378
- }) + `
4379
- `);
4380
- return;
4381
- }
4382
- if (result.userWarning) {
4383
- if (isJson) {} else {
4384
- process.stderr.write(`⚠ ${result.userWarning}
4385
- `);
4386
- }
4387
- }
4388
- const jsonResponse = {
4389
- success: true,
4390
- settingsPath: result.settingsPath,
4391
- existed: result.existed,
4392
- added: result.added,
4393
- alreadyPresent: result.alreadyPresent,
4394
- message: result.added.length > 0 ? `Installed ${String(result.added.length)} hook(s). ${String(result.alreadyPresent.length)} already present.` : `All hooks already present (${String(result.alreadyPresent.length)}).`
4395
- };
4396
- if (result.userWarning && isJson) {
4397
- jsonResponse.warnings = [result.userWarning];
4443
+ {
4444
+ traceId,
4445
+ sessionId,
4446
+ name: "Claude Code Session",
4447
+ source: "sdk",
4448
+ startTime,
4449
+ status: "running",
4450
+ spans: [
4451
+ {
4452
+ spanId: matchedSpanId,
4453
+ name: toolName,
4454
+ kind: "tool",
4455
+ startTime: matchedSpanStart,
4456
+ endTime: now,
4457
+ status: "error",
4458
+ ...errorPayload !== undefined ? { output: errorPayload } : {}
4459
+ }
4460
+ ]
4398
4461
  }
4399
- process.stdout.write(JSON.stringify(jsonResponse) + `
4400
- `);
4401
- });
4402
- const claudeCode = hooks.command("claude-code").description("Claude Code session telemetry");
4403
- claudeCode.command("session-start").description("Handle session start event").action(async () => {
4404
- await safeExecute(handleSessionStart);
4405
- });
4406
- claudeCode.command("session-end").description("Handle session end event").action(async () => {
4407
- await safeExecute(handleSessionEnd);
4408
- });
4409
- claudeCode.command("pre-tool-use").description("Handle pre-tool-use event").action(async () => {
4410
- await safeExecute(handlePreToolUse);
4411
- });
4412
- claudeCode.command("post-tool-use").description("Handle post-tool-use event").action(async () => {
4413
- await safeExecute(handlePostToolUse);
4414
- });
4415
- claudeCode.command("user-prompt-submit").description("Handle user prompt submit event (creates turn span)").action(async () => {
4416
- await safeExecute(handleUserPromptSubmit);
4417
- });
4418
- claudeCode.command("stop").description("Handle stop event (closes current turn span)").action(async () => {
4419
- await safeExecute(handleStop);
4420
- });
4421
- claudeCode.command("subagent-start").description("Handle subagent start event (creates nested agent span)").action(async () => {
4422
- await safeExecute(handleSubagentStart);
4423
- });
4424
- claudeCode.command("subagent-stop").description("Handle subagent stop event (closes subagent span)").action(async () => {
4425
- await safeExecute(handleSubagentStop);
4426
- });
4427
- claudeCode.command("pre-compact").description("Handle pre-compact event (opens compaction span)").action(async () => {
4428
- await safeExecute(handlePreCompact);
4429
- });
4430
- claudeCode.command("post-compact").description("Handle post-compact event (closes compaction span with summary)").action(async () => {
4431
- await safeExecute(handlePostCompact);
4432
- });
4433
- claudeCode.command("post-tool-use-failure").description("Handle post-tool-use-failure event (closes failed span with error status)").action(async () => {
4434
- await safeExecute(handlePostToolUseFailure);
4435
- });
4436
- return hooks;
4462
+ ]);
4437
4463
  }
4438
4464
 
4439
- // src/commands/feedback.ts
4440
- import { Command as Command10 } from "commander";
4441
- import chalk16 from "chalk";
4442
- import { type as osType, release as osRelease } from "os";
4443
- import { readFileSync as readFileSync7 } from "fs";
4444
- import { join as join10, dirname as dirname2 } from "path";
4445
- import { fileURLToPath } from "url";
4446
- init_errors();
4447
- init_config();
4448
-
4449
- // src/lib/transcript.ts
4450
- init_errors();
4451
- import { homedir as osHomedir } from "os";
4465
+ // src/commands/hooks/install.ts
4466
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
4452
4467
  import { join as join9 } from "path";
4453
- import {
4454
- existsSync as fsExistsSync,
4455
- statSync as fsStatSync,
4456
- readFileSync as readFileSync6,
4457
- readdirSync,
4458
- openSync,
4459
- readSync,
4460
- closeSync
4461
- } from "fs";
4462
- import chalk15 from "chalk";
4463
- var TAIL_BYTES = 200000;
4464
- function defaultReadTail(path, tailBytes) {
4465
- const { size } = fsStatSync(path);
4466
- if (size <= tailBytes) {
4467
- return { content: readFileSync6(path, "utf-8"), truncated: false };
4468
- }
4469
- const fd = openSync(path, "r");
4470
- try {
4471
- const buf = Buffer.alloc(tailBytes);
4472
- readSync(fd, buf, 0, tailBytes, size - tailBytes);
4473
- return { content: buf.toString("utf-8"), truncated: true };
4474
- } finally {
4475
- closeSync(fd);
4468
+
4469
+ class SettingsParseError extends Error {
4470
+ settingsPath;
4471
+ backupPath;
4472
+ constructor(settingsPath, backupPath, cause) {
4473
+ super(`settings.local.json contains unparseable JSON.
4474
+ ` + ` Original file backed up to: ${backupPath}
4475
+ ` + ` To recover: restore from backup, fix JSON syntax, then re-run ` + `\`mutagent hooks install\``);
4476
+ this.settingsPath = settingsPath;
4477
+ this.backupPath = backupPath;
4478
+ this.name = "SettingsParseError";
4479
+ if (cause instanceof Error)
4480
+ this.cause = cause;
4476
4481
  }
4477
4482
  }
4478
- function defaultScan(dir) {
4479
- if (!fsExistsSync(dir))
4480
- return [];
4481
- const out = [];
4482
- const walk = (current) => {
4483
- let entries;
4484
- try {
4485
- entries = readdirSync(current, { withFileTypes: true });
4486
- } catch {
4487
- return;
4488
- }
4489
- for (const entry of entries) {
4490
- const full = join9(current, entry.name);
4491
- if (entry.isDirectory())
4492
- walk(full);
4493
- else if (entry.isFile() && entry.name.endsWith(".jsonl"))
4494
- out.push(full);
4495
- }
4496
- };
4497
- walk(dir);
4498
- return out;
4499
- }
4500
- function buildSources(env, home) {
4501
- const ompBase = env.PI_CODING_AGENT_DIR ?? join9(home, ".omp", "agent");
4502
- return [
4503
- { harness: "claude-code", dir: join9(home, ".claude", "projects") },
4504
- { harness: "codex", dir: join9(home, ".codex", "sessions") },
4505
- { harness: "omp", dir: join9(ompBase, "sessions") }
4506
- ];
4483
+ var V1_MIGRATIONS = {
4484
+ Stop: ["mutagent hooks claude-code session-end"]
4485
+ };
4486
+ var MUTAGENT_HOOKS = {
4487
+ SessionStart: [{ matcher: "startup", hooks: [{ type: "command", command: "mutagent hooks claude-code session-start" }] }],
4488
+ PreToolUse: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code pre-tool-use" }] }],
4489
+ PostToolUse: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code post-tool-use" }] }],
4490
+ Stop: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code stop" }] }],
4491
+ SessionEnd: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code session-end" }] }],
4492
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code user-prompt-submit" }] }],
4493
+ SubagentStart: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code subagent-start" }] }],
4494
+ SubagentStop: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code subagent-stop" }] }],
4495
+ PreCompact: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code pre-compact" }] }],
4496
+ PostCompact: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code post-compact" }] }],
4497
+ PostToolUseFailure: [{ hooks: [{ type: "command", command: "mutagent hooks claude-code post-tool-use-failure" }] }]
4498
+ };
4499
+ function hasCommand(matchers, command) {
4500
+ return matchers.some((m) => m.hooks.some((h) => h.command === command));
4507
4501
  }
4508
- function resolveTranscript(attach, deps = {}) {
4509
- const env = deps.env ?? process.env;
4510
- const home = (deps.homedir ?? osHomedir)();
4511
- const existsSync9 = deps.existsSync ?? fsExistsSync;
4512
- const statSync = deps.statSync ?? fsStatSync;
4513
- const scan = deps.scan ?? defaultScan;
4514
- const readTail = deps.readTail ?? defaultReadTail;
4515
- const warn = deps.warn ?? ((message) => {
4516
- console.error(message);
4517
- });
4518
- if (attach === undefined || attach === false)
4519
- return null;
4520
- if (typeof attach === "string" && attach.length > 0) {
4521
- if (!existsSync9(attach)) {
4522
- throw new MutagentError("INVALID_ARGUMENTS", `Transcript file not found: ${attach}`, `Verify the path exists: ls -la "${attach}"
4523
- Or omit the path to auto-detect the newest coding-agent session.`);
4524
- }
4525
- const { content: content2, truncated: truncated2 } = readTail(attach, TAIL_BYTES);
4526
- return { harness: "unknown", sourcePath: attach, tailBytes: TAIL_BYTES, truncated: truncated2, content: content2 };
4527
- }
4528
- let newest = null;
4529
- for (const src of buildSources(env, home)) {
4530
- for (const file of scan(src.dir)) {
4531
- const { mtimeMs } = statSync(file);
4532
- if (!newest || mtimeMs > newest.mtimeMs) {
4533
- newest = { harness: src.harness, path: file, mtimeMs };
4534
- }
4502
+ function migrateV1Hooks(settings) {
4503
+ const migrated = [];
4504
+ if (!settings.hooks)
4505
+ return migrated;
4506
+ for (const [event, commandsToRemove] of Object.entries(V1_MIGRATIONS)) {
4507
+ const matchers = settings.hooks[event];
4508
+ if (!matchers)
4509
+ continue;
4510
+ const filtered = matchers.map((matcher) => {
4511
+ const keptHooks = matcher.hooks.filter((h) => {
4512
+ if (commandsToRemove.includes(h.command)) {
4513
+ migrated.push({ event, command: h.command });
4514
+ return false;
4515
+ }
4516
+ return true;
4517
+ });
4518
+ return keptHooks.length > 0 ? { ...matcher, hooks: keptHooks } : null;
4519
+ }).filter(Boolean);
4520
+ if (filtered.length === 0) {
4521
+ delete settings.hooks[event];
4522
+ } else {
4523
+ settings.hooks[event] = filtered;
4535
4524
  }
4536
4525
  }
4537
- if (!newest) {
4538
- warn(chalk15.yellow("⚠ No coding-agent transcript found; sending feedback without one."));
4539
- return null;
4540
- }
4541
- const { content, truncated } = readTail(newest.path, TAIL_BYTES);
4542
- return {
4543
- harness: newest.harness,
4544
- sourcePath: newest.path,
4545
- tailBytes: TAIL_BYTES,
4546
- truncated,
4547
- content
4548
- };
4526
+ return migrated;
4549
4527
  }
4550
-
4551
- // src/commands/feedback.ts
4552
- var MAX_FEEDBACK_LENGTH = 1e4;
4553
- var VALID_STAGES = ["spec", "build", "evaluate", "diagnose", "optimize"];
4554
- function getCliVersion() {
4555
- if (process.env.CLI_VERSION)
4556
- return process.env.CLI_VERSION;
4557
- try {
4558
- const __dirname2 = dirname2(fileURLToPath(import.meta.url));
4559
- const pkgPath = join10(__dirname2, "..", "..", "package.json");
4560
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
4561
- return pkg.version ?? "0.1.1";
4562
- } catch {
4563
- return "0.1.1";
4528
+ function installHooks(cwd) {
4529
+ const claudeDir = join9(cwd, ".claude");
4530
+ const settingsPath = join9(claudeDir, "settings.local.json");
4531
+ const existed = existsSync8(settingsPath);
4532
+ let settings = {};
4533
+ if (existed) {
4534
+ const raw = readFileSync6(settingsPath, "utf-8");
4535
+ try {
4536
+ settings = JSON.parse(raw);
4537
+ } catch (err) {
4538
+ const backupPath = `${settingsPath}.bak.${new Date().toISOString().replace(/:/g, "-")}`;
4539
+ writeFileSync6(backupPath, raw, "utf-8");
4540
+ throw new SettingsParseError(settingsPath, backupPath, err);
4541
+ }
4564
4542
  }
4565
- }
4566
- function parseCategory(input) {
4567
- if (input === "cli" || input === "helix")
4568
- return { category: input };
4569
- if (input.startsWith("stage:")) {
4570
- const stage = input.slice("stage:".length);
4571
- if (VALID_STAGES.includes(stage)) {
4572
- return { category: input };
4543
+ const added = [];
4544
+ const alreadyPresent = [];
4545
+ const migrated = migrateV1Hooks(settings);
4546
+ for (const [event, newMatchers] of Object.entries(MUTAGENT_HOOKS)) {
4547
+ if (!newMatchers)
4548
+ continue;
4549
+ settings.hooks ??= {};
4550
+ settings.hooks[event] ??= [];
4551
+ const existing = settings.hooks[event];
4552
+ for (const matcher of newMatchers) {
4553
+ for (const hook of matcher.hooks) {
4554
+ if (hasCommand(existing, hook.command)) {
4555
+ alreadyPresent.push(hook.command);
4556
+ } else {
4557
+ const newMatcher = {
4558
+ ...matcher.matcher !== undefined ? { matcher: matcher.matcher } : {},
4559
+ hooks: [hook]
4560
+ };
4561
+ existing.push(newMatcher);
4562
+ added.push(hook.command);
4563
+ }
4564
+ }
4573
4565
  }
4574
- throw new MutagentError("INVALID_ARGUMENTS", `Invalid stage: "${stage}". Must be one of: ${VALID_STAGES.join(", ")}.`, `Use --category stage:<${VALID_STAGES.join("|")}> (e.g. --category stage:evaluate).`);
4575
- }
4576
- throw new MutagentError("INVALID_ARGUMENTS", `Invalid category: "${input}". Must be 'cli', 'helix', or 'stage:<${VALID_STAGES.join("|")}>'.`, "Run: mutagent feedback send --help");
4577
- }
4578
- function buildAutoContext(harness) {
4579
- return {
4580
- harness,
4581
- cliVersion: getCliVersion(),
4582
- platform: process.platform,
4583
- os: `${osType()} ${osRelease()}`,
4584
- nodeVersion: process.version
4585
- };
4586
- }
4587
- async function postToServer(payload, endpoint, apiKey, workspaceId, organizationId) {
4588
- const headers = {
4589
- "Content-Type": "application/json",
4590
- "x-api-key": apiKey
4591
- };
4592
- if (workspaceId)
4593
- headers["x-workspace-id"] = workspaceId;
4594
- if (organizationId)
4595
- headers["x-organization-id"] = organizationId;
4596
- let response;
4597
- try {
4598
- response = await fetch(`${endpoint}/api/feedback`, {
4599
- method: "POST",
4600
- headers,
4601
- body: JSON.stringify(payload)
4602
- });
4603
- } catch {
4604
- throw new MutagentError("SERVER_UNAVAILABLE", "Server unavailable. Try again later.", "Check your network connection or verify the server endpoint with: mutagent config show");
4605
4566
  }
4606
- if (response.ok) {
4607
- return await response.json();
4567
+ let userWarning;
4568
+ if (added.length > 0 || migrated.length > 0) {
4569
+ if (!existsSync8(claudeDir)) {
4570
+ mkdirSync4(claudeDir, { recursive: true });
4571
+ }
4572
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
4573
+ `, "utf-8");
4608
4574
  }
4609
- let errorMessage = `Server returned ${String(response.status)}`;
4610
- try {
4611
- const body = await response.json();
4612
- errorMessage = body.message ?? body.error ?? errorMessage;
4613
- } catch {}
4614
- if (response.status === 401) {
4615
- throw new MutagentError("AUTH_REQUIRED", errorMessage, "Authenticate first: mutagent auth login");
4575
+ if (added.length > 0) {
4576
+ const addedList = added.map((cmd) => {
4577
+ const parts = cmd.split(" ");
4578
+ return parts[parts.length - 1] ?? cmd;
4579
+ }).join(", ");
4580
+ userWarning = `MutagenT hooks installed into .claude/settings.local.json
4581
+ ` + ` Added: ${addedList}
4582
+ ` + ` This file was modified. Review with: git diff .claude/settings.local.json
4583
+ ` + ` (To remove hooks, edit .claude/settings.local.json and delete the mutagent entries)`;
4616
4584
  }
4617
- throw new MutagentError("API_ERROR", errorMessage, `Server responded with status ${String(response.status)}. Check your configuration with: mutagent config show`);
4618
- }
4619
- function buildPayload(feedback, category, title, session, transcript) {
4620
- const feedbackV2 = {
4621
- category,
4622
- feedback,
4623
- context: buildAutoContext(transcript?.harness ?? "unknown")
4624
- };
4625
- if (title)
4626
- feedbackV2.title = title;
4627
- if (transcript)
4628
- feedbackV2.transcript = transcript;
4629
- const payload = { message: feedback, context: { feedbackV2 } };
4630
- if (session)
4631
- payload.sessionId = session;
4632
- return payload;
4633
- }
4634
- function createFeedbackCommand() {
4635
- const feedback = new Command10("feedback").description("Send product feedback to MutagenT").addHelpText("after", `
4636
- ${chalk16.bold("Examples:")}
4637
- ${chalk16.cyan('mutagent feedback send "Optimizer results were great"')}
4638
- ${chalk16.cyan('mutagent feedback send "Eval gate was confusing" --category stage:evaluate')}
4639
- ${chalk16.cyan('mutagent feedback send "CLI crashed on export" --category cli --attach-transcript --json')}
4640
-
4641
- ${chalk16.yellow("AI Agent (MANDATORY):")}
4642
- ALWAYS use --json: mutagent feedback send "..." --category cli --json
4643
- Attach the coding-agent session with --attach-transcript (auto-detects the newest session).
4644
- `).action(() => {
4645
- feedback.help();
4646
- });
4647
- registerFeedbackSend(feedback);
4648
- return feedback;
4585
+ return { settingsPath, existed, added, alreadyPresent, migrated, userWarning };
4649
4586
  }
4650
- function registerFeedbackSend(feedback) {
4651
- feedback.command("send").description("Send product feedback about the MutagenT platform or CLI").argument("<feedback>", "The feedback body (content), max 10000 characters").option("--title <string>", "Optional 5–8 word summary of the session timeline").option("--category <value>", `Feedback category: 'cli', 'helix', or 'stage:<${VALID_STAGES.join("|")}>'`, "cli").option("--session <id>", "Link this feedback to a session id (maps to server sessionId)").option("--attach-transcript [path]", "Attach the coding-agent session JSONL. Bare = auto-detect newest; or pass an explicit path.").addHelpText("after", `
4652
- ${chalk16.bold("Arguments & flags:")}
4653
- ${chalk16.bold("<feedback>")} Feedback body / content (required, ≤10000 chars)
4654
- ${chalk16.bold("--title <string>")} Optional 5–8 word summary of the session timeline
4655
- ${chalk16.bold("--category <value>")} ${chalk16.bold("cli")} (default) | ${chalk16.bold("helix")} | ${chalk16.bold("stage:<")}${VALID_STAGES.join("|")}${chalk16.bold(">")}
4656
- ${chalk16.dim("stage = the lifecycle skill (evaluator/diagnostics live under stage:*)")}
4657
- ${chalk16.bold("--session <id>")} Link feedback to a session id (server sessionId)
4658
- ${chalk16.bold("--attach-transcript")} ${chalk16.dim("[path]")} Attach the coding-agent session JSONL (bare = auto-detect newest)
4659
- ${chalk16.bold("--json")} Structured output (MANDATORY for AI agents)
4660
4587
 
4661
- ${chalk16.bold("Examples:")}
4662
- ${chalk16.dim("$")} mutagent feedback send "The setup flow could show progress better"
4663
- ${chalk16.dim("$")} mutagent feedback send "Eval gate was confusing" --category stage:evaluate --title "eval gate unclear"
4664
- ${chalk16.dim("$")} mutagent feedback send "Diagnose loop stalled" --category stage:diagnose --session sess_abc123 --json
4665
- ${chalk16.dim("$")} mutagent feedback send "CLI crashed mid-run" --category cli --attach-transcript --json
4666
- ${chalk16.dim("$")} mutagent feedback send "Repro attached" --category cli --attach-transcript /tmp/session.jsonl
4588
+ // src/commands/hooks/index.ts
4589
+ function createHooksCommand() {
4590
+ const hooks = new Command9("hooks").description("Hook handlers for AI coding assistants").addHelpText("after", `
4591
+ Claude Code Session Telemetry:
4592
+ Sends lightweight session activity to the MutagenT traces API for observability.
4667
4593
 
4668
- ${chalk16.bold("Category:")}
4669
- ${chalk16.bold("cli")} Feedback about the CLI itself (default)
4670
- ${chalk16.bold("helix")} Feedback about Helix
4671
- ${chalk16.bold("stage:<x>")} Feedback about a lifecycle stage: ${VALID_STAGES.join(", ")}
4594
+ Install by adding to .claude/settings.local.json:
4672
4595
 
4673
- ${chalk16.bold("Transcript (--attach-transcript):")}
4674
- Uploads your coding-agent SESSION JSONL as context (raw last-200KB tail) — a
4675
- SEPARATE artifact from the feedback body. Bare flag auto-detects the newest
4676
- session across claude-code, codex, and omp; explicit path overrides.
4596
+ {
4597
+ "hooks": {
4598
+ "SessionStart": [{"matcher": "startup", "hooks": [{"type": "command", "command": "mutagent hooks claude-code session-start"}]}],
4599
+ "Stop": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code session-end"}]}],
4600
+ "PreToolUse": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code pre-tool-use"}]}],
4601
+ "PostToolUse": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code post-tool-use"}]}],
4602
+ "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code user-prompt-submit"}]}],
4603
+ "SubagentStart": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code subagent-start"}]}],
4604
+ "SubagentStop": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code subagent-stop"}]}],
4605
+ "PreCompact": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code pre-compact"}]}],
4606
+ "PostCompact": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code post-compact"}]}],
4607
+ "PostToolUseFailure": [{"hooks": [{"type": "command", "command": "mutagent hooks claude-code post-tool-use-failure"}]}]
4608
+ }
4609
+ }
4677
4610
 
4678
- ${chalk16.yellow("AI Agent (MANDATORY):")}
4679
- ALWAYS use --json: mutagent feedback send "..." --json
4680
- Auto-captured context (harness, CLI version, platform, OS, node version) is included automatically.
4681
- `).action(async (feedbackBody, options) => {
4682
- const isJson = getJsonFlag(feedback);
4683
- const output = new OutputFormatter(isJson ? "json" : "table");
4611
+ Or run: mutagent hooks install
4612
+ `);
4613
+ hooks.command("install").description("Install MutagenT hooks into .claude/settings.local.json (safe merge never overwrites existing hooks)").option("--cwd <dir>", "Target directory (defaults to cwd)", process.cwd()).addHelpText("after", `
4614
+ Reads existing .claude/settings.local.json (if present) and deep-merges
4615
+ MutagenT telemetry hooks into each event array (all 10 events). Skips any
4616
+ hook already present (checked by command string) so running this multiple
4617
+ times is safe.
4618
+ `).action((opts) => {
4619
+ const targetDir = opts.cwd ?? process.cwd();
4620
+ const isJson = Boolean(opts.json);
4621
+ let result;
4684
4622
  try {
4685
- if (feedbackBody.length > MAX_FEEDBACK_LENGTH) {
4686
- throw new MutagentError("INVALID_ARGUMENTS", `Feedback exceeds ${String(MAX_FEEDBACK_LENGTH)} characters (${String(feedbackBody.length)}).`, "Shorten the message.");
4687
- }
4688
- const { category } = parseCategory(options.category);
4689
- const apiKey = getApiKey();
4690
- if (!apiKey) {
4691
- throw new MutagentError("AUTH_REQUIRED", "Authentication required to send feedback.", `Authenticate first: mutagent auth login
4692
- Or set an API key: mutagent config set apiKey <key>`);
4623
+ result = installHooks(targetDir);
4624
+ } catch (err) {
4625
+ if (err instanceof SettingsParseError) {
4626
+ if (isJson) {
4627
+ process.stdout.write(JSON.stringify({
4628
+ success: false,
4629
+ error: err.message,
4630
+ backupPath: err.backupPath,
4631
+ settingsPath: err.settingsPath
4632
+ }) + `
4633
+ `);
4634
+ } else {
4635
+ process.stderr.write(`[mutagent hooks install] ERROR: ${err.message}
4636
+ `);
4637
+ }
4638
+ process.exit(1);
4693
4639
  }
4694
- const transcript = resolveTranscript(options.attachTranscript);
4695
- const payload = buildPayload(feedbackBody, category, options.title, options.session, transcript);
4696
- const config = loadConfig();
4697
- const endpoint = config.endpoint ?? "https://api.mutagent.io";
4698
- const result = await postToServer(payload, endpoint, apiKey, config.defaultWorkspace, config.defaultOrganization);
4699
- if (isJson) {
4700
- output.output({
4701
- success: true,
4702
- id: result.id,
4703
- category,
4704
- title: options.title,
4705
- sessionId: options.session,
4706
- transcriptAttached: transcript !== null,
4707
- _links: {
4708
- send: "mutagent feedback send <feedback> [--title <s>] [--category cli|helix|stage:<x>] [--session <id>] [--attach-transcript [path]]"
4709
- }
4710
- });
4711
- } else {
4712
- const suffix = transcript ? ` + transcript (${String(TAIL_BYTES)}B tail${transcript.truncated ? ", truncated" : ""})` : "";
4713
- output.success(`Feedback sent! (${category})${suffix}`);
4640
+ throw err;
4641
+ }
4642
+ for (const { event, command } of result.migrated) {
4643
+ process.stderr.write(`[mutagent hooks install] ⚠️ Migrated v1 hook: removed '${event} → ${command}' (v2 wires this as 'SessionEnd')
4644
+ `);
4645
+ }
4646
+ if (result.added.length === 0 && result.alreadyPresent.length === 0) {
4647
+ process.stdout.write(JSON.stringify({
4648
+ success: true,
4649
+ settingsPath: result.settingsPath,
4650
+ added: [],
4651
+ alreadyPresent: [],
4652
+ message: "No hooks to install."
4653
+ }) + `
4654
+ `);
4655
+ return;
4656
+ }
4657
+ if (result.userWarning) {
4658
+ if (isJson) {} else {
4659
+ process.stderr.write(`⚠ ${result.userWarning}
4660
+ `);
4714
4661
  }
4715
- } catch (error) {
4716
- handleError(error, isJson);
4717
4662
  }
4663
+ const jsonResponse = {
4664
+ success: true,
4665
+ settingsPath: result.settingsPath,
4666
+ existed: result.existed,
4667
+ added: result.added,
4668
+ alreadyPresent: result.alreadyPresent,
4669
+ message: result.added.length > 0 ? `Installed ${String(result.added.length)} hook(s). ${String(result.alreadyPresent.length)} already present.` : `All hooks already present (${String(result.alreadyPresent.length)}).`
4670
+ };
4671
+ if (result.userWarning && isJson) {
4672
+ jsonResponse.warnings = [result.userWarning];
4673
+ }
4674
+ process.stdout.write(JSON.stringify(jsonResponse) + `
4675
+ `);
4676
+ });
4677
+ const claudeCode = hooks.command("claude-code").description("Claude Code session telemetry");
4678
+ claudeCode.command("session-start").description("Handle session start event").action(async () => {
4679
+ await safeExecute(handleSessionStart);
4680
+ });
4681
+ claudeCode.command("session-end").description("Handle session end event").action(async () => {
4682
+ await safeExecute(handleSessionEnd);
4683
+ });
4684
+ claudeCode.command("pre-tool-use").description("Handle pre-tool-use event").action(async () => {
4685
+ await safeExecute(handlePreToolUse);
4686
+ });
4687
+ claudeCode.command("post-tool-use").description("Handle post-tool-use event").action(async () => {
4688
+ await safeExecute(handlePostToolUse);
4689
+ });
4690
+ claudeCode.command("user-prompt-submit").description("Handle user prompt submit event (creates turn span)").action(async () => {
4691
+ await safeExecute(handleUserPromptSubmit);
4692
+ });
4693
+ claudeCode.command("stop").description("Handle stop event (closes current turn span)").action(async () => {
4694
+ await safeExecute(handleStop);
4695
+ });
4696
+ claudeCode.command("subagent-start").description("Handle subagent start event (creates nested agent span)").action(async () => {
4697
+ await safeExecute(handleSubagentStart);
4698
+ });
4699
+ claudeCode.command("subagent-stop").description("Handle subagent stop event (closes subagent span)").action(async () => {
4700
+ await safeExecute(handleSubagentStop);
4701
+ });
4702
+ claudeCode.command("pre-compact").description("Handle pre-compact event (opens compaction span)").action(async () => {
4703
+ await safeExecute(handlePreCompact);
4704
+ });
4705
+ claudeCode.command("post-compact").description("Handle post-compact event (closes compaction span with summary)").action(async () => {
4706
+ await safeExecute(handlePostCompact);
4707
+ });
4708
+ claudeCode.command("post-tool-use-failure").description("Handle post-tool-use-failure event (closes failed span with error status)").action(async () => {
4709
+ await safeExecute(handlePostToolUseFailure);
4718
4710
  });
4711
+ return hooks;
4719
4712
  }
4720
4713
 
4721
- // src/commands/install/index.ts
4722
- import { Command as Command11 } from "commander";
4723
- import chalk17 from "chalk";
4724
- init_errors();
4725
-
4726
- // src/lib/installer.ts
4714
+ // src/commands/feedback.ts
4715
+ import { Command as Command10 } from "commander";
4716
+ import chalk16 from "chalk";
4717
+ import { type as osType, release as osRelease } from "os";
4718
+ import { readFileSync as readFileSync8 } from "fs";
4719
+ import { join as join11, dirname as dirname2 } from "path";
4720
+ import { fileURLToPath } from "url";
4727
4721
  init_errors();
4728
4722
  init_config();
4729
- import { spawn as spawn2 } from "child_process";
4730
4723
 
4731
- // src/lib/installer-helix.ts
4724
+ // src/lib/transcript.ts
4732
4725
  init_errors();
4733
- init_config();
4734
- import { spawn } from "child_process";
4735
- import { createHash } from "crypto";
4736
- import { homedir as homedir2 } from "os";
4737
- import { join as join11 } from "path";
4726
+ import { homedir as osHomedir } from "os";
4727
+ import { join as join10 } from "path";
4738
4728
  import {
4739
- existsSync as existsSync9,
4740
- mkdirSync as mkdirSync5,
4741
- readFileSync as readFileSync8,
4742
- renameSync as renameSync2,
4743
- rmSync,
4744
- writeFileSync as writeFileSync6
4729
+ existsSync as fsExistsSync,
4730
+ statSync as fsStatSync,
4731
+ readFileSync as readFileSync7,
4732
+ readdirSync,
4733
+ openSync,
4734
+ readSync,
4735
+ closeSync
4745
4736
  } from "fs";
4746
- async function installHelix(opts, deps = {}) {
4747
- const auth = (deps.resolveAuth ?? defaultResolveAuth)();
4748
- const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
4749
- const descriptor = await fetchDescriptor(fetchImpl, auth, opts.version);
4750
- const baseDir = deps.homeDir ?? join11(homedir2(), ".mutagent", "helix");
4751
- const versionDir = join11(baseDir, descriptor.version);
4752
- mkdirSync5(versionDir, { recursive: true });
4753
- const tgzPath = join11(versionDir, `helix-plugin-${descriptor.version}.tgz`);
4754
- const tmpPath = `${tgzPath}.${String(process.pid)}.${String(Date.now())}.part`;
4755
- const download = deps.download ?? defaultDownload;
4756
- await download(descriptor.url, tmpPath);
4757
- const sha256 = deps.sha256 ?? defaultSha256;
4758
- const actual = (await sha256(tmpPath)).toLowerCase();
4759
- const expected = descriptor.sha256.toLowerCase();
4760
- if (actual !== expected) {
4761
- safeRm(tmpPath);
4762
- throw new MutagentError("INTEGRITY_ERROR", `Downloaded helix plugin failed sha256 verification (expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…).`, "The download may be corrupt or tampered with. Retry: mutagent install helix");
4763
- }
4764
- renameSync2(tmpPath, tgzPath);
4765
- const extract = deps.extract ?? defaultExtract;
4766
- await extract(tgzPath, versionDir);
4767
- const locateInitBin = deps.locateInitBin ?? defaultLocateInitBin;
4768
- const binPath = await locateInitBin(versionDir);
4769
- const initArgs = ["init", "--harness", opts.harness];
4770
- if (opts.global)
4771
- initArgs.push("--global");
4772
- const runInit = deps.runInit ?? defaultRunInit;
4773
- const code = await runInit(binPath, initArgs, process.cwd());
4774
- if (code !== 0) {
4775
- throw new MutagentError("INSTALL_FAILED", `helix plugin init exited with code ${String(code)}.`, "Re-run: mutagent install helix — or run the plugin init manually and report the error.");
4776
- }
4777
- await postTelemetry(fetchImpl, auth, {
4778
- pkg: "helix",
4779
- version: descriptor.version,
4780
- harness: opts.harness,
4781
- cliVersion: deps.cliVersion ?? readCliVersion()
4782
- });
4783
- return { version: descriptor.version };
4784
- }
4785
- async function fetchDescriptor(fetchImpl, auth, version) {
4786
- if (!auth.apiKey) {
4787
- throw new MutagentError("AUTH_REQUIRED", "Authentication required to install helix.", "Run: mutagent login");
4788
- }
4789
- const url = `${auth.apiBase}/api/helix/plugin/download?version=${encodeURIComponent(version)}`;
4790
- let res;
4791
- try {
4792
- res = await fetchImpl(url, {
4793
- method: "GET",
4794
- headers: { "x-api-key": auth.apiKey, ...auth.headers }
4795
- });
4796
- } catch {
4797
- throw new MutagentError("SERVER_UNAVAILABLE", "Could not reach the MutagenT plugin broker.", "Check your network connection or verify the endpoint with: mutagent config show");
4798
- }
4799
- if (res.status === 401) {
4800
- throw new MutagentError("AUTH_REQUIRED", "The plugin broker rejected your credentials.", "Re-authenticate: mutagent login");
4801
- }
4802
- if (!res.ok) {
4803
- const detail = await readErrorMessage(res);
4804
- throw new MutagentError("INSTALL_FAILED", `Plugin broker returned ${String(res.status)}.${detail ? ` ${detail}` : ""}`, "Verify the requested version exists, then retry: mutagent install helix");
4805
- }
4806
- const raw = await res.json();
4807
- if (!isBrokerResponse(raw)) {
4808
- throw new MutagentError("INSTALL_FAILED", "Plugin broker returned a malformed response (missing version/sha256/url).", "Retry: mutagent install helix — if it persists, report it.");
4737
+ import chalk15 from "chalk";
4738
+ var TAIL_BYTES = 200000;
4739
+ function defaultReadTail(path, tailBytes) {
4740
+ const { size } = fsStatSync(path);
4741
+ if (size <= tailBytes) {
4742
+ return { content: readFileSync7(path, "utf-8"), truncated: false };
4809
4743
  }
4810
- return raw;
4811
- }
4812
- async function postTelemetry(fetchImpl, auth, event) {
4813
- if (!auth.apiKey)
4814
- return;
4815
- try {
4816
- await fetchImpl(`${auth.apiBase}/api/helix/installs`, {
4817
- method: "POST",
4818
- headers: {
4819
- "Content-Type": "application/json",
4820
- "x-api-key": auth.apiKey,
4821
- ...auth.headers
4822
- },
4823
- body: JSON.stringify(event)
4824
- });
4825
- } catch {}
4826
- }
4827
- function defaultResolveAuth() {
4828
- const apiKey = getApiKey();
4829
- const config = loadConfig();
4830
- const apiBase = config.endpoint ?? "https://api.mutagent.io";
4831
- const headers = {};
4832
- if (config.defaultWorkspace)
4833
- headers["x-workspace-id"] = config.defaultWorkspace;
4834
- if (config.defaultOrganization)
4835
- headers["x-organization-id"] = config.defaultOrganization;
4836
- return { apiBase, apiKey, headers };
4837
- }
4838
- async function defaultDownload(url, destPath) {
4839
- let res;
4744
+ const fd = openSync(path, "r");
4840
4745
  try {
4841
- res = await globalThis.fetch(url);
4842
- } catch {
4843
- throw new MutagentError("SERVER_UNAVAILABLE", "Failed to download the helix plugin from storage.", "Check your network connection and retry: mutagent install helix");
4844
- }
4845
- if (!res.ok) {
4846
- throw new MutagentError("INSTALL_FAILED", `Plugin download failed with status ${String(res.status)}.`, "The signed URL may have expired — retry: mutagent install helix");
4746
+ const buf = Buffer.alloc(tailBytes);
4747
+ readSync(fd, buf, 0, tailBytes, size - tailBytes);
4748
+ return { content: buf.toString("utf-8"), truncated: true };
4749
+ } finally {
4750
+ closeSync(fd);
4847
4751
  }
4848
- const bytes = Buffer.from(await res.arrayBuffer());
4849
- writeFileSync6(destPath, bytes);
4850
- }
4851
- async function defaultSha256(filePath) {
4852
- return Promise.resolve(createHash("sha256").update(readFileSync8(filePath)).digest("hex"));
4853
4752
  }
4854
- function defaultExtract(tgzPath, destDir) {
4855
- return new Promise((resolve, reject) => {
4856
- const child = spawn("tar", ["-xzf", tgzPath, "-C", destDir], {
4857
- stdio: ["ignore", "ignore", "pipe"]
4858
- });
4859
- let stderr = "";
4860
- child.stderr.on("data", (chunk) => {
4861
- stderr += chunk.toString("utf-8");
4862
- });
4863
- child.on("error", (err) => {
4864
- reject(new MutagentError("INSTALL_FAILED", `Failed to extract the helix plugin: ${err.message}`, 'Ensure "tar" is installed and available on your PATH.'));
4865
- });
4866
- child.on("close", (code) => {
4867
- if (code === 0) {
4868
- resolve();
4869
- } else {
4870
- reject(new MutagentError("INSTALL_FAILED", `Extracting the helix plugin failed (tar exit ${String(code ?? 1)}).${stderr ? ` ${stderr.trim().slice(0, 200)}` : ""}`, "The downloaded archive may be corrupt — retry: mutagent install helix"));
4871
- }
4872
- });
4873
- });
4753
+ function defaultScan(dir) {
4754
+ if (!fsExistsSync(dir))
4755
+ return [];
4756
+ const out = [];
4757
+ const walk = (current) => {
4758
+ let entries;
4759
+ try {
4760
+ entries = readdirSync(current, { withFileTypes: true });
4761
+ } catch {
4762
+ return;
4763
+ }
4764
+ for (const entry of entries) {
4765
+ const full = join10(current, entry.name);
4766
+ if (entry.isDirectory())
4767
+ walk(full);
4768
+ else if (entry.isFile() && entry.name.endsWith(".jsonl"))
4769
+ out.push(full);
4770
+ }
4771
+ };
4772
+ walk(dir);
4773
+ return out;
4874
4774
  }
4875
- function defaultLocateInitBin(extractedDir) {
4876
- const pkgDir = existsSync9(join11(extractedDir, "package", "package.json")) ? join11(extractedDir, "package") : extractedDir;
4877
- const pkgJsonPath = join11(pkgDir, "package.json");
4878
- if (!existsSync9(pkgJsonPath)) {
4879
- return Promise.reject(new MutagentError("INSTALL_FAILED", "Could not find package.json in the extracted helix plugin.", "The archive layout is unexpected — retry or report: mutagent install helix"));
4880
- }
4881
- const raw = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
4882
- const binRel = resolveBinField(raw);
4883
- if (!binRel) {
4884
- return Promise.reject(new MutagentError("INSTALL_FAILED", "The helix plugin package.json declares no runnable bin.", "The plugin package is malformed — report: mutagent install helix"));
4885
- }
4886
- const binPath = join11(pkgDir, binRel);
4887
- if (!existsSync9(binPath)) {
4888
- return Promise.reject(new MutagentError("INSTALL_FAILED", `The helix plugin bin was not found at ${binRel}.`, "The plugin package is incomplete — report: mutagent install helix"));
4889
- }
4890
- return Promise.resolve(binPath);
4775
+ function buildSources(env, home) {
4776
+ const ompBase = env.PI_CODING_AGENT_DIR ?? join10(home, ".omp", "agent");
4777
+ return [
4778
+ { harness: "claude-code", dir: join10(home, ".claude", "projects") },
4779
+ { harness: "codex", dir: join10(home, ".codex", "sessions") },
4780
+ { harness: "omp", dir: join10(ompBase, "sessions") }
4781
+ ];
4891
4782
  }
4892
- function defaultRunInit(binPath, args, cwd) {
4893
- return new Promise((resolve, reject) => {
4894
- const child = spawn("node", [binPath, ...args], { cwd, stdio: "inherit" });
4895
- child.on("error", (err) => {
4896
- reject(new MutagentError("INSTALL_FAILED", `Failed to run the helix plugin init: ${err.message}`, 'Ensure "node" is installed and available on your PATH.'));
4897
- });
4898
- child.on("close", (code) => {
4899
- resolve(code ?? 1);
4900
- });
4783
+ function resolveTranscript(attach, deps = {}) {
4784
+ const env = deps.env ?? process.env;
4785
+ const home = (deps.homedir ?? osHomedir)();
4786
+ const existsSync9 = deps.existsSync ?? fsExistsSync;
4787
+ const statSync = deps.statSync ?? fsStatSync;
4788
+ const scan = deps.scan ?? defaultScan;
4789
+ const readTail = deps.readTail ?? defaultReadTail;
4790
+ const warn = deps.warn ?? ((message) => {
4791
+ console.error(message);
4901
4792
  });
4902
- }
4903
- function resolveBinField(pkg) {
4904
- if (!pkg || typeof pkg !== "object")
4905
- return;
4906
- const bin = pkg.bin;
4907
- if (typeof bin === "string")
4908
- return bin;
4909
- if (bin && typeof bin === "object") {
4910
- const entries = bin;
4911
- const preferred = entries["mutagent-helix"];
4912
- if (typeof preferred === "string")
4913
- return preferred;
4914
- for (const value of Object.values(entries)) {
4915
- if (typeof value === "string")
4916
- return value;
4793
+ if (attach === undefined || attach === false)
4794
+ return null;
4795
+ if (typeof attach === "string" && attach.length > 0) {
4796
+ if (!existsSync9(attach)) {
4797
+ throw new MutagentError("INVALID_ARGUMENTS", `Transcript file not found: ${attach}`, `Verify the path exists: ls -la "${attach}"
4798
+ Or omit the path to auto-detect the newest coding-agent session.`);
4917
4799
  }
4800
+ const { content: content2, truncated: truncated2 } = readTail(attach, TAIL_BYTES);
4801
+ return { harness: "unknown", sourcePath: attach, tailBytes: TAIL_BYTES, truncated: truncated2, content: content2 };
4918
4802
  }
4919
- return;
4920
- }
4921
- function isBrokerResponse(value) {
4922
- if (!value || typeof value !== "object")
4923
- return false;
4924
- const v = value;
4925
- return typeof v.version === "string" && typeof v.sha256 === "string" && typeof v.url === "string";
4926
- }
4927
- async function readErrorMessage(res) {
4928
- try {
4929
- const body = await res.json();
4930
- if (body && typeof body === "object") {
4931
- const b = body;
4932
- if (typeof b.message === "string")
4933
- return b.message;
4934
- if (typeof b.error === "string")
4935
- return b.error;
4803
+ let newest = null;
4804
+ for (const src of buildSources(env, home)) {
4805
+ for (const file of scan(src.dir)) {
4806
+ const { mtimeMs } = statSync(file);
4807
+ if (!newest || mtimeMs > newest.mtimeMs) {
4808
+ newest = { harness: src.harness, path: file, mtimeMs };
4809
+ }
4936
4810
  }
4937
- } catch {}
4938
- return;
4811
+ }
4812
+ if (!newest) {
4813
+ warn(chalk15.yellow("⚠ No coding-agent transcript found; sending feedback without one."));
4814
+ return null;
4815
+ }
4816
+ const { content, truncated } = readTail(newest.path, TAIL_BYTES);
4817
+ return {
4818
+ harness: newest.harness,
4819
+ sourcePath: newest.path,
4820
+ tailBytes: TAIL_BYTES,
4821
+ truncated,
4822
+ content
4823
+ };
4939
4824
  }
4940
- function readCliVersion() {
4825
+
4826
+ // src/commands/feedback.ts
4827
+ var MAX_FEEDBACK_LENGTH = 1e4;
4828
+ var VALID_STAGES = ["spec", "build", "evaluate", "diagnose", "optimize"];
4829
+ function getCliVersion() {
4941
4830
  if (process.env.CLI_VERSION)
4942
4831
  return process.env.CLI_VERSION;
4943
- return "unknown";
4944
- }
4945
- function safeRm(path) {
4946
4832
  try {
4947
- rmSync(path, { force: true });
4948
- } catch {}
4833
+ const __dirname2 = dirname2(fileURLToPath(import.meta.url));
4834
+ const pkgPath = join11(__dirname2, "..", "..", "package.json");
4835
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
4836
+ return pkg.version ?? "0.1.1";
4837
+ } catch {
4838
+ return "0.1.1";
4839
+ }
4949
4840
  }
4950
-
4951
- // src/lib/installer.ts
4952
- var VALID_PACKAGES = ["helix", "diagnostics", "evaluator"];
4953
- var VALID_HARNESSES = ["claude-code", "codex", "omp"];
4954
- var VERSION_MATRIX = {
4955
- helix: "latest",
4956
- diagnostics: "latest",
4957
- evaluator: "latest"
4958
- };
4959
- var NPM_PACKAGES = {
4960
- diagnostics: "@mutagent/diagnostics",
4961
- evaluator: "@mutagent/evaluator"
4962
- };
4963
- var defaultRunner = (cmd, args) => new Promise((resolve, reject) => {
4964
- const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
4965
- let stdout = "";
4966
- let stderr = "";
4967
- child.stdout.on("data", (chunk) => {
4968
- stdout += chunk.toString("utf-8");
4969
- });
4970
- child.stderr.on("data", (chunk) => {
4971
- stderr += chunk.toString("utf-8");
4972
- });
4973
- child.on("error", (err) => {
4974
- reject(new MutagentError("INSTALL_FAILED", `Failed to run ${cmd}: ${err.message}`, `Ensure "${cmd}" is installed and available on your PATH.`));
4975
- });
4976
- child.on("close", (code) => {
4977
- resolve({ code: code ?? 1, stdout, stderr });
4978
- });
4979
- });
4980
- function isValidPackage(pkg) {
4981
- return VALID_PACKAGES.includes(pkg);
4841
+ function parseCategory(input) {
4842
+ if (input === "cli" || input === "helix")
4843
+ return { category: input };
4844
+ if (input.startsWith("stage:")) {
4845
+ const stage = input.slice("stage:".length);
4846
+ if (VALID_STAGES.includes(stage)) {
4847
+ return { category: input };
4848
+ }
4849
+ throw new MutagentError("INVALID_ARGUMENTS", `Invalid stage: "${stage}". Must be one of: ${VALID_STAGES.join(", ")}.`, `Use --category stage:<${VALID_STAGES.join("|")}> (e.g. --category stage:evaluate).`);
4850
+ }
4851
+ throw new MutagentError("INVALID_ARGUMENTS", `Invalid category: "${input}". Must be 'cli', 'helix', or 'stage:<${VALID_STAGES.join("|")}>'.`, "Run: mutagent feedback send --help");
4982
4852
  }
4983
- function isValidHarness(harness) {
4984
- return VALID_HARNESSES.includes(harness);
4853
+ function buildAutoContext(harness) {
4854
+ return {
4855
+ harness,
4856
+ cliVersion: getCliVersion(),
4857
+ platform: process.platform,
4858
+ os: `${osType()} ${osRelease()}`,
4859
+ nodeVersion: process.version
4860
+ };
4985
4861
  }
4986
- async function installPackage(pkg, opts, deps = {}) {
4987
- const isAuthed = deps.isAuthed ?? hasCredentials;
4988
- if (!isAuthed()) {
4989
- throw new MutagentError("AUTH_REQUIRED", `Authentication required to install ${pkg}.`, "Run: mutagent login");
4990
- }
4991
- if (!isValidPackage(pkg)) {
4992
- throw new MutagentError("INVALID_ARGUMENTS", `Unknown package "${pkg}". Valid: ${VALID_PACKAGES.join(", ")}`, "Run: mutagent install --help");
4993
- }
4994
- if (!isValidHarness(opts.harness)) {
4995
- throw new MutagentError("INVALID_ARGUMENTS", `Unknown harness "${opts.harness}". Valid: ${VALID_HARNESSES.join(", ")}`, "Run: mutagent install --help");
4862
+ async function postToServer(payload, endpoint, apiKey, workspaceId, organizationId) {
4863
+ const headers = {
4864
+ "Content-Type": "application/json",
4865
+ "x-api-key": apiKey
4866
+ };
4867
+ if (workspaceId)
4868
+ headers["x-workspace-id"] = workspaceId;
4869
+ if (organizationId)
4870
+ headers["x-organization-id"] = organizationId;
4871
+ let response;
4872
+ try {
4873
+ response = await fetch(`${endpoint}/api/feedback`, {
4874
+ method: "POST",
4875
+ headers,
4876
+ body: JSON.stringify(payload)
4877
+ });
4878
+ } catch {
4879
+ throw new MutagentError("SERVER_UNAVAILABLE", "Server unavailable. Try again later.", "Check your network connection or verify the server endpoint with: mutagent config show");
4996
4880
  }
4997
- const version = opts.version ?? VERSION_MATRIX[pkg];
4998
- if (pkg === "helix") {
4999
- const { version: resolved } = await installHelix({ harness: opts.harness, global: opts.global, version }, deps.helix ?? {});
5000
- return {
5001
- package: pkg,
5002
- version: resolved,
5003
- harness: opts.harness,
5004
- global: opts.global
5005
- };
4881
+ if (response.ok) {
4882
+ return await response.json();
5006
4883
  }
5007
- const npmPackage = NPM_PACKAGES[pkg];
5008
- const args = ["install", "-g", `${npmPackage}@${version}`];
5009
- const runner = deps.runner ?? defaultRunner;
5010
- const result = await runner("npm", args);
5011
- if (result.code !== 0) {
5012
- const detail = result.stderr.trim();
5013
- throw new MutagentError("INSTALL_FAILED", `npm failed to install ${npmPackage}@${version} (exit ${String(result.code)}).${detail ? ` ${detail.slice(0, 200)}` : ""}`, "Verify the package and version exist and that you have permission for a global npm install.");
4884
+ let errorMessage = `Server returned ${String(response.status)}`;
4885
+ try {
4886
+ const body = await response.json();
4887
+ errorMessage = body.message ?? body.error ?? errorMessage;
4888
+ } catch {}
4889
+ if (response.status === 401) {
4890
+ throw new MutagentError("AUTH_REQUIRED", errorMessage, "Authenticate first: mutagent auth login");
5014
4891
  }
5015
- return {
5016
- package: pkg,
5017
- version,
5018
- harness: opts.harness,
5019
- global: opts.global,
5020
- command: `npm ${args.join(" ")}`
4892
+ throw new MutagentError("API_ERROR", errorMessage, `Server responded with status ${String(response.status)}. Check your configuration with: mutagent config show`);
4893
+ }
4894
+ function buildPayload(feedback, category, title, session, transcript) {
4895
+ const feedbackV2 = {
4896
+ category,
4897
+ feedback,
4898
+ context: buildAutoContext(transcript?.harness ?? "unknown")
5021
4899
  };
4900
+ if (title)
4901
+ feedbackV2.title = title;
4902
+ if (transcript)
4903
+ feedbackV2.transcript = transcript;
4904
+ const payload = { message: feedback, context: { feedbackV2 } };
4905
+ if (session)
4906
+ payload.sessionId = session;
4907
+ return payload;
4908
+ }
4909
+ function createFeedbackCommand() {
4910
+ const feedback = new Command10("feedback").description("Send product feedback to MutagenT").addHelpText("after", `
4911
+ ${chalk16.bold("Examples:")}
4912
+ ${chalk16.cyan('mutagent feedback send "Optimizer results were great"')}
4913
+ ${chalk16.cyan('mutagent feedback send "Eval gate was confusing" --category stage:evaluate')}
4914
+ ${chalk16.cyan('mutagent feedback send "CLI crashed on export" --category cli --attach-transcript --json')}
4915
+
4916
+ ${chalk16.yellow("AI Agent (MANDATORY):")}
4917
+ ALWAYS use --json: mutagent feedback send "..." --category cli --json
4918
+ Attach the coding-agent session with --attach-transcript (auto-detects the newest session).
4919
+ `).action(() => {
4920
+ feedback.help();
4921
+ });
4922
+ registerFeedbackSend(feedback);
4923
+ return feedback;
4924
+ }
4925
+ function registerFeedbackSend(feedback) {
4926
+ feedback.command("send").description("Send product feedback about the MutagenT platform or CLI").argument("<feedback>", "The feedback body (content), max 10000 characters").option("--title <string>", "Optional 5–8 word summary of the session timeline").option("--category <value>", `Feedback category: 'cli', 'helix', or 'stage:<${VALID_STAGES.join("|")}>'`, "cli").option("--session <id>", "Link this feedback to a session id (maps to server sessionId)").option("--attach-transcript [path]", "Attach the coding-agent session JSONL. Bare = auto-detect newest; or pass an explicit path.").addHelpText("after", `
4927
+ ${chalk16.bold("Arguments & flags:")}
4928
+ ${chalk16.bold("<feedback>")} Feedback body / content (required, ≤10000 chars)
4929
+ ${chalk16.bold("--title <string>")} Optional 5–8 word summary of the session timeline
4930
+ ${chalk16.bold("--category <value>")} ${chalk16.bold("cli")} (default) | ${chalk16.bold("helix")} | ${chalk16.bold("stage:<")}${VALID_STAGES.join("|")}${chalk16.bold(">")}
4931
+ ${chalk16.dim("stage = the lifecycle skill (evaluator/diagnostics live under stage:*)")}
4932
+ ${chalk16.bold("--session <id>")} Link feedback to a session id (server sessionId)
4933
+ ${chalk16.bold("--attach-transcript")} ${chalk16.dim("[path]")} Attach the coding-agent session JSONL (bare = auto-detect newest)
4934
+ ${chalk16.bold("--json")} Structured output (MANDATORY for AI agents)
4935
+
4936
+ ${chalk16.bold("Examples:")}
4937
+ ${chalk16.dim("$")} mutagent feedback send "The setup flow could show progress better"
4938
+ ${chalk16.dim("$")} mutagent feedback send "Eval gate was confusing" --category stage:evaluate --title "eval gate unclear"
4939
+ ${chalk16.dim("$")} mutagent feedback send "Diagnose loop stalled" --category stage:diagnose --session sess_abc123 --json
4940
+ ${chalk16.dim("$")} mutagent feedback send "CLI crashed mid-run" --category cli --attach-transcript --json
4941
+ ${chalk16.dim("$")} mutagent feedback send "Repro attached" --category cli --attach-transcript /tmp/session.jsonl
4942
+
4943
+ ${chalk16.bold("Category:")}
4944
+ ${chalk16.bold("cli")} Feedback about the CLI itself (default)
4945
+ ${chalk16.bold("helix")} Feedback about Helix
4946
+ ${chalk16.bold("stage:<x>")} Feedback about a lifecycle stage: ${VALID_STAGES.join(", ")}
4947
+
4948
+ ${chalk16.bold("Transcript (--attach-transcript):")}
4949
+ Uploads your coding-agent SESSION JSONL as context (raw last-200KB tail) — a
4950
+ SEPARATE artifact from the feedback body. Bare flag auto-detects the newest
4951
+ session across claude-code, codex, and omp; explicit path overrides.
4952
+
4953
+ ${chalk16.yellow("AI Agent (MANDATORY):")}
4954
+ ALWAYS use --json: mutagent feedback send "..." --json
4955
+ Auto-captured context (harness, CLI version, platform, OS, node version) is included automatically.
4956
+ `).action(async (feedbackBody, options) => {
4957
+ const isJson = getJsonFlag(feedback);
4958
+ const output = new OutputFormatter(isJson ? "json" : "table");
4959
+ try {
4960
+ if (feedbackBody.length > MAX_FEEDBACK_LENGTH) {
4961
+ throw new MutagentError("INVALID_ARGUMENTS", `Feedback exceeds ${String(MAX_FEEDBACK_LENGTH)} characters (${String(feedbackBody.length)}).`, "Shorten the message.");
4962
+ }
4963
+ const { category } = parseCategory(options.category);
4964
+ const apiKey = getApiKey();
4965
+ if (!apiKey) {
4966
+ throw new MutagentError("AUTH_REQUIRED", "Authentication required to send feedback.", `Authenticate first: mutagent auth login
4967
+ Or set an API key: mutagent config set apiKey <key>`);
4968
+ }
4969
+ const transcript = resolveTranscript(options.attachTranscript);
4970
+ const payload = buildPayload(feedbackBody, category, options.title, options.session, transcript);
4971
+ const config = loadConfig();
4972
+ const endpoint = config.endpoint ?? "https://api.mutagent.io";
4973
+ const result = await postToServer(payload, endpoint, apiKey, config.defaultWorkspace, config.defaultOrganization);
4974
+ if (isJson) {
4975
+ output.output({
4976
+ success: true,
4977
+ id: result.id,
4978
+ category,
4979
+ title: options.title,
4980
+ sessionId: options.session,
4981
+ transcriptAttached: transcript !== null,
4982
+ _links: {
4983
+ send: "mutagent feedback send <feedback> [--title <s>] [--category cli|helix|stage:<x>] [--session <id>] [--attach-transcript [path]]"
4984
+ }
4985
+ });
4986
+ } else {
4987
+ const suffix = transcript ? ` + transcript (${String(TAIL_BYTES)}B tail${transcript.truncated ? ", truncated" : ""})` : "";
4988
+ output.success(`Feedback sent! (${category})${suffix}`);
4989
+ }
4990
+ } catch (error) {
4991
+ handleError(error, isJson);
4992
+ }
4993
+ });
5022
4994
  }
5023
4995
 
5024
4996
  // src/commands/install/index.ts
4997
+ import { Command as Command11 } from "commander";
4998
+ import chalk17 from "chalk";
4999
+ init_errors();
5025
5000
  function createInstallCommand(deps) {
5026
5001
  const install = new Command11("install").description("Install a MutagenT package (helix, diagnostics, evaluator)").argument("<package>", `Package to install: ${VALID_PACKAGES.join(", ")}`).option("--harness <harness>", `Target harness: ${VALID_HARNESSES.join(", ")}`, "claude-code").option("--global", "Install globally", true).option("--version <version>", "Package version to install (default: latest)").addHelpText("after", `
5027
5002
  ${chalk17.bold("Arguments & flags:")}
@@ -5039,7 +5014,7 @@ ${chalk17.bold("Examples:")}
5039
5014
  ${chalk17.dim("$")} mutagent install diagnostics --harness codex --json
5040
5015
 
5041
5016
  ${chalk17.bold("Packages:")}
5042
- ${chalk17.bold("helix")} ${chalk17.green("(available)")} — the ADL conductor. Downloaded from a private
5017
+ ${chalk17.bold("helix")} ${chalk17.green("(available)")} — the ADLC (Agent Development Life Cycle Orchestrator). Downloaded from a private
5043
5018
  registry via a login-brokered signed URL, sha256-verified, then
5044
5019
  initialized into your project. No static secret ships in the CLI.
5045
5020
  ${chalk17.bold("diagnostics")} Public npm package @mutagent/diagnostics ${chalk17.green("(available)")}
@@ -5097,12 +5072,12 @@ program.name("mutagent").description(`MutagenT CLI - command-line client for the
5097
5072
  });
5098
5073
  program.addHelpText("after", `
5099
5074
  ${chalk18.bold.cyan("WORKFLOWS:")}
5100
- ${chalk18.bold("Setup")} mutagent login → mutagent init → mutagent skills install
5101
- ${chalk18.bold("Lifecycle Tools")} mutagent install <diagnostics|evaluator> ${chalk18.dim("(login-gated; helix pending #1191)")}
5075
+ ${chalk18.bold("Setup")} mutagent login → mutagent init
5076
+ ${chalk18.bold("Lifecycle Tools")} mutagent install <helix|diagnostics|evaluator> ${chalk18.dim("(login-gated)")}
5102
5077
  ${chalk18.bold("Feedback")} mutagent feedback send "<what happened>" --category <cli|helix|stage:<x>> ${chalk18.dim("[--session <id>] [--attach-transcript]")}
5103
5078
 
5104
5079
  ${chalk18.dim("For CLI usage guidance for AI agents, see the Skill at")}
5105
- ${chalk18.cyan(".claude/skills/mutagent-cli/SKILL.md")} ${chalk18.dim("(install via")} ${chalk18.cyan("mutagent skills install")}${chalk18.dim(")")}
5080
+ ${chalk18.cyan(".claude/skills/mutagent-cli/SKILL.md")}
5106
5081
 
5107
5082
  ${chalk18.yellow("Non-Interactive Mode (CI/CD & Coding Agents):")}
5108
5083
  export MUTAGENT_API_KEY=mt_... ${chalk18.dim("or")} --api-key mt_...
@@ -5119,11 +5094,10 @@ ${chalk18.yellow("Command Navigation:")}
5119
5094
  mutagent providers list --json ${chalk18.dim("List configured BYOK providers")}
5120
5095
  mutagent providers list --models ${chalk18.dim("See available models per provider")}
5121
5096
 
5122
- mutagent install diagnostics ${chalk18.dim("Install diagnostics from npm (login-gated)")}
5097
+ mutagent install helix ${chalk18.dim("Install the ADL lifecycle conductor (login-gated)")}
5123
5098
  mutagent install evaluator --version 1.2.3 ${chalk18.dim("Pin a version")}
5124
- mutagent install --help ${chalk18.dim("helix (pending #1191) | diagnostics | evaluator")}
5099
+ mutagent install --help ${chalk18.dim("helix | diagnostics | evaluator")}
5125
5100
 
5126
- mutagent skills install ${chalk18.dim("Install the MutagenT CLI skill for AI agents")}
5127
5101
  mutagent hooks --help ${chalk18.dim("Hook setup for Claude Code session telemetry upload")}
5128
5102
 
5129
5103
  ${chalk18.bold.red("Report Issues:")}
@@ -5143,7 +5117,6 @@ ${chalk18.yellow("Directive System:")}
5143
5117
  ${chalk18.bold("_compat")} Compat metadata: cliVersion, skillVersion, skillMinCliVersion
5144
5118
 
5145
5119
  ${chalk18.yellow("AI Agent Rules (MANDATORY for coding agents):")}
5146
- 0. If .claude/skills/mutagent-cli/SKILL.md does not exist, run mutagent skills install first
5147
5120
  1. Login (two paths):
5148
5121
  - CI / fully automated: export MUTAGENT_API_KEY=mt_... then mutagent login --json
5149
5122
  - Helping a user onboard: mutagent login --browser --json
@@ -5196,15 +5169,7 @@ program.addCommand(createUsageCommand());
5196
5169
  program.addCommand(createHooksCommand());
5197
5170
  program.addCommand(createInstallCommand());
5198
5171
  program.addCommand(createFeedbackCommand());
5199
- var isInteractive = process.stdin.isTTY && !rawArgs.includes("--json") && process.env.CI !== "true";
5200
- var isSkillCommand = rawArgs[0] === "skills" || rawArgs[0] === "hooks";
5201
- if (isInteractive && !isSkillCommand) {
5202
- const skillPath = join12(process.cwd(), ".claude/skills/mutagent-cli/SKILL.md");
5203
- if (!existsSync10(skillPath)) {
5204
- console.log(chalk18.dim("MutagenT SKILL not installed. Install it for AI agent support? Run:"), chalk18.cyan("mutagent skills install"));
5205
- }
5206
- }
5207
5172
  program.parse();
5208
5173
 
5209
- //# debugId=9CA84168C389E42C64756E2164756E21
5174
+ //# debugId=31350670A929169C64756E2164756E21
5210
5175
  //# sourceMappingURL=cli.js.map