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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # codecaine-cli
2
2
 
3
- Connects the [Codecaine](https://editor.codecainehq.com) web editor to your
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://editor.codecainehq.com`) |
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: there is nothing to start, connect, configure or open first.",
7297
+ "Your provider may show them prefixed \u2014 `mcp__bb-bridge__listLayers` \u2014 as bb hands them over as a session MCP server named `bb-bridge`. Prefixed or not, they are the editor's own, already connected: nothing to start, connect, configure or open first.",
7209
7298
  "It is the NAME that makes a tool this design's, never the prefix. A tool not listed above does not touch this design, however it sounds. Never use one for work on this board, or to explain a failure on it.",
7210
7299
  "THE BOARD SHOWS WHAT YOU ARE DOING. Every write you make already puts a band of colour round the layer for a few seconds. holdLayer is the longer claim: it keeps a layer for a whole job, says what you are doing to it, and refuses the person's edits meanwhile. Take one before your first change when you are REWRITING it \u2014 redesigning, restyling, replacing its insides \u2014 so nobody edits under a half-applied change. Reading takes nothing; nor do variants BESIDE one, which are new layers.",
7211
7300
  "Release before your turn ends even when you are giving up, a tool failed, or you are handing the question back \u2014 say so. askAboutLayer keeps the hold and puts your question on the board; holdLayer picks it back up. The person can take a layer back with the button beside its name, and a hold whose assistant goes quiet frees itself; getLayer reports the run on a layer, and an edit to one no longer held still lands.",
7212
7301
  "If a tool reports it cannot reach the editor, the tool has already retried: report its message as it stands \u2014 do not assume more than it says \u2014 and stop. If the tools are missing from this session altogether, say THAT, plainly, first. In neither case substitute another application's tools or edit the design's files: the editor overwrites them.",
7213
7302
  "WRITE ORDINARY WEB MARKUP. replaceLayer and insertHtml take any HTML \u2014 sections, tables, svg, canvas, forms with real inputs. Lay it out with flexbox and grid, which the inspector edits; absolute positioning only on the outermost element the board places. Linked stylesheets are ignored. Behaviour goes on a layer in a webpage (updateLayer's `script`, or script tags in markup) and runs in Interact and Preview with `element` as that layer, never on the canvas.",
7214
- "TO SEE AN EFFECT WORK: setPageView (in-view, motion playing), then observePage to read it or canvasPointer to use it with a real mouse. Restructure as a drag would with moveLayer, groupLayers and duplicateLayers.",
7303
+ "TO SEE AN EFFECT WORK: setPageView (in-view, motion playing), then observePage to read it or canvasPointer to use it with a real mouse. Restructure like a drag with moveLayer, groupLayers and duplicateLayers.",
7215
7304
  "A LIVE PROJECT ON THIS MACHINE (localhost, LAN): a change asked for there is done only once SAVED in its source \u2014 a layer-tool edit is discarded at the next rebuild. Write it as the project's own code: the component or stylesheet that renders it, its Tailwind classes and theme variables, no inline styles or hard-coded colours. Then check LIGHT AND DARK MODE: every colour from a token with a value for each (listVariables; a new one gets both), and look at the page in each (setPageView theme light, then dark, then system) before saying done. A page served from the web has no source here: say so.",
7216
7305
  "COPYING BETWEEN LIVE PROJECTS: a copy is the original, never a redesign. Read it in its source project first; keep its markup, words, data and look. Each colour, size, radius, shadow and font becomes var(--name) holding that same value: the target's own (listVariables) only if equal, else a new one (setVariable).",
7217
7306
  'Start by looking: getSelection is what the person is pointing at when they say "this"; listLayers gives bearings; listVariables gives the design system behind it \u2014 change a shared colour there, not on every layer.'
@@ -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 (about 240 MB) \u2014 after that it starts in seconds.</p>
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
- return { state: () => state, ready };
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.2";
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) : { 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).")) };
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://celebrated-souffle-e3f7bf.netlify.app";
13248
- var PUBLISHED_ORIGINS = ["https://editor.codecainehq.com", "https://celebrated-souffle-e3f7bf.netlify.app"];
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 about 240 MB)\u2026");
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) => spawnSync(process.platform === "win32" ? "where" : "which", [command], { stdio: "ignore" }).status === 0;
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 (spawnSync("claude", ["mcp", "get", "codecaine"], { stdio: "ignore" }).status === 0) {
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 = spawnSync("claude", ["mcp", "add", "codecaine", "-s", "user", "--", "npx", "-y", "codecaine-cli", "mcp"], { stdio: "ignore" });
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: there is nothing to start, connect, configure or open first.",
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 as a drag would with moveLayer, " +
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecaine-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Connects the Codecaine web editor to this machine: project folders, live projects, and an MCP server for your coding agent.",
5
5
  "type": "module",
6
6
  "bin": {