@agentvault/grok-bridge 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -6,6 +6,60 @@ Bridges a Grok agent into E2E-encrypted AgentVault direct messages and rooms.
6
6
  no runtime code. The only shared dependency is `@agentvault/agentvault`, the
7
7
  protocol and crypto library.
8
8
 
9
+ ## Usage
10
+
11
+ **Prerequisite:** install Grok and run `grok login`. The bridge copies that
12
+ credential into its sandbox once, at first run.
13
+
14
+ ### One paste (recommended) — `--install`
15
+
16
+ ```sh
17
+ npm i -g @agentvault/grok-bridge
18
+ AV_AGENT_NAME=<agent> AV_INVITE_TOKEN=<token> agentvault-grok-bridge --install
19
+ ```
20
+
21
+ Enrols the agent, waits for you to click **Approve** in the dashboard, installs a
22
+ supervised OS service (launchd on macOS, systemd on Linux), and **exits** — so
23
+ your terminal comes back and the agent survives closing it and rebooting.
24
+
25
+ Three things worth knowing:
26
+
27
+ - **`npm i -g` is not optional.** The service bakes the running entrypoint into
28
+ its launcher. Under `npx` that is a package cache the package manager later
29
+ prunes, so the service would work today and die silently weeks later.
30
+ `--install` refuses an ephemeral entrypoint at startup, before your single-use
31
+ invite token is spent.
32
+ - **`XAI_API_KEY` is NOT inherited by the service.** A supervisor does not read
33
+ your shell environment. Use `grok login`; the bridge refuses to install a
34
+ service that would have no credential and run mute.
35
+ - **Re-running is safe.** With credentials already saved you can omit the token.
36
+ If the installed service is current it is restarted, not re-installed, so
37
+ settings baked at install time are preserved.
38
+
39
+ ### Foreground (no service)
40
+
41
+ ```sh
42
+ AV_AGENT_NAME=<agent> AV_INVITE_TOKEN=<token> npx @agentvault/grok-bridge
43
+ ```
44
+
45
+ Runs in the terminal and stops when you close it. Unchanged by `--install`.
46
+
47
+ ### Managing the service
48
+
49
+ ```sh
50
+ agentvault-grok-bridge status # is it loaded?
51
+ agentvault-grok-bridge restart # pick up new credentials / recover
52
+ agentvault-grok-bridge uninstall # remove it
53
+ agentvault-grok-bridge logs # where the logs are
54
+ ```
55
+
56
+ Use `restart`, **never `install`**, to restart. `install` rebuilds the service
57
+ from your current shell and re-bakes `PATH`, silently discarding environment
58
+ baked at the original install.
59
+
60
+ Logs go to `<data-dir>/logs/grok-bridge.error.log` — the bridge writes to stderr,
61
+ so the `.error.log` file is the one with content in it.
62
+
9
63
  ## Copied-from ledger — REVIEW PERIODICALLY
10
64
 
11
65
  These files were copied once from the Claude bridge and are now owned here. A fix
package/dist/config.d.ts CHANGED
@@ -1,3 +1,40 @@
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
+ /** Informational flags. Handled BEFORE loadConfig in index.ts, because asking
15
+ * "what version is this?" must not require an invite token. Listed here too so
16
+ * the strict unknown-flag check does not reject them. */
17
+ export declare const VERSION_FLAGS: readonly ["--version", "-v"];
18
+ export declare const HELP_FLAGS: readonly ["--help", "-h"];
19
+ /** Pure argv read for the informational flags. Deliberately independent of
20
+ * loadConfig: that throws without a token, and `--version` must answer anyway. */
21
+ export declare function parseInfoFlags(argv: string[]): {
22
+ showVersion: boolean;
23
+ showHelp: boolean;
24
+ };
25
+ /**
26
+ * Package-manager scratch caches. `service/subcommand.ts` bakes `process.argv[1]`
27
+ * into the launcher script, so a service installed from one of these points at a
28
+ * directory the package manager later prunes — it works today and dies weeks later
29
+ * with nothing naming the cause.
30
+ *
31
+ * A DENYLIST, not proof of durability: it covers npm's npx cache, pnpm/yarn `dlx`,
32
+ * `bunx`, and anything under the OS temp dir. Other ephemeral runners are knowingly
33
+ * unhandled.
34
+ */
35
+ export declare function isEphemeralEntrypoint(entrypoint: string): boolean;
36
+ /** Throw unless `entrypoint` is durable enough to bake into a supervised service. */
37
+ export declare function assertInstallableEntrypoint(entrypoint: string): void;
1
38
  export declare function hasPersistedCreds(dataDir: string): boolean;
2
39
  export declare function slugify(name: string): string;
3
40
  export type DataDirSource = "explicit" | "per-agent";
@@ -38,6 +75,14 @@ export interface BridgeConfig {
38
75
  * instructs the agent. It does not: given a bare message the model answered
39
76
  * in prose and never called room__say, so the reply was discarded. */
40
77
  systemPrompt?: string;
78
+ /** Set by --install: enrol in the foreground, install the supervised service,
79
+ * then EXIT so the customer's terminal returns. False leaves a bare run
80
+ * byte-for-byte as it behaves today. */
81
+ installAfterEnroll: boolean;
82
+ /** --version / -v. index.ts answers these before loadConfig runs. */
83
+ showVersion: boolean;
84
+ /** --help / -h. Same. */
85
+ showHelp: boolean;
41
86
  }
42
87
  export declare function loadConfig(env: NodeJS.ProcessEnv, argv?: string[]): BridgeConfig;
43
88
  //# sourceMappingURL=config.d.ts.map
package/dist/index.js CHANGED
@@ -12,6 +12,24 @@ var __export = (target, all) => {
12
12
  // src/config.ts
13
13
  import { existsSync, readFileSync as readFileSync2, mkdirSync, chmodSync } from "node:fs";
14
14
  import { join as join5, resolve as resolve2, sep, dirname } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+ function parseInfoFlags(argv) {
17
+ return {
18
+ showVersion: argv.some((a2) => VERSION_FLAGS.includes(a2)),
19
+ showHelp: argv.some((a2) => HELP_FLAGS.includes(a2))
20
+ };
21
+ }
22
+ function isEphemeralEntrypoint(entrypoint) {
23
+ return entrypoint.includes("/_npx/") || entrypoint.includes("/dlx-") || entrypoint.includes("/.bun/install/cache/") || entrypoint.startsWith(tmpdir() + sep);
24
+ }
25
+ function assertInstallableEntrypoint(entrypoint) {
26
+ if (isEphemeralEntrypoint(entrypoint)) {
27
+ throw new Error(
28
+ `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:
29
+ npm i -g @agentvault/grok-bridge && agentvault-grok-bridge ` + INSTALL_FLAG
30
+ );
31
+ }
32
+ }
15
33
  function hasRecoverableBackup(dataDir) {
16
34
  try {
17
35
  const parsed = JSON.parse(readFileSync2(join5(dataDir, BACKUP_FILE), "utf-8"));
@@ -35,6 +53,12 @@ function resolveDataDir(env) {
35
53
  function loadConfig(env, argv = []) {
36
54
  const { dataDir, source: dataDirSource } = resolveDataDir(env);
37
55
  const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || "";
56
+ const unknownFlags = argv.filter((a2) => a2.startsWith("-") && !KNOWN_FLAGS.has(a2));
57
+ if (unknownFlags.length) {
58
+ throw new Error(
59
+ `unknown option(s): ${unknownFlags.join(", ")} \u2014 supported flags are ${[...KNOWN_FLAGS].join(", ")}`
60
+ );
61
+ }
38
62
  const workspaceDir = env.AV_WORKSPACE_DIR || join5(dirname(dataDir), "workspaces", slugify2(env.AV_AGENT_NAME ?? "grok"));
39
63
  const wsReal = resolve2(workspaceDir);
40
64
  const ddReal = resolve2(dataDir);
@@ -60,6 +84,10 @@ function loadConfig(env, argv = []) {
60
84
  inviteToken,
61
85
  dataDir,
62
86
  dataDirSource,
87
+ // One-paste install (--install): enrol, hand off to the supervisor, exit.
88
+ // Default false keeps a bare `npx @agentvault/grok-bridge` run identical.
89
+ installAfterEnroll: argv.includes(INSTALL_FLAG),
90
+ ...parseInfoFlags(argv),
63
91
  apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
64
92
  // Identity shown to the agent's own session. Set this to the agent's
65
93
  // AgentVault name via AV_AGENT_NAME (the native connect command passes it).
@@ -76,10 +104,14 @@ function loadConfig(env, argv = []) {
76
104
  systemPrompt: env.AV_SYSTEM_PROMPT || void 0
77
105
  };
78
106
  }
79
- var CRED_FILES, BACKUP_FILE;
107
+ var INSTALL_FLAG, VERSION_FLAGS, HELP_FLAGS, KNOWN_FLAGS, CRED_FILES, BACKUP_FILE;
80
108
  var init_config = __esm({
81
109
  "src/config.ts"() {
82
110
  "use strict";
111
+ INSTALL_FLAG = "--install";
112
+ VERSION_FLAGS = ["--version", "-v"];
113
+ HELP_FLAGS = ["--help", "-h"];
114
+ KNOWN_FLAGS = /* @__PURE__ */ new Set([INSTALL_FLAG, ...VERSION_FLAGS, ...HELP_FLAGS]);
83
115
  CRED_FILES = ["agentvault.json", "secure-channel.json"];
84
116
  BACKUP_FILE = "agentvault.json.bak";
85
117
  }
@@ -94,6 +126,9 @@ function stamp(message) {
94
126
  function logLine(...args) {
95
127
  console.error(stamp(format(...args)));
96
128
  }
129
+ function printLine(text) {
130
+ console.log(text);
131
+ }
97
132
  var init_log = __esm({
98
133
  "src/log.ts"() {
99
134
  "use strict";
@@ -130,6 +165,16 @@ function writeLauncher(deps, spec) {
130
165
  deps.writeFile(spec.launcherPath, spec.launcherScript);
131
166
  deps.chmod(spec.launcherPath, 493);
132
167
  }
168
+ function parseLauncherEntrypoint(script) {
169
+ if (!script) return void 0;
170
+ const m2 = /^if n=\$\(command -v .+\); then exec "\$n" (.+) "\$@"; fi$/m.exec(script);
171
+ if (!m2) return void 0;
172
+ return unSq(m2[1]);
173
+ }
174
+ function unSq(quoted) {
175
+ if (quoted.length < 2 || !quoted.startsWith("'") || !quoted.endsWith("'")) return void 0;
176
+ return quoted.slice(1, -1).replace(/'\\''/g, "'");
177
+ }
133
178
  var FALLBACK_NODES;
134
179
  var init_launcher = __esm({
135
180
  "src/service/launcher.ts"() {
@@ -353,6 +398,11 @@ var init_systemd = __esm({
353
398
  });
354
399
 
355
400
  // src/service/backend.ts
401
+ var backend_exports = {};
402
+ __export(backend_exports, {
403
+ defaultDeps: () => defaultDeps,
404
+ selectBackend: () => selectBackend
405
+ });
356
406
  import { spawnSync } from "node:child_process";
357
407
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync2, rmSync, existsSync as existsSync3, chmodSync as chmodSync4 } from "node:fs";
358
408
  import { userInfo } from "node:os";
@@ -399,7 +449,17 @@ function labelFor(env) {
399
449
  }
400
450
  function maybeRunServiceSubcommand(argv, env, deps) {
401
451
  const cmd = argv[2];
402
- if (!cmd || !SERVICE_CMDS.has(cmd)) return false;
452
+ if (!cmd) return false;
453
+ if (!SERVICE_CMDS.has(cmd)) {
454
+ const looksLikeToken = cmd.startsWith("av_tok_");
455
+ const looksLikeFlag = cmd.startsWith("-");
456
+ if (!looksLikeToken && !looksLikeFlag) {
457
+ throw new Error(
458
+ `unknown subcommand: ${cmd} \u2014 expected one of ${[...SERVICE_CMDS].join(", ")}, an invite token, or no argument at all`
459
+ );
460
+ }
461
+ return false;
462
+ }
403
463
  const backend = deps?.backend ?? selectBackend(process.platform);
404
464
  const entrypoint = deps?.entrypoint ?? process.argv[1];
405
465
  const log = deps?.log ?? ((m2) => logLine("[grok-bridge] " + m2));
@@ -420,7 +480,7 @@ function maybeRunServiceSubcommand(argv, env, deps) {
420
480
  path: env.PATH ?? ""
421
481
  });
422
482
  backend.install(spec);
423
- log(`installed service ${spec.label} \u2014 logs: ${spec.logOut}`);
483
+ log(`installed service ${spec.label} \u2014 logs: ${spec.logErr}`);
424
484
  log(`check it with: agentvault-grok-bridge status`);
425
485
  } else if (cmd === "uninstall") {
426
486
  const label = labelFor(env);
@@ -456,10 +516,103 @@ var init_subcommand = __esm({
456
516
  }
457
517
  });
458
518
 
519
+ // src/service/install-on-ready.ts
520
+ var install_on_ready_exports = {};
521
+ __export(install_on_ready_exports, {
522
+ installOnReady: () => installOnReady
523
+ });
524
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
525
+ import { join as join12 } from "node:path";
526
+ function installOnReady(channel, cfg, deps) {
527
+ const hasCreds = deps.hasCreds ?? hasPersistedCreds;
528
+ const readLauncher = deps.readLauncher ?? ((p2) => existsSync4(p2) ? readFileSync3(p2, "utf8") : void 0);
529
+ const fenceAuthExists = deps.fenceAuthExists ?? ((home) => existsSync4(join12(home, ".grok", "auth.json")));
530
+ let fired = false;
531
+ channel.on("ready", () => {
532
+ if (fired) return;
533
+ fired = true;
534
+ void (async () => {
535
+ try {
536
+ if (!hasCreds(cfg.dataDir)) {
537
+ throw new Error(
538
+ `enrolment did not persist credentials to ${cfg.dataDir} \u2014 refusing to install a service that would crash-loop`
539
+ );
540
+ }
541
+ if (!fenceAuthExists(cfg.fenceHome)) {
542
+ throw new Error(
543
+ "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."
544
+ );
545
+ }
546
+ if (deps.waitForIdle) {
547
+ const timeoutMs = deps.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
548
+ let timer;
549
+ const timedOut = await Promise.race([
550
+ deps.waitForIdle().then(() => false),
551
+ new Promise((r2) => {
552
+ timer = setTimeout(() => r2(true), timeoutMs);
553
+ })
554
+ ]);
555
+ if (timer) clearTimeout(timer);
556
+ if (timedOut) {
557
+ deps.log(
558
+ `agent still busy after ${timeoutMs}ms \u2014 handing off anyway; that reply may be lost`
559
+ );
560
+ }
561
+ }
562
+ const reaped = await deps.reap();
563
+ if (!reaped.ok) {
564
+ throw new Error(
565
+ `teardown did not complete (${reaped.failures.join("; ")}) \u2014 refusing to install over a session that may still be open`
566
+ );
567
+ }
568
+ const backend = deps.backend ?? selectBackend(process.platform);
569
+ const spec = buildServiceSpec(cfg, {
570
+ entrypoint: deps.entrypoint,
571
+ home: deps.env.HOME ?? "",
572
+ path: deps.env.PATH ?? ""
573
+ });
574
+ const installedEntrypoint = parseLauncherEntrypoint(readLauncher(spec.launcherPath));
575
+ const entrypointIsCurrent = installedEntrypoint !== void 0 && !isEphemeralEntrypoint(installedEntrypoint) && installedEntrypoint === deps.entrypoint;
576
+ if (backend.status(spec.label).loaded && entrypointIsCurrent) {
577
+ backend.restart(spec.label);
578
+ deps.log(`service ${spec.label} already installed and current \u2014 restarted`);
579
+ } else {
580
+ backend.install(spec);
581
+ deps.log(
582
+ installedEntrypoint && !entrypointIsCurrent ? `service ${spec.label} pointed at a stale entrypoint (${installedEntrypoint}) \u2014 re-installed` : `installed service ${spec.label}`
583
+ );
584
+ }
585
+ const after = backend.status(spec.label);
586
+ if (!after.loaded) {
587
+ throw new Error(`service did not load: ${after.detail}`);
588
+ }
589
+ deps.log(`it is running now. logs: ${spec.logErr}`);
590
+ deps.log(`check it: agentvault-grok-bridge status`);
591
+ deps.log(`restart it: agentvault-grok-bridge restart (never 'install')`);
592
+ deps.exit(0);
593
+ } catch (e) {
594
+ deps.log(`--install failed: ${e instanceof Error ? e.message : String(e)}`);
595
+ deps.exit(1);
596
+ }
597
+ })();
598
+ });
599
+ }
600
+ var DEFAULT_IDLE_TIMEOUT_MS;
601
+ var init_install_on_ready = __esm({
602
+ "src/service/install-on-ready.ts"() {
603
+ "use strict";
604
+ init_config();
605
+ init_spec();
606
+ init_launcher();
607
+ init_backend();
608
+ DEFAULT_IDLE_TIMEOUT_MS = 3e4;
609
+ }
610
+ });
611
+
459
612
  // src/index.ts
460
613
  import { spawn } from "node:child_process";
461
614
  import { mkdirSync as mkdirSync5 } from "node:fs";
462
- import { join as join12 } from "node:path";
615
+ import { join as join13 } from "node:path";
463
616
 
464
617
  // ../plugin/dist/index.js
465
618
  import * as nc from "node:crypto";
@@ -69938,7 +70091,8 @@ ${messageText}`;
69938
70091
  const candidateCount = candidates.filter(Boolean).length;
69939
70092
  const joined = await joinWithFirstMatchingKp(welcomeBytes, candidates);
69940
70093
  if (!joined) {
69941
- console.error(`[SecureChannel] No matching KeyPackage found for Welcome \u2014 cannot join group ${groupId?.slice(0, 8)}. The Welcome is encrypted to a published KP's public key; none of our ${candidateCount} candidate bundle(s) decrypted it. The existing Welcome recovery mechanism will re-request.`);
70094
+ console.error(`[SecureChannel] No matching KeyPackage found for Welcome \u2014 cannot join group ${groupId?.slice(0, 8)}. The Welcome is encrypted to a published KP's public key; none of our ${candidateCount} candidate bundle(s) decrypted it. Requesting a re-Welcome with a fresh KeyPackage.`);
70095
+ if (groupId) void this._requestWelcomeSelfHeal(groupId);
69942
70096
  return;
69943
70097
  }
69944
70098
  const mgr = joined.mgr;
@@ -98846,16 +99000,76 @@ function buildClientVersion2(version2) {
98846
99000
  return `@agentvault/grok-bridge@${version2 ?? "dev"}`;
98847
99001
  }
98848
99002
 
99003
+ // src/reap.ts
99004
+ var DEFAULT_KILL_TIMEOUT_MS = 5e3;
99005
+ var SIGKILL_GRACE_MS = 250;
99006
+ function childHasExited(child) {
99007
+ return child.exitCode !== null && child.exitCode !== void 0 || child.signalCode !== null && child.signalCode !== void 0;
99008
+ }
99009
+ function waitForChildExit(child, killTimeoutMs, log) {
99010
+ if (childHasExited(child)) return Promise.resolve();
99011
+ return new Promise((resolve3) => {
99012
+ let settled = false;
99013
+ const timers = [];
99014
+ const done = () => {
99015
+ if (settled) return;
99016
+ settled = true;
99017
+ for (const t2 of timers) clearTimeout(t2);
99018
+ resolve3();
99019
+ };
99020
+ child.on("exit", done);
99021
+ timers.push(
99022
+ setTimeout(() => {
99023
+ log(`grok child ignored SIGTERM after ${killTimeoutMs}ms \u2014 escalating to SIGKILL`);
99024
+ try {
99025
+ child.kill?.("SIGKILL");
99026
+ } catch {
99027
+ }
99028
+ timers.push(setTimeout(done, SIGKILL_GRACE_MS));
99029
+ }, killTimeoutMs)
99030
+ );
99031
+ });
99032
+ }
99033
+ async function reap(r2, opts = {}) {
99034
+ const log = opts.log ?? (() => {
99035
+ });
99036
+ const killTimeoutMs = opts.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
99037
+ const failures = [];
99038
+ const attempt = async (label, fn) => {
99039
+ try {
99040
+ await fn();
99041
+ } catch (e) {
99042
+ const msg = `${label}: ${e instanceof Error ? e.message : String(e)}`;
99043
+ failures.push(msg);
99044
+ log(`reap: ${msg}`);
99045
+ }
99046
+ };
99047
+ if (r2.channel) await attempt("channel.stop", () => r2.channel.stop());
99048
+ if (r2.driver) await attempt("driver.stop", () => r2.driver.stop());
99049
+ if (r2.child) await waitForChildExit(r2.child, killTimeoutMs, log);
99050
+ if (r2.mcp) await attempt("mcp.close", () => r2.mcp.close());
99051
+ return { ok: failures.length === 0, failures };
99052
+ }
99053
+
98849
99054
  // src/index.ts
98850
99055
  var shuttingDown = false;
99056
+ var exiting = false;
98851
99057
  var driver;
98852
99058
  var mcp;
98853
- var shutdown = async (code = 0) => {
98854
- if (shuttingDown) return;
99059
+ var channelRef;
99060
+ var childRef;
99061
+ var reapAll = async () => {
98855
99062
  shuttingDown = true;
99063
+ return reap(
99064
+ { channel: channelRef, driver, mcp, child: childRef },
99065
+ { log: (m2) => logLine("[grok-bridge] " + m2) }
99066
+ );
99067
+ };
99068
+ var shutdown = async (code = 0) => {
99069
+ if (exiting) return;
99070
+ exiting = true;
98856
99071
  try {
98857
- await driver?.stop();
98858
- await mcp?.close();
99072
+ await reapAll();
98859
99073
  } finally {
98860
99074
  process.exit(code);
98861
99075
  }
@@ -98867,7 +99081,53 @@ async function main() {
98867
99081
  })) {
98868
99082
  process.exit(process.exitCode ?? 0);
98869
99083
  }
99084
+ const info = parseInfoFlags(process.argv.slice(2));
99085
+ if (info.showVersion || info.showHelp) {
99086
+ const version2 = buildClientVersion2(
99087
+ true ? "0.2.1" : void 0
99088
+ );
99089
+ if (info.showVersion) printLine(version2);
99090
+ if (info.showHelp) {
99091
+ printLine(
99092
+ [
99093
+ "",
99094
+ `${version2}`,
99095
+ "",
99096
+ " Bridges a Grok agent into E2E-encrypted AgentVault DMs and rooms.",
99097
+ "",
99098
+ "USAGE",
99099
+ " agentvault-grok-bridge [<invite-token>] [--install]",
99100
+ " agentvault-grok-bridge <install|uninstall|restart|status|logs>",
99101
+ "",
99102
+ "FLAGS",
99103
+ " --install Enrol, install a supervised OS service, then EXIT so",
99104
+ " your terminal returns. Requires a global install:",
99105
+ " npm i -g @agentvault/grok-bridge",
99106
+ " --version, -v Print the version.",
99107
+ " --help, -h Print this.",
99108
+ "",
99109
+ "ENV",
99110
+ " AV_AGENT_NAME The agent's AgentVault name. Required.",
99111
+ " AV_INVITE_TOKEN First enrolment only; a saved data dir reconnects without it.",
99112
+ " AV_DATA_DIR Credential/state dir. Defaults per agent.",
99113
+ "",
99114
+ "NOTES",
99115
+ " Run `grok login` first. XAI_API_KEY from your shell is NOT inherited by a",
99116
+ " supervised service, so --install refuses rather than run mute.",
99117
+ " To restart later use `restart`, never `install` \u2014 install re-bakes the",
99118
+ " service from your current shell and drops env from the original install.",
99119
+ ""
99120
+ ].join("\n")
99121
+ );
99122
+ }
99123
+ process.exit(0);
99124
+ }
98870
99125
  const cfg = loadConfig(process.env, process.argv.slice(2));
99126
+ if (cfg.installAfterEnroll) {
99127
+ assertInstallableEntrypoint(process.argv[1]);
99128
+ const { selectBackend: selectBackend2 } = await Promise.resolve().then(() => (init_backend(), backend_exports));
99129
+ selectBackend2(process.platform);
99130
+ }
98871
99131
  mkdirSync5(cfg.workspaceDir, { recursive: true });
98872
99132
  mkdirSync5(cfg.fenceHome, { recursive: true });
98873
99133
  seedFenceCredential({
@@ -98882,9 +99142,10 @@ async function main() {
98882
99142
  agentName: cfg.agentName,
98883
99143
  platform: "node",
98884
99144
  clientVersion: buildClientVersion2(
98885
- true ? "0.1.0" : void 0
99145
+ true ? "0.2.1" : void 0
98886
99146
  )
98887
99147
  });
99148
+ channelRef = channel;
98888
99149
  let session;
98889
99150
  mcp = await startRoomMcpServer({
98890
99151
  // `session` is assigned further down. The current ordering is safe — grok
@@ -98902,7 +99163,7 @@ async function main() {
98902
99163
  },
98903
99164
  log: (m2) => logLine(m2)
98904
99165
  });
98905
- const pluginDir = join12(cfg.fenceHome, "av-room-plugin");
99166
+ const pluginDir = join13(cfg.fenceHome, "av-room-plugin");
98906
99167
  writeRoomPluginHttp(pluginDir, mcp.url);
98907
99168
  const spawnSpec = grokSpawnArgs({
98908
99169
  grokPath: cfg.grokPath,
@@ -98917,6 +99178,7 @@ async function main() {
98917
99178
  env: spawnSpec.env,
98918
99179
  stdio: ["pipe", "pipe", "pipe"]
98919
99180
  });
99181
+ childRef = child;
98920
99182
  child.on("error", (err) => {
98921
99183
  logLine(`[grok-bridge] fatal: ${describeSpawnFailure(err, spawnSpec)}`);
98922
99184
  process.exit(1);
@@ -98966,6 +99228,23 @@ async function main() {
98966
99228
  log: (m2) => logLine("[grok-bridge] " + m2),
98967
99229
  onTerminal: (_reason, o2) => void shutdown(o2.restart ? 1 : 0)
98968
99230
  });
99231
+ if (cfg.installAfterEnroll) {
99232
+ const { installOnReady: installOnReady2 } = await Promise.resolve().then(() => (init_install_on_ready(), install_on_ready_exports));
99233
+ installOnReady2(channel, cfg, {
99234
+ entrypoint: process.argv[1],
99235
+ env: process.env,
99236
+ // FOUND LIVE 2026-08-16: the owner's activation greeting is usually already
99237
+ // being answered when `ready` fires, and reaping mid-turn killed the reply
99238
+ // for good — the message was acked, so the service never saw it again.
99239
+ // GrokSession.idle() resolves when the queue drains.
99240
+ waitForIdle: () => session.idle(),
99241
+ // The one shared teardown. `exiting` is deliberately NOT set here: Ctrl-C
99242
+ // must keep working for the whole multi-second launchctl window.
99243
+ reap: reapAll,
99244
+ exit: (code) => process.exit(code),
99245
+ log: (m2) => logLine("[grok-bridge] " + m2)
99246
+ });
99247
+ }
98969
99248
  await channel.start();
98970
99249
  }
98971
99250
  main().catch(async (err) => {
package/dist/log.d.ts CHANGED
@@ -19,4 +19,16 @@ export declare function stamp(message: string): string;
19
19
  * just anchored to a clock.
20
20
  */
21
21
  export declare function logLine(...args: unknown[]): void;
22
+ /**
23
+ * Plain stdout, no timestamp. For CLI answers a human or a script reads —
24
+ * `--version` and `--help` — not for daemon logging.
25
+ *
26
+ * `logLine` stamps every line and writes to stderr, which is right for a daemon
27
+ * and wrong here: `agentvault-grok-bridge --version` must print a version, not a
28
+ * version wearing a date. Anything a script would parse belongs on stdout.
29
+ *
30
+ * This lives in log.ts because the "no bare console sinks" guard test excludes
31
+ * only this file — the sink belongs here, and nowhere else.
32
+ */
33
+ export declare function printLine(text: string): void;
22
34
  //# sourceMappingURL=log.d.ts.map
package/dist/reap.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The ONE teardown path. Both `shutdown()` and the `--install` hand-off call this;
3
+ * nothing copies its body.
4
+ *
5
+ * WHY IT IS SHARED. An earlier design inlined a second copy inside the install
6
+ * flow. That copy stopped only the driver and the MCP listener — it never closed
7
+ * the AgentVault WebSocket, and it did not wait for the grok child to die. Both
8
+ * omissions are invisible in a unit test that substitutes the teardown closure,
9
+ * and both matter: `LaunchdBackend.install` runs bootout -> poll-until-gone ->
10
+ * bootstrap -> kickstart synchronously, so the supervised service is alive the
11
+ * instant install returns. A half-reaped foreground process means two ACP sessions
12
+ * and two WebSockets for one device.
13
+ */
14
+ export interface Reapable {
15
+ /** SecureChannel. `stop()` closes the WS, flushes pending ACKs, and sets the
16
+ * stopped flag so it will not reconnect. */
17
+ channel?: {
18
+ stop(): Promise<void>;
19
+ };
20
+ driver?: {
21
+ stop(): Promise<void>;
22
+ };
23
+ mcp?: {
24
+ close(): Promise<void>;
25
+ };
26
+ /** The grok child. `exitCode`/`signalCode` are read to detect an ALREADY-dead
27
+ * child; both are non-null once it has exited.
28
+ *
29
+ * ⚠️ The signal parameter is `NodeJS.Signals`, NOT `string`. A real
30
+ * `ChildProcess.kill` is `(signal?: NodeJS.Signals | number) => boolean`, and
31
+ * parameter types are contravariant — declaring `string` here makes a real
32
+ * ChildProcess UNASSIGNABLE to this shape and fails the strict build at the
33
+ * index.ts call site. `signalCode` is `NodeJS.Signals | null` for the same
34
+ * reason. Verified: widening it to `string` reintroduces TS2322. */
35
+ child?: {
36
+ kill?: (signal: NodeJS.Signals) => unknown;
37
+ on(event: "exit", listener: () => void): unknown;
38
+ exitCode?: number | null;
39
+ signalCode?: NodeJS.Signals | null;
40
+ };
41
+ }
42
+ export interface ReapResult {
43
+ /** False if any stage failed. Callers that treat a complete teardown as a
44
+ * PRECONDITION (the --install hand-off does) must refuse to continue on false
45
+ * rather than install over a socket that may still be open. */
46
+ ok: boolean;
47
+ failures: string[];
48
+ }
49
+ /**
50
+ * Tear everything down, in the one order that is safe.
51
+ *
52
+ * CHANNEL FIRST. Closing it stops new inbound before anything else is dismantled.
53
+ * With the channel last, the WebSocket keeps delivering room and DM traffic into a
54
+ * session whose driver is already dying, for as long as the child takes to exit —
55
+ * turn failures and dropped messages inside the very window this function exists
56
+ * to make empty. `stop()` also flushes pending ACKs, which it can only do over a
57
+ * socket that is still up.
58
+ *
59
+ * Every stage is guarded separately: one failure must not strand the others. A
60
+ * driver that throws must never leave the WebSocket open.
61
+ */
62
+ export declare function reap(r: Reapable, opts?: {
63
+ killTimeoutMs?: number;
64
+ log?: (m: string) => void;
65
+ }): Promise<ReapResult>;
66
+ //# sourceMappingURL=reap.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { type BridgeConfig } from "../config.js";
2
+ import type { ReapResult } from "../reap.js";
3
+ import { type ServiceBackend } from "./backend.js";
4
+ export interface InstallOnReadyDeps {
5
+ entrypoint: string;
6
+ env: NodeJS.ProcessEnv;
7
+ backend?: ServiceBackend;
8
+ hasCreds?: (dataDir: string) => boolean;
9
+ /** Contents of the installed launcher, or undefined when none is installed. */
10
+ readLauncher?: (path: string) => string | undefined;
11
+ fenceAuthExists?: (fenceHome: string) => boolean;
12
+ /** Resolves when no turn is in flight (GrokSession.idle). See rule 2. */
13
+ waitForIdle?: () => Promise<void>;
14
+ /** Bound on that wait. The terminal coming back is the product promise, so a
15
+ * wedged turn must never hold the customer's shell open. */
16
+ idleTimeoutMs?: number;
17
+ /** The ONE teardown (src/reap.ts). Must complete before launchctl runs. */
18
+ reap: () => Promise<ReapResult>;
19
+ exit: (code: number) => void;
20
+ log: (msg: string) => void;
21
+ }
22
+ interface ReadyChannel {
23
+ on(ev: "ready", cb: () => void): unknown;
24
+ }
25
+ /**
26
+ * One-paste install: enrol in the foreground, hand off to the supervisor, exit —
27
+ * so the customer's terminal comes back and the agent survives a reboot.
28
+ *
29
+ * Four rules this module exists to enforce.
30
+ *
31
+ * 1. REFUSE BEFORE REAPING. Everything that can say no does so before anything is
32
+ * torn down. Refusing afterwards leaves the customer with an approved device,
33
+ * a spent invite token, and no running process — strictly worse than never
34
+ * having guarded at all.
35
+ *
36
+ * 2. REAP COMPLETELY, THEN BOOTSTRAP. `LaunchdBackend.install` runs bootout →
37
+ * poll-until-gone → bootstrap → kickstart synchronously, so the service is
38
+ * live the instant install returns. A partial teardown means two ACP sessions
39
+ * and two WebSockets on one device. `reap()` reports failure rather than
40
+ * swallowing it, and a failed reap is a refusal.
41
+ *
42
+ * 3. COMPARE, DON'T DETECT. `status().loaded` means only that a plist with this
43
+ * label exists. The old `install` subcommand never guarded its entrypoint
44
+ * (subcommand.ts), so a customer can hold a LOADED plist baked against a
45
+ * pruned npx cache; restarting that repairs nothing and reports success.
46
+ * Restart only when a service is loaded AND its baked entrypoint is the one we
47
+ * would install. Compare the ENTRYPOINT, never the whole launcher script —
48
+ * that embeds the install-time PATH and is volatile across shells.
49
+ *
50
+ * 4. VERIFY BEFORE CLAIMING. `backend.install` returns void, and launchd's
51
+ * implementation runs `kickstart` regardless of every `bootstrap` exit code.
52
+ * This log line is the customer's only feedback, so it comes after a
53
+ * `status()` that returned loaded:true.
54
+ */
55
+ export declare function installOnReady(channel: ReadyChannel, cfg: BridgeConfig, deps: InstallOnReadyDeps): void;
56
+ export {};
57
+ //# sourceMappingURL=install-on-ready.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-on-ready.d.ts","sourceRoot":"","sources":["../../src/service/install-on-ready.ts"],"names":[],"mappings":"AAEA,OAAO,EAA4C,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAElE,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACpD,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC;iEAC6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B;AAMD,UAAU,YAAY;IACpB,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,kBAAkB,GACvB,IAAI,CAgHN"}
@@ -13,4 +13,23 @@ export declare function renderLauncher(opts: {
13
13
  /** Write the launcher executable (0755) into the data-dir before the backend
14
14
  * points the service at it. Shared by both platform backends. */
15
15
  export declare function writeLauncher(deps: BackendDeps, spec: ServiceSpec): void;
16
+ /**
17
+ * Read back the entrypoint `renderLauncher` baked into an installed launcher.
18
+ *
19
+ * THE READER LIVES BESIDE THE WRITER ON PURPOSE. This pair decides
20
+ * restart-versus-reinstall, and a parser that drifts from the renderer fails
21
+ * SILENTLY — it just answers `undefined` forever, which the caller reads as
22
+ * "stale", which means every run re-installs and silently re-bakes the plist.
23
+ *
24
+ * WHY PARSE RATHER THAN BYTE-COMPARE THE SCRIPT. `renderLauncher` embeds the
25
+ * INSTALL-TIME PATH (`export PATH='<installPath>':"$PATH"`), taken from whichever
26
+ * shell ran the install. A whole-file comparison therefore reports "different"
27
+ * whenever the customer opens a new terminal, installs a brew formula, or
28
+ * switches node version manager — making the config-losing re-install the DEFAULT
29
+ * path. The entrypoint is the durable half, and the only half that can go stale
30
+ * (an npm cache gets pruned; the launcher keeps pointing at it).
31
+ *
32
+ * Returns undefined when there is no file, or when it is not one of ours.
33
+ */
34
+ export declare function parseLauncherEntrypoint(script: string | undefined): string | undefined;
16
35
  //# sourceMappingURL=launcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../../src/service/launcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,eAAO,MAAM,cAAc,UAO1B,CAAC;AAcF;;;wEAGwE;AACxE,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB,GAAG,MAAM,CAoBT;AAED;kEACkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAIxE"}
1
+ {"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../../src/service/launcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,eAAO,MAAM,cAAc,UAO1B,CAAC;AAcF;;;wEAGwE;AACxE,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB,GAAG,MAAM,CAoBT;AAED;kEACkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAIxE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOtF"}
@@ -1 +1 @@
1
- {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../src/service/subcommand.ts"],"names":[],"mappings":"AAGA,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;AAQD;;uFAEuF;AACvF,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,CAAC,EAAE,cAAc,GACpB,OAAO,CA4DT"}
1
+ {"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../src/service/subcommand.ts"],"names":[],"mappings":"AAGA,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;AAQD;;uFAEuF;AACvF,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,CAAC,EAAE,cAAc,GACpB,OAAO,CAoFT"}
package/package.json CHANGED
@@ -1,13 +1,21 @@
1
1
  {
2
2
  "name": "@agentvault/grok-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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",
7
7
  "types": "dist/index.d.ts",
8
- "bin": { "agentvault-grok-bridge": "dist/index.js" },
9
- "engines": { "node": ">=18" },
10
- "files": ["dist", "README.md", "!dist/*.map"],
8
+ "bin": {
9
+ "agentvault-grok-bridge": "dist/index.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "!dist/*.map"
18
+ ],
11
19
  "scripts": {
12
20
  "build": "npm --prefix ../plugin run build && tsc --emitDeclarationOnly && node build.mjs",
13
21
  "start": "node dist/index.js",
@@ -16,7 +24,10 @@
16
24
  "typecheck": "tsc -p tsconfig.test.json",
17
25
  "prepublishOnly": "npm run build"
18
26
  },
19
- "dependencies": { "ws": "^8.16.0", "zod": "^4.3.6" },
27
+ "dependencies": {
28
+ "ws": "^8.16.0",
29
+ "zod": "^4.3.6"
30
+ },
20
31
  "devDependencies": {
21
32
  "@agentvault/agentvault": "file:../plugin",
22
33
  "esbuild": "^0.27.3",
@@ -24,6 +35,8 @@
24
35
  "vitest": "^2.0.0",
25
36
  "@types/node": "^20.0.0"
26
37
  },
27
- "publishConfig": { "access": "public" },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
28
41
  "license": "MIT"
29
42
  }