codecaine-cli 0.1.0 → 0.1.3

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 as spawnSync2 } 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;
@@ -6494,6 +6494,17 @@ import os3 from "node:os";
6494
6494
  import path11 from "node:path";
6495
6495
 
6496
6496
  // ../editor/resources/bb-plugin/src/tools.ts
6497
+ var VECTOR_STYLE_SCHEMA = {
6498
+ type: "object",
6499
+ properties: {
6500
+ stroke: { type: ["string", "null"], description: "A CSS colour, or null for none." },
6501
+ fill: { type: ["string", "null"], description: "A CSS colour, or null for none." },
6502
+ strokeWidth: { type: "number" },
6503
+ cap: { type: "string", enum: ["butt", "round", "square"] },
6504
+ join: { type: "string", enum: ["miter", "round", "bevel"] },
6505
+ dashes: { type: "string", description: 'Dash and gap lengths, e.g. "4 2"; empty is solid.' }
6506
+ }
6507
+ };
6497
6508
  var TARGET_SCHEMA = {
6498
6509
  type: "object",
6499
6510
  description: "Layer handle: id (preferred \u2014 returned by listLayers/createLayer) or a CSS-shaped selector over the design.",
@@ -6949,6 +6960,84 @@ var CANVAS_TOOLS = [
6949
6960
  }
6950
6961
  }
6951
6962
  },
6963
+ {
6964
+ name: "getVector",
6965
+ description: "A drawing (made with the pen or drawVector) or a pasted <svg> icon, as the pen sees it: { id, viewBox, paths: [{ index, shape, closed, style, points: [{ x, y, in, out, mirror }] }] }. Points are in the drawing's own units (its viewBox); `in`/`out` are handles RELATIVE to their point (null = a corner); `mirror` is Figma's Mirroring: none, angle, or mirror (angle and length). Paths of one compound shape (a ring and its hole) share `shape`. Read this before drawVector operations, which name paths and points by these indices.",
6966
+ inputSchema: {
6967
+ type: "object",
6968
+ properties: { target: TARGET_SCHEMA },
6969
+ required: ["target"]
6970
+ }
6971
+ },
6972
+ {
6973
+ name: "drawVector",
6974
+ description: "Draw with the pen: icons, logos, illustrations, arrows, isometric shapes \u2014 anything that is lines and curves rather than boxes. Without `target` it makes a new drawing on the board (or inside `parent`) from `paths`; with `target` it changes that drawing or pasted <svg> with `operations` (and adds any `paths`), all or nothing, in one undo step. A path is SVG path data `d`, or `points` [{ x, y, in?, out? }] with handles relative to each point (out: {x: 30, y: 0} pulls the curve 30px forward from the point) and `closed`. Units are px; the drawing's own 0,0 lands at `at` (clear space by default) and its box fits the paths. Operations, each the pen tool of the same name \u2014 segment i runs from point i to point i+1: move {path, point, to:{x,y}}; handles {path, point, in, out}; mirror {path, point, mode: none|angle|mirror}; bend {path, segment, t?, through:{x,y}} (bend the curve through a point); smooth / corner {path, point}; insert {path, segment, t?} (add a point); cut {path, point | segment, t?}; erase {path, segment}; delete {path, point?}; close / open {path}; paint {path, fill} (the paint bucket; fill null empties it); style {path?, style}; translate {path?, by:{x,y}}; add {path}. Answers the drawing read back, as getVector does \u2014 check it, and screenshot it with canvasScreenshot before saying it is done.",
6975
+ inputSchema: {
6976
+ type: "object",
6977
+ properties: {
6978
+ target: TARGET_SCHEMA,
6979
+ paths: {
6980
+ type: "array",
6981
+ description: "Paths to draw (or to add to `target`).",
6982
+ items: {
6983
+ type: "object",
6984
+ properties: {
6985
+ d: { type: "string", description: 'SVG path data, e.g. "M0 0 L40 0 L20 30 Z".' },
6986
+ points: {
6987
+ type: "array",
6988
+ items: {
6989
+ type: "object",
6990
+ properties: {
6991
+ x: { type: "number" },
6992
+ y: { type: "number" },
6993
+ in: { type: "object", description: "Incoming handle, relative to the point.", properties: { x: { type: "number" }, y: { type: "number" } } },
6994
+ out: { type: "object", description: "Outgoing handle, relative to the point.", properties: { x: { type: "number" }, y: { type: "number" } } }
6995
+ },
6996
+ required: ["x", "y"]
6997
+ }
6998
+ },
6999
+ closed: { type: "boolean" },
7000
+ style: VECTOR_STYLE_SCHEMA
7001
+ }
7002
+ }
7003
+ },
7004
+ style: { ...VECTOR_STYLE_SCHEMA, description: "Paint for every path that does not set its own. Defaults: stroke #1c1917, strokeWidth 2, no fill, round cap and join." },
7005
+ operations: {
7006
+ type: "array",
7007
+ description: "Pen operations, applied in order after `paths` are added.",
7008
+ items: {
7009
+ type: "object",
7010
+ properties: {
7011
+ op: {
7012
+ type: "string",
7013
+ enum: ["add", "move", "handles", "mirror", "bend", "smooth", "corner", "insert", "cut", "erase", "delete", "close", "open", "paint", "style", "translate"]
7014
+ },
7015
+ path: { description: "A path index from getVector \u2014 or, for add, a path object like those in `paths`." },
7016
+ point: { type: "number" },
7017
+ segment: { type: "number" },
7018
+ t: { type: "number", description: "Where along the segment, 0\u20131 (0.5 by default)." },
7019
+ to: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
7020
+ through: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
7021
+ by: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
7022
+ in: { type: ["object", "null"], properties: { x: { type: "number" }, y: { type: "number" } } },
7023
+ out: { type: ["object", "null"], properties: { x: { type: "number" }, y: { type: "number" } } },
7024
+ mode: { type: "string", enum: ["none", "angle", "mirror"] },
7025
+ fill: { type: ["string", "null"] },
7026
+ style: VECTOR_STYLE_SCHEMA
7027
+ },
7028
+ required: ["op"]
7029
+ }
7030
+ },
7031
+ parent: { type: "string", description: "Id of the layer to draw a new drawing inside. Omit for the board." },
7032
+ at: {
7033
+ type: "object",
7034
+ description: "Where a new drawing's 0,0 lands, in the parent's coordinates (the board's without one).",
7035
+ properties: { x: { type: "number" }, y: { type: "number" } }
7036
+ },
7037
+ name: { type: "string", description: "The new drawing's layer name." }
7038
+ }
7039
+ }
7040
+ },
6952
7041
  {
6953
7042
  name: "exportHtml",
6954
7043
  description: "The design as markup \u2014 the whole page, or one layer with `target`. This is the fastest way to read a document you are about to change. `responsive` and `states` emit the media queries and the :hover rules the design carries. Pictures are named `asset:media/images/<hash>.<ext>` (getLayer's `files` gives the path on disk); pass the name back in markup and it is the same picture. Anything still inline comes back as a short `media:\u2026` reference that works the same way. A `target` inside a running project answers that element as its page has it \u2014 its own classes and attributes, its computed look inline \u2014 which is what to copy when moving a block from one project to another.",
@@ -7205,13 +7294,13 @@ var SHOT_DIR = join2(tmpdir(), "canvas-agent-shots");
7205
7294
  var CANVAS_TOOL_GUIDE = [
7206
7295
  "THE DESIGN ON THE BOARD IS REACHED THROUGH THESE TOOLS, BY THESE NAMES:",
7207
7296
  CANVAS_TOOLS.map((tool) => tool.name).join(", ") + ".",
7208
- "Your provider may show them prefixed \u2014 `mcp__bb-bridge__listLayers` \u2014 as bb hands them over as a session MCP server named `bb-bridge`. Prefixed or not, they are the editor's own, already connected: there is nothing to start, connect, configure or open first.",
7297
+ "Your provider may show them prefixed \u2014 `mcp__bb-bridge__listLayers` \u2014 as bb hands them over as a session MCP server named `bb-bridge`. Prefixed or not, they are the editor's own, already connected: nothing to start, connect, configure or open first.",
7209
7298
  "It is the NAME that makes a tool this design's, never the prefix. A tool not listed above does not touch this design, however it sounds. Never use one for work on this board, or to explain a failure on it.",
7210
7299
  "THE BOARD SHOWS WHAT YOU ARE DOING. Every write you make already puts a band of colour round the layer for a few seconds. holdLayer is the longer claim: it keeps a layer for a whole job, says what you are doing to it, and refuses the person's edits meanwhile. Take one before your first change when you are REWRITING it \u2014 redesigning, restyling, replacing its insides \u2014 so nobody edits under a half-applied change. Reading takes nothing; nor do variants BESIDE one, which are new layers.",
7211
7300
  "Release before your turn ends even when you are giving up, a tool failed, or you are handing the question back \u2014 say so. askAboutLayer keeps the hold and puts your question on the board; holdLayer picks it back up. The person can take a layer back with the button beside its name, and a hold whose assistant goes quiet frees itself; getLayer reports the run on a layer, and an edit to one no longer held still lands.",
7212
7301
  "If a tool reports it cannot reach the editor, the tool has already retried: report its message as it stands \u2014 do not assume more than it says \u2014 and stop. If the tools are missing from this session altogether, say THAT, plainly, first. In neither case substitute another application's tools or edit the design's files: the editor overwrites them.",
7213
7302
  "WRITE ORDINARY WEB MARKUP. replaceLayer and insertHtml take any HTML \u2014 sections, tables, svg, canvas, forms with real inputs. Lay it out with flexbox and grid, which the inspector edits; absolute positioning only on the outermost element the board places. Linked stylesheets are ignored. Behaviour goes on a layer in a webpage (updateLayer's `script`, or script tags in markup) and runs in Interact and Preview with `element` as that layer, never on the canvas.",
7214
- "TO SEE AN EFFECT WORK: setPageView (in-view, motion playing), then observePage to read it or canvasPointer to use it with a real mouse. Restructure as a drag would with moveLayer, groupLayers and duplicateLayers.",
7303
+ "TO SEE AN EFFECT WORK: setPageView (in-view, motion playing), then observePage to read it or canvasPointer to use it with a real mouse. Restructure like a drag with moveLayer, groupLayers and duplicateLayers.",
7215
7304
  "A LIVE PROJECT ON THIS MACHINE (localhost, LAN): a change asked for there is done only once SAVED in its source \u2014 a layer-tool edit is discarded at the next rebuild. Write it as the project's own code: the component or stylesheet that renders it, its Tailwind classes and theme variables, no inline styles or hard-coded colours. Then check LIGHT AND DARK MODE: every colour from a token with a value for each (listVariables; a new one gets both), and look at the page in each (setPageView theme light, then dark, then system) before saying done. A page served from the web has no source here: say so.",
7216
7305
  "COPYING BETWEEN LIVE PROJECTS: a copy is the original, never a redesign. Read it in its source project first; keep its markup, words, data and look. Each colour, size, radius, shadow and font becomes var(--name) holding that same value: the target's own (listVariables) only if equal, else a new one (setVariable).",
7217
7306
  'Start by looking: getSelection is what the person is pointing at when they say "this"; listLayers gives bearings; listVariables gives the design system behind it \u2014 change a shared colour there, not on every layer.'
@@ -8810,9 +8899,9 @@ function applyChanges(folderPath, changes) {
8810
8899
  ownedNodes.add(nodes);
8811
8900
  }
8812
8901
  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;
8902
+ for (const node2 of changes.nodes) {
8903
+ if (nodes[node2.id]) before[node2.id] = nodes[node2.id];
8904
+ nodes[node2.id] = node2;
8816
8905
  }
8817
8906
  for (const id of changes.removed) {
8818
8907
  if (!nodes[id]) continue;
@@ -9015,8 +9104,8 @@ function adopt(folderPath, options = {}) {
9015
9104
  return { ok: true, report: { wrote, scaffolded, ...moved.length ? { moved } : {} } };
9016
9105
  }
9017
9106
  function openProject(folderPath, options = {}) {
9018
- const adopted = adopt(folderPath, options);
9019
- if (!adopted.ok) return { ok: false, error: adopted.error };
9107
+ const adopted2 = adopt(folderPath, options);
9108
+ if (!adopted2.ok) return { ok: false, error: adopted2.error };
9020
9109
  let manifest;
9021
9110
  try {
9022
9111
  manifest = readJsonWithBackupSync(manifestPath(folderPath));
@@ -9041,7 +9130,7 @@ function openProject(folderPath, options = {}) {
9041
9130
  manifest,
9042
9131
  document: read.document,
9043
9132
  revision: read.revision,
9044
- ...adopted.report ? { adopted: adopted.report } : {},
9133
+ ...adopted2.report ? { adopted: adopted2.report } : {},
9045
9134
  ...read.repairs.length ? { repairs: read.repairs } : {}
9046
9135
  };
9047
9136
  }
@@ -9061,11 +9150,11 @@ function validateDocument(document) {
9061
9150
  if (onPath.has(id)) return `Cycle detected at node "${id}"`;
9062
9151
  if (visited.has(id)) continue;
9063
9152
  visited.add(id);
9064
- const node = document.nodes[id];
9065
- if (!node) return `Referenced node "${id}" does not exist`;
9153
+ const node2 = document.nodes[id];
9154
+ if (!node2) return `Referenced node "${id}" does not exist`;
9066
9155
  onPath.add(id);
9067
9156
  stack.push({ id, leaving: true });
9068
- for (const childId of node.children) {
9157
+ for (const childId of node2.children) {
9069
9158
  if (onPath.has(childId)) return `Cycle detected at node "${childId}"`;
9070
9159
  stack.push({ id: childId, leaving: false });
9071
9160
  }
@@ -9104,7 +9193,7 @@ async function saveChanges(folderPath, changes, media2, saved) {
9104
9193
  const { document, previous, headChanged } = applied;
9105
9194
  const changed = assetScope(
9106
9195
  document,
9107
- changes.nodes.map((node) => document.nodes[node.id]),
9196
+ changes.nodes.map((node2) => document.nodes[node2.id]),
9108
9197
  headChanged
9109
9198
  );
9110
9199
  const plan = planExtraction(changed);
@@ -9118,13 +9207,13 @@ async function saveChanges(folderPath, changes, media2, saved) {
9118
9207
  const { revision } = await writeDocumentAsync(
9119
9208
  folderPath,
9120
9209
  stored,
9121
- changes.nodes.map((node) => node.id),
9210
+ changes.nodes.map((node2) => node2.id),
9122
9211
  changes.removed
9123
9212
  );
9124
9213
  const after = referencedAssets(
9125
9214
  assetScope(
9126
9215
  stored,
9127
- changes.nodes.map((node) => stored.nodes[node.id]),
9216
+ changes.nodes.map((node2) => stored.nodes[node2.id]),
9128
9217
  headChanged
9129
9218
  )
9130
9219
  );
@@ -9141,7 +9230,7 @@ async function saveChanges(folderPath, changes, media2, saved) {
9141
9230
  }
9142
9231
  }
9143
9232
  function assetScope(document, nodes, withHead) {
9144
- const scoped = Object.fromEntries(nodes.map((node) => [node.id, node]));
9233
+ const scoped = Object.fromEntries(nodes.map((node2) => [node2.id, node2]));
9145
9234
  if (withHead) return { ...document, nodes: scoped };
9146
9235
  return { id: document.id, name: document.name, rootId: document.rootId, nodes: scoped };
9147
9236
  }
@@ -9758,6 +9847,11 @@ function registerProjectIpc(ipc) {
9758
9847
  return relative4 ? mediaFile(relative4) : null;
9759
9848
  });
9760
9849
  }
9850
+ function looksLikeProject(folderPath) {
9851
+ const folder = path10.resolve(folderPath);
9852
+ if (folder === path10.resolve(os2.homedir()) || folder === path10.parse(folder).root) return false;
9853
+ return fs12.existsSync(path10.join(folder, ".codecaine")) || fs12.existsSync(path10.join(folder, "package.json"));
9854
+ }
9761
9855
 
9762
9856
  // src/inbox.ts
9763
9857
  var MAX_CALLS = 60;
@@ -9771,9 +9865,17 @@ var Inbox = class {
9771
9865
  lastClient = null;
9772
9866
  theme = "dark";
9773
9867
  sequence = 0;
9868
+ /** Told whenever a request needs an agent: a new one, or one said again. */
9869
+ waiting = /* @__PURE__ */ new Set();
9870
+ /** Agents that receive requests as Claude Code channel pushes, by name —
9871
+ * what the Agent panel reports as "requests reach it". */
9872
+ pushTargets = /* @__PURE__ */ new Map();
9873
+ onWaiting(listener) {
9874
+ this.waiting.add(listener);
9875
+ return () => this.waiting.delete(listener);
9876
+ }
9774
9877
  heard(client) {
9775
- const name = client ?? this.lastClient;
9776
- if (!name) return;
9878
+ const name = client ?? this.lastClient ?? "Your coding agent";
9777
9879
  this.lastClient = name;
9778
9880
  this.clients.set(name, Date.now());
9779
9881
  }
@@ -9804,6 +9906,7 @@ var Inbox = class {
9804
9906
  };
9805
9907
  this.requests.unshift(request);
9806
9908
  if (this.requests.length > MAX_REQUESTS) this.requests.length = MAX_REQUESTS;
9909
+ for (const listener of this.waiting) listener(request);
9807
9910
  return request;
9808
9911
  }
9809
9912
  followUp(id, text3) {
@@ -9813,6 +9916,7 @@ var Inbox = class {
9813
9916
  request.status = "pending";
9814
9917
  request.step = "Waiting for your agent";
9815
9918
  request.updatedAt = Date.now();
9919
+ for (const listener of this.waiting) listener(request);
9816
9920
  return request;
9817
9921
  }
9818
9922
  /** Pending requests for a folder (every folder when none is named), handed
@@ -9858,18 +9962,169 @@ function titleOf(request) {
9858
9962
  return line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
9859
9963
  }
9860
9964
 
9861
- // src/agent.ts
9965
+ // ../editor/electron/bb/tool-server.ts
9966
+ import http2 from "node:http";
9967
+ import { randomBytes, timingSafeEqual } from "node:crypto";
9862
9968
  var DEFAULT_TIMEOUT_MS = 3e4;
9969
+ var SLOW_TOOL_TIMEOUT_MS = {
9970
+ observePage: 12e4,
9971
+ observeVideo: 12e4,
9972
+ canvasPointer: 9e4
9973
+ };
9974
+ var MAX_BODY_BYTES = 4 * 1024 * 1024;
9975
+ var pending = /* @__PURE__ */ new Map();
9976
+ var seq = 0;
9977
+ function routeToolResponse(requestId, result) {
9978
+ const entry = pending.get(requestId);
9979
+ if (!entry) return false;
9980
+ pending.delete(requestId);
9981
+ clearTimeout(entry.timer);
9982
+ entry.resolve(result);
9983
+ return true;
9984
+ }
9985
+ function tokenMatches(header3, token) {
9986
+ if (!header3) return false;
9987
+ const m = /^Bearer\s+(.+)$/i.exec(header3.trim());
9988
+ if (!m) return false;
9989
+ const got = Buffer.from(m[1], "utf8");
9990
+ const want = Buffer.from(token, "utf8");
9991
+ if (got.length !== want.length) return false;
9992
+ return timingSafeEqual(got, want);
9993
+ }
9994
+ function readBody(req) {
9995
+ return new Promise((resolve3) => {
9996
+ const chunks = [];
9997
+ let total = 0;
9998
+ req.on("data", (c) => {
9999
+ total += c.length;
10000
+ if (total > MAX_BODY_BYTES) {
10001
+ resolve3(null);
10002
+ req.destroy();
10003
+ return;
10004
+ }
10005
+ chunks.push(c);
10006
+ });
10007
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
10008
+ req.on("error", () => resolve3(null));
10009
+ });
10010
+ }
10011
+ function send(res, status, body) {
10012
+ const text3 = JSON.stringify(body);
10013
+ res.writeHead(status, {
10014
+ "content-type": "application/json",
10015
+ "content-length": Buffer.byteLength(text3)
10016
+ });
10017
+ res.end(text3);
10018
+ }
10019
+ function startToolServer(opts) {
10020
+ const token = randomBytes(32).toString("hex");
10021
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10022
+ const owner = {};
10023
+ const sockets = /* @__PURE__ */ new Set();
10024
+ const server2 = http2.createServer((req, res) => {
10025
+ void handle(req, res);
10026
+ });
10027
+ server2.on("connection", (s) => {
10028
+ sockets.add(s);
10029
+ s.on("close", () => sockets.delete(s));
10030
+ });
10031
+ async function handle(req, res) {
10032
+ const route = (req.url || "").split("?")[0];
10033
+ if (route !== "/tool") return send(res, 404, { error: "not found" });
10034
+ if (req.method !== "POST") return send(res, 405, { error: "use POST" });
10035
+ if (!tokenMatches(req.headers.authorization, token)) {
10036
+ return send(res, 401, { error: "unauthorized" });
10037
+ }
10038
+ const raw = await readBody(req);
10039
+ if (raw === null) return send(res, 413, { error: "request body too large" });
10040
+ let body;
10041
+ try {
10042
+ body = JSON.parse(raw || "{}");
10043
+ } catch {
10044
+ return send(res, 400, { error: "body must be JSON" });
10045
+ }
10046
+ const tool = typeof body.tool === "string" ? body.tool.trim() : "";
10047
+ if (!tool) return send(res, 200, { error: "missing 'tool'" });
10048
+ const params2 = body.params && typeof body.params === "object" ? body.params : {};
10049
+ const cwd = typeof body.cwd === "string" ? body.cwd : "";
10050
+ const deadline = opts.timeoutMs ?? SLOW_TOOL_TIMEOUT_MS[tool] ?? timeoutMs;
10051
+ const requestId = `bbtool-${process.pid}-${++seq}`;
10052
+ const result = await new Promise((resolve3) => {
10053
+ const timer = setTimeout(() => {
10054
+ pending.delete(requestId);
10055
+ resolve3({
10056
+ error: `no editor answered '${tool}' within ${deadline}ms \u2014 no project is open in the editor window, or it stopped responding`
10057
+ });
10058
+ }, deadline);
10059
+ timer.unref?.();
10060
+ pending.set(requestId, { resolve: resolve3, timer, owner });
10061
+ try {
10062
+ opts.send("canvasapi:call", { requestId, tool, params: params2, cwd });
10063
+ } catch (err) {
10064
+ pending.delete(requestId);
10065
+ clearTimeout(timer);
10066
+ resolve3({
10067
+ error: `the editor could not receive the call: ${err instanceof Error ? err.message : String(err)}`
10068
+ });
10069
+ }
10070
+ });
10071
+ if (!result || typeof result !== "object") {
10072
+ return send(res, 200, { error: "the editor sent an empty response" });
10073
+ }
10074
+ const failure = result.error;
10075
+ if (typeof failure === "string") return send(res, 200, { error: failure });
10076
+ return send(res, 200, { result });
10077
+ }
10078
+ return new Promise((resolve3, reject) => {
10079
+ server2.once("error", reject);
10080
+ server2.listen(0, "127.0.0.1", () => {
10081
+ server2.removeListener("error", reject);
10082
+ const addr = server2.address();
10083
+ let stopped = null;
10084
+ resolve3({
10085
+ url: `http://127.0.0.1:${addr.port}`,
10086
+ token,
10087
+ port: addr.port,
10088
+ stop() {
10089
+ if (stopped) return stopped;
10090
+ stopped = new Promise((done) => {
10091
+ for (const [id, entry] of pending) {
10092
+ if (entry.owner !== owner) continue;
10093
+ pending.delete(id);
10094
+ clearTimeout(entry.timer);
10095
+ entry.resolve({ error: "the editor's tool server stopped" });
10096
+ }
10097
+ const grace = setTimeout(() => {
10098
+ server2.closeAllConnections?.();
10099
+ for (const s of sockets) s.destroy();
10100
+ sockets.clear();
10101
+ }, 50);
10102
+ grace.unref?.();
10103
+ server2.close(() => {
10104
+ clearTimeout(grace);
10105
+ done();
10106
+ });
10107
+ server2.closeIdleConnections?.();
10108
+ });
10109
+ return stopped;
10110
+ }
10111
+ });
10112
+ });
10113
+ });
10114
+ }
10115
+
10116
+ // src/agent.ts
10117
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
9863
10118
  var LONG_TIMEOUT_MS = 18e4;
9864
10119
  var SHOT_DIR2 = path11.join(os3.tmpdir(), "codecaine-agent-shots");
9865
10120
  function createAgentHost(ipc, options) {
9866
- const pending = /* @__PURE__ */ new Map();
10121
+ const pending3 = /* @__PURE__ */ new Map();
9867
10122
  const inbox = new Inbox();
9868
10123
  let sequence = 0;
9869
10124
  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);
10125
+ const entry = message?.requestId ? pending3.get(message.requestId) : void 0;
10126
+ if (!entry) return { ok: routeToolResponse(String(message?.requestId ?? ""), message?.result) };
10127
+ pending3.delete(message.requestId);
9873
10128
  clearTimeout(entry.timer);
9874
10129
  entry.resolve(message.result);
9875
10130
  });
@@ -9945,15 +10200,15 @@ function createAgentHost(ipc, options) {
9945
10200
  });
9946
10201
  }
9947
10202
  const requestId = `mcp-${Date.now().toString(36)}-${++sequence}`;
9948
- const wait = timeoutMs ?? (FRAMES_TOOLS.includes(tool) ? LONG_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
10203
+ const wait = timeoutMs ?? (FRAMES_TOOLS.includes(tool) ? LONG_TIMEOUT_MS : DEFAULT_TIMEOUT_MS2);
9949
10204
  return new Promise((resolve3) => {
9950
10205
  const timer = setTimeout(() => {
9951
- pending.delete(requestId);
10206
+ pending3.delete(requestId);
9952
10207
  resolve3({
9953
10208
  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
10209
  });
9955
10210
  }, wait);
9956
- pending.set(requestId, { resolve: resolve3, timer });
10211
+ pending3.set(requestId, { resolve: resolve3, timer });
9957
10212
  ipc.broadcast("canvasapi:call", { requestId, tool, params: params2, cwd });
9958
10213
  });
9959
10214
  }
@@ -10109,6 +10364,7 @@ function createAgentHost(ipc, options) {
10109
10364
  ].join("\n\n");
10110
10365
  return {
10111
10366
  inbox,
10367
+ bringToFront,
10112
10368
  call,
10113
10369
  instructions,
10114
10370
  tools: () => [
@@ -10228,14 +10484,16 @@ function agentPage() {
10228
10484
  <h2>Connect your coding agent</h2>
10229
10485
  <p class="muted" style="margin:0">Keep <code>npx codecaine-cli</code> running, then add it once:</p>
10230
10486
  <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>
10487
+ <div class="copy"><pre>claude mcp add codecaine -- npx -y codecaine-cli mcp</pre><button type="button">Copy</button></div>
10488
+ <span class="faint">then start it so board requests reach it by themselves</span>
10489
+ <div class="copy"><pre>claude --dangerously-load-development-channels server:codecaine</pre><button type="button">Copy</button></div></div>
10232
10490
  <div class="step"><span class="faint">Cursor, Windsurf and others (mcp.json)</span>
10233
10491
  <div class="copy"><pre>{
10234
10492
  "mcpServers": {
10235
10493
  "codecaine": { "command": "npx", "args": ["-y", "codecaine-cli", "mcp"] }
10236
10494
  }
10237
10495
  }</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>
10496
+ <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
10497
  </section>
10240
10498
  <script>
10241
10499
  const project = new URLSearchParams(location.search).get("project") || "";
@@ -10271,6 +10529,9 @@ function agentPage() {
10271
10529
  const line = el("span");
10272
10530
  if (state.agent) {
10273
10531
  line.append(el("strong", null, state.agent.name), document.createTextNode(" is connected "), el("span", "faint", "\xB7 last heard " + ago(state.agent.lastSeen)));
10532
+ line.append(el("div", "faint", state.pushing
10533
+ ? "Requests from the board reach it by themselves."
10534
+ : "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
10535
  } else {
10275
10536
  line.append(el("strong", null, "No coding agent connected"), document.createTextNode(" "), el("span", "faint", "\xB7 add one below"));
10276
10537
  }
@@ -10305,59 +10566,2495 @@ function agentPage() {
10305
10566
  </body>
10306
10567
  </html>`;
10307
10568
  }
10569
+ function assistantStartingPage() {
10570
+ return `<!doctype html>
10571
+ <html lang="en">
10572
+ <head>
10573
+ <meta charset="utf-8" />
10574
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
10575
+ <title>Starting the assistant</title>
10576
+ <style>
10577
+ :root { --panel: #ffffff; --inset: #f5f5f5; --text: #17181c; --text-2: #696969; --text-3: #a8a8a8; --accent: #0099ff; --pink: #d42d73; }
10578
+ :root[data-theme="dark"] { --panel: #1f1f1f; --inset: #2e2e2e; --text: #f5f5f5; --text-2: #a8a8a8; --text-3: #8a8a8a; --pink: #f0679f; }
10579
+ * { box-sizing: border-box; }
10580
+ html, body { margin: 0; height: 100%; background: var(--panel); color: var(--text); }
10581
+ body { font: 12px/16px "Inter Variable", Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; letter-spacing: -0.01em;
10582
+ display: grid; place-content: center; padding: 24px 20px; text-align: center; }
10583
+ h1 { margin: 0 0 6px; font-size: 13px; line-height: 18px; font-weight: 600; }
10584
+ p { margin: 0; color: var(--text-2); }
10585
+ .bar { width: 220px; height: 4px; margin: 14px auto 8px; border-radius: 2px; background: var(--inset); overflow: hidden; }
10586
+ .fill { height: 100%; width: 0; background: var(--accent); border-radius: 2px; transition: width .4s ease; }
10587
+ .bar.unknown .fill { width: 35%; animation: slide 1.2s ease-in-out infinite; }
10588
+ @keyframes slide { from { transform: translateX(-100%); } to { transform: translateX(290%); } }
10589
+ @media (prefers-reduced-motion: reduce) { .bar.unknown .fill { animation: none; width: 100%; opacity: .5; } }
10590
+ .note { color: var(--text-3); }
10591
+ .error { color: var(--pink); margin-top: 8px; max-width: 280px; word-break: break-word; }
10592
+ </style>
10593
+ </head>
10594
+ <body>
10595
+ <h1 id="title">Starting the assistant\u2026</h1>
10596
+ <p id="what">The first time, it downloads the assistant \u2014 after that it starts in seconds.</p>
10597
+ <div class="bar unknown" id="bar"><div class="fill" id="fill"></div></div>
10598
+ <p class="note" id="note"></p>
10599
+ <p class="error" id="error" hidden></p>
10600
+ <script>
10601
+ const query = new URLSearchParams(location.search);
10602
+ document.documentElement.dataset.theme = matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
10603
+ async function poll() {
10604
+ let answer;
10605
+ try {
10606
+ answer = await (await fetch("assistant?" + query.toString(), { cache: "no-store" })).json();
10607
+ } catch {
10608
+ document.getElementById("note").textContent = "Lost the companion \u2014 is npx codecaine-cli still running?";
10609
+ return setTimeout(poll, 2000);
10610
+ }
10611
+ const s = answer.state;
10612
+ document.documentElement.dataset.theme = answer.theme || document.documentElement.dataset.theme;
10613
+ if (answer.url) return location.replace(answer.url);
10614
+ document.getElementById("title").textContent = s.phase === "failed" ? "The assistant did not start" : s.message || "Starting the assistant\u2026";
10615
+ const bar = document.getElementById("bar");
10616
+ bar.classList.toggle("unknown", s.percent == null);
10617
+ document.getElementById("fill").style.width = s.percent == null ? "" : s.percent + "%";
10618
+ document.getElementById("note").textContent = s.phase === "installing" && s.percent != null ? s.percent + "%" : "";
10619
+ const error = document.getElementById("error");
10620
+ error.hidden = s.phase !== "failed";
10621
+ error.textContent = s.error || "";
10622
+ document.getElementById("what").hidden = s.phase !== "installing" && s.phase !== "idle";
10623
+ setTimeout(poll, s.phase === "failed" ? 5000 : 800);
10624
+ }
10625
+ poll();
10626
+ </script>
10627
+ </body>
10628
+ </html>`;
10629
+ }
10308
10630
 
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);
10631
+ // src/bb.ts
10632
+ import { spawn, spawnSync } from "node:child_process";
10633
+ import fs19 from "node:fs";
10634
+ import path18 from "node:path";
10635
+
10636
+ // ../editor/electron/bb/supervisor.ts
10637
+ import os4 from "node:os";
10638
+ import path12 from "node:path";
10639
+ import net2 from "node:net";
10640
+ import fs14 from "node:fs";
10641
+ import { createRequire } from "node:module";
10642
+ import { spawn as nodeSpawn } from "node:child_process";
10643
+
10644
+ // ../editor/electron/bb/client.ts
10645
+ function isHostConnected(host) {
10646
+ return host.status === "connected";
10647
+ }
10648
+ var BB_PERMISSION_MODES = ["accept-edits", "auto", "full"];
10649
+ function isBbPermissionMode(value) {
10650
+ return typeof value === "string" && BB_PERMISSION_MODES.includes(value);
10651
+ }
10652
+ var BB_THREAD_STATUSES = [
10653
+ "pending",
10654
+ "idle",
10655
+ "starting",
10656
+ "active",
10657
+ "stopping",
10658
+ "error"
10659
+ ];
10660
+ var BbApiError = class extends Error {
10661
+ status;
10662
+ url;
10663
+ constructor(args2) {
10664
+ super(`Agent backend ${args2.status}: ${args2.message}`);
10665
+ this.name = "BbApiError";
10666
+ this.status = args2.status;
10667
+ this.url = args2.url;
10315
10668
  }
10316
- has(channel) {
10317
- return this.handlers.has(channel);
10669
+ };
10670
+ var DEFAULT_TIMEOUT_MS3 = 1e4;
10671
+ var REGISTRY_TIMEOUT_MS = 25e3;
10672
+ var INSTALL_TIMEOUT_MS = 13e4;
10673
+ function asRecord(value) {
10674
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
10675
+ }
10676
+ function errorMessage(status, body) {
10677
+ try {
10678
+ const rec = asRecord(JSON.parse(body));
10679
+ if (rec) {
10680
+ if (typeof rec.error === "string") return rec.error;
10681
+ if (typeof rec.message === "string") return rec.message;
10682
+ }
10683
+ } catch {
10318
10684
  }
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`);
10685
+ return body.trim() || `HTTP ${status}`;
10686
+ }
10687
+ function parseProject(value) {
10688
+ const rec = asRecord(value);
10689
+ if (!rec || typeof rec.id !== "string") return null;
10690
+ const sources = Array.isArray(rec.sources) ? rec.sources.flatMap((entry) => {
10691
+ const source = asRecord(entry);
10692
+ return source && typeof source.path === "string" && typeof source.hostId === "string" ? [{ hostId: source.hostId, path: source.path }] : [];
10693
+ }) : [];
10694
+ return {
10695
+ id: rec.id,
10696
+ name: typeof rec.name === "string" ? rec.name : rec.id,
10697
+ sources
10698
+ };
10699
+ }
10700
+ function parseThreadEvent(value) {
10701
+ const rec = asRecord(value);
10702
+ if (!rec || typeof rec.seq !== "number" || typeof rec.type !== "string") return null;
10703
+ return { seq: rec.seq, type: rec.type, data: asRecord(rec.data) ?? {} };
10704
+ }
10705
+ function parseThread(value) {
10706
+ const rec = asRecord(value);
10707
+ if (!rec || typeof rec.id !== "string") return null;
10708
+ return {
10709
+ id: rec.id,
10710
+ title: typeof rec.title === "string" ? rec.title : null,
10711
+ createdAt: typeof rec.createdAt === "number" ? rec.createdAt : 0,
10712
+ ...typeof rec.status === "string" && BB_THREAD_STATUSES.includes(rec.status) ? { status: rec.status } : {},
10713
+ ...typeof rec.providerId === "string" && rec.providerId ? { providerId: rec.providerId } : {},
10714
+ ...typeof rec.titleFallback === "string" ? { titleFallback: rec.titleFallback } : {},
10715
+ ...typeof rec.updatedAt === "number" ? { updatedAt: rec.updatedAt } : {},
10716
+ ...rec.hasPendingInteraction === true ? { hasPendingInteraction: true } : {}
10717
+ };
10718
+ }
10719
+ function parseSkill(value) {
10720
+ const rec = asRecord(value);
10721
+ if (!rec || typeof rec.id !== "string" || typeof rec.name !== "string") return null;
10722
+ return {
10723
+ id: rec.id,
10724
+ name: rec.name,
10725
+ description: typeof rec.description === "string" ? rec.description : null,
10726
+ scope: typeof rec.scope === "string" ? rec.scope : "unknown",
10727
+ manageable: rec.manageable === true,
10728
+ registrySkillId: typeof rec.registrySkillId === "string" ? rec.registrySkillId : null
10729
+ };
10730
+ }
10731
+ function parseRegistrySkill(value) {
10732
+ const rec = asRecord(value);
10733
+ if (!rec || typeof rec.id !== "string" || typeof rec.source !== "string" || typeof rec.skillId !== "string" || typeof rec.name !== "string") {
10734
+ return null;
10735
+ }
10736
+ return {
10737
+ id: rec.id,
10738
+ source: rec.source,
10739
+ skillId: rec.skillId,
10740
+ name: rec.name,
10741
+ installs: typeof rec.installs === "number" ? rec.installs : 0,
10742
+ url: typeof rec.url === "string" ? rec.url : `https://www.skills.sh/${rec.id}`,
10743
+ summary: typeof rec.summary === "string" ? rec.summary : null
10744
+ };
10745
+ }
10746
+ function parseTerminal(value) {
10747
+ const rec = asRecord(value);
10748
+ const id = rec && typeof rec.id === "string" ? rec.id : null;
10749
+ const status = rec?.status;
10750
+ if (!id || status !== "starting" && status !== "disconnected" && status !== "running" && status !== "exited") {
10751
+ throw new Error("agent backend returned a malformed terminal");
10752
+ }
10753
+ return { id, status, exitCode: typeof rec?.exitCode === "number" ? rec.exitCode : null };
10754
+ }
10755
+ var BbClient = class {
10756
+ origin;
10757
+ baseUrl;
10758
+ fetchImpl;
10759
+ constructor(options) {
10760
+ this.origin = `http://127.0.0.1:${options.port}`;
10761
+ this.baseUrl = `${this.origin}/api/v1`;
10762
+ this.fetchImpl = options.fetchImpl ?? fetch;
10763
+ }
10764
+ async request(method, url, body, timeoutMs = DEFAULT_TIMEOUT_MS3) {
10765
+ const controller = new AbortController();
10766
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
10767
+ timer.unref?.();
10768
+ let response;
10769
+ try {
10770
+ response = await this.fetchImpl(url, {
10771
+ method,
10772
+ // A JSON content-type is not decoration on the plugin routes: bb's
10773
+ // "local" auth requires it for a mutation, which is what stops a page
10774
+ // in a browser from forging one.
10775
+ headers: body === void 0 ? {} : { "content-type": "application/json" },
10776
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
10777
+ signal: controller.signal
10778
+ });
10779
+ } finally {
10780
+ clearTimeout(timer);
10781
+ }
10782
+ const text3 = await response.text();
10783
+ if (!response.ok) {
10784
+ throw new BbApiError({
10785
+ status: response.status,
10786
+ message: errorMessage(response.status, text3),
10787
+ url
10788
+ });
10789
+ }
10790
+ if (!text3) return null;
10791
+ try {
10792
+ return JSON.parse(text3);
10793
+ } catch {
10794
+ return text3;
10323
10795
  }
10324
- return handler(...args2);
10325
10796
  }
10326
- addPeer(peer) {
10327
- this.peers.add(peer);
10328
- return () => this.peers.delete(peer);
10797
+ /** `GET /health`, off the API prefix. Answers before the host daemon has
10798
+ * enrolled, which is why it is one of three probes and not the gate. */
10799
+ async health(timeoutMs) {
10800
+ try {
10801
+ await this.request("GET", `${this.origin}/health`, void 0, timeoutMs);
10802
+ return true;
10803
+ } catch {
10804
+ return false;
10805
+ }
10806
+ }
10807
+ /** `GET /api/v1/system/config`. Read as an opaque record: we only need it to
10808
+ * prove the server finished booting, since answering it reads config and the
10809
+ * database. */
10810
+ async systemConfig(timeoutMs) {
10811
+ const value = await this.request(
10812
+ "GET",
10813
+ `${this.baseUrl}/system/config`,
10814
+ void 0,
10815
+ timeoutMs
10816
+ );
10817
+ const rec = asRecord(value);
10818
+ if (!rec) throw new Error("agent backend returned a malformed system config");
10819
+ return rec;
10820
+ }
10821
+ /** `GET /api/v1/system/providers/state`: each provider CLI's health, with
10822
+ * the command that signs it in. Opaque here; bb-login.ts reads it. A health
10823
+ * check runs the CLI on the host, hence the longer wait. */
10824
+ async providerStates(timeoutMs = 2e4) {
10825
+ return this.request("GET", `${this.baseUrl}/system/providers/state`, void 0, timeoutMs);
10826
+ }
10827
+ /** `GET /api/v1/system/providers`: the providers bb offers, already in the
10828
+ * order its Settings › Providers list shows them. Opaque here;
10829
+ * provider-choice.ts reads it. */
10830
+ async providers(timeoutMs = 2e4) {
10831
+ return this.request("GET", `${this.baseUrl}/system/providers`, void 0, timeoutMs);
10832
+ }
10833
+ /** `GET /api/v1/system/execution-options`: for one provider, its models
10834
+ * (each with the reasoning levels it takes), the permission modes and
10835
+ * composer actions (plan) it supports, and the host's permission ceiling.
10836
+ * Opaque here; compose-options.ts reads it. Listing models runs the CLI on
10837
+ * the host, hence the longer wait. */
10838
+ async executionOptions(providerId, timeoutMs = 2e4) {
10839
+ const query = providerId ? `?providerId=${encodeURIComponent(providerId)}` : "";
10840
+ return this.request("GET", `${this.baseUrl}/system/execution-options${query}`, void 0, timeoutMs);
10841
+ }
10842
+ /** `GET /api/v1/projects/:id/branches`: the checkout of the project's source
10843
+ * on a host — the branch it is on, the local branches, whether the tree is
10844
+ * dirty. Opaque here; compose-options.ts reads it. */
10845
+ async projectBranches(projectId, hostId, timeoutMs = 15e3) {
10846
+ return this.request(
10847
+ "GET",
10848
+ `${this.baseUrl}/projects/${encodeURIComponent(projectId)}/branches?hostId=${encodeURIComponent(hostId)}`,
10849
+ void 0,
10850
+ timeoutMs
10851
+ );
10329
10852
  }
10330
- peerCount() {
10331
- return this.peers.size;
10853
+ /** A provider's logo, at the `logoUrl` path `providers()` answered, as its
10854
+ * text. bb's logos are SVG; the caller checks before trusting that. */
10855
+ async providerLogo(logoPath, timeoutMs) {
10856
+ const value = await this.request("GET", new URL(logoPath, this.origin).toString(), void 0, timeoutMs);
10857
+ return typeof value === "string" ? value : "";
10858
+ }
10859
+ /** `PUT /api/v1/settings/general`. bb's schema is strict and whole — every
10860
+ * field, no partial update — so the caller sends back the `generalSettings`
10861
+ * it read from `systemConfig` with its one change. */
10862
+ async putGeneralSettings(settings) {
10863
+ return this.request("PUT", `${this.baseUrl}/settings/general`, settings);
10864
+ }
10865
+ /** `GET /api/v1/hosts`. Rows bb cannot parse are dropped rather than thrown
10866
+ * over: one unknown host must not hide the connected one beside it. */
10867
+ async hosts(timeoutMs) {
10868
+ const value = await this.request(
10869
+ "GET",
10870
+ `${this.baseUrl}/hosts`,
10871
+ void 0,
10872
+ timeoutMs
10873
+ );
10874
+ if (!Array.isArray(value)) {
10875
+ throw new Error("agent backend returned a malformed host list");
10876
+ }
10877
+ return value.flatMap((entry) => {
10878
+ const rec = asRecord(entry);
10879
+ const id = rec && typeof rec.id === "string" ? rec.id : null;
10880
+ const status = rec && (rec.status === "connected" || rec.status === "disconnected") ? rec.status : null;
10881
+ if (!id || !status) return [];
10882
+ const name = rec && typeof rec.name === "string" ? rec.name : id;
10883
+ return [{ id, name, status }];
10884
+ });
10332
10885
  }
10333
- /** `webContents.send` to every connected editor. */
10334
- broadcast(channel, ...args2) {
10335
- for (const peer of this.peers) peer.send(channel, ...args2);
10886
+ // ─────────────────────────── projects ───────────────────────────
10887
+ /** `GET /api/v1/projects`. Rows bb answers with that this build cannot read
10888
+ * are dropped rather than thrown over: one unfamiliar project must not hide
10889
+ * the folder the editor has open. */
10890
+ async projects() {
10891
+ const value = await this.request("GET", `${this.baseUrl}/projects`);
10892
+ if (!Array.isArray(value)) {
10893
+ throw new Error("agent backend returned a malformed project list");
10894
+ }
10895
+ return value.flatMap((entry) => {
10896
+ const project = parseProject(entry);
10897
+ return project ? [project] : [];
10898
+ });
10899
+ }
10900
+ /**
10901
+ * `POST /api/v1/projects` → 201.
10902
+ *
10903
+ * The wire body is `{name, source:{hostId, type:"local_path", path}}`, whose
10904
+ * `source` arm is strict. The caller has a flat `{name, root, hostId}`, which
10905
+ * is what it knows, so the nesting is assembled here.
10906
+ */
10907
+ async createProject(args2) {
10908
+ const value = await this.request("POST", `${this.baseUrl}/projects`, {
10909
+ name: args2.name,
10910
+ source: { hostId: args2.hostId, type: "local_path", path: args2.root }
10911
+ });
10912
+ const project = parseProject(value);
10913
+ if (!project) throw new Error("agent backend returned a malformed project");
10914
+ return project;
10915
+ }
10916
+ // ─────────────────────────── threads ───────────────────────────
10917
+ /** `GET /api/v1/threads?projectId=…`. An unreadable list is an empty one:
10918
+ * the panel then frames bb's compose view, which still works. */
10919
+ async listThreads(projectId) {
10920
+ const value = await this.request(
10921
+ "GET",
10922
+ `${this.baseUrl}/threads?projectId=${encodeURIComponent(projectId)}&archived=false`
10923
+ );
10924
+ const rows = Array.isArray(value) ? value : Array.isArray(asRecord(value)?.threads) ? asRecord(value).threads : [];
10925
+ return rows.flatMap((entry) => {
10926
+ const thread = parseThread(entry);
10927
+ return thread ? [thread] : [];
10928
+ });
10929
+ }
10930
+ /**
10931
+ * `GET /api/v1/threads/:id` → the thread's `status`.
10932
+ *
10933
+ * What tells a save that the assistant's turn is over: `bb:send` resolves
10934
+ * when the message is posted, and this is the one place bb says whether the
10935
+ * turn it started is still running. Anything this build does not recognise
10936
+ * is answered as null rather than guessed at.
10937
+ */
10938
+ async threadStatus(threadId) {
10939
+ const value = await this.request(
10940
+ "GET",
10941
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}`
10942
+ );
10943
+ const status = asRecord(value)?.status;
10944
+ return typeof status === "string" && BB_THREAD_STATUSES.includes(status) ? status : null;
10945
+ }
10946
+ /** `POST /api/v1/threads` → 201. `origin` is required; "sdk" is what a
10947
+ * programmatic sender is. With no `execution`, bb picks the provider
10948
+ * itself — see model-choice.ts for why the editor names one. */
10949
+ async createThread(args2) {
10950
+ const execution = args2.execution;
10951
+ const value = await this.request("POST", `${this.baseUrl}/threads`, {
10952
+ projectId: args2.projectId,
10953
+ origin: args2.origin ?? "sdk",
10954
+ input: args2.input,
10955
+ environment: args2.environment,
10956
+ ...args2.visibility ? { visibility: args2.visibility } : {},
10957
+ ...execution ? { providerId: execution.providerId } : {},
10958
+ ...execution?.model ? { model: execution.model } : {},
10959
+ ...execution?.reasoningLevel ? { reasoningLevel: execution.reasoningLevel } : {},
10960
+ ...args2.permissionMode ? { permissionMode: args2.permissionMode } : {}
10961
+ }, args2.timeoutMs);
10962
+ const thread = parseThread(value);
10963
+ if (!thread) throw new Error("agent backend returned a malformed thread");
10964
+ return thread;
10965
+ }
10966
+ /**
10967
+ * `POST /api/v1/threads/:id/send`.
10968
+ *
10969
+ * `steer-if-active` is what a composer means: deliver into a running turn if
10970
+ * there is one, otherwise start one.
10971
+ */
10972
+ async sendMessage(threadId, input, options = {}) {
10973
+ await this.request(
10974
+ "POST",
10975
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/send`,
10976
+ {
10977
+ input,
10978
+ mode: options.mode ?? "steer-if-active",
10979
+ ...options.reasoningLevel ? { reasoningLevel: options.reasoningLevel } : {}
10980
+ }
10981
+ );
10982
+ }
10983
+ /**
10984
+ * The newest few items of a thread's log, newest first — one small request
10985
+ * however long the conversation is. What the Agent bar's jobs list names
10986
+ * each running job's current step from (thread-step.ts).
10987
+ */
10988
+ async latestThreadItems(threadId, limit = 6) {
10989
+ const value = await this.request(
10990
+ "GET",
10991
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/events?order=desc&limit=${Math.max(1, Math.floor(limit))}&types=item/started,item/completed`
10992
+ );
10993
+ const rows = Array.isArray(value) ? value : [];
10994
+ return rows.flatMap((entry) => {
10995
+ const event = parseThreadEvent(entry);
10996
+ return event ? [event] : [];
10997
+ });
10998
+ }
10999
+ /**
11000
+ * `GET /api/v1/threads/:id/events?afterSeq=…&order=asc`.
11001
+ *
11002
+ * The thread's event log, oldest first. bb PRUNES `item/agentMessage/delta`
11003
+ * rows once their `item/completed` lands (measured on 0.42.1: seqs 9–11 of a
11004
+ * finished reply were gone), so a reader that wants the stream has to be
11005
+ * reading while it happens; the completed item carries the whole text for
11006
+ * anyone who arrives late.
11007
+ */
11008
+ async threadEvents(threadId, afterSeq, limit = 500) {
11009
+ const value = await this.request(
11010
+ "GET",
11011
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/events?afterSeq=${Math.max(0, Math.floor(afterSeq))}&limit=${limit}&order=asc`
11012
+ );
11013
+ const rows = Array.isArray(value) ? value : [];
11014
+ return rows.flatMap((entry) => {
11015
+ const event = parseThreadEvent(entry);
11016
+ return event ? [event] : [];
11017
+ });
11018
+ }
11019
+ /** The newest event's `seq`, or 0 for an empty log — where a reader starts
11020
+ * so it sees only what a message it is about to send produces. */
11021
+ async latestEventSeq(threadId) {
11022
+ const value = await this.request(
11023
+ "GET",
11024
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/events?limit=1&order=desc`
11025
+ );
11026
+ const first = Array.isArray(value) ? parseThreadEvent(value[0]) : null;
11027
+ return first?.seq ?? 0;
11028
+ }
11029
+ /**
11030
+ * `GET /api/v1/threads/:id/events/wait?type=…&afterSeq=…&waitMs=…` — a long
11031
+ * poll that returns as soon as an event of `type` lands after `afterSeq`
11032
+ * (200) or when `waitMs` runs out (204). bb takes exactly ONE type here, so
11033
+ * a reader waits on the one it is latency-sensitive to and re-reads the whole
11034
+ * log on every wake. Answers whether something arrived; never throws for a
11035
+ * plain timeout.
11036
+ */
11037
+ async waitForEvent(threadId, type, afterSeq, waitMs) {
11038
+ const value = await this.request(
11039
+ "GET",
11040
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/events/wait?type=${encodeURIComponent(type)}&afterSeq=${Math.max(0, Math.floor(afterSeq))}&waitMs=${Math.max(0, Math.floor(waitMs))}`,
11041
+ void 0,
11042
+ waitMs + DEFAULT_TIMEOUT_MS3
11043
+ );
11044
+ return value !== null;
11045
+ }
11046
+ /** `POST /api/v1/threads/:id/stop` — interrupts the running turn. bb answers
11047
+ * `{ok:true}` at once and reaches `idle` a moment later. */
11048
+ async stopThread(threadId) {
11049
+ await this.request(
11050
+ "POST",
11051
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/stop`,
11052
+ {}
11053
+ );
11054
+ }
11055
+ /** `POST /api/v1/threads/:id/archive`. What bb's own docs ask of anything that
11056
+ * makes worker threads: put them away once they are done with. */
11057
+ async archiveThread(threadId) {
11058
+ await this.request(
11059
+ "POST",
11060
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/archive`,
11061
+ {}
11062
+ );
11063
+ }
11064
+ /**
11065
+ * `GET /api/v1/system/providers/state` — which providers are installed and
11066
+ * signed in on this machine. `ready` is the only status that can run a turn;
11067
+ * bb's own default provider need not be one of them (it chose Codex on a
11068
+ * machine with only Claude Code, and answered 503 to every thread).
11069
+ */
11070
+ async readyProviders() {
11071
+ const value = await this.request("GET", `${this.baseUrl}/system/providers/state`);
11072
+ const rows = asRecord(value)?.providers;
11073
+ return Array.isArray(rows) ? rows.flatMap((row2) => {
11074
+ const rec = asRecord(row2);
11075
+ return rec && rec.status === "ready" && typeof rec.providerId === "string" ? [rec.providerId] : [];
11076
+ }) : [];
11077
+ }
11078
+ /** `GET /api/v1/threads/:id/output` — the latest final assistant text, or
11079
+ * null when the thread has none yet. */
11080
+ async threadOutput(threadId) {
11081
+ const value = await this.request(
11082
+ "GET",
11083
+ `${this.baseUrl}/threads/${encodeURIComponent(threadId)}/output`
11084
+ );
11085
+ const output = asRecord(value)?.output;
11086
+ return typeof output === "string" ? output : null;
11087
+ }
11088
+ // ─────────────────────────── plugins ───────────────────────────
11089
+ /**
11090
+ * `POST /api/v1/plugins/install` `{source}`.
11091
+ *
11092
+ * bb answers `{ok:false, error}` with a 422 for a plugin it refuses — most
11093
+ * often one already registered from this same path, which is success as far
11094
+ * as the editor is concerned. The caller decides that; this only reports it.
11095
+ */
11096
+ async installPlugin(source) {
11097
+ const url = `${this.baseUrl}/plugins/install`;
11098
+ const value = await this.request("POST", url, { source });
11099
+ const rec = asRecord(value);
11100
+ if (rec && rec.ok === false) {
11101
+ throw new BbApiError({
11102
+ status: 422,
11103
+ message: typeof rec.error === "string" ? rec.error : "plugin install failed",
11104
+ url
11105
+ });
11106
+ }
11107
+ return rec ? asRecord(rec.plugin) ?? rec : null;
11108
+ }
11109
+ /**
11110
+ * `POST /api/v1/plugins/reload?id=<pluginId>`.
11111
+ *
11112
+ * Restarts a plugin's host, which is the only way code on disk becomes code
11113
+ * bb is running: the plugin is registered by path once and never read again,
11114
+ * so a `git pull` leaves an editor talking to the version installed months
11115
+ * ago — with the tools, the schemas and the instructions of that version.
11116
+ *
11117
+ * Everything the plugin holds in memory goes with the restart, the editor's
11118
+ * callback included. That is the caller's problem to sequence: the config
11119
+ * POST has to come after.
11120
+ */
11121
+ async reloadPlugin(pluginId) {
11122
+ const url = `${this.baseUrl}/plugins/reload?id=${encodeURIComponent(pluginId)}`;
11123
+ const value = await this.request("POST", url, {});
11124
+ const rec = asRecord(value);
11125
+ if (rec && rec.ok === false) {
11126
+ throw new BbApiError({
11127
+ status: 422,
11128
+ message: typeof rec.error === "string" ? rec.error : "plugin reload failed",
11129
+ url
11130
+ });
11131
+ }
11132
+ return rec;
11133
+ }
11134
+ /** One row of `GET /api/v1/plugins`, reduced to what the editor decides on. */
11135
+ async listPlugins() {
11136
+ const value = await this.request("GET", `${this.baseUrl}/plugins`);
11137
+ const rows = Array.isArray(value) ? value : Array.isArray(asRecord(value)?.plugins) ? asRecord(value).plugins : [];
11138
+ return rows.flatMap((entry) => {
11139
+ const rec = asRecord(entry);
11140
+ if (!rec || typeof rec.id !== "string") return [];
11141
+ return [
11142
+ {
11143
+ id: rec.id,
11144
+ source: typeof rec.source === "string" ? rec.source : "",
11145
+ rootDir: typeof rec.rootDir === "string" ? rec.rootDir : "",
11146
+ provenance: typeof rec.provenance === "string" ? rec.provenance : ""
11147
+ }
11148
+ ];
11149
+ });
11150
+ }
11151
+ /**
11152
+ * `DELETE /api/v1/plugins/:id`.
11153
+ *
11154
+ * Unloads the plugin's host and forgets its registration, settings and
11155
+ * secrets. bb refuses it for a built-in (409), and the editor only ever asks
11156
+ * it of a record that points at the editor's own plugin folder — see
11157
+ * `evictShadowingPlugins` in canvas-tools.ts for the one case that needs it.
11158
+ */
11159
+ async removePlugin(pluginId) {
11160
+ const url = `${this.baseUrl}/plugins/${encodeURIComponent(pluginId)}`;
11161
+ const value = await this.request("DELETE", url);
11162
+ const rec = asRecord(value);
11163
+ if (rec && rec.ok === false) {
11164
+ throw new BbApiError({
11165
+ status: 422,
11166
+ message: typeof rec.error === "string" ? rec.error : "plugin remove failed",
11167
+ url
11168
+ });
11169
+ }
11170
+ }
11171
+ /** `GET /api/v1/plugins/:id/settings` → the plugin's current values. bb
11172
+ * answers 404 until the plugin's factory has run. */
11173
+ async pluginSettings(pluginId) {
11174
+ const value = await this.request(
11175
+ "GET",
11176
+ `${this.baseUrl}/plugins/${encodeURIComponent(pluginId)}/settings`
11177
+ );
11178
+ return asRecord(asRecord(value)?.values) ?? {};
11179
+ }
11180
+ /** `PUT /api/v1/plugins/:id/settings` `{values}` — a partial update, which bb
11181
+ * validates against the plugin's schema (400 with the reason) and hands to
11182
+ * the running plugin. */
11183
+ async putPluginSettings(pluginId, values) {
11184
+ await this.request(
11185
+ "PUT",
11186
+ `${this.baseUrl}/plugins/${encodeURIComponent(pluginId)}/settings`,
11187
+ { values }
11188
+ );
11189
+ }
11190
+ // ─────────────────────────── skills ───────────────────────────
11191
+ /**
11192
+ * `GET /api/v1/projects/:id/skills?environmentId=`.
11193
+ *
11194
+ * Every skill bb would load for that project's workspace — builtin, user,
11195
+ * provider and plugin alike. `environmentId` is required-but-nullable on the
11196
+ * wire, which a query string can only say as an empty value.
11197
+ */
11198
+ async projectSkills(projectId) {
11199
+ const value = await this.request(
11200
+ "GET",
11201
+ `${this.baseUrl}/projects/${encodeURIComponent(projectId)}/skills?environmentId=`
11202
+ );
11203
+ const rows = asRecord(value)?.skills;
11204
+ if (!Array.isArray(rows)) {
11205
+ throw new Error("agent backend returned a malformed skill list");
11206
+ }
11207
+ return rows.flatMap((entry) => {
11208
+ const skill = parseSkill(entry);
11209
+ return skill ? [skill] : [];
11210
+ });
11211
+ }
11212
+ /** `DELETE /api/v1/projects/:id/skills` `{skillId, environmentId}`. bb
11213
+ * refuses (403) anything it did not install itself — bundled and plugin
11214
+ * skills are not the user's to remove. */
11215
+ async deleteSkill(projectId, skillId) {
11216
+ await this.request(
11217
+ "DELETE",
11218
+ `${this.baseUrl}/projects/${encodeURIComponent(projectId)}/skills`,
11219
+ { skillId, environmentId: null }
11220
+ );
11221
+ }
11222
+ /**
11223
+ * `GET /api/v1/skills-registry?q=&page=&perPage=`.
11224
+ *
11225
+ * bb's proxy onto skills.sh: trending with no query, all-time installs with
11226
+ * one. Server-wide, so no project is named.
11227
+ */
11228
+ async searchSkillsRegistry(args2) {
11229
+ const params2 = new URLSearchParams({
11230
+ q: args2.query,
11231
+ page: String(args2.page),
11232
+ perPage: String(args2.perPage)
11233
+ });
11234
+ const value = await this.request(
11235
+ "GET",
11236
+ `${this.baseUrl}/skills-registry?${params2.toString()}`,
11237
+ void 0,
11238
+ REGISTRY_TIMEOUT_MS
11239
+ );
11240
+ const rec = asRecord(value);
11241
+ if (!rec || !Array.isArray(rec.skills)) {
11242
+ throw new Error("agent backend returned a malformed registry page");
11243
+ }
11244
+ const pagination = asRecord(rec.pagination);
11245
+ return {
11246
+ skills: rec.skills.flatMap((entry) => {
11247
+ const skill = parseRegistrySkill(entry);
11248
+ return skill ? [skill] : [];
11249
+ }),
11250
+ hasMore: pagination?.hasMore === true,
11251
+ total: typeof pagination?.total === "number" ? pagination.total : rec.skills.length
11252
+ };
11253
+ }
11254
+ /**
11255
+ * `POST /api/v1/skills-registry/entries` `{ids}`.
11256
+ *
11257
+ * The same rows as a search, but read off each skill's own page, which is the
11258
+ * only place skills.sh carries a summary. bb drops ids it cannot resolve
11259
+ * rather than failing the batch.
11260
+ */
11261
+ async registryEntries(ids) {
11262
+ const value = await this.request(
11263
+ "POST",
11264
+ `${this.baseUrl}/skills-registry/entries`,
11265
+ { ids },
11266
+ REGISTRY_TIMEOUT_MS
11267
+ );
11268
+ const rows = asRecord(value)?.entries;
11269
+ return Array.isArray(rows) ? rows.flatMap((entry) => {
11270
+ const skill = parseRegistrySkill(entry);
11271
+ return skill ? [skill] : [];
11272
+ }) : [];
11273
+ }
11274
+ /**
11275
+ * `POST /api/v1/skills-registry/install` `{registrySkillId}`.
11276
+ *
11277
+ * bb runs the `skills` CLI under `npx` to fetch the package, validates the
11278
+ * SKILL.md against the registry entry and moves it into `<dataDir>/skills`.
11279
+ * Its own ceiling on that is two minutes, so ours sits just past it: a
11280
+ * shorter one would report a failure for an install that went on to land.
11281
+ */
11282
+ async installRegistrySkill(registrySkillId) {
11283
+ await this.request(
11284
+ "POST",
11285
+ `${this.baseUrl}/skills-registry/install`,
11286
+ { registrySkillId },
11287
+ INSTALL_TIMEOUT_MS
11288
+ );
11289
+ }
11290
+ // ─────────────────────────── terminals ───────────────────────────
11291
+ /** `POST /api/v1/terminals`: a pty on a host, in a folder, running one
11292
+ * command. The same terminals bb's own UI lists, so a dev server started
11293
+ * from the editor is one a person can find and stop there too. */
11294
+ async createTerminal(args2) {
11295
+ const value = await this.request("POST", `${this.baseUrl}/terminals`, {
11296
+ cols: args2.cols ?? 120,
11297
+ rows: args2.rows ?? 32,
11298
+ target: { kind: "host_path", hostId: args2.hostId, cwd: args2.cwd },
11299
+ start: { mode: "command", command: args2.command },
11300
+ title: args2.title
11301
+ });
11302
+ return parseTerminal(value);
11303
+ }
11304
+ /** `GET /api/v1/terminals/:id`. */
11305
+ async terminal(terminalId) {
11306
+ return parseTerminal(
11307
+ await this.request("GET", `${this.baseUrl}/terminals/${encodeURIComponent(terminalId)}`)
11308
+ );
11309
+ }
11310
+ /** `GET /api/v1/terminals/:id/output?sinceSeq=`: what the pty printed after
11311
+ * `sinceSeq`, and the sequence number to ask from next time. */
11312
+ async terminalOutput(terminalId, sinceSeq) {
11313
+ const value = asRecord(
11314
+ await this.request(
11315
+ "GET",
11316
+ `${this.baseUrl}/terminals/${encodeURIComponent(terminalId)}/output?sinceSeq=${sinceSeq}`
11317
+ )
11318
+ );
11319
+ const chunks = Array.isArray(value?.chunks) ? value.chunks : [];
11320
+ const text3 = chunks.map((chunk) => asRecord(chunk)?.dataBase64).filter((data) => typeof data === "string").map((data) => Buffer.from(data, "base64").toString("utf8")).join("");
11321
+ const nextSeq = typeof value?.nextSeq === "number" ? value.nextSeq : sinceSeq;
11322
+ return { text: text3, nextSeq };
11323
+ }
11324
+ /** `POST /api/v1/terminals/:id/close`. The body is required: bb answers
11325
+ * `{}` with a 400 ("expected one of force|if-clean"), which is how Cancel
11326
+ * once left the dev server running in bb while the panel said cancelled.
11327
+ * `force` because a dev server is never "clean" — it is always running. */
11328
+ async closeTerminal(terminalId) {
11329
+ await this.request(
11330
+ "POST",
11331
+ `${this.baseUrl}/terminals/${encodeURIComponent(terminalId)}/close`,
11332
+ { mode: "force", reason: "user" }
11333
+ );
11334
+ }
11335
+ /** Call a route the plugin registered with `bb.http.route`, which bb mounts
11336
+ * at `/api/v1/plugins/<id>/http/<path>`. */
11337
+ async pluginHttp(pluginId, routePath, body) {
11338
+ const path21 = routePath.replace(/^\/+/, "");
11339
+ const url = `${this.baseUrl}/plugins/${encodeURIComponent(pluginId)}/http/${path21}`;
11340
+ return this.request("POST", url, body ?? {});
10336
11341
  }
10337
11342
  };
10338
11343
 
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));
11344
+ // ../editor/electron/process-tree.ts
11345
+ import { execFileSync } from "node:child_process";
11346
+ function descendantsOf(table, root) {
11347
+ const children = /* @__PURE__ */ new Map();
11348
+ for (const [pid, ppid] of table) {
11349
+ if (pid === ppid) continue;
11350
+ const list = children.get(ppid) ?? [];
11351
+ list.push(pid);
11352
+ children.set(ppid, list);
11353
+ }
11354
+ const out = [];
11355
+ const seen = /* @__PURE__ */ new Set([root]);
11356
+ const queue = [root];
11357
+ while (queue.length) {
11358
+ const next = queue.shift();
11359
+ for (const child of children.get(next) ?? []) {
11360
+ if (seen.has(child)) continue;
11361
+ seen.add(child);
11362
+ out.push(child);
11363
+ queue.push(child);
11364
+ }
11365
+ }
11366
+ return out;
11367
+ }
11368
+ function readProcessTable() {
11369
+ try {
11370
+ const text3 = execFileSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8", timeout: 2e3 });
11371
+ const rows = [];
11372
+ for (const line of text3.split("\n")) {
11373
+ const [pid, ppid] = line.trim().split(/\s+/).map(Number);
11374
+ if (Number.isInteger(pid) && Number.isInteger(ppid)) rows.push([pid, ppid]);
11375
+ }
11376
+ return rows;
11377
+ } catch {
11378
+ return [];
10346
11379
  }
10347
- return btoa(binary);
10348
11380
  }
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);
11381
+ function killProcessTree(root, deps = {}) {
11382
+ const kill = deps.kill ?? ((pid, signal) => process.kill(pid, signal));
11383
+ const pids = [root, ...descendantsOf((deps.table ?? readProcessTable)(), root)];
11384
+ for (const pid of pids) {
11385
+ try {
11386
+ kill(pid, "SIGKILL");
11387
+ } catch {
11388
+ }
10353
11389
  }
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;
11390
+ return pids;
10358
11391
  }
10359
- function encode(value) {
10360
- return JSON.stringify(value, (_key, item) => {
11392
+
11393
+ // ../editor/electron/bb/supervisor.ts
11394
+ var BB_APP_VERSION = "0.42.1";
11395
+ var BB_DEFAULT_SERVER_PORT = 38886;
11396
+ var BB_DEFAULT_DAEMON_PORT = 38887;
11397
+ var USER_BB_DATA_DIR_NAME = ".bb";
11398
+ var PROBE_TIMEOUT_MS = 1e3;
11399
+ var ADOPT_READY_TIMEOUT_MS = 15e3;
11400
+ var SPAWN_READY_TIMEOUT_MS = 18e4;
11401
+ var READY_POLL_INTERVAL_MS = 250;
11402
+ function bbDataDir(homeDir = os4.homedir(), env = process.env) {
11403
+ return env.CANVAS_BB_DATA_DIR || path12.join(homeDir, ".canvas", "bb");
11404
+ }
11405
+ function bbServerPort(env = process.env) {
11406
+ const n = Number(env.CANVAS_BB_SERVER_PORT);
11407
+ return Number.isInteger(n) && n > 0 && n < 65536 ? n : BB_DEFAULT_SERVER_PORT;
11408
+ }
11409
+ function bbDaemonPort(env = process.env) {
11410
+ const n = Number(env.CANVAS_BB_DAEMON_PORT);
11411
+ return Number.isInteger(n) && n > 0 && n < 65536 ? n : BB_DEFAULT_DAEMON_PORT;
11412
+ }
11413
+ function assertSafeDataDir(dataDir2, homeDir) {
11414
+ const userBb = path12.resolve(homeDir, USER_BB_DATA_DIR_NAME);
11415
+ const resolved = path12.resolve(dataDir2);
11416
+ if (resolved === userBb || resolved.startsWith(`${userBb}${path12.sep}`)) {
11417
+ throw new Error(
11418
+ `refusing to run the agent backend against the user's own data dir (${resolved})`
11419
+ );
11420
+ }
11421
+ }
11422
+ 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) {
11423
+ let launcher;
11424
+ try {
11425
+ launcher = resolveFrom("bb-app/dist/bb-app.js");
11426
+ } catch {
11427
+ return null;
11428
+ }
11429
+ const unpacked = launcher.replace(
11430
+ `${path12.sep}app.asar${path12.sep}`,
11431
+ `${path12.sep}app.asar.unpacked${path12.sep}`
11432
+ );
11433
+ return exists(unpacked) ? unpacked : null;
11434
+ }
11435
+ function buildLaunchCommand(args2) {
11436
+ const flags = [
11437
+ "start",
11438
+ "--data-dir",
11439
+ shQuote(args2.dataDir),
11440
+ "--server-bind-host",
11441
+ "127.0.0.1",
11442
+ "--server-port",
11443
+ String(args2.serverPort),
11444
+ "--host-daemon-port",
11445
+ String(args2.daemonPort)
11446
+ ].join(" ");
11447
+ if (args2.launcher) {
11448
+ 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`;
11449
+ }
11450
+ 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`;
11451
+ }
11452
+ function loginShellInvocation(command, env = process.env, shell) {
11453
+ return { shell: shell ?? env.SHELL ?? "/bin/zsh", args: ["-lc", command] };
11454
+ }
11455
+ function shQuote(value) {
11456
+ return `'${value.replace(/'/g, `'\\''`)}'`;
11457
+ }
11458
+ var childExited = /* @__PURE__ */ new WeakSet();
11459
+ async function probeReady(client, timeoutMs) {
11460
+ if (!await client.health(timeoutMs)) return null;
11461
+ try {
11462
+ await client.systemConfig(timeoutMs);
11463
+ } catch {
11464
+ return null;
11465
+ }
11466
+ let hosts;
11467
+ try {
11468
+ hosts = await client.hosts(timeoutMs);
11469
+ } catch {
11470
+ return null;
11471
+ }
11472
+ return hosts.find(isHostConnected) ?? null;
11473
+ }
11474
+ async function waitReady(client, deps, args2) {
11475
+ for (; ; ) {
11476
+ const host = await probeReady(client, args2.probeTimeoutMs);
11477
+ if (host) return host;
11478
+ if (args2.child && childExited.has(args2.child)) return null;
11479
+ if (deps.now() >= args2.deadline) return null;
11480
+ await deps.sleep(args2.pollIntervalMs);
11481
+ }
11482
+ }
11483
+ function defaultIsPortInUse(port, timeoutMs = 300) {
11484
+ return new Promise((resolve3) => {
11485
+ const socket = new net2.Socket();
11486
+ const done = (inUse) => {
11487
+ socket.removeAllListeners();
11488
+ socket.destroy();
11489
+ resolve3(inUse);
11490
+ };
11491
+ socket.setTimeout(timeoutMs);
11492
+ socket.once("connect", () => done(true));
11493
+ socket.once("timeout", () => done(true));
11494
+ socket.once(
11495
+ "error",
11496
+ (err) => done(err.code !== "ECONNREFUSED")
11497
+ );
11498
+ socket.connect(port, "127.0.0.1");
11499
+ });
11500
+ }
11501
+ var owned = null;
11502
+ var adopted = null;
11503
+ var launching = null;
11504
+ var starting = null;
11505
+ function ensureBbServer(options = {}) {
11506
+ if (starting) return starting;
11507
+ starting = startBbServer(options).finally(() => {
11508
+ starting = null;
11509
+ });
11510
+ return starting;
11511
+ }
11512
+ async function startBbServer(options = {}) {
11513
+ const env = options.env ?? process.env;
11514
+ const homeDir = options.homeDir ?? os4.homedir();
11515
+ const serverPort = options.serverPort ?? bbServerPort(env);
11516
+ const daemonPort = options.daemonPort ?? bbDaemonPort(env);
11517
+ const dataDir2 = options.dataDir ?? bbDataDir(homeDir, env);
11518
+ const probeTimeoutMs = options.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
11519
+ const pollIntervalMs = options.pollIntervalMs ?? READY_POLL_INTERVAL_MS;
11520
+ const deps = {
11521
+ now: options.now ?? Date.now,
11522
+ sleep: options.sleep ?? ((ms) => new Promise((r) => {
11523
+ const t = setTimeout(r, ms);
11524
+ t.unref?.();
11525
+ })),
11526
+ log: options.log ?? (() => {
11527
+ })
11528
+ };
11529
+ if (owned && await probeReady(owned.handle.client, probeTimeoutMs)) {
11530
+ return owned.handle;
11531
+ }
11532
+ const client = new BbClient({ port: serverPort, fetchImpl: options.fetchImpl });
11533
+ if (await client.health(probeTimeoutMs)) {
11534
+ deps.log(`[bb] server already listening on :${serverPort} \u2014 adopting it`);
11535
+ const host2 = await waitReady(client, deps, {
11536
+ probeTimeoutMs,
11537
+ pollIntervalMs,
11538
+ deadline: deps.now() + (options.readyTimeoutMs ?? ADOPT_READY_TIMEOUT_MS),
11539
+ child: null
11540
+ });
11541
+ if (!host2) {
11542
+ throw new Error(
11543
+ `an agent backend is listening on :${serverPort} but never reported a connected host \u2014 nothing can be run against it`
11544
+ );
11545
+ }
11546
+ let adoptedDaemonPort = daemonPort;
11547
+ let adoptedDataDir = dataDir2;
11548
+ try {
11549
+ const config = await client.systemConfig(probeTimeoutMs);
11550
+ if (typeof config.hostDaemonPort === "number") {
11551
+ adoptedDaemonPort = config.hostDaemonPort;
11552
+ }
11553
+ if (typeof config.dataDir === "string" && config.dataDir) {
11554
+ adoptedDataDir = config.dataDir;
11555
+ }
11556
+ } catch {
11557
+ }
11558
+ adopted = {
11559
+ client,
11560
+ serverPort,
11561
+ daemonPort: adoptedDaemonPort,
11562
+ dataDir: adoptedDataDir,
11563
+ hostId: host2.id,
11564
+ spawned: false
11565
+ };
11566
+ return adopted;
11567
+ }
11568
+ assertSafeDataDir(dataDir2, homeDir);
11569
+ const portInUse = options.isPortInUse ?? defaultIsPortInUse;
11570
+ if (await portInUse(daemonPort)) {
11571
+ throw new Error(
11572
+ `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\`).`
11573
+ );
11574
+ }
11575
+ const launcher = options.launcher === void 0 ? bundledLauncher() : options.launcher;
11576
+ const command = buildLaunchCommand({
11577
+ launcher,
11578
+ dataDir: dataDir2,
11579
+ serverPort,
11580
+ daemonPort,
11581
+ version: options.bbAppVersion ?? BB_APP_VERSION
11582
+ });
11583
+ deps.log(
11584
+ launcher ? `[bb] starting the bundled agent backend (${launcher})` : `[bb] no bundled agent backend \u2014 falling back to bb-app@${options.bbAppVersion ?? BB_APP_VERSION}`
11585
+ );
11586
+ const spawnImpl = options.spawnImpl ?? ((cmd, args2, opts) => nodeSpawn(cmd, args2, opts));
11587
+ const login = loginShellInvocation(command, env, options.shell);
11588
+ const child = spawnImpl(login.shell, login.args, {
11589
+ env: {
11590
+ ...env,
11591
+ // bb's telemetry defaults ON; this is not our data to send.
11592
+ BB_TELEMETRY: "false",
11593
+ BB_DATA_DIR: dataDir2,
11594
+ BB_SERVER_BIND_HOST: "127.0.0.1",
11595
+ BB_SERVER_PORT: String(serverPort),
11596
+ BB_HOST_DAEMON_PORT: String(daemonPort)
11597
+ }
11598
+ });
11599
+ let markExited;
11600
+ const exited = new Promise((resolve3) => markExited = resolve3);
11601
+ child.once("exit", () => {
11602
+ childExited.add(child);
11603
+ markExited();
11604
+ });
11605
+ child.once("error", () => {
11606
+ childExited.add(child);
11607
+ markExited();
11608
+ });
11609
+ launching = { child, exited };
11610
+ let stderr = "";
11611
+ child.stderr?.on("data", (chunk) => {
11612
+ stderr = (stderr + String(chunk)).slice(-2e3);
11613
+ });
11614
+ child.stdout?.on("data", () => {
11615
+ });
11616
+ const host = await waitReady(client, deps, {
11617
+ probeTimeoutMs,
11618
+ pollIntervalMs,
11619
+ deadline: deps.now() + (options.readyTimeoutMs ?? SPAWN_READY_TIMEOUT_MS),
11620
+ child
11621
+ });
11622
+ if (launching?.child === child) launching = null;
11623
+ if (!host) {
11624
+ try {
11625
+ child.kill("SIGTERM");
11626
+ } catch {
11627
+ }
11628
+ const tail = stderr.trim() ? `
11629
+ ${stderr.trim()}` : "";
11630
+ throw new Error(
11631
+ `The agent backend did not become ready on :${serverPort} (data dir ${dataDir2})${tail}`
11632
+ );
11633
+ }
11634
+ const handle = {
11635
+ client,
11636
+ serverPort,
11637
+ daemonPort,
11638
+ dataDir: dataDir2,
11639
+ hostId: host.id,
11640
+ spawned: true
11641
+ };
11642
+ if (owned && owned.child !== child) {
11643
+ try {
11644
+ owned.child.kill("SIGTERM");
11645
+ } catch {
11646
+ }
11647
+ }
11648
+ owned = { child, handle, exited };
11649
+ adopted = null;
11650
+ deps.log(`[bb] server ready on :${serverPort} (host ${host.id})`);
11651
+ return handle;
11652
+ }
11653
+ var STOP_GRACE_MS = 13e3;
11654
+ async function stopBbServer(options = {}) {
11655
+ adopted = null;
11656
+ const targets = [owned, launching].filter((t) => t !== null);
11657
+ owned = null;
11658
+ launching = null;
11659
+ await Promise.all(targets.map((t) => stopLauncher(t.child, t.exited, options)));
11660
+ }
11661
+ async function stopLauncher(child, exited, options) {
11662
+ if (childExited.has(child)) return;
11663
+ try {
11664
+ child.kill("SIGTERM");
11665
+ } catch {
11666
+ }
11667
+ let timer;
11668
+ const stopped = await Promise.race([
11669
+ exited.then(() => true),
11670
+ new Promise((resolve3) => {
11671
+ timer = setTimeout(() => resolve3(false), options.graceMs ?? STOP_GRACE_MS);
11672
+ })
11673
+ ]);
11674
+ clearTimeout(timer);
11675
+ if (stopped || child.pid === void 0) return;
11676
+ (options.killTree ?? ((pid) => void killProcessTree(pid)))(child.pid);
11677
+ }
11678
+
11679
+ // ../editor/electron/bb/canvas-tools.ts
11680
+ import fs16 from "node:fs";
11681
+ import path14 from "node:path";
11682
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
11683
+
11684
+ // ../editor/electron/bb/antigravity.ts
11685
+ import { execFile as execFile2 } from "node:child_process";
11686
+ import fs15 from "node:fs";
11687
+ import os5 from "node:os";
11688
+ import path13 from "node:path";
11689
+ import { fileURLToPath } from "node:url";
11690
+
11691
+ // ../editor/electron/bb/antigravity-ids.ts
11692
+ var ANTIGRAVITY_AGENT_ID = "antigravity";
11693
+ var ANTIGRAVITY_PROVIDER_ID = `acp-${ANTIGRAVITY_AGENT_ID}`;
11694
+ 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>';
11695
+ var ANTIGRAVITY_LOGO_URL = `data:image/svg+xml,${encodeURIComponent(ANTIGRAVITY_LOGO_SVG)}`;
11696
+ var ANTIGRAVITY_TINT = { light: "#3186FF", dark: "#3186FF" };
11697
+
11698
+ // ../editor/electron/bb/antigravity.ts
11699
+ var PROVIDER_ACP_PLUGIN_ID = "provider-acp";
11700
+ function antigravityAgentEntry(paths) {
11701
+ const dirs = [.../* @__PURE__ */ new Set([path13.dirname(paths.node), path13.dirname(paths.agy), path13.dirname(paths.adapter), "/usr/bin", "/bin"])];
11702
+ return {
11703
+ id: ANTIGRAVITY_AGENT_ID,
11704
+ displayName: "Antigravity",
11705
+ command: paths.node,
11706
+ args: [paths.shim, paths.adapter],
11707
+ env: {
11708
+ PATH: dirs.join(":"),
11709
+ // Without it agy-acp looks for its own agy, and downloads one when it
11710
+ // finds none — not the person's, signed-in copy.
11711
+ AGY_BIN: paths.agy,
11712
+ AGY_SKIP_DOWNLOAD: "1",
11713
+ CANVAS_ANTIGRAVITY_MCP_ROOT: paths.mcpRoot
11714
+ },
11715
+ nativeSkillRoots: {
11716
+ user: [{ path: ".gemini/config/skills", recursive: true }, { path: ".agents/skills", recursive: true }],
11717
+ project: [{ path: ".agents/skills", recursive: true, ancestors: true }]
11718
+ }
11719
+ };
11720
+ }
11721
+ function readCustomAgents(value) {
11722
+ if (Array.isArray(value)) return [...value];
11723
+ if (typeof value !== "string" || !value.trim()) return [];
11724
+ const parsed = JSON.parse(value);
11725
+ if (!Array.isArray(parsed)) throw new Error("bb's custom ACP agents setting is not a list");
11726
+ return parsed;
11727
+ }
11728
+ var idOf = (entry) => entry && typeof entry === "object" ? entry.id : void 0;
11729
+ function withAntigravity(list, entry) {
11730
+ const at = list.findIndex((item) => idOf(item) === ANTIGRAVITY_AGENT_ID);
11731
+ if (!entry) return at === -1 ? null : list.filter((_, index) => index !== at);
11732
+ if (at !== -1 && JSON.stringify(list[at]) === JSON.stringify(entry)) return null;
11733
+ const next = [...list];
11734
+ if (at === -1) next.push(entry);
11735
+ else next[at] = entry;
11736
+ return next;
11737
+ }
11738
+ function antigravityShimPath(resourcesPath = process.resourcesPath, dirname5 = path13.dirname(fileURLToPath(import.meta.url))) {
11739
+ const shim = (dir2) => path13.join(dir2, "antigravity", "acp-shim.mjs");
11740
+ if (resourcesPath && fs15.existsSync(shim(resourcesPath))) return shim(resourcesPath);
11741
+ let dir = path13.resolve(dirname5);
11742
+ for (let up = 0; up < 6; up++) {
11743
+ if (fs15.existsSync(shim(path13.join(dir, "resources")))) return shim(path13.join(dir, "resources"));
11744
+ const parent = path13.dirname(dir);
11745
+ if (parent === dir) break;
11746
+ dir = parent;
11747
+ }
11748
+ return null;
11749
+ }
11750
+ function antigravityBinaries(home = os5.homedir()) {
11751
+ const bin = path13.join(home, ".local", "bin");
11752
+ return { agy: path13.join(bin, "agy"), adapter: path13.join(bin, "agy-acp") };
11753
+ }
11754
+ function executable(file) {
11755
+ try {
11756
+ fs15.accessSync(file, fs15.constants.X_OK);
11757
+ return fs15.statSync(file).isFile();
11758
+ } catch {
11759
+ return false;
11760
+ }
11761
+ }
11762
+ async function ensureAntigravityAgent(client, deps) {
11763
+ const log = deps.log ?? ((msg) => console.log(msg));
11764
+ const isExecutable = deps.isExecutable ?? executable;
11765
+ const { agy, adapter } = (deps.binaries ?? antigravityBinaries)();
11766
+ const shim = (deps.shimPath ?? antigravityShimPath)();
11767
+ let entry = null;
11768
+ if (shim && isExecutable(agy) && isExecutable(adapter)) {
11769
+ const node2 = await deps.resolveNode();
11770
+ if (node2) {
11771
+ entry = antigravityAgentEntry({
11772
+ node: node2,
11773
+ shim,
11774
+ adapter,
11775
+ agy,
11776
+ mcpRoot: deps.mcpRoot ?? path13.join(os5.homedir(), ".canvas", "antigravity-mcp")
11777
+ });
11778
+ } else {
11779
+ log("[bb] Antigravity not registered: no node on the login shell's PATH");
11780
+ }
11781
+ }
11782
+ try {
11783
+ const values = await client.pluginSettings(PROVIDER_ACP_PLUGIN_ID);
11784
+ const next = withAntigravity(readCustomAgents(values.customAgents), entry);
11785
+ if (!next) return entry ? "unchanged" : "unavailable";
11786
+ await client.putPluginSettings(PROVIDER_ACP_PLUGIN_ID, {
11787
+ customAgents: next.length === 0 ? "" : JSON.stringify(next)
11788
+ });
11789
+ log(entry ? `[bb] Antigravity registered (${adapter})` : "[bb] Antigravity removed: agy or agy-acp is gone");
11790
+ return entry ? "registered" : "removed";
11791
+ } catch (err) {
11792
+ log(`[bb] Antigravity not registered: ${err instanceof Error ? err.message : String(err)}`);
11793
+ return "unavailable";
11794
+ }
11795
+ }
11796
+ var node = null;
11797
+ function loginShellNode() {
11798
+ node ??= new Promise((resolve3) => {
11799
+ const login = loginShellInvocation('printf "node=%s\\n" "$(command -v node)"');
11800
+ execFile2(login.shell, login.args, { timeout: 1e4 }, (_error, stdout) => {
11801
+ const line = String(stdout ?? "").split(/\r?\n/).reverse().find((row2) => row2.startsWith("node="));
11802
+ const found = line?.slice(5).trim();
11803
+ resolve3(found && path13.isAbsolute(found) ? found : null);
11804
+ });
11805
+ }).then((found) => {
11806
+ if (!found) node = null;
11807
+ return found;
11808
+ });
11809
+ return node;
11810
+ }
11811
+ function registerAntigravity(client) {
11812
+ return ensureAntigravityAgent(client, { resolveNode: loginShellNode });
11813
+ }
11814
+
11815
+ // ../editor/electron/bb/canvas-tools.ts
11816
+ var PLUGIN_ID = "canvas-editor";
11817
+ var pending2 = null;
11818
+ var server = null;
11819
+ var lastError = null;
11820
+ function pluginSourceDir(resourcesPath = process.resourcesPath, dirname5 = path14.dirname(fileURLToPath2(import.meta.url))) {
11821
+ const has = (dir2) => fs16.existsSync(path14.join(dir2, "package.json"));
11822
+ if (resourcesPath) {
11823
+ const packaged = path14.join(resourcesPath, "bb-plugin");
11824
+ if (has(packaged)) return packaged;
11825
+ }
11826
+ let dir = path14.resolve(dirname5);
11827
+ for (let up = 0; up < 6; up++) {
11828
+ const candidate = path14.join(dir, "resources", "bb-plugin");
11829
+ if (has(candidate)) return candidate;
11830
+ const parent = path14.dirname(dir);
11831
+ if (parent === dir) break;
11832
+ dir = parent;
11833
+ }
11834
+ return null;
11835
+ }
11836
+ function sameDir(a, b) {
11837
+ const norm = (dir) => {
11838
+ const resolved = path14.resolve(dir);
11839
+ try {
11840
+ return fs16.realpathSync(resolved);
11841
+ } catch {
11842
+ return resolved;
11843
+ }
11844
+ };
11845
+ return norm(a) === norm(b);
11846
+ }
11847
+ async function evictShadowingPlugins(client, dir, log) {
11848
+ let records;
11849
+ try {
11850
+ records = await client.listPlugins();
11851
+ } catch (err) {
11852
+ log(`[bb] plugin list unavailable, not checking for a shadowing record: ${String(err)}`);
11853
+ return;
11854
+ }
11855
+ for (const record of records) {
11856
+ if (record.provenance === "builtin") continue;
11857
+ const ours = record.id === PLUGIN_ID;
11858
+ const here2 = record.rootDir !== "" && sameDir(record.rootDir, dir);
11859
+ if (ours === here2) continue;
11860
+ 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`;
11861
+ try {
11862
+ await client.removePlugin(record.id);
11863
+ log(`[bb] removed plugin record "${record.id}": ${why}`);
11864
+ } catch (err) {
11865
+ log(`[bb] could not remove plugin record "${record.id}" (${why}): ${String(err)}`);
11866
+ }
11867
+ }
11868
+ }
11869
+ async function configure(client, handle) {
11870
+ await client.pluginHttp(PLUGIN_ID, "config", {
11871
+ url: handle.url,
11872
+ token: handle.token
11873
+ });
11874
+ }
11875
+ function ensureCanvasTools(send2, deps = {}) {
11876
+ if (pending2) return pending2;
11877
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
11878
+ const startServer = deps.startServer ?? startToolServer;
11879
+ const sourceDir = deps.sourceDir ?? (() => pluginSourceDir());
11880
+ const log = deps.log ?? ((m) => console.log(m));
11881
+ pending2 = (async () => {
11882
+ try {
11883
+ const dir = sourceDir();
11884
+ if (!dir) {
11885
+ lastError = "resources/bb-plugin not found";
11886
+ log(`[bb] layer tools unavailable: ${lastError}`);
11887
+ return null;
11888
+ }
11889
+ const { client } = await ensureServer();
11890
+ server ??= await startServer({ send: send2 });
11891
+ await evictShadowingPlugins(client, dir, log);
11892
+ try {
11893
+ await client.installPlugin(`path:${dir}`);
11894
+ } catch (err) {
11895
+ log(
11896
+ `[bb] plugin install skipped (likely already installed): ${String(err)}`
11897
+ );
11898
+ }
11899
+ try {
11900
+ await client.reloadPlugin(PLUGIN_ID);
11901
+ } catch (err) {
11902
+ log(`[bb] plugin reload skipped: ${String(err)}`);
11903
+ }
11904
+ await configure(client, server);
11905
+ await registerAntigravity(client);
11906
+ lastError = null;
11907
+ log(`[bb] layer tools ready on ${server.url}`);
11908
+ return server;
11909
+ } catch (err) {
11910
+ lastError = err instanceof Error ? err.message : String(err);
11911
+ log(`[bb] layer tools unavailable: ${lastError}`);
11912
+ pending2 = null;
11913
+ return null;
11914
+ }
11915
+ })();
11916
+ return pending2;
11917
+ }
11918
+ async function waitForCanvasTools(send2, options = {}) {
11919
+ const attempts = Math.max(1, options.attempts ?? 3);
11920
+ const attemptTimeoutMs = options.attemptTimeoutMs ?? 3e4;
11921
+ const backoffMs = options.backoffMs ?? [1e3, 3e3];
11922
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
11923
+ let reason = null;
11924
+ for (let attempt = 0; attempt < attempts; attempt++) {
11925
+ if (attempt > 0) {
11926
+ await sleep(backoffMs[Math.min(attempt - 1, backoffMs.length - 1)] ?? 0);
11927
+ }
11928
+ const trying = ensureCanvasTools(send2, options);
11929
+ let timer;
11930
+ const timedOut = new Promise((resolve3) => {
11931
+ timer = setTimeout(() => resolve3("timeout"), attemptTimeoutMs);
11932
+ });
11933
+ try {
11934
+ const outcome = await Promise.race([trying, timedOut]);
11935
+ if (outcome === "timeout") {
11936
+ if (pending2 === trying) pending2 = null;
11937
+ reason = `loading them took longer than ${Math.round(attemptTimeoutMs / 1e3)}s`;
11938
+ continue;
11939
+ }
11940
+ if (outcome) return outcome;
11941
+ reason = lastError;
11942
+ } catch (err) {
11943
+ if (pending2 === trying) pending2 = null;
11944
+ reason = err instanceof Error ? err.message : String(err);
11945
+ } finally {
11946
+ clearTimeout(timer);
11947
+ }
11948
+ }
11949
+ throw new Error(canvasToolsUnavailableMessage(reason));
11950
+ }
11951
+ function canvasToolsUnavailableMessage(reason) {
11952
+ 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.";
11953
+ }
11954
+ async function setAgentInstructions(send2, args2, deps = {}) {
11955
+ const log = deps.log ?? ((m) => console.log(m));
11956
+ try {
11957
+ return await pushToPlugin(
11958
+ send2,
11959
+ "instructions",
11960
+ {
11961
+ projectId: args2.projectId,
11962
+ instructions: args2.instructions,
11963
+ ...args2.path ? { path: args2.path } : {}
11964
+ },
11965
+ deps
11966
+ );
11967
+ } catch (err) {
11968
+ log(`[bb] instructions not delivered: ${String(err)}`);
11969
+ return false;
11970
+ }
11971
+ }
11972
+ async function pushToPlugin(send2, route, body, deps = {}) {
11973
+ const ready = await ensureCanvasTools(send2, deps);
11974
+ if (!ready) return false;
11975
+ const client = deps.client ?? (await (deps.ensureServer ?? ensureBbServer)()).client;
11976
+ await client.pluginHttp(PLUGIN_ID, route, {
11977
+ ...body,
11978
+ url: ready.url,
11979
+ token: ready.token
11980
+ });
11981
+ return true;
11982
+ }
11983
+ async function setCanvasSelection(send2, args2, deps = {}) {
11984
+ try {
11985
+ return await pushToPlugin(
11986
+ send2,
11987
+ "selection",
11988
+ {
11989
+ projectId: args2.projectId,
11990
+ layers: args2.layers,
11991
+ ...args2.path ? { path: args2.path } : {}
11992
+ },
11993
+ deps
11994
+ );
11995
+ } catch {
11996
+ return false;
11997
+ }
11998
+ }
11999
+ async function canvasToolsConnection(send2, args2 = {}, deps = {}) {
12000
+ try {
12001
+ const ready = await ensureCanvasTools(send2, deps);
12002
+ if (!ready) return { connected: false, projects: [] };
12003
+ const client = deps.client ?? (await (deps.ensureServer ?? ensureBbServer)()).client;
12004
+ const answer = await client.pluginHttp(PLUGIN_ID, "state", {
12005
+ url: ready.url,
12006
+ token: ready.token,
12007
+ // Only what we actually have: a project id with no picture behind it
12008
+ // would clear what the plugin is holding rather than restore it.
12009
+ ...args2.projectId ? { projectId: args2.projectId } : {},
12010
+ ...args2.projectId && args2.instructions !== void 0 ? { instructions: args2.instructions } : {},
12011
+ ...args2.projectId && args2.layers !== void 0 ? { layers: args2.layers } : {},
12012
+ ...args2.projectId && args2.path ? { path: args2.path } : {}
12013
+ });
12014
+ const projects = Array.isArray(answer?.projects) ? answer.projects.flatMap((entry) => {
12015
+ const row2 = entry ?? {};
12016
+ return typeof row2.projectId === "string" ? [
12017
+ {
12018
+ projectId: row2.projectId,
12019
+ instructions: typeof row2.instructions === "number" ? row2.instructions : 0,
12020
+ selection: typeof row2.selection === "number" ? row2.selection : 0
12021
+ }
12022
+ ] : [];
12023
+ }) : [];
12024
+ return { connected: answer?.connected === true, projects };
12025
+ } catch {
12026
+ return { connected: false, projects: [] };
12027
+ }
12028
+ }
12029
+ async function stopCanvasTools() {
12030
+ pending2 = null;
12031
+ lastError = null;
12032
+ const current = server;
12033
+ server = null;
12034
+ if (current) await current.stop();
12035
+ }
12036
+ function canvasToolsStatus() {
12037
+ return { connected: server !== null, url: server?.url ?? null, error: lastError };
12038
+ }
12039
+
12040
+ // ../editor/electron/bb/project-map.ts
12041
+ import path15 from "node:path";
12042
+ function normalizeProjectRoot(root) {
12043
+ const trimmed = root.trim();
12044
+ if (!trimmed) return "";
12045
+ const normalized = path15.normalize(trimmed);
12046
+ return normalized === path15.sep ? normalized : normalized.replace(/[/\\]+$/u, "");
12047
+ }
12048
+ async function pickConnectedHostId(client) {
12049
+ const hosts = await client.hosts();
12050
+ const connected = hosts.find(isHostConnected);
12051
+ if (connected) return connected.id;
12052
+ throw new Error(noConnectedHostMessage(hosts));
12053
+ }
12054
+ function noConnectedHostMessage(hosts) {
12055
+ const seen = hosts.length ? `Hosts it reported: ${hosts.map((host) => `${host.name} (${host.status})`).join(", ")}.` : "It reported no hosts at all.";
12056
+ 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.`;
12057
+ }
12058
+ function findProjectForRoot(projects, root, hostId) {
12059
+ const atRoot = (project, onHost) => project.sources.some(
12060
+ (source) => normalizeProjectRoot(source.path) === root && (onHost === void 0 || source.hostId === onHost)
12061
+ );
12062
+ const matches = projects.filter((project) => atRoot(project));
12063
+ return matches.find((project) => atRoot(project, hostId)) ?? matches[0];
12064
+ }
12065
+ function createProjectMap(client) {
12066
+ const pending3 = /* @__PURE__ */ new Map();
12067
+ async function resolveUncached(root) {
12068
+ const hostId = await pickConnectedHostId(client);
12069
+ const existing = findProjectForRoot(await client.projects(), root, hostId);
12070
+ const projectId = existing ? existing.id : (await client.createProject({ name: path15.basename(root), root, hostId })).id;
12071
+ return {
12072
+ projectId,
12073
+ hostId,
12074
+ /**
12075
+ * ALWAYS `unmanaged`, at the real path. bb's default workspace is a
12076
+ * managed git worktree — a checkout inside bb's own data dir — so the
12077
+ * assistant would be reading and writing a directory that is not the one
12078
+ * the editor has open, and every change it made would appear to do
12079
+ * nothing. `hostId` is required alongside it.
12080
+ */
12081
+ environment: {
12082
+ type: "host",
12083
+ hostId,
12084
+ workspace: { type: "unmanaged", path: root }
12085
+ }
12086
+ };
12087
+ }
12088
+ return {
12089
+ resolve(cwd) {
12090
+ const root = normalizeProjectRoot(cwd);
12091
+ const cached = pending3.get(root);
12092
+ if (cached) return cached;
12093
+ const attempt = resolveUncached(root).catch((err) => {
12094
+ pending3.delete(root);
12095
+ throw err;
12096
+ });
12097
+ pending3.set(root, attempt);
12098
+ return attempt;
12099
+ },
12100
+ clear() {
12101
+ pending3.clear();
12102
+ }
12103
+ };
12104
+ }
12105
+
12106
+ // ../editor/electron/bb/thread-step.ts
12107
+ var CANVAS_TOOL_STEPS = {
12108
+ listLayers: "Reading the layers",
12109
+ getLayer: "Reading a layer",
12110
+ getSelection: "Reading the selection",
12111
+ listAnnotations: "Reading the comments",
12112
+ resolveAnnotation: "Resolving a comment",
12113
+ createLayer: "Adding a layer",
12114
+ insertHtml: "Adding layers",
12115
+ updateLayer: "Editing a layer",
12116
+ replaceLayer: "Rebuilding a layer",
12117
+ removeLayer: "Removing a layer",
12118
+ moveLayer: "Moving a layer",
12119
+ duplicateLayers: "Duplicating layers",
12120
+ groupLayers: "Grouping layers",
12121
+ scaleLayers: "Scaling layers",
12122
+ setLayerFont: "Setting a font",
12123
+ selectLayers: "Selecting layers",
12124
+ holdLayer: "Holding a layer",
12125
+ releaseLayer: "Letting a layer go",
12126
+ askAboutLayer: "Asking about a layer",
12127
+ setPageView: "Changing the view",
12128
+ canvasPointer: "Using the page",
12129
+ canvasHistory: "Undoing a change",
12130
+ listElementTypes: "Reading the elements",
12131
+ getCanvasScene: "Reading a 3D scene",
12132
+ setCanvasScene: "Building a 3D scene",
12133
+ getVector: "Reading a drawing",
12134
+ drawVector: "Drawing with the pen",
12135
+ exportHtml: "Reading the markup",
12136
+ listVariables: "Reading the variables",
12137
+ setVariable: "Setting a variable",
12138
+ deleteVariable: "Removing a variable",
12139
+ canvasReadPage: "Reading the page",
12140
+ observePage: "Watching the page",
12141
+ observeVideo: "Watching a video",
12142
+ setAppBrief: "Writing the app brief",
12143
+ canvasScreenshot: "Looking at the board"
12144
+ };
12145
+ var MAX = 60;
12146
+ function clip(text3) {
12147
+ const one = text3.replace(/\s+/g, " ").trim();
12148
+ return one.length > MAX ? `${one.slice(0, MAX - 1)}\u2026` : one;
12149
+ }
12150
+ function baseName(path21) {
12151
+ if (typeof path21 !== "string" || !path21) return null;
12152
+ return path21.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || path21;
12153
+ }
12154
+ function humanize(name) {
12155
+ const words = name.replace(/^mcp__[^_]+__/, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().toLowerCase();
12156
+ return words ? words[0].toUpperCase() + words.slice(1) : "Using a tool";
12157
+ }
12158
+ function describeItem(item) {
12159
+ const presentation = item.presentation;
12160
+ if (presentation && presentation.suppress !== true && typeof presentation.title === "string" && presentation.title.trim()) {
12161
+ return clip(presentation.title);
12162
+ }
12163
+ switch (item.type) {
12164
+ case "toolCall": {
12165
+ const tool = typeof item.tool === "string" ? item.tool : "";
12166
+ const bare = tool.replace(/^mcp__[^_]+__/, "");
12167
+ return CANVAS_TOOL_STEPS[bare] ?? clip(humanize(tool));
12168
+ }
12169
+ case "commandExecution":
12170
+ return typeof item.command === "string" && item.command.trim() ? clip(`Running ${item.command}`) : "Running a command";
12171
+ case "fileChange": {
12172
+ const changes = Array.isArray(item.changes) ? item.changes : [];
12173
+ const first = baseName(changes[0]?.path);
12174
+ if (!first) return "Editing files";
12175
+ return changes.length > 1 ? clip(`Editing ${first} and ${changes.length - 1} more`) : clip(`Editing ${first}`);
12176
+ }
12177
+ case "fileRead": {
12178
+ const name = baseName(item.path);
12179
+ return name ? clip(`Reading ${name}`) : "Reading a file";
12180
+ }
12181
+ case "search":
12182
+ return "Searching the project";
12183
+ case "webSearch": {
12184
+ const queries = Array.isArray(item.queries) ? item.queries : [];
12185
+ return typeof queries[0] === "string" ? clip(`Searching the web for ${queries[0]}`) : "Searching the web";
12186
+ }
12187
+ case "webFetch": {
12188
+ try {
12189
+ return clip(`Reading ${new URL(String(item.url)).host}`);
12190
+ } catch {
12191
+ return "Reading a page";
12192
+ }
12193
+ }
12194
+ case "imageView":
12195
+ return "Looking at an image";
12196
+ case "reasoning":
12197
+ return "Thinking";
12198
+ case "plan":
12199
+ case "planSteps":
12200
+ return "Planning";
12201
+ case "agentMessage":
12202
+ return "Writing a reply";
12203
+ case "delegation":
12204
+ case "backgroundTask":
12205
+ return "Working in the background";
12206
+ case "contextCompaction":
12207
+ return "Tidying its notes";
12208
+ default:
12209
+ return null;
12210
+ }
12211
+ }
12212
+ function describeStep(eventsNewestFirst) {
12213
+ for (const event of eventsNewestFirst) {
12214
+ if (event.type !== "item/started" && event.type !== "item/completed") continue;
12215
+ const item = event.data.item;
12216
+ if (!item || typeof item !== "object") continue;
12217
+ const step = describeItem(item);
12218
+ if (step) return step;
12219
+ }
12220
+ return null;
12221
+ }
12222
+
12223
+ // ../editor/electron/bb/model-choice.ts
12224
+ import * as fs17 from "node:fs";
12225
+ import * as path16 from "node:path";
12226
+ var REASONING_LEVELS = [
12227
+ "none",
12228
+ "low",
12229
+ "medium",
12230
+ "high",
12231
+ "xhigh",
12232
+ "ultracode",
12233
+ "max",
12234
+ "ultra"
12235
+ ];
12236
+ function parseModelChoice(value) {
12237
+ if (typeof value !== "object" || value === null) return null;
12238
+ const entry = value;
12239
+ const providerId = typeof entry.providerId === "string" ? entry.providerId.trim() : "";
12240
+ if (!providerId) return null;
12241
+ const choice = { providerId };
12242
+ const model = typeof entry.model === "string" ? entry.model.trim() : "";
12243
+ if (model) choice.model = model;
12244
+ const reasoning = entry.reasoningLevel;
12245
+ if (typeof reasoning === "string" && REASONING_LEVELS.includes(reasoning)) {
12246
+ choice.reasoningLevel = reasoning;
12247
+ }
12248
+ return choice;
12249
+ }
12250
+ function createModelChoiceStore(deps) {
12251
+ const take = async () => {
12252
+ let reading;
12253
+ try {
12254
+ reading = await deps.read();
12255
+ } catch {
12256
+ reading = "unreachable";
12257
+ }
12258
+ if (reading !== "unreachable") writeMirror(deps.file, reading);
12259
+ return reading;
12260
+ };
12261
+ return {
12262
+ async current() {
12263
+ const reading = await take();
12264
+ return reading === "unreachable" ? readMirror(deps.file) : reading;
12265
+ },
12266
+ async refresh() {
12267
+ await take();
12268
+ }
12269
+ };
12270
+ }
12271
+ function readMirror(file) {
12272
+ try {
12273
+ return parseModelChoice(JSON.parse(fs17.readFileSync(file, "utf-8")));
12274
+ } catch {
12275
+ return null;
12276
+ }
12277
+ }
12278
+ function writeMirror(file, choice) {
12279
+ try {
12280
+ fs17.mkdirSync(path16.dirname(file), { recursive: true });
12281
+ fs17.writeFileSync(file, JSON.stringify(choice ?? {}, null, 2));
12282
+ } catch {
12283
+ }
12284
+ }
12285
+
12286
+ // ../editor/electron/bb/compose-options.ts
12287
+ function asRecord2(value) {
12288
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
12289
+ }
12290
+ var PERMISSION_ORDER = ["accept-edits", "auto", "full"];
12291
+ function parseExecutionOptions(value, providerId) {
12292
+ const rec = asRecord2(value) ?? {};
12293
+ const models = (Array.isArray(rec.models) ? rec.models : []).flatMap((entry) => {
12294
+ const m = asRecord2(entry);
12295
+ if (!m || typeof m.model !== "string" || !m.model) return [];
12296
+ const levels = (Array.isArray(m.supportedReasoningEfforts) ? m.supportedReasoningEfforts : []).flatMap((level) => {
12297
+ const l = asRecord2(level);
12298
+ return l && typeof l.reasoningEffort === "string" ? [{ id: l.reasoningEffort, description: typeof l.description === "string" ? l.description : "" }] : [];
12299
+ });
12300
+ return [
12301
+ {
12302
+ model: m.model,
12303
+ displayName: typeof m.displayName === "string" && m.displayName ? m.displayName : m.model,
12304
+ description: typeof m.description === "string" ? m.description : "",
12305
+ reasoningLevels: levels,
12306
+ defaultReasoning: typeof m.defaultReasoningEffort === "string" ? m.defaultReasoningEffort : null,
12307
+ isDefault: m.isDefault === true
12308
+ }
12309
+ ];
12310
+ });
12311
+ const provider = (Array.isArray(rec.providers) ? rec.providers : []).map(asRecord2).find((p) => p?.id === providerId);
12312
+ const supported = asRecord2(provider?.capabilities)?.permissionModes?.filter(isBbPermissionMode);
12313
+ const ceiling = isBbPermissionMode(rec.permissionCeiling) ? rec.permissionCeiling : "full";
12314
+ const permissionModes = PERMISSION_ORDER.filter(
12315
+ (mode) => (supported && supported.length > 0 ? supported.includes(mode) : true) && PERMISSION_ORDER.indexOf(mode) <= PERMISSION_ORDER.indexOf(ceiling)
12316
+ );
12317
+ const commandFor = (kind) => {
12318
+ for (const action of Array.isArray(provider?.composerActions) ? provider.composerActions : []) {
12319
+ const a = asRecord2(action);
12320
+ const command = asRecord2(a?.command);
12321
+ if (a?.kind === kind && command && typeof command.trigger === "string" && typeof command.name === "string") {
12322
+ const trailing = typeof command.trailingText === "string" && command.trailingText ? command.trailingText : " ";
12323
+ return `${command.trigger}${command.name}${trailing}`;
12324
+ }
12325
+ }
12326
+ return null;
12327
+ };
12328
+ const reasoningLabels = {};
12329
+ for (const level of Array.isArray(provider?.reasoningLevels) ? provider.reasoningLevels : []) {
12330
+ const l = asRecord2(level);
12331
+ if (l && typeof l.id === "string" && typeof l.label === "string") reasoningLabels[l.id] = l.label;
12332
+ }
12333
+ const loadError = asRecord2(rec.modelLoadError);
12334
+ return {
12335
+ providerId,
12336
+ models,
12337
+ permissionModes,
12338
+ planCommand: commandFor("plan"),
12339
+ goalCommand: commandFor("goal"),
12340
+ reasoningLabels,
12341
+ modelLoadError: loadError && typeof loadError.code === "string" ? loadError.code : null
12342
+ };
12343
+ }
12344
+ function parseBranches(value) {
12345
+ const rec = asRecord2(value) ?? {};
12346
+ const checkout = asRecord2(rec.checkout);
12347
+ const current = checkout?.kind === "branch" && typeof checkout.branchName === "string" ? checkout.branchName : null;
12348
+ const branches = (Array.isArray(rec.branches) ? rec.branches : []).filter(
12349
+ (name) => typeof name === "string" && name.length > 0
12350
+ );
12351
+ return {
12352
+ current,
12353
+ branches: current && !branches.includes(current) ? [current, ...branches] : branches,
12354
+ defaultBranch: typeof rec.defaultBranch === "string" ? rec.defaultBranch : null,
12355
+ dirty: rec.hasUncommittedChanges === true
12356
+ };
12357
+ }
12358
+ function branchName(value) {
12359
+ if (typeof value !== "string") return void 0;
12360
+ const name = value.trim();
12361
+ if (!name || name.length > 200 || name.startsWith("-") || name.includes("..")) return void 0;
12362
+ if (/[\s~^:?*[\\]/.test(name) || [...name].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127)) {
12363
+ return void 0;
12364
+ }
12365
+ return name;
12366
+ }
12367
+ function parseComposeWorkspace(value) {
12368
+ const rec = asRecord2(value);
12369
+ if (!rec) return null;
12370
+ if (rec.kind === "local") {
12371
+ const branch = branchName(rec.branch);
12372
+ return branch ? { kind: "local", branch } : { kind: "local" };
12373
+ }
12374
+ if (rec.kind === "worktree") {
12375
+ const baseBranch = branchName(rec.baseBranch);
12376
+ return baseBranch ? { kind: "worktree", baseBranch } : { kind: "worktree" };
12377
+ }
12378
+ return null;
12379
+ }
12380
+ function environmentFor(base, workspace) {
12381
+ if (!workspace) return base;
12382
+ if (workspace.kind === "worktree") {
12383
+ return {
12384
+ type: "host",
12385
+ hostId: base.hostId,
12386
+ workspace: {
12387
+ type: "managed-worktree",
12388
+ baseBranch: workspace.baseBranch ? { kind: "named", name: workspace.baseBranch } : { kind: "default" }
12389
+ }
12390
+ };
12391
+ }
12392
+ if (!workspace.branch || base.workspace.type !== "unmanaged") return base;
12393
+ return { ...base, workspace: { ...base.workspace, branch: { kind: "existing", name: workspace.branch } } };
12394
+ }
12395
+ function parseComposeExecution(value) {
12396
+ return parseModelChoice(value);
12397
+ }
12398
+
12399
+ // ../editor/electron/bb/workspace-url.ts
12400
+ var mapFor = null;
12401
+ var maps = /* @__PURE__ */ new WeakMap();
12402
+ function projectMap(client) {
12403
+ if (mapFor) return mapFor(client);
12404
+ let map = maps.get(client);
12405
+ if (!map) {
12406
+ map = createProjectMap(client);
12407
+ maps.set(client, map);
12408
+ }
12409
+ return map;
12410
+ }
12411
+ async function bbProjectFor(cwd, deps = {}) {
12412
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12413
+ const server2 = await ensureServer();
12414
+ return projectMap(server2.client).resolve(cwd);
12415
+ }
12416
+ async function bbWorkspaceUrl(cwd, deps = {}, preferThreadId, home = false) {
12417
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12418
+ const server2 = await ensureServer();
12419
+ const target = await projectMap(server2.client).resolve(cwd);
12420
+ const threads = home || !preferThreadId ? [] : await server2.client.listThreads(target.projectId).catch(() => []);
12421
+ const preferred = preferThreadId ? threads.find((thread) => thread.id === preferThreadId) : void 0;
12422
+ if (!preferred) {
12423
+ return {
12424
+ url: `${server2.client.origin}/projects/${encodeURIComponent(target.projectId)}`,
12425
+ origin: server2.client.origin,
12426
+ projectId: target.projectId
12427
+ };
12428
+ }
12429
+ return {
12430
+ url: threadUrl(server2.client.origin, target.projectId, preferred.id),
12431
+ origin: server2.client.origin,
12432
+ projectId: target.projectId,
12433
+ threadId: preferred.id
12434
+ };
12435
+ }
12436
+ function threadUrl(origin, projectId, threadId) {
12437
+ return `${origin}/projects/${encodeURIComponent(projectId)}/threads/${encodeURIComponent(threadId)}`;
12438
+ }
12439
+ function latestThread(threads) {
12440
+ let best;
12441
+ for (const thread of threads) {
12442
+ if (!best || (thread.createdAt ?? 0) >= (best.createdAt ?? 0)) best = thread;
12443
+ }
12444
+ return best;
12445
+ }
12446
+ async function sendToWorkspace(cwd, text3, deps = {}) {
12447
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12448
+ const server2 = await ensureServer();
12449
+ await deps.ensureTools?.();
12450
+ const target = await projectMap(server2.client).resolve(cwd);
12451
+ const threads = await server2.client.listThreads(target.projectId).catch(() => []);
12452
+ const latest = latestThread(threads);
12453
+ if (latest) {
12454
+ await server2.client.sendMessage(latest.id, [{ type: "text", text: text3 }]);
12455
+ return { threadId: latest.id, created: false };
12456
+ }
12457
+ const thread = await createThreadAsPicked(server2.client, deps, {
12458
+ projectId: target.projectId,
12459
+ input: [{ type: "text", text: text3 }],
12460
+ environment: target.environment
12461
+ });
12462
+ return { threadId: thread.id, created: true };
12463
+ }
12464
+ async function listWorkspaceThreads(cwd, deps = {}) {
12465
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12466
+ const server2 = await ensureServer();
12467
+ const target = await projectMap(server2.client).resolve(cwd);
12468
+ const threads = await server2.client.listThreads(target.projectId);
12469
+ return [...threads].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
12470
+ }
12471
+ var ACTIVITY_WINDOW = 12;
12472
+ async function workspaceThreadActivity(cwd, deps = {}) {
12473
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12474
+ const server2 = await ensureServer();
12475
+ const target = await projectMap(server2.client).resolve(cwd);
12476
+ const threads = (await server2.client.listThreads(target.projectId)).sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0)).slice(0, ACTIVITY_WINDOW);
12477
+ const rows = await Promise.all(
12478
+ threads.map(async (thread) => {
12479
+ const status = thread.status ?? await server2.client.threadStatus(thread.id).catch(() => null);
12480
+ const working = status === "pending" || status === "starting" || status === "active" || status === "stopping";
12481
+ const step = working ? await server2.client.latestThreadItems(thread.id).then(describeStep).catch(() => null) : null;
12482
+ return status ? {
12483
+ id: thread.id,
12484
+ title: thread.title ?? thread.titleFallback ?? null,
12485
+ createdAt: thread.createdAt,
12486
+ updatedAt: thread.updatedAt ?? thread.createdAt,
12487
+ status,
12488
+ providerId: thread.providerId ?? null,
12489
+ needsYou: thread.hasPendingInteraction === true,
12490
+ step
12491
+ } : null;
12492
+ })
12493
+ );
12494
+ return rows.flatMap((row2) => row2 ? [row2] : []);
12495
+ }
12496
+ async function sendToWorkspaceThread(cwd, threadId, text3, deps = {}) {
12497
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12498
+ const server2 = await ensureServer();
12499
+ await deps.ensureTools?.();
12500
+ const target = await projectMap(server2.client).resolve(cwd);
12501
+ const threads = await server2.client.listThreads(target.projectId);
12502
+ if (!threads.some((thread) => thread.id === threadId)) {
12503
+ throw new Error("that conversation is not in this project");
12504
+ }
12505
+ await server2.client.sendMessage(threadId, [{ type: "text", text: text3 }], { mode: "queue-if-active" });
12506
+ const origin = server2.client.origin;
12507
+ return { url: threadUrl(origin, target.projectId, threadId), origin, projectId: target.projectId, threadId };
12508
+ }
12509
+ async function startWorkspaceThread(cwd, text3, deps = {}, options = {}) {
12510
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12511
+ const server2 = await ensureServer();
12512
+ await deps.ensureTools?.();
12513
+ const target = await projectMap(server2.client).resolve(cwd);
12514
+ const picked = options.execution;
12515
+ const thread = await createThreadAsPicked(
12516
+ server2.client,
12517
+ picked ? { ...deps, modelChoice: async () => picked } : deps,
12518
+ {
12519
+ projectId: target.projectId,
12520
+ input: [{ type: "text", text: text3 }],
12521
+ environment: environmentFor(target.environment, options.workspace ?? null),
12522
+ ...options.permissionMode ? { permissionMode: options.permissionMode } : {}
12523
+ }
12524
+ );
12525
+ const origin = server2.client.origin;
12526
+ return {
12527
+ url: threadUrl(origin, target.projectId, thread.id),
12528
+ origin,
12529
+ projectId: target.projectId,
12530
+ threadId: thread.id
12531
+ };
12532
+ }
12533
+ async function workspaceBranches(cwd, deps = {}) {
12534
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12535
+ const server2 = await ensureServer();
12536
+ const target = await projectMap(server2.client).resolve(cwd);
12537
+ return parseBranches(await server2.client.projectBranches(target.projectId, target.hostId));
12538
+ }
12539
+ async function createThreadAsPicked(client, deps, args2) {
12540
+ const execution = deps.modelChoice ? await deps.modelChoice().catch(() => null) : null;
12541
+ if (!execution) return client.createThread(args2);
12542
+ try {
12543
+ return await client.createThread({ ...args2, execution, origin: "app" });
12544
+ } catch (err) {
12545
+ if (!(err instanceof BbApiError) || err.status < 400 || err.status >= 500) throw err;
12546
+ return client.createThread(args2);
12547
+ }
12548
+ }
12549
+ async function bbAppUrl(deps = {}) {
12550
+ const ensureServer = deps.ensureServer ?? ensureBbServer;
12551
+ const server2 = await ensureServer();
12552
+ const origin = server2.client.origin;
12553
+ return { url: `${origin}/`, origin };
12554
+ }
12555
+
12556
+ // ../editor/electron/bb/provider-choice.ts
12557
+ var STATUSES = [
12558
+ "ready",
12559
+ "not_installed",
12560
+ "unauthenticated",
12561
+ "expired",
12562
+ "unsupported_version",
12563
+ "unknown"
12564
+ ];
12565
+ function asRecord3(value) {
12566
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
12567
+ }
12568
+ function parseProviders(value, origin) {
12569
+ if (!Array.isArray(value)) return [];
12570
+ const out = [];
12571
+ for (const entry of value) {
12572
+ const rec = asRecord3(entry);
12573
+ const id = typeof rec?.id === "string" ? rec.id.trim() : "";
12574
+ if (!rec || !id) continue;
12575
+ const displayName = typeof rec.displayName === "string" && rec.displayName.trim() ? rec.displayName.trim() : id;
12576
+ let logoUrl = null;
12577
+ if (typeof rec.logoUrl === "string" && rec.logoUrl) {
12578
+ try {
12579
+ logoUrl = new URL(rec.logoUrl, origin).toString();
12580
+ } catch {
12581
+ logoUrl = null;
12582
+ }
12583
+ }
12584
+ let iconTint = parseTint(rec);
12585
+ if (id === ANTIGRAVITY_PROVIDER_ID && !logoUrl) {
12586
+ logoUrl = ANTIGRAVITY_LOGO_URL;
12587
+ iconTint ??= { ...ANTIGRAVITY_TINT };
12588
+ }
12589
+ out.push({ id, displayName, logoUrl, iconTint, available: rec.available !== false });
12590
+ }
12591
+ return out;
12592
+ }
12593
+ var COLOUR = /^(#[0-9a-f]{3,8}|(rgb|rgba|hsl|hsla|oklch|oklab)\([0-9a-z.,%\s/+-]*\)|[a-z]+)$/i;
12594
+ function parseTint(rec) {
12595
+ const tint = asRecord3(asRecord3(rec.strings)?.iconTint);
12596
+ const light = typeof tint?.light === "string" ? tint.light.trim() : "";
12597
+ const dark = typeof tint?.dark === "string" ? tint.dark.trim() : "";
12598
+ return COLOUR.test(light) && COLOUR.test(dark) ? { light, dark } : null;
12599
+ }
12600
+ async function inlineLogo(client, logoUrl) {
12601
+ if (!logoUrl) return null;
12602
+ if (logoUrl.startsWith("data:")) return logoUrl;
12603
+ try {
12604
+ const text3 = (await client.providerLogo(logoUrl, 5e3)).trim();
12605
+ if (/^(<\?xml[^>]*>\s*)?<svg[\s>]/i.test(text3)) {
12606
+ return `data:image/svg+xml;base64,${Buffer.from(text3, "utf-8").toString("base64")}`;
12607
+ }
12608
+ } catch {
12609
+ }
12610
+ return logoUrl;
12611
+ }
12612
+ function parseProviderStatuses(value) {
12613
+ const statuses = /* @__PURE__ */ new Map();
12614
+ const list = asRecord3(value)?.providers;
12615
+ if (!Array.isArray(list)) return statuses;
12616
+ for (const entry of list) {
12617
+ const rec = asRecord3(entry);
12618
+ if (typeof rec?.providerId !== "string") continue;
12619
+ const status = STATUSES.includes(rec.status) ? rec.status : "unknown";
12620
+ statuses.set(rec.providerId, status);
12621
+ }
12622
+ return statuses;
12623
+ }
12624
+ function generalSettingsOf(config) {
12625
+ return asRecord3(config.generalSettings);
12626
+ }
12627
+ async function readProviderChoice(client) {
12628
+ const [providers, statuses, config] = await Promise.all([
12629
+ client.providers(),
12630
+ client.providerStates().catch(() => null),
12631
+ client.systemConfig()
12632
+ ]);
12633
+ const byId = parseProviderStatuses(statuses);
12634
+ const defaultId = generalSettingsOf(config)?.defaultProviderId;
12635
+ const listed = parseProviders(providers, client.origin);
12636
+ const logos = await Promise.all(listed.map((provider) => inlineLogo(client, provider.logoUrl)));
12637
+ return {
12638
+ providers: listed.map((provider, index) => ({
12639
+ ...provider,
12640
+ logoUrl: logos[index],
12641
+ status: byId.get(provider.id) ?? "unknown"
12642
+ })),
12643
+ defaultProviderId: typeof defaultId === "string" && defaultId ? defaultId : null
12644
+ };
12645
+ }
12646
+ async function setDefaultProvider(client, providerId) {
12647
+ const id = providerId.trim();
12648
+ if (!id) throw new Error("No provider was chosen.");
12649
+ const settings = generalSettingsOf(await client.systemConfig());
12650
+ if (!settings) throw new Error("The agent backend did not report its settings.");
12651
+ await client.putGeneralSettings({ ...settings, defaultProviderId: id });
12652
+ }
12653
+
12654
+ // ../editor/electron/bb/attachments.ts
12655
+ import fs18 from "node:fs";
12656
+ import path17 from "node:path";
12657
+ var ATTACHMENTS_DIR = path17.join(".codecaine", "attachments");
12658
+ var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
12659
+ function attachmentFileName(name, now = Date.now()) {
12660
+ const last = String(name || "attachment").split(/[\\/]/).pop() ?? "";
12661
+ const base = last.replace(/[^\w.\- ]+/g, "_").replace(/\.{2,}/g, ".").replace(/^\.+/, "");
12662
+ const trimmed = (base || "attachment").slice(-80);
12663
+ return `${now.toString(36)}-${trimmed}`;
12664
+ }
12665
+ function saveAttachment(cwd, name, bytes) {
12666
+ if (!cwd || !path17.isAbsolute(cwd) || !fs18.existsSync(cwd)) {
12667
+ return { ok: false, error: "This document has no project folder to attach files to." };
12668
+ }
12669
+ if (!(bytes instanceof Uint8Array)) return { ok: false, error: "Nothing to attach." };
12670
+ if (bytes.byteLength > MAX_ATTACHMENT_BYTES) {
12671
+ return { ok: false, error: `${name} is larger than ${MAX_ATTACHMENT_BYTES / 1024 / 1024}MB.` };
12672
+ }
12673
+ const dir = path17.join(cwd, ATTACHMENTS_DIR);
12674
+ fs18.mkdirSync(dir, { recursive: true });
12675
+ const ignore = path17.join(dir, ".gitignore");
12676
+ if (!fs18.existsSync(ignore)) fs18.writeFileSync(ignore, "*\n");
12677
+ const relative4 = path17.join(ATTACHMENTS_DIR, attachmentFileName(name));
12678
+ fs18.writeFileSync(path17.join(cwd, relative4), bytes);
12679
+ return { ok: true, path: relative4.split(path17.sep).join("/") };
12680
+ }
12681
+
12682
+ // src/bb.ts
12683
+ var BB_SERVER_PORT = 38896;
12684
+ var BB_DAEMON_PORT = 38897;
12685
+ var EXPECTED_INSTALL_BYTES = 244e6;
12686
+ var bbAppDir = () => path18.join(dataDir(), "bb-app");
12687
+ var launcherFile = () => path18.join(bbAppDir(), "node_modules", "bb-app", "dist", "bb-app.js");
12688
+ function isolate(env = process.env) {
12689
+ env.CANVAS_BB_DATA_DIR ??= path18.join(dataDir(), "bb");
12690
+ env.CANVAS_BB_SERVER_PORT ??= String(BB_SERVER_PORT);
12691
+ env.CANVAS_BB_DAEMON_PORT ??= String(BB_DAEMON_PORT);
12692
+ env.CODECAINE_BB_APP_DIR ??= bbAppDir();
12693
+ }
12694
+ function bbInstalled() {
12695
+ try {
12696
+ const manifest = JSON.parse(
12697
+ fs19.readFileSync(path18.join(bbAppDir(), "node_modules", "bb-app", "package.json"), "utf8")
12698
+ );
12699
+ return manifest.version === BB_APP_VERSION && fs19.existsSync(launcherFile());
12700
+ } catch {
12701
+ return false;
12702
+ }
12703
+ }
12704
+ async function folderBytes(dir) {
12705
+ let total = 0;
12706
+ const walk2 = async (at) => {
12707
+ let entries;
12708
+ try {
12709
+ entries = await fs19.promises.readdir(at, { withFileTypes: true });
12710
+ } catch {
12711
+ return;
12712
+ }
12713
+ for (const entry of entries) {
12714
+ const full = path18.join(at, entry.name);
12715
+ if (entry.isDirectory()) await walk2(full);
12716
+ else if (entry.isFile()) total += (await fs19.promises.stat(full).catch(() => ({ size: 0 }))).size;
12717
+ }
12718
+ };
12719
+ await walk2(dir);
12720
+ return total;
12721
+ }
12722
+ function installBb(onProgress) {
12723
+ fs19.mkdirSync(bbAppDir(), { recursive: true });
12724
+ return new Promise((resolve3, reject) => {
12725
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
12726
+ const child = spawn(
12727
+ npm,
12728
+ ["install", `bb-app@${BB_APP_VERSION}`, "--prefix", bbAppDir(), "--no-audit", "--no-fund", "--loglevel=error"],
12729
+ { stdio: ["ignore", "ignore", "pipe"], shell: process.platform === "win32" }
12730
+ );
12731
+ let stderr = "";
12732
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
12733
+ let measuring = false;
12734
+ let done = false;
12735
+ const timer = setInterval(async () => {
12736
+ if (measuring || done) return;
12737
+ measuring = true;
12738
+ const bytes = await folderBytes(path18.join(bbAppDir(), "node_modules"));
12739
+ measuring = false;
12740
+ if (!done) onProgress(Math.min(99, Math.round(bytes / EXPECTED_INSTALL_BYTES * 100)));
12741
+ }, 700);
12742
+ child.on("error", (error) => {
12743
+ done = true;
12744
+ clearInterval(timer);
12745
+ reject(error);
12746
+ });
12747
+ child.on("exit", (code) => {
12748
+ done = true;
12749
+ clearInterval(timer);
12750
+ if (code === 0 && bbInstalled()) {
12751
+ onProgress(100);
12752
+ resolve3();
12753
+ } else {
12754
+ reject(new Error(`installing the assistant failed (npm exited ${code})${stderr ? `: ${stderr.trim().split("\n").slice(-3).join(" ")}` : ""}`));
12755
+ }
12756
+ });
12757
+ });
12758
+ }
12759
+ function stopWhateverIsOnOurPorts() {
12760
+ if (process.platform === "win32") return;
12761
+ for (const port of [BB_SERVER_PORT, BB_DAEMON_PORT]) {
12762
+ const found = spawnSync("lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], { encoding: "utf8" });
12763
+ for (const pid of String(found.stdout ?? "").split(/\s+/).filter(Boolean)) {
12764
+ try {
12765
+ process.kill(Number(pid), "SIGTERM");
12766
+ } catch {
12767
+ }
12768
+ }
12769
+ }
12770
+ }
12771
+ function createAssistant(ipc, onState = () => {
12772
+ }) {
12773
+ isolate();
12774
+ let state = { phase: bbInstalled() ? "idle" : "idle", percent: null, message: "", error: null };
12775
+ const set = (next) => {
12776
+ state = { ...state, ...next };
12777
+ onState(state);
12778
+ ipc.broadcast("companion:assistant", state);
12779
+ };
12780
+ const send2 = (channel, payload) => ipc.broadcast(channel, payload);
12781
+ let pending3 = null;
12782
+ function ready() {
12783
+ if (state.phase === "ready") return Promise.resolve();
12784
+ pending3 ??= (async () => {
12785
+ try {
12786
+ if (!bbInstalled()) {
12787
+ set({ phase: "installing", percent: 0, message: "Downloading the assistant\u2026", error: null });
12788
+ await installBb((percent) => set({ phase: "installing", percent, message: "Downloading the assistant\u2026" }));
12789
+ }
12790
+ set({ phase: "starting", percent: null, message: "Starting the assistant\u2026", error: null });
12791
+ let lastError2 = null;
12792
+ for (const pause of [0, 2e3, 5e3]) {
12793
+ if (pause) await new Promise((resolve3) => setTimeout(resolve3, pause));
12794
+ try {
12795
+ const tools = await ensureCanvasTools(send2);
12796
+ if (!tools) throw new Error(canvasToolsStatus().error ?? "the editor's tools could not be loaded into the assistant");
12797
+ lastError2 = null;
12798
+ break;
12799
+ } catch (error) {
12800
+ lastError2 = error;
12801
+ }
12802
+ }
12803
+ if (lastError2) throw lastError2;
12804
+ set({ phase: "ready", percent: 100, message: "Ready", error: null });
12805
+ } catch (error) {
12806
+ set({ phase: "failed", percent: null, message: "The assistant did not start", error: error instanceof Error ? error.message : String(error) });
12807
+ throw error;
12808
+ } finally {
12809
+ pending3 = null;
12810
+ }
12811
+ })();
12812
+ return pending3;
12813
+ }
12814
+ const modelChoices = createModelChoiceStore({
12815
+ file: path18.join(dataDir(), "agent-model-choice.json"),
12816
+ read: async () => "unreachable"
12817
+ });
12818
+ const fail = (error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) });
12819
+ const ensureTools = () => waitForCanvasTools(send2);
12820
+ ipc.handle("agent:connect", async () => {
12821
+ try {
12822
+ await ready();
12823
+ return { ok: true, url: `http://127.0.0.1:${BB_SERVER_PORT}` };
12824
+ } catch (error) {
12825
+ return fail(error);
12826
+ }
12827
+ });
12828
+ ipc.handle("agent:status", async () => canvasToolsStatus());
12829
+ ipc.handle("agent:connection", async (options = {}) => {
12830
+ if (state.phase !== "ready") return { connected: true, projects: [] };
12831
+ let projectId;
12832
+ if (options?.cwd) {
12833
+ try {
12834
+ projectId = (await bbProjectFor(options.cwd)).projectId;
12835
+ } catch {
12836
+ }
12837
+ }
12838
+ return canvasToolsConnection(send2, {
12839
+ ...projectId ? { projectId, path: options.cwd } : {},
12840
+ ...options?.instructions !== void 0 ? { instructions: options.instructions } : {},
12841
+ ...options?.layers !== void 0 ? { layers: options.layers } : {}
12842
+ });
12843
+ });
12844
+ ipc.handle("bb:providers", async () => {
12845
+ try {
12846
+ await ready();
12847
+ const { client } = await ensureBbServer();
12848
+ return { ok: true, ...await readProviderChoice(client) };
12849
+ } catch (error) {
12850
+ return fail(error);
12851
+ }
12852
+ });
12853
+ ipc.handle("bb:set-default-provider", async (options) => {
12854
+ try {
12855
+ const { client } = await ensureBbServer();
12856
+ await setDefaultProvider(client, String(options?.providerId ?? ""));
12857
+ return { ok: true };
12858
+ } catch (error) {
12859
+ return fail(error);
12860
+ }
12861
+ });
12862
+ ipc.handle("bb:compose-options", async (options) => {
12863
+ try {
12864
+ await ready();
12865
+ const { client } = await ensureBbServer();
12866
+ const picked = await modelChoices.current().catch(() => null);
12867
+ const asked = typeof options?.providerId === "string" && options.providerId ? options.providerId : null;
12868
+ const fallback = typeof options?.fallbackProviderId === "string" && options.fallbackProviderId ? options.fallbackProviderId : null;
12869
+ const providerId = asked ?? picked?.providerId ?? fallback;
12870
+ if (!providerId) return { ok: false, error: "No agent is set up yet." };
12871
+ return { ok: true, picked, options: parseExecutionOptions(await client.executionOptions(providerId), providerId) };
12872
+ } catch (error) {
12873
+ return fail(error);
12874
+ }
12875
+ });
12876
+ ipc.handle("bb:branches", async (options) => {
12877
+ try {
12878
+ const cwd = String(options?.cwd ?? "");
12879
+ if (!path18.isAbsolute(cwd)) throw new Error("not a project folder");
12880
+ await ready();
12881
+ return { ok: true, ...await workspaceBranches(cwd) };
12882
+ } catch (error) {
12883
+ return fail(error);
12884
+ }
12885
+ });
12886
+ ipc.handle("bb:agent-url", async () => {
12887
+ try {
12888
+ await ready();
12889
+ return { ok: true, ...await bbAppUrl() };
12890
+ } catch (error) {
12891
+ return fail(error);
12892
+ }
12893
+ });
12894
+ ipc.handle("bb:instructions", async (options) => {
12895
+ if (state.phase !== "ready") return { ok: true };
12896
+ try {
12897
+ const target = await bbProjectFor(options.cwd);
12898
+ return { ok: await setAgentInstructions(send2, { projectId: target.projectId, path: options.cwd, instructions: options.instructions }) };
12899
+ } catch (error) {
12900
+ return fail(error);
12901
+ }
12902
+ });
12903
+ ipc.handle("bb:selection", async (options) => {
12904
+ if (state.phase !== "ready" || !options?.cwd) return { ok: true, shown: false, attached: true };
12905
+ try {
12906
+ const target = await bbProjectFor(options.cwd);
12907
+ const attached = await setCanvasSelection(send2, { projectId: target.projectId, path: options.cwd, layers: options.layers ?? [] });
12908
+ return { ok: true, shown: false, attached };
12909
+ } catch (error) {
12910
+ return { ok: true, shown: false, attached: false, error: error instanceof Error ? error.message : String(error) };
12911
+ }
12912
+ });
12913
+ ipc.handle("bb:apply-chrome", async () => ({ ok: true }));
12914
+ ipc.handle("bb:magic-cast", async () => ({ ok: true }));
12915
+ ipc.handle("bb:send", async (options) => {
12916
+ try {
12917
+ await ready();
12918
+ return { ok: true, ...await sendToWorkspace(options.cwd, options.text, { modelChoice: () => modelChoices.current(), ensureTools }) };
12919
+ } catch (error) {
12920
+ return fail(error);
12921
+ }
12922
+ });
12923
+ ipc.handle("bb:thread-status", async (options) => {
12924
+ try {
12925
+ const { client } = await ensureBbServer();
12926
+ return { ok: true, status: await client.threadStatus(options.threadId) };
12927
+ } catch (error) {
12928
+ return fail(error);
12929
+ }
12930
+ });
12931
+ ipc.handle("bb:thread-activity", async (options) => {
12932
+ if (state.phase !== "ready") return { ok: true, threads: [] };
12933
+ try {
12934
+ return { ok: true, threads: await workspaceThreadActivity(String(options?.cwd ?? "")) };
12935
+ } catch (error) {
12936
+ return fail(error);
12937
+ }
12938
+ });
12939
+ ipc.handle("bb:threads", async (options) => {
12940
+ if (state.phase !== "ready") return { ok: true, threads: [] };
12941
+ try {
12942
+ return { ok: true, threads: await listWorkspaceThreads(String(options?.cwd ?? "")) };
12943
+ } catch (error) {
12944
+ return fail(error);
12945
+ }
12946
+ });
12947
+ ipc.handle("bb:send-thread", async (options) => {
12948
+ try {
12949
+ await ready();
12950
+ return { ok: true, ...await sendToWorkspaceThread(String(options.cwd ?? ""), String(options.threadId ?? ""), String(options.text ?? ""), { ensureTools }) };
12951
+ } catch (error) {
12952
+ return fail(error);
12953
+ }
12954
+ });
12955
+ ipc.handle(
12956
+ "bb:new-thread",
12957
+ async (options) => {
12958
+ try {
12959
+ await ready();
12960
+ return {
12961
+ ok: true,
12962
+ ...await startWorkspaceThread(
12963
+ options.cwd,
12964
+ options.text,
12965
+ { modelChoice: () => modelChoices.current(), ensureTools },
12966
+ {
12967
+ ...isBbPermissionMode(options.permissionMode) ? { permissionMode: options.permissionMode } : {},
12968
+ execution: parseComposeExecution(options.execution),
12969
+ workspace: parseComposeWorkspace(options.workspace)
12970
+ }
12971
+ )
12972
+ };
12973
+ } catch (error) {
12974
+ return fail(error);
12975
+ }
12976
+ }
12977
+ );
12978
+ ipc.handle("bb:save-attachment", async (options) => {
12979
+ try {
12980
+ return saveAttachment(options.cwd, options.name, options.bytes);
12981
+ } catch (error) {
12982
+ return fail(error);
12983
+ }
12984
+ });
12985
+ async function stop() {
12986
+ await pending3?.catch(() => {
12987
+ });
12988
+ await stopCanvasTools().catch(() => {
12989
+ });
12990
+ await stopBbServer().catch(() => {
12991
+ });
12992
+ stopWhateverIsOnOurPorts();
12993
+ set({ phase: "idle", percent: null, message: "Stopped", error: null });
12994
+ }
12995
+ return { state: () => state, ready, stop };
12996
+ }
12997
+ async function assistantTarget(cwd, threadId, home) {
12998
+ const target = await bbWorkspaceUrl(cwd, {}, threadId, home);
12999
+ return target;
13000
+ }
13001
+
13002
+ // src/ipc.ts
13003
+ var Ipc = class {
13004
+ handlers = /* @__PURE__ */ new Map();
13005
+ peers = /* @__PURE__ */ new Set();
13006
+ handle(channel, handler) {
13007
+ this.handlers.set(channel, handler);
13008
+ }
13009
+ /** The handler a channel has now — for wrapping one in another. */
13010
+ get(channel) {
13011
+ return this.handlers.get(channel);
13012
+ }
13013
+ has(channel) {
13014
+ return this.handlers.has(channel);
13015
+ }
13016
+ async invoke(channel, args2) {
13017
+ const handler = this.handlers.get(channel);
13018
+ if (!handler) {
13019
+ throw new Error(`${channel} is not available in the web build`);
13020
+ }
13021
+ return handler(...args2);
13022
+ }
13023
+ addPeer(peer) {
13024
+ this.peers.add(peer);
13025
+ return () => this.peers.delete(peer);
13026
+ }
13027
+ peerCount() {
13028
+ return this.peers.size;
13029
+ }
13030
+ /** `webContents.send` to every connected editor. */
13031
+ broadcast(channel, ...args2) {
13032
+ for (const peer of this.peers) peer.send(channel, ...args2);
13033
+ }
13034
+ };
13035
+
13036
+ // src/wire.ts
13037
+ var BYTES = "$bytes";
13038
+ function toBase64(bytes) {
13039
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
13040
+ let binary = "";
13041
+ for (let i = 0; i < bytes.length; i += 32768) {
13042
+ binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
13043
+ }
13044
+ return btoa(binary);
13045
+ }
13046
+ function fromBase64(text3) {
13047
+ if (typeof Buffer !== "undefined") {
13048
+ const buffer = Buffer.from(text3, "base64");
13049
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
13050
+ }
13051
+ const binary = atob(text3);
13052
+ const out = new Uint8Array(binary.length);
13053
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
13054
+ return out.buffer;
13055
+ }
13056
+ function encode(value) {
13057
+ return JSON.stringify(value, (_key, item) => {
10361
13058
  if (item instanceof ArrayBuffer) return { [BYTES]: toBase64(new Uint8Array(item)) };
10362
13059
  if (ArrayBuffer.isView(item)) {
10363
13060
  return { [BYTES]: toBase64(new Uint8Array(item.buffer, item.byteOffset, item.byteLength)) };
@@ -10375,9 +13072,9 @@ function decode(text3) {
10375
13072
  }
10376
13073
 
10377
13074
  // src/server.ts
10378
- var VERSION = "0.1.0";
13075
+ var VERSION = "0.1.3";
10379
13076
  var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
10380
- var MAX_BODY_BYTES = 8 * 1024 * 1024;
13077
+ var MAX_BODY_BYTES2 = 8 * 1024 * 1024;
10381
13078
  function hostIsLocal(host) {
10382
13079
  if (!host) return true;
10383
13080
  try {
@@ -10386,19 +13083,19 @@ function hostIsLocal(host) {
10386
13083
  return false;
10387
13084
  }
10388
13085
  }
10389
- function tokenMatches(expected, given) {
13086
+ function tokenMatches2(expected, given) {
10390
13087
  if (!given) return false;
10391
13088
  const a = Buffer.from(expected);
10392
13089
  const b = Buffer.from(given);
10393
13090
  return a.length === b.length && crypto3.timingSafeEqual(a, b);
10394
13091
  }
10395
- function readBody(request) {
13092
+ function readBody2(request) {
10396
13093
  return new Promise((resolve3, reject) => {
10397
13094
  const chunks = [];
10398
13095
  let size = 0;
10399
13096
  request.on("data", (chunk) => {
10400
13097
  size += chunk.length;
10401
- if (size > MAX_BODY_BYTES) {
13098
+ if (size > MAX_BODY_BYTES2) {
10402
13099
  reject(new Error("Request body too large."));
10403
13100
  request.destroy();
10404
13101
  return;
@@ -10432,8 +13129,64 @@ async function startCompanion(options) {
10432
13129
  })
10433
13130
  });
10434
13131
  const info = { name: "codecaine", version: VERSION, instructions: agent.instructions };
13132
+ const viaInbox = Object.fromEntries(
13133
+ ["bb:new-thread", "bb:send", "bb:send-thread", "bb:workspace-url"].map((channel) => [channel, ipc.get(channel)])
13134
+ );
13135
+ const useAssistant = options.assistant !== false;
13136
+ const assistant = useAssistant ? createAssistant(ipc) : {
13137
+ state: () => ({ phase: "failed", percent: null, message: "Off", error: "The assistant is off (--no-assistant)." }),
13138
+ ready: () => Promise.reject(new Error("The assistant is off (--no-assistant).")),
13139
+ stop: async () => {
13140
+ }
13141
+ };
13142
+ for (const [channel, inboxHandler] of Object.entries(viaInbox)) {
13143
+ if (channel === "bb:workspace-url") continue;
13144
+ const bbHandler = ipc.get(channel);
13145
+ ipc.handle(channel, async (...args2) => {
13146
+ const answer = await bbHandler(...args2);
13147
+ return answer?.ok === false && assistant.state().phase === "failed" ? inboxHandler(...args2) : answer;
13148
+ });
13149
+ }
13150
+ const status = () => ({
13151
+ version: VERSION,
13152
+ assistant: useAssistant ? assistant.state() : null,
13153
+ agent: agent.inbox.connected()
13154
+ });
13155
+ const stopEverything = async () => {
13156
+ await assistant.stop().catch(() => {
13157
+ });
13158
+ options.onStopRequested?.();
13159
+ };
13160
+ ipc.handle("companion:status", async () => status());
13161
+ ipc.handle("companion:stop-assistant", async () => {
13162
+ await assistant.stop();
13163
+ return status();
13164
+ });
13165
+ ipc.handle("companion:stop", async () => {
13166
+ setTimeout(() => void stopEverything(), 200);
13167
+ return { ok: true };
13168
+ });
13169
+ const startingUrl = (cwd, threadId, home) => `http://127.0.0.1:${port}/agent/${pageKey}/starting?` + new URLSearchParams({ cwd, ...threadId ? { thread: threadId } : {}, ...home ? { home: "1" } : {} }).toString();
13170
+ ipc.handle("bb:workspace-url", async (args2) => {
13171
+ if (!useAssistant) return viaInbox["bb:workspace-url"](args2);
13172
+ const cwd = String(args2?.cwd ?? "");
13173
+ const threadId = typeof args2?.threadId === "string" ? args2.threadId : null;
13174
+ if (assistant.state().phase === "ready") {
13175
+ try {
13176
+ const target = await assistantTarget(cwd, threadId, args2?.home === true);
13177
+ await waitForCanvasTools((channel, payload) => ipc.broadcast(channel, payload), { attempts: 1 }).catch(() => {
13178
+ });
13179
+ return { ok: true, ...target };
13180
+ } catch (error) {
13181
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
13182
+ }
13183
+ }
13184
+ void assistant.ready().catch(() => {
13185
+ });
13186
+ return { ok: true, url: startingUrl(cwd, threadId, args2?.home === true), origin: `http://127.0.0.1:${port}`, projectId: cwd };
13187
+ });
10435
13188
  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) => {
13189
+ const server2 = http3.createServer((request, response) => {
10437
13190
  void route(request, response).catch((error) => {
10438
13191
  if (!response.headersSent) json(response, 500, { error: error instanceof Error ? error.message : String(error) });
10439
13192
  else response.destroy();
@@ -10465,12 +13218,34 @@ async function startCompanion(options) {
10465
13218
  response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-length": Buffer.byteLength(html) });
10466
13219
  return void response.end(html);
10467
13220
  }
13221
+ if (url.pathname === `/agent/${pageKey}/starting`) {
13222
+ const html = assistantStartingPage();
13223
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-length": Buffer.byteLength(html) });
13224
+ return void response.end(html);
13225
+ }
13226
+ if (url.pathname === `/agent/${pageKey}/assistant`) {
13227
+ const state = assistant.state();
13228
+ if (state.phase === "idle") void assistant.ready().catch(() => {
13229
+ });
13230
+ let target = null;
13231
+ if (state.phase === "ready") {
13232
+ try {
13233
+ target = (await assistantTarget(url.searchParams.get("cwd") ?? "", url.searchParams.get("thread"), url.searchParams.get("home") === "1")).url;
13234
+ } catch (error) {
13235
+ return json(response, 200, { state: { ...state, phase: "failed", error: error instanceof Error ? error.message : String(error) }, theme: agent.inbox.theme }, { "cache-control": "no-store" });
13236
+ }
13237
+ }
13238
+ return json(response, 200, { state, url: target, theme: agent.inbox.theme }, { "cache-control": "no-store" });
13239
+ }
10468
13240
  if (url.pathname === `/agent/${pageKey}/state`) {
10469
13241
  const project = url.searchParams.get("project") ?? "";
10470
13242
  const inbox = agent.inbox;
10471
13243
  return json(response, 200, {
10472
13244
  theme: inbox.theme,
10473
13245
  agent: inbox.connected(),
13246
+ // Is a `codecaine mcp` waiting to push requests right now? It asks
13247
+ // again every 25s, so a minute without asking means it has gone.
13248
+ pushing: [...inbox.pushTargets.values()].some((at) => Date.now() - at < 6e4),
10474
13249
  requests: (project ? inbox.requests.filter((request2) => sameFolder(request2.cwd, project)) : inbox.requests).map(
10475
13250
  (request2) => ({ ...request2, words: wordsOf(request2.text) })
10476
13251
  ),
@@ -10478,8 +13253,92 @@ async function startCompanion(options) {
10478
13253
  }, { "cache-control": "no-store" });
10479
13254
  }
10480
13255
  if (url.pathname === "/mcp") return mcp(request, response);
13256
+ if (url.pathname === "/inbox/wait") return inboxWait(request, response, url);
13257
+ if (url.pathname === "/open" && request.method === "POST") return openOnStart(request, response);
13258
+ if (url.pathname === "/assistant/start" && request.method === "POST") {
13259
+ if (!agentAllowed(request, response)) return;
13260
+ void assistant.ready().catch(() => {
13261
+ });
13262
+ return json(response, 200, assistant.state());
13263
+ }
13264
+ if (url.pathname === "/companion/stop" && request.method === "POST") {
13265
+ if (!agentAllowed(request, response)) return;
13266
+ json(response, 200, { ok: true, version: VERSION });
13267
+ setTimeout(() => void stopEverything(), 200);
13268
+ return;
13269
+ }
13270
+ if (url.pathname === "/assistant/state") {
13271
+ if (!agentAllowed(request, response)) return;
13272
+ return json(response, 200, assistant.state());
13273
+ }
10481
13274
  json(response, 404, { error: "Not found." });
10482
13275
  }
13276
+ function agentAllowed(request, response) {
13277
+ const origin = request.headers.origin;
13278
+ if (origin && origin !== "null") {
13279
+ try {
13280
+ if (!LOOPBACK.has(new URL(origin).hostname)) {
13281
+ json(response, 403, { error: "Forbidden origin." });
13282
+ return false;
13283
+ }
13284
+ } catch {
13285
+ json(response, 403, { error: "Forbidden origin." });
13286
+ return false;
13287
+ }
13288
+ }
13289
+ const header3 = request.headers.authorization;
13290
+ const given = header3 && /^Bearer\s+(.+)$/i.exec(header3.trim())?.[1];
13291
+ if (!tokenMatches2(options.mcpToken, given)) {
13292
+ json(response, 401, { error: "Missing or wrong bearer token." }, { "www-authenticate": "Bearer" });
13293
+ return false;
13294
+ }
13295
+ return true;
13296
+ }
13297
+ function inboxWait(request, response, url) {
13298
+ if (!agentAllowed(request, response)) return;
13299
+ const since = Number(url.searchParams.get("since") ?? 0);
13300
+ const client = String(request.headers["x-codecaine-client"] ?? "") || "Your coding agent";
13301
+ const inbox = agent.inbox;
13302
+ inbox.pushTargets.set(client, Date.now());
13303
+ inbox.heard(client);
13304
+ const waiting = () => inbox.requests.filter((entry) => entry.status === "pending" && entry.updatedAt > since);
13305
+ const ready = waiting();
13306
+ if (ready.length) return json(response, 200, { requests: ready });
13307
+ let done = false;
13308
+ const finish = (requests) => {
13309
+ if (done) return;
13310
+ done = true;
13311
+ clearTimeout(timer);
13312
+ stop();
13313
+ json(response, 200, { requests });
13314
+ };
13315
+ const stop = inbox.onWaiting(() => finish(waiting()));
13316
+ const timer = setTimeout(() => finish([]), 25e3);
13317
+ request.on("close", () => {
13318
+ done = true;
13319
+ clearTimeout(timer);
13320
+ stop();
13321
+ });
13322
+ }
13323
+ const broughtForward = /* @__PURE__ */ new Set();
13324
+ async function openOnStart(request, response) {
13325
+ if (!agentAllowed(request, response)) return;
13326
+ let folder;
13327
+ try {
13328
+ folder = String(JSON.parse(await readBody2(request)).folder ?? "");
13329
+ } catch {
13330
+ return json(response, 400, { error: "Expected { folder }." });
13331
+ }
13332
+ if (!folder || !looksLikeProject(folder)) return json(response, 200, { opened: false, reason: "not a project" });
13333
+ const tabs = readOpenTabs(openTabsFile());
13334
+ const inFront = tabs.folderPaths[tabs.activeIndex] === path19.resolve(folder);
13335
+ if (broughtForward.has(path19.resolve(folder)) || inFront && ipc.peerCount() > 0) {
13336
+ return json(response, 200, { opened: false, reason: "already in front" });
13337
+ }
13338
+ broughtForward.add(path19.resolve(folder));
13339
+ const result = await agent.bringToFront(folder);
13340
+ json(response, 200, result.ok ? { opened: true, folder: result.folderPath } : { opened: false, reason: result.error });
13341
+ }
10483
13342
  async function mcp(request, response) {
10484
13343
  const origin = request.headers.origin;
10485
13344
  if (origin && origin !== "null") {
@@ -10491,7 +13350,7 @@ async function startCompanion(options) {
10491
13350
  }
10492
13351
  const header3 = request.headers.authorization;
10493
13352
  const given = header3 && /^Bearer\s+(.+)$/i.exec(header3.trim())?.[1];
10494
- if (!tokenMatches(options.mcpToken, given)) {
13353
+ if (!tokenMatches2(options.mcpToken, given)) {
10495
13354
  return json(response, 401, { error: "Missing or wrong bearer token." }, { "www-authenticate": "Bearer" });
10496
13355
  }
10497
13356
  if (request.method === "GET") {
@@ -10501,14 +13360,17 @@ async function startCompanion(options) {
10501
13360
  if (request.method !== "POST") return json(response, 405, { error: "POST only." }, { allow: "POST" });
10502
13361
  let payload;
10503
13362
  try {
10504
- payload = JSON.parse(await readBody(request));
13363
+ payload = JSON.parse(await readBody2(request));
10505
13364
  } catch {
10506
13365
  return json(response, 400, rpcError(null, RPC_PARSE_ERROR, "Parse error."));
10507
13366
  }
10508
13367
  const cwd = String(request.headers["x-codecaine-cwd"] ?? "");
13368
+ const named = String(request.headers["x-codecaine-client"] ?? "") || null;
10509
13369
  for (const message of Array.isArray(payload) ? payload : [payload]) {
10510
13370
  const hello = message;
10511
- agent.inbox.heard(hello?.method === "initialize" && typeof hello.params?.clientInfo?.name === "string" ? hello.params.clientInfo.name : null);
13371
+ agent.inbox.heard(
13372
+ hello?.method === "initialize" && typeof hello.params?.clientInfo?.name === "string" ? hello.params.clientInfo.name : named
13373
+ );
10512
13374
  }
10513
13375
  const answer = await handlePayload(
10514
13376
  payload,
@@ -10522,7 +13384,7 @@ async function startCompanion(options) {
10522
13384
  json(response, 200, answer);
10523
13385
  }
10524
13386
  const sockets = new import_websocket_server.default({ noServer: true, maxPayload: 256 * 1024 * 1024 });
10525
- server.on("upgrade", (request, socket, head) => {
13387
+ server2.on("upgrade", (request, socket, head) => {
10526
13388
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
10527
13389
  const origin = request.headers.origin;
10528
13390
  if (url.pathname !== "/bridge" || !hostIsLocal(request.headers.host) || !origin || !allowed.has(origin)) {
@@ -10534,14 +13396,14 @@ async function startCompanion(options) {
10534
13396
  });
10535
13397
  let peerIds = 0;
10536
13398
  function connect(ws) {
10537
- const send = (message) => {
13399
+ const send2 = (message) => {
10538
13400
  if (ws.readyState === ws.OPEN) ws.send(encode(message));
10539
13401
  };
10540
13402
  const remove = ipc.addPeer({
10541
13403
  id: ++peerIds,
10542
- send: (channel, ...args2) => send({ t: "event", channel, args: args2 })
13404
+ send: (channel, ...args2) => send2({ t: "event", channel, args: args2 })
10543
13405
  });
10544
- send({
13406
+ send2({
10545
13407
  t: "hello",
10546
13408
  version: VERSION,
10547
13409
  mediaOrigin: `http://127.0.0.1:${port}/media/${mediaKey}/`,
@@ -10561,15 +13423,15 @@ async function startCompanion(options) {
10561
13423
  }
10562
13424
  if (message.t !== "invoke") return;
10563
13425
  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) })
13426
+ (value) => send2({ t: "result", id: message.id, ok: true, value }),
13427
+ (error) => send2({ t: "result", id: message.id, ok: false, error: error instanceof Error ? error.message : String(error) })
10566
13428
  );
10567
13429
  });
10568
13430
  ws.on("close", remove);
10569
13431
  }
10570
13432
  port = await new Promise((resolve3, reject) => {
10571
- server.once("error", reject);
10572
- server.listen(options.port, "127.0.0.1", () => resolve3(server.address().port));
13433
+ server2.once("error", reject);
13434
+ server2.listen(options.port, "127.0.0.1", () => resolve3(server2.address().port));
10573
13435
  });
10574
13436
  return {
10575
13437
  port,
@@ -10577,13 +13439,13 @@ async function startCompanion(options) {
10577
13439
  close: () => new Promise((resolve3) => {
10578
13440
  live.close();
10579
13441
  for (const ws of sockets.clients) ws.terminate();
10580
- server.closeAllConnections?.();
10581
- server.close(() => resolve3());
13442
+ server2.closeAllConnections?.();
13443
+ server2.close(() => resolve3());
10582
13444
  })
10583
13445
  };
10584
13446
  }
10585
13447
  function serveFile(request, response, file, type, headers) {
10586
- const size = fs14.statSync(file).size;
13448
+ const size = fs20.statSync(file).size;
10587
13449
  const range = /^bytes=(\d*)-(\d*)$/.exec(String(request.headers.range ?? ""));
10588
13450
  const base = { ...headers, "content-type": type, "accept-ranges": "bytes", "cache-control": "private, max-age=31536000, immutable" };
10589
13451
  if (range) {
@@ -10594,21 +13456,25 @@ function serveFile(request, response, file, type, headers) {
10594
13456
  return void response.end();
10595
13457
  }
10596
13458
  response.writeHead(206, { ...base, "content-range": `bytes ${start}-${end}/${size}`, "content-length": end - start + 1 });
10597
- fs14.createReadStream(file, { start, end }).pipe(response);
13459
+ fs20.createReadStream(file, { start, end }).pipe(response);
10598
13460
  return;
10599
13461
  }
10600
13462
  response.writeHead(200, { ...base, "content-length": size });
10601
- fs14.createReadStream(file).pipe(response);
13463
+ fs20.createReadStream(file).pipe(response);
10602
13464
  }
10603
13465
  function guestCandidates(here2) {
10604
- return [path12.join(here2, "canvas-live-guest.js"), path12.join(here2, "..", "..", "dist", "canvas-live-guest.js")];
13466
+ return [path19.join(here2, "canvas-live-guest.js"), path19.join(here2, "..", "..", "dist", "canvas-live-guest.js")];
10605
13467
  }
10606
13468
 
10607
13469
  // src/cli.ts
10608
13470
  var DEFAULT_PORT = 4317;
10609
- var DEFAULT_EDITOR = "https://celebrated-souffle-e3f7bf.netlify.app";
10610
- var PUBLISHED_ORIGINS = ["https://editor.codecainehq.com", "https://celebrated-souffle-e3f7bf.netlify.app"];
10611
- var here = path13.dirname(fileURLToPath(import.meta.url));
13471
+ var DEFAULT_EDITOR = "https://web.codecainehq.com";
13472
+ var PUBLISHED_ORIGINS = [
13473
+ "https://web.codecainehq.com",
13474
+ "https://editor.codecainehq.com",
13475
+ "https://celebrated-souffle-e3f7bf.netlify.app"
13476
+ ];
13477
+ var here = path20.dirname(fileURLToPath3(import.meta.url));
10612
13478
  function flag(args2, name) {
10613
13479
  const at = args2.indexOf(name);
10614
13480
  return at >= 0 ? args2[at + 1] : void 0;
@@ -10616,7 +13482,10 @@ function flag(args2, name) {
10616
13482
  function openBrowser(url) {
10617
13483
  const [file, args2] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
10618
13484
  try {
10619
- spawn(file, args2, { stdio: "ignore", detached: true }).unref();
13485
+ const child = spawn2(file, args2, { stdio: "ignore", detached: true });
13486
+ child.on("error", () => {
13487
+ });
13488
+ child.unref();
10620
13489
  } catch {
10621
13490
  }
10622
13491
  }
@@ -10642,10 +13511,11 @@ async function serve(args2) {
10642
13511
  const origins = [
10643
13512
  .../* @__PURE__ */ new Set([new URL(editorUrl).origin, ...PUBLISHED_ORIGINS, ...extra, "http://localhost:5180", "http://127.0.0.1:5180"])
10644
13513
  ];
10645
- const guestFile = guestCandidates(here).find((file) => fs15.existsSync(file));
13514
+ const guestFile = guestCandidates(here).find((file) => fs21.existsSync(file));
10646
13515
  if (!guestFile) throw new Error("canvas-live-guest.js is missing \u2014 build the web app first (npm run build).");
10647
13516
  const quiet = args2.includes("--no-open");
10648
- const folder = args2.find((arg, i) => !arg.startsWith("--") && !args2[i - 1]?.startsWith("--"));
13517
+ const named = args2.find((arg, i) => !arg.startsWith("--") && !["--port", "--editor", "--origin"].includes(args2[i - 1] ?? ""));
13518
+ const folder = named ?? (looksLikeProject(process.cwd()) ? process.cwd() : void 0);
10649
13519
  if (await health(port)) {
10650
13520
  console.log(`codecaine is already running on port ${port}.`);
10651
13521
  if (!quiet) openBrowser(editorUrl);
@@ -10653,7 +13523,20 @@ async function serve(args2) {
10653
13523
  }
10654
13524
  restoreOpenFolders();
10655
13525
  const token = mcpToken();
10656
- const companion = await startCompanion({ port, editorUrl, origins, guestFile, mcpToken: token, openBrowser });
13526
+ let closing = null;
13527
+ const stop = () => {
13528
+ closing ??= companion.close().then(() => process.exit(0));
13529
+ };
13530
+ const companion = await startCompanion({
13531
+ port,
13532
+ editorUrl,
13533
+ origins,
13534
+ guestFile,
13535
+ mcpToken: token,
13536
+ openBrowser,
13537
+ assistant: !args2.includes("--no-assistant") && process.env.CODECAINE_NO_ASSISTANT !== "1",
13538
+ onStopRequested: () => stop()
13539
+ });
10657
13540
  writeState({ pid: process.pid, port: companion.port, mcpPort: companion.port, mcpToken: token, version: VERSION });
10658
13541
  if (folder) {
10659
13542
  const loaded = openFolder(folder);
@@ -10677,14 +13560,13 @@ async function serve(args2) {
10677
13560
  ].join("\n")
10678
13561
  );
10679
13562
  if (!quiet) openBrowser(editorUrl);
10680
- const stop = () => void companion.close().then(() => process.exit(0));
10681
13563
  process.on("SIGINT", stop);
10682
13564
  process.on("SIGTERM", stop);
10683
13565
  }
10684
- async function mcpPipe() {
13566
+ async function ensureCompanion() {
10685
13567
  let state = readState();
10686
13568
  if (!state || !await health(state.port)) {
10687
- const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "--no-open"], {
13569
+ const child = spawn2(process.execPath, [fileURLToPath3(import.meta.url), "--no-open"], {
10688
13570
  detached: true,
10689
13571
  stdio: "ignore",
10690
13572
  env: process.env
@@ -10697,7 +13579,11 @@ async function mcpPipe() {
10697
13579
  if (state && await health(state.port)) break;
10698
13580
  }
10699
13581
  }
10700
- if (!state || !await health(state.port)) {
13582
+ return state && await health(state.port) ? state : null;
13583
+ }
13584
+ async function mcpPipe() {
13585
+ const state = await ensureCompanion();
13586
+ if (!state) {
10701
13587
  process.stderr.write("codecaine: the companion did not start; run `npx codecaine-cli` to see why.\n");
10702
13588
  process.exit(1);
10703
13589
  }
@@ -10707,31 +13593,172 @@ async function mcpPipe() {
10707
13593
  authorization: `Bearer ${state.mcpToken}`,
10708
13594
  "x-codecaine-cwd": process.cwd()
10709
13595
  };
13596
+ const write = (message) => process.stdout.write(JSON.stringify(message) + "\n");
10710
13597
  const lines = readline.createInterface({ input: process.stdin });
10711
13598
  let chain = Promise.resolve();
13599
+ let pushing = false;
10712
13600
  lines.on("line", (line) => {
10713
13601
  if (!line.trim()) return;
13602
+ let message = null;
13603
+ try {
13604
+ message = JSON.parse(line);
13605
+ } catch {
13606
+ }
13607
+ if (message?.method === "initialize" && typeof message.params?.clientInfo?.name === "string") {
13608
+ headers["x-codecaine-client"] = message.params.clientInfo.name;
13609
+ }
10714
13610
  chain = chain.then(async () => {
10715
13611
  try {
10716
13612
  const response = await fetch(target, { method: "POST", headers, body: line });
10717
13613
  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 {
13614
+ const text3 = await response.text();
13615
+ if (message?.method !== "initialize") {
13616
+ process.stdout.write(text3 + "\n");
13617
+ return;
10724
13618
  }
10725
- process.stdout.write(
10726
- JSON.stringify({ jsonrpc: "2.0", id, error: { code: -32603, message: `codecaine companion unreachable: ${String(error)}` } }) + "\n"
10727
- );
13619
+ const answer = JSON.parse(text3);
13620
+ if (answer.result?.capabilities) {
13621
+ const experimental = answer.result.capabilities.experimental ?? {};
13622
+ answer.result.capabilities.experimental = { ...experimental, "claude/channel": {} };
13623
+ }
13624
+ write(answer);
13625
+ void fetch(`http://127.0.0.1:${state.port}/open`, {
13626
+ method: "POST",
13627
+ headers,
13628
+ body: JSON.stringify({ folder: process.cwd() })
13629
+ }).catch(() => {
13630
+ });
13631
+ if (!pushing) {
13632
+ pushing = true;
13633
+ void pushRequests(headers, write);
13634
+ }
13635
+ } catch (error) {
13636
+ write({ jsonrpc: "2.0", id: message?.id ?? null, error: { code: -32603, message: `codecaine companion unreachable: ${String(error)}` } });
10728
13637
  }
10729
13638
  });
10730
13639
  });
10731
13640
  lines.on("close", () => void chain.then(() => process.exit(0)));
10732
13641
  }
13642
+ async function pushRequests(headers, write) {
13643
+ const pushed = /* @__PURE__ */ new Map();
13644
+ let since = Date.now() - 10 * 6e4;
13645
+ for (; ; ) {
13646
+ const state = readState();
13647
+ try {
13648
+ if (!state) throw new Error("no companion");
13649
+ const response = await fetch(`http://127.0.0.1:${state.port}/inbox/wait?since=${since}`, {
13650
+ headers: { ...headers, authorization: `Bearer ${state.mcpToken}` },
13651
+ signal: AbortSignal.timeout(4e4)
13652
+ });
13653
+ const { requests = [] } = await response.json();
13654
+ for (const request of requests) {
13655
+ since = Math.max(since, request.updatedAt);
13656
+ if (pushed.get(request.id) === request.updatedAt) continue;
13657
+ pushed.set(request.id, request.updatedAt);
13658
+ write({
13659
+ jsonrpc: "2.0",
13660
+ method: "notifications/claude/channel",
13661
+ params: {
13662
+ content: [
13663
+ `New request from the Codecaine board${request.cwd ? ` (project ${request.cwd})` : ""}:`,
13664
+ "",
13665
+ wordsOf(request.text),
13666
+ ...request.followUps.map((more) => `+ ${more}`),
13667
+ "",
13668
+ `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.`
13669
+ ].join("\n"),
13670
+ meta: { request_id: request.id, kind: "board_request" }
13671
+ }
13672
+ });
13673
+ }
13674
+ } catch {
13675
+ await new Promise((r) => setTimeout(r, 3e3));
13676
+ }
13677
+ }
13678
+ }
13679
+ async function setup(args2) {
13680
+ const tty = process.stdout.isTTY;
13681
+ const say = (line) => process.stdout.write(line + "\n");
13682
+ say(`codecaine ${VERSION} \u2014 setting up this machine for Codecaine
13683
+ `);
13684
+ say("1/3 Starting the companion\u2026");
13685
+ const running = readState();
13686
+ if (running && running.version !== VERSION && await health(running.port)) {
13687
+ say(` Replacing the running companion (${running.version}) with ${VERSION}\u2026`);
13688
+ await fetch(`http://127.0.0.1:${running.port}/companion/stop`, {
13689
+ method: "POST",
13690
+ headers: { authorization: `Bearer ${running.mcpToken}` }
13691
+ }).catch(() => {
13692
+ });
13693
+ const until = Date.now() + 1e4;
13694
+ while (Date.now() < until && await health(running.port)) await new Promise((r) => setTimeout(r, 300));
13695
+ if (await health(running.port)) {
13696
+ say(` The running companion (${running.version}) did not stop by itself.`);
13697
+ say(" Stop it \u2014 Ctrl+C in the terminal it is running in \u2014 then run this again.");
13698
+ process.exit(1);
13699
+ }
13700
+ }
13701
+ const state = await ensureCompanion();
13702
+ if (!state) {
13703
+ say(" It did not start. Run `npx codecaine-cli` to see why.");
13704
+ process.exit(1);
13705
+ }
13706
+ say(` Running on http://127.0.0.1:${state.port}`);
13707
+ say("2/3 Getting the assistant ready (the first time it downloads it)\u2026");
13708
+ const headers = { authorization: `Bearer ${state.mcpToken}` };
13709
+ const base = `http://127.0.0.1:${state.port}`;
13710
+ let assistant = await (await fetch(`${base}/assistant/start`, { method: "POST", headers })).json();
13711
+ let lastShown = -1;
13712
+ const width = 28;
13713
+ while (assistant.phase !== "ready" && assistant.phase !== "failed") {
13714
+ const percent = assistant.percent;
13715
+ if (percent != null && percent !== lastShown) {
13716
+ const filled = Math.round(percent / 100 * width);
13717
+ const bar = ` [${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}] ${String(percent).padStart(3)}% ${assistant.message}`;
13718
+ if (tty) process.stdout.write(`\r${bar}`);
13719
+ else if (Math.floor(percent / 10) !== Math.floor(lastShown / 10)) say(bar);
13720
+ lastShown = percent;
13721
+ } else if (percent == null && lastShown !== -2 && assistant.phase === "starting") {
13722
+ if (tty && lastShown >= 0) process.stdout.write("\n");
13723
+ say(" Starting it\u2026");
13724
+ lastShown = -2;
13725
+ }
13726
+ await new Promise((r) => setTimeout(r, 500));
13727
+ assistant = await (await fetch(`${base}/assistant/state`, { headers })).json();
13728
+ }
13729
+ if (tty && lastShown >= 0) process.stdout.write("\n");
13730
+ if (assistant.phase === "failed") {
13731
+ say(` The assistant did not start: ${assistant.error ?? "no reason given"}`);
13732
+ say(" The editor still works; its Agent panel says the same and retries.");
13733
+ } else {
13734
+ say(" Ready \u2014 it is the Agent panel in the editor.");
13735
+ }
13736
+ say("3/3 Connecting your coding agent over MCP\u2026");
13737
+ const has = (command) => spawnSync2(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" }).status === 0;
13738
+ if (args2.includes("--no-mcp")) {
13739
+ say(" Skipped (--no-mcp). Add the command `npx -y codecaine-cli mcp` as an MCP server whenever you want.");
13740
+ } else if (has("claude")) {
13741
+ if (spawnSync2("claude", ["mcp", "get", "codecaine"], { stdio: "ignore" }).status === 0) {
13742
+ say(" Claude Code already has it.");
13743
+ } else {
13744
+ const added = spawnSync2("claude", ["mcp", "add", "codecaine", "-s", "user", "--", "npx", "-y", "codecaine-cli", "mcp"], { stdio: "ignore" });
13745
+ 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");
13746
+ }
13747
+ } else {
13748
+ say(" Claude Code is not installed \u2014 skipped. Other agents: add the command `npx -y codecaine-cli mcp` as an MCP server.");
13749
+ }
13750
+ const editorUrl = (flag(args2, "--editor") ?? process.env.CODECAINE_EDITOR_URL ?? DEFAULT_EDITOR).replace(/\/+$/, "");
13751
+ say(`
13752
+ Done. Open ${editorUrl} \u2014 it connects to this machine by itself.`);
13753
+ if (!args2.includes("--no-open")) openBrowser(editorUrl);
13754
+ }
10733
13755
  var args = process.argv.slice(2);
10734
- if (args[0] === "mcp") {
13756
+ if (args[0] === "setup") {
13757
+ setup(args.slice(1)).catch((error) => {
13758
+ console.error(error instanceof Error ? error.message : String(error));
13759
+ process.exit(1);
13760
+ });
13761
+ } else if (args[0] === "mcp") {
10735
13762
  void mcpPipe();
10736
13763
  } else if (args[0] === "--version" || args[0] === "-v") {
10737
13764
  console.log(VERSION);