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