@agentvault/claude-bridge 0.7.15 → 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;
@@ -439,11 +652,12 @@ function maybeRunServiceSubcommand(argv, env, deps) {
439
652
  const log = deps?.log ?? ((m6) => console.error("[bridge] " + m6));
440
653
  try {
441
654
  if (cmd === "install") {
655
+ requireAgentName(env);
656
+ assertInstallableEntrypoint(entrypoint);
442
657
  const cfg = loadConfig(env, []);
443
- if (!hasPersistedCreds(cfg.dataDir)) {
444
- throw new Error(
445
- "no credentials found \u2014 enroll first by running the bridge once (AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge), then re-run install"
446
- );
658
+ const firstBoot = !hasPersistedCreds(cfg.dataDir) && !!cfg.inviteToken;
659
+ if (firstBoot) {
660
+ writePendingInvite(cfg.dataDir, cfg.inviteToken);
447
661
  }
448
662
  const spec = buildServiceSpec(cfg, {
449
663
  entrypoint,
@@ -454,7 +668,11 @@ function maybeRunServiceSubcommand(argv, env, deps) {
454
668
  path: env.PATH ?? ""
455
669
  });
456
670
  backend.install(spec);
457
- 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
+ }
458
676
  log(
459
677
  `check it with: AV_AGENT_NAME=${cfg.agentName} agentvault-claude-bridge status`
460
678
  );
@@ -486,14 +704,324 @@ var init_subcommand = __esm({
486
704
  "use strict";
487
705
  init_config();
488
706
  init_spec();
707
+ init_pending_invite();
489
708
  init_backend();
490
709
  SERVICE_CMDS = /* @__PURE__ */ new Set(["install", "uninstall", "restart", "status", "logs"]);
491
710
  }
492
711
  });
493
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
+
494
1022
  // src/index.ts
495
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6 } from "node:fs";
496
- 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";
497
1025
 
498
1026
  // ../plugin/dist/index.js
499
1027
  import * as nc from "node:crypto";
@@ -553,6 +1081,25 @@ var __toESM = (mod4, isNodeMode, target) => (target = mod4 != null ? __create(__
553
1081
  isNodeMode || !mod4 || !mod4.__esModule ? __defProp2(target, "default", { value: mod4, enumerable: true }) : target,
554
1082
  mod4
555
1083
  ));
1084
+ function toEpoch(v22) {
1085
+ if (typeof v22 === "bigint") return v22;
1086
+ if (typeof v22 === "number") {
1087
+ return Number.isFinite(v22) ? BigInt(Math.trunc(v22)) : null;
1088
+ }
1089
+ if (typeof v22 === "string" && /^\d+$/.test(v22)) return BigInt(v22);
1090
+ return null;
1091
+ }
1092
+ function classifyCommitFailure(messageEpoch, groupEpoch) {
1093
+ const msg = toEpoch(messageEpoch);
1094
+ const grp = toEpoch(groupEpoch);
1095
+ if (msg === null || grp === null) return "count";
1096
+ return msg < grp ? "already-applied" : "count";
1097
+ }
1098
+ var init_mls_commit_classify = __esm2({
1099
+ "src/mls-commit-classify.ts"() {
1100
+ "use strict";
1101
+ }
1102
+ });
556
1103
  var __filename;
557
1104
  var __dirname;
558
1105
  var url2;
@@ -63900,7 +64447,7 @@ var init_mls_kp_pool = __esm2({
63900
64447
  }
63901
64448
  });
63902
64449
  function ownIdentity() {
63903
- const v22 = true ? "0.23.22" : FALLBACK;
64450
+ const v22 = true ? "0.23.23" : FALLBACK;
63904
64451
  return `${PACKAGE}@${v22}`;
63905
64452
  }
63906
64453
  function buildClientVersion(override) {
@@ -64812,6 +65359,7 @@ var SecureChannel;
64812
65359
  var init_channel = __esm2({
64813
65360
  async "src/channel.ts"() {
64814
65361
  "use strict";
65362
+ init_mls_commit_classify();
64815
65363
  await init_libsodium_wrappers();
64816
65364
  await init_dist();
64817
65365
  await init_dist();
@@ -65878,7 +66426,7 @@ var init_channel = __esm2({
65878
66426
  */
65879
66427
  sendActivitySpan(spanData) {
65880
66428
  if (!this._ws || this._ws.readyState !== WebSocket2.OPEN) return;
65881
- const pluginVersion = true ? "0.23.22" : "0.0.0-dev";
66429
+ const pluginVersion = true ? "0.23.23" : "0.0.0-dev";
65882
66430
  const agentName = this.config.agentName ?? "Agent";
65883
66431
  const resource = {
65884
66432
  "service.name": "agentvault-agent",
@@ -67788,7 +68336,7 @@ var init_channel = __esm2({
67788
68336
  agentVersion: this.config.agentVersion ?? "0.0.0",
67789
68337
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
67790
68338
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
67791
- pluginVersion: true ? "0.23.22" : "0.0.0-dev"
68339
+ pluginVersion: true ? "0.23.23" : "0.0.0-dev"
67792
68340
  });
67793
68341
  this._telemetryReporter.startAutoFlush(3e4);
67794
68342
  }
@@ -68112,7 +68660,7 @@ var init_channel = __esm2({
68112
68660
  agentVersion: this.config.agentVersion ?? "0.0.0",
68113
68661
  // __AV_VERSION__ is injected by esbuild from package.json at build time.
68114
68662
  // Falls back to "0.0.0-dev" in non-bundled contexts (tests).
68115
- pluginVersion: true ? "0.23.22" : "0.0.0-dev"
68663
+ pluginVersion: true ? "0.23.23" : "0.0.0-dev"
68116
68664
  });
68117
68665
  this._telemetryReporter.startAutoFlush(3e4);
68118
68666
  }
@@ -69518,7 +70066,14 @@ ${messageText}`;
69518
70066
  console.log(`[SecureChannel] MLS commit processed for room ${roomId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
69519
70067
  this._mlsCommitFailCounts.delete(roomId);
69520
70068
  } catch (err) {
69521
- await this._onCommitFailure(roomId, groupId, err, `room ${roomId.slice(0, 8)}`);
70069
+ await this._onCommitFailure(
70070
+ roomId,
70071
+ groupId,
70072
+ err,
70073
+ `room ${roomId.slice(0, 8)}`,
70074
+ data.epoch,
70075
+ mlsGroup.epoch
70076
+ );
69522
70077
  }
69523
70078
  } else {
69524
70079
  this._bufferMlsCommit(groupId, epoch, data);
@@ -69538,7 +70093,14 @@ ${messageText}`;
69538
70093
  console.log(`[SecureChannel] MLS commit processed for A2A ${chId.slice(0, 8)} (epoch=${mlsGroup.epoch})`);
69539
70094
  this._mlsCommitFailCounts.delete(`a2a:${chId}`);
69540
70095
  } catch (err) {
69541
- await this._onCommitFailure(`a2a:${chId}`, groupId, err, `A2A ${chId.slice(0, 8)}`);
70096
+ await this._onCommitFailure(
70097
+ `a2a:${chId}`,
70098
+ groupId,
70099
+ err,
70100
+ `A2A ${chId.slice(0, 8)}`,
70101
+ data.epoch,
70102
+ mlsGroup.epoch
70103
+ );
69542
70104
  }
69543
70105
  } else {
69544
70106
  this._bufferMlsCommit(groupId, epoch, data);
@@ -69624,7 +70186,13 @@ ${messageText}`;
69624
70186
  * Never re-throws. The caller's only handler logs and drops, so throwing here
69625
70187
  * is indistinguishable from swallowing — it just moves the swallow one frame up.
69626
70188
  */
69627
- async _onCommitFailure(groupKey, mlsGroupId, err, label) {
70189
+ async _onCommitFailure(groupKey, mlsGroupId, err, label, messageEpoch, groupEpoch) {
70190
+ if (classifyCommitFailure(messageEpoch, groupEpoch) === "already-applied") {
70191
+ console.debug(
70192
+ `[SecureChannel] Skipping already-applied MLS commit for ${label} (commit epoch ${String(messageEpoch)} is behind group epoch ${String(groupEpoch)})`
70193
+ );
70194
+ return;
70195
+ }
69628
70196
  const count = (this._mlsCommitFailCounts.get(groupKey) ?? 0) + 1;
69629
70197
  this._mlsCommitFailCounts.set(groupKey, count);
69630
70198
  console.error(
@@ -71522,7 +72090,7 @@ ${messageText}`;
71522
72090
  return;
71523
72091
  }
71524
72092
  this._authFailedThisSession = true;
71525
- const authReason = data?.reason ?? "device_revoked";
72093
+ const authReason = data?.reason ?? "unknown";
71526
72094
  console.warn(
71527
72095
  `[SecureChannel] connection_rejected (reason=${data?.reason ?? "unknown"}, retryable=false) \u2014 terminal; surfacing auth_failed`
71528
72096
  );
@@ -98103,13 +98671,14 @@ var init_index = __esm2({
98103
98671
  init_skill_invoker();
98104
98672
  await init_skill_telemetry();
98105
98673
  await init_policy_enforcer();
98106
- VERSION = true ? "0.23.22" : "0.0.0-dev";
98674
+ VERSION = true ? "0.23.23" : "0.0.0-dev";
98107
98675
  }
98108
98676
  });
98109
98677
  await init_index();
98110
98678
 
98111
98679
  // src/index.ts
98112
98680
  init_config();
98681
+ init_pending_invite();
98113
98682
 
98114
98683
  // ../../node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
98115
98684
  import { createRequire as $S } from "node:module";
@@ -119410,18 +119979,18 @@ import { realpathSync as realpathSync2 } from "node:fs";
119410
119979
  import { resolve as resolve3, dirname as dirname2, basename, sep as sep2, isAbsolute } from "node:path";
119411
119980
 
119412
119981
  // src/host-trust.ts
119413
- import { mkdirSync as mkdirSync3, writeFileSync, rmSync as rmSync2, readdirSync as readdirSync2, lstatSync as lstatSync2, readFileSync as readFileSync4, chmodSync as chmodSync2 } from "node:fs";
119414
- 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";
119415
119984
  import { homedir, hostname as hostname3 } from "node:os";
119416
119985
  var TRUST_SUBDIR = "host-trust";
119417
- var DIR_MODE2 = 448;
119418
- var FILE_MODE2 = 384;
119986
+ var DIR_MODE3 = 448;
119987
+ var FILE_MODE3 = 384;
119419
119988
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
119420
119989
  var IDENTITY_FILES = ["agentvault.json", "secure-channel.json", "agentvault.json.bak"];
119421
119990
  var HostTrustError = class extends Error {
119422
119991
  };
119423
119992
  function trustDir(root3) {
119424
- return join6(root3 ?? join6(homedir(), ".agentvault"), TRUST_SUBDIR);
119993
+ return join7(root3 ?? join7(homedir(), ".agentvault"), TRUST_SUBDIR);
119425
119994
  }
119426
119995
  function sanitize(deviceId) {
119427
119996
  if (typeof deviceId !== "string" || !ID_RE.test(deviceId)) {
@@ -119432,7 +120001,7 @@ function sanitize(deviceId) {
119432
120001
  function isTrusted(deviceId, root3) {
119433
120002
  try {
119434
120003
  if (typeof deviceId !== "string" || !ID_RE.test(deviceId)) return false;
119435
- const p2 = join6(trustDir(root3), deviceId);
120004
+ const p2 = join7(trustDir(root3), deviceId);
119436
120005
  return lstatSync2(p2).isFile();
119437
120006
  } catch {
119438
120007
  return false;
@@ -119441,26 +120010,26 @@ function isTrusted(deviceId, root3) {
119441
120010
  function grant(deviceId, root3) {
119442
120011
  const id = sanitize(deviceId);
119443
120012
  const dir = trustDir(root3);
119444
- const p2 = join6(dir, id);
119445
- mkdirSync3(dir, { recursive: true, mode: DIR_MODE2 });
119446
- writeFileSync(
120013
+ const p2 = join7(dir, id);
120014
+ mkdirSync4(dir, { recursive: true, mode: DIR_MODE3 });
120015
+ writeFileSync2(
119447
120016
  p2,
119448
120017
  `granted_at=${(/* @__PURE__ */ new Date()).toISOString()} host=${hostname3()}
119449
120018
  `,
119450
- { mode: FILE_MODE2 }
120019
+ { mode: FILE_MODE3 }
119451
120020
  );
119452
- chmodSync2(dir, DIR_MODE2);
119453
- chmodSync2(p2, FILE_MODE2);
120021
+ chmodSync3(dir, DIR_MODE3);
120022
+ chmodSync3(p2, FILE_MODE3);
119454
120023
  }
119455
120024
  function revoke(deviceId, root3) {
119456
120025
  const id = sanitize(deviceId);
119457
- const p2 = join6(trustDir(root3), id);
120026
+ const p2 = join7(trustDir(root3), id);
119458
120027
  try {
119459
120028
  if (!lstatSync2(p2).isFile()) return false;
119460
120029
  } catch {
119461
120030
  return false;
119462
120031
  }
119463
- rmSync2(p2, { force: true });
120032
+ rmSync3(p2, { force: true });
119464
120033
  return true;
119465
120034
  }
119466
120035
  function listTrusted(root3) {
@@ -119475,7 +120044,7 @@ function listTrusted(root3) {
119475
120044
  function readDeviceId(dataDir) {
119476
120045
  for (const f7 of IDENTITY_FILES) {
119477
120046
  try {
119478
- const parsed = JSON.parse(readFileSync4(join6(dataDir, f7), "utf-8"));
120047
+ const parsed = JSON.parse(readFileSync5(join7(dataDir, f7), "utf-8"));
119479
120048
  if (parsed?.deviceId && ID_RE.test(parsed.deviceId)) return parsed.deviceId;
119480
120049
  } catch {
119481
120050
  }
@@ -119485,17 +120054,8 @@ function readDeviceId(dataDir) {
119485
120054
  );
119486
120055
  }
119487
120056
 
119488
- // src/log.ts
119489
- import { format } from "node:util";
119490
- function stamp(message) {
119491
- const ts2 = (/* @__PURE__ */ new Date()).toISOString();
119492
- return message.split("\n").map((line) => `${ts2} ${line}`).join("\n");
119493
- }
119494
- function logLine(...args) {
119495
- console.error(stamp(format(...args)));
119496
- }
119497
-
119498
120057
  // src/worker-permission.ts
120058
+ init_log();
119499
120059
  var PATH_FIELDS = ["file_path", "path", "notebook_path"];
119500
120060
  function canonical(p2) {
119501
120061
  const abs = resolve3(p2);
@@ -133421,6 +133981,7 @@ function date7(params) {
133421
133981
  config2(en_default3());
133422
133982
 
133423
133983
  // src/session.ts
133984
+ init_log();
133424
133985
  function makeRoomSayTool(onSay) {
133425
133986
  return bs(
133426
133987
  "say",
@@ -134031,9 +134592,36 @@ var ArmingState = class {
134031
134592
  }
134032
134593
  };
134033
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
+
134034
134622
  // src/approve-cli.ts
134035
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, rmSync as rmSync3, existsSync as existsSync3 } from "node:fs";
134036
- 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";
134037
134625
  var APPROVALS_SUBDIR = "arm-approvals";
134038
134626
  var ID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
134039
134627
  var ApproveArmError = class extends Error {
@@ -134053,24 +134641,24 @@ function sanitizeRoomId(roomId) {
134053
134641
  function writeApproval(dataDir, requestId, roomId) {
134054
134642
  const id = sanitizeRequestId(requestId);
134055
134643
  const room = sanitizeRoomId(roomId);
134056
- const dir = join7(dataDir, APPROVALS_SUBDIR);
134057
- mkdirSync4(dir, { recursive: true });
134058
- writeFileSync2(join7(dir, id), room);
134644
+ const dir = join11(dataDir, APPROVALS_SUBDIR);
134645
+ mkdirSync6(dir, { recursive: true });
134646
+ writeFileSync4(join11(dir, id), room);
134059
134647
  }
134060
134648
  function drainApprovals(dataDir) {
134061
- const dir = join7(dataDir, APPROVALS_SUBDIR);
134062
- if (!existsSync3(dir)) return [];
134649
+ const dir = join11(dataDir, APPROVALS_SUBDIR);
134650
+ if (!existsSync5(dir)) return [];
134063
134651
  const out = [];
134064
- for (const name of readdirSync3(dir)) {
134652
+ for (const name of readdirSync4(dir)) {
134065
134653
  if (!ID_RE2.test(name)) continue;
134066
134654
  let roomId = "";
134067
134655
  try {
134068
- roomId = readFileSync5(join7(dir, name), "utf8").trim();
134656
+ roomId = readFileSync7(join11(dir, name), "utf8").trim();
134069
134657
  } catch {
134070
134658
  }
134071
134659
  out.push({ requestId: name, roomId: ID_RE2.test(roomId) ? roomId : "" });
134072
134660
  try {
134073
- rmSync3(join7(dir, name), { force: true, recursive: true });
134661
+ rmSync5(join11(dir, name), { force: true, recursive: true });
134074
134662
  } catch {
134075
134663
  }
134076
134664
  }
@@ -134211,11 +134799,13 @@ var ActiveTarget = class {
134211
134799
  function attachLifecycle2(channel, opts = {}) {
134212
134800
  const log = opts.log ?? (() => {
134213
134801
  });
134802
+ const selfUninstall = opts.selfUninstall ?? selfUninstallOnTerminal;
134214
134803
  const onTerminal = opts.onTerminal ?? ((_reason, o10) => process.exit(o10.restart ? 1 : 0));
134215
134804
  const terminal = (reason, restart) => {
134216
134805
  log(
134217
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.`
134218
134807
  );
134808
+ selfUninstall(reason, process.env);
134219
134809
  onTerminal(reason, { restart });
134220
134810
  };
134221
134811
  channel.on("auth_failed", (e7) => terminal(e7.reason, false));
@@ -134438,7 +135028,12 @@ function wireBridge(channel, session, target, opts = {}) {
134438
135028
  }
134439
135029
 
134440
135030
  // src/index.ts
135031
+ init_log();
134441
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
+ }
134442
135037
  const { dataDir: dataDirForSubcommand } = resolveDataDir(process.env);
134443
135038
  if (maybeRunApproveArmSubcommand(process.argv, dataDirForSubcommand)) {
134444
135039
  process.exit(process.exitCode ?? 0);
@@ -134457,7 +135052,7 @@ async function main() {
134457
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"
134458
135053
  );
134459
135054
  }
134460
- logLine(`[bridge] version: ${true ? "0.7.15" : "dev"}`);
135055
+ logLine(`[bridge] version: ${true ? "0.8.0" : "dev"}`);
134461
135056
  logLine(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
134462
135057
  logLine(`[bridge] workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
134463
135058
  if (cfg.armRoom) {
@@ -134489,7 +135084,7 @@ async function main() {
134489
135084
  // its default would render "@agentvault/agentvault@0.7.x" — the wrong
134490
135085
  // package name attached to the bridge's version number, which is worse
134491
135086
  // than either alone.
134492
- clientVersion: `@agentvault/claude-bridge@${true ? "0.7.15" : "dev"}`
135087
+ clientVersion: `@agentvault/claude-bridge@${true ? "0.8.0" : "dev"}`
134493
135088
  });
134494
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.`;
134495
135090
  const deviceJwt = () => {
@@ -134553,9 +135148,9 @@ async function main() {
134553
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}` : "")
134554
135149
  );
134555
135150
  try {
134556
- const dir = join11(cfg.dataDir, "logs");
134557
- mkdirSync6(dir, { recursive: true });
134558
- 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");
134559
135154
  } catch (err) {
134560
135155
  logLine(`[worker-trap] could not persist incident: ${err.message}`);
134561
135156
  }
@@ -134587,6 +135182,11 @@ async function main() {
134587
135182
  attachLifecycle2(channel, {
134588
135183
  log: (m6) => logLine("[bridge] " + m6)
134589
135184
  });
135185
+ channel.on("ready", () => {
135186
+ if (clearPendingInviteIfConsumed(cfg.dataDir, hasPersistedCreds)) {
135187
+ logLine("[bridge] enrolled \u2014 pending invite consumed and removed");
135188
+ }
135189
+ });
134590
135190
  channel.on("state", (s10) => logLine(`[bridge] channel state: ${JSON.stringify(s10)}`));
134591
135191
  channel.on(
134592
135192
  "room_joined",