@securecoms/agent-bridge 0.7.0 → 0.9.0

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/dist/bin.js CHANGED
@@ -27,6 +27,8 @@ function loadConfig(opts) {
27
27
  let memoryProvider;
28
28
  let memoryBaseDir;
29
29
  let memoryAgent;
30
+ let memoryWorkspaceDir;
31
+ let memoryFactsCharBudget;
30
32
  if (memProvRaw === "none") {
31
33
  memoryProvider = "none";
32
34
  } else if (memProvRaw === "hermes") {
@@ -40,6 +42,10 @@ function loadConfig(opts) {
40
42
  memoryBaseDir = typeof bd === "string" && bd ? bd : "~/.openclaw";
41
43
  const ag = oc?.agent;
42
44
  memoryAgent = typeof ag === "string" && ag ? ag : void 0;
45
+ const wd = oc?.workspace_dir;
46
+ memoryWorkspaceDir = typeof wd === "string" && wd ? wd : void 0;
47
+ const fb = oc?.facts_char_budget;
48
+ memoryFactsCharBudget = typeof fb === "number" && Number.isInteger(fb) && fb >= 0 ? fb : void 0;
43
49
  } else {
44
50
  memoryProvider = "securecoms";
45
51
  }
@@ -56,7 +62,9 @@ function loadConfig(opts) {
56
62
  rateLimitRefillPerMinute,
57
63
  memoryProvider,
58
64
  memoryBaseDir,
59
- memoryAgent
65
+ memoryAgent,
66
+ memoryWorkspaceDir,
67
+ memoryFactsCharBudget
60
68
  };
61
69
  }
62
70
  function arg(argv, flag) {
@@ -3947,7 +3955,7 @@ function loadPackageMeta(importMetaUrl) {
3947
3955
 
3948
3956
  // src/bridge-agent.ts
3949
3957
  import { promises as fsp } from "node:fs";
3950
- import { join as join7, dirname as dirname3 } from "node:path";
3958
+ import { join as join8, dirname as dirname3 } from "node:path";
3951
3959
 
3952
3960
  // src/latency.ts
3953
3961
  var DEFAULT_CAPACITY = 1024;
@@ -4460,29 +4468,153 @@ function readRecentTurns(baseDir, agent, sessionId, k) {
4460
4468
  return k > 0 ? turns.slice(-k) : [];
4461
4469
  }
4462
4470
 
4471
+ // src/memory/openclaw-workspace-facts.ts
4472
+ import { readdirSync, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
4473
+ import { join as join7 } from "node:path";
4474
+ var DEFAULT_FACTS_BUDGET = 8e3;
4475
+ var MEMORY_MD_CAP = 6e3;
4476
+ var MIN_FRAGMENT = 200;
4477
+ var MAX_SOURCE_BYTES = 1024 * 1024;
4478
+ var DAILY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
4479
+ function resolveWorkspaceDir(baseDir, agent, override) {
4480
+ if (override) return expandHome(override);
4481
+ const base = expandHome(baseDir);
4482
+ const isMain2 = agent === "" || agent === "main";
4483
+ try {
4484
+ const cfg = JSON.parse(readFileSync5(join7(base, "openclaw.json"), "utf8"));
4485
+ const agents = cfg?.agents;
4486
+ if (agents && typeof agents === "object") {
4487
+ const list = Array.isArray(agents.list) ? agents.list : [];
4488
+ const wanted = isMain2 ? "main" : agent;
4489
+ const entry = list.find((e) => e && typeof e === "object" && e.id === wanted);
4490
+ const ws = entry?.workspace;
4491
+ if (typeof ws === "string" && ws) return expandHome(ws);
4492
+ const def = agents.defaults?.workspace;
4493
+ if (typeof def === "string" && def) return expandHome(def);
4494
+ }
4495
+ } catch {
4496
+ }
4497
+ return isMain2 ? join7(base, "workspace") : join7(base, `workspace-${agent}`);
4498
+ }
4499
+ var TRUNC = "\u2026[truncated]";
4500
+ function headSlice(s, n) {
4501
+ if (s.length <= n) return { text: s, clipped: false };
4502
+ let out = s.slice(0, n);
4503
+ const last = out.charCodeAt(out.length - 1);
4504
+ if (last >= 55296 && last <= 56319) out = out.slice(0, -1);
4505
+ return { text: out, clipped: true };
4506
+ }
4507
+ function tailSlice(s, n) {
4508
+ if (s.length <= n) return { text: s, clipped: false };
4509
+ let out = s.slice(s.length - n);
4510
+ const first = out.charCodeAt(0);
4511
+ if (first >= 56320 && first <= 57343) out = out.slice(1);
4512
+ return { text: out, clipped: true };
4513
+ }
4514
+ function readBounded(path) {
4515
+ if (statSync2(path).size > MAX_SOURCE_BYTES) return { text: null, oversize: true };
4516
+ return { text: readFileSync5(path, "utf8"), oversize: false };
4517
+ }
4518
+ function readWorkspaceFacts(baseDir, agent, opts) {
4519
+ const raw = opts?.charBudget;
4520
+ const budget = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : DEFAULT_FACTS_BUDGET;
4521
+ if (budget === 0) return { facts: [], truncated: false, missing: false };
4522
+ const dir = resolveWorkspaceDir(baseDir, agent, opts?.workspaceDir);
4523
+ const facts = [];
4524
+ let truncated = false;
4525
+ let remaining = budget;
4526
+ try {
4527
+ const r = readBounded(join7(dir, "MEMORY.md"));
4528
+ if (r.oversize) truncated = true;
4529
+ else if (r.text && r.text.trim()) {
4530
+ const { text, clipped } = headSlice(r.text, Math.min(MEMORY_MD_CAP, budget));
4531
+ truncated ||= clipped;
4532
+ facts.push(`workspace MEMORY.md:
4533
+ ${text}${clipped ? `
4534
+ ${TRUNC}` : ""}`);
4535
+ remaining -= text.length;
4536
+ }
4537
+ } catch {
4538
+ }
4539
+ try {
4540
+ const dailies = readdirSync(join7(dir, "memory")).filter((f) => DAILY_RE.test(f)).sort().reverse().slice(0, 2);
4541
+ for (const f of dailies) {
4542
+ if (remaining < MIN_FRAGMENT) {
4543
+ truncated = true;
4544
+ break;
4545
+ }
4546
+ let r;
4547
+ try {
4548
+ r = readBounded(join7(dir, "memory", f));
4549
+ } catch {
4550
+ continue;
4551
+ }
4552
+ if (r.oversize) {
4553
+ truncated = true;
4554
+ continue;
4555
+ }
4556
+ if (!r.text || !r.text.trim()) continue;
4557
+ const { text, clipped } = tailSlice(r.text, remaining);
4558
+ truncated ||= clipped;
4559
+ facts.push(`workspace memory/${f} (daily):
4560
+ ${clipped ? `${TRUNC}
4561
+ ` : ""}${text}`);
4562
+ remaining -= text.length;
4563
+ }
4564
+ } catch {
4565
+ }
4566
+ return { facts, truncated, missing: facts.length === 0 };
4567
+ }
4568
+
4463
4569
  // src/memory/openclaw-provider.ts
4464
4570
  var DEFAULT_K2 = 200;
4465
4571
  var OpenClawMemoryProvider = class {
4466
- constructor(store, baseDir, agent) {
4572
+ // gauge: last successful returned total, including prefixes/markers
4573
+ constructor(store, baseDir, agent, factsOpts) {
4467
4574
  this.store = store;
4468
4575
  this.baseDir = baseDir;
4469
4576
  this.agent = agent;
4577
+ this.factsOpts = factsOpts;
4470
4578
  }
4471
4579
  noSession = 0;
4472
4580
  readFailed = 0;
4581
+ factsMissing = 0;
4582
+ // absence-shaped: resolved path missing or zero sources (per-call)
4583
+ factsReadFailed = 0;
4584
+ // anything THROWN out of the facts computation
4585
+ factsTruncated = 0;
4586
+ // budget clipped/dropped a source (incl. the 1MB skip)
4587
+ factsChars = 0;
4473
4588
  async record(input) {
4474
4589
  if (input.modality !== "voice") return { seq: 0, deduped: true };
4475
4590
  return this.store.recordMemory({ ...input, text: `[voice] ${input.text}` });
4476
4591
  }
4477
4592
  async context(conversationId, limit) {
4478
4593
  const k = limit ?? DEFAULT_K2;
4594
+ let wsFacts = [];
4595
+ try {
4596
+ const r = readWorkspaceFacts(this.baseDir, this.agent, {
4597
+ workspaceDir: this.factsOpts?.workspaceDir,
4598
+ charBudget: this.factsOpts?.factsCharBudget
4599
+ });
4600
+ wsFacts = r.facts;
4601
+ if (r.missing) this.factsMissing++;
4602
+ if (r.truncated) this.factsTruncated++;
4603
+ this.factsChars = r.facts.reduce((n, f) => n + f.length, 0);
4604
+ } catch {
4605
+ this.factsReadFailed++;
4606
+ }
4607
+ const withFacts = (res) => ({
4608
+ recent: res.recent,
4609
+ facts: [...wsFacts, ...res.facts]
4610
+ });
4479
4611
  const voice = await this.store.memoryContext(conversationId, k);
4480
4612
  let textTurns = [];
4481
4613
  try {
4482
4614
  const sid = resolveOwnerDmSession(this.baseDir, this.agent);
4483
4615
  if (!sid) {
4484
4616
  this.noSession++;
4485
- return voice;
4617
+ return withFacts(voice);
4486
4618
  }
4487
4619
  textTurns = readRecentTurns(this.baseDir, this.agent, sid, k).map((t) => ({
4488
4620
  seq: 0,
@@ -4492,7 +4624,7 @@ var OpenClawMemoryProvider = class {
4492
4624
  }));
4493
4625
  } catch {
4494
4626
  this.readFailed++;
4495
- return voice;
4627
+ return withFacts(voice);
4496
4628
  }
4497
4629
  const tagged = [
4498
4630
  ...textTurns.map((t, i) => ({ ...t, _src: 0, _i: i })),
@@ -4511,10 +4643,17 @@ var OpenClawMemoryProvider = class {
4511
4643
  text: t.text,
4512
4644
  ts: t.ts
4513
4645
  }));
4514
- return { recent, facts: voice.facts };
4646
+ return withFacts({ recent, facts: voice.facts });
4515
4647
  }
4516
4648
  stats() {
4517
- return { openclaw_no_session: this.noSession, openclaw_read_failed: this.readFailed };
4649
+ return {
4650
+ openclaw_no_session: this.noSession,
4651
+ openclaw_read_failed: this.readFailed,
4652
+ openclaw_facts_missing: this.factsMissing,
4653
+ openclaw_facts_read_failed: this.factsReadFailed,
4654
+ openclaw_facts_truncated: this.factsTruncated,
4655
+ openclaw_facts_chars: this.factsChars
4656
+ };
4518
4657
  }
4519
4658
  };
4520
4659
 
@@ -4538,10 +4677,10 @@ var NullMemory = class {
4538
4677
  return { recent: [], facts: [] };
4539
4678
  }
4540
4679
  };
4541
- function createMemoryProvider(provider, store, baseDir, agent) {
4680
+ function createMemoryProvider(provider, store, baseDir, agent, openclawFacts) {
4542
4681
  if (provider === "none") return new NullMemory();
4543
4682
  if (provider === "hermes") return new HermesMemoryProvider(baseDir ?? "~/.hermes");
4544
- if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "");
4683
+ if (provider === "openclaw") return new OpenClawMemoryProvider(store, baseDir ?? "~/.openclaw", agent ?? "", openclawFacts);
4545
4684
  return new SecureComsLocalMemory(store);
4546
4685
  }
4547
4686
 
@@ -4649,7 +4788,10 @@ var BridgeAgent = class _BridgeAgent {
4649
4788
  identityId: cfg.identityId,
4650
4789
  cipherFor: () => new KeyedInboxCipher(deriveMlsStoreKey(seed))
4651
4790
  });
4652
- this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent);
4791
+ this.memory = createMemoryProvider(this.cfg.memoryProvider, this.store, this.cfg.memoryBaseDir, this.cfg.memoryAgent, {
4792
+ workspaceDir: this.cfg.memoryWorkspaceDir,
4793
+ factsCharBudget: this.cfg.memoryFactsCharBudget
4794
+ });
4653
4795
  this.provider = new MlsGroupKeyProvider({
4654
4796
  store: this.store,
4655
4797
  backend: this.backend,
@@ -4729,7 +4871,7 @@ var BridgeAgent = class _BridgeAgent {
4729
4871
  const j = await res.json();
4730
4872
  return String(j.attachment_id);
4731
4873
  };
4732
- this.attachmentsDirImpl = deps.attachmentsDir ?? join7(dirname3(cfg.storePath), "attachments");
4874
+ this.attachmentsDirImpl = deps.attachmentsDir ?? join8(dirname3(cfg.storePath), "attachments");
4733
4875
  this.action = {
4734
4876
  begin: (channelId) => this.actionBegin(channelId),
4735
4877
  start: (opts) => this.actionStart(opts),
@@ -6257,11 +6399,11 @@ var BridgeAgent = class _BridgeAgent {
6257
6399
 
6258
6400
  // src/auth.ts
6259
6401
  import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
6260
- import { existsSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
6402
+ import { existsSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
6261
6403
  function resolveToken(opts) {
6262
6404
  if (opts.token) return opts.token;
6263
6405
  const f = opts.tokenFile;
6264
- if (f && existsSync(f)) return readFileSync5(f, "utf8").trim();
6406
+ if (f && existsSync(f)) return readFileSync6(f, "utf8").trim();
6265
6407
  const tok = randomBytes3(32).toString("base64url");
6266
6408
  if (f) writeFileSync(f, tok, { mode: 384 });
6267
6409
  return tok;
@@ -6832,8 +6974,8 @@ async function handleInbound(sock, agent, data, inflight) {
6832
6974
  }
6833
6975
 
6834
6976
  // src/bin.ts
6835
- import { readFileSync as readFileSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync, existsSync as existsSync2, realpathSync, readdirSync, statSync as statSync2 } from "node:fs";
6836
- import { join as join11, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
6977
+ import { readFileSync as readFileSync8, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, rmSync, existsSync as existsSync2, realpathSync, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
6978
+ import { join as join12, dirname as dirname7, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
6837
6979
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6838
6980
  import { homedir as homedir2, userInfo } from "node:os";
6839
6981
  import { spawnSync } from "node:child_process";
@@ -7042,11 +7184,164 @@ function uninstallService(input, deps) {
7042
7184
  }
7043
7185
 
7044
7186
  // src/service/install-prep.ts
7045
- import { join as join9 } from "node:path";
7187
+ import { join as join10 } from "node:path";
7046
7188
 
7047
7189
  // src/service/wire.ts
7048
- import { join as join8, dirname as dirname5 } from "node:path";
7190
+ import { join as join9, dirname as dirname5 } from "node:path";
7049
7191
  import { fileURLToPath } from "node:url";
7192
+
7193
+ // src/service/hermes-config.ts
7194
+ var HERMES_PLUGIN_NAME = "securecoms-platform";
7195
+ function indentOf(line) {
7196
+ const m = line.match(/^( *)/);
7197
+ return m ? m[1].length : 0;
7198
+ }
7199
+ function isCommentLine(line) {
7200
+ return /^\s*#/.test(line);
7201
+ }
7202
+ function isBlank(line) {
7203
+ return line.trim() === "";
7204
+ }
7205
+ function parseKeyLine(line, key) {
7206
+ const m = line.match(new RegExp(`^( *)${key}:(.*)$`));
7207
+ if (!m) return null;
7208
+ const indent = m[1].length;
7209
+ const rest = m[2].trim();
7210
+ if (rest === "" || rest.startsWith("#")) return { idx: -1, indent, inline: null };
7211
+ return { idx: -1, indent, inline: rest };
7212
+ }
7213
+ function findKeyLine(lines, key, start, end) {
7214
+ for (let i = start; i < end; i++) {
7215
+ const parsed = parseKeyLine(lines[i], key);
7216
+ if (parsed) return { ...parsed, idx: i };
7217
+ }
7218
+ return null;
7219
+ }
7220
+ function findBlockEnd(lines, start, parentIndent) {
7221
+ let i = start;
7222
+ for (; i < lines.length; i++) {
7223
+ if (isBlank(lines[i])) continue;
7224
+ if (indentOf(lines[i]) > parentIndent) continue;
7225
+ break;
7226
+ }
7227
+ return i;
7228
+ }
7229
+ function stripQuotesAndComment(raw) {
7230
+ let value = raw.replace(/\s+#.*$/, "").trim();
7231
+ if (value.length >= 2) {
7232
+ const first = value[0];
7233
+ const last = value[value.length - 1];
7234
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
7235
+ value = value.slice(1, -1);
7236
+ }
7237
+ }
7238
+ return value;
7239
+ }
7240
+ function collectItems(lines, start) {
7241
+ const items = [];
7242
+ let i = start;
7243
+ for (; i < lines.length; i++) {
7244
+ const line = lines[i];
7245
+ if (isCommentLine(line)) {
7246
+ return { items, endIdx: i, ambiguous: true };
7247
+ }
7248
+ const m = line.match(/^( *)-\s*(.+?)\s*$/);
7249
+ if (!m) break;
7250
+ items.push({ idx: i, indent: m[1].length, value: stripQuotesAndComment(m[2]) });
7251
+ }
7252
+ return { items, endIdx: i, ambiguous: false };
7253
+ }
7254
+ var SIBLING_KEY_RE = /^[\w.-]+:(\s|$)/;
7255
+ function isLegitimateListEnd(lines, stopIdx, itemCol, enabledIndent) {
7256
+ if (stopIdx >= lines.length) return true;
7257
+ if (!isBlank(lines[stopIdx])) {
7258
+ return indentOf(lines[stopIdx]) === enabledIndent && SIBLING_KEY_RE.test(lines[stopIdx].trim());
7259
+ }
7260
+ let i = stopIdx;
7261
+ while (i < lines.length && isBlank(lines[i])) i++;
7262
+ if (i >= lines.length) return true;
7263
+ return indentOf(lines[i]) < itemCol;
7264
+ }
7265
+ function parseInlineList(inline) {
7266
+ const trimmed = inline.trim();
7267
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return [];
7268
+ const inner = trimmed.slice(1, -1).trim();
7269
+ if (inner === "") return [];
7270
+ return inner.split(",").map((s) => stripQuotesAndComment(s.trim()));
7271
+ }
7272
+ function noop(reason) {
7273
+ return reason ? { action: "noop", reason } : { action: "noop" };
7274
+ }
7275
+ var MANUAL_INSTRUCTIONS = [
7276
+ "add the following manually under the plugin's config (`~/.hermes/config.yaml`):",
7277
+ " plugins:",
7278
+ " enabled:",
7279
+ ` - ${HERMES_PLUGIN_NAME}`
7280
+ ].join("\n");
7281
+ function ensureHermesPluginEnabled(configText) {
7282
+ const eol = configText.includes("\r\n") ? "\r\n" : "\n";
7283
+ const lines = configText.split(/\r\n|\n/);
7284
+ const pluginsIdx = lines.findIndex((l) => /^plugins:\s*(#.*)?$/.test(l));
7285
+ if (pluginsIdx === -1) {
7286
+ return noop(`no \`plugins:\` block found in config.yaml \u2014 ${MANUAL_INSTRUCTIONS}`);
7287
+ }
7288
+ const pluginsIndent = indentOf(lines[pluginsIdx]);
7289
+ const blockEnd = findBlockEnd(lines, pluginsIdx + 1, pluginsIndent);
7290
+ const enabledKey = findKeyLine(lines, "enabled", pluginsIdx + 1, blockEnd);
7291
+ if (!enabledKey) {
7292
+ return noop(`no \`plugins.enabled\` key found in config.yaml \u2014 ${MANUAL_INSTRUCTIONS}`);
7293
+ }
7294
+ if (enabledKey.inline !== null) {
7295
+ return noop(
7296
+ `plugins.enabled uses the inline \`enabled: [...]\` form, which this editor refuses to rewrite (never reformat) \u2014 ${MANUAL_INSTRUCTIONS}`
7297
+ );
7298
+ }
7299
+ const { items, endIdx, ambiguous } = collectItems(lines, enabledKey.idx + 1);
7300
+ if (ambiguous) {
7301
+ return noop(
7302
+ `a comment line interrupts the plugins.enabled list \u2014 refusing rather than guess whether it belongs to the item above or below; ${MANUAL_INSTRUCTIONS}`
7303
+ );
7304
+ }
7305
+ if (items.length === 0) {
7306
+ return noop(`plugins.enabled has no items to copy an indent column from (empty list) \u2014 ${MANUAL_INSTRUCTIONS}`);
7307
+ }
7308
+ const col = items[0].indent;
7309
+ if (items.some((it) => it.indent !== col)) {
7310
+ return noop(`plugins.enabled items are inconsistently indented \u2014 refusing to guess the right column; ${MANUAL_INSTRUCTIONS}`);
7311
+ }
7312
+ const disabledKey = findKeyLine(lines, "disabled", pluginsIdx + 1, blockEnd);
7313
+ let disabledEntries = [];
7314
+ if (disabledKey) {
7315
+ if (disabledKey.inline !== null) {
7316
+ disabledEntries = parseInlineList(disabledKey.inline);
7317
+ } else {
7318
+ disabledEntries = collectItems(lines, disabledKey.idx + 1).items.map((it) => it.value);
7319
+ }
7320
+ }
7321
+ if (disabledEntries.includes(HERMES_PLUGIN_NAME)) {
7322
+ return noop(
7323
+ `${HERMES_PLUGIN_NAME} is listed under plugins.disabled, which wins over plugins.enabled in the Hermes loader \u2014 remove it from plugins.disabled (or move it to plugins.enabled) manually, then re-run wire.`
7324
+ );
7325
+ }
7326
+ if (items.some((it) => it.value === HERMES_PLUGIN_NAME)) {
7327
+ return { action: "noop" };
7328
+ }
7329
+ if (!isLegitimateListEnd(lines, endIdx, col, enabledKey.indent)) {
7330
+ return noop(
7331
+ `plugins.enabled's list doesn't end at a recognized boundary (expected a sibling key, a blank-then-dedent, or end of file) \u2014 refusing to insert at an unverified spot; ${MANUAL_INSTRUCTIONS}`
7332
+ );
7333
+ }
7334
+ const insertLine = `${" ".repeat(col)}- ${HERMES_PLUGIN_NAME}`;
7335
+ const newLines = [...lines.slice(0, endIdx), insertLine, ...lines.slice(endIdx)];
7336
+ return { action: "edited", text: newLines.join(eol) };
7337
+ }
7338
+
7339
+ // src/service/wire.ts
7340
+ var CONNECTOR_FILE_EXTENSIONS = {
7341
+ openclaw: [".js", ".json"],
7342
+ hermes: [".py", ".yaml", ".json"]
7343
+ };
7344
+ var CONNECTOR_SKIP_DIRS = /* @__PURE__ */ new Set(["tests", "__pycache__"]);
7050
7345
  function detectHarness(home, statDir) {
7051
7346
  if (statDir(`${home}/.openclaw`)) return "openclaw";
7052
7347
  if (statDir(`${home}/.hermes`)) return "hermes";
@@ -7054,11 +7349,41 @@ function detectHarness(home, statDir) {
7054
7349
  }
7055
7350
  function connectorTargetDir(home, harness) {
7056
7351
  if (harness === "openclaw") return `${home}/.openclaw/local-plugins/securecoms`;
7057
- return `${home}/.hermes/local-plugins/securecoms`;
7352
+ return `${home}/.hermes/plugins/securecoms`;
7058
7353
  }
7059
- function connectorsSrcDir(importMetaUrl) {
7354
+ function connectorsSrcDir(importMetaUrl, harness) {
7060
7355
  const pkgRoot = dirname5(dirname5(fileURLToPath(importMetaUrl)));
7061
- return join8(pkgRoot, "connectors", "openclaw");
7356
+ return join9(pkgRoot, "connectors", harness);
7357
+ }
7358
+ function hermesConfigPath(home) {
7359
+ return `${home}/.hermes/config.yaml`;
7360
+ }
7361
+ function hermesConfigBackupPath(configPath, ts, pid) {
7362
+ return `${configPath}.bak-wire-${ts}-${pid}`;
7363
+ }
7364
+ function enableHermesConfig(home, deps) {
7365
+ const path = hermesConfigPath(home);
7366
+ const text = deps.readHermesConfig(path);
7367
+ if (text === null) {
7368
+ return {
7369
+ ok: false,
7370
+ message: `Hermes config.yaml not found at ${path} (is ~/.hermes installed?) \u2014 add the plugin manually:
7371
+ plugins:
7372
+ enabled:
7373
+ - ${HERMES_PLUGIN_NAME}`
7374
+ };
7375
+ }
7376
+ const result = ensureHermesPluginEnabled(text);
7377
+ if (result.action === "noop") {
7378
+ if (result.reason) return { ok: false, message: result.reason };
7379
+ return { ok: true, message: `${HERMES_PLUGIN_NAME} is already enabled in ${path}.` };
7380
+ }
7381
+ deps.backupHermesConfig(path, text);
7382
+ deps.writeHermesConfig(path, result.text);
7383
+ return {
7384
+ ok: true,
7385
+ message: `enabled ${HERMES_PLUGIN_NAME} in ${path} (the original was backed up alongside it as config.yaml.bak-wire-<ts>-<pid>).`
7386
+ };
7062
7387
  }
7063
7388
  function parseArg(argv, flag) {
7064
7389
  const i = argv.indexOf(flag);
@@ -7086,15 +7411,19 @@ function runWire(deps) {
7086
7411
  const resolved = resolveEntry(deps);
7087
7412
  if (!resolved) return 1;
7088
7413
  const { entry, harness } = resolved;
7089
- if (harness === "hermes") {
7090
- deps.log(
7091
- "[securecoms] Hermes connector is not available yet (fast-follow); detection and target dir only. Nothing installed."
7092
- );
7093
- return 1;
7094
- }
7095
7414
  const targetDir = connectorTargetDir(deps.home, harness);
7096
- deps.copyConnector(deps.connectorsSrcDir, targetDir);
7415
+ deps.copyConnector(deps.connectorsSrcDir(harness), targetDir, harness);
7097
7416
  deps.writeBinding(targetDir, entry.identityId);
7417
+ if (harness === "hermes") {
7418
+ const enabled = enableHermesConfig(deps.home, deps);
7419
+ deps.log(`[securecoms] ${enabled.message}`);
7420
+ if (!enabled.ok) {
7421
+ deps.log(
7422
+ "wire: Hermes connector files were copied and the binding was written, but the config.yaml plugin entry was NOT enabled automatically \u2014 the harness is NOT wired until you add it manually (see above) and restart the gateway."
7423
+ );
7424
+ return 1;
7425
+ }
7426
+ }
7098
7427
  if (typeof entry.port === "number") {
7099
7428
  const h = deps.getHealth(entry.port);
7100
7429
  if (h && (h.agent_sessions ?? 0) >= 1) {
@@ -7105,17 +7434,23 @@ function runWire(deps) {
7105
7434
  } else {
7106
7435
  deps.log(`wire: connector installed (no bridge port known yet) \u2014 restart the harness to pick it up.`);
7107
7436
  }
7437
+ if (harness === "hermes") {
7438
+ deps.log(
7439
+ "wire: restart the Hermes gateway (`launchctl kickstart -k gui/$UID/ai.hermes.gateway` or your service manager) to pick up the connector."
7440
+ );
7441
+ }
7108
7442
  return 0;
7109
7443
  }
7110
7444
  function runUnwire(deps) {
7111
7445
  const resolved = resolveEntry(deps);
7112
7446
  if (!resolved) return 1;
7113
7447
  const { harness } = resolved;
7448
+ deps.removeBinding(connectorTargetDir(deps.home, harness));
7114
7449
  if (harness === "hermes") {
7115
- deps.log("[securecoms] Hermes connector is not available yet (fast-follow); nothing to remove.");
7116
- return 0;
7450
+ deps.log(
7451
+ "[securecoms] Hermes binding removed. The connector files under ~/.hermes/plugins/securecoms and the plugins.enabled entry in config.yaml remain \u2014 removing a listed plugin's files would make Hermes log load errors at boot. Edit config.yaml manually to fully remove it, then restart the gateway."
7452
+ );
7117
7453
  }
7118
- deps.removeBinding(connectorTargetDir(deps.home, harness));
7119
7454
  return 0;
7120
7455
  }
7121
7456
 
@@ -7132,8 +7467,8 @@ function customFacts(input) {
7132
7467
  function autoWireHarness(input, deps) {
7133
7468
  const h = input.harness;
7134
7469
  if (!h || h === "none") return { echo: "", wired: false, error: false };
7135
- const retry = `agent-bridge wire --harness openclaw --identity ${input.slug}`;
7136
7470
  if (h === "openclaw") {
7471
+ const retry = `agent-bridge wire --harness openclaw --identity ${input.slug}`;
7137
7472
  const parent = `${input.home}/.openclaw`;
7138
7473
  if (!deps.statDir(parent)) {
7139
7474
  return {
@@ -7147,7 +7482,7 @@ function autoWireHarness(input, deps) {
7147
7482
  const targetDir = connectorTargetDir(input.home, "openclaw");
7148
7483
  try {
7149
7484
  const prev = deps.readBinding(targetDir);
7150
- deps.copyConnector(input.connectorsSrcDir, targetDir);
7485
+ deps.copyConnector(input.connectorsSrcDir, targetDir, "openclaw");
7151
7486
  deps.writeBinding(targetDir, input.identityId);
7152
7487
  const rebind = prev && prev !== input.identityId ? `
7153
7488
  rebind: ${prev} \u2192 ${input.identityId}` : "";
@@ -7167,8 +7502,48 @@ function autoWireHarness(input, deps) {
7167
7502
  }
7168
7503
  }
7169
7504
  if (h === "hermes") {
7170
- return { echo: `[securecoms] Hermes turnkey connector is coming (Phase 2). For now drive Hermes via the custom WS path:
7171
- ${customFacts(input)}`, wired: false, error: false };
7505
+ const retry = `agent-bridge wire --harness hermes --identity ${input.slug}`;
7506
+ const parent = `${input.home}/.hermes`;
7507
+ if (!deps.statDir(parent)) {
7508
+ return {
7509
+ echo: `[securecoms] Hermes not found at ~/.hermes \u2014 the bridge service is installed, but the harness was NOT wired.
7510
+ Install Hermes, then re-run install, or run: ${retry}
7511
+ (or re-run install with --harness custom).`,
7512
+ wired: false,
7513
+ error: true
7514
+ };
7515
+ }
7516
+ const targetDir = connectorTargetDir(input.home, "hermes");
7517
+ try {
7518
+ const prev = deps.readBinding(targetDir);
7519
+ deps.copyConnector(input.connectorsSrcDir, targetDir, "hermes");
7520
+ deps.writeBinding(targetDir, input.identityId);
7521
+ const enabled = enableHermesConfig(input.home, deps);
7522
+ if (!enabled.ok) {
7523
+ return {
7524
+ echo: `[securecoms] Hermes connector copied and bound to identity ${input.identityId}, but the config.yaml plugin entry was NOT enabled automatically: ${enabled.message}
7525
+ The harness is NOT wired until you add it manually and restart the gateway.`,
7526
+ wired: false,
7527
+ error: true
7528
+ };
7529
+ }
7530
+ const rebind = prev && prev !== input.identityId ? `
7531
+ rebind: ${prev} \u2192 ${input.identityId}` : "";
7532
+ return {
7533
+ echo: `[securecoms] Hermes connector installed and bound to identity ${input.identityId}.${rebind}
7534
+ ${enabled.message}
7535
+ Restart the Hermes gateway (\`launchctl kickstart -k gui/$UID/ai.hermes.gateway\` or your service manager) after install completes.`,
7536
+ wired: true,
7537
+ error: false
7538
+ };
7539
+ } catch (e) {
7540
+ return {
7541
+ echo: `[securecoms] the bridge service is installed, but wiring the Hermes connector FAILED: ${e.message}
7542
+ Retry after fixing the path: ${retry}`,
7543
+ wired: false,
7544
+ error: true
7545
+ };
7546
+ }
7172
7547
  }
7173
7548
  return { echo: `[securecoms] Custom harness \u2014 bring your own connector:
7174
7549
  ${customFacts(input)}`, wired: false, error: false };
@@ -7197,10 +7572,10 @@ function assertReadableCredential(absPath, deps) {
7197
7572
  return { deviceId: json2.device_id, identityId: json2.identity_id, issuedAt };
7198
7573
  }
7199
7574
  function durableDir(home, slug) {
7200
- return join9(home, `securecoms-agent-${slug}`);
7575
+ return join10(home, `securecoms-agent-${slug}`);
7201
7576
  }
7202
7577
  function durableCredentialPath(home, slug) {
7203
- return join9(durableDir(home, slug), `${slug}.json`);
7578
+ return join10(durableDir(home, slug), `${slug}.json`);
7204
7579
  }
7205
7580
  function takeCustody(args2, deps) {
7206
7581
  const dir = durableDir(args2.home, args2.slug);
@@ -7247,7 +7622,7 @@ function readSiblingDefs(dir, prefix, deps) {
7247
7622
  for (const n of names) {
7248
7623
  if (!n.startsWith(prefix)) continue;
7249
7624
  try {
7250
- out.push(deps.readFile(join9(dir, n)));
7625
+ out.push(deps.readFile(join10(dir, n)));
7251
7626
  } catch {
7252
7627
  }
7253
7628
  }
@@ -7443,29 +7818,29 @@ function renderStatusTable(rows) {
7443
7818
  import {
7444
7819
  chmodSync,
7445
7820
  mkdirSync,
7446
- readFileSync as readFileSync6,
7821
+ readFileSync as readFileSync7,
7447
7822
  renameSync,
7448
7823
  unlinkSync,
7449
7824
  writeFileSync as writeFileSync2
7450
7825
  } from "node:fs";
7451
7826
  import { randomUUID } from "node:crypto";
7452
- import { join as join10 } from "node:path";
7827
+ import { join as join11 } from "node:path";
7453
7828
  var REAL_FS = {
7454
7829
  mkdir: (p, mode) => mkdirSync(p, { recursive: true, mode }),
7455
7830
  chmod: (p, mode) => chmodSync(p, mode),
7456
7831
  writeFile: (p, data, mode) => writeFileSync2(p, data, { mode }),
7457
7832
  rename: (a, b) => renameSync(a, b),
7458
- readFile: (p) => readFileSync6(p, "utf8"),
7833
+ readFile: (p) => readFileSync7(p, "utf8"),
7459
7834
  unlink: (p) => unlinkSync(p)
7460
7835
  };
7461
7836
  function securecomsDir(home) {
7462
- return join10(home, ".securecoms");
7837
+ return join11(home, ".securecoms");
7463
7838
  }
7464
7839
  function connectionsDir(home) {
7465
- return join10(securecomsDir(home), "connections");
7840
+ return join11(securecomsDir(home), "connections");
7466
7841
  }
7467
7842
  function connectionFilePath(home, identityId) {
7468
- return join10(connectionsDir(home), `${identityId}.json`);
7843
+ return join11(connectionsDir(home), `${identityId}.json`);
7469
7844
  }
7470
7845
  function newInstanceId() {
7471
7846
  return randomUUID();
@@ -7580,7 +7955,7 @@ function realResolveDeps() {
7580
7955
  }
7581
7956
  function realPrepDeps() {
7582
7957
  return {
7583
- readFile: (p) => readFileSync7(p, "utf8"),
7958
+ readFile: (p) => readFileSync8(p, "utf8"),
7584
7959
  writeFileMode: (p, data, mode) => writeFileSync3(p, data, { mode }),
7585
7960
  mkdirp: (p, mode) => {
7586
7961
  mkdirSync2(p, { recursive: true, mode: mode ?? 493 });
@@ -7589,7 +7964,7 @@ function realPrepDeps() {
7589
7964
  realpath: (p) => realpathSync(p),
7590
7965
  listDir: (p) => {
7591
7966
  try {
7592
- return readdirSync(p);
7967
+ return readdirSync2(p);
7593
7968
  } catch {
7594
7969
  return [];
7595
7970
  }
@@ -7631,12 +8006,12 @@ function realReadFleet() {
7631
8006
  home: homedir2(),
7632
8007
  listDir: (p) => {
7633
8008
  try {
7634
- return readdirSync(p);
8009
+ return readdirSync2(p);
7635
8010
  } catch {
7636
8011
  return [];
7637
8012
  }
7638
8013
  },
7639
- readFile: (p) => readFileSync7(p, "utf8")
8014
+ readFile: (p) => readFileSync8(p, "utf8")
7640
8015
  });
7641
8016
  }
7642
8017
  function runStatus() {
@@ -7656,40 +8031,56 @@ function runStatus() {
7656
8031
  else process.stdout.write(renderStatusTable(rows));
7657
8032
  process.exit(0);
7658
8033
  }
7659
- function realCopyConnector(srcDir, dstDir) {
8034
+ function realCopyConnector(srcDir, dstDir, harness) {
8035
+ const allowedExt = CONNECTOR_FILE_EXTENSIONS[harness];
7660
8036
  mkdirSync2(dstDir, { recursive: true });
7661
- for (const name of readdirSync(srcDir)) {
7662
- const srcPath = join11(srcDir, name);
7663
- if (statSync2(srcPath).isDirectory()) {
7664
- realCopyConnector(srcPath, join11(dstDir, name));
8037
+ for (const name of readdirSync2(srcDir)) {
8038
+ const srcPath = join12(srcDir, name);
8039
+ if (statSync3(srcPath).isDirectory()) {
8040
+ if (CONNECTOR_SKIP_DIRS.has(name)) continue;
8041
+ realCopyConnector(srcPath, join12(dstDir, name), harness);
7665
8042
  continue;
7666
8043
  }
7667
- if (!/\.(js|json)$/.test(name)) continue;
7668
- writeFileSync3(join11(dstDir, name), readFileSync7(srcPath));
8044
+ if (name.includes(".test.")) continue;
8045
+ if (!allowedExt.some((ext) => name.endsWith(ext))) continue;
8046
+ writeFileSync3(join12(dstDir, name), readFileSync8(srcPath));
7669
8047
  }
7670
8048
  }
7671
8049
  function realWriteBinding(dstDir, identityId) {
7672
8050
  mkdirSync2(dstDir, { recursive: true });
7673
- writeFileSync3(join11(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
8051
+ writeFileSync3(join12(dstDir, "securecoms.binding.json"), JSON.stringify({ identity_id: identityId }) + "\n", { mode: 384 });
7674
8052
  }
7675
8053
  function realReadBinding(dstDir) {
7676
8054
  try {
7677
- const parsed = JSON.parse(readFileSync7(join11(dstDir, "securecoms.binding.json"), "utf8"));
8055
+ const parsed = JSON.parse(readFileSync8(join12(dstDir, "securecoms.binding.json"), "utf8"));
7678
8056
  return parsed.identity_id ?? null;
7679
8057
  } catch {
7680
8058
  return null;
7681
8059
  }
7682
8060
  }
7683
8061
  function realRemoveBinding(dstDir) {
7684
- rmSync(join11(dstDir, "securecoms.binding.json"), { force: true });
8062
+ rmSync(join12(dstDir, "securecoms.binding.json"), { force: true });
7685
8063
  }
7686
8064
  function realStatDir(p) {
7687
8065
  try {
7688
- return statSync2(p).isDirectory();
8066
+ return statSync3(p).isDirectory();
7689
8067
  } catch {
7690
8068
  return false;
7691
8069
  }
7692
8070
  }
8071
+ function realReadHermesConfig(p) {
8072
+ try {
8073
+ return readFileSync8(p, "utf8");
8074
+ } catch {
8075
+ return null;
8076
+ }
8077
+ }
8078
+ function realBackupHermesConfig(p, originalText) {
8079
+ writeFileSync3(hermesConfigBackupPath(p, Date.now(), process.pid), originalText, "utf8");
8080
+ }
8081
+ function realWriteHermesConfig(p, newText) {
8082
+ writeFileSync3(p, newText, "utf8");
8083
+ }
7693
8084
  function realWireArgv() {
7694
8085
  if (process.argv.includes("--harness")) return process.argv;
7695
8086
  const detected = detectHarness(homedir2(), realStatDir);
@@ -7706,8 +8097,12 @@ function realWireDeps() {
7706
8097
  getHealth: realGetHealth,
7707
8098
  log: (s) => process.stdout.write(s + "\n"),
7708
8099
  // bin.ts's OWN import.meta.url — see connectorsSrcDir's doc comment in
7709
- // wire.ts for why it must be the entry point's URL, not wire.ts's.
7710
- connectorsSrcDir: connectorsSrcDir(import.meta.url)
8100
+ // wire.ts for why it must be the entry point's URL, not wire.ts's. Keyed
8101
+ // by harness (Task 6) so one WireDeps instance can wire either harness.
8102
+ connectorsSrcDir: (h) => connectorsSrcDir(import.meta.url, h),
8103
+ readHermesConfig: realReadHermesConfig,
8104
+ backupHermesConfig: realBackupHermesConfig,
8105
+ writeHermesConfig: realWriteHermesConfig
7711
8106
  };
7712
8107
  }
7713
8108
  function runWireVerb(verb) {
@@ -7736,21 +8131,25 @@ async function runServiceVerb(verb) {
7736
8131
  argOrEnv("--exec-path", "SECURECOMS_EXEC_PATH"),
7737
8132
  realResolveNodeDeps()
7738
8133
  );
8134
+ const installHarness = argOrEnv("--harness", "SECURECOMS_HARNESS");
7739
8135
  const r = await orchestrateInstall({
7740
8136
  credentialAbsPath: credAbs,
7741
8137
  label,
7742
8138
  explicitPort: portStr ? Number(portStr) : void 0,
7743
- harness: argOrEnv("--harness", "SECURECOMS_HARNESS"),
8139
+ harness: installHarness,
7744
8140
  home: homedir2(),
7745
- connectorsSrcDir: connectorsSrcDir(import.meta.url),
8141
+ connectorsSrcDir: connectorsSrcDir(import.meta.url, installHarness === "hermes" ? "hermes" : "openclaw"),
7746
8142
  autoWireDeps: {
7747
8143
  statDir: realStatDir,
7748
- // reuse bin.ts:235
8144
+ // reuse bin.ts:266
7749
8145
  readBinding: realReadBinding,
7750
8146
  copyConnector: realCopyConnector,
7751
- // bin.ts:212 — one copy impl
8147
+ // bin.ts:225 — one copy impl
7752
8148
  writeBinding: realWriteBinding,
7753
- // bin.ts:226 — writes {identity_id}
8149
+ // bin.ts:245 — writes {identity_id}
8150
+ readHermesConfig: realReadHermesConfig,
8151
+ backupHermesConfig: realBackupHermesConfig,
8152
+ writeHermesConfig: realWriteHermesConfig,
7754
8153
  log: (s) => process.stderr.write(s + "\n")
7755
8154
  },
7756
8155
  prepDeps: realPrepDeps(),
@@ -7830,11 +8229,11 @@ async function main() {
7830
8229
  `);
7831
8230
  process.exit(1);
7832
8231
  }
7833
- const tokenFile = daemon.tokenFile ?? join11(dirname7(credentialPath), "bridge-token");
8232
+ const tokenFile = daemon.tokenFile ?? join12(dirname7(credentialPath), "bridge-token");
7834
8233
  const token = resolveToken({ tokenFile, token: daemon.token });
7835
8234
  const agent = new BridgeAgent(cfg);
7836
8235
  await agent.start();
7837
- const tls = daemon.tls ? { cert: readFileSync7(daemon.tlsCert, "utf8"), key: readFileSync7(daemon.tlsKey, "utf8") } : void 0;
8236
+ const tls = daemon.tls ? { cert: readFileSync8(daemon.tlsCert, "utf8"), key: readFileSync8(daemon.tlsKey, "utf8") } : void 0;
7838
8237
  const { server, closeAllSseStreams } = createHttpServer(agent, { token, allowOrigins: daemon.allowOrigins, tls, websocket: true });
7839
8238
  const agentWs = attachAgentWebSocket(server, agent, { token, allowOrigins: daemon.allowOrigins });
7840
8239
  let down = false;
@@ -7897,6 +8296,7 @@ export {
7897
8296
  chooseInstallExecPath,
7898
8297
  nodeMajorOk,
7899
8298
  parseVerb,
8299
+ realCopyConnector,
7900
8300
  slugFromCredentialPath,
7901
8301
  tokenFingerprint
7902
8302
  };