forge-memory 0.2.96 → 0.2.99
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 -0
- package/bin/forge-memory.mjs +766 -150
- package/package.json +1 -1
package/bin/forge-memory.mjs
CHANGED
|
@@ -8,13 +8,19 @@ import os from "node:os";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import readline from "node:readline";
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { TextDecoder, TextEncoder } from "node:util";
|
|
11
12
|
import { createRequire } from "node:module";
|
|
12
13
|
import YAML from "yaml";
|
|
13
14
|
import qrcode from "qrcode-terminal";
|
|
14
15
|
import open from "open";
|
|
15
16
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
17
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
CallToolRequestSchema,
|
|
20
|
+
ErrorCode,
|
|
21
|
+
ListToolsRequestSchema,
|
|
22
|
+
McpError
|
|
23
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
18
24
|
|
|
19
25
|
const require = createRequire(import.meta.url);
|
|
20
26
|
const VERSION = require("../package.json").version;
|
|
@@ -64,26 +70,35 @@ function parseArgs(argv) {
|
|
|
64
70
|
else if (arg === "--dry-run") flags.dryRun = true;
|
|
65
71
|
else if (arg === "--no-start") flags.noStart = true;
|
|
66
72
|
else if (arg === "--json") flags.json = true;
|
|
67
|
-
else if (arg === "--skip-pair-ios" || arg === "--no-pair-ios")
|
|
73
|
+
else if (arg === "--skip-pair-ios" || arg === "--no-pair-ios")
|
|
74
|
+
flags.skipPairIos = true;
|
|
68
75
|
else if (arg === "--pair-ios") flags.pairIos = true;
|
|
69
76
|
else if (arg === "--skip-adapters") flags.skipAdapters = true;
|
|
70
77
|
else if (arg === "--print-url") flags.printUrl = true;
|
|
71
78
|
else if (arg === "--remove-data") flags.removeData = true;
|
|
72
79
|
else if (arg === "--remove-adapters") flags.removeAdapters = true;
|
|
73
|
-
else if (arg === "--manual-http" || arg === "--no-iroh")
|
|
74
|
-
|
|
80
|
+
else if (arg === "--manual-http" || arg === "--no-iroh")
|
|
81
|
+
flags.manualHttp = true;
|
|
82
|
+
else if (arg.startsWith("--output="))
|
|
83
|
+
values.output = arg.slice("--output=".length);
|
|
75
84
|
else if (arg === "--output") values.output = argv[++index];
|
|
76
|
-
else if (arg.startsWith("--data-root="))
|
|
85
|
+
else if (arg.startsWith("--data-root="))
|
|
86
|
+
values.dataRoot = arg.slice("--data-root=".length);
|
|
77
87
|
else if (arg === "--data-root") values.dataRoot = argv[++index];
|
|
78
|
-
else if (arg.startsWith("--adapters="))
|
|
88
|
+
else if (arg.startsWith("--adapters="))
|
|
89
|
+
values.adapters = arg.slice("--adapters=".length);
|
|
79
90
|
else if (arg === "--adapters") values.adapters = argv[++index];
|
|
80
|
-
else if (arg.startsWith("--origin="))
|
|
91
|
+
else if (arg.startsWith("--origin="))
|
|
92
|
+
values.origin = arg.slice("--origin=".length);
|
|
81
93
|
else if (arg === "--origin") values.origin = argv[++index];
|
|
82
|
-
else if (arg.startsWith("--port="))
|
|
94
|
+
else if (arg.startsWith("--port="))
|
|
95
|
+
values.port = arg.slice("--port=".length);
|
|
83
96
|
else if (arg === "--port") values.port = argv[++index];
|
|
84
|
-
else if (arg.startsWith("--web-port="))
|
|
97
|
+
else if (arg.startsWith("--web-port="))
|
|
98
|
+
values.webPort = arg.slice("--web-port=".length);
|
|
85
99
|
else if (arg === "--web-port") values.webPort = argv[++index];
|
|
86
|
-
else if (arg.startsWith("--repo="))
|
|
100
|
+
else if (arg.startsWith("--repo="))
|
|
101
|
+
values.repo = arg.slice("--repo=".length);
|
|
87
102
|
else if (arg === "--repo") values.repo = argv[++index];
|
|
88
103
|
else if (arg === "--help" || arg === "-h") flags.help = true;
|
|
89
104
|
else if (arg === "--version" || arg === "-v") flags.version = true;
|
|
@@ -128,7 +143,9 @@ function defaultDataRoot() {
|
|
|
128
143
|
|
|
129
144
|
function normalizePort(value, fallback = DEFAULT_PORT) {
|
|
130
145
|
const parsed = Number(value);
|
|
131
|
-
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 65535
|
|
146
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 65535
|
|
147
|
+
? parsed
|
|
148
|
+
: fallback;
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
function normalizeAdapterList(value) {
|
|
@@ -170,11 +187,19 @@ async function backupIfExists(filePath) {
|
|
|
170
187
|
return backupPath;
|
|
171
188
|
}
|
|
172
189
|
|
|
173
|
-
async function writeJson(
|
|
190
|
+
async function writeJson(
|
|
191
|
+
filePath,
|
|
192
|
+
payload,
|
|
193
|
+
{ dryRun = false, backup = true } = {}
|
|
194
|
+
) {
|
|
174
195
|
if (dryRun) return { filePath, backupPath: null, dryRun: true };
|
|
175
196
|
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
176
197
|
const backupPath = backup ? await backupIfExists(filePath) : null;
|
|
177
|
-
await fsp.writeFile(
|
|
198
|
+
await fsp.writeFile(
|
|
199
|
+
filePath,
|
|
200
|
+
`${JSON.stringify(payload, null, 2)}\n`,
|
|
201
|
+
"utf8"
|
|
202
|
+
);
|
|
178
203
|
return { filePath, backupPath, dryRun: false };
|
|
179
204
|
}
|
|
180
205
|
|
|
@@ -186,8 +211,13 @@ async function readConfig() {
|
|
|
186
211
|
origin: typeof config?.origin === "string" ? config.origin : DEFAULT_ORIGIN,
|
|
187
212
|
port: normalizePort(config?.port, DEFAULT_PORT),
|
|
188
213
|
webPort: normalizePort(config?.webPort, DEFAULT_WEB_PORT),
|
|
189
|
-
dataRoot:
|
|
190
|
-
|
|
214
|
+
dataRoot:
|
|
215
|
+
typeof config?.dataRoot === "string"
|
|
216
|
+
? path.resolve(config.dataRoot)
|
|
217
|
+
: defaultDataRoot(),
|
|
218
|
+
adapters: Array.isArray(config?.adapters)
|
|
219
|
+
? config.adapters.filter((entry) => ADAPTERS.includes(entry))
|
|
220
|
+
: [],
|
|
191
221
|
updatedAt: typeof config?.updatedAt === "string" ? config.updatedAt : null,
|
|
192
222
|
repo: typeof config?.repo === "string" ? config.repo : null
|
|
193
223
|
};
|
|
@@ -209,10 +239,14 @@ async function writeConfig(next, options) {
|
|
|
209
239
|
}
|
|
210
240
|
|
|
211
241
|
function commandExists(command) {
|
|
212
|
-
const result = spawnSync(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
242
|
+
const result = spawnSync(
|
|
243
|
+
process.platform === "win32" ? "where" : "command",
|
|
244
|
+
process.platform === "win32" ? [command] : ["-v", command],
|
|
245
|
+
{
|
|
246
|
+
shell: process.platform !== "win32",
|
|
247
|
+
stdio: "ignore"
|
|
248
|
+
}
|
|
249
|
+
);
|
|
216
250
|
return result.status === 0;
|
|
217
251
|
}
|
|
218
252
|
|
|
@@ -226,15 +260,19 @@ function runCapture(command, args, timeoutMs = 2_000) {
|
|
|
226
260
|
}
|
|
227
261
|
|
|
228
262
|
function detectOpenClaw() {
|
|
229
|
-
const installed =
|
|
230
|
-
|
|
263
|
+
const installed =
|
|
264
|
+
commandExists("openclaw") ||
|
|
265
|
+
fs.existsSync(path.join(homeDir(), ".openclaw"));
|
|
266
|
+
const version = commandExists("openclaw")
|
|
267
|
+
? runCapture("openclaw", ["--version"])
|
|
268
|
+
: null;
|
|
231
269
|
const config = path.join(homeDir(), ".openclaw", "openclaw.json");
|
|
232
270
|
return {
|
|
233
271
|
id: "openclaw",
|
|
234
272
|
label: "OpenClaw",
|
|
235
273
|
installed,
|
|
236
274
|
disabled: !installed,
|
|
237
|
-
status: installed ?
|
|
275
|
+
status: installed ? version || "detected" : "not found",
|
|
238
276
|
configPath: config,
|
|
239
277
|
hint: "Install OpenClaw first, then rerun npx forge-memory configure."
|
|
240
278
|
};
|
|
@@ -242,15 +280,26 @@ function detectOpenClaw() {
|
|
|
242
280
|
|
|
243
281
|
function detectHermes() {
|
|
244
282
|
const hermesRoot = path.join(homeDir(), ".hermes");
|
|
245
|
-
const hermesPython = path.join(
|
|
246
|
-
|
|
247
|
-
|
|
283
|
+
const hermesPython = path.join(
|
|
284
|
+
hermesRoot,
|
|
285
|
+
"hermes-agent",
|
|
286
|
+
"venv",
|
|
287
|
+
"bin",
|
|
288
|
+
"python"
|
|
289
|
+
);
|
|
290
|
+
const installed =
|
|
291
|
+
commandExists("hermes") ||
|
|
292
|
+
fs.existsSync(hermesPython) ||
|
|
293
|
+
fs.existsSync(hermesRoot);
|
|
294
|
+
const version = commandExists("hermes")
|
|
295
|
+
? runCapture("hermes", ["--version"])
|
|
296
|
+
: null;
|
|
248
297
|
return {
|
|
249
298
|
id: "hermes",
|
|
250
299
|
label: "Hermes",
|
|
251
300
|
installed,
|
|
252
301
|
disabled: !installed,
|
|
253
|
-
status: installed ?
|
|
302
|
+
status: installed ? version || "detected" : "not found",
|
|
254
303
|
configPath: path.join(hermesRoot, "forge", "config.json"),
|
|
255
304
|
pythonPath: hermesPython,
|
|
256
305
|
hint: "Install Hermes first, then rerun npx forge-memory configure."
|
|
@@ -260,13 +309,15 @@ function detectHermes() {
|
|
|
260
309
|
function detectCodex() {
|
|
261
310
|
const codexRoot = path.join(homeDir(), ".codex");
|
|
262
311
|
const installed = commandExists("codex") || fs.existsSync(codexRoot);
|
|
263
|
-
const version = commandExists("codex")
|
|
312
|
+
const version = commandExists("codex")
|
|
313
|
+
? runCapture("codex", ["--version"])
|
|
314
|
+
: null;
|
|
264
315
|
return {
|
|
265
316
|
id: "codex",
|
|
266
317
|
label: "Codex",
|
|
267
318
|
installed,
|
|
268
319
|
disabled: !installed,
|
|
269
|
-
status: installed ?
|
|
320
|
+
status: installed ? version || "detected" : "not found",
|
|
270
321
|
configPath: path.join(codexRoot, "config.toml"),
|
|
271
322
|
hint: "Install Codex first, then rerun npx forge-memory configure."
|
|
272
323
|
};
|
|
@@ -287,7 +338,10 @@ function printBanner() {
|
|
|
287
338
|
|
|
288
339
|
async function promptLine(question, defaultValue) {
|
|
289
340
|
const suffix = defaultValue ? ` ${color.dim(`[${defaultValue}]`)}` : "";
|
|
290
|
-
const rl = readline.createInterface({
|
|
341
|
+
const rl = readline.createInterface({
|
|
342
|
+
input: process.stdin,
|
|
343
|
+
output: process.stdout
|
|
344
|
+
});
|
|
291
345
|
return await new Promise((resolve) => {
|
|
292
346
|
rl.question(`${question}${suffix}: `, (answer) => {
|
|
293
347
|
rl.close();
|
|
@@ -297,7 +351,9 @@ async function promptLine(question, defaultValue) {
|
|
|
297
351
|
}
|
|
298
352
|
|
|
299
353
|
async function promptYesNo(question, defaultValue = true) {
|
|
300
|
-
const answer = (
|
|
354
|
+
const answer = (
|
|
355
|
+
await promptLine(`${question} ${defaultValue ? "[Y/n]" : "[y/N]"}`, "")
|
|
356
|
+
).toLowerCase();
|
|
301
357
|
if (!answer) return defaultValue;
|
|
302
358
|
return answer === "y" || answer === "yes";
|
|
303
359
|
}
|
|
@@ -328,7 +384,9 @@ async function promptCheckbox(adapters, defaults) {
|
|
|
328
384
|
process.stdout.write("\u001b[?25l");
|
|
329
385
|
process.stdout.write("\u001b[2J\u001b[H");
|
|
330
386
|
printBanner();
|
|
331
|
-
console.log(
|
|
387
|
+
console.log(
|
|
388
|
+
"Select host adapters. Space toggles, arrows move, Enter confirms.\n"
|
|
389
|
+
);
|
|
332
390
|
for (let index = 0; index < rows.length; index += 1) {
|
|
333
391
|
const row = rows[index];
|
|
334
392
|
const prefix = index === cursor ? color.cyan(">") : " ";
|
|
@@ -354,7 +412,13 @@ async function promptCheckbox(adapters, defaults) {
|
|
|
354
412
|
resolve([]);
|
|
355
413
|
return;
|
|
356
414
|
}
|
|
357
|
-
resolve(
|
|
415
|
+
resolve(
|
|
416
|
+
rows
|
|
417
|
+
.filter(
|
|
418
|
+
(entry) => entry.selected && !entry.disabled && !entry.action
|
|
419
|
+
)
|
|
420
|
+
.map((entry) => entry.id)
|
|
421
|
+
);
|
|
358
422
|
return;
|
|
359
423
|
}
|
|
360
424
|
if (key === " ") {
|
|
@@ -403,19 +467,30 @@ async function findFreePort(startPort) {
|
|
|
403
467
|
server.once("error", reject);
|
|
404
468
|
server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, () => {
|
|
405
469
|
const address = server.address();
|
|
406
|
-
const port =
|
|
470
|
+
const port =
|
|
471
|
+
typeof address === "object" && address ? address.port : DEFAULT_PORT;
|
|
407
472
|
server.close(() => resolve(port));
|
|
408
473
|
});
|
|
409
474
|
});
|
|
410
475
|
}
|
|
411
|
-
for (
|
|
476
|
+
for (
|
|
477
|
+
let port = startPort;
|
|
478
|
+
port < startPort + 30 && port <= 65535;
|
|
479
|
+
port += 1
|
|
480
|
+
) {
|
|
412
481
|
if (await isPortAvailable(port)) return port;
|
|
413
482
|
}
|
|
414
483
|
throw new Error(`No free localhost port found near ${startPort}`);
|
|
415
484
|
}
|
|
416
485
|
|
|
417
486
|
async function resolveDevDataRoot(repoRoot) {
|
|
418
|
-
const preferencePath = path.resolve(
|
|
487
|
+
const preferencePath = path.resolve(
|
|
488
|
+
repoRoot,
|
|
489
|
+
"..",
|
|
490
|
+
"..",
|
|
491
|
+
"data",
|
|
492
|
+
"forge-runtime.json"
|
|
493
|
+
);
|
|
419
494
|
const monorepoDataRoot = path.resolve(repoRoot, "..", "..", "data", "forge");
|
|
420
495
|
const preference = await readJson(preferencePath, null);
|
|
421
496
|
if (typeof preference?.dataRoot === "string" && preference.dataRoot.trim()) {
|
|
@@ -432,7 +507,10 @@ function findForgeRepo(start = process.cwd()) {
|
|
|
432
507
|
if (fs.existsSync(packageJsonPath)) {
|
|
433
508
|
try {
|
|
434
509
|
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
435
|
-
if (
|
|
510
|
+
if (
|
|
511
|
+
parsed?.name === "forge" &&
|
|
512
|
+
fs.existsSync(path.join(current, "server", "src", "index.ts"))
|
|
513
|
+
) {
|
|
436
514
|
return current;
|
|
437
515
|
}
|
|
438
516
|
} catch {
|
|
@@ -446,21 +524,41 @@ function findForgeRepo(start = process.cwd()) {
|
|
|
446
524
|
}
|
|
447
525
|
|
|
448
526
|
async function buildInstallConfig(parsed, currentConfig, discovery, command) {
|
|
449
|
-
const repo = parsed.values.repo
|
|
527
|
+
const repo = parsed.values.repo
|
|
528
|
+
? path.resolve(parsed.values.repo)
|
|
529
|
+
: findForgeRepo();
|
|
450
530
|
const mode = parsed.flags.dev ? "dev" : currentConfig.mode;
|
|
451
|
-
const detectedDefaults = discovery.adapters
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
531
|
+
const detectedDefaults = discovery.adapters
|
|
532
|
+
.filter((adapter) => adapter.installed)
|
|
533
|
+
.map((adapter) => adapter.id);
|
|
534
|
+
const currentDefaults =
|
|
535
|
+
currentConfig.adapters.length > 0
|
|
536
|
+
? currentConfig.adapters
|
|
537
|
+
: detectedDefaults;
|
|
538
|
+
const adapterOverride = parsed.flags.skipAdapters
|
|
539
|
+
? []
|
|
540
|
+
: normalizeAdapterList(parsed.values.adapters);
|
|
541
|
+
const adapters =
|
|
542
|
+
adapterOverride ??
|
|
543
|
+
(parsed.flags.yes
|
|
544
|
+
? currentDefaults
|
|
545
|
+
: await promptCheckbox(discovery.adapters, currentDefaults));
|
|
455
546
|
const dataRootDefault =
|
|
456
547
|
parsed.values.dataRoot ??
|
|
457
|
-
(parsed.flags.dev && repo
|
|
548
|
+
(parsed.flags.dev && repo
|
|
549
|
+
? await resolveDevDataRoot(repo)
|
|
550
|
+
: currentConfig.dataRoot || defaultDataRoot());
|
|
458
551
|
const dataRoot = parsed.flags.yes
|
|
459
552
|
? dataRootDefault
|
|
460
553
|
: await promptLine("Forge data folder", dataRootDefault);
|
|
461
554
|
const portInput = parsed.values.port ?? currentConfig.port;
|
|
462
555
|
const port = await findFreePort(normalizePort(portInput, DEFAULT_PORT));
|
|
463
|
-
const webPort = await findFreePort(
|
|
556
|
+
const webPort = await findFreePort(
|
|
557
|
+
normalizePort(
|
|
558
|
+
parsed.values.webPort ?? currentConfig.webPort,
|
|
559
|
+
DEFAULT_WEB_PORT
|
|
560
|
+
)
|
|
561
|
+
);
|
|
464
562
|
|
|
465
563
|
return {
|
|
466
564
|
version: VERSION,
|
|
@@ -478,10 +576,22 @@ async function buildInstallConfig(parsed, currentConfig, discovery, command) {
|
|
|
478
576
|
async function patchOpenClawConfig(config, options) {
|
|
479
577
|
const filePath = path.join(homeDir(), ".openclaw", "openclaw.json");
|
|
480
578
|
const payload = (await readJson(filePath, {})) ?? {};
|
|
481
|
-
const plugins =
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
579
|
+
const plugins =
|
|
580
|
+
payload.plugins && typeof payload.plugins === "object"
|
|
581
|
+
? { ...payload.plugins }
|
|
582
|
+
: {};
|
|
583
|
+
const entries =
|
|
584
|
+
plugins.entries && typeof plugins.entries === "object"
|
|
585
|
+
? { ...plugins.entries }
|
|
586
|
+
: {};
|
|
587
|
+
const currentEntry =
|
|
588
|
+
entries[FORGE_PLUGIN_ID] && typeof entries[FORGE_PLUGIN_ID] === "object"
|
|
589
|
+
? { ...entries[FORGE_PLUGIN_ID] }
|
|
590
|
+
: {};
|
|
591
|
+
const currentPluginConfig =
|
|
592
|
+
currentEntry.config && typeof currentEntry.config === "object"
|
|
593
|
+
? { ...currentEntry.config }
|
|
594
|
+
: {};
|
|
485
595
|
currentEntry.enabled = true;
|
|
486
596
|
currentEntry.config = {
|
|
487
597
|
...currentPluginConfig,
|
|
@@ -496,7 +606,12 @@ async function patchOpenClawConfig(config, options) {
|
|
|
496
606
|
}
|
|
497
607
|
|
|
498
608
|
async function patchHermesConfig(config, options) {
|
|
499
|
-
const forgeConfigPath = path.join(
|
|
609
|
+
const forgeConfigPath = path.join(
|
|
610
|
+
homeDir(),
|
|
611
|
+
".hermes",
|
|
612
|
+
"forge",
|
|
613
|
+
"config.json"
|
|
614
|
+
);
|
|
500
615
|
await writeJson(
|
|
501
616
|
forgeConfigPath,
|
|
502
617
|
{
|
|
@@ -516,7 +631,8 @@ async function patchHermesConfig(config, options) {
|
|
|
516
631
|
const root = doc.toJSON() ?? {};
|
|
517
632
|
if (!root.plugins || typeof root.plugins !== "object") root.plugins = {};
|
|
518
633
|
if (!Array.isArray(root.plugins.enabled)) root.plugins.enabled = [];
|
|
519
|
-
if (!root.plugins.enabled.includes("forge"))
|
|
634
|
+
if (!root.plugins.enabled.includes("forge"))
|
|
635
|
+
root.plugins.enabled.push("forge");
|
|
520
636
|
doc.contents = doc.createNode(root);
|
|
521
637
|
if (!options.dryRun) {
|
|
522
638
|
await backupIfExists(hermesYamlPath);
|
|
@@ -527,7 +643,9 @@ async function patchHermesConfig(config, options) {
|
|
|
527
643
|
|
|
528
644
|
async function patchCodexConfig(config, options) {
|
|
529
645
|
const filePath = path.join(homeDir(), ".codex", "config.toml");
|
|
530
|
-
let source = fs.existsSync(filePath)
|
|
646
|
+
let source = fs.existsSync(filePath)
|
|
647
|
+
? await fsp.readFile(filePath, "utf8")
|
|
648
|
+
: "";
|
|
531
649
|
const block = [
|
|
532
650
|
"[mcp_servers.forge]",
|
|
533
651
|
'command = "npx"',
|
|
@@ -541,7 +659,8 @@ async function patchCodexConfig(config, options) {
|
|
|
541
659
|
`FORGE_DATA_ROOT = "${config.dataRoot.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`,
|
|
542
660
|
""
|
|
543
661
|
].join("\n");
|
|
544
|
-
const pattern =
|
|
662
|
+
const pattern =
|
|
663
|
+
/(?:^|\n)\[mcp_servers\.forge\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/m;
|
|
545
664
|
if (pattern.test(source)) {
|
|
546
665
|
source = source.replace(pattern, `\n${block}`.trimEnd());
|
|
547
666
|
} else {
|
|
@@ -550,12 +669,20 @@ async function patchCodexConfig(config, options) {
|
|
|
550
669
|
if (!options.dryRun) {
|
|
551
670
|
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
552
671
|
await backupIfExists(filePath);
|
|
553
|
-
await fsp.writeFile(
|
|
672
|
+
await fsp.writeFile(
|
|
673
|
+
filePath,
|
|
674
|
+
source.endsWith("\n") ? source : `${source}\n`,
|
|
675
|
+
"utf8"
|
|
676
|
+
);
|
|
554
677
|
}
|
|
555
678
|
return { filePath };
|
|
556
679
|
}
|
|
557
680
|
|
|
558
|
-
async function runCommand(
|
|
681
|
+
async function runCommand(
|
|
682
|
+
command,
|
|
683
|
+
args,
|
|
684
|
+
{ cwd, dryRun = false, env = process.env } = {}
|
|
685
|
+
) {
|
|
559
686
|
if (dryRun) {
|
|
560
687
|
return { ok: true, dryRun: true, command, args, cwd };
|
|
561
688
|
}
|
|
@@ -569,14 +696,39 @@ async function runCommand(command, args, { cwd, dryRun = false, env = process.en
|
|
|
569
696
|
async function installOpenClawAdapter(config, options) {
|
|
570
697
|
await patchOpenClawConfig(config, options);
|
|
571
698
|
if (!commandExists("openclaw")) {
|
|
572
|
-
return {
|
|
699
|
+
return {
|
|
700
|
+
adapter: "openclaw",
|
|
701
|
+
ok: false,
|
|
702
|
+
skipped: true,
|
|
703
|
+
message: "openclaw command not found"
|
|
704
|
+
};
|
|
573
705
|
}
|
|
574
|
-
const installTarget =
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
706
|
+
const installTarget =
|
|
707
|
+
config.mode === "dev" && config.repo
|
|
708
|
+
? path.join(config.repo, "openclaw-plugin")
|
|
709
|
+
: FORGE_PLUGIN_ID;
|
|
710
|
+
const installArgs =
|
|
711
|
+
config.mode === "dev"
|
|
712
|
+
? [
|
|
713
|
+
"plugins",
|
|
714
|
+
"install",
|
|
715
|
+
"--link",
|
|
716
|
+
"--dangerously-force-unsafe-install",
|
|
717
|
+
installTarget
|
|
718
|
+
]
|
|
719
|
+
: [
|
|
720
|
+
"plugins",
|
|
721
|
+
"install",
|
|
722
|
+
"--dangerously-force-unsafe-install",
|
|
723
|
+
installTarget
|
|
724
|
+
];
|
|
578
725
|
const installResult = await runCommand("openclaw", installArgs, options);
|
|
579
|
-
if (!installResult.ok)
|
|
726
|
+
if (!installResult.ok)
|
|
727
|
+
return {
|
|
728
|
+
adapter: "openclaw",
|
|
729
|
+
ok: false,
|
|
730
|
+
message: "OpenClaw plugin install failed"
|
|
731
|
+
};
|
|
580
732
|
await runCommand("openclaw", ["plugins", "enable", FORGE_PLUGIN_ID], options);
|
|
581
733
|
await runCommand("openclaw", ["gateway", "restart"], options);
|
|
582
734
|
return { adapter: "openclaw", ok: true };
|
|
@@ -584,15 +736,39 @@ async function installOpenClawAdapter(config, options) {
|
|
|
584
736
|
|
|
585
737
|
async function installHermesAdapter(config, options) {
|
|
586
738
|
await patchHermesConfig(config, options);
|
|
587
|
-
const pythonPath = path.join(
|
|
739
|
+
const pythonPath = path.join(
|
|
740
|
+
homeDir(),
|
|
741
|
+
".hermes",
|
|
742
|
+
"hermes-agent",
|
|
743
|
+
"venv",
|
|
744
|
+
"bin",
|
|
745
|
+
"python"
|
|
746
|
+
);
|
|
588
747
|
if (!fs.existsSync(pythonPath)) {
|
|
589
|
-
return {
|
|
748
|
+
return {
|
|
749
|
+
adapter: "hermes",
|
|
750
|
+
ok: false,
|
|
751
|
+
skipped: true,
|
|
752
|
+
message: "Hermes Python environment not found"
|
|
753
|
+
};
|
|
590
754
|
}
|
|
591
|
-
const target =
|
|
592
|
-
|
|
593
|
-
|
|
755
|
+
const target =
|
|
756
|
+
config.mode === "dev" && config.repo
|
|
757
|
+
? [
|
|
758
|
+
"-m",
|
|
759
|
+
"pip",
|
|
760
|
+
"install",
|
|
761
|
+
"--upgrade",
|
|
762
|
+
"-e",
|
|
763
|
+
path.join(config.repo, "plugins", "forge-hermes")
|
|
764
|
+
]
|
|
765
|
+
: ["-m", "pip", "install", "--upgrade", "forge-hermes-plugin"];
|
|
594
766
|
const result = await runCommand(pythonPath, target, options);
|
|
595
|
-
return {
|
|
767
|
+
return {
|
|
768
|
+
adapter: "hermes",
|
|
769
|
+
ok: result.ok,
|
|
770
|
+
message: result.ok ? undefined : "Hermes plugin install failed"
|
|
771
|
+
};
|
|
596
772
|
}
|
|
597
773
|
|
|
598
774
|
async function installCodexAdapter(config, options) {
|
|
@@ -603,9 +779,12 @@ async function installCodexAdapter(config, options) {
|
|
|
603
779
|
async function configureAdapters(config, options) {
|
|
604
780
|
const results = [];
|
|
605
781
|
for (const adapter of config.adapters) {
|
|
606
|
-
if (adapter === "openclaw")
|
|
607
|
-
|
|
608
|
-
if (adapter === "
|
|
782
|
+
if (adapter === "openclaw")
|
|
783
|
+
results.push(await installOpenClawAdapter(config, options));
|
|
784
|
+
if (adapter === "hermes")
|
|
785
|
+
results.push(await installHermesAdapter(config, options));
|
|
786
|
+
if (adapter === "codex")
|
|
787
|
+
results.push(await installCodexAdapter(config, options));
|
|
609
788
|
}
|
|
610
789
|
return results;
|
|
611
790
|
}
|
|
@@ -621,7 +800,10 @@ async function health(config, timeoutMs = 1_500) {
|
|
|
621
800
|
if (!response.ok) return { ok: false, status: response.status };
|
|
622
801
|
return { ok: true, payload: await response.json() };
|
|
623
802
|
} catch (error) {
|
|
624
|
-
return {
|
|
803
|
+
return {
|
|
804
|
+
ok: false,
|
|
805
|
+
error: error instanceof Error ? error.message : String(error)
|
|
806
|
+
};
|
|
625
807
|
} finally {
|
|
626
808
|
clearTimeout(timeout);
|
|
627
809
|
}
|
|
@@ -652,7 +834,10 @@ async function waitForHealth(config, timeoutMs = 30_000) {
|
|
|
652
834
|
|
|
653
835
|
function resolveOpenClawPluginRoot() {
|
|
654
836
|
const candidates = [require];
|
|
655
|
-
const installedRuntimePackageJson = path.join(
|
|
837
|
+
const installedRuntimePackageJson = path.join(
|
|
838
|
+
runtimeInstallRoot(),
|
|
839
|
+
"package.json"
|
|
840
|
+
);
|
|
656
841
|
if (fs.existsSync(installedRuntimePackageJson)) {
|
|
657
842
|
candidates.push(createRequire(installedRuntimePackageJson));
|
|
658
843
|
}
|
|
@@ -689,7 +874,13 @@ async function ensurePackagedRuntimeInstalled() {
|
|
|
689
874
|
try {
|
|
690
875
|
const result = spawnSync(
|
|
691
876
|
"npm",
|
|
692
|
-
[
|
|
877
|
+
[
|
|
878
|
+
"install",
|
|
879
|
+
`${RUNTIME_PACKAGE}@${RUNTIME_PACKAGE_VERSION}`,
|
|
880
|
+
"--omit=dev",
|
|
881
|
+
"--ignore-scripts",
|
|
882
|
+
"--silent"
|
|
883
|
+
],
|
|
693
884
|
{
|
|
694
885
|
cwd: installRoot,
|
|
695
886
|
stdio: ["ignore", out, out],
|
|
@@ -697,13 +888,18 @@ async function ensurePackagedRuntimeInstalled() {
|
|
|
697
888
|
}
|
|
698
889
|
);
|
|
699
890
|
if (result.status !== 0) {
|
|
700
|
-
throw new Error(
|
|
891
|
+
throw new Error(
|
|
892
|
+
`Failed to install ${RUNTIME_PACKAGE}@${RUNTIME_PACKAGE_VERSION}. Check ${logPath()}.`
|
|
893
|
+
);
|
|
701
894
|
}
|
|
702
895
|
} finally {
|
|
703
896
|
fs.closeSync(out);
|
|
704
897
|
}
|
|
705
898
|
const installed = resolveOpenClawPluginRoot();
|
|
706
|
-
if (!installed)
|
|
899
|
+
if (!installed)
|
|
900
|
+
throw new Error(
|
|
901
|
+
`${RUNTIME_PACKAGE} installed but its runtime entry could not be resolved.`
|
|
902
|
+
);
|
|
707
903
|
return installed;
|
|
708
904
|
}
|
|
709
905
|
|
|
@@ -721,30 +917,56 @@ async function startRuntime(config) {
|
|
|
721
917
|
const children = [];
|
|
722
918
|
|
|
723
919
|
if (config.mode === "dev") {
|
|
724
|
-
if (!config.repo)
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
920
|
+
if (!config.repo)
|
|
921
|
+
throw new Error("Dev mode requires a Forge repo checkout.");
|
|
922
|
+
const tsx = path.join(
|
|
923
|
+
config.repo,
|
|
924
|
+
"node_modules",
|
|
925
|
+
"tsx",
|
|
926
|
+
"dist",
|
|
927
|
+
"cli.mjs"
|
|
928
|
+
);
|
|
929
|
+
if (!fs.existsSync(tsx))
|
|
930
|
+
throw new Error(
|
|
931
|
+
`tsx was not found at ${tsx}. Run npm install in the Forge repo.`
|
|
932
|
+
);
|
|
933
|
+
const server = spawn(
|
|
934
|
+
process.execPath,
|
|
935
|
+
[tsx, path.join(config.repo, "server", "src", "index.ts")],
|
|
936
|
+
{
|
|
937
|
+
cwd: config.repo,
|
|
938
|
+
detached: true,
|
|
939
|
+
stdio: ["ignore", out, out],
|
|
940
|
+
env: {
|
|
941
|
+
...process.env,
|
|
942
|
+
HOST: "127.0.0.1",
|
|
943
|
+
PORT: String(config.port),
|
|
944
|
+
FORGE_BASE_PATH: "/forge/",
|
|
945
|
+
FORGE_DATA_ROOT: config.dataRoot,
|
|
946
|
+
FORGE_DEV_WEB_ORIGIN: `http://127.0.0.1:${config.webPort}/forge/`
|
|
947
|
+
}
|
|
738
948
|
}
|
|
739
|
-
|
|
949
|
+
);
|
|
740
950
|
server.unref();
|
|
741
951
|
children.push({ role: "server", pid: server.pid });
|
|
742
|
-
const web = spawn(
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
952
|
+
const web = spawn(
|
|
953
|
+
"npm",
|
|
954
|
+
[
|
|
955
|
+
"run",
|
|
956
|
+
"dev:web",
|
|
957
|
+
"--",
|
|
958
|
+
"--host",
|
|
959
|
+
"127.0.0.1",
|
|
960
|
+
"--port",
|
|
961
|
+
String(config.webPort)
|
|
962
|
+
],
|
|
963
|
+
{
|
|
964
|
+
cwd: config.repo,
|
|
965
|
+
detached: true,
|
|
966
|
+
stdio: ["ignore", out, out],
|
|
967
|
+
env: { ...process.env, FORGE_BASE_PATH: "/forge/" }
|
|
968
|
+
}
|
|
969
|
+
);
|
|
748
970
|
web.unref();
|
|
749
971
|
children.push({ role: "web", pid: web.pid });
|
|
750
972
|
} else {
|
|
@@ -783,7 +1005,12 @@ async function startRuntime(config) {
|
|
|
783
1005
|
|
|
784
1006
|
async function stopRuntime() {
|
|
785
1007
|
const state = await readRuntimeState();
|
|
786
|
-
if (!state?.children?.length)
|
|
1008
|
+
if (!state?.children?.length)
|
|
1009
|
+
return {
|
|
1010
|
+
ok: true,
|
|
1011
|
+
stopped: false,
|
|
1012
|
+
message: "No forge-memory runtime state found."
|
|
1013
|
+
};
|
|
787
1014
|
const stopped = [];
|
|
788
1015
|
for (const child of state.children) {
|
|
789
1016
|
if (!child?.pid || !processExists(child.pid)) continue;
|
|
@@ -803,9 +1030,12 @@ async function exportForgeData(parsed) {
|
|
|
803
1030
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
804
1031
|
const requestedOutput = parsed.values.output ?? parsed.positionals[1];
|
|
805
1032
|
const outputPath = path.resolve(
|
|
806
|
-
requestedOutput ??
|
|
1033
|
+
requestedOutput ??
|
|
1034
|
+
path.join(forgeHome(), "exports", `forge-memory-export-${stamp}.tar.gz`)
|
|
1035
|
+
);
|
|
1036
|
+
const stagingRoot = await fsp.mkdtemp(
|
|
1037
|
+
path.join(os.tmpdir(), "forge-memory-export-")
|
|
807
1038
|
);
|
|
808
|
-
const stagingRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "forge-memory-export-"));
|
|
809
1039
|
const stagingData = path.join(stagingRoot, "data");
|
|
810
1040
|
const manifest = {
|
|
811
1041
|
exportedAt: new Date().toISOString(),
|
|
@@ -832,42 +1062,77 @@ async function exportForgeData(parsed) {
|
|
|
832
1062
|
}
|
|
833
1063
|
});
|
|
834
1064
|
if (fs.existsSync(configPath())) {
|
|
835
|
-
await fsp.copyFile(
|
|
1065
|
+
await fsp.copyFile(
|
|
1066
|
+
configPath(),
|
|
1067
|
+
path.join(stagingRoot, "forge-memory-config.json")
|
|
1068
|
+
);
|
|
836
1069
|
}
|
|
837
|
-
await fsp.writeFile(
|
|
1070
|
+
await fsp.writeFile(
|
|
1071
|
+
path.join(stagingRoot, "manifest.json"),
|
|
1072
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
1073
|
+
"utf8"
|
|
1074
|
+
);
|
|
838
1075
|
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
839
1076
|
|
|
840
|
-
const wantsArchive =
|
|
1077
|
+
const wantsArchive =
|
|
1078
|
+
outputPath.endsWith(".tar.gz") || outputPath.endsWith(".tgz");
|
|
841
1079
|
if (wantsArchive && commandExists("tar")) {
|
|
842
|
-
const result = spawnSync(
|
|
843
|
-
|
|
844
|
-
|
|
1080
|
+
const result = spawnSync(
|
|
1081
|
+
"tar",
|
|
1082
|
+
["-czf", outputPath, "-C", stagingRoot, "."],
|
|
1083
|
+
{
|
|
1084
|
+
stdio: parsed.flags.json ? "ignore" : "inherit"
|
|
1085
|
+
}
|
|
1086
|
+
);
|
|
845
1087
|
await fsp.rm(stagingRoot, { recursive: true, force: true });
|
|
846
|
-
if (result.status !== 0)
|
|
847
|
-
|
|
1088
|
+
if (result.status !== 0)
|
|
1089
|
+
throw new Error(`Failed to write export archive: ${outputPath}`);
|
|
1090
|
+
return {
|
|
1091
|
+
ok: true,
|
|
1092
|
+
outputPath,
|
|
1093
|
+
archive: true,
|
|
1094
|
+
sourceDataRoot: config.dataRoot
|
|
1095
|
+
};
|
|
848
1096
|
}
|
|
849
1097
|
|
|
850
1098
|
await fsp.rm(outputPath, { recursive: true, force: true });
|
|
851
1099
|
await fsp.cp(stagingRoot, outputPath, { recursive: true });
|
|
852
1100
|
await fsp.rm(stagingRoot, { recursive: true, force: true });
|
|
853
|
-
return {
|
|
1101
|
+
return {
|
|
1102
|
+
ok: true,
|
|
1103
|
+
outputPath,
|
|
1104
|
+
archive: false,
|
|
1105
|
+
sourceDataRoot: config.dataRoot
|
|
1106
|
+
};
|
|
854
1107
|
}
|
|
855
1108
|
|
|
856
1109
|
async function removeOpenClawAdapterConfig() {
|
|
857
1110
|
const filePath = path.join(homeDir(), ".openclaw", "openclaw.json");
|
|
858
1111
|
const payload = await readJson(filePath, null);
|
|
859
|
-
if (!payload?.plugins?.entries?.[FORGE_PLUGIN_ID])
|
|
1112
|
+
if (!payload?.plugins?.entries?.[FORGE_PLUGIN_ID])
|
|
1113
|
+
return { filePath, changed: false };
|
|
860
1114
|
await backupIfExists(filePath);
|
|
861
1115
|
delete payload.plugins.entries[FORGE_PLUGIN_ID];
|
|
862
1116
|
if (Array.isArray(payload.plugins.allow)) {
|
|
863
|
-
payload.plugins.allow = payload.plugins.allow.filter(
|
|
1117
|
+
payload.plugins.allow = payload.plugins.allow.filter(
|
|
1118
|
+
(entry) => entry !== FORGE_PLUGIN_ID
|
|
1119
|
+
);
|
|
864
1120
|
}
|
|
865
|
-
await fsp.writeFile(
|
|
1121
|
+
await fsp.writeFile(
|
|
1122
|
+
filePath,
|
|
1123
|
+
`${JSON.stringify(payload, null, 2)}\n`,
|
|
1124
|
+
"utf8"
|
|
1125
|
+
);
|
|
866
1126
|
return { filePath, changed: true };
|
|
867
1127
|
}
|
|
868
1128
|
|
|
869
1129
|
async function removeHermesAdapterConfig() {
|
|
870
|
-
const forgeConfigPath = path.join(
|
|
1130
|
+
const forgeConfigPath = path.join(
|
|
1131
|
+
homeDir(),
|
|
1132
|
+
".hermes",
|
|
1133
|
+
"forge",
|
|
1134
|
+
"config.json"
|
|
1135
|
+
);
|
|
871
1136
|
const changed = fs.existsSync(forgeConfigPath);
|
|
872
1137
|
if (changed) {
|
|
873
1138
|
await backupIfExists(forgeConfigPath);
|
|
@@ -880,10 +1145,14 @@ async function removeCodexAdapterConfig() {
|
|
|
880
1145
|
const filePath = path.join(homeDir(), ".codex", "config.toml");
|
|
881
1146
|
if (!fs.existsSync(filePath)) return { filePath, changed: false };
|
|
882
1147
|
const source = await fsp.readFile(filePath, "utf8");
|
|
883
|
-
const pattern =
|
|
1148
|
+
const pattern =
|
|
1149
|
+
/(?:^|\n)\[mcp_servers\.forge\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/m;
|
|
884
1150
|
if (!pattern.test(source)) return { filePath, changed: false };
|
|
885
1151
|
await backupIfExists(filePath);
|
|
886
|
-
const next = source
|
|
1152
|
+
const next = source
|
|
1153
|
+
.replace(pattern, "\n")
|
|
1154
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1155
|
+
.trimEnd();
|
|
887
1156
|
await fsp.writeFile(filePath, next ? `${next}\n` : "", "utf8");
|
|
888
1157
|
return { filePath, changed: true };
|
|
889
1158
|
}
|
|
@@ -900,7 +1169,12 @@ async function uninstallForgeMemory(parsed) {
|
|
|
900
1169
|
|
|
901
1170
|
const stop = await stopRuntime();
|
|
902
1171
|
const removed = [];
|
|
903
|
-
for (const target of [
|
|
1172
|
+
for (const target of [
|
|
1173
|
+
runtimeInstallRoot(),
|
|
1174
|
+
runtimeStatePath(),
|
|
1175
|
+
logPath(),
|
|
1176
|
+
configPath()
|
|
1177
|
+
]) {
|
|
904
1178
|
if (fs.existsSync(target)) {
|
|
905
1179
|
await fsp.rm(target, { recursive: true, force: true });
|
|
906
1180
|
removed.push(target);
|
|
@@ -908,7 +1182,13 @@ async function uninstallForgeMemory(parsed) {
|
|
|
908
1182
|
}
|
|
909
1183
|
|
|
910
1184
|
let adapterResults = [];
|
|
911
|
-
const removeAdapters =
|
|
1185
|
+
const removeAdapters =
|
|
1186
|
+
parsed.flags.removeAdapters ||
|
|
1187
|
+
(!parsed.flags.yes &&
|
|
1188
|
+
(await promptYesNo(
|
|
1189
|
+
"Remove Forge adapter entries from OpenClaw, Hermes, and Codex?",
|
|
1190
|
+
false
|
|
1191
|
+
)));
|
|
912
1192
|
if (removeAdapters) {
|
|
913
1193
|
adapterResults = [
|
|
914
1194
|
await removeOpenClawAdapterConfig(),
|
|
@@ -921,7 +1201,10 @@ async function uninstallForgeMemory(parsed) {
|
|
|
921
1201
|
if (parsed.flags.removeData) {
|
|
922
1202
|
const dataConfirmed = parsed.flags.yes
|
|
923
1203
|
? true
|
|
924
|
-
: await promptYesNo(
|
|
1204
|
+
: await promptYesNo(
|
|
1205
|
+
`Delete Forge data folder ${config.dataRoot}? This cannot be undone.`,
|
|
1206
|
+
false
|
|
1207
|
+
);
|
|
925
1208
|
if (dataConfirmed) {
|
|
926
1209
|
await fsp.rm(config.dataRoot, { recursive: true, force: true });
|
|
927
1210
|
removedDataRoot = true;
|
|
@@ -941,12 +1224,19 @@ async function uninstallForgeMemory(parsed) {
|
|
|
941
1224
|
|
|
942
1225
|
async function createPairing(config, options = {}) {
|
|
943
1226
|
const transportMode = options.transportMode ?? "iroh";
|
|
944
|
-
const response = await fetch(
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1227
|
+
const response = await fetch(
|
|
1228
|
+
new URL("/api/v1/health/pairing-sessions", baseUrl(config)),
|
|
1229
|
+
{
|
|
1230
|
+
method: "POST",
|
|
1231
|
+
headers: {
|
|
1232
|
+
"content-type": "application/json",
|
|
1233
|
+
accept: "application/json"
|
|
1234
|
+
},
|
|
1235
|
+
body: JSON.stringify({ userId: null, transportMode })
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
if (!response.ok)
|
|
1239
|
+
throw new Error(`Pairing request failed with ${response.status}`);
|
|
950
1240
|
return response.json();
|
|
951
1241
|
}
|
|
952
1242
|
|
|
@@ -955,11 +1245,12 @@ function printPairing(pairing) {
|
|
|
955
1245
|
qrcode.generate(JSON.stringify(pairing.qrPayload), { small: true });
|
|
956
1246
|
const transport = pairing.qrPayload?.transport;
|
|
957
1247
|
if (transport?.provider) {
|
|
958
|
-
const label =
|
|
959
|
-
|
|
960
|
-
: pairing.qrPayload.transportMode === "iroh"
|
|
1248
|
+
const label =
|
|
1249
|
+
pairing.qrPayload.transport?.protocol === "iroh"
|
|
961
1250
|
? "Iroh"
|
|
962
|
-
:
|
|
1251
|
+
: pairing.qrPayload.transportMode === "iroh"
|
|
1252
|
+
? "Iroh"
|
|
1253
|
+
: "Manual HTTP";
|
|
963
1254
|
console.log(`${color.cyan(label)}: ${pairing.qrPayload.apiBaseUrl}`);
|
|
964
1255
|
if (transport.recreateCommand) {
|
|
965
1256
|
console.log(`${color.dim("recreate:")} ${transport.recreateCommand}`);
|
|
@@ -976,16 +1267,34 @@ async function runInstall(parsed, command) {
|
|
|
976
1267
|
const discovery = discover();
|
|
977
1268
|
if (!parsed.flags.yes) {
|
|
978
1269
|
printBanner();
|
|
979
|
-
console.log(
|
|
1270
|
+
console.log(
|
|
1271
|
+
color.dim(
|
|
1272
|
+
"Discovery runs in the background. Forge UI/runtime is always installed.\n"
|
|
1273
|
+
)
|
|
1274
|
+
);
|
|
980
1275
|
}
|
|
981
|
-
const config = await buildInstallConfig(
|
|
982
|
-
|
|
983
|
-
|
|
1276
|
+
const config = await buildInstallConfig(
|
|
1277
|
+
parsed,
|
|
1278
|
+
currentConfig,
|
|
1279
|
+
discovery,
|
|
1280
|
+
command
|
|
1281
|
+
);
|
|
1282
|
+
const writeResult = await writeConfig(config, {
|
|
1283
|
+
dryRun: parsed.flags.dryRun
|
|
1284
|
+
});
|
|
1285
|
+
const adapterResults = await configureAdapters(config, {
|
|
1286
|
+
dryRun: parsed.flags.dryRun
|
|
1287
|
+
});
|
|
984
1288
|
let runtimeResult = null;
|
|
985
1289
|
if (!parsed.flags.noStart && !parsed.flags.dryRun) {
|
|
986
1290
|
runtimeResult = await startRuntime(config);
|
|
987
1291
|
}
|
|
988
|
-
const shouldPair =
|
|
1292
|
+
const shouldPair =
|
|
1293
|
+
parsed.flags.pairIos ||
|
|
1294
|
+
(!parsed.flags.skipPairIos &&
|
|
1295
|
+
(parsed.flags.yes
|
|
1296
|
+
? true
|
|
1297
|
+
: await promptYesNo("Pair the iOS companion now?", true)));
|
|
989
1298
|
let pairing = null;
|
|
990
1299
|
if (shouldPair && !parsed.flags.dryRun) {
|
|
991
1300
|
if (!runtimeResult) await startRuntime(config);
|
|
@@ -996,13 +1305,23 @@ async function runInstall(parsed, command) {
|
|
|
996
1305
|
printPairing(pairing);
|
|
997
1306
|
}
|
|
998
1307
|
}
|
|
999
|
-
const summary = {
|
|
1308
|
+
const summary = {
|
|
1309
|
+
ok: true,
|
|
1310
|
+
config,
|
|
1311
|
+
writeResult,
|
|
1312
|
+
adapterResults,
|
|
1313
|
+
runtimeResult,
|
|
1314
|
+
pairing: Boolean(pairing)
|
|
1315
|
+
};
|
|
1000
1316
|
if (parsed.flags.json) console.log(JSON.stringify(summary, null, 2));
|
|
1001
1317
|
else {
|
|
1002
1318
|
console.log(color.green("Forge Memory configured."));
|
|
1003
1319
|
console.log(`UI: ${webUrl(config)}`);
|
|
1004
1320
|
console.log(`Data: ${config.dataRoot}`);
|
|
1005
|
-
if (parsed.flags.dryRun)
|
|
1321
|
+
if (parsed.flags.dryRun)
|
|
1322
|
+
console.log(
|
|
1323
|
+
color.yellow("Dry run only; no files or adapter installs were changed.")
|
|
1324
|
+
);
|
|
1006
1325
|
}
|
|
1007
1326
|
}
|
|
1008
1327
|
|
|
@@ -1023,11 +1342,15 @@ async function runStatus(parsed) {
|
|
|
1023
1342
|
if (parsed.flags.json) console.log(JSON.stringify(payload, null, 2));
|
|
1024
1343
|
else {
|
|
1025
1344
|
console.log(`${color.bold("Forge Memory Status")}`);
|
|
1026
|
-
console.log(
|
|
1345
|
+
console.log(
|
|
1346
|
+
`Runtime: ${currentHealth.ok ? color.green("healthy") : color.yellow("not reachable")}`
|
|
1347
|
+
);
|
|
1027
1348
|
console.log(`Mode: ${config.mode}`);
|
|
1028
1349
|
console.log(`UI: ${webUrl(config)}`);
|
|
1029
1350
|
console.log(`Data: ${config.dataRoot}`);
|
|
1030
|
-
console.log(
|
|
1351
|
+
console.log(
|
|
1352
|
+
`Adapters: ${config.adapters.length ? config.adapters.join(", ") : "none configured"}`
|
|
1353
|
+
);
|
|
1031
1354
|
if (state?.logPath) console.log(`Logs: ${state.logPath}`);
|
|
1032
1355
|
}
|
|
1033
1356
|
}
|
|
@@ -1036,18 +1359,35 @@ async function runDoctor(parsed) {
|
|
|
1036
1359
|
const config = await readConfig();
|
|
1037
1360
|
const discovery = discover();
|
|
1038
1361
|
const checks = [
|
|
1039
|
-
{
|
|
1362
|
+
{
|
|
1363
|
+
id: "node",
|
|
1364
|
+
ok: Number(process.versions.node.split(".")[0]) >= 22,
|
|
1365
|
+
detail: process.versions.node
|
|
1366
|
+
},
|
|
1040
1367
|
{ id: "config", ok: fs.existsSync(configPath()), detail: configPath() },
|
|
1041
|
-
{
|
|
1368
|
+
{
|
|
1369
|
+
id: "dataRoot",
|
|
1370
|
+
ok: fs.existsSync(config.dataRoot),
|
|
1371
|
+
detail: config.dataRoot
|
|
1372
|
+
},
|
|
1042
1373
|
{ id: "runtime", ok: (await health(config)).ok, detail: baseUrl(config) },
|
|
1043
|
-
...discovery.adapters.map((adapter) => ({
|
|
1374
|
+
...discovery.adapters.map((adapter) => ({
|
|
1375
|
+
id: adapter.id,
|
|
1376
|
+
ok: adapter.installed,
|
|
1377
|
+
detail: adapter.status
|
|
1378
|
+
}))
|
|
1044
1379
|
];
|
|
1045
|
-
const payload = {
|
|
1380
|
+
const payload = {
|
|
1381
|
+
ok: checks.every((check) => check.ok || ADAPTERS.includes(check.id)),
|
|
1382
|
+
checks
|
|
1383
|
+
};
|
|
1046
1384
|
if (parsed.flags.json) console.log(JSON.stringify(payload, null, 2));
|
|
1047
1385
|
else {
|
|
1048
1386
|
console.log(color.bold("Forge Memory Doctor"));
|
|
1049
1387
|
for (const check of checks) {
|
|
1050
|
-
console.log(
|
|
1388
|
+
console.log(
|
|
1389
|
+
`${check.ok ? color.green("ok") : color.yellow("warn")} ${check.id}: ${check.detail}`
|
|
1390
|
+
);
|
|
1051
1391
|
}
|
|
1052
1392
|
}
|
|
1053
1393
|
}
|
|
@@ -1056,7 +1396,11 @@ async function runUi(parsed) {
|
|
|
1056
1396
|
const config = await readConfig();
|
|
1057
1397
|
if (!parsed.flags.noStart) await startRuntime(config);
|
|
1058
1398
|
if (parsed.flags.printUrl || parsed.flags.json) {
|
|
1059
|
-
console.log(
|
|
1399
|
+
console.log(
|
|
1400
|
+
parsed.flags.json
|
|
1401
|
+
? JSON.stringify({ url: webUrl(config) }, null, 2)
|
|
1402
|
+
: webUrl(config)
|
|
1403
|
+
);
|
|
1060
1404
|
return;
|
|
1061
1405
|
}
|
|
1062
1406
|
await open(webUrl(config));
|
|
@@ -1088,9 +1432,181 @@ function sha(input) {
|
|
|
1088
1432
|
return createHash("sha1").update(input).digest("hex").slice(0, 12);
|
|
1089
1433
|
}
|
|
1090
1434
|
|
|
1435
|
+
const DEFAULT_MCP_TEXT_CONTENT_LIMIT_BYTES = 1_500_000;
|
|
1436
|
+
const DEFAULT_MCP_STRUCTURED_CONTENT_LIMIT_BYTES = 750_000;
|
|
1437
|
+
const MCP_PREVIEW_BYTES = 24_000;
|
|
1438
|
+
const textEncoder = new TextEncoder();
|
|
1439
|
+
const textDecoder = new TextDecoder();
|
|
1440
|
+
|
|
1441
|
+
function importFile(filePath) {
|
|
1442
|
+
return import(pathToFileURL(filePath).href);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function normalizePositiveNumber(value, fallback) {
|
|
1446
|
+
const parsed = Number(value);
|
|
1447
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function resolveMcpResponseLimits() {
|
|
1451
|
+
return {
|
|
1452
|
+
textContentLimitBytes: normalizePositiveNumber(
|
|
1453
|
+
process.env.FORGE_MCP_TEXT_CONTENT_LIMIT_BYTES,
|
|
1454
|
+
DEFAULT_MCP_TEXT_CONTENT_LIMIT_BYTES
|
|
1455
|
+
),
|
|
1456
|
+
structuredContentLimitBytes: normalizePositiveNumber(
|
|
1457
|
+
process.env.FORGE_MCP_STRUCTURED_CONTENT_LIMIT_BYTES,
|
|
1458
|
+
DEFAULT_MCP_STRUCTURED_CONTENT_LIMIT_BYTES
|
|
1459
|
+
)
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function safeStringify(value) {
|
|
1464
|
+
try {
|
|
1465
|
+
return JSON.stringify(value, null, 2);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
return JSON.stringify(
|
|
1468
|
+
{
|
|
1469
|
+
error: "Forge MCP could not serialize this response.",
|
|
1470
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1471
|
+
},
|
|
1472
|
+
null,
|
|
1473
|
+
2
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function utf8ByteLength(value) {
|
|
1479
|
+
return textEncoder.encode(value).byteLength;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function truncateUtf8(value, limitBytes) {
|
|
1483
|
+
const encoded = textEncoder.encode(value);
|
|
1484
|
+
if (encoded.byteLength <= limitBytes) return value;
|
|
1485
|
+
return textDecoder.decode(encoded.slice(0, Math.max(0, limitBytes)));
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function createTruncatedMcpPayload({ kind, value, limitBytes }) {
|
|
1489
|
+
const serialized = typeof value === "string" ? value : safeStringify(value);
|
|
1490
|
+
return {
|
|
1491
|
+
forgeMcpResponseTruncated: true,
|
|
1492
|
+
kind,
|
|
1493
|
+
approximateBytes: utf8ByteLength(serialized),
|
|
1494
|
+
limitBytes,
|
|
1495
|
+
preview: truncateUtf8(serialized, Math.min(MCP_PREVIEW_BYTES, limitBytes)),
|
|
1496
|
+
guidance:
|
|
1497
|
+
"The Forge MCP bridge truncated this response before writing to stdio. Narrow the request, lower the limit, or fetch one specific wiki page/result."
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function toMcpContent(result, limits) {
|
|
1502
|
+
const source =
|
|
1503
|
+
Array.isArray(result?.content) && result.content.length > 0
|
|
1504
|
+
? result.content
|
|
1505
|
+
: [{ type: "text", text: safeStringify(result?.details ?? null) }];
|
|
1506
|
+
|
|
1507
|
+
return source.map((item) => {
|
|
1508
|
+
const text =
|
|
1509
|
+
item && typeof item === "object" && item.type === "text" && "text" in item
|
|
1510
|
+
? typeof item.text === "string"
|
|
1511
|
+
? item.text
|
|
1512
|
+
: safeStringify(item.text ?? null)
|
|
1513
|
+
: safeStringify(item);
|
|
1514
|
+
|
|
1515
|
+
if (utf8ByteLength(text) <= limits.textContentLimitBytes) {
|
|
1516
|
+
return { type: "text", text };
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
return {
|
|
1520
|
+
type: "text",
|
|
1521
|
+
text: safeStringify(
|
|
1522
|
+
createTruncatedMcpPayload({
|
|
1523
|
+
kind: "content",
|
|
1524
|
+
value: text,
|
|
1525
|
+
limitBytes: limits.textContentLimitBytes
|
|
1526
|
+
})
|
|
1527
|
+
)
|
|
1528
|
+
};
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function maybeStructuredContent(details, limits) {
|
|
1533
|
+
if (typeof details !== "object" || details === null) return undefined;
|
|
1534
|
+
const serialized = safeStringify(details);
|
|
1535
|
+
if (utf8ByteLength(serialized) <= limits.structuredContentLimitBytes) {
|
|
1536
|
+
return details;
|
|
1537
|
+
}
|
|
1538
|
+
return createTruncatedMcpPayload({
|
|
1539
|
+
kind: "structuredContent",
|
|
1540
|
+
value: serialized,
|
|
1541
|
+
limitBytes: limits.structuredContentLimitBytes
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function resolveMcpRuntimeRoot(config) {
|
|
1546
|
+
if (config.mode === "dev" && config.repo) {
|
|
1547
|
+
const devRoot = path.join(config.repo, "openclaw-plugin");
|
|
1548
|
+
if (fs.existsSync(path.join(devRoot, "dist", "openclaw", "tools.js"))) {
|
|
1549
|
+
return devRoot;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return resolveOpenClawPluginRoot();
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
async function loadForgeToolRuntime(config) {
|
|
1556
|
+
const pluginRoot = resolveMcpRuntimeRoot(config);
|
|
1557
|
+
if (!pluginRoot) return null;
|
|
1558
|
+
|
|
1559
|
+
const pluginRequire = createRequire(path.join(pluginRoot, "package.json"));
|
|
1560
|
+
const [
|
|
1561
|
+
{ Value },
|
|
1562
|
+
{ resolveForgePluginConfig },
|
|
1563
|
+
{ registerForgePluginTools }
|
|
1564
|
+
] = await Promise.all([
|
|
1565
|
+
importFile(pluginRequire.resolve("@sinclair/typebox/value")),
|
|
1566
|
+
importFile(
|
|
1567
|
+
path.join(pluginRoot, "dist", "openclaw", "plugin-entry-shared.js")
|
|
1568
|
+
),
|
|
1569
|
+
importFile(path.join(pluginRoot, "dist", "openclaw", "tools.js"))
|
|
1570
|
+
]);
|
|
1571
|
+
|
|
1572
|
+
const forgeConfig = resolveForgePluginConfig({
|
|
1573
|
+
origin: process.env.FORGE_ORIGIN ?? config.origin,
|
|
1574
|
+
port: normalizePort(process.env.FORGE_PORT, config.port),
|
|
1575
|
+
dataRoot: process.env.FORGE_DATA_ROOT?.trim() || config.dataRoot,
|
|
1576
|
+
apiToken: process.env.FORGE_API_TOKEN ?? "",
|
|
1577
|
+
actorLabel: process.env.FORGE_ACTOR_LABEL ?? "codex",
|
|
1578
|
+
timeoutMs: normalizePositiveNumber(process.env.FORGE_TIMEOUT_MS, 15_000)
|
|
1579
|
+
});
|
|
1580
|
+
const tools = [];
|
|
1581
|
+
registerForgePluginTools(
|
|
1582
|
+
{ registerTool: (tool) => tools.push(tool) },
|
|
1583
|
+
forgeConfig
|
|
1584
|
+
);
|
|
1585
|
+
return { pluginRoot, Value, tools };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function validationErrorMessage(Value, schema, value) {
|
|
1589
|
+
const firstError = Value.Errors(schema, value).First();
|
|
1590
|
+
if (!firstError) return "Invalid arguments";
|
|
1591
|
+
return `${firstError.path || "input"}: ${firstError.message}`;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1091
1594
|
async function runMcp() {
|
|
1092
1595
|
const config = await readConfig();
|
|
1093
|
-
const
|
|
1596
|
+
const responseLimits = resolveMcpResponseLimits();
|
|
1597
|
+
let toolRuntime = null;
|
|
1598
|
+
let toolRuntimeError = null;
|
|
1599
|
+
try {
|
|
1600
|
+
toolRuntime = await loadForgeToolRuntime(config);
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
toolRuntimeError = error instanceof Error ? error.message : String(error);
|
|
1603
|
+
}
|
|
1604
|
+
const forgeTools = toolRuntime?.tools ?? [];
|
|
1605
|
+
const forgeToolByName = new Map(forgeTools.map((tool) => [tool.name, tool]));
|
|
1606
|
+
const server = new Server(
|
|
1607
|
+
{ name: "forge-memory", version: VERSION },
|
|
1608
|
+
{ capabilities: { tools: {} } }
|
|
1609
|
+
);
|
|
1094
1610
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1095
1611
|
tools: [
|
|
1096
1612
|
{
|
|
@@ -1102,8 +1618,20 @@ async function runMcp() {
|
|
|
1102
1618
|
name: "forge_memory_health",
|
|
1103
1619
|
description: "Check the configured Forge API health endpoint.",
|
|
1104
1620
|
inputSchema: { type: "object", properties: {} }
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
name: "forge_memory_mcp_diagnostics",
|
|
1624
|
+
description: "Return Forge Memory MCP runtime loading diagnostics.",
|
|
1625
|
+
inputSchema: { type: "object", properties: {} }
|
|
1105
1626
|
}
|
|
1106
|
-
]
|
|
1627
|
+
].concat(
|
|
1628
|
+
forgeTools.map((tool) => ({
|
|
1629
|
+
name: tool.name,
|
|
1630
|
+
title: tool.label,
|
|
1631
|
+
description: tool.description,
|
|
1632
|
+
inputSchema: tool.parameters
|
|
1633
|
+
}))
|
|
1634
|
+
)
|
|
1107
1635
|
}));
|
|
1108
1636
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1109
1637
|
if (request.params.name === "forge_memory_status") {
|
|
@@ -1111,15 +1639,87 @@ async function runMcp() {
|
|
|
1111
1639
|
content: [
|
|
1112
1640
|
{
|
|
1113
1641
|
type: "text",
|
|
1114
|
-
text: JSON.stringify(
|
|
1642
|
+
text: JSON.stringify(
|
|
1643
|
+
{
|
|
1644
|
+
baseUrl: baseUrl(config),
|
|
1645
|
+
webUrl: webUrl(config),
|
|
1646
|
+
dataRoot: config.dataRoot,
|
|
1647
|
+
identity: sha(config.dataRoot)
|
|
1648
|
+
},
|
|
1649
|
+
null,
|
|
1650
|
+
2
|
|
1651
|
+
)
|
|
1115
1652
|
}
|
|
1116
1653
|
]
|
|
1117
1654
|
};
|
|
1118
1655
|
}
|
|
1119
1656
|
if (request.params.name === "forge_memory_health") {
|
|
1120
|
-
return {
|
|
1657
|
+
return {
|
|
1658
|
+
content: [
|
|
1659
|
+
{ type: "text", text: JSON.stringify(await health(config), null, 2) }
|
|
1660
|
+
]
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
if (request.params.name === "forge_memory_mcp_diagnostics") {
|
|
1664
|
+
return {
|
|
1665
|
+
content: [
|
|
1666
|
+
{
|
|
1667
|
+
type: "text",
|
|
1668
|
+
text: JSON.stringify(
|
|
1669
|
+
{
|
|
1670
|
+
runtimeLoaded: Boolean(toolRuntime),
|
|
1671
|
+
runtimeRoot: toolRuntime?.pluginRoot ?? null,
|
|
1672
|
+
runtimeError: toolRuntimeError,
|
|
1673
|
+
forgeToolCount: forgeTools.length,
|
|
1674
|
+
wikiTools: forgeTools
|
|
1675
|
+
.filter((tool) => tool.name.includes("wiki"))
|
|
1676
|
+
.map((tool) => tool.name)
|
|
1677
|
+
.sort()
|
|
1678
|
+
},
|
|
1679
|
+
null,
|
|
1680
|
+
2
|
|
1681
|
+
)
|
|
1682
|
+
}
|
|
1683
|
+
]
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const tool = forgeToolByName.get(request.params.name);
|
|
1688
|
+
if (!tool) {
|
|
1689
|
+
throw new McpError(
|
|
1690
|
+
ErrorCode.InvalidParams,
|
|
1691
|
+
`Forge tool not found: ${request.params.name}`
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const args = request.params.arguments ?? {};
|
|
1696
|
+
if (!toolRuntime.Value.Check(tool.parameters, args)) {
|
|
1697
|
+
throw new McpError(
|
|
1698
|
+
ErrorCode.InvalidParams,
|
|
1699
|
+
validationErrorMessage(toolRuntime.Value, tool.parameters, args)
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
try {
|
|
1704
|
+
const result = await tool.execute(request.params.name, args);
|
|
1705
|
+
return {
|
|
1706
|
+
content: toMcpContent(result, responseLimits),
|
|
1707
|
+
structuredContent: maybeStructuredContent(
|
|
1708
|
+
result.details,
|
|
1709
|
+
responseLimits
|
|
1710
|
+
)
|
|
1711
|
+
};
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
return {
|
|
1714
|
+
content: [
|
|
1715
|
+
{
|
|
1716
|
+
type: "text",
|
|
1717
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1718
|
+
}
|
|
1719
|
+
],
|
|
1720
|
+
isError: true
|
|
1721
|
+
};
|
|
1121
1722
|
}
|
|
1122
|
-
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
1123
1723
|
});
|
|
1124
1724
|
await server.connect(new StdioServerTransport());
|
|
1125
1725
|
}
|
|
@@ -1179,7 +1779,9 @@ async function main() {
|
|
|
1179
1779
|
await runDoctor(parsed);
|
|
1180
1780
|
break;
|
|
1181
1781
|
case "start":
|
|
1182
|
-
console.log(
|
|
1782
|
+
console.log(
|
|
1783
|
+
JSON.stringify(await startRuntime(await readConfig()), null, 2)
|
|
1784
|
+
);
|
|
1183
1785
|
break;
|
|
1184
1786
|
case "stop":
|
|
1185
1787
|
console.log(JSON.stringify(await stopRuntime(), null, 2));
|
|
@@ -1187,18 +1789,30 @@ async function main() {
|
|
|
1187
1789
|
case "export":
|
|
1188
1790
|
{
|
|
1189
1791
|
const result = await exportForgeData(parsed);
|
|
1190
|
-
console.log(
|
|
1792
|
+
console.log(
|
|
1793
|
+
parsed.flags.json
|
|
1794
|
+
? JSON.stringify(result, null, 2)
|
|
1795
|
+
: `Exported Forge data to ${result.outputPath}`
|
|
1796
|
+
);
|
|
1191
1797
|
}
|
|
1192
1798
|
break;
|
|
1193
1799
|
case "uninstall":
|
|
1194
1800
|
{
|
|
1195
1801
|
const result = await uninstallForgeMemory(parsed);
|
|
1196
|
-
console.log(
|
|
1802
|
+
console.log(
|
|
1803
|
+
parsed.flags.json
|
|
1804
|
+
? JSON.stringify(result, null, 2)
|
|
1805
|
+
: result.cancelled
|
|
1806
|
+
? "Uninstall cancelled."
|
|
1807
|
+
: "Forge Memory uninstalled."
|
|
1808
|
+
);
|
|
1197
1809
|
}
|
|
1198
1810
|
break;
|
|
1199
1811
|
case "restart":
|
|
1200
1812
|
await stopRuntime();
|
|
1201
|
-
console.log(
|
|
1813
|
+
console.log(
|
|
1814
|
+
JSON.stringify(await startRuntime(await readConfig()), null, 2)
|
|
1815
|
+
);
|
|
1202
1816
|
break;
|
|
1203
1817
|
case "ui":
|
|
1204
1818
|
await runUi(parsed);
|
|
@@ -1221,6 +1835,8 @@ async function main() {
|
|
|
1221
1835
|
}
|
|
1222
1836
|
|
|
1223
1837
|
main().catch((error) => {
|
|
1224
|
-
console.error(
|
|
1838
|
+
console.error(
|
|
1839
|
+
color.red(error instanceof Error ? error.message : String(error))
|
|
1840
|
+
);
|
|
1225
1841
|
process.exitCode = 1;
|
|
1226
1842
|
});
|