@pen.dev/cli 0.3.7 → 0.3.8

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.
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -13,6 +13,7 @@ pen.dev is an infinite canvas design tool with nested object hierarchy.
13
13
  | --- | --- | --- |
14
14
  | `.pen schema` | Learn the .pen schema that is used to represent. Required to read. | [pen-schema.md](./pen-schema.md) |
15
15
  | `execute` | Learn how to use the `execute` MCP tool to generating designs on the canvas. Required to read. | [execute.md](./execute.md) |
16
+ | `generate` | Using the `Generate` function: AI and stock images, SVG artwork, and transforming an image already in the document. | [generate.md](./generate.md) |
16
17
  | `components` | Learn how to use components and instances in .pen files. | [components.md](./guide/components.md) |
17
18
  | `scripts-and-shaders` | Learn how to use scripts and shaders on the pen.dev canvas. | [scripts-and-shaders.md](./scripts-and-shaders.md) |
18
19
  | `code` | Generating code from .pen files. | [code.md](./guide/code.md) |
@@ -37,7 +37,11 @@ function Update(path: string, updateData: Child): void;
37
37
  function Replace(path: string, nodeData: Child): string; // returns the replacement node's id
38
38
  function Move(path: string, parent: string | undefined, index?: number): void;
39
39
  function Delete(path: string): void;
40
- function Generate(nodeId: string, type: "ai" | "svg" | "stock", prompt: string): void; // generate image or SVG
40
+ function Generate(type: "ai" | "svg", nodeId: string, prompt: string): void; // generate an image fill or an SVG drawing - see generate.md
41
+ function Generate(type: "stock", nodeId: string, query: string): void;
42
+ function Generate(type: "vectorize-image", nodeId: string, imageUrl: string): void;
43
+ function Generate(type: "remove-background", imageUrl: string): string; // returns the new asset url
44
+ function Generate(type: "replace-background", imageUrl: string, prompt: string): string;
41
45
  function SetVariables(variables: Record<string, VariableDefinition>, replace?: boolean): void;
42
46
 
43
47
  // Reading
@@ -228,79 +232,12 @@ Export([heroId, pricingId], "png", "./exports")
228
232
  Export([pageId], "html-tailwind", "./landing.html")
229
233
  ```
230
234
 
231
- ### Generate image or SVG
232
-
233
- - IMPORTANT: There is NO `image` node type! Images are applied as FILLS to
234
- existing nodes; SVGs are transformed into paths and added to a parent frame.
235
- - For `"ai"` and `"svg"`, Generate is an async, non-blocking operation. It
236
- returns nothing and finishes independently of the execute progress.
237
- (`"stock"` is the exception: its fill is applied during the call itself.)
238
- - The result lands in the document after the `execute` call that started it has
239
- already returned. The target node stays empty or unfilled until then, and
240
- screenshots taken right away will not show it. That is expected. Never
241
- re-generate it, draw it by hand, or add children to a frame you generated an
242
- SVG into.
243
- - The frame an SVG is generated into keeps `placeholder: true` while the
244
- drawing is in progress; the flag is cleared when the generation finishes.
245
- Check on it with a cheap read in a later `execute` call - never with
246
- screenshots: `Print(Get(logoFrameId, {depth: 0}).placeholder)`.
247
- - Generations are slow: SVGs can take multiple minutes. While the placeholder
248
- flag is still set, don't wait idly: continue with the rest of the design and
249
- re-check only occasionally, e.g. after finishing another section - not after
250
- every execute call.
251
- - Only when no other work is left, poll with that tiny Get/Print call, leaving
252
- a generous interval between checks (do other verification passes in
253
- between). Never fire checks back-to-back.
254
- - Screenshot the generated result only after the placeholder flag is cleared.
255
- If the flag is cleared but the frame is still empty, the generation failed -
256
- that is the one case where calling Generate again on the same node is
257
- correct.
258
-
259
- #### Generating an image (`type: "ai"` or `type: "stock"`)
260
-
261
- - Never guess or invent URLs for image fills. Always use the Generate function
262
- to get an image from a stock or AI service.
263
- - To display an image: first Insert a frame or rectangle, then use Generate to
264
- apply the image as a fill to that node.
265
- - `nodeId`: The ID of the node to apply the image fill to.
266
- - `type`: "ai" for AI-generated images, "stock" for stock photos from Unsplash.
267
- - `prompt`:
268
- - For "ai": a detailed descriptive prompt.
269
- - For "stock": a 1-3 keyword search query following the Stock query rules
270
- (simple, concrete, no use-case or abstract terms).
271
- - `"stock"` fills are applied during the call; if no matching photo is found,
272
- a warning is returned and the node is left without an image fill.
235
+ ### Generate
273
236
 
274
- ```js
275
- const pos = FindEmptySpace({width: 1440, height: 800, padding: 80, nodeId: logoFrameId})
276
- heroId = Insert(document, {type: "frame", name: "Section", x: pos.x, y: pos.y, width: 1440, height: 800, placeholder: true})
277
- Generate(heroId, "ai", "Tropical jungle canopy, dense green foliage, sunlight filtering through trees, cinematic aerial view, vibrant greens, moody atmospheric lighting, nature photography")
278
- ```
279
-
280
- #### Generating an SVG (`type: "svg"`)
281
-
282
- - SVG generation is the only way to produce freeform vector artwork, and it is currently the slowest and most expensive operation in Pencil. Use it deliberately - it is not the default way to fill visual space.
283
- - Generate an SVG only when the artwork itself is the point:
284
- - the user asks you to draw, illustrate, or create a logo, mark, mascot, or illustration,
285
- - the design is for a brand or product that needs its own logo mark and the document doesn't already contain one,
286
- - a section is carried by a custom illustration or diagram that cannot be assembled from icons, shapes, or an image.
287
- - Do NOT generate an SVG for these - each has a cheaper, better option:
288
- - icons, arrows, chevrons, checkmarks, UI glyphs -> an `icon` node (`lucide`, `feather`, `Material Symbols`, `phosphor`)
289
- - photos, textures, hero and background imagery, avatars, product shots -> an image fill via `Generate` with `type: "stock"` or `type: "ai"`
290
- - badges, tags, pills, buttons, cards, dividers, blobs, glows, gradient backgrounds, abstract patterns, charts -> frames, shapes, strokes, gradient fills, and layout
291
- - artwork that already exists in the document -> `Copy` it or instance it
292
- - Budget: be mindful about the number of generated SVG per design. Turn it into a component and reuse it whenever needed. Never generate a variant per screen, section, or card.
293
- - When you're unsure whether artwork is needed, build the design without it first. Adding one generated graphic afterwards is cheap; deleting several that weren't wanted is not.
294
- - To display an SVG: first Insert a frame with the desired width and height (no other element type is allowed), then use Generate. The resulting path elements are scaled to fit the frame's bounding box and inserted as its children.
295
- - `nodeId`: The ID of the frame to insert the SVG into.
296
- - `type`: "svg".
297
- - `prompt`: a detailed descriptive prompt. The generator sees nothing of the document, so state the subject, composition, line/fill style, and colors explicitly. One generation per prompt - don't ask for a set of variants.
298
-
299
- ```js
300
- const pos = FindEmptySpace({width: 400, height: 400, padding: 80})
301
- logoFrameId = Insert(document, {type: "frame", name: "Logo", x: pos.x, y: pos.y, width: 400, height: 400, placeholder: true})
302
- Generate(logoFrameId, "svg", "A cute fox logo, minimalist vector style, colourful, simple, clean, modern flat design, suitable for a brand logo.")
303
- ```
237
+ - Brings imagery and vector artwork into the document: `"ai"` and `"stock"` for a new image, `"svg"` for new vector artwork, and `"remove-background"`, `"replace-background"`, and `"vectorize-image"` for an image the document ALREADY has.
238
+ - IMPORTANT: There is NO `image` node type. Images are applied as FILLS to existing nodes; SVGs become paths inside a parent frame.
239
+ - Every type is async: the result lands after the `execute` call that started it has returned, so a screenshot taken right away will not show it. Never re-generate it, draw the result by hand, or add children to a frame you generated into.
240
+ - Read [generate.md](./generate.md) before calling it.
304
241
 
305
242
  ### FindEmptySpace
306
243
 
@@ -0,0 +1,124 @@
1
+ # Generate: images and vector artwork
2
+
3
+ `Generate` is an `execute` function - see [execute.md](./execute.md) for the rest of the API.
4
+ Read this file before calling it.
5
+
6
+ ```ts
7
+ function Generate(type: "ai" | "svg", nodeId: string, prompt: string): void;
8
+ function Generate(type: "stock", nodeId: string, query: string): void;
9
+ function Generate(type: "vectorize-image", nodeId: string, imageUrl: string): void;
10
+ function Generate(type: "remove-background", imageUrl: string): string; // returns the new asset url
11
+ function Generate(type: "replace-background", imageUrl: string, prompt: string): string; // returns the new asset url
12
+ ```
13
+
14
+ - `"ai"` and `"stock"` bring in a new image, `"svg"` draws new vector artwork, and `"remove-background"`, `"replace-background"`, and `"vectorize-image"` transform an image the document ALREADY has.
15
+ - IMPORTANT: There is NO `image` node type. Images are applied as FILLS to existing nodes; SVGs become paths inside a parent frame.
16
+ - `type` comes first. The four types that write into the document take the target `nodeId` next - the node receiving the image fill, or the frame the artwork is drawn into. `"remove-background"` and `"replace-background"` target no node: they RETURN an asset url for you to apply, so the source url follows the type directly. Last comes the input: a prompt for `"ai"` and `"svg"`, a query for `"stock"`, `imageUrl` for the transforms, plus a prompt after it for `"replace-background"`. Leave unused arguments out - never pass `undefined`.
17
+
18
+ ```js
19
+ Generate("ai", nodeId, "A dew-covered fern frond, macro, soft morning light")
20
+ Generate("stock", nodeId, "fern frond")
21
+ Generate("vectorize-image", frameId, url)
22
+ Generate("remove-background", url)
23
+ Generate("replace-background", url, "On a wet slate slab, muted sage background")
24
+ ```
25
+
26
+ ## Waiting for a result
27
+
28
+ - EVERY type is async. Only the bookkeeping happens during the call - the fill is written, or the url returned; the image or drawing itself lands after the `execute` call that started it has returned. Screenshots taken right away will not show it. Never re-generate it, draw the result by hand, or add children to a frame you generated into.
29
+ - Check with a cheap read in a LATER `execute` call, never with a screenshot:
30
+ - frame types (`"svg"`, `"vectorize-image"`): Insert the frame with `placeholder: true`; the generator clears that flag when it finishes. `Print(Get(logoFrameId, {depth: 0}).placeholder)`
31
+ - fill types (`"ai"`, `"stock"`, and both background transforms): the url is a `pencil:pending-image-...` placeholder until the image arrives, then rewritten in place to the real asset path. `Print(Get(heroId, {depth: 0}).fill)`
32
+ - Generations are slow - SVGs take minutes. Keep designing while one is pending and re-check occasionally, e.g. after finishing a section, not after every call. Never call Generate again for pending work. Only when no other work is left, poll with that tiny read, generously spaced - never back to back.
33
+ - Failures are silent: a `placeholder` flag cleared on a frame that still has no children, or a fill whose `url` has disappeared. That is the one case where calling Generate again for the same result is correct.
34
+ - Screenshot only once a check says the result arrived.
35
+
36
+ ## `"ai"` and `"stock"`: a new image
37
+
38
+ - Never guess or invent an image url - it only ever comes from `Generate`.
39
+ - Insert the frame or rectangle first, then `Generate` applies the image as its fill.
40
+ - `"ai"` takes a detailed descriptive prompt. `"stock"` takes a 1-3 keyword Unsplash query following the Stock query rules: simple, concrete, no use-case or abstract terms.
41
+ - Both write the fill during the call and resolve the image afterwards. If no stock photo matches the query, the fill's `url` ends up gone and the node shows nothing - nothing is reported back to you, so check the fill.
42
+
43
+ ```js
44
+ const pos = FindEmptySpace({width: 1440, height: 800, padding: 80, nodeId: logoFrameId})
45
+ heroId = Insert(document, {type: "frame", name: "Section", x: pos.x, y: pos.y, width: 1440, height: 800, placeholder: true})
46
+ Generate("ai", heroId, "Tropical jungle canopy, dense green foliage, sunlight filtering through trees, cinematic aerial view, vibrant greens, moody atmospheric lighting, nature photography")
47
+ ```
48
+
49
+ ## `"svg"`: new vector artwork
50
+
51
+ - The only way to produce freeform vector artwork, and the slowest and most expensive operation in Pencil. It is not the default way to fill visual space.
52
+ - Generate one only when the artwork itself is the point:
53
+ - the user asks you to draw, illustrate, or create a logo, mark, mascot, or illustration,
54
+ - the design is for a brand or product that needs its own logo mark and the document doesn't already contain one,
55
+ - a section is carried by a custom illustration or diagram that cannot be assembled from icons, shapes, or an image.
56
+ - Everything else has a cheaper, better option:
57
+ - icons, arrows, chevrons, checkmarks, UI glyphs -> an `icon` node (`lucide`, `feather`, `Material Symbols`, `phosphor`)
58
+ - photos, textures, hero and background imagery, avatars, product shots -> `"stock"` or `"ai"`
59
+ - badges, tags, pills, buttons, cards, dividers, blobs, glows, gradient backgrounds, abstract patterns, charts -> frames, shapes, strokes, gradient fills, and layout
60
+ - artwork already in the document -> `Copy` it or instance it
61
+ - Budget it: turn a generated SVG into a component and reuse it. Never generate a variant per screen, section, or card. When unsure, build the design without artwork first - adding one afterwards is cheap, deleting several is not.
62
+ - Insert a frame with the desired width and height and `placeholder: true` first (no other node type is allowed) and pass its id as `nodeId`; the resulting paths are scaled to the frame's bounding box and inserted as its children.
63
+ - The generator sees nothing of the document, so the prompt must state the subject, composition, line/fill style, and colors. One generation per prompt - don't ask for a set of variants.
64
+
65
+ ```js
66
+ const pos = FindEmptySpace({width: 400, height: 400, padding: 80})
67
+ logoFrameId = Insert(document, {type: "frame", name: "Logo", x: pos.x, y: pos.y, width: 400, height: 400, placeholder: true})
68
+ Generate("svg", logoFrameId, "A cute fox logo, minimalist vector style, colourful, simple, clean, modern flat design, suitable for a brand logo.")
69
+ ```
70
+
71
+ ## Transforming an image the document already has
72
+
73
+ - `"vectorize-image"` draws into a frame you insert first, so that frame's `nodeId` follows the type, and it returns nothing. `"remove-background"` and `"replace-background"` write nothing and target no node: they produce a new asset and RETURN its url - nothing in the document moves until you apply it.
74
+ - `imageUrl` is the last argument of all three and must ALREADY be in the document: `Get` the node, pick the `{type: "image"}` fill you actually mean, and pass its `url`. A node's `fill` may be a list drawn in list order, so the visible image is usually the last image fill. Never invent, guess, or hand-write this url.
75
+
76
+ ```js
77
+ const imageUrl = n => [Get(n, {depth: 0}).fill].flat().filter(f => f?.type === "image").pop()?.url
78
+ ```
79
+
80
+ - The source must have ARRIVED: a url still starting with `pencil:pending-image-` fails, so a generated image or an earlier transform's output has to resolve in a previous `execute` call before you can transform it.
81
+ - Apply a returned url with `Update` on the node's `fill` (it holds one fill or a list), in the SAME execute call - no `TakeScreenshot`, `Export`, or further `Generate` in between. Those hand control back to the editor, and once the transform resolves the url you are still holding stops referring to anything. Never carry it into a later call through a global.
82
+
83
+ ```js
84
+ Update(nodeId, {fill: {type: "image", url: Generate("remove-background", sourceUrl)}})
85
+
86
+ const cutoutUrl = Generate("remove-background", sourceUrl)
87
+ Update(cardId, {fill: [{type: "image", url: cutoutUrl}, ...]})
88
+ ```
89
+
90
+ - The returned url is a pending placeholder like any other generated image, rewritten in place once the transform resolves. Until then the node renders the SOURCE image, so an early screenshot looks like the transform did nothing - check the url instead.
91
+ - Nothing else on the node is touched: the fill you passed as `imageUrl` changes only if you `Update` it with the returned url, and other fills stay as they are.
92
+ - The source must be a JPEG, PNG or WEBP, at most 16MB, and between 256px and 4096px in both dimensions - the image's own pixels, not the size of the node showing it.
93
+
94
+ ### `"remove-background"`
95
+
96
+ - Returns a transparent cutout of the subject.
97
+ - Use it when a photo's own background is in the way: a product shot, mascot, portrait, or object that has to sit on a design background, overlap another section, or bleed outside its frame.
98
+ - Whatever is behind the node shows through the cutout, so give it a transparent or design-colored backdrop rather than leaving an unrelated solid fill behind it.
99
+
100
+ ### `"replace-background"`
101
+
102
+ - Keeps the subject and generates a new background around it, returning the composed image. The one transform type that takes a prompt.
103
+ - Use it when the subject is right but its surroundings are not: a product in a styled scene, a portrait on a different backdrop, or stock photos across a design made to share one setting.
104
+ - The prompt describes the background, not the subject: state the setting, surface, lighting, and colors, consistent with the design's palette.
105
+ - Reach for `"remove-background"` instead when the design itself should show through behind the subject. The result here is a normal opaque image and needs no backdrop of its own.
106
+
107
+ ### `"vectorize-image"`
108
+
109
+ - Traces the image into vector paths. Insert a frame with the desired width and height and `placeholder: true` FIRST (no other node type is allowed) and pass its id as `nodeId`; the traced paths are scaled to its bounding box and inserted as its children, exactly like `"svg"`.
110
+ - Use it to turn an existing raster logo, mark, or flat illustration into clean, resolution-independent artwork. For artwork that does not exist yet, generate an SVG directly - never generate an image just to vectorize it.
111
+ - Works on flat, high-contrast artwork; photographs trace into a mess of paths.
112
+
113
+ ```js
114
+ Update(productShotId,{fill:{type:"image",url:Generate("remove-background",imageUrl(productShotId))}})
115
+
116
+ const pos = FindEmptySpace({width: 400, height: 400, padding: 80, nodeId: logoId})
117
+ tracedLogoId = Insert(document,{type:"frame",name:"Logo",x:pos.x,y:pos.y,width:400,height:400,placeholder:true})
118
+ Generate("vectorize-image",tracedLogoId,imageUrl(logoId))
119
+
120
+ const bottleUrl = Generate("replace-background",imageUrl(bottleId),"On a wet slate slab, soft morning light from the left, muted sage background, shallow depth of field")
121
+ for (const id of [bottleHeroId, bottleThumbId]) {
122
+ Update(id,{fill:{type:"image",url:bottleUrl}})
123
+ }
124
+ ```
@@ -185,6 +185,19 @@ export interface Script extends Entity, Size {
185
185
  /** Input values by name. */
186
186
  inputs?: { [key: string]: string | number | boolean | Variable };
187
187
  }
188
+ /** Live web page shown on the canvas. */
189
+ export interface Browser extends Entity, Size, CanHaveEffects, CanHaveStroke {
190
+ type: "browser";
191
+ /** Page URL. Empty shows a URL prompt. */
192
+ url?: string;
193
+ /** Device emulation preset id. Omit for responsive. */
194
+ deviceId?: string;
195
+ /** Page zoom factor. Default 1. */
196
+ zoom?: number;
197
+ scrollX?: number;
198
+ scrollY?: number;
199
+ cornerRadius?: NumberOrVariable | [NumberOrVariable, NumberOrVariable, NumberOrVariable, NumberOrVariable];
200
+ }
188
201
  /** Reuses another object. */
189
202
  export interface Ref extends Entity {
190
203
  type: "ref";
@@ -196,7 +209,7 @@ export interface Ref extends Entity {
196
209
  - `type` is present = replacement: the descendant node is fully replaced with a new node tree. */ };
197
210
  [key: string]: any;
198
211
  }
199
- export type Child = Frame | Group | Rectangle | Ellipse | Path | Polygon | Text | Note | Prompt | Context | Icon | Script | Ref;
212
+ export type Child = Frame | Group | Rectangle | Ellipse | Path | Polygon | Text | Note | Prompt | Context | Icon | Script | Browser | Ref;
200
213
  export type IdPath = string;
201
- export interface Document { version: "2.17"; themes?: { [key: string /** RegEx: [^:]+ */]: string[] }; imports?: { [key: string]: string /** Value: relative URI of imported .pen file. Key: short alias. */ }; variables?: { [key: string /** RegEx: [^:]+ */]: { type: "boolean"; value: BooleanOrVariable | { value: BooleanOrVariable; theme?: Theme }[] } | { type: "color"; value: ColorOrVariable | { value: ColorOrVariable; theme?: Theme }[] } | { type: "number"; value: NumberOrVariable | { value: NumberOrVariable; theme?: Theme }[] } | { type: "string"; value: StringOrVariable | { value: StringOrVariable; theme?: Theme }[] } }; children: (Frame | Group | Rectangle | Ellipse | Polygon | Path | Text | Note | Context | Prompt | Icon | Script | Ref)[]; }
214
+ export interface Document { version: "2.18"; themes?: { [key: string /** RegEx: [^:]+ */]: string[] }; imports?: { [key: string]: string /** Value: relative URI of imported .pen file. Key: short alias. */ }; variables?: { [key: string /** RegEx: [^:]+ */]: { type: "boolean"; value: BooleanOrVariable | { value: BooleanOrVariable; theme?: Theme }[] } | { type: "color"; value: ColorOrVariable | { value: ColorOrVariable; theme?: Theme }[] } | { type: "number"; value: NumberOrVariable | { value: NumberOrVariable; theme?: Theme }[] } | { type: "string"; value: StringOrVariable | { value: StringOrVariable; theme?: Theme }[] } }; children: (Frame | Group | Rectangle | Ellipse | Polygon | Path | Text | Note | Context | Prompt | Icon | Script | Browser | Ref)[]; }
202
215
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pen.dev/cli",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "CLI tool for running the pen.dev AI agent manipulating .pen design files",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -36,7 +36,7 @@
36
36
  "license": "UNLICENSED",
37
37
  "dependencies": {
38
38
  "@anthropic-ai/claude-agent-sdk": "^0.3.257",
39
- "@earendil-works/pi-coding-agent": "^0.84.4",
39
+ "@earendil-works/pi-coding-agent": "^0.85.1",
40
40
  "@inquirer/prompts": "^8.3.0",
41
41
  "@node-ipc/node-ipc": "^11.0.3",
42
42
  "@openai/codex-sdk": "^0.153.4",
@@ -48,6 +48,7 @@
48
48
  "devDependencies": {
49
49
  "@biomejs/biome": "^2.4.5",
50
50
  "@ha/agent": "file:../../lib/agent",
51
+ "@ha/device-auth": "file:../../lib/device-auth",
51
52
  "@ha/ipc": "file:../../lib/ipc",
52
53
  "@ha/mcp": "file:../../lib/mcp",
53
54
  "@ha/pencil-editor": "file:../../lib/pencil-editor",