codecaine-cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -2269,10 +2269,10 @@ var require_websocket = __commonJS({
2269
2269
  "use strict";
2270
2270
  var EventEmitter = __require("events");
2271
2271
  var https2 = __require("https");
2272
- var http3 = __require("http");
2273
- var net2 = __require("net");
2272
+ var http4 = __require("http");
2273
+ var net3 = __require("net");
2274
2274
  var tls2 = __require("tls");
2275
- var { randomBytes, createHash: createHash2 } = __require("crypto");
2275
+ var { randomBytes: randomBytes2, createHash: createHash2 } = __require("crypto");
2276
2276
  var { Duplex, Readable: Readable2 } = __require("stream");
2277
2277
  var { URL: URL2 } = __require("url");
2278
2278
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2823,8 +2823,8 @@ var require_websocket = __commonJS({
2823
2823
  }
2824
2824
  }
2825
2825
  const defaultPort = isSecure ? 443 : 80;
2826
- const key = randomBytes(16).toString("base64");
2827
- const request = isSecure ? https2.request : http3.request;
2826
+ const key = randomBytes2(16).toString("base64");
2827
+ const request = isSecure ? https2.request : http4.request;
2828
2828
  const protocolSet = /* @__PURE__ */ new Set();
2829
2829
  let perMessageDeflate;
2830
2830
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -3027,12 +3027,12 @@ var require_websocket = __commonJS({
3027
3027
  }
3028
3028
  function netConnect(options) {
3029
3029
  options.path = options.socketPath;
3030
- return net2.connect(options);
3030
+ return net3.connect(options);
3031
3031
  }
3032
3032
  function tlsConnect(options) {
3033
3033
  options.path = void 0;
3034
3034
  if (!options.servername && options.servername !== "") {
3035
- options.servername = net2.isIP(options.host) ? "" : options.host;
3035
+ options.servername = net3.isIP(options.host) ? "" : options.host;
3036
3036
  }
3037
3037
  return tls2.connect(options);
3038
3038
  }
@@ -3320,7 +3320,7 @@ var require_websocket_server = __commonJS({
3320
3320
  "node_modules/ws/lib/websocket-server.js"(exports, module) {
3321
3321
  "use strict";
3322
3322
  var EventEmitter = __require("events");
3323
- var http3 = __require("http");
3323
+ var http4 = __require("http");
3324
3324
  var { Duplex } = __require("stream");
3325
3325
  var { createHash: createHash2 } = __require("crypto");
3326
3326
  var extension2 = require_extension();
@@ -3401,8 +3401,8 @@ var require_websocket_server = __commonJS({
3401
3401
  );
3402
3402
  }
3403
3403
  if (options.port != null) {
3404
- this._server = http3.createServer((req, res) => {
3405
- const body = http3.STATUS_CODES[426];
3404
+ this._server = http4.createServer((req, res) => {
3405
+ const body = http4.STATUS_CODES[426];
3406
3406
  res.writeHead(426, {
3407
3407
  "Content-Length": body.length,
3408
3408
  "Content-Type": "text/plain"
@@ -3487,10 +3487,10 @@ var require_websocket_server = __commonJS({
3487
3487
  process.nextTick(emitClose, this);
3488
3488
  }
3489
3489
  } else {
3490
- const server = this._server;
3490
+ const server2 = this._server;
3491
3491
  this._removeListeners();
3492
3492
  this._removeListeners = this._server = null;
3493
- server.close(() => {
3493
+ server2.close(() => {
3494
3494
  emitClose(this);
3495
3495
  });
3496
3496
  }
@@ -3675,23 +3675,23 @@ var require_websocket_server = __commonJS({
3675
3675
  }
3676
3676
  };
3677
3677
  module.exports = WebSocketServer2;
3678
- function addListeners(server, map) {
3679
- for (const event of Object.keys(map)) server.on(event, map[event]);
3678
+ function addListeners(server2, map) {
3679
+ for (const event of Object.keys(map)) server2.on(event, map[event]);
3680
3680
  return function removeListeners() {
3681
3681
  for (const event of Object.keys(map)) {
3682
- server.removeListener(event, map[event]);
3682
+ server2.removeListener(event, map[event]);
3683
3683
  }
3684
3684
  };
3685
3685
  }
3686
- function emitClose(server) {
3687
- server._state = CLOSED;
3688
- server.emit("close");
3686
+ function emitClose(server2) {
3687
+ server2._state = CLOSED;
3688
+ server2.emit("close");
3689
3689
  }
3690
3690
  function socketOnError() {
3691
3691
  this.destroy();
3692
3692
  }
3693
3693
  function abortHandshake(socket, code, message, headers) {
3694
- message = message || http3.STATUS_CODES[code];
3694
+ message = message || http4.STATUS_CODES[code];
3695
3695
  headers = {
3696
3696
  Connection: "close",
3697
3697
  "Content-Type": "text/html",
@@ -3700,15 +3700,15 @@ var require_websocket_server = __commonJS({
3700
3700
  };
3701
3701
  socket.once("finish", socket.destroy);
3702
3702
  socket.end(
3703
- `HTTP/1.1 ${code} ${http3.STATUS_CODES[code]}\r
3703
+ `HTTP/1.1 ${code} ${http4.STATUS_CODES[code]}\r
3704
3704
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
3705
3705
  );
3706
3706
  }
3707
- function abortHandshakeOrEmitwsClientError(server, req, socket, code, message, headers) {
3708
- if (server.listenerCount("wsClientError")) {
3707
+ function abortHandshakeOrEmitwsClientError(server2, req, socket, code, message, headers) {
3708
+ if (server2.listenerCount("wsClientError")) {
3709
3709
  const err = new Error(message);
3710
3710
  Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
3711
- server.emit("wsClientError", err, socket, req);
3711
+ server2.emit("wsClientError", err, socket, req);
3712
3712
  } else {
3713
3713
  abortHandshake(socket, code, message, headers);
3714
3714
  }
@@ -3718,17 +3718,17 @@ var require_websocket_server = __commonJS({
3718
3718
 
3719
3719
  // src/cli.ts
3720
3720
  import crypto4 from "node:crypto";
3721
- import { spawn } from "node:child_process";
3722
- import fs15 from "node:fs";
3723
- import path13 from "node:path";
3721
+ import { spawn as spawn2, spawnSync } from "node:child_process";
3722
+ import fs21 from "node:fs";
3723
+ import path20 from "node:path";
3724
3724
  import readline from "node:readline";
3725
- import { fileURLToPath } from "node:url";
3725
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
3726
3726
 
3727
3727
  // src/server.ts
3728
3728
  import crypto3 from "node:crypto";
3729
- import fs14 from "node:fs";
3730
- import http2 from "node:http";
3731
- import path12 from "node:path";
3729
+ import fs20 from "node:fs";
3730
+ import http3 from "node:http";
3731
+ import path19 from "node:path";
3732
3732
 
3733
3733
  // node_modules/ws/wrapper.mjs
3734
3734
  var import_stream = __toESM(require_stream(), 1);
@@ -3988,8 +3988,8 @@ function createProjectProxy(target, editorOrigin, guest) {
3988
3988
  current = new URL(origin.origin);
3989
3989
  }
3990
3990
  };
3991
- const port = () => server.address()?.port ?? -1;
3992
- const server = http.createServer(async (req, res) => {
3991
+ const port = () => server2.address()?.port ?? -1;
3992
+ const server2 = http.createServer(async (req, res) => {
3993
3993
  const url = req.url ?? "/";
3994
3994
  if (!isProxyHost(req.headers.host, port())) {
3995
3995
  res.writeHead(403, { connection: "close" });
@@ -4023,14 +4023,14 @@ function createProjectProxy(target, editorOrigin, guest) {
4023
4023
  }
4024
4024
  await relay(req, res, project, editorOrigin);
4025
4025
  });
4026
- server.on("upgrade", (req, socket, head) => {
4026
+ server2.on("upgrade", (req, socket, head) => {
4027
4027
  if (!isProxyHost(req.headers.host, port()) || !upstreamUrl(req.url ?? "/", project.current())) {
4028
4028
  socket.end("HTTP/1.1 403 Forbidden\r\nconnection: close\r\n\r\n");
4029
4029
  return;
4030
4030
  }
4031
4031
  upgrade(req, socket, head, project.current());
4032
4032
  });
4033
- return server;
4033
+ return server2;
4034
4034
  }
4035
4035
  function wantsDocument(headers) {
4036
4036
  const one = (name) => {
@@ -4108,7 +4108,7 @@ async function relay(req, res, project, editorOrigin) {
4108
4108
  return;
4109
4109
  }
4110
4110
  const hasBody = req.method !== "GET" && req.method !== "HEAD";
4111
- const send = () => fetch(upstream, {
4111
+ const send2 = () => fetch(upstream, {
4112
4112
  method: req.method,
4113
4113
  headers: forwardedHeaders(req, target),
4114
4114
  body: hasBody ? req : void 0,
@@ -4120,10 +4120,10 @@ async function relay(req, res, project, editorOrigin) {
4120
4120
  let response;
4121
4121
  try {
4122
4122
  try {
4123
- response = await send();
4123
+ response = await send2();
4124
4124
  } catch (error) {
4125
4125
  if (!isConnectionFailure(error) || hasBody) throw error;
4126
- response = await send();
4126
+ response = await send2();
4127
4127
  }
4128
4128
  } catch (error) {
4129
4129
  if (!wantsDocument(req.headers)) {
@@ -4244,17 +4244,17 @@ function createLiveProxyHost(options) {
4244
4244
  if (!entry) {
4245
4245
  entry = (async () => {
4246
4246
  await readGuest();
4247
- const server = createProjectProxy(address, options.editorOrigin, readGuest);
4247
+ const server2 = createProjectProxy(address, options.editorOrigin, readGuest);
4248
4248
  await new Promise((resolve3, reject) => {
4249
- server.once("error", reject);
4250
- server.listen(0, "127.0.0.1", resolve3);
4249
+ server2.once("error", reject);
4250
+ server2.listen(0, "127.0.0.1", resolve3);
4251
4251
  });
4252
- return { origin: `http://127.0.0.1:${server.address().port}`, server };
4252
+ return { origin: `http://127.0.0.1:${server2.address().port}`, server: server2 };
4253
4253
  })();
4254
4254
  attached.set(key, entry);
4255
- const pending = entry;
4256
- pending.catch(() => {
4257
- if (attached.get(key) === pending) attached.delete(key);
4255
+ const pending3 = entry;
4256
+ pending3.catch(() => {
4257
+ if (attached.get(key) === pending3) attached.delete(key);
4258
4258
  });
4259
4259
  }
4260
4260
  const { origin } = await entry;
@@ -4266,7 +4266,7 @@ function createLiveProxyHost(options) {
4266
4266
  function close() {
4267
4267
  for (const entry of attached.values()) {
4268
4268
  entry.then(
4269
- ({ server }) => server.close(),
4269
+ ({ server: server2 }) => server2.close(),
4270
4270
  () => {
4271
4271
  }
4272
4272
  );
@@ -5127,11 +5127,11 @@ function createDocument(name = "Untitled") {
5127
5127
  };
5128
5128
  return { id: makeId(), name, rootId, nodes: { [rootId]: root } };
5129
5129
  }
5130
- function insertNodeInto(nodes, parentId, node, index) {
5130
+ function insertNodeInto(nodes, parentId, node2, index) {
5131
5131
  const parent = nodes[parentId];
5132
5132
  if (!parent) throw new Error(`addNode: parent "${parentId}" not found`);
5133
5133
  const id = makeId();
5134
- const newNode = { ...node, props: withElementId(node.props), id, parent: parentId };
5134
+ const newNode = { ...node2, props: withElementId(node2.props), id, parent: parentId };
5135
5135
  const children = [...parent.children];
5136
5136
  children.splice(index ?? children.length, 0, id);
5137
5137
  nodes[id] = newNode;
@@ -5145,8 +5145,8 @@ function addSubtree(doc, parentId, tree, index) {
5145
5145
  return { doc: { ...doc, nodes }, id };
5146
5146
  }
5147
5147
  function addSubtreeInto(nodes, parentId, tree, index, created) {
5148
- const { childNodes = [], ...node } = tree;
5149
- const id = insertNodeInto(nodes, parentId, node, index);
5148
+ const { childNodes = [], ...node2 } = tree;
5149
+ const id = insertNodeInto(nodes, parentId, node2, index);
5150
5150
  created?.push(id);
5151
5151
  for (const child of childNodes) addSubtreeInto(nodes, id, child, void 0, created);
5152
5152
  return id;
@@ -5254,16 +5254,16 @@ function hash(text3) {
5254
5254
  }
5255
5255
  return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
5256
5256
  }
5257
- function serializeNode(node, order) {
5257
+ function serializeNode(node2, order) {
5258
5258
  const out = {
5259
- id: node.id,
5260
- type: node.type,
5261
- tag: node.tag
5259
+ id: node2.id,
5260
+ type: node2.type,
5261
+ tag: node2.tag
5262
5262
  };
5263
- if (node.name !== void 0) out.name = node.name;
5264
- out.parent = node.parent;
5263
+ if (node2.name !== void 0) out.name = node2.name;
5264
+ out.parent = node2.parent;
5265
5265
  if (order !== null) out.order = order;
5266
- const record = node;
5266
+ const record = node2;
5267
5267
  for (const key of Object.keys(record).sort()) {
5268
5268
  if (key === "children" || LEADING_KEYS.includes(key)) continue;
5269
5269
  if (record[key] === void 0) continue;
@@ -5316,18 +5316,18 @@ function documentFiles(document, changed, previousOrders, removed = []) {
5316
5316
  }
5317
5317
  } else {
5318
5318
  for (const id of changed) {
5319
- const node = document.nodes[id];
5320
- if (!node) continue;
5319
+ const node2 = document.nodes[id];
5320
+ if (!node2) continue;
5321
5321
  write.add(id);
5322
- rekey(node);
5322
+ rekey(node2);
5323
5323
  }
5324
5324
  }
5325
5325
  const files = /* @__PURE__ */ new Map();
5326
5326
  for (const id of write) {
5327
- const node = document.nodes[id];
5328
- if (!node) continue;
5329
- const order = node.parent === null ? null : orders.get(id) ?? null;
5330
- files.set(id, { name: nodeFileName(id), text: serializeNode(node, order), order });
5327
+ const node2 = document.nodes[id];
5328
+ if (!node2) continue;
5329
+ const order = node2.parent === null ? null : orders.get(id) ?? null;
5330
+ files.set(id, { name: nodeFileName(id), text: serializeNode(node2, order), order });
5331
5331
  }
5332
5332
  if (changed === null) {
5333
5333
  for (const id of orders.keys()) if (!document.nodes[id]) orders.delete(id);
@@ -5349,12 +5349,12 @@ function readDocumentFiles(headText, nodeTexts) {
5349
5349
  throw new Error("a layer file has no id or type");
5350
5350
  }
5351
5351
  const { order, ...rest2 } = parsed;
5352
- const node = { props: {}, style: {}, ...rest2, children: [] };
5353
- if (typeof node.tag !== "string") node.tag = "div";
5354
- if (node.parent === void 0) node.parent = null;
5355
- if (nodes[node.id]) repairs.push(`two files hold layer ${node.id}; the last read was kept`);
5356
- nodes[node.id] = node;
5357
- if (isKey(order)) orders.set(node.id, order);
5352
+ const node2 = { props: {}, style: {}, ...rest2, children: [] };
5353
+ if (typeof node2.tag !== "string") node2.tag = "div";
5354
+ if (node2.parent === void 0) node2.parent = null;
5355
+ if (nodes[node2.id]) repairs.push(`two files hold layer ${node2.id}; the last read was kept`);
5356
+ nodes[node2.id] = node2;
5357
+ if (isKey(order)) orders.set(node2.id, order);
5358
5358
  }
5359
5359
  const root = nodes[rootId];
5360
5360
  if (!root) throw new Error(`the root layer ${rootId} has no file`);
@@ -5392,11 +5392,11 @@ function readDocumentFiles(headText, nodeTexts) {
5392
5392
  if (reached.has(id)) continue;
5393
5393
  const parent = nodes[id].parent;
5394
5394
  if (parent && reached.has(parent)) continue;
5395
- const node = nodes[id];
5395
+ const node2 = nodes[id];
5396
5396
  if (parent && nodes[parent]) {
5397
5397
  nodes[parent].children = nodes[parent].children.filter((child) => child !== id);
5398
5398
  }
5399
- node.parent = rootId;
5399
+ node2.parent = rootId;
5400
5400
  root.children = [...root.children, id];
5401
5401
  repairs.push(`layer ${id} was inside itself and was put at the top level`);
5402
5402
  const lift = [id];
@@ -6258,10 +6258,10 @@ function rewriteStyled(styled, css) {
6258
6258
  }
6259
6259
  function rewriteDocument(document, rewrite) {
6260
6260
  let nodes = null;
6261
- for (const [id, node] of Object.entries(document.nodes)) {
6262
- const props = rewriteBag(node.props, rewrite.attr);
6263
- const styled = rewriteStyled(node, rewrite.css);
6264
- if (props === node.props && styled === node) continue;
6261
+ for (const [id, node2] of Object.entries(document.nodes)) {
6262
+ const props = rewriteBag(node2.props, rewrite.attr);
6263
+ const styled = rewriteStyled(node2, rewrite.css);
6264
+ if (props === node2.props && styled === node2) continue;
6265
6265
  (nodes ??= { ...document.nodes })[id] = { ...styled, props };
6266
6266
  }
6267
6267
  let styles = document.styles;
@@ -8810,9 +8810,9 @@ function applyChanges(folderPath, changes) {
8810
8810
  ownedNodes.add(nodes);
8811
8811
  }
8812
8812
  const before = {};
8813
- for (const node of changes.nodes) {
8814
- if (nodes[node.id]) before[node.id] = nodes[node.id];
8815
- nodes[node.id] = node;
8813
+ for (const node2 of changes.nodes) {
8814
+ if (nodes[node2.id]) before[node2.id] = nodes[node2.id];
8815
+ nodes[node2.id] = node2;
8816
8816
  }
8817
8817
  for (const id of changes.removed) {
8818
8818
  if (!nodes[id]) continue;
@@ -9015,8 +9015,8 @@ function adopt(folderPath, options = {}) {
9015
9015
  return { ok: true, report: { wrote, scaffolded, ...moved.length ? { moved } : {} } };
9016
9016
  }
9017
9017
  function openProject(folderPath, options = {}) {
9018
- const adopted = adopt(folderPath, options);
9019
- if (!adopted.ok) return { ok: false, error: adopted.error };
9018
+ const adopted2 = adopt(folderPath, options);
9019
+ if (!adopted2.ok) return { ok: false, error: adopted2.error };
9020
9020
  let manifest;
9021
9021
  try {
9022
9022
  manifest = readJsonWithBackupSync(manifestPath(folderPath));
@@ -9041,7 +9041,7 @@ function openProject(folderPath, options = {}) {
9041
9041
  manifest,
9042
9042
  document: read.document,
9043
9043
  revision: read.revision,
9044
- ...adopted.report ? { adopted: adopted.report } : {},
9044
+ ...adopted2.report ? { adopted: adopted2.report } : {},
9045
9045
  ...read.repairs.length ? { repairs: read.repairs } : {}
9046
9046
  };
9047
9047
  }
@@ -9061,11 +9061,11 @@ function validateDocument(document) {
9061
9061
  if (onPath.has(id)) return `Cycle detected at node "${id}"`;
9062
9062
  if (visited.has(id)) continue;
9063
9063
  visited.add(id);
9064
- const node = document.nodes[id];
9065
- if (!node) return `Referenced node "${id}" does not exist`;
9064
+ const node2 = document.nodes[id];
9065
+ if (!node2) return `Referenced node "${id}" does not exist`;
9066
9066
  onPath.add(id);
9067
9067
  stack.push({ id, leaving: true });
9068
- for (const childId of node.children) {
9068
+ for (const childId of node2.children) {
9069
9069
  if (onPath.has(childId)) return `Cycle detected at node "${childId}"`;
9070
9070
  stack.push({ id: childId, leaving: false });
9071
9071
  }
@@ -9104,7 +9104,7 @@ async function saveChanges(folderPath, changes, media2, saved) {
9104
9104
  const { document, previous, headChanged } = applied;
9105
9105
  const changed = assetScope(
9106
9106
  document,
9107
- changes.nodes.map((node) => document.nodes[node.id]),
9107
+ changes.nodes.map((node2) => document.nodes[node2.id]),
9108
9108
  headChanged
9109
9109
  );
9110
9110
  const plan = planExtraction(changed);
@@ -9118,13 +9118,13 @@ async function saveChanges(folderPath, changes, media2, saved) {
9118
9118
  const { revision } = await writeDocumentAsync(
9119
9119
  folderPath,
9120
9120
  stored,
9121
- changes.nodes.map((node) => node.id),
9121
+ changes.nodes.map((node2) => node2.id),
9122
9122
  changes.removed
9123
9123
  );
9124
9124
  const after = referencedAssets(
9125
9125
  assetScope(
9126
9126
  stored,
9127
- changes.nodes.map((node) => stored.nodes[node.id]),
9127
+ changes.nodes.map((node2) => stored.nodes[node2.id]),
9128
9128
  headChanged
9129
9129
  )
9130
9130
  );
@@ -9141,7 +9141,7 @@ async function saveChanges(folderPath, changes, media2, saved) {
9141
9141
  }
9142
9142
  }
9143
9143
  function assetScope(document, nodes, withHead) {
9144
- const scoped = Object.fromEntries(nodes.map((node) => [node.id, node]));
9144
+ const scoped = Object.fromEntries(nodes.map((node2) => [node2.id, node2]));
9145
9145
  if (withHead) return { ...document, nodes: scoped };
9146
9146
  return { id: document.id, name: document.name, rootId: document.rootId, nodes: scoped };
9147
9147
  }
@@ -9758,6 +9758,11 @@ function registerProjectIpc(ipc) {
9758
9758
  return relative4 ? mediaFile(relative4) : null;
9759
9759
  });
9760
9760
  }
9761
+ function looksLikeProject(folderPath) {
9762
+ const folder = path10.resolve(folderPath);
9763
+ if (folder === path10.resolve(os2.homedir()) || folder === path10.parse(folder).root) return false;
9764
+ return fs12.existsSync(path10.join(folder, ".codecaine")) || fs12.existsSync(path10.join(folder, "package.json"));
9765
+ }
9761
9766
 
9762
9767
  // src/inbox.ts
9763
9768
  var MAX_CALLS = 60;
@@ -9771,9 +9776,17 @@ var Inbox = class {
9771
9776
  lastClient = null;
9772
9777
  theme = "dark";
9773
9778
  sequence = 0;
9779
+ /** Told whenever a request needs an agent: a new one, or one said again. */
9780
+ waiting = /* @__PURE__ */ new Set();
9781
+ /** Agents that receive requests as Claude Code channel pushes, by name —
9782
+ * what the Agent panel reports as "requests reach it". */
9783
+ pushTargets = /* @__PURE__ */ new Map();
9784
+ onWaiting(listener) {
9785
+ this.waiting.add(listener);
9786
+ return () => this.waiting.delete(listener);
9787
+ }
9774
9788
  heard(client) {
9775
- const name = client ?? this.lastClient;
9776
- if (!name) return;
9789
+ const name = client ?? this.lastClient ?? "Your coding agent";
9777
9790
  this.lastClient = name;
9778
9791
  this.clients.set(name, Date.now());
9779
9792
  }
@@ -9804,6 +9817,7 @@ var Inbox = class {
9804
9817
  };
9805
9818
  this.requests.unshift(request);
9806
9819
  if (this.requests.length > MAX_REQUESTS) this.requests.length = MAX_REQUESTS;
9820
+ for (const listener of this.waiting) listener(request);
9807
9821
  return request;
9808
9822
  }
9809
9823
  followUp(id, text3) {
@@ -9813,6 +9827,7 @@ var Inbox = class {
9813
9827
  request.status = "pending";
9814
9828
  request.step = "Waiting for your agent";
9815
9829
  request.updatedAt = Date.now();
9830
+ for (const listener of this.waiting) listener(request);
9816
9831
  return request;
9817
9832
  }
9818
9833
  /** Pending requests for a folder (every folder when none is named), handed
@@ -9858,18 +9873,169 @@ function titleOf(request) {
9858
9873
  return line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
9859
9874
  }
9860
9875
 
9861
- // src/agent.ts
9876
+ // ../editor/electron/bb/tool-server.ts
9877
+ import http2 from "node:http";
9878
+ import { randomBytes, timingSafeEqual } from "node:crypto";
9862
9879
  var DEFAULT_TIMEOUT_MS = 3e4;
9880
+ var SLOW_TOOL_TIMEOUT_MS = {
9881
+ observePage: 12e4,
9882
+ observeVideo: 12e4,
9883
+ canvasPointer: 9e4
9884
+ };
9885
+ var MAX_BODY_BYTES = 4 * 1024 * 1024;
9886
+ var pending = /* @__PURE__ */ new Map();
9887
+ var seq = 0;
9888
+ function routeToolResponse(requestId, result) {
9889
+ const entry = pending.get(requestId);
9890
+ if (!entry) return false;
9891
+ pending.delete(requestId);
9892
+ clearTimeout(entry.timer);
9893
+ entry.resolve(result);
9894
+ return true;
9895
+ }
9896
+ function tokenMatches(header3, token) {
9897
+ if (!header3) return false;
9898
+ const m = /^Bearer\s+(.+)$/i.exec(header3.trim());
9899
+ if (!m) return false;
9900
+ const got = Buffer.from(m[1], "utf8");
9901
+ const want = Buffer.from(token, "utf8");
9902
+ if (got.length !== want.length) return false;
9903
+ return timingSafeEqual(got, want);
9904
+ }
9905
+ function readBody(req) {
9906
+ return new Promise((resolve3) => {
9907
+ const chunks = [];
9908
+ let total = 0;
9909
+ req.on("data", (c) => {
9910
+ total += c.length;
9911
+ if (total > MAX_BODY_BYTES) {
9912
+ resolve3(null);
9913
+ req.destroy();
9914
+ return;
9915
+ }
9916
+ chunks.push(c);
9917
+ });
9918
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
9919
+ req.on("error", () => resolve3(null));
9920
+ });
9921
+ }
9922
+ function send(res, status, body) {
9923
+ const text3 = JSON.stringify(body);
9924
+ res.writeHead(status, {
9925
+ "content-type": "application/json",
9926
+ "content-length": Buffer.byteLength(text3)
9927
+ });
9928
+ res.end(text3);
9929
+ }
9930
+ function startToolServer(opts) {
9931
+ const token = randomBytes(32).toString("hex");
9932
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9933
+ const owner = {};
9934
+ const sockets = /* @__PURE__ */ new Set();
9935
+ const server2 = http2.createServer((req, res) => {
9936
+ void handle(req, res);
9937
+ });
9938
+ server2.on("connection", (s) => {
9939
+ sockets.add(s);
9940
+ s.on("close", () => sockets.delete(s));
9941
+ });
9942
+ async function handle(req, res) {
9943
+ const route = (req.url || "").split("?")[0];
9944
+ if (route !== "/tool") return send(res, 404, { error: "not found" });
9945
+ if (req.method !== "POST") return send(res, 405, { error: "use POST" });
9946
+ if (!tokenMatches(req.headers.authorization, token)) {
9947
+ return send(res, 401, { error: "unauthorized" });
9948
+ }
9949
+ const raw = await readBody(req);
9950
+ if (raw === null) return send(res, 413, { error: "request body too large" });
9951
+ let body;
9952
+ try {
9953
+ body = JSON.parse(raw || "{}");
9954
+ } catch {
9955
+ return send(res, 400, { error: "body must be JSON" });
9956
+ }
9957
+ const tool = typeof body.tool === "string" ? body.tool.trim() : "";
9958
+ if (!tool) return send(res, 200, { error: "missing 'tool'" });
9959
+ const params2 = body.params && typeof body.params === "object" ? body.params : {};
9960
+ const cwd = typeof body.cwd === "string" ? body.cwd : "";
9961
+ const deadline = opts.timeoutMs ?? SLOW_TOOL_TIMEOUT_MS[tool] ?? timeoutMs;
9962
+ const requestId = `bbtool-${process.pid}-${++seq}`;
9963
+ const result = await new Promise((resolve3) => {
9964
+ const timer = setTimeout(() => {
9965
+ pending.delete(requestId);
9966
+ resolve3({
9967
+ error: `no editor answered '${tool}' within ${deadline}ms \u2014 no project is open in the editor window, or it stopped responding`
9968
+ });
9969
+ }, deadline);
9970
+ timer.unref?.();
9971
+ pending.set(requestId, { resolve: resolve3, timer, owner });
9972
+ try {
9973
+ opts.send("canvasapi:call", { requestId, tool, params: params2, cwd });
9974
+ } catch (err) {
9975
+ pending.delete(requestId);
9976
+ clearTimeout(timer);
9977
+ resolve3({
9978
+ error: `the editor could not receive the call: ${err instanceof Error ? err.message : String(err)}`
9979
+ });
9980
+ }
9981
+ });
9982
+ if (!result || typeof result !== "object") {
9983
+ return send(res, 200, { error: "the editor sent an empty response" });
9984
+ }
9985
+ const failure = result.error;
9986
+ if (typeof failure === "string") return send(res, 200, { error: failure });
9987
+ return send(res, 200, { result });
9988
+ }
9989
+ return new Promise((resolve3, reject) => {
9990
+ server2.once("error", reject);
9991
+ server2.listen(0, "127.0.0.1", () => {
9992
+ server2.removeListener("error", reject);
9993
+ const addr = server2.address();
9994
+ let stopped = null;
9995
+ resolve3({
9996
+ url: `http://127.0.0.1:${addr.port}`,
9997
+ token,
9998
+ port: addr.port,
9999
+ stop() {
10000
+ if (stopped) return stopped;
10001
+ stopped = new Promise((done) => {
10002
+ for (const [id, entry] of pending) {
10003
+ if (entry.owner !== owner) continue;
10004
+ pending.delete(id);
10005
+ clearTimeout(entry.timer);
10006
+ entry.resolve({ error: "the editor's tool server stopped" });
10007
+ }
10008
+ const grace = setTimeout(() => {
10009
+ server2.closeAllConnections?.();
10010
+ for (const s of sockets) s.destroy();
10011
+ sockets.clear();
10012
+ }, 50);
10013
+ grace.unref?.();
10014
+ server2.close(() => {
10015
+ clearTimeout(grace);
10016
+ done();
10017
+ });
10018
+ server2.closeIdleConnections?.();
10019
+ });
10020
+ return stopped;
10021
+ }
10022
+ });
10023
+ });
10024
+ });
10025
+ }
10026
+
10027
+ // src/agent.ts
10028
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
9863
10029
  var LONG_TIMEOUT_MS = 18e4;
9864
10030
  var SHOT_DIR2 = path11.join(os3.tmpdir(), "codecaine-agent-shots");
9865
10031
  function createAgentHost(ipc, options) {
9866
- const pending = /* @__PURE__ */ new Map();
10032
+ const pending3 = /* @__PURE__ */ new Map();
9867
10033
  const inbox = new Inbox();
9868
10034
  let sequence = 0;
9869
10035
  ipc.handle("canvasapi:respond", async (message) => {
9870
- const entry = message?.requestId ? pending.get(message.requestId) : void 0;
9871
- if (!entry) return;
9872
- pending.delete(message.requestId);
10036
+ const entry = message?.requestId ? pending3.get(message.requestId) : void 0;
10037
+ if (!entry) return { ok: routeToolResponse(String(message?.requestId ?? ""), message?.result) };
10038
+ pending3.delete(message.requestId);
9873
10039
  clearTimeout(entry.timer);
9874
10040
  entry.resolve(message.result);
9875
10041
  });
@@ -9945,15 +10111,15 @@ function createAgentHost(ipc, options) {
9945
10111
  });
9946
10112
  }
9947
10113
  const requestId = `mcp-${Date.now().toString(36)}-${++sequence}`;
9948
- const wait = timeoutMs ?? (FRAMES_TOOLS.includes(tool) ? LONG_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
10114
+ const wait = timeoutMs ?? (FRAMES_TOOLS.includes(tool) ? LONG_TIMEOUT_MS : DEFAULT_TIMEOUT_MS2);
9949
10115
  return new Promise((resolve3) => {
9950
10116
  const timer = setTimeout(() => {
9951
- pending.delete(requestId);
10117
+ pending3.delete(requestId);
9952
10118
  resolve3({
9953
10119
  error: "The editor did not answer. Its tab may be on the Projects screen rather than a design \u2014 open the project (or call open-project) and try again."
9954
10120
  });
9955
10121
  }, wait);
9956
- pending.set(requestId, { resolve: resolve3, timer });
10122
+ pending3.set(requestId, { resolve: resolve3, timer });
9957
10123
  ipc.broadcast("canvasapi:call", { requestId, tool, params: params2, cwd });
9958
10124
  });
9959
10125
  }
@@ -10109,6 +10275,7 @@ function createAgentHost(ipc, options) {
10109
10275
  ].join("\n\n");
10110
10276
  return {
10111
10277
  inbox,
10278
+ bringToFront,
10112
10279
  call,
10113
10280
  instructions,
10114
10281
  tools: () => [
@@ -10228,14 +10395,16 @@ function agentPage() {
10228
10395
  <h2>Connect your coding agent</h2>
10229
10396
  <p class="muted" style="margin:0">Keep <code>npx codecaine-cli</code> running, then add it once:</p>
10230
10397
  <div class="step"><span class="faint">Claude Code</span>
10231
- <div class="copy"><pre>claude mcp add codecaine -- npx -y codecaine-cli mcp</pre><button type="button">Copy</button></div></div>
10398
+ <div class="copy"><pre>claude mcp add codecaine -- npx -y codecaine-cli mcp</pre><button type="button">Copy</button></div>
10399
+ <span class="faint">then start it so board requests reach it by themselves</span>
10400
+ <div class="copy"><pre>claude --dangerously-load-development-channels server:codecaine</pre><button type="button">Copy</button></div></div>
10232
10401
  <div class="step"><span class="faint">Cursor, Windsurf and others (mcp.json)</span>
10233
10402
  <div class="copy"><pre>{
10234
10403
  "mcpServers": {
10235
10404
  "codecaine": { "command": "npx", "args": ["-y", "codecaine-cli", "mcp"] }
10236
10405
  }
10237
10406
  }</pre><button type="button">Copy</button></div></div>
10238
- <p class="muted" style="margin:8px 0 0">Then, in your project's folder, ask it to <em>run the project and open it in Codecaine</em>. Requests typed on the board wait above until you tell it to <em>check Codecaine</em>.</p>
10407
+ <p class="muted" style="margin:8px 0 0">Then, in your project's folder, ask it to <em>run the project and open it in Codecaine</em>. Requests typed on the board go straight to it.</p>
10239
10408
  </section>
10240
10409
  <script>
10241
10410
  const project = new URLSearchParams(location.search).get("project") || "";
@@ -10271,6 +10440,9 @@ function agentPage() {
10271
10440
  const line = el("span");
10272
10441
  if (state.agent) {
10273
10442
  line.append(el("strong", null, state.agent.name), document.createTextNode(" is connected "), el("span", "faint", "\xB7 last heard " + ago(state.agent.lastSeen)));
10443
+ line.append(el("div", "faint", state.pushing
10444
+ ? "Requests from the board reach it by themselves."
10445
+ : "Requests wait until you tell it to check Codecaine \u2014 start it with --dangerously-load-development-channels server:codecaine to send them straight away."));
10274
10446
  } else {
10275
10447
  line.append(el("strong", null, "No coding agent connected"), document.createTextNode(" "), el("span", "faint", "\xB7 add one below"));
10276
10448
  }
@@ -10305,103 +10477,2434 @@ function agentPage() {
10305
10477
  </body>
10306
10478
  </html>`;
10307
10479
  }
10480
+ function assistantStartingPage() {
10481
+ return `<!doctype html>
10482
+ <html lang="en">
10483
+ <head>
10484
+ <meta charset="utf-8" />
10485
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10486
+ <title>Starting the assistant</title>
10487
+ <style>
10488
+ :root { --panel: #ffffff; --inset: #f5f5f5; --text: #17181c; --text-2: #696969; --text-3: #a8a8a8; --accent: #0099ff; --pink: #d42d73; }
10489
+ :root[data-theme="dark"] { --panel: #1f1f1f; --inset: #2e2e2e; --text: #f5f5f5; --text-2: #a8a8a8; --text-3: #8a8a8a; --pink: #f0679f; }
10490
+ * { box-sizing: border-box; }
10491
+ html, body { margin: 0; height: 100%; background: var(--panel); color: var(--text); }
10492
+ body { font: 12px/16px "Inter Variable", Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; letter-spacing: -0.01em;
10493
+ display: grid; place-content: center; padding: 24px 20px; text-align: center; }
10494
+ h1 { margin: 0 0 6px; font-size: 13px; line-height: 18px; font-weight: 600; }
10495
+ p { margin: 0; color: var(--text-2); }
10496
+ .bar { width: 220px; height: 4px; margin: 14px auto 8px; border-radius: 2px; background: var(--inset); overflow: hidden; }
10497
+ .fill { height: 100%; width: 0; background: var(--accent); border-radius: 2px; transition: width .4s ease; }
10498
+ .bar.unknown .fill { width: 35%; animation: slide 1.2s ease-in-out infinite; }
10499
+ @keyframes slide { from { transform: translateX(-100%); } to { transform: translateX(290%); } }
10500
+ @media (prefers-reduced-motion: reduce) { .bar.unknown .fill { animation: none; width: 100%; opacity: .5; } }
10501
+ .note { color: var(--text-3); }
10502
+ .error { color: var(--pink); margin-top: 8px; max-width: 280px; word-break: break-word; }
10503
+ </style>
10504
+ </head>
10505
+ <body>
10506
+ <h1 id="title">Starting the assistant\u2026</h1>
10507
+ <p id="what">The first time, it downloads (about 240 MB) \u2014 after that it starts in seconds.</p>
10508
+ <div class="bar unknown" id="bar"><div class="fill" id="fill"></div></div>
10509
+ <p class="note" id="note"></p>
10510
+ <p class="error" id="error" hidden></p>
10511
+ <script>
10512
+ const query = new URLSearchParams(location.search);
10513
+ document.documentElement.dataset.theme = matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
10514
+ async function poll() {
10515
+ let answer;
10516
+ try {
10517
+ answer = await (await fetch("assistant?" + query.toString(), { cache: "no-store" })).json();
10518
+ } catch {
10519
+ document.getElementById("note").textContent = "Lost the companion \u2014 is npx codecaine-cli still running?";
10520
+ return setTimeout(poll, 2000);
10521
+ }
10522
+ const s = answer.state;
10523
+ document.documentElement.dataset.theme = answer.theme || document.documentElement.dataset.theme;
10524
+ if (answer.url) return location.replace(answer.url);
10525
+ document.getElementById("title").textContent = s.phase === "failed" ? "The assistant did not start" : s.message || "Starting the assistant\u2026";
10526
+ const bar = document.getElementById("bar");
10527
+ bar.classList.toggle("unknown", s.percent == null);
10528
+ document.getElementById("fill").style.width = s.percent == null ? "" : s.percent + "%";
10529
+ document.getElementById("note").textContent = s.phase === "installing" && s.percent != null ? s.percent + "%" : "";
10530
+ const error = document.getElementById("error");
10531
+ error.hidden = s.phase !== "failed";
10532
+ error.textContent = s.error || "";
10533
+ document.getElementById("what").hidden = s.phase !== "installing" && s.phase !== "idle";
10534
+ setTimeout(poll, s.phase === "failed" ? 5000 : 800);
10535
+ }
10536
+ poll();
10537
+ </script>
10538
+ </body>
10539
+ </html>`;
10540
+ }
10308
10541
 
10309
- // src/ipc.ts
10310
- var Ipc = class {
10311
- handlers = /* @__PURE__ */ new Map();
10312
- peers = /* @__PURE__ */ new Set();
10313
- handle(channel, handler) {
10314
- this.handlers.set(channel, handler);
10542
+ // src/bb.ts
10543
+ import { spawn } from "node:child_process";
10544
+ import fs19 from "node:fs";
10545
+ import path18 from "node:path";
10546
+
10547
+ // ../editor/electron/bb/supervisor.ts
10548
+ import os4 from "node:os";
10549
+ import path12 from "node:path";
10550
+ import net2 from "node:net";
10551
+ import fs14 from "node:fs";
10552
+ import { createRequire } from "node:module";
10553
+ import { spawn as nodeSpawn } from "node:child_process";
10554
+
10555
+ // ../editor/electron/bb/client.ts
10556
+ function isHostConnected(host) {
10557
+ return host.status === "connected";
10558
+ }
10559
+ var BB_PERMISSION_MODES = ["accept-edits", "auto", "full"];
10560
+ function isBbPermissionMode(value) {
10561
+ return typeof value === "string" && BB_PERMISSION_MODES.includes(value);
10562
+ }
10563
+ var BB_THREAD_STATUSES = [
10564
+ "pending",
10565
+ "idle",
10566
+ "starting",
10567
+ "active",
10568
+ "stopping",
10569
+ "error"
10570
+ ];
10571
+ var BbApiError = class extends Error {
10572
+ status;
10573
+ url;
10574
+ constructor(args2) {
10575
+ super(`Agent backend ${args2.status}: ${args2.message}`);
10576
+ this.name = "BbApiError";
10577
+ this.status = args2.status;
10578
+ this.url = args2.url;
10315
10579
  }
10316
- has(channel) {
10317
- return this.handlers.has(channel);
10580
+ };
10581
+ var DEFAULT_TIMEOUT_MS3 = 1e4;
10582
+ var REGISTRY_TIMEOUT_MS = 25e3;
10583
+ var INSTALL_TIMEOUT_MS = 13e4;
10584
+ function asRecord(value) {
10585
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
10586
+ }
10587
+ function errorMessage(status, body) {
10588
+ try {
10589
+ const rec = asRecord(JSON.parse(body));
10590
+ if (rec) {
10591
+ if (typeof rec.error === "string") return rec.error;
10592
+ if (typeof rec.message === "string") return rec.message;
10593
+ }
10594
+ } catch {
10318
10595
  }
10319
- async invoke(channel, args2) {
10320
- const handler = this.handlers.get(channel);
10321
- if (!handler) {
10322
- throw new Error(`${channel} is not available in the web build`);
10596
+ return body.trim() || `HTTP ${status}`;
10597
+ }
10598
+ function parseProject(value) {
10599
+ const rec = asRecord(value);
10600
+ if (!rec || typeof rec.id !== "string") return null;
10601
+ const sources = Array.isArray(rec.sources) ? rec.sources.flatMap((entry) => {
10602
+ const source = asRecord(entry);
10603
+ return source && typeof source.path === "string" && typeof source.hostId === "string" ? [{ hostId: source.hostId, path: source.path }] : [];
10604
+ }) : [];
10605
+ return {
10606
+ id: rec.id,
10607
+ name: typeof rec.name === "string" ? rec.name : rec.id,
10608
+ sources
10609
+ };
10610
+ }
10611
+ function parseThreadEvent(value) {
10612
+ const rec = asRecord(value);
10613
+ if (!rec || typeof rec.seq !== "number" || typeof rec.type !== "string") return null;
10614
+ return { seq: rec.seq, type: rec.type, data: asRecord(rec.data) ?? {} };
10615
+ }
10616
+ function parseThread(value) {
10617
+ const rec = asRecord(value);
10618
+ if (!rec || typeof rec.id !== "string") return null;
10619
+ return {
10620
+ id: rec.id,
10621
+ title: typeof rec.title === "string" ? rec.title : null,
10622
+ createdAt: typeof rec.createdAt === "number" ? rec.createdAt : 0,
10623
+ ...typeof rec.status === "string" && BB_THREAD_STATUSES.includes(rec.status) ? { status: rec.status } : {},
10624
+ ...typeof rec.providerId === "string" && rec.providerId ? { providerId: rec.providerId } : {},
10625
+ ...typeof rec.titleFallback === "string" ? { titleFallback: rec.titleFallback } : {},
10626
+ ...typeof rec.updatedAt === "number" ? { updatedAt: rec.updatedAt } : {},
10627
+ ...rec.hasPendingInteraction === true ? { hasPendingInteraction: true } : {}
10628
+ };
10629
+ }
10630
+ function parseSkill(value) {
10631
+ const rec = asRecord(value);
10632
+ if (!rec || typeof rec.id !== "string" || typeof rec.name !== "string") return null;
10633
+ return {
10634
+ id: rec.id,
10635
+ name: rec.name,
10636
+ description: typeof rec.description === "string" ? rec.description : null,
10637
+ scope: typeof rec.scope === "string" ? rec.scope : "unknown",
10638
+ manageable: rec.manageable === true,
10639
+ registrySkillId: typeof rec.registrySkillId === "string" ? rec.registrySkillId : null
10640
+ };
10641
+ }
10642
+ function parseRegistrySkill(value) {
10643
+ const rec = asRecord(value);
10644
+ if (!rec || typeof rec.id !== "string" || typeof rec.source !== "string" || typeof rec.skillId !== "string" || typeof rec.name !== "string") {
10645
+ return null;
10646
+ }
10647
+ return {
10648
+ id: rec.id,
10649
+ source: rec.source,
10650
+ skillId: rec.skillId,
10651
+ name: rec.name,
10652
+ installs: typeof rec.installs === "number" ? rec.installs : 0,
10653
+ url: typeof rec.url === "string" ? rec.url : `https://www.skills.sh/${rec.id}`,
10654
+ summary: typeof rec.summary === "string" ? rec.summary : null
10655
+ };
10656
+ }
10657
+ function parseTerminal(value) {
10658
+ const rec = asRecord(value);
10659
+ const id = rec && typeof rec.id === "string" ? rec.id : null;
10660
+ const status = rec?.status;
10661
+ if (!id || status !== "starting" && status !== "disconnected" && status !== "running" && status !== "exited") {
10662
+ throw new Error("agent backend returned a malformed terminal");
10663
+ }
10664
+ return { id, status, exitCode: typeof rec?.exitCode === "number" ? rec.exitCode : null };
10665
+ }
10666
+ var BbClient = class {
10667
+ origin;
10668
+ baseUrl;
10669
+ fetchImpl;
10670
+ constructor(options) {
10671
+ this.origin = `http://127.0.0.1:${options.port}`;
10672
+ this.baseUrl = `${this.origin}/api/v1`;
10673
+ this.fetchImpl = options.fetchImpl ?? fetch;
10674
+ }
10675
+ async request(method, url, body, timeoutMs = DEFAULT_TIMEOUT_MS3) {
10676
+ const controller = new AbortController();
10677
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
10678
+ timer.unref?.();
10679
+ let response;
10680
+ try {
10681
+ response = await this.fetchImpl(url, {
10682
+ method,
10683
+ // A JSON content-type is not decoration on the plugin routes: bb's
10684
+ // "local" auth requires it for a mutation, which is what stops a page
10685
+ // in a browser from forging one.
10686
+ headers: body === void 0 ? {} : { "content-type": "application/json" },
10687
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
10688
+ signal: controller.signal
10689
+ });
10690
+ } finally {
10691
+ clearTimeout(timer);
10692
+ }
10693
+ const text3 = await response.text();
10694
+ if (!response.ok) {
10695
+ throw new BbApiError({
10696
+ status: response.status,
10697
+ message: errorMessage(response.status, text3),
10698
+ url
10699
+ });
10700
+ }
10701
+ if (!text3) return null;
10702
+ try {
10703
+ return JSON.parse(text3);
10704
+ } catch {
10705
+ return text3;
10323
10706
  }
10324
- return handler(...args2);
10325
10707
  }
10326
- addPeer(peer) {
10327
- this.peers.add(peer);
10328
- return () => this.peers.delete(peer);
10708
+ /** `GET /health`, off the API prefix. Answers before the host daemon has
10709
+ * enrolled, which is why it is one of three probes and not the gate. */
10710
+ async health(timeoutMs) {
10711
+ try {
10712
+ await this.request("GET", `${this.origin}/health`, void 0, timeoutMs);
10713
+ return true;
10714
+ } catch {
10715
+ return false;
10716
+ }
10717
+ }
10718
+ /** `GET /api/v1/system/config`. Read as an opaque record: we only need it to
10719
+ * prove the server finished booting, since answering it reads config and the
10720
+ * database. */
10721
+ async systemConfig(timeoutMs) {
10722
+ const value = await this.request(
10723
+ "GET",
10724
+ `${this.baseUrl}/system/config`,
10725
+ void 0,
10726
+ timeoutMs
10727
+ );
10728
+ const rec = asRecord(value);
10729
+ if (!rec) throw new Error("agent backend returned a malformed system config");
10730
+ return rec;
10731
+ }
10732
+ /** `GET /api/v1/system/providers/state`: each provider CLI's health, with
10733
+ * the command that signs it in. Opaque here; bb-login.ts reads it. A health
10734
+ * check runs the CLI on the host, hence the longer wait. */
10735
+ async providerStates(timeoutMs = 2e4) {
10736
+ return this.request("GET", `${this.baseUrl}/system/providers/state`, void 0, timeoutMs);
10737
+ }
10738
+ /** `GET /api/v1/system/providers`: the providers bb offers, already in the
10739
+ * order its Settings › Providers list shows them. Opaque here;
10740
+ * provider-choice.ts reads it. */
10741
+ async providers(timeoutMs = 2e4) {
10742
+ return this.request("GET", `${this.baseUrl}/system/providers`, void 0, timeoutMs);
10743
+ }
10744
+ /** `GET /api/v1/system/execution-options`: for one provider, its models
10745
+ * (each with the reasoning levels it takes), the permission modes and
10746
+ * composer actions (plan) it supports, and the host's permission ceiling.
10747
+ * Opaque here; compose-options.ts reads it. Listing models runs the CLI on
10748
+ * the host, hence the longer wait. */
10749
+ async executionOptions(providerId, timeoutMs = 2e4) {
10750
+ const query = providerId ? `?providerId=${encodeURIComponent(providerId)}` : "";
10751
+ return this.request("GET", `${this.baseUrl}/system/execution-options${query}`, void 0, timeoutMs);
10752
+ }
10753
+ /** `GET /api/v1/projects/:id/branches`: the checkout of the project's source
10754
+ * on a host — the branch it is on, the local branches, whether the tree is
10755
+ * dirty. Opaque here; compose-options.ts reads it. */
10756
+ async projectBranches(projectId, hostId, timeoutMs = 15e3) {
10757
+ return this.request(
10758
+ "GET",
10759
+ `${this.baseUrl}/projects/${encodeURIComponent(projectId)}/branches?hostId=${encodeURIComponent(hostId)}`,
10760
+ void 0,
10761
+ timeoutMs
10762
+ );
10329
10763
  }
10330
- peerCount() {
10331
- return this.peers.size;
10764
+ /** A provider's logo, at the `logoUrl` path `providers()` answered, as its
10765
+ * text. bb's logos are SVG; the caller checks before trusting that. */
10766
+ async providerLogo(logoPath, timeoutMs) {
10767
+ const value = await this.request("GET", new URL(logoPath, this.origin).toString(), void 0, timeoutMs);
10768
+ return typeof value === "string" ? value : "";
10769
+ }
10770
+ /** `PUT /api/v1/settings/general`. bb's schema is strict and whole — every
10771
+ * field, no partial update — so the caller sends back the `generalSettings`
10772
+ * it read from `systemConfig` with its one change. */
10773
+ async putGeneralSettings(settings) {
10774
+ return this.request("PUT", `${this.baseUrl}/settings/general`, settings);
10775
+ }
10776
+ /** `GET /api/v1/hosts`. Rows bb cannot parse are dropped rather than thrown
10777
+ * over: one unknown host must not hide the connected one beside it. */
10778
+ async hosts(timeoutMs) {
10779
+ const value = await this.request(
10780
+ "GET",
10781
+ `${this.baseUrl}/hosts`,
10782
+ void 0,
10783
+ timeoutMs
10784
+ );
10785
+ if (!Array.isArray(value)) {
10786
+ throw new Error("agent backend returned a malformed host list");
10787
+ }
10788
+ return value.flatMap((entry) => {
10789
+ const rec = asRecord(entry);
10790
+ const id = rec && typeof rec.id === "string" ? rec.id : null;
10791
+ const status = rec && (rec.status === "connected" || rec.status === "disconnected") ? rec.status : null;
10792
+ if (!id || !status) return [];
10793
+ const name = rec && typeof rec.name === "string" ? rec.name : id;
10794
+ return [{ id, name, status }];
10795
+ });
10332
10796
  }
10333
- /** `webContents.send` to every connected editor. */
10334
- broadcast(channel, ...args2) {
10335
- for (const peer of this.peers) peer.send(channel, ...args2);
10797
+ // ─────────────────────────── projects ───────────────────────────
10798
+ /** `GET /api/v1/projects`. Rows bb answers with that this build cannot read
10799
+ * are dropped rather than thrown over: one unfamiliar project must not hide
10800
+ * the folder the editor has open. */
10801
+ async projects() {
10802
+ const value = await this.request("GET", `${this.baseUrl}/projects`);
10803
+ if (!Array.isArray(value)) {
10804
+ throw new Error("agent backend returned a malformed project list");
10805
+ }
10806
+ return value.flatMap((entry) => {
10807
+ const project = parseProject(entry);
10808
+ return project ? [project] : [];
10809
+ });
10336
10810
  }
10337
- };
10338
-
10339
- // src/wire.ts
10340
- var BYTES = "$bytes";
10341
- function toBase64(bytes) {
10342
- if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
10343
- let binary = "";
10344
- for (let i = 0; i < bytes.length; i += 32768) {
10345
- binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
10811
+ /**
10812
+ * `POST /api/v1/projects` → 201.
10813
+ *
10814
+ * The wire body is `{name, source:{hostId, type:"local_path", path}}`, whose
10815
+ * `source` arm is strict. The caller has a flat `{name, root, hostId}`, which
10816
+ * is what it knows, so the nesting is assembled here.
10817
+ */
10818
+ async createProject(args2) {
10819
+ const value = await this.request("POST", `${this.baseUrl}/projects`, {
10820
+ name: args2.name,
10821
+ source: { hostId: args2.hostId, type: "local_path", path: args2.root }
10822
+ });
10823
+ const project = parseProject(value);
10824
+ if (!project) throw new Error("agent backend returned a malformed project");
10825
+ return project;
10826
+ }
10827
+ // ─────────────────────────── threads ───────────────────────────
10828
+ /** `GET /api/v1/threads?projectId=…`. An unreadable list is an empty one:
10829
+ * the panel then frames bb's compose view, which still works. */
10830
+ async listThreads(projectId) {
10831
+ const value = await this.request(
10832
+ "GET",
10833
+ `${this.baseUrl}/threads?projectId=${encodeURIComponent(projectId)}&archived=false`
10834
+ );
10835
+ const rows = Array.isArray(value) ? value : Array.isArray(asRecord(value)?.threads) ? asRecord(value).threads : [];
10836
+ return rows.flatMap((entry) => {
10837
+ const thread = parseThread(entry);
10838
+ return thread ? [thread] : [];
10839
+ });
10346
10840
  }
10347
- return btoa(binary);
10348
- }
10349
- function fromBase64(text3) {
10350
- if (typeof Buffer !== "undefined") {
10351
- const buffer = Buffer.from(text3, "base64");
10352
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
10841
+ /**
10842
+ * `GET /api/v1/threads/:id` → the thread's `status`.
10843
+ *
10844
+ * What tells a save that the assistant's turn is over: `bb:send` resolves
10845
+ * when the message is posted, and this is the one place bb says whether the
10846
+ * turn it started is still running. Anything this build does not recognise
10847
+ * is answered as null rather than guessed at.
10848
+ */
10849
+ async threadStatus(threadId) {
10850
+ const value = await this.request(
10851
+ "GET",
10852
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}`
10853
+ );
10854
+ const status = asRecord(value)?.status;
10855
+ return typeof status === "string" && BB_THREAD_STATUSES.includes(status) ? status : null;
10856
+ }
10857
+ /** `POST /api/v1/threads` → 201. `origin` is required; "sdk" is what a
10858
+ * programmatic sender is. With no `execution`, bb picks the provider
10859
+ * itself — see model-choice.ts for why the editor names one. */
10860
+ async createThread(args2) {
10861
+ const execution = args2.execution;
10862
+ const value = await this.request("POST", `${this.baseUrl}/threads`, {
10863
+ projectId: args2.projectId,
10864
+ origin: args2.origin ?? "sdk",
10865
+ input: args2.input,
10866
+ environment: args2.environment,
10867
+ ...args2.visibility ? { visibility: args2.visibility } : {},
10868
+ ...execution ? { providerId: execution.providerId } : {},
10869
+ ...execution?.model ? { model: execution.model } : {},
10870
+ ...execution?.reasoningLevel ? { reasoningLevel: execution.reasoningLevel } : {},
10871
+ ...args2.permissionMode ? { permissionMode: args2.permissionMode } : {}
10872
+ }, args2.timeoutMs);
10873
+ const thread = parseThread(value);
10874
+ if (!thread) throw new Error("agent backend returned a malformed thread");
10875
+ return thread;
10876
+ }
10877
+ /**
10878
+ * `POST /api/v1/threads/:id/send`.
10879
+ *
10880
+ * `steer-if-active` is what a composer means: deliver into a running turn if
10881
+ * there is one, otherwise start one.
10882
+ */
10883
+ async sendMessage(threadId, input, options = {}) {
10884
+ await this.request(
10885
+ "POST",
10886
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/send`,
10887
+ {
10888
+ input,
10889
+ mode: options.mode ?? "steer-if-active",
10890
+ ...options.reasoningLevel ? { reasoningLevel: options.reasoningLevel } : {}
10891
+ }
10892
+ );
10353
10893
  }
10354
- const binary = atob(text3);
10355
- const out = new Uint8Array(binary.length);
10356
- for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
10357
- return out.buffer;
10358
- }
10359
- function encode(value) {
10360
- return JSON.stringify(value, (_key, item) => {
10361
- if (item instanceof ArrayBuffer) return { [BYTES]: toBase64(new Uint8Array(item)) };
10362
- if (ArrayBuffer.isView(item)) {
10363
- return { [BYTES]: toBase64(new Uint8Array(item.buffer, item.byteOffset, item.byteLength)) };
10894
+ /**
10895
+ * The newest few items of a thread's log, newest first — one small request
10896
+ * however long the conversation is. What the Agent bar's jobs list names
10897
+ * each running job's current step from (thread-step.ts).
10898
+ */
10899
+ async latestThreadItems(threadId, limit = 6) {
10900
+ const value = await this.request(
10901
+ "GET",
10902
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/events?order=desc&limit=${Math.max(1, Math.floor(limit))}&types=item/started,item/completed`
10903
+ );
10904
+ const rows = Array.isArray(value) ? value : [];
10905
+ return rows.flatMap((entry) => {
10906
+ const event = parseThreadEvent(entry);
10907
+ return event ? [event] : [];
10908
+ });
10909
+ }
10910
+ /**
10911
+ * `GET /api/v1/threads/:id/events?afterSeq=…&order=asc`.
10912
+ *
10913
+ * The thread's event log, oldest first. bb PRUNES `item/agentMessage/delta`
10914
+ * rows once their `item/completed` lands (measured on 0.42.1: seqs 9–11 of a
10915
+ * finished reply were gone), so a reader that wants the stream has to be
10916
+ * reading while it happens; the completed item carries the whole text for
10917
+ * anyone who arrives late.
10918
+ */
10919
+ async threadEvents(threadId, afterSeq, limit = 500) {
10920
+ const value = await this.request(
10921
+ "GET",
10922
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/events?afterSeq=${Math.max(0, Math.floor(afterSeq))}&limit=${limit}&order=asc`
10923
+ );
10924
+ const rows = Array.isArray(value) ? value : [];
10925
+ return rows.flatMap((entry) => {
10926
+ const event = parseThreadEvent(entry);
10927
+ return event ? [event] : [];
10928
+ });
10929
+ }
10930
+ /** The newest event's `seq`, or 0 for an empty log — where a reader starts
10931
+ * so it sees only what a message it is about to send produces. */
10932
+ async latestEventSeq(threadId) {
10933
+ const value = await this.request(
10934
+ "GET",
10935
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/events?limit=1&order=desc`
10936
+ );
10937
+ const first = Array.isArray(value) ? parseThreadEvent(value[0]) : null;
10938
+ return first?.seq ?? 0;
10939
+ }
10940
+ /**
10941
+ * `GET /api/v1/threads/:id/events/wait?type=…&afterSeq=…&waitMs=…` — a long
10942
+ * poll that returns as soon as an event of `type` lands after `afterSeq`
10943
+ * (200) or when `waitMs` runs out (204). bb takes exactly ONE type here, so
10944
+ * a reader waits on the one it is latency-sensitive to and re-reads the whole
10945
+ * log on every wake. Answers whether something arrived; never throws for a
10946
+ * plain timeout.
10947
+ */
10948
+ async waitForEvent(threadId, type, afterSeq, waitMs) {
10949
+ const value = await this.request(
10950
+ "GET",
10951
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/events/wait?type=${encodeURIComponent(type)}&afterSeq=${Math.max(0, Math.floor(afterSeq))}&waitMs=${Math.max(0, Math.floor(waitMs))}`,
10952
+ void 0,
10953
+ waitMs + DEFAULT_TIMEOUT_MS3
10954
+ );
10955
+ return value !== null;
10956
+ }
10957
+ /** `POST /api/v1/threads/:id/stop` — interrupts the running turn. bb answers
10958
+ * `{ok:true}` at once and reaches `idle` a moment later. */
10959
+ async stopThread(threadId) {
10960
+ await this.request(
10961
+ "POST",
10962
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/stop`,
10963
+ {}
10964
+ );
10965
+ }
10966
+ /** `POST /api/v1/threads/:id/archive`. What bb's own docs ask of anything that
10967
+ * makes worker threads: put them away once they are done with. */
10968
+ async archiveThread(threadId) {
10969
+ await this.request(
10970
+ "POST",
10971
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/archive`,
10972
+ {}
10973
+ );
10974
+ }
10975
+ /**
10976
+ * `GET /api/v1/system/providers/state` — which providers are installed and
10977
+ * signed in on this machine. `ready` is the only status that can run a turn;
10978
+ * bb's own default provider need not be one of them (it chose Codex on a
10979
+ * machine with only Claude Code, and answered 503 to every thread).
10980
+ */
10981
+ async readyProviders() {
10982
+ const value = await this.request("GET", `${this.baseUrl}/system/providers/state`);
10983
+ const rows = asRecord(value)?.providers;
10984
+ return Array.isArray(rows) ? rows.flatMap((row2) => {
10985
+ const rec = asRecord(row2);
10986
+ return rec && rec.status === "ready" && typeof rec.providerId === "string" ? [rec.providerId] : [];
10987
+ }) : [];
10988
+ }
10989
+ /** `GET /api/v1/threads/:id/output` — the latest final assistant text, or
10990
+ * null when the thread has none yet. */
10991
+ async threadOutput(threadId) {
10992
+ const value = await this.request(
10993
+ "GET",
10994
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/output`
10995
+ );
10996
+ const output = asRecord(value)?.output;
10997
+ return typeof output === "string" ? output : null;
10998
+ }
10999
+ // ─────────────────────────── plugins ───────────────────────────
11000
+ /**
11001
+ * `POST /api/v1/plugins/install` `{source}`.
11002
+ *
11003
+ * bb answers `{ok:false, error}` with a 422 for a plugin it refuses — most
11004
+ * often one already registered from this same path, which is success as far
11005
+ * as the editor is concerned. The caller decides that; this only reports it.
11006
+ */
11007
+ async installPlugin(source) {
11008
+ const url = `${this.baseUrl}/plugins/install`;
11009
+ const value = await this.request("POST", url, { source });
11010
+ const rec = asRecord(value);
11011
+ if (rec && rec.ok === false) {
11012
+ throw new BbApiError({
11013
+ status: 422,
11014
+ message: typeof rec.error === "string" ? rec.error : "plugin install failed",
11015
+ url
11016
+ });
10364
11017
  }
10365
- return item;
10366
- });
10367
- }
10368
- function decode(text3) {
10369
- return JSON.parse(text3, (_key, item) => {
10370
- if (item && typeof item === "object" && BYTES in item && Object.keys(item).length === 1) {
10371
- return fromBase64(String(item[BYTES]));
11018
+ return rec ? asRecord(rec.plugin) ?? rec : null;
11019
+ }
11020
+ /**
11021
+ * `POST /api/v1/plugins/reload?id=<pluginId>`.
11022
+ *
11023
+ * Restarts a plugin's host, which is the only way code on disk becomes code
11024
+ * bb is running: the plugin is registered by path once and never read again,
11025
+ * so a `git pull` leaves an editor talking to the version installed months
11026
+ * ago — with the tools, the schemas and the instructions of that version.
11027
+ *
11028
+ * Everything the plugin holds in memory goes with the restart, the editor's
11029
+ * callback included. That is the caller's problem to sequence: the config
11030
+ * POST has to come after.
11031
+ */
11032
+ async reloadPlugin(pluginId) {
11033
+ const url = `${this.baseUrl}/plugins/reload?id=${encodeURIComponent(pluginId)}`;
11034
+ const value = await this.request("POST", url, {});
11035
+ const rec = asRecord(value);
11036
+ if (rec && rec.ok === false) {
11037
+ throw new BbApiError({
11038
+ status: 422,
11039
+ message: typeof rec.error === "string" ? rec.error : "plugin reload failed",
11040
+ url
11041
+ });
10372
11042
  }
10373
- return item;
10374
- });
10375
- }
11043
+ return rec;
11044
+ }
11045
+ /** One row of `GET /api/v1/plugins`, reduced to what the editor decides on. */
11046
+ async listPlugins() {
11047
+ const value = await this.request("GET", `${this.baseUrl}/plugins`);
11048
+ const rows = Array.isArray(value) ? value : Array.isArray(asRecord(value)?.plugins) ? asRecord(value).plugins : [];
11049
+ return rows.flatMap((entry) => {
11050
+ const rec = asRecord(entry);
11051
+ if (!rec || typeof rec.id !== "string") return [];
11052
+ return [
11053
+ {
11054
+ id: rec.id,
11055
+ source: typeof rec.source === "string" ? rec.source : "",
11056
+ rootDir: typeof rec.rootDir === "string" ? rec.rootDir : "",
11057
+ provenance: typeof rec.provenance === "string" ? rec.provenance : ""
11058
+ }
11059
+ ];
11060
+ });
11061
+ }
11062
+ /**
11063
+ * `DELETE /api/v1/plugins/:id`.
11064
+ *
11065
+ * Unloads the plugin's host and forgets its registration, settings and
11066
+ * secrets. bb refuses it for a built-in (409), and the editor only ever asks
11067
+ * it of a record that points at the editor's own plugin folder — see
11068
+ * `evictShadowingPlugins` in canvas-tools.ts for the one case that needs it.
11069
+ */
11070
+ async removePlugin(pluginId) {
11071
+ const url = `${this.baseUrl}/plugins/${encodeURIComponent(pluginId)}`;
11072
+ const value = await this.request("DELETE", url);
11073
+ const rec = asRecord(value);
11074
+ if (rec && rec.ok === false) {
11075
+ throw new BbApiError({
11076
+ status: 422,
11077
+ message: typeof rec.error === "string" ? rec.error : "plugin remove failed",
11078
+ url
11079
+ });
11080
+ }
11081
+ }
11082
+ /** `GET /api/v1/plugins/:id/settings` → the plugin's current values. bb
11083
+ * answers 404 until the plugin's factory has run. */
11084
+ async pluginSettings(pluginId) {
11085
+ const value = await this.request(
11086
+ "GET",
11087
+ `${this.baseUrl}/plugins/${encodeURIComponent(pluginId)}/settings`
11088
+ );
11089
+ return asRecord(asRecord(value)?.values) ?? {};
11090
+ }
11091
+ /** `PUT /api/v1/plugins/:id/settings` `{values}` — a partial update, which bb
11092
+ * validates against the plugin's schema (400 with the reason) and hands to
11093
+ * the running plugin. */
11094
+ async putPluginSettings(pluginId, values) {
11095
+ await this.request(
11096
+ "PUT",
11097
+ `${this.baseUrl}/plugins/${encodeURIComponent(pluginId)}/settings`,
11098
+ { values }
11099
+ );
11100
+ }
11101
+ // ─────────────────────────── skills ───────────────────────────
11102
+ /**
11103
+ * `GET /api/v1/projects/:id/skills?environmentId=`.
11104
+ *
11105
+ * Every skill bb would load for that project's workspace — builtin, user,
11106
+ * provider and plugin alike. `environmentId` is required-but-nullable on the
11107
+ * wire, which a query string can only say as an empty value.
11108
+ */
11109
+ async projectSkills(projectId) {
11110
+ const value = await this.request(
11111
+ "GET",
11112
+ `${this.baseUrl}/projects/${encodeURIComponent(projectId)}/skills?environmentId=`
11113
+ );
11114
+ const rows = asRecord(value)?.skills;
11115
+ if (!Array.isArray(rows)) {
11116
+ throw new Error("agent backend returned a malformed skill list");
11117
+ }
11118
+ return rows.flatMap((entry) => {
11119
+ const skill = parseSkill(entry);
11120
+ return skill ? [skill] : [];
11121
+ });
11122
+ }
11123
+ /** `DELETE /api/v1/projects/:id/skills` `{skillId, environmentId}`. bb
11124
+ * refuses (403) anything it did not install itself — bundled and plugin
11125
+ * skills are not the user's to remove. */
11126
+ async deleteSkill(projectId, skillId) {
11127
+ await this.request(
11128
+ "DELETE",
11129
+ `${this.baseUrl}/projects/${encodeURIComponent(projectId)}/skills`,
11130
+ { skillId, environmentId: null }
11131
+ );
11132
+ }
11133
+ /**
11134
+ * `GET /api/v1/skills-registry?q=&page=&perPage=`.
11135
+ *
11136
+ * bb's proxy onto skills.sh: trending with no query, all-time installs with
11137
+ * one. Server-wide, so no project is named.
11138
+ */
11139
+ async searchSkillsRegistry(args2) {
11140
+ const params2 = new URLSearchParams({
11141
+ q: args2.query,
11142
+ page: String(args2.page),
11143
+ perPage: String(args2.perPage)
11144
+ });
11145
+ const value = await this.request(
11146
+ "GET",
11147
+ `${this.baseUrl}/skills-registry?${params2.toString()}`,
11148
+ void 0,
11149
+ REGISTRY_TIMEOUT_MS
11150
+ );
11151
+ const rec = asRecord(value);
11152
+ if (!rec || !Array.isArray(rec.skills)) {
11153
+ throw new Error("agent backend returned a malformed registry page");
11154
+ }
11155
+ const pagination = asRecord(rec.pagination);
11156
+ return {
11157
+ skills: rec.skills.flatMap((entry) => {
11158
+ const skill = parseRegistrySkill(entry);
11159
+ return skill ? [skill] : [];
11160
+ }),
11161
+ hasMore: pagination?.hasMore === true,
11162
+ total: typeof pagination?.total === "number" ? pagination.total : rec.skills.length
11163
+ };
11164
+ }
11165
+ /**
11166
+ * `POST /api/v1/skills-registry/entries` `{ids}`.
11167
+ *
11168
+ * The same rows as a search, but read off each skill's own page, which is the
11169
+ * only place skills.sh carries a summary. bb drops ids it cannot resolve
11170
+ * rather than failing the batch.
11171
+ */
11172
+ async registryEntries(ids) {
11173
+ const value = await this.request(
11174
+ "POST",
11175
+ `${this.baseUrl}/skills-registry/entries`,
11176
+ { ids },
11177
+ REGISTRY_TIMEOUT_MS
11178
+ );
11179
+ const rows = asRecord(value)?.entries;
11180
+ return Array.isArray(rows) ? rows.flatMap((entry) => {
11181
+ const skill = parseRegistrySkill(entry);
11182
+ return skill ? [skill] : [];
11183
+ }) : [];
11184
+ }
11185
+ /**
11186
+ * `POST /api/v1/skills-registry/install` `{registrySkillId}`.
11187
+ *
11188
+ * bb runs the `skills` CLI under `npx` to fetch the package, validates the
11189
+ * SKILL.md against the registry entry and moves it into `<dataDir>/skills`.
11190
+ * Its own ceiling on that is two minutes, so ours sits just past it: a
11191
+ * shorter one would report a failure for an install that went on to land.
11192
+ */
11193
+ async installRegistrySkill(registrySkillId) {
11194
+ await this.request(
11195
+ "POST",
11196
+ `${this.baseUrl}/skills-registry/install`,
11197
+ { registrySkillId },
11198
+ INSTALL_TIMEOUT_MS
11199
+ );
11200
+ }
11201
+ // ─────────────────────────── terminals ───────────────────────────
11202
+ /** `POST /api/v1/terminals`: a pty on a host, in a folder, running one
11203
+ * command. The same terminals bb's own UI lists, so a dev server started
11204
+ * from the editor is one a person can find and stop there too. */
11205
+ async createTerminal(args2) {
11206
+ const value = await this.request("POST", `${this.baseUrl}/terminals`, {
11207
+ cols: args2.cols ?? 120,
11208
+ rows: args2.rows ?? 32,
11209
+ target: { kind: "host_path", hostId: args2.hostId, cwd: args2.cwd },
11210
+ start: { mode: "command", command: args2.command },
11211
+ title: args2.title
11212
+ });
11213
+ return parseTerminal(value);
11214
+ }
11215
+ /** `GET /api/v1/terminals/:id`. */
11216
+ async terminal(terminalId) {
11217
+ return parseTerminal(
11218
+ await this.request("GET", `${this.baseUrl}/terminals/${encodeURIComponent(terminalId)}`)
11219
+ );
11220
+ }
11221
+ /** `GET /api/v1/terminals/:id/output?sinceSeq=`: what the pty printed after
11222
+ * `sinceSeq`, and the sequence number to ask from next time. */
11223
+ async terminalOutput(terminalId, sinceSeq) {
11224
+ const value = asRecord(
11225
+ await this.request(
11226
+ "GET",
11227
+ `${this.baseUrl}/terminals/${encodeURIComponent(terminalId)}/output?sinceSeq=${sinceSeq}`
11228
+ )
11229
+ );
11230
+ const chunks = Array.isArray(value?.chunks) ? value.chunks : [];
11231
+ const text3 = chunks.map((chunk) => asRecord(chunk)?.dataBase64).filter((data) => typeof data === "string").map((data) => Buffer.from(data, "base64").toString("utf8")).join("");
11232
+ const nextSeq = typeof value?.nextSeq === "number" ? value.nextSeq : sinceSeq;
11233
+ return { text: text3, nextSeq };
11234
+ }
11235
+ /** `POST /api/v1/terminals/:id/close`. The body is required: bb answers
11236
+ * `{}` with a 400 ("expected one of force|if-clean"), which is how Cancel
11237
+ * once left the dev server running in bb while the panel said cancelled.
11238
+ * `force` because a dev server is never "clean" — it is always running. */
11239
+ async closeTerminal(terminalId) {
11240
+ await this.request(
11241
+ "POST",
11242
+ `${this.baseUrl}/terminals/${encodeURIComponent(terminalId)}/close`,
11243
+ { mode: "force", reason: "user" }
11244
+ );
11245
+ }
11246
+ /** Call a route the plugin registered with `bb.http.route`, which bb mounts
11247
+ * at `/api/v1/plugins/<id>/http/<path>`. */
11248
+ async pluginHttp(pluginId, routePath, body) {
11249
+ const path21 = routePath.replace(/^\/+/, "");
11250
+ const url = `${this.baseUrl}/plugins/${encodeURIComponent(pluginId)}/http/${path21}`;
11251
+ return this.request("POST", url, body ?? {});
11252
+ }
11253
+ };
10376
11254
 
10377
- // src/server.ts
10378
- var VERSION = "0.1.0";
10379
- var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
10380
- var MAX_BODY_BYTES = 8 * 1024 * 1024;
10381
- function hostIsLocal(host) {
10382
- if (!host) return true;
11255
+ // ../editor/electron/bb/supervisor.ts
11256
+ var BB_APP_VERSION = "0.42.1";
11257
+ var BB_DEFAULT_SERVER_PORT = 38886;
11258
+ var BB_DEFAULT_DAEMON_PORT = 38887;
11259
+ var USER_BB_DATA_DIR_NAME = ".bb";
11260
+ var PROBE_TIMEOUT_MS = 1e3;
11261
+ var ADOPT_READY_TIMEOUT_MS = 15e3;
11262
+ var SPAWN_READY_TIMEOUT_MS = 18e4;
11263
+ var READY_POLL_INTERVAL_MS = 250;
11264
+ function bbDataDir(homeDir = os4.homedir(), env = process.env) {
11265
+ return env.CANVAS_BB_DATA_DIR || path12.join(homeDir, ".canvas", "bb");
11266
+ }
11267
+ function bbServerPort(env = process.env) {
11268
+ const n = Number(env.CANVAS_BB_SERVER_PORT);
11269
+ return Number.isInteger(n) && n > 0 && n < 65536 ? n : BB_DEFAULT_SERVER_PORT;
11270
+ }
11271
+ function bbDaemonPort(env = process.env) {
11272
+ const n = Number(env.CANVAS_BB_DAEMON_PORT);
11273
+ return Number.isInteger(n) && n > 0 && n < 65536 ? n : BB_DEFAULT_DAEMON_PORT;
11274
+ }
11275
+ function assertSafeDataDir(dataDir2, homeDir) {
11276
+ const userBb = path12.resolve(homeDir, USER_BB_DATA_DIR_NAME);
11277
+ const resolved = path12.resolve(dataDir2);
11278
+ if (resolved === userBb || resolved.startsWith(`${userBb}${path12.sep}`)) {
11279
+ throw new Error(
11280
+ `refusing to run the agent backend against the user's own data dir (${resolved})`
11281
+ );
11282
+ }
11283
+ }
11284
+ function bundledLauncher(resolveFrom = (specifier) => createRequire(process.env.CODECAINE_BB_APP_DIR ? path12.join(process.env.CODECAINE_BB_APP_DIR, "package.json") : import.meta.url).resolve(specifier), exists = fs14.existsSync) {
11285
+ let launcher;
10383
11286
  try {
10384
- return LOOPBACK.has(new URL(`http://${host}`).hostname);
11287
+ launcher = resolveFrom("bb-app/dist/bb-app.js");
10385
11288
  } catch {
10386
- return false;
11289
+ return null;
11290
+ }
11291
+ const unpacked = launcher.replace(
11292
+ `${path12.sep}app.asar${path12.sep}`,
11293
+ `${path12.sep}app.asar.unpacked${path12.sep}`
11294
+ );
11295
+ return exists(unpacked) ? unpacked : null;
11296
+ }
11297
+ function buildLaunchCommand(args2) {
11298
+ const flags = [
11299
+ "start",
11300
+ "--data-dir",
11301
+ shQuote(args2.dataDir),
11302
+ "--server-bind-host",
11303
+ "127.0.0.1",
11304
+ "--server-port",
11305
+ String(args2.serverPort),
11306
+ "--host-daemon-port",
11307
+ String(args2.daemonPort)
11308
+ ].join(" ");
11309
+ if (args2.launcher) {
11310
+ return `if command -v node >/dev/null 2>&1; then exec node ${shQuote(args2.launcher)} ${flags}; else echo "the agent backend needs Node on your PATH to run" >&2; exit 127; fi`;
11311
+ }
11312
+ return `if command -v bb-app >/dev/null 2>&1; then exec bb-app ${flags}; else exec npx -y bb-app@${args2.version} ${flags}; fi`;
11313
+ }
11314
+ function loginShellInvocation(command, env = process.env, shell) {
11315
+ return { shell: shell ?? env.SHELL ?? "/bin/zsh", args: ["-lc", command] };
11316
+ }
11317
+ function shQuote(value) {
11318
+ return `'${value.replace(/'/g, `'\\''`)}'`;
11319
+ }
11320
+ var childExited = /* @__PURE__ */ new WeakSet();
11321
+ async function probeReady(client, timeoutMs) {
11322
+ if (!await client.health(timeoutMs)) return null;
11323
+ try {
11324
+ await client.systemConfig(timeoutMs);
11325
+ } catch {
11326
+ return null;
11327
+ }
11328
+ let hosts;
11329
+ try {
11330
+ hosts = await client.hosts(timeoutMs);
11331
+ } catch {
11332
+ return null;
10387
11333
  }
11334
+ return hosts.find(isHostConnected) ?? null;
10388
11335
  }
10389
- function tokenMatches(expected, given) {
10390
- if (!given) return false;
10391
- const a = Buffer.from(expected);
10392
- const b = Buffer.from(given);
10393
- return a.length === b.length && crypto3.timingSafeEqual(a, b);
11336
+ async function waitReady(client, deps, args2) {
11337
+ for (; ; ) {
11338
+ const host = await probeReady(client, args2.probeTimeoutMs);
11339
+ if (host) return host;
11340
+ if (args2.child && childExited.has(args2.child)) return null;
11341
+ if (deps.now() >= args2.deadline) return null;
11342
+ await deps.sleep(args2.pollIntervalMs);
11343
+ }
10394
11344
  }
10395
- function readBody(request) {
10396
- return new Promise((resolve3, reject) => {
10397
- const chunks = [];
10398
- let size = 0;
10399
- request.on("data", (chunk) => {
10400
- size += chunk.length;
10401
- if (size > MAX_BODY_BYTES) {
10402
- reject(new Error("Request body too large."));
10403
- request.destroy();
10404
- return;
11345
+ function defaultIsPortInUse(port, timeoutMs = 300) {
11346
+ return new Promise((resolve3) => {
11347
+ const socket = new net2.Socket();
11348
+ const done = (inUse) => {
11349
+ socket.removeAllListeners();
11350
+ socket.destroy();
11351
+ resolve3(inUse);
11352
+ };
11353
+ socket.setTimeout(timeoutMs);
11354
+ socket.once("connect", () => done(true));
11355
+ socket.once("timeout", () => done(true));
11356
+ socket.once(
11357
+ "error",
11358
+ (err) => done(err.code !== "ECONNREFUSED")
11359
+ );
11360
+ socket.connect(port, "127.0.0.1");
11361
+ });
11362
+ }
11363
+ var owned = null;
11364
+ var adopted = null;
11365
+ var launching = null;
11366
+ var starting = null;
11367
+ function ensureBbServer(options = {}) {
11368
+ if (starting) return starting;
11369
+ starting = startBbServer(options).finally(() => {
11370
+ starting = null;
11371
+ });
11372
+ return starting;
11373
+ }
11374
+ async function startBbServer(options = {}) {
11375
+ const env = options.env ?? process.env;
11376
+ const homeDir = options.homeDir ?? os4.homedir();
11377
+ const serverPort = options.serverPort ?? bbServerPort(env);
11378
+ const daemonPort = options.daemonPort ?? bbDaemonPort(env);
11379
+ const dataDir2 = options.dataDir ?? bbDataDir(homeDir, env);
11380
+ const probeTimeoutMs = options.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
11381
+ const pollIntervalMs = options.pollIntervalMs ?? READY_POLL_INTERVAL_MS;
11382
+ const deps = {
11383
+ now: options.now ?? Date.now,
11384
+ sleep: options.sleep ?? ((ms) => new Promise((r) => {
11385
+ const t = setTimeout(r, ms);
11386
+ t.unref?.();
11387
+ })),
11388
+ log: options.log ?? (() => {
11389
+ })
11390
+ };
11391
+ if (owned && await probeReady(owned.handle.client, probeTimeoutMs)) {
11392
+ return owned.handle;
11393
+ }
11394
+ const client = new BbClient({ port: serverPort, fetchImpl: options.fetchImpl });
11395
+ if (await client.health(probeTimeoutMs)) {
11396
+ deps.log(`[bb] server already listening on :${serverPort} \u2014 adopting it`);
11397
+ const host2 = await waitReady(client, deps, {
11398
+ probeTimeoutMs,
11399
+ pollIntervalMs,
11400
+ deadline: deps.now() + (options.readyTimeoutMs ?? ADOPT_READY_TIMEOUT_MS),
11401
+ child: null
11402
+ });
11403
+ if (!host2) {
11404
+ throw new Error(
11405
+ `an agent backend is listening on :${serverPort} but never reported a connected host \u2014 nothing can be run against it`
11406
+ );
11407
+ }
11408
+ let adoptedDaemonPort = daemonPort;
11409
+ let adoptedDataDir = dataDir2;
11410
+ try {
11411
+ const config = await client.systemConfig(probeTimeoutMs);
11412
+ if (typeof config.hostDaemonPort === "number") {
11413
+ adoptedDaemonPort = config.hostDaemonPort;
11414
+ }
11415
+ if (typeof config.dataDir === "string" && config.dataDir) {
11416
+ adoptedDataDir = config.dataDir;
11417
+ }
11418
+ } catch {
11419
+ }
11420
+ adopted = {
11421
+ client,
11422
+ serverPort,
11423
+ daemonPort: adoptedDaemonPort,
11424
+ dataDir: adoptedDataDir,
11425
+ hostId: host2.id,
11426
+ spawned: false
11427
+ };
11428
+ return adopted;
11429
+ }
11430
+ assertSafeDataDir(dataDir2, homeDir);
11431
+ const portInUse = options.isPortInUse ?? defaultIsPortInUse;
11432
+ if (await portInUse(daemonPort)) {
11433
+ throw new Error(
11434
+ `the agent backend's host-daemon port :${daemonPort} is already in use, but nothing is serving on :${serverPort} \u2014 usually a daemon orphaned by a previous session. Stop it and try again (\`lsof -nP -iTCP:${daemonPort} -sTCP:LISTEN\`).`
11435
+ );
11436
+ }
11437
+ const launcher = options.launcher === void 0 ? bundledLauncher() : options.launcher;
11438
+ const command = buildLaunchCommand({
11439
+ launcher,
11440
+ dataDir: dataDir2,
11441
+ serverPort,
11442
+ daemonPort,
11443
+ version: options.bbAppVersion ?? BB_APP_VERSION
11444
+ });
11445
+ deps.log(
11446
+ launcher ? `[bb] starting the bundled agent backend (${launcher})` : `[bb] no bundled agent backend \u2014 falling back to bb-app@${options.bbAppVersion ?? BB_APP_VERSION}`
11447
+ );
11448
+ const spawnImpl = options.spawnImpl ?? ((cmd, args2, opts) => nodeSpawn(cmd, args2, opts));
11449
+ const login = loginShellInvocation(command, env, options.shell);
11450
+ const child = spawnImpl(login.shell, login.args, {
11451
+ env: {
11452
+ ...env,
11453
+ // bb's telemetry defaults ON; this is not our data to send.
11454
+ BB_TELEMETRY: "false",
11455
+ BB_DATA_DIR: dataDir2,
11456
+ BB_SERVER_BIND_HOST: "127.0.0.1",
11457
+ BB_SERVER_PORT: String(serverPort),
11458
+ BB_HOST_DAEMON_PORT: String(daemonPort)
11459
+ }
11460
+ });
11461
+ let markExited;
11462
+ const exited = new Promise((resolve3) => markExited = resolve3);
11463
+ child.once("exit", () => {
11464
+ childExited.add(child);
11465
+ markExited();
11466
+ });
11467
+ child.once("error", () => {
11468
+ childExited.add(child);
11469
+ markExited();
11470
+ });
11471
+ launching = { child, exited };
11472
+ let stderr = "";
11473
+ child.stderr?.on("data", (chunk) => {
11474
+ stderr = (stderr + String(chunk)).slice(-2e3);
11475
+ });
11476
+ child.stdout?.on("data", () => {
11477
+ });
11478
+ const host = await waitReady(client, deps, {
11479
+ probeTimeoutMs,
11480
+ pollIntervalMs,
11481
+ deadline: deps.now() + (options.readyTimeoutMs ?? SPAWN_READY_TIMEOUT_MS),
11482
+ child
11483
+ });
11484
+ if (launching?.child === child) launching = null;
11485
+ if (!host) {
11486
+ try {
11487
+ child.kill("SIGTERM");
11488
+ } catch {
11489
+ }
11490
+ const tail = stderr.trim() ? `
11491
+ ${stderr.trim()}` : "";
11492
+ throw new Error(
11493
+ `The agent backend did not become ready on :${serverPort} (data dir ${dataDir2})${tail}`
11494
+ );
11495
+ }
11496
+ const handle = {
11497
+ client,
11498
+ serverPort,
11499
+ daemonPort,
11500
+ dataDir: dataDir2,
11501
+ hostId: host.id,
11502
+ spawned: true
11503
+ };
11504
+ if (owned && owned.child !== child) {
11505
+ try {
11506
+ owned.child.kill("SIGTERM");
11507
+ } catch {
11508
+ }
11509
+ }
11510
+ owned = { child, handle, exited };
11511
+ adopted = null;
11512
+ deps.log(`[bb] server ready on :${serverPort} (host ${host.id})`);
11513
+ return handle;
11514
+ }
11515
+
11516
+ // ../editor/electron/bb/canvas-tools.ts
11517
+ import fs16 from "node:fs";
11518
+ import path14 from "node:path";
11519
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
11520
+
11521
+ // ../editor/electron/bb/antigravity.ts
11522
+ import { execFile as execFile2 } from "node:child_process";
11523
+ import fs15 from "node:fs";
11524
+ import os5 from "node:os";
11525
+ import path13 from "node:path";
11526
+ import { fileURLToPath } from "node:url";
11527
+
11528
+ // ../editor/electron/bb/antigravity-ids.ts
11529
+ var ANTIGRAVITY_AGENT_ID = "antigravity";
11530
+ var ANTIGRAVITY_PROVIDER_ID = `acp-${ANTIGRAVITY_AGENT_ID}`;
11531
+ var ANTIGRAVITY_LOGO_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="12 13 88 88" fill="none"><path fill="currentColor" d="M89.6992 93.695C94.3659 97.195 101.366 94.8617 94.9492 88.445C75.6992 69.7783 79.7825 18.445 55.8659 18.445C31.9492 18.445 36.0325 69.7783 16.7825 88.445C9.78251 95.445 17.3658 97.195 22.0325 93.695C40.1159 81.445 38.9492 59.8617 55.8659 59.8617C72.7825 59.8617 71.6159 81.445 89.6992 93.695Z"/></svg>';
11532
+ var ANTIGRAVITY_LOGO_URL = `data:image/svg+xml,${encodeURIComponent(ANTIGRAVITY_LOGO_SVG)}`;
11533
+ var ANTIGRAVITY_TINT = { light: "#3186FF", dark: "#3186FF" };
11534
+
11535
+ // ../editor/electron/bb/antigravity.ts
11536
+ var PROVIDER_ACP_PLUGIN_ID = "provider-acp";
11537
+ function antigravityAgentEntry(paths) {
11538
+ const dirs = [.../* @__PURE__ */ new Set([path13.dirname(paths.node), path13.dirname(paths.agy), path13.dirname(paths.adapter), "/usr/bin", "/bin"])];
11539
+ return {
11540
+ id: ANTIGRAVITY_AGENT_ID,
11541
+ displayName: "Antigravity",
11542
+ command: paths.node,
11543
+ args: [paths.shim, paths.adapter],
11544
+ env: {
11545
+ PATH: dirs.join(":"),
11546
+ // Without it agy-acp looks for its own agy, and downloads one when it
11547
+ // finds none — not the person's, signed-in copy.
11548
+ AGY_BIN: paths.agy,
11549
+ AGY_SKIP_DOWNLOAD: "1",
11550
+ CANVAS_ANTIGRAVITY_MCP_ROOT: paths.mcpRoot
11551
+ },
11552
+ nativeSkillRoots: {
11553
+ user: [{ path: ".gemini/config/skills", recursive: true }, { path: ".agents/skills", recursive: true }],
11554
+ project: [{ path: ".agents/skills", recursive: true, ancestors: true }]
11555
+ }
11556
+ };
11557
+ }
11558
+ function readCustomAgents(value) {
11559
+ if (Array.isArray(value)) return [...value];
11560
+ if (typeof value !== "string" || !value.trim()) return [];
11561
+ const parsed = JSON.parse(value);
11562
+ if (!Array.isArray(parsed)) throw new Error("bb's custom ACP agents setting is not a list");
11563
+ return parsed;
11564
+ }
11565
+ var idOf = (entry) => entry && typeof entry === "object" ? entry.id : void 0;
11566
+ function withAntigravity(list, entry) {
11567
+ const at = list.findIndex((item) => idOf(item) === ANTIGRAVITY_AGENT_ID);
11568
+ if (!entry) return at === -1 ? null : list.filter((_, index) => index !== at);
11569
+ if (at !== -1 && JSON.stringify(list[at]) === JSON.stringify(entry)) return null;
11570
+ const next = [...list];
11571
+ if (at === -1) next.push(entry);
11572
+ else next[at] = entry;
11573
+ return next;
11574
+ }
11575
+ function antigravityShimPath(resourcesPath = process.resourcesPath, dirname5 = path13.dirname(fileURLToPath(import.meta.url))) {
11576
+ const shim = (dir2) => path13.join(dir2, "antigravity", "acp-shim.mjs");
11577
+ if (resourcesPath && fs15.existsSync(shim(resourcesPath))) return shim(resourcesPath);
11578
+ let dir = path13.resolve(dirname5);
11579
+ for (let up = 0; up < 6; up++) {
11580
+ if (fs15.existsSync(shim(path13.join(dir, "resources")))) return shim(path13.join(dir, "resources"));
11581
+ const parent = path13.dirname(dir);
11582
+ if (parent === dir) break;
11583
+ dir = parent;
11584
+ }
11585
+ return null;
11586
+ }
11587
+ function antigravityBinaries(home = os5.homedir()) {
11588
+ const bin = path13.join(home, ".local", "bin");
11589
+ return { agy: path13.join(bin, "agy"), adapter: path13.join(bin, "agy-acp") };
11590
+ }
11591
+ function executable(file) {
11592
+ try {
11593
+ fs15.accessSync(file, fs15.constants.X_OK);
11594
+ return fs15.statSync(file).isFile();
11595
+ } catch {
11596
+ return false;
11597
+ }
11598
+ }
11599
+ async function ensureAntigravityAgent(client, deps) {
11600
+ const log = deps.log ?? ((msg) => console.log(msg));
11601
+ const isExecutable = deps.isExecutable ?? executable;
11602
+ const { agy, adapter } = (deps.binaries ?? antigravityBinaries)();
11603
+ const shim = (deps.shimPath ?? antigravityShimPath)();
11604
+ let entry = null;
11605
+ if (shim && isExecutable(agy) && isExecutable(adapter)) {
11606
+ const node2 = await deps.resolveNode();
11607
+ if (node2) {
11608
+ entry = antigravityAgentEntry({
11609
+ node: node2,
11610
+ shim,
11611
+ adapter,
11612
+ agy,
11613
+ mcpRoot: deps.mcpRoot ?? path13.join(os5.homedir(), ".canvas", "antigravity-mcp")
11614
+ });
11615
+ } else {
11616
+ log("[bb] Antigravity not registered: no node on the login shell's PATH");
11617
+ }
11618
+ }
11619
+ try {
11620
+ const values = await client.pluginSettings(PROVIDER_ACP_PLUGIN_ID);
11621
+ const next = withAntigravity(readCustomAgents(values.customAgents), entry);
11622
+ if (!next) return entry ? "unchanged" : "unavailable";
11623
+ await client.putPluginSettings(PROVIDER_ACP_PLUGIN_ID, {
11624
+ customAgents: next.length === 0 ? "" : JSON.stringify(next)
11625
+ });
11626
+ log(entry ? `[bb] Antigravity registered (${adapter})` : "[bb] Antigravity removed: agy or agy-acp is gone");
11627
+ return entry ? "registered" : "removed";
11628
+ } catch (err) {
11629
+ log(`[bb] Antigravity not registered: ${err instanceof Error ? err.message : String(err)}`);
11630
+ return "unavailable";
11631
+ }
11632
+ }
11633
+ var node = null;
11634
+ function loginShellNode() {
11635
+ node ??= new Promise((resolve3) => {
11636
+ const login = loginShellInvocation('printf "node=%s\\n" "$(command -v node)"');
11637
+ execFile2(login.shell, login.args, { timeout: 1e4 }, (_error, stdout) => {
11638
+ const line = String(stdout ?? "").split(/\r?\n/).reverse().find((row2) => row2.startsWith("node="));
11639
+ const found = line?.slice(5).trim();
11640
+ resolve3(found && path13.isAbsolute(found) ? found : null);
11641
+ });
11642
+ }).then((found) => {
11643
+ if (!found) node = null;
11644
+ return found;
11645
+ });
11646
+ return node;
11647
+ }
11648
+ function registerAntigravity(client) {
11649
+ return ensureAntigravityAgent(client, { resolveNode: loginShellNode });
11650
+ }
11651
+
11652
+ // ../editor/electron/bb/canvas-tools.ts
11653
+ var PLUGIN_ID = "canvas-editor";
11654
+ var pending2 = null;
11655
+ var server = null;
11656
+ var lastError = null;
11657
+ function pluginSourceDir(resourcesPath = process.resourcesPath, dirname5 = path14.dirname(fileURLToPath2(import.meta.url))) {
11658
+ const has = (dir2) => fs16.existsSync(path14.join(dir2, "package.json"));
11659
+ if (resourcesPath) {
11660
+ const packaged = path14.join(resourcesPath, "bb-plugin");
11661
+ if (has(packaged)) return packaged;
11662
+ }
11663
+ let dir = path14.resolve(dirname5);
11664
+ for (let up = 0; up < 6; up++) {
11665
+ const candidate = path14.join(dir, "resources", "bb-plugin");
11666
+ if (has(candidate)) return candidate;
11667
+ const parent = path14.dirname(dir);
11668
+ if (parent === dir) break;
11669
+ dir = parent;
11670
+ }
11671
+ return null;
11672
+ }
11673
+ function sameDir(a, b) {
11674
+ const norm = (dir) => {
11675
+ const resolved = path14.resolve(dir);
11676
+ try {
11677
+ return fs16.realpathSync(resolved);
11678
+ } catch {
11679
+ return resolved;
11680
+ }
11681
+ };
11682
+ return norm(a) === norm(b);
11683
+ }
11684
+ async function evictShadowingPlugins(client, dir, log) {
11685
+ let records;
11686
+ try {
11687
+ records = await client.listPlugins();
11688
+ } catch (err) {
11689
+ log(`[bb] plugin list unavailable, not checking for a shadowing record: ${String(err)}`);
11690
+ return;
11691
+ }
11692
+ for (const record of records) {
11693
+ if (record.provenance === "builtin") continue;
11694
+ const ours = record.id === PLUGIN_ID;
11695
+ const here2 = record.rootDir !== "" && sameDir(record.rootDir, dir);
11696
+ if (ours === here2) continue;
11697
+ const why = ours ? `it loads from ${record.rootDir || "an unknown place"} rather than ${dir}` : `it is this plugin under another id and would own its tools`;
11698
+ try {
11699
+ await client.removePlugin(record.id);
11700
+ log(`[bb] removed plugin record "${record.id}": ${why}`);
11701
+ } catch (err) {
11702
+ log(`[bb] could not remove plugin record "${record.id}" (${why}): ${String(err)}`);
11703
+ }
11704
+ }
11705
+ }
11706
+ async function configure(client, handle) {
11707
+ await client.pluginHttp(PLUGIN_ID, "config", {
11708
+ url: handle.url,
11709
+ token: handle.token
11710
+ });
11711
+ }
11712
+ function ensureCanvasTools(send2, deps = {}) {
11713
+ if (pending2) return pending2;
11714
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
11715
+ const startServer = deps.startServer ?? startToolServer;
11716
+ const sourceDir = deps.sourceDir ?? (() => pluginSourceDir());
11717
+ const log = deps.log ?? ((m) => console.log(m));
11718
+ pending2 = (async () => {
11719
+ try {
11720
+ const dir = sourceDir();
11721
+ if (!dir) {
11722
+ lastError = "resources/bb-plugin not found";
11723
+ log(`[bb] layer tools unavailable: ${lastError}`);
11724
+ return null;
11725
+ }
11726
+ const { client } = await ensureServer();
11727
+ server ??= await startServer({ send: send2 });
11728
+ await evictShadowingPlugins(client, dir, log);
11729
+ try {
11730
+ await client.installPlugin(`path:${dir}`);
11731
+ } catch (err) {
11732
+ log(
11733
+ `[bb] plugin install skipped (likely already installed): ${String(err)}`
11734
+ );
11735
+ }
11736
+ try {
11737
+ await client.reloadPlugin(PLUGIN_ID);
11738
+ } catch (err) {
11739
+ log(`[bb] plugin reload skipped: ${String(err)}`);
11740
+ }
11741
+ await configure(client, server);
11742
+ await registerAntigravity(client);
11743
+ lastError = null;
11744
+ log(`[bb] layer tools ready on ${server.url}`);
11745
+ return server;
11746
+ } catch (err) {
11747
+ lastError = err instanceof Error ? err.message : String(err);
11748
+ log(`[bb] layer tools unavailable: ${lastError}`);
11749
+ pending2 = null;
11750
+ return null;
11751
+ }
11752
+ })();
11753
+ return pending2;
11754
+ }
11755
+ async function waitForCanvasTools(send2, options = {}) {
11756
+ const attempts = Math.max(1, options.attempts ?? 3);
11757
+ const attemptTimeoutMs = options.attemptTimeoutMs ?? 3e4;
11758
+ const backoffMs = options.backoffMs ?? [1e3, 3e3];
11759
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
11760
+ let reason = null;
11761
+ for (let attempt = 0; attempt < attempts; attempt++) {
11762
+ if (attempt > 0) {
11763
+ await sleep(backoffMs[Math.min(attempt - 1, backoffMs.length - 1)] ?? 0);
11764
+ }
11765
+ const trying = ensureCanvasTools(send2, options);
11766
+ let timer;
11767
+ const timedOut = new Promise((resolve3) => {
11768
+ timer = setTimeout(() => resolve3("timeout"), attemptTimeoutMs);
11769
+ });
11770
+ try {
11771
+ const outcome = await Promise.race([trying, timedOut]);
11772
+ if (outcome === "timeout") {
11773
+ if (pending2 === trying) pending2 = null;
11774
+ reason = `loading them took longer than ${Math.round(attemptTimeoutMs / 1e3)}s`;
11775
+ continue;
11776
+ }
11777
+ if (outcome) return outcome;
11778
+ reason = lastError;
11779
+ } catch (err) {
11780
+ if (pending2 === trying) pending2 = null;
11781
+ reason = err instanceof Error ? err.message : String(err);
11782
+ } finally {
11783
+ clearTimeout(timer);
11784
+ }
11785
+ }
11786
+ throw new Error(canvasToolsUnavailableMessage(reason));
11787
+ }
11788
+ function canvasToolsUnavailableMessage(reason) {
11789
+ return "the assistant's canvas tools didn't load, so nothing was sent \u2014 a conversation started without them can't see the board" + (reason ? ` (${reason})` : "") + ". Try again in a moment. If it keeps failing, open the Agent panel and press Reconnect, or quit and reopen the app.";
11790
+ }
11791
+ async function setAgentInstructions(send2, args2, deps = {}) {
11792
+ const log = deps.log ?? ((m) => console.log(m));
11793
+ try {
11794
+ return await pushToPlugin(
11795
+ send2,
11796
+ "instructions",
11797
+ {
11798
+ projectId: args2.projectId,
11799
+ instructions: args2.instructions,
11800
+ ...args2.path ? { path: args2.path } : {}
11801
+ },
11802
+ deps
11803
+ );
11804
+ } catch (err) {
11805
+ log(`[bb] instructions not delivered: ${String(err)}`);
11806
+ return false;
11807
+ }
11808
+ }
11809
+ async function pushToPlugin(send2, route, body, deps = {}) {
11810
+ const ready = await ensureCanvasTools(send2, deps);
11811
+ if (!ready) return false;
11812
+ const client = deps.client ?? (await (deps.ensureServer ?? ensureBbServer)()).client;
11813
+ await client.pluginHttp(PLUGIN_ID, route, {
11814
+ ...body,
11815
+ url: ready.url,
11816
+ token: ready.token
11817
+ });
11818
+ return true;
11819
+ }
11820
+ async function setCanvasSelection(send2, args2, deps = {}) {
11821
+ try {
11822
+ return await pushToPlugin(
11823
+ send2,
11824
+ "selection",
11825
+ {
11826
+ projectId: args2.projectId,
11827
+ layers: args2.layers,
11828
+ ...args2.path ? { path: args2.path } : {}
11829
+ },
11830
+ deps
11831
+ );
11832
+ } catch {
11833
+ return false;
11834
+ }
11835
+ }
11836
+ async function canvasToolsConnection(send2, args2 = {}, deps = {}) {
11837
+ try {
11838
+ const ready = await ensureCanvasTools(send2, deps);
11839
+ if (!ready) return { connected: false, projects: [] };
11840
+ const client = deps.client ?? (await (deps.ensureServer ?? ensureBbServer)()).client;
11841
+ const answer = await client.pluginHttp(PLUGIN_ID, "state", {
11842
+ url: ready.url,
11843
+ token: ready.token,
11844
+ // Only what we actually have: a project id with no picture behind it
11845
+ // would clear what the plugin is holding rather than restore it.
11846
+ ...args2.projectId ? { projectId: args2.projectId } : {},
11847
+ ...args2.projectId && args2.instructions !== void 0 ? { instructions: args2.instructions } : {},
11848
+ ...args2.projectId && args2.layers !== void 0 ? { layers: args2.layers } : {},
11849
+ ...args2.projectId && args2.path ? { path: args2.path } : {}
11850
+ });
11851
+ const projects = Array.isArray(answer?.projects) ? answer.projects.flatMap((entry) => {
11852
+ const row2 = entry ?? {};
11853
+ return typeof row2.projectId === "string" ? [
11854
+ {
11855
+ projectId: row2.projectId,
11856
+ instructions: typeof row2.instructions === "number" ? row2.instructions : 0,
11857
+ selection: typeof row2.selection === "number" ? row2.selection : 0
11858
+ }
11859
+ ] : [];
11860
+ }) : [];
11861
+ return { connected: answer?.connected === true, projects };
11862
+ } catch {
11863
+ return { connected: false, projects: [] };
11864
+ }
11865
+ }
11866
+ function canvasToolsStatus() {
11867
+ return { connected: server !== null, url: server?.url ?? null, error: lastError };
11868
+ }
11869
+
11870
+ // ../editor/electron/bb/project-map.ts
11871
+ import path15 from "node:path";
11872
+ function normalizeProjectRoot(root) {
11873
+ const trimmed = root.trim();
11874
+ if (!trimmed) return "";
11875
+ const normalized = path15.normalize(trimmed);
11876
+ return normalized === path15.sep ? normalized : normalized.replace(/[/\\]+$/u, "");
11877
+ }
11878
+ async function pickConnectedHostId(client) {
11879
+ const hosts = await client.hosts();
11880
+ const connected = hosts.find(isHostConnected);
11881
+ if (connected) return connected.id;
11882
+ throw new Error(noConnectedHostMessage(hosts));
11883
+ }
11884
+ function noConnectedHostMessage(hosts) {
11885
+ const seen = hosts.length ? `Hosts it reported: ${hosts.map((host) => `${host.name} (${host.status})`).join(", ")}.` : "It reported no hosts at all.";
11886
+ return `The agent backend has no connected host, so there is nowhere to run the assistant. ${seen} This machine enrols as a host a moment after the backend comes up \u2014 wait for it to finish, or reopen the project to relaunch it.`;
11887
+ }
11888
+ function findProjectForRoot(projects, root, hostId) {
11889
+ const atRoot = (project, onHost) => project.sources.some(
11890
+ (source) => normalizeProjectRoot(source.path) === root && (onHost === void 0 || source.hostId === onHost)
11891
+ );
11892
+ const matches = projects.filter((project) => atRoot(project));
11893
+ return matches.find((project) => atRoot(project, hostId)) ?? matches[0];
11894
+ }
11895
+ function createProjectMap(client) {
11896
+ const pending3 = /* @__PURE__ */ new Map();
11897
+ async function resolveUncached(root) {
11898
+ const hostId = await pickConnectedHostId(client);
11899
+ const existing = findProjectForRoot(await client.projects(), root, hostId);
11900
+ const projectId = existing ? existing.id : (await client.createProject({ name: path15.basename(root), root, hostId })).id;
11901
+ return {
11902
+ projectId,
11903
+ hostId,
11904
+ /**
11905
+ * ALWAYS `unmanaged`, at the real path. bb's default workspace is a
11906
+ * managed git worktree — a checkout inside bb's own data dir — so the
11907
+ * assistant would be reading and writing a directory that is not the one
11908
+ * the editor has open, and every change it made would appear to do
11909
+ * nothing. `hostId` is required alongside it.
11910
+ */
11911
+ environment: {
11912
+ type: "host",
11913
+ hostId,
11914
+ workspace: { type: "unmanaged", path: root }
11915
+ }
11916
+ };
11917
+ }
11918
+ return {
11919
+ resolve(cwd) {
11920
+ const root = normalizeProjectRoot(cwd);
11921
+ const cached = pending3.get(root);
11922
+ if (cached) return cached;
11923
+ const attempt = resolveUncached(root).catch((err) => {
11924
+ pending3.delete(root);
11925
+ throw err;
11926
+ });
11927
+ pending3.set(root, attempt);
11928
+ return attempt;
11929
+ },
11930
+ clear() {
11931
+ pending3.clear();
11932
+ }
11933
+ };
11934
+ }
11935
+
11936
+ // ../editor/electron/bb/thread-step.ts
11937
+ var CANVAS_TOOL_STEPS = {
11938
+ listLayers: "Reading the layers",
11939
+ getLayer: "Reading a layer",
11940
+ getSelection: "Reading the selection",
11941
+ listAnnotations: "Reading the comments",
11942
+ resolveAnnotation: "Resolving a comment",
11943
+ createLayer: "Adding a layer",
11944
+ insertHtml: "Adding layers",
11945
+ updateLayer: "Editing a layer",
11946
+ replaceLayer: "Rebuilding a layer",
11947
+ removeLayer: "Removing a layer",
11948
+ moveLayer: "Moving a layer",
11949
+ duplicateLayers: "Duplicating layers",
11950
+ groupLayers: "Grouping layers",
11951
+ scaleLayers: "Scaling layers",
11952
+ setLayerFont: "Setting a font",
11953
+ selectLayers: "Selecting layers",
11954
+ holdLayer: "Holding a layer",
11955
+ releaseLayer: "Letting a layer go",
11956
+ askAboutLayer: "Asking about a layer",
11957
+ setPageView: "Changing the view",
11958
+ canvasPointer: "Using the page",
11959
+ canvasHistory: "Undoing a change",
11960
+ listElementTypes: "Reading the elements",
11961
+ getCanvasScene: "Reading a 3D scene",
11962
+ setCanvasScene: "Building a 3D scene",
11963
+ exportHtml: "Reading the markup",
11964
+ listVariables: "Reading the variables",
11965
+ setVariable: "Setting a variable",
11966
+ deleteVariable: "Removing a variable",
11967
+ canvasReadPage: "Reading the page",
11968
+ observePage: "Watching the page",
11969
+ observeVideo: "Watching a video",
11970
+ setAppBrief: "Writing the app brief",
11971
+ canvasScreenshot: "Looking at the board"
11972
+ };
11973
+ var MAX = 60;
11974
+ function clip(text3) {
11975
+ const one = text3.replace(/\s+/g, " ").trim();
11976
+ return one.length > MAX ? `${one.slice(0, MAX - 1)}\u2026` : one;
11977
+ }
11978
+ function baseName(path21) {
11979
+ if (typeof path21 !== "string" || !path21) return null;
11980
+ return path21.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || path21;
11981
+ }
11982
+ function humanize(name) {
11983
+ const words = name.replace(/^mcp__[^_]+__/, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().toLowerCase();
11984
+ return words ? words[0].toUpperCase() + words.slice(1) : "Using a tool";
11985
+ }
11986
+ function describeItem(item) {
11987
+ const presentation = item.presentation;
11988
+ if (presentation && presentation.suppress !== true && typeof presentation.title === "string" && presentation.title.trim()) {
11989
+ return clip(presentation.title);
11990
+ }
11991
+ switch (item.type) {
11992
+ case "toolCall": {
11993
+ const tool = typeof item.tool === "string" ? item.tool : "";
11994
+ const bare = tool.replace(/^mcp__[^_]+__/, "");
11995
+ return CANVAS_TOOL_STEPS[bare] ?? clip(humanize(tool));
11996
+ }
11997
+ case "commandExecution":
11998
+ return typeof item.command === "string" && item.command.trim() ? clip(`Running ${item.command}`) : "Running a command";
11999
+ case "fileChange": {
12000
+ const changes = Array.isArray(item.changes) ? item.changes : [];
12001
+ const first = baseName(changes[0]?.path);
12002
+ if (!first) return "Editing files";
12003
+ return changes.length > 1 ? clip(`Editing ${first} and ${changes.length - 1} more`) : clip(`Editing ${first}`);
12004
+ }
12005
+ case "fileRead": {
12006
+ const name = baseName(item.path);
12007
+ return name ? clip(`Reading ${name}`) : "Reading a file";
12008
+ }
12009
+ case "search":
12010
+ return "Searching the project";
12011
+ case "webSearch": {
12012
+ const queries = Array.isArray(item.queries) ? item.queries : [];
12013
+ return typeof queries[0] === "string" ? clip(`Searching the web for ${queries[0]}`) : "Searching the web";
12014
+ }
12015
+ case "webFetch": {
12016
+ try {
12017
+ return clip(`Reading ${new URL(String(item.url)).host}`);
12018
+ } catch {
12019
+ return "Reading a page";
12020
+ }
12021
+ }
12022
+ case "imageView":
12023
+ return "Looking at an image";
12024
+ case "reasoning":
12025
+ return "Thinking";
12026
+ case "plan":
12027
+ case "planSteps":
12028
+ return "Planning";
12029
+ case "agentMessage":
12030
+ return "Writing a reply";
12031
+ case "delegation":
12032
+ case "backgroundTask":
12033
+ return "Working in the background";
12034
+ case "contextCompaction":
12035
+ return "Tidying its notes";
12036
+ default:
12037
+ return null;
12038
+ }
12039
+ }
12040
+ function describeStep(eventsNewestFirst) {
12041
+ for (const event of eventsNewestFirst) {
12042
+ if (event.type !== "item/started" && event.type !== "item/completed") continue;
12043
+ const item = event.data.item;
12044
+ if (!item || typeof item !== "object") continue;
12045
+ const step = describeItem(item);
12046
+ if (step) return step;
12047
+ }
12048
+ return null;
12049
+ }
12050
+
12051
+ // ../editor/electron/bb/model-choice.ts
12052
+ import * as fs17 from "node:fs";
12053
+ import * as path16 from "node:path";
12054
+ var REASONING_LEVELS = [
12055
+ "none",
12056
+ "low",
12057
+ "medium",
12058
+ "high",
12059
+ "xhigh",
12060
+ "ultracode",
12061
+ "max",
12062
+ "ultra"
12063
+ ];
12064
+ function parseModelChoice(value) {
12065
+ if (typeof value !== "object" || value === null) return null;
12066
+ const entry = value;
12067
+ const providerId = typeof entry.providerId === "string" ? entry.providerId.trim() : "";
12068
+ if (!providerId) return null;
12069
+ const choice = { providerId };
12070
+ const model = typeof entry.model === "string" ? entry.model.trim() : "";
12071
+ if (model) choice.model = model;
12072
+ const reasoning = entry.reasoningLevel;
12073
+ if (typeof reasoning === "string" && REASONING_LEVELS.includes(reasoning)) {
12074
+ choice.reasoningLevel = reasoning;
12075
+ }
12076
+ return choice;
12077
+ }
12078
+ function createModelChoiceStore(deps) {
12079
+ const take = async () => {
12080
+ let reading;
12081
+ try {
12082
+ reading = await deps.read();
12083
+ } catch {
12084
+ reading = "unreachable";
12085
+ }
12086
+ if (reading !== "unreachable") writeMirror(deps.file, reading);
12087
+ return reading;
12088
+ };
12089
+ return {
12090
+ async current() {
12091
+ const reading = await take();
12092
+ return reading === "unreachable" ? readMirror(deps.file) : reading;
12093
+ },
12094
+ async refresh() {
12095
+ await take();
12096
+ }
12097
+ };
12098
+ }
12099
+ function readMirror(file) {
12100
+ try {
12101
+ return parseModelChoice(JSON.parse(fs17.readFileSync(file, "utf-8")));
12102
+ } catch {
12103
+ return null;
12104
+ }
12105
+ }
12106
+ function writeMirror(file, choice) {
12107
+ try {
12108
+ fs17.mkdirSync(path16.dirname(file), { recursive: true });
12109
+ fs17.writeFileSync(file, JSON.stringify(choice ?? {}, null, 2));
12110
+ } catch {
12111
+ }
12112
+ }
12113
+
12114
+ // ../editor/electron/bb/compose-options.ts
12115
+ function asRecord2(value) {
12116
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
12117
+ }
12118
+ var PERMISSION_ORDER = ["accept-edits", "auto", "full"];
12119
+ function parseExecutionOptions(value, providerId) {
12120
+ const rec = asRecord2(value) ?? {};
12121
+ const models = (Array.isArray(rec.models) ? rec.models : []).flatMap((entry) => {
12122
+ const m = asRecord2(entry);
12123
+ if (!m || typeof m.model !== "string" || !m.model) return [];
12124
+ const levels = (Array.isArray(m.supportedReasoningEfforts) ? m.supportedReasoningEfforts : []).flatMap((level) => {
12125
+ const l = asRecord2(level);
12126
+ return l && typeof l.reasoningEffort === "string" ? [{ id: l.reasoningEffort, description: typeof l.description === "string" ? l.description : "" }] : [];
12127
+ });
12128
+ return [
12129
+ {
12130
+ model: m.model,
12131
+ displayName: typeof m.displayName === "string" && m.displayName ? m.displayName : m.model,
12132
+ description: typeof m.description === "string" ? m.description : "",
12133
+ reasoningLevels: levels,
12134
+ defaultReasoning: typeof m.defaultReasoningEffort === "string" ? m.defaultReasoningEffort : null,
12135
+ isDefault: m.isDefault === true
12136
+ }
12137
+ ];
12138
+ });
12139
+ const provider = (Array.isArray(rec.providers) ? rec.providers : []).map(asRecord2).find((p) => p?.id === providerId);
12140
+ const supported = asRecord2(provider?.capabilities)?.permissionModes?.filter(isBbPermissionMode);
12141
+ const ceiling = isBbPermissionMode(rec.permissionCeiling) ? rec.permissionCeiling : "full";
12142
+ const permissionModes = PERMISSION_ORDER.filter(
12143
+ (mode) => (supported && supported.length > 0 ? supported.includes(mode) : true) && PERMISSION_ORDER.indexOf(mode) <= PERMISSION_ORDER.indexOf(ceiling)
12144
+ );
12145
+ const commandFor = (kind) => {
12146
+ for (const action of Array.isArray(provider?.composerActions) ? provider.composerActions : []) {
12147
+ const a = asRecord2(action);
12148
+ const command = asRecord2(a?.command);
12149
+ if (a?.kind === kind && command && typeof command.trigger === "string" && typeof command.name === "string") {
12150
+ const trailing = typeof command.trailingText === "string" && command.trailingText ? command.trailingText : " ";
12151
+ return `${command.trigger}${command.name}${trailing}`;
12152
+ }
12153
+ }
12154
+ return null;
12155
+ };
12156
+ const reasoningLabels = {};
12157
+ for (const level of Array.isArray(provider?.reasoningLevels) ? provider.reasoningLevels : []) {
12158
+ const l = asRecord2(level);
12159
+ if (l && typeof l.id === "string" && typeof l.label === "string") reasoningLabels[l.id] = l.label;
12160
+ }
12161
+ const loadError = asRecord2(rec.modelLoadError);
12162
+ return {
12163
+ providerId,
12164
+ models,
12165
+ permissionModes,
12166
+ planCommand: commandFor("plan"),
12167
+ goalCommand: commandFor("goal"),
12168
+ reasoningLabels,
12169
+ modelLoadError: loadError && typeof loadError.code === "string" ? loadError.code : null
12170
+ };
12171
+ }
12172
+ function parseBranches(value) {
12173
+ const rec = asRecord2(value) ?? {};
12174
+ const checkout = asRecord2(rec.checkout);
12175
+ const current = checkout?.kind === "branch" && typeof checkout.branchName === "string" ? checkout.branchName : null;
12176
+ const branches = (Array.isArray(rec.branches) ? rec.branches : []).filter(
12177
+ (name) => typeof name === "string" && name.length > 0
12178
+ );
12179
+ return {
12180
+ current,
12181
+ branches: current && !branches.includes(current) ? [current, ...branches] : branches,
12182
+ defaultBranch: typeof rec.defaultBranch === "string" ? rec.defaultBranch : null,
12183
+ dirty: rec.hasUncommittedChanges === true
12184
+ };
12185
+ }
12186
+ function branchName(value) {
12187
+ if (typeof value !== "string") return void 0;
12188
+ const name = value.trim();
12189
+ if (!name || name.length > 200 || name.startsWith("-") || name.includes("..")) return void 0;
12190
+ if (/[\s~^:?*[\\]/.test(name) || [...name].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127)) {
12191
+ return void 0;
12192
+ }
12193
+ return name;
12194
+ }
12195
+ function parseComposeWorkspace(value) {
12196
+ const rec = asRecord2(value);
12197
+ if (!rec) return null;
12198
+ if (rec.kind === "local") {
12199
+ const branch = branchName(rec.branch);
12200
+ return branch ? { kind: "local", branch } : { kind: "local" };
12201
+ }
12202
+ if (rec.kind === "worktree") {
12203
+ const baseBranch = branchName(rec.baseBranch);
12204
+ return baseBranch ? { kind: "worktree", baseBranch } : { kind: "worktree" };
12205
+ }
12206
+ return null;
12207
+ }
12208
+ function environmentFor(base, workspace) {
12209
+ if (!workspace) return base;
12210
+ if (workspace.kind === "worktree") {
12211
+ return {
12212
+ type: "host",
12213
+ hostId: base.hostId,
12214
+ workspace: {
12215
+ type: "managed-worktree",
12216
+ baseBranch: workspace.baseBranch ? { kind: "named", name: workspace.baseBranch } : { kind: "default" }
12217
+ }
12218
+ };
12219
+ }
12220
+ if (!workspace.branch || base.workspace.type !== "unmanaged") return base;
12221
+ return { ...base, workspace: { ...base.workspace, branch: { kind: "existing", name: workspace.branch } } };
12222
+ }
12223
+ function parseComposeExecution(value) {
12224
+ return parseModelChoice(value);
12225
+ }
12226
+
12227
+ // ../editor/electron/bb/workspace-url.ts
12228
+ var mapFor = null;
12229
+ var maps = /* @__PURE__ */ new WeakMap();
12230
+ function projectMap(client) {
12231
+ if (mapFor) return mapFor(client);
12232
+ let map = maps.get(client);
12233
+ if (!map) {
12234
+ map = createProjectMap(client);
12235
+ maps.set(client, map);
12236
+ }
12237
+ return map;
12238
+ }
12239
+ async function bbProjectFor(cwd, deps = {}) {
12240
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12241
+ const server2 = await ensureServer();
12242
+ return projectMap(server2.client).resolve(cwd);
12243
+ }
12244
+ async function bbWorkspaceUrl(cwd, deps = {}, preferThreadId, home = false) {
12245
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12246
+ const server2 = await ensureServer();
12247
+ const target = await projectMap(server2.client).resolve(cwd);
12248
+ const threads = home || !preferThreadId ? [] : await server2.client.listThreads(target.projectId).catch(() => []);
12249
+ const preferred = preferThreadId ? threads.find((thread) => thread.id === preferThreadId) : void 0;
12250
+ if (!preferred) {
12251
+ return {
12252
+ url: `${server2.client.origin}/projects/${encodeURIComponent(target.projectId)}`,
12253
+ origin: server2.client.origin,
12254
+ projectId: target.projectId
12255
+ };
12256
+ }
12257
+ return {
12258
+ url: threadUrl(server2.client.origin, target.projectId, preferred.id),
12259
+ origin: server2.client.origin,
12260
+ projectId: target.projectId,
12261
+ threadId: preferred.id
12262
+ };
12263
+ }
12264
+ function threadUrl(origin, projectId, threadId) {
12265
+ return `${origin}/projects/${encodeURIComponent(projectId)}/threads/${encodeURIComponent(threadId)}`;
12266
+ }
12267
+ function latestThread(threads) {
12268
+ let best;
12269
+ for (const thread of threads) {
12270
+ if (!best || (thread.createdAt ?? 0) >= (best.createdAt ?? 0)) best = thread;
12271
+ }
12272
+ return best;
12273
+ }
12274
+ async function sendToWorkspace(cwd, text3, deps = {}) {
12275
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12276
+ const server2 = await ensureServer();
12277
+ await deps.ensureTools?.();
12278
+ const target = await projectMap(server2.client).resolve(cwd);
12279
+ const threads = await server2.client.listThreads(target.projectId).catch(() => []);
12280
+ const latest = latestThread(threads);
12281
+ if (latest) {
12282
+ await server2.client.sendMessage(latest.id, [{ type: "text", text: text3 }]);
12283
+ return { threadId: latest.id, created: false };
12284
+ }
12285
+ const thread = await createThreadAsPicked(server2.client, deps, {
12286
+ projectId: target.projectId,
12287
+ input: [{ type: "text", text: text3 }],
12288
+ environment: target.environment
12289
+ });
12290
+ return { threadId: thread.id, created: true };
12291
+ }
12292
+ async function listWorkspaceThreads(cwd, deps = {}) {
12293
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12294
+ const server2 = await ensureServer();
12295
+ const target = await projectMap(server2.client).resolve(cwd);
12296
+ const threads = await server2.client.listThreads(target.projectId);
12297
+ return [...threads].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
12298
+ }
12299
+ var ACTIVITY_WINDOW = 12;
12300
+ async function workspaceThreadActivity(cwd, deps = {}) {
12301
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12302
+ const server2 = await ensureServer();
12303
+ const target = await projectMap(server2.client).resolve(cwd);
12304
+ const threads = (await server2.client.listThreads(target.projectId)).sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0)).slice(0, ACTIVITY_WINDOW);
12305
+ const rows = await Promise.all(
12306
+ threads.map(async (thread) => {
12307
+ const status = thread.status ?? await server2.client.threadStatus(thread.id).catch(() => null);
12308
+ const working = status === "pending" || status === "starting" || status === "active" || status === "stopping";
12309
+ const step = working ? await server2.client.latestThreadItems(thread.id).then(describeStep).catch(() => null) : null;
12310
+ return status ? {
12311
+ id: thread.id,
12312
+ title: thread.title ?? thread.titleFallback ?? null,
12313
+ createdAt: thread.createdAt,
12314
+ updatedAt: thread.updatedAt ?? thread.createdAt,
12315
+ status,
12316
+ providerId: thread.providerId ?? null,
12317
+ needsYou: thread.hasPendingInteraction === true,
12318
+ step
12319
+ } : null;
12320
+ })
12321
+ );
12322
+ return rows.flatMap((row2) => row2 ? [row2] : []);
12323
+ }
12324
+ async function sendToWorkspaceThread(cwd, threadId, text3, deps = {}) {
12325
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12326
+ const server2 = await ensureServer();
12327
+ await deps.ensureTools?.();
12328
+ const target = await projectMap(server2.client).resolve(cwd);
12329
+ const threads = await server2.client.listThreads(target.projectId);
12330
+ if (!threads.some((thread) => thread.id === threadId)) {
12331
+ throw new Error("that conversation is not in this project");
12332
+ }
12333
+ await server2.client.sendMessage(threadId, [{ type: "text", text: text3 }], { mode: "queue-if-active" });
12334
+ const origin = server2.client.origin;
12335
+ return { url: threadUrl(origin, target.projectId, threadId), origin, projectId: target.projectId, threadId };
12336
+ }
12337
+ async function startWorkspaceThread(cwd, text3, deps = {}, options = {}) {
12338
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12339
+ const server2 = await ensureServer();
12340
+ await deps.ensureTools?.();
12341
+ const target = await projectMap(server2.client).resolve(cwd);
12342
+ const picked = options.execution;
12343
+ const thread = await createThreadAsPicked(
12344
+ server2.client,
12345
+ picked ? { ...deps, modelChoice: async () => picked } : deps,
12346
+ {
12347
+ projectId: target.projectId,
12348
+ input: [{ type: "text", text: text3 }],
12349
+ environment: environmentFor(target.environment, options.workspace ?? null),
12350
+ ...options.permissionMode ? { permissionMode: options.permissionMode } : {}
12351
+ }
12352
+ );
12353
+ const origin = server2.client.origin;
12354
+ return {
12355
+ url: threadUrl(origin, target.projectId, thread.id),
12356
+ origin,
12357
+ projectId: target.projectId,
12358
+ threadId: thread.id
12359
+ };
12360
+ }
12361
+ async function workspaceBranches(cwd, deps = {}) {
12362
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12363
+ const server2 = await ensureServer();
12364
+ const target = await projectMap(server2.client).resolve(cwd);
12365
+ return parseBranches(await server2.client.projectBranches(target.projectId, target.hostId));
12366
+ }
12367
+ async function createThreadAsPicked(client, deps, args2) {
12368
+ const execution = deps.modelChoice ? await deps.modelChoice().catch(() => null) : null;
12369
+ if (!execution) return client.createThread(args2);
12370
+ try {
12371
+ return await client.createThread({ ...args2, execution, origin: "app" });
12372
+ } catch (err) {
12373
+ if (!(err instanceof BbApiError) || err.status < 400 || err.status >= 500) throw err;
12374
+ return client.createThread(args2);
12375
+ }
12376
+ }
12377
+ async function bbAppUrl(deps = {}) {
12378
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12379
+ const server2 = await ensureServer();
12380
+ const origin = server2.client.origin;
12381
+ return { url: `${origin}/`, origin };
12382
+ }
12383
+
12384
+ // ../editor/electron/bb/provider-choice.ts
12385
+ var STATUSES = [
12386
+ "ready",
12387
+ "not_installed",
12388
+ "unauthenticated",
12389
+ "expired",
12390
+ "unsupported_version",
12391
+ "unknown"
12392
+ ];
12393
+ function asRecord3(value) {
12394
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
12395
+ }
12396
+ function parseProviders(value, origin) {
12397
+ if (!Array.isArray(value)) return [];
12398
+ const out = [];
12399
+ for (const entry of value) {
12400
+ const rec = asRecord3(entry);
12401
+ const id = typeof rec?.id === "string" ? rec.id.trim() : "";
12402
+ if (!rec || !id) continue;
12403
+ const displayName = typeof rec.displayName === "string" && rec.displayName.trim() ? rec.displayName.trim() : id;
12404
+ let logoUrl = null;
12405
+ if (typeof rec.logoUrl === "string" && rec.logoUrl) {
12406
+ try {
12407
+ logoUrl = new URL(rec.logoUrl, origin).toString();
12408
+ } catch {
12409
+ logoUrl = null;
12410
+ }
12411
+ }
12412
+ let iconTint = parseTint(rec);
12413
+ if (id === ANTIGRAVITY_PROVIDER_ID && !logoUrl) {
12414
+ logoUrl = ANTIGRAVITY_LOGO_URL;
12415
+ iconTint ??= { ...ANTIGRAVITY_TINT };
12416
+ }
12417
+ out.push({ id, displayName, logoUrl, iconTint, available: rec.available !== false });
12418
+ }
12419
+ return out;
12420
+ }
12421
+ var COLOUR = /^(#[0-9a-f]{3,8}|(rgb|rgba|hsl|hsla|oklch|oklab)\([0-9a-z.,%\s/+-]*\)|[a-z]+)$/i;
12422
+ function parseTint(rec) {
12423
+ const tint = asRecord3(asRecord3(rec.strings)?.iconTint);
12424
+ const light = typeof tint?.light === "string" ? tint.light.trim() : "";
12425
+ const dark = typeof tint?.dark === "string" ? tint.dark.trim() : "";
12426
+ return COLOUR.test(light) && COLOUR.test(dark) ? { light, dark } : null;
12427
+ }
12428
+ async function inlineLogo(client, logoUrl) {
12429
+ if (!logoUrl) return null;
12430
+ if (logoUrl.startsWith("data:")) return logoUrl;
12431
+ try {
12432
+ const text3 = (await client.providerLogo(logoUrl, 5e3)).trim();
12433
+ if (/^(<\?xml[^>]*>\s*)?<svg[\s>]/i.test(text3)) {
12434
+ return `data:image/svg+xml;base64,${Buffer.from(text3, "utf-8").toString("base64")}`;
12435
+ }
12436
+ } catch {
12437
+ }
12438
+ return logoUrl;
12439
+ }
12440
+ function parseProviderStatuses(value) {
12441
+ const statuses = /* @__PURE__ */ new Map();
12442
+ const list = asRecord3(value)?.providers;
12443
+ if (!Array.isArray(list)) return statuses;
12444
+ for (const entry of list) {
12445
+ const rec = asRecord3(entry);
12446
+ if (typeof rec?.providerId !== "string") continue;
12447
+ const status = STATUSES.includes(rec.status) ? rec.status : "unknown";
12448
+ statuses.set(rec.providerId, status);
12449
+ }
12450
+ return statuses;
12451
+ }
12452
+ function generalSettingsOf(config) {
12453
+ return asRecord3(config.generalSettings);
12454
+ }
12455
+ async function readProviderChoice(client) {
12456
+ const [providers, statuses, config] = await Promise.all([
12457
+ client.providers(),
12458
+ client.providerStates().catch(() => null),
12459
+ client.systemConfig()
12460
+ ]);
12461
+ const byId = parseProviderStatuses(statuses);
12462
+ const defaultId = generalSettingsOf(config)?.defaultProviderId;
12463
+ const listed = parseProviders(providers, client.origin);
12464
+ const logos = await Promise.all(listed.map((provider) => inlineLogo(client, provider.logoUrl)));
12465
+ return {
12466
+ providers: listed.map((provider, index) => ({
12467
+ ...provider,
12468
+ logoUrl: logos[index],
12469
+ status: byId.get(provider.id) ?? "unknown"
12470
+ })),
12471
+ defaultProviderId: typeof defaultId === "string" && defaultId ? defaultId : null
12472
+ };
12473
+ }
12474
+ async function setDefaultProvider(client, providerId) {
12475
+ const id = providerId.trim();
12476
+ if (!id) throw new Error("No provider was chosen.");
12477
+ const settings = generalSettingsOf(await client.systemConfig());
12478
+ if (!settings) throw new Error("The agent backend did not report its settings.");
12479
+ await client.putGeneralSettings({ ...settings, defaultProviderId: id });
12480
+ }
12481
+
12482
+ // ../editor/electron/bb/attachments.ts
12483
+ import fs18 from "node:fs";
12484
+ import path17 from "node:path";
12485
+ var ATTACHMENTS_DIR = path17.join(".codecaine", "attachments");
12486
+ var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
12487
+ function attachmentFileName(name, now = Date.now()) {
12488
+ const last = String(name || "attachment").split(/[\\/]/).pop() ?? "";
12489
+ const base = last.replace(/[^\w.\- ]+/g, "_").replace(/\.{2,}/g, ".").replace(/^\.+/, "");
12490
+ const trimmed = (base || "attachment").slice(-80);
12491
+ return `${now.toString(36)}-${trimmed}`;
12492
+ }
12493
+ function saveAttachment(cwd, name, bytes) {
12494
+ if (!cwd || !path17.isAbsolute(cwd) || !fs18.existsSync(cwd)) {
12495
+ return { ok: false, error: "This document has no project folder to attach files to." };
12496
+ }
12497
+ if (!(bytes instanceof Uint8Array)) return { ok: false, error: "Nothing to attach." };
12498
+ if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
12499
+ return { ok: false, error: `${name} is larger than ${MAX_ATTACHMENT_BYTES / 1024 / 1024}MB.` };
12500
+ }
12501
+ const dir = path17.join(cwd, ATTACHMENTS_DIR);
12502
+ fs18.mkdirSync(dir, { recursive: true });
12503
+ const ignore = path17.join(dir, ".gitignore");
12504
+ if (!fs18.existsSync(ignore)) fs18.writeFileSync(ignore, "*\n");
12505
+ const relative4 = path17.join(ATTACHMENTS_DIR, attachmentFileName(name));
12506
+ fs18.writeFileSync(path17.join(cwd, relative4), bytes);
12507
+ return { ok: true, path: relative4.split(path17.sep).join("/") };
12508
+ }
12509
+
12510
+ // src/bb.ts
12511
+ var BB_SERVER_PORT = 38896;
12512
+ var BB_DAEMON_PORT = 38897;
12513
+ var EXPECTED_INSTALL_BYTES = 244e6;
12514
+ var bbAppDir = () => path18.join(dataDir(), "bb-app");
12515
+ var launcherFile = () => path18.join(bbAppDir(), "node_modules", "bb-app", "dist", "bb-app.js");
12516
+ function isolate(env = process.env) {
12517
+ env.CANVAS_BB_DATA_DIR ??= path18.join(dataDir(), "bb");
12518
+ env.CANVAS_BB_SERVER_PORT ??= String(BB_SERVER_PORT);
12519
+ env.CANVAS_BB_DAEMON_PORT ??= String(BB_DAEMON_PORT);
12520
+ env.CODECAINE_BB_APP_DIR ??= bbAppDir();
12521
+ }
12522
+ function bbInstalled() {
12523
+ try {
12524
+ const manifest = JSON.parse(
12525
+ fs19.readFileSync(path18.join(bbAppDir(), "node_modules", "bb-app", "package.json"), "utf8")
12526
+ );
12527
+ return manifest.version === BB_APP_VERSION && fs19.existsSync(launcherFile());
12528
+ } catch {
12529
+ return false;
12530
+ }
12531
+ }
12532
+ async function folderBytes(dir) {
12533
+ let total = 0;
12534
+ const walk2 = async (at) => {
12535
+ let entries;
12536
+ try {
12537
+ entries = await fs19.promises.readdir(at, { withFileTypes: true });
12538
+ } catch {
12539
+ return;
12540
+ }
12541
+ for (const entry of entries) {
12542
+ const full = path18.join(at, entry.name);
12543
+ if (entry.isDirectory()) await walk2(full);
12544
+ else if (entry.isFile()) total += (await fs19.promises.stat(full).catch(() => ({ size: 0 }))).size;
12545
+ }
12546
+ };
12547
+ await walk2(dir);
12548
+ return total;
12549
+ }
12550
+ function installBb(onProgress) {
12551
+ fs19.mkdirSync(bbAppDir(), { recursive: true });
12552
+ return new Promise((resolve3, reject) => {
12553
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
12554
+ const child = spawn(
12555
+ npm,
12556
+ ["install", `bb-app@${BB_APP_VERSION}`, "--prefix", bbAppDir(), "--no-audit", "--no-fund", "--loglevel=error"],
12557
+ { stdio: ["ignore", "ignore", "pipe"], shell: process.platform === "win32" }
12558
+ );
12559
+ let stderr = "";
12560
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
12561
+ let measuring = false;
12562
+ let done = false;
12563
+ const timer = setInterval(async () => {
12564
+ if (measuring || done) return;
12565
+ measuring = true;
12566
+ const bytes = await folderBytes(path18.join(bbAppDir(), "node_modules"));
12567
+ measuring = false;
12568
+ if (!done) onProgress(Math.min(99, Math.round(bytes / EXPECTED_INSTALL_BYTES * 100)));
12569
+ }, 700);
12570
+ child.on("error", (error) => {
12571
+ done = true;
12572
+ clearInterval(timer);
12573
+ reject(error);
12574
+ });
12575
+ child.on("exit", (code) => {
12576
+ done = true;
12577
+ clearInterval(timer);
12578
+ if (code === 0 && bbInstalled()) {
12579
+ onProgress(100);
12580
+ resolve3();
12581
+ } else {
12582
+ reject(new Error(`installing the assistant failed (npm exited ${code})${stderr ? `: ${stderr.trim().split("\n").slice(-3).join(" ")}` : ""}`));
12583
+ }
12584
+ });
12585
+ });
12586
+ }
12587
+ function createAssistant(ipc, onState = () => {
12588
+ }) {
12589
+ isolate();
12590
+ let state = { phase: bbInstalled() ? "idle" : "idle", percent: null, message: "", error: null };
12591
+ const set = (next) => {
12592
+ state = { ...state, ...next };
12593
+ onState(state);
12594
+ ipc.broadcast("companion:assistant", state);
12595
+ };
12596
+ const send2 = (channel, payload) => ipc.broadcast(channel, payload);
12597
+ let pending3 = null;
12598
+ function ready() {
12599
+ if (state.phase === "ready") return Promise.resolve();
12600
+ pending3 ??= (async () => {
12601
+ try {
12602
+ if (!bbInstalled()) {
12603
+ set({ phase: "installing", percent: 0, message: "Downloading the assistant\u2026", error: null });
12604
+ await installBb((percent) => set({ phase: "installing", percent, message: "Downloading the assistant\u2026" }));
12605
+ }
12606
+ set({ phase: "starting", percent: null, message: "Starting the assistant\u2026", error: null });
12607
+ let lastError2 = null;
12608
+ for (const pause of [0, 2e3, 5e3]) {
12609
+ if (pause) await new Promise((resolve3) => setTimeout(resolve3, pause));
12610
+ try {
12611
+ const tools = await ensureCanvasTools(send2);
12612
+ if (!tools) throw new Error(canvasToolsStatus().error ?? "the editor's tools could not be loaded into the assistant");
12613
+ lastError2 = null;
12614
+ break;
12615
+ } catch (error) {
12616
+ lastError2 = error;
12617
+ }
12618
+ }
12619
+ if (lastError2) throw lastError2;
12620
+ set({ phase: "ready", percent: 100, message: "Ready", error: null });
12621
+ } catch (error) {
12622
+ set({ phase: "failed", percent: null, message: "The assistant did not start", error: error instanceof Error ? error.message : String(error) });
12623
+ throw error;
12624
+ } finally {
12625
+ pending3 = null;
12626
+ }
12627
+ })();
12628
+ return pending3;
12629
+ }
12630
+ const modelChoices = createModelChoiceStore({
12631
+ file: path18.join(dataDir(), "agent-model-choice.json"),
12632
+ read: async () => "unreachable"
12633
+ });
12634
+ const fail = (error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) });
12635
+ const ensureTools = () => waitForCanvasTools(send2);
12636
+ ipc.handle("agent:connect", async () => {
12637
+ try {
12638
+ await ready();
12639
+ return { ok: true, url: `http://127.0.0.1:${BB_SERVER_PORT}` };
12640
+ } catch (error) {
12641
+ return fail(error);
12642
+ }
12643
+ });
12644
+ ipc.handle("agent:status", async () => canvasToolsStatus());
12645
+ ipc.handle("agent:connection", async (options = {}) => {
12646
+ if (state.phase !== "ready") return { connected: true, projects: [] };
12647
+ let projectId;
12648
+ if (options?.cwd) {
12649
+ try {
12650
+ projectId = (await bbProjectFor(options.cwd)).projectId;
12651
+ } catch {
12652
+ }
12653
+ }
12654
+ return canvasToolsConnection(send2, {
12655
+ ...projectId ? { projectId, path: options.cwd } : {},
12656
+ ...options?.instructions !== void 0 ? { instructions: options.instructions } : {},
12657
+ ...options?.layers !== void 0 ? { layers: options.layers } : {}
12658
+ });
12659
+ });
12660
+ ipc.handle("bb:providers", async () => {
12661
+ try {
12662
+ await ready();
12663
+ const { client } = await ensureBbServer();
12664
+ return { ok: true, ...await readProviderChoice(client) };
12665
+ } catch (error) {
12666
+ return fail(error);
12667
+ }
12668
+ });
12669
+ ipc.handle("bb:set-default-provider", async (options) => {
12670
+ try {
12671
+ const { client } = await ensureBbServer();
12672
+ await setDefaultProvider(client, String(options?.providerId ?? ""));
12673
+ return { ok: true };
12674
+ } catch (error) {
12675
+ return fail(error);
12676
+ }
12677
+ });
12678
+ ipc.handle("bb:compose-options", async (options) => {
12679
+ try {
12680
+ await ready();
12681
+ const { client } = await ensureBbServer();
12682
+ const picked = await modelChoices.current().catch(() => null);
12683
+ const asked = typeof options?.providerId === "string" && options.providerId ? options.providerId : null;
12684
+ const fallback = typeof options?.fallbackProviderId === "string" && options.fallbackProviderId ? options.fallbackProviderId : null;
12685
+ const providerId = asked ?? picked?.providerId ?? fallback;
12686
+ if (!providerId) return { ok: false, error: "No agent is set up yet." };
12687
+ return { ok: true, picked, options: parseExecutionOptions(await client.executionOptions(providerId), providerId) };
12688
+ } catch (error) {
12689
+ return fail(error);
12690
+ }
12691
+ });
12692
+ ipc.handle("bb:branches", async (options) => {
12693
+ try {
12694
+ const cwd = String(options?.cwd ?? "");
12695
+ if (!path18.isAbsolute(cwd)) throw new Error("not a project folder");
12696
+ await ready();
12697
+ return { ok: true, ...await workspaceBranches(cwd) };
12698
+ } catch (error) {
12699
+ return fail(error);
12700
+ }
12701
+ });
12702
+ ipc.handle("bb:agent-url", async () => {
12703
+ try {
12704
+ await ready();
12705
+ return { ok: true, ...await bbAppUrl() };
12706
+ } catch (error) {
12707
+ return fail(error);
12708
+ }
12709
+ });
12710
+ ipc.handle("bb:instructions", async (options) => {
12711
+ if (state.phase !== "ready") return { ok: true };
12712
+ try {
12713
+ const target = await bbProjectFor(options.cwd);
12714
+ return { ok: await setAgentInstructions(send2, { projectId: target.projectId, path: options.cwd, instructions: options.instructions }) };
12715
+ } catch (error) {
12716
+ return fail(error);
12717
+ }
12718
+ });
12719
+ ipc.handle("bb:selection", async (options) => {
12720
+ if (state.phase !== "ready" || !options?.cwd) return { ok: true, shown: false, attached: true };
12721
+ try {
12722
+ const target = await bbProjectFor(options.cwd);
12723
+ const attached = await setCanvasSelection(send2, { projectId: target.projectId, path: options.cwd, layers: options.layers ?? [] });
12724
+ return { ok: true, shown: false, attached };
12725
+ } catch (error) {
12726
+ return { ok: true, shown: false, attached: false, error: error instanceof Error ? error.message : String(error) };
12727
+ }
12728
+ });
12729
+ ipc.handle("bb:apply-chrome", async () => ({ ok: true }));
12730
+ ipc.handle("bb:magic-cast", async () => ({ ok: true }));
12731
+ ipc.handle("bb:send", async (options) => {
12732
+ try {
12733
+ await ready();
12734
+ return { ok: true, ...await sendToWorkspace(options.cwd, options.text, { modelChoice: () => modelChoices.current(), ensureTools }) };
12735
+ } catch (error) {
12736
+ return fail(error);
12737
+ }
12738
+ });
12739
+ ipc.handle("bb:thread-status", async (options) => {
12740
+ try {
12741
+ const { client } = await ensureBbServer();
12742
+ return { ok: true, status: await client.threadStatus(options.threadId) };
12743
+ } catch (error) {
12744
+ return fail(error);
12745
+ }
12746
+ });
12747
+ ipc.handle("bb:thread-activity", async (options) => {
12748
+ if (state.phase !== "ready") return { ok: true, threads: [] };
12749
+ try {
12750
+ return { ok: true, threads: await workspaceThreadActivity(String(options?.cwd ?? "")) };
12751
+ } catch (error) {
12752
+ return fail(error);
12753
+ }
12754
+ });
12755
+ ipc.handle("bb:threads", async (options) => {
12756
+ if (state.phase !== "ready") return { ok: true, threads: [] };
12757
+ try {
12758
+ return { ok: true, threads: await listWorkspaceThreads(String(options?.cwd ?? "")) };
12759
+ } catch (error) {
12760
+ return fail(error);
12761
+ }
12762
+ });
12763
+ ipc.handle("bb:send-thread", async (options) => {
12764
+ try {
12765
+ await ready();
12766
+ return { ok: true, ...await sendToWorkspaceThread(String(options.cwd ?? ""), String(options.threadId ?? ""), String(options.text ?? ""), { ensureTools }) };
12767
+ } catch (error) {
12768
+ return fail(error);
12769
+ }
12770
+ });
12771
+ ipc.handle(
12772
+ "bb:new-thread",
12773
+ async (options) => {
12774
+ try {
12775
+ await ready();
12776
+ return {
12777
+ ok: true,
12778
+ ...await startWorkspaceThread(
12779
+ options.cwd,
12780
+ options.text,
12781
+ { modelChoice: () => modelChoices.current(), ensureTools },
12782
+ {
12783
+ ...isBbPermissionMode(options.permissionMode) ? { permissionMode: options.permissionMode } : {},
12784
+ execution: parseComposeExecution(options.execution),
12785
+ workspace: parseComposeWorkspace(options.workspace)
12786
+ }
12787
+ )
12788
+ };
12789
+ } catch (error) {
12790
+ return fail(error);
12791
+ }
12792
+ }
12793
+ );
12794
+ ipc.handle("bb:save-attachment", async (options) => {
12795
+ try {
12796
+ return saveAttachment(options.cwd, options.name, options.bytes);
12797
+ } catch (error) {
12798
+ return fail(error);
12799
+ }
12800
+ });
12801
+ return { state: () => state, ready };
12802
+ }
12803
+ async function assistantTarget(cwd, threadId, home) {
12804
+ const target = await bbWorkspaceUrl(cwd, {}, threadId, home);
12805
+ return target;
12806
+ }
12807
+
12808
+ // src/ipc.ts
12809
+ var Ipc = class {
12810
+ handlers = /* @__PURE__ */ new Map();
12811
+ peers = /* @__PURE__ */ new Set();
12812
+ handle(channel, handler) {
12813
+ this.handlers.set(channel, handler);
12814
+ }
12815
+ /** The handler a channel has now — for wrapping one in another. */
12816
+ get(channel) {
12817
+ return this.handlers.get(channel);
12818
+ }
12819
+ has(channel) {
12820
+ return this.handlers.has(channel);
12821
+ }
12822
+ async invoke(channel, args2) {
12823
+ const handler = this.handlers.get(channel);
12824
+ if (!handler) {
12825
+ throw new Error(`${channel} is not available in the web build`);
12826
+ }
12827
+ return handler(...args2);
12828
+ }
12829
+ addPeer(peer) {
12830
+ this.peers.add(peer);
12831
+ return () => this.peers.delete(peer);
12832
+ }
12833
+ peerCount() {
12834
+ return this.peers.size;
12835
+ }
12836
+ /** `webContents.send` to every connected editor. */
12837
+ broadcast(channel, ...args2) {
12838
+ for (const peer of this.peers) peer.send(channel, ...args2);
12839
+ }
12840
+ };
12841
+
12842
+ // src/wire.ts
12843
+ var BYTES = "$bytes";
12844
+ function toBase64(bytes) {
12845
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
12846
+ let binary = "";
12847
+ for (let i = 0; i < bytes.length; i += 32768) {
12848
+ binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
12849
+ }
12850
+ return btoa(binary);
12851
+ }
12852
+ function fromBase64(text3) {
12853
+ if (typeof Buffer !== "undefined") {
12854
+ const buffer = Buffer.from(text3, "base64");
12855
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
12856
+ }
12857
+ const binary = atob(text3);
12858
+ const out = new Uint8Array(binary.length);
12859
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
12860
+ return out.buffer;
12861
+ }
12862
+ function encode(value) {
12863
+ return JSON.stringify(value, (_key, item) => {
12864
+ if (item instanceof ArrayBuffer) return { [BYTES]: toBase64(new Uint8Array(item)) };
12865
+ if (ArrayBuffer.isView(item)) {
12866
+ return { [BYTES]: toBase64(new Uint8Array(item.buffer, item.byteOffset, item.byteLength)) };
12867
+ }
12868
+ return item;
12869
+ });
12870
+ }
12871
+ function decode(text3) {
12872
+ return JSON.parse(text3, (_key, item) => {
12873
+ if (item && typeof item === "object" && BYTES in item && Object.keys(item).length === 1) {
12874
+ return fromBase64(String(item[BYTES]));
12875
+ }
12876
+ return item;
12877
+ });
12878
+ }
12879
+
12880
+ // src/server.ts
12881
+ var VERSION = "0.1.2";
12882
+ var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
12883
+ var MAX_BODY_BYTES2 = 8 * 1024 * 1024;
12884
+ function hostIsLocal(host) {
12885
+ if (!host) return true;
12886
+ try {
12887
+ return LOOPBACK.has(new URL(`http://${host}`).hostname);
12888
+ } catch {
12889
+ return false;
12890
+ }
12891
+ }
12892
+ function tokenMatches2(expected, given) {
12893
+ if (!given) return false;
12894
+ const a = Buffer.from(expected);
12895
+ const b = Buffer.from(given);
12896
+ return a.length === b.length && crypto3.timingSafeEqual(a, b);
12897
+ }
12898
+ function readBody2(request) {
12899
+ return new Promise((resolve3, reject) => {
12900
+ const chunks = [];
12901
+ let size = 0;
12902
+ request.on("data", (chunk) => {
12903
+ size += chunk.length;
12904
+ if (size > MAX_BODY_BYTES2) {
12905
+ reject(new Error("Request body too large."));
12906
+ request.destroy();
12907
+ return;
10405
12908
  }
10406
12909
  chunks.push(chunk);
10407
12910
  });
@@ -10432,8 +12935,40 @@ async function startCompanion(options) {
10432
12935
  })
10433
12936
  });
10434
12937
  const info = { name: "codecaine", version: VERSION, instructions: agent.instructions };
12938
+ const viaInbox = Object.fromEntries(
12939
+ ["bb:new-thread", "bb:send", "bb:send-thread", "bb:workspace-url"].map((channel) => [channel, ipc.get(channel)])
12940
+ );
12941
+ const useAssistant = options.assistant !== false;
12942
+ const assistant = useAssistant ? createAssistant(ipc) : { state: () => ({ phase: "failed", percent: null, message: "Off", error: "The assistant is off (--no-assistant)." }), ready: () => Promise.reject(new Error("The assistant is off (--no-assistant).")) };
12943
+ for (const [channel, inboxHandler] of Object.entries(viaInbox)) {
12944
+ if (channel === "bb:workspace-url") continue;
12945
+ const bbHandler = ipc.get(channel);
12946
+ ipc.handle(channel, async (...args2) => {
12947
+ const answer = await bbHandler(...args2);
12948
+ return answer?.ok === false && assistant.state().phase === "failed" ? inboxHandler(...args2) : answer;
12949
+ });
12950
+ }
12951
+ const startingUrl = (cwd, threadId, home) => `http://127.0.0.1:${port}/agent/${pageKey}/starting?` + new URLSearchParams({ cwd, ...threadId ? { thread: threadId } : {}, ...home ? { home: "1" } : {} }).toString();
12952
+ ipc.handle("bb:workspace-url", async (args2) => {
12953
+ if (!useAssistant) return viaInbox["bb:workspace-url"](args2);
12954
+ const cwd = String(args2?.cwd ?? "");
12955
+ const threadId = typeof args2?.threadId === "string" ? args2.threadId : null;
12956
+ if (assistant.state().phase === "ready") {
12957
+ try {
12958
+ const target = await assistantTarget(cwd, threadId, args2?.home === true);
12959
+ await waitForCanvasTools((channel, payload) => ipc.broadcast(channel, payload), { attempts: 1 }).catch(() => {
12960
+ });
12961
+ return { ok: true, ...target };
12962
+ } catch (error) {
12963
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
12964
+ }
12965
+ }
12966
+ void assistant.ready().catch(() => {
12967
+ });
12968
+ return { ok: true, url: startingUrl(cwd, threadId, args2?.home === true), origin: `http://127.0.0.1:${port}`, projectId: cwd };
12969
+ });
10435
12970
  const corsFor = (origin) => origin && allowed.has(origin) ? { "access-control-allow-origin": origin, vary: "Origin", "access-control-allow-private-network": "true" } : { vary: "Origin" };
10436
- const server = http2.createServer((request, response) => {
12971
+ const server2 = http3.createServer((request, response) => {
10437
12972
  void route(request, response).catch((error) => {
10438
12973
  if (!response.headersSent) json(response, 500, { error: error instanceof Error ? error.message : String(error) });
10439
12974
  else response.destroy();
@@ -10465,12 +13000,34 @@ async function startCompanion(options) {
10465
13000
  response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-length": Buffer.byteLength(html) });
10466
13001
  return void response.end(html);
10467
13002
  }
13003
+ if (url.pathname === `/agent/${pageKey}/starting`) {
13004
+ const html = assistantStartingPage();
13005
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-length": Buffer.byteLength(html) });
13006
+ return void response.end(html);
13007
+ }
13008
+ if (url.pathname === `/agent/${pageKey}/assistant`) {
13009
+ const state = assistant.state();
13010
+ if (state.phase === "idle") void assistant.ready().catch(() => {
13011
+ });
13012
+ let target = null;
13013
+ if (state.phase === "ready") {
13014
+ try {
13015
+ target = (await assistantTarget(url.searchParams.get("cwd") ?? "", url.searchParams.get("thread"), url.searchParams.get("home") === "1")).url;
13016
+ } catch (error) {
13017
+ return json(response, 200, { state: { ...state, phase: "failed", error: error instanceof Error ? error.message : String(error) }, theme: agent.inbox.theme }, { "cache-control": "no-store" });
13018
+ }
13019
+ }
13020
+ return json(response, 200, { state, url: target, theme: agent.inbox.theme }, { "cache-control": "no-store" });
13021
+ }
10468
13022
  if (url.pathname === `/agent/${pageKey}/state`) {
10469
13023
  const project = url.searchParams.get("project") ?? "";
10470
13024
  const inbox = agent.inbox;
10471
13025
  return json(response, 200, {
10472
13026
  theme: inbox.theme,
10473
13027
  agent: inbox.connected(),
13028
+ // Is a `codecaine mcp` waiting to push requests right now? It asks
13029
+ // again every 25s, so a minute without asking means it has gone.
13030
+ pushing: [...inbox.pushTargets.values()].some((at) => Date.now() - at < 6e4),
10474
13031
  requests: (project ? inbox.requests.filter((request2) => sameFolder(request2.cwd, project)) : inbox.requests).map(
10475
13032
  (request2) => ({ ...request2, words: wordsOf(request2.text) })
10476
13033
  ),
@@ -10478,8 +13035,86 @@ async function startCompanion(options) {
10478
13035
  }, { "cache-control": "no-store" });
10479
13036
  }
10480
13037
  if (url.pathname === "/mcp") return mcp(request, response);
13038
+ if (url.pathname === "/inbox/wait") return inboxWait(request, response, url);
13039
+ if (url.pathname === "/open" && request.method === "POST") return openOnStart(request, response);
13040
+ if (url.pathname === "/assistant/start" && request.method === "POST") {
13041
+ if (!agentAllowed(request, response)) return;
13042
+ void assistant.ready().catch(() => {
13043
+ });
13044
+ return json(response, 200, assistant.state());
13045
+ }
13046
+ if (url.pathname === "/assistant/state") {
13047
+ if (!agentAllowed(request, response)) return;
13048
+ return json(response, 200, assistant.state());
13049
+ }
10481
13050
  json(response, 404, { error: "Not found." });
10482
13051
  }
13052
+ function agentAllowed(request, response) {
13053
+ const origin = request.headers.origin;
13054
+ if (origin && origin !== "null") {
13055
+ try {
13056
+ if (!LOOPBACK.has(new URL(origin).hostname)) {
13057
+ json(response, 403, { error: "Forbidden origin." });
13058
+ return false;
13059
+ }
13060
+ } catch {
13061
+ json(response, 403, { error: "Forbidden origin." });
13062
+ return false;
13063
+ }
13064
+ }
13065
+ const header3 = request.headers.authorization;
13066
+ const given = header3 && /^Bearer\s+(.+)$/i.exec(header3.trim())?.[1];
13067
+ if (!tokenMatches2(options.mcpToken, given)) {
13068
+ json(response, 401, { error: "Missing or wrong bearer token." }, { "www-authenticate": "Bearer" });
13069
+ return false;
13070
+ }
13071
+ return true;
13072
+ }
13073
+ function inboxWait(request, response, url) {
13074
+ if (!agentAllowed(request, response)) return;
13075
+ const since = Number(url.searchParams.get("since") ?? 0);
13076
+ const client = String(request.headers["x-codecaine-client"] ?? "") || "Your coding agent";
13077
+ const inbox = agent.inbox;
13078
+ inbox.pushTargets.set(client, Date.now());
13079
+ inbox.heard(client);
13080
+ const waiting = () => inbox.requests.filter((entry) => entry.status === "pending" && entry.updatedAt > since);
13081
+ const ready = waiting();
13082
+ if (ready.length) return json(response, 200, { requests: ready });
13083
+ let done = false;
13084
+ const finish = (requests) => {
13085
+ if (done) return;
13086
+ done = true;
13087
+ clearTimeout(timer);
13088
+ stop();
13089
+ json(response, 200, { requests });
13090
+ };
13091
+ const stop = inbox.onWaiting(() => finish(waiting()));
13092
+ const timer = setTimeout(() => finish([]), 25e3);
13093
+ request.on("close", () => {
13094
+ done = true;
13095
+ clearTimeout(timer);
13096
+ stop();
13097
+ });
13098
+ }
13099
+ const broughtForward = /* @__PURE__ */ new Set();
13100
+ async function openOnStart(request, response) {
13101
+ if (!agentAllowed(request, response)) return;
13102
+ let folder;
13103
+ try {
13104
+ folder = String(JSON.parse(await readBody2(request)).folder ?? "");
13105
+ } catch {
13106
+ return json(response, 400, { error: "Expected { folder }." });
13107
+ }
13108
+ if (!folder || !looksLikeProject(folder)) return json(response, 200, { opened: false, reason: "not a project" });
13109
+ const tabs = readOpenTabs(openTabsFile());
13110
+ const inFront = tabs.folderPaths[tabs.activeIndex] === path19.resolve(folder);
13111
+ if (broughtForward.has(path19.resolve(folder)) || inFront && ipc.peerCount() > 0) {
13112
+ return json(response, 200, { opened: false, reason: "already in front" });
13113
+ }
13114
+ broughtForward.add(path19.resolve(folder));
13115
+ const result = await agent.bringToFront(folder);
13116
+ json(response, 200, result.ok ? { opened: true, folder: result.folderPath } : { opened: false, reason: result.error });
13117
+ }
10483
13118
  async function mcp(request, response) {
10484
13119
  const origin = request.headers.origin;
10485
13120
  if (origin && origin !== "null") {
@@ -10491,7 +13126,7 @@ async function startCompanion(options) {
10491
13126
  }
10492
13127
  const header3 = request.headers.authorization;
10493
13128
  const given = header3 && /^Bearer\s+(.+)$/i.exec(header3.trim())?.[1];
10494
- if (!tokenMatches(options.mcpToken, given)) {
13129
+ if (!tokenMatches2(options.mcpToken, given)) {
10495
13130
  return json(response, 401, { error: "Missing or wrong bearer token." }, { "www-authenticate": "Bearer" });
10496
13131
  }
10497
13132
  if (request.method === "GET") {
@@ -10501,14 +13136,17 @@ async function startCompanion(options) {
10501
13136
  if (request.method !== "POST") return json(response, 405, { error: "POST only." }, { allow: "POST" });
10502
13137
  let payload;
10503
13138
  try {
10504
- payload = JSON.parse(await readBody(request));
13139
+ payload = JSON.parse(await readBody2(request));
10505
13140
  } catch {
10506
13141
  return json(response, 400, rpcError(null, RPC_PARSE_ERROR, "Parse error."));
10507
13142
  }
10508
13143
  const cwd = String(request.headers["x-codecaine-cwd"] ?? "");
13144
+ const named = String(request.headers["x-codecaine-client"] ?? "") || null;
10509
13145
  for (const message of Array.isArray(payload) ? payload : [payload]) {
10510
13146
  const hello = message;
10511
- agent.inbox.heard(hello?.method === "initialize" && typeof hello.params?.clientInfo?.name === "string" ? hello.params.clientInfo.name : null);
13147
+ agent.inbox.heard(
13148
+ hello?.method === "initialize" && typeof hello.params?.clientInfo?.name === "string" ? hello.params.clientInfo.name : named
13149
+ );
10512
13150
  }
10513
13151
  const answer = await handlePayload(
10514
13152
  payload,
@@ -10522,7 +13160,7 @@ async function startCompanion(options) {
10522
13160
  json(response, 200, answer);
10523
13161
  }
10524
13162
  const sockets = new import_websocket_server.default({ noServer: true, maxPayload: 256 * 1024 * 1024 });
10525
- server.on("upgrade", (request, socket, head) => {
13163
+ server2.on("upgrade", (request, socket, head) => {
10526
13164
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
10527
13165
  const origin = request.headers.origin;
10528
13166
  if (url.pathname !== "/bridge" || !hostIsLocal(request.headers.host) || !origin || !allowed.has(origin)) {
@@ -10534,14 +13172,14 @@ async function startCompanion(options) {
10534
13172
  });
10535
13173
  let peerIds = 0;
10536
13174
  function connect(ws) {
10537
- const send = (message) => {
13175
+ const send2 = (message) => {
10538
13176
  if (ws.readyState === ws.OPEN) ws.send(encode(message));
10539
13177
  };
10540
13178
  const remove = ipc.addPeer({
10541
13179
  id: ++peerIds,
10542
- send: (channel, ...args2) => send({ t: "event", channel, args: args2 })
13180
+ send: (channel, ...args2) => send2({ t: "event", channel, args: args2 })
10543
13181
  });
10544
- send({
13182
+ send2({
10545
13183
  t: "hello",
10546
13184
  version: VERSION,
10547
13185
  mediaOrigin: `http://127.0.0.1:${port}/media/${mediaKey}/`,
@@ -10561,15 +13199,15 @@ async function startCompanion(options) {
10561
13199
  }
10562
13200
  if (message.t !== "invoke") return;
10563
13201
  ipc.invoke(message.channel, message.args).then(
10564
- (value) => send({ t: "result", id: message.id, ok: true, value }),
10565
- (error) => send({ t: "result", id: message.id, ok: false, error: error instanceof Error ? error.message : String(error) })
13202
+ (value) => send2({ t: "result", id: message.id, ok: true, value }),
13203
+ (error) => send2({ t: "result", id: message.id, ok: false, error: error instanceof Error ? error.message : String(error) })
10566
13204
  );
10567
13205
  });
10568
13206
  ws.on("close", remove);
10569
13207
  }
10570
13208
  port = await new Promise((resolve3, reject) => {
10571
- server.once("error", reject);
10572
- server.listen(options.port, "127.0.0.1", () => resolve3(server.address().port));
13209
+ server2.once("error", reject);
13210
+ server2.listen(options.port, "127.0.0.1", () => resolve3(server2.address().port));
10573
13211
  });
10574
13212
  return {
10575
13213
  port,
@@ -10577,13 +13215,13 @@ async function startCompanion(options) {
10577
13215
  close: () => new Promise((resolve3) => {
10578
13216
  live.close();
10579
13217
  for (const ws of sockets.clients) ws.terminate();
10580
- server.closeAllConnections?.();
10581
- server.close(() => resolve3());
13218
+ server2.closeAllConnections?.();
13219
+ server2.close(() => resolve3());
10582
13220
  })
10583
13221
  };
10584
13222
  }
10585
13223
  function serveFile(request, response, file, type, headers) {
10586
- const size = fs14.statSync(file).size;
13224
+ const size = fs20.statSync(file).size;
10587
13225
  const range = /^bytes=(\d*)-(\d*)$/.exec(String(request.headers.range ?? ""));
10588
13226
  const base = { ...headers, "content-type": type, "accept-ranges": "bytes", "cache-control": "private, max-age=31536000, immutable" };
10589
13227
  if (range) {
@@ -10594,21 +13232,21 @@ function serveFile(request, response, file, type, headers) {
10594
13232
  return void response.end();
10595
13233
  }
10596
13234
  response.writeHead(206, { ...base, "content-range": `bytes ${start}-${end}/${size}`, "content-length": end - start + 1 });
10597
- fs14.createReadStream(file, { start, end }).pipe(response);
13235
+ fs20.createReadStream(file, { start, end }).pipe(response);
10598
13236
  return;
10599
13237
  }
10600
13238
  response.writeHead(200, { ...base, "content-length": size });
10601
- fs14.createReadStream(file).pipe(response);
13239
+ fs20.createReadStream(file).pipe(response);
10602
13240
  }
10603
13241
  function guestCandidates(here2) {
10604
- return [path12.join(here2, "canvas-live-guest.js"), path12.join(here2, "..", "..", "dist", "canvas-live-guest.js")];
13242
+ return [path19.join(here2, "canvas-live-guest.js"), path19.join(here2, "..", "..", "dist", "canvas-live-guest.js")];
10605
13243
  }
10606
13244
 
10607
13245
  // src/cli.ts
10608
13246
  var DEFAULT_PORT = 4317;
10609
13247
  var DEFAULT_EDITOR = "https://celebrated-souffle-e3f7bf.netlify.app";
10610
13248
  var PUBLISHED_ORIGINS = ["https://editor.codecainehq.com", "https://celebrated-souffle-e3f7bf.netlify.app"];
10611
- var here = path13.dirname(fileURLToPath(import.meta.url));
13249
+ var here = path20.dirname(fileURLToPath3(import.meta.url));
10612
13250
  function flag(args2, name) {
10613
13251
  const at = args2.indexOf(name);
10614
13252
  return at >= 0 ? args2[at + 1] : void 0;
@@ -10616,7 +13254,10 @@ function flag(args2, name) {
10616
13254
  function openBrowser(url) {
10617
13255
  const [file, args2] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
10618
13256
  try {
10619
- spawn(file, args2, { stdio: "ignore", detached: true }).unref();
13257
+ const child = spawn2(file, args2, { stdio: "ignore", detached: true });
13258
+ child.on("error", () => {
13259
+ });
13260
+ child.unref();
10620
13261
  } catch {
10621
13262
  }
10622
13263
  }
@@ -10642,10 +13283,11 @@ async function serve(args2) {
10642
13283
  const origins = [
10643
13284
  .../* @__PURE__ */ new Set([new URL(editorUrl).origin, ...PUBLISHED_ORIGINS, ...extra, "http://localhost:5180", "http://127.0.0.1:5180"])
10644
13285
  ];
10645
- const guestFile = guestCandidates(here).find((file) => fs15.existsSync(file));
13286
+ const guestFile = guestCandidates(here).find((file) => fs21.existsSync(file));
10646
13287
  if (!guestFile) throw new Error("canvas-live-guest.js is missing \u2014 build the web app first (npm run build).");
10647
13288
  const quiet = args2.includes("--no-open");
10648
- const folder = args2.find((arg, i) => !arg.startsWith("--") && !args2[i - 1]?.startsWith("--"));
13289
+ const named = args2.find((arg, i) => !arg.startsWith("--") && !["--port", "--editor", "--origin"].includes(args2[i - 1] ?? ""));
13290
+ const folder = named ?? (looksLikeProject(process.cwd()) ? process.cwd() : void 0);
10649
13291
  if (await health(port)) {
10650
13292
  console.log(`codecaine is already running on port ${port}.`);
10651
13293
  if (!quiet) openBrowser(editorUrl);
@@ -10653,7 +13295,15 @@ async function serve(args2) {
10653
13295
  }
10654
13296
  restoreOpenFolders();
10655
13297
  const token = mcpToken();
10656
- const companion = await startCompanion({ port, editorUrl, origins, guestFile, mcpToken: token, openBrowser });
13298
+ const companion = await startCompanion({
13299
+ port,
13300
+ editorUrl,
13301
+ origins,
13302
+ guestFile,
13303
+ mcpToken: token,
13304
+ openBrowser,
13305
+ assistant: !args2.includes("--no-assistant") && process.env.CODECAINE_NO_ASSISTANT !== "1"
13306
+ });
10657
13307
  writeState({ pid: process.pid, port: companion.port, mcpPort: companion.port, mcpToken: token, version: VERSION });
10658
13308
  if (folder) {
10659
13309
  const loaded = openFolder(folder);
@@ -10681,10 +13331,10 @@ async function serve(args2) {
10681
13331
  process.on("SIGINT", stop);
10682
13332
  process.on("SIGTERM", stop);
10683
13333
  }
10684
- async function mcpPipe() {
13334
+ async function ensureCompanion() {
10685
13335
  let state = readState();
10686
13336
  if (!state || !await health(state.port)) {
10687
- const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "--no-open"], {
13337
+ const child = spawn2(process.execPath, [fileURLToPath3(import.meta.url), "--no-open"], {
10688
13338
  detached: true,
10689
13339
  stdio: "ignore",
10690
13340
  env: process.env
@@ -10697,7 +13347,11 @@ async function mcpPipe() {
10697
13347
  if (state && await health(state.port)) break;
10698
13348
  }
10699
13349
  }
10700
- if (!state || !await health(state.port)) {
13350
+ return state && await health(state.port) ? state : null;
13351
+ }
13352
+ async function mcpPipe() {
13353
+ const state = await ensureCompanion();
13354
+ if (!state) {
10701
13355
  process.stderr.write("codecaine: the companion did not start; run `npx codecaine-cli` to see why.\n");
10702
13356
  process.exit(1);
10703
13357
  }
@@ -10707,31 +13361,156 @@ async function mcpPipe() {
10707
13361
  authorization: `Bearer ${state.mcpToken}`,
10708
13362
  "x-codecaine-cwd": process.cwd()
10709
13363
  };
13364
+ const write = (message) => process.stdout.write(JSON.stringify(message) + "\n");
10710
13365
  const lines = readline.createInterface({ input: process.stdin });
10711
13366
  let chain = Promise.resolve();
13367
+ let pushing = false;
10712
13368
  lines.on("line", (line) => {
10713
13369
  if (!line.trim()) return;
13370
+ let message = null;
13371
+ try {
13372
+ message = JSON.parse(line);
13373
+ } catch {
13374
+ }
13375
+ if (message?.method === "initialize" && typeof message.params?.clientInfo?.name === "string") {
13376
+ headers["x-codecaine-client"] = message.params.clientInfo.name;
13377
+ }
10714
13378
  chain = chain.then(async () => {
10715
13379
  try {
10716
13380
  const response = await fetch(target, { method: "POST", headers, body: line });
10717
13381
  if (response.status === 202) return;
10718
- process.stdout.write(await response.text() + "\n");
10719
- } catch (error) {
10720
- let id = null;
10721
- try {
10722
- id = JSON.parse(line).id ?? null;
10723
- } catch {
13382
+ const text3 = await response.text();
13383
+ if (message?.method !== "initialize") {
13384
+ process.stdout.write(text3 + "\n");
13385
+ return;
10724
13386
  }
10725
- process.stdout.write(
10726
- JSON.stringify({ jsonrpc: "2.0", id, error: { code: -32603, message: `codecaine companion unreachable: ${String(error)}` } }) + "\n"
10727
- );
13387
+ const answer = JSON.parse(text3);
13388
+ if (answer.result?.capabilities) {
13389
+ const experimental = answer.result.capabilities.experimental ?? {};
13390
+ answer.result.capabilities.experimental = { ...experimental, "claude/channel": {} };
13391
+ }
13392
+ write(answer);
13393
+ void fetch(`http://127.0.0.1:${state.port}/open`, {
13394
+ method: "POST",
13395
+ headers,
13396
+ body: JSON.stringify({ folder: process.cwd() })
13397
+ }).catch(() => {
13398
+ });
13399
+ if (!pushing) {
13400
+ pushing = true;
13401
+ void pushRequests(headers, write);
13402
+ }
13403
+ } catch (error) {
13404
+ write({ jsonrpc: "2.0", id: message?.id ?? null, error: { code: -32603, message: `codecaine companion unreachable: ${String(error)}` } });
10728
13405
  }
10729
13406
  });
10730
13407
  });
10731
13408
  lines.on("close", () => void chain.then(() => process.exit(0)));
10732
13409
  }
13410
+ async function pushRequests(headers, write) {
13411
+ const pushed = /* @__PURE__ */ new Map();
13412
+ let since = Date.now() - 10 * 6e4;
13413
+ for (; ; ) {
13414
+ const state = readState();
13415
+ try {
13416
+ if (!state) throw new Error("no companion");
13417
+ const response = await fetch(`http://127.0.0.1:${state.port}/inbox/wait?since=${since}`, {
13418
+ headers: { ...headers, authorization: `Bearer ${state.mcpToken}` },
13419
+ signal: AbortSignal.timeout(4e4)
13420
+ });
13421
+ const { requests = [] } = await response.json();
13422
+ for (const request of requests) {
13423
+ since = Math.max(since, request.updatedAt);
13424
+ if (pushed.get(request.id) === request.updatedAt) continue;
13425
+ pushed.set(request.id, request.updatedAt);
13426
+ write({
13427
+ jsonrpc: "2.0",
13428
+ method: "notifications/claude/channel",
13429
+ params: {
13430
+ content: [
13431
+ `New request from the Codecaine board${request.cwd ? ` (project ${request.cwd})` : ""}:`,
13432
+ "",
13433
+ wordsOf(request.text),
13434
+ ...request.followUps.map((more) => `+ ${more}`),
13435
+ "",
13436
+ `Call the codecaine takeRequests tool${request.cwd ? ` with folder "${request.cwd}"` : ""} to collect it \u2014 it carries the editor's full instructions \u2014 do it, then finishRequest with one sentence about what you did.`
13437
+ ].join("\n"),
13438
+ meta: { request_id: request.id, kind: "board_request" }
13439
+ }
13440
+ });
13441
+ }
13442
+ } catch {
13443
+ await new Promise((r) => setTimeout(r, 3e3));
13444
+ }
13445
+ }
13446
+ }
13447
+ async function setup(args2) {
13448
+ const tty = process.stdout.isTTY;
13449
+ const say = (line) => process.stdout.write(line + "\n");
13450
+ say(`codecaine ${VERSION} \u2014 setting up this machine for Codecaine
13451
+ `);
13452
+ say("1/3 Starting the companion\u2026");
13453
+ const state = await ensureCompanion();
13454
+ if (!state) {
13455
+ say(" It did not start. Run `npx codecaine-cli` to see why.");
13456
+ process.exit(1);
13457
+ }
13458
+ say(` Running on http://127.0.0.1:${state.port}`);
13459
+ say("2/3 Getting the assistant ready (the first time it downloads about 240 MB)\u2026");
13460
+ const headers = { authorization: `Bearer ${state.mcpToken}` };
13461
+ const base = `http://127.0.0.1:${state.port}`;
13462
+ let assistant = await (await fetch(`${base}/assistant/start`, { method: "POST", headers })).json();
13463
+ let lastShown = -1;
13464
+ const width = 28;
13465
+ while (assistant.phase !== "ready" && assistant.phase !== "failed") {
13466
+ const percent = assistant.percent;
13467
+ if (percent != null && percent !== lastShown) {
13468
+ const filled = Math.round(percent / 100 * width);
13469
+ const bar = ` [${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}] ${String(percent).padStart(3)}% ${assistant.message}`;
13470
+ if (tty) process.stdout.write(`\r${bar}`);
13471
+ else if (Math.floor(percent / 10) !== Math.floor(lastShown / 10)) say(bar);
13472
+ lastShown = percent;
13473
+ } else if (percent == null && lastShown !== -2 && assistant.phase === "starting") {
13474
+ if (tty && lastShown >= 0) process.stdout.write("\n");
13475
+ say(" Starting it\u2026");
13476
+ lastShown = -2;
13477
+ }
13478
+ await new Promise((r) => setTimeout(r, 500));
13479
+ assistant = await (await fetch(`${base}/assistant/state`, { headers })).json();
13480
+ }
13481
+ if (tty && lastShown >= 0) process.stdout.write("\n");
13482
+ if (assistant.phase === "failed") {
13483
+ say(` The assistant did not start: ${assistant.error ?? "no reason given"}`);
13484
+ say(" The editor still works; its Agent panel says the same and retries.");
13485
+ } else {
13486
+ say(" Ready \u2014 it is the Agent panel in the editor.");
13487
+ }
13488
+ say("3/3 Connecting your coding agent over MCP\u2026");
13489
+ const has = (command) => spawnSync(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" }).status === 0;
13490
+ if (args2.includes("--no-mcp")) {
13491
+ say(" Skipped (--no-mcp). Add the command `npx -y codecaine-cli mcp` as an MCP server whenever you want.");
13492
+ } else if (has("claude")) {
13493
+ if (spawnSync("claude", ["mcp", "get", "codecaine"], { stdio: "ignore" }).status === 0) {
13494
+ say(" Claude Code already has it.");
13495
+ } else {
13496
+ const added = spawnSync("claude", ["mcp", "add", "codecaine", "-s", "user", "--", "npx", "-y", "codecaine-cli", "mcp"], { stdio: "ignore" });
13497
+ say(added.status === 0 ? " Added to Claude Code. Restart Claude Code to load it." : " Could not add it to Claude Code; run: claude mcp add codecaine -- npx -y codecaine-cli mcp");
13498
+ }
13499
+ } else {
13500
+ say(" Claude Code is not installed \u2014 skipped. Other agents: add the command `npx -y codecaine-cli mcp` as an MCP server.");
13501
+ }
13502
+ const editorUrl = (flag(args2, "--editor") ?? process.env.CODECAINE_EDITOR_URL ?? DEFAULT_EDITOR).replace(/\/+$/, "");
13503
+ say(`
13504
+ Done. Open ${editorUrl} \u2014 it connects to this machine by itself.`);
13505
+ if (!args2.includes("--no-open")) openBrowser(editorUrl);
13506
+ }
10733
13507
  var args = process.argv.slice(2);
10734
- if (args[0] === "mcp") {
13508
+ if (args[0] === "setup") {
13509
+ setup(args.slice(1)).catch((error) => {
13510
+ console.error(error instanceof Error ? error.message : String(error));
13511
+ process.exit(1);
13512
+ });
13513
+ } else if (args[0] === "mcp") {
10735
13514
  void mcpPipe();
10736
13515
  } else if (args[0] === "--version" || args[0] === "-v") {
10737
13516
  console.log(VERSION);