forge-memory 0.2.109 → 0.2.110
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 +299 -45
- 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) {
|
|
@@ -1330,14 +1463,9 @@ async function removeCodexAdapterConfig() {
|
|
|
1330
1463
|
const filePath = path.join(homeDir(), ".codex", "config.toml");
|
|
1331
1464
|
if (!fs.existsSync(filePath)) return { filePath, changed: false };
|
|
1332
1465
|
const source = await fsp.readFile(filePath, "utf8");
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
if (!pattern.test(source)) return { filePath, changed: false };
|
|
1466
|
+
const next = stripCodexForgeMcpConfig(source);
|
|
1467
|
+
if (next === source.trimEnd()) return { filePath, changed: false };
|
|
1336
1468
|
await backupIfExists(filePath);
|
|
1337
|
-
const next = source
|
|
1338
|
-
.replace(pattern, "\n")
|
|
1339
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
1340
|
-
.trimEnd();
|
|
1341
1469
|
await fsp.writeFile(filePath, next ? `${next}\n` : "", "utf8");
|
|
1342
1470
|
return { filePath, changed: true };
|
|
1343
1471
|
}
|
|
@@ -1419,10 +1547,100 @@ function normalizePublicPairingUrl(value) {
|
|
|
1419
1547
|
}
|
|
1420
1548
|
}
|
|
1421
1549
|
|
|
1550
|
+
function readSetCookieHeader(headers) {
|
|
1551
|
+
if (typeof headers.getSetCookie === "function") {
|
|
1552
|
+
const values = headers.getSetCookie();
|
|
1553
|
+
if (values.length > 0) return values[0];
|
|
1554
|
+
}
|
|
1555
|
+
return headers.get("set-cookie");
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function cookiePairFromSetCookie(value) {
|
|
1559
|
+
if (!value) return null;
|
|
1560
|
+
return value.split(";")[0]?.trim() || null;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
class PairingAuthError extends Error {
|
|
1564
|
+
constructor(message, detail = {}) {
|
|
1565
|
+
super(message);
|
|
1566
|
+
this.name = "PairingAuthError";
|
|
1567
|
+
this.code = "pairing_auth_failed";
|
|
1568
|
+
this.detail = detail;
|
|
1569
|
+
this.guidance = [
|
|
1570
|
+
"Forge is reachable, but Forge Memory could not create a local operator session for iOS pairing.",
|
|
1571
|
+
"Run npx forge-memory doctor to confirm the runtime is healthy.",
|
|
1572
|
+
"Then rerun npx forge-memory pair-ios."
|
|
1573
|
+
];
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
class PairingRequestError extends Error {
|
|
1578
|
+
constructor(message, detail = {}) {
|
|
1579
|
+
super(message);
|
|
1580
|
+
this.name = "PairingRequestError";
|
|
1581
|
+
this.code = "pairing_request_failed";
|
|
1582
|
+
this.detail = detail;
|
|
1583
|
+
this.guidance =
|
|
1584
|
+
detail.status === 401
|
|
1585
|
+
? [
|
|
1586
|
+
"Forge is reachable, but the pairing request was not authenticated.",
|
|
1587
|
+
"Forge Memory should bootstrap a local operator session before pairing; rerun npx forge-memory pair-ios after updating.",
|
|
1588
|
+
"Run npx forge-memory doctor only if runtime health also fails."
|
|
1589
|
+
]
|
|
1590
|
+
: [
|
|
1591
|
+
"Forge is reachable, but it rejected the iOS pairing request.",
|
|
1592
|
+
"Run npx forge-memory doctor to confirm runtime health, then inspect the response above."
|
|
1593
|
+
];
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
async function bootstrapLocalOperatorSession(config) {
|
|
1598
|
+
const sessionUrl = forgeApiUrl(config, "/api/v1/auth/operator-session");
|
|
1599
|
+
let response;
|
|
1600
|
+
try {
|
|
1601
|
+
response = await fetch(sessionUrl, {
|
|
1602
|
+
method: "GET",
|
|
1603
|
+
headers: {
|
|
1604
|
+
accept: "application/json",
|
|
1605
|
+
host: localHostHeader(config)
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
} catch (error) {
|
|
1609
|
+
throw new PairingAuthError(
|
|
1610
|
+
[
|
|
1611
|
+
`Could not create a local operator session at ${sessionUrl}.`,
|
|
1612
|
+
`Network: ${describeNetworkError(error)}.`
|
|
1613
|
+
].join(" "),
|
|
1614
|
+
{ url: sessionUrl.toString() }
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
if (!response.ok) {
|
|
1618
|
+
const body = await response.text().catch(() => "");
|
|
1619
|
+
throw new PairingAuthError(
|
|
1620
|
+
[
|
|
1621
|
+
`Could not create a local operator session at ${sessionUrl}: Forge returned HTTP ${response.status}.`,
|
|
1622
|
+
body ? `Response: ${body.slice(0, 500)}` : ""
|
|
1623
|
+
]
|
|
1624
|
+
.filter(Boolean)
|
|
1625
|
+
.join(" "),
|
|
1626
|
+
{ url: sessionUrl.toString(), status: response.status }
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
const cookie = cookiePairFromSetCookie(readSetCookieHeader(response.headers));
|
|
1630
|
+
if (!cookie) {
|
|
1631
|
+
throw new PairingAuthError(
|
|
1632
|
+
`Could not create a local operator session at ${sessionUrl}: Forge did not return a session cookie.`,
|
|
1633
|
+
{ url: sessionUrl.toString(), status: response.status }
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
return cookie;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1422
1639
|
async function createPairing(config, options = {}) {
|
|
1423
1640
|
const transportMode = options.transportMode ?? "iroh";
|
|
1424
1641
|
const publicUrl = validatePairingOptions({ transportMode, publicUrl: options.publicUrl });
|
|
1425
|
-
const pairingUrl =
|
|
1642
|
+
const pairingUrl = forgeApiUrl(config, "/api/v1/health/pairing-sessions");
|
|
1643
|
+
const operatorCookie = await bootstrapLocalOperatorSession(config);
|
|
1426
1644
|
let response;
|
|
1427
1645
|
try {
|
|
1428
1646
|
response = await fetch(pairingUrl, {
|
|
@@ -1430,6 +1648,7 @@ async function createPairing(config, options = {}) {
|
|
|
1430
1648
|
headers: {
|
|
1431
1649
|
"content-type": "application/json",
|
|
1432
1650
|
accept: "application/json",
|
|
1651
|
+
cookie: operatorCookie,
|
|
1433
1652
|
...(publicUrl ? { referer: publicUrl } : {})
|
|
1434
1653
|
},
|
|
1435
1654
|
body: JSON.stringify({ userId: null, transportMode })
|
|
@@ -1452,14 +1671,17 @@ async function createPairing(config, options = {}) {
|
|
|
1452
1671
|
}
|
|
1453
1672
|
if (!response.ok) {
|
|
1454
1673
|
const body = await response.text().catch(() => "");
|
|
1455
|
-
throw new
|
|
1674
|
+
throw new PairingRequestError(
|
|
1456
1675
|
[
|
|
1457
1676
|
`Could not create iOS pairing at ${pairingUrl}: Forge returned HTTP ${response.status}.`,
|
|
1458
1677
|
body ? `Response: ${body.slice(0, 500)}` : "",
|
|
1459
|
-
|
|
1678
|
+
response.status === 401
|
|
1679
|
+
? "Forge Memory did not obtain or pass a valid local operator session cookie."
|
|
1680
|
+
: "Inspect the response and rerun npx forge-memory doctor if runtime health is uncertain."
|
|
1460
1681
|
]
|
|
1461
1682
|
.filter(Boolean)
|
|
1462
|
-
.join(" ")
|
|
1683
|
+
.join(" "),
|
|
1684
|
+
{ url: pairingUrl.toString(), status: response.status }
|
|
1463
1685
|
);
|
|
1464
1686
|
}
|
|
1465
1687
|
return response.json();
|
|
@@ -1773,25 +1995,34 @@ async function runStatus(parsed) {
|
|
|
1773
1995
|
const config = await readConfig();
|
|
1774
1996
|
const state = await readRuntimeState();
|
|
1775
1997
|
const currentHealth = await health(config);
|
|
1998
|
+
const stateExists = fs.existsSync(runtimeStatePath());
|
|
1999
|
+
const running = isHealthyForgeRuntime(currentHealth);
|
|
1776
2000
|
const payload = {
|
|
1777
|
-
ok:
|
|
1778
|
-
running
|
|
2001
|
+
ok: running,
|
|
2002
|
+
running,
|
|
1779
2003
|
mode: config.mode,
|
|
1780
2004
|
baseUrl: baseUrl(config),
|
|
1781
2005
|
webUrl: webUrl(config),
|
|
1782
2006
|
dataRoot: config.dataRoot,
|
|
1783
2007
|
adapters: config.adapters,
|
|
2008
|
+
health: currentHealth,
|
|
2009
|
+
runtimeStatePath: runtimeStatePath(),
|
|
2010
|
+
runtimeStateExists: stateExists,
|
|
2011
|
+
adoptedRuntime: running && !stateExists,
|
|
1784
2012
|
state
|
|
1785
2013
|
};
|
|
1786
2014
|
if (parsed.flags.json) console.log(JSON.stringify(payload, null, 2));
|
|
1787
2015
|
else {
|
|
1788
2016
|
console.log(`${color.bold("Forge Memory Status")}`);
|
|
1789
2017
|
console.log(
|
|
1790
|
-
`Runtime: ${
|
|
2018
|
+
`Runtime: ${running ? color.green("healthy") : color.yellow(describeHealthResult(currentHealth))}`
|
|
1791
2019
|
);
|
|
1792
2020
|
console.log(`Mode: ${config.mode}`);
|
|
1793
2021
|
console.log(`UI: ${webUrl(config)}`);
|
|
1794
2022
|
console.log(`Data: ${config.dataRoot}`);
|
|
2023
|
+
console.log(
|
|
2024
|
+
`Runtime state: ${stateExists ? runtimeStatePath() : color.yellow("missing; healthy runtimes will be adopted")}`
|
|
2025
|
+
);
|
|
1795
2026
|
console.log(
|
|
1796
2027
|
`Adapters: ${config.adapters.length ? config.adapters.join(", ") : "none configured"}`
|
|
1797
2028
|
);
|
|
@@ -1803,20 +2034,35 @@ async function doctorCheckRuntime(config, options) {
|
|
|
1803
2034
|
let result = await health(config);
|
|
1804
2035
|
let repaired = false;
|
|
1805
2036
|
let repairRecordPath = null;
|
|
1806
|
-
if (
|
|
2037
|
+
if (
|
|
2038
|
+
!isHealthyForgeRuntime(result) &&
|
|
2039
|
+
!(result.ok && result.forge === false) &&
|
|
2040
|
+
options.repair &&
|
|
2041
|
+
!options.noStart &&
|
|
2042
|
+
!options.dryRun
|
|
2043
|
+
) {
|
|
1807
2044
|
const repair = await repairPackagedRuntimeCache(config);
|
|
1808
2045
|
repairRecordPath = repair.repairRecordPath ?? null;
|
|
1809
2046
|
result = await health(config, 3_000);
|
|
1810
|
-
repaired = result
|
|
2047
|
+
repaired = isHealthyForgeRuntime(result);
|
|
1811
2048
|
}
|
|
2049
|
+
const ok = isHealthyForgeRuntime(result);
|
|
1812
2050
|
return {
|
|
1813
2051
|
id: "runtime",
|
|
1814
|
-
ok
|
|
1815
|
-
detail:
|
|
2052
|
+
ok,
|
|
2053
|
+
detail:
|
|
2054
|
+
result.ok && result.forge === false
|
|
2055
|
+
? `${baseUrl(config)} (non-Forge service responded)`
|
|
2056
|
+
: baseUrl(config),
|
|
1816
2057
|
repaired,
|
|
1817
2058
|
repairRecordPath,
|
|
1818
|
-
|
|
2059
|
+
health: result,
|
|
2060
|
+
statePath: runtimeStatePath(),
|
|
2061
|
+
stateExists: fs.existsSync(runtimeStatePath()),
|
|
2062
|
+
guidance: ok
|
|
1819
2063
|
? "Forge API is reachable."
|
|
2064
|
+
: result.ok && result.forge === false
|
|
2065
|
+
? `Port ${config.port || DEFAULT_PORT} responded, but not with Forge runtime health. Stop the conflicting process or choose another --port.`
|
|
1820
2066
|
: `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
2067
|
};
|
|
1822
2068
|
}
|
|
@@ -2378,14 +2624,22 @@ async function main() {
|
|
|
2378
2624
|
|
|
2379
2625
|
function printFatalError(error, { json = false } = {}) {
|
|
2380
2626
|
const message = error instanceof Error ? error.message : String(error);
|
|
2627
|
+
const guidance =
|
|
2628
|
+
error && typeof error === "object" && Array.isArray(error.guidance)
|
|
2629
|
+
? error.guidance
|
|
2630
|
+
: [
|
|
2631
|
+
"Run npx forge-memory doctor --repair to check and repair the local install.",
|
|
2632
|
+
"Run npx forge-memory logs to inspect the runtime log.",
|
|
2633
|
+
"Forge Memory repair never deletes your data folder."
|
|
2634
|
+
];
|
|
2381
2635
|
const payload = {
|
|
2382
2636
|
ok: false,
|
|
2637
|
+
code:
|
|
2638
|
+
error && typeof error === "object" && typeof error.code === "string"
|
|
2639
|
+
? error.code
|
|
2640
|
+
: "forge_memory_failed",
|
|
2383
2641
|
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
|
-
],
|
|
2642
|
+
guidance,
|
|
2389
2643
|
logPath: logPath()
|
|
2390
2644
|
};
|
|
2391
2645
|
if (json) {
|
package/package.json
CHANGED