@agentvault/claude-bridge 0.7.16 → 0.8.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/index.js CHANGED
@@ -9,36 +9,138 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/pending-invite.ts
13
+ import { existsSync, mkdirSync, chmodSync, readFileSync as readFileSync2, writeFileSync, rmSync } from "node:fs";
14
+ import { join as join5 } from "node:path";
15
+ function pendingInvitePath(dataDir) {
16
+ return join5(dataDir, PENDING_INVITE_FILE);
17
+ }
18
+ function writePendingInvite(dataDir, token) {
19
+ mkdirSync(dataDir, { recursive: true, mode: DIR_MODE2 });
20
+ chmodSync(dataDir, DIR_MODE2);
21
+ const p2 = pendingInvitePath(dataDir);
22
+ writeFileSync(p2, token, { mode: FILE_MODE2 });
23
+ chmodSync(p2, FILE_MODE2);
24
+ }
25
+ function readPendingInvite(dataDir) {
26
+ const p2 = pendingInvitePath(dataDir);
27
+ if (!existsSync(p2)) return void 0;
28
+ try {
29
+ const raw = readFileSync2(p2, "utf-8").trim();
30
+ return raw.length > 0 ? raw : void 0;
31
+ } catch {
32
+ return void 0;
33
+ }
34
+ }
35
+ function clearPendingInvite(dataDir) {
36
+ try {
37
+ rmSync(pendingInvitePath(dataDir), { force: true });
38
+ } catch {
39
+ }
40
+ }
41
+ function clearPendingInviteIfConsumed(dataDir, credsExist) {
42
+ if (!existsSync(pendingInvitePath(dataDir))) return false;
43
+ if (!credsExist(dataDir)) return false;
44
+ clearPendingInvite(dataDir);
45
+ return true;
46
+ }
47
+ var DIR_MODE2, FILE_MODE2, PENDING_INVITE_FILE;
48
+ var init_pending_invite = __esm({
49
+ "src/pending-invite.ts"() {
50
+ "use strict";
51
+ DIR_MODE2 = 448;
52
+ FILE_MODE2 = 384;
53
+ PENDING_INVITE_FILE = "pending-invite";
54
+ }
55
+ });
56
+
57
+ // src/log.ts
58
+ import { format } from "node:util";
59
+ function stamp(message) {
60
+ const ts2 = (/* @__PURE__ */ new Date()).toISOString();
61
+ return message.split("\n").map((line) => `${ts2} ${line}`).join("\n");
62
+ }
63
+ function logLine(...args) {
64
+ console.error(stamp(format(...args)));
65
+ }
66
+ function printLine(text) {
67
+ console.log(text);
68
+ }
69
+ var init_log = __esm({
70
+ "src/log.ts"() {
71
+ "use strict";
72
+ }
73
+ });
74
+
12
75
  // src/config.ts
13
- import { existsSync, readFileSync as readFileSync2, mkdirSync, chmodSync } from "node:fs";
14
- import { join as join5, resolve as resolve2, sep, dirname } from "node:path";
76
+ import { existsSync as existsSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "node:fs";
77
+ import { join as join6, resolve as resolve2, sep, dirname } from "node:path";
78
+ import { tmpdir } from "node:os";
79
+ function isEphemeralEntrypoint(entrypoint) {
80
+ return entrypoint.includes("/_npx/") || entrypoint.includes("/dlx-") || entrypoint.includes("/.bun/install/cache/") || entrypoint.startsWith(tmpdir() + sep);
81
+ }
82
+ function assertInstallableEntrypoint(entrypoint) {
83
+ if (isEphemeralEntrypoint(entrypoint)) {
84
+ throw new Error(
85
+ `cannot install a background service from an ephemeral package cache (${entrypoint}) \u2014 it is deleted on cleanup and the service would silently die later. Install globally first:
86
+ npm i -g @agentvault/claude-bridge && agentvault-claude-bridge install`
87
+ );
88
+ }
89
+ }
15
90
  function hasRecoverableBackup(dataDir) {
16
91
  try {
17
- const parsed = JSON.parse(readFileSync2(join5(dataDir, BACKUP_FILE), "utf-8"));
92
+ const parsed = JSON.parse(readFileSync3(join6(dataDir, BACKUP_FILE), "utf-8"));
18
93
  return !!(parsed && parsed.deviceId && parsed.deviceJwt && parsed.sessions && Object.keys(parsed.sessions).length > 0);
19
94
  } catch {
20
95
  return false;
21
96
  }
22
97
  }
98
+ function readPersistedDeviceId(dataDir) {
99
+ for (const f7 of [...CRED_FILES, BACKUP_FILE]) {
100
+ try {
101
+ const parsed = JSON.parse(readFileSync3(join6(dataDir, f7), "utf-8"));
102
+ const id = parsed?.deviceId;
103
+ if (typeof id === "string" && id) return id;
104
+ } catch {
105
+ }
106
+ }
107
+ return null;
108
+ }
23
109
  function hasPersistedCreds(dataDir) {
24
- if (CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)))) return true;
110
+ if (CRED_FILES.some((f7) => existsSync2(join6(dataDir, f7)))) return true;
25
111
  return hasRecoverableBackup(dataDir);
26
112
  }
27
113
  function slugify2(name) {
28
114
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
29
115
  }
30
116
  function resolveDataDir(env) {
31
- if (env.AV_DATA_DIR) return { dataDir: env.AV_DATA_DIR, source: "explicit" };
32
- const base = join5(env.HOME ?? "", ".agentvault", "claude-room-bridge");
33
- const perAgent = join5(base, slugify2(env.AV_AGENT_NAME ?? "claude"));
34
- if (hasPersistedCreds(perAgent)) return { dataDir: perAgent, source: "per-agent" };
35
- if (hasPersistedCreds(base)) return { dataDir: base, source: "legacy" };
36
- return { dataDir: perAgent, source: "per-agent" };
117
+ const supplied = env.AV_AGENT_NAME?.trim();
118
+ const agentName = supplied || LEGACY_DEFAULT_AGENT_NAME;
119
+ const agentNameSource = supplied ? "supplied" : "defaulted";
120
+ if (env.AV_DATA_DIR)
121
+ return { dataDir: env.AV_DATA_DIR, source: "explicit", agentName, agentNameSource };
122
+ const base = join6(env.HOME ?? "", ".agentvault", "claude-room-bridge");
123
+ const perAgent = join6(base, slugify2(agentName));
124
+ if (hasPersistedCreds(perAgent))
125
+ return { dataDir: perAgent, source: "per-agent", agentName, agentNameSource };
126
+ if (hasPersistedCreds(base))
127
+ return { dataDir: base, source: "legacy", agentName, agentNameSource };
128
+ if (!supplied) {
129
+ throw new Error(
130
+ `AV_AGENT_NAME is required \u2014 it names WHICH agent this bridge runs as, and therefore where its credentials live. No credentials were found at ${perAgent} or ${base}, so there is no existing install to infer a name from. Re-run with AV_AGENT_NAME=<agent-name> set.`
131
+ );
132
+ }
133
+ return { dataDir: perAgent, source: "per-agent", agentName, agentNameSource };
37
134
  }
38
135
  function loadConfig(env, argv = []) {
39
- const { dataDir, source: dataDirSource } = resolveDataDir(env);
40
- const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || "";
41
- const workspaceDir = env.AV_WORKSPACE_DIR || join5(dirname(dataDir), "workspaces", slugify2(env.AV_AGENT_NAME ?? "claude"));
136
+ const { dataDir, source: dataDirSource, agentName, agentNameSource } = resolveDataDir(env);
137
+ if (agentNameSource === "defaulted") {
138
+ logLine(
139
+ `[config] AV_AGENT_NAME was not set \u2014 running as "${agentName}" from the existing credentials at ${dataDir}. Set AV_AGENT_NAME to name this agent explicitly; a future install will require it.`
140
+ );
141
+ }
142
+ const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || readPendingInvite(dataDir) || "";
143
+ const workspaceDir = env.AV_WORKSPACE_DIR || join6(dirname(dataDir), "workspaces", slugify2(agentName));
42
144
  const wsReal = resolve2(workspaceDir);
43
145
  const ddReal = resolve2(dataDir);
44
146
  if (wsReal === ddReal || wsReal.startsWith(ddReal + sep) || ddReal.startsWith(wsReal + sep)) {
@@ -47,8 +149,8 @@ function loadConfig(env, argv = []) {
47
149
  );
48
150
  }
49
151
  try {
50
- mkdirSync(workspaceDir, { recursive: true, mode: 448 });
51
- chmodSync(workspaceDir, 448);
152
+ mkdirSync2(workspaceDir, { recursive: true, mode: 448 });
153
+ chmodSync2(workspaceDir, 448);
52
154
  } catch (e7) {
53
155
  throw new Error(
54
156
  `Failed to create/secure the agent workspace at ${workspaceDir} (override with AV_WORKSPACE_DIR): ${e7.message}`
@@ -83,8 +185,10 @@ function loadConfig(env, argv = []) {
83
185
  apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
84
186
  // Identity shown to the agent's own session. Set this to the agent's
85
187
  // AgentVault name via AV_AGENT_NAME (the native connect command passes it).
86
- // The neutral default avoids impersonating any specific named agent when unset.
87
- agentName: env.AV_AGENT_NAME ?? "Claude",
188
+ // Resolved in resolveDataDir so the name, the data dir and the workspace dir
189
+ // can never disagree about who this agent is (#921/#927).
190
+ agentName,
191
+ agentNameSource,
88
192
  roomFilter: env.AV_ROOM_ID || void 0,
89
193
  model: env.AV_CLAUDE_MODEL || void 0,
90
194
  systemPrompt: env.AV_SYSTEM_PROMPT || void 0,
@@ -94,12 +198,15 @@ function loadConfig(env, argv = []) {
94
198
  osIsolated
95
199
  };
96
200
  }
97
- var CRED_FILES, BACKUP_FILE;
201
+ var CRED_FILES, BACKUP_FILE, LEGACY_DEFAULT_AGENT_NAME;
98
202
  var init_config = __esm({
99
203
  "src/config.ts"() {
100
204
  "use strict";
205
+ init_pending_invite();
206
+ init_log();
101
207
  CRED_FILES = ["agentvault.json", "secure-channel.json"];
102
208
  BACKUP_FILE = "agentvault.json.bak";
209
+ LEGACY_DEFAULT_AGENT_NAME = "Claude";
103
210
  }
104
211
  });
105
212
 
@@ -227,6 +334,37 @@ ${envEntries}
227
334
  </plist>
228
335
  `;
229
336
  }
337
+ function parsePlistEnv(xml) {
338
+ const out = {};
339
+ const keyIdx = xml.indexOf("<key>EnvironmentVariables</key>");
340
+ if (keyIdx < 0) return out;
341
+ const open = xml.indexOf("<dict>", keyIdx);
342
+ if (open < 0) return out;
343
+ let depth = 0;
344
+ let i2 = open;
345
+ let close = -1;
346
+ const tag = /<(\/?)dict>/g;
347
+ tag.lastIndex = open;
348
+ let m6;
349
+ while (m6 = tag.exec(xml)) {
350
+ depth += m6[1] ? -1 : 1;
351
+ if (depth === 0) {
352
+ close = m6.index;
353
+ break;
354
+ }
355
+ i2 = m6.index;
356
+ }
357
+ void i2;
358
+ if (close < 0) return out;
359
+ const body = xml.slice(open, close);
360
+ const pair = /<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/g;
361
+ let p2;
362
+ while (p2 = pair.exec(body)) out[unesc(p2[1])] = unesc(p2[2]);
363
+ return out;
364
+ }
365
+ function unesc(s10) {
366
+ return s10.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
367
+ }
230
368
  var LaunchdBackend;
231
369
  var init_launchd = __esm({
232
370
  "src/service/launchd.ts"() {
@@ -281,6 +419,26 @@ var init_launchd = __esm({
281
419
  const r7 = this.deps.exec("launchctl", ["print", `${this.domain()}/${label}`]);
282
420
  return { loaded: r7.code === 0, detail: r7.stdout };
283
421
  }
422
+ /** #913 item 3 — every installed service whose label starts with `prefix`.
423
+ *
424
+ * Reads the plist directory rather than `launchctl list`, because an ORPHAN
425
+ * is typically installed-but-not-loaded: the revoked device exits 0 and
426
+ * KeepAlive leaves it down. `launchctl list` would not show the very thing
427
+ * doctor exists to find. */
428
+ list(prefix) {
429
+ const dir = join9(this.deps.home, "Library", "LaunchAgents");
430
+ const out = [];
431
+ for (const name of this.deps.readdir(dir)) {
432
+ if (!name.startsWith(prefix) || !name.endsWith(".plist")) continue;
433
+ const path2 = join9(dir, name);
434
+ out.push({
435
+ label: name.slice(0, -".plist".length),
436
+ path: path2,
437
+ env: parsePlistEnv(this.deps.readFile(path2))
438
+ });
439
+ }
440
+ return out.sort((a2, b5) => a2.label.localeCompare(b5.label));
441
+ }
284
442
  };
285
443
  }
286
444
  });
@@ -312,6 +470,24 @@ StandardError=append:${spec.logErr}
312
470
  WantedBy=default.target
313
471
  `;
314
472
  }
473
+ function parseUnitEnv(text) {
474
+ const out = {};
475
+ for (const line of text.split("\n")) {
476
+ const m6 = /^Environment="([\s\S]*)"\s*$/.exec(line.trim());
477
+ if (!m6) continue;
478
+ const eq2 = unq(m6[1]);
479
+ const at = eq2.indexOf("=");
480
+ if (at <= 0) continue;
481
+ out[eq2.slice(0, at)] = eq2.slice(at + 1);
482
+ }
483
+ return out;
484
+ }
485
+ function unq(s10) {
486
+ return s10.replace(/%%/g, "%").replace(
487
+ /\\([\s\S])/g,
488
+ (_4, c4) => c4 === "n" ? "\n" : c4 === "r" ? "\r" : c4 === "t" ? " " : c4
489
+ );
490
+ }
315
491
  var SystemdBackend;
316
492
  var init_systemd = __esm({
317
493
  "src/service/systemd.ts"() {
@@ -353,13 +529,33 @@ var init_systemd = __esm({
353
529
  const active = this.deps.exec("systemctl", ["--user", "is-active", `${label}.service`]);
354
530
  return { loaded: enabled.code === 0, detail: `${enabled.stdout.trim() || "?"} / ${active.stdout.trim() || "?"}` };
355
531
  }
532
+ /** #913 item 3 — every installed unit whose label starts with `prefix`.
533
+ *
534
+ * Reads the unit directory rather than `systemctl list-units`, because an
535
+ * ORPHAN is installed-but-inactive: the revoked device exits 0, which
536
+ * SuccessExitStatus=0 treats as success, so it is never restarted. A
537
+ * running-units listing would not show the thing doctor exists to find. */
538
+ list(prefix) {
539
+ const dir = join10(this.deps.home, ".config", "systemd", "user");
540
+ const out = [];
541
+ for (const name of this.deps.readdir(dir)) {
542
+ if (!name.startsWith(prefix) || !name.endsWith(".service")) continue;
543
+ const path2 = join10(dir, name);
544
+ out.push({
545
+ label: name.slice(0, -".service".length),
546
+ path: path2,
547
+ env: parseUnitEnv(this.deps.readFile(path2))
548
+ });
549
+ }
550
+ return out.sort((a2, b5) => a2.label.localeCompare(b5.label));
551
+ }
356
552
  };
357
553
  }
358
554
  });
359
555
 
360
556
  // src/service/backend.ts
361
557
  import { spawnSync } from "node:child_process";
362
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync4, existsSync as existsSync4, chmodSync as chmodSync3 } from "node:fs";
558
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync4, existsSync as existsSync4, chmodSync as chmodSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "node:fs";
363
559
  import { userInfo } from "node:os";
364
560
  function defaultDeps() {
365
561
  return {
@@ -372,9 +568,23 @@ function defaultDeps() {
372
568
  },
373
569
  writeFile: (p2, d10) => writeFileSync3(p2, d10),
374
570
  mkdir: (p2) => mkdirSync5(p2, { recursive: true }),
375
- chmod: (p2, m6) => chmodSync3(p2, m6),
571
+ chmod: (p2, m6) => chmodSync4(p2, m6),
376
572
  rm: (p2) => rmSync4(p2, { force: true }),
377
- exists: (p2) => existsSync4(p2)
573
+ exists: (p2) => existsSync4(p2),
574
+ readdir: (p2) => {
575
+ try {
576
+ return readdirSync3(p2);
577
+ } catch {
578
+ return [];
579
+ }
580
+ },
581
+ readFile: (p2) => {
582
+ try {
583
+ return readFileSync6(p2, "utf8");
584
+ } catch {
585
+ return "";
586
+ }
587
+ }
378
588
  };
379
589
  }
380
590
  function selectBackend(platform, deps) {
@@ -397,7 +607,8 @@ var init_backend = __esm({
397
607
  // src/service/subcommand.ts
398
608
  var subcommand_exports = {};
399
609
  __export(subcommand_exports, {
400
- maybeRunServiceSubcommand: () => maybeRunServiceSubcommand
610
+ maybeRunServiceSubcommand: () => maybeRunServiceSubcommand,
611
+ resolveLabel: () => resolveLabel
401
612
  });
402
613
  function requireAgentName(env) {
403
614
  const name = env.AV_AGENT_NAME?.trim();
@@ -429,7 +640,9 @@ function maybeRunServiceSubcommand(argv, env, deps) {
429
640
  const looksLikeFlag = cmd.startsWith("-");
430
641
  if (!looksLikeToken && !looksLikeFlag) {
431
642
  throw new Error(
432
- `unknown subcommand: ${cmd} \u2014 expected one of ${[...SERVICE_CMDS].join(", ")}, an invite token, or no argument at all`
643
+ // "doctor" is dispatched earlier, in index.ts, so it never reaches here.
644
+ // Named anyway: a customer who typed `doctr` should see it offered.
645
+ `unknown subcommand: ${cmd} \u2014 expected one of ${[...SERVICE_CMDS, "doctor"].join(", ")}, an invite token, or no argument at all`
433
646
  );
434
647
  }
435
648
  return false;
@@ -440,11 +653,11 @@ function maybeRunServiceSubcommand(argv, env, deps) {
440
653
  try {
441
654
  if (cmd === "install") {
442
655
  requireAgentName(env);
656
+ assertInstallableEntrypoint(entrypoint);
443
657
  const cfg = loadConfig(env, []);
444
- if (!hasPersistedCreds(cfg.dataDir)) {
445
- throw new Error(
446
- "no credentials found \u2014 enroll first by running the bridge once (AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge), then re-run install"
447
- );
658
+ const firstBoot = !hasPersistedCreds(cfg.dataDir) && !!cfg.inviteToken;
659
+ if (firstBoot) {
660
+ writePendingInvite(cfg.dataDir, cfg.inviteToken);
448
661
  }
449
662
  const spec = buildServiceSpec(cfg, {
450
663
  entrypoint,
@@ -455,7 +668,11 @@ function maybeRunServiceSubcommand(argv, env, deps) {
455
668
  path: env.PATH ?? ""
456
669
  });
457
670
  backend.install(spec);
458
- log(`installed service ${spec.label} \u2014 logs: ${spec.logOut}`);
671
+ log(`installed service ${spec.label} \u2014 logs: ${spec.logErr}`);
672
+ if (firstBoot) {
673
+ log("now APPROVE the device in AgentVault \u2014 the service is waiting to enrol");
674
+ log("if it does not connect within a minute, the log above says why");
675
+ }
459
676
  log(
460
677
  `check it with: AV_AGENT_NAME=${cfg.agentName} agentvault-claude-bridge status`
461
678
  );
@@ -487,14 +704,324 @@ var init_subcommand = __esm({
487
704
  "use strict";
488
705
  init_config();
489
706
  init_spec();
707
+ init_pending_invite();
490
708
  init_backend();
491
709
  SERVICE_CMDS = /* @__PURE__ */ new Set(["install", "uninstall", "restart", "status", "logs"]);
492
710
  }
493
711
  });
494
712
 
713
+ // src/service/doctor.ts
714
+ import { basename as basename2 } from "node:path";
715
+ async function buildDoctorReport(d10) {
716
+ const installed = d10.labelPrefixes.flatMap((p2) => d10.backend.list(p2));
717
+ const services = [];
718
+ for (const s10 of installed) services.push(await inspect(s10, d10));
719
+ const claimed = new Set(installed.map((s10) => s10.env.AV_DATA_DIR).filter(Boolean));
720
+ const unsupervised = [];
721
+ for (const dataDir of d10.credDirs()) {
722
+ if (claimed.has(dataDir)) continue;
723
+ unsupervised.push(await inspectUnsupervised(dataDir, d10));
724
+ }
725
+ return {
726
+ labelPrefixes: d10.labelPrefixes,
727
+ services,
728
+ unsupervised,
729
+ notChecked: d10.otherLabels()
730
+ };
731
+ }
732
+ async function inspect(s10, d10) {
733
+ const agentName = s10.env.AV_AGENT_NAME ?? "";
734
+ const dataDir = s10.env.AV_DATA_DIR ?? "";
735
+ let loaded = false;
736
+ try {
737
+ loaded = d10.backend.status(s10.label).loaded;
738
+ } catch {
739
+ }
740
+ const base = { label: s10.label, agentName, dataDir, loaded, deviceId: "" };
741
+ if (!dataDir) {
742
+ return {
743
+ ...base,
744
+ verdict: "WARN",
745
+ detail: "the service definition bakes no AV_DATA_DIR, so doctor cannot find its credentials and cannot check its device"
746
+ };
747
+ }
748
+ const creds = d10.readCreds(dataDir);
749
+ if (!creds?.deviceId) {
750
+ return {
751
+ ...base,
752
+ verdict: "WARN",
753
+ detail: `no credentials at ${dataDir} \u2014 installed, but this agent never enrolled`
754
+ };
755
+ }
756
+ const deviceId = creds.deviceId;
757
+ let probe;
758
+ try {
759
+ probe = await d10.fetchStatus(d10.apiUrl, deviceId);
760
+ } catch (e7) {
761
+ return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e7.message}` };
762
+ }
763
+ if (probe.inconclusive) {
764
+ return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
765
+ }
766
+ if (probe.gone) {
767
+ return {
768
+ ...base,
769
+ deviceId,
770
+ verdict: "FAIL",
771
+ detail: "the device no longer exists \u2014 this service is an orphan",
772
+ fix: d10.uninstallCommand(agentName)
773
+ };
774
+ }
775
+ switch (probe.status) {
776
+ case "ACTIVE":
777
+ return { ...base, deviceId, verdict: "PASS", detail: "device ACTIVE" };
778
+ case "PENDING":
779
+ return {
780
+ ...base,
781
+ deviceId,
782
+ verdict: "WARN",
783
+ detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
784
+ };
785
+ case "REVOKED":
786
+ return {
787
+ ...base,
788
+ deviceId,
789
+ verdict: "FAIL",
790
+ detail: "the device was REVOKED \u2014 this service is an orphan",
791
+ fix: d10.uninstallCommand(agentName)
792
+ };
793
+ default:
794
+ return {
795
+ ...base,
796
+ deviceId,
797
+ verdict: "SKIP",
798
+ detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
799
+ };
800
+ }
801
+ }
802
+ async function inspectUnsupervised(dataDir, d10) {
803
+ const agentName = basename2(dataDir);
804
+ const base = { dataDir, agentName, deviceId: "" };
805
+ const deviceId = d10.readCreds(dataDir)?.deviceId;
806
+ if (!deviceId) {
807
+ return { ...base, verdict: "WARN", detail: "credentials present but they hold no device id" };
808
+ }
809
+ let probe;
810
+ try {
811
+ probe = await d10.fetchStatus(d10.apiUrl, deviceId);
812
+ } catch (e7) {
813
+ return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e7.message}` };
814
+ }
815
+ if (probe.inconclusive) {
816
+ return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
817
+ }
818
+ if (probe.gone) {
819
+ return {
820
+ ...base,
821
+ deviceId,
822
+ verdict: "WARN",
823
+ detail: "the device no longer exists \u2014 these are leftover credentials for a deleted agent"
824
+ };
825
+ }
826
+ if (probe.status === "ACTIVE") {
827
+ return {
828
+ ...base,
829
+ deviceId,
830
+ verdict: "WARN",
831
+ detail: "the device is ACTIVE but nothing supervises it \u2014 this agent is not running",
832
+ fix: d10.installCommand(agentName)
833
+ };
834
+ }
835
+ if (probe.status === "PENDING") {
836
+ return {
837
+ ...base,
838
+ deviceId,
839
+ verdict: "WARN",
840
+ detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
841
+ };
842
+ }
843
+ if (probe.status === "REVOKED") {
844
+ return {
845
+ ...base,
846
+ deviceId,
847
+ verdict: "WARN",
848
+ detail: "the device was REVOKED \u2014 these are leftover credentials"
849
+ };
850
+ }
851
+ return {
852
+ ...base,
853
+ deviceId,
854
+ verdict: "SKIP",
855
+ detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
856
+ };
857
+ }
858
+ function formatDoctorReport(r7) {
859
+ const out = [];
860
+ out.push(`Services installed for this bridge (${r7.labelPrefixes.map((p2) => p2 + "*").join(", ")})`);
861
+ if (r7.services.length === 0) {
862
+ out.push(` No services installed for this bridge on this machine.`);
863
+ }
864
+ for (const s10 of r7.services) {
865
+ out.push(
866
+ ` ${s10.verdict.padEnd(PAD)}${s10.label} [${s10.agentName || "unnamed"}] ${s10.loaded ? "loaded" : "not loaded"} \u2014 ${s10.detail}`
867
+ );
868
+ if (s10.fix) out.push(` ${"".padEnd(PAD)}fix: ${s10.fix}`);
869
+ }
870
+ if (r7.unsupervised.length) {
871
+ out.push("");
872
+ out.push("Credentials on disk that no installed service points at");
873
+ for (const u2 of r7.unsupervised) {
874
+ out.push(` ${u2.verdict.padEnd(PAD)}${u2.agentName} \u2014 ${u2.detail}`);
875
+ out.push(` ${"".padEnd(PAD)}${u2.dataDir}`);
876
+ if (u2.fix) out.push(` ${"".padEnd(PAD)}fix: ${u2.fix}`);
877
+ }
878
+ }
879
+ if (r7.notChecked.length) {
880
+ out.push("");
881
+ out.push("Found on this machine, NOT checked by this command");
882
+ for (const l10 of r7.notChecked) out.push(` ${l10}`);
883
+ out.push(
884
+ " These belong to other AgentVault agent families. This command can only read the credentials of its own, so it says nothing about their health."
885
+ );
886
+ }
887
+ const counts = { PASS: 0, WARN: 0, FAIL: 0, SKIP: 0 };
888
+ for (const s10 of r7.services) counts[s10.verdict]++;
889
+ for (const u2 of r7.unsupervised) counts[u2.verdict]++;
890
+ out.push("");
891
+ out.push(
892
+ `Summary: ${counts.PASS} PASS, ${counts.WARN} WARN, ${counts.FAIL} FAIL, ${counts.SKIP} SKIP` + (counts.SKIP ? " (SKIP = could not check, NOT a problem found)" : "")
893
+ );
894
+ if (counts.FAIL) {
895
+ out.push(
896
+ "`uninstall` removes the SERVICE only \u2014 your credentials stay on disk, so a mistake here costs a re-install, not an identity."
897
+ );
898
+ }
899
+ return out;
900
+ }
901
+ var PAD;
902
+ var init_doctor = __esm({
903
+ "src/service/doctor.ts"() {
904
+ "use strict";
905
+ PAD = 6;
906
+ }
907
+ });
908
+
909
+ // src/service/doctor-cli.ts
910
+ var doctor_cli_exports = {};
911
+ __export(doctor_cli_exports, {
912
+ fetchDeviceStatus: () => fetchDeviceStatus,
913
+ maybeRunDoctorSubcommand: () => maybeRunDoctorSubcommand,
914
+ realDoctorDeps: () => realDoctorDeps
915
+ });
916
+ import { join as join12 } from "node:path";
917
+ import { readdirSync as readdirSync5 } from "node:fs";
918
+ async function fetchDeviceStatus(apiUrl, deviceId, doFetch = fetch) {
919
+ const res = await doFetch(`${apiUrl}/api/v1/devices/${deviceId}/status`, {
920
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
921
+ });
922
+ if (res.status === 404) {
923
+ try {
924
+ await res.json();
925
+ } catch {
926
+ return {
927
+ inconclusive: `${apiUrl} answered 404 with a non-JSON body \u2014 check AV_API_URL points at the API`
928
+ };
929
+ }
930
+ return { gone: true };
931
+ }
932
+ if (res.status === 429) return { inconclusive: "rate limited by the API (429)" };
933
+ if (!res.ok) return { inconclusive: `the API answered ${res.status}` };
934
+ let body;
935
+ try {
936
+ body = await res.json();
937
+ } catch {
938
+ return { inconclusive: `${apiUrl} answered 200 with a non-JSON body \u2014 check AV_API_URL` };
939
+ }
940
+ return { status: body?.status };
941
+ }
942
+ function listCredDirs(base) {
943
+ const out = [];
944
+ if (hasPersistedCreds(base)) out.push(base);
945
+ let names;
946
+ try {
947
+ names = readdirSync5(base);
948
+ } catch {
949
+ return out;
950
+ }
951
+ for (const name of names) {
952
+ const dir = join12(base, name);
953
+ if (hasPersistedCreds(dir)) out.push(dir);
954
+ }
955
+ return out;
956
+ }
957
+ function otherFamilyLabels(backend) {
958
+ const out = [];
959
+ for (const p2 of OTHER_FAMILY_PREFIXES) {
960
+ try {
961
+ for (const s10 of backend.list(p2)) out.push(s10.label);
962
+ } catch {
963
+ }
964
+ }
965
+ return out.sort();
966
+ }
967
+ function realDoctorDeps(env) {
968
+ const backend = selectBackend(process.platform);
969
+ const base = join12(env.HOME ?? "", ".agentvault", "claude-room-bridge");
970
+ return {
971
+ backend,
972
+ labelPrefixes: [LABEL_PREFIX, LEGACY_LABEL_PREFIX],
973
+ base,
974
+ // Same default as config.ts. Read from the env directly rather than through
975
+ // loadConfig, which now REFUSES without AV_AGENT_NAME (#921) — and doctor's
976
+ // whole job is to run when you do not know which agents exist.
977
+ apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
978
+ uninstallCommand: (n10) => `AV_AGENT_NAME=${n10 || "<agent-name>"} agentvault-claude-bridge uninstall`,
979
+ installCommand: (n10) => `AV_AGENT_NAME=${n10 || "<agent-name>"} agentvault-claude-bridge install`,
980
+ readCreds: (dir) => {
981
+ const deviceId = readPersistedDeviceId(dir);
982
+ return deviceId ? { deviceId } : null;
983
+ },
984
+ credDirs: () => listCredDirs(base),
985
+ fetchStatus: (apiUrl, deviceId) => fetchDeviceStatus(apiUrl, deviceId),
986
+ otherLabels: () => otherFamilyLabels(backend)
987
+ };
988
+ }
989
+ async function maybeRunDoctorSubcommand(argv, opts = {}) {
990
+ if (argv[2] !== "doctor") return false;
991
+ const log = opts.log ?? printLine;
992
+ try {
993
+ const report = await buildDoctorReport(opts.deps ?? realDoctorDeps(opts.env ?? process.env));
994
+ for (const line of formatDoctorReport(report)) log(line);
995
+ if (report.services.some((s10) => s10.verdict === "FAIL")) process.exitCode = 1;
996
+ } catch (e7) {
997
+ log(`doctor could not run: ${e7.message}`);
998
+ process.exitCode = 1;
999
+ }
1000
+ return true;
1001
+ }
1002
+ var LABEL_PREFIX, LEGACY_LABEL_PREFIX, OTHER_FAMILY_PREFIXES, PROBE_TIMEOUT_MS;
1003
+ var init_doctor_cli = __esm({
1004
+ "src/service/doctor-cli.ts"() {
1005
+ "use strict";
1006
+ init_backend();
1007
+ init_config();
1008
+ init_log();
1009
+ init_doctor();
1010
+ LABEL_PREFIX = "dev.agentvault.bridge-";
1011
+ LEGACY_LABEL_PREFIX = "com.agentvault.claude-bridge.";
1012
+ OTHER_FAMILY_PREFIXES = [
1013
+ "dev.agentvault.grok-",
1014
+ // grok bridge
1015
+ "ai.hermes.gateway"
1016
+ // hermes, one service per profile
1017
+ ];
1018
+ PROBE_TIMEOUT_MS = 8e3;
1019
+ }
1020
+ });
1021
+
495
1022
  // src/index.ts
496
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
497
- import { join as join11 } from "node:path";
1023
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7 } from "node:fs";
1024
+ import { join as join13 } from "node:path";
498
1025
 
499
1026
  // ../plugin/dist/index.js
500
1027
  import * as nc from "node:crypto";
@@ -98151,6 +98678,7 @@ await init_index();
98151
98678
 
98152
98679
  // src/index.ts
98153
98680
  init_config();
98681
+ init_pending_invite();
98154
98682
 
98155
98683
  // ../../node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
98156
98684
  import { createRequire as $S } from "node:module";
@@ -119451,18 +119979,18 @@ import { realpathSync as realpathSync2 } from "node:fs";
119451
119979
  import { resolve as resolve3, dirname as dirname2, basename, sep as sep2, isAbsolute } from "node:path";
119452
119980
 
119453
119981
  // src/host-trust.ts
119454
- import { mkdirSync as mkdirSync3, writeFileSync, rmSync as rmSync2, readdirSync as readdirSync2, lstatSync as lstatSync2, readFileSync as readFileSync4, chmodSync as chmodSync2 } from "node:fs";
119455
- import { join as join6 } from "node:path";
119982
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, rmSync as rmSync3, readdirSync as readdirSync2, lstatSync as lstatSync2, readFileSync as readFileSync5, chmodSync as chmodSync3 } from "node:fs";
119983
+ import { join as join7 } from "node:path";
119456
119984
  import { homedir, hostname as hostname3 } from "node:os";
119457
119985
  var TRUST_SUBDIR = "host-trust";
119458
- var DIR_MODE2 = 448;
119459
- var FILE_MODE2 = 384;
119986
+ var DIR_MODE3 = 448;
119987
+ var FILE_MODE3 = 384;
119460
119988
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
119461
119989
  var IDENTITY_FILES = ["agentvault.json", "secure-channel.json", "agentvault.json.bak"];
119462
119990
  var HostTrustError = class extends Error {
119463
119991
  };
119464
119992
  function trustDir(root3) {
119465
- return join6(root3 ?? join6(homedir(), ".agentvault"), TRUST_SUBDIR);
119993
+ return join7(root3 ?? join7(homedir(), ".agentvault"), TRUST_SUBDIR);
119466
119994
  }
119467
119995
  function sanitize(deviceId) {
119468
119996
  if (typeof deviceId !== "string" || !ID_RE.test(deviceId)) {
@@ -119473,7 +120001,7 @@ function sanitize(deviceId) {
119473
120001
  function isTrusted(deviceId, root3) {
119474
120002
  try {
119475
120003
  if (typeof deviceId !== "string" || !ID_RE.test(deviceId)) return false;
119476
- const p2 = join6(trustDir(root3), deviceId);
120004
+ const p2 = join7(trustDir(root3), deviceId);
119477
120005
  return lstatSync2(p2).isFile();
119478
120006
  } catch {
119479
120007
  return false;
@@ -119482,26 +120010,26 @@ function isTrusted(deviceId, root3) {
119482
120010
  function grant(deviceId, root3) {
119483
120011
  const id = sanitize(deviceId);
119484
120012
  const dir = trustDir(root3);
119485
- const p2 = join6(dir, id);
119486
- mkdirSync3(dir, { recursive: true, mode: DIR_MODE2 });
119487
- writeFileSync(
120013
+ const p2 = join7(dir, id);
120014
+ mkdirSync4(dir, { recursive: true, mode: DIR_MODE3 });
120015
+ writeFileSync2(
119488
120016
  p2,
119489
120017
  `granted_at=${(/* @__PURE__ */ new Date()).toISOString()} host=${hostname3()}
119490
120018
  `,
119491
- { mode: FILE_MODE2 }
120019
+ { mode: FILE_MODE3 }
119492
120020
  );
119493
- chmodSync2(dir, DIR_MODE2);
119494
- chmodSync2(p2, FILE_MODE2);
120021
+ chmodSync3(dir, DIR_MODE3);
120022
+ chmodSync3(p2, FILE_MODE3);
119495
120023
  }
119496
120024
  function revoke(deviceId, root3) {
119497
120025
  const id = sanitize(deviceId);
119498
- const p2 = join6(trustDir(root3), id);
120026
+ const p2 = join7(trustDir(root3), id);
119499
120027
  try {
119500
120028
  if (!lstatSync2(p2).isFile()) return false;
119501
120029
  } catch {
119502
120030
  return false;
119503
120031
  }
119504
- rmSync2(p2, { force: true });
120032
+ rmSync3(p2, { force: true });
119505
120033
  return true;
119506
120034
  }
119507
120035
  function listTrusted(root3) {
@@ -119516,7 +120044,7 @@ function listTrusted(root3) {
119516
120044
  function readDeviceId(dataDir) {
119517
120045
  for (const f7 of IDENTITY_FILES) {
119518
120046
  try {
119519
- const parsed = JSON.parse(readFileSync4(join6(dataDir, f7), "utf-8"));
120047
+ const parsed = JSON.parse(readFileSync5(join7(dataDir, f7), "utf-8"));
119520
120048
  if (parsed?.deviceId && ID_RE.test(parsed.deviceId)) return parsed.deviceId;
119521
120049
  } catch {
119522
120050
  }
@@ -119526,17 +120054,8 @@ function readDeviceId(dataDir) {
119526
120054
  );
119527
120055
  }
119528
120056
 
119529
- // src/log.ts
119530
- import { format } from "node:util";
119531
- function stamp(message) {
119532
- const ts2 = (/* @__PURE__ */ new Date()).toISOString();
119533
- return message.split("\n").map((line) => `${ts2} ${line}`).join("\n");
119534
- }
119535
- function logLine(...args) {
119536
- console.error(stamp(format(...args)));
119537
- }
119538
-
119539
120057
  // src/worker-permission.ts
120058
+ init_log();
119540
120059
  var PATH_FIELDS = ["file_path", "path", "notebook_path"];
119541
120060
  function canonical(p2) {
119542
120061
  const abs = resolve3(p2);
@@ -133462,6 +133981,7 @@ function date7(params) {
133462
133981
  config2(en_default3());
133463
133982
 
133464
133983
  // src/session.ts
133984
+ init_log();
133465
133985
  function makeRoomSayTool(onSay) {
133466
133986
  return bs(
133467
133987
  "say",
@@ -134072,9 +134592,36 @@ var ArmingState = class {
134072
134592
  }
134073
134593
  };
134074
134594
 
134595
+ // src/service/self-uninstall.ts
134596
+ init_subcommand();
134597
+ init_backend();
134598
+ init_log();
134599
+ var DEVICE_IS_GONE = "device_revoked";
134600
+ function selfUninstallOnTerminal(reason, env, deps = {}) {
134601
+ if (reason !== DEVICE_IS_GONE) return false;
134602
+ const log = deps.log ?? ((m6) => logLine("[bridge] " + m6));
134603
+ let label = "";
134604
+ try {
134605
+ if (!deps.backend && process.platform !== "darwin" && process.platform !== "linux") return false;
134606
+ const backend = deps.backend ?? selectBackend(process.platform);
134607
+ label = resolveLabel(backend, env);
134608
+ if (!backend.status(label).loaded) return false;
134609
+ backend.uninstall(label);
134610
+ log(
134611
+ `device was revoked \u2014 removed the supervised service ${label} so it does not sit installed on this machine pointing at an agent that no longer exists. Your credentials are still on disk and were NOT touched; re-install with a fresh invite token from AgentVault to reconnect.`
134612
+ );
134613
+ return true;
134614
+ } catch (e7) {
134615
+ log(
134616
+ `device was revoked, but the supervised service ${label || "for this agent"} could not be removed: ${e7.message}. Remove it by hand with: AV_AGENT_NAME=<agent-name> agentvault-claude-bridge uninstall`
134617
+ );
134618
+ return false;
134619
+ }
134620
+ }
134621
+
134075
134622
  // src/approve-cli.ts
134076
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync as rmSync3, existsSync as existsSync3 } from "node:fs";
134077
- import { join as join7 } from "node:path";
134623
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync4, readFileSync as readFileSync7, readdirSync as readdirSync4, rmSync as rmSync5, existsSync as existsSync5 } from "node:fs";
134624
+ import { join as join11 } from "node:path";
134078
134625
  var APPROVALS_SUBDIR = "arm-approvals";
134079
134626
  var ID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
134080
134627
  var ApproveArmError = class extends Error {
@@ -134094,24 +134641,24 @@ function sanitizeRoomId(roomId) {
134094
134641
  function writeApproval(dataDir, requestId, roomId) {
134095
134642
  const id = sanitizeRequestId(requestId);
134096
134643
  const room = sanitizeRoomId(roomId);
134097
- const dir = join7(dataDir, APPROVALS_SUBDIR);
134098
- mkdirSync4(dir, { recursive: true });
134099
- writeFileSync2(join7(dir, id), room);
134644
+ const dir = join11(dataDir, APPROVALS_SUBDIR);
134645
+ mkdirSync6(dir, { recursive: true });
134646
+ writeFileSync4(join11(dir, id), room);
134100
134647
  }
134101
134648
  function drainApprovals(dataDir) {
134102
- const dir = join7(dataDir, APPROVALS_SUBDIR);
134103
- if (!existsSync3(dir)) return [];
134649
+ const dir = join11(dataDir, APPROVALS_SUBDIR);
134650
+ if (!existsSync5(dir)) return [];
134104
134651
  const out = [];
134105
- for (const name of readdirSync3(dir)) {
134652
+ for (const name of readdirSync4(dir)) {
134106
134653
  if (!ID_RE2.test(name)) continue;
134107
134654
  let roomId = "";
134108
134655
  try {
134109
- roomId = readFileSync5(join7(dir, name), "utf8").trim();
134656
+ roomId = readFileSync7(join11(dir, name), "utf8").trim();
134110
134657
  } catch {
134111
134658
  }
134112
134659
  out.push({ requestId: name, roomId: ID_RE2.test(roomId) ? roomId : "" });
134113
134660
  try {
134114
- rmSync3(join7(dir, name), { force: true, recursive: true });
134661
+ rmSync5(join11(dir, name), { force: true, recursive: true });
134115
134662
  } catch {
134116
134663
  }
134117
134664
  }
@@ -134252,11 +134799,13 @@ var ActiveTarget = class {
134252
134799
  function attachLifecycle2(channel, opts = {}) {
134253
134800
  const log = opts.log ?? (() => {
134254
134801
  });
134802
+ const selfUninstall = opts.selfUninstall ?? selfUninstallOnTerminal;
134255
134803
  const onTerminal = opts.onTerminal ?? ((_reason, o10) => process.exit(o10.restart ? 1 : 0));
134256
134804
  const terminal = (reason, restart) => {
134257
134805
  log(
134258
134806
  `terminal condition (${reason}) \u2014 ${restart ? "gave up reconnecting; supervisor may respawn" : "this device must re-enroll or another session has taken over"}. Re-run/re-install with a fresh invite token from AgentVault if this persists.`
134259
134807
  );
134808
+ selfUninstall(reason, process.env);
134260
134809
  onTerminal(reason, { restart });
134261
134810
  };
134262
134811
  channel.on("auth_failed", (e7) => terminal(e7.reason, false));
@@ -134479,7 +135028,12 @@ function wireBridge(channel, session, target, opts = {}) {
134479
135028
  }
134480
135029
 
134481
135030
  // src/index.ts
135031
+ init_log();
134482
135032
  async function main() {
135033
+ const { maybeRunDoctorSubcommand: maybeRunDoctorSubcommand2 } = await Promise.resolve().then(() => (init_doctor_cli(), doctor_cli_exports));
135034
+ if (await maybeRunDoctorSubcommand2(process.argv)) {
135035
+ process.exit(process.exitCode ?? 0);
135036
+ }
134483
135037
  const { dataDir: dataDirForSubcommand } = resolveDataDir(process.env);
134484
135038
  if (maybeRunApproveArmSubcommand(process.argv, dataDirForSubcommand)) {
134485
135039
  process.exit(process.exitCode ?? 0);
@@ -134498,7 +135052,7 @@ async function main() {
134498
135052
  "[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
134499
135053
  );
134500
135054
  }
134501
- logLine(`[bridge] version: ${true ? "0.7.16" : "dev"}`);
135055
+ logLine(`[bridge] version: ${true ? "0.8.0" : "dev"}`);
134502
135056
  logLine(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
134503
135057
  logLine(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
134504
135058
  if (cfg.armRoom) {
@@ -134530,7 +135084,7 @@ async function main() {
134530
135084
  // its default would render "@agentvault/agentvault@0.7.x" — the wrong
134531
135085
  // package name attached to the bridge's version number, which is worse
134532
135086
  // than either alone.
134533
- clientVersion: `@agentvault/claude-bridge@${true ? "0.7.16" : "dev"}`
135087
+ clientVersion: `@agentvault/claude-bridge@${true ? "0.8.0" : "dev"}`
134534
135088
  });
134535
135089
  const agentSystemPrompt = cfg.systemPrompt ?? `You are ${cfg.agentName}, an AI agent on AgentVault. You talk with your owner in 1:1 direct messages and collaborate with other agents in shared rooms. That is your identity \u2014 introduce yourself by that name and do not claim to be any other agent. To speak, call the say tool. In a 1:1 with your owner, reply to what they say. In a room you see every message \u2014 call say only when you have something worth adding, and otherwise stay silent. Keep messages concise.`;
134536
135090
  const deviceJwt = () => {
@@ -134594,9 +135148,9 @@ async function main() {
134594
135148
  `[worker-trap] ${rec.at} ${rec.outcome} after ${rec.ranMs}ms (waited ${rec.waitedMs}ms behind ${rec.queueDepthAtEnqueue}, ${rec.queueDepthAtStart} still queued) replyExpected=${rec.replyExpected}` + (rec.session ? ` composed=${rec.session.composedChars} result=${rec.session.sawResult} said=${rec.session.said}` : "")
134595
135149
  );
134596
135150
  try {
134597
- const dir = join11(cfg.dataDir, "logs");
134598
- mkdirSync6(dir, { recursive: true });
134599
- appendFileSync2(join11(dir, "worker-incidents.jsonl"), JSON.stringify(rec) + "\n");
135151
+ const dir = join13(cfg.dataDir, "logs");
135152
+ mkdirSync7(dir, { recursive: true });
135153
+ appendFileSync2(join13(dir, "worker-incidents.jsonl"), JSON.stringify(rec) + "\n");
134600
135154
  } catch (err) {
134601
135155
  logLine(`[worker-trap] could not persist incident: ${err.message}`);
134602
135156
  }
@@ -134628,6 +135182,11 @@ async function main() {
134628
135182
  attachLifecycle2(channel, {
134629
135183
  log: (m6) => logLine("[bridge] " + m6)
134630
135184
  });
135185
+ channel.on("ready", () => {
135186
+ if (clearPendingInviteIfConsumed(cfg.dataDir, hasPersistedCreds)) {
135187
+ logLine("[bridge] enrolled \u2014 pending invite consumed and removed");
135188
+ }
135189
+ });
134631
135190
  channel.on("state", (s10) => logLine(`[bridge] channel state: ${JSON.stringify(s10)}`));
134632
135191
  channel.on(
134633
135192
  "room_joined",