@agentvault/grok-bridge 0.3.2 → 0.4.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/bridge.d.ts CHANGED
@@ -136,6 +136,15 @@ export declare function attachLifecycle(channel: LifecycleChannel, opts?: {
136
136
  onTerminal?: (reason: string, o: {
137
137
  restart: boolean;
138
138
  }) => void;
139
+ /** #913 item 2 — remove this agent's supervised service when its device is
140
+ * gone for good. Injected for tests; defaults to the real remover, because
141
+ * an opt-in default is a feature nobody gets.
142
+ *
143
+ * Every terminal reason is offered to it and IT decides. Filtering here as
144
+ * well would put the same rule in two files, and the two would eventually
145
+ * disagree — which is how `session_replaced` (device still alive) would end
146
+ * up removing a healthy install. */
147
+ selfUninstall?: (reason: string, env: NodeJS.ProcessEnv) => boolean;
139
148
  }): void;
140
149
  /**
141
150
  * How a ROOM turn is announced to the model (#366 / #499 Facet A).
package/dist/config.d.ts CHANGED
@@ -1,16 +1,3 @@
1
- /**
2
- * The one-paste install flag: enrol, install the supervised service, then EXIT so
3
- * the customer's terminal comes back.
4
- *
5
- * WIRE LITERAL. `packages/web/lib/framework-install.ts` emits this exact string in
6
- * the connect command. The two packages cannot import each other, so the only
7
- * tripwire is a test on each side that names the other — see config.test.ts
8
- * "freezes the wire literal".
9
- *
10
- * Deliberately "-" prefixed: loadConfig's token slot rejects "-" args, so adding a
11
- * flag can never be mistaken for an invite token.
12
- */
13
- export declare const INSTALL_FLAG = "--install";
14
1
  /** Informational flags. Handled BEFORE loadConfig in index.ts, because asking
15
2
  * "what version is this?" must not require an invite token. Listed here too so
16
3
  * the strict unknown-flag check does not reject them. */
@@ -35,12 +22,38 @@ export declare function parseInfoFlags(argv: string[]): {
35
22
  export declare function isEphemeralEntrypoint(entrypoint: string): boolean;
36
23
  /** Throw unless `entrypoint` is durable enough to bake into a supervised service. */
37
24
  export declare function assertInstallableEntrypoint(entrypoint: string): void;
25
+ /**
26
+ * #913 item 3 — the device id inside this data dir's credentials, or null.
27
+ *
28
+ * Lives HERE, beside CRED_FILES and BACKUP_FILE, because this module already
29
+ * owns the knowledge of which files hold credentials. `doctor` reading those
30
+ * names itself would make a second copy of the list, and the copy that is not
31
+ * next to the constant is the one that goes stale.
32
+ *
33
+ * Checks the same files in the same order as hasPersistedCreds, INCLUDING the
34
+ * .bak: a device_revoked wipe deletes agentvault.json and leaves the backup, so
35
+ * the .bak is exactly where the id lives for the machine most likely to be
36
+ * running doctor.
37
+ *
38
+ * Returns null rather than throwing on missing/unreadable/malformed input — a
39
+ * doctor that dies on one bad file reports nothing about the others.
40
+ */
41
+ export declare function readPersistedDeviceId(dataDir: string): string | null;
38
42
  export declare function hasPersistedCreds(dataDir: string): boolean;
39
43
  export declare function slugify(name: string): string;
40
44
  export type DataDirSource = "explicit" | "per-agent";
45
+ /** Where the agent name came from.
46
+ *
47
+ * #927's test for a safe default is: can a reader of the output tell the input
48
+ * was MISSING? A bare `agentName` cannot answer that — "Grok" reads the same
49
+ * whether the customer typed it or the bridge chose it. This field is what
50
+ * makes the two distinguishable, so callers can SAY which one happened. */
51
+ export type AgentNameSource = "supplied" | "defaulted";
41
52
  export declare function resolveDataDir(env: NodeJS.ProcessEnv): {
42
53
  dataDir: string;
43
54
  source: DataDirSource;
55
+ agentName: string;
56
+ agentNameSource: AgentNameSource;
44
57
  };
45
58
  export interface BridgeConfig {
46
59
  inviteToken: string;
@@ -48,6 +61,8 @@ export interface BridgeConfig {
48
61
  dataDirSource: DataDirSource;
49
62
  apiUrl: string;
50
63
  agentName: string;
64
+ /** Whether AV_AGENT_NAME was given, or the package default was used (#927). */
65
+ agentNameSource: AgentNameSource;
51
66
  roomFilter?: string;
52
67
  /** Per-agent filesystem sandbox. Always set: auto-defaults to a sibling of
53
68
  * the key/data dir unless AV_WORKSPACE_DIR overrides it. */
@@ -78,7 +93,6 @@ export interface BridgeConfig {
78
93
  /** Set by --install: enrol in the foreground, install the supervised service,
79
94
  * then EXIT so the customer's terminal returns. False leaves a bare run
80
95
  * byte-for-byte as it behaves today. */
81
- installAfterEnroll: boolean;
82
96
  /** --version / -v. index.ts answers these before loadConfig runs. */
83
97
  showVersion: boolean;
84
98
  /** --help / -h. Same. */
package/dist/index.js CHANGED
@@ -54,6 +54,24 @@ var init_pending_invite = __esm({
54
54
  }
55
55
  });
56
56
 
57
+ // src/log.ts
58
+ import { format } from "node:util";
59
+ function stamp(message) {
60
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
61
+ return message.split("\n").map((line) => `${ts} ${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
+
57
75
  // src/config.ts
58
76
  import { existsSync as existsSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "node:fs";
59
77
  import { join as join6, resolve as resolve2, sep, dirname } from "node:path";
@@ -71,7 +89,7 @@ function assertInstallableEntrypoint(entrypoint) {
71
89
  if (isEphemeralEntrypoint(entrypoint)) {
72
90
  throw new Error(
73
91
  `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:
74
- npm i -g @agentvault/grok-bridge && agentvault-grok-bridge ` + INSTALL_FLAG
92
+ npm i -g @agentvault/grok-bridge && agentvault-grok-bridge install`
75
93
  );
76
94
  }
77
95
  }
@@ -83,6 +101,17 @@ function hasRecoverableBackup(dataDir) {
83
101
  return false;
84
102
  }
85
103
  }
104
+ function readPersistedDeviceId(dataDir) {
105
+ for (const f2 of [...CRED_FILES, BACKUP_FILE]) {
106
+ try {
107
+ const parsed = JSON.parse(readFileSync3(join6(dataDir, f2), "utf-8"));
108
+ const id = parsed?.deviceId;
109
+ if (typeof id === "string" && id) return id;
110
+ } catch {
111
+ }
112
+ }
113
+ return null;
114
+ }
86
115
  function hasPersistedCreds(dataDir) {
87
116
  if (CRED_FILES.some((f2) => existsSync2(join6(dataDir, f2)))) return true;
88
117
  return hasRecoverableBackup(dataDir);
@@ -91,20 +120,41 @@ function slugify2(name) {
91
120
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
92
121
  }
93
122
  function resolveDataDir(env) {
94
- if (env.AV_DATA_DIR) return { dataDir: env.AV_DATA_DIR, source: "explicit" };
123
+ const supplied = env.AV_AGENT_NAME?.trim();
124
+ const agentName = supplied || LEGACY_DEFAULT_AGENT_NAME;
125
+ const agentNameSource = supplied ? "supplied" : "defaulted";
126
+ if (env.AV_DATA_DIR)
127
+ return { dataDir: env.AV_DATA_DIR, source: "explicit", agentName, agentNameSource };
95
128
  const base = join6(env.HOME ?? "", ".agentvault", "grok-room-bridge");
96
- return { dataDir: join6(base, slugify2(env.AV_AGENT_NAME ?? "grok")), source: "per-agent" };
129
+ const perAgent = join6(base, slugify2(agentName));
130
+ if (!supplied && !hasPersistedCreds(perAgent)) {
131
+ throw new Error(
132
+ `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}, so there is no existing install to infer a name from. Re-run with AV_AGENT_NAME=<agent-name> set.`
133
+ );
134
+ }
135
+ return { dataDir: perAgent, source: "per-agent", agentName, agentNameSource };
97
136
  }
98
137
  function loadConfig(env, argv = []) {
99
- const { dataDir, source: dataDirSource } = resolveDataDir(env);
138
+ const { dataDir, source: dataDirSource, agentName, agentNameSource } = resolveDataDir(env);
139
+ if (agentNameSource === "defaulted") {
140
+ logLine(
141
+ `[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.`
142
+ );
143
+ }
100
144
  const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || readPendingInvite(dataDir) || "";
145
+ if (argv.includes(REMOVED_INSTALL_FLAG)) {
146
+ throw new Error(
147
+ `${REMOVED_INSTALL_FLAG} was removed \u2014 it handed the agent over from a foreground process that could be reaped before the encrypted group finished forming, leaving an agent that was connected and mute (#909). Use the subcommand instead, which lets the service enrol itself:
148
+ npm i -g @agentvault/grok-bridge && AV_AGENT_NAME=<agent-name> AV_INVITE_TOKEN=<token> agentvault-grok-bridge install`
149
+ );
150
+ }
101
151
  const unknownFlags = argv.filter((a2) => a2.startsWith("-") && !KNOWN_FLAGS.has(a2));
102
152
  if (unknownFlags.length) {
103
153
  throw new Error(
104
154
  `unknown option(s): ${unknownFlags.join(", ")} \u2014 supported flags are ${[...KNOWN_FLAGS].join(", ")}`
105
155
  );
106
156
  }
107
- const workspaceDir = env.AV_WORKSPACE_DIR || join6(dirname(dataDir), "workspaces", slugify2(env.AV_AGENT_NAME ?? "grok"));
157
+ const workspaceDir = env.AV_WORKSPACE_DIR || join6(dirname(dataDir), "workspaces", slugify2(agentName));
108
158
  const wsReal = resolve2(workspaceDir);
109
159
  const ddReal = resolve2(dataDir);
110
160
  if (wsReal === ddReal || wsReal.startsWith(ddReal + sep) || ddReal.startsWith(wsReal + sep)) {
@@ -129,17 +179,14 @@ function loadConfig(env, argv = []) {
129
179
  inviteToken,
130
180
  dataDir,
131
181
  dataDirSource,
132
- // One-paste install (--install): enrol, hand off to the supervisor, exit.
133
- // Default false keeps a bare `npx @agentvault/grok-bridge` run identical.
134
- installAfterEnroll: argv.includes(INSTALL_FLAG),
135
182
  ...parseInfoFlags(argv),
136
183
  apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
137
184
  // Identity shown to the agent's own session. Set this to the agent's
138
185
  // AgentVault name via AV_AGENT_NAME (the native connect command passes it).
139
- // Defaults to "Grok" — this package IS the Grok bridge, so an unset
140
- // AV_AGENT_NAME still names the actual model running the session, not a
141
- // placeholder impersonating some other agent.
142
- agentName: env.AV_AGENT_NAME ?? "Grok",
186
+ // Resolved in resolveDataDir so the name, the data dir and the workspace dir
187
+ // can never disagree about who this agent is (#921/#927).
188
+ agentName,
189
+ agentNameSource,
143
190
  roomFilter: env.AV_ROOM_ID || void 0,
144
191
  workspaceDir,
145
192
  grokPath: env.AV_GROK_PATH || void 0,
@@ -149,35 +196,19 @@ function loadConfig(env, argv = []) {
149
196
  systemPrompt: env.AV_SYSTEM_PROMPT || void 0
150
197
  };
151
198
  }
152
- var INSTALL_FLAG, VERSION_FLAGS, HELP_FLAGS, KNOWN_FLAGS, CRED_FILES, BACKUP_FILE;
199
+ var REMOVED_INSTALL_FLAG, VERSION_FLAGS, HELP_FLAGS, KNOWN_FLAGS, CRED_FILES, BACKUP_FILE, LEGACY_DEFAULT_AGENT_NAME;
153
200
  var init_config = __esm({
154
201
  "src/config.ts"() {
155
202
  "use strict";
156
203
  init_pending_invite();
157
- INSTALL_FLAG = "--install";
204
+ init_log();
205
+ REMOVED_INSTALL_FLAG = "--install";
158
206
  VERSION_FLAGS = ["--version", "-v"];
159
207
  HELP_FLAGS = ["--help", "-h"];
160
- KNOWN_FLAGS = /* @__PURE__ */ new Set([INSTALL_FLAG, ...VERSION_FLAGS, ...HELP_FLAGS]);
208
+ KNOWN_FLAGS = /* @__PURE__ */ new Set([...VERSION_FLAGS, ...HELP_FLAGS]);
161
209
  CRED_FILES = ["agentvault.json", "secure-channel.json"];
162
210
  BACKUP_FILE = "agentvault.json.bak";
163
- }
164
- });
165
-
166
- // src/log.ts
167
- import { format } from "node:util";
168
- function stamp(message) {
169
- const ts = (/* @__PURE__ */ new Date()).toISOString();
170
- return message.split("\n").map((line) => `${ts} ${line}`).join("\n");
171
- }
172
- function logLine(...args) {
173
- console.error(stamp(format(...args)));
174
- }
175
- function printLine(text) {
176
- console.log(text);
177
- }
178
- var init_log = __esm({
179
- "src/log.ts"() {
180
- "use strict";
211
+ LEGACY_DEFAULT_AGENT_NAME = "Grok";
181
212
  }
182
213
  });
183
214
 
@@ -211,16 +242,6 @@ function writeLauncher(deps, spec) {
211
242
  deps.writeFile(spec.launcherPath, spec.launcherScript);
212
243
  deps.chmod(spec.launcherPath, 493);
213
244
  }
214
- function parseLauncherEntrypoint(script) {
215
- if (!script) return void 0;
216
- const m2 = /^if n=\$\(command -v .+\); then exec "\$n" (.+) "\$@"; fi$/m.exec(script);
217
- if (!m2) return void 0;
218
- return unSq(m2[1]);
219
- }
220
- function unSq(quoted) {
221
- if (quoted.length < 2 || !quoted.startsWith("'") || !quoted.endsWith("'")) return void 0;
222
- return quoted.slice(1, -1).replace(/'\\''/g, "'");
223
- }
224
245
  var FALLBACK_NODES;
225
246
  var init_launcher = __esm({
226
247
  "src/service/launcher.ts"() {
@@ -314,6 +335,37 @@ ${envEntries}
314
335
  </plist>
315
336
  `;
316
337
  }
338
+ function parsePlistEnv(xml) {
339
+ const out = {};
340
+ const keyIdx = xml.indexOf("<key>EnvironmentVariables</key>");
341
+ if (keyIdx < 0) return out;
342
+ const open = xml.indexOf("<dict>", keyIdx);
343
+ if (open < 0) return out;
344
+ let depth = 0;
345
+ let i2 = open;
346
+ let close = -1;
347
+ const tag = /<(\/?)dict>/g;
348
+ tag.lastIndex = open;
349
+ let m2;
350
+ while (m2 = tag.exec(xml)) {
351
+ depth += m2[1] ? -1 : 1;
352
+ if (depth === 0) {
353
+ close = m2.index;
354
+ break;
355
+ }
356
+ i2 = m2.index;
357
+ }
358
+ void i2;
359
+ if (close < 0) return out;
360
+ const body = xml.slice(open, close);
361
+ const pair = /<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/g;
362
+ let p2;
363
+ while (p2 = pair.exec(body)) out[unesc(p2[1])] = unesc(p2[2]);
364
+ return out;
365
+ }
366
+ function unesc(s2) {
367
+ return s2.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
368
+ }
317
369
  var LaunchdBackend;
318
370
  var init_launchd = __esm({
319
371
  "src/service/launchd.ts"() {
@@ -368,6 +420,26 @@ var init_launchd = __esm({
368
420
  const r2 = this.deps.exec("launchctl", ["print", `${this.domain()}/${label}`]);
369
421
  return { loaded: r2.code === 0, detail: r2.stdout };
370
422
  }
423
+ /** #913 item 3 — every installed service whose label starts with `prefix`.
424
+ *
425
+ * Reads the plist directory rather than `launchctl list`, because an ORPHAN
426
+ * is typically installed-but-not-loaded: the revoked device exits 0 and
427
+ * KeepAlive leaves it down. `launchctl list` would not show the very thing
428
+ * doctor exists to find. */
429
+ list(prefix) {
430
+ const dir = join11(this.deps.home, "Library", "LaunchAgents");
431
+ const out = [];
432
+ for (const name of this.deps.readdir(dir)) {
433
+ if (!name.startsWith(prefix) || !name.endsWith(".plist")) continue;
434
+ const path2 = join11(dir, name);
435
+ out.push({
436
+ label: name.slice(0, -".plist".length),
437
+ path: path2,
438
+ env: parsePlistEnv(this.deps.readFile(path2))
439
+ });
440
+ }
441
+ return out.sort((a2, b2) => a2.label.localeCompare(b2.label));
442
+ }
371
443
  };
372
444
  }
373
445
  });
@@ -399,6 +471,24 @@ StandardError=append:${spec.logErr}
399
471
  WantedBy=default.target
400
472
  `;
401
473
  }
474
+ function parseUnitEnv(text) {
475
+ const out = {};
476
+ for (const line of text.split("\n")) {
477
+ const m2 = /^Environment="([\s\S]*)"\s*$/.exec(line.trim());
478
+ if (!m2) continue;
479
+ const eq = unq(m2[1]);
480
+ const at = eq.indexOf("=");
481
+ if (at <= 0) continue;
482
+ out[eq.slice(0, at)] = eq.slice(at + 1);
483
+ }
484
+ return out;
485
+ }
486
+ function unq(s2) {
487
+ return s2.replace(/%%/g, "%").replace(
488
+ /\\([\s\S])/g,
489
+ (_2, c2) => c2 === "n" ? "\n" : c2 === "r" ? "\r" : c2 === "t" ? " " : c2
490
+ );
491
+ }
402
492
  var SystemdBackend;
403
493
  var init_systemd = __esm({
404
494
  "src/service/systemd.ts"() {
@@ -439,18 +529,33 @@ var init_systemd = __esm({
439
529
  const active = this.deps.exec("systemctl", ["--user", "is-active", `${label}.service`]);
440
530
  return { loaded: enabled.code === 0, detail: `${enabled.stdout.trim() || "?"} / ${active.stdout.trim() || "?"}` };
441
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 = join12(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 = join12(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, b2) => a2.label.localeCompare(b2.label));
551
+ }
442
552
  };
443
553
  }
444
554
  });
445
555
 
446
556
  // src/service/backend.ts
447
- var backend_exports = {};
448
- __export(backend_exports, {
449
- defaultDeps: () => defaultDeps,
450
- selectBackend: () => selectBackend
451
- });
452
557
  import { spawnSync } from "node:child_process";
453
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync2, existsSync as existsSync4, chmodSync as chmodSync5 } from "node:fs";
558
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync2, existsSync as existsSync4, chmodSync as chmodSync5, readdirSync, readFileSync as readFileSync4 } from "node:fs";
454
559
  import { userInfo } from "node:os";
455
560
  function defaultDeps() {
456
561
  return {
@@ -465,7 +570,21 @@ function defaultDeps() {
465
570
  mkdir: (p2) => mkdirSync5(p2, { recursive: true }),
466
571
  chmod: (p2, m2) => chmodSync5(p2, m2),
467
572
  rm: (p2) => rmSync2(p2, { force: true }),
468
- exists: (p2) => existsSync4(p2)
573
+ exists: (p2) => existsSync4(p2),
574
+ readdir: (p2) => {
575
+ try {
576
+ return readdirSync(p2);
577
+ } catch {
578
+ return [];
579
+ }
580
+ },
581
+ readFile: (p2) => {
582
+ try {
583
+ return readFileSync4(p2, "utf8");
584
+ } catch {
585
+ return "";
586
+ }
587
+ }
469
588
  };
470
589
  }
471
590
  function selectBackend(platform, deps) {
@@ -488,6 +607,7 @@ var init_backend = __esm({
488
607
  // src/service/subcommand.ts
489
608
  var subcommand_exports = {};
490
609
  __export(subcommand_exports, {
610
+ labelFor: () => labelFor,
491
611
  maybeRunServiceSubcommand: () => maybeRunServiceSubcommand
492
612
  });
493
613
  function requireAgentName(env) {
@@ -510,7 +630,9 @@ function maybeRunServiceSubcommand(argv, env, deps) {
510
630
  const looksLikeFlag = cmd.startsWith("-");
511
631
  if (!looksLikeToken && !looksLikeFlag) {
512
632
  throw new Error(
513
- `unknown subcommand: ${cmd} \u2014 expected one of ${[...SERVICE_CMDS].join(", ")}, an invite token, or no argument at all`
633
+ // "doctor" is dispatched earlier, in index.ts, so it never reaches here.
634
+ // Named anyway: a customer who typed `doctr` should see it offered.
635
+ `unknown subcommand: ${cmd} \u2014 expected one of ${[...SERVICE_CMDS, "doctor"].join(", ")}, an invite token, or no argument at all`
514
636
  );
515
637
  }
516
638
  return false;
@@ -521,6 +643,7 @@ function maybeRunServiceSubcommand(argv, env, deps) {
521
643
  try {
522
644
  if (cmd === "install") {
523
645
  requireAgentName(env);
646
+ assertInstallableEntrypoint(entrypoint);
524
647
  const cfg = loadConfig(env, []);
525
648
  const firstBoot = !hasPersistedCreds(cfg.dataDir) && !!cfg.inviteToken;
526
649
  if (firstBoot) {
@@ -578,96 +701,313 @@ var init_subcommand = __esm({
578
701
  }
579
702
  });
580
703
 
581
- // src/service/install-on-ready.ts
582
- var install_on_ready_exports = {};
583
- __export(install_on_ready_exports, {
584
- installOnReady: () => installOnReady
704
+ // src/service/doctor.ts
705
+ import { basename } from "node:path";
706
+ async function buildDoctorReport(d2) {
707
+ const installed = d2.labelPrefixes.flatMap((p2) => d2.backend.list(p2));
708
+ const services = [];
709
+ for (const s2 of installed) services.push(await inspect(s2, d2));
710
+ const claimed = new Set(installed.map((s2) => s2.env.AV_DATA_DIR).filter(Boolean));
711
+ const unsupervised = [];
712
+ for (const dataDir of d2.credDirs()) {
713
+ if (claimed.has(dataDir)) continue;
714
+ unsupervised.push(await inspectUnsupervised(dataDir, d2));
715
+ }
716
+ return {
717
+ labelPrefixes: d2.labelPrefixes,
718
+ services,
719
+ unsupervised,
720
+ notChecked: d2.otherLabels()
721
+ };
722
+ }
723
+ async function inspect(s2, d2) {
724
+ const agentName = s2.env.AV_AGENT_NAME ?? "";
725
+ const dataDir = s2.env.AV_DATA_DIR ?? "";
726
+ let loaded = false;
727
+ try {
728
+ loaded = d2.backend.status(s2.label).loaded;
729
+ } catch {
730
+ }
731
+ const base = { label: s2.label, agentName, dataDir, loaded, deviceId: "" };
732
+ if (!dataDir) {
733
+ return {
734
+ ...base,
735
+ verdict: "WARN",
736
+ detail: "the service definition bakes no AV_DATA_DIR, so doctor cannot find its credentials and cannot check its device"
737
+ };
738
+ }
739
+ const creds = d2.readCreds(dataDir);
740
+ if (!creds?.deviceId) {
741
+ return {
742
+ ...base,
743
+ verdict: "WARN",
744
+ detail: `no credentials at ${dataDir} \u2014 installed, but this agent never enrolled`
745
+ };
746
+ }
747
+ const deviceId = creds.deviceId;
748
+ let probe;
749
+ try {
750
+ probe = await d2.fetchStatus(d2.apiUrl, deviceId);
751
+ } catch (e) {
752
+ return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e.message}` };
753
+ }
754
+ if (probe.inconclusive) {
755
+ return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
756
+ }
757
+ if (probe.gone) {
758
+ return {
759
+ ...base,
760
+ deviceId,
761
+ verdict: "FAIL",
762
+ detail: "the device no longer exists \u2014 this service is an orphan",
763
+ fix: d2.uninstallCommand(agentName)
764
+ };
765
+ }
766
+ switch (probe.status) {
767
+ case "ACTIVE":
768
+ return { ...base, deviceId, verdict: "PASS", detail: "device ACTIVE" };
769
+ case "PENDING":
770
+ return {
771
+ ...base,
772
+ deviceId,
773
+ verdict: "WARN",
774
+ detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
775
+ };
776
+ case "REVOKED":
777
+ return {
778
+ ...base,
779
+ deviceId,
780
+ verdict: "FAIL",
781
+ detail: "the device was REVOKED \u2014 this service is an orphan",
782
+ fix: d2.uninstallCommand(agentName)
783
+ };
784
+ default:
785
+ return {
786
+ ...base,
787
+ deviceId,
788
+ verdict: "SKIP",
789
+ detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
790
+ };
791
+ }
792
+ }
793
+ async function inspectUnsupervised(dataDir, d2) {
794
+ const agentName = basename(dataDir);
795
+ const base = { dataDir, agentName, deviceId: "" };
796
+ const deviceId = d2.readCreds(dataDir)?.deviceId;
797
+ if (!deviceId) {
798
+ return { ...base, verdict: "WARN", detail: "credentials present but they hold no device id" };
799
+ }
800
+ let probe;
801
+ try {
802
+ probe = await d2.fetchStatus(d2.apiUrl, deviceId);
803
+ } catch (e) {
804
+ return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e.message}` };
805
+ }
806
+ if (probe.inconclusive) {
807
+ return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
808
+ }
809
+ if (probe.gone) {
810
+ return {
811
+ ...base,
812
+ deviceId,
813
+ verdict: "WARN",
814
+ detail: "the device no longer exists \u2014 these are leftover credentials for a deleted agent"
815
+ };
816
+ }
817
+ if (probe.status === "ACTIVE") {
818
+ return {
819
+ ...base,
820
+ deviceId,
821
+ verdict: "WARN",
822
+ detail: "the device is ACTIVE but nothing supervises it \u2014 this agent is not running",
823
+ fix: d2.installCommand(agentName)
824
+ };
825
+ }
826
+ if (probe.status === "PENDING") {
827
+ return {
828
+ ...base,
829
+ deviceId,
830
+ verdict: "WARN",
831
+ detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
832
+ };
833
+ }
834
+ if (probe.status === "REVOKED") {
835
+ return {
836
+ ...base,
837
+ deviceId,
838
+ verdict: "WARN",
839
+ detail: "the device was REVOKED \u2014 these are leftover credentials"
840
+ };
841
+ }
842
+ return {
843
+ ...base,
844
+ deviceId,
845
+ verdict: "SKIP",
846
+ detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
847
+ };
848
+ }
849
+ function formatDoctorReport(r2) {
850
+ const out = [];
851
+ out.push(`Services installed for this bridge (${r2.labelPrefixes.map((p2) => p2 + "*").join(", ")})`);
852
+ if (r2.services.length === 0) {
853
+ out.push(` No services installed for this bridge on this machine.`);
854
+ }
855
+ for (const s2 of r2.services) {
856
+ out.push(
857
+ ` ${s2.verdict.padEnd(PAD)}${s2.label} [${s2.agentName || "unnamed"}] ${s2.loaded ? "loaded" : "not loaded"} \u2014 ${s2.detail}`
858
+ );
859
+ if (s2.fix) out.push(` ${"".padEnd(PAD)}fix: ${s2.fix}`);
860
+ }
861
+ if (r2.unsupervised.length) {
862
+ out.push("");
863
+ out.push("Credentials on disk that no installed service points at");
864
+ for (const u2 of r2.unsupervised) {
865
+ out.push(` ${u2.verdict.padEnd(PAD)}${u2.agentName} \u2014 ${u2.detail}`);
866
+ out.push(` ${"".padEnd(PAD)}${u2.dataDir}`);
867
+ if (u2.fix) out.push(` ${"".padEnd(PAD)}fix: ${u2.fix}`);
868
+ }
869
+ }
870
+ if (r2.notChecked.length) {
871
+ out.push("");
872
+ out.push("Found on this machine, NOT checked by this command");
873
+ for (const l2 of r2.notChecked) out.push(` ${l2}`);
874
+ out.push(
875
+ " These belong to other AgentVault agent families. This command can only read the credentials of its own, so it says nothing about their health."
876
+ );
877
+ }
878
+ const counts = { PASS: 0, WARN: 0, FAIL: 0, SKIP: 0 };
879
+ for (const s2 of r2.services) counts[s2.verdict]++;
880
+ for (const u2 of r2.unsupervised) counts[u2.verdict]++;
881
+ out.push("");
882
+ out.push(
883
+ `Summary: ${counts.PASS} PASS, ${counts.WARN} WARN, ${counts.FAIL} FAIL, ${counts.SKIP} SKIP` + (counts.SKIP ? " (SKIP = could not check, NOT a problem found)" : "")
884
+ );
885
+ if (counts.FAIL) {
886
+ out.push(
887
+ "`uninstall` removes the SERVICE only \u2014 your credentials stay on disk, so a mistake here costs a re-install, not an identity."
888
+ );
889
+ }
890
+ return out;
891
+ }
892
+ var PAD;
893
+ var init_doctor = __esm({
894
+ "src/service/doctor.ts"() {
895
+ "use strict";
896
+ PAD = 6;
897
+ }
898
+ });
899
+
900
+ // src/service/doctor-cli.ts
901
+ var doctor_cli_exports = {};
902
+ __export(doctor_cli_exports, {
903
+ fetchDeviceStatus: () => fetchDeviceStatus,
904
+ maybeRunDoctorSubcommand: () => maybeRunDoctorSubcommand,
905
+ realDoctorDeps: () => realDoctorDeps
585
906
  });
586
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
587
907
  import { join as join13 } from "node:path";
588
- function installOnReady(channel, cfg, deps) {
589
- const hasCreds = deps.hasCreds ?? hasPersistedCreds;
590
- const readLauncher = deps.readLauncher ?? ((p2) => existsSync5(p2) ? readFileSync4(p2, "utf8") : void 0);
591
- const fenceAuthExists = deps.fenceAuthExists ?? ((home) => existsSync5(join13(home, ".grok", "auth.json")));
592
- let fired = false;
593
- channel.on("ready", () => {
594
- if (fired) return;
595
- fired = true;
596
- void (async () => {
597
- try {
598
- if (!hasCreds(cfg.dataDir)) {
599
- throw new Error(
600
- `enrolment did not persist credentials to ${cfg.dataDir} \u2014 refusing to install a service that would crash-loop`
601
- );
602
- }
603
- if (!fenceAuthExists(cfg.fenceHome)) {
604
- throw new Error(
605
- "no grok credential in the fence \u2014 run `grok login`, then paste the same command again (your enrolment is already saved, so the token is not needed). Note that XAI_API_KEY from your shell is NOT inherited by the background service."
606
- );
607
- }
608
- if (deps.waitForIdle) {
609
- const timeoutMs = deps.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
610
- let timer;
611
- const timedOut = await Promise.race([
612
- deps.waitForIdle().then(() => false),
613
- new Promise((r2) => {
614
- timer = setTimeout(() => r2(true), timeoutMs);
615
- })
616
- ]);
617
- if (timer) clearTimeout(timer);
618
- if (timedOut) {
619
- deps.log(
620
- `agent still busy after ${timeoutMs}ms \u2014 handing off anyway; that reply may be lost`
621
- );
622
- }
623
- }
624
- const reaped = await deps.reap();
625
- if (!reaped.ok) {
626
- throw new Error(
627
- `teardown did not complete (${reaped.failures.join("; ")}) \u2014 refusing to install over a session that may still be open`
628
- );
629
- }
630
- const backend = deps.backend ?? selectBackend(process.platform);
631
- const spec = buildServiceSpec(cfg, {
632
- entrypoint: deps.entrypoint,
633
- home: deps.env.HOME ?? "",
634
- path: deps.env.PATH ?? ""
635
- });
636
- const installedEntrypoint = parseLauncherEntrypoint(readLauncher(spec.launcherPath));
637
- const entrypointIsCurrent = installedEntrypoint !== void 0 && !isEphemeralEntrypoint(installedEntrypoint) && installedEntrypoint === deps.entrypoint;
638
- if (backend.status(spec.label).loaded && entrypointIsCurrent) {
639
- backend.restart(spec.label);
640
- deps.log(`service ${spec.label} already installed and current \u2014 restarted`);
641
- } else {
642
- backend.install(spec);
643
- deps.log(
644
- installedEntrypoint && !entrypointIsCurrent ? `service ${spec.label} pointed at a stale entrypoint (${installedEntrypoint}) \u2014 re-installed` : `installed service ${spec.label}`
645
- );
646
- }
647
- const after = backend.status(spec.label);
648
- if (!after.loaded) {
649
- throw new Error(`service did not load: ${after.detail}`);
650
- }
651
- deps.log(`it is running now. logs: ${spec.logErr}`);
652
- deps.log(`check it: agentvault-grok-bridge status`);
653
- deps.log(`restart it: agentvault-grok-bridge restart (never 'install')`);
654
- deps.exit(0);
655
- } catch (e) {
656
- deps.log(`--install failed: ${e instanceof Error ? e.message : String(e)}`);
657
- deps.exit(1);
658
- }
659
- })();
908
+ import { readdirSync as readdirSync2 } from "node:fs";
909
+ async function fetchDeviceStatus(apiUrl, deviceId, doFetch = fetch) {
910
+ const res = await doFetch(`${apiUrl}/api/v1/devices/${deviceId}/status`, {
911
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
660
912
  });
913
+ if (res.status === 404) {
914
+ try {
915
+ await res.json();
916
+ } catch {
917
+ return {
918
+ inconclusive: `${apiUrl} answered 404 with a non-JSON body \u2014 check AV_API_URL points at the API`
919
+ };
920
+ }
921
+ return { gone: true };
922
+ }
923
+ if (res.status === 429) return { inconclusive: "rate limited by the API (429)" };
924
+ if (!res.ok) return { inconclusive: `the API answered ${res.status}` };
925
+ let body;
926
+ try {
927
+ body = await res.json();
928
+ } catch {
929
+ return { inconclusive: `${apiUrl} answered 200 with a non-JSON body \u2014 check AV_API_URL` };
930
+ }
931
+ return { status: body?.status };
932
+ }
933
+ function listCredDirs(base) {
934
+ const out = [];
935
+ if (hasPersistedCreds(base)) out.push(base);
936
+ let names;
937
+ try {
938
+ names = readdirSync2(base);
939
+ } catch {
940
+ return out;
941
+ }
942
+ for (const name of names) {
943
+ const dir = join13(base, name);
944
+ if (hasPersistedCreds(dir)) out.push(dir);
945
+ }
946
+ return out;
947
+ }
948
+ function otherFamilyLabels(backend) {
949
+ const out = [];
950
+ for (const p2 of OTHER_FAMILY_PREFIXES) {
951
+ try {
952
+ for (const s2 of backend.list(p2)) out.push(s2.label);
953
+ } catch {
954
+ }
955
+ }
956
+ return out.sort();
957
+ }
958
+ function realDoctorDeps(env) {
959
+ const backend = selectBackend(process.platform);
960
+ const base = join13(env.HOME ?? "", ".agentvault", "grok-room-bridge");
961
+ return {
962
+ backend,
963
+ labelPrefixes: [LABEL_PREFIX],
964
+ base,
965
+ // Same default as config.ts. Read from the env directly rather than through
966
+ // loadConfig, which now REFUSES without AV_AGENT_NAME (#921) — and doctor's
967
+ // whole job is to run when you do not know which agents exist.
968
+ apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
969
+ uninstallCommand: (n2) => `AV_AGENT_NAME=${n2 || "<agent-name>"} agentvault-grok-bridge uninstall`,
970
+ installCommand: (n2) => `AV_AGENT_NAME=${n2 || "<agent-name>"} agentvault-grok-bridge install`,
971
+ readCreds: (dir) => {
972
+ const deviceId = readPersistedDeviceId(dir);
973
+ return deviceId ? { deviceId } : null;
974
+ },
975
+ credDirs: () => listCredDirs(base),
976
+ fetchStatus: (apiUrl, deviceId) => fetchDeviceStatus(apiUrl, deviceId),
977
+ otherLabels: () => otherFamilyLabels(backend)
978
+ };
979
+ }
980
+ async function maybeRunDoctorSubcommand(argv, opts = {}) {
981
+ if (argv[2] !== "doctor") return false;
982
+ const log = opts.log ?? printLine;
983
+ try {
984
+ const report = await buildDoctorReport(opts.deps ?? realDoctorDeps(opts.env ?? process.env));
985
+ for (const line of formatDoctorReport(report)) log(line);
986
+ if (report.services.some((s2) => s2.verdict === "FAIL")) process.exitCode = 1;
987
+ } catch (e) {
988
+ log(`doctor could not run: ${e.message}`);
989
+ process.exitCode = 1;
990
+ }
991
+ return true;
661
992
  }
662
- var DEFAULT_IDLE_TIMEOUT_MS;
663
- var init_install_on_ready = __esm({
664
- "src/service/install-on-ready.ts"() {
993
+ var LABEL_PREFIX, OTHER_FAMILY_PREFIXES, PROBE_TIMEOUT_MS;
994
+ var init_doctor_cli = __esm({
995
+ "src/service/doctor-cli.ts"() {
665
996
  "use strict";
666
- init_config();
667
- init_spec();
668
- init_launcher();
669
997
  init_backend();
670
- DEFAULT_IDLE_TIMEOUT_MS = 3e4;
998
+ init_config();
999
+ init_log();
1000
+ init_doctor();
1001
+ LABEL_PREFIX = "dev.agentvault.grok-";
1002
+ OTHER_FAMILY_PREFIXES = [
1003
+ "dev.agentvault.bridge-",
1004
+ // claude bridge, current label
1005
+ "com.agentvault.claude-bridge.",
1006
+ // claude bridge, pre-rename label (#837)
1007
+ "ai.hermes.gateway"
1008
+ // hermes, one service per profile
1009
+ ];
1010
+ PROBE_TIMEOUT_MS = 8e3;
671
1011
  }
672
1012
  });
673
1013
 
@@ -98965,6 +99305,40 @@ async function startRoomMcpServer(opts) {
98965
99305
  };
98966
99306
  }
98967
99307
 
99308
+ // src/service/self-uninstall.ts
99309
+ init_subcommand();
99310
+ init_backend();
99311
+ init_log();
99312
+ var DEVICE_IS_GONE = "device_revoked";
99313
+ function selfUninstallOnTerminal(reason, env, deps = {}) {
99314
+ if (reason !== DEVICE_IS_GONE) return false;
99315
+ const log = deps.log ?? ((m2) => logLine("[grok-bridge] " + m2));
99316
+ let label;
99317
+ try {
99318
+ label = labelFor(env);
99319
+ } catch (e) {
99320
+ log(
99321
+ `device was revoked, but ${e.message} \u2014 cannot tell which service to remove. Remove it by hand with: AV_AGENT_NAME=<agent-name> agentvault-grok-bridge uninstall`
99322
+ );
99323
+ return false;
99324
+ }
99325
+ try {
99326
+ if (!deps.backend && process.platform !== "darwin" && process.platform !== "linux") return false;
99327
+ const backend = deps.backend ?? selectBackend(process.platform);
99328
+ if (!backend.status(label).loaded) return false;
99329
+ backend.uninstall(label);
99330
+ log(
99331
+ `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.`
99332
+ );
99333
+ return true;
99334
+ } catch (e) {
99335
+ log(
99336
+ `device was revoked, but removing the service ${label} failed: ${e.message}. Remove it by hand with: AV_AGENT_NAME=<agent-name> agentvault-grok-bridge uninstall`
99337
+ );
99338
+ return false;
99339
+ }
99340
+ }
99341
+
98968
99342
  // src/bridge.ts
98969
99343
  var RoomHushState = class {
98970
99344
  until = /* @__PURE__ */ new Map();
@@ -99020,11 +99394,13 @@ var ActiveTarget = class {
99020
99394
  function attachLifecycle2(channel, opts = {}) {
99021
99395
  const log = opts.log ?? (() => {
99022
99396
  });
99397
+ const selfUninstall = opts.selfUninstall ?? selfUninstallOnTerminal;
99023
99398
  const onTerminal = opts.onTerminal ?? ((_reason, o2) => process.exit(o2.restart ? 1 : 0));
99024
99399
  const terminal = (reason, restart) => {
99025
99400
  log(
99026
99401
  `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.`
99027
99402
  );
99403
+ selfUninstall(reason, process.env);
99028
99404
  onTerminal(reason, { restart });
99029
99405
  };
99030
99406
  channel.on("auth_failed", (e) => terminal(e.reason, false));
@@ -99178,6 +99554,10 @@ var shutdown = async (code = 0) => {
99178
99554
  }
99179
99555
  };
99180
99556
  async function main() {
99557
+ const { maybeRunDoctorSubcommand: maybeRunDoctorSubcommand2 } = await Promise.resolve().then(() => (init_doctor_cli(), doctor_cli_exports));
99558
+ if (await maybeRunDoctorSubcommand2(process.argv)) {
99559
+ process.exit(process.exitCode ?? 0);
99560
+ }
99181
99561
  const { maybeRunServiceSubcommand: maybeRunServiceSubcommand2 } = await Promise.resolve().then(() => (init_subcommand(), subcommand_exports));
99182
99562
  if (maybeRunServiceSubcommand2(process.argv, process.env, {
99183
99563
  log: (m2) => logLine("[grok-bridge] " + m2)
@@ -99187,7 +99567,7 @@ async function main() {
99187
99567
  const info = parseInfoFlags(process.argv.slice(2));
99188
99568
  if (info.showVersion || info.showHelp) {
99189
99569
  const version2 = buildClientVersion2(
99190
- true ? "0.3.2" : void 0
99570
+ true ? "0.4.0" : void 0
99191
99571
  );
99192
99572
  if (info.showVersion) printLine(version2);
99193
99573
  if (info.showHelp) {
@@ -99226,11 +99606,6 @@ async function main() {
99226
99606
  process.exit(0);
99227
99607
  }
99228
99608
  const cfg = loadConfig(process.env, process.argv.slice(2));
99229
- if (cfg.installAfterEnroll) {
99230
- assertInstallableEntrypoint(process.argv[1]);
99231
- const { selectBackend: selectBackend2 } = await Promise.resolve().then(() => (init_backend(), backend_exports));
99232
- selectBackend2(process.platform);
99233
- }
99234
99609
  mkdirSync6(cfg.workspaceDir, { recursive: true });
99235
99610
  mkdirSync6(cfg.fenceHome, { recursive: true });
99236
99611
  seedFenceCredential({
@@ -99245,7 +99620,7 @@ async function main() {
99245
99620
  agentName: cfg.agentName,
99246
99621
  platform: "node",
99247
99622
  clientVersion: buildClientVersion2(
99248
- true ? "0.3.2" : void 0
99623
+ true ? "0.4.0" : void 0
99249
99624
  )
99250
99625
  });
99251
99626
  channelRef = channel;
@@ -99336,23 +99711,6 @@ async function main() {
99336
99711
  logLine("[grok-bridge] enrolled \u2014 pending invite consumed and removed");
99337
99712
  }
99338
99713
  });
99339
- if (cfg.installAfterEnroll) {
99340
- const { installOnReady: installOnReady2 } = await Promise.resolve().then(() => (init_install_on_ready(), install_on_ready_exports));
99341
- installOnReady2(channel, cfg, {
99342
- entrypoint: process.argv[1],
99343
- env: process.env,
99344
- // FOUND LIVE 2026-08-16: the owner's activation greeting is usually already
99345
- // being answered when `ready` fires, and reaping mid-turn killed the reply
99346
- // for good — the message was acked, so the service never saw it again.
99347
- // GrokSession.idle() resolves when the queue drains.
99348
- waitForIdle: () => session.idle(),
99349
- // The one shared teardown. `exiting` is deliberately NOT set here: Ctrl-C
99350
- // must keep working for the whole multi-second launchctl window.
99351
- reap: reapAll,
99352
- exit: (code) => process.exit(code),
99353
- log: (m2) => logLine("[grok-bridge] " + m2)
99354
- });
99355
- }
99356
99714
  await channel.start();
99357
99715
  }
99358
99716
  main().catch(async (err) => {
@@ -1,7 +1,18 @@
1
1
  import type { ServiceSpec } from "./spec.js";
2
+ /** One installed service found on disk, with the env its definition bakes in. */
3
+ export interface InstalledService {
4
+ label: string;
5
+ /** Absolute path of the plist / unit file. */
6
+ path: string;
7
+ env: Record<string, string>;
8
+ }
2
9
  export interface ServiceBackend {
3
10
  install(spec: ServiceSpec): void;
4
11
  uninstall(label: string): void;
12
+ /** #913 item 3 — every installed service whose label starts with `prefix`,
13
+ * read from the definition DIRECTORY, not from the running-service list. An
14
+ * orphan is installed-but-not-running by construction. */
15
+ list(prefix: string): InstalledService[];
5
16
  /** Restart an ALREADY-INSTALLED service without touching its spec (#837).
6
17
  * Distinct from install, which re-bakes the plist from the current shell. */
7
18
  restart(label: string): void;
@@ -23,6 +34,10 @@ export interface BackendDeps {
23
34
  chmod: (path: string, mode: number) => void;
24
35
  rm: (path: string) => void;
25
36
  exists: (path: string) => boolean;
37
+ /** Names in a directory. Returns [] when the directory does not exist — a
38
+ * machine with no services installed is not an error. */
39
+ readdir: (path: string) => string[];
40
+ readFile: (path: string) => string;
26
41
  }
27
42
  export declare function defaultDeps(): BackendDeps;
28
43
  export declare function selectBackend(platform: NodeJS.Platform, deps?: BackendDeps): ServiceBackend;
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/service/backend.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;kFAC8E;IAC9E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAED,wBAAgB,WAAW,IAAI,WAAW,CAoBzC;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,cAAc,CAU3F"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/service/backend.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;+DAE2D;IAC3D,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAAC;IACzC;kFAC8E;IAC9E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAClC;8DAC0D;IAC1D,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACpC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACpC;AAED,wBAAgB,WAAW,IAAI,WAAW,CAoCzC;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,cAAc,CAU3F"}
@@ -0,0 +1,36 @@
1
+ import { type DeviceProbe, type DoctorDeps } from "./doctor.js";
2
+ /**
3
+ * Ask the public status endpoint about one device.
4
+ *
5
+ * `GET /api/v1/devices/{id}/status` needs no auth and is rate limited per
6
+ * device_id, so checking several agents in a row is fine.
7
+ *
8
+ * ⚠️ A 404 is the strongest claim this command makes — it is what turns into
9
+ * "orphan, run uninstall". So it is only believed when the body is JSON, i.e.
10
+ * an actual API error shape. `agentvault.dev` serves a GoDaddy parked page that
11
+ * answers with confident HTTP codes and no API behind it, and on 2026-07-25
12
+ * that fooled a smoke check into reporting a healthy backend. A wrong
13
+ * AV_API_URL must read as "I could not check", never as "all your agents are
14
+ * dead".
15
+ */
16
+ export declare function fetchDeviceStatus(apiUrl: string, deviceId: string, doFetch?: typeof fetch): Promise<DeviceProbe>;
17
+ export declare function realDoctorDeps(env: NodeJS.ProcessEnv): DoctorDeps;
18
+ /**
19
+ * Handle `agentvault-grok-bridge doctor`. Returns true when handled.
20
+ *
21
+ * Async, which is why it does not live in maybeRunServiceSubcommand: that
22
+ * dispatcher is synchronous and doctor has to await the network. index.ts runs
23
+ * this FIRST. If it ever did not, `doctor` would fall through to the
24
+ * unknown-subcommand guard and throw — loud, and safe. A typo must never boot a
25
+ * daemon (#921), and neither must a command we forgot to wire.
26
+ *
27
+ * READ-ONLY by construction: it never calls install, uninstall or restart. It
28
+ * prints the command instead, because deciding to remove a customer's service
29
+ * is theirs to make with the evidence in front of them.
30
+ */
31
+ export declare function maybeRunDoctorSubcommand(argv: string[], opts?: {
32
+ log?: (msg: string) => void;
33
+ deps?: DoctorDeps;
34
+ env?: NodeJS.ProcessEnv;
35
+ }): Promise<boolean>;
36
+ //# sourceMappingURL=doctor-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-cli.d.ts","sourceRoot":"","sources":["../../src/service/doctor-cli.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAoBrB;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,WAAW,CAAC,CAuBtB;AA+BD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,UAAU,CAsBjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACrF,OAAO,CAAC,OAAO,CAAC,CAelB"}
@@ -0,0 +1,93 @@
1
+ import type { ServiceBackend } from "./backend.js";
2
+ /**
3
+ * #913 item 3 — `doctor`: list what is installed, check each against the API,
4
+ * and say which ones are orphaned.
5
+ *
6
+ * Deleting an agent in the dashboard removes the DEVICE ROW and nothing else.
7
+ * The service stays installed, pointing at a device that no longer exists, and
8
+ * the customer has no signal at all. Item 1 names the uninstall command AT
9
+ * DELETE TIME; item 2 removes the service when the running bridge is told the
10
+ * device is gone. Neither helps the machine that already accumulated orphans —
11
+ * from a delete done in another browser, before those shipped, or while the
12
+ * bridge was not running. That is what this reads.
13
+ *
14
+ * TWO RULES, both earned the hard way:
15
+ *
16
+ * 1. AN UNREACHABLE API IS NOT AN ORPHAN. #919: a default for a missing input is
17
+ * a guess, and reporting on it is a lie. Every inconclusive probe is SKIP,
18
+ * never FAIL — including a status string this build does not recognise.
19
+ * Printing FAIL on a network blip sends a customer to uninstall a healthy
20
+ * agent.
21
+ *
22
+ * 2. SILENCE ABOUT WHAT IT CANNOT SEE IS A FALSE ALL-CLEAR. #941: the delete
23
+ * modal covered 3 of 15 devices and said nothing for the other 12, which
24
+ * reads as "nothing to clean up". This command can only check its own family
25
+ * — the agents whose credentials it can read — so it LISTS the other
26
+ * AgentVault services it found and says it did not check them.
27
+ */
28
+ export type Verdict = "PASS" | "WARN" | "FAIL" | "SKIP";
29
+ /** What the server said about one device. Exactly one field is meaningful. */
30
+ export interface DeviceProbe {
31
+ /** The device's status string, verbatim from the API. */
32
+ status?: string;
33
+ /** 404 — the device row does not exist. */
34
+ gone?: boolean;
35
+ /** Why the check could not conclude. Present => the answer is UNKNOWN. */
36
+ inconclusive?: string;
37
+ }
38
+ export interface ServiceFinding {
39
+ label: string;
40
+ agentName: string;
41
+ dataDir: string;
42
+ loaded: boolean;
43
+ deviceId: string;
44
+ verdict: Verdict;
45
+ detail: string;
46
+ /** The exact command that fixes it. Set ONLY for a confirmed orphan. */
47
+ fix?: string;
48
+ }
49
+ export interface UnsupervisedFinding {
50
+ dataDir: string;
51
+ agentName: string;
52
+ deviceId: string;
53
+ verdict: Verdict;
54
+ detail: string;
55
+ /** How to bring a live agent back. Never a suggestion to delete key material:
56
+ * `uninstall` leaves credentials ON PURPOSE, and a doctor that says "remove
57
+ * this directory" turns a recoverable mistake into a lost identity. */
58
+ fix?: string;
59
+ }
60
+ export interface DoctorReport {
61
+ labelPrefixes: string[];
62
+ services: ServiceFinding[];
63
+ /** Credentials on disk that no installed service points at — CHECKED against
64
+ * the API, not merely listed. "Leftover from a deleted agent" and "a live
65
+ * agent whose service was removed" look identical on disk and need opposite
66
+ * responses, so the verdict is the whole point. */
67
+ unsupervised: UnsupervisedFinding[];
68
+ /** AgentVault services from other families. Found, listed, NOT checked. */
69
+ notChecked: string[];
70
+ }
71
+ export interface DoctorDeps {
72
+ backend: ServiceBackend;
73
+ /** Every label prefix this bridge OWNS — plural because #837 renamed the
74
+ * claude label and pre-rename installs are still registered under the old
75
+ * one. Those are the longest-running services on customer machines, i.e. the
76
+ * ones most likely to have accumulated an orphan. */
77
+ labelPrefixes: string[];
78
+ base: string;
79
+ apiUrl: string;
80
+ uninstallCommand: (agentName: string) => string;
81
+ installCommand: (agentName: string) => string;
82
+ readCreds: (dataDir: string) => {
83
+ deviceId?: string;
84
+ } | null;
85
+ /** Absolute data dirs under `base` that hold credentials. */
86
+ credDirs: () => string[];
87
+ fetchStatus: (apiUrl: string, deviceId: string) => Promise<DeviceProbe>;
88
+ /** Labels belonging to other AgentVault families installed on this machine. */
89
+ otherLabels: () => string[];
90
+ }
91
+ export declare function buildDoctorReport(d: DoctorDeps): Promise<DoctorReport>;
92
+ export declare function formatDoctorReport(r: DoctorReport): string[];
93
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/service/doctor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAExD,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;4EAEwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B;;;wDAGoD;IACpD,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,2EAA2E;IAC3E,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,cAAc,CAAC;IACxB;;;0DAGsD;IACtD,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9C,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7D,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACxE,+EAA+E;IAC/E,WAAW,EAAE,MAAM,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAqB5E;AAiJD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAqD5D"}
@@ -1,7 +1,26 @@
1
1
  import type { ServiceSpec } from "./spec.js";
2
- import type { BackendDeps, ServiceBackend } from "./backend.js";
2
+ import type { BackendDeps, ServiceBackend, InstalledService } from "./backend.js";
3
3
  /** Render a launchd LaunchAgent plist for a bridge ServiceSpec. Pure string. */
4
4
  export declare function renderLaunchdPlist(spec: ServiceSpec): string;
5
+ /**
6
+ * Read the EnvironmentVariables dict back out of an installed plist.
7
+ *
8
+ * Deliberately tolerant of formatting, and deliberately NOT tolerant of scope.
9
+ *
10
+ * TOLERANT: `loopita`'s plist is hand-edited, so it keeps none of the
11
+ * whitespace `renderLaunchdPlist` writes. A parser that only understood its own
12
+ * output would report "no data dir" for precisely the long-lived install a
13
+ * customer is most likely to be asking `doctor` about.
14
+ *
15
+ * NOT TOLERANT: it reads ONLY the EnvironmentVariables dict. `Label`,
16
+ * `WorkingDirectory` and `StandardOutPath` are <key>/<string> pairs too, so a
17
+ * whole-file scan would hand back a log path as if it were AV_DATA_DIR and then
18
+ * go looking for credentials in it.
19
+ *
20
+ * Returns {} for junk. A doctor that throws on one malformed plist reports
21
+ * nothing about the other five.
22
+ */
23
+ export declare function parsePlistEnv(xml: string): Record<string, string>;
5
24
  export declare class LaunchdBackend implements ServiceBackend {
6
25
  private deps;
7
26
  constructor(deps: BackendDeps);
@@ -17,5 +36,12 @@ export declare class LaunchdBackend implements ServiceBackend {
17
36
  loaded: boolean;
18
37
  detail: string;
19
38
  };
39
+ /** #913 item 3 — every installed service whose label starts with `prefix`.
40
+ *
41
+ * Reads the plist directory rather than `launchctl list`, because an ORPHAN
42
+ * is typically installed-but-not-loaded: the revoked device exits 0 and
43
+ * KeepAlive leaves it down. `launchctl list` would not show the very thing
44
+ * doctor exists to find. */
45
+ list(prefix: string): InstalledService[];
20
46
  }
21
47
  //# sourceMappingURL=launchd.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"launchd.d.ts","sourceRoot":"","sources":["../../src/service/launchd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAUhE,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAkC5D;AAED,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,WAAW;IAErC,OAAO,CAAC,SAAS;IAGjB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IA2BhC,OAAO,CAAC,aAAa;IAOrB;+EAC2E;IAC3E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM9B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;CAI3D"}
1
+ {"version":3,"file":"launchd.d.ts","sourceRoot":"","sources":["../../src/service/launchd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAUlF,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAkC5D;AAGD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BjE;AAOD,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,WAAW;IAErC,OAAO,CAAC,SAAS;IAGjB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IA2BhC,OAAO,CAAC,aAAa;IAOrB;+EAC2E;IAC3E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM9B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAK1D;;;;;iCAK6B;IAC7B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAezC"}
@@ -0,0 +1,19 @@
1
+ import { type ServiceBackend } from "./backend.js";
2
+ export interface SelfUninstallDeps {
3
+ backend?: ServiceBackend;
4
+ log?: (msg: string) => void;
5
+ }
6
+ /**
7
+ * Remove this agent's supervised service after its device was revoked.
8
+ *
9
+ * Returns whether a service was actually removed. NEVER throws: this runs on
10
+ * the shutdown path, and a cleanup that cannot happen must not stop the bridge
11
+ * exiting cleanly — `KeepAlive {SuccessfulExit: false}` depends on that exit(0)
12
+ * to leave the service down rather than crash-looping it.
13
+ *
14
+ * The stakes are asymmetric and worth stating: `uninstall` removes the SERVICE
15
+ * and LEAVES the credentials on disk. A false positive therefore costs the
16
+ * customer a re-install, not their identity.
17
+ */
18
+ export declare function selfUninstallOnTerminal(reason: string, env: NodeJS.ProcessEnv, deps?: SelfUninstallDeps): boolean;
19
+ //# sourceMappingURL=self-uninstall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-uninstall.d.ts","sourceRoot":"","sources":["../../src/service/self-uninstall.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAuBlE,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAyCT"}
@@ -4,6 +4,7 @@ export interface ServiceCmdDeps {
4
4
  entrypoint?: string;
5
5
  log?: (msg: string) => void;
6
6
  }
7
+ export declare function labelFor(env: NodeJS.ProcessEnv): string;
7
8
  /** Handle `agentvault-grok-bridge <install|uninstall|status|logs>` before boot.
8
9
  * Returns true when handled (caller should exit). Secrets are never placed on
9
10
  * the service command line — config flows via the plist env; the token is omitted. */
@@ -1 +1 @@
1
- {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../src/service/subcommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAElE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAiDD;;uFAEuF;AACvF,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,CAAC,EAAE,cAAc,GACpB,OAAO,CAqHT"}
1
+ {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../src/service/subcommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAElE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AA6CD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAEvD;AAED;;uFAEuF;AACvF,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,CAAC,EAAE,cAAc,GACpB,OAAO,CAgIT"}
@@ -1,10 +1,21 @@
1
1
  import type { ServiceSpec } from "./spec.js";
2
- import type { BackendDeps, ServiceBackend } from "./backend.js";
2
+ import type { BackendDeps, ServiceBackend, InstalledService } from "./backend.js";
3
3
  /** Render a systemd --user service unit for a bridge ServiceSpec. Pure string.
4
4
  * Restart=on-failure + SuccessExitStatus=0 is the KeepAlive={SuccessfulExit:false}
5
5
  * equivalent: a clean exit(0) (revoked device) is treated as success => NOT
6
6
  * restarted; a non-zero crash is restarted. WantedBy=default.target is RunAtLoad. */
7
7
  export declare function renderSystemdUnit(spec: ServiceSpec): string;
8
+ /**
9
+ * Read `Environment=` back out of an installed systemd unit.
10
+ *
11
+ * Reverses `q` exactly: un-double the '%' first, then walk the backslash
12
+ * escapes left to right in ONE pass. Two passes would turn a literal
13
+ * backslash-n (written as \\n) into a newline.
14
+ *
15
+ * Returns {} for junk — a doctor that throws on one malformed unit reports
16
+ * nothing about the others.
17
+ */
18
+ export declare function parseUnitEnv(text: string): Record<string, string>;
8
19
  export declare class SystemdBackend implements ServiceBackend {
9
20
  private deps;
10
21
  constructor(deps: BackendDeps);
@@ -18,5 +29,12 @@ export declare class SystemdBackend implements ServiceBackend {
18
29
  loaded: boolean;
19
30
  detail: string;
20
31
  };
32
+ /** #913 item 3 — every installed unit whose label starts with `prefix`.
33
+ *
34
+ * Reads the unit directory rather than `systemctl list-units`, because an
35
+ * ORPHAN is installed-but-inactive: the revoked device exits 0, which
36
+ * SuccessExitStatus=0 treats as success, so it is never restarted. A
37
+ * running-units listing would not show the thing doctor exists to find. */
38
+ list(prefix: string): InstalledService[];
21
39
  }
22
40
  //# sourceMappingURL=systemd.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"systemd.d.ts","sourceRoot":"","sources":["../../src/service/systemd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAmBhE;;;sFAGsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAsB3D;AAED,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,WAAW;IAErC,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAoBhC;2DACuD;IACvD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAU9B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;CAQ3D"}
1
+ {"version":3,"file":"systemd.d.ts","sourceRoot":"","sources":["../../src/service/systemd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAmBlF;;;sFAGsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAsB3D;AAGD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWjE;AAQD,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,WAAW;IAErC,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAoBhC;2DACuD;IACvD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAU9B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAS1D;;;;;gFAK4E;IAC5E,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAczC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/grok-bridge",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "AgentVault Grok Bridge — daemon bridging a Grok agent into E2E-encrypted AgentVault direct messages and rooms.",
6
6
  "main": "dist/index.js",