@q-agent/agent 0.1.0 → 0.1.6

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
@@ -17,7 +17,18 @@ Chromium is installed automatically: the first time you run `qagent-agent start`
17
17
  the agent downloads Playwright's Chromium if it isn't already present (one-time,
18
18
  ~100 MB, with visible progress). No manual `npx playwright install` step is needed.
19
19
 
20
- ## Install & run
20
+ ## Run with the UI (easiest)
21
+
22
+ ```bash
23
+ npx @q-agent/agent
24
+ ```
25
+
26
+ Running with no command opens a local web UI in your browser. Paste a pairing
27
+ code (from the Q-Agent app's **Local Agent** screen) and the server URL, click
28
+ **Connect**, and watch the pull/run progress live. The device token is stored
29
+ locally, so next time it reconnects and starts pulling automatically.
30
+
31
+ ## Command-line
21
32
 
22
33
  ```bash
23
34
  npx @q-agent/agent pair <code> --server https://your-qagent-server.example.com/api
@@ -38,6 +49,46 @@ node dist/src/cli.js pair <code> --server http://127.0.0.1:8787
38
49
  node dist/src/cli.js start
39
50
  ```
40
51
 
52
+ ## Windows binary (no Node required)
53
+
54
+ For machines without Node, build a self-contained Windows bundle:
55
+
56
+ ```bash
57
+ cd agent
58
+ npm install
59
+ npm run package:win
60
+ ```
61
+
62
+ This produces `dist-bin/qagent-agent-win-x64/` — a `qagent-agent.exe` (the CLI as
63
+ a Node Single Executable Application) plus a bundled Node runtime, production
64
+ `node_modules`, and `vendor/`. Zip that folder for distribution; the user unzips
65
+ and runs it with **no global Node installed**:
66
+
67
+ ```bat
68
+ qagent-agent.exe pair <code> --server https://your-qagent-server.example.com/api
69
+ qagent-agent.exe start
70
+ ```
71
+
72
+ Chromium still auto-installs on first `start`. Build the bundle on Windows with a
73
+ Node 20+ that supports SEA (the local `node.exe` is used as the executable base —
74
+ no compiler or download needed).
75
+
76
+ ## Desktop app (Electron)
77
+
78
+ A native window that wraps the same web UI:
79
+
80
+ ```bash
81
+ cd agent
82
+ npm install
83
+ npm run desktop # dev: builds, then opens the app window
84
+ npm run dist:desktop # build a Windows installer (electron-builder → dist-bin/desktop/)
85
+ ```
86
+
87
+ The Electron shell runs the agent's UI server in-process and shows it in a
88
+ window — pair by entering the 6-digit code + server URL, then watch progress.
89
+ Child processes (Playwright / login capture) run via Electron-as-Node
90
+ (`ELECTRON_RUN_AS_NODE`), so no separate Node install is needed.
91
+
41
92
  ## Commands
42
93
 
43
94
  | Command | Description |
package/dist/src/api.js CHANGED
@@ -41,10 +41,15 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.ApiError = void 0;
43
43
  exports.redeemDevice = redeemDevice;
44
+ exports.disconnectDevice = disconnectDevice;
44
45
  exports.claimNextJob = claimNextJob;
46
+ exports.claimNextCapture = claimNextCapture;
47
+ exports.postCaptureComplete = postCaptureComplete;
45
48
  exports.postEvent = postEvent;
46
49
  exports.postResult = postResult;
47
50
  exports.postEvidence = postEvidence;
51
+ exports.postHealFix = postHealFix;
52
+ exports.postHealFinalize = postHealFinalize;
48
53
  exports.postComplete = postComplete;
49
54
  const fs = __importStar(require("fs"));
50
55
  /** Raised for any non-2xx/204 response; carries the HTTP status for callers that care (e.g. 409). */
@@ -76,6 +81,16 @@ async function redeemDevice(serverUrl, code, name) {
76
81
  await throwIfNotOk(res);
77
82
  return (await res.json());
78
83
  }
84
+ /** Self-revoke this device on the server (the "Disconnect" action). Best-effort:
85
+ * the caller wipes the local token regardless, so a failure here only means the
86
+ * server list clears on `last_seen_at` staleness instead of immediately. */
87
+ async function disconnectDevice(cfg) {
88
+ const res = await fetch(`${cfg.serverUrl}/agent/disconnect`, {
89
+ method: "POST",
90
+ headers: authHeaders(cfg.deviceToken),
91
+ });
92
+ await throwIfNotOk(res);
93
+ }
79
94
  /** Long-poll claim the next queued job for this device's owner. `null` on 204 (nothing queued). */
80
95
  async function claimNextJob(cfg) {
81
96
  const res = await fetch(`${cfg.serverUrl}/agent/jobs/next`, {
@@ -87,6 +102,26 @@ async function claimNextJob(cfg) {
87
102
  await throwIfNotOk(res);
88
103
  return (await res.json());
89
104
  }
105
+ /** Claim the next queued auth-capture for this device's owner. `null` on 204. */
106
+ async function claimNextCapture(cfg) {
107
+ const res = await fetch(`${cfg.serverUrl}/agent/auth/next`, {
108
+ method: "POST",
109
+ headers: authHeaders(cfg.deviceToken),
110
+ });
111
+ if (res.status === 204)
112
+ return null;
113
+ await throwIfNotOk(res);
114
+ return (await res.json());
115
+ }
116
+ /** Report a capture's outcome so the server clears "capturing" + stamps the marker. */
117
+ async function postCaptureComplete(cfg, captureId, ok, error) {
118
+ const res = await fetch(`${cfg.serverUrl}/agent/auth/${captureId}/complete`, {
119
+ method: "POST",
120
+ headers: { ...authHeaders(cfg.deviceToken), "Content-Type": "application/json" },
121
+ body: JSON.stringify({ ok, error }),
122
+ });
123
+ await throwIfNotOk(res);
124
+ }
90
125
  /** Push one progress event; the server re-emits it on the run's WebSocket unchanged. */
91
126
  async function postEvent(cfg, executionId, event, payload) {
92
127
  const res = await fetch(`${cfg.serverUrl}/agent/jobs/${executionId}/events`, {
@@ -120,6 +155,25 @@ async function postEvidence(cfg, executionId, ev) {
120
155
  });
121
156
  await throwIfNotOk(res);
122
157
  }
158
+ /** Ask the server to classify + fix one failed heal attempt (Claude + KB live server-side). */
159
+ async function postHealFix(cfg, caseId, body) {
160
+ const res = await fetch(`${cfg.serverUrl}/agent/heal/${caseId}/fix`, {
161
+ method: "POST",
162
+ headers: { ...authHeaders(cfg.deviceToken), "Content-Type": "application/json" },
163
+ body: JSON.stringify(body),
164
+ });
165
+ await throwIfNotOk(res);
166
+ return (await res.json());
167
+ }
168
+ /** Persist the heal's final outcome + feed a passing DOM-grounded heal into the KB. */
169
+ async function postHealFinalize(cfg, caseId, body) {
170
+ const res = await fetch(`${cfg.serverUrl}/agent/heal/${caseId}/finalize`, {
171
+ method: "POST",
172
+ headers: { ...authHeaders(cfg.deviceToken), "Content-Type": "application/json" },
173
+ body: JSON.stringify(body),
174
+ });
175
+ await throwIfNotOk(res);
176
+ }
123
177
  /** Finalize an execution with its aggregate counts + captured log tail. */
124
178
  async function postComplete(cfg, executionId, body) {
125
179
  const res = await fetch(`${cfg.serverUrl}/agent/jobs/${executionId}/complete`, {
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bus = void 0;
4
+ exports.emit = emit;
5
+ exports.recentEvents = recentEvents;
6
+ /**
7
+ * In-process event bus bridging the job loop (runner.ts) to the local web UI
8
+ * (ui.ts). The runner emits structured activity events; the UI streams them to
9
+ * the browser over SSE. A small ring buffer lets a UI that connects mid-run
10
+ * replay recent activity.
11
+ */
12
+ const node_events_1 = require("node:events");
13
+ const BUFFER_MAX = 300;
14
+ const buffer = [];
15
+ exports.bus = new node_events_1.EventEmitter();
16
+ /** Record + broadcast an agent activity event. */
17
+ function emit(type, data = {}) {
18
+ const event = { ts: Date.now(), type, ...data };
19
+ buffer.push(event);
20
+ if (buffer.length > BUFFER_MAX)
21
+ buffer.shift();
22
+ exports.bus.emit("event", event);
23
+ }
24
+ /** Snapshot of buffered events, for a UI client that just connected. */
25
+ function recentEvents() {
26
+ return buffer.slice();
27
+ }
package/dist/src/cli.js CHANGED
@@ -48,22 +48,28 @@ const os = __importStar(require("os"));
48
48
  const config_1 = require("./config");
49
49
  const api_1 = require("./api");
50
50
  const runner_1 = require("./runner");
51
+ const ui_1 = require("./ui");
52
+ /** How the user invoked this agent, for accurate "run X" hints: the bare command
53
+ * inside a packaged bundle, else the `npx` form of the published package. */
54
+ function invocation() {
55
+ return process.pkg ? "qagent-agent" : "npx @q-agent/agent";
56
+ }
51
57
  const program = new commander_1.Command();
52
58
  program.name("qagent-agent").description("Q-Agent Local Agent — runs Playwright locally for this machine's owner.");
53
59
  program
54
60
  .command("pair")
55
61
  .description("Redeem a one-time pairing code (from the Q-Agent SPA) for a device token")
56
62
  .argument("<code>", "pairing code shown in the SPA")
57
- .option("--server <url>", "Q-Agent server base URL", "http://127.0.0.1:8787")
63
+ .option("--server <url>", "Q-Agent server base URL (defaults to the server baked into this build)")
58
64
  .option("--name <name>", "device name shown in the paired-devices list", os.hostname())
59
65
  .action(async (code, opts) => {
60
- const serverUrl = opts.server.replace(/\/+$/, "");
66
+ const serverUrl = (opts.server || (0, config_1.defaultServerUrl)() || "http://127.0.0.1:8787").replace(/\/+$/, "");
61
67
  try {
62
68
  const { deviceToken, deviceId } = await (0, api_1.redeemDevice)(serverUrl, code, opts.name);
63
69
  const cfg = { serverUrl, deviceToken, deviceId, deviceName: opts.name };
64
70
  (0, config_1.saveConfig)(cfg);
65
71
  console.log(`Paired as device #${deviceId} (${opts.name}) against ${serverUrl}`);
66
- console.log("Device token stored — run `qagent-agent start` to begin claiming jobs.");
72
+ console.log(`Device token stored — run \`${invocation()} start\` to begin claiming jobs.`);
67
73
  }
68
74
  catch (err) {
69
75
  console.error("Pairing failed:", err.message);
@@ -77,7 +83,7 @@ program
77
83
  .action(async (opts) => {
78
84
  const cfg = (0, config_1.loadConfig)();
79
85
  if (!cfg) {
80
- console.error("Not paired yet — run `qagent-agent pair <code>` first.");
86
+ console.error(`Not paired yet — run \`${invocation()} pair <code>\` first.`);
81
87
  process.exitCode = 1;
82
88
  return;
83
89
  }
@@ -99,7 +105,7 @@ program
99
105
  .action(() => {
100
106
  const cfg = (0, config_1.loadConfig)();
101
107
  if (!cfg) {
102
- console.log("Not paired. Run `qagent-agent pair <code>` to pair this machine.");
108
+ console.log(`Not paired. Run \`${invocation()} pair <code>\` to pair this machine.`);
103
109
  return;
104
110
  }
105
111
  console.log(`Paired as device #${cfg.deviceId} (${cfg.deviceName})`);
@@ -110,7 +116,15 @@ program
110
116
  .description("Forget the paired device token")
111
117
  .action(() => {
112
118
  (0, config_1.clearConfig)();
113
- console.log("Device token cleared. Run `qagent-agent pair <code>` to pair again.");
119
+ console.log(`Device token cleared. Run \`${invocation()} pair <code>\` to pair again.`);
120
+ });
121
+ program
122
+ .command("ui", { isDefault: true })
123
+ .description("Open the local web UI to pair and watch progress in your browser (default)")
124
+ .option("--port <port>", "port for the local UI server (default 7420)")
125
+ .option("--no-open", "start the UI server without opening a browser")
126
+ .action((opts) => {
127
+ (0, ui_1.startUi)({ port: opts.port ? Number(opts.port) : undefined, open: opts.open !== false });
114
128
  });
115
129
  program.parseAsync(process.argv).catch((err) => {
116
130
  console.error(err);
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.defaultServerUrl = defaultServerUrl;
41
42
  exports.configDir = configDir;
42
43
  exports.loadConfig = loadConfig;
43
44
  exports.saveConfig = saveConfig;
@@ -45,6 +46,30 @@ exports.clearConfig = clearConfig;
45
46
  const fs = __importStar(require("fs"));
46
47
  const os = __importStar(require("os"));
47
48
  const path = __importStar(require("path"));
49
+ /** The server URL baked into this build, so the packaged desktop app can
50
+ * pre-fill it and the user only types the 6-digit code (no URL to enter).
51
+ *
52
+ * Resolution: a runtime ``QAGENT_SERVER`` env override wins (dev/testing), then
53
+ * the ``qagentServer`` field electron-builder merges into the packaged
54
+ * ``package.json`` (see build.extraMetadata). Empty for the generic npm/npx
55
+ * build — those callers still pass ``--server`` (or type the URL) as before. */
56
+ function defaultServerUrl() {
57
+ const fromEnv = (process.env.QAGENT_SERVER || "").trim();
58
+ if (fromEnv)
59
+ return fromEnv.replace(/\/+$/, "");
60
+ try {
61
+ // dist/src/config.js → ../../package.json (the app's own manifest, which in
62
+ // a packaged build carries the baked-in qagentServer).
63
+ const pkgPath = path.join(__dirname, "..", "..", "package.json");
64
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
65
+ if (typeof pkg.qagentServer === "string")
66
+ return pkg.qagentServer.trim().replace(/\/+$/, "");
67
+ }
68
+ catch {
69
+ // Generic build / manifest unreadable — no baked server.
70
+ }
71
+ return "";
72
+ }
48
73
  /** Directory holding the agent's persisted config + local session store. */
49
74
  function configDir() {
50
75
  return path.join(os.homedir(), ".qagent-agent");
@@ -44,19 +44,7 @@ exports.ensureChromium = ensureChromium;
44
44
  */
45
45
  const child_process_1 = require("child_process");
46
46
  const fs = __importStar(require("fs"));
47
- const path = __importStar(require("path"));
48
- function firstExisting(paths) {
49
- for (const p of paths) {
50
- if (fs.existsSync(p))
51
- return p;
52
- }
53
- return null;
54
- }
55
- /** The agent package's own node_modules (mirrors runner.ts's resolver). */
56
- function agentNodeModules() {
57
- return (firstExisting([path.join(__dirname, "..", "..", "node_modules"), path.join(__dirname, "..", "node_modules")]) ??
58
- path.join(__dirname, "..", "..", "node_modules"));
59
- }
47
+ const paths_1 = require("./paths");
60
48
  /** Path Playwright expects the Chromium build at, or null if it can't be resolved. */
61
49
  function chromiumExecutable() {
62
50
  try {
@@ -72,7 +60,9 @@ function chromiumExecutable() {
72
60
  * Ensure Chromium is installed, running `playwright install chromium` if it's missing.
73
61
  *
74
62
  * Fast no-op once the browser exists. Streams the download progress so a first-run
75
- * fetch (~100 MB) is visible rather than a silent hang.
63
+ * fetch (~100 MB) is visible rather than a silent hang. Invokes Playwright's CLI
64
+ * through the resolved node runtime (`node cli.js install chromium`) so it works
65
+ * from source, via npx, and in a packaged bundle alike.
76
66
  *
77
67
  * Returns:
78
68
  * true when Chromium is available (already present, or installed successfully);
@@ -83,13 +73,11 @@ async function ensureChromium() {
83
73
  if (exe && fs.existsSync(exe))
84
74
  return true;
85
75
  console.log("Chromium not found — installing Playwright's Chromium (one-time download)...");
86
- const nm = agentNodeModules();
87
- const bin = path.join(nm, ".bin", process.platform === "win32" ? "playwright.cmd" : "playwright");
88
- const useBin = fs.existsSync(bin);
89
- const cmd = useBin ? bin : "npx";
90
- const args = useBin ? ["install", "chromium"] : ["playwright", "install", "chromium"];
91
76
  const code = await new Promise((resolve) => {
92
- const child = (0, child_process_1.spawn)(cmd, args, { stdio: "inherit", shell: process.platform === "win32" });
77
+ const child = (0, child_process_1.spawn)((0, paths_1.nodeBin)(), [(0, paths_1.playwrightCli)(), "install", "chromium"], {
78
+ stdio: "inherit",
79
+ env: { ...process.env, ...(0, paths_1.childNodeEnv)() },
80
+ });
93
81
  child.on("close", resolve);
94
82
  child.on("error", () => resolve(null));
95
83
  });
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.firstExisting = firstExisting;
37
+ exports.isElectron = isElectron;
38
+ exports.childNodeEnv = childNodeEnv;
39
+ exports.packagedRoot = packagedRoot;
40
+ exports.nodeBin = nodeBin;
41
+ exports.agentNodeModules = agentNodeModules;
42
+ exports.playwrightCli = playwrightCli;
43
+ exports.vendorCaptureScript = vendorCaptureScript;
44
+ /**
45
+ * Runtime path resolution for the agent's child processes (Playwright CLI +
46
+ * the headed-login capture script), working in three layouts:
47
+ *
48
+ * 1. from source / `npx @q-agent/agent` — `node` on PATH, deps in the package's
49
+ * own node_modules;
50
+ * 2. a packaged Windows bundle (built by `scripts/package-win.mjs`, run as a
51
+ * `@yao-pkg/pkg` binary) — a bundled `node.exe`, `node_modules/`, and
52
+ * `vendor/` sit next to the executable.
53
+ *
54
+ * Everything the agent spawns is invoked as `nodeBin() <script/cli.js> …`, so
55
+ * the same code path works whether `node` comes from PATH or the bundle.
56
+ */
57
+ const fs = __importStar(require("fs"));
58
+ const path = __importStar(require("path"));
59
+ function firstExisting(candidates) {
60
+ for (const c of candidates) {
61
+ if (fs.existsSync(c))
62
+ return c;
63
+ }
64
+ return null;
65
+ }
66
+ /** True when running inside Electron (the desktop app) rather than plain Node. */
67
+ function isElectron() {
68
+ return Boolean(process.versions?.electron);
69
+ }
70
+ /**
71
+ * Environment additions for spawned child node processes. Under Electron,
72
+ * `process.execPath` is the Electron binary, so `nodeBin()` returns it — set
73
+ * ELECTRON_RUN_AS_NODE=1 so it runs the given script as Node rather than
74
+ * launching another app window.
75
+ */
76
+ function childNodeEnv() {
77
+ return isElectron() ? { ELECTRON_RUN_AS_NODE: "1" } : {};
78
+ }
79
+ /** True when running as a Node Single Executable Application (the packaged .exe). */
80
+ function isSea() {
81
+ try {
82
+ return require("node:sea").isSea();
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * The folder beside the executable in a packaged bundle (holds the bundled
90
+ * node runtime, node_modules, and vendor/). Null when running from source/npx.
91
+ * Set when running as a SEA binary (the Windows bundle) — or a pkg binary.
92
+ */
93
+ function packagedRoot() {
94
+ return isSea() || process.pkg ? path.dirname(process.execPath) : null;
95
+ }
96
+ /**
97
+ * Node runtime used to spawn child processes. Prefers the bundled `node.exe`
98
+ * in a packaged install (so no global Node is required), else `node` from PATH.
99
+ */
100
+ function nodeBin() {
101
+ const root = packagedRoot();
102
+ if (root) {
103
+ const bundled = path.join(root, process.platform === "win32" ? "node.exe" : "node");
104
+ if (fs.existsSync(bundled))
105
+ return bundled;
106
+ }
107
+ // From source / npx, the agent itself runs under a real Node — reuse it. An
108
+ // absolute path lets children spawn without a shell (safe with spaces in the
109
+ // path); `process.execPath` is only the pkg binary when packagedRoot() is set.
110
+ return process.execPath;
111
+ }
112
+ /** node_modules holding `playwright` + `@playwright/test`. */
113
+ function agentNodeModules() {
114
+ const root = packagedRoot();
115
+ if (root) {
116
+ const nm = path.join(root, "node_modules");
117
+ if (fs.existsSync(nm))
118
+ return nm;
119
+ }
120
+ // From source / npx / global install, let Node's own resolver find the
121
+ // node_modules that actually holds the deps. npm (and `npx`) HOIST
122
+ // `@playwright/test` and `playwright` to a parent node_modules — the npx
123
+ // cache root or the global root — rather than nesting them under the package,
124
+ // so a hardcoded relative guess misses them (the cause of the
125
+ // "Cannot find module '@playwright/test'" failure on npx/global installs).
126
+ // `.../node_modules/@playwright/test/package.json` → `.../node_modules`.
127
+ const testPkgJson = require.resolve("@playwright/test/package.json");
128
+ return path.dirname(path.dirname(path.dirname(testPkgJson)));
129
+ }
130
+ /** Playwright's CLI entry — drives both `test` (run specs) and `install` (browsers). */
131
+ function playwrightCli() {
132
+ const root = packagedRoot();
133
+ if (root) {
134
+ const bundled = path.join(root, "node_modules", "playwright", "cli.js");
135
+ if (fs.existsSync(bundled))
136
+ return bundled;
137
+ }
138
+ // Resolve via Node so it works whether npm nested or hoisted `playwright`.
139
+ return path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
140
+ }
141
+ /** The vendored headed-login capture script (`capture_auth.cjs`). */
142
+ function vendorCaptureScript() {
143
+ const root = packagedRoot();
144
+ const found = firstExisting([
145
+ ...(root ? [path.join(root, "vendor", "capture_auth.cjs")] : []),
146
+ path.join(__dirname, "..", "..", "vendor", "capture_auth.cjs"),
147
+ path.join(__dirname, "..", "vendor", "capture_auth.cjs"),
148
+ ]);
149
+ if (!found)
150
+ throw new Error("capture_auth.cjs not found — the agent package is missing vendor/");
151
+ return found;
152
+ }