forge-memory 0.2.118 → 0.3.1
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 +20 -17
- package/bin/forge-memory.mjs +396 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,32 +37,35 @@ the wiki tools (`forge_search_wiki`, `forge_get_wiki_page`, and maintenance
|
|
|
37
37
|
tools). It also exposes `forge_memory_mcp_diagnostics` so adapter startup issues
|
|
38
38
|
show up as a tool result instead of a closed MCP transport.
|
|
39
39
|
|
|
40
|
-
`pair-ios` prefers
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
to
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
`pair-ios` prefers Tailscale when it is installed, running, authenticated, and Forge
|
|
41
|
+
is reachable through the Mac's MagicDNS HTTPS URL. That gives the iPhone a normal
|
|
42
|
+
phone-reachable web URL for sync and the embedded Forge WebView. If Tailscale is
|
|
43
|
+
running but Forge is not served yet, the installer asks before configuring
|
|
44
|
+
`tailscale serve` for the local Forge runtime. If Tailscale is unavailable or
|
|
45
|
+
declined, Forge falls back to the Iroh QR: a Rust Iroh host with the desktop node id,
|
|
46
|
+
pairing token, optional relay hint, and ALPN `forge-companion/1`.
|
|
47
|
+
|
|
48
|
+
The CLI renders a short-schema QR to keep the terminal code scannable and saves the
|
|
49
|
+
full manual payload under `~/.forge/pairing/` so you can paste it into the iPhone app
|
|
50
|
+
if the camera cannot scan. Use `--public-url` when you intentionally want to force a
|
|
51
|
+
specific Tailscale, LAN, or fixed/private URL:
|
|
49
52
|
|
|
50
53
|
```bash
|
|
51
|
-
npx forge-memory pair-ios --
|
|
54
|
+
npx forge-memory pair-ios --public-url https://your-mac.tailnet.ts.net/forge/
|
|
52
55
|
```
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
`--manual-http` is still available as an explicit direct HTTP/TCP override. Loopback
|
|
58
|
+
URLs such as `127.0.0.1` and `localhost` are rejected for physical-phone pairing.
|
|
56
59
|
|
|
57
60
|
The base install stays one command on purpose. The detailed companion transport
|
|
58
61
|
reference lives in the Forge repo at `docs/companion-iroh.md` and in the published
|
|
59
62
|
docs at `https://albertbuchard.github.io/forge/companion-transport.html`. Forge
|
|
60
63
|
Memory ships Forge's Iroh host source and lockfile, not native desktop binaries. When
|
|
61
|
-
the
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
the Iroh fallback is selected, the installer checks for Cargo, offers to install the
|
|
65
|
+
minimal Rust toolchain when the platform supports it, builds the local host from the
|
|
66
|
+
bundled source, then creates the QR. If Cargo cannot be installed automatically,
|
|
67
|
+
`install`, `configure`, and `pair-ios` stop with platform-specific steps instead of
|
|
68
|
+
printing a localhost QR that a physical iPhone cannot use.
|
|
66
69
|
|
|
67
70
|
`configure` reruns the full guided flow using the current config as defaults.
|
|
68
71
|
Install and configure run Forge doctor before finishing. `doctor --repair` creates
|
package/bin/forge-memory.mjs
CHANGED
|
@@ -362,6 +362,219 @@ function rustInstallGuidance() {
|
|
|
362
362
|
];
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
function tailscaleInstallPlan() {
|
|
366
|
+
if (process.platform === "darwin") {
|
|
367
|
+
return {
|
|
368
|
+
installable: true,
|
|
369
|
+
autoInstallCommand: commandExists("brew")
|
|
370
|
+
? { command: "brew", args: ["install", "--cask", "tailscale"] }
|
|
371
|
+
: null,
|
|
372
|
+
guidance: [
|
|
373
|
+
"Install Tailscale for macOS from https://tailscale.com/download/mac or with Homebrew: brew install --cask tailscale",
|
|
374
|
+
"Open Tailscale, sign in, and make sure this Mac and the iPhone are in the same tailnet.",
|
|
375
|
+
"Then rerun: npx forge-memory pair-ios"
|
|
376
|
+
]
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (process.platform === "linux") {
|
|
380
|
+
return {
|
|
381
|
+
installable: true,
|
|
382
|
+
autoInstallCommand: commandExists("curl")
|
|
383
|
+
? {
|
|
384
|
+
command: "sh",
|
|
385
|
+
args: ["-c", "curl -fsSL https://tailscale.com/install.sh | sh"]
|
|
386
|
+
}
|
|
387
|
+
: null,
|
|
388
|
+
guidance: [
|
|
389
|
+
"Install Tailscale with your package manager or the official installer: curl -fsSL https://tailscale.com/install.sh | sh",
|
|
390
|
+
"Start and authenticate it: sudo tailscale up",
|
|
391
|
+
"Then rerun: npx forge-memory pair-ios"
|
|
392
|
+
]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (process.platform === "win32") {
|
|
396
|
+
return {
|
|
397
|
+
installable: true,
|
|
398
|
+
autoInstallCommand: commandExists("winget")
|
|
399
|
+
? {
|
|
400
|
+
command: "winget",
|
|
401
|
+
args: [
|
|
402
|
+
"install",
|
|
403
|
+
"--id",
|
|
404
|
+
"Tailscale.Tailscale",
|
|
405
|
+
"-e",
|
|
406
|
+
"--source",
|
|
407
|
+
"winget",
|
|
408
|
+
"--accept-package-agreements",
|
|
409
|
+
"--accept-source-agreements"
|
|
410
|
+
]
|
|
411
|
+
}
|
|
412
|
+
: null,
|
|
413
|
+
guidance: [
|
|
414
|
+
"Install Tailscale for Windows from https://tailscale.com/download/windows or with winget: winget install Tailscale.Tailscale",
|
|
415
|
+
"Sign in, make sure this PC and the iPhone are in the same tailnet, then rerun: npx forge-memory pair-ios"
|
|
416
|
+
]
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
installable: false,
|
|
421
|
+
autoInstallCommand: null,
|
|
422
|
+
guidance: [
|
|
423
|
+
"Install Tailscale from https://tailscale.com/download if your platform supports it, then rerun: npx forge-memory pair-ios"
|
|
424
|
+
]
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function tailscaleAutodetectDisabled() {
|
|
429
|
+
return ["1", "true", "yes"].includes(
|
|
430
|
+
String(process.env.FORGE_MEMORY_SKIP_TAILSCALE_AUTODETECT ?? "").toLowerCase()
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function parseTailscaleStatus(raw) {
|
|
435
|
+
if (!raw) return { running: false, authenticated: false, dnsName: null };
|
|
436
|
+
try {
|
|
437
|
+
const payload = JSON.parse(raw);
|
|
438
|
+
const self = payload.Self ?? payload.self ?? null;
|
|
439
|
+
const dnsName = String(self?.DNSName ?? self?.dnsName ?? "")
|
|
440
|
+
.trim()
|
|
441
|
+
.replace(/\.$/, "");
|
|
442
|
+
const backendState = String(payload.BackendState ?? payload.backendState ?? "");
|
|
443
|
+
const running = backendState.toLowerCase() === "running";
|
|
444
|
+
return {
|
|
445
|
+
running,
|
|
446
|
+
authenticated: running && Boolean(dnsName),
|
|
447
|
+
dnsName: dnsName || null,
|
|
448
|
+
backendState: backendState || null
|
|
449
|
+
};
|
|
450
|
+
} catch {
|
|
451
|
+
return { running: false, authenticated: false, dnsName: null };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function normalizeForgePublicUiUrl(value) {
|
|
456
|
+
const normalized = normalizePublicPairingUrl(value);
|
|
457
|
+
if (!normalized) return null;
|
|
458
|
+
const url = new URL(normalized);
|
|
459
|
+
if (!url.pathname || url.pathname === "/" || url.pathname.startsWith("/api/")) {
|
|
460
|
+
url.pathname = "/forge/";
|
|
461
|
+
}
|
|
462
|
+
if (!url.pathname.endsWith("/")) url.pathname = `${url.pathname}/`;
|
|
463
|
+
url.search = "";
|
|
464
|
+
url.hash = "";
|
|
465
|
+
return url.toString();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function forgePublicHealthUrl(publicUiUrl) {
|
|
469
|
+
const url = new URL(publicUiUrl);
|
|
470
|
+
url.pathname = "/api/v1/health";
|
|
471
|
+
url.search = "";
|
|
472
|
+
url.hash = "";
|
|
473
|
+
return url;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function publicUrlFallbackMode(publicUrl) {
|
|
477
|
+
try {
|
|
478
|
+
const host = new URL(publicUrl).hostname.toLowerCase();
|
|
479
|
+
return host.endsWith(".ts.net") ? "tailscale" : "fixed-ip";
|
|
480
|
+
} catch {
|
|
481
|
+
return "fixed-ip";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function detectTailscaleState() {
|
|
486
|
+
if (tailscaleAutodetectDisabled()) {
|
|
487
|
+
return {
|
|
488
|
+
installed: false,
|
|
489
|
+
running: false,
|
|
490
|
+
authenticated: false,
|
|
491
|
+
publicUrl: null,
|
|
492
|
+
disabled: true
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (!commandExists("tailscale")) {
|
|
496
|
+
return {
|
|
497
|
+
installed: false,
|
|
498
|
+
running: false,
|
|
499
|
+
authenticated: false,
|
|
500
|
+
publicUrl: null,
|
|
501
|
+
installPlan: tailscaleInstallPlan()
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const status = parseTailscaleStatus(runCapture("tailscale", ["status", "--json"], 4_000));
|
|
505
|
+
const envPublicUrl = normalizeForgePublicUiUrl(process.env.FORGE_MEMORY_TAILSCALE_PUBLIC_URL);
|
|
506
|
+
const publicUrl =
|
|
507
|
+
envPublicUrl ??
|
|
508
|
+
(status.dnsName ? `https://${status.dnsName}/forge/` : null);
|
|
509
|
+
return {
|
|
510
|
+
installed: true,
|
|
511
|
+
running: status.running,
|
|
512
|
+
authenticated: status.authenticated,
|
|
513
|
+
publicUrl,
|
|
514
|
+
backendState: status.backendState,
|
|
515
|
+
dnsName: status.dnsName
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function probePublicForgeUrl(publicUrl, timeoutMs = 4_000) {
|
|
520
|
+
if (!publicUrl) return { ok: false, error: "no public URL candidate" };
|
|
521
|
+
const fakeProbeSequencePath =
|
|
522
|
+
process.env.FORGE_MEMORY_FAKE_TAILSCALE_PUBLIC_PROBE_SEQUENCE;
|
|
523
|
+
if (fakeProbeSequencePath) {
|
|
524
|
+
const source = fs.existsSync(fakeProbeSequencePath)
|
|
525
|
+
? fs.readFileSync(fakeProbeSequencePath, "utf8")
|
|
526
|
+
: "";
|
|
527
|
+
const entries = source
|
|
528
|
+
.split(/\r?\n/)
|
|
529
|
+
.map((entry) => entry.trim())
|
|
530
|
+
.filter(Boolean);
|
|
531
|
+
const current = entries.shift() ?? "fail";
|
|
532
|
+
fs.writeFileSync(fakeProbeSequencePath, entries.length ? `${entries.join("\n")}\n` : "");
|
|
533
|
+
return current === "ok"
|
|
534
|
+
? { ok: true, fake: true }
|
|
535
|
+
: { ok: false, error: current === "fail" ? "fake probe failure" : current };
|
|
536
|
+
}
|
|
537
|
+
if (
|
|
538
|
+
["1", "true", "yes"].includes(
|
|
539
|
+
String(process.env.FORGE_MEMORY_SKIP_TAILSCALE_PUBLIC_PROBE ?? "").toLowerCase()
|
|
540
|
+
)
|
|
541
|
+
) {
|
|
542
|
+
return { ok: true, skipped: true };
|
|
543
|
+
}
|
|
544
|
+
const controller = new AbortController();
|
|
545
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
546
|
+
try {
|
|
547
|
+
const response = await fetch(forgePublicHealthUrl(publicUrl), {
|
|
548
|
+
headers: { accept: "application/json", "x-forge-runtime-probe": "1" },
|
|
549
|
+
signal: controller.signal
|
|
550
|
+
});
|
|
551
|
+
if (!response.ok) return { ok: false, status: response.status };
|
|
552
|
+
const payload = await response.json().catch(() => null);
|
|
553
|
+
if (!isForgeHealthPayload(payload)) {
|
|
554
|
+
return { ok: false, status: response.status, error: "non-Forge health payload" };
|
|
555
|
+
}
|
|
556
|
+
return { ok: true, status: response.status };
|
|
557
|
+
} catch (error) {
|
|
558
|
+
return { ok: false, error: describeNetworkError(error) };
|
|
559
|
+
} finally {
|
|
560
|
+
clearTimeout(timeout);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function configureTailscaleServe(config, flags) {
|
|
565
|
+
const target = `http://127.0.0.1:${config.port || DEFAULT_PORT}`;
|
|
566
|
+
return runCommand("tailscale", ["serve", "--bg", target], {
|
|
567
|
+
dryRun: flags?.dryRun
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function tailscalePreferredMessage() {
|
|
572
|
+
return [
|
|
573
|
+
"Tailscale is preferred when available: it gives the iPhone a normal HTTPS Forge URL, is usually faster and steadier than relayed Iroh, and avoids the custom WebView URL scheme.",
|
|
574
|
+
"Iroh remains the fallback when Tailscale is missing, declined, or unreachable."
|
|
575
|
+
].join(" ");
|
|
576
|
+
}
|
|
577
|
+
|
|
365
578
|
function refreshCargoPath() {
|
|
366
579
|
const cargoBin = path.join(homeDir(), ".cargo", "bin");
|
|
367
580
|
if (fs.existsSync(cargoBin)) {
|
|
@@ -1972,11 +2185,7 @@ async function createPairing(config, options = {}) {
|
|
|
1972
2185
|
body: JSON.stringify({
|
|
1973
2186
|
userId: null,
|
|
1974
2187
|
transportMode,
|
|
1975
|
-
fallbackMode: publicUrl
|
|
1976
|
-
? publicUrl.includes(".ts.net")
|
|
1977
|
-
? "tailscale"
|
|
1978
|
-
: "fixed-ip"
|
|
1979
|
-
: "none",
|
|
2188
|
+
fallbackMode: publicUrl ? publicUrlFallbackMode(publicUrl) : "none",
|
|
1980
2189
|
publicUrl: publicUrl ?? undefined
|
|
1981
2190
|
})
|
|
1982
2191
|
});
|
|
@@ -2016,7 +2225,128 @@ async function createPairing(config, options = {}) {
|
|
|
2016
2225
|
return pairing;
|
|
2017
2226
|
}
|
|
2018
2227
|
|
|
2019
|
-
async function
|
|
2228
|
+
async function maybeInstallTailscaleForPairing(parsed) {
|
|
2229
|
+
const plan = tailscaleInstallPlan();
|
|
2230
|
+
if (parsed.flags.json || parsed.flags.dryRun || !process.stdin.isTTY) {
|
|
2231
|
+
return { installed: false, guidance: plan.guidance };
|
|
2232
|
+
}
|
|
2233
|
+
console.log(color.bold("Tailscale pairing"));
|
|
2234
|
+
console.log(tailscalePreferredMessage());
|
|
2235
|
+
console.log(color.dim("Tailscale is not installed or not visible on PATH."));
|
|
2236
|
+
for (const item of plan.guidance) console.log(color.dim(`- ${item}`));
|
|
2237
|
+
if (!plan.installable || !plan.autoInstallCommand) {
|
|
2238
|
+
return { installed: false, guidance: plan.guidance };
|
|
2239
|
+
}
|
|
2240
|
+
const shouldInstall = await promptYesNo(
|
|
2241
|
+
"Install Tailscale now before falling back to Iroh?",
|
|
2242
|
+
true
|
|
2243
|
+
);
|
|
2244
|
+
if (!shouldInstall) return { installed: false, guidance: plan.guidance };
|
|
2245
|
+
const result = await runCommand(plan.autoInstallCommand.command, plan.autoInstallCommand.args);
|
|
2246
|
+
return { installed: result.ok, guidance: plan.guidance, result };
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
async function resolveTailscalePairingOptions(parsed, config) {
|
|
2250
|
+
const state = detectTailscaleState();
|
|
2251
|
+
if (state.disabled) return null;
|
|
2252
|
+
if (!state.installed) {
|
|
2253
|
+
const installAttempt = await maybeInstallTailscaleForPairing(parsed);
|
|
2254
|
+
if (installAttempt.installed) {
|
|
2255
|
+
const installedState = detectTailscaleState();
|
|
2256
|
+
if (installedState.installed && installedState.running && installedState.authenticated) {
|
|
2257
|
+
return resolveTailscalePairingOptions(parsed, config);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return null;
|
|
2261
|
+
}
|
|
2262
|
+
if (!state.running || !state.authenticated || !state.publicUrl) {
|
|
2263
|
+
if (!parsed.flags.json && process.stdin.isTTY) {
|
|
2264
|
+
console.log(color.bold("Tailscale pairing"));
|
|
2265
|
+
console.log(tailscalePreferredMessage());
|
|
2266
|
+
console.log(
|
|
2267
|
+
color.yellow(
|
|
2268
|
+
"Tailscale is installed, but it is not running/authenticated or does not expose a MagicDNS name."
|
|
2269
|
+
)
|
|
2270
|
+
);
|
|
2271
|
+
console.log(color.dim("Open Tailscale, sign in, then rerun npx forge-memory pair-ios. Falling back to Iroh for this pairing."));
|
|
2272
|
+
}
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
const firstProbe = await probePublicForgeUrl(state.publicUrl);
|
|
2277
|
+
if (firstProbe.ok) {
|
|
2278
|
+
if (!parsed.flags.json) {
|
|
2279
|
+
console.log(color.green(`Using Tailscale for iOS pairing: ${state.publicUrl}`));
|
|
2280
|
+
}
|
|
2281
|
+
return {
|
|
2282
|
+
transportMode: "manual-http",
|
|
2283
|
+
publicUrl: validatePairingOptions({
|
|
2284
|
+
transportMode: "manual-http",
|
|
2285
|
+
publicUrl: state.publicUrl
|
|
2286
|
+
}),
|
|
2287
|
+
source: "tailscale"
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
const canConfigureServe =
|
|
2292
|
+
!parsed.flags.json &&
|
|
2293
|
+
!parsed.flags.dryRun &&
|
|
2294
|
+
(parsed.flags.yes ||
|
|
2295
|
+
(process.stdin.isTTY &&
|
|
2296
|
+
(await promptYesNo(
|
|
2297
|
+
[
|
|
2298
|
+
`Tailscale is running, but Forge is not reachable at ${state.publicUrl}.`,
|
|
2299
|
+
"Serve Forge through Tailscale now?"
|
|
2300
|
+
].join(" "),
|
|
2301
|
+
true
|
|
2302
|
+
))));
|
|
2303
|
+
if (!canConfigureServe) {
|
|
2304
|
+
if (!parsed.flags.json && process.stdin.isTTY) {
|
|
2305
|
+
console.log(
|
|
2306
|
+
color.yellow(
|
|
2307
|
+
`Tailscale is running, but Forge was not reachable at ${state.publicUrl}: ${firstProbe.error ?? firstProbe.status ?? "unknown error"}. Falling back to Iroh.`
|
|
2308
|
+
)
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
return null;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const serveResult = await configureTailscaleServe(config, parsed.flags);
|
|
2315
|
+
if (!serveResult.ok) {
|
|
2316
|
+
if (!parsed.flags.json) {
|
|
2317
|
+
console.log(
|
|
2318
|
+
color.yellow(
|
|
2319
|
+
"Could not configure Tailscale Serve automatically. Falling back to Iroh."
|
|
2320
|
+
)
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
return null;
|
|
2324
|
+
}
|
|
2325
|
+
const secondProbe = await probePublicForgeUrl(state.publicUrl, 8_000);
|
|
2326
|
+
if (!secondProbe.ok) {
|
|
2327
|
+
if (!parsed.flags.json) {
|
|
2328
|
+
console.log(
|
|
2329
|
+
color.yellow(
|
|
2330
|
+
`Tailscale Serve was configured, but Forge still was not reachable at ${state.publicUrl}: ${secondProbe.error ?? secondProbe.status ?? "unknown error"}. Falling back to Iroh.`
|
|
2331
|
+
)
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
if (!parsed.flags.json) {
|
|
2337
|
+
console.log(color.green(`Using Tailscale for iOS pairing: ${state.publicUrl}`));
|
|
2338
|
+
}
|
|
2339
|
+
return {
|
|
2340
|
+
transportMode: "manual-http",
|
|
2341
|
+
publicUrl: validatePairingOptions({
|
|
2342
|
+
transportMode: "manual-http",
|
|
2343
|
+
publicUrl: state.publicUrl
|
|
2344
|
+
}),
|
|
2345
|
+
source: "tailscale"
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
async function resolveIosPairingOptions(parsed, config = null) {
|
|
2020
2350
|
if (parsed.flags.manualHttp) {
|
|
2021
2351
|
return {
|
|
2022
2352
|
transportMode: "manual-http",
|
|
@@ -2026,7 +2356,21 @@ async function resolveIosPairingOptions(parsed) {
|
|
|
2026
2356
|
})
|
|
2027
2357
|
};
|
|
2028
2358
|
}
|
|
2029
|
-
if (parsed.
|
|
2359
|
+
if (parsed.values.publicUrl) {
|
|
2360
|
+
return {
|
|
2361
|
+
transportMode: "manual-http",
|
|
2362
|
+
publicUrl: validatePairingOptions({
|
|
2363
|
+
transportMode: "manual-http",
|
|
2364
|
+
publicUrl: parsed.values.publicUrl
|
|
2365
|
+
}),
|
|
2366
|
+
source: publicUrlFallbackMode(parsed.values.publicUrl)
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
if (config) {
|
|
2370
|
+
const tailscalePairing = await resolveTailscalePairingOptions(parsed, config);
|
|
2371
|
+
if (tailscalePairing) return tailscalePairing;
|
|
2372
|
+
}
|
|
2373
|
+
if (parsed.flags.yes || parsed.flags.json || !process.stdin.isTTY) {
|
|
2030
2374
|
return {
|
|
2031
2375
|
transportMode: "iroh",
|
|
2032
2376
|
publicUrl: validatePairingOptions({
|
|
@@ -2036,23 +2380,11 @@ async function resolveIosPairingOptions(parsed) {
|
|
|
2036
2380
|
};
|
|
2037
2381
|
}
|
|
2038
2382
|
console.log(color.bold("iOS companion connection"));
|
|
2039
|
-
console.log(color.dim(
|
|
2383
|
+
console.log(color.dim(tailscalePreferredMessage()));
|
|
2384
|
+
console.log(color.dim("Choose Iroh or a fixed/private IP fallback for this pairing."));
|
|
2040
2385
|
const choice = (
|
|
2041
|
-
await promptLine("Connection [iroh/
|
|
2386
|
+
await promptLine("Connection [iroh/ip]", "iroh")
|
|
2042
2387
|
).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
2388
|
if (choice === "ip" || choice === "fixed" || choice === "private") {
|
|
2057
2389
|
const publicUrl = await promptLine(
|
|
2058
2390
|
"Private/fixed Forge URL",
|
|
@@ -2375,22 +2707,6 @@ async function runInstall(parsed, command) {
|
|
|
2375
2707
|
(parsed.flags.yes
|
|
2376
2708
|
? true
|
|
2377
2709
|
: await promptYesNo("Pair the iOS companion now?", true)));
|
|
2378
|
-
const pairingOptions = shouldPair
|
|
2379
|
-
? await resolveIosPairingOptions(parsed)
|
|
2380
|
-
: null;
|
|
2381
|
-
let irohTransportResult = null;
|
|
2382
|
-
if (
|
|
2383
|
-
shouldPair &&
|
|
2384
|
-
pairingOptions?.transportMode !== "manual-http" &&
|
|
2385
|
-
!parsed.flags.dryRun
|
|
2386
|
-
) {
|
|
2387
|
-
irohTransportResult = await withProgress(
|
|
2388
|
-
"Preparing Forge Companion Iroh transport",
|
|
2389
|
-
"checking Rust/Cargo and building the local host",
|
|
2390
|
-
parsed.flags,
|
|
2391
|
-
() => ensureIrohTransportPrepared(config, parsed.flags)
|
|
2392
|
-
);
|
|
2393
|
-
}
|
|
2394
2710
|
let runtimeResult = null;
|
|
2395
2711
|
if (!parsed.flags.noStart && !parsed.flags.dryRun) {
|
|
2396
2712
|
runtimeResult = await withProgress(
|
|
@@ -2428,6 +2744,22 @@ async function runInstall(parsed, command) {
|
|
|
2428
2744
|
}
|
|
2429
2745
|
}
|
|
2430
2746
|
}
|
|
2747
|
+
const pairingOptions = shouldPair
|
|
2748
|
+
? await resolveIosPairingOptions(parsed, config)
|
|
2749
|
+
: null;
|
|
2750
|
+
let irohTransportResult = null;
|
|
2751
|
+
if (
|
|
2752
|
+
shouldPair &&
|
|
2753
|
+
pairingOptions?.transportMode !== "manual-http" &&
|
|
2754
|
+
!parsed.flags.dryRun
|
|
2755
|
+
) {
|
|
2756
|
+
irohTransportResult = await withProgress(
|
|
2757
|
+
"Preparing Forge Companion Iroh transport",
|
|
2758
|
+
"checking Rust/Cargo and building the local host",
|
|
2759
|
+
parsed.flags,
|
|
2760
|
+
() => ensureIrohTransportPrepared(config, parsed.flags)
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2431
2763
|
let pairing = null;
|
|
2432
2764
|
if (shouldPair && !parsed.flags.dryRun) {
|
|
2433
2765
|
if (!runtimeResult) {
|
|
@@ -2441,7 +2773,7 @@ async function runInstall(parsed, command) {
|
|
|
2441
2773
|
assertRuntimeStartedForPairing(runtimeResult, config);
|
|
2442
2774
|
pairing = await withProgress(
|
|
2443
2775
|
"Creating iOS companion pairing",
|
|
2444
|
-
|
|
2776
|
+
pairingOptions.transportMode === "manual-http" ? "phone-reachable HTTP" : "Iroh QR",
|
|
2445
2777
|
parsed.flags,
|
|
2446
2778
|
() =>
|
|
2447
2779
|
createPairing(config, {
|
|
@@ -2672,10 +3004,15 @@ async function runUi(parsed) {
|
|
|
2672
3004
|
|
|
2673
3005
|
async function runPairIos(parsed) {
|
|
2674
3006
|
const config = await readConfig();
|
|
2675
|
-
const
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
3007
|
+
const explicitPairingOptions =
|
|
3008
|
+
parsed.flags.manualHttp || parsed.values.publicUrl
|
|
3009
|
+
? await resolveIosPairingOptions(parsed, config)
|
|
3010
|
+
: null;
|
|
3011
|
+
const noStartPairingOptions =
|
|
3012
|
+
!explicitPairingOptions && parsed.flags.noStart
|
|
3013
|
+
? await resolveIosPairingOptions(parsed, config)
|
|
3014
|
+
: null;
|
|
3015
|
+
if (noStartPairingOptions?.transportMode === "iroh") {
|
|
2679
3016
|
await withProgress(
|
|
2680
3017
|
"Preparing Forge Companion Iroh transport",
|
|
2681
3018
|
"checking Rust/Cargo and building the local host",
|
|
@@ -2698,6 +3035,20 @@ async function runPairIos(parsed) {
|
|
|
2698
3035
|
config
|
|
2699
3036
|
);
|
|
2700
3037
|
}
|
|
3038
|
+
const pairingOptions =
|
|
3039
|
+
explicitPairingOptions ??
|
|
3040
|
+
noStartPairingOptions ??
|
|
3041
|
+
await resolveIosPairingOptions(parsed, config);
|
|
3042
|
+
const transportMode = pairingOptions.transportMode;
|
|
3043
|
+
const publicUrl = pairingOptions.publicUrl;
|
|
3044
|
+
if (transportMode === "iroh" && noStartPairingOptions?.transportMode !== "iroh") {
|
|
3045
|
+
await withProgress(
|
|
3046
|
+
"Preparing Forge Companion Iroh transport",
|
|
3047
|
+
"checking Rust/Cargo and building the local host",
|
|
3048
|
+
parsed.flags,
|
|
3049
|
+
() => ensureIrohTransportPrepared(config, parsed.flags)
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
2701
3052
|
const pairing = await createPairing(config, {
|
|
2702
3053
|
transportMode,
|
|
2703
3054
|
publicUrl
|
|
@@ -3037,8 +3388,8 @@ Options:
|
|
|
3037
3388
|
--adapters <list> Comma list: openclaw,hermes,codex or none
|
|
3038
3389
|
--skip-adapters Configure UI/runtime only
|
|
3039
3390
|
--skip-pair-ios Do not prompt or create iOS pairing
|
|
3040
|
-
--manual-http
|
|
3041
|
-
--public-url <url> Phone-facing Tailscale/LAN/fixed URL for direct pairing
|
|
3391
|
+
--manual-http Force direct HTTP/TCP for iOS pairing
|
|
3392
|
+
--public-url <url> Phone-facing Tailscale/LAN/fixed URL for direct pairing; never localhost
|
|
3042
3393
|
--no-start Configure without starting runtime
|
|
3043
3394
|
--no-doctor Skip install-time doctor checks
|
|
3044
3395
|
--repair Let doctor create missing folders and restart unhealthy runtime
|
package/package.json
CHANGED