forge-memory 0.1.0 → 0.2.63

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
@@ -1,18 +1,20 @@
1
1
  # forge-memory
2
2
 
3
- Preferred Forge installer:
3
+ Single-command Forge install:
4
4
 
5
5
  ```bash
6
6
  npx forge-memory
7
7
  ```
8
8
 
9
+ This is the preferred setup path for Forge. The command launches a guided CLI that installs the local Forge UI/runtime first, then discovers OpenClaw, Hermes, and Codex and offers to configure the detected adapters against the same Forge data folder.
10
+
9
11
  Development install from a Forge checkout:
10
12
 
11
13
  ```bash
12
14
  npx forge-memory --dev
13
15
  ```
14
16
 
15
- This package installs and manages the local Forge UI/runtime, then configures detected host adapters for OpenClaw, Hermes, and Codex. The Forge UI/runtime is always the base install; the adapter checkbox list only contains host integrations.
17
+ The Forge UI/runtime is always installed. The adapter checkbox list only contains host integrations, with detected adapters selected by default and missing adapters shown as disabled rows. You can skip adapter setup during install and return later with `configure`.
16
18
 
17
19
  Useful commands:
18
20
 
@@ -22,7 +24,26 @@ npx forge-memory status
22
24
  npx forge-memory doctor
23
25
  npx forge-memory ui
24
26
  npx forge-memory restart
27
+ npx forge-memory stop
28
+ npx forge-memory export
29
+ npx forge-memory uninstall
25
30
  npx forge-memory pair-ios
26
31
  ```
27
32
 
33
+ `pair-ios` prefers the Iroh QR. Forge starts a Rust Iroh host, prints a QR payload
34
+ with the desktop node id, pairing token, optional relay hint, and ALPN
35
+ `forge-companion/1`, and the iPhone app connects through its native Rust bridge. Use
36
+ `--manual-http` only when you intentionally want a LAN, Tailscale, or direct HTTP/TCP
37
+ route.
38
+
28
39
  `configure` reruns the full guided flow using the current config as defaults.
40
+ `export` writes a portable backup of the real Forge data folder. `uninstall` removes the Forge Memory runtime manager and cache while keeping the data folder by default; pass `--remove-data` only when you intentionally want the data deleted too.
41
+
42
+ Typical first run:
43
+
44
+ 1. Run `npx forge-memory`.
45
+ 2. Keep or change the real Forge data folder.
46
+ 3. Select OpenClaw, Hermes, and Codex adapters with Space.
47
+ 4. Pair the iOS companion when prompted, or skip and run `npx forge-memory pair-ios` later.
48
+
49
+ Manual OpenClaw, Hermes, and Codex commands still exist in the Forge repository for advanced recovery, source-linking, and adapter debugging. The normal user path should start here.
@@ -17,9 +17,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
17
17
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
18
18
 
19
19
  const require = createRequire(import.meta.url);
20
- const VERSION = "0.1.0";
20
+ const VERSION = require("../package.json").version;
21
21
  const RUNTIME_PACKAGE = "forge-openclaw-plugin";
22
- const RUNTIME_PACKAGE_VERSION = "0.2.61";
22
+ const RUNTIME_PACKAGE_VERSION = VERSION;
23
23
  const DEFAULT_ORIGIN = "http://127.0.0.1";
24
24
  const DEFAULT_PORT = 4317;
25
25
  const DEFAULT_WEB_PORT = 3027;
@@ -45,7 +45,10 @@ function parseArgs(argv) {
45
45
  skipPairIos: false,
46
46
  pairIos: false,
47
47
  skipAdapters: false,
48
- printUrl: false
48
+ printUrl: false,
49
+ removeData: false,
50
+ removeAdapters: false,
51
+ manualHttp: false
49
52
  };
50
53
  const values = {};
51
54
  const positionals = [];
@@ -65,6 +68,11 @@ function parseArgs(argv) {
65
68
  else if (arg === "--pair-ios") flags.pairIos = true;
66
69
  else if (arg === "--skip-adapters") flags.skipAdapters = true;
67
70
  else if (arg === "--print-url") flags.printUrl = true;
71
+ else if (arg === "--remove-data") flags.removeData = true;
72
+ else if (arg === "--remove-adapters") flags.removeAdapters = true;
73
+ else if (arg === "--manual-http" || arg === "--no-iroh") flags.manualHttp = true;
74
+ else if (arg.startsWith("--output=")) values.output = arg.slice("--output=".length);
75
+ else if (arg === "--output") values.output = argv[++index];
68
76
  else if (arg.startsWith("--data-root=")) values.dataRoot = arg.slice("--data-root=".length);
69
77
  else if (arg === "--data-root") values.dataRoot = argv[++index];
70
78
  else if (arg.startsWith("--adapters=")) values.adapters = arg.slice("--adapters=".length);
@@ -786,16 +794,183 @@ async function stopRuntime() {
786
794
  return { ok: true, stopped: stopped.length > 0, pids: stopped };
787
795
  }
788
796
 
789
- async function createPairing(config) {
797
+ async function exportForgeData(parsed) {
798
+ const config = await readConfig();
799
+ if (!fs.existsSync(config.dataRoot)) {
800
+ throw new Error(`Forge data folder does not exist: ${config.dataRoot}`);
801
+ }
802
+
803
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
804
+ const requestedOutput = parsed.values.output ?? parsed.positionals[1];
805
+ const outputPath = path.resolve(
806
+ requestedOutput ?? path.join(forgeHome(), "exports", `forge-memory-export-${stamp}.tar.gz`)
807
+ );
808
+ const stagingRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "forge-memory-export-"));
809
+ const stagingData = path.join(stagingRoot, "data");
810
+ const manifest = {
811
+ exportedAt: new Date().toISOString(),
812
+ forgeMemoryVersion: VERSION,
813
+ sourceDataRoot: config.dataRoot,
814
+ config: {
815
+ mode: config.mode,
816
+ origin: config.origin,
817
+ port: config.port,
818
+ webPort: config.webPort,
819
+ adapters: config.adapters
820
+ }
821
+ };
822
+
823
+ const skipTopLevel = new Set(["exports", "logs", "run", "runtime"]);
824
+ await fsp.cp(config.dataRoot, stagingData, {
825
+ recursive: true,
826
+ force: false,
827
+ errorOnExist: false,
828
+ filter: (source) => {
829
+ const relative = path.relative(config.dataRoot, source);
830
+ if (!relative) return true;
831
+ return !skipTopLevel.has(relative.split(path.sep)[0]);
832
+ }
833
+ });
834
+ if (fs.existsSync(configPath())) {
835
+ await fsp.copyFile(configPath(), path.join(stagingRoot, "forge-memory-config.json"));
836
+ }
837
+ await fsp.writeFile(path.join(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
838
+ await fsp.mkdir(path.dirname(outputPath), { recursive: true });
839
+
840
+ const wantsArchive = outputPath.endsWith(".tar.gz") || outputPath.endsWith(".tgz");
841
+ if (wantsArchive && commandExists("tar")) {
842
+ const result = spawnSync("tar", ["-czf", outputPath, "-C", stagingRoot, "."], {
843
+ stdio: parsed.flags.json ? "ignore" : "inherit"
844
+ });
845
+ await fsp.rm(stagingRoot, { recursive: true, force: true });
846
+ if (result.status !== 0) throw new Error(`Failed to write export archive: ${outputPath}`);
847
+ return { ok: true, outputPath, archive: true, sourceDataRoot: config.dataRoot };
848
+ }
849
+
850
+ await fsp.rm(outputPath, { recursive: true, force: true });
851
+ await fsp.cp(stagingRoot, outputPath, { recursive: true });
852
+ await fsp.rm(stagingRoot, { recursive: true, force: true });
853
+ return { ok: true, outputPath, archive: false, sourceDataRoot: config.dataRoot };
854
+ }
855
+
856
+ async function removeOpenClawAdapterConfig() {
857
+ const filePath = path.join(homeDir(), ".openclaw", "openclaw.json");
858
+ const payload = await readJson(filePath, null);
859
+ if (!payload?.plugins?.entries?.[FORGE_PLUGIN_ID]) return { filePath, changed: false };
860
+ await backupIfExists(filePath);
861
+ delete payload.plugins.entries[FORGE_PLUGIN_ID];
862
+ if (Array.isArray(payload.plugins.allow)) {
863
+ payload.plugins.allow = payload.plugins.allow.filter((entry) => entry !== FORGE_PLUGIN_ID);
864
+ }
865
+ await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
866
+ return { filePath, changed: true };
867
+ }
868
+
869
+ async function removeHermesAdapterConfig() {
870
+ const forgeConfigPath = path.join(homeDir(), ".hermes", "forge", "config.json");
871
+ const changed = fs.existsSync(forgeConfigPath);
872
+ if (changed) {
873
+ await backupIfExists(forgeConfigPath);
874
+ await fsp.rm(forgeConfigPath, { force: true });
875
+ }
876
+ return { filePath: forgeConfigPath, changed };
877
+ }
878
+
879
+ async function removeCodexAdapterConfig() {
880
+ const filePath = path.join(homeDir(), ".codex", "config.toml");
881
+ if (!fs.existsSync(filePath)) return { filePath, changed: false };
882
+ const source = await fsp.readFile(filePath, "utf8");
883
+ const pattern = /(?:^|\n)\[mcp_servers\.forge\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/m;
884
+ if (!pattern.test(source)) return { filePath, changed: false };
885
+ await backupIfExists(filePath);
886
+ const next = source.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").trimEnd();
887
+ await fsp.writeFile(filePath, next ? `${next}\n` : "", "utf8");
888
+ return { filePath, changed: true };
889
+ }
890
+
891
+ async function uninstallForgeMemory(parsed) {
892
+ const config = await readConfig();
893
+ const confirmed = parsed.flags.yes
894
+ ? true
895
+ : await promptYesNo(
896
+ `Uninstall Forge Memory runtime manager and keep data at ${config.dataRoot}?`,
897
+ true
898
+ );
899
+ if (!confirmed) return { ok: false, cancelled: true };
900
+
901
+ const stop = await stopRuntime();
902
+ const removed = [];
903
+ for (const target of [runtimeInstallRoot(), runtimeStatePath(), logPath(), configPath()]) {
904
+ if (fs.existsSync(target)) {
905
+ await fsp.rm(target, { recursive: true, force: true });
906
+ removed.push(target);
907
+ }
908
+ }
909
+
910
+ let adapterResults = [];
911
+ const removeAdapters = parsed.flags.removeAdapters || (!parsed.flags.yes && await promptYesNo("Remove Forge adapter entries from OpenClaw, Hermes, and Codex?", false));
912
+ if (removeAdapters) {
913
+ adapterResults = [
914
+ await removeOpenClawAdapterConfig(),
915
+ await removeHermesAdapterConfig(),
916
+ await removeCodexAdapterConfig()
917
+ ];
918
+ }
919
+
920
+ let removedDataRoot = false;
921
+ if (parsed.flags.removeData) {
922
+ const dataConfirmed = parsed.flags.yes
923
+ ? true
924
+ : await promptYesNo(`Delete Forge data folder ${config.dataRoot}? This cannot be undone.`, false);
925
+ if (dataConfirmed) {
926
+ await fsp.rm(config.dataRoot, { recursive: true, force: true });
927
+ removedDataRoot = true;
928
+ }
929
+ }
930
+
931
+ return {
932
+ ok: true,
933
+ stop,
934
+ removed,
935
+ adapterResults,
936
+ dataRoot: config.dataRoot,
937
+ dataKept: !removedDataRoot,
938
+ removedDataRoot
939
+ };
940
+ }
941
+
942
+ async function createPairing(config, options = {}) {
943
+ const transportMode = options.transportMode ?? "iroh";
790
944
  const response = await fetch(new URL("/api/v1/health/pairing-sessions", baseUrl(config)), {
791
945
  method: "POST",
792
946
  headers: { "content-type": "application/json", accept: "application/json" },
793
- body: JSON.stringify({ userId: null })
947
+ body: JSON.stringify({ userId: null, transportMode })
794
948
  });
795
949
  if (!response.ok) throw new Error(`Pairing request failed with ${response.status}`);
796
950
  return response.json();
797
951
  }
798
952
 
953
+ function printPairing(pairing) {
954
+ console.log("\nScan this QR in Forge Companion:\n");
955
+ qrcode.generate(JSON.stringify(pairing.qrPayload), { small: true });
956
+ const transport = pairing.qrPayload?.transport;
957
+ if (transport?.provider) {
958
+ const label = pairing.qrPayload.transport?.protocol === "iroh"
959
+ ? "Iroh"
960
+ : pairing.qrPayload.transportMode === "iroh"
961
+ ? "Iroh"
962
+ : "Manual HTTP";
963
+ console.log(`${color.cyan(label)}: ${pairing.qrPayload.apiBaseUrl}`);
964
+ if (transport.recreateCommand) {
965
+ console.log(`${color.dim("recreate:")} ${transport.recreateCommand}`);
966
+ }
967
+ for (const note of transport.notes ?? []) {
968
+ console.log(color.dim(note));
969
+ }
970
+ }
971
+ console.log(JSON.stringify(pairing.qrPayload, null, 2));
972
+ }
973
+
799
974
  async function runInstall(parsed, command) {
800
975
  const currentConfig = await readConfig();
801
976
  const discovery = discover();
@@ -814,11 +989,11 @@ async function runInstall(parsed, command) {
814
989
  let pairing = null;
815
990
  if (shouldPair && !parsed.flags.dryRun) {
816
991
  if (!runtimeResult) await startRuntime(config);
817
- pairing = await createPairing(config);
992
+ pairing = await createPairing(config, {
993
+ transportMode: parsed.flags.manualHttp ? "manual-http" : "iroh"
994
+ });
818
995
  if (pairing?.qrPayload && !parsed.flags.json) {
819
- console.log("\nScan this QR in Forge Companion:\n");
820
- qrcode.generate(JSON.stringify(pairing.qrPayload), { small: true });
821
- console.log(JSON.stringify(pairing.qrPayload, null, 2));
996
+ printPairing(pairing);
822
997
  }
823
998
  }
824
999
  const summary = { ok: true, config, writeResult, adapterResults, runtimeResult, pairing: Boolean(pairing) };
@@ -890,13 +1065,14 @@ async function runUi(parsed) {
890
1065
  async function runPairIos(parsed) {
891
1066
  const config = await readConfig();
892
1067
  await startRuntime(config);
893
- const pairing = await createPairing(config);
1068
+ const pairing = await createPairing(config, {
1069
+ transportMode: parsed.flags.manualHttp ? "manual-http" : "iroh"
1070
+ });
894
1071
  if (parsed.flags.json) {
895
1072
  console.log(JSON.stringify(pairing, null, 2));
896
1073
  return;
897
1074
  }
898
- qrcode.generate(JSON.stringify(pairing.qrPayload), { small: true });
899
- console.log(JSON.stringify(pairing.qrPayload, null, 2));
1075
+ printPairing(pairing);
900
1076
  }
901
1077
 
902
1078
  async function runLogs() {
@@ -959,6 +1135,9 @@ Usage:
959
1135
  npx forge-memory doctor
960
1136
  npx forge-memory ui
961
1137
  npx forge-memory restart
1138
+ npx forge-memory stop
1139
+ npx forge-memory export
1140
+ npx forge-memory uninstall
962
1141
  npx forge-memory pair-ios
963
1142
 
964
1143
  Options:
@@ -968,7 +1147,11 @@ Options:
968
1147
  --adapters <list> Comma list: openclaw,hermes,codex or none
969
1148
  --skip-adapters Configure UI/runtime only
970
1149
  --skip-pair-ios Do not prompt or create iOS pairing
1150
+ --manual-http Use direct HTTP/TCP for iOS pairing instead of the default Iroh transport
971
1151
  --no-start Configure without starting runtime
1152
+ --output <path> Export destination for forge-memory export
1153
+ --remove-adapters During uninstall, remove host adapter config entries
1154
+ --remove-data During uninstall, delete the Forge data folder too
972
1155
  --dry-run Show actions without writing files or installing adapters
973
1156
  --json Print machine-readable output where supported
974
1157
  `);
@@ -1001,6 +1184,18 @@ async function main() {
1001
1184
  case "stop":
1002
1185
  console.log(JSON.stringify(await stopRuntime(), null, 2));
1003
1186
  break;
1187
+ case "export":
1188
+ {
1189
+ const result = await exportForgeData(parsed);
1190
+ console.log(parsed.flags.json ? JSON.stringify(result, null, 2) : `Exported Forge data to ${result.outputPath}`);
1191
+ }
1192
+ break;
1193
+ case "uninstall":
1194
+ {
1195
+ const result = await uninstallForgeMemory(parsed);
1196
+ console.log(parsed.flags.json ? JSON.stringify(result, null, 2) : result.cancelled ? "Uninstall cancelled." : "Forge Memory uninstalled.");
1197
+ }
1198
+ break;
1004
1199
  case "restart":
1005
1200
  await stopRuntime();
1006
1201
  console.log(JSON.stringify(await startRuntime(await readConfig()), null, 2));
package/package.json CHANGED
@@ -1,10 +1,19 @@
1
1
  {
2
2
  "name": "forge-memory",
3
- "version": "0.1.0",
3
+ "version": "0.2.63",
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
- "license": "MIT",
6
+ "license": "Apache-2.0",
7
7
  "private": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/albertbuchard/forge.git",
11
+ "directory": "packages/forge-memory"
12
+ },
13
+ "homepage": "https://github.com/albertbuchard/forge/tree/main/packages/forge-memory#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/albertbuchard/forge/issues"
16
+ },
8
17
  "bin": {
9
18
  "forge-memory": "./bin/forge-memory.mjs"
10
19
  },