@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/RELEASE_NOTES.md +5 -6
- package/app/api/auth/keys/[id]/route.ts +16 -0
- package/app/api/auth/keys/route.ts +36 -0
- package/app/api/mcp/route.ts +144 -0
- package/cli/key.ts +67 -0
- package/cli/mcp-install.ts +106 -0
- package/cli/mcp-proxy.ts +196 -0
- package/cli/mw.mjs +453 -35
- package/cli/mw.ts +26 -1
- package/components/SettingsModal.tsx +123 -0
- package/lib/api-auth.ts +50 -0
- package/lib/api-keys.ts +157 -0
- package/lib/auth/idp-login.ts +25 -3
- package/lib/jobs/store.ts +25 -0
- package/lib/projects.ts +79 -5
- package/lib/settings.ts +12 -2
- package/mcp/README.md +46 -0
- package/mcp/server.ts +30 -0
- package/mcp/tools/_shared.ts +103 -0
- package/mcp/tools/automation.ts +244 -0
- package/mcp/tools/connectors.ts +83 -0
- package/mcp/tools/help.ts +50 -0
- package/mcp/tools/index.ts +39 -0
- package/mcp/tools/integrations.ts +97 -0
- package/mcp/tools/logs.ts +57 -0
- package/mcp/tools/marketplace.ts +75 -0
- package/mcp/tools/observability.ts +96 -0
- package/mcp/tools/pipelines.ts +150 -0
- package/mcp/tools/projects.ts +54 -0
- package/mcp/tools/tasks.ts +93 -0
- package/mcp/tools/workspace.ts +94 -0
- package/package.json +1 -1
- package/proxy.ts +27 -16
- package/src/core/db/database.ts +50 -43
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
|
|
768
|
-
const src = join3(configDir,
|
|
769
|
-
const dest = join3(dataDir2,
|
|
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(` ${
|
|
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:
|
|
833
|
-
const { join:
|
|
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(
|
|
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:
|
|
870
|
-
const { join:
|
|
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(
|
|
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:
|
|
882
|
-
const { join:
|
|
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 =
|
|
1282
|
+
const serverScript = join8(dirname(fileURLToPath(import.meta.url)), "..", "bin", "forge-server.mjs");
|
|
885
1283
|
const passthru = process.argv.slice(3);
|
|
886
|
-
|
|
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:
|
|
891
|
-
const { join:
|
|
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 =
|
|
894
|
-
const r =
|
|
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:
|
|
1197
|
-
const { join:
|
|
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 =
|
|
1613
|
+
const codeFile = join8(dataDir2, "session-code.json");
|
|
1201
1614
|
try {
|
|
1202
|
-
if (
|
|
1203
|
-
const data = JSON.parse(
|
|
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(
|
|
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:
|
|
1675
|
+
const { join: join8, dirname } = await import("node:path");
|
|
1263
1676
|
const { fileURLToPath } = await import("node:url");
|
|
1264
|
-
const serverScript =
|
|
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:
|
|
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(
|
|
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 " +
|
|
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:
|
|
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:
|
|
1769
|
+
cwd: homedir6()
|
|
1357
1770
|
});
|
|
1358
1771
|
try {
|
|
1359
|
-
const { readFileSync:
|
|
1360
|
-
const globalRoot = execSync2("npm root -g", { encoding: "utf-8", cwd:
|
|
1361
|
-
const pkg = JSON.parse(
|
|
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);
|