forge-memory 0.2.116 → 0.2.118

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 +105 -19
  2. package/package.json +1 -1
@@ -1969,7 +1969,16 @@ async function createPairing(config, options = {}) {
1969
1969
  cookie: operatorCookie,
1970
1970
  ...(publicUrl ? { referer: publicUrl } : {})
1971
1971
  },
1972
- body: JSON.stringify({ userId: null, transportMode })
1972
+ body: JSON.stringify({
1973
+ userId: null,
1974
+ transportMode,
1975
+ fallbackMode: publicUrl
1976
+ ? publicUrl.includes(".ts.net")
1977
+ ? "tailscale"
1978
+ : "fixed-ip"
1979
+ : "none",
1980
+ publicUrl: publicUrl ?? undefined
1981
+ })
1973
1982
  });
1974
1983
  } catch (error) {
1975
1984
  const healthResult = await health(config, 1_500);
@@ -2007,6 +2016,65 @@ async function createPairing(config, options = {}) {
2007
2016
  return pairing;
2008
2017
  }
2009
2018
 
2019
+ async function resolveIosPairingOptions(parsed) {
2020
+ if (parsed.flags.manualHttp) {
2021
+ return {
2022
+ transportMode: "manual-http",
2023
+ publicUrl: validatePairingOptions({
2024
+ transportMode: "manual-http",
2025
+ publicUrl: parsed.values.publicUrl
2026
+ })
2027
+ };
2028
+ }
2029
+ if (parsed.flags.yes || parsed.flags.json || !process.stdin.isTTY || parsed.values.publicUrl) {
2030
+ return {
2031
+ transportMode: "iroh",
2032
+ publicUrl: validatePairingOptions({
2033
+ transportMode: "iroh",
2034
+ publicUrl: parsed.values.publicUrl
2035
+ })
2036
+ };
2037
+ }
2038
+ console.log(color.bold("iOS companion connection"));
2039
+ console.log(color.dim("Default: Iroh tunnel. You can choose Tailscale or a fixed/private IP as the direct phone path."));
2040
+ const choice = (
2041
+ await promptLine("Connection [iroh/tailscale/ip]", "iroh")
2042
+ ).toLowerCase();
2043
+ if (choice === "tailscale" || choice === "ts") {
2044
+ const publicUrl = await promptLine(
2045
+ "Tailscale Forge URL",
2046
+ parsed.values.publicUrl ?? "https://your-mac.tailnet.ts.net/forge/"
2047
+ );
2048
+ return {
2049
+ transportMode: "manual-http",
2050
+ publicUrl: validatePairingOptions({
2051
+ transportMode: "manual-http",
2052
+ publicUrl
2053
+ })
2054
+ };
2055
+ }
2056
+ if (choice === "ip" || choice === "fixed" || choice === "private") {
2057
+ const publicUrl = await promptLine(
2058
+ "Private/fixed Forge URL",
2059
+ parsed.values.publicUrl ?? "http://192.168.1.98:4317/forge/"
2060
+ );
2061
+ return {
2062
+ transportMode: "manual-http",
2063
+ publicUrl: validatePairingOptions({
2064
+ transportMode: "manual-http",
2065
+ publicUrl
2066
+ })
2067
+ };
2068
+ }
2069
+ return {
2070
+ transportMode: "iroh",
2071
+ publicUrl: validatePairingOptions({
2072
+ transportMode: "iroh",
2073
+ publicUrl: parsed.values.publicUrl
2074
+ })
2075
+ };
2076
+ }
2077
+
2010
2078
  function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
2011
2079
  const payload = pairing?.qrPayload;
2012
2080
  if (!payload || requestedTransportMode !== "iroh") {
@@ -2015,6 +2083,23 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
2015
2083
  const resolvedTransportMode = payload.transportMode ?? payload.transport?.protocol;
2016
2084
  const resolvedProtocol = payload.transport?.protocol;
2017
2085
  if (resolvedTransportMode === "iroh" || resolvedProtocol === "iroh") {
2086
+ const phoneFacingUrls = [
2087
+ payload.apiBaseUrl,
2088
+ payload.uiBaseUrl,
2089
+ payload.transport?.publicBaseUrl
2090
+ ].filter(Boolean);
2091
+ const loopbackUrl = phoneFacingUrls.find((url) => isLoopbackPairingUrl(url));
2092
+ if (loopbackUrl) {
2093
+ throw new PairingTransportUnavailableError(
2094
+ [
2095
+ "Forge created an Iroh pairing that exposes a loopback URL as phone-facing pairing data.",
2096
+ `Bad URL: ${loopbackUrl}.`,
2097
+ "A physical iPhone cannot reach localhost on this Mac.",
2098
+ "Use Iroh logical URLs, a selected Tailscale URL, or a selected private/fixed IP URL."
2099
+ ].join(" "),
2100
+ { apiBaseUrl: payload.apiBaseUrl, transportMode: resolvedTransportMode, protocol: resolvedProtocol }
2101
+ );
2102
+ }
2018
2103
  return;
2019
2104
  }
2020
2105
  const apiBaseUrl = payload.apiBaseUrl ?? "";
@@ -2039,6 +2124,15 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
2039
2124
 
2040
2125
  function validatePairingOptions({ transportMode, publicUrl }) {
2041
2126
  const normalizedPublicUrl = normalizePublicPairingUrl(publicUrl);
2127
+ if (normalizedPublicUrl && isLoopbackPairingUrl(normalizedPublicUrl)) {
2128
+ throw new Error(
2129
+ [
2130
+ `--public-url points at ${normalizedPublicUrl}, which is loopback-only.`,
2131
+ "A physical iPhone cannot reach localhost on this Mac.",
2132
+ "Use Iroh, Tailscale, or a private/fixed IP URL."
2133
+ ].join(" ")
2134
+ );
2135
+ }
2042
2136
  if (transportMode !== "manual-http") {
2043
2137
  return normalizedPublicUrl;
2044
2138
  }
@@ -2052,15 +2146,6 @@ function validatePairingOptions({ transportMode, publicUrl }) {
2052
2146
  ].join(" ")
2053
2147
  );
2054
2148
  }
2055
- if (isLoopbackPairingUrl(normalizedPublicUrl)) {
2056
- throw new Error(
2057
- [
2058
- `Manual HTTP --public-url points at ${normalizedPublicUrl}, which is loopback-only.`,
2059
- "A physical iPhone cannot reach localhost on this Mac.",
2060
- "Use a Tailscale or LAN URL, or omit --manual-http and use Iroh pairing."
2061
- ].join(" ")
2062
- );
2063
- }
2064
2149
  return normalizedPublicUrl;
2065
2150
  }
2066
2151
 
@@ -2290,10 +2375,13 @@ async function runInstall(parsed, command) {
2290
2375
  (parsed.flags.yes
2291
2376
  ? true
2292
2377
  : await promptYesNo("Pair the iOS companion now?", true)));
2378
+ const pairingOptions = shouldPair
2379
+ ? await resolveIosPairingOptions(parsed)
2380
+ : null;
2293
2381
  let irohTransportResult = null;
2294
2382
  if (
2295
2383
  shouldPair &&
2296
- !parsed.flags.manualHttp &&
2384
+ pairingOptions?.transportMode !== "manual-http" &&
2297
2385
  !parsed.flags.dryRun
2298
2386
  ) {
2299
2387
  irohTransportResult = await withProgress(
@@ -2357,8 +2445,8 @@ async function runInstall(parsed, command) {
2357
2445
  parsed.flags,
2358
2446
  () =>
2359
2447
  createPairing(config, {
2360
- transportMode: parsed.flags.manualHttp ? "manual-http" : "iroh",
2361
- publicUrl: parsed.values.publicUrl
2448
+ transportMode: pairingOptions.transportMode,
2449
+ publicUrl: pairingOptions.publicUrl
2362
2450
  })
2363
2451
  );
2364
2452
  if (pairing?.qrPayload && !parsed.flags.json) {
@@ -2584,11 +2672,9 @@ async function runUi(parsed) {
2584
2672
 
2585
2673
  async function runPairIos(parsed) {
2586
2674
  const config = await readConfig();
2587
- const transportMode = parsed.flags.manualHttp ? "manual-http" : "iroh";
2588
- const publicUrl = validatePairingOptions({
2589
- transportMode,
2590
- publicUrl: parsed.values.publicUrl
2591
- });
2675
+ const pairingOptions = await resolveIosPairingOptions(parsed);
2676
+ const transportMode = pairingOptions.transportMode;
2677
+ const publicUrl = pairingOptions.publicUrl;
2592
2678
  if (transportMode === "iroh") {
2593
2679
  await withProgress(
2594
2680
  "Preparing Forge Companion Iroh transport",
@@ -2952,7 +3038,7 @@ Options:
2952
3038
  --skip-adapters Configure UI/runtime only
2953
3039
  --skip-pair-ios Do not prompt or create iOS pairing
2954
3040
  --manual-http Use direct HTTP/TCP for iOS pairing instead of the default Iroh transport
2955
- --public-url <url> Phone-reachable URL for manual HTTP pairing, such as a Tailscale or LAN Forge URL
3041
+ --public-url <url> Phone-facing Tailscale/LAN/fixed URL for direct pairing or Iroh fallback; never localhost
2956
3042
  --no-start Configure without starting runtime
2957
3043
  --no-doctor Skip install-time doctor checks
2958
3044
  --repair Let doctor create missing folders and restart unhealthy runtime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-memory",
3
- "version": "0.2.116",
3
+ "version": "0.2.118",
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",