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 +23 -2
- package/bin/forge-memory.mjs +207 -12
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# forge-memory
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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.
|
package/bin/forge-memory.mjs
CHANGED
|
@@ -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 = "
|
|
20
|
+
const VERSION = require("../package.json").version;
|
|
21
21
|
const RUNTIME_PACKAGE = "forge-openclaw-plugin";
|
|
22
|
-
const RUNTIME_PACKAGE_VERSION =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
},
|