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.
@@ -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 { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
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") flags.skipPairIos = true;
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") flags.manualHttp = true;
74
- else if (arg.startsWith("--output=")) values.output = arg.slice("--output=".length);
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=")) values.dataRoot = arg.slice("--data-root=".length);
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=")) values.adapters = arg.slice("--adapters=".length);
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=")) values.origin = arg.slice("--origin=".length);
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=")) values.port = arg.slice("--port=".length);
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=")) values.webPort = arg.slice("--web-port=".length);
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=")) values.repo = arg.slice("--repo=".length);
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 ? parsed : fallback;
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(filePath, payload, { dryRun = false, backup = true } = {}) {
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(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
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: typeof config?.dataRoot === "string" ? path.resolve(config.dataRoot) : defaultDataRoot(),
190
- adapters: Array.isArray(config?.adapters) ? config.adapters.filter((entry) => ADAPTERS.includes(entry)) : [],
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(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
213
- shell: process.platform !== "win32",
214
- stdio: "ignore"
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 = commandExists("openclaw") || fs.existsSync(path.join(homeDir(), ".openclaw"));
230
- const version = commandExists("openclaw") ? runCapture("openclaw", ["--version"]) : null;
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 ? (version || "detected") : "not found",
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(hermesRoot, "hermes-agent", "venv", "bin", "python");
246
- const installed = commandExists("hermes") || fs.existsSync(hermesPython) || fs.existsSync(hermesRoot);
247
- const version = commandExists("hermes") ? runCapture("hermes", ["--version"]) : null;
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 ? (version || "detected") : "not found",
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") ? runCapture("codex", ["--version"]) : null;
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 ? (version || "detected") : "not found",
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({ input: process.stdin, output: process.stdout });
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 = (await promptLine(`${question} ${defaultValue ? "[Y/n]" : "[y/N]"}`, "")).toLowerCase();
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("Select host adapters. Space toggles, arrows move, Enter confirms.\n");
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(rows.filter((entry) => entry.selected && !entry.disabled && !entry.action).map((entry) => entry.id));
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 = typeof address === "object" && address ? address.port : DEFAULT_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 (let port = startPort; port < startPort + 30 && port <= 65535; port += 1) {
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(repoRoot, "..", "..", "data", "forge-runtime.json");
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 (parsed?.name === "forge" && fs.existsSync(path.join(current, "server", "src", "index.ts"))) {
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 ? path.resolve(parsed.values.repo) : findForgeRepo();
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.filter((adapter) => adapter.installed).map((adapter) => adapter.id);
452
- const currentDefaults = currentConfig.adapters.length > 0 ? currentConfig.adapters : detectedDefaults;
453
- const adapterOverride = parsed.flags.skipAdapters ? [] : normalizeAdapterList(parsed.values.adapters);
454
- const adapters = adapterOverride ?? (parsed.flags.yes ? currentDefaults : await promptCheckbox(discovery.adapters, currentDefaults));
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 ? await resolveDevDataRoot(repo) : currentConfig.dataRoot || defaultDataRoot());
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(normalizePort(parsed.values.webPort ?? currentConfig.webPort, DEFAULT_WEB_PORT));
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 = payload.plugins && typeof payload.plugins === "object" ? { ...payload.plugins } : {};
482
- const entries = plugins.entries && typeof plugins.entries === "object" ? { ...plugins.entries } : {};
483
- const currentEntry = entries[FORGE_PLUGIN_ID] && typeof entries[FORGE_PLUGIN_ID] === "object" ? { ...entries[FORGE_PLUGIN_ID] } : {};
484
- const currentPluginConfig = currentEntry.config && typeof currentEntry.config === "object" ? { ...currentEntry.config } : {};
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(homeDir(), ".hermes", "forge", "config.json");
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")) root.plugins.enabled.push("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) ? await fsp.readFile(filePath, "utf8") : "";
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 = /(?:^|\n)\[mcp_servers\.forge\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/m;
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(filePath, source.endsWith("\n") ? source : `${source}\n`, "utf8");
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(command, args, { cwd, dryRun = false, env = process.env } = {}) {
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 { adapter: "openclaw", ok: false, skipped: true, message: "openclaw command not found" };
699
+ return {
700
+ adapter: "openclaw",
701
+ ok: false,
702
+ skipped: true,
703
+ message: "openclaw command not found"
704
+ };
573
705
  }
574
- const installTarget = config.mode === "dev" && config.repo ? path.join(config.repo, "openclaw-plugin") : FORGE_PLUGIN_ID;
575
- const installArgs = config.mode === "dev"
576
- ? ["plugins", "install", "--link", "--dangerously-force-unsafe-install", installTarget]
577
- : ["plugins", "install", "--dangerously-force-unsafe-install", installTarget];
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) return { adapter: "openclaw", ok: false, message: "OpenClaw plugin install failed" };
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(homeDir(), ".hermes", "hermes-agent", "venv", "bin", "python");
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 { adapter: "hermes", ok: false, skipped: true, message: "Hermes Python environment not found" };
748
+ return {
749
+ adapter: "hermes",
750
+ ok: false,
751
+ skipped: true,
752
+ message: "Hermes Python environment not found"
753
+ };
590
754
  }
591
- const target = config.mode === "dev" && config.repo
592
- ? ["-m", "pip", "install", "--upgrade", "-e", path.join(config.repo, "plugins", "forge-hermes")]
593
- : ["-m", "pip", "install", "--upgrade", "forge-hermes-plugin"];
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 { adapter: "hermes", ok: result.ok, message: result.ok ? undefined : "Hermes plugin install failed" };
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") results.push(await installOpenClawAdapter(config, options));
607
- if (adapter === "hermes") results.push(await installHermesAdapter(config, options));
608
- if (adapter === "codex") results.push(await installCodexAdapter(config, options));
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 { ok: false, error: error instanceof Error ? error.message : String(error) };
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(runtimeInstallRoot(), "package.json");
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
- ["install", `${RUNTIME_PACKAGE}@${RUNTIME_PACKAGE_VERSION}`, "--omit=dev", "--ignore-scripts", "--silent"],
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(`Failed to install ${RUNTIME_PACKAGE}@${RUNTIME_PACKAGE_VERSION}. Check ${logPath()}.`);
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) throw new Error(`${RUNTIME_PACKAGE} installed but its runtime entry could not be resolved.`);
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) throw new Error("Dev mode requires a Forge repo checkout.");
725
- const tsx = path.join(config.repo, "node_modules", "tsx", "dist", "cli.mjs");
726
- if (!fs.existsSync(tsx)) throw new Error(`tsx was not found at ${tsx}. Run npm install in the Forge repo.`);
727
- const server = spawn(process.execPath, [tsx, path.join(config.repo, "server", "src", "index.ts")], {
728
- cwd: config.repo,
729
- detached: true,
730
- stdio: ["ignore", out, out],
731
- env: {
732
- ...process.env,
733
- HOST: "127.0.0.1",
734
- PORT: String(config.port),
735
- FORGE_BASE_PATH: "/forge/",
736
- FORGE_DATA_ROOT: config.dataRoot,
737
- FORGE_DEV_WEB_ORIGIN: `http://127.0.0.1:${config.webPort}/forge/`
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("npm", ["run", "dev:web", "--", "--host", "127.0.0.1", "--port", String(config.webPort)], {
743
- cwd: config.repo,
744
- detached: true,
745
- stdio: ["ignore", out, out],
746
- env: { ...process.env, FORGE_BASE_PATH: "/forge/" }
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) return { ok: true, stopped: false, message: "No forge-memory runtime state found." };
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 ?? path.join(forgeHome(), "exports", `forge-memory-export-${stamp}.tar.gz`)
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(configPath(), path.join(stagingRoot, "forge-memory-config.json"));
1065
+ await fsp.copyFile(
1066
+ configPath(),
1067
+ path.join(stagingRoot, "forge-memory-config.json")
1068
+ );
836
1069
  }
837
- await fsp.writeFile(path.join(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
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 = outputPath.endsWith(".tar.gz") || outputPath.endsWith(".tgz");
1077
+ const wantsArchive =
1078
+ outputPath.endsWith(".tar.gz") || outputPath.endsWith(".tgz");
841
1079
  if (wantsArchive && commandExists("tar")) {
842
- const result = spawnSync("tar", ["-czf", outputPath, "-C", stagingRoot, "."], {
843
- stdio: parsed.flags.json ? "ignore" : "inherit"
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) throw new Error(`Failed to write export archive: ${outputPath}`);
847
- return { ok: true, outputPath, archive: true, sourceDataRoot: config.dataRoot };
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 { ok: true, outputPath, archive: false, sourceDataRoot: config.dataRoot };
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]) return { filePath, changed: false };
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((entry) => entry !== FORGE_PLUGIN_ID);
1117
+ payload.plugins.allow = payload.plugins.allow.filter(
1118
+ (entry) => entry !== FORGE_PLUGIN_ID
1119
+ );
864
1120
  }
865
- await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
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(homeDir(), ".hermes", "forge", "config.json");
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 = /(?:^|\n)\[mcp_servers\.forge\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/m;
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.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").trimEnd();
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 [runtimeInstallRoot(), runtimeStatePath(), logPath(), configPath()]) {
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 = parsed.flags.removeAdapters || (!parsed.flags.yes && await promptYesNo("Remove Forge adapter entries from OpenClaw, Hermes, and Codex?", false));
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(`Delete Forge data folder ${config.dataRoot}? This cannot be undone.`, false);
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(new URL("/api/v1/health/pairing-sessions", baseUrl(config)), {
945
- method: "POST",
946
- headers: { "content-type": "application/json", accept: "application/json" },
947
- body: JSON.stringify({ userId: null, transportMode })
948
- });
949
- if (!response.ok) throw new Error(`Pairing request failed with ${response.status}`);
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 = pairing.qrPayload.transport?.protocol === "iroh"
959
- ? "Iroh"
960
- : pairing.qrPayload.transportMode === "iroh"
1248
+ const label =
1249
+ pairing.qrPayload.transport?.protocol === "iroh"
961
1250
  ? "Iroh"
962
- : "Manual HTTP";
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(color.dim("Discovery runs in the background. Forge UI/runtime is always installed.\n"));
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(parsed, currentConfig, discovery, command);
982
- const writeResult = await writeConfig(config, { dryRun: parsed.flags.dryRun });
983
- const adapterResults = await configureAdapters(config, { dryRun: parsed.flags.dryRun });
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 = parsed.flags.pairIos || (!parsed.flags.skipPairIos && (parsed.flags.yes ? true : await promptYesNo("Pair the iOS companion now?", true)));
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 = { ok: true, config, writeResult, adapterResults, runtimeResult, pairing: Boolean(pairing) };
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) console.log(color.yellow("Dry run only; no files or adapter installs were changed."));
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(`Runtime: ${currentHealth.ok ? color.green("healthy") : color.yellow("not reachable")}`);
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(`Adapters: ${config.adapters.length ? config.adapters.join(", ") : "none configured"}`);
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
- { id: "node", ok: Number(process.versions.node.split(".")[0]) >= 22, detail: process.versions.node },
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
- { id: "dataRoot", ok: fs.existsSync(config.dataRoot), detail: config.dataRoot },
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) => ({ id: adapter.id, ok: adapter.installed, detail: adapter.status }))
1374
+ ...discovery.adapters.map((adapter) => ({
1375
+ id: adapter.id,
1376
+ ok: adapter.installed,
1377
+ detail: adapter.status
1378
+ }))
1044
1379
  ];
1045
- const payload = { ok: checks.every((check) => check.ok || ADAPTERS.includes(check.id)), checks };
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(`${check.ok ? color.green("ok") : color.yellow("warn")} ${check.id}: ${check.detail}`);
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(parsed.flags.json ? JSON.stringify({ url: webUrl(config) }, null, 2) : webUrl(config));
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 server = new Server({ name: "forge-memory", version: VERSION }, { capabilities: { tools: {} } });
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({ baseUrl: baseUrl(config), webUrl: webUrl(config), dataRoot: config.dataRoot, identity: sha(config.dataRoot) }, null, 2)
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 { content: [{ type: "text", text: JSON.stringify(await health(config), null, 2) }] };
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(JSON.stringify(await startRuntime(await readConfig()), null, 2));
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(parsed.flags.json ? JSON.stringify(result, null, 2) : `Exported Forge data to ${result.outputPath}`);
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(parsed.flags.json ? JSON.stringify(result, null, 2) : result.cancelled ? "Uninstall cancelled." : "Forge Memory uninstalled.");
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(JSON.stringify(await startRuntime(await readConfig()), null, 2));
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(color.red(error instanceof Error ? error.message : String(error)));
1838
+ console.error(
1839
+ color.red(error instanceof Error ? error.message : String(error))
1840
+ );
1225
1841
  process.exitCode = 1;
1226
1842
  });