@maintainer-pro/ai-bridge 0.1.19 → 0.1.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainer-pro/ai-bridge",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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,7 @@
28
28
  "node": ">=22"
29
29
  },
30
30
  "dependencies": {
31
- "@maintainer-pro/ai-cli": "^0.1.11",
31
+ "@maintainer-pro/ai-cli": "^0.1.12",
32
32
  "@maintainer-pro/ai-server": "^0.1.7"
33
33
  }
34
34
  }
package/src/daemon.mjs CHANGED
@@ -257,7 +257,21 @@ function loadConfig() {
257
257
  const file = configPath();
258
258
  if (!fs.existsSync(file)) return null;
259
259
  try {
260
- return JSON.parse(fs.readFileSync(file, "utf8"));
260
+ const cfg = JSON.parse(fs.readFileSync(file, "utf8"));
261
+ let dirty = false;
262
+ for (const ws of cfg.workspaces || []) {
263
+ const before = JSON.stringify(ws.hostApps || []);
264
+ const beforeProxy = JSON.stringify(ws.proxy || null);
265
+ stripAiServerFromWorkspace(ws);
266
+ if (
267
+ JSON.stringify(ws.hostApps || []) !== before ||
268
+ JSON.stringify(ws.proxy || null) !== beforeProxy
269
+ ) {
270
+ dirty = true;
271
+ }
272
+ }
273
+ if (dirty) saveConfig(cfg);
274
+ return cfg;
261
275
  } catch {
262
276
  return null;
263
277
  }
@@ -434,7 +448,7 @@ async function resolveWorkspaceHostApps(ws, opts = {}) {
434
448
  const match = previous.find((row) => row && row.id === app.id);
435
449
  return match ? { ...app, host: match.host === true || app.host === true } : app;
436
450
  });
437
- ws.hostApps = result.apps;
451
+ ws.hostApps = listedHostApps(result.apps);
438
452
  if (opts.cfg) persistWorkspaceEntry(opts.cfg, ws);
439
453
  const summary = result.apps
440
454
  .map((app) => `${app.name}:${app.port}`)
@@ -455,19 +469,10 @@ async function resolveWorkspaceHostApps(ws, opts = {}) {
455
469
  activity(
456
470
  ws.sandboxId,
457
471
  "warn",
458
- `port detect failed (${message}) — using AI server ${Number(ws.port) || 3100} so setup can continue`
472
+ `port detect failed (${message}) — no host apps listed yet`
459
473
  );
460
474
  if (!Array.isArray(ws.hostApps) || !ws.hostApps.length) {
461
- ws.hostApps = [
462
- {
463
- id: "ai-server",
464
- name: "AI server",
465
- role: "ai-server",
466
- port: Number(ws.port) || 3100,
467
- source: "default",
468
- locked: true,
469
- },
470
- ];
475
+ ws.hostApps = [];
471
476
  }
472
477
  return {
473
478
  apps: ws.hostApps,
@@ -763,8 +768,31 @@ function isLocalAppUrl(url) {
763
768
  }
764
769
  }
765
770
 
771
+ function isAiServerApp(app) {
772
+ return Boolean(app && (app.role === "ai-server" || app.id === "ai-server"));
773
+ }
774
+
775
+ function listedHostApps(apps) {
776
+ return (Array.isArray(apps) ? apps : []).filter((app) => !isAiServerApp(app));
777
+ }
778
+
779
+ function stripAiServerFromWorkspace(ws) {
780
+ if (!ws || typeof ws !== "object") return ws;
781
+ if (Array.isArray(ws.hostApps)) ws.hostApps = listedHostApps(ws.hostApps);
782
+ if (ws.proxy && typeof ws.proxy === "object") {
783
+ if (ws.proxy.slugs && typeof ws.proxy.slugs === "object") {
784
+ delete ws.proxy.slugs["ai-server"];
785
+ }
786
+ if (ws.proxy.urls && typeof ws.proxy.urls === "object") {
787
+ delete ws.proxy.urls["ai-server"];
788
+ }
789
+ }
790
+ return ws;
791
+ }
792
+
766
793
  function persistWorkspaceEntry(cfg, ws) {
767
794
  if (!cfg || !ws?.sandboxId) return;
795
+ stripAiServerFromWorkspace(ws);
768
796
  cfg.workspaces = cfg.workspaces || [];
769
797
  const index = cfg.workspaces.findIndex((row) => row.sandboxId === ws.sandboxId);
770
798
  if (index >= 0) cfg.workspaces[index] = { ...cfg.workspaces[index], ...ws };
@@ -1001,16 +1029,7 @@ function probeEmbedConfig(url, timeoutMs = 2500) {
1001
1029
 
1002
1030
  function sanitizeAiServerPort(ws) {
1003
1031
  if (!ws || typeof ws !== "object") return;
1004
- const apps = Array.isArray(ws.hostApps) ? ws.hostApps : [];
1005
- for (const app of apps) {
1006
- if (
1007
- app &&
1008
- (app.role === "ai-server" || app.id === "ai-server") &&
1009
- isAdminListenPort(app.port)
1010
- ) {
1011
- app.port = 3100;
1012
- }
1013
- }
1032
+ if (Array.isArray(ws.hostApps)) ws.hostApps = listedHostApps(ws.hostApps);
1014
1033
  if (isAdminListenPort(ws.port)) ws.port = 3100;
1015
1034
  }
1016
1035
 
@@ -1304,10 +1323,6 @@ async function reconcileWorkspacePresence(ws, cfg, opts = {}) {
1304
1323
  // 3. Host + CORS origins for Maintainer Pro
1305
1324
  const host = workspaceHostReport(ws);
1306
1325
  const uiShare = proxyUrlForApp(ws, hostAppOf(ws) || { id: "ui", role: "ui", host: true });
1307
- const aiShare = proxyUrlForApp(ws, {
1308
- id: "ai-server",
1309
- role: "ai-server",
1310
- });
1311
1326
  if (uiShare) {
1312
1327
  host.appUrl = uiShare;
1313
1328
  } else if (probe.running) {
@@ -1316,12 +1331,9 @@ async function reconcileWorkspacePresence(ws, cfg, opts = {}) {
1316
1331
  );
1317
1332
  if (ui?.port) {
1318
1333
  host.appUrl = `http://localhost:${ui.port}`;
1319
- } else if (probe.chatUp) {
1320
- host.appUrl = `http://localhost:${probe.chatPort}`;
1321
1334
  }
1322
1335
  }
1323
1336
  if (uiShare && !host.origins.includes(uiShare)) host.origins.push(uiShare);
1324
- if (aiShare && !host.origins.includes(aiShare)) host.origins.push(aiShare);
1325
1337
  const proxyOrigin = String(ws?.proxy?.origin || "").replace(/\/$/, "");
1326
1338
  if (proxyOrigin && !host.origins.includes(proxyOrigin)) {
1327
1339
  host.origins.push(proxyOrigin);
@@ -1491,7 +1503,7 @@ function uniqueProxySlugs(apps) {
1491
1503
  const used = new Set();
1492
1504
  /** @type {Record<string, string>} */
1493
1505
  const slugs = {};
1494
- for (const app of apps || []) {
1506
+ for (const app of listedHostApps(apps)) {
1495
1507
  let slug = proxySlugForApp(app);
1496
1508
  if (!slug || !app?.id) continue;
1497
1509
  if (used.has(slug)) {
@@ -1512,7 +1524,7 @@ function uniqueProxySlugs(apps) {
1512
1524
  }
1513
1525
 
1514
1526
  function sharePortUrls(ws) {
1515
- const apps = Array.isArray(ws?.hostApps) ? ws.hostApps : [];
1527
+ const apps = listedHostApps(ws?.hostApps);
1516
1528
  /** @type {Record<string, string>} */
1517
1529
  const out = {};
1518
1530
  for (const app of apps) {
@@ -1547,8 +1559,10 @@ function applyAssignedProxy(ws, remote, cfg) {
1547
1559
  }
1548
1560
 
1549
1561
  function appsFrom(ws, remote) {
1550
- if (Array.isArray(ws?.hostApps) && ws.hostApps.length) return ws.hostApps;
1551
- if (Array.isArray(remote?.hostApps)) return remote.hostApps;
1562
+ if (Array.isArray(ws?.hostApps) && ws.hostApps.length) {
1563
+ return listedHostApps(ws.hostApps);
1564
+ }
1565
+ if (Array.isArray(remote?.hostApps)) return listedHostApps(remote.hostApps);
1552
1566
  return [];
1553
1567
  }
1554
1568
 
@@ -1569,10 +1583,9 @@ function proxyUrlForApp(ws, app) {
1569
1583
  }
1570
1584
 
1571
1585
  function jobsFromHostApps(ws, filter = {}) {
1572
- const apps = Array.isArray(ws.hostApps) ? ws.hostApps : [];
1586
+ const apps = listedHostApps(ws.hostApps);
1573
1587
  const hostId = hostAppOf(ws)?.id;
1574
1588
  return apps
1575
- .filter((app) => app && app.role !== "ai-server")
1576
1589
  .filter((app) => (filter.appId ? app.id === filter.appId : true))
1577
1590
  .map((app) => {
1578
1591
  const port = Number(app.port) || 3000;
@@ -1622,7 +1635,7 @@ async function probeRunningApps(ws, timeoutMs = 2500) {
1622
1635
  appId: job.appId || job.role,
1623
1636
  });
1624
1637
  }
1625
- const listed = Array.isArray(ws.hostApps) ? ws.hostApps : [];
1638
+ const listed = listedHostApps(ws.hostApps);
1626
1639
  const hostApps = await Promise.all(
1627
1640
  listed.map(async (app) => {
1628
1641
  const port = Number(app.port) || 0;
@@ -1645,7 +1658,7 @@ async function probeRunningApps(ws, timeoutMs = 2500) {
1645
1658
  chatPort: chat.port,
1646
1659
  hosts,
1647
1660
  hostApps,
1648
- running: chat.up || hosts.some((host) => host.up) || hostApps.some((app) => app.running),
1661
+ running: hosts.some((host) => host.up) || hostApps.some((app) => app.running),
1649
1662
  };
1650
1663
  }
1651
1664
 
@@ -1662,9 +1675,7 @@ async function restoreHostsAfterReconnect(cfg) {
1662
1675
  .join(" ");
1663
1676
  if (status.appsRunning) {
1664
1677
  log(
1665
- `found running apps for ${label}: chat=${status.probe.chatPort}${
1666
- status.probe.chatUp ? "(up)" : "(down)"
1667
- }${hostSummary ? ` ${hostSummary}` : ""} share=${
1678
+ `found running apps for ${label}: ${hostSummary || "(none)"} share=${
1668
1679
  status.host.appUrl || "(none)"
1669
1680
  } origins=${
1670
1681
  status.host.origins.join(",") || "none"
@@ -1703,10 +1714,7 @@ async function prepareWorkspaceLaunch(ws, cfg, reserved) {
1703
1714
  return { aiPort, jobs: [] };
1704
1715
  }
1705
1716
  log(`ports pick ${label} in ${folder}`);
1706
- const listedAi = Array.isArray(ws.hostApps)
1707
- ? ws.hostApps.find((app) => app.role === "ai-server" || app.id === "ai-server")
1708
- : null;
1709
- const preferredRaw = Number(listedAi?.port || ws.port) || 3100;
1717
+ const preferredRaw = Number(ws.port) || 3100;
1710
1718
  const preferredAi = isAdminListenPort(preferredRaw) ? 3100 : preferredRaw;
1711
1719
  const aiUp = await isChatServerOnPort(preferredAi);
1712
1720
  const aiPort = aiUp
@@ -4679,12 +4687,11 @@ async function runActions(cfg, actions) {
4679
4687
  String(action.payload.folderPathRemaining || remaining[0] || ws.folderPath);
4680
4688
  ws.folderPath = primary;
4681
4689
  ws.extraFolders = remaining.filter((folder) => !sameFolder(folder, primary));
4682
- ws.hostApps = (Array.isArray(ws.hostApps) ? ws.hostApps : []).filter(
4690
+ ws.hostApps = listedHostApps(
4691
+ Array.isArray(ws.hostApps) ? ws.hostApps : []
4692
+ ).filter(
4683
4693
  (app) =>
4684
- !app ||
4685
- app.role === "ai-server" ||
4686
- !app.folderPath ||
4687
- !sameFolder(app.folderPath, removed)
4694
+ !app.folderPath || !sameFolder(app.folderPath, removed)
4688
4695
  );
4689
4696
  persistWorkspaceEntry(cfg, ws);
4690
4697
  result = {
@@ -284,8 +284,8 @@ function isProbablyUtf8Text(buf) {
284
284
  }
285
285
 
286
286
  /**
287
- * Replace app-list origins (`localhost:8080`, LAN IPs, hostnames) with the
288
- * share URL for that port. Self-contained so the browser shim/SW can embed it.
287
+ * Replace loopback app origins only (`localhost:8080`, `127.0.0.1`, `::1`,
288
+ * `0.0.0.0`). Real hostnames and other base URLs are left unchanged.
289
289
  *
290
290
  * @param {string} text
291
291
  * @param {Record<string, string> | null | undefined} portUrls
@@ -297,14 +297,7 @@ export function rewriteMappedLocalUrls(text, portUrls) {
297
297
  .filter((port) => Number.isInteger(port) && port > 0 && port <= 65535)
298
298
  .sort((a, b) => b - a);
299
299
  if (!ports.length) return text;
300
- const localHost =
301
- "(?:localhost|127\\.0\\.0\\.1|\\[::1\\]|::1|0\\.0\\.0\\.0|" +
302
- "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" +
303
- "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" +
304
- "172\\.(?:1[6-9]|2\\d|3[01])\\.\\d{1,3}\\.\\d{1,3}|" +
305
- "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" +
306
- "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.(?:local|lan|internal|home))?)";
307
- const anyHost = "(?:[^\\s'\"\\\\/<>@:]+|\\[[0-9a-fA-F:]+\\])";
300
+ const localHost = "(?:localhost|127\\.0\\.0\\.1|\\[::1\\]|::1|0\\.0\\.0\\.0)";
308
301
  let out = String(text);
309
302
  for (let i = 0; i < ports.length; i++) {
310
303
  const port = ports[i];
@@ -339,12 +332,6 @@ export function rewriteMappedLocalUrls(text, portUrls) {
339
332
  out = out.replace(new RegExp("//" + localHost + ":" + port + tail, "gi"), function (m) {
340
333
  return swap(m, dest.replace(/^https?:/i, ""));
341
334
  });
342
- out = out.replace(new RegExp("http://" + anyHost + ":" + port + tail, "gi"), function (m) {
343
- return swap(m, dest);
344
- });
345
- out = out.replace(new RegExp("ws://" + anyHost + ":" + port + tail, "gi"), function (m) {
346
- return swap(m, destWs);
347
- });
348
335
  out = out.replace(
349
336
  new RegExp("(?<![\\w./])" + localHost + ":" + port + tail, "gi"),
350
337
  function (m) {
@@ -360,7 +347,7 @@ export function rewriteMappedLocalUrls(text, portUrls) {
360
347
  * @param {Record<string, string>} headers
361
348
  * @param {Record<string, string> | null | undefined} portUrls
362
349
  */
363
- export function rewriteShareRequestBody(body, headers, portUrls, publicBase) {
350
+ export function rewriteShareRequestBody(body, headers, portUrls, _publicBase) {
364
351
  if (!body) return body;
365
352
  const buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
366
353
  if (!buf.length) return buf;
@@ -370,7 +357,6 @@ export function rewriteShareRequestBody(body, headers, portUrls, publicBase) {
370
357
  if (portUrls && Object.keys(portUrls).length) {
371
358
  next = rewriteMappedLocalUrls(next, portUrls);
372
359
  }
373
- if (publicBase) next = rewriteShareOriginPaths(next, publicBase);
374
360
  if (next === text) return buf;
375
361
  return Buffer.from(next, "utf8");
376
362
  }
@@ -452,9 +438,10 @@ export function prefixShareOriginUrl(value, publicBase) {
452
438
  const basePort = base.port || (base.protocol === "https:" ? "443" : "80");
453
439
  if (reqPort !== basePort) return value;
454
440
  const path = u.pathname || "/";
441
+ if (path === "/" || path === "") return value;
455
442
  if (path === prefix || path.indexOf(prefix + "/") === 0) return u.toString();
456
443
  if (path.indexOf("/p/") === 0) return u.toString();
457
- u.pathname = path === "/" ? prefix : prefix + path;
444
+ u.pathname = prefix + path;
458
445
  return u.toString();
459
446
  } catch {
460
447
  return value;
@@ -475,11 +462,11 @@ export function rewriteShareOriginPaths(text, publicBase) {
475
462
  .replace(/\//g, "\\/")
476
463
  .replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
477
464
  let out = String(text);
478
- out = out.replace(new RegExp(originEsc + "(?:/[^\\s'\"<>\\\\]*)?", "gi"), function (matched) {
465
+ out = out.replace(new RegExp(originEsc + "/(?!p/)[^\\s'\"<>\\\\]+", "gi"), function (matched) {
479
466
  return prefixShareOriginUrl(matched, publicBase);
480
467
  });
481
468
  out = out.replace(
482
- new RegExp(originSlashEsc + "(?:\\\\/[^\\s'\"<>]*)?", "gi"),
469
+ new RegExp(originSlashEsc + "\\\\/(?!p/)[^\\s'\"<>]+", "gi"),
483
470
  function (matched) {
484
471
  const unescaped = matched.replace(/\\\//g, "/");
485
472
  return prefixShareOriginUrl(unescaped, publicBase).replace(/\//g, "\\/");
@@ -821,12 +808,11 @@ function prefixRootPaths(body, publicBase, contentType = "") {
821
808
  let out = body;
822
809
  if (
823
810
  html &&
811
+ isViteDocument(out) &&
824
812
  /<head[\s>]/i.test(out) &&
825
- !/<base\s/i.test(out) &&
826
- !isNextDocument(out)
813
+ !/<base\s/i.test(out)
827
814
  ) {
828
- const viteBase = isViteDocument(out) ? inferViteBase(out) : "/";
829
- const href = joinProxyAndViteBase(pathPrefix, viteBase);
815
+ const href = joinProxyAndViteBase(pathPrefix, inferViteBase(out));
830
816
  out = out.replace(/<head([^>]*)>/i, `<head$1><base href="${href}">`);
831
817
  }
832
818
  if (html) {
@@ -883,6 +869,12 @@ function isScriptRequestPath(path) {
883
869
  return /\.(?:m?js|cjs)$/.test(p);
884
870
  }
885
871
 
872
+ function isScriptOrJsonBody(headers, path) {
873
+ const mime = contentMime(headerGet(headers, "content-type"));
874
+ if (/javascript|ecmascript|json/.test(mime)) return true;
875
+ return isScriptRequestPath(path);
876
+ }
877
+
886
878
  function rewriteLocalSidecarUrls(html, aiPublicBase, uiPublicBase) {
887
879
  const ai = String(aiPublicBase || "").replace(/\/$/, "");
888
880
  const ui = String(uiPublicBase || "").replace(/\/$/, "");
@@ -980,6 +972,14 @@ function shareProxyShim(p, portMap, rewriteMappedLocalUrls) {
980
972
  )
981
973
  );
982
974
  }
975
+ function loopback(h) {
976
+ h = String(h || "")
977
+ .replace(/^\[|\]$/g, "")
978
+ .toLowerCase();
979
+ return (
980
+ h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0"
981
+ );
982
+ }
983
983
  function prefixPath(path) {
984
984
  if (typeof path !== "string" || !path) return path;
985
985
  if (path.charAt(0) === "/" && path.charAt(1) !== "/") {
@@ -1019,17 +1019,29 @@ function shareProxyShim(p, portMap, rewriteMappedLocalUrls) {
1019
1019
  }
1020
1020
  function addNav(v) {
1021
1021
  if (typeof v !== "string" || !v) return v;
1022
- v = mapLocal(v);
1022
+ var mapped = mapLocal(v);
1023
+ if (mapped !== v) return mapped;
1023
1024
  if (v.charAt(0) === "/" && v.charAt(1) !== "/") return prefixPath(v);
1024
1025
  try {
1025
1026
  var nav = new URL(v, location.href);
1027
+ if (!loopback(nav.hostname)) return v;
1026
1028
  if (nav.origin !== location.origin) return v;
1029
+ if (nav.pathname === "/" || nav.pathname === "") return v;
1027
1030
  nav.pathname = prefixPath(nav.pathname);
1028
1031
  return nav.toString();
1029
1032
  } catch (e) {}
1030
1033
  return v;
1031
1034
  }
1032
1035
  function addNet(v) {
1036
+ if (typeof v !== "string" || !v) return v;
1037
+ var mapped = mapLocal(v);
1038
+ if (mapped !== v) return mapped;
1039
+ if (/^(https?:|wss?:)\/\//i.test(v)) {
1040
+ try {
1041
+ var u = new URL(v);
1042
+ if (!loopback(u.hostname)) return v;
1043
+ } catch (e) {}
1044
+ }
1033
1045
  return addNav(v);
1034
1046
  }
1035
1047
  function mapSel(sel) {
@@ -1224,11 +1236,24 @@ function shareServiceWorkerMain(portMap, tokenRoot, rewriteMappedLocalUrls) {
1224
1236
  } catch (e) {}
1225
1237
  return "";
1226
1238
  }
1239
+ function loopback(h) {
1240
+ h = String(h || "")
1241
+ .replace(/^\[|\]$/g, "")
1242
+ .toLowerCase();
1243
+ return (
1244
+ h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0"
1245
+ );
1246
+ }
1227
1247
  function prefixWith(url, prefix) {
1228
1248
  if (!prefix) return url;
1229
1249
  try {
1250
+ if (/^(https?:|wss?:)\/\//i.test(url)) {
1251
+ var abs = new URL(url);
1252
+ if (!loopback(abs.hostname)) return url;
1253
+ }
1230
1254
  var u = new URL(url, self.location.href);
1231
1255
  if (u.origin !== self.location.origin) return url;
1256
+ if (u.pathname === "/" || u.pathname === "") return url;
1232
1257
  if (u.pathname.indexOf("/p/") === 0) return url;
1233
1258
  if (u.pathname === prefix || u.pathname.indexOf(prefix + "/") === 0) return url;
1234
1259
  u.pathname = u.pathname === "/" ? prefix : prefix + u.pathname;
@@ -1402,7 +1427,7 @@ export function processShareHttpResponse(opts) {
1402
1427
  mutated = true;
1403
1428
  }
1404
1429
  }
1405
- if (publicBase && rewriteTextBody) {
1430
+ if (publicBase && rewriteTextBody && !isScriptOrJsonBody(headers, path)) {
1406
1431
  const prefixed = rewriteShareOriginPaths(text, publicBase);
1407
1432
  if (prefixed !== text) {
1408
1433
  text = prefixed;