@mangomagic/cli 0.1.15 → 0.1.16
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 +10 -1
- package/package.json +1 -1
- package/src/chat/natural-language.mjs +60 -4
- package/src/export/cards.mjs +278 -0
- package/src/index.mjs +2 -0
- package/src/tools/catalog.mjs +4 -3
- package/src/tools/run.mjs +17 -6
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ runs skip auth.
|
|
|
24
24
|
| `mangomagic ask "..."` | Run one natural-language request. |
|
|
25
25
|
| `mangomagic tools` | Show the CLI/MCP catalog. Add `--all` for the full list or `--json` for machine-readable output. |
|
|
26
26
|
| `mangomagic tool <name> '{"json":"args"}'` | Run the same tool exposed to MCP clients. |
|
|
27
|
-
| `mangomagic cards "idea"` | Create talking cards
|
|
27
|
+
| `mangomagic cards "idea"` | Create talking cards, save cloud drafts, and export local files under `~/MangoMagic/exports/talking-cards`. Add `--count 3`. |
|
|
28
28
|
| `mangomagic open cards` | Open the Talking Cards workspace in a browser. |
|
|
29
29
|
| `mangomagic splash` | Show the splash once. `--anim`, `--loop` supported. |
|
|
30
30
|
| `mangomagic mcp` | Run as a stdio MCP server. Wire into Claude Desktop / Cursor. |
|
|
@@ -65,6 +65,15 @@ npx -y @mangomagic/cli cards "why founder-led sales stalls" --count 3
|
|
|
65
65
|
Obvious requests are handled locally. Fuzzy routing uses MangoMagic's backend AI
|
|
66
66
|
router, so end users never need to provide a Moonshot/Kimi key.
|
|
67
67
|
|
|
68
|
+
Talking card creation writes a local bundle by default:
|
|
69
|
+
|
|
70
|
+
- `index.html` for a browser preview
|
|
71
|
+
- `README.md` for the slide copy and edit links
|
|
72
|
+
- `captions.txt` for captions
|
|
73
|
+
- `cards.json` for structured data
|
|
74
|
+
|
|
75
|
+
Set `MANGOMAGIC_EXPORT_DIR` to change the export root.
|
|
76
|
+
|
|
68
77
|
## Requirements
|
|
69
78
|
|
|
70
79
|
Node 18 or later. The splash uses true-color ANSI; on terminals without it,
|
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@ import readline from "node:readline/promises";
|
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
3
|
import { apiCall } from "../api.mjs";
|
|
4
4
|
import { planWithKimi } from "../ai/kimi.mjs";
|
|
5
|
+
import { exportTalkingCards } from "../export/cards.mjs";
|
|
6
|
+
import { openUrl } from "../system/open-url.mjs";
|
|
5
7
|
import { runCatalogTool } from "../tools/run.mjs";
|
|
6
8
|
|
|
7
9
|
const GOLD = "\x1b[38;2;241;171;28m";
|
|
@@ -51,6 +53,13 @@ function localPlan(text) {
|
|
|
51
53
|
if (t.includes("brand doc") || t.includes("brand colour") || t.includes("brand color")) {
|
|
52
54
|
return { action: "open_brand_doc", args: {} };
|
|
53
55
|
}
|
|
56
|
+
if (
|
|
57
|
+
/\b(save|export|download|where|file|files|computer|local|view|open)\b/.test(t) &&
|
|
58
|
+
/\b(them|these|those|cards?|carousels?|files?)\b/.test(t) &&
|
|
59
|
+
!/\b(create|generate|make)\b/.test(t)
|
|
60
|
+
) {
|
|
61
|
+
return { action: "export_latest_cards", args: { open: /\b(open|view|show)\b/.test(t) } };
|
|
62
|
+
}
|
|
54
63
|
if (t.includes("talking card") || t.includes("carousel") || /\bcards?\b/.test(t)) {
|
|
55
64
|
return { action: "create_talking_cards", args: { focus: raw, count: inferCount(raw) } };
|
|
56
65
|
}
|
|
@@ -338,6 +347,29 @@ function cardUrl(card) {
|
|
|
338
347
|
return card?.id ? `https://mangomagic.live/carousels/${card.id}` : null;
|
|
339
348
|
}
|
|
340
349
|
|
|
350
|
+
function printExportInfo(info, { opened = false } = {}) {
|
|
351
|
+
process.stdout.write(`${BOLD}Saved locally.${RESET}\n`);
|
|
352
|
+
process.stdout.write(` Folder: ${GOLD}${info.dir}${RESET}\n`);
|
|
353
|
+
process.stdout.write(` View: ${GOLD}${info.indexPath}${RESET}${opened ? ` ${DIM}(opened)${RESET}` : ""}\n`);
|
|
354
|
+
process.stdout.write(` Copy: ${GOLD}${info.markdownPath}${RESET}\n`);
|
|
355
|
+
process.stdout.write(` Data: ${GOLD}${info.jsonPath}${RESET}\n`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function exportLatestCards(state = {}, { open = false } = {}) {
|
|
359
|
+
let info = state.latestCardExport;
|
|
360
|
+
if (!info && state.latestCards) {
|
|
361
|
+
info = exportTalkingCards(state.latestCards, { focus: state.latestCardFocus || "talking-cards" });
|
|
362
|
+
state.latestCardExport = info;
|
|
363
|
+
}
|
|
364
|
+
if (!info) {
|
|
365
|
+
process.stdout.write(`No recent talking cards in this session yet. Try ${GOLD}create 3 talking cards about founder-led sales${RESET} first.\n`);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const opened = open ? openUrl(info.indexPath) : false;
|
|
369
|
+
printExportInfo(info, { opened });
|
|
370
|
+
return info;
|
|
371
|
+
}
|
|
372
|
+
|
|
341
373
|
export function printCards(result) {
|
|
342
374
|
const cards = Array.isArray(result?.carousels) ? result.carousels : [];
|
|
343
375
|
if (!cards.length) {
|
|
@@ -348,6 +380,7 @@ export function printCards(result) {
|
|
|
348
380
|
process.stdout.write(`${BOLD}Created ${cards.length} talking card${cards.length === 1 ? "" : "s"}.${RESET}\n\n`);
|
|
349
381
|
for (const [idx, card] of cards.entries()) {
|
|
350
382
|
process.stdout.write(`${GOLD}${idx + 1}. ${card.title || "Untitled card"}${RESET}\n`);
|
|
383
|
+
if (card.hook) process.stdout.write(` Hook: ${card.hook}\n`);
|
|
351
384
|
if (card.seam) process.stdout.write(` ${DIM}${card.seam}${RESET}\n`);
|
|
352
385
|
const slides = Array.isArray(card?.spec?.slides) ? card.spec.slides : [];
|
|
353
386
|
for (const slide of slides.slice(0, 8)) {
|
|
@@ -361,7 +394,7 @@ export function printCards(result) {
|
|
|
361
394
|
}
|
|
362
395
|
}
|
|
363
396
|
|
|
364
|
-
export async function createTalkingCards({ focus, count = 3 } = {}) {
|
|
397
|
+
export async function createTalkingCards({ focus, count = 3, state = null, saveLocal = true, openLocal = false } = {}) {
|
|
365
398
|
const cleanFocus = compact(focus);
|
|
366
399
|
if (!cleanFocus) {
|
|
367
400
|
process.stdout.write(`Give me an idea, for example:\n ${GOLD}mangomagic cards "why founder-led sales stalls" --count 3${RESET}\n`);
|
|
@@ -374,6 +407,25 @@ export async function createTalkingCards({ focus, count = 3 } = {}) {
|
|
|
374
407
|
body: { focus: cleanFocus, count: Math.max(1, Math.min(Number(count || 3), 10)) },
|
|
375
408
|
});
|
|
376
409
|
printCards(result);
|
|
410
|
+
if (state) {
|
|
411
|
+
state.latestCards = result;
|
|
412
|
+
state.latestCardFocus = cleanFocus;
|
|
413
|
+
}
|
|
414
|
+
if (saveLocal) {
|
|
415
|
+
try {
|
|
416
|
+
const info = exportTalkingCards(result, { focus: cleanFocus });
|
|
417
|
+
result.localExport = info;
|
|
418
|
+
if (state) state.latestCardExport = info;
|
|
419
|
+
if (info) {
|
|
420
|
+
const opened = openLocal ? openUrl(info.indexPath) : false;
|
|
421
|
+
printExportInfo(info, { opened });
|
|
422
|
+
}
|
|
423
|
+
} catch (err) {
|
|
424
|
+
result.localExportError = err?.message ?? String(err);
|
|
425
|
+
process.stdout.write(`${DIM}Created in MangoMagic Cloud, but I could not write the local export: ${result.localExportError}${RESET}\n`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return result;
|
|
377
429
|
} catch (err) {
|
|
378
430
|
const message = err?.message ?? String(err);
|
|
379
431
|
if (message.includes("HTTP 404")) {
|
|
@@ -385,13 +437,14 @@ export async function createTalkingCards({ focus, count = 3 } = {}) {
|
|
|
385
437
|
}
|
|
386
438
|
}
|
|
387
439
|
|
|
388
|
-
export async function handleNaturalLanguage(text, actions, { allowModel = true } = {}) {
|
|
440
|
+
export async function handleNaturalLanguage(text, actions, { allowModel = true, state = null } = {}) {
|
|
389
441
|
let plan = localPlan(text);
|
|
390
442
|
if (!plan && allowModel) {
|
|
391
443
|
try {
|
|
392
444
|
plan = await planWithKimi(text, {
|
|
393
445
|
availableTools: [
|
|
394
446
|
"create_talking_cards",
|
|
447
|
+
"export_latest_cards",
|
|
395
448
|
"list_inbox",
|
|
396
449
|
"list_leads",
|
|
397
450
|
"analyze_guests",
|
|
@@ -430,7 +483,9 @@ ${DIM}${err?.message ?? err}${RESET}
|
|
|
430
483
|
case "exit":
|
|
431
484
|
return "exit";
|
|
432
485
|
case "create_talking_cards":
|
|
433
|
-
return createTalkingCards({ focus: args.focus || text, count: args.count || 3 });
|
|
486
|
+
return createTalkingCards({ focus: args.focus || text, count: args.count || 3, state });
|
|
487
|
+
case "export_latest_cards":
|
|
488
|
+
return exportLatestCards(state, { open: Boolean(args.open) });
|
|
434
489
|
case "open_cards":
|
|
435
490
|
return actions.cards({ path: "/carousels/generate", focus: args.focus });
|
|
436
491
|
case "open_brand_doc":
|
|
@@ -527,6 +582,7 @@ ${DIM}${err?.message ?? err}${RESET}
|
|
|
527
582
|
|
|
528
583
|
export async function chat(actions, { showHeader = true } = {}) {
|
|
529
584
|
const rl = readline.createInterface({ input, output });
|
|
585
|
+
const state = {};
|
|
530
586
|
if (showHeader) {
|
|
531
587
|
process.stdout.write(`
|
|
532
588
|
${BOLD}MangoMagic Chat${RESET}
|
|
@@ -544,7 +600,7 @@ Type ${DIM}exit${RESET} to leave.
|
|
|
544
600
|
if (err?.message === "readline was closed") break;
|
|
545
601
|
throw err;
|
|
546
602
|
}
|
|
547
|
-
const result = await handleNaturalLanguage(text, actions);
|
|
603
|
+
const result = await handleNaturalLanguage(text, actions, { state });
|
|
548
604
|
if (result === "exit") break;
|
|
549
605
|
process.stdout.write("\n");
|
|
550
606
|
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
function compact(s) {
|
|
6
|
+
return String(s || "").trim().replace(/\s+/g, " ");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function slugify(s) {
|
|
10
|
+
return compact(s)
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
13
|
+
.replace(/^-+|-+$/g, "")
|
|
14
|
+
.slice(0, 60) || "talking-cards";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stamp() {
|
|
18
|
+
return new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function escapeHtml(s) {
|
|
22
|
+
return String(s ?? "")
|
|
23
|
+
.replace(/&/g, "&")
|
|
24
|
+
.replace(/</g, "<")
|
|
25
|
+
.replace(/>/g, ">")
|
|
26
|
+
.replace(/"/g, """);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cardsFrom(result) {
|
|
30
|
+
return Array.isArray(result?.carousels) ? result.carousels : [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function titleFromSlug(s) {
|
|
34
|
+
return compact(s)
|
|
35
|
+
.replace(/[-_]+/g, " ")
|
|
36
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cardTitle(card) {
|
|
40
|
+
return titleFromSlug(card?.title) || "Untitled card";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function lineText(line) {
|
|
44
|
+
if (typeof line === "string") return line;
|
|
45
|
+
return line?.text || "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function slideLines(slide) {
|
|
49
|
+
return Array.isArray(slide?.lines) ? slide.lines.map(lineText).filter(Boolean) : [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cardUrl(card) {
|
|
53
|
+
return card?.id ? `https://mangomagic.live/carousels/${card.id}` : "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasFullSlideSpec(card) {
|
|
57
|
+
return Array.isArray(card?.spec?.slides) && card.spec.slides.length > 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function line(text, size = "body", gold = false) {
|
|
61
|
+
return { text, size, gold };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fallbackSlides(card) {
|
|
65
|
+
const hook = compact(card?.hook) || cardTitle(card);
|
|
66
|
+
const seam = compact(card?.seam) || "A useful idea worth turning into a post.";
|
|
67
|
+
const engine = compact(card?.engine);
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
type: "hook",
|
|
71
|
+
lines: [line(hook, "hook", true)],
|
|
72
|
+
swipe: true,
|
|
73
|
+
swipe_label: "Swipe",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "build",
|
|
77
|
+
lines: [line(seam, "body")],
|
|
78
|
+
swipe: true,
|
|
79
|
+
swipe_label: "Then",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "turn",
|
|
83
|
+
lines: [
|
|
84
|
+
line("Name the tension.", "punch", true),
|
|
85
|
+
line("Make it obvious enough that the right reader feels seen.", "body"),
|
|
86
|
+
],
|
|
87
|
+
swipe: true,
|
|
88
|
+
swipe_label: "Use it",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: "cta",
|
|
92
|
+
lines: [
|
|
93
|
+
line("Draft saved in MangoMagic.", "punch", true),
|
|
94
|
+
line("Use this local preview as the working copy, then polish the cloud draft.", "small"),
|
|
95
|
+
],
|
|
96
|
+
swipe: false,
|
|
97
|
+
swipe_label: "",
|
|
98
|
+
},
|
|
99
|
+
].map((slide) => engine && slide.type === "cta"
|
|
100
|
+
? { ...slide, lines: [...slide.lines, line(`Engine: ${engine}`, "small")] }
|
|
101
|
+
: slide);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function slidesFor(card) {
|
|
105
|
+
return hasFullSlideSpec(card) ? card.spec.slides : fallbackSlides(card);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function captionFor(card) {
|
|
109
|
+
if (card?.caption) return card.caption;
|
|
110
|
+
const parts = [
|
|
111
|
+
compact(card?.hook),
|
|
112
|
+
compact(card?.seam),
|
|
113
|
+
"Turn this into a post that makes the right people feel caught in the act.",
|
|
114
|
+
cardUrl(card) ? `Edit the cloud draft: ${cardUrl(card)}` : "",
|
|
115
|
+
].filter(Boolean);
|
|
116
|
+
return parts.join("\n\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cardForExport(card) {
|
|
120
|
+
return {
|
|
121
|
+
...card,
|
|
122
|
+
localPreview: {
|
|
123
|
+
derived: !hasFullSlideSpec(card),
|
|
124
|
+
slides: slidesFor(card),
|
|
125
|
+
caption: captionFor(card),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function exportRoot() {
|
|
131
|
+
return process.env.MANGOMAGIC_EXPORT_DIR || join(homedir(), "MangoMagic", "exports", "talking-cards");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function markdownFor(cards, meta) {
|
|
135
|
+
const out = [
|
|
136
|
+
`# MangoMagic Talking Cards`,
|
|
137
|
+
"",
|
|
138
|
+
`Exported: ${new Date().toLocaleString()}`,
|
|
139
|
+
`Directory: ${meta.dir}`,
|
|
140
|
+
"",
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
cards.forEach((card, cardIdx) => {
|
|
144
|
+
out.push(`## ${cardIdx + 1}. ${cardTitle(card)}`, "");
|
|
145
|
+
if (card.seam) out.push(`Scenario: ${card.seam}`, "");
|
|
146
|
+
if (card.hook) out.push(`Hook: ${card.hook}`, "");
|
|
147
|
+
if (card.engine) out.push(`Engine: ${card.engine}`, "");
|
|
148
|
+
if (cardUrl(card)) out.push(`Cloud edit link: ${cardUrl(card)}`, "");
|
|
149
|
+
|
|
150
|
+
if (!hasFullSlideSpec(card)) {
|
|
151
|
+
out.push("Note: the cloud response did not include the full slide spec, so this local preview was derived from the returned hook and scenario.", "");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const slides = slidesFor(card);
|
|
155
|
+
if (slides.length) {
|
|
156
|
+
out.push("### Slides", "");
|
|
157
|
+
slides.forEach((slide, idx) => {
|
|
158
|
+
out.push(`Slide ${idx + 1}${slide.type ? ` (${slide.type})` : ""}`);
|
|
159
|
+
for (const line of slideLines(slide)) out.push(`- ${line}`);
|
|
160
|
+
if (slide.swipe_label) out.push(`- Swipe label: ${slide.swipe_label}`);
|
|
161
|
+
out.push("");
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
out.push("### Caption", "", captionFor(card), "");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return out.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function captionsFor(cards) {
|
|
172
|
+
return cards.map((card, idx) => [
|
|
173
|
+
`# ${idx + 1}. ${cardTitle(card)}`,
|
|
174
|
+
captionFor(card),
|
|
175
|
+
cardUrl(card) ? `Edit: ${cardUrl(card)}` : "",
|
|
176
|
+
].filter(Boolean).join("\n")).join("\n\n---\n\n");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function slideHtml(card, slide, idx, total) {
|
|
180
|
+
const bg = card?.spec?.bg || "#111111";
|
|
181
|
+
const ink = card?.spec?.ink || "#f8f4ec";
|
|
182
|
+
const accent = card?.spec?.accent || "#f1ab1c";
|
|
183
|
+
const lines = slideLines(slide);
|
|
184
|
+
return `
|
|
185
|
+
<section class="slide" style="--bg:${escapeHtml(bg)};--ink:${escapeHtml(ink)};--accent:${escapeHtml(accent)}">
|
|
186
|
+
<div class="slide-kicker">${idx + 1}/${total}${slide.type ? ` · ${escapeHtml(slide.type)}` : ""}</div>
|
|
187
|
+
<div class="slide-lines">
|
|
188
|
+
${lines.map((line, lineIdx) => `<p class="${lineIdx === 0 ? "primary" : ""}">${escapeHtml(line)}</p>`).join("")}
|
|
189
|
+
</div>
|
|
190
|
+
${slide.swipe_label ? `<div class="swipe">${escapeHtml(slide.swipe_label)}</div>` : ""}
|
|
191
|
+
${card?.spec?.footer ? `<div class="footer">${escapeHtml(card.spec.footer)}</div>` : ""}
|
|
192
|
+
</section>`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function htmlFor(cards) {
|
|
196
|
+
const decks = cards.map((card, idx) => {
|
|
197
|
+
const slides = slidesFor(card);
|
|
198
|
+
return `
|
|
199
|
+
<article class="deck">
|
|
200
|
+
<header>
|
|
201
|
+
<p class="eyebrow">Talking Card ${idx + 1}</p>
|
|
202
|
+
<h2>${escapeHtml(cardTitle(card))}</h2>
|
|
203
|
+
${card.seam ? `<p>${escapeHtml(card.seam)}</p>` : ""}
|
|
204
|
+
${cardUrl(card) ? `<a href="${escapeHtml(cardUrl(card))}">Open cloud draft</a>` : ""}
|
|
205
|
+
${!hasFullSlideSpec(card) ? `<p class="note">Local preview derived from returned hook and scenario.</p>` : ""}
|
|
206
|
+
</header>
|
|
207
|
+
<div class="slides">
|
|
208
|
+
${slides.map((slide, slideIdx) => slideHtml(card, slide, slideIdx, slides.length)).join("")}
|
|
209
|
+
</div>
|
|
210
|
+
<section class="caption"><h3>Caption</h3><pre>${escapeHtml(captionFor(card))}</pre></section>
|
|
211
|
+
</article>`;
|
|
212
|
+
}).join("\n");
|
|
213
|
+
|
|
214
|
+
return `<!doctype html>
|
|
215
|
+
<html lang="en">
|
|
216
|
+
<head>
|
|
217
|
+
<meta charset="utf-8" />
|
|
218
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
219
|
+
<title>MangoMagic Talking Cards</title>
|
|
220
|
+
<style>
|
|
221
|
+
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f7f4ef; color: #1d1c19; }
|
|
222
|
+
* { box-sizing: border-box; }
|
|
223
|
+
body { margin: 0; padding: 32px; }
|
|
224
|
+
main { max-width: 1180px; margin: 0 auto; }
|
|
225
|
+
h1 { margin: 0 0 8px; font-size: 32px; }
|
|
226
|
+
.meta { color: #6f6a61; margin-bottom: 32px; }
|
|
227
|
+
.deck { border-top: 1px solid #d8d0c2; padding: 28px 0 36px; }
|
|
228
|
+
header { max-width: 760px; margin-bottom: 20px; }
|
|
229
|
+
.eyebrow { color: #9b6b0f; text-transform: uppercase; font-size: 12px; font-weight: 800; letter-spacing: .08em; }
|
|
230
|
+
h2 { margin: 0 0 8px; font-size: 26px; }
|
|
231
|
+
a { color: #9b6b0f; font-weight: 700; }
|
|
232
|
+
.note { color: #7b725f; font-size: 14px; }
|
|
233
|
+
.slides { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; }
|
|
234
|
+
.slide { aspect-ratio: 1 / 1; display: flex; flex-direction: column; justify-content: center; padding: 24px; position: relative; background: var(--bg); color: var(--ink); border-radius: 10px; overflow: hidden; box-shadow: 0 16px 36px rgba(20, 16, 8, .16); }
|
|
235
|
+
.slide-kicker { position: absolute; top: 14px; left: 16px; color: var(--accent); font-size: 12px; font-weight: 800; text-transform: uppercase; }
|
|
236
|
+
.slide-lines p { margin: 10px 0; font-size: 24px; line-height: 1.02; font-weight: 800; }
|
|
237
|
+
.slide-lines .primary { color: var(--accent); font-size: 30px; }
|
|
238
|
+
.swipe { position: absolute; right: 16px; bottom: 40px; color: var(--accent); font-weight: 800; }
|
|
239
|
+
.footer { position: absolute; left: 16px; right: 16px; bottom: 14px; font-size: 11px; color: color-mix(in srgb, var(--ink), transparent 36%); }
|
|
240
|
+
.caption { margin-top: 20px; max-width: 760px; background: #fff; border: 1px solid #e3dccf; border-radius: 10px; padding: 18px; }
|
|
241
|
+
.caption h3 { margin-top: 0; }
|
|
242
|
+
pre { white-space: pre-wrap; font: inherit; margin: 0; }
|
|
243
|
+
.empty { background: #24211c; }
|
|
244
|
+
</style>
|
|
245
|
+
</head>
|
|
246
|
+
<body>
|
|
247
|
+
<main>
|
|
248
|
+
<h1>MangoMagic Talking Cards</h1>
|
|
249
|
+
<p class="meta">Exported ${escapeHtml(new Date().toLocaleString())}</p>
|
|
250
|
+
${decks}
|
|
251
|
+
</main>
|
|
252
|
+
</body>
|
|
253
|
+
</html>`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function exportTalkingCards(result, { focus = "talking-cards" } = {}) {
|
|
257
|
+
const cards = cardsFrom(result);
|
|
258
|
+
if (!cards.length) return null;
|
|
259
|
+
|
|
260
|
+
const dir = join(exportRoot(), `${stamp()}_${slugify(focus || cards[0]?.title)}`);
|
|
261
|
+
mkdirSync(dir, { recursive: true });
|
|
262
|
+
|
|
263
|
+
const meta = {
|
|
264
|
+
dir,
|
|
265
|
+
count: cards.length,
|
|
266
|
+
indexPath: join(dir, "index.html"),
|
|
267
|
+
markdownPath: join(dir, "README.md"),
|
|
268
|
+
jsonPath: join(dir, "cards.json"),
|
|
269
|
+
captionsPath: join(dir, "captions.txt"),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
writeFileSync(meta.jsonPath, JSON.stringify({ ...result, carousels: cards.map(cardForExport), exportedAt: new Date().toISOString() }, null, 2));
|
|
273
|
+
writeFileSync(meta.markdownPath, markdownFor(cards, meta));
|
|
274
|
+
writeFileSync(meta.captionsPath, captionsFor(cards));
|
|
275
|
+
writeFileSync(meta.indexPath, htmlFor(cards));
|
|
276
|
+
|
|
277
|
+
return meta;
|
|
278
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -45,6 +45,7 @@ ${BOLD}You can type naturally now.${RESET}
|
|
|
45
45
|
|
|
46
46
|
Try:
|
|
47
47
|
${GOLD}create 3 talking cards about founder-led sales${RESET}
|
|
48
|
+
${GOLD}where are those files?${RESET}
|
|
48
49
|
${GOLD}analyse my guests${RESET}
|
|
49
50
|
${GOLD}show me my clips${RESET}
|
|
50
51
|
${GOLD}what episodes do I have?${RESET}
|
|
@@ -72,6 +73,7 @@ ${DIM}Try this in Claude, Cursor, or Codex after MCP is connected:${RESET}
|
|
|
72
73
|
${DIM}Talk in the terminal:${RESET}
|
|
73
74
|
${GOLD}${COMMAND_PREFIX} login${RESET}
|
|
74
75
|
${GOLD}${COMMAND_PREFIX} create talking cards about my LinkedIn idea${RESET}
|
|
76
|
+
${GOLD}${COMMAND_PREFIX} cards "why founder-led sales stalls" --count 3${RESET}
|
|
75
77
|
|
|
76
78
|
${DIM}Open app:${RESET} ${APP_ORIGIN}
|
|
77
79
|
${DIM}Token cache:${RESET} ${credentialsPath()}
|
package/src/tools/catalog.mjs
CHANGED
|
@@ -42,12 +42,13 @@ export const MCP_TOOL_CATALOG = [
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: "create_talking_cards",
|
|
45
|
-
description: "Generate branded LinkedIn-ready talking cards from a short idea and
|
|
45
|
+
description: "Generate branded LinkedIn-ready talking cards from a short idea, save cloud drafts, and export a local preview bundle.",
|
|
46
46
|
inputSchema: {
|
|
47
47
|
type: "object",
|
|
48
48
|
properties: {
|
|
49
49
|
focus: { type: "string", description: "The idea, theme, or LinkedIn angle to turn into cards" },
|
|
50
50
|
count: { type: "integer", minimum: 1, maximum: 10, default: 3 },
|
|
51
|
+
saveLocal: { type: "boolean", default: true, description: "Write index.html, README.md, captions.txt, and cards.json to ~/MangoMagic/exports/talking-cards" },
|
|
51
52
|
},
|
|
52
53
|
required: ["focus"],
|
|
53
54
|
},
|
|
@@ -65,7 +66,7 @@ export const QUICK_WINS = [
|
|
|
65
66
|
{
|
|
66
67
|
command: `${COMMAND_PREFIX} cards "why founder-led sales stalls" --count 3`,
|
|
67
68
|
title: "Create Talking Cards",
|
|
68
|
-
description: "Generate LinkedIn-ready talking cards
|
|
69
|
+
description: "Generate LinkedIn-ready talking cards, save cloud drafts, and export local files you can open immediately.",
|
|
69
70
|
status: "ready",
|
|
70
71
|
},
|
|
71
72
|
{
|
|
@@ -91,7 +92,7 @@ export const QUICK_WINS = [
|
|
|
91
92
|
export const NEXT_WORKFLOWS = [
|
|
92
93
|
{
|
|
93
94
|
name: "create_talking_cards",
|
|
94
|
-
description: "Generate a batch of branded talking cards
|
|
95
|
+
description: "Generate a batch of branded talking cards, save cloud drafts, and write local preview/copy/data files.",
|
|
95
96
|
},
|
|
96
97
|
{
|
|
97
98
|
name: "sync_linkedin_brand_context",
|
package/src/tools/run.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { apiCall } from "../api.mjs";
|
|
2
|
+
import { exportTalkingCards } from "../export/cards.mjs";
|
|
2
3
|
import { ALL_MCP_TOOL_CATALOG } from "./catalog.mjs";
|
|
3
4
|
import { functionNameFromEdgeTool } from "./edge-functions.mjs";
|
|
4
5
|
|
|
@@ -56,12 +57,22 @@ const BUILTIN_TOOL_HANDLERS = {
|
|
|
56
57
|
list_episodes: async ({ limit = 10 }) => apiCall("cli-list-episodes", { body: { limit } }),
|
|
57
58
|
get_episode: async ({ episode }) => apiCall("cli-get-episode", { body: { episode } }),
|
|
58
59
|
search_episodes: async ({ query }) => apiCall("cli-search-episodes", { body: { query } }),
|
|
59
|
-
create_talking_cards: async ({ focus, count = 3 }) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
create_talking_cards: async ({ focus, count = 3, saveLocal = true } = {}) => {
|
|
61
|
+
const result = await apiCall("cli-create-talking-cards", {
|
|
62
|
+
body: {
|
|
63
|
+
focus,
|
|
64
|
+
count: Math.max(1, Math.min(Number(count || 3), 10)),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (saveLocal !== false) {
|
|
68
|
+
try {
|
|
69
|
+
result.localExport = exportTalkingCards(result, { focus });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
result.localExportError = err?.message ?? String(err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
},
|
|
65
76
|
qualify_leads: async ({ stage = "all", limit = 10 } = {}) => {
|
|
66
77
|
const leads = await apiCall("magic-assistant", {
|
|
67
78
|
body: {
|