codecaine-cli 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.mjs +264 -16
- package/dist/resources/bb-plugin/src/server.ts +2 -2
- package/dist/resources/bb-plugin/src/tools.ts +96 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# codecaine-cli
|
|
2
2
|
|
|
3
|
-
Connects the [Codecaine](https://
|
|
3
|
+
Connects the [Codecaine](https://web.codecainehq.com) web editor to your
|
|
4
4
|
machine.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
@@ -43,7 +43,7 @@ companion prints.
|
|
|
43
43
|
|
|
44
44
|
| | |
|
|
45
45
|
| --- | --- |
|
|
46
|
-
| `--editor <url>` | The editor's address (default `https://
|
|
46
|
+
| `--editor <url>` | The editor's address (default `https://web.codecainehq.com`) |
|
|
47
47
|
| `--origin <a,b>` | More origins allowed to connect |
|
|
48
48
|
| `--port <n>` | Default 4317 |
|
|
49
49
|
| `--no-open` | Don't open the browser |
|
package/dist/cli.mjs
CHANGED
|
@@ -3718,7 +3718,7 @@ var require_websocket_server = __commonJS({
|
|
|
3718
3718
|
|
|
3719
3719
|
// src/cli.ts
|
|
3720
3720
|
import crypto4 from "node:crypto";
|
|
3721
|
-
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
3721
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
3722
3722
|
import fs21 from "node:fs";
|
|
3723
3723
|
import path20 from "node:path";
|
|
3724
3724
|
import readline from "node:readline";
|
|
@@ -6494,6 +6494,17 @@ import os3 from "node:os";
|
|
|
6494
6494
|
import path11 from "node:path";
|
|
6495
6495
|
|
|
6496
6496
|
// ../editor/resources/bb-plugin/src/tools.ts
|
|
6497
|
+
var VECTOR_STYLE_SCHEMA = {
|
|
6498
|
+
type: "object",
|
|
6499
|
+
properties: {
|
|
6500
|
+
stroke: { type: ["string", "null"], description: "A CSS colour, or null for none." },
|
|
6501
|
+
fill: { type: ["string", "null"], description: "A CSS colour, or null for none." },
|
|
6502
|
+
strokeWidth: { type: "number" },
|
|
6503
|
+
cap: { type: "string", enum: ["butt", "round", "square"] },
|
|
6504
|
+
join: { type: "string", enum: ["miter", "round", "bevel"] },
|
|
6505
|
+
dashes: { type: "string", description: 'Dash and gap lengths, e.g. "4 2"; empty is solid.' }
|
|
6506
|
+
}
|
|
6507
|
+
};
|
|
6497
6508
|
var TARGET_SCHEMA = {
|
|
6498
6509
|
type: "object",
|
|
6499
6510
|
description: "Layer handle: id (preferred \u2014 returned by listLayers/createLayer) or a CSS-shaped selector over the design.",
|
|
@@ -6949,6 +6960,84 @@ var CANVAS_TOOLS = [
|
|
|
6949
6960
|
}
|
|
6950
6961
|
}
|
|
6951
6962
|
},
|
|
6963
|
+
{
|
|
6964
|
+
name: "getVector",
|
|
6965
|
+
description: "A drawing (made with the pen or drawVector) or a pasted <svg> icon, as the pen sees it: { id, viewBox, paths: [{ index, shape, closed, style, points: [{ x, y, in, out, mirror }] }] }. Points are in the drawing's own units (its viewBox); `in`/`out` are handles RELATIVE to their point (null = a corner); `mirror` is Figma's Mirroring: none, angle, or mirror (angle and length). Paths of one compound shape (a ring and its hole) share `shape`. Read this before drawVector operations, which name paths and points by these indices.",
|
|
6966
|
+
inputSchema: {
|
|
6967
|
+
type: "object",
|
|
6968
|
+
properties: { target: TARGET_SCHEMA },
|
|
6969
|
+
required: ["target"]
|
|
6970
|
+
}
|
|
6971
|
+
},
|
|
6972
|
+
{
|
|
6973
|
+
name: "drawVector",
|
|
6974
|
+
description: "Draw with the pen: icons, logos, illustrations, arrows, isometric shapes \u2014 anything that is lines and curves rather than boxes. Without `target` it makes a new drawing on the board (or inside `parent`) from `paths`; with `target` it changes that drawing or pasted <svg> with `operations` (and adds any `paths`), all or nothing, in one undo step. A path is SVG path data `d`, or `points` [{ x, y, in?, out? }] with handles relative to each point (out: {x: 30, y: 0} pulls the curve 30px forward from the point) and `closed`. Units are px; the drawing's own 0,0 lands at `at` (clear space by default) and its box fits the paths. Operations, each the pen tool of the same name \u2014 segment i runs from point i to point i+1: move {path, point, to:{x,y}}; handles {path, point, in, out}; mirror {path, point, mode: none|angle|mirror}; bend {path, segment, t?, through:{x,y}} (bend the curve through a point); smooth / corner {path, point}; insert {path, segment, t?} (add a point); cut {path, point | segment, t?}; erase {path, segment}; delete {path, point?}; close / open {path}; paint {path, fill} (the paint bucket; fill null empties it); style {path?, style}; translate {path?, by:{x,y}}; add {path}. Answers the drawing read back, as getVector does \u2014 check it, and screenshot it with canvasScreenshot before saying it is done.",
|
|
6975
|
+
inputSchema: {
|
|
6976
|
+
type: "object",
|
|
6977
|
+
properties: {
|
|
6978
|
+
target: TARGET_SCHEMA,
|
|
6979
|
+
paths: {
|
|
6980
|
+
type: "array",
|
|
6981
|
+
description: "Paths to draw (or to add to `target`).",
|
|
6982
|
+
items: {
|
|
6983
|
+
type: "object",
|
|
6984
|
+
properties: {
|
|
6985
|
+
d: { type: "string", description: 'SVG path data, e.g. "M0 0 L40 0 L20 30 Z".' },
|
|
6986
|
+
points: {
|
|
6987
|
+
type: "array",
|
|
6988
|
+
items: {
|
|
6989
|
+
type: "object",
|
|
6990
|
+
properties: {
|
|
6991
|
+
x: { type: "number" },
|
|
6992
|
+
y: { type: "number" },
|
|
6993
|
+
in: { type: "object", description: "Incoming handle, relative to the point.", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
6994
|
+
out: { type: "object", description: "Outgoing handle, relative to the point.", properties: { x: { type: "number" }, y: { type: "number" } } }
|
|
6995
|
+
},
|
|
6996
|
+
required: ["x", "y"]
|
|
6997
|
+
}
|
|
6998
|
+
},
|
|
6999
|
+
closed: { type: "boolean" },
|
|
7000
|
+
style: VECTOR_STYLE_SCHEMA
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
},
|
|
7004
|
+
style: { ...VECTOR_STYLE_SCHEMA, description: "Paint for every path that does not set its own. Defaults: stroke #1c1917, strokeWidth 2, no fill, round cap and join." },
|
|
7005
|
+
operations: {
|
|
7006
|
+
type: "array",
|
|
7007
|
+
description: "Pen operations, applied in order after `paths` are added.",
|
|
7008
|
+
items: {
|
|
7009
|
+
type: "object",
|
|
7010
|
+
properties: {
|
|
7011
|
+
op: {
|
|
7012
|
+
type: "string",
|
|
7013
|
+
enum: ["add", "move", "handles", "mirror", "bend", "smooth", "corner", "insert", "cut", "erase", "delete", "close", "open", "paint", "style", "translate"]
|
|
7014
|
+
},
|
|
7015
|
+
path: { description: "A path index from getVector \u2014 or, for add, a path object like those in `paths`." },
|
|
7016
|
+
point: { type: "number" },
|
|
7017
|
+
segment: { type: "number" },
|
|
7018
|
+
t: { type: "number", description: "Where along the segment, 0\u20131 (0.5 by default)." },
|
|
7019
|
+
to: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
7020
|
+
through: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
7021
|
+
by: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
7022
|
+
in: { type: ["object", "null"], properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
7023
|
+
out: { type: ["object", "null"], properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
7024
|
+
mode: { type: "string", enum: ["none", "angle", "mirror"] },
|
|
7025
|
+
fill: { type: ["string", "null"] },
|
|
7026
|
+
style: VECTOR_STYLE_SCHEMA
|
|
7027
|
+
},
|
|
7028
|
+
required: ["op"]
|
|
7029
|
+
}
|
|
7030
|
+
},
|
|
7031
|
+
parent: { type: "string", description: "Id of the layer to draw a new drawing inside. Omit for the board." },
|
|
7032
|
+
at: {
|
|
7033
|
+
type: "object",
|
|
7034
|
+
description: "Where a new drawing's 0,0 lands, in the parent's coordinates (the board's without one).",
|
|
7035
|
+
properties: { x: { type: "number" }, y: { type: "number" } }
|
|
7036
|
+
},
|
|
7037
|
+
name: { type: "string", description: "The new drawing's layer name." }
|
|
7038
|
+
}
|
|
7039
|
+
}
|
|
7040
|
+
},
|
|
6952
7041
|
{
|
|
6953
7042
|
name: "exportHtml",
|
|
6954
7043
|
description: "The design as markup \u2014 the whole page, or one layer with `target`. This is the fastest way to read a document you are about to change. `responsive` and `states` emit the media queries and the :hover rules the design carries. Pictures are named `asset:media/images/<hash>.<ext>` (getLayer's `files` gives the path on disk); pass the name back in markup and it is the same picture. Anything still inline comes back as a short `media:\u2026` reference that works the same way. A `target` inside a running project answers that element as its page has it \u2014 its own classes and attributes, its computed look inline \u2014 which is what to copy when moving a block from one project to another.",
|
|
@@ -7205,13 +7294,13 @@ var SHOT_DIR = join2(tmpdir(), "canvas-agent-shots");
|
|
|
7205
7294
|
var CANVAS_TOOL_GUIDE = [
|
|
7206
7295
|
"THE DESIGN ON THE BOARD IS REACHED THROUGH THESE TOOLS, BY THESE NAMES:",
|
|
7207
7296
|
CANVAS_TOOLS.map((tool) => tool.name).join(", ") + ".",
|
|
7208
|
-
"Your provider may show them prefixed \u2014 `mcp__bb-bridge__listLayers` \u2014 as bb hands them over as a session MCP server named `bb-bridge`. Prefixed or not, they are the editor's own, already connected:
|
|
7297
|
+
"Your provider may show them prefixed \u2014 `mcp__bb-bridge__listLayers` \u2014 as bb hands them over as a session MCP server named `bb-bridge`. Prefixed or not, they are the editor's own, already connected: nothing to start, connect, configure or open first.",
|
|
7209
7298
|
"It is the NAME that makes a tool this design's, never the prefix. A tool not listed above does not touch this design, however it sounds. Never use one for work on this board, or to explain a failure on it.",
|
|
7210
7299
|
"THE BOARD SHOWS WHAT YOU ARE DOING. Every write you make already puts a band of colour round the layer for a few seconds. holdLayer is the longer claim: it keeps a layer for a whole job, says what you are doing to it, and refuses the person's edits meanwhile. Take one before your first change when you are REWRITING it \u2014 redesigning, restyling, replacing its insides \u2014 so nobody edits under a half-applied change. Reading takes nothing; nor do variants BESIDE one, which are new layers.",
|
|
7211
7300
|
"Release before your turn ends even when you are giving up, a tool failed, or you are handing the question back \u2014 say so. askAboutLayer keeps the hold and puts your question on the board; holdLayer picks it back up. The person can take a layer back with the button beside its name, and a hold whose assistant goes quiet frees itself; getLayer reports the run on a layer, and an edit to one no longer held still lands.",
|
|
7212
7301
|
"If a tool reports it cannot reach the editor, the tool has already retried: report its message as it stands \u2014 do not assume more than it says \u2014 and stop. If the tools are missing from this session altogether, say THAT, plainly, first. In neither case substitute another application's tools or edit the design's files: the editor overwrites them.",
|
|
7213
7302
|
"WRITE ORDINARY WEB MARKUP. replaceLayer and insertHtml take any HTML \u2014 sections, tables, svg, canvas, forms with real inputs. Lay it out with flexbox and grid, which the inspector edits; absolute positioning only on the outermost element the board places. Linked stylesheets are ignored. Behaviour goes on a layer in a webpage (updateLayer's `script`, or script tags in markup) and runs in Interact and Preview with `element` as that layer, never on the canvas.",
|
|
7214
|
-
"TO SEE AN EFFECT WORK: setPageView (in-view, motion playing), then observePage to read it or canvasPointer to use it with a real mouse. Restructure
|
|
7303
|
+
"TO SEE AN EFFECT WORK: setPageView (in-view, motion playing), then observePage to read it or canvasPointer to use it with a real mouse. Restructure like a drag with moveLayer, groupLayers and duplicateLayers.",
|
|
7215
7304
|
"A LIVE PROJECT ON THIS MACHINE (localhost, LAN): a change asked for there is done only once SAVED in its source \u2014 a layer-tool edit is discarded at the next rebuild. Write it as the project's own code: the component or stylesheet that renders it, its Tailwind classes and theme variables, no inline styles or hard-coded colours. Then check LIGHT AND DARK MODE: every colour from a token with a value for each (listVariables; a new one gets both), and look at the page in each (setPageView theme light, then dark, then system) before saying done. A page served from the web has no source here: say so.",
|
|
7216
7305
|
"COPYING BETWEEN LIVE PROJECTS: a copy is the original, never a redesign. Read it in its source project first; keep its markup, words, data and look. Each colour, size, radius, shadow and font becomes var(--name) holding that same value: the target's own (listVariables) only if equal, else a new one (setVariable).",
|
|
7217
7306
|
'Start by looking: getSelection is what the person is pointing at when they say "this"; listLayers gives bearings; listVariables gives the design system behind it \u2014 change a shared colour there, not on every layer.'
|
|
@@ -10504,7 +10593,7 @@ function assistantStartingPage() {
|
|
|
10504
10593
|
</head>
|
|
10505
10594
|
<body>
|
|
10506
10595
|
<h1 id="title">Starting the assistant\u2026</h1>
|
|
10507
|
-
<p id="what">The first time, it downloads
|
|
10596
|
+
<p id="what">The first time, it downloads the assistant \u2014 after that it starts in seconds.</p>
|
|
10508
10597
|
<div class="bar unknown" id="bar"><div class="fill" id="fill"></div></div>
|
|
10509
10598
|
<p class="note" id="note"></p>
|
|
10510
10599
|
<p class="error" id="error" hidden></p>
|
|
@@ -10540,7 +10629,7 @@ function assistantStartingPage() {
|
|
|
10540
10629
|
}
|
|
10541
10630
|
|
|
10542
10631
|
// src/bb.ts
|
|
10543
|
-
import { spawn } from "node:child_process";
|
|
10632
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10544
10633
|
import fs19 from "node:fs";
|
|
10545
10634
|
import path18 from "node:path";
|
|
10546
10635
|
|
|
@@ -11252,6 +11341,55 @@ var BbClient = class {
|
|
|
11252
11341
|
}
|
|
11253
11342
|
};
|
|
11254
11343
|
|
|
11344
|
+
// ../editor/electron/process-tree.ts
|
|
11345
|
+
import { execFileSync } from "node:child_process";
|
|
11346
|
+
function descendantsOf(table, root) {
|
|
11347
|
+
const children = /* @__PURE__ */ new Map();
|
|
11348
|
+
for (const [pid, ppid] of table) {
|
|
11349
|
+
if (pid === ppid) continue;
|
|
11350
|
+
const list = children.get(ppid) ?? [];
|
|
11351
|
+
list.push(pid);
|
|
11352
|
+
children.set(ppid, list);
|
|
11353
|
+
}
|
|
11354
|
+
const out = [];
|
|
11355
|
+
const seen = /* @__PURE__ */ new Set([root]);
|
|
11356
|
+
const queue = [root];
|
|
11357
|
+
while (queue.length) {
|
|
11358
|
+
const next = queue.shift();
|
|
11359
|
+
for (const child of children.get(next) ?? []) {
|
|
11360
|
+
if (seen.has(child)) continue;
|
|
11361
|
+
seen.add(child);
|
|
11362
|
+
out.push(child);
|
|
11363
|
+
queue.push(child);
|
|
11364
|
+
}
|
|
11365
|
+
}
|
|
11366
|
+
return out;
|
|
11367
|
+
}
|
|
11368
|
+
function readProcessTable() {
|
|
11369
|
+
try {
|
|
11370
|
+
const text3 = execFileSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8", timeout: 2e3 });
|
|
11371
|
+
const rows = [];
|
|
11372
|
+
for (const line of text3.split("\n")) {
|
|
11373
|
+
const [pid, ppid] = line.trim().split(/\s+/).map(Number);
|
|
11374
|
+
if (Number.isInteger(pid) && Number.isInteger(ppid)) rows.push([pid, ppid]);
|
|
11375
|
+
}
|
|
11376
|
+
return rows;
|
|
11377
|
+
} catch {
|
|
11378
|
+
return [];
|
|
11379
|
+
}
|
|
11380
|
+
}
|
|
11381
|
+
function killProcessTree(root, deps = {}) {
|
|
11382
|
+
const kill = deps.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
11383
|
+
const pids = [root, ...descendantsOf((deps.table ?? readProcessTable)(), root)];
|
|
11384
|
+
for (const pid of pids) {
|
|
11385
|
+
try {
|
|
11386
|
+
kill(pid, "SIGKILL");
|
|
11387
|
+
} catch {
|
|
11388
|
+
}
|
|
11389
|
+
}
|
|
11390
|
+
return pids;
|
|
11391
|
+
}
|
|
11392
|
+
|
|
11255
11393
|
// ../editor/electron/bb/supervisor.ts
|
|
11256
11394
|
var BB_APP_VERSION = "0.42.1";
|
|
11257
11395
|
var BB_DEFAULT_SERVER_PORT = 38886;
|
|
@@ -11512,6 +11650,31 @@ ${stderr.trim()}` : "";
|
|
|
11512
11650
|
deps.log(`[bb] server ready on :${serverPort} (host ${host.id})`);
|
|
11513
11651
|
return handle;
|
|
11514
11652
|
}
|
|
11653
|
+
var STOP_GRACE_MS = 13e3;
|
|
11654
|
+
async function stopBbServer(options = {}) {
|
|
11655
|
+
adopted = null;
|
|
11656
|
+
const targets = [owned, launching].filter((t) => t !== null);
|
|
11657
|
+
owned = null;
|
|
11658
|
+
launching = null;
|
|
11659
|
+
await Promise.all(targets.map((t) => stopLauncher(t.child, t.exited, options)));
|
|
11660
|
+
}
|
|
11661
|
+
async function stopLauncher(child, exited, options) {
|
|
11662
|
+
if (childExited.has(child)) return;
|
|
11663
|
+
try {
|
|
11664
|
+
child.kill("SIGTERM");
|
|
11665
|
+
} catch {
|
|
11666
|
+
}
|
|
11667
|
+
let timer;
|
|
11668
|
+
const stopped = await Promise.race([
|
|
11669
|
+
exited.then(() => true),
|
|
11670
|
+
new Promise((resolve3) => {
|
|
11671
|
+
timer = setTimeout(() => resolve3(false), options.graceMs ?? STOP_GRACE_MS);
|
|
11672
|
+
})
|
|
11673
|
+
]);
|
|
11674
|
+
clearTimeout(timer);
|
|
11675
|
+
if (stopped || child.pid === void 0) return;
|
|
11676
|
+
(options.killTree ?? ((pid) => void killProcessTree(pid)))(child.pid);
|
|
11677
|
+
}
|
|
11515
11678
|
|
|
11516
11679
|
// ../editor/electron/bb/canvas-tools.ts
|
|
11517
11680
|
import fs16 from "node:fs";
|
|
@@ -11863,6 +12026,13 @@ async function canvasToolsConnection(send2, args2 = {}, deps = {}) {
|
|
|
11863
12026
|
return { connected: false, projects: [] };
|
|
11864
12027
|
}
|
|
11865
12028
|
}
|
|
12029
|
+
async function stopCanvasTools() {
|
|
12030
|
+
pending2 = null;
|
|
12031
|
+
lastError = null;
|
|
12032
|
+
const current = server;
|
|
12033
|
+
server = null;
|
|
12034
|
+
if (current) await current.stop();
|
|
12035
|
+
}
|
|
11866
12036
|
function canvasToolsStatus() {
|
|
11867
12037
|
return { connected: server !== null, url: server?.url ?? null, error: lastError };
|
|
11868
12038
|
}
|
|
@@ -11960,6 +12130,8 @@ var CANVAS_TOOL_STEPS = {
|
|
|
11960
12130
|
listElementTypes: "Reading the elements",
|
|
11961
12131
|
getCanvasScene: "Reading a 3D scene",
|
|
11962
12132
|
setCanvasScene: "Building a 3D scene",
|
|
12133
|
+
getVector: "Reading a drawing",
|
|
12134
|
+
drawVector: "Drawing with the pen",
|
|
11963
12135
|
exportHtml: "Reading the markup",
|
|
11964
12136
|
listVariables: "Reading the variables",
|
|
11965
12137
|
setVariable: "Setting a variable",
|
|
@@ -12584,6 +12756,18 @@ function installBb(onProgress) {
|
|
|
12584
12756
|
});
|
|
12585
12757
|
});
|
|
12586
12758
|
}
|
|
12759
|
+
function stopWhateverIsOnOurPorts() {
|
|
12760
|
+
if (process.platform === "win32") return;
|
|
12761
|
+
for (const port of [BB_SERVER_PORT, BB_DAEMON_PORT]) {
|
|
12762
|
+
const found = spawnSync("lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], { encoding: "utf8" });
|
|
12763
|
+
for (const pid of String(found.stdout ?? "").split(/\s+/).filter(Boolean)) {
|
|
12764
|
+
try {
|
|
12765
|
+
process.kill(Number(pid), "SIGTERM");
|
|
12766
|
+
} catch {
|
|
12767
|
+
}
|
|
12768
|
+
}
|
|
12769
|
+
}
|
|
12770
|
+
}
|
|
12587
12771
|
function createAssistant(ipc, onState = () => {
|
|
12588
12772
|
}) {
|
|
12589
12773
|
isolate();
|
|
@@ -12798,7 +12982,17 @@ function createAssistant(ipc, onState = () => {
|
|
|
12798
12982
|
return fail(error);
|
|
12799
12983
|
}
|
|
12800
12984
|
});
|
|
12801
|
-
|
|
12985
|
+
async function stop() {
|
|
12986
|
+
await pending3?.catch(() => {
|
|
12987
|
+
});
|
|
12988
|
+
await stopCanvasTools().catch(() => {
|
|
12989
|
+
});
|
|
12990
|
+
await stopBbServer().catch(() => {
|
|
12991
|
+
});
|
|
12992
|
+
stopWhateverIsOnOurPorts();
|
|
12993
|
+
set({ phase: "idle", percent: null, message: "Stopped", error: null });
|
|
12994
|
+
}
|
|
12995
|
+
return { state: () => state, ready, stop };
|
|
12802
12996
|
}
|
|
12803
12997
|
async function assistantTarget(cwd, threadId, home) {
|
|
12804
12998
|
const target = await bbWorkspaceUrl(cwd, {}, threadId, home);
|
|
@@ -12878,7 +13072,7 @@ function decode(text3) {
|
|
|
12878
13072
|
}
|
|
12879
13073
|
|
|
12880
13074
|
// src/server.ts
|
|
12881
|
-
var VERSION = "0.1.
|
|
13075
|
+
var VERSION = "0.1.3";
|
|
12882
13076
|
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
12883
13077
|
var MAX_BODY_BYTES2 = 8 * 1024 * 1024;
|
|
12884
13078
|
function hostIsLocal(host) {
|
|
@@ -12939,7 +13133,12 @@ async function startCompanion(options) {
|
|
|
12939
13133
|
["bb:new-thread", "bb:send", "bb:send-thread", "bb:workspace-url"].map((channel) => [channel, ipc.get(channel)])
|
|
12940
13134
|
);
|
|
12941
13135
|
const useAssistant = options.assistant !== false;
|
|
12942
|
-
const assistant = useAssistant ? createAssistant(ipc) : {
|
|
13136
|
+
const assistant = useAssistant ? createAssistant(ipc) : {
|
|
13137
|
+
state: () => ({ phase: "failed", percent: null, message: "Off", error: "The assistant is off (--no-assistant)." }),
|
|
13138
|
+
ready: () => Promise.reject(new Error("The assistant is off (--no-assistant).")),
|
|
13139
|
+
stop: async () => {
|
|
13140
|
+
}
|
|
13141
|
+
};
|
|
12943
13142
|
for (const [channel, inboxHandler] of Object.entries(viaInbox)) {
|
|
12944
13143
|
if (channel === "bb:workspace-url") continue;
|
|
12945
13144
|
const bbHandler = ipc.get(channel);
|
|
@@ -12948,6 +13147,25 @@ async function startCompanion(options) {
|
|
|
12948
13147
|
return answer?.ok === false && assistant.state().phase === "failed" ? inboxHandler(...args2) : answer;
|
|
12949
13148
|
});
|
|
12950
13149
|
}
|
|
13150
|
+
const status = () => ({
|
|
13151
|
+
version: VERSION,
|
|
13152
|
+
assistant: useAssistant ? assistant.state() : null,
|
|
13153
|
+
agent: agent.inbox.connected()
|
|
13154
|
+
});
|
|
13155
|
+
const stopEverything = async () => {
|
|
13156
|
+
await assistant.stop().catch(() => {
|
|
13157
|
+
});
|
|
13158
|
+
options.onStopRequested?.();
|
|
13159
|
+
};
|
|
13160
|
+
ipc.handle("companion:status", async () => status());
|
|
13161
|
+
ipc.handle("companion:stop-assistant", async () => {
|
|
13162
|
+
await assistant.stop();
|
|
13163
|
+
return status();
|
|
13164
|
+
});
|
|
13165
|
+
ipc.handle("companion:stop", async () => {
|
|
13166
|
+
setTimeout(() => void stopEverything(), 200);
|
|
13167
|
+
return { ok: true };
|
|
13168
|
+
});
|
|
12951
13169
|
const startingUrl = (cwd, threadId, home) => `http://127.0.0.1:${port}/agent/${pageKey}/starting?` + new URLSearchParams({ cwd, ...threadId ? { thread: threadId } : {}, ...home ? { home: "1" } : {} }).toString();
|
|
12952
13170
|
ipc.handle("bb:workspace-url", async (args2) => {
|
|
12953
13171
|
if (!useAssistant) return viaInbox["bb:workspace-url"](args2);
|
|
@@ -13043,6 +13261,12 @@ async function startCompanion(options) {
|
|
|
13043
13261
|
});
|
|
13044
13262
|
return json(response, 200, assistant.state());
|
|
13045
13263
|
}
|
|
13264
|
+
if (url.pathname === "/companion/stop" && request.method === "POST") {
|
|
13265
|
+
if (!agentAllowed(request, response)) return;
|
|
13266
|
+
json(response, 200, { ok: true, version: VERSION });
|
|
13267
|
+
setTimeout(() => void stopEverything(), 200);
|
|
13268
|
+
return;
|
|
13269
|
+
}
|
|
13046
13270
|
if (url.pathname === "/assistant/state") {
|
|
13047
13271
|
if (!agentAllowed(request, response)) return;
|
|
13048
13272
|
return json(response, 200, assistant.state());
|
|
@@ -13244,8 +13468,12 @@ function guestCandidates(here2) {
|
|
|
13244
13468
|
|
|
13245
13469
|
// src/cli.ts
|
|
13246
13470
|
var DEFAULT_PORT = 4317;
|
|
13247
|
-
var DEFAULT_EDITOR = "https://
|
|
13248
|
-
var PUBLISHED_ORIGINS = [
|
|
13471
|
+
var DEFAULT_EDITOR = "https://web.codecainehq.com";
|
|
13472
|
+
var PUBLISHED_ORIGINS = [
|
|
13473
|
+
"https://web.codecainehq.com",
|
|
13474
|
+
"https://editor.codecainehq.com",
|
|
13475
|
+
"https://celebrated-souffle-e3f7bf.netlify.app"
|
|
13476
|
+
];
|
|
13249
13477
|
var here = path20.dirname(fileURLToPath3(import.meta.url));
|
|
13250
13478
|
function flag(args2, name) {
|
|
13251
13479
|
const at = args2.indexOf(name);
|
|
@@ -13295,6 +13523,10 @@ async function serve(args2) {
|
|
|
13295
13523
|
}
|
|
13296
13524
|
restoreOpenFolders();
|
|
13297
13525
|
const token = mcpToken();
|
|
13526
|
+
let closing = null;
|
|
13527
|
+
const stop = () => {
|
|
13528
|
+
closing ??= companion.close().then(() => process.exit(0));
|
|
13529
|
+
};
|
|
13298
13530
|
const companion = await startCompanion({
|
|
13299
13531
|
port,
|
|
13300
13532
|
editorUrl,
|
|
@@ -13302,7 +13534,8 @@ async function serve(args2) {
|
|
|
13302
13534
|
guestFile,
|
|
13303
13535
|
mcpToken: token,
|
|
13304
13536
|
openBrowser,
|
|
13305
|
-
assistant: !args2.includes("--no-assistant") && process.env.CODECAINE_NO_ASSISTANT !== "1"
|
|
13537
|
+
assistant: !args2.includes("--no-assistant") && process.env.CODECAINE_NO_ASSISTANT !== "1",
|
|
13538
|
+
onStopRequested: () => stop()
|
|
13306
13539
|
});
|
|
13307
13540
|
writeState({ pid: process.pid, port: companion.port, mcpPort: companion.port, mcpToken: token, version: VERSION });
|
|
13308
13541
|
if (folder) {
|
|
@@ -13327,7 +13560,6 @@ async function serve(args2) {
|
|
|
13327
13560
|
].join("\n")
|
|
13328
13561
|
);
|
|
13329
13562
|
if (!quiet) openBrowser(editorUrl);
|
|
13330
|
-
const stop = () => void companion.close().then(() => process.exit(0));
|
|
13331
13563
|
process.on("SIGINT", stop);
|
|
13332
13564
|
process.on("SIGTERM", stop);
|
|
13333
13565
|
}
|
|
@@ -13450,13 +13682,29 @@ async function setup(args2) {
|
|
|
13450
13682
|
say(`codecaine ${VERSION} \u2014 setting up this machine for Codecaine
|
|
13451
13683
|
`);
|
|
13452
13684
|
say("1/3 Starting the companion\u2026");
|
|
13685
|
+
const running = readState();
|
|
13686
|
+
if (running && running.version !== VERSION && await health(running.port)) {
|
|
13687
|
+
say(` Replacing the running companion (${running.version}) with ${VERSION}\u2026`);
|
|
13688
|
+
await fetch(`http://127.0.0.1:${running.port}/companion/stop`, {
|
|
13689
|
+
method: "POST",
|
|
13690
|
+
headers: { authorization: `Bearer ${running.mcpToken}` }
|
|
13691
|
+
}).catch(() => {
|
|
13692
|
+
});
|
|
13693
|
+
const until = Date.now() + 1e4;
|
|
13694
|
+
while (Date.now() < until && await health(running.port)) await new Promise((r) => setTimeout(r, 300));
|
|
13695
|
+
if (await health(running.port)) {
|
|
13696
|
+
say(` The running companion (${running.version}) did not stop by itself.`);
|
|
13697
|
+
say(" Stop it \u2014 Ctrl+C in the terminal it is running in \u2014 then run this again.");
|
|
13698
|
+
process.exit(1);
|
|
13699
|
+
}
|
|
13700
|
+
}
|
|
13453
13701
|
const state = await ensureCompanion();
|
|
13454
13702
|
if (!state) {
|
|
13455
13703
|
say(" It did not start. Run `npx codecaine-cli` to see why.");
|
|
13456
13704
|
process.exit(1);
|
|
13457
13705
|
}
|
|
13458
13706
|
say(` Running on http://127.0.0.1:${state.port}`);
|
|
13459
|
-
say("2/3 Getting the assistant ready (the first time it downloads
|
|
13707
|
+
say("2/3 Getting the assistant ready (the first time it downloads it)\u2026");
|
|
13460
13708
|
const headers = { authorization: `Bearer ${state.mcpToken}` };
|
|
13461
13709
|
const base = `http://127.0.0.1:${state.port}`;
|
|
13462
13710
|
let assistant = await (await fetch(`${base}/assistant/start`, { method: "POST", headers })).json();
|
|
@@ -13486,14 +13734,14 @@ async function setup(args2) {
|
|
|
13486
13734
|
say(" Ready \u2014 it is the Agent panel in the editor.");
|
|
13487
13735
|
}
|
|
13488
13736
|
say("3/3 Connecting your coding agent over MCP\u2026");
|
|
13489
|
-
const has = (command) =>
|
|
13737
|
+
const has = (command) => spawnSync2(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" }).status === 0;
|
|
13490
13738
|
if (args2.includes("--no-mcp")) {
|
|
13491
13739
|
say(" Skipped (--no-mcp). Add the command `npx -y codecaine-cli mcp` as an MCP server whenever you want.");
|
|
13492
13740
|
} else if (has("claude")) {
|
|
13493
|
-
if (
|
|
13741
|
+
if (spawnSync2("claude", ["mcp", "get", "codecaine"], { stdio: "ignore" }).status === 0) {
|
|
13494
13742
|
say(" Claude Code already has it.");
|
|
13495
13743
|
} else {
|
|
13496
|
-
const added =
|
|
13744
|
+
const added = spawnSync2("claude", ["mcp", "add", "codecaine", "-s", "user", "--", "npx", "-y", "codecaine-cli", "mcp"], { stdio: "ignore" });
|
|
13497
13745
|
say(added.status === 0 ? " Added to Claude Code. Restart Claude Code to load it." : " Could not add it to Claude Code; run: claude mcp add codecaine -- npx -y codecaine-cli mcp");
|
|
13498
13746
|
}
|
|
13499
13747
|
} else {
|
|
@@ -237,7 +237,7 @@ export const CANVAS_TOOL_GUIDE = [
|
|
|
237
237
|
CANVAS_TOOLS.map((tool) => tool.name).join(", ") + ".",
|
|
238
238
|
"Your provider may show them prefixed — `mcp__bb-bridge__listLayers` — as bb hands them " +
|
|
239
239
|
"over as a session MCP server named `bb-bridge`. Prefixed or not, they are the editor's own, " +
|
|
240
|
-
"already connected:
|
|
240
|
+
"already connected: nothing to start, connect, configure or open first.",
|
|
241
241
|
"It is the NAME that makes a tool this design's, never the prefix. A tool not listed above " +
|
|
242
242
|
"does not touch this design, however it sounds. " +
|
|
243
243
|
"Never use one for work on this board, or to explain a failure on it.",
|
|
@@ -262,7 +262,7 @@ export const CANVAS_TOOL_GUIDE = [
|
|
|
262
262
|
"Behaviour goes on a layer in a webpage (updateLayer's `script`, or script tags in markup) " +
|
|
263
263
|
"and runs in Interact and Preview with `element` as that layer, never on the canvas.",
|
|
264
264
|
"TO SEE AN EFFECT WORK: setPageView (in-view, motion playing), then observePage to read it or " +
|
|
265
|
-
"canvasPointer to use it with a real mouse. Restructure
|
|
265
|
+
"canvasPointer to use it with a real mouse. Restructure like a drag with moveLayer, " +
|
|
266
266
|
"groupLayers and duplicateLayers.",
|
|
267
267
|
"A LIVE PROJECT ON THIS MACHINE (localhost, LAN): a change asked for there is done only once " +
|
|
268
268
|
"SAVED in its source — a layer-tool edit is discarded at the next rebuild. Write it as the " +
|
|
@@ -17,6 +17,19 @@
|
|
|
17
17
|
/** Which layer a tool is about. An id is exact; a selector is the fallback for
|
|
18
18
|
* an agent that has only read the markup — see docs/canvas-api.md for the
|
|
19
19
|
* grammar it accepts. */
|
|
20
|
+
/** A path's paint, as the pen's Vector panel sets it. */
|
|
21
|
+
const VECTOR_STYLE_SCHEMA = {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
stroke: { type: ["string", "null"], description: "A CSS colour, or null for none." },
|
|
25
|
+
fill: { type: ["string", "null"], description: "A CSS colour, or null for none." },
|
|
26
|
+
strokeWidth: { type: "number" },
|
|
27
|
+
cap: { type: "string", enum: ["butt", "round", "square"] },
|
|
28
|
+
join: { type: "string", enum: ["miter", "round", "bevel"] },
|
|
29
|
+
dashes: { type: "string", description: "Dash and gap lengths, e.g. \"4 2\"; empty is solid." },
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
20
33
|
export const TARGET_SCHEMA = {
|
|
21
34
|
type: "object",
|
|
22
35
|
description:
|
|
@@ -541,6 +554,89 @@ export const CANVAS_TOOLS = [
|
|
|
541
554
|
},
|
|
542
555
|
},
|
|
543
556
|
},
|
|
557
|
+
{
|
|
558
|
+
name: "getVector",
|
|
559
|
+
description:
|
|
560
|
+
"A drawing (made with the pen or drawVector) or a pasted <svg> icon, as the pen sees it: { id, viewBox, paths: [{ index, shape, closed, style, points: [{ x, y, in, out, mirror }] }] }. Points are in the drawing's own units (its viewBox); `in`/`out` are handles RELATIVE to their point (null = a corner); `mirror` is Figma's Mirroring: none, angle, or mirror (angle and length). Paths of one compound shape (a ring and its hole) share `shape`. Read this before drawVector operations, which name paths and points by these indices.",
|
|
561
|
+
inputSchema: {
|
|
562
|
+
type: "object",
|
|
563
|
+
properties: { target: TARGET_SCHEMA },
|
|
564
|
+
required: ["target"],
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: "drawVector",
|
|
569
|
+
description:
|
|
570
|
+
"Draw with the pen: icons, logos, illustrations, arrows, isometric shapes — anything that is lines and curves rather than boxes. Without `target` it makes a new drawing on the board (or inside `parent`) from `paths`; with `target` it changes that drawing or pasted <svg> with `operations` (and adds any `paths`), all or nothing, in one undo step. " +
|
|
571
|
+
"A path is SVG path data `d`, or `points` [{ x, y, in?, out? }] with handles relative to each point (out: {x: 30, y: 0} pulls the curve 30px forward from the point) and `closed`. Units are px; the drawing's own 0,0 lands at `at` (clear space by default) and its box fits the paths. " +
|
|
572
|
+
"Operations, each the pen tool of the same name — segment i runs from point i to point i+1: move {path, point, to:{x,y}}; handles {path, point, in, out}; mirror {path, point, mode: none|angle|mirror}; bend {path, segment, t?, through:{x,y}} (bend the curve through a point); smooth / corner {path, point}; insert {path, segment, t?} (add a point); cut {path, point | segment, t?}; erase {path, segment}; delete {path, point?}; close / open {path}; paint {path, fill} (the paint bucket; fill null empties it); style {path?, style}; translate {path?, by:{x,y}}; add {path}. " +
|
|
573
|
+
"Answers the drawing read back, as getVector does — check it, and screenshot it with canvasScreenshot before saying it is done.",
|
|
574
|
+
inputSchema: {
|
|
575
|
+
type: "object",
|
|
576
|
+
properties: {
|
|
577
|
+
target: TARGET_SCHEMA,
|
|
578
|
+
paths: {
|
|
579
|
+
type: "array",
|
|
580
|
+
description: "Paths to draw (or to add to `target`).",
|
|
581
|
+
items: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
d: { type: "string", description: "SVG path data, e.g. \"M0 0 L40 0 L20 30 Z\"." },
|
|
585
|
+
points: {
|
|
586
|
+
type: "array",
|
|
587
|
+
items: {
|
|
588
|
+
type: "object",
|
|
589
|
+
properties: {
|
|
590
|
+
x: { type: "number" },
|
|
591
|
+
y: { type: "number" },
|
|
592
|
+
in: { type: "object", description: "Incoming handle, relative to the point.", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
593
|
+
out: { type: "object", description: "Outgoing handle, relative to the point.", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
594
|
+
},
|
|
595
|
+
required: ["x", "y"],
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
closed: { type: "boolean" },
|
|
599
|
+
style: VECTOR_STYLE_SCHEMA,
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
style: { ...VECTOR_STYLE_SCHEMA, description: "Paint for every path that does not set its own. Defaults: stroke #1c1917, strokeWidth 2, no fill, round cap and join." },
|
|
604
|
+
operations: {
|
|
605
|
+
type: "array",
|
|
606
|
+
description: "Pen operations, applied in order after `paths` are added.",
|
|
607
|
+
items: {
|
|
608
|
+
type: "object",
|
|
609
|
+
properties: {
|
|
610
|
+
op: {
|
|
611
|
+
type: "string",
|
|
612
|
+
enum: ["add", "move", "handles", "mirror", "bend", "smooth", "corner", "insert", "cut", "erase", "delete", "close", "open", "paint", "style", "translate"],
|
|
613
|
+
},
|
|
614
|
+
path: { description: "A path index from getVector — or, for add, a path object like those in `paths`." },
|
|
615
|
+
point: { type: "number" },
|
|
616
|
+
segment: { type: "number" },
|
|
617
|
+
t: { type: "number", description: "Where along the segment, 0–1 (0.5 by default)." },
|
|
618
|
+
to: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
619
|
+
through: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
620
|
+
by: { type: "object", properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
621
|
+
in: { type: ["object", "null"], properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
622
|
+
out: { type: ["object", "null"], properties: { x: { type: "number" }, y: { type: "number" } } },
|
|
623
|
+
mode: { type: "string", enum: ["none", "angle", "mirror"] },
|
|
624
|
+
fill: { type: ["string", "null"] },
|
|
625
|
+
style: VECTOR_STYLE_SCHEMA,
|
|
626
|
+
},
|
|
627
|
+
required: ["op"],
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
parent: { type: "string", description: "Id of the layer to draw a new drawing inside. Omit for the board." },
|
|
631
|
+
at: {
|
|
632
|
+
type: "object",
|
|
633
|
+
description: "Where a new drawing's 0,0 lands, in the parent's coordinates (the board's without one).",
|
|
634
|
+
properties: { x: { type: "number" }, y: { type: "number" } },
|
|
635
|
+
},
|
|
636
|
+
name: { type: "string", description: "The new drawing's layer name." },
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
},
|
|
544
640
|
{
|
|
545
641
|
name: "exportHtml",
|
|
546
642
|
description:
|
package/package.json
CHANGED