@maintainer-pro/ai-bridge 0.1.29 → 0.1.31

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.
Files changed (2) hide show
  1. package/package.json +4 -3
  2. package/src/daemon.mjs +191 -47
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainer-pro/ai-bridge",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Local bridge daemon that pairs a machine to Maintainer Pro and configures multiple client sandboxes.",
5
5
  "keywords": [
6
6
  "maintainer-pro",
@@ -28,7 +28,8 @@
28
28
  "node": ">=22"
29
29
  },
30
30
  "dependencies": {
31
- "@maintainer-pro/ai-cli": "^0.1.15",
32
- "@maintainer-pro/ai-server": "^0.1.8"
31
+ "@maintainer-pro/ai-cli": "^0.1.16",
32
+ "@maintainer-pro/ai-server": "^0.1.8",
33
+ "ws": "^8.21.3"
33
34
  }
34
35
  }
package/src/daemon.mjs CHANGED
@@ -43,9 +43,21 @@ import {
43
43
  mergeCookieHeader,
44
44
  storeCorsCookies,
45
45
  } from "./cors-cookies.mjs";
46
+ import { WebSocket as WsWebSocket } from "ws";
46
47
 
47
48
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
48
49
  const requireFromHere = createRequire(import.meta.url);
50
+
51
+ /**
52
+ * Node 22+ has a global WHATWG WebSocket. Node 20 (still common on partner
53
+ * machines) does not — `new WebSocket()` throws "WebSocket is not defined".
54
+ * Fall back to the `ws` package, which exposes the same addEventListener API.
55
+ */
56
+ const WebSocketImpl =
57
+ typeof globalThis.WebSocket === "function"
58
+ ? globalThis.WebSocket
59
+ : WsWebSocket;
60
+ const usingWsFallback = WebSocketImpl === WsWebSocket;
49
61
  const PACKAGE_VERSION = readPackageVersion();
50
62
  const PACKAGE_VERSIONS = readPackageVersions();
51
63
  const HEARTBEAT_MS = 15_000;
@@ -451,13 +463,30 @@ async function loadAiCli() {
451
463
  /** @type {{ at: number, ids: string[] } | null} */
452
464
  let cliProviderCache = null;
453
465
 
454
- async function detectCliProviders() {
455
- if (cliProviderCache && Date.now() - cliProviderCache.at < 60_000) {
466
+ function missingCliSetupText(folder) {
467
+ const folderLine = folder
468
+ ? `No coding agent CLI is available in this folder:\n ${folder}`
469
+ : "No coding agent CLI found on this computer.";
470
+ return [
471
+ `${folderLine} Chat will not work until one is installed and reachable from this folder.`,
472
+ "",
473
+ " • Cursor Agent CLI — https://cursor.com (command: agent)",
474
+ " • Claude Code — https://docs.anthropic.com/en/docs/claude-code (command: claude)",
475
+ " • Antigravity — https://antigravity.google/ (command: agy)",
476
+ "",
477
+ "Then either sign in and re-run the Maintainer Pro command from a terminal where that CLI works, or set the full path in this folder's .env:",
478
+ " AI_CLI_PROVIDER=cursor",
479
+ " AI_CLI_COMMAND=C:\\\\Users\\\\you\\\\AppData\\\\Local\\\\cursor-agent\\\\agent.cmd",
480
+ ].join("\n");
481
+ }
482
+
483
+ async function detectCliProviders(force = false) {
484
+ if (!force && cliProviderCache && Date.now() - cliProviderCache.at < 60_000) {
456
485
  return cliProviderCache.ids;
457
486
  }
458
487
  try {
459
- const { resolveProvider } = await loadAiCli();
460
- const provider = await resolveProvider({ preference: "auto" });
488
+ const cli = await loadAiCli();
489
+ const provider = await cli.resolveProvider({ preference: "auto" });
461
490
  cliProviderCache = { at: Date.now(), ids: [provider.id] };
462
491
  return cliProviderCache.ids;
463
492
  } catch {
@@ -466,6 +495,65 @@ async function detectCliProviders() {
466
495
  }
467
496
  }
468
497
 
498
+ async function withCliProbe(result, folders) {
499
+ const roots = [...new Set((folders || []).filter(Boolean).map((row) => path.resolve(row)))];
500
+ let probe = { available: false };
501
+ for (const folder of roots) {
502
+ probe = await probeFolderCli(folder);
503
+ if (probe.available) break;
504
+ }
505
+ const folder = probe.folder || roots[0] || result.folderPath;
506
+ if (probe.available) {
507
+ log(
508
+ `coding agent CLI ready in ${folder} (${probe.providerId || "auto"}${
509
+ probe.source ? ` via ${probe.source}` : ""
510
+ })`
511
+ );
512
+ } else if (folder) {
513
+ activity(
514
+ result.sandboxId,
515
+ "warn",
516
+ `No coding agent CLI in ${folder}. Chat needs agent, claude, or agy — or AI_CLI_COMMAND in this folder's .env.`
517
+ );
518
+ }
519
+ return {
520
+ ...result,
521
+ missingCli: !probe.available,
522
+ cliProvider: probe.providerId || null,
523
+ cliCommand: probe.command || null,
524
+ warning: !probe.available
525
+ ? [result.warning, missingCliSetupText(folder)].filter(Boolean).join("\n\n")
526
+ : result.warning,
527
+ };
528
+ }
529
+
530
+ async function probeFolderCli(folder) {
531
+ const root = folder ? path.resolve(folder) : "";
532
+ if (!root) return { available: false };
533
+ try {
534
+ const cli = await loadAiCli();
535
+ if (typeof cli.probeCliInFolder === "function") {
536
+ return await cli.probeCliInFolder(root);
537
+ }
538
+ const provider = await cli.resolveProvider({
539
+ preference: "auto",
540
+ workspaceDir: root,
541
+ });
542
+ return { available: true, providerId: provider.id, folder: root };
543
+ } catch {
544
+ return { available: false, folder: root };
545
+ }
546
+ }
547
+
548
+ async function warnIfMissingCli() {
549
+ const ids = await detectCliProviders(true);
550
+ if (ids.length) {
551
+ log(`coding agent CLI ready (${ids.join(", ")})`);
552
+ return;
553
+ }
554
+ warn(missingCliSetupText());
555
+ }
556
+
469
557
  async function resolveWorkspaceHostApps(ws, opts = {}) {
470
558
  const folders = workspaceFolders(ws);
471
559
  const folder = folders[0] || path.resolve(ws.folderPath || "");
@@ -2984,7 +3072,7 @@ function attachProxyLocalWs(id, socket) {
2984
3072
  try {
2985
3073
  socket.binaryType = "arraybuffer";
2986
3074
  } catch {
2987
- /* WHATWG WebSocket in Node 22 */
3075
+ /* WHATWG WebSocket in Node 22; `ws` fallback on Node 20 */
2988
3076
  }
2989
3077
  proxyLocalSockets.set(id, socket);
2990
3078
  const announceOpen = () => {
@@ -3058,7 +3146,9 @@ function openProxyLocalWs(id, port, path, protocols) {
3058
3146
  let socket;
3059
3147
  try {
3060
3148
  // Vite HMR only accepts upgrades with subprotocol `vite-hmr` / `vite-ping`.
3061
- socket = proto.length ? new WebSocket(url, proto) : new WebSocket(url);
3149
+ socket = proto.length
3150
+ ? new WebSocketImpl(url, proto)
3151
+ : new WebSocketImpl(url);
3062
3152
  } catch (err) {
3063
3153
  if (index + 1 < hosts.length) {
3064
3154
  tryHost(index + 1);
@@ -3714,11 +3804,18 @@ async function startAiServerForWorkspace(ws, opts = {}) {
3714
3804
 
3715
3805
  const storeEnv = await loadSandboxStoreEnv(ws, cfg);
3716
3806
  const folderEnv = readProjectEnvValues(folder);
3807
+ const cliProbe = await probeFolderCli(folder);
3717
3808
  const dataDir = dataDirFor(folder, ws.sandboxId);
3718
3809
  const instanceEnv = {
3719
3810
  ...folderEnv,
3720
3811
  ...overrideEnv,
3721
3812
  ...storeEnv,
3813
+ ...(cliProbe.command && !folderEnv.AI_CLI_COMMAND
3814
+ ? { AI_CLI_COMMAND: cliProbe.command }
3815
+ : {}),
3816
+ ...(cliProbe.providerId && !folderEnv.AI_CLI_PROVIDER
3817
+ ? { AI_CLI_PROVIDER: cliProbe.providerId }
3818
+ : {}),
3722
3819
  AI_SERVER_URL: localAi,
3723
3820
  CORS_ORIGIN: overrideEnv.CORS_ORIGIN || folderEnv.CORS_ORIGIN || "",
3724
3821
  CORS_ORIGINS: overrideEnv.CORS_ORIGINS || folderEnv.CORS_ORIGINS || "",
@@ -4448,7 +4545,7 @@ async function setupWorkspace(cfg, action) {
4448
4545
  const host = workspaceHostReport(existingWs);
4449
4546
  const aiServerUp = await isChatServerOnPort(existingWs.port);
4450
4547
  log(`setup added folder ${resolved} sandbox=${shortId(sandboxId)}`);
4451
- return {
4548
+ return await withCliProbe({
4452
4549
  sandboxId,
4453
4550
  folderPath: existingWs.folderPath,
4454
4551
  extraFolders: extra,
@@ -4476,7 +4573,9 @@ async function setupWorkspace(cfg, action) {
4476
4573
  reasons: ports.reasons || [],
4477
4574
  projectInfo: existingWs.projectInfo || null,
4478
4575
  ignorePaths: access.ignorePaths,
4479
- };
4576
+ },
4577
+ workspaceFolders(existingWs)
4578
+ );
4480
4579
  }
4481
4580
 
4482
4581
  const client = configureClient({
@@ -4566,32 +4665,35 @@ async function setupWorkspace(cfg, action) {
4566
4665
  );
4567
4666
 
4568
4667
  const host = workspaceHostReport(entry);
4569
- return {
4570
- sandboxId,
4571
- folderPath: entry.folderPath,
4572
- extraFolders: entry.extraFolders || [],
4573
- addFolder: false,
4574
- port: entry.port,
4575
- appUrl: host.appUrl || entry.appUrl || appUrl,
4576
- origins: host.origins.length
4577
- ? host.origins
4578
- : [...new Set([corsOrigin, aiOrigin, entry.appUrl].filter(Boolean))],
4579
- wroteEnv: false,
4580
- clientKind: client.kind,
4581
- clientFiles: client.filesWritten,
4582
- clientNotes: client.notes,
4583
- aiServerUp,
4584
- startedHosts: [],
4585
- openUrl,
4586
- processIssues,
4587
- warning,
4588
- waitingForStart,
4589
- needsReview,
4590
- hostApps: ports.apps || entry.hostApps || [],
4591
- reasons: ports.reasons || [],
4592
- projectInfo,
4593
- ignorePaths: access.ignorePaths,
4594
- };
4668
+ return await withCliProbe(
4669
+ {
4670
+ sandboxId,
4671
+ folderPath: entry.folderPath,
4672
+ extraFolders: entry.extraFolders || [],
4673
+ addFolder: false,
4674
+ port: entry.port,
4675
+ appUrl: host.appUrl || entry.appUrl || appUrl,
4676
+ origins: host.origins.length
4677
+ ? host.origins
4678
+ : [...new Set([corsOrigin, aiOrigin, entry.appUrl].filter(Boolean))],
4679
+ wroteEnv: false,
4680
+ clientKind: client.kind,
4681
+ clientFiles: client.filesWritten,
4682
+ clientNotes: client.notes,
4683
+ aiServerUp,
4684
+ startedHosts: [],
4685
+ openUrl,
4686
+ processIssues,
4687
+ warning,
4688
+ waitingForStart,
4689
+ needsReview,
4690
+ hostApps: ports.apps || entry.hostApps || [],
4691
+ reasons: ports.reasons || [],
4692
+ projectInfo,
4693
+ ignorePaths: access.ignorePaths,
4694
+ },
4695
+ workspaceFolders(entry)
4696
+ );
4595
4697
  }
4596
4698
 
4597
4699
  async function proposeSetupForWorkspace(ws, cfg, opts = {}) {
@@ -4738,16 +4840,28 @@ async function runActions(cfg, actions) {
4738
4840
  };
4739
4841
  }
4740
4842
  } else if (action.code === "recheck") {
4843
+ cliProviderCache = null;
4741
4844
  const sandboxId = action.sandboxId || action.payload?.sandboxId;
4742
4845
  const ws = (cfg.workspaces || []).find((w) => w.sandboxId === sandboxId);
4743
4846
  if (!ws) {
4744
4847
  log(`${label} recheck: no local workspace`);
4745
4848
  }
4849
+ const cli = await detectCliProviders(true);
4850
+ const folderProbe = ws
4851
+ ? await withCliProbe(
4852
+ { sandboxId, folderPath: ws.folderPath },
4853
+ workspaceFolders(ws)
4854
+ )
4855
+ : { missingCli: cli.length === 0 };
4746
4856
  const hostApps = ws
4747
4857
  ? await resolveWorkspaceHostApps(ws, { cfg, force: true, allowAi: false })
4748
4858
  : null;
4749
4859
  result = {
4750
4860
  recheckedAt: new Date().toISOString(),
4861
+ cliProviders: cli,
4862
+ missingCli: folderProbe.missingCli === true,
4863
+ cliProvider: folderProbe.cliProvider || null,
4864
+ cliCommand: folderProbe.cliCommand || null,
4751
4865
  hostApps: hostApps?.apps || [],
4752
4866
  projectInfo: ws?.projectInfo || null,
4753
4867
  };
@@ -5216,18 +5330,41 @@ function adminWsUrl(adminUrl, token) {
5216
5330
  async function buildIssues(cfg, workspaceStates) {
5217
5331
  /** @type {Array<Record<string, unknown>>} */
5218
5332
  const issues = [];
5219
- const cli = await detectCliProviders();
5220
- if (!cli.length) {
5221
- issues.push({
5222
- code: "missing_cli",
5223
- severity: "error",
5224
- title: "No coding agent CLI found",
5225
- message:
5226
- "PATH has no agent, claude, or agy. Chat turns will fail until one is installed.",
5227
- resolution: "Install and authenticate a CLI, then Recheck.",
5228
- actionCode: "recheck",
5229
- sandboxId: null,
5230
- });
5333
+ if (workspaceStates.length) {
5334
+ for (const st of workspaceStates) {
5335
+ const ws = (cfg.workspaces || []).find((row) => row.sandboxId === st.sandboxId);
5336
+ const folders = ws ? workspaceFolders(ws) : [st.folderPath].filter(Boolean);
5337
+ let probe = { available: false, folder: folders[0] || st.folderPath };
5338
+ for (const folder of folders) {
5339
+ probe = await probeFolderCli(folder);
5340
+ if (probe.available) break;
5341
+ }
5342
+ if (probe.available) continue;
5343
+ issues.push({
5344
+ code: "missing_cli",
5345
+ severity: "error",
5346
+ title: "No coding agent CLI in this folder",
5347
+ message: missingCliSetupText(probe.folder || folders[0] || st.folderPath),
5348
+ resolution:
5349
+ "Install a CLI and re-run the Maintainer Pro command from a terminal where it works, or set AI_CLI_COMMAND in this folder's .env, then Check again.",
5350
+ actionCode: "recheck",
5351
+ sandboxId: st.sandboxId,
5352
+ });
5353
+ }
5354
+ } else {
5355
+ const cli = await detectCliProviders();
5356
+ if (!cli.length) {
5357
+ issues.push({
5358
+ code: "missing_cli",
5359
+ severity: "error",
5360
+ title: "No coding agent CLI found",
5361
+ message: missingCliSetupText(),
5362
+ resolution:
5363
+ "Install Cursor (agent), Claude Code (claude), or Antigravity (agy), sign in, then stop and re-run the Maintainer Pro command. Use Check again after it is on PATH.",
5364
+ actionCode: "recheck",
5365
+ sandboxId: null,
5366
+ });
5367
+ }
5231
5368
  }
5232
5369
  for (const st of workspaceStates) {
5233
5370
  if (st.aiServerUp || st.appsRunning || st.startingAi) continue;
@@ -5352,6 +5489,11 @@ async function main() {
5352
5489
  { versions: PACKAGE_VERSIONS },
5353
5490
  `bridge v${PACKAGE_VERSION} starting (${formatPackageVersions()})`
5354
5491
  );
5492
+ if (usingWsFallback) {
5493
+ log(
5494
+ `Node ${process.versions.node} has no global WebSocket; using the ws package`
5495
+ );
5496
+ }
5355
5497
  void warnIfBridgeOutdated();
5356
5498
 
5357
5499
  let cfg = loadConfig() || {};
@@ -5377,6 +5519,7 @@ async function main() {
5377
5519
  log(
5378
5520
  `online via websocket (presence every ${HEARTBEAT_MS / 1000}s, ping every ${WS_PING_MS / 1000}s; reconnects until stopped)`
5379
5521
  );
5522
+ await warnIfMissingCli();
5380
5523
  await restoreHostsAfterReconnect(cfg);
5381
5524
 
5382
5525
  /** @type {unknown[]} */
@@ -5391,6 +5534,7 @@ async function main() {
5391
5534
  const batch = claimedActions.splice(0, claimedActions.length);
5392
5535
  await runActions(cfg, batch);
5393
5536
  }
5537
+ await sendPresenceOverWs();
5394
5538
  } catch (err) {
5395
5539
  warn(err instanceof Error ? err.message : String(err));
5396
5540
  } finally {
@@ -5702,7 +5846,7 @@ async function main() {
5702
5846
  /** @type {WebSocket} */
5703
5847
  let ws;
5704
5848
  try {
5705
- ws = new WebSocket(url);
5849
+ ws = new WebSocketImpl(url);
5706
5850
  } catch (err) {
5707
5851
  warn(
5708
5852
  `websocket connect failed: ${