@hypersocial/decks-cli 0.6.0 → 0.7.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.
- package/package.json +1 -1
- package/skills/hyperdecks/SKILL.md +45 -7
- package/skills/hyperdecks/chat.md +17 -5
- package/src/brandCli.mjs +41 -2
- package/src/brandImport.mjs +981 -0
- package/src/brandPreview.mjs +24 -0
- package/src/checkRules.mjs +36 -8
- package/src/commands.mjs +27 -8
- package/src/importer.mjs +1 -1
- package/src/ops.mjs +33 -12
- package/src/runtime.mjs +5 -2
package/package.json
CHANGED
|
@@ -124,6 +124,7 @@ Hyperdecks server; use the CLI where you have a shell and the tool otherwise.
|
|
|
124
124
|
| Step | CLI | MCP tool |
|
|
125
125
|
|---|---|---|
|
|
126
126
|
| Brand (once; every new deck starts from it) | `hyperdecks brand show` / `brand set` / `brand edit` | `get_brand` / `set_brand` |
|
|
127
|
+
| Import a brand (website, .pptx/.potx, .pdf, a deck; preview first) | `hyperdecks brand import <url\|file\|slug>` | `import_brand` |
|
|
127
128
|
| Scaffold | `hyperdecks deck new <slug> "<title>" --narrated [--layouts hero,product,number,closer]` | `new_deck` (`layouts`) |
|
|
128
129
|
| Images | `hyperdecks gen-image "<prompt>" --slug <slug>` | `generate_image` |
|
|
129
130
|
| Video clips | `hyperdecks gen-video "<prompt>" --slug <slug>` | `generate_video` |
|
|
@@ -226,6 +227,9 @@ its `##` heading.
|
|
|
226
227
|
| `glass` | dark or busy slides: head and captions on one frosted panel |
|
|
227
228
|
| `corner` | the slide is the point (a product shot, a chart, a demo): small head bottom-right, subtitles along the bottom |
|
|
228
229
|
|
|
230
|
+
The live head's face follows script cues too (`[nod]`, `[look #card]`,
|
|
231
|
+
`[smile]`, `[gesture: lightbulb]`; see the script topic); a still face ignores them.
|
|
232
|
+
|
|
229
233
|
Below 650 px stage and glass become corner on their own. Run
|
|
230
234
|
`hyperdecks check <deck> --layout` to be warned about any slide whose presenter
|
|
231
235
|
would cover its title.
|
|
@@ -307,10 +311,13 @@ a presenter style and a design.md (voice, do and don't). `deck new` and
|
|
|
307
311
|
`new_deck` put its tokens in `:root` and its logo on the first slide, so read
|
|
308
312
|
it first (`hyperdecks brand show --json`, `get_brand`) and write in its voice.
|
|
309
313
|
An explicit request for this deck wins over the brand; `--no-brand`
|
|
310
|
-
(`brand: false`) starts from the template's look. No brand yet? Offer to
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
(`brand: false`) starts from the template's look. No brand yet? Offer to
|
|
315
|
+
import it from what they already have: `hyperdecks brand import
|
|
316
|
+
<url|file.pptx|file.pdf|deck-slug>` (`import_brand`) reads the real colours,
|
|
317
|
+
fonts and logo (a PowerPoint's theme exactly) and returns a preview link, a
|
|
318
|
+
sample slide in that brand, and a token. Always show them the preview and
|
|
319
|
+
ask; save with `brand set --draft <token>` (`set_brand { draft }`) only after
|
|
320
|
+
they say yes.
|
|
314
321
|
|
|
315
322
|
**Fonts: pick a pairing.** Fonts load from Google Fonts. Use one of the
|
|
316
323
|
curated pairings below (display for titles, body for text, mono for kickers
|
|
@@ -371,7 +378,26 @@ where there is room so slide changes move the presenter.
|
|
|
371
378
|
palette hexes, "no text"). `--model nano-banana-2.1` when the image must
|
|
372
379
|
carry legible type.
|
|
373
380
|
- Video: `gen-video` for a short loop behind a hero or a product moment, with
|
|
374
|
-
`[play: video]` at its word; never needed for a deck to work.
|
|
381
|
+
`[play: video]` at its word; never needed for a deck to work. A clip that
|
|
382
|
+
plays on repeat should be made with `--loop` (`loop: true`): it starts and
|
|
383
|
+
ends on the same frame, so there is no jump at the seam. Give it `--image`
|
|
384
|
+
to loop a still you already have; without one a keyframe is rendered first.
|
|
385
|
+
- Moving cut-outs: `--transparent` (`transparent: true`) mattes the
|
|
386
|
+
background out, the moving version of a `gen-image --transparent` PNG: a
|
|
387
|
+
spinning part, a product turning, over the slide's own background. Use it
|
|
388
|
+
with `--loop` and a subject on a plain background. It returns a VP9 WebM
|
|
389
|
+
with alpha (Chrome, Firefox, Edge) and a stacked-alpha MP4 that the hosted
|
|
390
|
+
player draws on a canvas in Safari and on iOS, which ignore WebM alpha. The
|
|
391
|
+
job's `markup` is the tag to paste, as is:
|
|
392
|
+
|
|
393
|
+
```html
|
|
394
|
+
<video muted loop playsinline autoplay crossorigin="anonymous"
|
|
395
|
+
src="(video.url)" data-alpha-stacked="(video.stacked.url)"></video>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Keep `crossorigin` and `data-alpha-stacked`: the player needs both. A deck
|
|
399
|
+
without the hosted player shows the clip in a box in Safari. A transparent
|
|
400
|
+
clip costs 2 credits a second more than an opaque one.
|
|
375
401
|
- People: real photos only with permission; initials otherwise.
|
|
376
402
|
|
|
377
403
|
**Motion.** The presenter's moves are the deck's motion; keep everything else
|
|
@@ -425,6 +451,12 @@ phone screenshots, the same `fix` bar.
|
|
|
425
451
|
showcase is a slide of stacked cards, one point each, with a `go` to each
|
|
426
452
|
card at the word that starts it: `Write [go: #step-1] the slides, write
|
|
427
453
|
[go: #step-2] what you'd say`. Give each card an id.
|
|
454
|
+
- Face: the live head already moves with the speech (eyes, brows, a nod on a
|
|
455
|
+
falling end); add a few face cues only where they carry the point, each on
|
|
456
|
+
the word before it: `This is the big one [nod] and here [look #m3] is the
|
|
457
|
+
proof.` / `Not a slide tool [shake], a presenter.` / `Then it clicked
|
|
458
|
+
[gesture: lightbulb].` / `Really? [brow up] Yes [smile].` A still or
|
|
459
|
+
captions-only presenter ignores them.
|
|
428
460
|
|
|
429
461
|
| Cue | Effect |
|
|
430
462
|
|---|---|
|
|
@@ -434,7 +466,12 @@ phone screenshots, the same `fix` bar.
|
|
|
434
466
|
| `[pause: 0.5]` | a beat of silence (≤ 5 s) |
|
|
435
467
|
| `[play: video]` / `[pause: video]` | play or pause the slide's video |
|
|
436
468
|
| `[go: .list]` / `[go: slot]` | fly beside `.list` / back to the slot |
|
|
437
|
-
| `[look: h2]` | look at the title |
|
|
469
|
+
| `[look: h2]` / `[look #card]` | look at the title / the card, then back |
|
|
470
|
+
| `[look viewer]` / `[look away]` | look back at the viewer / glance aside |
|
|
471
|
+
| `[nod]` / `[shake]` | a nod or head shake that suits the mood |
|
|
472
|
+
| `[brow up]` / `[brow down]` | raise or lower the brows |
|
|
473
|
+
| `[smile]` / `[think]` | `[mood: happy]` / `[mood: thinking]` |
|
|
474
|
+
| `[gesture: <id>]` | one of the head's tracks: `knowing-nod`, `ponder-tilt`, `lightbulb`, `side-eye-hmm`, `chuckle-bob`, `sunny-nod`, `double-take`… (check lists them all) |
|
|
438
475
|
| `[custom: name]` | only the slide's controller sees it |
|
|
439
476
|
- `voice: dash` narrates with D.A.S.H.'s own robot voice, a synthetic
|
|
440
477
|
character voice (not a clone of anyone) any account may use, for decks that
|
|
@@ -515,7 +552,8 @@ the deck's QA link on desktop and phone and list each problem as `fix` or
|
|
|
515
552
|
|
|
516
553
|
Pick image models by job: `gen-image` defaults to GPT Image 2.5; `--model
|
|
517
554
|
nano-banana-2.1` is stronger on typography and photoreal scenes. `gen-video`
|
|
518
|
-
defaults to MiniMax H3 Max (`--model kling-v3` is the fallback)
|
|
555
|
+
defaults to MiniMax H3 Max (`--model kling-v3` is the fallback); `--loop` and
|
|
556
|
+
`--transparent` work with both. Use `chart` for
|
|
519
557
|
real numbers instead of a generated picture of a chart, `icons search` for
|
|
520
558
|
real line icons, and `icons --style` with the deck's palette for an
|
|
521
559
|
illustrated set that matches.
|
|
@@ -23,10 +23,13 @@ makes the hello deck; continue from step 5 below.
|
|
|
23
23
|
|
|
24
24
|
1. `deck_guide` (this page), then `deck_guide { topic: "layouts" }` to pick slides.
|
|
25
25
|
`get_brand` for the person's saved brand: write in its designMd's voice.
|
|
26
|
-
None yet
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
26
|
+
None yet? Offer to import it from what they have: `import_brand` with
|
|
27
|
+
their website (`url`), a PowerPoint or PDF they attach (`file`) or one of
|
|
28
|
+
their decks (`slug`). It reads the real colours, fonts and logo and returns
|
|
29
|
+
a `previewUrl` (a sample slide in that brand) and a `token`. Always give
|
|
30
|
+
them the preview link, say what it found (and its notes), and ask; only
|
|
31
|
+
after a yes call `set_brand { draft: token }`. Never save without the yes.
|
|
32
|
+
(`deck_guide { topic: "design" }` lists the font pairings.)
|
|
30
33
|
2. `new_deck { slug, title, layouts }`: registers your draft and returns the
|
|
31
34
|
full text of every file plus a `previewUrl` (the draft, untracked). It
|
|
32
35
|
starts in the saved brand's colours, fonts and logo (`brand` in the result
|
|
@@ -40,6 +43,12 @@ makes the hello deck; continue from step 5 below.
|
|
|
40
43
|
you swap each for an `<img>` of its real file, or delete the hero's art.
|
|
41
44
|
Icons: `search_icons { query }` returns real line icons as inline `<svg>`
|
|
42
45
|
in `currentColor`; paste one where a features card has its icon.
|
|
46
|
+
For motion, `generate_video` (poll `check_video_job`): `loop: true` for a
|
|
47
|
+
clip that repeats with no jump, `transparent: true` for a moving cut-out
|
|
48
|
+
over the slide. Paste the finished job's `markup` as is (a transparent
|
|
49
|
+
clip's `<video>` names a second file in `data-alpha-stacked`, which the
|
|
50
|
+
player shows in Safari and on iOS).
|
|
51
|
+
`deck_guide { topic: "design" }` says when each fits.
|
|
43
52
|
4. `write_deck_source` to change a file: `edits: [{ find, replace }]` with
|
|
44
53
|
exact text that occurs once (copy it from `get_deck_source`), or `content`
|
|
45
54
|
to replace the file. Pass `baseRev` (the `rev` you last read) so a
|
|
@@ -55,7 +64,10 @@ makes the hello deck; continue from step 5 below.
|
|
|
55
64
|
poll `check_narration_job` until `done`. The draft's player tag then has its
|
|
56
65
|
`data-manifest`. Its `presenter` says who will present (`face: null` is
|
|
57
66
|
captions only, `reason` says why): tell the person. No voice of their own
|
|
58
|
-
yet? Pass `voice: "dash"`.
|
|
67
|
+
yet? Pass `voice: "dash"`. To direct the face, put a few cues in
|
|
68
|
+
script.md on the words that carry the point (`[nod]`, `[look #card]`,
|
|
69
|
+
`[smile]`, `[gesture: lightbulb]`; `deck_guide script` lists them) and
|
|
70
|
+
narrate again.
|
|
59
71
|
8. `check_deck { slug }` again, then `review_deck { slug, draft: true }` and
|
|
60
72
|
`check_review_job`: it shoots desktop and 390×844 phone frames, which is how
|
|
61
73
|
you catch a slide that overflows a phone (the hosted check cannot measure
|
package/src/brandCli.mjs
CHANGED
|
@@ -7,20 +7,23 @@
|
|
|
7
7
|
// [--fonts <pairing>] [--display "<family>"] [--body …] [--mono …]
|
|
8
8
|
// [--logo <url|file>] [--no-logo] [--presenter-style s]
|
|
9
9
|
// [--design <file.md|->] [--name "<company>"]
|
|
10
|
+
// [--draft <token>] (a brand import, saved after you saw its preview)
|
|
11
|
+
// brand import <url|file|slug> read a brand from a website, a .pptx / .potx /
|
|
12
|
+
// .pdf or one of your decks into a draft + preview
|
|
10
13
|
// brand edit design.md in $VISUAL / $EDITOR, saved on exit
|
|
11
14
|
// brand fonts the curated font pairings
|
|
12
15
|
// brand clear forget the brand (new decks get the template look)
|
|
13
16
|
import { spawnSync } from "node:child_process";
|
|
14
17
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
15
18
|
import { tmpdir } from "node:os";
|
|
16
|
-
import { extname, join } from "node:path";
|
|
19
|
+
import { basename, extname, join } from "node:path";
|
|
17
20
|
import * as ops from "./ops.mjs";
|
|
18
21
|
import { FONT_PAIRINGS } from "./brand.mjs";
|
|
19
22
|
|
|
20
23
|
const LOGO_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", ".svg": "image/svg+xml", ".avif": "image/avif" };
|
|
21
24
|
const COLOR_FLAGS = { ink: "ink", paper: "paper", accent: "accent", "accent-2": "accent2", card: "card" };
|
|
22
25
|
const FONT_FLAGS = ["display", "body", "mono"];
|
|
23
|
-
const SET_USAGE = 'usage: brand set [--accent "#hex"] [--ink …] [--paper …] [--accent-2 …] [--card …] [--fonts <pairing>] [--display "<family>"] [--body …] [--mono …] [--logo <url|file>] [--no-logo] [--presenter-style stage|bubble|glass|corner] [--design <file.md|->] [--name "<company>"]';
|
|
26
|
+
const SET_USAGE = 'usage: brand set [--draft <token>] [--accent "#hex"] [--ink …] [--paper …] [--accent-2 …] [--card …] [--fonts <pairing>] [--display "<family>"] [--body …] [--mono …] [--logo <url|file>] [--no-logo] [--presenter-style stage|bubble|glass|corner] [--design <file.md|->] [--name "<company>"]';
|
|
24
27
|
|
|
25
28
|
export function brandLines(b) {
|
|
26
29
|
if (!b) return ["no brand saved: new decks get the template look", " set one: hyperdecks brand set --accent \"#ff5a1f\" --fonts editorial --logo ./logo.svg"];
|
|
@@ -64,6 +67,7 @@ export function brandArgs(opts, { readFile = readFileSync, stdin = () => readFil
|
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
if (opts["no-logo"]) args.clearLogo = true;
|
|
70
|
+
if (flag(opts, "draft") !== undefined) args.draft = flag(opts, "draft");
|
|
67
71
|
return args;
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -125,3 +129,38 @@ export async function brandClear(opts = {}, { log = (line) => console.log(line)
|
|
|
125
129
|
log(opts.json ? JSON.stringify(out) : "✓ brand cleared; new decks get the template look");
|
|
126
130
|
return out;
|
|
127
131
|
}
|
|
132
|
+
|
|
133
|
+
// What `brand import` read, for the terminal.
|
|
134
|
+
export function importLines(d) {
|
|
135
|
+
const colors = Object.entries(d.colors ?? {}).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
136
|
+
const fonts = Object.entries(d.fonts ?? {}).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
137
|
+
return [
|
|
138
|
+
`read from ${d.source?.kind} ${d.source?.ref}`,
|
|
139
|
+
...(d.name ? [`name: ${d.name}`] : []),
|
|
140
|
+
`colours: ${colors || "(none found)"}`,
|
|
141
|
+
`fonts: ${fonts || "(none found)"}`,
|
|
142
|
+
`logo: ${d.logoUrl ?? "(none found)"}`,
|
|
143
|
+
...(d.notes ?? []).map((n) => ` · ${n}`),
|
|
144
|
+
"",
|
|
145
|
+
`preview: ${d.previewUrl}`,
|
|
146
|
+
`Nothing is saved yet. To make it your brand: hyperdecks brand set --draft ${d.token}`,
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// brand import <url|file|slug>: an http(s) URL or a domain is a website, an
|
|
151
|
+
// existing path a file, anything else one of your deck slugs.
|
|
152
|
+
export async function brandImport(from, opts = {}, { log = (line) => console.log(line), readFile = readFileSync, exists = existsSync } = {}) {
|
|
153
|
+
const src = String(from ?? "").trim();
|
|
154
|
+
if (!src) throw new Error("usage: brand import <https://your.site | deck.pptx | brand.pdf | deck-slug> [--json]");
|
|
155
|
+
let args;
|
|
156
|
+
if (/^https?:\/\//i.test(src)) args = { url: src };
|
|
157
|
+
else if (exists(src)) args = { bytes: new Uint8Array(readFile(src)), fileName: basename(src) };
|
|
158
|
+
else if (/\.(pptx|potx|pdf)$/i.test(src)) throw new Error(`${src}: no such file`);
|
|
159
|
+
else if (/^[a-z0-9][a-z0-9-]*$/.test(src)) args = { slug: src };
|
|
160
|
+
else if (/^[\w-]+(\.[\w-]+)+(\/.*)?$/.test(src)) args = { url: `https://${src}` };
|
|
161
|
+
else throw new Error(`${src}: not a URL, a file or a deck slug`);
|
|
162
|
+
const d = await ops.importBrand(opts, args);
|
|
163
|
+
if (opts.json) log(JSON.stringify(d, null, 2));
|
|
164
|
+
else for (const line of importLines(d)) log(line);
|
|
165
|
+
return d;
|
|
166
|
+
}
|