@hypersocial/decks-cli 0.2.2 → 0.3.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/src/commands.mjs CHANGED
@@ -237,6 +237,12 @@ export function help() {
237
237
  console.log(`decks ${VERSION}
238
238
 
239
239
  commands:
240
+ init [--yes] [--json] [--agent <id>]... [--all-agents] [--voice dash] [--name <n> --about <a> --ask <k>] [--dir <path>]
241
+ [--no-voice] [--no-skill] [--no-release] [--no-open]
242
+ the first run: sign in, install the skill, pick a voice (clone yours, or D.A.S.H.),
243
+ scaffold a 3-slide hello deck from your answers, narrate it and release a QA link.
244
+ Re-running resumes where it stopped. --json prints one object; it never clones
245
+ (consent is typed by a person) and reports that step as needs-human.
240
246
  login [--token <pat>] [--broker <url>] [--dev]
241
247
  logout [--dev]
242
248
  whoami [--json] [--dev]
@@ -247,6 +253,14 @@ commands:
247
253
  check <slug|deck-dir> [--forbid "<phrase>,<phrase>"] [--max-duration <s>] [--layout] [--json]
248
254
  the narrated-deck gate: slides vs manifest, timing, captions, clip URLs,
249
255
  local refs, pinned player, a data-note per slide, mobile titles
256
+ deck new|check|release … --hosted
257
+ the same on a hosted draft (no folder, no git: what chat agents use over the remote MCP)
258
+ source pull <slug> [dir] | source push <slug> [dir] [--base-rev N]
259
+ move a hosted draft (index.html, script.md, evidence.md) to a folder and back
260
+ asset add <slug> <file|url> [--name n]
261
+ file an image, video or font under the deck; prints its hosted URL
262
+ guide [workflow|format|script|design|layouts|presenter|transitions|check]
263
+ the skill, one topic at a time (what deck_guide serves)
250
264
  release <slug|deck-dir> [--to "<name>" [--email <e>]]... [--qa] [--no-notify] [--title t] [--voice v]
251
265
  on a committed deck: check → narrate → import → share new per --to
252
266
  share new|list|on|off|mute|unmute|rm|url
@@ -256,6 +270,7 @@ commands:
256
270
  import-design <project-id> --serve-token <token> --entry <file.html> --slug <slug>
257
271
  skill install [--agent codex,claude-code] [--all-agents]
258
272
  skills install [--agent codex,claude-code] [--all-agents]
273
+ default: every agent found in your home directory (else codex, claude-code)
259
274
  activity [--deck <slug>] [--limit <n>] [--json]
260
275
  gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--transparent] [--image <url>] [--size <s>]
261
276
  [--quality <q>] [--count <n>] [--seed <n>] [--slug <deck>] [--download <file>]
@@ -523,18 +538,44 @@ function commandTable(pos, rest, opts) {
523
538
  const result = await createPreview(rest, opts);
524
539
  console.log(opts.json ? JSON.stringify(result) : `Preview saved: ${result.manifestPath}\n${result.email.sendable ? result.email.path : "Insert the GIF or PNG manually; upload with an existing share for linked email HTML."}`);
525
540
  },
541
+ init: async () => {
542
+ const { init } = await import("./init.mjs");
543
+ const out = await init(pos.slice(1), opts, opts.json ? { log: (line) => console.error(line) } : {});
544
+ if (opts.json) console.log(JSON.stringify(out, null, 2));
545
+ },
526
546
  "deck new": async () => {
547
+ if (opts.hosted) {
548
+ const { hostedNewDeck } = await import("./source.mjs");
549
+ if (!opts.narrated) throw new Error("the published CLI scaffolds narrated decks only: add --narrated");
550
+ const out = await hostedNewDeck(opts, { slug: rest[0], title: rest[1], layouts: opts.layouts, presenterStyle: opts["presenter-style"] });
551
+ return void console.log(opts.json ? JSON.stringify(out, null, 2) : `✓ created hosted deck "${out.slug}" (rev ${out.rev})\n preview: ${out.previewUrl}\n edit locally: hyperdecks source pull ${out.slug}`);
552
+ }
527
553
  const { deckNew } = await import("./scaffold.mjs");
528
554
  const out = await deckNew(rest, opts, opts.json ? { log: (line) => console.error(line) } : {});
529
555
  if (opts.json) console.log(JSON.stringify(out, null, 2));
530
556
  },
531
557
  check: async () => {
532
558
  const { check } = await import("./check.mjs");
533
- const result = await check(pos.slice(1), opts);
559
+ const { hostedCheck } = await import("./source.mjs");
560
+ const result = opts.hosted ? await hostedCheck(opts, { slug: pos[1], forbid: opts.forbid, maxDuration: opts["max-duration"] }) : await check(pos.slice(1), opts);
561
+ if (opts.hosted && !opts.json) {
562
+ console.log(`deck "${pos[1]}" rev ${result.rev}: ${result.slides} slides${result.totalDuration != null ? `, ${result.totalDuration}s narrated` : ""}`);
563
+ for (const e of result.errors) console.log(` - ${e}`);
564
+ if (!result.errors.length) console.log("✓ check passed");
565
+ for (const w of result.warnings) console.log(` ⚠ ${w}`);
566
+ }
534
567
  if (opts.json) console.log(JSON.stringify(result, null, 2));
535
568
  if (result.errors.length) throw new Error(`check found ${result.errors.length} issue(s)`);
536
569
  },
537
570
  release: async () => {
571
+ if (opts.hosted) {
572
+ const { hostedRelease } = await import("./source.mjs");
573
+ const out = await hostedRelease(opts, { slug: pos[1], recipients: opts.recipients, qa: opts.qa, title: opts.title, forbid: opts.forbid });
574
+ if (opts.json) return void console.log(JSON.stringify(out, null, 2));
575
+ console.log(`✓ released ${out.slug} rev ${out.rev}\n deck: ${out.deckUrl}`);
576
+ for (const s of out.shares) console.log(`✓ share for "${s.recipient}"${s.qa ? " (QA, untracked)" : ""} → ${s.url}`);
577
+ return;
578
+ }
538
579
  const { release } = await import("./release.mjs");
539
580
  const out = await release(pos.slice(1), opts, opts.json ? { log: (line) => console.error(line) } : {});
540
581
  if (opts.json) console.log(JSON.stringify(out, null, 2));
@@ -544,6 +585,26 @@ function commandTable(pos, rest, opts) {
544
585
  const result = await narrate(pos.slice(1), opts, opts.json ? { log: (line) => console.error(line) } : {});
545
586
  if (opts.json) console.log(JSON.stringify(result, null, 2));
546
587
  },
588
+ "source pull": async () => {
589
+ const { sourcePull } = await import("./source.mjs");
590
+ const out = await sourcePull(rest, opts, opts.json ? { log: (line) => console.error(line) } : {});
591
+ if (opts.json) console.log(JSON.stringify(out, null, 2));
592
+ },
593
+ "source push": async () => {
594
+ const { sourcePush } = await import("./source.mjs");
595
+ const out = await sourcePush(rest, opts, opts.json ? { log: (line) => console.error(line) } : {});
596
+ if (opts.json) console.log(JSON.stringify(out, null, 2));
597
+ },
598
+ "asset add": async () => {
599
+ const { assetAdd } = await import("./source.mjs");
600
+ const out = await assetAdd(rest, opts, opts.json ? { log: () => {} } : {});
601
+ if (opts.json) console.log(JSON.stringify(out, null, 2));
602
+ },
603
+ guide: async () => {
604
+ const { guide } = await import("./guide.mjs");
605
+ const out = await guide(pos.slice(1), opts);
606
+ if (opts.json) console.log(JSON.stringify(out, null, 2));
607
+ },
547
608
  "voice list": async () => {
548
609
  const voices = await ops.listVoices(opts);
549
610
  if (opts.json) return void console.log(JSON.stringify(voices, null, 2));
package/src/files.mjs ADDED
@@ -0,0 +1,35 @@
1
+ // The packaged text files the hosted MCP route reads at runtime: templates and
2
+ // the skill. Each is a literal new URL(…, import.meta.url), which the Next.js
3
+ // bundler copies next to the route and the CLI resolves next to this file. A
4
+ // path that is not here cannot be read, so nothing outside the package is.
5
+ import { readFileSync } from "node:fs";
6
+
7
+ const FILES = {
8
+ "templates/narrated-deck.html": () => new URL("../templates/narrated-deck.html", import.meta.url),
9
+ "templates/script.md": () => new URL("../templates/script.md", import.meta.url),
10
+ "templates/evidence.md": () => new URL("../templates/evidence.md", import.meta.url),
11
+ "templates/layouts/_deck.html": () => new URL("../templates/layouts/_deck.html", import.meta.url),
12
+ "templates/layouts/chart.html": () => new URL("../templates/layouts/chart.html", import.meta.url),
13
+ "templates/layouts/closer.html": () => new URL("../templates/layouts/closer.html", import.meta.url),
14
+ "templates/layouts/compare.html": () => new URL("../templates/layouts/compare.html", import.meta.url),
15
+ "templates/layouts/features.html": () => new URL("../templates/layouts/features.html", import.meta.url),
16
+ "templates/layouts/hero.html": () => new URL("../templates/layouts/hero.html", import.meta.url),
17
+ "templates/layouts/number.html": () => new URL("../templates/layouts/number.html", import.meta.url),
18
+ "templates/layouts/product.html": () => new URL("../templates/layouts/product.html", import.meta.url),
19
+ "templates/layouts/quote.html": () => new URL("../templates/layouts/quote.html", import.meta.url),
20
+ "templates/layouts/team.html": () => new URL("../templates/layouts/team.html", import.meta.url),
21
+ "templates/layouts/timeline.html": () => new URL("../templates/layouts/timeline.html", import.meta.url),
22
+ "skills/hyperdecks/SKILL.md": () => new URL("../skills/hyperdecks/SKILL.md", import.meta.url),
23
+ "skills/hyperdecks/chat.md": () => new URL("../skills/hyperdecks/chat.md", import.meta.url),
24
+ };
25
+
26
+ const cache = new Map();
27
+
28
+ export function packageText(path) {
29
+ const url = FILES[path];
30
+ if (!url) throw new Error(`no packaged file ${path}`);
31
+ if (!cache.has(path)) cache.set(path, readFileSync(url(), "utf8"));
32
+ return cache.get(path);
33
+ }
34
+
35
+ export const PACKAGE_FILES = Object.keys(FILES);
package/src/guide.mjs ADDED
@@ -0,0 +1,55 @@
1
+ // hyperdecks guide [topic]
2
+ //
3
+ // The skill, served in sections, for agents that cannot install it (chat apps on
4
+ // the hosted MCP call deck_guide). `workflow` is chat.md; the other topics are
5
+ // the SKILL.md sections marked <!-- topic: … -->, and `layouts` is built from
6
+ // each layout file's leading comment.
7
+ import { packageText } from "./files.mjs";
8
+ import { LAYOUTS, readLayout } from "./layouts.mjs";
9
+
10
+ export const GUIDE_TOPICS = ["workflow", "format", "script", "design", "layouts", "presenter", "transitions", "check"];
11
+ const CLI_NOTE = "_Written for the CLI. On the hosted MCP there is no folder: use the tool names in `deck_guide` (workflow), hosted URLs for every file, and write_deck_source for edits._\n\n";
12
+
13
+ // One <!-- topic: name --> section of SKILL.md: its heading and body, up to the
14
+ // next topic marker or the next heading of the same or a higher level.
15
+ export function skillSection(skill, topic) {
16
+ const lines = skill.split("\n");
17
+ const start = lines.findIndex((l) => l.trim() === `<!-- topic: ${topic} -->`);
18
+ if (start === -1) return null;
19
+ const level = /^(#+)\s/.exec(lines[start + 1] ?? "")?.[1].length ?? 6;
20
+ const out = [];
21
+ for (const line of lines.slice(start + 1)) {
22
+ if (out.length && (/^<!-- topic: /.test(line.trim()) || ((/^(#+)\s/.exec(line)?.[1].length ?? 99) <= level))) break;
23
+ out.push(line);
24
+ }
25
+ return out.join("\n").trim();
26
+ }
27
+
28
+ function layoutsTopic() {
29
+ const rows = LAYOUTS.map((name) => {
30
+ const { meta } = readLayout(name);
31
+ return [`### ${name}`, meta.summary, meta.use && `Use: ${meta.use}`, meta.slot && `Slots: ${meta.slot}`, meta.cues && `Cues: ${meta.cues}`].filter(Boolean).join("\n");
32
+ });
33
+ return `# Layouts\n\nPass any of these, in order, as new_deck's \`layouts\` (repeat one for more of the same: number, number gives slides number and number-2).\n\n${rows.join("\n\n")}`;
34
+ }
35
+
36
+ // { topic, topics, text } for one topic; no topic gives the workflow.
37
+ export function deckGuide(topic = "workflow") {
38
+ const t = String(topic || "workflow").toLowerCase();
39
+ if (!GUIDE_TOPICS.includes(t)) throw new Error(`unknown topic "${topic}": pick one of ${GUIDE_TOPICS.join(", ")}`);
40
+ let text;
41
+ if (t === "workflow") text = packageText("skills/hyperdecks/chat.md");
42
+ else if (t === "layouts") text = layoutsTopic();
43
+ else {
44
+ const section = skillSection(packageText("skills/hyperdecks/SKILL.md"), t);
45
+ if (!section) throw new Error(`SKILL.md has no <!-- topic: ${t} --> section`);
46
+ text = CLI_NOTE + section;
47
+ }
48
+ return { topic: t, topics: GUIDE_TOPICS, text };
49
+ }
50
+
51
+ export async function guide(pos, opts = {}, { log = (line) => console.log(line) } = {}) {
52
+ const out = deckGuide(pos[0]);
53
+ if (!opts.json) log(out.text);
54
+ return out;
55
+ }
package/src/hosted.mjs ADDED
@@ -0,0 +1,74 @@
1
+ // The hosted deck source (convex/deckSources.ts) as ops: a deck's files kept on
2
+ // Hyperdecks as a draft. The hosted MCP route, the stdio MCP and the CLI
3
+ // (source.mjs) share these; no local file I/O, so the route can bundle them.
4
+ import { api, client, target } from "./runtime.mjs";
5
+ import { PRESENTER_STYLES } from "./checkRules.mjs";
6
+ import { NARRATED_FILES, assembleLayoutDeck, fillTemplate } from "./layouts.mjs";
7
+
8
+ export const SOURCE_FILES = Object.keys(NARRATED_FILES);
9
+ const SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
10
+ const urls = (opts, slug, previewToken) => {
11
+ const base = target(opts).shareBase;
12
+ return { previewUrl: previewToken ? `${base}/p/${previewToken}` : null, dashboardUrl: `${base}/dashboard/d/${slug}` };
13
+ };
14
+
15
+ // Scaffold the files (the same templates as deck new) and register the draft.
16
+ export async function hostedNewDeck(opts, { slug, title, layouts, presenterStyle } = {}) {
17
+ if (!slug || !title) throw new Error("slug and title are required");
18
+ if (!SLUG.test(slug)) throw new Error("slug must be lowercase letters, digits and dashes (max 64)");
19
+ if (presenterStyle !== undefined && !PRESENTER_STYLES.includes(presenterStyle)) throw new Error(`presenterStyle must be one of ${PRESENTER_STYLES.join(", ")}`);
20
+ const list = Array.isArray(layouts) ? layouts.join(",") : layouts;
21
+ const files = list
22
+ ? assembleLayoutDeck({ slug, title, presenterStyle, layouts: list })
23
+ : Object.fromEntries(Object.entries(NARRATED_FILES).map(([out, file]) => [out, fillTemplate(file, { slug, title, presenterStyle })]));
24
+ const made = await (await client(opts)).mutation(api.deckSources.create, { slug, title, files });
25
+ return { slug, rev: made.rev, files, ...urls(opts, slug, made.previewToken) };
26
+ }
27
+
28
+ export async function getDeckSource(opts, { slug, file } = {}) {
29
+ if (!slug) throw new Error("slug is required");
30
+ const got = await (await client(opts)).query(api.deckSources.get, { slug, ...(file ? { file } : {}) });
31
+ return { ...got, ...urls(opts, slug, got.previewToken) };
32
+ }
33
+
34
+ export async function writeDeckSource(opts, { slug, file, content, edits, baseRev } = {}) {
35
+ if (!slug || !file) throw new Error("slug and file are required");
36
+ if ((content === undefined) === (edits === undefined)) throw new Error("pass exactly one of content or edits");
37
+ const args = { slug, file, ...(content !== undefined ? { content: String(content) } : { edits }) };
38
+ if (baseRev !== undefined && baseRev !== null) args.baseRev = Number(baseRev);
39
+ return (await client(opts)).mutation(api.deckSources.write, args);
40
+ }
41
+
42
+ // A public URL (or a ChatGPT upload's download_url) filed under the deck.
43
+ export async function uploadDeckAsset(opts, { slug, name, url, file } = {}) {
44
+ if (!slug) throw new Error("slug is required");
45
+ const from = url ?? file?.download_url;
46
+ if (!from) throw new Error("pass url, or file (a chat upload with a download_url)");
47
+ // A same-name upload replaces the earlier asset (and its URL stops working), so
48
+ // a name taken from the file or URL gets a unique suffix; only an explicit name replaces.
49
+ const fallback = file?.file_name ?? (() => { try { return new URL(from).pathname.split("/").pop(); } catch { return ""; } })();
50
+ const clean = (s) => String(s).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[^A-Za-z0-9]+/, "");
51
+ const unique = (s) => { const m = /^(.*?)(\.[A-Za-z0-9]{1,8})?$/.exec(clean(s) || "asset"); return `${(m[1] || "asset").slice(0, 100)}-${Date.now().toString(36)}${m[2] ?? ""}`; };
52
+ const assetName = name ? clean(name).slice(0, 120) || "asset" : unique(fallback || "asset");
53
+ return (await client(opts)).action(api.assetsNode.importAssetFromUrl, { slug, name: assetName, url: String(from), ...(file?.mime_type ? { mimeType: String(file.mime_type) } : {}) });
54
+ }
55
+
56
+ export async function hostedCheck(opts, { slug, forbid, maxDuration } = {}) {
57
+ if (!slug) throw new Error("slug is required");
58
+ const list = typeof forbid === "string" ? forbid.split(",").map((s) => s.trim()).filter(Boolean) : forbid;
59
+ return (await client(opts)).action(api.deckSources.check, { slug, ...(list?.length ? { forbid: list } : {}), ...(maxDuration ? { maxDuration: Number(maxDuration) } : {}) });
60
+ }
61
+
62
+ export async function hostedRelease(opts, { slug, recipients, qa, title, forbid, rev, voice } = {}) {
63
+ if (!slug) throw new Error("slug is required");
64
+ const list = typeof forbid === "string" ? forbid.split(",").map((s) => s.trim()).filter(Boolean) : forbid;
65
+ const args = { slug };
66
+ if (recipients?.length) args.recipients = recipients.map((r) => ({ recipient: String(r.recipient), ...(r.email ? { email: String(r.email) } : {}) }));
67
+ if (qa) args.qa = true;
68
+ if (title) args.title = String(title);
69
+ if (list?.length) args.forbid = list;
70
+ if (rev !== undefined && rev !== null) args.rev = Number(rev);
71
+ if (voice) args.voice = String(voice);
72
+ return (await client(opts)).action(api.deckSources.release, args);
73
+ }
74
+