forge-memory 0.2.109 → 0.2.111
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/bin/forge-memory.mjs +324 -48
- package/package.json +1 -1
package/bin/forge-memory.mjs
CHANGED
|
@@ -179,6 +179,14 @@ function webUrl(config) {
|
|
|
179
179
|
return `${baseUrl(config)}/forge/`;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
function localHostHeader(config) {
|
|
183
|
+
return `127.0.0.1:${config.port || DEFAULT_PORT}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function forgeApiUrl(config, pathname) {
|
|
187
|
+
return new URL(pathname, baseUrl(config));
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
async function readJson(filePath, fallback = null) {
|
|
183
191
|
try {
|
|
184
192
|
return JSON.parse(await fsp.readFile(filePath, "utf8"));
|
|
@@ -734,13 +742,8 @@ async function patchCodexConfig(config, options) {
|
|
|
734
742
|
`FORGE_DATA_ROOT = "${config.dataRoot.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`,
|
|
735
743
|
""
|
|
736
744
|
].join("\n");
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
if (pattern.test(source)) {
|
|
740
|
-
source = source.replace(pattern, `\n${block}`.trimEnd());
|
|
741
|
-
} else {
|
|
742
|
-
source = `${source.trimEnd()}\n\n${block}`.trimStart();
|
|
743
|
-
}
|
|
745
|
+
const cleaned = stripCodexForgeMcpConfig(source).trimEnd();
|
|
746
|
+
source = `${cleaned ? `${cleaned}\n\n` : ""}${block}`;
|
|
744
747
|
if (!options.dryRun) {
|
|
745
748
|
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
746
749
|
await backupIfExists(filePath);
|
|
@@ -753,6 +756,44 @@ async function patchCodexConfig(config, options) {
|
|
|
753
756
|
return { filePath };
|
|
754
757
|
}
|
|
755
758
|
|
|
759
|
+
function stripCodexForgeMcpConfig(source) {
|
|
760
|
+
const orphanForgeLines = new Set([
|
|
761
|
+
'command = "npx"',
|
|
762
|
+
'args = ["forge-memory", "mcp"]',
|
|
763
|
+
"FORGE_ORIGIN",
|
|
764
|
+
"FORGE_PORT",
|
|
765
|
+
"FORGE_ACTOR_LABEL",
|
|
766
|
+
"FORGE_TIMEOUT_MS",
|
|
767
|
+
"FORGE_DATA_ROOT"
|
|
768
|
+
]);
|
|
769
|
+
const lines = source.split(/\r?\n/);
|
|
770
|
+
const kept = [];
|
|
771
|
+
let skippingForgeTable = false;
|
|
772
|
+
let currentTable = null;
|
|
773
|
+
for (const line of lines) {
|
|
774
|
+
const trimmed = line.trim();
|
|
775
|
+
const tableMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
776
|
+
if (tableMatch) {
|
|
777
|
+
currentTable = tableMatch[1];
|
|
778
|
+
skippingForgeTable = currentTable === "mcp_servers.forge" ||
|
|
779
|
+
currentTable.startsWith("mcp_servers.forge.");
|
|
780
|
+
if (skippingForgeTable) continue;
|
|
781
|
+
}
|
|
782
|
+
if (skippingForgeTable) continue;
|
|
783
|
+
const isGlobalOrphanForgeLine =
|
|
784
|
+
currentTable === null &&
|
|
785
|
+
(orphanForgeLines.has(trimmed) ||
|
|
786
|
+
Array.from(orphanForgeLines).some((prefix) =>
|
|
787
|
+
trimmed.startsWith(`${prefix} =`)
|
|
788
|
+
));
|
|
789
|
+
if (isGlobalOrphanForgeLine) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
kept.push(line);
|
|
793
|
+
}
|
|
794
|
+
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
795
|
+
}
|
|
796
|
+
|
|
756
797
|
async function runCommand(
|
|
757
798
|
command,
|
|
758
799
|
args,
|
|
@@ -895,12 +936,28 @@ async function health(config, timeoutMs = 1_500) {
|
|
|
895
936
|
const controller = new AbortController();
|
|
896
937
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
897
938
|
try {
|
|
898
|
-
const response = await fetch(
|
|
899
|
-
headers: { accept: "application/json" },
|
|
939
|
+
const response = await fetch(forgeApiUrl(config, "/api/v1/health"), {
|
|
940
|
+
headers: { accept: "application/json", "x-forge-runtime-probe": "1" },
|
|
900
941
|
signal: controller.signal
|
|
901
942
|
});
|
|
902
943
|
if (!response.ok) return { ok: false, status: response.status };
|
|
903
|
-
|
|
944
|
+
const text = await response.text();
|
|
945
|
+
let payload;
|
|
946
|
+
try {
|
|
947
|
+
payload = JSON.parse(text);
|
|
948
|
+
} catch {
|
|
949
|
+
return {
|
|
950
|
+
ok: true,
|
|
951
|
+
status: response.status,
|
|
952
|
+
error: "HTTP health endpoint returned non-JSON content",
|
|
953
|
+
forge: false
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
ok: true,
|
|
958
|
+
payload,
|
|
959
|
+
forge: isForgeHealthPayload(payload)
|
|
960
|
+
};
|
|
904
961
|
} catch (error) {
|
|
905
962
|
return {
|
|
906
963
|
ok: false,
|
|
@@ -911,6 +968,11 @@ async function health(config, timeoutMs = 1_500) {
|
|
|
911
968
|
}
|
|
912
969
|
}
|
|
913
970
|
|
|
971
|
+
function isForgeHealthPayload(payload) {
|
|
972
|
+
if (!payload || typeof payload !== "object") return false;
|
|
973
|
+
return payload.app === "forge" && payload.backend === "forge-node-runtime";
|
|
974
|
+
}
|
|
975
|
+
|
|
914
976
|
function describeNetworkError(error) {
|
|
915
977
|
if (error instanceof Error) {
|
|
916
978
|
if (error.name === "AbortError") return "request timed out";
|
|
@@ -922,12 +984,17 @@ function describeNetworkError(error) {
|
|
|
922
984
|
}
|
|
923
985
|
|
|
924
986
|
function describeHealthResult(result) {
|
|
987
|
+
if (result.ok && result.forge === false) return "HTTP 200 from a non-Forge service";
|
|
925
988
|
if (result.ok) return "healthy";
|
|
926
989
|
if (result.status) return `HTTP ${result.status}`;
|
|
927
990
|
if (result.error) return result.error;
|
|
928
991
|
return "not reachable";
|
|
929
992
|
}
|
|
930
993
|
|
|
994
|
+
function isHealthyForgeRuntime(result) {
|
|
995
|
+
return Boolean(result?.ok && result.forge !== false);
|
|
996
|
+
}
|
|
997
|
+
|
|
931
998
|
async function readRuntimeState() {
|
|
932
999
|
return readJson(runtimeStatePath(), null);
|
|
933
1000
|
}
|
|
@@ -1077,10 +1144,48 @@ async function repairPackagedRuntimeCache(config) {
|
|
|
1077
1144
|
}
|
|
1078
1145
|
|
|
1079
1146
|
async function startRuntime(config) {
|
|
1147
|
+
const current = await health(config);
|
|
1148
|
+
if (isHealthyForgeRuntime(current)) {
|
|
1149
|
+
const existing = await readRuntimeState();
|
|
1150
|
+
return {
|
|
1151
|
+
ok: true,
|
|
1152
|
+
started: false,
|
|
1153
|
+
adopted: true,
|
|
1154
|
+
state: existing,
|
|
1155
|
+
health: current
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
if (current.ok && current.forge === false) {
|
|
1159
|
+
return {
|
|
1160
|
+
ok: false,
|
|
1161
|
+
started: false,
|
|
1162
|
+
state: await readRuntimeState(),
|
|
1163
|
+
health: current,
|
|
1164
|
+
portConflict: true,
|
|
1165
|
+
message: `Port ${config.port || DEFAULT_PORT} is already serving a non-Forge HTTP service.`
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
if (!(await isPortAvailable(config.port || DEFAULT_PORT))) {
|
|
1169
|
+
return {
|
|
1170
|
+
ok: false,
|
|
1171
|
+
started: false,
|
|
1172
|
+
state: await readRuntimeState(),
|
|
1173
|
+
health: current,
|
|
1174
|
+
portConflict: true,
|
|
1175
|
+
message: `Port ${config.port || DEFAULT_PORT} is already in use.`
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1080
1178
|
const existing = await readRuntimeState();
|
|
1081
1179
|
if (existing?.pid && processExists(existing.pid)) {
|
|
1082
|
-
const
|
|
1083
|
-
if (
|
|
1180
|
+
const existingHealth = await health(config);
|
|
1181
|
+
if (isHealthyForgeRuntime(existingHealth))
|
|
1182
|
+
return {
|
|
1183
|
+
ok: true,
|
|
1184
|
+
started: false,
|
|
1185
|
+
adopted: true,
|
|
1186
|
+
state: existing,
|
|
1187
|
+
health: existingHealth
|
|
1188
|
+
};
|
|
1084
1189
|
}
|
|
1085
1190
|
|
|
1086
1191
|
await fsp.mkdir(path.dirname(logPath()), { recursive: true });
|
|
@@ -1178,6 +1283,18 @@ async function startRuntime(config) {
|
|
|
1178
1283
|
|
|
1179
1284
|
function assertRuntimeStartedForPairing(result, config) {
|
|
1180
1285
|
if (result?.ok) return;
|
|
1286
|
+
if (result?.portConflict) {
|
|
1287
|
+
throw new Error(
|
|
1288
|
+
[
|
|
1289
|
+
`Forge runtime did not start because ${baseUrl(config)} is already in use by another service.`,
|
|
1290
|
+
`Health check: ${describeHealthResult(result?.health ?? { ok: false })}.`,
|
|
1291
|
+
"Run npx forge-memory status to inspect the configured runtime.",
|
|
1292
|
+
"If Forge Memory owns the running process, run npx forge-memory stop; otherwise stop the conflicting process or choose another --port.",
|
|
1293
|
+
`Runtime log: ${logPath()}.`,
|
|
1294
|
+
"Your data folder is unchanged."
|
|
1295
|
+
].join(" ")
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1181
1298
|
throw new Error(
|
|
1182
1299
|
[
|
|
1183
1300
|
`Forge runtime did not become healthy at ${baseUrl(config)}, so iOS pairing was not started.`,
|
|
@@ -1188,22 +1305,38 @@ function assertRuntimeStartedForPairing(result, config) {
|
|
|
1188
1305
|
);
|
|
1189
1306
|
}
|
|
1190
1307
|
|
|
1191
|
-
async function stopRuntime() {
|
|
1308
|
+
async function stopRuntime(config = null) {
|
|
1309
|
+
const effectiveConfig = config ?? (await readConfig());
|
|
1192
1310
|
const state = await readRuntimeState();
|
|
1193
|
-
if (!state?.children?.length)
|
|
1194
|
-
return {
|
|
1195
|
-
ok: true,
|
|
1196
|
-
stopped: false,
|
|
1197
|
-
message: "No forge-memory runtime state found."
|
|
1198
|
-
};
|
|
1199
1311
|
const stopped = [];
|
|
1200
|
-
for (const child of state
|
|
1312
|
+
for (const child of state?.children ?? []) {
|
|
1201
1313
|
if (!child?.pid || !processExists(child.pid)) continue;
|
|
1202
1314
|
process.kill(child.pid, "SIGTERM");
|
|
1203
1315
|
stopped.push(child.pid);
|
|
1204
1316
|
}
|
|
1317
|
+
const current = await health(effectiveConfig);
|
|
1318
|
+
const runtimePid = Number(current?.payload?.runtime?.pid);
|
|
1319
|
+
if (
|
|
1320
|
+
isHealthyForgeRuntime(current) &&
|
|
1321
|
+
Number.isInteger(runtimePid) &&
|
|
1322
|
+
runtimePid > 0 &&
|
|
1323
|
+
!stopped.includes(runtimePid) &&
|
|
1324
|
+
processExists(runtimePid)
|
|
1325
|
+
) {
|
|
1326
|
+
process.kill(runtimePid, "SIGTERM");
|
|
1327
|
+
stopped.push(runtimePid);
|
|
1328
|
+
}
|
|
1205
1329
|
await fsp.rm(runtimeStatePath(), { force: true });
|
|
1206
|
-
|
|
1330
|
+
if (stopped.length === 0) {
|
|
1331
|
+
return {
|
|
1332
|
+
ok: true,
|
|
1333
|
+
stopped: false,
|
|
1334
|
+
message: state?.children?.length
|
|
1335
|
+
? "No recorded Forge Memory runtime processes were alive."
|
|
1336
|
+
: "No Forge Memory runtime state or live Forge runtime was found."
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
return { ok: true, stopped: true, pids: stopped };
|
|
1207
1340
|
}
|
|
1208
1341
|
|
|
1209
1342
|
async function exportForgeData(parsed) {
|
|
@@ -1318,26 +1451,43 @@ async function removeHermesAdapterConfig() {
|
|
|
1318
1451
|
"forge",
|
|
1319
1452
|
"config.json"
|
|
1320
1453
|
);
|
|
1321
|
-
|
|
1322
|
-
if (
|
|
1454
|
+
let changed = false;
|
|
1455
|
+
if (fs.existsSync(forgeConfigPath)) {
|
|
1323
1456
|
await backupIfExists(forgeConfigPath);
|
|
1324
1457
|
await fsp.rm(forgeConfigPath, { force: true });
|
|
1458
|
+
changed = true;
|
|
1459
|
+
}
|
|
1460
|
+
const forgeConfigDir = path.dirname(forgeConfigPath);
|
|
1461
|
+
if (fs.existsSync(forgeConfigDir)) {
|
|
1462
|
+
await fsp.rm(forgeConfigDir, { recursive: false, force: true }).catch(() => {});
|
|
1325
1463
|
}
|
|
1326
|
-
|
|
1464
|
+
|
|
1465
|
+
const hermesYamlPath = path.join(homeDir(), ".hermes", "config.yaml");
|
|
1466
|
+
if (fs.existsSync(hermesYamlPath)) {
|
|
1467
|
+
const raw = await fsp.readFile(hermesYamlPath, "utf8");
|
|
1468
|
+
const doc = YAML.parseDocument(raw);
|
|
1469
|
+
const root = doc.toJSON() ?? {};
|
|
1470
|
+
const enabled = Array.isArray(root.plugins?.enabled)
|
|
1471
|
+
? root.plugins.enabled
|
|
1472
|
+
: null;
|
|
1473
|
+
if (enabled?.includes("forge")) {
|
|
1474
|
+
root.plugins.enabled = enabled.filter((entry) => entry !== "forge");
|
|
1475
|
+
doc.contents = doc.createNode(root);
|
|
1476
|
+
await backupIfExists(hermesYamlPath);
|
|
1477
|
+
await fsp.writeFile(hermesYamlPath, String(doc), "utf8");
|
|
1478
|
+
changed = true;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return { filePath: forgeConfigPath, yamlPath: hermesYamlPath, changed };
|
|
1327
1482
|
}
|
|
1328
1483
|
|
|
1329
1484
|
async function removeCodexAdapterConfig() {
|
|
1330
1485
|
const filePath = path.join(homeDir(), ".codex", "config.toml");
|
|
1331
1486
|
if (!fs.existsSync(filePath)) return { filePath, changed: false };
|
|
1332
1487
|
const source = await fsp.readFile(filePath, "utf8");
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
if (!pattern.test(source)) return { filePath, changed: false };
|
|
1488
|
+
const next = stripCodexForgeMcpConfig(source);
|
|
1489
|
+
if (next === source.trimEnd()) return { filePath, changed: false };
|
|
1336
1490
|
await backupIfExists(filePath);
|
|
1337
|
-
const next = source
|
|
1338
|
-
.replace(pattern, "\n")
|
|
1339
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
1340
|
-
.trimEnd();
|
|
1341
1491
|
await fsp.writeFile(filePath, next ? `${next}\n` : "", "utf8");
|
|
1342
1492
|
return { filePath, changed: true };
|
|
1343
1493
|
}
|
|
@@ -1419,10 +1569,100 @@ function normalizePublicPairingUrl(value) {
|
|
|
1419
1569
|
}
|
|
1420
1570
|
}
|
|
1421
1571
|
|
|
1572
|
+
function readSetCookieHeader(headers) {
|
|
1573
|
+
if (typeof headers.getSetCookie === "function") {
|
|
1574
|
+
const values = headers.getSetCookie();
|
|
1575
|
+
if (values.length > 0) return values[0];
|
|
1576
|
+
}
|
|
1577
|
+
return headers.get("set-cookie");
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function cookiePairFromSetCookie(value) {
|
|
1581
|
+
if (!value) return null;
|
|
1582
|
+
return value.split(";")[0]?.trim() || null;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
class PairingAuthError extends Error {
|
|
1586
|
+
constructor(message, detail = {}) {
|
|
1587
|
+
super(message);
|
|
1588
|
+
this.name = "PairingAuthError";
|
|
1589
|
+
this.code = "pairing_auth_failed";
|
|
1590
|
+
this.detail = detail;
|
|
1591
|
+
this.guidance = [
|
|
1592
|
+
"Forge is reachable, but Forge Memory could not create a local operator session for iOS pairing.",
|
|
1593
|
+
"Run npx forge-memory doctor to confirm the runtime is healthy.",
|
|
1594
|
+
"Then rerun npx forge-memory pair-ios."
|
|
1595
|
+
];
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
class PairingRequestError extends Error {
|
|
1600
|
+
constructor(message, detail = {}) {
|
|
1601
|
+
super(message);
|
|
1602
|
+
this.name = "PairingRequestError";
|
|
1603
|
+
this.code = "pairing_request_failed";
|
|
1604
|
+
this.detail = detail;
|
|
1605
|
+
this.guidance =
|
|
1606
|
+
detail.status === 401
|
|
1607
|
+
? [
|
|
1608
|
+
"Forge is reachable, but the pairing request was not authenticated.",
|
|
1609
|
+
"Forge Memory should bootstrap a local operator session before pairing; rerun npx forge-memory pair-ios after updating.",
|
|
1610
|
+
"Run npx forge-memory doctor only if runtime health also fails."
|
|
1611
|
+
]
|
|
1612
|
+
: [
|
|
1613
|
+
"Forge is reachable, but it rejected the iOS pairing request.",
|
|
1614
|
+
"Run npx forge-memory doctor to confirm runtime health, then inspect the response above."
|
|
1615
|
+
];
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
async function bootstrapLocalOperatorSession(config) {
|
|
1620
|
+
const sessionUrl = forgeApiUrl(config, "/api/v1/auth/operator-session");
|
|
1621
|
+
let response;
|
|
1622
|
+
try {
|
|
1623
|
+
response = await fetch(sessionUrl, {
|
|
1624
|
+
method: "GET",
|
|
1625
|
+
headers: {
|
|
1626
|
+
accept: "application/json",
|
|
1627
|
+
host: localHostHeader(config)
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
throw new PairingAuthError(
|
|
1632
|
+
[
|
|
1633
|
+
`Could not create a local operator session at ${sessionUrl}.`,
|
|
1634
|
+
`Network: ${describeNetworkError(error)}.`
|
|
1635
|
+
].join(" "),
|
|
1636
|
+
{ url: sessionUrl.toString() }
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
if (!response.ok) {
|
|
1640
|
+
const body = await response.text().catch(() => "");
|
|
1641
|
+
throw new PairingAuthError(
|
|
1642
|
+
[
|
|
1643
|
+
`Could not create a local operator session at ${sessionUrl}: Forge returned HTTP ${response.status}.`,
|
|
1644
|
+
body ? `Response: ${body.slice(0, 500)}` : ""
|
|
1645
|
+
]
|
|
1646
|
+
.filter(Boolean)
|
|
1647
|
+
.join(" "),
|
|
1648
|
+
{ url: sessionUrl.toString(), status: response.status }
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
const cookie = cookiePairFromSetCookie(readSetCookieHeader(response.headers));
|
|
1652
|
+
if (!cookie) {
|
|
1653
|
+
throw new PairingAuthError(
|
|
1654
|
+
`Could not create a local operator session at ${sessionUrl}: Forge did not return a session cookie.`,
|
|
1655
|
+
{ url: sessionUrl.toString(), status: response.status }
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
return cookie;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1422
1661
|
async function createPairing(config, options = {}) {
|
|
1423
1662
|
const transportMode = options.transportMode ?? "iroh";
|
|
1424
1663
|
const publicUrl = validatePairingOptions({ transportMode, publicUrl: options.publicUrl });
|
|
1425
|
-
const pairingUrl =
|
|
1664
|
+
const pairingUrl = forgeApiUrl(config, "/api/v1/health/pairing-sessions");
|
|
1665
|
+
const operatorCookie = await bootstrapLocalOperatorSession(config);
|
|
1426
1666
|
let response;
|
|
1427
1667
|
try {
|
|
1428
1668
|
response = await fetch(pairingUrl, {
|
|
@@ -1430,6 +1670,7 @@ async function createPairing(config, options = {}) {
|
|
|
1430
1670
|
headers: {
|
|
1431
1671
|
"content-type": "application/json",
|
|
1432
1672
|
accept: "application/json",
|
|
1673
|
+
cookie: operatorCookie,
|
|
1433
1674
|
...(publicUrl ? { referer: publicUrl } : {})
|
|
1434
1675
|
},
|
|
1435
1676
|
body: JSON.stringify({ userId: null, transportMode })
|
|
@@ -1452,14 +1693,17 @@ async function createPairing(config, options = {}) {
|
|
|
1452
1693
|
}
|
|
1453
1694
|
if (!response.ok) {
|
|
1454
1695
|
const body = await response.text().catch(() => "");
|
|
1455
|
-
throw new
|
|
1696
|
+
throw new PairingRequestError(
|
|
1456
1697
|
[
|
|
1457
1698
|
`Could not create iOS pairing at ${pairingUrl}: Forge returned HTTP ${response.status}.`,
|
|
1458
1699
|
body ? `Response: ${body.slice(0, 500)}` : "",
|
|
1459
|
-
|
|
1700
|
+
response.status === 401
|
|
1701
|
+
? "Forge Memory did not obtain or pass a valid local operator session cookie."
|
|
1702
|
+
: "Inspect the response and rerun npx forge-memory doctor if runtime health is uncertain."
|
|
1460
1703
|
]
|
|
1461
1704
|
.filter(Boolean)
|
|
1462
|
-
.join(" ")
|
|
1705
|
+
.join(" "),
|
|
1706
|
+
{ url: pairingUrl.toString(), status: response.status }
|
|
1463
1707
|
);
|
|
1464
1708
|
}
|
|
1465
1709
|
return response.json();
|
|
@@ -1773,25 +2017,34 @@ async function runStatus(parsed) {
|
|
|
1773
2017
|
const config = await readConfig();
|
|
1774
2018
|
const state = await readRuntimeState();
|
|
1775
2019
|
const currentHealth = await health(config);
|
|
2020
|
+
const stateExists = fs.existsSync(runtimeStatePath());
|
|
2021
|
+
const running = isHealthyForgeRuntime(currentHealth);
|
|
1776
2022
|
const payload = {
|
|
1777
|
-
ok:
|
|
1778
|
-
running
|
|
2023
|
+
ok: running,
|
|
2024
|
+
running,
|
|
1779
2025
|
mode: config.mode,
|
|
1780
2026
|
baseUrl: baseUrl(config),
|
|
1781
2027
|
webUrl: webUrl(config),
|
|
1782
2028
|
dataRoot: config.dataRoot,
|
|
1783
2029
|
adapters: config.adapters,
|
|
2030
|
+
health: currentHealth,
|
|
2031
|
+
runtimeStatePath: runtimeStatePath(),
|
|
2032
|
+
runtimeStateExists: stateExists,
|
|
2033
|
+
adoptedRuntime: running && !stateExists,
|
|
1784
2034
|
state
|
|
1785
2035
|
};
|
|
1786
2036
|
if (parsed.flags.json) console.log(JSON.stringify(payload, null, 2));
|
|
1787
2037
|
else {
|
|
1788
2038
|
console.log(`${color.bold("Forge Memory Status")}`);
|
|
1789
2039
|
console.log(
|
|
1790
|
-
`Runtime: ${
|
|
2040
|
+
`Runtime: ${running ? color.green("healthy") : color.yellow(describeHealthResult(currentHealth))}`
|
|
1791
2041
|
);
|
|
1792
2042
|
console.log(`Mode: ${config.mode}`);
|
|
1793
2043
|
console.log(`UI: ${webUrl(config)}`);
|
|
1794
2044
|
console.log(`Data: ${config.dataRoot}`);
|
|
2045
|
+
console.log(
|
|
2046
|
+
`Runtime state: ${stateExists ? runtimeStatePath() : color.yellow("missing; healthy runtimes will be adopted")}`
|
|
2047
|
+
);
|
|
1795
2048
|
console.log(
|
|
1796
2049
|
`Adapters: ${config.adapters.length ? config.adapters.join(", ") : "none configured"}`
|
|
1797
2050
|
);
|
|
@@ -1803,20 +2056,35 @@ async function doctorCheckRuntime(config, options) {
|
|
|
1803
2056
|
let result = await health(config);
|
|
1804
2057
|
let repaired = false;
|
|
1805
2058
|
let repairRecordPath = null;
|
|
1806
|
-
if (
|
|
2059
|
+
if (
|
|
2060
|
+
!isHealthyForgeRuntime(result) &&
|
|
2061
|
+
!(result.ok && result.forge === false) &&
|
|
2062
|
+
options.repair &&
|
|
2063
|
+
!options.noStart &&
|
|
2064
|
+
!options.dryRun
|
|
2065
|
+
) {
|
|
1807
2066
|
const repair = await repairPackagedRuntimeCache(config);
|
|
1808
2067
|
repairRecordPath = repair.repairRecordPath ?? null;
|
|
1809
2068
|
result = await health(config, 3_000);
|
|
1810
|
-
repaired = result
|
|
2069
|
+
repaired = isHealthyForgeRuntime(result);
|
|
1811
2070
|
}
|
|
2071
|
+
const ok = isHealthyForgeRuntime(result);
|
|
1812
2072
|
return {
|
|
1813
2073
|
id: "runtime",
|
|
1814
|
-
ok
|
|
1815
|
-
detail:
|
|
2074
|
+
ok,
|
|
2075
|
+
detail:
|
|
2076
|
+
result.ok && result.forge === false
|
|
2077
|
+
? `${baseUrl(config)} (non-Forge service responded)`
|
|
2078
|
+
: baseUrl(config),
|
|
1816
2079
|
repaired,
|
|
1817
2080
|
repairRecordPath,
|
|
1818
|
-
|
|
2081
|
+
health: result,
|
|
2082
|
+
statePath: runtimeStatePath(),
|
|
2083
|
+
stateExists: fs.existsSync(runtimeStatePath()),
|
|
2084
|
+
guidance: ok
|
|
1819
2085
|
? "Forge API is reachable."
|
|
2086
|
+
: result.ok && result.forge === false
|
|
2087
|
+
? `Port ${config.port || DEFAULT_PORT} responded, but not with Forge runtime health. Stop the conflicting process or choose another --port.`
|
|
1820
2088
|
: `Run npx forge-memory doctor --repair, then inspect ${logPath()} if the runtime still does not start. Repair reinstalls only the owned runtime cache and preserves the data folder.`
|
|
1821
2089
|
};
|
|
1822
2090
|
}
|
|
@@ -2378,14 +2646,22 @@ async function main() {
|
|
|
2378
2646
|
|
|
2379
2647
|
function printFatalError(error, { json = false } = {}) {
|
|
2380
2648
|
const message = error instanceof Error ? error.message : String(error);
|
|
2649
|
+
const guidance =
|
|
2650
|
+
error && typeof error === "object" && Array.isArray(error.guidance)
|
|
2651
|
+
? error.guidance
|
|
2652
|
+
: [
|
|
2653
|
+
"Run npx forge-memory doctor --repair to check and repair the local install.",
|
|
2654
|
+
"Run npx forge-memory logs to inspect the runtime log.",
|
|
2655
|
+
"Forge Memory repair never deletes your data folder."
|
|
2656
|
+
];
|
|
2381
2657
|
const payload = {
|
|
2382
2658
|
ok: false,
|
|
2659
|
+
code:
|
|
2660
|
+
error && typeof error === "object" && typeof error.code === "string"
|
|
2661
|
+
? error.code
|
|
2662
|
+
: "forge_memory_failed",
|
|
2383
2663
|
error: message,
|
|
2384
|
-
guidance
|
|
2385
|
-
"Run npx forge-memory doctor --repair to check and repair the local install.",
|
|
2386
|
-
"Run npx forge-memory logs to inspect the runtime log.",
|
|
2387
|
-
"Forge Memory repair never deletes your data folder."
|
|
2388
|
-
],
|
|
2664
|
+
guidance,
|
|
2389
2665
|
logPath: logPath()
|
|
2390
2666
|
};
|
|
2391
2667
|
if (json) {
|
package/package.json
CHANGED