@mangomagic/cli 0.1.15 → 0.1.17

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
@@ -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 in the terminal and save them to your library. Add `--count 3`. |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mangomagic/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "MangoMagic CLI — sign in, manage episodes, and expose MangoMagic to MCP clients (Claude Desktop, Cursor).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,292 @@
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, "&amp;")
24
+ .replace(/</g, "&lt;")
25
+ .replace(/>/g, "&gt;")
26
+ .replace(/"/g, "&quot;");
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 lineClasses(line, idx) {
53
+ const rawSize = typeof line === "object" && line ? line.size : "";
54
+ const size = ["hook", "punch", "body", "small"].includes(rawSize) ? rawSize : "";
55
+ return [
56
+ idx === 0 ? "primary" : "",
57
+ size ? `size-${size}` : "",
58
+ line?.gold ? "gold" : "",
59
+ ].filter(Boolean).join(" ");
60
+ }
61
+
62
+ function cardUrl(card) {
63
+ return card?.id ? `https://mangomagic.live/carousels/${card.id}` : "";
64
+ }
65
+
66
+ function hasFullSlideSpec(card) {
67
+ return Array.isArray(card?.spec?.slides) && card.spec.slides.length > 0;
68
+ }
69
+
70
+ function line(text, size = "body", gold = false) {
71
+ return { text, size, gold };
72
+ }
73
+
74
+ function fallbackSlides(card) {
75
+ const hook = compact(card?.hook) || cardTitle(card);
76
+ const seam = compact(card?.seam) || "A useful idea worth turning into a post.";
77
+ const engine = compact(card?.engine);
78
+ return [
79
+ {
80
+ type: "hook",
81
+ lines: [line(hook, "hook", true)],
82
+ swipe: true,
83
+ swipe_label: "Swipe",
84
+ },
85
+ {
86
+ type: "build",
87
+ lines: [line(seam, "body")],
88
+ swipe: true,
89
+ swipe_label: "Then",
90
+ },
91
+ {
92
+ type: "turn",
93
+ lines: [
94
+ line("Name the tension.", "punch", true),
95
+ line("Make it obvious enough that the right reader feels seen.", "body"),
96
+ ],
97
+ swipe: true,
98
+ swipe_label: "Use it",
99
+ },
100
+ {
101
+ type: "cta",
102
+ lines: [
103
+ line("Draft saved in MangoMagic.", "punch", true),
104
+ line("Use this local preview as the working copy, then polish the cloud draft.", "small"),
105
+ ],
106
+ swipe: false,
107
+ swipe_label: "",
108
+ },
109
+ ].map((slide) => engine && slide.type === "cta"
110
+ ? { ...slide, lines: [...slide.lines, line(`Engine: ${engine}`, "small")] }
111
+ : slide);
112
+ }
113
+
114
+ function slidesFor(card) {
115
+ return hasFullSlideSpec(card) ? card.spec.slides : fallbackSlides(card);
116
+ }
117
+
118
+ function captionFor(card) {
119
+ if (card?.caption) return card.caption;
120
+ const parts = [
121
+ compact(card?.hook),
122
+ compact(card?.seam),
123
+ "Turn this into a post that makes the right people feel caught in the act.",
124
+ cardUrl(card) ? `Edit the cloud draft: ${cardUrl(card)}` : "",
125
+ ].filter(Boolean);
126
+ return parts.join("\n\n");
127
+ }
128
+
129
+ function cardForExport(card) {
130
+ return {
131
+ ...card,
132
+ localPreview: {
133
+ derived: !hasFullSlideSpec(card),
134
+ slides: slidesFor(card),
135
+ caption: captionFor(card),
136
+ },
137
+ };
138
+ }
139
+
140
+ function exportRoot() {
141
+ return process.env.MANGOMAGIC_EXPORT_DIR || join(homedir(), "MangoMagic", "exports", "talking-cards");
142
+ }
143
+
144
+ function markdownFor(cards, meta) {
145
+ const out = [
146
+ `# MangoMagic Talking Cards`,
147
+ "",
148
+ `Exported: ${new Date().toLocaleString()}`,
149
+ `Directory: ${meta.dir}`,
150
+ "",
151
+ ];
152
+
153
+ cards.forEach((card, cardIdx) => {
154
+ out.push(`## ${cardIdx + 1}. ${cardTitle(card)}`, "");
155
+ if (card.seam) out.push(`Scenario: ${card.seam}`, "");
156
+ if (card.hook) out.push(`Hook: ${card.hook}`, "");
157
+ if (card.engine) out.push(`Engine: ${card.engine}`, "");
158
+ if (cardUrl(card)) out.push(`Cloud edit link: ${cardUrl(card)}`, "");
159
+
160
+ if (!hasFullSlideSpec(card)) {
161
+ 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.", "");
162
+ }
163
+
164
+ const slides = slidesFor(card);
165
+ if (slides.length) {
166
+ out.push("### Slides", "");
167
+ slides.forEach((slide, idx) => {
168
+ out.push(`Slide ${idx + 1}${slide.type ? ` (${slide.type})` : ""}`);
169
+ for (const line of slideLines(slide)) out.push(`- ${line}`);
170
+ if (slide.swipe_label) out.push(`- Swipe label: ${slide.swipe_label}`);
171
+ out.push("");
172
+ });
173
+ }
174
+
175
+ out.push("### Caption", "", captionFor(card), "");
176
+ });
177
+
178
+ return out.join("\n");
179
+ }
180
+
181
+ function captionsFor(cards) {
182
+ return cards.map((card, idx) => [
183
+ `# ${idx + 1}. ${cardTitle(card)}`,
184
+ captionFor(card),
185
+ cardUrl(card) ? `Edit: ${cardUrl(card)}` : "",
186
+ ].filter(Boolean).join("\n")).join("\n\n---\n\n");
187
+ }
188
+
189
+ function slideHtml(card, slide, idx, total) {
190
+ const bg = card?.spec?.bg || "#111111";
191
+ const ink = card?.spec?.ink || "#f8f4ec";
192
+ const accent = card?.spec?.accent || "#f1ab1c";
193
+ const lines = Array.isArray(slide?.lines) ? slide.lines : [];
194
+ return `
195
+ <section class="slide" style="--bg:${escapeHtml(bg)};--ink:${escapeHtml(ink)};--accent:${escapeHtml(accent)}">
196
+ <div class="slide-kicker">${idx + 1}/${total}${slide.type ? ` · ${escapeHtml(slide.type)}` : ""}</div>
197
+ <div class="slide-lines">
198
+ ${lines.map((line, lineIdx) => `<p class="${lineClasses(line, lineIdx)}">${escapeHtml(lineText(line))}</p>`).join("")}
199
+ </div>
200
+ ${slide.swipe_label ? `<div class="swipe">${escapeHtml(slide.swipe_label)}</div>` : ""}
201
+ ${card?.spec?.footer ? `<div class="footer">${escapeHtml(card.spec.footer)}</div>` : ""}
202
+ </section>`;
203
+ }
204
+
205
+ function htmlFor(cards) {
206
+ const decks = cards.map((card, idx) => {
207
+ const slides = slidesFor(card);
208
+ return `
209
+ <article class="deck">
210
+ <header>
211
+ <p class="eyebrow">Talking Card ${idx + 1}</p>
212
+ <h2>${escapeHtml(cardTitle(card))}</h2>
213
+ ${card.seam ? `<p>${escapeHtml(card.seam)}</p>` : ""}
214
+ ${cardUrl(card) ? `<a href="${escapeHtml(cardUrl(card))}">Open cloud draft</a>` : ""}
215
+ ${!hasFullSlideSpec(card) ? `<p class="note">Local preview derived from returned hook and scenario.</p>` : ""}
216
+ </header>
217
+ <div class="slides">
218
+ ${slides.map((slide, slideIdx) => slideHtml(card, slide, slideIdx, slides.length)).join("")}
219
+ </div>
220
+ <section class="caption"><h3>Caption</h3><pre>${escapeHtml(captionFor(card))}</pre></section>
221
+ </article>`;
222
+ }).join("\n");
223
+
224
+ return `<!doctype html>
225
+ <html lang="en">
226
+ <head>
227
+ <meta charset="utf-8" />
228
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
229
+ <title>MangoMagic Talking Cards</title>
230
+ <style>
231
+ :root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f7f4ef; color: #1d1c19; }
232
+ * { box-sizing: border-box; }
233
+ body { margin: 0; padding: 32px; }
234
+ main { max-width: 1180px; margin: 0 auto; }
235
+ h1 { margin: 0 0 8px; font-size: 32px; }
236
+ .meta { color: #6f6a61; margin-bottom: 32px; }
237
+ .deck { border-top: 1px solid #d8d0c2; padding: 28px 0 36px; }
238
+ header { max-width: 760px; margin-bottom: 20px; }
239
+ .eyebrow { color: #9b6b0f; text-transform: uppercase; font-size: 12px; font-weight: 800; letter-spacing: .08em; }
240
+ h2 { margin: 0 0 8px; font-size: 26px; }
241
+ a { color: #9b6b0f; font-weight: 700; }
242
+ .note { color: #7b725f; font-size: 14px; }
243
+ .slides { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; }
244
+ .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); }
245
+ .slide-kicker { position: absolute; top: 14px; left: 16px; color: var(--accent); font-size: 12px; font-weight: 800; text-transform: uppercase; }
246
+ .slide-lines p { margin: 10px 0; font-size: 20px; line-height: 1.08; font-weight: 800; overflow-wrap: anywhere; }
247
+ .slide-lines .gold, .slide-lines .primary { color: var(--accent); }
248
+ .slide-lines .size-hook { font-size: 30px; line-height: 1; }
249
+ .slide-lines .size-punch { font-size: 26px; line-height: 1.02; }
250
+ .slide-lines .size-body { font-size: 18px; line-height: 1.12; }
251
+ .slide-lines .size-small { font-size: 13px; line-height: 1.25; font-weight: 700; }
252
+ .swipe { position: absolute; right: 16px; bottom: 40px; color: var(--accent); font-weight: 800; }
253
+ .footer { position: absolute; left: 16px; right: 16px; bottom: 14px; font-size: 11px; color: color-mix(in srgb, var(--ink), transparent 36%); }
254
+ .caption { margin-top: 20px; max-width: 760px; background: #fff; border: 1px solid #e3dccf; border-radius: 10px; padding: 18px; }
255
+ .caption h3 { margin-top: 0; }
256
+ pre { white-space: pre-wrap; font: inherit; margin: 0; }
257
+ .empty { background: #24211c; }
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <main>
262
+ <h1>MangoMagic Talking Cards</h1>
263
+ <p class="meta">Exported ${escapeHtml(new Date().toLocaleString())}</p>
264
+ ${decks}
265
+ </main>
266
+ </body>
267
+ </html>`;
268
+ }
269
+
270
+ export function exportTalkingCards(result, { focus = "talking-cards" } = {}) {
271
+ const cards = cardsFrom(result);
272
+ if (!cards.length) return null;
273
+
274
+ const dir = join(exportRoot(), `${stamp()}_${slugify(focus || cards[0]?.title)}`);
275
+ mkdirSync(dir, { recursive: true });
276
+
277
+ const meta = {
278
+ dir,
279
+ count: cards.length,
280
+ indexPath: join(dir, "index.html"),
281
+ markdownPath: join(dir, "README.md"),
282
+ jsonPath: join(dir, "cards.json"),
283
+ captionsPath: join(dir, "captions.txt"),
284
+ };
285
+
286
+ writeFileSync(meta.jsonPath, JSON.stringify({ ...result, carousels: cards.map(cardForExport), exportedAt: new Date().toISOString() }, null, 2));
287
+ writeFileSync(meta.markdownPath, markdownFor(cards, meta));
288
+ writeFileSync(meta.captionsPath, captionsFor(cards));
289
+ writeFileSync(meta.indexPath, htmlFor(cards));
290
+
291
+ return meta;
292
+ }
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()}
@@ -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 save them as draft carousels.",
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 from the terminal and save them to your library.",
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 from a short idea and save them to the user's carousel library.",
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 }) => apiCall("cli-create-talking-cards", {
60
- body: {
61
- focus,
62
- count: Math.max(1, Math.min(Number(count || 3), 10)),
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: {