@hypersocial/decks-cli 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ decks activity
34
34
 
35
35
  ## Auth
36
36
 
37
- `decks login` opens the production broker at `https://decks.hypersocial.ai` and
37
+ `decks login` opens the production broker at `https://hyperdecks.ai` and
38
38
  stores a refresh credential in:
39
39
 
40
40
  ```text
@@ -261,7 +261,7 @@ Capture defaults to the initial slide, 4 seconds, 600px width and 10 fps, with a
261
261
  `--json` prints one line on stdout, identical to `<out>/manifest.json`:
262
262
 
263
263
  ```json
264
- { "status": "exported | uploaded | failed", "shareUrl": "https://decks.hypersocial.ai/v/<token>",
264
+ { "status": "exported | uploaded | failed", "shareUrl": "https://hyperdecks.ai/v/<token>",
265
265
  "files": [{ "type": "gif | png | mp4", "path": "...", "url": "https://...convex.cloud/api/storage/...", "bytes": 74662, "width": 600, "height": 338 }],
266
266
  "email": { "sendable": true, "path": "<out>/email.html", "html": "<a href=\"<shareUrl>\"><img src=\"<gif url>\" ...></a>" },
267
267
  "warnings": [] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypersocial/decks-cli",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "Command-line client for Hypersocial Decks live analytics and share management.",
5
5
  "type": "module",
6
6
  "main": "./src/commands.mjs",
@@ -225,7 +225,7 @@ The deck contract (`check` enforces or warns on each line):
225
225
  so put slots on different sides across slides, as the scaffold's
226
226
  `.slot-left` does); the first slide has `data-intro`.
227
227
  - The player script stays
228
- `<script type="module" src="https://decks.hypersocial.ai/player/v1/player.js" data-manifest="…">`.
228
+ `<script type="module" src="https://hyperdecks.ai/player/v1/player.js" data-manifest="…">`.
229
229
  `narrate` fills `data-manifest`; do not edit it by hand.
230
230
  - A deck with no narration (a product tour, an imported design deck, a silent
231
231
  walkthrough) says so: `data-silent` on the player script. `check` then warns
@@ -448,11 +448,8 @@ saves one.
448
448
 
449
449
  <!-- font pairings -->
450
450
 
451
- **Icons: real ones first.** `hyperdecks icons search <word>` (`search_icons`)
452
- returns Lucide line icons as inline SVG stroked in `currentColor`, so they take
453
- the deck's colours: paste the `<svg>` in place of a features card's icon. Use
454
- `icons <subject...> --style` (`generate_icon_set`) only for illustrated icons
455
- in a style of your own.
451
+ **Textures, icons, pictures:** the `look` topic (one texture, one icon set
452
+ from `search_icons` or `generate_icon_set`, a picture on every other slide).
456
453
 
457
454
  **Pick the layout from what the slide says.** Each layout's file in
458
455
  `templates/layouts/` opens with its slot side, presenter and the cues that
@@ -463,6 +460,7 @@ suit it.
463
460
  | `hero` | the opening, a section opener; the presenter is the visual, no `.art` unless it shows the real subject (check fails while the placeholder `<div class="art">` is in: swap it for an `<img class="art">` or delete it) | stage, bottom-right; `[wave]`, `[look: h1]` |
464
461
  | `number` | one number with its source: proof, traction, a delta | stage; `[look: #<id>-figure]` as it is said |
465
462
  | `product` | a screenshot in a browser frame (`.frame.card` for a phone) with the point beside it | stage; `[look: #<id>-frame]`, `[reveal: #<id>-callout]` |
463
+ | `split` | the workhorse: a real picture (phone, laptop, 16:9) beside sticker, title, three ticks | stage, bottom-left; `[look: #<id>-frame]`, `[go: #<id>-t1]` … `t3` |
466
464
  | `phones` | a phone app's flow: a fanned row of phone cards | stage, bottom-left; `[go: #<id>-p1]` … `p3`, `[go: slot]` |
467
465
  | `quote` | a customer or user in their own words, quoted exactly | stage, bottom-left; `[look: #<id>-quote]` |
468
466
  | `compare` | before/after, them/us, three parallel lines each | stage; `[look: #<id>-before]`, `[go: #<id>-after]` |
@@ -476,12 +474,10 @@ suit it.
476
474
  two to four: `phones` (`--tilt`, `--shadow` on a card). A few slides at most:
477
475
  `bg-dots` (never under text), a `.deco` of inked tiles.
478
476
 
479
- Presenter style follows the deck (step 2, *Which presenter style*): `stage`
480
- when the presenter carries the story, which is what the layouts are drawn for
481
- (they keep the slot's corner clear); `corner` on a slide where a big
482
- visual is the point and moves are not needed; `glass` on a dark or busy
483
- picture. Three layouts (`quote`, `phones`, `closer`) sit left; put a `slot-left` on others
484
- where there is room so slide changes move the presenter.
477
+ Presenter style follows the deck (step 2): `stage` when the presenter carries
478
+ the story (the layouts keep the slot's corner clear); `corner` where a big
479
+ visual is the point; `glass` on a dark or busy picture. `quote`, `split`,
480
+ `phones` and `closer` sit left; add `slot-left` elsewhere where there is room.
485
481
 
486
482
  **Visuals: real first, generated second.**
487
483
 
@@ -495,10 +491,12 @@ where there is room so slide changes move the presenter.
495
491
  floating shapes, abstract "tech" art, clip-art metaphors) makes a deck look
496
492
  cheap. A visual earns its place only if it shows something real from the
497
493
  story: the product, the data, a concrete situation or object. When in doubt,
498
- leave it out: one big statement in type beats a picture of nothing. The
499
- presenter is the hero of the opening slide; don't put art behind it.
500
- - Icons: only when they label something the viewer must tell apart at a
501
- glance. Three short statements set large usually beat three icon cards.
494
+ leave it out: one big statement in type beats a picture of nothing. But a
495
+ deck of nothing but type is not the answer: reach for the real things
496
+ first (`look` topic). The presenter is the hero of the opening slide;
497
+ don't put generated art behind it.
498
+ - Icons: from the deck's one set (`look` topic), labelling things or in a
499
+ `.sticker`; not one per sentence. Bullets are `.ticks`.
502
500
  - Images: `gen-image` (`generate_image`) only for a concrete subject the
503
501
  deck is about (a product in use, a place, a real object), never a metaphor.
504
502
  One style per deck, described the same way every time (subject, medium,
@@ -543,6 +541,61 @@ still.
543
541
  - No CSS animation on slides beyond `.reveal`; the player animates the slide
544
542
  change.
545
543
 
544
+ <!-- topic: look -->
545
+ #### Look: textures, icons, pictures
546
+
547
+ What the house decks do that a plain agent deck doesn't. Decide these three
548
+ once per deck, before writing slides, and say them in your plan.
549
+
550
+ **1. A picture on at least every other slide**, the most real one you have:
551
+
552
+ 1. the product itself: `capture_url` (phone: width 390, height 844), or a
553
+ screen recording or photo the person gives you (`upload_deck_asset`);
554
+ 2. their people, logos and app icons (with permission);
555
+ 3. real data drawn with `render_chart`;
556
+ 4. only then `generate_image`, of a concrete subject (a place, an object,
557
+ the product in use), never a metaphor or abstract "tech" art.
558
+
559
+ No product to show (a plan, a strategy)? Make the slides visual with type
560
+ and the kit below: a `number` slide, stickers, ticks on a `panel`.
561
+
562
+ **2. One texture**, on two or three slides (an opener, a closer, a slide
563
+ with a visual), never under body text. On the section:
564
+
565
+ | Class | Looks like | For |
566
+ |---|---|---|
567
+ | `bg-lines` | diagonal speed lines fading toward `--fade-at` | product slides, beside a picture |
568
+ | `bg-halftone` | a corner of halftone dots | openers, numbers |
569
+ | `bg-rays` | rays from `--rays-at` | an opener or a closer |
570
+ | `bg-dots` | a fading dot grid | a phones row |
571
+
572
+ Grounds carry the rest: alternate `on-accent`, plain, `on-ink` and the soft
573
+ washes `on-tint`, `on-tint-2`, `on-tint-3` so each slide change reads.
574
+
575
+ **3. One icon set.** Pick one source and keep it for every icon in the deck:
576
+ Lucide (`search_icons`, same `strokeWidth` throughout); or one
577
+ `generate_icon_set` call with every subject you need and one `preset`
578
+ (`line`, `duotone`, `sticker`, `clay`; the palette defaults to the brand's);
579
+ or the product's own app icons and logos. Never mix two.
580
+ Put an icon or logo in a `.sticker` (tilted tile, hard shadow) on openers
581
+ and product slides; use `.ticks` (accent squares) for lists, not icons.
582
+
583
+ **Kit (in every layout deck's shell):** `.sticker` (`--tilt`; `.photo` for
584
+ an app icon or a face), `.ticks`, `.panel`, `.pill`, `.frame.card` (a phone),
585
+ `.frame.laptop` (a desktop shot). The `split` layout is image plus copy.
586
+
587
+ **Image prompts.** One recipe per deck, the same every time:
588
+ `<concrete subject>, <setting>, <medium: e.g. editorial photo, soft daylight>,
589
+ palette <ink hex> <accent hex> <paper hex>, no text, no logos`
590
+ (`generate_image { brand: true }` adds the palette and the "no text" for
591
+ you). Generate at the slide's shape (wide for a hero), in one batch.
592
+
593
+ **Slide recipes.** Opener: `on-accent` + `bg-rays` or `bg-halftone`, the logo
594
+ or a few app-icon stickers, one display line. Product or feature: `split`
595
+ with a real screen. Proof: `number` (huge, sourced) or `chart`. Plan:
596
+ `timeline` on a tint. People: `team` with real faces. Closer: `on-ink`, the
597
+ one action, stickers again.
598
+
546
599
  <!-- topic: script -->
547
600
  ### 3. Write slides and script
548
601
 
@@ -653,7 +706,7 @@ overflowing slide; `check <slug> --hosted --layout` measures a hosted draft.
653
706
  `--forbid` lists phrases that must not reach this audience (another customer,
654
707
  an internal codename). It matches the page's text, alt text, titles and
655
708
  script.md, not URLs in attributes, so forbidding your own brand still passes
656
- with the player's `decks.hypersocial.ai` script tag. Messages and what to do:
709
+ with the player's `hyperdecks.ai` script tag. Messages and what to do:
657
710
 
658
711
  | Message starts with | Fix |
659
712
  |---|---|
@@ -842,7 +895,7 @@ Capture defaults to the initial slide, 4 seconds, 600px width and 10 fps, with a
842
895
  `--json` prints one line on stdout, identical to `<out>/manifest.json`:
843
896
 
844
897
  ```json
845
- { "status": "exported | uploaded | failed", "shareUrl": "https://decks.hypersocial.ai/v/<token>",
898
+ { "status": "exported | uploaded | failed", "shareUrl": "https://hyperdecks.ai/v/<token>",
846
899
  "files": [{ "type": "gif | png | mp4", "path": "...", "url": "https://...convex.cloud/api/storage/...", "bytes": 74662, "width": 600, "height": 338 }],
847
900
  "email": { "sendable": true, "path": "<out>/email.html", "html": "<a href=\"<shareUrl>\"><img src=\"<gif url>\" ...></a>" },
848
901
  "warnings": [] }
@@ -4,7 +4,7 @@ You are building a narrated deck through the Hyperdecks connector: no terminal,
4
4
  no git, no folder. The deck's files (`index.html`, `script.md`, `evidence.md`)
5
5
  live on Hyperdecks as a draft; you read and edit them with tools, and nothing
6
6
  reaches a recipient until `release_deck`. Topics: call `deck_guide` with
7
- `format`, `script`, `scripts`, `design`, `layouts`, `presenter`, `transitions` or `check`
7
+ `format`, `script`, `scripts`, `design`, `look`, `layouts`, `presenter`, `transitions` or `check`
8
8
  before the step that needs it.
9
9
 
10
10
  ## First time here?
@@ -21,7 +21,8 @@ makes the hello deck; continue from step 5 below.
21
21
 
22
22
  ## The loop
23
23
 
24
- 1. `deck_guide` (this page), then `deck_guide { topic: "layouts" }` to pick slides.
24
+ 1. `deck_guide` (this page), then `deck_guide { topic: "layouts" }` to pick slides
25
+ and `deck_guide { topic: "look" }` to pick the deck's texture, icon set and pictures.
25
26
  `get_brand` for the person's saved brand: write in its designMd's voice.
26
27
  None yet? Offer to import it from what they have: `import_brand` with
27
28
  their website (`url`), a PowerPoint or PDF they attach (`file`) or one of
@@ -46,8 +47,8 @@ makes the hello deck; continue from step 5 below.
46
47
  layouts' stand-ins (hero's `<div class="art">`, product's
47
48
  `<div class="shot">`, chart's placeholder `<svg>`) fail `check_deck` until
48
49
  you swap each for an `<img>` of its real file, or delete the hero's art.
49
- Icons: `search_icons { query }` returns real line icons as inline `<svg>`
50
- in `currentColor`; paste one where a features card has its icon.
50
+ Icons: one set per deck (`look` topic): `search_icons { query }` for real
51
+ line icons as inline `<svg>` in `currentColor`, or one `generate_icon_set`.
51
52
  For motion, `generate_video` (poll `check_video_job`): `loop: true` for a
52
53
  clip that repeats with no jump, `transparent: true` for a moving cut-out
53
54
  over the slide. Paste the finished job's `markup` as is (a transparent
@@ -5,7 +5,11 @@
5
5
  // the deck's index.html and script.md as text, with the manifest and clip fetches
6
6
  // injected.
7
7
 
8
- export const PLAYER_V1 = "https://decks.hypersocial.ai/player/v1/player.js";
8
+ export const PLAYER_V1 = "https://hyperdecks.ai/player/v1/player.js";
9
+ // Decks released before the app moved to hyperdecks.ai load the same player
10
+ // from the old host, which serves it forever; both pass.
11
+ export const PLAYER_V1_LEGACY = "https://decks.hypersocial.ai/player/v1/player.js";
12
+ export const isPlayerV1 = (src) => src === PLAYER_V1 || src === PLAYER_V1_LEGACY;
9
13
  export const DEFAULT_MAX_DURATION = 180;
10
14
  export const MOBILE_TITLE_CHARS = 28;
11
15
  export const CAPTION_MIN_CHARS = 4;
@@ -61,6 +65,9 @@ export const MESSAGES = {
61
65
  morphPair: (id, prev) => `slide ${id}: morph needs an element with the same data-morph="<name>" on this slide and the one before it (${prev})`,
62
66
  transitionStale: (where, script, manifest) => `${where}: script.md says transition "${script}" but the manifest has "${manifest}": run \`hyperdecks narrate\` so the player gets it`,
63
67
  flyTwice: (ids) => `fly is for the deck's one big moment, but slides ${ids.join(", ")} all use it`,
68
+ // Warnings from the look topic (deck_guide look): the deck passes, but a person should look.
69
+ fewPictures: (shown, total) => `only ${shown} of ${total} slides show a picture (a screenshot, a recording, a photo, a chart); aim for a real one on every other slide (deck_guide topic look)`,
70
+ mixedIcons: (sets) => `the deck mixes icon sets (${sets.join(" and ")}); keep one set per deck (deck_guide topic look)`,
64
71
  // A layout's stand-in left in: `how` is the tool (hosted) or command (CLI) that makes the real file.
65
72
  placeholder: (id, what, el, how, img) => `slide ${id}: ${what} is still the layout's placeholder (${el}); ${how}, then put it in as ${img}`,
66
73
  // Warnings (check --layout): the deck still passes.
@@ -522,6 +529,10 @@ const PLACEHOLDERS = [
522
529
  layout: "l-product", tag: "div", cls: "shot", what: "the product screenshot", img: '<img class="shot" src="…" alt="…">',
523
530
  how: { hosted: "capture the real page with capture_url", cli: "capture the real page with `hyperdecks screenshot <url>`" },
524
531
  },
532
+ {
533
+ layout: "l-split", tag: "div", cls: "shot", what: "the picture", img: '<img class="shot" src="…" alt="…"> (or <video class="shot" src="…" autoplay muted loop playsinline>)',
534
+ how: { hosted: "capture the app at phone size with capture_url (width 390, height 844), or upload a screen recording or photo with upload_deck_asset", cli: "capture the app at phone size with `hyperdecks screenshot <url> --width 390 --height 844`, or add a screen recording or photo with `hyperdecks asset add`" },
535
+ },
525
536
  {
526
537
  layout: "l-phones", tag: "div", cls: "shot", what: "each phone screen", img: '<img class="shot" src="…" alt="…"> (or <video class="shot" src="…" autoplay muted loop playsinline>) in each card',
527
538
  how: { hosted: "capture the app at phone size with capture_url (width 390, height 844), or upload a screen recording with upload_deck_asset", cli: "capture the app at phone size with `hyperdecks screenshot <url> --width 390 --height 844`, or add a screen recording with `hyperdecks asset add`" },
@@ -549,7 +560,7 @@ export function htmlErrors(html, { exists = () => true, forbid = [], script = ""
549
560
  const errs = [...presenterStyleErrors(html, script), ...transitionErrors(html, script), ...cueSelectorErrors(html, script), ...cueGestureErrors(script), ...placeholderErrors(html, { hosted })];
550
561
  const player = playerScript(html);
551
562
  if (!player) errs.push(MESSAGES.noPlayer());
552
- else if (player.src !== PLAYER_V1) errs.push(MESSAGES.player(player.src));
563
+ else if (!isPlayerV1(player.src)) errs.push(MESSAGES.player(player.src));
553
564
  // The loading state must be in <head> to hold the page from its first paint,
554
565
  // with its fail-open so a deck whose player never loads still shows.
555
566
  const loading = /<style\b[^>]*\sdata-hd-loading(?=[\s>=])[^>]*>([\s\S]*?)<\/style>/i.exec(/<head\b[\s\S]*?<\/head>/i.exec(uncommented(html))?.[0] ?? "");
@@ -574,7 +585,7 @@ export function htmlErrors(html, { exists = () => true, forbid = [], script = ""
574
585
  const page = textOf(uncommented(html).replace(/<(script|style)\b[\s\S]*?<\/\1>/g, " ")).toLowerCase();
575
586
  // The raw HTML too (alt text, <title>, meta content, inline script), less
576
587
  // attribute values that are URLs: the player's own src (PLAYER_V1) names
577
- // decks.hypersocial.ai, so forbidding the brand would otherwise always fail.
588
+ // hyperdecks.ai (or decks.hypersocial.ai), so forbidding the brand would otherwise always fail.
578
589
  const raw = withoutUrlAttrs(uncommented(html)).toLowerCase();
579
590
  for (const phrase of phraseList(forbid)) {
580
591
  const p = phrase.toLowerCase();
@@ -740,7 +751,7 @@ export function absoluteRefs(html) {
740
751
  }
741
752
 
742
753
  // Hosts a released deck can rely on: our storage and app, the player, and fonts.
743
- const HOSTED_ASSET_HOSTS = [/\.convex\.cloud$/, /\.convex\.site$/, /(^|\.)hypersocial\.ai$/, /^fonts\.(googleapis|gstatic)\.com$/];
754
+ const HOSTED_ASSET_HOSTS = [/\.convex\.cloud$/, /\.convex\.site$/, /(^|\.)hypersocial\.ai$/, /(^|\.)hyperdecks\.ai$/, /^fonts\.(googleapis|gstatic)\.com$/];
744
755
  const isHostedAsset = (url) => {
745
756
  try {
746
757
  const host = new URL(url).hostname;
@@ -750,6 +761,35 @@ const isHostedAsset = (url) => {
750
761
  }
751
762
  };
752
763
 
764
+ // The look topic as warnings. A slide shows a picture when it holds an <img>
765
+ // (not the brand logo or an icon), <video>, <canvas>, <iframe>, a chart <svg> or an
766
+ // inline-style url() background (not a stylesheet's: that is usually a texture); a deck of three or more slides with fewer than a third that do
767
+ // reads as all type (the topic aims for every other slide; a plan with no
768
+ // product to show may have fewer). Icons count as one set each: inline Lucide <svg> (by stroke width),
769
+ // and <img> icons (class icon, or a generated icon-* asset).
770
+ const iconImg = (tag) => /\bicon\b/.test(attr(tag, "class") ?? "") || /\/icon-[\w-]+\.(png|webp|svg)\b/i.test(attr(tag, "src") ?? "");
771
+ export function lookWarnings(html) {
772
+ const all = slides(html);
773
+ const out = [];
774
+ const picture = (body) =>
775
+ /<(video|canvas|iframe)\b/i.test(body) ||
776
+ /<svg\b[^>]*class=["'][^"']*\bchart\b/i.test(body) ||
777
+ [...body.matchAll(/\bstyle=("[^"]*"|'[^']*')/gi)].some(([, v]) => /\burl\(/i.test(v)) ||
778
+ [...body.matchAll(/<img\b[^>]*>/gi)].some(([tag]) => !/\bbrand-logo\b/.test(attr(tag, "class") ?? "") && !iconImg(tag));
779
+ const shown = all.filter((s) => picture(uncommented(s.body)) || /\burl\(/i.test(attr(s.tag, "style") ?? "")).length;
780
+ if (all.length >= 3 && shown * 3 < all.length) out.push(MESSAGES.fewPictures(shown, all.length));
781
+ const body = uncommented(all.map((s) => s.body).join("\n"));
782
+ const sets = new Set();
783
+ for (const [tag] of body.matchAll(/<svg\b[^>]*>/gi)) {
784
+ if (/\blucide/.test(attr(tag, "class") ?? "")) sets.add(`Lucide at stroke-width ${attr(tag, "stroke-width") ?? "2"}`);
785
+ }
786
+ for (const [tag] of body.matchAll(/<img\b[^>]*>/gi)) {
787
+ if (iconImg(tag)) sets.add("image icons");
788
+ }
789
+ if (sets.size > 1) out.push(MESSAGES.mixedIcons([...sets].sort()));
790
+ return out;
791
+ }
792
+
753
793
  // Every rule on the deck's text. `exists(ref)` answers whether a local reference
754
794
  // resolves (true, false or "outside"); `hosted: true` means there is no folder,
755
795
  // so every local reference is an error naming upload_deck_asset, a hotlinked
@@ -769,6 +809,7 @@ export async function checkSources({ html, script = "" }, opts = {}, { fetch: fe
769
809
  const errors = htmlErrors(html, { exists, forbid, script, hosted: !!opts.hosted, strict });
770
810
  const warnings = [];
771
811
  if (!notesRequired(html, { strict })) for (const id of slidesWithoutNotes(html)) warnings.push(MESSAGES.noteSoft(id));
812
+ warnings.push(...lookWarnings(html));
772
813
  // Without a folder (hosted, or a caller that passes no reader) only the inline scripts are read.
773
814
  const walked = deckFiles(html, opts.hosted ? {} : { pathExists: opts.pathExists, read: opts.read, isDir: opts.isDir });
774
815
  for (const m of walked.missing) errors.push(MESSAGES.scriptRef(m.ref, m.from));
package/src/commands.mjs CHANGED
@@ -376,8 +376,9 @@ commands:
376
376
  engagement <recipient|token> [--deck <slug>] [--json]
377
377
  what one recipient watched per deck: narration heard, replays, drop-off, visits
378
378
  gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--transparent] [--image <url>] [--size <s>]
379
- [--quality <q>] [--count <n>] [--seed <n>] [--slug <deck>] [--download <file>]
380
- default model gpt-image-2.5; --seed applies to nano-banana-2.1
379
+ [--quality <q>] [--count <n>] [--seed <n>] [--brand] [--slug <deck>] [--download <file>]
380
+ default model gpt-image-2.5; --seed applies to nano-banana-2.1;
381
+ --brand adds your brand's colours and "no text, no logos" to the prompt
381
382
  image-job <jobId> [--json]
382
383
  gen-video "<prompt>" [--model h3-max|kling-v3] [--image <url>] [--seconds 5] [--aspect 16:9|9:16] [--loop] [--transparent] [--slug <deck>] [--name <name>] [--download <file>]
383
384
  a 3–10s clip via fal (default MiniMax H3 Max); --image animates that still and keeps its aspect
@@ -390,8 +391,9 @@ commands:
390
391
  a branded SVG: { "kind": "bar|line|donut|number", "labels", "series", "values", "value", "palette" }
391
392
  icons search <word...> [--limit 8] [--stroke-width 2] [--json]
392
393
  real icons from the Lucide library as inline SVG in currentColor (free, offline)
393
- icons <subject...> --style "<style>" [--palette "#hex,#hex"] [--slug <deck>] [--name <prefix>]
394
- a style-consistent set of generated (illustrated) icons, one per subject (max 12)
394
+ icons <subject...> --preset line|duotone|sticker|clay | --style "<style>" [--palette "#hex,#hex"] [--slug <deck>] [--name <prefix>]
395
+ a style-consistent set of generated (illustrated) icons, one per subject (max 12);
396
+ palette defaults to your brand's colours
395
397
  brand [show] your saved brand (colours, fonts, logo, design.md) that every new deck starts from
396
398
  brand set [--accent "#hex"] [--ink|--paper|--accent-2|--card "#hex"] [--fonts <pairing>]
397
399
  [--display|--body|--mono "<Google Fonts family>"] [--logo <url|file>] [--no-logo]
@@ -415,13 +417,16 @@ commands:
415
417
  a PDF of the released deck: one 1600x900 page per slide (a script page after each
416
418
  with --notes), a footer with the deck's tracked link; kept 30 days
417
419
  export <slug|deck-dir> --video [--size 1080p|720p|4k] [--shape 16:9|9:16|1:1] [--no-captions]
418
- [--no-avatar] [--title-card] [--end-card [--link <url>]] [--upload] [--out <file.mp4>] [--json]
420
+ [--no-avatar] [--title-card] [--end-card [--link <url>]] [--slides <a-b>] [--upload]
421
+ [--out <file.mp4>] [--json]
419
422
  an MP4 of the narrated deck, drawn frame by frame so the avatar is smooth (slower
420
423
  than real time: a 4-minute deck takes about 25 minutes with the live head on a
421
- laptop): the avatar, voice and captions, none of the player's chrome. A slug records
422
- the released copy, a deck folder its working copy. --title-card adds the title
423
- and presenter (2s), --end-card the deck's "Video viewers" link (3s); --upload
424
- keeps it with the deck's exports for 30 days
424
+ laptop; the dashboard's Export → Video splits it across machines, about 10
425
+ minutes): the avatar, voice and captions, none of the player's chrome. A slug
426
+ records the released copy, a deck folder its working copy. --slides 3-5 records
427
+ only those slides (counting from 1). --title-card adds the title and presenter
428
+ (2s), --end-card the deck's "Video viewers" link (3s); --upload keeps it with the
429
+ deck's exports for 30 days
425
430
  export list [<slug>] [--json] your exports and their download links
426
431
  export status <exportId> [--json] one export: a hosted video's progress, its link when done
427
432
  export cancel <exportId> stop a hosted video export (Export → Video, export_deck mp4)
@@ -474,7 +479,7 @@ async function genImage(pos, opts) {
474
479
  const prompt = pos.join(" ").trim() || opts.prompt;
475
480
  if (!prompt) {
476
481
  throw new Error(
477
- 'usage: gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--image <url>] [--transparent] [--size <s>] [--quality <q>] [--count <n>] [--format png|jpeg|webp] [--seed <n>] [--slug <deck>] [--name <name>] [--download <file>]',
482
+ 'usage: gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--image <url>] [--transparent] [--size <s>] [--quality <q>] [--count <n>] [--format png|jpeg|webp] [--seed <n>] [--brand] [--slug <deck>] [--name <name>] [--download <file>]',
478
483
  );
479
484
  }
480
485
  let out = await ops.generateImage(opts, {
@@ -487,6 +492,7 @@ async function genImage(pos, opts) {
487
492
  format: opts.format,
488
493
  model: opts.model,
489
494
  seed: opts.seed,
495
+ brand: opts.brand,
490
496
  slug: opts.slug,
491
497
  name: opts.name,
492
498
  });
@@ -625,12 +631,13 @@ async function chart(pos, opts) {
625
631
  }
626
632
 
627
633
  async function icons(pos, opts) {
628
- if (!pos.length || !opts.style) {
629
- throw new Error('usage: icons <subject...> --style "<style>" [--palette "#hex,#hex"] [--model nano-banana-2.1|gpt-image-2.5|gpt-image-2] [--slug <deck>] [--name <prefix>]');
634
+ if (!pos.length || (!opts.style && !opts.preset)) {
635
+ throw new Error('usage: icons <subject...> --preset line|duotone|sticker|clay | --style "<style>" [--palette "#hex,#hex"] [--model nano-banana-2.1|gpt-image-2.5|gpt-image-2] [--slug <deck>] [--name <prefix>]');
630
636
  }
631
637
  const queued = await ops.generateIconSet(opts, {
632
638
  subjects: pos,
633
639
  style: opts.style,
640
+ preset: opts.preset,
634
641
  palette: opts.palette ? String(opts.palette).split(",").map((c) => c.trim()).filter(Boolean) : undefined,
635
642
  model: opts.model,
636
643
  slug: opts.slug,