@mangomagic/cli 0.1.5 → 0.1.7

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
@@ -20,8 +20,10 @@ at `~/.mangomagic/credentials.json` (mode 0600) so the next runs skip auth.
20
20
  | `mangomagic home` | Show fast wins after sign-in. |
21
21
  | `mangomagic chat` | Start an interactive natural-language session. |
22
22
  | `mangomagic ask "..."` | Run one natural-language request. |
23
- | `mangomagic tools` | Show available MCP tools and near-next workflows. Add `--json` for machine-readable output. |
24
- | `mangomagic cards` | Open the Talking Cards workspace. Add `--no-open` to print the URL only. |
23
+ | `mangomagic tools` | Show the CLI/MCP catalog. Add `--all` for the full list or `--json` for machine-readable output. |
24
+ | `mangomagic tool <name> '{"json":"args"}'` | Run the same tool exposed to MCP clients. |
25
+ | `mangomagic cards "idea"` | Create talking cards in the terminal and save them to your library. Add `--count 3`. |
26
+ | `mangomagic open cards` | Open the Talking Cards workspace in a browser. |
25
27
  | `mangomagic splash` | Show the splash once. `--anim`, `--loop` supported. |
26
28
  | `mangomagic mcp` | Run as a stdio MCP server. Wire into Claude Desktop / Cursor. |
27
29
  | `mangomagic mcp-config` | Print MCP client JSON for copy-paste. |
@@ -44,11 +46,18 @@ Add to Claude Desktop's `claude_desktop_config.json` (or Cursor's MCP settings):
44
46
  The MCP server uses the same token cached by `mangomagic login`, so make sure
45
47
  you sign in first.
46
48
 
49
+ MCP exposes:
50
+
51
+ - Core user tools such as `list_episodes`, `get_episode`, and `search_episodes`.
52
+ - First-class MangoMagic business tools such as `get_user_stats`, `get_user_clips`, `create_clip`, `qualify_leads`, and `generate_viral_caption`.
53
+ - Admin/raw Edge Function tools prefixed with `edge_`, for example `edge_generate_carousels`. These go through a server-side runner with role checks and dry-run protection for webhook/auth/billing callbacks.
54
+
47
55
  ## Natural language
48
56
 
49
57
  ```bash
50
58
  npx -y @mangomagic/cli chat
51
59
  npx -y @mangomagic/cli create talking cards about AI adoption
60
+ npx -y @mangomagic/cli cards "why founder-led sales stalls" --count 3
52
61
  ```
53
62
 
54
63
  Obvious requests are handled locally. Fuzzy routing uses MangoMagic's backend AI
@@ -2,6 +2,12 @@
2
2
  // MangoMagic CLI entrypoint. Pure-Node, ESM, zero deps (MCP server uses
3
3
  // @modelcontextprotocol/sdk loaded lazily on demand).
4
4
  import { run } from "../src/index.mjs";
5
+
6
+ process.stdout.on("error", (err) => {
7
+ if (err?.code === "EPIPE") process.exit(0);
8
+ throw err;
9
+ });
10
+
5
11
  run(process.argv.slice(2)).catch((err) => {
6
12
  process.stderr.write(`\nmangomagic: ${err?.message ?? err}\n`);
7
13
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mangomagic/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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": {
package/src/api.mjs CHANGED
@@ -19,8 +19,13 @@ export async function apiCall(path, { method = "POST", body, query } = {}) {
19
19
  const text = await res.text();
20
20
  let json; try { json = JSON.parse(text); } catch { json = { raw: text }; }
21
21
  if (res.status === 401) {
22
- clearToken();
23
- throw new Error("Your CLI token was rejected. Run `mangomagic login` again.");
22
+ const message = json.error ?? json.raw ?? "";
23
+ const tokenWasChecked = path.startsWith("cli-") || /not_authenticated|token_revoked|token_expired/i.test(String(message));
24
+ if (tokenWasChecked) {
25
+ clearToken();
26
+ throw new Error("Your CLI token was rejected. Run `mangomagic login` again.");
27
+ }
28
+ throw new Error(`${path} rejected the CLI token. This backend function may need the CLI-auth bridge deployed.`);
24
29
  }
25
30
  if (!res.ok) throw new Error(json.error ?? `HTTP ${res.status}`);
26
31
  return json;
@@ -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 { ALL_MCP_TOOL_CATALOG } from "../tools/catalog.mjs";
6
+ import { runCatalogTool } from "../tools/run.mjs";
5
7
 
6
8
  const GOLD = "\x1b[38;2;241;171;28m";
7
9
  const DIM = "\x1b[2m";
@@ -22,7 +24,7 @@ function localPlan(text) {
22
24
  return { action: "open_brand_doc", args: {} };
23
25
  }
24
26
  if (t.includes("talking card") || t.includes("carousel") || /\bcards?\b/.test(t)) {
25
- return { action: "open_cards", args: { focus: raw } };
27
+ return { action: "create_talking_cards", args: { focus: raw, count: inferCount(raw) } };
26
28
  }
27
29
  if (t.includes("mcp") && (t.includes("config") || t.includes("connect") || t.includes("claude") || t.includes("cursor") || t.includes("codex"))) {
28
30
  return { action: "mcp_config", args: {} };
@@ -40,6 +42,12 @@ function localPlan(text) {
40
42
  return null;
41
43
  }
42
44
 
45
+ function inferCount(text) {
46
+ const match = text.match(/(?:^|\s)(?:make|create|generate)?\s*(\d{1,2})\s+(?:talking\s+)?cards?\b/i);
47
+ if (!match) return 3;
48
+ return Math.max(1, Math.min(Number(match[1]), 10));
49
+ }
50
+
43
51
  function asArray(data) {
44
52
  if (Array.isArray(data)) return data;
45
53
  if (Array.isArray(data?.episodes)) return data.episodes;
@@ -76,12 +84,71 @@ function printEpisode(data) {
76
84
  if (ep.id) process.stdout.write(`\n${DIM}${ep.id}${RESET}\n`);
77
85
  }
78
86
 
87
+ function cardUrl(card) {
88
+ return card?.id ? `https://mangomagic.live/carousels/${card.id}` : null;
89
+ }
90
+
91
+ export function printCards(result) {
92
+ const cards = Array.isArray(result?.carousels) ? result.carousels : [];
93
+ if (!cards.length) {
94
+ process.stdout.write("No cards were created.\n");
95
+ return;
96
+ }
97
+
98
+ process.stdout.write(`${BOLD}Created ${cards.length} talking card${cards.length === 1 ? "" : "s"}.${RESET}\n\n`);
99
+ for (const [idx, card] of cards.entries()) {
100
+ process.stdout.write(`${GOLD}${idx + 1}. ${card.title || "Untitled card"}${RESET}\n`);
101
+ if (card.seam) process.stdout.write(` ${DIM}${card.seam}${RESET}\n`);
102
+ const slides = Array.isArray(card?.spec?.slides) ? card.spec.slides : [];
103
+ for (const slide of slides.slice(0, 8)) {
104
+ const line = (slide.lines || []).map((l) => l.text).filter(Boolean).join(" / ");
105
+ if (line) process.stdout.write(` - ${line}\n`);
106
+ }
107
+ if (card.caption) process.stdout.write(`\n Caption: ${card.caption.replace(/\n/g, "\n ")}\n`);
108
+ const url = cardUrl(card);
109
+ if (url) process.stdout.write(` Edit: ${url}\n`);
110
+ process.stdout.write("\n");
111
+ }
112
+ }
113
+
114
+ export async function createTalkingCards({ focus, count = 3 } = {}) {
115
+ const cleanFocus = compact(focus);
116
+ if (!cleanFocus) {
117
+ process.stdout.write(`Give me an idea, for example:\n ${GOLD}mangomagic cards "why founder-led sales stalls" --count 3${RESET}\n`);
118
+ return;
119
+ }
120
+
121
+ process.stdout.write(`${DIM}Creating ${count} talking card${count === 1 ? "" : "s"}...${RESET}\n`);
122
+ try {
123
+ const result = await apiCall("cli-create-talking-cards", {
124
+ body: { focus: cleanFocus, count: Math.max(1, Math.min(Number(count || 3), 10)) },
125
+ });
126
+ printCards(result);
127
+ } catch (err) {
128
+ const message = err?.message ?? String(err);
129
+ if (message.includes("HTTP 404")) {
130
+ process.stdout.write(`${BOLD}The terminal card-creation backend is not deployed yet.${RESET}\n`);
131
+ process.stdout.write(`This CLI is ready, but MangoMagic Cloud still needs the \`cli-create-talking-cards\` function deployed.\n`);
132
+ return;
133
+ }
134
+ throw err;
135
+ }
136
+ }
137
+
79
138
  export async function handleNaturalLanguage(text, actions, { allowModel = true } = {}) {
80
139
  let plan = localPlan(text);
81
140
  if (!plan && allowModel) {
82
141
  try {
83
142
  plan = await planWithKimi(text, {
84
- availableTools: ["open_cards", "open_brand_doc", "show_tools", "mcp_config", "list_episodes", "search_episodes", "get_episode", "home"],
143
+ availableTools: [
144
+ "create_talking_cards",
145
+ "open_cards",
146
+ "open_brand_doc",
147
+ "show_tools",
148
+ "mcp_config",
149
+ "home",
150
+ ...ALL_MCP_TOOL_CATALOG.map((tool) => tool.name),
151
+ ],
85
152
  });
86
153
  } catch (err) {
87
154
  process.stdout.write(`
@@ -107,6 +174,8 @@ ${DIM}${err?.message ?? err}${RESET}
107
174
  switch (action) {
108
175
  case "exit":
109
176
  return "exit";
177
+ case "create_talking_cards":
178
+ return createTalkingCards({ focus: args.focus || text, count: args.count || 3 });
110
179
  case "open_cards":
111
180
  return actions.cards({ path: "/carousels/generate", focus: args.focus });
112
181
  case "open_brand_doc":
@@ -137,6 +206,15 @@ ${DIM}${err?.message ?? err}${RESET}
137
206
  const data = await apiCall("cli-get-episode", { body: { episode: args.episode } });
138
207
  return printEpisode(data);
139
208
  }
209
+ case "run_tool": {
210
+ if (!args.toolName) {
211
+ process.stdout.write("Which tool should I run?\n");
212
+ return;
213
+ }
214
+ const data = await runCatalogTool(args.toolName, args.args || {});
215
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
216
+ return;
217
+ }
140
218
  case "answer":
141
219
  process.stdout.write(`${args.text || "I can help with MangoMagic episodes, talking cards, and MCP setup."}\n`);
142
220
  return;
package/src/index.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  import { deviceLogin } from "./auth/device-flow.mjs";
2
- import { chat, handleNaturalLanguage } from "./chat/natural-language.mjs";
2
+ import { chat, createTalkingCards, handleNaturalLanguage } from "./chat/natural-language.mjs";
3
3
  import { loadToken, saveToken, clearToken, credentialsPath } from "./auth/token-store.mjs";
4
4
  import { APP_ORIGIN } from "./config.mjs";
5
5
  import { openUrl } from "./system/open-url.mjs";
6
- import { COMMAND_PREFIX, MCP_TOOL_CATALOG, NEXT_WORKFLOWS, QUICK_WINS } from "./tools/catalog.mjs";
6
+ import { ALL_MCP_TOOL_CATALOG, COMMAND_PREFIX, MCP_TOOL_CATALOG, NEXT_WORKFLOWS, QUICK_WINS } from "./tools/catalog.mjs";
7
+ import { runCatalogTool } from "./tools/run.mjs";
7
8
  import { playSplash } from "./ui/splash.mjs";
8
9
 
9
10
  const GOLD = "\x1b[38;2;241;171;28m";
@@ -21,7 +22,9 @@ ${BOLD}mangomagic${RESET} ${DIM}— sign into MangoMagic and bring your account
21
22
  ${GOLD}mangomagic home${RESET} Show fast wins for creating value right now.
22
23
  ${GOLD}mangomagic chat${RESET} Talk to MangoMagic in natural language.
23
24
  ${GOLD}mangomagic tools${RESET} Show available MCP tools and next workflows.
24
- ${GOLD}mangomagic cards${RESET} Open the Talking Cards workspace.
25
+ ${GOLD}mangomagic tool <name>${RESET} Run a CLI/MCP tool with optional JSON args.
26
+ ${GOLD}mangomagic cards "idea"${RESET} Create talking cards in the terminal.
27
+ ${GOLD}mangomagic open cards${RESET} Open the Talking Cards workspace in a browser.
25
28
  ${GOLD}mangomagic splash${RESET} Show the MangoMagic splash once. Add --anim or --loop for motion.
26
29
  ${GOLD}mangomagic mcp${RESET} Run as a stdio MCP server (for Claude Desktop / Cursor).
27
30
  ${GOLD}mangomagic mcp-config${RESET} Print MCP client config for copy-paste.
@@ -58,10 +61,10 @@ ${DIM}Token cache:${RESET} ${credentialsPath()}
58
61
  `);
59
62
  }
60
63
 
61
- function tools({ json = false } = {}) {
64
+ function tools({ json = false, all = false } = {}) {
62
65
  if (json) {
63
66
  process.stdout.write(JSON.stringify({
64
- mcpTools: MCP_TOOL_CATALOG,
67
+ mcpTools: ALL_MCP_TOOL_CATALOG,
65
68
  quickWins: QUICK_WINS,
66
69
  nextWorkflows: NEXT_WORKFLOWS,
67
70
  }, null, 2) + "\n");
@@ -71,13 +74,35 @@ function tools({ json = false } = {}) {
71
74
  process.stdout.write(`
72
75
  ${BOLD}MangoMagic MCP Tools${RESET}
73
76
 
74
- ${DIM}Available now:${RESET}
77
+ ${DIM}Core tools available now:${RESET}
75
78
  `);
76
79
  for (const tool of MCP_TOOL_CATALOG) {
77
80
  process.stdout.write(` ${GOLD}${tool.name.padEnd(18)}${RESET} ${tool.description}\n`);
78
81
  process.stdout.write(` ${DIM}${"".padEnd(18)} Try: ${tool.example}${RESET}\n`);
79
82
  }
80
83
 
84
+ process.stdout.write(`
85
+ ${DIM}Catalog:${RESET} ${ALL_MCP_TOOL_CATALOG.length} tools total (${MCP_TOOL_CATALOG.length} core, ${ALL_MCP_TOOL_CATALOG.length - MCP_TOOL_CATALOG.length} business/admin/raw tools).
86
+ ${DIM}Run:${RESET} ${GOLD}mangomagic tool get_user_stats${RESET}
87
+ ${DIM}Run with JSON:${RESET} ${GOLD}mangomagic tool get_user_episodes '{"limit":3}'${RESET}
88
+ ${DIM}Full list:${RESET} ${GOLD}mangomagic tools --all${RESET}
89
+ `);
90
+
91
+ if (all) {
92
+ const byCategory = new Map();
93
+ for (const tool of ALL_MCP_TOOL_CATALOG) {
94
+ const category = tool.category || "core";
95
+ byCategory.set(category, [...(byCategory.get(category) || []), tool]);
96
+ }
97
+ for (const [category, rows] of [...byCategory.entries()].sort(([a], [b]) => a.localeCompare(b))) {
98
+ process.stdout.write(`\n${BOLD}${category}${RESET}\n`);
99
+ for (const tool of rows) {
100
+ process.stdout.write(` ${GOLD}${tool.name}${RESET} ${DIM}${tool.description}${RESET}\n`);
101
+ }
102
+ }
103
+ process.stdout.write("\n");
104
+ }
105
+
81
106
  process.stdout.write(`
82
107
  ${DIM}Next high-value workflows to wire in:${RESET}
83
108
  `);
@@ -90,6 +115,27 @@ ${DIM}Use \`mangomagic mcp-config\` to connect an MCP client.${RESET}
90
115
  `);
91
116
  }
92
117
 
118
+ function parseJsonArgs(raw) {
119
+ const text = raw.join(" ").trim();
120
+ if (!text) return {};
121
+ try {
122
+ return JSON.parse(text);
123
+ } catch {
124
+ throw new Error(`Tool arguments must be JSON. Example: mangomagic tool get_user_episodes '{"limit":3}'`);
125
+ }
126
+ }
127
+
128
+ async function runToolCommand(argv) {
129
+ const name = argv[0];
130
+ if (!name) {
131
+ process.stdout.write(`Name a tool to run. See ${GOLD}mangomagic tools --all${RESET}\n`);
132
+ return;
133
+ }
134
+ const args = parseJsonArgs(argv.slice(1));
135
+ const result = await runCatalogTool(name, args);
136
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
137
+ }
138
+
93
139
  async function login({ openInBrowser = true } = {}) {
94
140
  if (loadToken()) {
95
141
  process.stdout.write(`${DIM}You're already signed in. Run \`mangomagic logout\` first to switch accounts.${RESET}\n`);
@@ -148,6 +194,25 @@ function cards({ openInBrowser = process.env.MANGOMAGIC_CLI_NO_OPEN !== "1", pat
148
194
  if (focus) process.stdout.write(`${DIM}Idea: ${focus}${RESET}\n`);
149
195
  }
150
196
 
197
+ function parseCount(argv, fallback = 3) {
198
+ const flagIndex = argv.findIndex((v) => v === "--count" || v === "-n");
199
+ if (flagIndex >= 0 && argv[flagIndex + 1]) return Math.max(1, Math.min(Number(argv[flagIndex + 1]), 10));
200
+ const inline = argv.find((v) => /^--count=/.test(v));
201
+ if (inline) return Math.max(1, Math.min(Number(inline.split("=")[1]), 10));
202
+ return fallback;
203
+ }
204
+
205
+ function stripFlags(argv) {
206
+ const out = [];
207
+ for (let i = 0; i < argv.length; i++) {
208
+ const v = argv[i];
209
+ if (v === "--count" || v === "-n") { i++; continue; }
210
+ if (v.startsWith("--count=")) continue;
211
+ out.push(v);
212
+ }
213
+ return out;
214
+ }
215
+
151
216
  async function mcpServer() {
152
217
  // Lazy-load so users who only run `login` don't pay the import cost.
153
218
  const { startMcpServer } = await import("./mcp/server.mjs");
@@ -179,8 +244,12 @@ export async function run(argv) {
179
244
  case "home": return home();
180
245
  case "chat": return chat(actions());
181
246
  case "ask": return handleNaturalLanguage(argv.slice(1).join(" "), actions());
182
- case "tools": return tools({ json: argv.includes("--json") });
183
- case "cards": return cards({ openInBrowser: !argv.includes("--no-open") && process.env.MANGOMAGIC_CLI_NO_OPEN !== "1" });
247
+ case "tools": return tools({ json: argv.includes("--json"), all: argv.includes("--all") });
248
+ case "tool": return runToolCommand(argv.slice(1));
249
+ case "cards": return createTalkingCards({ focus: stripFlags(argv.slice(1)).join(" "), count: parseCount(argv.slice(1)) });
250
+ case "open":
251
+ if (argv[1] === "cards" || argv[1] === "carousels") return cards({ openInBrowser: !argv.includes("--no-open") && process.env.MANGOMAGIC_CLI_NO_OPEN !== "1" });
252
+ return openPath(`/${argv[1] || ""}`);
184
253
  case "splash": return playSplash({ mode: argv.includes("--loop") ? "loop" : argv.includes("--anim") ? "anim" : "static" });
185
254
  case "mcp": return mcpServer();
186
255
  case "mcp-config": return mcpConfig();
@@ -1,26 +1,15 @@
1
- // Stdio MCP server exposing a small set of MangoMagic tools to clients like
2
- // Claude Desktop and Cursor. All tools call existing edge functions using the
3
- // user's CLI PAT cached at ~/.mangomagic/credentials.json.
1
+ // Stdio MCP server exposing MangoMagic tools to clients like Claude Desktop
2
+ // and Cursor. All tools call backend functions using the user's CLI PAT cached
3
+ // at ~/.mangomagic/credentials.json.
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import {
7
7
  CallToolRequestSchema,
8
8
  ListToolsRequestSchema,
9
9
  } from "@modelcontextprotocol/sdk/types.js";
10
- import { apiCall } from "../api.mjs";
11
10
  import { loadToken } from "../auth/token-store.mjs";
12
- import { MCP_TOOL_CATALOG } from "../tools/catalog.mjs";
13
-
14
- const TOOL_HANDLERS = {
15
- list_episodes: async ({ limit = 10 }) => apiCall("cli-list-episodes", { body: { limit } }),
16
- get_episode: async ({ episode }) => apiCall("cli-get-episode", { body: { episode } }),
17
- search_episodes: async ({ query }) => apiCall("cli-search-episodes", { body: { query } }),
18
- };
19
-
20
- const TOOLS = MCP_TOOL_CATALOG.map((tool) => ({
21
- ...tool,
22
- handler: TOOL_HANDLERS[tool.name],
23
- }));
11
+ import { ALL_MCP_TOOL_CATALOG } from "../tools/catalog.mjs";
12
+ import { runCatalogTool } from "../tools/run.mjs";
24
13
 
25
14
  export async function startMcpServer() {
26
15
  if (!loadToken()) {
@@ -34,16 +23,16 @@ export async function startMcpServer() {
34
23
  );
35
24
 
36
25
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
37
- tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
26
+ tools: ALL_MCP_TOOL_CATALOG.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
38
27
  }));
39
28
 
40
29
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
41
- const tool = TOOLS.find(t => t.name === req.params.name);
30
+ const tool = ALL_MCP_TOOL_CATALOG.find(t => t.name === req.params.name);
42
31
  if (!tool) {
43
32
  return { isError: true, content: [{ type: "text", text: `Unknown tool ${req.params.name}` }] };
44
33
  }
45
34
  try {
46
- const result = await tool.handler(req.params.arguments ?? {});
35
+ const result = await runCatalogTool(req.params.name, req.params.arguments ?? {});
47
36
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
48
37
  } catch (err) {
49
38
  return {
@@ -1,4 +1,6 @@
1
1
  import { APP_ORIGIN } from "../config.mjs";
2
+ import { EDGE_FUNCTION_TOOL_CATALOG } from "./edge-functions.mjs";
3
+ import { MAGIC_TOOL_CATALOG } from "./magic-catalog.mjs";
2
4
 
3
5
  export const COMMAND_PREFIX = "npx -y @mangomagic/cli";
4
6
 
@@ -38,14 +40,32 @@ export const MCP_TOOL_CATALOG = [
38
40
  },
39
41
  example: "Use MangoMagic to search my episodes for customer discovery.",
40
42
  },
43
+ {
44
+ name: "create_talking_cards",
45
+ description: "Generate branded LinkedIn-ready talking cards from a short idea and save them as draft carousels.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ focus: { type: "string", description: "The idea, theme, or LinkedIn angle to turn into cards" },
50
+ count: { type: "integer", minimum: 1, maximum: 10, default: 3 },
51
+ },
52
+ required: ["focus"],
53
+ },
54
+ example: "Use MangoMagic to create three talking cards about why founder-led sales stalls.",
55
+ },
56
+ ];
57
+
58
+ export const ALL_MCP_TOOL_CATALOG = [
59
+ ...MCP_TOOL_CATALOG,
60
+ ...MAGIC_TOOL_CATALOG,
61
+ ...EDGE_FUNCTION_TOOL_CATALOG,
41
62
  ];
42
63
 
43
64
  export const QUICK_WINS = [
44
65
  {
45
- command: `${COMMAND_PREFIX} cards`,
66
+ command: `${COMMAND_PREFIX} cards "why founder-led sales stalls" --count 3`,
46
67
  title: "Create Talking Cards",
47
- description: "Open the card workspace, build a Brand Doc, choose colours, and generate LinkedIn-ready cards.",
48
- url: `${APP_ORIGIN}/carousels`,
68
+ description: "Generate LinkedIn-ready talking cards from the terminal and save them to your library.",
49
69
  status: "ready",
50
70
  },
51
71
  {
@@ -63,7 +83,7 @@ export const QUICK_WINS = [
63
83
  {
64
84
  command: `${COMMAND_PREFIX} tools`,
65
85
  title: "See Every Tool",
66
- description: "Show the full MCP tool list plus the next high-value workflows to wire in.",
86
+ description: "Show the full CLI/MCP tool catalog, including business tools and admin Edge Functions.",
67
87
  status: "ready",
68
88
  },
69
89
  ];
@@ -71,7 +91,7 @@ export const QUICK_WINS = [
71
91
  export const NEXT_WORKFLOWS = [
72
92
  {
73
93
  name: "create_talking_cards",
74
- description: "Generate a batch of branded cards from a short idea, saved to the user's carousel library.",
94
+ description: "Generate a batch of branded talking cards from a short idea, saved to the user's carousel library.",
75
95
  },
76
96
  {
77
97
  name: "sync_linkedin_brand_context",
@@ -0,0 +1,214 @@
1
+ export const EDGE_FUNCTION_NAMES = [
2
+ "add-calendar-event",
3
+ "add-credits",
4
+ "ai-fill-missing-data",
5
+ "analyze-guests",
6
+ "analyze-highlights",
7
+ "analyze-inbox-message",
8
+ "analyze-logs",
9
+ "analyze-reply",
10
+ "application-email-cron",
11
+ "approve-log-fixes",
12
+ "attribute-referral",
13
+ "auto-create-episode",
14
+ "auto-topup",
15
+ "batch-process-all-episodes",
16
+ "bulk-process-pending-episodes",
17
+ "cancel-booking",
18
+ "check-subscription",
19
+ "clay-receive-company",
20
+ "clay-receive-email",
21
+ "clay-receive-mobile",
22
+ "clay-receive-people",
23
+ "cli-ai-route",
24
+ "cli-create-talking-cards",
25
+ "client-error-log",
26
+ "create-booking",
27
+ "create-checkout",
28
+ "customer-portal",
29
+ "deduct-credits",
30
+ "detect-stale-processing",
31
+ "download-episode-video",
32
+ "draft-demographic-doc",
33
+ "draft-inbox-reply",
34
+ "draft-reply",
35
+ "elevenlabs-health-check",
36
+ "elevenlabs-scribe-token",
37
+ "email-sequence-cron",
38
+ "email-unsubscribe",
39
+ "enrich-episode-content",
40
+ "enrich-guest-profile",
41
+ "enrich-profile-company",
42
+ "enrich-visitor",
43
+ "enterprise-onboarding-ai",
44
+ "enterprise-send-invitation",
45
+ "enterprise-trigger-enrichment",
46
+ "enterprise-validate-invitation",
47
+ "episode-share",
48
+ "estimate-processing-cost",
49
+ "extract-clip",
50
+ "fix-episode-urls",
51
+ "fix-referral-codes",
52
+ "fix-stuck-episodes",
53
+ "generate-carousels",
54
+ "generate-content",
55
+ "generate-email-sequence",
56
+ "generate-episode",
57
+ "generate-episode-articles",
58
+ "generate-episode-clips",
59
+ "generate-episode-og-image",
60
+ "generate-episode-transcript",
61
+ "generate-og-image",
62
+ "generate-outreach-email",
63
+ "generate-personalized-onboarding-email",
64
+ "generate-referral-code",
65
+ "generate-spike-og-image",
66
+ "generate-transcript",
67
+ "generate-use-cases",
68
+ "generate-video-thumbnail",
69
+ "generate-viral-caption",
70
+ "generate-visitor-email",
71
+ "get-host-availability",
72
+ "get-mango-network-stats",
73
+ "get-signed-url",
74
+ "gold-coach",
75
+ "google-drive-upload",
76
+ "grant-daily-referral-bonus",
77
+ "host-assistant",
78
+ "import-from-gdrive",
79
+ "import-gcs-episodes",
80
+ "import-google-sheet",
81
+ "linkedin-ads-accounts",
82
+ "linkedin-ads-analytics",
83
+ "linkedin-ads-auth",
84
+ "linkedin-ads-campaigns",
85
+ "linkedin-archive-reminder",
86
+ "linkedin-auth",
87
+ "linkedin-end-event-beacon",
88
+ "linkedin-post-carousel",
89
+ "linkedin-publish",
90
+ "linkedin-refresh-stats",
91
+ "linkedin-stream",
92
+ "livekit-token",
93
+ "log-analysis-cron",
94
+ "magic-assistant",
95
+ "magic-import",
96
+ "mango-schedule",
97
+ "migrate-drive-video",
98
+ "migrate-guest-photos",
99
+ "notify-admin-email",
100
+ "notify-host-join",
101
+ "notify-linkedin-lead",
102
+ "parse-display-name",
103
+ "process-email-queue",
104
+ "process-episode-complete",
105
+ "process-episodes-batch",
106
+ "process-first-pod-referral",
107
+ "process-pending-episodes",
108
+ "process-podcast",
109
+ "process-revenue-share",
110
+ "process-subscription-referral",
111
+ "proxy-gdrive-video",
112
+ "proxy-recording",
113
+ "qc-email",
114
+ "qualify-leads",
115
+ "rb2b-webhook",
116
+ "recalculate-referral-tier",
117
+ "recover-all-stuck-videos",
118
+ "recover-orphan-recordings",
119
+ "regenerate-episode-section",
120
+ "request-magic-link",
121
+ "resend-inbound-webhook",
122
+ "reset-free-credits",
123
+ "retry-enrichment",
124
+ "sales-call-coach",
125
+ "save-highlight-clip",
126
+ "save-transcript",
127
+ "seed-discover-episodes",
128
+ "send-admin-reply",
129
+ "send-application-confirmation",
130
+ "send-application-sequence-email",
131
+ "send-auth-email",
132
+ "send-booking-request",
133
+ "send-leads-qualified",
134
+ "send-meeting-invite",
135
+ "send-meeting-summary",
136
+ "send-recording-complete",
137
+ "send-referral-notification",
138
+ "send-sequence-email",
139
+ "send-visitor-email",
140
+ "send-welcome-email",
141
+ "social-share",
142
+ "stripe-webhook",
143
+ "sync-discover-to-leads",
144
+ "sync-resend-inbox",
145
+ "track-email",
146
+ "trigger-enrichment",
147
+ "update-referral-code",
148
+ "upload-recording",
149
+ "upload-recording-chunk",
150
+ "validate-referral-code",
151
+ "verify-magic-link",
152
+ "visitor-email-cron",
153
+ ];
154
+
155
+ const CATEGORY_PREFIXES = [
156
+ ["linkedin", "linkedin"],
157
+ ["generate", "generation"],
158
+ ["send", "messaging"],
159
+ ["process", "processing"],
160
+ ["import", "import"],
161
+ ["sync", "sync"],
162
+ ["fix", "maintenance"],
163
+ ["recover", "maintenance"],
164
+ ["cron", "scheduled"],
165
+ ["webhook", "webhook"],
166
+ ["stripe", "billing"],
167
+ ["episode", "episodes"],
168
+ ["booking", "bookings"],
169
+ ["enrich", "enrichment"],
170
+ ["analyze", "analysis"],
171
+ ["draft", "drafting"],
172
+ ["upload", "upload"],
173
+ ["download", "download"],
174
+ ["save", "save"],
175
+ ["cli", "cli"],
176
+ ];
177
+
178
+ export function edgeToolName(functionName) {
179
+ return `edge_${functionName.replace(/-/g, "_")}`;
180
+ }
181
+
182
+ export function functionNameFromEdgeTool(toolName) {
183
+ if (!toolName.startsWith("edge_")) return null;
184
+ const functionName = toolName.slice("edge_".length).replace(/_/g, "-");
185
+ return EDGE_FUNCTION_NAMES.includes(functionName) ? functionName : null;
186
+ }
187
+
188
+ function categoryFor(functionName) {
189
+ const found = CATEGORY_PREFIXES.find(([prefix]) => functionName.startsWith(prefix) || functionName.includes(`-${prefix}`));
190
+ return found?.[1] || "edge";
191
+ }
192
+
193
+ export const EDGE_FUNCTION_TOOL_CATALOG = EDGE_FUNCTION_NAMES.map((functionName) => ({
194
+ name: edgeToolName(functionName),
195
+ backendName: functionName,
196
+ category: categoryFor(functionName),
197
+ rawEdgeFunction: true,
198
+ description: `Admin/raw Edge Function invocation for \`${functionName}\`. Use first-class MangoMagic tools when available; this is for operational access.`,
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ input: {
203
+ type: "object",
204
+ description: "JSON body to send to the Edge Function.",
205
+ additionalProperties: true,
206
+ },
207
+ dryRun: {
208
+ type: "boolean",
209
+ description: "When true, validates and describes the invocation without running it.",
210
+ default: true,
211
+ },
212
+ },
213
+ },
214
+ }));
@@ -0,0 +1,345 @@
1
+ export const MAGIC_TOOL_CATALOG = [
2
+ {
3
+ name: "get_user_episodes",
4
+ backendName: "get_user_episodes",
5
+ category: "episodes",
6
+ description: "Fetch the signed-in user's episodes, recordings, or content.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ limit: { type: "number", description: "Maximum number of episodes to return", default: 5 },
11
+ status: { type: "string", enum: ["all", "published", "draft"], description: "Filter by publication status" },
12
+ },
13
+ },
14
+ },
15
+ {
16
+ name: "get_user_clips",
17
+ backendName: "get_user_clips",
18
+ category: "clips",
19
+ description: "Fetch the signed-in user's video clips and highlights.",
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ limit: { type: "number", description: "Maximum number of clips to return", default: 5 },
24
+ },
25
+ },
26
+ },
27
+ {
28
+ name: "get_user_leads",
29
+ backendName: "get_user_leads",
30
+ category: "leads",
31
+ description: "Fetch leads, LinkedIn connections, guests, or contacts.",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ stage: { type: "string", enum: ["all", "new", "contacted", "scheduled", "booked", "declined"], description: "Pipeline stage" },
36
+ limit: { type: "number", description: "Maximum number of leads to return", default: 10 },
37
+ },
38
+ },
39
+ },
40
+ {
41
+ name: "get_user_stats",
42
+ backendName: "get_user_stats",
43
+ category: "account",
44
+ description: "Get account stats including level, XP, streak, episodes count, plan, and credits.",
45
+ inputSchema: { type: "object", properties: {} },
46
+ },
47
+ {
48
+ name: "get_upcoming_meetings",
49
+ backendName: "get_upcoming_meetings",
50
+ category: "meetings",
51
+ description: "Get upcoming scheduled meetings and recording sessions.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ limit: { type: "number", description: "Maximum number of meetings to return", default: 5 },
56
+ },
57
+ },
58
+ },
59
+ {
60
+ name: "search_user_episodes",
61
+ backendName: "search_episodes",
62
+ category: "episodes",
63
+ description: "Search the signed-in user's episodes by title, guest name, or topic.",
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: {
67
+ query: { type: "string", description: "Search query" },
68
+ },
69
+ required: ["query"],
70
+ },
71
+ },
72
+ {
73
+ name: "get_achievements",
74
+ backendName: "get_achievements",
75
+ category: "account",
76
+ description: "Get achievement progress and unlocked badges.",
77
+ inputSchema: { type: "object", properties: {} },
78
+ },
79
+ {
80
+ name: "create_clip",
81
+ backendName: "create_clip",
82
+ category: "clips",
83
+ description: "Create a highlight clip from an episode recording.",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {
87
+ episodeId: { type: "string", description: "Episode ID" },
88
+ startTime: { type: "number", description: "Start time in seconds" },
89
+ endTime: { type: "number", description: "End time in seconds" },
90
+ title: { type: "string", description: "Optional clip title" },
91
+ },
92
+ required: ["episodeId", "startTime", "endTime"],
93
+ },
94
+ },
95
+ {
96
+ name: "enrich_lead",
97
+ backendName: "enrich_lead",
98
+ category: "leads",
99
+ description: "Prepare enrichment for a lead to get contact/company data.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ leadId: { type: "string", description: "Lead ID" },
104
+ enrichmentType: { type: "string", enum: ["full", "email", "mobile", "company"], default: "full" },
105
+ },
106
+ required: ["leadId"],
107
+ },
108
+ },
109
+ {
110
+ name: "send_outreach_email",
111
+ backendName: "send_outreach_email",
112
+ category: "leads",
113
+ description: "Draft or prepare sending an outreach email to a lead.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ leadId: { type: "string", description: "Lead ID" },
118
+ angle: { type: "string", description: "Outreach angle or topic" },
119
+ send: { type: "boolean", description: "Whether to send or only draft", default: false },
120
+ },
121
+ required: ["leadId"],
122
+ },
123
+ },
124
+ {
125
+ name: "schedule_meeting",
126
+ backendName: "schedule_meeting",
127
+ category: "meetings",
128
+ description: "Create a new meeting or recording session.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ name: { type: "string", description: "Meeting name" },
133
+ scheduledAt: { type: "string", description: "ISO date/time" },
134
+ guestEmail: { type: "string", description: "Optional guest email" },
135
+ },
136
+ required: ["name"],
137
+ },
138
+ },
139
+ {
140
+ name: "get_transcript",
141
+ backendName: "get_transcript",
142
+ category: "episodes",
143
+ description: "Get the transcript for an episode.",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ episodeId: { type: "string", description: "Episode ID" },
148
+ },
149
+ required: ["episodeId"],
150
+ },
151
+ },
152
+ {
153
+ name: "update_episode",
154
+ backendName: "update_episode",
155
+ category: "episodes",
156
+ description: "Prepare metadata updates for an episode.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ episodeId: { type: "string", description: "Episode ID" },
161
+ title: { type: "string", description: "New title" },
162
+ description: { type: "string", description: "New description" },
163
+ isPublic: { type: "boolean", description: "Public/private visibility" },
164
+ seoTitle: { type: "string", description: "SEO title" },
165
+ seoDescription: { type: "string", description: "SEO description" },
166
+ },
167
+ required: ["episodeId"],
168
+ },
169
+ },
170
+ {
171
+ name: "upload_video_url",
172
+ backendName: "upload_video_url",
173
+ category: "episodes",
174
+ description: "Prepare importing a video URL into a full episode page.",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ videoUrl: { type: "string", description: "YouTube, Vimeo, or direct video URL" },
179
+ title: { type: "string", description: "Optional title" },
180
+ },
181
+ required: ["videoUrl"],
182
+ },
183
+ },
184
+ {
185
+ name: "generate_viral_caption",
186
+ backendName: "generate_viral_caption",
187
+ category: "social",
188
+ description: "Generate a viral LinkedIn/social caption for a clip.",
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: {
192
+ clipId: { type: "string", description: "Clip ID" },
193
+ },
194
+ required: ["clipId"],
195
+ },
196
+ },
197
+ {
198
+ name: "generate_transcript",
199
+ backendName: "generate_transcript",
200
+ category: "episodes",
201
+ description: "Prepare transcript generation for an episode that has a recording.",
202
+ inputSchema: {
203
+ type: "object",
204
+ properties: {
205
+ episodeId: { type: "string", description: "Episode ID" },
206
+ },
207
+ required: ["episodeId"],
208
+ },
209
+ },
210
+ {
211
+ name: "get_episode_highlights",
212
+ backendName: "get_episode_highlights",
213
+ category: "episodes",
214
+ description: "Get or prepare AI highlights, key takeaways, and quotes for an episode.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ episodeId: { type: "string", description: "Episode ID" },
219
+ },
220
+ required: ["episodeId"],
221
+ },
222
+ },
223
+ {
224
+ name: "bulk_enrich_leads",
225
+ backendName: "bulk_enrich_leads",
226
+ category: "leads",
227
+ description: "Prepare enrichment for multiple leads in a stage.",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ stage: { type: "string", enum: ["new", "contacted", "scheduled", "all"], description: "Pipeline stage" },
232
+ limit: { type: "number", description: "Maximum leads to enrich, max 10", default: 5 },
233
+ },
234
+ },
235
+ },
236
+ {
237
+ name: "bulk_send_emails",
238
+ backendName: "bulk_send_emails",
239
+ category: "leads",
240
+ description: "Draft outreach emails for multiple leads in a stage.",
241
+ inputSchema: {
242
+ type: "object",
243
+ properties: {
244
+ stage: { type: "string", enum: ["new", "contacted", "scheduled", "all"], description: "Pipeline stage" },
245
+ angle: { type: "string", description: "Outreach angle or topic" },
246
+ limit: { type: "number", description: "Maximum emails to draft, max 10", default: 5 },
247
+ },
248
+ },
249
+ },
250
+ {
251
+ name: "get_inbox",
252
+ backendName: "get_inbox",
253
+ category: "admin",
254
+ description: "Admin: get inbox threads and messages.",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ status: { type: "string", enum: ["all", "open", "archived"], default: "open" },
259
+ limit: { type: "number", description: "Maximum threads to return", default: 10 },
260
+ },
261
+ },
262
+ },
263
+ {
264
+ name: "draft_reply",
265
+ backendName: "draft_reply",
266
+ category: "admin",
267
+ description: "Admin: draft an AI reply to an inbox thread.",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ threadId: { type: "string", description: "Inbox thread ID" },
272
+ instructions: { type: "string", description: "Optional tone/key point instructions" },
273
+ },
274
+ required: ["threadId"],
275
+ },
276
+ },
277
+ {
278
+ name: "get_bookings",
279
+ backendName: "get_bookings",
280
+ category: "bookings",
281
+ description: "Get booking requests and upcoming bookings.",
282
+ inputSchema: {
283
+ type: "object",
284
+ properties: {
285
+ status: { type: "string", enum: ["all", "confirmed", "pending", "cancelled"], description: "Booking status" },
286
+ limit: { type: "number", description: "Maximum bookings to return", default: 10 },
287
+ },
288
+ },
289
+ },
290
+ {
291
+ name: "qualify_leads",
292
+ backendName: "qualify_leads",
293
+ category: "leads",
294
+ description: "Run AI qualification scoring on leads to identify promising guests.",
295
+ inputSchema: {
296
+ type: "object",
297
+ properties: {
298
+ stage: { type: "string", enum: ["new", "contacted", "all"], default: "new" },
299
+ limit: { type: "number", description: "Number of leads to qualify", default: 10 },
300
+ },
301
+ },
302
+ },
303
+ {
304
+ name: "update_credits",
305
+ backendName: "update_credits",
306
+ category: "admin",
307
+ description: "Admin: prepare adding credits to a user's balance.",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ amount: { type: "number", description: "Credit amount" },
312
+ targetUserId: { type: "string", description: "Target user ID, defaults to current user" },
313
+ },
314
+ required: ["amount"],
315
+ },
316
+ },
317
+ {
318
+ name: "update_plan",
319
+ backendName: "update_plan",
320
+ category: "admin",
321
+ description: "Admin: prepare updating a user's plan type.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ planType: { type: "string", enum: ["free_trial", "pro", "enterprise"] },
326
+ targetUserId: { type: "string", description: "Target user ID, defaults to current user" },
327
+ },
328
+ required: ["planType"],
329
+ },
330
+ },
331
+ {
332
+ name: "assign_role",
333
+ backendName: "assign_role",
334
+ category: "admin",
335
+ description: "Super admin: prepare assigning a role to a user.",
336
+ inputSchema: {
337
+ type: "object",
338
+ properties: {
339
+ role: { type: "string", enum: ["admin", "moderator", "reporting"] },
340
+ targetUserId: { type: "string", description: "Target user ID" },
341
+ },
342
+ required: ["role", "targetUserId"],
343
+ },
344
+ },
345
+ ];
@@ -0,0 +1,44 @@
1
+ import { apiCall } from "../api.mjs";
2
+ import { ALL_MCP_TOOL_CATALOG } from "./catalog.mjs";
3
+ import { functionNameFromEdgeTool } from "./edge-functions.mjs";
4
+
5
+ const BUILTIN_TOOL_HANDLERS = {
6
+ list_episodes: async ({ limit = 10 }) => apiCall("cli-list-episodes", { body: { limit } }),
7
+ get_episode: async ({ episode }) => apiCall("cli-get-episode", { body: { episode } }),
8
+ search_episodes: async ({ query }) => apiCall("cli-search-episodes", { body: { query } }),
9
+ create_talking_cards: async ({ focus, count = 3 }) => apiCall("cli-create-talking-cards", {
10
+ body: {
11
+ focus,
12
+ count: Math.max(1, Math.min(Number(count || 3), 10)),
13
+ },
14
+ }),
15
+ };
16
+
17
+ export function findTool(name) {
18
+ return ALL_MCP_TOOL_CATALOG.find((tool) => tool.name === name);
19
+ }
20
+
21
+ export async function runCatalogTool(name, args = {}) {
22
+ const tool = findTool(name);
23
+ if (!tool) throw new Error(`Unknown tool: ${name}`);
24
+
25
+ if (BUILTIN_TOOL_HANDLERS[name]) return BUILTIN_TOOL_HANDLERS[name](args);
26
+
27
+ const edgeFunctionName = functionNameFromEdgeTool(name);
28
+ if (edgeFunctionName) {
29
+ return apiCall("cli-run-edge-function", {
30
+ body: {
31
+ functionName: edgeFunctionName,
32
+ input: args.input && typeof args.input === "object" ? args.input : {},
33
+ dryRun: args.dryRun !== false,
34
+ },
35
+ });
36
+ }
37
+
38
+ return apiCall("magic-assistant", {
39
+ body: {
40
+ toolName: tool.backendName || name,
41
+ args,
42
+ },
43
+ });
44
+ }