@hypersocial/decks-cli 0.2.2 → 0.3.1
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 +8 -3
- package/package.json +1 -1
- package/skills/hyperdecks/SKILL.md +113 -1
- package/skills/hyperdecks/chat.md +49 -0
- package/src/check.mjs +8 -363
- package/src/checkRules.mjs +558 -0
- package/src/commands.mjs +101 -2
- package/src/files.mjs +35 -0
- package/src/guide.mjs +55 -0
- package/src/hosted.mjs +74 -0
- package/src/init.mjs +605 -0
- package/src/layouts.mjs +71 -0
- package/src/narrate.mjs +2 -12
- package/src/ops.mjs +19 -2
- package/src/runtime.mjs +62 -14
- package/src/scaffold.mjs +8 -73
- package/src/source.mjs +69 -0
- package/src/voice.mjs +11 -0
- package/templates/first-deck/character.json +26 -0
- package/templates/first-deck/self.json +26 -0
- package/templates/script.md +7 -2
package/src/commands.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import * as ops from "./ops.mjs";
|
|
|
12
12
|
|
|
13
13
|
export const playbackLabel = (p) => p.narrationFinished ? "narration finished"
|
|
14
14
|
: p.narrationStarted ? "narration started"
|
|
15
|
-
: p.telemetryVersion
|
|
15
|
+
: (p.telemetryVersion ?? 0) >= 2 ? "narration not started" : "playback unknown (legacy)";
|
|
16
16
|
|
|
17
17
|
const fmtMs = (ms) => (ms >= 60000 ? `${(ms / 60000).toFixed(1)}m` : `${Math.round(ms / 1000)}s`);
|
|
18
18
|
|
|
@@ -167,6 +167,41 @@ async function activity(pos, opts) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
const pct = (n) => (n === null || n === undefined ? "n/a" : `${n}%`);
|
|
171
|
+
const slideName = (s) => (s.title ? `${s.index} ${s.title}` : `slide ${s.index}`);
|
|
172
|
+
|
|
173
|
+
async function engagement(pos, opts) {
|
|
174
|
+
const who = pos.join(" ");
|
|
175
|
+
if (!who) throw new Error("usage: engagement <recipient|token> [--deck <slug>] [--json]");
|
|
176
|
+
const entries = await ops.recipientEngagement(opts, who, opts.deck);
|
|
177
|
+
if (opts.json) {
|
|
178
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!entries.length) {
|
|
182
|
+
console.log(`(no links for "${who}"${opts.deck ? ` on ${opts.deck}` : ""})`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
for (const e of entries) {
|
|
186
|
+
const s = e.summary;
|
|
187
|
+
console.log(`${e.recipient} — ${e.deck.title} (${e.deck.slug}, link ${e.link.token}${e.link.enabled ? "" : ", off"})`);
|
|
188
|
+
if (!s.visits) {
|
|
189
|
+
console.log(" never opened");
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const titles = new Map(e.slides.map((r) => [r.index, r]));
|
|
193
|
+
const list = (xs) => (xs === null ? "n/a" : xs.length ? xs.map((i) => slideName(titles.get(i) ?? { index: i })).join(", ") : "none");
|
|
194
|
+
console.log(` visits: ${s.visits} (last ${new Date(s.lastSeenAt).toISOString().slice(0, 16).replace("T", " ")}) · ${fmtMs(s.totalMs)} engaged`);
|
|
195
|
+
console.log(` narration: ${s.narration.finished ? "finished" : s.narration.started ? "started" : "not started"} · heard ${pct(s.narration.heardPct)}${s.narration.telemetry === "v3" ? "" : " (playback detail unavailable)"}`);
|
|
196
|
+
console.log(` reached: slide ${s.furthestSlide}${e.deck.slides ? ` of ${e.deck.slides}` : ""}${s.droppedOffOn ? ` · dropped off on ${slideName(s.droppedOffOn)}` : ""}`);
|
|
197
|
+
console.log(` replayed: ${list(s.replayedSlides)}`);
|
|
198
|
+
console.log(` skipped: ${list(s.skippedSlides)}`);
|
|
199
|
+
console.log(` most time: ${s.mostTime.map((m) => `${slideName(m)} (${fmtMs(m.ms)})`).join(", ") || "none"}`);
|
|
200
|
+
if (!s.slideData) console.log(" slide data unavailable (this deck reports time only)");
|
|
201
|
+
if (s.deckChangedSinceView) console.log(" the deck changed since their last visit");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
170
205
|
async function credits(pos, opts) {
|
|
171
206
|
const bal = await ops.creditsBalance(opts);
|
|
172
207
|
const ledger = await ops.creditsLedger(opts, opts.limit || 10);
|
|
@@ -237,6 +272,12 @@ export function help() {
|
|
|
237
272
|
console.log(`decks ${VERSION}
|
|
238
273
|
|
|
239
274
|
commands:
|
|
275
|
+
init [--yes] [--json] [--agent <id>]... [--all-agents] [--voice dash] [--name <n> --about <a> --ask <k>] [--dir <path>]
|
|
276
|
+
[--no-voice] [--no-skill] [--no-release] [--no-open]
|
|
277
|
+
the first run: sign in, install the skill, pick a voice (clone yours, or D.A.S.H.),
|
|
278
|
+
scaffold a 3-slide hello deck from your answers, narrate it and release a QA link.
|
|
279
|
+
Re-running resumes where it stopped. --json prints one object; it never clones
|
|
280
|
+
(consent is typed by a person) and reports that step as needs-human.
|
|
240
281
|
login [--token <pat>] [--broker <url>] [--dev]
|
|
241
282
|
logout [--dev]
|
|
242
283
|
whoami [--json] [--dev]
|
|
@@ -247,6 +288,14 @@ commands:
|
|
|
247
288
|
check <slug|deck-dir> [--forbid "<phrase>,<phrase>"] [--max-duration <s>] [--layout] [--json]
|
|
248
289
|
the narrated-deck gate: slides vs manifest, timing, captions, clip URLs,
|
|
249
290
|
local refs, pinned player, a data-note per slide, mobile titles
|
|
291
|
+
deck new|check|release … --hosted
|
|
292
|
+
the same on a hosted draft (no folder, no git: what chat agents use over the remote MCP)
|
|
293
|
+
source pull <slug> [dir] | source push <slug> [dir] [--base-rev N]
|
|
294
|
+
move a hosted draft (index.html, script.md, evidence.md) to a folder and back
|
|
295
|
+
asset add <slug> <file|url> [--name n]
|
|
296
|
+
file an image, video or font under the deck; prints its hosted URL
|
|
297
|
+
guide [workflow|format|script|design|layouts|presenter|transitions|check]
|
|
298
|
+
the skill, one topic at a time (what deck_guide serves)
|
|
250
299
|
release <slug|deck-dir> [--to "<name>" [--email <e>]]... [--qa] [--no-notify] [--title t] [--voice v]
|
|
251
300
|
on a committed deck: check → narrate → import → share new per --to
|
|
252
301
|
share new|list|on|off|mute|unmute|rm|url
|
|
@@ -256,7 +305,10 @@ commands:
|
|
|
256
305
|
import-design <project-id> --serve-token <token> --entry <file.html> --slug <slug>
|
|
257
306
|
skill install [--agent codex,claude-code] [--all-agents]
|
|
258
307
|
skills install [--agent codex,claude-code] [--all-agents]
|
|
308
|
+
default: every agent found in your home directory (else codex, claude-code)
|
|
259
309
|
activity [--deck <slug>] [--limit <n>] [--json]
|
|
310
|
+
engagement <recipient|token> [--deck <slug>] [--json]
|
|
311
|
+
what one recipient watched per deck: narration heard, replays, drop-off, visits
|
|
260
312
|
gen-image "<prompt>" [--model gpt-image-2.5|nano-banana-2.1|gpt-image-2] [--transparent] [--image <url>] [--size <s>]
|
|
261
313
|
[--quality <q>] [--count <n>] [--seed <n>] [--slug <deck>] [--download <file>]
|
|
262
314
|
default model gpt-image-2.5; --seed applies to nano-banana-2.1
|
|
@@ -523,18 +575,44 @@ function commandTable(pos, rest, opts) {
|
|
|
523
575
|
const result = await createPreview(rest, opts);
|
|
524
576
|
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
577
|
},
|
|
578
|
+
init: async () => {
|
|
579
|
+
const { init } = await import("./init.mjs");
|
|
580
|
+
const out = await init(pos.slice(1), opts, opts.json ? { log: (line) => console.error(line) } : {});
|
|
581
|
+
if (opts.json) console.log(JSON.stringify(out, null, 2));
|
|
582
|
+
},
|
|
526
583
|
"deck new": async () => {
|
|
584
|
+
if (opts.hosted) {
|
|
585
|
+
const { hostedNewDeck } = await import("./source.mjs");
|
|
586
|
+
if (!opts.narrated) throw new Error("the published CLI scaffolds narrated decks only: add --narrated");
|
|
587
|
+
const out = await hostedNewDeck(opts, { slug: rest[0], title: rest[1], layouts: opts.layouts, presenterStyle: opts["presenter-style"] });
|
|
588
|
+
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}`);
|
|
589
|
+
}
|
|
527
590
|
const { deckNew } = await import("./scaffold.mjs");
|
|
528
591
|
const out = await deckNew(rest, opts, opts.json ? { log: (line) => console.error(line) } : {});
|
|
529
592
|
if (opts.json) console.log(JSON.stringify(out, null, 2));
|
|
530
593
|
},
|
|
531
594
|
check: async () => {
|
|
532
595
|
const { check } = await import("./check.mjs");
|
|
533
|
-
const
|
|
596
|
+
const { hostedCheck } = await import("./source.mjs");
|
|
597
|
+
const result = opts.hosted ? await hostedCheck(opts, { slug: pos[1], forbid: opts.forbid, maxDuration: opts["max-duration"] }) : await check(pos.slice(1), opts);
|
|
598
|
+
if (opts.hosted && !opts.json) {
|
|
599
|
+
console.log(`deck "${pos[1]}" rev ${result.rev}: ${result.slides} slides${result.totalDuration != null ? `, ${result.totalDuration}s narrated` : ""}`);
|
|
600
|
+
for (const e of result.errors) console.log(` - ${e}`);
|
|
601
|
+
if (!result.errors.length) console.log("✓ check passed");
|
|
602
|
+
for (const w of result.warnings) console.log(` ⚠ ${w}`);
|
|
603
|
+
}
|
|
534
604
|
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
535
605
|
if (result.errors.length) throw new Error(`check found ${result.errors.length} issue(s)`);
|
|
536
606
|
},
|
|
537
607
|
release: async () => {
|
|
608
|
+
if (opts.hosted) {
|
|
609
|
+
const { hostedRelease } = await import("./source.mjs");
|
|
610
|
+
const out = await hostedRelease(opts, { slug: pos[1], recipients: opts.recipients, qa: opts.qa, title: opts.title, forbid: opts.forbid });
|
|
611
|
+
if (opts.json) return void console.log(JSON.stringify(out, null, 2));
|
|
612
|
+
console.log(`✓ released ${out.slug} rev ${out.rev}\n deck: ${out.deckUrl}`);
|
|
613
|
+
for (const s of out.shares) console.log(`✓ share for "${s.recipient}"${s.qa ? " (QA, untracked)" : ""} → ${s.url}`);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
538
616
|
const { release } = await import("./release.mjs");
|
|
539
617
|
const out = await release(pos.slice(1), opts, opts.json ? { log: (line) => console.error(line) } : {});
|
|
540
618
|
if (opts.json) console.log(JSON.stringify(out, null, 2));
|
|
@@ -544,6 +622,26 @@ function commandTable(pos, rest, opts) {
|
|
|
544
622
|
const result = await narrate(pos.slice(1), opts, opts.json ? { log: (line) => console.error(line) } : {});
|
|
545
623
|
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
546
624
|
},
|
|
625
|
+
"source pull": async () => {
|
|
626
|
+
const { sourcePull } = await import("./source.mjs");
|
|
627
|
+
const out = await sourcePull(rest, opts, opts.json ? { log: (line) => console.error(line) } : {});
|
|
628
|
+
if (opts.json) console.log(JSON.stringify(out, null, 2));
|
|
629
|
+
},
|
|
630
|
+
"source push": async () => {
|
|
631
|
+
const { sourcePush } = await import("./source.mjs");
|
|
632
|
+
const out = await sourcePush(rest, opts, opts.json ? { log: (line) => console.error(line) } : {});
|
|
633
|
+
if (opts.json) console.log(JSON.stringify(out, null, 2));
|
|
634
|
+
},
|
|
635
|
+
"asset add": async () => {
|
|
636
|
+
const { assetAdd } = await import("./source.mjs");
|
|
637
|
+
const out = await assetAdd(rest, opts, opts.json ? { log: () => {} } : {});
|
|
638
|
+
if (opts.json) console.log(JSON.stringify(out, null, 2));
|
|
639
|
+
},
|
|
640
|
+
guide: async () => {
|
|
641
|
+
const { guide } = await import("./guide.mjs");
|
|
642
|
+
const out = await guide(pos.slice(1), opts);
|
|
643
|
+
if (opts.json) console.log(JSON.stringify(out, null, 2));
|
|
644
|
+
},
|
|
547
645
|
"voice list": async () => {
|
|
548
646
|
const voices = await ops.listVoices(opts);
|
|
549
647
|
if (opts.json) return void console.log(JSON.stringify(voices, null, 2));
|
|
@@ -592,6 +690,7 @@ function commandTable(pos, rest, opts) {
|
|
|
592
690
|
"skill install": () => installHyperdecksSkill(opts, rest),
|
|
593
691
|
"skills install": () => installHyperdecksSkill(opts, rest),
|
|
594
692
|
activity: () => activity(pos.slice(1), opts),
|
|
693
|
+
engagement: () => engagement(pos.slice(1), opts),
|
|
595
694
|
"gen-image": () => genImage(pos.slice(1), opts),
|
|
596
695
|
"image-job": () => imageJob(pos.slice(1), opts),
|
|
597
696
|
"gen-video": () => genVideo(pos.slice(1), opts),
|
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
|
+
|