forge-memory 0.2.111 → 0.2.113

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/README.md CHANGED
@@ -40,8 +40,9 @@ show up as a tool result instead of a closed MCP transport.
40
40
  `pair-ios` prefers the Iroh QR. Forge starts a Rust Iroh host, prints a QR payload
41
41
  with the desktop node id, pairing token, optional relay hint, and ALPN
42
42
  `forge-companion/1`, and the iPhone app connects through its native Rust bridge. The
43
- CLI renders a compact QR and saves the same compact payload under
44
- `~/.forge/pairing/` so you can paste it into the iPhone app if the camera cannot scan.
43
+ CLI renders a short-schema QR to keep the terminal code scannable and saves the
44
+ full manual payload under `~/.forge/pairing/` so you can paste it into the iPhone
45
+ app if the camera cannot scan.
45
46
  Use `--manual-http` only when you intentionally want a LAN, Tailscale, or direct
46
47
  HTTP/TCP route. For a real iPhone, pass a phone-reachable URL:
47
48
 
@@ -54,7 +55,11 @@ the iOS Simulator but not for a physical phone.
54
55
 
55
56
  The base install stays one command on purpose. The detailed companion transport
56
57
  reference lives in the Forge repo at `docs/companion-iroh.md` and in the published
57
- docs at `https://albertbuchard.github.io/forge/companion-transport.html`.
58
+ docs at `https://albertbuchard.github.io/forge/companion-transport.html`. Forge
59
+ Memory ships prebuilt Iroh host binaries for common desktop platforms and a bundled
60
+ Rust source fallback for other machines. If neither a prebuilt host nor Cargo is
61
+ available, `pair-ios` stops with transport-specific repair guidance instead of
62
+ printing a localhost QR that a physical iPhone cannot use.
58
63
 
59
64
  `configure` reruns the full guided flow using the current config as defaults.
60
65
  Install and configure run Forge doctor before finishing. `doctor --repair` creates
@@ -1616,6 +1616,22 @@ class PairingRequestError extends Error {
1616
1616
  }
1617
1617
  }
1618
1618
 
1619
+ class PairingTransportUnavailableError extends Error {
1620
+ constructor(message, detail = {}) {
1621
+ super(message);
1622
+ this.name = "PairingTransportUnavailableError";
1623
+ this.code = "pairing_transport_unavailable";
1624
+ this.detail = detail;
1625
+ this.guidance = [
1626
+ "Run npx forge-memory doctor --repair so Forge Memory refreshes the packaged runtime and companion transport files.",
1627
+ "Then rerun npx forge-memory pair-ios.",
1628
+ "On unsupported platforms, install Rust/Cargo so Forge can build the bundled companion Iroh source fallback.",
1629
+ "For an explicit Tailscale or LAN fallback, rerun with npx forge-memory pair-ios --manual-http --public-url <phone-reachable Forge URL>.",
1630
+ "Do not scan a QR whose API URL is 127.0.0.1 on a physical iPhone; that address only works in the iOS Simulator."
1631
+ ];
1632
+ }
1633
+ }
1634
+
1619
1635
  async function bootstrapLocalOperatorSession(config) {
1620
1636
  const sessionUrl = forgeApiUrl(config, "/api/v1/auth/operator-session");
1621
1637
  let response;
@@ -1706,7 +1722,39 @@ async function createPairing(config, options = {}) {
1706
1722
  { url: pairingUrl.toString(), status: response.status }
1707
1723
  );
1708
1724
  }
1709
- return response.json();
1725
+ const pairing = await response.json();
1726
+ assertPairingTransportUsable(pairing, { requestedTransportMode: transportMode });
1727
+ return pairing;
1728
+ }
1729
+
1730
+ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
1731
+ const payload = pairing?.qrPayload;
1732
+ if (!payload || requestedTransportMode !== "iroh") {
1733
+ return;
1734
+ }
1735
+ const resolvedTransportMode = payload.transportMode ?? payload.transport?.protocol;
1736
+ const resolvedProtocol = payload.transport?.protocol;
1737
+ if (resolvedTransportMode === "iroh" || resolvedProtocol === "iroh") {
1738
+ return;
1739
+ }
1740
+ const apiBaseUrl = payload.apiBaseUrl ?? "";
1741
+ const lastError = payload.transport?.lastError;
1742
+ const notes = Array.isArray(payload.transport?.notes) ? payload.transport.notes : [];
1743
+ throw new PairingTransportUnavailableError(
1744
+ [
1745
+ "Forge created a direct HTTP pairing while default iOS pairing requested Iroh.",
1746
+ isLoopbackPairingUrl(apiBaseUrl)
1747
+ ? `The generated API URL is ${apiBaseUrl}, which a physical iPhone cannot reach.`
1748
+ : apiBaseUrl
1749
+ ? `The generated API URL is ${apiBaseUrl}, but this was not an Iroh pairing.`
1750
+ : "The response did not include a usable Iroh API URL.",
1751
+ lastError ? `Iroh error: ${lastError}` : "",
1752
+ notes.length ? `Notes: ${notes.join(" ")}` : ""
1753
+ ]
1754
+ .filter(Boolean)
1755
+ .join(" "),
1756
+ { apiBaseUrl, transportMode: resolvedTransportMode, protocol: resolvedProtocol }
1757
+ );
1710
1758
  }
1711
1759
 
1712
1760
  function validatePairingOptions({ transportMode, publicUrl }) {
@@ -1766,6 +1814,48 @@ function compactPairingPayload(payload) {
1766
1814
  });
1767
1815
  }
1768
1816
 
1817
+ function compactQrPairingPayload(payload) {
1818
+ const manualPayload = compactPairingPayload(payload);
1819
+ const transport = manualPayload.transport
1820
+ ? compactObject({
1821
+ p: manualPayload.transport.protocol,
1822
+ d: manualPayload.transport.provider,
1823
+ s: manualPayload.transport.status,
1824
+ pb: manualPayload.transport.publicBaseUrl,
1825
+ lb: manualPayload.transport.localBaseUrl,
1826
+ n: manualPayload.transport.nodeId,
1827
+ r: manualPayload.transport.relay,
1828
+ a: manualPayload.transport.alpn,
1829
+ g: manualPayload.transport.agent,
1830
+ pp: manualPayload.transport.pairPayload
1831
+ ? compactObject({
1832
+ v: manualPayload.transport.pairPayload.v,
1833
+ n:
1834
+ manualPayload.transport.pairPayload.node_id ??
1835
+ manualPayload.transport.pairPayload.nodeId,
1836
+ t: manualPayload.transport.pairPayload.token,
1837
+ h:
1838
+ manualPayload.transport.pairPayload.host_name ??
1839
+ manualPayload.transport.pairPayload.hostName,
1840
+ r: manualPayload.transport.pairPayload.relay
1841
+ })
1842
+ : undefined,
1843
+ le: manualPayload.transport.lastError
1844
+ })
1845
+ : undefined;
1846
+ return compactObject({
1847
+ k: "fcp1",
1848
+ a: manualPayload.apiBaseUrl,
1849
+ u: manualPayload.uiBaseUrl,
1850
+ m: manualPayload.transportMode,
1851
+ t: transport,
1852
+ s: manualPayload.sessionId,
1853
+ pt: manualPayload.pairingToken,
1854
+ e: manualPayload.expiresAt,
1855
+ c: manualPayload.capabilities
1856
+ });
1857
+ }
1858
+
1769
1859
  function compactObject(value) {
1770
1860
  if (Array.isArray(value)) {
1771
1861
  const compacted = value.map((entry) => compactObject(entry)).filter((entry) => entry !== undefined);
@@ -1805,27 +1895,28 @@ function isLoopbackPairingUrl(value) {
1805
1895
  }
1806
1896
 
1807
1897
  async function printPairing(pairing) {
1808
- const payload = compactPairingPayload(pairing.qrPayload);
1809
- const payloadText = JSON.stringify(payload);
1898
+ const manualPayload = compactPairingPayload(pairing.qrPayload);
1899
+ const qrPayload = compactQrPairingPayload(pairing.qrPayload);
1900
+ const qrPayloadText = JSON.stringify(qrPayload);
1810
1901
  const terminalColumns = process.stdout.columns ?? 120;
1811
- if (terminalColumns >= 72 && payloadText.length <= 2_950) {
1902
+ if (terminalColumns >= 72 && qrPayloadText.length <= 1_500) {
1812
1903
  console.log("\nScan this compact QR in Forge Companion:\n");
1813
- qrcode.generate(payloadText, { small: true });
1904
+ qrcode.generate(qrPayloadText, { small: true });
1814
1905
  } else {
1815
1906
  console.log("");
1816
1907
  console.log(color.yellow("QR skipped because the terminal is too narrow or the payload is too large to scan reliably."));
1817
1908
  console.log("Use Manual connection in the iPhone app and paste the saved payload below.");
1818
1909
  }
1819
- const transport = payload.transport;
1910
+ const transport = manualPayload.transport;
1820
1911
  if (transport?.provider) {
1821
1912
  const label =
1822
- payload.transport?.protocol === "iroh"
1913
+ manualPayload.transport?.protocol === "iroh"
1823
1914
  ? "Iroh"
1824
- : payload.transportMode === "iroh"
1915
+ : manualPayload.transportMode === "iroh"
1825
1916
  ? "Iroh"
1826
1917
  : "Manual HTTP";
1827
- console.log(`${color.cyan(label)}: ${payload.apiBaseUrl}`);
1828
- if (label === "Manual HTTP" && isLoopbackPairingUrl(payload.apiBaseUrl)) {
1918
+ console.log(`${color.cyan(label)}: ${manualPayload.apiBaseUrl}`);
1919
+ if (label === "Manual HTTP" && isLoopbackPairingUrl(manualPayload.apiBaseUrl)) {
1829
1920
  console.log(
1830
1921
  color.yellow(
1831
1922
  "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."
@@ -1837,7 +1928,7 @@ async function printPairing(pairing) {
1837
1928
  }
1838
1929
  }
1839
1930
  try {
1840
- const filePath = await writePairingPayloadFile(payload);
1931
+ const filePath = await writePairingPayloadFile(manualPayload);
1841
1932
  console.log("");
1842
1933
  console.log(color.bold("If the QR is too large or the camera will not scan:"));
1843
1934
  console.log("1. Open Manual connection in the iPhone app.");
@@ -1845,14 +1936,18 @@ async function printPairing(pairing) {
1845
1936
  console.log(`3. Paste the payload saved at: ${filePath}`);
1846
1937
  console.log(color.dim(` cat ${filePath}`));
1847
1938
  console.log("");
1848
- console.log(color.dim(`Compact payload bytes: ${payloadText.length}`));
1939
+ console.log(
1940
+ color.dim(
1941
+ `QR payload bytes: ${qrPayloadText.length}; manual payload bytes: ${JSON.stringify(manualPayload).length}`
1942
+ )
1943
+ );
1849
1944
  } catch (error) {
1850
1945
  console.log(
1851
1946
  color.yellow(
1852
1947
  `Could not save pairing payload file: ${error instanceof Error ? error.message : String(error)}`
1853
1948
  )
1854
1949
  );
1855
- console.log(payloadText);
1950
+ console.log(JSON.stringify(manualPayload));
1856
1951
  }
1857
1952
  }
1858
1953
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-memory",
3
- "version": "0.2.111",
3
+ "version": "0.2.113",
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",