@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 +1412 -1447
- package/dist/bin/cli.js.map +11 -10
- package/package.json +1 -1
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
|
|
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
|
|
741
|
-
import { join as
|
|
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/
|
|
941
|
+
// src/lib/rc-config.ts
|
|
994
942
|
init_config();
|
|
995
|
-
|
|
996
|
-
|
|
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/
|
|
1002
|
-
import {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
1039
|
-
|
|
1012
|
+
execSync("bun --version", { stdio: "ignore" });
|
|
1013
|
+
return "bun";
|
|
1040
1014
|
} catch {
|
|
1041
|
-
|
|
1015
|
+
return "npm";
|
|
1042
1016
|
}
|
|
1043
1017
|
}
|
|
1044
|
-
function
|
|
1045
|
-
|
|
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
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
function hasRcConfig(cwd = process.cwd()) {
|
|
1051
|
+
return existsSync3(join3(cwd, ".mutagentrc.json"));
|
|
1106
1052
|
}
|
|
1107
1053
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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/
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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 (
|
|
1178
|
-
|
|
1150
|
+
if (res.status === 401) {
|
|
1151
|
+
throw new MutagentError("AUTH_REQUIRED", "The plugin broker rejected your credentials.", "Re-authenticate: mutagent login");
|
|
1179
1152
|
}
|
|
1180
|
-
|
|
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
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
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
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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/
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
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
|
-
|
|
1428
|
+
console.error("");
|
|
1429
|
+
process.exitCode = 1;
|
|
1398
1430
|
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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 =
|
|
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
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
return
|
|
2758
|
+
});
|
|
2759
|
+
registerAddCommand(providers);
|
|
2760
|
+
registerUpdateCommand(providers);
|
|
2761
|
+
registerDeleteCommand(providers);
|
|
2762
|
+
return providers;
|
|
2436
2763
|
}
|
|
2437
2764
|
|
|
2438
2765
|
// src/commands/init.ts
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
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(
|
|
2635
|
-
output.info(
|
|
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:
|
|
2683
|
-
|
|
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
|
|
2734
|
-
import { dirname, join as
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3514
|
-
import { join as
|
|
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
|
|
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(
|
|
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
|
-
|
|
3540
|
-
|
|
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
|
-
|
|
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/
|
|
4440
|
-
import {
|
|
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
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
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
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
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
|
|
4509
|
-
const
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
const
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
if (
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
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
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
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
|
-
|
|
4607
|
-
|
|
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
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
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
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
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
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
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
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
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/
|
|
4722
|
-
import { Command as
|
|
4723
|
-
import
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
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/
|
|
4724
|
+
// src/lib/transcript.ts
|
|
4732
4725
|
init_errors();
|
|
4733
|
-
|
|
4734
|
-
import {
|
|
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
|
|
4740
|
-
|
|
4741
|
-
readFileSync as
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
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
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
const
|
|
4750
|
-
|
|
4751
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
}
|
|
4845
|
-
|
|
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
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
let
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
}
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
if (
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
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
|
|
4876
|
-
const
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
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
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
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
|
-
|
|
4904
|
-
if (
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
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
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
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
|
-
}
|
|
4938
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4948
|
-
|
|
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
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
};
|
|
4959
|
-
|
|
4960
|
-
|
|
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
|
|
4984
|
-
return
|
|
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
|
|
4987
|
-
const
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
}
|
|
4991
|
-
if (
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
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
|
-
|
|
4998
|
-
|
|
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
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
throw new MutagentError("
|
|
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
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
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
|
|
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
|
|
5101
|
-
${chalk18.bold("Lifecycle Tools")} mutagent install <diagnostics|evaluator> ${chalk18.dim("(login-gated
|
|
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")}
|
|
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
|
|
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
|
|
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=
|
|
5174
|
+
//# debugId=31350670A929169C64756E2164756E21
|
|
5210
5175
|
//# sourceMappingURL=cli.js.map
|