nudge-canvas 0.0.1 → 0.4.0

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.
Files changed (41) hide show
  1. package/LICENSE.md +51 -0
  2. package/README.md +51 -3
  3. package/THIRD_PARTY_NOTICES.md +2373 -0
  4. package/data/icon-catalog.json +1 -0
  5. package/data/licenses/Apache-2.0.txt +73 -0
  6. package/data/licenses/CC-BY-3.0.txt +319 -0
  7. package/data/licenses/CC-BY-4.0.txt +156 -0
  8. package/data/licenses/CC0-1.0.txt +121 -0
  9. package/data/licenses/ISC.txt +7 -0
  10. package/data/licenses/MIT.txt +18 -0
  11. package/data/licenses/OFL-1.1.txt +43 -0
  12. package/dist/chunk-D6z-TZJJ.js +1 -0
  13. package/dist/chunk-D_nbgLii.js +14 -0
  14. package/dist/chunk-DuSDpWhn.js +550 -0
  15. package/dist/chunk-Sq5RPyiX.js +1 -0
  16. package/dist/init.js +2 -0
  17. package/dist/mcp.js +151 -0
  18. package/editor/apple-touch-icon-180.png +0 -0
  19. package/editor/assets/main-Cmp_J2OR.js +45 -0
  20. package/editor/assets/main-D0fYiDhO.css +1 -0
  21. package/editor/assets/proto-q6SnqtFF.js +1 -0
  22. package/editor/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  23. package/editor/assets/src-C4_CxgrI.js +415 -0
  24. package/editor/favicon-16.png +0 -0
  25. package/editor/favicon-32.png +0 -0
  26. package/editor/favicon.svg +1 -0
  27. package/editor/fonts/pretendard/LICENSE.txt +94 -0
  28. package/editor/fonts/pretendard/PretendardVariable.woff2 +0 -0
  29. package/editor/fonts/pretendard.css +8 -0
  30. package/editor/index.html +20 -0
  31. package/editor/proto.html +15 -0
  32. package/icons/cib.LICENSE.txt +124 -0
  33. package/icons/cib.json.gz +0 -0
  34. package/icons/logos.LICENSE.txt +124 -0
  35. package/icons/logos.json.gz +0 -0
  36. package/icons/lucide.LICENSE.txt +46 -0
  37. package/icons/lucide.json.gz +0 -0
  38. package/icons/simple-icons.LICENSE.txt +86 -0
  39. package/icons/simple-icons.json.gz +0 -0
  40. package/package.json +55 -5
  41. package/not-yet.js +0 -3
@@ -0,0 +1 @@
1
+ export{B as ancestors,h as channelFlag,_ as freePort,n as liveCanvas,H as recordSeen,b as samePath,a as startServer}from"./chunk-DuSDpWhn.js";
package/dist/init.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{n as e}from"./chunk-D_nbgLii.js";process.exit(await e(process.argv.slice(2)));export{};
package/dist/mcp.js ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ import{$ as e,Ct as t,J as n,K as r,Q as i,S as a,St as o,X as s,Y as c,Z as l,_t as u,at as d,bt as f,ct as p,dt as m,et as h,ft as g,g as _,gt as v,ht as y,it as b,lt as x,mt as S,nt as C,ot as w,pt as T,q as E,rt as D,st as O,tt as k,ut as A,v as ee,vt as te,xt as ne,yt as re}from"./chunk-DuSDpWhn.js";import{McpServer as ie}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as ae}from"@modelcontextprotocol/sdk/server/stdio.js";import{mkdirSync as j,readFileSync as oe,realpathSync as se,statSync as ce,writeFileSync as M}from"node:fs";import{dirname as N,extname as le,isAbsolute as ue,join as de,relative as P,resolve as F}from"node:path";import{z as I}from"zod";var L=class e{url;project;port;seen=0;constructor(e,t,n=0){this.url=e,this.project=t,this.port=n}static async connect(t,n){return new e(await e.canvasFor(t,n),t,n)}static async canvasFor(t,n,r=3){try{return await e.startOrFind(t,n)}catch(i){if(i?.code!==`EADDRINUSE`||r<=1)throw i;return await new Promise(e=>setTimeout(e,200+Math.random()*400)),e.canvasFor(t,n,r-1)}}static async startOrFind(e,t){let{liveCanvas:n,samePath:r}=await import(`./chunk-Sq5RPyiX.js`),i=await n(e);i&&(t=i.port);let a=`http://localhost:${t}`,o=await fetch(`${a}/api/health`).then(e=>e.json()).catch(()=>null);if(o?.project&&r(o.project,e))return a;let{freePort:s,startServer:c}=await import(`./chunk-Sq5RPyiX.js`);return(o||await fetch(a,{signal:AbortSignal.timeout(2e3)}).then(()=>!0,()=>!1))&&(t=await s()),(await c({project:e,port:t})).url}reviving=null;revivedAt=0;revive(){return ee()?Promise.reject(Error(`Nudge was disconnected from the agents on this computer (nudge-canvas init --remove). To use it again, run nudge-canvas init and start a new session.`)):!this.reviving&&Date.now()-this.revivedAt<3e4?Promise.reject(Error(`The canvas isn't running; it will be tried again shortly.`)):this.reviving??=(this.revivedAt=Date.now(),e.canvasFor(this.project,this.port).then(e=>{this.url=e}).finally(()=>{this.reviving=null}))}async req(e,t,n){let r=()=>fetch(`${this.url}/api/${t}`,{method:e,headers:n?{"content-type":`application/json`}:void 0,body:n?JSON.stringify(n):void 0}),i=await r().catch(async()=>{if(!this.port)throw Error(`The canvas isn't running.`);return await this.revive(),r()});if(!i.ok){let e=await i.json().catch(()=>({error:i.statusText}));throw Error(e.error)}return i.headers.get(`content-type`)?.includes(`json`)?i.json():i.arrayBuffer()}linkSession(e,t,n){return this.req(`POST`,`sessions/link`,{sessionId:e,client:t,channel:n})}waitRequest(e){return this.req(`GET`,`sessions/wait?sessionId=${encodeURIComponent(e)}`).then(e=>e.request)}replyToCanvas(e,t,n){return this.req(`POST`,`sessions/reply`,{chatId:e,text:t,...n??{}})}comment(e){return this.req(`POST`,`comments`,{...e,actor:`claude`})}systemFile(e){return this.req(`GET`,`system/file?path=${encodeURIComponent(e)}`)}async peek(){let e=await this.req(`GET`,`doc`);return{...e,doc:D(e.doc)}}async state(){let e=await this.req(`GET`,`doc`);return this.seen=Math.max(this.seen,e.version),{...e,doc:D(e.doc)}}async assetFromBytes(e,t){let n=await fetch(`${this.url}/api/asset`,{method:`POST`,headers:{"content-type":t},body:new Uint8Array(e)}),r=await n.json();if(!n.ok||!r.asset)throw Error(r.error??`Couldn’t store the image (${n.status})`);return r.asset}licenses(e){return this.req(`GET`,e?`licenses?roots=${encodeURIComponent(e.join(`,`))}`:`licenses`)}iconSearch(e,t,n=12){return this.req(`GET`,`icons/search?q=${encodeURIComponent(e)}&limit=${n}${t?`&pack=${encodeURIComponent(t)}`:``}`)}iconAsset(e,t,n){return this.req(`POST`,`icons/asset`,{pack:e,name:t,...n===void 0?{}:{stroke:n}})}iconDownload(e){return this.req(`POST`,`icons/download`,{pack:e})}async assetFromUrl(e){return(await this.req(`POST`,`asset/url`,{url:e})).asset}async ops(e,t){let n=await fetch(`${this.url}/api/ops`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({ops:e,label:t,actor:`claude`,measure:!0,seen:this.seen})}),r=await n.json().catch(()=>({error:n.statusText}));if(n.status===409&&typeof r.version==`number`)throw this.seen=Math.max(this.seen,r.version),Error(r.error);if(!n.ok)throw Error(r.error??n.statusText);return this.seen=Math.max(this.seen,r.version),r}async flatten(e,t){let n=await this.req(`POST`,`flatten`,{ids:e,label:t,actor:`claude`,key:`v`});return this.seen=Math.max(this.seen,n.version),n}async rasterize(e,t){let n=await this.req(`POST`,`rasterize`,{ids:e,label:t,actor:`claude`});return this.seen=Math.max(this.seen,n.version),n}hasApp(){return this.req(`GET`,`project`).then(e=>e.app)}async startSeen(){let e=await this.req(`GET`,`health`);this.seen=Math.max(this.seen,e.version)}openBrowser(){return this.req(`POST`,`open-browser`,{})}unseen(){return this.req(`GET`,`changes?since=${this.seen}&actor=designer`).then(e=>e.entries)}async render(e,t){return Buffer.from(await this.req(`GET`,`render?id=${encodeURIComponent(e)}&scale=${t}`))}compare(e,t,n){return this.req(`GET`,`compare?id=${encodeURIComponent(e)}&url=${encodeURIComponent(t)}${n?`&selector=${encodeURIComponent(n)}`:``}`)}frameChanges(e){return this.req(`GET`,`frame-changes?id=${encodeURIComponent(e)}`)}videoFrames(e,t,n,r){return this.req(`GET`,`video-frames?path=${encodeURIComponent(e)}&from=${t}&to=${n}&step=${r}`)}installBrowser(){return this.req(`POST`,`browser/install`,{})}sourceCheck(e){return this.req(`GET`,`source-check?id=${encodeURIComponent(e)}`)}motionCheck(e){return this.req(`GET`,`motion-check?id=${encodeURIComponent(e)}`)}acceptFrame(e){return this.req(`POST`,`frame-changes/accept`,{id:e})}async exportFiles(e,t){let n=await fetch(`${this.url}/api/export`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({ids:e,...t?{settings:t}:{}})});if(!n.ok)throw Error((await n.json().catch(()=>({error:n.statusText}))).error);return{name:decodeURIComponent(n.headers.get(`x-nudge-filename`)??`export`),files:JSON.parse(decodeURIComponent(n.headers.get(`x-nudge-files`)??`[]`)),data:Buffer.from(await n.arrayBuffer())}}layout(e){return this.req(`GET`,`layout?id=${encodeURIComponent(e)}`)}};function R(e){let t=F(e);try{return se.native(t)}catch{}let n=N(t);return n===t?t:F(R(n),P(n,t))}function z(e,t){let n=R(F(e,t)),r=P(R(e),n);return!r||r.startsWith(`..`)||ue(r)?null:n}var fe=`Nudge is a design canvas the user edits by hand while you edit it through these tools: both of you change one document, drawn with real browser CSS.
3
+
4
+ Start every design task with get_context, even when you only need to ask what they want (a request to design a page or screen is one). The session's first Nudge result carries the full guide: the node model, layout and sizing, styles and design systems, components and UI kits, icons, implementing a frame in code, prototypes. Read it before you build and follow it for the whole session.
5
+
6
+ Essentials:
7
+ - The first time you use the canvas, tell the user where it is (the canvas: link) in your reply.
8
+ - Build a whole screen in one create call with nested auto-layout trees. Frames hold content; shapes are drawn marks; icons come from the icon tool, not from drawn shapes.
9
+ - Repeated items (cards, list rows, buttons) are one component with instances, matching in structure, styles and size.
10
+ - With a design system, use its styles and roles, never raw values where one fits.
11
+ - Every write returns lint issues measured in a real browser: fix them, and render the frame to look at it, before you say it's done.
12
+ - The user edits too: read before you change what they touched, and keep their changes.
13
+ - Implementing a frame in code follows the guide's IMPLEMENTING A FRAME; when get_context says the folder has no app, ask before writing code.
14
+ - A question with a few possible answers, asked in a canvas chat: use the ask tool (or reply's question and choices) so they show as buttons, not a numbered list; add at most one short line of text.`,pe=`Nudge is a design canvas the user edits by hand while you edit it through these tools. Both of you change the same document; the canvas renders it with real browser CSS, so what you build is exactly what ships.
15
+
16
+ MODEL
17
+ - Node types: frame (containers, cards, images and videos via fills), text, shape (drawn geometry: rect, ellipse/arc, line, polygon, pen path; editable by the user), vector (an imported SVG asset, recolor only).
18
+ - Shapes vs frames: a box that holds content is a frame. A drawn mark (icon part, divider line, blob, logo geometry, chart arc) is a shape. Combine shapes into one outline with a boolean group (see Tree node).
19
+ - Frame layout: mode "free" (children placed by x/y) | "row" | "column" (auto layout).
20
+ gap: number (px between children). pad: n | [vertical, horizontal] | [top, right, bottom, left].
21
+ alignMain: "start" | "center" | "end" | "between" (space-between: children spread along the main axis, gap stays as their least spacing; set it back to start/center/end to turn it off). alignCross: "start" | "center" | "end".
22
+ gap: "between" is read as alignMain "between".
23
+ - Sizing per axis (sizeX, sizeY): "fixed" (uses w/h) | "hug" (fits content) | "fill" (takes remaining space in an auto-layout parent).
24
+ Text: hug/hug = single line auto width; sizeX "fixed" or "fill" + sizeY "hug" = wraps.
25
+ - absolute: true takes a child out of auto-layout flow (badges, floating buttons); position with x/y.
26
+ - x/y only matter for root frames, children of free frames, and absolute children.
27
+ - The canvas (page) color behind the frames: {op:"doc", canvas:"#hex"|"$style"|null}.
28
+ - Ruler guides are canvas-wide lines the user drags from the rulers: {op:"doc", guides:[{axis:"x"|"y", at}]} replaces them (x = vertical line at that x).
29
+ - Colors: "#hex" or a style ref "$name". Text styles: style: "$name". Prefer style refs over raw values, and spacing values from the space scale.
30
+ - Stroke is drawn inside and never changes layout.
31
+ - Text styles govern typography (fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textCase), like Figma. Don't combine style with those overrides: to change them, drop the style (style: null) and set values, or use/define another style. color and textAlign may be set alongside a style.
32
+ - In update, null removes an optional prop (fill, stroke, radius, effects, opacity, blend, note, and text overrides so the text falls back to its style).
33
+ - Any layer may carry exports:[{format:"png"|"jpg"|"svg"|"pdf", scale?:"2x"|"512w"|"512h" (png/jpg), suffix?}] — the user's Export settings (Figma). The export tool writes the files.
34
+
35
+ WORKFLOW
36
+ 1. get_context first: frames, selection ("this"/"이거" means the selection), styles, open comments. A design request (a page, a screen, "디자인해 줘") is work on this canvas: call get_context before you answer, even if you only need to ask what they want.
37
+ The first time you use the canvas in a session, Nudge opens it in the user's browser if no tab shows it yet; either way, tell the user in your reply where it is (the canvas: line, http://localhost:…) in case it didn't open.
38
+ 2. Build a whole screen in one create call with nested trees. Use auto layout (row/column) for structured UI; free only for visual composition.
39
+ Screen widths (unless the user or existing frames say otherwise): desktop web 1440, tablet 820, mobile 390 (360 narrow / 430 wide). Same widths as Import and the browser's device views.
40
+ Repeated items (cards, list rows, tabs, chips) are one design used several times: same structure, same text styles, same padding and radius. Side by side in a row they share one height: sizeY "fill" on each (the tallest sets the row), and keep their copy of similar length so one doesn't wrap alone. A first draft must already be consistent; the user shouldn't have to tidy it.
41
+ 3. Every write returns lint: warnings (invalid sizing combos, auto-resolved) and issues measured in a real browser (text overflow, clipped, collapsed, overflows parent, uneven-items / inconsistent-items: matching items that differ in size or style, and repeated-copies: three or more of the same item that aren't instances of one component). Fix issues before reporting done.
42
+ 4. render the frame and look at it before saying you are finished.
43
+ 5. History labels (the label arg) show in the canvas UI, which is English: keep them short and English ("Create dashboard"). Design content itself stays in whatever language the product uses.
44
+ 6. The user may have hand-edited since your last read. Re-read before editing something you created earlier. Preserve their values unless asked to change them.
45
+
46
+ CANVAS CHAT (requests the user types on the canvas)
47
+ When this session is linked to the canvas as a channel, the user's canvas requests arrive here as <channel source="nudge" chat_id="…">. They are part of this conversation: do what they ask with these tools (the request lists the selection), then call reply { chat_id, text } with 1-3 short sentences in the user's language — that is what the canvas chat shows. Always reply, even when you could not do it.
48
+
49
+ COMMENTS (notes the user pins on the canvas)
50
+ The user collects comments while reviewing, then sends the queued ones to you as one request listing each comment id, the layer it is pinned on, and its text. Handle them all in that run, and after each call reply { comment_id, text } (one short sentence on what you did, in the user's language) with resolve: true when it's done; leave it open and say why if you couldn't or need their decision. get_context lists open comments ([queued] = not sent yet: act on those only when the user asks).
51
+
52
+ IMPLEMENTING A FRAME (handoff to code)
53
+ When you build a frame as real code (the user asks to implement it, add motion, make it work), close the loop on the canvas — the user never types the link:
54
+ 0. No app in the project (no package.json, no pages or app code) and the user asked to implement: don't quietly make a prototype instead. Say there is no app here and let them choose: a prototype (a live page of the frame, see PROTOTYPES; it isn't code to ship) or a new app in this folder (ask which stack if they have no preference; a plain Vite + React app by default), which you create, then implement the frame in it as below.
55
+ 1. Implement it in the project and run it locally (start the dev server in the background if it isn't running). If the file has a design system, link design/system/tokens.css from the page and use its variables; never copy its values into your own CSS (a copy stops following the canvas: the user changes a color on the canvas and the site keeps the old one).
56
+ 2. Link it to the frame: update { changes: [{ id: <root frame>, set: { build: { url: "http://localhost:<port>/<route>", selector?: "<CSS selector of the matching element>" } } }] }. The user opens the live page from the frame (a browser tab, motion and all).
57
+ 3. Write where each part is implemented, in the same update: on the root frame and on every component and section you built as its own piece of code (header, hero, a card, a list row), set { source: { file: "src/components/Card.tsx", symbol?: "Card", selector?: ".card" } }. file is relative to the project (absolute paths and ../ are refused); symbol is the component or function in that file; selector finds that element on the built page and must reach exactly one element (for repeated cards, link the first, e.g. ".card:first-of-type"). The layers inside inherit the link: don't link every text and icon. The user sees it in the Code tab and opens the file from there; you use it later to find the code to change. A live prototype (PROTOTYPES) takes no source links: its page is drawn from the design, and motion.css is not where a layer is implemented.
58
+ 4. Check it: render { id, compare: true } reports text-by-text position/size/type differences and an overlay of differing pixels, and checks every source link (file exists, symbol is in it, selector finds exactly one element). Fix the code and the broken links, and compare again until it matches (motion is not compared; the user watches it live).
59
+ 5. Rebuilding at the same URL needs no re-link. To unlink: set { build: null }; a source link: set { source: null }.
60
+ 6. Icon licenses: when the design uses icons from icon sets, design/LICENSES.md (Nudge keeps it current) has each set's license and, for CC BY sets, the credit to show. Carry it into the product, every time, without being asked: a /licenses page or a NOTICE file on the web, the open source licenses screen in an app, and the credit line where the design is shown. Copying icons without their license breaks it.
61
+ A frame that already has a build (imported from the user's app, or implemented before) is the app's screen: don't rebuild it from the design. Call changes { id } for what the user changed since it matched, edit only those parts of the existing code (the source links in read and changes say which file and component each layer lives in; a layer without one inherits its nearest linked parent's), compare, then changes { id, accept: true }. Link the parts that have no source link yet as you go.
62
+
63
+ DESIGN SYSTEM (get_context shows it; the design_system tool reads and changes it)
64
+ A document can have one design system, in a light and a dark theme. Either a published system (shadcn/ui, Material 3, Carbon, Primer, Ant Design) — that system's own tokens under its own names, e.g. $carbon/text-primary, $carbon/layer-01, style "$carbon/body-01", radius "$carbon/border-radius-04", $material3/on-surface-variant (shadcn/ui's are bare: $primary, $muted-foreground); design_system get lists them all — use them as that system's documentation describes, don't map them onto other names. Or one made from scratch: a few foundation slots (display and body fonts, background, text and accent colors, a corner character) from which these roles follow, referenced like any style:
65
+ - Colors: $background $foreground $card $muted $muted-foreground $border $input $primary $primary-foreground $primary-subtle $primary-subtle-foreground $ring $destructive $success $warning $info; tags, alerts and banners on $destructive-subtle / $success-subtle / $warning-subtle / $info-subtle with the matching -subtle-foreground text. Text: style "$display" "$heading-l" "$heading-m" "$heading-s" "$body-l" "$body-m" "$label" "$caption". Spacing from the scale. Corner radius as a token: radius "$radius-sm" | "$radius-md" | "$radius-lg" | "$radius-xl" | "$radius-full" (it follows the system's corner character); a number only when no token fits.
66
+ - With a system, build only from it: no raw hex where a role fits, no font sizes by hand where a text style fits. A color it has no role for means a role is missing: say so (and, if the user agrees, add it with design_system overrides) rather than inventing one.
67
+ - From code or a file: when the project or the user already has a design system, import it under its own names with design_system { action: "import" } — file for CSS variables, a page's <style>, W3C tokens or Tokens Studio / Style Dictionary JSON; for any other form (a CSS framework config, SCSS, a JS theme object, a design tool's variables read through that tool's own MCP server) write it as W3C tokens JSON yourself, keeping every name and value as they are, and pass it as content. Importing replaces the file's system: if there is one, ask the user first.
68
+ - Themes per frame: theme: "light" | "dark" on a frame makes it and everything in it paint with that theme's colors (unset = the canvas theme). For a dark version of a screen, duplicate it and set theme: "dark" on the copy: the roles resolve themselves, never recolor by hand.
69
+ - Changing the system restyles every screen bound to it. Change it when the request is about the system ("make the brand green", "tighter type", "dark theme"), not to adjust one screen.
70
+ - Nudge writes the system for code to design/system/ (tokens.css with the same variable names, tokens.json, README.md). When you implement a frame, import tokens.css and use its variables instead of copying values.
71
+
72
+ SENDING TO FIGMA
73
+ Nudge doesn't connect to Figma itself. When the user wants a frame in Figma, use Figma's own MCP server if it's connected (tools like generate_figma_design): export { id, format: "html" } gives a localhost page of the frame, and Figma's tool captures that page into their file. If it isn't connected, tell them they can add it (Connect → Recommended MCP servers in the canvas).
74
+
75
+ ICONS
76
+ Icons come from icon packs, never drawn with shapes or left out: icon { action: "search", query } then icon { action: "insert", icon, parent, size }. The design's pack is in get_context (icons:) and the design system (design_system set icons); search looks there and in the brand logos (Kakao, Google, Apple, Microsoft, Naver, GitHub, LinkedIn…: sign-in buttons, social links): one color (simple-icons, cib) and in the brand's own colors (logos, marked multicolor). A logo is its company's trademark: use it as that brand presents it. A sign-in button takes the brand's guideline logo, in color where the brand has one: Google's four-color G (logos/google-icon), Microsoft's four squares (logos/microsoft-icon); Apple's is one color (simple-icons/apple, black or white); Kakao's is its black speech bubble on yellow (simple-icons/kakaotalk). An icon is a vector layer named icon/<name>, colored with the text role by default (color: any color or $role), stroke width for outline packs (stroke). Multicolor sets (flags, color logos, emoji; marked multicolor) keep their own colors: never give them a color. A set under CC BY needs its author credited: the insert result says who. Swap one for another with action "swap". Packs other than Lucide and the brand logos download on first use: ask the user, then download.
77
+
78
+ COMPONENTS (main components and instances, as in Figma)
79
+ Repeated cards, list rows and buttons: build one, make it a main component (component { action: "create", ids }), then place instances of it (component { action: "instance", main, parent }, or create with { type: "frame", instanceOf: <main id> }) instead of copying layers. The outline marks them main-component and instance-of:<main>. An instance's layers have ids "<instance id>/<main layer id>": update them like any layer to change that copy only (text, fills, strokes, effects, names, visibility; the instance itself also its size). That change is the instance's override and stays when the main changes; everything else follows the main. Adding, removing or moving layers, and positions inside, only happen in the main: change the main to change every copy. A request about "all the cards" goes to the main; one about a single card to that instance. component { action: "reset" } puts back an instance's changes (keys, a layer, or all), "push" writes them into the main, "detach" makes an instance a plain frame.
80
+ Variants and properties: buttons, inputs and the like that differ only in state, size or style are one component set, not separate components. Build one main per variant, then component { action: "combine", ids, name } puts them in a set; name each main "Property=Value, Property=Value" first (e.g. "Type=Primary, Size=Small") so the set gets those variant properties. "add_variant" copies a variant into a new one. Then give the set properties for what an instance changes: the label is a Text property, whether an icon shows is a Boolean one, which icon (or which nested component) is an Instance swap one: component { action: "prop", id: <set>, name, type: "text" | "boolean" | "swap" }, then connect the layer in each variant: component { action: "bind", id: <layer>, content | visible | swap: <property> }. An instance changes through its properties, not overrides: component { action: "set", id: <instance>, values: { Type: "Ghost", Label: "Save", "Show icon": false } } (a Variant value switches the variant; changes to layers with the same name carry over). An icon swap value is { icon: { pack, name }, asset } from icon search; a component swap value is a main's id. The outline shows component-set, variant:{…}, props:{…} and an instance's values:{…}. One variant property per axis: Type and Size are two properties ("Type=Primary, Size=Small"), never one combined value like "Primary / Small".
81
+
82
+ UI KITS (ready-made components drawn with the design system)
83
+ get_context lists the UI kits and whether each is on. With a kit on, buttons, inputs, selects, checkboxes, switches, badges, cards, tabs, alerts and menus are its instances: component { action: "instance", main: "<kit>/<component>" } (e.g. "shadcn/Button"), then component { action: "set", id, values } for its variant and its text (Label, Placeholder, Title…). Don't draw what the kit has, and don't restyle its instances with fills; build only what the kit lacks, with the design system's roles. Kit components can't be edited; component { action: "adopt", id } copies one into the file to change it. A kit is off unless the user asked for it or turned it on: component { action: "kit", name, on: true } (it brings its own design system when the file has none).
84
+
85
+ PROTOTYPES (a working page when there is no app to implement in)
86
+ When the user wants a frame to move or work (motion, hover, a flow) and the project has no app for it (or they ask for a prototype; asked to implement with no app, offer the choice first, IMPLEMENTING A FRAME 0): the prototype page IS the frame, drawn live from the document by Nudge's renderer, so every design edit shows at once. You add only the motion layer, in design/builds/<root frame id>/:
87
+ - motion.css: animations, transitions and hover/press states. Target layers by id: [data-node-id="n_…"] (ids from read). The renderer sets layout, fills and text as inline styles, so change those with !important; prefer transform, opacity, filter and box-shadow. Fills are drawn as background-image layers (a solid fill too): recolor with background: <color> !important, not background-color. An entrance animation with fill-mode both/forwards keeps owning transform afterwards, so move things on hover with the translate/scale properties (or a transition on another layer), not transform.
88
+ - motion.js (optional, plain script): behavior CSS can't do (cursor follow, scroll effects, sequences). window.nudge gives { frame, el(id) → element, onRender(cb) } — onRender runs after every redraw (the user may edit while the page is open), so (re)bind listeners there.
89
+ Don't write an index.html (a folder with one is an older hand-written prototype and replaces the live page). Link the frame: build { url: "<the prototypes: address get_context prints><root frame id>/" }, that address as it is (it is already the prototype port: add nothing to it). To express a state the document can't (a hover look), prefer adding it to motion.css over changing the design. Canvas runs may write only in design/builds/. Prototypes are not the app: when there is one, implement there instead (IMPLEMENTING A FRAME).
90
+
91
+ VARIANTS (try a change without touching the app)
92
+ A copy of a frame that has a build (duplicate) is a variant: the user edits the copy and asks you to implement it "as a variant" / "on a branch" / "without touching the original". Then:
93
+ 1. In the project (a git repo; if it isn't, ask the user before running git init), make a worktree: git worktree add ../<repo>-nudge-<slug> -b nudge/<slug>. Their uncommitted work stays where it is.
94
+ 2. Start the dev server inside the worktree on a free port, in the background (install dependencies there first if needed).
95
+ 3. changes { id: <variant> } lists what the user changed on the copy: implement exactly those, in the worktree only.
96
+ 4. Link the variant: update { changes: [{ id: <variant>, set: { build: { url: "http://localhost:<port>/<route>", branch: "nudge/<slug>" } } }] } (re-linking starts its change list fresh), then render { id, compare: true }.
97
+ 5. Merge or discard only when the user says so. Merge: commit in the worktree, merge the branch in the main checkout, then point the variant at the main dev server and drop branch. Discard: stop that dev server, git worktree remove, git branch -D, and set the variant's build back to the original's.`,me=`Tree node: { type: "frame"|"text"|"shape"|"vector", key?: string (your temp name, mapped to the real id in the result), name?: string, children?: Tree[], ...props }.
98
+ Props: x y w h sizeX sizeY absolute constraints?({x:"left"|"right"|"leftRight"|"center",y:"top"|"bottom"|"topBottom"|"center",ref:[parentW,parentH]}; children of a free frame and absolute children; a hug layer pinned right grows leftward) opacity blend("multiply"|"screen"|"overlay"|…: layer blend mode, mixes the layer with what is behind it) flipX flipY(mirror in place, contents too; shapes with paths/lines/arcs are better flipped in their geometry) rotation(any layer but a boolean group: CSS degrees, clockwise positive, around the box center, a frame's contents with it; x/y stay the unturned box's corner) hidden locked note source?({file:"src/components/Card.tsx" relative to the project, symbol?:"Card", selector?:".card"}: where the layer is implemented; see IMPLEMENTING A FRAME) effects[{type:"shadow",x,y,blur,spread,color,inset?}|{type:"layerBlur",radius}|{type:"backdropBlur",radius}|{type:"shader",shader} (over a frame's image fill; see Shader)] (inset shadow = inner shadow; visible:false hides one) |
99
+ frame: layout{mode gap(number) pad alignMain("start"|"center"|"end"|"between") alignCross wrap?(row only, flex-wrap) gapCross?(gap between wrapped lines)} clip fill("#hex"|"$ref" shorthand) fills[] (bottom first; each paint takes blend?: how it mixes with the fills below it in the same frame, e.g. a texture or gradient set to "overlay" over a color; the bottom fill has nothing under it) stroke{color width align?:"inside"(default)|"center"|"outside" visible? sides?:[top,right,bottom,left] per-side weights, e.g. a bottom rule [0,0,1,0] dash?:[dash,gap] dashed (a frame draws CSS dashes, the browser sets their length)} (never affects layout) radius(n|[tl,tr,br,bl]|"$radius-…" token) theme("light"|"dark": the design system theme it and its contents paint with) grids[{type:"columns"|"rows",count,gutter,margin,align:"stretch"|"start"|"center"|"end",size?,color}|{type:"grid",size,color}] (editor-only guides; place content on them) |
100
+ text: content style("$ref") fontFamily fontSize fontWeight fontStyle("normal"|"italic") textDecoration("underline"|"line-through", the text's own, not part of a style) textCase("upper"|"lower"|"title": the letters drawn in upper, lower or Title Case while content stays as typed, like Figma; part of a text style, set on the text it overrides the style's; null = the style's, else as typed) lineHeight(px) letterSpacing(px; a new fontSize without them scales both in proportion, as the designer sees them in %) textAlign textAlignV("top"|"center"|"bottom", fixed-height boxes) color spans([{start, end, fontFamily?, fontSize?, fontWeight?, fontStyle?, lineHeight? (px), letterSpacing?, color?, textDecoration?("underline"|"line-through"|"none"), textCase?("upper"|"lower"|"title"|"none")}]: parts of the text styled apart, start/end = character offsets into content, end excluded; they follow later edits of content; null clears them) |
101
+ shape: shape (required geometry) fill / fills[] stroke{color width align? cap?:"none"|"round"|"square" (lines and open paths) dash?:[dash,gap] (px, drawn exactly)} radius rotation(CSS degrees, clockwise positive; the user's panel shows Figma's sign, so a rotation of 30 here reads −30° there — when the user says "−30°" they mean rotation:30) — no children, sizeX/sizeY fixed or fill. Geometry is laid into the box w×h:
102
+ {kind:"rect"} (radius: n | [tl,tr,br,bl] | "$radius-…"; clamps at half the shorter side = pill) ·
103
+ {kind:"ellipse", start?:deg clockwise from 3 o'clock, sweep?:0–1 share of the turn (default 1), ratio?:0–1 inner radius (donut/ring)} ·
104
+ {kind:"line", flip?:true} (top-left→bottom-right, flipped = bottom-left→top-right; h:0 = horizontal; give it a stroke) ·
105
+ {kind:"polygon", count:3–60} (radius rounds its corners) ·
106
+ {kind:"path", points:[{x,y,in?:[dx,dy],out?:[dx,dy],r?:corner radius of this point,mirror?:"none" (handles move independently)}], closed:boolean} (points in the box's px; in/out are bezier handles relative to the point) or {kind:"path", d:"M… C… Z"} (SVG path data, no arcs; several subpaths (M … Z M … Z) become contours of one path, filled nonzero: draw a hole the other way round; omit w/h and the box fits the path; in an update without x/y, d is in the node's own px and it stays where it is). Open paths: stroke, no fill by default; closed: fill. radius on a path (one number) rounds every sharp corner as it's drawn, convex and concave; the points stay as written. More contours of the same path: subpaths:[{points, closed}] (same units as points; e.g. what flatten writes).
107
+ To move one corner of a rect (or reshape an ellipse/polygon/line point by point): update set { shape: { kind: "path" } } with no points converts its current outline to path points (a rect = its 4 corners, a polygon = its N points, their radius kept as the path's radius or each point's r; ellipses and arcs become curves; same look), then update the points.
108
+ Defaults like the user's tools: rect/ellipse/polygon fill "#d9d9d9", line/path stroke {color:"#000000",width:1}.
109
+ Boolean group: a frame with boolean:"union"|"subtract"|"intersect"|"exclude" and shape (or boolean) children positioned inside it; it draws their combined outline with its own fills/stroke (children[0] is the bottom: subtract = first child minus the others). radius on the group (one number) rounds the corners of the RESULT, e.g. a union of overlapping squares becomes connected blobs; squares that only touch at a corner get no join (add a small square over the touch point, side ≈ 4× radius, to make a smooth neck). Its x/y/w/h always follow the combined result (the document fits them; the children keep their place on screen); setting its w/h scales the children. Children must be shapes or boolean groups; no auto layout; no video fill; no rotation of its own (turn its shapes). Example (a rounded square with a quarter-circle bite): {"type":"frame","boolean":"subtract","fill":"#1f3a2e","children":[{"type":"shape","shape":{"kind":"rect"},"w":200,"h":200,"radius":40},{"type":"shape","shape":{"kind":"ellipse"},"x":100,"y":100,"w":200,"h":200}]}
110
+ vector: asset color.
111
+ Blend modes (fills[].blend and the layer's blend; absent = normal): normal darken multiply color-burn lighten screen color-dodge overlay soft-light hard-light difference exclusion hue saturation color luminosity.
112
+ Gradients: a fill {type:"linear", angle, stops:[{color, at}]} (angle in CSS degrees: 0 points up, 90 right, 180 down; at = 0–1 along the line; 2 stops or more) or {type:"radial", stops, center?:[x,y], radius?:[rx,ry], rotation?:deg} (an ellipse; rotation turns it about its center, degrees clockwise, default 0; center and radii are 0–1 shares of the layer's width and height, default center [0.5,0.5] and radius [0.5,0.5] = the ellipse through the middle of each edge; at 0 is the center, 1 the ellipse's edge; equal shares make an oval in a layer that isn't square: for a circle, pick rx*w = ry*h). Colors are "#hex" or "$style"; a stop can be transparent ("#rrggbb00"). Both draw as browser gradients and copy out as CSS.
113
+ Images: a fill {type:"image", src, fit:"cover"|"contain"|"fill"|"crop"} (crop: also crop:[x,y,w,h], the part of the image shown, as 0–1 shares of it; the image keeps its proportions). src is an image file in the project (path relative to it, e.g. "photos/hero.jpg") or an http(s) image URL; Nudge stores it and swaps in its asset id. When the project has photos, use them: never stand in a colored box with a file name.
114
+ Video: a frame fill {type:"video", src:"clips/hero.mp4", fit, autoplay?, loop?, muted?} (fit and crop as an image's; mp4, webm or mov in the project) plays under the frame's children (frames only: shapes and boolean groups refuse it). autoplay, loop and muted default to true; autoplay:false shows the first frame until the viewer plays it, muted:false gives it sound once played.
115
+ Shader: a frame fill {type:"shader", shader:{name, code, params?}} drawn under the frame's children (frames only), for procedural looks: mesh gradients, noise and grain, waves, patterns. code = a GLSL ES 1.0 fragment shader body that sets gl_FragColor (straight alpha); u_resolution (vec2, px), u_time (float, seconds; use it only for motion — it then animates, and holds still under reduced motion) and every param are declared for you: don't declare them. params = the controls the designer gets: {id:"u_name", label, kind:"number", value, min, max, step?} (a float) or {id:"u_name", label, kind:"color", value:"#rrggbb"} (a vec3). Grain over a picture: a second, top fill with alpha and blend:"overlay". A shader effect works on a picture itself (halftone, dither, duotone, glitch, pixelate): effects [{type:"shader", shader:{name, input:"image", code, params?}}] on a frame with an image fill; its code reads the picture as the frame shows it with image(uv) (uv = gl_FragCoord.xy / u_resolution, 0–1, y up; transparent outside the picture) and u_image/u_fit are declared for you too. Read the layer back: a shader that fails to compile draws nothing, and the designer's panel shows its log.
116
+ Defaults: frame 100×100 fixed, free layout; text hug/hug.
117
+ Example card: {"type":"frame","key":"card","name":"Card","w":343,"sizeY":"hug","fill":"$surface","radius":16,"layout":{"mode":"column","gap":12,"pad":20},"children":[{"type":"text","content":"잔액","style":"$caption"},{"type":"text","content":"1,240,000원","style":"$display"}]}`,B=e=>({content:[{type:`text`,text:e}]}),V=e=>{let t=e instanceof Error?e.message:String(e),n=t.match(/Unrecognized keys?: (.+)/)?.[1]??``;return/code|source|file|origin|impl|component|module|dev|route|url|link|data/i.test(n)&&(t+=`. To record where a layer is implemented use source: { file, symbol?, selector? } (file relative to the project); the root frame's page is build: { url, selector? }.`),{...B(`Error: ${t}`),isError:!0}};function he(e,t,n=10){let r=[],i=t=>`${e.nodes[t].name}#${t}`,a=t=>(e.nodes[t].children??[]).filter(t=>e.nodes[t]?.type===`frame`);r.push(`${i(t)} (the page)`);for(let n of a(t)){r.push(i(n));let t=a(n),o=new Map;for(let n of t){let t=e.nodes[n],r=t.component??t.name;o.set(r,[...o.get(r)??[],n])}for(let e of o.values())e.length>1&&r.push(`${i(e[0])} (first of ${e.length})`)}return r.slice(0,n)}function H(e,t){return`source links: none yet. Write where each part is implemented, in one update: { changes: [{ id, set: { source: { file: "<path relative to the project>", symbol?: "<component or function>", selector?: "<CSS selector, exactly one element on the page>" } } }, …] } for ${he(e,t).join(`, `)}. The layers inside inherit; the user opens the code from the Code tab.`}function U(e){let t=[];e.warnings.length&&t.push(`warnings (auto-resolved):`,...e.warnings.map(e=>`- ${e.id} ${e.code}: ${e.message}`));let n=e.issues.filter(e=>e.code!==`unavailable`),r=e.issues.find(e=>e.code===`unavailable`);return n.length&&t.push(`issues (measured, fix these):`,...n.map(e=>`- ${e.name}#${e.id} ${e.code}: ${e.message}`)),r&&t.push(`note: ${r.message} (the write succeeded)`),t.length?t.join(`
118
+ `):`lint: clean`}function ge(e,t=0,n){let r=h(e);if(!r.length)return{x:0,y:0};let i=n?e.nodes[n]:void 0;return i?.type===`frame`&&i.parent===null?{x:i.x+(i.build?i.w*2+80:i.w)+80+t,y:i.y}:{x:Math.max(...r.map(e=>e.x+(e.build?e.w*2+80:e.w)))+80+t,y:Math.min(...r.map(e=>e.y))}}function _e(e){return e.length?[`The user edited the canvas since your last look (keep these values unless asked):`,...e.flatMap(e=>[`- ${e.label}`,...e.lines.map(e=>` ${e}`)]),``].join(`
119
+ `):``}var W=e=>{let t=new URL(e);return t.port=String(Number(t.port)+2e3),t.origin+`/`},G=(e,t,n)=>typeof n==`string`&&n.replace(/\/?$/,`/`).startsWith(`${W(e)}${t}/`),ve={".mp4":`video/mp4`,".webm":`video/webm`,".mov":`video/quicktime`,".jpg":`image/jpeg`,".jpeg":`image/jpeg`,".png":`image/png`,".webp":`image/webp`,".gif":`image/gif`,".avif":`image/avif`,".svg":`image/svg+xml`};async function ye(e,t){let n=[],r=e=>{if(Array.isArray(e))return e.forEach(r);if(!e||typeof e!=`object`)return;let t=e;(t.type===`image`||t.type===`video`)&&typeof t.src==`string`&&n.push(t),Object.values(t).forEach(r)};if(r(t),!n.length)return t;let i=new Map;for(let t of new Set(n.map(e=>e.src))){if(/^https?:\/\//.test(t)){if(n.some(e=>e.src===t&&e.type===`video`))throw Error(`Video ${t}: put the file in the project and give its path`);i.set(t,await e.assetFromUrl(t));continue}let r=z(e.project,t);if(!r)throw Error(`Image ${t} is outside the project`);let a=ve[le(r).toLowerCase()];if(!a)throw Error(`${t}: not an image or video file (jpg, png, webp, gif, avif, svg, mp4, webm, mov)`);if(ce(r,{throwIfNoEntry:!1})?.isFile()!==!0)throw Error(`Image ${t} not found in the project`);i.set(t,await e.assetFromBytes(oe(r),a))}for(let e of n)e.asset=i.get(e.src).id,delete e.src,e.fit??=`cover`;return[{op:`asset`,add:[...new Map([...i.values()].map(e=>[e.id,e])).values()]},...t]}function be(e,t){let n=e=>Math.round(e*100)/100,r=(e,t)=>{let i=e.shape;if(i?.kind===`path`&&typeof i.d==`string`){let r=p(i.d).map(e=>({points:e.points,closed:typeof i.closed==`boolean`?i.closed:e.closed}));if(e.w===void 0&&e.h===void 0){let i=x(r);e.x=n((e.x??t?.x??0)+i.x),e.y=n((e.y??t?.y??0)+i.y),e.w=n(i.w),e.h=n(i.h),e.shape=A(i.contours,[n(i.w),n(i.h)])}else{let{view:t,...n}=A(r,[1,1]);e.shape=n}}Array.isArray(e.children)&&e.children.forEach(e=>e&&typeof e==`object`&&r(e))};for(let n of e)n?.op===`create`&&n.tree&&typeof n.tree==`object`&&r(n.tree),n?.op===`update`&&n.set&&typeof n.set==`object`&&r(n.set,typeof n.id==`string`?t?.nodes[n.id]:void 0);return e}function xe(e,n){return e.map(e=>{let r=n.nodes[e.id];if(r?.type!==`text`||typeof e.set.fontSize!=`number`)return e;let i=r.style?n.styles.text[r.style.slice(1)]:void 0,a=r.fontSize??i?.fontSize??16;return{...e,set:t(e.set,{fontSize:a,lineHeight:r.lineHeight??i?.lineHeight,letterSpacing:r.letterSpacing??i?.letterSpacing})}})}async function Se(e){let t=await e.licenses().catch(()=>({sets:[]}));return t.sets.length?`icon licenses: ${t.sets.map(e=>`${e.name} (${e.license})`).join(`, `)}. Put design/LICENSES.md into the product's licenses (/licenses page or NOTICE; an app's open source licenses screen)${t.sets.some(e=>e.credit)?` and show the credit: ${t.sets.filter(e=>e.credit).map(e=>e.credit).join(`; `)}`:``}.`:``}async function Ce(e,t){let n=await e.licenses(t).catch(()=>({sets:[],kits:[]})),r=n.sets,i=n.kits??[];if(!r.length&&!i.length)return``;let a=r.flatMap(e=>e.credit?[e.credit]:[]);return`\n\nBuilding this as code: ${[...i.length?[`its components are from ${i.map(e=>`${e.name} (${e.license})`).join(`, `)}`]:[],...r.length?[`its icons are from ${r.map(e=>`${e.name} (${e.license})`).join(`, `)}`]:[]].join(`; `)}. In the same change, without being asked (a comment in the code isn't enough): ${r.length?`use the icons themselves (export { id, format: "html" } has their SVGs; don't redraw or swap them), `:``}copy design/LICENSES.md into the product (a LICENSES.md or NOTICE file; on the web a /licenses page linked from the footer)${a.length?`, and show this credit on the page: ${a.join(`; `)}`:``}.`}function we(e){return e.checked?[e.broken.length?`source links: ${e.broken.length} of ${e.checked} broken — fix the link (update { changes: [{ id, set: { source: { file, symbol?, selector? } } }] }) or the code:`:`source links: ${e.checked} checked, all reach the code${e.note?``:` and the page`}.`,...e.broken.map(e=>`- ${e.name}#${e.id} (${e.link.file}${e.link.symbol?`#${e.link.symbol}`:``}): ${e.problems.join(`; `)}`),...e.note?[e.note]:[]].join(`
120
+ `):``}var Te=(e,t=null)=>t===`opened`?`The Nudge canvas for this folder just opened in the user's browser (${e}). Tell them, in your reply, that it's there and give the link in case it didn't show.`:t===`viewing`?`The Nudge canvas for this folder (${e}) is already open in the user's browser. Mention the link in your reply.`:`Tell the user, in your reply: the Nudge canvas for this folder is at ${e} — open it in a browser.`,Ee=e=>`This session has no project folder of its own, so the canvas lives in ${e} (its design/ folder holds the document, builds and design system). Files the instructions put in design/ go there.`,De=`Nudge was disconnected from the agents on this computer (nudge-canvas init --remove), so its canvas doesn't run. Tell the user: to use it again, run nudge-canvas init and start a new session.`,Oe=e=>`Nudge needs a project folder. This agent was started in ${e}, the home folder or above it, so there is no canvas here and nothing was created. Tell the user to open the agent again from their project's folder (cd into it first), then ask again.`;function ke(t,r,a,p){let _=process.env.NUDGE_RUN===`1`,x=!1,E=(e,n,i)=>t.registerTool(e,n,async(...e)=>{if(a)return{content:[{type:`text`,text:a}],isError:!0};x||await r.startSeen?.().catch(()=>{});let t=await r.unseen().catch(()=>[]),n=await i(...e),o=_e(t);if(!_&&Array.isArray(n?.content)){_=!0;let e=process.env.NUDGE_NO_OPEN?null:await r.openBrowser?.().catch(()=>null);o=[Te(r.url,e?.opened?`opened`:e?.viewing?`viewing`:null),p&&Ee(p),o].filter(Boolean).join(`
121
+ `)}if(o&&Array.isArray(n?.content)){let e=n.content[0];e?.type===`text`?e.text=`${o}\n${e.text}`:n.content.unshift({type:`text`,text:o})}return!x&&Array.isArray(n?.content)&&(x=!0,n.content.push({type:`text`,text:`NUDGE GUIDE (sent once; it holds for the whole session)\n\n${pe}\n\nTREE NODE FORMAT (create and update)\n${me}`})),n});E(`get_context`,{description:`Canvas overview: root frames, the user's current selection, styles (colors, text styles, spacing scale), canvas URL. Call first: the session's first result also carries Nudge's guide.`,inputSchema:{}},async()=>{try{let{doc:t,selection:n,savedTo:a}=await r.state(),o=await r.hasApp?.().catch(()=>!0)??!0,c=h(t),u=[...c.slice(0,60).map(n=>`- ${e(t,n)}`),...c.length>60?[`- … ${c.length-60} more: read (no id) lists every root frame`]:[]],d=n.filter(e=>t.nodes[e]).map(e=>i(t,e,2)),f=t.styles;return B([`canvas: ${r.url}${t.name?` · project "${t.name}"`:``}`,...p?[`project folder: ${p}`]:[],...o?[]:[`app: none in this folder (no package.json). If the user asks to implement a frame, write no code files yet: tell them there is no app here and let them choose (in a canvas chat with the ask tool) — a prototype (a live page of the frame, not code to ship) or a new app in this folder (Vite + React unless they prefer another) — then do what they chose.`],...a?[`moved: the user saved this project to ${a.path} (canvas ${a.url}) and works there now; edits here no longer reach it. Tell them to open the agent in that folder to keep going.`]:[],`prototypes: ${W(r.url)}<frame id>/ draws the frame live; motion layer in design/builds/<frame id>/motion.css (+ motion.js)`,`frames (${c.length}):`,...u.length?u:[`- none`],`selection: ${d.length?`
122
+ `+d.join(`
123
+ `):`none`}`,`colors: ${Object.entries(f.colors).map(([e,t])=>`$${e}=${t}`).join(` `)||`none`}`,`text styles: ${Object.entries(f.text).map(([e,t])=>`$${e}=${t.fontSize}/${t.lineHeight??`normal`} w${t.fontWeight}${t.fontStyle===`italic`?` italic`:``}${t.letterSpacing?` ls${t.letterSpacing}`:``}${t.textCase?` case:${t.textCase}`:``} ${t.color}`).join(` | `)||`none`}`,`space scale: ${f.space.join(` `)}`,t.system?`design system "${t.system.name}" (canvas theme ${t.system.theme}; slots ${JSON.stringify(t.system.slots)}; radius tokens ${Object.entries(re(t.system.slots)).map(([e,t])=>`$${e}=${t}`).join(` `)}). Build with its roles (see DESIGN SYSTEM); design_system get for both themes.`:`design system: none`,...D(t),`icons: ${m(t)} (the design's pack; brand logos: simple-icons and cib in one color, logos in their own colors). Put icons in with the icon tool, never draw them.`,...await r.licenses().then(e=>e.sets.length?[`icon licenses: ${e.sets.map(e=>`${e.name} (${e.license}${e.credit?`, credit required`:``})`).join(`, `)} — design/LICENSES.md. When you build a frame as code, the same change adds a LICENSES.md or NOTICE to the product${e.sets.some(e=>e.credit)?` and shows the credit on the page`:``} (IMPLEMENTING A FRAME 6); a comment in the code isn't enough`]:[],()=>[]),...t.guides?.length?[`ruler guides (align to these when the user asks): ${t.guides.map(e=>`${e.axis}=${e.at}`).join(` `)}`]:[],`open comments: ${l(t).length?`
124
+ `+l(t).map(e=>s(t,e)).join(`
125
+ `):`none`}`].join(`
126
+ `))}catch(e){return V(e)}}),E(`read`,{description:`Read a subtree as a compact outline (default) or full JSON. Omit id to read every root frame.`,inputSchema:{id:I.string().optional(),depth:I.number().int().min(0).optional().describe(`Levels below the node to include. Default: all.`),format:I.enum([`outline`,`json`]).optional()}},async({id:e,depth:t,format:n=`outline`})=>{try{let{doc:a}=await r.state();if(e&&!a.nodes[e])throw Error(`unknown node "${e}"`);let o=await Ce(r,e?[e]:a.roots);if(n===`outline`)return B((i(a,e,t??1/0)||`(empty canvas)`)+o);let s=e?[e]:a.roots;return B(JSON.stringify(s.map(e=>k(a,e)),null,1)+o)}catch(e){return V(e)}}),E(`create`,{description:`Create one or more node trees in a single atomic transaction. parent null = new root frame(s) on the canvas, auto-placed to the right of existing frames unless x/y are given.
127
+ Tree node: { type: "frame"|"text"|"shape"|"vector", key?: string (your temp name, mapped to the real id in the result), name?: string, children?: Tree[], ...props }. Every prop and paint is in the guide's TREE NODE FORMAT (sent with the session's first result).
128
+ - A screen: one root frame with nested auto layout (layout { mode: "row"|"column", gap, pad, alignMain, alignCross }), sizeX/sizeY "fixed"|"hug"|"fill".
129
+ - Icons: place them with the icon tool (a real icon set), never as drawn shapes or letters.
130
+ - Repeated items (cards, list rows, buttons, chips): build one, make it a component (component { action: "create" }), then place instances of it; don't paste copies.
131
+ - Colors and text: style refs ("$name") or the design system's roles where they fit.
132
+ - Images: a frame fill { type: "image", src } with a file in the project or an http(s) URL.`,inputSchema:{parent:I.string().nullable().describe(`Parent frame id, or null for the canvas root.`),index:I.number().int().min(0).optional().describe(`Insert position among the parent's children. Default: end.`),trees:I.array(I.any()).min(1).describe(`Node trees (see description).`),label:I.string().optional().describe(`Short history label, e.g. "Create dashboard".`)}},async({parent:e,index:t,trees:n,label:i})=>{try{let{doc:a,selection:o}=await r.state(),s=o[0];for(;s&&a.nodes[s]?.parent;)s=a.nodes[s].parent;let c=0,l=n.map((n,r)=>{if(e===null&&n&&typeof n==`object`&&n.x===void 0&&n.y===void 0){let e=ge(a,c,s);c+=(typeof n.w==`number`?n.w:100)+80,n={...n,...e}}return{op:`create`,parent:e,index:t===void 0?void 0:t+r,tree:n}}),u=await r.ops(await ye(r,be(l)),i??`Create`),d=(await r.state()).doc,f=u.created.filter(t=>d.nodes[t]?.parent===e),p=Object.entries(u.ids).map(([e,t])=>`${e}=${t}`).join(` `);return B([`created ${u.created.length} nodes; top-level: ${f.join(` `)}`,p&&`keys: ${p}`,U(u)].filter(Boolean).join(`
133
+ `))}catch(e){return V(e)}}),E(`update`,{description:`Patch props on one or more nodes in a single atomic transaction. Only listed props change. layout merges (e.g. {layout:{gap:12}} keeps mode/pad). Structural changes use move. After implementing a frame in code, this is also how you connect it: build: { url } on the root frame and source: { file, symbol?, selector? } on the parts you implemented (see IMPLEMENTING A FRAME).`,inputSchema:{changes:I.array(I.object({id:I.string(),set:I.record(I.string(),I.any())})).min(1),label:I.string().optional()}},async({changes:e,label:t})=>{try{let i=e.map(e=>({op:`update`,id:e.id,set:e.set})),a=i.some(e=>typeof e.set.shape?.d==`string`),o=i.some(e=>typeof e.set.fontSize==`number`),s=a||o?(await r.peek()).doc:void 0;o&&(i=xe(i,s));let c=await r.ops(await ye(r,be(i,a?s:void 0)),t??`Update`),l=e.filter(e=>e.set.build&&typeof e.set.build==`object`&&!G(r.url,e.id,e.set.build.url)).map(e=>e.id),u=``;if(l.length){let{doc:e}=await r.peek(),t=l.filter(t=>e.nodes[t]&&!n(e,t).length);t.length&&(u=`
134
+ `+t.map(t=>H(e,t)).join(`
135
+ `))}if(e.some(e=>e.set.source)){let{doc:t}=await r.peek(),n=new Set;for(let i of e){if(!i.set.source)continue;let e=t.nodes[i.id];for(;e?.parent;)e=t.nodes[e.parent];let a=e?.type===`frame`?e.build?.url:void 0;e&&G(r.url,e.id,a)&&n.add(e.id)}for(let e of n)u+=`\nnote: ${t.nodes[e].name}#${e} is still linked to its live prototype (build ${t.nodes[e].build.url}). If you implemented it in the app, set build { url } on it to the app's page, or build: null; compare and the user's Code tab follow the build.`}return B(`updated ${e.length} nodes\n${U(c)}${u}`)}catch(e){return V(e)}}),E(`delete`,{description:`Delete nodes and their descendants (one atomic transaction; the user can undo).`,inputSchema:{ids:I.array(I.string()).min(1),label:I.string().optional()}},async({ids:e,label:t})=>{try{return await r.ops([{op:`delete`,ids:e}],t??`Delete`),B(`deleted ${e.length} nodes`)}catch(e){return V(e)}}),E(`move`,{description:`Reparent and/or reorder nodes. In auto layout, order = visual order. parent null moves to canvas root. index refers to the parent's current child list: the nodes are inserted before the child now at that index (omit for end).`,inputSchema:{ids:I.array(I.string()).min(1),parent:I.string().nullable(),index:I.number().int().min(0).optional(),label:I.string().optional()}},async({ids:e,parent:t,index:n,label:i})=>{try{let a=await r.ops([{op:`move`,ids:e,parent:t,index:n}],i??`Move`);return B(`moved ${e.length} nodes\n${U(a)}`)}catch(e){return V(e)}}),E(`flatten`,{description:`Flatten (Figma ⌘E): layers in one parent become path layers that look the same. Shapes and/or boolean groups become ONE path layer ("Vector"): each layer's outline as drawn (radius, rotation and boolean results baked in) becomes a contour; overlapping layers are NOT united (they stay separate contours, filled once, nonzero). It takes the top layer's id, place in the stack, fill/stroke/effects/opacity. One layer alone works too (a rect becomes editable points with its radius baked; a boolean group bakes its result). Text and frames flatten too: text becomes its glyph outlines in its color (no longer editable as text), a frame's box and everything in it become paths cut to the frame where it clips, images keeping their place. Layers of the same plain look merge into one path; when looks differ, the result is a group (the top layer's id and name) with one path per look, bottom to top. Images/SVG vectors, instances and turned layers inside a frame are refused. One undo step.`,inputSchema:{ids:I.array(I.string()).min(1),label:I.string().optional()}},async({ids:e,label:t})=>{try{let{doc:n}=await r.state();if(e.some(e=>n.nodes[e]?.type===`text`||n.nodes[e]?.type===`frame`&&!n.nodes[e].boolean)){let n=await r.flatten(e,t??`Flatten`);return B(`flattened ${e.length} layer${e.length>1?`s`:``} into ${n.created[0]}${n.warnings.length?`\n${n.warnings.join(`
136
+ `)}`:``}`)}let i=n.nodes[e[0]],a=i?.parent?n.nodes[i.parent]:null,o;if(a?.type===`frame`&&a.layout.mode!==`free`&&e.filter(e=>!n.nodes[e]?.absolute).length>1){let{boxes:t}=await r.layout(a.id);o={};for(let r of e){let e=t.find(e=>e.id===r),i=n.nodes[r];if(!e||!i)continue;let a=i.type===`shape`&&i.rotation;o[r]=a?{x:e.x+e.w/2-i.w/2,y:e.y+e.h/2-i.h/2,w:i.w,h:i.h}:{x:e.x,y:e.y,w:e.w,h:e.h}}}let s=await r.ops([{op:`flatten`,ids:e,key:`v`,...o?{at:o}:{}}],t??`Flatten`);return B(`flattened ${e.length} layer${e.length>1?`s`:``} into ${s.ids.v}\n${U(s)}`)}catch(e){return V(e)}}),E(`rasterize`,{description:`Rasterize selection: layers in one parent become ONE image layer that looks the same — drawn as rendered at 2x (opacity and effects baked in, transparent around them) and placed where they were, taking the top layer's id, name and place in the stack. Nothing inside stays editable. Use it when a heavy illustration or a mix of layers only needs to be seen, not edited. In an auto layout, one layer at a time. One undo step.`,inputSchema:{ids:I.array(I.string()).min(1),label:I.string().optional()}},async({ids:e,label:t})=>{try{let n=await r.rasterize(e,t??`Rasterize selection`);return B(`rasterized ${e.length} layer${e.length>1?`s`:``} into ${n.ids.img}`)}catch(e){return V(e)}}),E(`outline_stroke`,{description:`Outline stroke (Figma ⌥⌘O): each layer's stroke becomes filled path geometry that looks the same (the stroke's position, cap and miter joins baked in; a closed shape's stroke becomes a ring of 2 contours). Stroke only (no visible fill): the layer itself becomes that path, filled with the stroke color. Fill and stroke: a group named like the layer holding the layer (stroke removed) and the outlined stroke ("Vector") above it. Shapes and boolean groups only (not text, images, frames, or boolean operands). One undo step.`,inputSchema:{ids:I.array(I.string()).min(1),label:I.string().optional()}},async({ids:e,label:t})=>{try{let n=await r.ops([{op:`outlineStroke`,ids:e,key:`o`}],t??`Outline stroke`);return B(`outlined ${e.length} stroke${e.length>1?`s`:``}: ${e.map((e,t)=>n.ids[`o${t}`]).join(` `)}\n${U(n)}`)}catch(e){return V(e)}}),E(`export`,{description:`Export a layer to a file in the project (Figma's Export): png / jpg (any scale: 2 = 2x, or a width/height like "512w" / "512h"), svg or pdf (1x). PNG, JPG and PDF are what the browser renders (the layer alone: PNG transparent outside its own paint, JPG on white, PDF vector with real text); SVG is true vector (shapes, paths, boolean groups, frames, images embedded) and refuses layers containing text. Without format, uses the layer's own export settings (or PNG 1x); several files are written as a zip. Writes to \`path\` (relative to the project; default exports/<layer name><suffix>.<ext>). html: the layer as a plain page (its HTML and CSS, the same as the user's Code tab), served live at a localhost address — the way to send a frame to Figma: give that address to Figma's MCP (see SENDING TO FIGMA); with path it's also written to a file.`,inputSchema:{id:I.string(),format:I.enum([`png`,`jpg`,`svg`,`pdf`,`html`]).optional(),scale:I.union([I.number().positive(),I.string()]).optional().describe(`png/jpg: 2 or "2x", "512w", "512h". Default 1x.`),suffix:I.string().optional(),path:I.string().optional().describe(`File path relative to the project (default exports/…).`)}},async({id:e,format:t,scale:n,suffix:i,path:a})=>{try{if(t===`html`){let t=`${r.url}/html/${encodeURIComponent(e)}`,n=await Se(r);if(!a)return B(`${t}\nthe layer as a page, always the design as it is now (open it, or give it to Figma's generate_figma_design)${n?`\n${n}`:``}`);let i=await fetch(t).then(t=>t.ok?t.text():Promise.reject(Error(`no layer ${e}`))),o=z(r.project,a);if(!o)throw Error(`path must be inside the project`);return j(N(o),{recursive:!0}),M(o,i),B(`${t}\nwrote ${P(r.project,F(r.project,a))} (${i.length} bytes; its images and fonts load from the canvas)`)}let o=n===void 0?void 0:typeof n==`number`?`${n}x`:String(n),s=t?[{format:t,...o&&(t===`png`||t===`jpg`)?{scale:o}:{},...i?{suffix:i}:{}}]:void 0,c=await r.exportFiles([e],s),l=a??de(`exports`,c.name),u=z(r.project,l);if(!u)throw Error(`path must be inside the project`);return j(N(u),{recursive:!0}),M(u,c.data),B(`wrote ${P(r.project,F(r.project,l))} (${c.data.length} bytes)${c.files.length>1?`: a zip of ${c.files.join(`, `)}`:``}`)}catch(e){return V(e)}}),E(`duplicate`,{description:`Copy a node with its subtree. Root frames are placed beside the original (for variants / next screens); other nodes are inserted right after the original, inside the same parent. With toCanvas, a layer inside a screen (a header, a card) is copied out instead: a new root frame on the canvas beside the screen, at the layer's measured size, linked to the layer (variantOf) so the user can swap it in with Use this. Use toCanvas for variations, states or alternatives of a layer inside a screen: copies inside the screen would push its layout around.`,inputSchema:{id:I.string(),name:I.string().optional(),label:I.string().optional(),toCanvas:I.boolean().optional().describe(`Copy a layer inside a screen out to the canvas as its own root frame (for variations of that layer).`)}},async({id:e,name:t,label:n,toCanvas:i})=>{try{let{doc:a}=await r.state(),o=a.nodes[e];if(!o)throw Error(`unknown node "${e}"`);let s={...k(a,e),key:`copy`,...t?{name:t}:{}},c;if(o.parent===null)c={op:`create`,parent:null,tree:{...s,x:o.x+(o.type===`frame`&&o.build?o.w*2+80:o.w)+80,y:o.y}};else if(i){let n=o;for(;n.parent;)n=a.nodes[n.parent];let i=(await r.layout(n.id)).boxes,l=i.find(t=>t.id===e),u=Math.round(i.find(e=>e.id===n.id)?.h??n.h),d=Math.round(l?.w??o.w),f=Math.round(l?.h??o.h),{absolute:p,constraints:m,x:g,y:_,...v}=s,y;for(let e=o.parent?a.nodes[o.parent]:void 0;e&&!y;e=e.parent?a.nodes[e.parent]:void 0)e.type===`frame`&&e.fills?.length&&(y=e.fills);let b=o.type===`frame`?{...v,w:d,h:f,sizeX:`fixed`,sizeY:o.sizeY===`hug`?`hug`:`fixed`,...!o.fills?.length&&y?{fills:y}:{}}:{type:`frame`,key:`copy`,name:t??o.name,w:d,h:f,sizeX:`fixed`,sizeY:`hug`,...y?{fills:y}:{},children:[{...v,key:void 0}]},x=n.y+u+160;for(let e=!0;e;){e=!1;for(let t of h(a))t.id===n.id||t.x>=n.x+d||t.x+t.w<=n.x||t.y>=x+f+80||t.y+t.h+80<=x||(x=t.y+t.h+80,e=!0)}c={op:`create`,parent:null,tree:{...b,variantOf:e,x:n.x,y:x}}}else{let t=a.nodes[o.parent].children;c={op:`create`,parent:o.parent,index:t.indexOf(e)+1,tree:s}}let l=await r.ops([c],n??`Duplicate ${o.name}`),u=o.type===`frame`&&o.parent===null?o.build:void 0,d=l.ids.copy;return u&&d&&u.url.replace(/\/$/,``).endsWith(`/${e}`)&&new URL(u.url).port===String(Number(new URL(r.url).port)+2e3)&&await r.ops([{op:`update`,id:d,set:{build:{url:`${W(r.url)}${d}/`}}}],`Link the copy's own prototype`),B(`duplicated → ${d}${i&&o.parent!==null?` (a root frame on the canvas, linked to ${e} for Use this)`:``}\n${U(l)}`)}catch(e){return V(e)}}),E(`render`,{description:"PNG of a node as rendered in a real browser. Use to check visual composition before reporting done. With `compare` (a URL, e.g. your localhost page), checks an implementation against this frame instead: both are rendered at the frame's size and you get text-by-text position/size/type differences and a red overlay of differing pixels, and it checks the frame's source links (where each part is implemented). Use it after implementing a frame, fix what it reports, and run it again.",inputSchema:{id:I.string(),scale:I.number().min(.25).max(3).optional().describe(`Device pixel ratio. Default 1 (cheaper); 2 for detail.`),compare:I.union([I.string().url(),I.literal(!0)]).optional().describe(`URL of the built page to check against this frame (http/https, localhost works), or true to use the frame's linked build (set it with update { changes: [{ id, set: { build: { url, selector? } } }] }; the user opens it from the frame).`),selector:I.string().optional().describe(`With compare: CSS selector of the built element that corresponds to the frame. Default: the page from its top-left.`),install_browser:I.boolean().optional().describe(`Only after the user agreed: download the headless Chromium render/compare/import need when this computer has no Chrome (the error says when, and how big).`)}},async({id:e,scale:t=1,compare:n,selector:i,install_browser:a})=>{try{if(a&&await r.installBrowser(),n){let t=n===!0?void 0:n,a=i;if(n===!0){let n=(await r.peek()).doc.nodes[e],i=n&&n.type===`frame`?n.build:void 0;if(!i)throw Error(`frame "${e}" has no build link: pass a URL, or update it with { build: { url } } first`);t=i.url,a??=i.selector}let o=await r.compare(e,t,a),s=await r.sourceCheck(e).catch(()=>({checked:0,broken:[]})),c=(await r.peek()).doc,l=s.checked?we(s):c.nodes[e]?.parent===null&&!G(r.url,e,t)?H(c,e):``,u=n===!0?void 0:(await r.peek()).doc.nodes[e],d=u?.type===`frame`&&u.parent===null&&!u.build?`\n\nThis frame isn't linked to its build yet: link it so the user sees the live page beside it (update { changes: [{ id: "${e}", set: { build: { url: "${t}" } } }] }).`:``,f=await Ce(r,[e]);return{content:[{type:`text`,text:o.text+(l?`\n\n${l}`:``)+d+f},{type:`image`,data:o.overlay,mimeType:`image/png`}]}}let o=await r.render(e,t),s=await r.motionCheck(e).catch(()=>null),c=[...s?.unmatched?.length?[`${s.unmatched.length} selectors match nothing on the page (target layers as [data-node-id="n_…"], from the list at the top of motion.css): ${s.unmatched.slice(0,12).join(` | `)}`]:[],...s?.noEffect?.length?[`these hover/press declarations change nothing on the page (inline styles need !important; a fill is a background-image, so use background: <color> !important; after an entrance animation, move with translate/scale, not transform): ${s.noEffect.slice(0,12).join(` | `)}`]:[]],l=s?.live?s.error?`prototype: ${s.error}`:c.length?`prototype motion.css does not work yet. ${c.join(`. `)}. Fix it and render again; don't describe an effect as working until render reports no problems.`:`prototype motion.css: ${s.rules} rules, all reach the page and every hover/press rule changes it.`:null;return{content:[...l?[{type:`text`,text:l}]:[],{type:`image`,data:o.toString(`base64`),mimeType:`image/png`}]}}catch(e){return V(e)}}),E(`video_frames`,{description:"Look closely at a span of a video the user attached (its path is in the request): contact sheets of stills every `step` seconds between `from` and `to`, each labeled with its time. Use it to read an animation's timing, easing and order.",inputSchema:{path:I.string(),from:I.number().min(0),to:I.number().min(0),step:I.number().min(.02).max(5).optional().describe(`Seconds between stills. Default 0.1.`)}},async({path:e,from:t,to:n,step:i=.1})=>{try{let a=await r.videoFrames(e,t,Math.max(t,n),i);return{content:[{type:`text`,text:`${a.times.length} stills, ${a.times[0]?.toFixed(2)}–${a.times[a.times.length-1]?.toFixed(2)} s of ${a.duration.toFixed(2)} s`},...a.sheets.map(e=>({type:`image`,data:e,mimeType:`image/png`}))]}}catch(e){return V(e)}}),E(`get_layout`,{description:`Measured boxes (x, y, w, h relative to the node) and layout issues for a subtree, from a real browser.`,inputSchema:{id:I.string()}},async({id:e})=>{try{let{boxes:t,issues:n}=await r.layout(e),i=t.map(e=>`${e.name}#${e.id} ${e.x},${e.y} ${e.w}×${e.h}`),a=n.map(e=>`- ${e.name}#${e.id} ${e.code}: ${e.message}`);return B([...i,a.length?`issues:`:`issues: none`,...a].join(`
137
+ `))}catch(e){return V(e)}}),E(`icon`,{description:`Icons from icon packs (see ICONS). search: by name or keyword in the design's pack and the brand logos (one color, and in color: logos/…). insert: an icon as a vector layer (icon: "house", "lucide/house", "simple-icons/kakaotalk" or "logos/google-icon"; size px, default 24; stroke for outline packs; color default the text role). swap: replace the icon of an icon layer (id), keeping its size and color. download: fetch a pack the user agreed to (phosphor, tabler, material).`,inputSchema:{action:I.enum([`search`,`insert`,`swap`,`download`]),query:I.string().optional().describe(`search: words, e.g. "cart", "arrow left", "kakao"`),pack:I.string().optional().describe(`search / download: an Iconify set (lucide, simple-icons, ph, tabler, material-symbols, mdi, flag … any set the Libraries tab lists) or folder:<path>; brand logos: "brands" (one color) or "brands-color"`),icon:I.string().optional().describe(`insert / swap: "name" or "pack/name"`),id:I.string().optional().describe(`swap: the icon layer`),parent:I.string().nullable().optional(),index:I.number().int().min(0).optional(),x:I.number().optional(),y:I.number().optional(),size:I.number().positive().optional(),stroke:I.number().positive().optional(),color:I.string().optional().describe(`a color (#hex) or a role ($foreground, $primary…); default: the text role`),key:I.string().optional()}},async({action:e,query:t,pack:n,icon:i,id:a,parent:o,index:s,x:c,y:l,size:u,stroke:d,color:f,key:p})=>{try{let{doc:m}=await r.peek(),h=m.system?.icons??`lucide`;if(e===`download`){let e=await r.iconDownload(n??h);return B(`downloaded ${e.pack}: ${e.icons} icons`)}if(e===`search`){if(!t)throw Error(`search needs query`);let e=n?[n]:[...new Set([h,`brands`,`brands-color`])],i=[];for(let n of e){let e=await r.iconSearch(t,n,12);i.push(...e.hits.map(t=>`${t.pack}/${t.name}${t.category?` — ${t.category}`:``}${e.palette?` (multicolor)`:``}`))}return B(i.length?i.join(`
138
+ `):`no icon for "${t}"; try other words`)}if(!i)throw Error(`${e} needs icon`);let g=i.startsWith(`folder:`)?i.lastIndexOf(`/`):i.indexOf(`/`),_=g>0?[[i.slice(0,g),i.slice(g+1)]]:[[h,i],[`simple-icons`,i],[`cib`,i],[`logos`,i]],v=null,b;for(let[e,t]of _)try{v=await r.iconAsset(e,t,d);break}catch(e){b=e}if(!v)throw b;let x={pack:v.pack,name:v.name,...v.palette?{multicolor:!0}:{}},S=v.credit?`\n${v.credit}`:``;if(e===`swap`){if((a?m.nodes[a]:void 0)?.type!==`vector`)throw Error(`swap needs id of a vector layer`);let e=await r.ops([{op:`asset`,add:[v.asset]},{op:`update`,id:a,set:{asset:v.asset.id,icon:x,name:`icon/${v.name}`,...v.palette?{color:null}:{}}}],`Swap icon`);return B(`swapped to ${v.pack}/${v.name}${S}\n${U(e)}`)}let C=u??24,w=Math.round(C*v.w/v.h*100)/100,T=v.palette?void 0:f??y(m).text,E=await r.ops([{op:`asset`,add:[v.asset]},{op:`create`,parent:o??null,...s===void 0?{}:{index:s},tree:{type:`vector`,key:p??`icon`,name:`icon/${v.name}`,asset:v.asset.id,w,h:C,...T?{color:T}:{},icon:x,...c===void 0?{}:{x:c},...l===void 0?{}:{y:l}}}],`Insert icon`);return B(`icon ${E.ids[p??`icon`]} (${v.pack}/${v.name}${v.palette?`, multicolor: keeps its own colors, don't recolor it`:``})${S}\n${U(E)}`)}catch(e){return V(e)}});function D(e){let t=e.libraries?.kits??[];return Object.values(b).flatMap(n=>{if(!t.includes(n.id))return[`UI kit ${n.name} (${n.id}): off`];let r=n.components.map(t=>{let r=e.nodes[t.id]?.type===`frame`&&e.nodes[t.id].componentSet?O(e,t.id)[0]?.id:t.id,i=r?d(e,r):[];return`- ${n.id}/${t.name}${i.length?`: ${i.map(e=>e.type===`variant`?`${e.name} ${e.values.join(`|`)}`:`${e.name} (${e.type})`).join(`; `)}`:``}`});return[`UI kit ${n.name} ${n.version} (${n.id}): on. Place these instead of drawing them (see UI KITS):`,...r]})}async function A(e,t){let{doc:n}=await r.peek();if(e===`combine`&&t.set){let e=w(n,t.set),r=O(n,t.set),i=e=>r.some(t=>{let r=t=>{let i=n.nodes[t];return!!i&&(i.type===e||i.type===`frame`&&!i.instanceOf&&i.children.some(r))};return t.children.some(r)}),a=e.filter(e=>e.values.length===2&&e.values.every(e=>/^(on|off|true|false|yes|no|with|without)$/i.test(e))),o=e.filter(e=>e.values.some(e=>/\s[/|·,-]\s|\//.test(e))),s=n.nodes[t.set],c=[`\nVariant properties: ${e.map(e=>`${e.name} (${e.values.join(`, `)})`).join(`; `)||`none`}.`];return o.length&&c.push(`${o.map(e=>e.name).join(`, `)} holds several axes in one value (${o[0].values[0]}): give each axis its own property. Rename each variant "Type=Primary, Size=Medium" (update its variant: { Type: "Primary", Size: "Medium" }).`),a.length&&c.push(`${a.map(e=>e.name).join(`, `)} only shows or hides a layer: that is a Boolean property connected to the layer (visible), not a variant. Keep one variant per other combination.`),!s.props&&(i(`text`)||i(`vector`))&&c.push(`Next: what instances change goes in properties, then each variant's layer connects to them: ${i(`text`)?`the label is a Text property (prop type "text", then bind content on every variant's label)`:``}${i(`text`)&&i(`vector`)?`; `:``}${i(`vector`)?`an optional icon is a Boolean property (bind visible) and which icon is an Instance swap property (bind swap)`:``}.`),c.join(` `)}if(e===`instance`&&t.inst){let e=n.nodes[t.inst],r=e?.instanceOf?n.nodes[e.instanceOf]:void 0;if(r?.variant)return`\nIt is ${Object.entries(r.variant).map(([e,t])=>`${e}=${t}`).join(`, `)}. Choose its variant and its properties with component { action: "set", id, values } (e.g. { ${Object.keys(r.variant).map(e=>`${JSON.stringify(e)}: …`).join(`, `)} }), not by changing its fills or text. Place instances straight into the screen's layout.`}return``}E(`component`,{description:`Main components, instances, variants and properties (see COMPONENTS). create: make frames main components. instance: place an instance of a main or a set (parent, index, x, y). reset: put back an instance's changes (keys on one layer, one layer, or the whole instance when id is the instance and no keys). push: write an instance's changes into its main. detach: make instances plain frames. instances: list a main's instances. combine: put mains in one component set (Combine as variants). add_variant: copy a variant (id: it, or the set) into a new one. prop: add a property to a set or main (name, type), rename it (rename) or remove it (remove: true). bind: connect a layer of a main to a property (content, visible, swap; null disconnects). set: an instance's property values (values; null = back to the main's). adopt: copy a kit's component (id: an instance of it, or the kit component) into this file. restore: put a deleted main component back on the canvas (id: an instance that still uses it, or the main); a deleted main stays for its instances until then. kit: turn a UI kit on or off (name, on).`,inputSchema:{action:I.enum([`create`,`instance`,`reset`,`push`,`detach`,`instances`,`combine`,`add_variant`,`prop`,`bind`,`set`,`adopt`,`restore`,`kit`]),on:I.boolean().optional().describe(`kit: turn it on (true) or off (false)`),ids:I.array(I.string()).optional().describe(`create, detach, combine: the frames / instances / mains`),id:I.string().optional().describe(`reset: an instance or a layer in one; push, set: the instance; instances: the main; add_variant: a variant or the set; prop: the set or main; bind: the layer`),keys:I.array(I.string()).optional().describe(`reset: only these fields (e.g. ["content"])`),main:I.string().optional().describe(`instance: the main component's id, a set's (its first variant), or a kit component as "<kit>/<name>" (e.g. "shadcn/Button")`),parent:I.string().nullable().optional().describe(`instance: where to put it (null = the canvas)`),index:I.number().int().min(0).optional(),x:I.number().optional(),y:I.number().optional(),name:I.string().optional().describe(`combine: the set's name; prop: the property; kit: the kit (e.g. "shadcn")`),type:I.enum([`variant`,`boolean`,`text`,`swap`]).optional().describe(`prop: a new property's type`),rename:I.string().optional().describe(`prop: its new name`),remove:I.boolean().optional().describe(`prop: remove it (and its connections and values)`),preferred:I.array(I.unknown()).optional().describe(`prop (swap): values offered first, main ids or { icon, asset }`),content:I.string().nullable().optional().describe(`bind: the Text property for this text layer`),visible:I.string().nullable().optional().describe(`bind: the Boolean property that shows this layer`),swap:I.string().nullable().optional().describe(`bind: the Instance swap property for this nested instance or icon`),values:I.record(I.string(),I.unknown()).nullable().optional().describe(`set: property name → value (Variant: a value; Boolean: true/false; Text: a string; Instance swap: a main id or { icon, asset }; null: the main's)`)}},async({action:e,ids:t,id:n,keys:i,main:a,parent:o,index:s,x:c,y:l,name:d,type:f,rename:p,remove:m,preferred:h,content:g,visible:_,swap:v,values:y,on:x})=>{try{let S=(t,n)=>{if(t===void 0||Array.isArray(t)&&!t.length)throw Error(`${e} needs ${n}`);return t};if(e===`instances`){let{doc:e}=await r.state(),t=S(n,`id (the main)`),i=C(e,t);return B(i.length?i.map(t=>`${e.nodes[t].name}#${t}${e.nodes[t].type===`frame`&&e.nodes[t].overrides?` (changed)`:``}`).join(`
139
+ `):`no instances`)}if(e===`kit`){let{doc:e}=await r.state(),t=b[S(d,`name`)]??Object.values(b).find(e=>e.name.toLowerCase()===d.toLowerCase());if(!t)throw Error(`no UI kit "${d}"; there is ${Object.values(b).map(e=>`${e.id} (${e.name})`).join(`, `)}`);let n=e.libraries?.kits??[],i=T.find(e=>e.kit===t.id),a=x===!1?[{op:`doc`,libraries:{kits:n.filter(e=>e!==t.id)}}]:[...!e.system&&i?[{op:`system`,set:u(i)}]:[],{op:`doc`,libraries:{kits:[...new Set([...n,t.id])]}}],o=await r.ops(a,x===!1?`Turn off ${t.name}`:`Use ${t.name}`);return B(`${t.name} ${x===!1?`off: its instances here are plain frames now`:`on${!e.system&&i?`, with its design system (${i.name})`:``}. Its components are in get_context; place them with component { action: "instance", main: "${t.id}/<component>" }`}\n${U(o)}`)}let w=e===`create`?{op:`component`,ids:S(t,`ids`)}:e===`adopt`?{op:`adopt`,id:S(n,`id`),...c===void 0?{}:{x:c},...l===void 0?{}:{y:l},key:`set`}:e===`instance`?{op:`instance`,main:(e=>{let[t,...n]=e.split(`/`),r=b[t];if(!r||!n.length)return e;let i=r.components.find(e=>e.name.toLowerCase()===n.join(`/`).toLowerCase());if(!i)throw Error(`${r.name} has no "${n.join(`/`)}"; it has ${r.components.map(e=>e.name).join(`, `)}`);return i.id})(S(a,`main`)),parent:o??null,...s===void 0?{}:{index:s},...c===void 0?{}:{x:c},...l===void 0?{}:{y:l},key:`inst`}:e===`restore`?{op:`restore`,id:S(n,`id`),...c===void 0?{}:{x:c},...l===void 0?{}:{y:l}}:e===`reset`?{op:`reset`,id:S(n,`id`),...i?.length?{keys:i}:{}}:e===`push`?{op:`push`,id:S(n,`id`)}:e===`combine`?{op:`combine`,ids:S(t,`ids`),...d?{name:d}:{},key:`set`}:e===`add_variant`?{op:`addVariant`,id:S(n,`id`),key:`variant`}:e===`prop`?{op:`prop`,id:S(n,`id`),name:S(d,`name`),...f?{type:f}:{},...p?{rename:p}:{},...m?{remove:!0}:{},...h?{preferred:h}:{}}:e===`bind`?{op:`bind`,id:S(n,`id`),...g===void 0?{}:{content:g},..._===void 0?{}:{visible:_},...v===void 0?{}:{swap:v}}:e===`set`?{op:`update`,id:S(n,`id`),set:{values:y===void 0?S(y,`values`):y}}:{op:`detach`,ids:S(t,`ids`)},E={adopt:`Bring into this file`,restore:`Restore main component`,create:`Create component`,instance:`Place instance`,reset:`Reset changes`,push:`Push changes to main`,detach:`Detach instance`,combine:`Combine as variants`,add_variant:`Add variant`,prop:`Component property`,bind:`Connect property`,set:`Instance properties`}[e],D=await r.ops([w],E);return B(`${D.ids.inst?`instance ${D.ids.inst}`:D.ids.set?`component set ${D.ids.set}`:D.ids.variant?`variant ${D.ids.variant}`:`${e} done`}${await A(e,D.ids)}\n${U(D)}`)}catch(e){return V(e)}}),E(`changes`,{description:`For a root frame linked to a build: what the user changed on it since it last matched the build (node, property, before → after). Implement exactly these in the code, then call again with accept: true.`,inputSchema:{id:I.string().describe(`Root frame id`),accept:I.boolean().optional().describe(`The build now matches the frame: start counting changes from here.`)}},async({id:e,accept:t})=>{try{if(t)return B((await r.acceptFrame(e)).ok?`baseline moved: the frame and its build match`:`this frame isn't linked to a build`);let n=await r.frameChanges(e);if(!n.linked)return B(`not linked to a build: implement the whole frame (see IMPLEMENTING A FRAME)`);let i=n.since?new Date(n.since).toISOString().slice(0,16).replace(`T`,` `):`?`,a=n.auto?null:await r.sourceCheck(e).catch(()=>({checked:0,broken:[]})),o=a?a.checked?we(a):H((await r.peek()).doc,e):``,s=await Se(r);return B([`build: ${n.url} · changes since ${i} UTC (${n.lines.length}):`,...n.lines.length?n.lines:[`none: the frame still matches its build`],...o?[``,o]:[],...s?[``,s]:[]].join(`
140
+ `))}catch(e){return V(e)}}),process.env.NUDGE_RUN===`1`&&E(`ask`,{description:`Ask the user to pick one of a few answers (a direction, which of two options, prototype or new app): the canvas chat shows the question with a button per choice. Use it instead of a numbered list in your text whenever the answer is one of a few. Then end your turn: their pick comes as their next request.`,inputSchema:{question:I.string().describe(`One short question.`),choices:I.array(I.string()).min(2).max(6).describe(`2–6 short answers, each a button (the user can still type something else).`)}},async()=>B(`shown in the canvas chat as buttons. End your turn now; the user's pick comes as their next request.`)),E(`reply`,{description:`Answer the user on the canvas. chat_id: a canvas chat request that arrived as <channel source="nudge" chat_id=…> (the text shows in their canvas chat and marks the request done). comment_id: a comment pinned on the canvas (the text shows on its pin); resolve: true marks the comment done. To have the user pick one of a few answers in a canvas chat, give question and choices: they show as buttons, and the pick comes back as their next request.`,inputSchema:{chat_id:I.string().optional(),comment_id:I.string().optional(),text:I.string(),resolve:I.boolean().optional().describe(`With comment_id: the comment is handled.`),question:I.string().optional().describe(`With chat_id: a question the user answers by picking one of choices.`),choices:I.array(I.string()).min(2).max(6).optional().describe(`With question: 2–6 short answers, each a button.`)}},async({chat_id:e,comment_id:t,text:n,resolve:i,question:a,choices:o})=>{try{if(!!e==!!t)throw Error(`pass exactly one of chat_id or comment_id`);return t?(await r.comment({action:`reply`,id:t,text:n,...i?{resolve:!0}:{}}),B(`replied to ${t}${i?` and resolved it`:``}`)):B((await r.replyToCanvas(e,n,a&&o?{question:a,choices:o}:void 0)).ok?`sent to the canvas chat`:`that request is no longer waiting (finished or cancelled)`)}catch(e){return V(e)}}),E(`set_styles`,{description:`Add or change named styles. colors: {name: "#hex"|null}, text: {name: {fontFamily, fontSize, fontWeight, fontStyle?, lineHeight?, letterSpacing?, textAlign?, textCase?("upper"|"lower"|"title"), color} | null}, space: number[] (replaces the scale). Nodes referencing "$name" update everywhere. A null deletes a style: the layers using it keep its value as plain values (detached), in one undo step. rename: {colors?|text?|radius?: {oldName: newName}} renames a style and every use of it (refused if the new name exists).`,inputSchema:{colors:I.record(I.string(),I.string().nullable()).optional(),text:I.record(I.string(),I.any()).optional(),space:I.array(I.number()).optional(),rename:I.object({colors:I.record(I.string(),I.string()).optional(),text:I.record(I.string(),I.string()).optional(),radius:I.record(I.string(),I.string()).optional()}).optional(),label:I.string().optional()}},async({colors:e,text:t,space:n,rename:i,label:a})=>{try{let{doc:o}=await r.state(),s=[...o.system?.owned.colors??[],...o.system?.owned.text??[]],c=[...Object.keys(e??{}),...Object.keys(t??{}),...Object.keys(i?.colors??{}),...Object.keys(i?.text??{})].filter(e=>s.includes(e));if(c.length)throw Error(`${c.join(`, `)} belong to the design system: change them with design_system (slots, or overrides for an exact value)`);return await r.ops([{op:`styles`,colors:e,text:t,space:n,rename:i}],a??`Styles`),B(`styles updated`)}catch(e){return V(e)}}),E(`design_system`,{description:`The document's design system (see DESIGN SYSTEM). get: every color token in both themes, the text styles, corners, spacing, changes made in this file. A published system (shadcn/ui, Material 3, Carbon, Primer, Ant Design) is that system's own tokens under its own names, filed under the system (as get lists them; shadcn/ui's have no group): set published: "<id>" to use one, overrides to change a token (the system then shows as Changed; null puts the published value back), own: true to make it this file's own system (keeps the values and names). A system made from scratch derives roles from slots. set: create it (if there is none) or change it: name, theme (the one the canvas shows), icons (the icon pack: lucide | phosphor | tabler | material | folder:<path>; see ICONS), slots {fontDisplay, fontBody (a family name, e.g. Inter), bg, fg, accent (light theme, #rrggbb), dark: {bg, fg, accent} | null, radius: sharp|precise|friendly, radii: [sm, md, lg, xl] | null (exact corners, as a preset has them; choosing a radius character clears them)}, overrides {colors: {role: {light?, dark?} | null} (a new name adds a role), text: {display|heading-l|heading-m|heading-s|body-l|body-m|label|caption: {fontFamily?, fontSize?, fontWeight?, lineHeight? (px), letterSpacing? (px), textCase? ("upper"|"lower"|"title")} | null}, radius: {<corner token as get lists it>: px | null}} — an override wins over the derived value; null puts it back. remove: drop the system (its styles stay as plain styles). Everything bound to the roles updates; one undo step. import: replace it with a system read from file or content, under that system's own names (ask the user first when the file already has one).`,inputSchema:{action:I.enum([`get`,`set`,`remove`,`import`]),file:I.string().optional().describe(`import: a file in the project (relative path) holding the system: CSS variables (.css, or a page's <style> in .html), W3C design tokens (.json, $value) or Tokens Studio / Style Dictionary JSON (value, sets and themes)`),content:I.string().optional().describe(`import: the same, as text, when it isn't a file in the project (e.g. tokens you wrote from a framework config, SCSS, a JS theme or a design tool's variables)`),name:I.string().optional(),theme:I.enum([`light`,`dark`]).optional(),icons:I.string().optional().describe(`set: the icon pack the design uses`),slots:I.record(I.string(),I.any()).optional().describe(`set: a system made from scratch, from a few choices (fonts, colors, corners)`),overrides:I.object({colors:I.record(I.string(),I.any()).optional(),text:I.record(I.string(),I.any()).optional(),radius:I.record(I.string(),I.number().min(0).nullable()).optional(),space:I.record(I.string(),I.number().int().min(0).nullable()).optional().describe(`a spacing step's px, keyed by the step's own px ({"16": 20}); null = its own value again`)}).optional(),published:I.string().nullable().optional().describe(`set: the id of a published system (as get_context lists the presets) to use its own tokens; null = a system made from scratch`),own:I.literal(!0).optional().describe(`set: make the published system this file's own (keeps values and names; it stops being that system)`),corners:I.enum([`sharp`,`precise`,`friendly`]).optional().describe(`set: a system made the file's own (own): its corner tokens set to this character, smallest first (sharp 0; precise 4/6/8/12; friendly 8/12/16/24)`),label:I.string().optional()}},async({action:e,name:t,theme:n,icons:i,slots:a,overrides:s,published:l,own:u,corners:d,label:p,file:m,content:h})=>{try{if(e===`import`){if(!m==!h)throw Error(`import takes file or content`);let e=m?(await r.systemFile(m)).text:h,n=m??(/^\s*[{[]/.test(e)?`tokens.json`:/^\s*</.test(e)?`page.html`:`tokens.css`),i=c(e,n);if(`error`in i)throw Error(i.error);let a={...i.fonts.body?{fontBody:i.fonts.body}:{},...i.fonts.display??i.fonts.body?{fontDisplay:i.fonts.display??i.fonts.body}:{}},o=t??i.name;await r.ops([{op:`system`,set:{name:o,tokens:i.tokens,...Object.keys(a).length?{slots:a}:{}}}],p??`Import system: ${o}`);let s=i.tokens.defaults??{},l=Object.entries(s).map(([e,t])=>`${e} $${t}`).join(`, `);return B(`imported "${o}" under its own names: ${Object.keys(i.tokens.colors).length} colors, ${Object.keys(i.tokens.text).length} text styles, ${Object.keys(i.tokens.radius).length} corners${i.skipped.length?`; not read: ${i.skipped.slice(0,12).join(`, `)}${i.skipped.length>12?` (+${i.skipped.length-12})`:``}`:``}. Roles Nudge fills in on its own: ${l||`none found by name (icons and new text use plain black)`}.`)}if(e===`remove`)return await r.ops([{op:`system`,remove:!0}],p??`Remove design system`),B(`design system removed; its styles stay`);if(e===`set`){let e=(await r.state()).doc.system,c=e&&!l?Object.keys(o(e)):S.map(e=>e.name),f=Object.keys(s?.text??{}).filter(e=>!c.includes(e));if(f.length&&!l)throw Error(`no text style ${f.map(e=>`"${e}"`).join(`, `)} in the system; its text styles are ${c.join(`, `)}`);let m=Object.fromEntries(Object.entries({name:t,theme:n,icons:i,slots:a,overrides:s,published:l,own:u,corners:d}).filter(([,e])=>e!==void 0));await r.ops([{op:`system`,set:m}],p??`Design system`)}let{doc:_}=await r.state(),y=_.system;if(!y)return B(`no design system (design_system set creates one)`);let b=f(y,`light`),x=f(y,`dark`),C=o(y),w=te(y),T=(e,t)=>{let n=e.find(e=>e.name===t)?.usage;return n?` — ${n}`:``};return B([`design system "${y.name}" · canvas theme ${y.theme}`,w?`${w.name}'s own tokens, as published (${w.version}, ${w.license}; use them as ${w.url} documents)${v(y)?` · Changed in this file (overrides below)`:``}`:y.tokens?`this file's own tokens`:`made from scratch · slots: ${JSON.stringify(y.slots)}`,`colors (light / dark):`,...Object.keys(b).map(e=>` $${e} ${b[e]} / ${x[e]}${T(g,e)}`),`text styles:`,...Object.entries(C).map(([e,t])=>` $${e} ${t.fontFamily} ${t.fontSize}/${t.lineHeight} w${t.fontWeight}${t.fontStyle===`italic`?` italic`:``}${t.letterSpacing?` ls${t.letterSpacing}`:``}${t.textCase?` case:${t.textCase}`:``} ${t.color}${T(S,e)}`),`radius tokens: ${Object.entries(ne(y)).map(([e,t])=>`$${e}=${t}`).join(` `)||`none`} · spacing: ${w&&!w.space?`none published (${w.name})`:_.styles.space.join(` `)}`,...y.overrides&&(Object.keys(y.overrides.colors??{}).length||Object.keys(y.overrides.text??{}).length)?[`overrides: ${JSON.stringify(y.overrides)}`]:[],`code copy: design/system/tokens.css · tokens.json · README.md`].join(`
141
+ `))}catch(e){return V(e)}})}var Ae=`Nudge — a design canvas your coding agent works on with you.
142
+
143
+ nudge-canvas init Register Nudge with the coding agents on this computer
144
+ nudge-canvas init --remove Take it out of them again
145
+ nudge-canvas --version Print the version
146
+
147
+ nudge-canvas is the MCP server your coding agent starts; you don't run it yourself.
148
+ After init, start a new session of your agent in a project folder and ask: "Open the Nudge canvas".
149
+ More: https://docs.nudgecanvas.com
150
+ `,K=process.argv[2];(K===`--version`||K===`-v`)&&(process.stdout.write(`${r}\n`),process.exit(0)),(K===`--help`||K===`-h`||!K&&process.stdin.isTTY)&&(process.stdout.write(Ae),process.exit(0)),K===`init`&&process.exit(await(await import(`./chunk-D6z-TZJJ.js`)).runInit(process.argv.slice(3))),E(`Nudge`);var q=F(process.env.NUDGE_PROJECT??process.cwd()),J=process.env.NUDGE_PROJECT?q:_(q),je=J!==q;je&&j(J,{recursive:!0});var Me=Number(process.env.NUDGE_PORT??5191),Y=a(J),X=!Y&&ee(),Z=Y||X?new L(``,J):await L.connect(J,Me),Q=new ie({name:`nudge`,version:r},{instructions:fe,capabilities:{experimental:{"claude/channel":{}}}});ke(Q,Z,Y?Oe(J):X?De:void 0,je?J:void 0),Q.server.oninitialized=()=>{process.env.NUDGE_RUN||Y||X||import(`./chunk-Sq5RPyiX.js`).then(({ancestors:e,recordSeen:t,channelFlag:n})=>{let r=Q.server.getClientVersion(),i=e();t(r?.name??`unknown`,r?.version??``,i.map(e=>e.exe));let a=process.env.CLAUDE_CODE_SESSION_ID;if(!a)return;let o=n(i.slice(0,5).map(e=>e.args).join(`
151
+ `));Z.linkSession(a,r?.name??`unknown`,o).then(()=>{o&&Ne(a)})}).catch(()=>{})};async function Ne(e){for(;;)try{let t=await Z.waitRequest(e);t&&await Q.server.notification({method:`notifications/claude/channel`,params:{content:t.content,meta:{chat_id:t.chatId}}})}catch{await new Promise(e=>setTimeout(e,3e3))}}await Q.connect(new ae),process.stdin.on(`close`,()=>process.exit(0)),process.on(`SIGTERM`,()=>process.exit(0));var $=e=>process.stderr.write(`nudge: unexpected error (still running): ${e instanceof Error?e.stack??e.message:e}\n`);process.on(`uncaughtException`,$),process.on(`unhandledRejection`,$);export{};
Binary file