forge-memory 0.3.2 → 0.3.3

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 +194 -50
  2. package/package.json +1 -1
@@ -427,7 +427,9 @@ function tailscaleInstallPlan() {
427
427
 
428
428
  function tailscaleAutodetectDisabled() {
429
429
  return ["1", "true", "yes"].includes(
430
- String(process.env.FORGE_MEMORY_SKIP_TAILSCALE_AUTODETECT ?? "").toLowerCase()
430
+ String(
431
+ process.env.FORGE_MEMORY_SKIP_TAILSCALE_AUTODETECT ?? ""
432
+ ).toLowerCase()
431
433
  );
432
434
  }
433
435
 
@@ -439,7 +441,9 @@ function parseTailscaleStatus(raw) {
439
441
  const dnsName = String(self?.DNSName ?? self?.dnsName ?? "")
440
442
  .trim()
441
443
  .replace(/\.$/, "");
442
- const backendState = String(payload.BackendState ?? payload.backendState ?? "");
444
+ const backendState = String(
445
+ payload.BackendState ?? payload.backendState ?? ""
446
+ );
443
447
  const running = backendState.toLowerCase() === "running";
444
448
  return {
445
449
  running,
@@ -456,7 +460,11 @@ function normalizeForgePublicUiUrl(value) {
456
460
  const normalized = normalizePublicPairingUrl(value);
457
461
  if (!normalized) return null;
458
462
  const url = new URL(normalized);
459
- if (!url.pathname || url.pathname === "/" || url.pathname.startsWith("/api/")) {
463
+ if (
464
+ !url.pathname ||
465
+ url.pathname === "/" ||
466
+ url.pathname.startsWith("/api/")
467
+ ) {
460
468
  url.pathname = "/forge/";
461
469
  }
462
470
  if (!url.pathname.endsWith("/")) url.pathname = `${url.pathname}/`;
@@ -501,8 +509,12 @@ function detectTailscaleState() {
501
509
  installPlan: tailscaleInstallPlan()
502
510
  };
503
511
  }
504
- const status = parseTailscaleStatus(runCapture("tailscale", ["status", "--json"], 4_000));
505
- const envPublicUrl = normalizeForgePublicUiUrl(process.env.FORGE_MEMORY_TAILSCALE_PUBLIC_URL);
512
+ const status = parseTailscaleStatus(
513
+ runCapture("tailscale", ["status", "--json"], 4_000)
514
+ );
515
+ const envPublicUrl = normalizeForgePublicUiUrl(
516
+ process.env.FORGE_MEMORY_TAILSCALE_PUBLIC_URL
517
+ );
506
518
  const publicUrl =
507
519
  envPublicUrl ??
508
520
  (status.dnsName ? `https://${status.dnsName}/forge/` : null);
@@ -529,14 +541,22 @@ async function probePublicForgeUrl(publicUrl, timeoutMs = 4_000) {
529
541
  .map((entry) => entry.trim())
530
542
  .filter(Boolean);
531
543
  const current = entries.shift() ?? "fail";
532
- fs.writeFileSync(fakeProbeSequencePath, entries.length ? `${entries.join("\n")}\n` : "");
544
+ fs.writeFileSync(
545
+ fakeProbeSequencePath,
546
+ entries.length ? `${entries.join("\n")}\n` : ""
547
+ );
533
548
  return current === "ok"
534
549
  ? { ok: true, fake: true }
535
- : { ok: false, error: current === "fail" ? "fake probe failure" : current };
550
+ : {
551
+ ok: false,
552
+ error: current === "fail" ? "fake probe failure" : current
553
+ };
536
554
  }
537
555
  if (
538
556
  ["1", "true", "yes"].includes(
539
- String(process.env.FORGE_MEMORY_SKIP_TAILSCALE_PUBLIC_PROBE ?? "").toLowerCase()
557
+ String(
558
+ process.env.FORGE_MEMORY_SKIP_TAILSCALE_PUBLIC_PROBE ?? ""
559
+ ).toLowerCase()
540
560
  )
541
561
  ) {
542
562
  return { ok: true, skipped: true };
@@ -551,7 +571,11 @@ async function probePublicForgeUrl(publicUrl, timeoutMs = 4_000) {
551
571
  if (!response.ok) return { ok: false, status: response.status };
552
572
  const payload = await response.json().catch(() => null);
553
573
  if (!isForgeHealthPayload(payload)) {
554
- return { ok: false, status: response.status, error: "non-Forge health payload" };
574
+ return {
575
+ ok: false,
576
+ status: response.status,
577
+ error: "non-Forge health payload"
578
+ };
555
579
  }
556
580
  return { ok: true, status: response.status };
557
581
  } catch (error) {
@@ -854,7 +878,9 @@ async function withProgress(title, detail, options, task) {
854
878
 
855
879
  function printStep(title, detail, options = {}) {
856
880
  if (!progressEnabled(options)) return;
857
- console.log(`${color.cyan("->")} ${title}${detail ? color.dim(` ${detail}`) : ""}`);
881
+ console.log(
882
+ `${color.cyan("->")} ${title}${detail ? color.dim(` ${detail}`) : ""}`
883
+ );
858
884
  }
859
885
 
860
886
  async function promptLine(question, defaultValue) {
@@ -1128,7 +1154,10 @@ async function buildInstallConfig(parsed, currentConfig, discovery, command) {
1128
1154
  const origin = parsed.values.origin ?? currentConfig.origin ?? DEFAULT_ORIGIN;
1129
1155
  const runtimeTarget = await resolveInstallRuntimeTarget({
1130
1156
  origin,
1131
- requestedPort: normalizePort(parsed.values.port ?? currentConfig.port, DEFAULT_PORT),
1157
+ requestedPort: normalizePort(
1158
+ parsed.values.port ?? currentConfig.port,
1159
+ DEFAULT_PORT
1160
+ ),
1132
1161
  requestedWebPort: normalizePort(
1133
1162
  parsed.values.webPort ?? currentConfig.webPort,
1134
1163
  DEFAULT_WEB_PORT
@@ -1269,7 +1298,8 @@ function stripCodexForgeMcpConfig(source) {
1269
1298
  const tableMatch = trimmed.match(/^\[([^\]]+)\]$/);
1270
1299
  if (tableMatch) {
1271
1300
  currentTable = tableMatch[1];
1272
- skippingForgeTable = currentTable === "mcp_servers.forge" ||
1301
+ skippingForgeTable =
1302
+ currentTable === "mcp_servers.forge" ||
1273
1303
  currentTable.startsWith("mcp_servers.forge.");
1274
1304
  if (skippingForgeTable) continue;
1275
1305
  }
@@ -1285,7 +1315,10 @@ function stripCodexForgeMcpConfig(source) {
1285
1315
  }
1286
1316
  kept.push(line);
1287
1317
  }
1288
- return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
1318
+ return kept
1319
+ .join("\n")
1320
+ .replace(/\n{3,}/g, "\n\n")
1321
+ .trimEnd();
1289
1322
  }
1290
1323
 
1291
1324
  async function runCommand(
@@ -1478,7 +1511,8 @@ function describeNetworkError(error) {
1478
1511
  }
1479
1512
 
1480
1513
  function describeHealthResult(result) {
1481
- if (result.ok && result.forge === false) return "HTTP 200 from a non-Forge service";
1514
+ if (result.ok && result.forge === false)
1515
+ return "HTTP 200 from a non-Forge service";
1482
1516
  if (result.ok) return "healthy";
1483
1517
  if (result.status) return `HTTP ${result.status}`;
1484
1518
  if (result.error) return result.error;
@@ -1502,6 +1536,45 @@ function processExists(pid) {
1502
1536
  }
1503
1537
  }
1504
1538
 
1539
+ function signalProcess(pid, signal = "SIGTERM") {
1540
+ try {
1541
+ process.kill(pid, signal);
1542
+ return true;
1543
+ } catch {
1544
+ return false;
1545
+ }
1546
+ }
1547
+
1548
+ function signalDetachedProcessGroup(pid, signal = "SIGTERM") {
1549
+ if (process.platform === "win32") return false;
1550
+ try {
1551
+ process.kill(-pid, signal);
1552
+ return true;
1553
+ } catch {
1554
+ return false;
1555
+ }
1556
+ }
1557
+
1558
+ async function waitForProcessExit(pid, timeoutMs = 1_500) {
1559
+ const deadline = Date.now() + timeoutMs;
1560
+ while (Date.now() < deadline) {
1561
+ if (!processExists(pid)) return true;
1562
+ await new Promise((resolve) => setTimeout(resolve, 100));
1563
+ }
1564
+ return !processExists(pid);
1565
+ }
1566
+
1567
+ async function stopRecordedRuntimeProcess(pid) {
1568
+ if (!Number.isInteger(pid) || pid <= 0 || !processExists(pid)) return false;
1569
+ const signaled =
1570
+ signalDetachedProcessGroup(pid, "SIGTERM") || signalProcess(pid, "SIGTERM");
1571
+ if (!signaled) return false;
1572
+ if (await waitForProcessExit(pid)) return true;
1573
+ signalDetachedProcessGroup(pid, "SIGKILL") || signalProcess(pid, "SIGKILL");
1574
+ await waitForProcessExit(pid, 500);
1575
+ return true;
1576
+ }
1577
+
1505
1578
  async function waitForHealth(config, timeoutMs = 30_000) {
1506
1579
  const deadline = Date.now() + timeoutMs;
1507
1580
  while (Date.now() < deadline) {
@@ -1540,9 +1613,7 @@ function resolveOpenClawPluginRoot(options = {}) {
1540
1613
  }
1541
1614
 
1542
1615
  async function ensurePackagedRuntimeInstalled(options = {}) {
1543
- const existing = options.forceInstall
1544
- ? null
1545
- : resolveOpenClawPluginRoot();
1616
+ const existing = options.forceInstall ? null : resolveOpenClawPluginRoot();
1546
1617
  if (existing) return existing;
1547
1618
  const installRoot = runtimeInstallRoot();
1548
1619
  await fsp.mkdir(installRoot, { recursive: true });
@@ -1616,7 +1687,9 @@ async function repairPackagedRuntimeCache(config) {
1616
1687
  const rotatedLogPath = await rotateRuntimeLog("repair");
1617
1688
  await fsp.rm(runtimeStatePath(), { force: true });
1618
1689
  await fsp.rm(runtimeInstallRoot(), { recursive: true, force: true });
1619
- const pluginRoot = await ensurePackagedRuntimeInstalled({ forceInstall: true });
1690
+ const pluginRoot = await ensurePackagedRuntimeInstalled({
1691
+ forceInstall: true
1692
+ });
1620
1693
  const result = await startRuntime(config);
1621
1694
  const record = {
1622
1695
  repairedAt: new Date().toISOString(),
@@ -1631,9 +1704,17 @@ async function repairPackagedRuntimeCache(config) {
1631
1704
  health: result.health ?? { ok: result.ok }
1632
1705
  };
1633
1706
  const stamp = record.repairedAt.replace(/[:.]/g, "-");
1634
- const repairRecordPath = path.join(forgeHome(), "run", `runtime-repair-${stamp}.json`);
1707
+ const repairRecordPath = path.join(
1708
+ forgeHome(),
1709
+ "run",
1710
+ `runtime-repair-${stamp}.json`
1711
+ );
1635
1712
  await fsp.mkdir(path.dirname(repairRecordPath), { recursive: true });
1636
- await fsp.writeFile(repairRecordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
1713
+ await fsp.writeFile(
1714
+ repairRecordPath,
1715
+ `${JSON.stringify(record, null, 2)}\n`,
1716
+ "utf8"
1717
+ );
1637
1718
  return { ...record, repairRecordPath };
1638
1719
  }
1639
1720
 
@@ -1805,8 +1886,7 @@ async function stopRuntime(config = null) {
1805
1886
  const stopped = [];
1806
1887
  for (const child of state?.children ?? []) {
1807
1888
  if (!child?.pid || !processExists(child.pid)) continue;
1808
- process.kill(child.pid, "SIGTERM");
1809
- stopped.push(child.pid);
1889
+ if (await stopRecordedRuntimeProcess(child.pid)) stopped.push(child.pid);
1810
1890
  }
1811
1891
  const current = await health(effectiveConfig);
1812
1892
  const runtimePid = Number(current?.payload?.runtime?.pid);
@@ -1817,8 +1897,7 @@ async function stopRuntime(config = null) {
1817
1897
  !stopped.includes(runtimePid) &&
1818
1898
  processExists(runtimePid)
1819
1899
  ) {
1820
- process.kill(runtimePid, "SIGTERM");
1821
- stopped.push(runtimePid);
1900
+ if (await stopRecordedRuntimeProcess(runtimePid)) stopped.push(runtimePid);
1822
1901
  }
1823
1902
  await fsp.rm(runtimeStatePath(), { force: true });
1824
1903
  if (stopped.length === 0) {
@@ -1953,7 +2032,9 @@ async function removeHermesAdapterConfig() {
1953
2032
  }
1954
2033
  const forgeConfigDir = path.dirname(forgeConfigPath);
1955
2034
  if (fs.existsSync(forgeConfigDir)) {
1956
- await fsp.rm(forgeConfigDir, { recursive: false, force: true }).catch(() => {});
2035
+ await fsp
2036
+ .rm(forgeConfigDir, { recursive: false, force: true })
2037
+ .catch(() => {});
1957
2038
  }
1958
2039
 
1959
2040
  const hermesYamlPath = path.join(homeDir(), ".hermes", "config.yaml");
@@ -2169,7 +2250,10 @@ async function bootstrapLocalOperatorSession(config) {
2169
2250
 
2170
2251
  async function createPairing(config, options = {}) {
2171
2252
  const transportMode = options.transportMode ?? "iroh";
2172
- const publicUrl = validatePairingOptions({ transportMode, publicUrl: options.publicUrl });
2253
+ const publicUrl = validatePairingOptions({
2254
+ transportMode,
2255
+ publicUrl: options.publicUrl
2256
+ });
2173
2257
  const pairingUrl = forgeApiUrl(config, "/api/v1/health/pairing-sessions");
2174
2258
  const operatorCookie = await bootstrapLocalOperatorSession(config);
2175
2259
  let response;
@@ -2221,7 +2305,9 @@ async function createPairing(config, options = {}) {
2221
2305
  );
2222
2306
  }
2223
2307
  const pairing = await response.json();
2224
- assertPairingTransportUsable(pairing, { requestedTransportMode: transportMode });
2308
+ assertPairingTransportUsable(pairing, {
2309
+ requestedTransportMode: transportMode
2310
+ });
2225
2311
  return pairing;
2226
2312
  }
2227
2313
 
@@ -2242,7 +2328,10 @@ async function maybeInstallTailscaleForPairing(parsed) {
2242
2328
  true
2243
2329
  );
2244
2330
  if (!shouldInstall) return { installed: false, guidance: plan.guidance };
2245
- const result = await runCommand(plan.autoInstallCommand.command, plan.autoInstallCommand.args);
2331
+ const result = await runCommand(
2332
+ plan.autoInstallCommand.command,
2333
+ plan.autoInstallCommand.args
2334
+ );
2246
2335
  return { installed: result.ok, guidance: plan.guidance, result };
2247
2336
  }
2248
2337
 
@@ -2253,7 +2342,11 @@ async function resolveTailscalePairingOptions(parsed, config) {
2253
2342
  const installAttempt = await maybeInstallTailscaleForPairing(parsed);
2254
2343
  if (installAttempt.installed) {
2255
2344
  const installedState = detectTailscaleState();
2256
- if (installedState.installed && installedState.running && installedState.authenticated) {
2345
+ if (
2346
+ installedState.installed &&
2347
+ installedState.running &&
2348
+ installedState.authenticated
2349
+ ) {
2257
2350
  return resolveTailscalePairingOptions(parsed, config);
2258
2351
  }
2259
2352
  }
@@ -2268,7 +2361,11 @@ async function resolveTailscalePairingOptions(parsed, config) {
2268
2361
  "Tailscale is installed, but it is not running/authenticated or does not expose a MagicDNS name."
2269
2362
  )
2270
2363
  );
2271
- console.log(color.dim("Open Tailscale, sign in, then rerun npx forge-memory pair-ios. Falling back to Iroh for this pairing."));
2364
+ console.log(
2365
+ color.dim(
2366
+ "Open Tailscale, sign in, then rerun npx forge-memory pair-ios. Falling back to Iroh for this pairing."
2367
+ )
2368
+ );
2272
2369
  }
2273
2370
  return null;
2274
2371
  }
@@ -2276,7 +2373,9 @@ async function resolveTailscalePairingOptions(parsed, config) {
2276
2373
  const firstProbe = await probePublicForgeUrl(state.publicUrl);
2277
2374
  if (firstProbe.ok) {
2278
2375
  if (!parsed.flags.json) {
2279
- console.log(color.green(`Using Tailscale for iOS pairing: ${state.publicUrl}`));
2376
+ console.log(
2377
+ color.green(`Using Tailscale for iOS pairing: ${state.publicUrl}`)
2378
+ );
2280
2379
  }
2281
2380
  return {
2282
2381
  transportMode: "manual-http",
@@ -2334,7 +2433,9 @@ async function resolveTailscalePairingOptions(parsed, config) {
2334
2433
  return null;
2335
2434
  }
2336
2435
  if (!parsed.flags.json) {
2337
- console.log(color.green(`Using Tailscale for iOS pairing: ${state.publicUrl}`));
2436
+ console.log(
2437
+ color.green(`Using Tailscale for iOS pairing: ${state.publicUrl}`)
2438
+ );
2338
2439
  }
2339
2440
  return {
2340
2441
  transportMode: "manual-http",
@@ -2367,7 +2468,10 @@ async function resolveIosPairingOptions(parsed, config = null) {
2367
2468
  };
2368
2469
  }
2369
2470
  if (config) {
2370
- const tailscalePairing = await resolveTailscalePairingOptions(parsed, config);
2471
+ const tailscalePairing = await resolveTailscalePairingOptions(
2472
+ parsed,
2473
+ config
2474
+ );
2371
2475
  if (tailscalePairing) return tailscalePairing;
2372
2476
  }
2373
2477
  if (parsed.flags.yes || parsed.flags.json || !process.stdin.isTTY) {
@@ -2381,7 +2485,9 @@ async function resolveIosPairingOptions(parsed, config = null) {
2381
2485
  }
2382
2486
  console.log(color.bold("iOS companion connection"));
2383
2487
  console.log(color.dim(tailscalePreferredMessage()));
2384
- console.log(color.dim("Choose Iroh or a fixed/private IP fallback for this pairing."));
2488
+ console.log(
2489
+ color.dim("Choose Iroh or a fixed/private IP fallback for this pairing.")
2490
+ );
2385
2491
  const choice = (
2386
2492
  await promptLine("Connection [iroh/ip]", "iroh")
2387
2493
  ).toLowerCase();
@@ -2412,7 +2518,8 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
2412
2518
  if (!payload || requestedTransportMode !== "iroh") {
2413
2519
  return;
2414
2520
  }
2415
- const resolvedTransportMode = payload.transportMode ?? payload.transport?.protocol;
2521
+ const resolvedTransportMode =
2522
+ payload.transportMode ?? payload.transport?.protocol;
2416
2523
  const resolvedProtocol = payload.transport?.protocol;
2417
2524
  if (resolvedTransportMode === "iroh" || resolvedProtocol === "iroh") {
2418
2525
  const phoneFacingUrls = [
@@ -2420,7 +2527,9 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
2420
2527
  payload.uiBaseUrl,
2421
2528
  payload.transport?.publicBaseUrl
2422
2529
  ].filter(Boolean);
2423
- const loopbackUrl = phoneFacingUrls.find((url) => isLoopbackPairingUrl(url));
2530
+ const loopbackUrl = phoneFacingUrls.find((url) =>
2531
+ isLoopbackPairingUrl(url)
2532
+ );
2424
2533
  if (loopbackUrl) {
2425
2534
  throw new PairingTransportUnavailableError(
2426
2535
  [
@@ -2429,14 +2538,20 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
2429
2538
  "A physical iPhone cannot reach localhost on this Mac.",
2430
2539
  "Use Iroh logical URLs, a selected Tailscale URL, or a selected private/fixed IP URL."
2431
2540
  ].join(" "),
2432
- { apiBaseUrl: payload.apiBaseUrl, transportMode: resolvedTransportMode, protocol: resolvedProtocol }
2541
+ {
2542
+ apiBaseUrl: payload.apiBaseUrl,
2543
+ transportMode: resolvedTransportMode,
2544
+ protocol: resolvedProtocol
2545
+ }
2433
2546
  );
2434
2547
  }
2435
2548
  return;
2436
2549
  }
2437
2550
  const apiBaseUrl = payload.apiBaseUrl ?? "";
2438
2551
  const lastError = payload.transport?.lastError;
2439
- const notes = Array.isArray(payload.transport?.notes) ? payload.transport.notes : [];
2552
+ const notes = Array.isArray(payload.transport?.notes)
2553
+ ? payload.transport.notes
2554
+ : [];
2440
2555
  throw new PairingTransportUnavailableError(
2441
2556
  [
2442
2557
  "Forge created a direct HTTP pairing while default iOS pairing requested Iroh.",
@@ -2450,7 +2565,11 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
2450
2565
  ]
2451
2566
  .filter(Boolean)
2452
2567
  .join(" "),
2453
- { apiBaseUrl, transportMode: resolvedTransportMode, protocol: resolvedProtocol }
2568
+ {
2569
+ apiBaseUrl,
2570
+ transportMode: resolvedTransportMode,
2571
+ protocol: resolvedProtocol
2572
+ }
2454
2573
  );
2455
2574
  }
2456
2575
 
@@ -2555,7 +2674,9 @@ function compactQrPairingPayload(payload) {
2555
2674
 
2556
2675
  function compactObject(value) {
2557
2676
  if (Array.isArray(value)) {
2558
- const compacted = value.map((entry) => compactObject(entry)).filter((entry) => entry !== undefined);
2677
+ const compacted = value
2678
+ .map((entry) => compactObject(entry))
2679
+ .filter((entry) => entry !== undefined);
2559
2680
  return compacted.length ? compacted : undefined;
2560
2681
  }
2561
2682
  if (!value || typeof value !== "object") {
@@ -2564,7 +2685,10 @@ function compactObject(value) {
2564
2685
  const output = {};
2565
2686
  for (const [key, entry] of Object.entries(value)) {
2566
2687
  const compacted = compactObject(entry);
2567
- if (compacted !== undefined && !(Array.isArray(compacted) && compacted.length === 0)) {
2688
+ if (
2689
+ compacted !== undefined &&
2690
+ !(Array.isArray(compacted) && compacted.length === 0)
2691
+ ) {
2568
2692
  output[key] = compacted;
2569
2693
  }
2570
2694
  }
@@ -2578,7 +2702,11 @@ async function writePairingPayloadFile(payload) {
2578
2702
  pairingDir,
2579
2703
  `forge-companion-${payload.sessionId}.json`
2580
2704
  );
2581
- await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
2705
+ await fsp.writeFile(
2706
+ filePath,
2707
+ `${JSON.stringify(payload, null, 2)}\n`,
2708
+ "utf8"
2709
+ );
2582
2710
  return filePath;
2583
2711
  }
2584
2712
 
@@ -2601,8 +2729,14 @@ async function printPairing(pairing) {
2601
2729
  qrcode.generate(qrPayloadText, { small: true });
2602
2730
  } else {
2603
2731
  console.log("");
2604
- console.log(color.yellow("QR skipped because the terminal is too narrow or the payload is too large to scan reliably."));
2605
- console.log("Use Manual connection in the iPhone app and paste the saved payload below.");
2732
+ console.log(
2733
+ color.yellow(
2734
+ "QR skipped because the terminal is too narrow or the payload is too large to scan reliably."
2735
+ )
2736
+ );
2737
+ console.log(
2738
+ "Use Manual connection in the iPhone app and paste the saved payload below."
2739
+ );
2606
2740
  }
2607
2741
  const transport = manualPayload.transport;
2608
2742
  if (transport?.provider) {
@@ -2613,7 +2747,10 @@ async function printPairing(pairing) {
2613
2747
  ? "Iroh"
2614
2748
  : "Manual HTTP";
2615
2749
  console.log(`${color.cyan(label)}: ${manualPayload.apiBaseUrl}`);
2616
- if (label === "Manual HTTP" && isLoopbackPairingUrl(manualPayload.apiBaseUrl)) {
2750
+ if (
2751
+ label === "Manual HTTP" &&
2752
+ isLoopbackPairingUrl(manualPayload.apiBaseUrl)
2753
+ ) {
2617
2754
  console.log(
2618
2755
  color.yellow(
2619
2756
  "Manual HTTP points at this machine's loopback address. That is only useful for the iOS Simulator; a real iPhone needs Iroh, Tailscale, or a LAN URL passed with --public-url."
@@ -2627,7 +2764,9 @@ async function printPairing(pairing) {
2627
2764
  try {
2628
2765
  const filePath = await writePairingPayloadFile(manualPayload);
2629
2766
  console.log("");
2630
- console.log(color.bold("If the QR is too large or the camera will not scan:"));
2767
+ console.log(
2768
+ color.bold("If the QR is too large or the camera will not scan:")
2769
+ );
2631
2770
  console.log("1. Open Manual connection in the iPhone app.");
2632
2771
  console.log("2. Tap Paste pairing payload.");
2633
2772
  console.log(`3. Paste the payload saved at: ${filePath}`);
@@ -2773,7 +2912,9 @@ async function runInstall(parsed, command) {
2773
2912
  assertRuntimeStartedForPairing(runtimeResult, config);
2774
2913
  pairing = await withProgress(
2775
2914
  "Creating iOS companion pairing",
2776
- pairingOptions.transportMode === "manual-http" ? "phone-reachable HTTP" : "Iroh QR",
2915
+ pairingOptions.transportMode === "manual-http"
2916
+ ? "phone-reachable HTTP"
2917
+ : "Iroh QR",
2777
2918
  parsed.flags,
2778
2919
  () =>
2779
2920
  createPairing(config, {
@@ -2894,7 +3035,7 @@ async function doctorCheckRuntime(config, options) {
2894
3035
  ? "Forge API is reachable."
2895
3036
  : result.ok && result.forge === false
2896
3037
  ? `Port ${config.port || DEFAULT_PORT} responded, but not with Forge runtime health. Stop the conflicting process or choose another --port.`
2897
- : `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.`
3038
+ : `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.`
2898
3039
  };
2899
3040
  }
2900
3041
 
@@ -3038,10 +3179,13 @@ async function runPairIos(parsed) {
3038
3179
  const pairingOptions =
3039
3180
  explicitPairingOptions ??
3040
3181
  noStartPairingOptions ??
3041
- await resolveIosPairingOptions(parsed, config);
3182
+ (await resolveIosPairingOptions(parsed, config));
3042
3183
  const transportMode = pairingOptions.transportMode;
3043
3184
  const publicUrl = pairingOptions.publicUrl;
3044
- if (transportMode === "iroh" && noStartPairingOptions?.transportMode !== "iroh") {
3185
+ if (
3186
+ transportMode === "iroh" &&
3187
+ noStartPairingOptions?.transportMode !== "iroh"
3188
+ ) {
3045
3189
  await withProgress(
3046
3190
  "Preparing Forge Companion Iroh transport",
3047
3191
  "checking Rust/Cargo and building the local host",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-memory",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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",