forge-memory 0.2.108 → 0.2.109
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 +136 -14
- package/package.json +1 -1
package/bin/forge-memory.mjs
CHANGED
|
@@ -951,8 +951,8 @@ async function waitForHealth(config, timeoutMs = 30_000) {
|
|
|
951
951
|
return health(config);
|
|
952
952
|
}
|
|
953
953
|
|
|
954
|
-
function resolveOpenClawPluginRoot() {
|
|
955
|
-
const candidates = [
|
|
954
|
+
function resolveOpenClawPluginRoot(options = {}) {
|
|
955
|
+
const candidates = [];
|
|
956
956
|
const installedRuntimePackageJson = path.join(
|
|
957
957
|
runtimeInstallRoot(),
|
|
958
958
|
"package.json"
|
|
@@ -960,6 +960,9 @@ function resolveOpenClawPluginRoot() {
|
|
|
960
960
|
if (fs.existsSync(installedRuntimePackageJson)) {
|
|
961
961
|
candidates.push(createRequire(installedRuntimePackageJson));
|
|
962
962
|
}
|
|
963
|
+
if (!options.installedOnly) {
|
|
964
|
+
candidates.push(require);
|
|
965
|
+
}
|
|
963
966
|
|
|
964
967
|
for (const candidateRequire of candidates) {
|
|
965
968
|
try {
|
|
@@ -975,8 +978,10 @@ function resolveOpenClawPluginRoot() {
|
|
|
975
978
|
return null;
|
|
976
979
|
}
|
|
977
980
|
|
|
978
|
-
async function ensurePackagedRuntimeInstalled() {
|
|
979
|
-
const existing =
|
|
981
|
+
async function ensurePackagedRuntimeInstalled(options = {}) {
|
|
982
|
+
const existing = options.forceInstall
|
|
983
|
+
? null
|
|
984
|
+
: resolveOpenClawPluginRoot();
|
|
980
985
|
if (existing) return existing;
|
|
981
986
|
const installRoot = runtimeInstallRoot();
|
|
982
987
|
await fsp.mkdir(installRoot, { recursive: true });
|
|
@@ -1012,14 +1017,65 @@ async function ensurePackagedRuntimeInstalled() {
|
|
|
1012
1017
|
].join(" ")
|
|
1013
1018
|
);
|
|
1014
1019
|
}
|
|
1015
|
-
const installed = resolveOpenClawPluginRoot();
|
|
1020
|
+
const installed = resolveOpenClawPluginRoot({ installedOnly: true });
|
|
1016
1021
|
if (!installed)
|
|
1017
1022
|
throw new Error(
|
|
1018
1023
|
`${RUNTIME_PACKAGE} installed but its runtime entry could not be resolved. Log: ${logPath()}`
|
|
1019
1024
|
);
|
|
1025
|
+
const entry = path.join(installed, "server", "index.js");
|
|
1026
|
+
if (!fs.existsSync(entry)) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`${RUNTIME_PACKAGE} installed but ${entry} is missing. Log: ${logPath()}`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1020
1031
|
return installed;
|
|
1021
1032
|
}
|
|
1022
1033
|
|
|
1034
|
+
async function rotateRuntimeLog(reason) {
|
|
1035
|
+
if (!fs.existsSync(logPath())) return null;
|
|
1036
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1037
|
+
const backupPath = `${logPath()}.${reason}-${stamp}.log`;
|
|
1038
|
+
await fsp.mkdir(path.dirname(backupPath), { recursive: true });
|
|
1039
|
+
await fsp.copyFile(logPath(), backupPath);
|
|
1040
|
+
return backupPath;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function repairPackagedRuntimeCache(config) {
|
|
1044
|
+
if (config.mode === "dev") {
|
|
1045
|
+
const result = await startRuntime(config);
|
|
1046
|
+
return {
|
|
1047
|
+
ok: result.ok,
|
|
1048
|
+
mode: "dev",
|
|
1049
|
+
dataRoot: config.dataRoot,
|
|
1050
|
+
health: result.health ?? { ok: result.ok }
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
await stopRuntime();
|
|
1055
|
+
const rotatedLogPath = await rotateRuntimeLog("repair");
|
|
1056
|
+
await fsp.rm(runtimeStatePath(), { force: true });
|
|
1057
|
+
await fsp.rm(runtimeInstallRoot(), { recursive: true, force: true });
|
|
1058
|
+
const pluginRoot = await ensurePackagedRuntimeInstalled({ forceInstall: true });
|
|
1059
|
+
const result = await startRuntime(config);
|
|
1060
|
+
const record = {
|
|
1061
|
+
repairedAt: new Date().toISOString(),
|
|
1062
|
+
ok: result.ok,
|
|
1063
|
+
mode: config.mode,
|
|
1064
|
+
runtimePackage: RUNTIME_PACKAGE,
|
|
1065
|
+
runtimePackageVersion: RUNTIME_PACKAGE_VERSION,
|
|
1066
|
+
pluginRoot,
|
|
1067
|
+
dataRoot: config.dataRoot,
|
|
1068
|
+
dataPreserved: true,
|
|
1069
|
+
rotatedLogPath,
|
|
1070
|
+
health: result.health ?? { ok: result.ok }
|
|
1071
|
+
};
|
|
1072
|
+
const stamp = record.repairedAt.replace(/[:.]/g, "-");
|
|
1073
|
+
const repairRecordPath = path.join(forgeHome(), "run", `runtime-repair-${stamp}.json`);
|
|
1074
|
+
await fsp.mkdir(path.dirname(repairRecordPath), { recursive: true });
|
|
1075
|
+
await fsp.writeFile(repairRecordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
1076
|
+
return { ...record, repairRecordPath };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1023
1079
|
async function startRuntime(config) {
|
|
1024
1080
|
const existing = await readRuntimeState();
|
|
1025
1081
|
if (existing?.pid && processExists(existing.pid)) {
|
|
@@ -1365,7 +1421,7 @@ function normalizePublicPairingUrl(value) {
|
|
|
1365
1421
|
|
|
1366
1422
|
async function createPairing(config, options = {}) {
|
|
1367
1423
|
const transportMode = options.transportMode ?? "iroh";
|
|
1368
|
-
const publicUrl =
|
|
1424
|
+
const publicUrl = validatePairingOptions({ transportMode, publicUrl: options.publicUrl });
|
|
1369
1425
|
const pairingUrl = new URL("/api/v1/health/pairing-sessions", baseUrl(config));
|
|
1370
1426
|
let response;
|
|
1371
1427
|
try {
|
|
@@ -1409,6 +1465,33 @@ async function createPairing(config, options = {}) {
|
|
|
1409
1465
|
return response.json();
|
|
1410
1466
|
}
|
|
1411
1467
|
|
|
1468
|
+
function validatePairingOptions({ transportMode, publicUrl }) {
|
|
1469
|
+
const normalizedPublicUrl = normalizePublicPairingUrl(publicUrl);
|
|
1470
|
+
if (transportMode !== "manual-http") {
|
|
1471
|
+
return normalizedPublicUrl;
|
|
1472
|
+
}
|
|
1473
|
+
if (!normalizedPublicUrl) {
|
|
1474
|
+
throw new Error(
|
|
1475
|
+
[
|
|
1476
|
+
"Manual HTTP pairing for a physical iPhone requires --public-url.",
|
|
1477
|
+
"Use a phone-reachable Tailscale or LAN Forge URL, for example:",
|
|
1478
|
+
"npx forge-memory pair-ios --manual-http --public-url https://your-mac.tailnet.ts.net/forge/",
|
|
1479
|
+
"For normal pairing, omit --manual-http and use the default Iroh transport."
|
|
1480
|
+
].join(" ")
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
if (isLoopbackPairingUrl(normalizedPublicUrl)) {
|
|
1484
|
+
throw new Error(
|
|
1485
|
+
[
|
|
1486
|
+
`Manual HTTP --public-url points at ${normalizedPublicUrl}, which is loopback-only.`,
|
|
1487
|
+
"A physical iPhone cannot reach localhost on this Mac.",
|
|
1488
|
+
"Use a Tailscale or LAN URL, or omit --manual-http and use Iroh pairing."
|
|
1489
|
+
].join(" ")
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
return normalizedPublicUrl;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1412
1495
|
function compactPairingPayload(payload) {
|
|
1413
1496
|
const transport = payload.transport
|
|
1414
1497
|
? {
|
|
@@ -1426,7 +1509,7 @@ function compactPairingPayload(payload) {
|
|
|
1426
1509
|
notes: []
|
|
1427
1510
|
}
|
|
1428
1511
|
: undefined;
|
|
1429
|
-
return {
|
|
1512
|
+
return compactObject({
|
|
1430
1513
|
kind: payload.kind,
|
|
1431
1514
|
apiBaseUrl: payload.apiBaseUrl,
|
|
1432
1515
|
uiBaseUrl: payload.uiBaseUrl,
|
|
@@ -1436,7 +1519,25 @@ function compactPairingPayload(payload) {
|
|
|
1436
1519
|
pairingToken: payload.pairingToken,
|
|
1437
1520
|
expiresAt: payload.expiresAt,
|
|
1438
1521
|
capabilities: payload.capabilities
|
|
1439
|
-
};
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function compactObject(value) {
|
|
1526
|
+
if (Array.isArray(value)) {
|
|
1527
|
+
const compacted = value.map((entry) => compactObject(entry)).filter((entry) => entry !== undefined);
|
|
1528
|
+
return compacted.length ? compacted : undefined;
|
|
1529
|
+
}
|
|
1530
|
+
if (!value || typeof value !== "object") {
|
|
1531
|
+
return value ?? undefined;
|
|
1532
|
+
}
|
|
1533
|
+
const output = {};
|
|
1534
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1535
|
+
const compacted = compactObject(entry);
|
|
1536
|
+
if (compacted !== undefined && !(Array.isArray(compacted) && compacted.length === 0)) {
|
|
1537
|
+
output[key] = compacted;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return Object.keys(output).length ? output : undefined;
|
|
1440
1541
|
}
|
|
1441
1542
|
|
|
1442
1543
|
async function writePairingPayloadFile(payload) {
|
|
@@ -1462,8 +1563,15 @@ function isLoopbackPairingUrl(value) {
|
|
|
1462
1563
|
async function printPairing(pairing) {
|
|
1463
1564
|
const payload = compactPairingPayload(pairing.qrPayload);
|
|
1464
1565
|
const payloadText = JSON.stringify(payload);
|
|
1465
|
-
|
|
1466
|
-
|
|
1566
|
+
const terminalColumns = process.stdout.columns ?? 120;
|
|
1567
|
+
if (terminalColumns >= 72 && payloadText.length <= 2_950) {
|
|
1568
|
+
console.log("\nScan this compact QR in Forge Companion:\n");
|
|
1569
|
+
qrcode.generate(payloadText, { small: true });
|
|
1570
|
+
} else {
|
|
1571
|
+
console.log("");
|
|
1572
|
+
console.log(color.yellow("QR skipped because the terminal is too narrow or the payload is too large to scan reliably."));
|
|
1573
|
+
console.log("Use Manual connection in the iPhone app and paste the saved payload below.");
|
|
1574
|
+
}
|
|
1467
1575
|
const transport = payload.transport;
|
|
1468
1576
|
if (transport?.provider) {
|
|
1469
1577
|
const label =
|
|
@@ -1694,8 +1802,10 @@ async function runStatus(parsed) {
|
|
|
1694
1802
|
async function doctorCheckRuntime(config, options) {
|
|
1695
1803
|
let result = await health(config);
|
|
1696
1804
|
let repaired = false;
|
|
1805
|
+
let repairRecordPath = null;
|
|
1697
1806
|
if (!result.ok && options.repair && !options.noStart && !options.dryRun) {
|
|
1698
|
-
await
|
|
1807
|
+
const repair = await repairPackagedRuntimeCache(config);
|
|
1808
|
+
repairRecordPath = repair.repairRecordPath ?? null;
|
|
1699
1809
|
result = await health(config, 3_000);
|
|
1700
1810
|
repaired = result.ok;
|
|
1701
1811
|
}
|
|
@@ -1704,9 +1814,10 @@ async function doctorCheckRuntime(config, options) {
|
|
|
1704
1814
|
ok: result.ok,
|
|
1705
1815
|
detail: baseUrl(config),
|
|
1706
1816
|
repaired,
|
|
1817
|
+
repairRecordPath,
|
|
1707
1818
|
guidance: result.ok
|
|
1708
1819
|
? "Forge API is reachable."
|
|
1709
|
-
: `Run npx forge-memory doctor --repair, then inspect ${logPath()} if the runtime still does not start.`
|
|
1820
|
+
: `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.`
|
|
1710
1821
|
};
|
|
1711
1822
|
}
|
|
1712
1823
|
|
|
@@ -1816,6 +1927,11 @@ async function runUi(parsed) {
|
|
|
1816
1927
|
|
|
1817
1928
|
async function runPairIos(parsed) {
|
|
1818
1929
|
const config = await readConfig();
|
|
1930
|
+
const transportMode = parsed.flags.manualHttp ? "manual-http" : "iroh";
|
|
1931
|
+
const publicUrl = validatePairingOptions({
|
|
1932
|
+
transportMode,
|
|
1933
|
+
publicUrl: parsed.values.publicUrl
|
|
1934
|
+
});
|
|
1819
1935
|
if (!parsed.flags.noStart) {
|
|
1820
1936
|
const runtimeResult = await withProgress(
|
|
1821
1937
|
"Starting Forge runtime for iOS pairing",
|
|
@@ -1824,10 +1940,16 @@ async function runPairIos(parsed) {
|
|
|
1824
1940
|
() => startRuntime(config)
|
|
1825
1941
|
);
|
|
1826
1942
|
assertRuntimeStartedForPairing(runtimeResult, config);
|
|
1943
|
+
} else {
|
|
1944
|
+
const currentHealth = await health(config, 3_000);
|
|
1945
|
+
assertRuntimeStartedForPairing(
|
|
1946
|
+
{ ok: currentHealth.ok, started: false, health: currentHealth },
|
|
1947
|
+
config
|
|
1948
|
+
);
|
|
1827
1949
|
}
|
|
1828
1950
|
const pairing = await createPairing(config, {
|
|
1829
|
-
transportMode
|
|
1830
|
-
publicUrl
|
|
1951
|
+
transportMode,
|
|
1952
|
+
publicUrl
|
|
1831
1953
|
});
|
|
1832
1954
|
if (parsed.flags.json) {
|
|
1833
1955
|
console.log(JSON.stringify(pairing, null, 2));
|
package/package.json
CHANGED