forge-memory 0.2.115 → 0.2.116
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 +6 -4
- package/bin/forge-memory.mjs +255 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,10 +57,12 @@ the iOS Simulator but not for a physical phone.
|
|
|
57
57
|
The base install stays one command on purpose. The detailed companion transport
|
|
58
58
|
reference lives in the Forge repo at `docs/companion-iroh.md` and in the published
|
|
59
59
|
docs at `https://albertbuchard.github.io/forge/companion-transport.html`. Forge
|
|
60
|
-
Memory ships
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
Memory ships Forge's Iroh host source and lockfile, not native desktop binaries. When
|
|
61
|
+
the default iOS pairing flow is selected, the installer checks for Cargo, offers to
|
|
62
|
+
install the minimal Rust toolchain when the platform supports it, builds the local
|
|
63
|
+
host from the bundled source, then creates the QR. If Cargo cannot be installed
|
|
64
|
+
automatically, `install`, `configure`, and `pair-ios` stop with platform-specific
|
|
65
|
+
steps instead of printing a localhost QR that a physical iPhone cannot use.
|
|
64
66
|
|
|
65
67
|
`configure` reruns the full guided flow using the current config as defaults.
|
|
66
68
|
Install and configure run Forge doctor before finishing. `doctor --repair` creates
|
package/bin/forge-memory.mjs
CHANGED
|
@@ -277,6 +277,231 @@ function runCapture(command, args, timeoutMs = 2_000) {
|
|
|
277
277
|
return `${result.stdout}${result.stderr}`.trim();
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
function binaryNameForPlatform() {
|
|
281
|
+
return process.platform === "win32"
|
|
282
|
+
? "forge-companion-iroh.exe"
|
|
283
|
+
: "forge-companion-iroh";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function candidateIrohRoots(config) {
|
|
287
|
+
const roots = [];
|
|
288
|
+
if (config.mode === "dev" && config.repo) {
|
|
289
|
+
roots.push(config.repo);
|
|
290
|
+
roots.push(path.join(config.repo, "openclaw-plugin", "dist"));
|
|
291
|
+
}
|
|
292
|
+
const pluginRoot = resolveOpenClawPluginRoot();
|
|
293
|
+
if (pluginRoot) {
|
|
294
|
+
roots.push(pluginRoot);
|
|
295
|
+
roots.push(path.join(pluginRoot, "dist"));
|
|
296
|
+
}
|
|
297
|
+
return [...new Set(roots.map((entry) => path.resolve(entry)))];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function candidateIrohBinariesForInstall(config) {
|
|
301
|
+
const binaryName = binaryNameForPlatform();
|
|
302
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
303
|
+
const explicitBin = process.env.FORGE_COMPANION_IROH_BIN?.trim();
|
|
304
|
+
return [
|
|
305
|
+
...(explicitBin ? [explicitBin] : []),
|
|
306
|
+
...candidateIrohRoots(config).flatMap((root) => [
|
|
307
|
+
path.join(root, "companion-iroh", "target", "release", binaryName),
|
|
308
|
+
path.join(root, "companion-iroh", "target", "debug", binaryName),
|
|
309
|
+
path.join(root, "companion-iroh-src", "target", "release", binaryName),
|
|
310
|
+
path.join(root, "companion-iroh-src", "target", "debug", binaryName),
|
|
311
|
+
path.join(root, "companion-iroh", platformKey, binaryName),
|
|
312
|
+
path.join(root, "companion-iroh", binaryName)
|
|
313
|
+
])
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function findIrohBinaryForInstall(config) {
|
|
318
|
+
return candidateIrohBinariesForInstall(config).find((candidate) =>
|
|
319
|
+
fs.existsSync(candidate)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function candidateIrohManifestsForInstall(config) {
|
|
324
|
+
return candidateIrohRoots(config).flatMap((root) => [
|
|
325
|
+
path.join(root, "companion-iroh", "Cargo.toml"),
|
|
326
|
+
path.join(root, "companion-iroh-src", "Cargo.toml")
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function findIrohManifestForInstall(config) {
|
|
331
|
+
return candidateIrohManifestsForInstall(config).find((candidate) =>
|
|
332
|
+
fs.existsSync(candidate)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function rustInstallGuidance() {
|
|
337
|
+
if (process.platform === "darwin") {
|
|
338
|
+
return [
|
|
339
|
+
"Install Apple's command line tools first if prompted: xcode-select --install",
|
|
340
|
+
"Then install Rust with the official minimal installer:",
|
|
341
|
+
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal",
|
|
342
|
+
"Restart the terminal or run: source ~/.cargo/env"
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
if (process.platform === "linux") {
|
|
346
|
+
return [
|
|
347
|
+
"Install build tools with your system package manager, for example: sudo apt-get install -y build-essential pkg-config libssl-dev",
|
|
348
|
+
"Then install Rust with the official minimal installer:",
|
|
349
|
+
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal",
|
|
350
|
+
"Restart the terminal or run: source ~/.cargo/env"
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
if (process.platform === "win32") {
|
|
354
|
+
return [
|
|
355
|
+
"Install Rustup for Windows:",
|
|
356
|
+
"winget install Rustlang.Rustup",
|
|
357
|
+
"Then reopen PowerShell and rerun: npx forge-memory install"
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
return [
|
|
361
|
+
"Install Rust/Cargo from https://rustup.rs, then rerun: npx forge-memory install"
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function refreshCargoPath() {
|
|
366
|
+
const cargoBin = path.join(homeDir(), ".cargo", "bin");
|
|
367
|
+
if (fs.existsSync(cargoBin)) {
|
|
368
|
+
const current = process.env.PATH ?? "";
|
|
369
|
+
if (!current.split(path.delimiter).includes(cargoBin)) {
|
|
370
|
+
process.env.PATH = `${cargoBin}${path.delimiter}${current}`;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function maybeInstallRustToolchain(flags) {
|
|
376
|
+
refreshCargoPath();
|
|
377
|
+
if (commandExists("cargo")) {
|
|
378
|
+
return { ok: true, installed: false };
|
|
379
|
+
}
|
|
380
|
+
const guidance = rustInstallGuidance();
|
|
381
|
+
const canUseRustupScript =
|
|
382
|
+
(process.platform === "darwin" || process.platform === "linux") &&
|
|
383
|
+
commandExists("curl");
|
|
384
|
+
const canUseWinget = process.platform === "win32" && commandExists("winget");
|
|
385
|
+
if (!canUseRustupScript && !canUseWinget) {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
installed: false,
|
|
389
|
+
guidance:
|
|
390
|
+
process.platform === "darwin" || process.platform === "linux"
|
|
391
|
+
? ["Install curl first, then install Rust/Cargo.", ...guidance]
|
|
392
|
+
: guidance
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (flags?.json || flags?.dryRun) {
|
|
396
|
+
return { ok: false, installed: false, guidance };
|
|
397
|
+
}
|
|
398
|
+
const shouldInstall = flags?.yes
|
|
399
|
+
? true
|
|
400
|
+
: await promptYesNo(
|
|
401
|
+
"Forge Companion Iroh needs Rust/Cargo to build the local transport host. Install the minimal Rust toolchain now?",
|
|
402
|
+
true
|
|
403
|
+
);
|
|
404
|
+
if (!shouldInstall) {
|
|
405
|
+
return { ok: false, installed: false, guidance };
|
|
406
|
+
}
|
|
407
|
+
console.log(color.cyan("Installing minimal Rust toolchain..."));
|
|
408
|
+
const result = canUseWinget
|
|
409
|
+
? await runCommand("winget", [
|
|
410
|
+
"install",
|
|
411
|
+
"--id",
|
|
412
|
+
"Rustlang.Rustup",
|
|
413
|
+
"-e",
|
|
414
|
+
"--source",
|
|
415
|
+
"winget",
|
|
416
|
+
"--accept-package-agreements",
|
|
417
|
+
"--accept-source-agreements"
|
|
418
|
+
])
|
|
419
|
+
: await runCommand("sh", [
|
|
420
|
+
"-c",
|
|
421
|
+
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal"
|
|
422
|
+
]);
|
|
423
|
+
refreshCargoPath();
|
|
424
|
+
return {
|
|
425
|
+
ok: result.ok && commandExists("cargo"),
|
|
426
|
+
installed: result.ok,
|
|
427
|
+
guidance
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function ensureIrohTransportPrepared(config, flags = {}) {
|
|
432
|
+
const existingBinary = findIrohBinaryForInstall(config);
|
|
433
|
+
if (existingBinary) {
|
|
434
|
+
return { ok: true, built: false, binary: existingBinary };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (config.mode !== "dev") {
|
|
438
|
+
await ensurePackagedRuntimeInstalled();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const manifestPath = findIrohManifestForInstall(config);
|
|
442
|
+
if (!manifestPath) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
[
|
|
445
|
+
"Forge could not find the bundled companion Iroh source.",
|
|
446
|
+
"Run npx forge-memory doctor --repair so Forge Memory refreshes the packaged runtime.",
|
|
447
|
+
`Runtime log: ${logPath()}.`
|
|
448
|
+
].join(" ")
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const rust = await maybeInstallRustToolchain(flags);
|
|
453
|
+
if (!rust.ok) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
[
|
|
456
|
+
"Forge Companion Iroh is source-built on this machine, but Rust/Cargo is not installed yet.",
|
|
457
|
+
"Install steps:",
|
|
458
|
+
...rustInstallGuidance().map((entry) => `- ${entry}`),
|
|
459
|
+
"Then rerun: npx forge-memory install",
|
|
460
|
+
"For a temporary direct network fallback, use: npx forge-memory pair-ios --manual-http --public-url <phone-reachable Forge URL>"
|
|
461
|
+
].join("\n")
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const result = await runLoggedCommand(
|
|
466
|
+
"cargo",
|
|
467
|
+
[
|
|
468
|
+
"build",
|
|
469
|
+
"--release",
|
|
470
|
+
"--manifest-path",
|
|
471
|
+
manifestPath,
|
|
472
|
+
"--bin",
|
|
473
|
+
"forge-companion-iroh"
|
|
474
|
+
],
|
|
475
|
+
{
|
|
476
|
+
cwd: path.dirname(manifestPath),
|
|
477
|
+
dryRun: flags.dryRun,
|
|
478
|
+
env: process.env,
|
|
479
|
+
logFile: logPath()
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
if (!result.ok) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
[
|
|
485
|
+
"Forge could not build the local companion Iroh transport host from source.",
|
|
486
|
+
`Manifest: ${manifestPath}`,
|
|
487
|
+
`Log: ${logPath()}`,
|
|
488
|
+
"Install or repair Rust/Cargo, then rerun: npx forge-memory install"
|
|
489
|
+
].join(" ")
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
const binary = findIrohBinaryForInstall(config);
|
|
493
|
+
if (!binary && !flags.dryRun) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
[
|
|
496
|
+
"Cargo finished, but Forge could not find the built companion Iroh binary.",
|
|
497
|
+
`Manifest: ${manifestPath}`,
|
|
498
|
+
`Expected one of: ${candidateIrohBinariesForInstall(config).join(", ")}`
|
|
499
|
+
].join(" ")
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
return { ok: true, built: true, binary: binary ?? null, manifestPath };
|
|
503
|
+
}
|
|
504
|
+
|
|
280
505
|
function detectOpenClaw() {
|
|
281
506
|
const installed =
|
|
282
507
|
commandExists("openclaw") ||
|
|
@@ -1679,9 +1904,8 @@ class PairingTransportUnavailableError extends Error {
|
|
|
1679
1904
|
this.code = "pairing_transport_unavailable";
|
|
1680
1905
|
this.detail = detail;
|
|
1681
1906
|
this.guidance = [
|
|
1682
|
-
"Run npx forge-memory
|
|
1683
|
-
"
|
|
1684
|
-
"On unsupported platforms, install Rust/Cargo so Forge can build the bundled companion Iroh source fallback.",
|
|
1907
|
+
"Run npx forge-memory install or npx forge-memory configure so the installer can prepare the Iroh transport.",
|
|
1908
|
+
"If Rust/Cargo is missing, the installer will guide you through installing the minimal Rust toolchain and building Forge's bundled Iroh host source.",
|
|
1685
1909
|
"For an explicit Tailscale or LAN fallback, rerun with npx forge-memory pair-ios --manual-http --public-url <phone-reachable Forge URL>.",
|
|
1686
1910
|
"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."
|
|
1687
1911
|
];
|
|
@@ -2060,6 +2284,25 @@ async function runInstall(parsed, command) {
|
|
|
2060
2284
|
dryRun: parsed.flags.dryRun
|
|
2061
2285
|
})
|
|
2062
2286
|
);
|
|
2287
|
+
const shouldPair =
|
|
2288
|
+
parsed.flags.pairIos ||
|
|
2289
|
+
(!parsed.flags.skipPairIos &&
|
|
2290
|
+
(parsed.flags.yes
|
|
2291
|
+
? true
|
|
2292
|
+
: await promptYesNo("Pair the iOS companion now?", true)));
|
|
2293
|
+
let irohTransportResult = null;
|
|
2294
|
+
if (
|
|
2295
|
+
shouldPair &&
|
|
2296
|
+
!parsed.flags.manualHttp &&
|
|
2297
|
+
!parsed.flags.dryRun
|
|
2298
|
+
) {
|
|
2299
|
+
irohTransportResult = await withProgress(
|
|
2300
|
+
"Preparing Forge Companion Iroh transport",
|
|
2301
|
+
"checking Rust/Cargo and building the local host",
|
|
2302
|
+
parsed.flags,
|
|
2303
|
+
() => ensureIrohTransportPrepared(config, parsed.flags)
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2063
2306
|
let runtimeResult = null;
|
|
2064
2307
|
if (!parsed.flags.noStart && !parsed.flags.dryRun) {
|
|
2065
2308
|
runtimeResult = await withProgress(
|
|
@@ -2097,12 +2340,6 @@ async function runInstall(parsed, command) {
|
|
|
2097
2340
|
}
|
|
2098
2341
|
}
|
|
2099
2342
|
}
|
|
2100
|
-
const shouldPair =
|
|
2101
|
-
parsed.flags.pairIos ||
|
|
2102
|
-
(!parsed.flags.skipPairIos &&
|
|
2103
|
-
(parsed.flags.yes
|
|
2104
|
-
? true
|
|
2105
|
-
: await promptYesNo("Pair the iOS companion now?", true)));
|
|
2106
2343
|
let pairing = null;
|
|
2107
2344
|
if (shouldPair && !parsed.flags.dryRun) {
|
|
2108
2345
|
if (!runtimeResult) {
|
|
@@ -2141,6 +2378,7 @@ async function runInstall(parsed, command) {
|
|
|
2141
2378
|
adapterResults,
|
|
2142
2379
|
runtimeResult,
|
|
2143
2380
|
doctorResult,
|
|
2381
|
+
irohTransportResult,
|
|
2144
2382
|
pairing: Boolean(pairing)
|
|
2145
2383
|
};
|
|
2146
2384
|
if (parsed.flags.json) console.log(JSON.stringify(summary, null, 2));
|
|
@@ -2351,6 +2589,14 @@ async function runPairIos(parsed) {
|
|
|
2351
2589
|
transportMode,
|
|
2352
2590
|
publicUrl: parsed.values.publicUrl
|
|
2353
2591
|
});
|
|
2592
|
+
if (transportMode === "iroh") {
|
|
2593
|
+
await withProgress(
|
|
2594
|
+
"Preparing Forge Companion Iroh transport",
|
|
2595
|
+
"checking Rust/Cargo and building the local host",
|
|
2596
|
+
parsed.flags,
|
|
2597
|
+
() => ensureIrohTransportPrepared(config, parsed.flags)
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2354
2600
|
if (!parsed.flags.noStart) {
|
|
2355
2601
|
const runtimeResult = await withProgress(
|
|
2356
2602
|
"Starting Forge runtime for iOS pairing",
|
package/package.json
CHANGED