@aion0/forge 0.10.47 → 0.10.49

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/cli/mw.mjs CHANGED
@@ -764,13 +764,13 @@ function migrateDataDir() {
764
764
  if (!existsSync3(oldSettings) || existsSync3(newSettings)) return;
765
765
  console.log("[forge] Migrating data from ~/.forge/ to ~/.forge/data/...");
766
766
  if (!existsSync3(dataDir2)) mkdirSync2(dataDir2, { recursive: true });
767
- for (const file of MIGRATE_FILES) {
768
- const src = join3(configDir, file);
769
- const dest = join3(dataDir2, file);
767
+ for (const file2 of MIGRATE_FILES) {
768
+ const src = join3(configDir, file2);
769
+ const dest = join3(dataDir2, file2);
770
770
  if (existsSync3(src) && !existsSync3(dest)) {
771
771
  try {
772
772
  copyFileSync(src, dest);
773
- console.log(` ${file}`);
773
+ console.log(` ${file2}`);
774
774
  } catch {
775
775
  }
776
776
  }
@@ -823,16 +823,414 @@ var init_dirs = __esm({
823
823
  }
824
824
  });
825
825
 
826
+ // lib/api-keys.ts
827
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
828
+ import { join as join4 } from "node:path";
829
+ import { randomBytes, createHash, timingSafeEqual } from "node:crypto";
830
+ function file() {
831
+ return join4(getDataDir(), "api-keys.json");
832
+ }
833
+ function load() {
834
+ try {
835
+ const fp = file();
836
+ if (!existsSync4(fp)) {
837
+ _cache = null;
838
+ return [];
839
+ }
840
+ const mtimeMs = statSync2(fp).mtimeMs;
841
+ if (_cache && _cache.mtimeMs === mtimeMs) return _cache.keys;
842
+ const parsed = JSON.parse(readFileSync2(fp, "utf-8"));
843
+ const keys = Array.isArray(parsed) ? parsed : [];
844
+ _cache = { mtimeMs, keys };
845
+ return keys;
846
+ } catch {
847
+ return [];
848
+ }
849
+ }
850
+ function persist(keys) {
851
+ const dir = getDataDir();
852
+ mkdirSync3(dir, { recursive: true });
853
+ const tmp = join4(dir, `.api-keys.${randomBytes(6).toString("hex")}.tmp`);
854
+ try {
855
+ writeFileSync2(tmp, JSON.stringify(keys, null, 2), { mode: 384 });
856
+ renameSync2(tmp, file());
857
+ _cache = null;
858
+ } catch (e) {
859
+ try {
860
+ rmSync2(tmp, { force: true });
861
+ } catch {
862
+ }
863
+ throw e;
864
+ }
865
+ }
866
+ function sha256(s) {
867
+ return createHash("sha256").update(s).digest("hex");
868
+ }
869
+ function createApiKey(name) {
870
+ const key = `${KEY_PREFIX}${randomBytes(24).toString("base64url")}`;
871
+ const record = {
872
+ id: randomBytes(6).toString("hex"),
873
+ name: name.trim() || "agent key",
874
+ prefix: `${key.slice(0, KEY_PREFIX.length + 6)}\u2026`,
875
+ hash: sha256(key),
876
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
877
+ lastUsedAt: null
878
+ };
879
+ const keys = load();
880
+ keys.push(record);
881
+ persist(keys);
882
+ return { key, record: toPublic(record) };
883
+ }
884
+ function listApiKeys() {
885
+ return load().map(toPublic);
886
+ }
887
+ function revokeApiKey(id) {
888
+ const keys = load();
889
+ const idx = keys.findIndex((k) => k.id === id);
890
+ if (idx < 0) return false;
891
+ keys.splice(idx, 1);
892
+ persist(keys);
893
+ return true;
894
+ }
895
+ var KEY_PREFIX, _cache, toPublic;
896
+ var init_api_keys = __esm({
897
+ "lib/api-keys.ts"() {
898
+ "use strict";
899
+ init_dirs();
900
+ KEY_PREFIX = "forge_sk_";
901
+ _cache = null;
902
+ toPublic = (r) => ({
903
+ id: r.id,
904
+ name: r.name,
905
+ prefix: r.prefix,
906
+ createdAt: r.createdAt,
907
+ lastUsedAt: r.lastUsedAt
908
+ });
909
+ }
910
+ });
911
+
912
+ // cli/mcp-install.ts
913
+ var mcp_install_exports = {};
914
+ __export(mcp_install_exports, {
915
+ mcpInstallCommand: () => mcpInstallCommand
916
+ });
917
+ import { spawnSync as spawnSync2 } from "node:child_process";
918
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
919
+ import { homedir as homedir3 } from "node:os";
920
+ import { join as join5 } from "node:path";
921
+ function savedKey() {
922
+ const env = (process.env.FORGE_API_KEY || process.env.FORGE_API_TOKEN || "").trim();
923
+ if (env) return env;
924
+ try {
925
+ if (existsSync5(keyFile())) return readFileSync3(keyFile(), "utf-8").trim();
926
+ } catch {
927
+ }
928
+ return "";
929
+ }
930
+ function ensureKey(remote) {
931
+ const existing = savedKey();
932
+ if (existing) return existing;
933
+ if (remote) throw new Error("remote install needs a key \u2014 set FORGE_API_KEY (mint one in the remote's Settings \u2192 API Key)");
934
+ const { key } = createApiKey("claude-code");
935
+ mkdirSync4(join5(homedir3(), ".forge"), { recursive: true });
936
+ writeFileSync3(keyFile(), key, { mode: 384 });
937
+ console.error(`[forge mcp install] minted a local API key \u2192 ${keyFile()}`);
938
+ return key;
939
+ }
940
+ async function mcpInstallCommand(args2) {
941
+ const name = opt(args2, "--name") || "forge";
942
+ const scope = opt(args2, "--scope");
943
+ const remote = opt(args2, "--remote");
944
+ const useHttp = args2.includes("--http");
945
+ const printOnly = args2.includes("--print");
946
+ const port = opt(args2, "--port");
947
+ const base = (remote || process.env.MW_URL || `http://localhost:${port || "8403"}`).replace(/\/$/, "");
948
+ if (!printOnly && !remote && !savedKey() && !process.env.FORGE_DATA_DIR && (process.env.MW_URL || port && port !== "8403")) {
949
+ throw new Error("targeting a non-default instance \u2014 set FORGE_DATA_DIR to its data dir so the key is minted into the store that server reads");
950
+ }
951
+ const key = printOnly ? savedKey() : ensureKey(remote);
952
+ const add = ["mcp", "add"];
953
+ if (scope) add.push("--scope", scope);
954
+ if (useHttp) {
955
+ add.push("--transport", "http", name, `${base}/api/mcp`);
956
+ if (key) add.push("--header", `Authorization: Bearer ${key}`);
957
+ else if (printOnly) add.push("--header", "Authorization: Bearer forge_sk_YOUR_KEY");
958
+ } else {
959
+ if (remote || process.env.MW_URL) add.push("--env", `MW_URL=${base}`);
960
+ add.push(name, "--", "forge", "mcp");
961
+ }
962
+ const pretty = `claude ${add.map((a) => a.includes(" ") ? JSON.stringify(a) : a).join(" ")}`;
963
+ if (printOnly) {
964
+ console.log(pretty);
965
+ return;
966
+ }
967
+ const r = spawnSync2("claude", add, { stdio: "inherit" });
968
+ if (r.error && r.error.code === "ENOENT") {
969
+ console.error("[forge mcp install] the `claude` CLI is not on PATH. Run this yourself:\n");
970
+ console.log(` ${pretty}
971
+ `);
972
+ return;
973
+ }
974
+ if (r.status && r.status !== 0) process.exit(r.status);
975
+ console.error(`
976
+ [forge mcp install] added '${name}'. Run /mcp in a Claude Code session to confirm.`);
977
+ }
978
+ var opt, keyFile;
979
+ var init_mcp_install = __esm({
980
+ "cli/mcp-install.ts"() {
981
+ "use strict";
982
+ init_api_keys();
983
+ opt = (args2, name) => {
984
+ const i = args2.indexOf(name);
985
+ return i >= 0 ? args2[i + 1] : void 0;
986
+ };
987
+ keyFile = () => join5(homedir3(), ".forge", "mcp-key");
988
+ }
989
+ });
990
+
991
+ // cli/mcp-proxy.ts
992
+ var mcp_proxy_exports = {};
993
+ __export(mcp_proxy_exports, {
994
+ mcpProxyCommand: () => mcpProxyCommand
995
+ });
996
+ import { createInterface } from "node:readline";
997
+ import { readFileSync as readFileSync4, existsSync as existsSync6 } from "node:fs";
998
+ import { homedir as homedir4 } from "node:os";
999
+ import { join as join6 } from "node:path";
1000
+ function resolveCredential() {
1001
+ const env = process.env.FORGE_API_KEY || process.env.FORGE_API_TOKEN;
1002
+ if (env) return env.trim();
1003
+ try {
1004
+ const f = join6(homedir4(), ".forge", "mcp-key");
1005
+ if (existsSync6(f)) return readFileSync4(f, "utf-8").trim();
1006
+ } catch {
1007
+ }
1008
+ return "";
1009
+ }
1010
+ async function mcpProxyCommand(_args, deps = {}) {
1011
+ const input = deps.input ?? process.stdin;
1012
+ const out = deps.writeOut ?? ((s) => {
1013
+ process.stdout.write(s);
1014
+ });
1015
+ const logErr = deps.logErr ?? ((s) => {
1016
+ process.stderr.write(s);
1017
+ });
1018
+ const doFetch = deps.fetchImpl ?? fetch;
1019
+ const base = (process.env.MW_URL || "http://localhost:8403").replace(/\/$/, "");
1020
+ const endpoint = `${base}/api/mcp`;
1021
+ const credential = resolveCredential();
1022
+ const applyAuth = (headers) => {
1023
+ if (!credential) return;
1024
+ if (credential.startsWith("forge_sk_")) headers["authorization"] = `Bearer ${credential}`;
1025
+ else headers["x-forge-token"] = credential;
1026
+ };
1027
+ const err = (m) => logErr(`[forge mcp] ${m}
1028
+ `);
1029
+ err(`proxy \u2192 ${endpoint}`);
1030
+ let sessionId = "";
1031
+ let protocolVersion = "";
1032
+ let initFrame = "";
1033
+ const writeFrame = (text) => out(text.endsWith("\n") ? text : text + "\n");
1034
+ const emit = (obj) => out(JSON.stringify(obj) + "\n");
1035
+ const rpcError = (id, message, code = -32e3) => emit({ jsonrpc: "2.0", id, error: { code, message } });
1036
+ const post = (frame, useSession) => {
1037
+ const headers = {
1038
+ "content-type": "application/json",
1039
+ accept: "application/json, text/event-stream"
1040
+ };
1041
+ applyAuth(headers);
1042
+ if (useSession && sessionId) headers["mcp-session-id"] = sessionId;
1043
+ if (protocolVersion) headers["mcp-protocol-version"] = protocolVersion;
1044
+ return doFetch(endpoint, { method: "POST", headers, body: frame });
1045
+ };
1046
+ const adoptSession = (res) => {
1047
+ const sid = res.headers.get("mcp-session-id");
1048
+ if (sid) sessionId = sid;
1049
+ };
1050
+ const learnProtocol = (text) => {
1051
+ try {
1052
+ const v = JSON.parse(text)?.result?.protocolVersion;
1053
+ if (typeof v === "string") protocolVersion = v;
1054
+ } catch {
1055
+ }
1056
+ };
1057
+ const errorMessage = (status, text) => {
1058
+ try {
1059
+ const m = JSON.parse(text)?.error?.message;
1060
+ if (typeof m === "string") return m;
1061
+ } catch {
1062
+ }
1063
+ return `Forge server returned HTTP ${status}`;
1064
+ };
1065
+ const sessionDead = (status, text) => status === 404 || status === 400 && /not initialized|session not found/i.test(text);
1066
+ async function recover(frame, id, hasId) {
1067
+ err("session expired \u2014 re-initializing");
1068
+ sessionId = "";
1069
+ protocolVersion = "";
1070
+ let initRes;
1071
+ try {
1072
+ initRes = await post(initFrame, false);
1073
+ } catch (e) {
1074
+ if (hasId) rpcError(id, `forge mcp: re-init failed: ${e.message}`);
1075
+ return;
1076
+ }
1077
+ adoptSession(initRes);
1078
+ if (!initRes.ok) {
1079
+ if (hasId) rpcError(id, "forge mcp: re-initialization failed");
1080
+ return;
1081
+ }
1082
+ learnProtocol(await initRes.text());
1083
+ try {
1084
+ await post('{"jsonrpc":"2.0","method":"notifications/initialized"}', true);
1085
+ } catch {
1086
+ }
1087
+ let retry;
1088
+ try {
1089
+ retry = await post(frame, true);
1090
+ } catch (e) {
1091
+ if (hasId) rpcError(id, `forge mcp: ${e.message}`);
1092
+ return;
1093
+ }
1094
+ adoptSession(retry);
1095
+ if (retry.status === 202) return;
1096
+ const rtext = await retry.text();
1097
+ if (retry.ok) {
1098
+ if (rtext) writeFrame(rtext);
1099
+ return;
1100
+ }
1101
+ if (hasId) rpcError(id, errorMessage(retry.status, rtext));
1102
+ }
1103
+ const rl = createInterface({ input, crlfDelay: Infinity });
1104
+ for await (const line of rl) {
1105
+ const frame = line.trim();
1106
+ if (!frame) continue;
1107
+ let msg;
1108
+ try {
1109
+ msg = JSON.parse(frame);
1110
+ } catch {
1111
+ err("dropping non-JSON stdin line");
1112
+ continue;
1113
+ }
1114
+ const hasId = !!msg && "id" in msg && msg.id !== null && msg.id !== void 0;
1115
+ const id = hasId ? msg.id : void 0;
1116
+ if (msg?.method === "initialize") initFrame = frame;
1117
+ let res;
1118
+ try {
1119
+ res = await post(frame, true);
1120
+ } catch (e) {
1121
+ err(`request failed: ${e.message}`);
1122
+ if (hasId) rpcError(id, `forge mcp: ${e.message}`);
1123
+ continue;
1124
+ }
1125
+ adoptSession(res);
1126
+ if (res.status === 202) continue;
1127
+ const text = await res.text();
1128
+ if (res.ok) {
1129
+ learnProtocol(text);
1130
+ if (text) writeFrame(text);
1131
+ continue;
1132
+ }
1133
+ if (sessionDead(res.status, text) && initFrame && msg?.method !== "initialize") {
1134
+ await recover(frame, id, hasId);
1135
+ continue;
1136
+ }
1137
+ if (hasId) rpcError(id, errorMessage(res.status, text));
1138
+ else err(`server ${res.status} on notification (dropped)`);
1139
+ }
1140
+ if (sessionId) {
1141
+ try {
1142
+ const headers = { "mcp-session-id": sessionId };
1143
+ applyAuth(headers);
1144
+ if (protocolVersion) headers["mcp-protocol-version"] = protocolVersion;
1145
+ await doFetch(endpoint, { method: "DELETE", headers });
1146
+ } catch {
1147
+ }
1148
+ }
1149
+ }
1150
+ var init_mcp_proxy = __esm({
1151
+ "cli/mcp-proxy.ts"() {
1152
+ "use strict";
1153
+ }
1154
+ });
1155
+
1156
+ // cli/key.ts
1157
+ var key_exports = {};
1158
+ __export(key_exports, {
1159
+ keyCommand: () => keyCommand
1160
+ });
1161
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5 } from "node:fs";
1162
+ import { homedir as homedir5 } from "node:os";
1163
+ import { join as join7 } from "node:path";
1164
+ async function keyCommand(args2) {
1165
+ const sub = args2[0];
1166
+ const positional = args2.slice(1).filter((a) => !a.startsWith("--"));
1167
+ if (sub === "list") {
1168
+ const keys = listApiKeys();
1169
+ if (!keys.length) {
1170
+ console.log("No API keys. Create one: forge key create <name>");
1171
+ return;
1172
+ }
1173
+ for (const k of keys) {
1174
+ const used = k.lastUsedAt ? k.lastUsedAt.slice(0, 10) : "never";
1175
+ console.log(` ${k.id} ${String(k.prefix).padEnd(20)} ${k.name} (created ${k.createdAt?.slice(0, 10)}, last used ${used})`);
1176
+ }
1177
+ return;
1178
+ }
1179
+ if (sub === "create") {
1180
+ const name = positional[0] || "agent key";
1181
+ const { key } = createApiKey(name);
1182
+ console.log(`
1183
+ \u2713 API key created (${name}):
1184
+
1185
+ ${key}
1186
+ `);
1187
+ console.log(" Copy it now \u2014 it will not be shown again.");
1188
+ if (args2.includes("--save")) {
1189
+ const dir = join7(homedir5(), ".forge");
1190
+ mkdirSync5(dir, { recursive: true });
1191
+ const f = join7(dir, "mcp-key");
1192
+ writeFileSync4(f, key, { mode: 384 });
1193
+ console.log(` Saved to ${f} \u2014 forge mcp will read it automatically.`);
1194
+ } else {
1195
+ console.log(" Use it: export FORGE_API_KEY=<key> (or rerun with --save)");
1196
+ }
1197
+ return;
1198
+ }
1199
+ if (sub === "revoke") {
1200
+ const id = positional[0];
1201
+ if (!id) {
1202
+ console.log("Usage: forge key revoke <id>");
1203
+ process.exit(1);
1204
+ }
1205
+ console.log(revokeApiKey(id) ? `\u2713 Revoked key ${id}` : `Key ${id} not found`);
1206
+ return;
1207
+ }
1208
+ console.log(`forge key \u2014 manage API keys (service/agent credentials, incl. MCP)
1209
+
1210
+ forge key create [name] [--save] Mint a key (--save writes it to ~/.forge/mcp-key)
1211
+ forge key list List keys
1212
+ forge key revoke <id> Revoke a key
1213
+
1214
+ No admin password \u2014 operates on the local key store directly.
1215
+ Set FORGE_DATA_DIR to target a non-default instance.`);
1216
+ }
1217
+ var init_key = __esm({
1218
+ "cli/key.ts"() {
1219
+ "use strict";
1220
+ init_api_keys();
1221
+ }
1222
+ });
1223
+
826
1224
  // cli/mw.ts
827
1225
  var _cliPort = process.argv.find((a, i) => i > 0 && process.argv[i - 1] === "--port");
828
1226
  var BASE2 = process.env.MW_URL || `http://localhost:${_cliPort || "3000"}`;
829
1227
  var [, , cmd, ...args] = process.argv;
830
1228
  async function checkForUpdate() {
831
1229
  try {
832
- const { readFileSync: readFileSync2 } = await import("node:fs");
833
- const { join: join4, dirname } = await import("node:path");
1230
+ const { readFileSync: readFileSync5 } = await import("node:fs");
1231
+ const { join: join8, dirname } = await import("node:path");
834
1232
  const { fileURLToPath } = await import("node:url");
835
- const pkg = JSON.parse(readFileSync2(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
1233
+ const pkg = JSON.parse(readFileSync5(join8(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
836
1234
  const current = pkg.version;
837
1235
  const controller = new AbortController();
838
1236
  const timeout = setTimeout(() => controller.abort(), 3e3);
@@ -866,11 +1264,11 @@ async function api3(path, opts) {
866
1264
  }
867
1265
  async function main() {
868
1266
  if (cmd === "--version" || cmd === "-v") {
869
- const { readFileSync: readFileSync2 } = await import("node:fs");
870
- const { join: join4, dirname } = await import("node:path");
1267
+ const { readFileSync: readFileSync5 } = await import("node:fs");
1268
+ const { join: join8, dirname } = await import("node:path");
871
1269
  const { fileURLToPath } = await import("node:url");
872
1270
  try {
873
- const pkg = JSON.parse(readFileSync2(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
1271
+ const pkg = JSON.parse(readFileSync5(join8(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
874
1272
  console.log(`@aion0/forge v${pkg.version}`);
875
1273
  } catch {
876
1274
  console.log("forge (version unknown)");
@@ -878,20 +1276,20 @@ async function main() {
878
1276
  process.exit(0);
879
1277
  }
880
1278
  if (cmd === "--reset-password") {
881
- const { spawnSync: spawnSync2 } = await import("node:child_process");
882
- const { join: join4, dirname } = await import("node:path");
1279
+ const { spawnSync: spawnSync3 } = await import("node:child_process");
1280
+ const { join: join8, dirname } = await import("node:path");
883
1281
  const { fileURLToPath } = await import("node:url");
884
- const serverScript = join4(dirname(fileURLToPath(import.meta.url)), "..", "bin", "forge-server.mjs");
1282
+ const serverScript = join8(dirname(fileURLToPath(import.meta.url)), "..", "bin", "forge-server.mjs");
885
1283
  const passthru = process.argv.slice(3);
886
- spawnSync2("node", [serverScript, "--reset-password", ...passthru], { stdio: "inherit" });
1284
+ spawnSync3("node", [serverScript, "--reset-password", ...passthru], { stdio: "inherit" });
887
1285
  process.exit(0);
888
1286
  }
889
1287
  if (cmd === "--add-enterprise-key") {
890
- const { spawnSync: spawnSync2 } = await import("node:child_process");
891
- const { join: join4, dirname } = await import("node:path");
1288
+ const { spawnSync: spawnSync3 } = await import("node:child_process");
1289
+ const { join: join8, dirname } = await import("node:path");
892
1290
  const { fileURLToPath } = await import("node:url");
893
- const serverScript = join4(dirname(fileURLToPath(import.meta.url)), "..", "bin", "forge-server.mjs");
894
- const r = spawnSync2("node", [serverScript, "--add-enterprise-key", ...args], { stdio: "inherit" });
1291
+ const serverScript = join8(dirname(fileURLToPath(import.meta.url)), "..", "bin", "forge-server.mjs");
1292
+ const r = spawnSync3("node", [serverScript, "--add-enterprise-key", ...args], { stdio: "inherit" });
895
1293
  process.exit(r.status ?? 1);
896
1294
  }
897
1295
  switch (cmd) {
@@ -913,6 +1311,21 @@ async function main() {
913
1311
  await worktreeCommand2(args);
914
1312
  break;
915
1313
  }
1314
+ case "mcp": {
1315
+ if (args[0] === "install") {
1316
+ const { mcpInstallCommand: mcpInstallCommand2 } = await Promise.resolve().then(() => (init_mcp_install(), mcp_install_exports));
1317
+ await mcpInstallCommand2(args.slice(1));
1318
+ break;
1319
+ }
1320
+ const { mcpProxyCommand: mcpProxyCommand2 } = await Promise.resolve().then(() => (init_mcp_proxy(), mcp_proxy_exports));
1321
+ await mcpProxyCommand2(args);
1322
+ break;
1323
+ }
1324
+ case "key": {
1325
+ const { keyCommand: keyCommand2 } = await Promise.resolve().then(() => (init_key(), key_exports));
1326
+ await keyCommand2(args);
1327
+ break;
1328
+ }
916
1329
  case "task":
917
1330
  case "t": {
918
1331
  const newSession = args.includes("--new");
@@ -1193,14 +1606,14 @@ Resume in CLI:`);
1193
1606
  }
1194
1607
  case "tunnel_code":
1195
1608
  case "tcode": {
1196
- const { readFileSync: readFileSync2, existsSync: existsSync4 } = await import("node:fs");
1197
- const { join: join4 } = await import("node:path");
1609
+ const { readFileSync: readFileSync5, existsSync: existsSync7 } = await import("node:fs");
1610
+ const { join: join8 } = await import("node:path");
1198
1611
  const { getDataDir: _gdd } = await Promise.resolve().then(() => (init_dirs(), dirs_exports));
1199
1612
  const dataDir2 = _gdd();
1200
- const codeFile = join4(dataDir2, "session-code.json");
1613
+ const codeFile = join8(dataDir2, "session-code.json");
1201
1614
  try {
1202
- if (existsSync4(codeFile)) {
1203
- const data = JSON.parse(readFileSync2(codeFile, "utf-8"));
1615
+ if (existsSync7(codeFile)) {
1616
+ const data = JSON.parse(readFileSync5(codeFile, "utf-8"));
1204
1617
  if (data.code) {
1205
1618
  console.log(`Session code: ${data.code}`);
1206
1619
  } else {
@@ -1212,7 +1625,7 @@ Resume in CLI:`);
1212
1625
  } catch {
1213
1626
  }
1214
1627
  try {
1215
- const tunnelState = JSON.parse(readFileSync2(join4(dataDir2, "tunnel-state.json"), "utf-8"));
1628
+ const tunnelState = JSON.parse(readFileSync5(join8(dataDir2, "tunnel-state.json"), "utf-8"));
1216
1629
  if (tunnelState.url) console.log(`Tunnel URL: ${tunnelState.url}`);
1217
1630
  } catch {
1218
1631
  }
@@ -1259,9 +1672,9 @@ ${projects.length} projects`);
1259
1672
  }
1260
1673
  case "server": {
1261
1674
  const { execSync: execSync2 } = await import("node:child_process");
1262
- const { join: join4, dirname } = await import("node:path");
1675
+ const { join: join8, dirname } = await import("node:path");
1263
1676
  const { fileURLToPath } = await import("node:url");
1264
- const serverScript = join4(dirname(fileURLToPath(import.meta.url)), "..", "bin", "forge-server.mjs");
1677
+ const serverScript = join8(dirname(fileURLToPath(import.meta.url)), "..", "bin", "forge-server.mjs");
1265
1678
  const sub = args[0] || "start";
1266
1679
  const serverArgs = args.slice(1);
1267
1680
  const flagMap = {
@@ -1334,31 +1747,31 @@ ${task.gitDiff.slice(0, 2e3)}`);
1334
1747
  case "upgrade": {
1335
1748
  const { execSync: execSync2 } = await import("node:child_process");
1336
1749
  const { lstatSync } = await import("node:fs");
1337
- const { join: join4, dirname } = await import("node:path");
1750
+ const { join: join8, dirname } = await import("node:path");
1338
1751
  const { fileURLToPath } = await import("node:url");
1339
1752
  const cliDir = dirname(fileURLToPath(import.meta.url));
1340
1753
  let isLinked = false;
1341
1754
  try {
1342
- isLinked = lstatSync(join4(cliDir, "..")).isSymbolicLink();
1755
+ isLinked = lstatSync(join8(cliDir, "..")).isSymbolicLink();
1343
1756
  } catch {
1344
1757
  }
1345
1758
  if (isLinked) {
1346
1759
  console.log("[forge] Installed via npm link (local source)");
1347
1760
  console.log("[forge] Pull latest and rebuild:");
1348
- console.log(" cd " + join4(cliDir, ".."));
1761
+ console.log(" cd " + join8(cliDir, ".."));
1349
1762
  console.log(" git pull && pnpm install && pnpm build");
1350
1763
  } else {
1351
1764
  console.log("[forge] Upgrading from npm...");
1352
1765
  try {
1353
- const { homedir: homedir3 } = await import("node:os");
1766
+ const { homedir: homedir6 } = await import("node:os");
1354
1767
  execSync2("npm install -g @aion0/forge@latest --prefer-online", {
1355
1768
  stdio: "inherit",
1356
- cwd: homedir3()
1769
+ cwd: homedir6()
1357
1770
  });
1358
1771
  try {
1359
- const { readFileSync: readFileSync2 } = await import("node:fs");
1360
- const globalRoot = execSync2("npm root -g", { encoding: "utf-8", cwd: homedir3() }).trim();
1361
- const pkg = JSON.parse(readFileSync2(join4(globalRoot, "@aion0", "forge", "package.json"), "utf-8"));
1772
+ const { readFileSync: readFileSync5 } = await import("node:fs");
1773
+ const globalRoot = execSync2("npm root -g", { encoding: "utf-8", cwd: homedir6() }).trim();
1774
+ const pkg = JSON.parse(readFileSync5(join8(globalRoot, "@aion0", "forge", "package.json"), "utf-8"));
1362
1775
  console.log(`[forge] Upgraded to v${pkg.version}. Run: forge server restart`);
1363
1776
  } catch {
1364
1777
  console.log("[forge] Upgraded. Run: forge server restart");
@@ -1409,6 +1822,11 @@ Usage:
1409
1822
  forge cancel <id> Cancel a task
1410
1823
  forge retry <id> Retry a failed task
1411
1824
 
1825
+ forge mcp install Add the Forge management MCP to Claude Code
1826
+ forge key create [name] Create an API key (for agents/MCP)
1827
+ forge key list List API keys
1828
+ forge key revoke <id> Revoke an API key
1829
+
1412
1830
  forge run <flow> Run a workflow
1413
1831
  forge flows List workflows
1414
1832
  forge projects List projects
@@ -1428,7 +1846,7 @@ Options for 'forge server start':
1428
1846
  Shortcuts: c=chat, j=jobs, wt=worktree, t=task, ls=tasks, w=watch, s=status, l=log, f=flows, p=projects, pw=password`);
1429
1847
  }
1430
1848
  }
1431
- var skipUpdateCheck = ["upgrade", "uninstall", "--version", "-v", "--reset-password", "--add-enterprise-key"];
1849
+ var skipUpdateCheck = ["upgrade", "uninstall", "--version", "-v", "--reset-password", "--add-enterprise-key", "mcp", "key"];
1432
1850
  main().then(() => {
1433
1851
  if (!skipUpdateCheck.includes(cmd)) return checkForUpdate();
1434
1852
  }).catch((err) => {
package/cli/mw.ts CHANGED
@@ -120,6 +120,26 @@ async function main() {
120
120
  break;
121
121
  }
122
122
 
123
+ case 'mcp': {
124
+ // `forge mcp install` adds the server to Claude Code; bare `forge mcp` is
125
+ // the stdio<->HTTP proxy the client spawns (transport-only, see mcp/README.md).
126
+ if (args[0] === 'install') {
127
+ const { mcpInstallCommand } = await import('./mcp-install');
128
+ await mcpInstallCommand(args.slice(1));
129
+ break;
130
+ }
131
+ const { mcpProxyCommand } = await import('./mcp-proxy');
132
+ await mcpProxyCommand(args);
133
+ break;
134
+ }
135
+
136
+ case 'key': {
137
+ // Manage API keys (service/agent credentials, incl. MCP auth).
138
+ const { keyCommand } = await import('./key');
139
+ await keyCommand(args);
140
+ break;
141
+ }
142
+
123
143
  case 'task':
124
144
  case 't': {
125
145
  // Parse --new flag to force a fresh session
@@ -617,6 +637,11 @@ Usage:
617
637
  forge cancel <id> Cancel a task
618
638
  forge retry <id> Retry a failed task
619
639
 
640
+ forge mcp install Add the Forge management MCP to Claude Code
641
+ forge key create [name] Create an API key (for agents/MCP)
642
+ forge key list List API keys
643
+ forge key revoke <id> Revoke an API key
644
+
620
645
  forge run <flow> Run a workflow
621
646
  forge flows List workflows
622
647
  forge projects List projects
@@ -637,7 +662,7 @@ Shortcuts: c=chat, j=jobs, wt=worktree, t=task, ls=tasks, w=watch, s=status, l=l
637
662
  }
638
663
  }
639
664
 
640
- const skipUpdateCheck = ['upgrade', 'uninstall', '--version', '-v', '--reset-password', '--add-enterprise-key'];
665
+ const skipUpdateCheck = ['upgrade', 'uninstall', '--version', '-v', '--reset-password', '--add-enterprise-key', 'mcp', 'key'];
641
666
  main().then(() => { if (!skipUpdateCheck.includes(cmd)) return checkForUpdate(); }).catch(err => {
642
667
  console.error(err.message);
643
668
  process.exit(1);