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.
Files changed (2) hide show
  1. package/bin/forge-memory.mjs +136 -14
  2. package/package.json +1 -1
@@ -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 = [require];
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 = resolveOpenClawPluginRoot();
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 = normalizePublicPairingUrl(options.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
- console.log("\nScan this compact QR in Forge Companion:\n");
1466
- qrcode.generate(payloadText, { small: true });
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 startRuntime(config);
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: parsed.flags.manualHttp ? "manual-http" : "iroh",
1830
- publicUrl: parsed.values.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-memory",
3
- "version": "0.2.108",
3
+ "version": "0.2.109",
4
4
  "description": "Guided Forge installer and local runtime manager for the Forge UI, OpenClaw, Hermes, Codex, and iOS pairing.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",