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 +8 -3
- package/bin/forge-memory.mjs +108 -13
- package/package.json +1 -1
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
|
|
44
|
-
`~/.forge/pairing/` so you can paste it into the iPhone
|
|
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
|
package/bin/forge-memory.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1809
|
-
const
|
|
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 &&
|
|
1902
|
+
if (terminalColumns >= 72 && qrPayloadText.length <= 1_500) {
|
|
1812
1903
|
console.log("\nScan this compact QR in Forge Companion:\n");
|
|
1813
|
-
qrcode.generate(
|
|
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 =
|
|
1910
|
+
const transport = manualPayload.transport;
|
|
1820
1911
|
if (transport?.provider) {
|
|
1821
1912
|
const label =
|
|
1822
|
-
|
|
1913
|
+
manualPayload.transport?.protocol === "iroh"
|
|
1823
1914
|
? "Iroh"
|
|
1824
|
-
:
|
|
1915
|
+
: manualPayload.transportMode === "iroh"
|
|
1825
1916
|
? "Iroh"
|
|
1826
1917
|
: "Manual HTTP";
|
|
1827
|
-
console.log(`${color.cyan(label)}: ${
|
|
1828
|
-
if (label === "Manual HTTP" && isLoopbackPairingUrl(
|
|
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(
|
|
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(
|
|
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(
|
|
1950
|
+
console.log(JSON.stringify(manualPayload));
|
|
1856
1951
|
}
|
|
1857
1952
|
}
|
|
1858
1953
|
|
package/package.json
CHANGED