@mangomagic/cli 0.1.12 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mangomagic/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
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,13 +2,25 @@ 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
5
  import { runCatalogTool } from "../tools/run.mjs";
7
6
 
8
7
  const GOLD = "\x1b[38;2;241;171;28m";
9
8
  const DIM = "\x1b[2m";
10
9
  const BOLD = "\x1b[1m";
11
10
  const RESET = "\x1b[0m";
11
+ const CHAT_TOOL_NAMES = [
12
+ "create_talking_cards",
13
+ "list_episodes",
14
+ "search_episodes",
15
+ "get_episode",
16
+ "get_user_episodes",
17
+ "get_user_clips",
18
+ "get_user_leads",
19
+ "get_user_stats",
20
+ "get_upcoming_meetings",
21
+ "get_bookings",
22
+ "get_inbox",
23
+ ];
12
24
 
13
25
  function compact(s) {
14
26
  return String(s || "").trim().replace(/\s+/g, " ");
@@ -42,6 +54,27 @@ function localPlan(text) {
42
54
  if (t.includes("talking card") || t.includes("carousel") || /\bcards?\b/.test(t)) {
43
55
  return { action: "create_talking_cards", args: { focus: raw, count: inferCount(raw) } };
44
56
  }
57
+ if (/\b(email|emails|inbox|messages?)\b/.test(t) && /\b(show|list|get|what|my|check)\b/.test(t)) {
58
+ return { action: "list_inbox", args: { limit: 5 } };
59
+ }
60
+ if (/\b(stats?|account|credits?|streak|level|usage)\b/.test(t) && /\b(show|get|what|my|how|check)\b/.test(t)) {
61
+ return { action: "account_stats", args: {} };
62
+ }
63
+ if (/\b(clips?|highlights?)\b/.test(t) && /\b(show|list|get|my|what)\b/.test(t)) {
64
+ return { action: "list_clips", args: { limit: 5 } };
65
+ }
66
+ if (/\b(guests?|leads?|contacts?)\b/.test(t) && /\b(analy[sz]e|qualify|score|best|priority|prioritise|prioritize)\b/.test(t)) {
67
+ return { action: "analyze_guests", args: { limit: 10 } };
68
+ }
69
+ if (/\b(guests?|leads?|contacts?)\b/.test(t) && /\b(show|list|get|my|what)\b/.test(t)) {
70
+ return { action: "list_leads", args: { limit: 8 } };
71
+ }
72
+ if (/\b(meetings?|recording sessions?|sessions?)\b/.test(t) && /\b(show|list|get|upcoming|my|what)\b/.test(t)) {
73
+ return { action: "list_meetings", args: { limit: 5 } };
74
+ }
75
+ if (/\b(bookings?|booking requests?)\b/.test(t) && /\b(show|list|get|my|what)\b/.test(t)) {
76
+ return { action: "list_bookings", args: { limit: 5 } };
77
+ }
45
78
  if (t.includes("mcp") && (t.includes("config") || t.includes("connect") || t.includes("claude") || t.includes("cursor") || t.includes("codex"))) {
46
79
  return { action: "mcp_config", args: {} };
47
80
  }
@@ -80,6 +113,28 @@ function asArray(data) {
80
113
  return [];
81
114
  }
82
115
 
116
+ function items(data) {
117
+ if (Array.isArray(data?.items)) return data.items;
118
+ return asArray(data).map((row) => ({
119
+ id: row.id,
120
+ title: row.title || row.name || [row.first_name, row.last_name].filter(Boolean).join(" ") || row.subject || row.id,
121
+ subtitle: row.subtitle || row.company || row.participant_email || row.status || "",
122
+ link: row.link,
123
+ metadata: row.metadata || row,
124
+ }));
125
+ }
126
+
127
+ function formatDate(value) {
128
+ if (!value) return "";
129
+ const date = new Date(value);
130
+ if (Number.isNaN(date.getTime())) return "";
131
+ return date.toLocaleDateString("en-AU", { month: "short", day: "numeric", year: "numeric" });
132
+ }
133
+
134
+ function plural(count, singular, pluralName = `${singular}s`) {
135
+ return `${count} ${count === 1 ? singular : pluralName}`;
136
+ }
137
+
83
138
  function printEpisodeList(data) {
84
139
  const rows = asArray(data);
85
140
  if (!rows.length) {
@@ -93,6 +148,177 @@ function printEpisodeList(data) {
93
148
  }
94
149
  }
95
150
 
151
+ function printInbox(data) {
152
+ const rows = asArray(data);
153
+ if (!rows.length) {
154
+ process.stdout.write("No MangoMagic inbox threads found.\n");
155
+ return;
156
+ }
157
+ const unread = rows.reduce((sum, row) => sum + Number(row.unread_count || 0), 0);
158
+ process.stdout.write(`${BOLD}MangoMagic inbox:${RESET} ${plural(rows.length, "thread")} shown${unread ? `, ${plural(unread, "unread message")}` : ""}.\n\n`);
159
+ for (const row of rows.slice(0, 8)) {
160
+ const sender = row.participant_name || row.participant_email || "Unknown sender";
161
+ const unreadText = Number(row.unread_count || 0) ? `${row.unread_count} unread` : "read";
162
+ const when = formatDate(row.last_message_at);
163
+ process.stdout.write(` ${GOLD}${row.subject || "Untitled thread"}${RESET}\n`);
164
+ process.stdout.write(` ${DIM}${sender} • ${unreadText}${when ? ` • ${when}` : ""}${RESET}\n`);
165
+ }
166
+ process.stdout.write(`\n${DIM}This is your MangoMagic inbox. Ask "show my stats" or "analyse my guests" for the next useful snapshot.${RESET}\n`);
167
+ }
168
+
169
+ function printStats(data) {
170
+ const stats = data?.data || data;
171
+ if (!stats || typeof stats !== "object") {
172
+ process.stdout.write("No account stats found.\n");
173
+ return;
174
+ }
175
+ process.stdout.write(`${BOLD}Account snapshot${RESET}\n`);
176
+ process.stdout.write(` Level: ${GOLD}${stats.level ?? "?"}${RESET}${stats.xp_points != null ? ` (${stats.xp_points} XP)` : ""}\n`);
177
+ process.stdout.write(` Streak: ${GOLD}${stats.current_streak ?? 0}${RESET} days\n`);
178
+ process.stdout.write(` Recordings: ${GOLD}${stats.total_recordings ?? 0}${RESET}${stats.total_minutes_recorded != null ? ` • ${stats.total_minutes_recorded} minutes` : ""}\n`);
179
+ process.stdout.write(` Clips: ${GOLD}${stats.total_clips_created ?? 0}${RESET}\n`);
180
+ process.stdout.write(` Credits: ${GOLD}${stats.credit_balance ?? stats.episodes_remaining ?? "?"}${RESET}\n`);
181
+ }
182
+
183
+ function printClips(data) {
184
+ const rows = asArray(data);
185
+ if (!rows.length) {
186
+ process.stdout.write("No clips found yet.\n");
187
+ return;
188
+ }
189
+ const failed = rows.filter((row) => row.status === "failed").length;
190
+ process.stdout.write(`${BOLD}Recent clips${RESET}${failed ? ` ${DIM}(${failed} need attention)${RESET}` : ""}\n\n`);
191
+ for (const clip of rows.slice(0, 8)) {
192
+ const status = clip.status || clip.metadata?.status || "unknown";
193
+ const views = clip.view_count ?? clip.metadata?.view_count ?? 0;
194
+ const link = clip.slug ? `https://mangomagic.live/clips/${clip.slug}` : clip.link ? `https://mangomagic.live${clip.link}` : "";
195
+ process.stdout.write(` ${GOLD}${clip.title || "Untitled clip"}${RESET}\n`);
196
+ process.stdout.write(` ${DIM}${status} • ${plural(Number(views), "view")}${link ? ` • ${link}` : ""}${RESET}\n`);
197
+ }
198
+ }
199
+
200
+ function leadName(row) {
201
+ return row.title || [row.first_name, row.last_name].filter(Boolean).join(" ") || row.name || row.email || row.id || "Unnamed lead";
202
+ }
203
+
204
+ function printLeads(data) {
205
+ const rows = asArray(data);
206
+ if (!rows.length) {
207
+ process.stdout.write("No guests or leads found yet.\n");
208
+ return;
209
+ }
210
+ process.stdout.write(`${BOLD}Guests and leads${RESET}\n\n`);
211
+ for (const lead of rows.slice(0, 10)) {
212
+ const role = [lead.position, lead.company].filter(Boolean).join(" at ");
213
+ const stage = lead.stage || lead.metadata?.stage || "unknown stage";
214
+ const enrichment = lead.enrichment_status || lead.metadata?.enrichment_status;
215
+ process.stdout.write(` ${GOLD}${leadName(lead)}${RESET}\n`);
216
+ process.stdout.write(` ${DIM}${[role, stage, enrichment].filter(Boolean).join(" • ")}${RESET}\n`);
217
+ }
218
+ }
219
+
220
+ function printGuestAnalysis(data) {
221
+ const rows = asArray(data);
222
+ if (!rows.length) {
223
+ process.stdout.write("I could not find guests or leads to analyse yet.\n");
224
+ return;
225
+ }
226
+
227
+ const stageCounts = new Map();
228
+ const pending = [];
229
+ const ready = [];
230
+ for (const lead of rows) {
231
+ const stage = lead.stage || "unknown";
232
+ stageCounts.set(stage, (stageCounts.get(stage) || 0) + 1);
233
+ if ((lead.enrichment_status || "").toLowerCase() === "pending") pending.push(lead);
234
+ if (lead.email || lead.profile_url) ready.push(lead);
235
+ }
236
+
237
+ process.stdout.write(`${BOLD}Guest pipeline snapshot${RESET}\n`);
238
+ process.stdout.write(` Analysed: ${GOLD}${plural(rows.length, "guest")}${RESET}\n`);
239
+ process.stdout.write(` Contactable now: ${GOLD}${ready.length}${RESET}\n`);
240
+ process.stdout.write(` Need enrichment: ${GOLD}${pending.length}${RESET}\n`);
241
+ if (stageCounts.size) {
242
+ const summary = [...stageCounts.entries()].map(([stage, count]) => `${stage}: ${count}`).join(", ");
243
+ process.stdout.write(` Stages: ${DIM}${summary}${RESET}\n`);
244
+ }
245
+
246
+ process.stdout.write(`\n${BOLD}Best immediate moves${RESET}\n`);
247
+ for (const lead of ready.slice(0, 5)) {
248
+ const contact = lead.email || lead.profile_url || "contact details available";
249
+ process.stdout.write(` ${GOLD}${leadName(lead)}${RESET} ${DIM}${contact}${RESET}\n`);
250
+ }
251
+ const lead = leadName(ready[0] || pending[0]);
252
+ process.stdout.write(`\n${DIM}Next: ask "show me my leads" or "create talking cards about why ${lead} should join my podcast".${RESET}\n`);
253
+ }
254
+
255
+ function printMeetings(data) {
256
+ const rows = asArray(data);
257
+ if (!rows.length) {
258
+ process.stdout.write("No upcoming meetings found.\n");
259
+ return;
260
+ }
261
+ process.stdout.write(`${BOLD}Upcoming meetings${RESET}\n\n`);
262
+ for (const meeting of rows.slice(0, 8)) {
263
+ const when = formatDate(meeting.scheduled_at || meeting.scheduledAt || meeting.start_time);
264
+ process.stdout.write(` ${GOLD}${meeting.title || meeting.name || "Untitled meeting"}${RESET}\n`);
265
+ process.stdout.write(` ${DIM}${[when, meeting.status, meeting.guest_email || meeting.guestEmail].filter(Boolean).join(" • ")}${RESET}\n`);
266
+ }
267
+ }
268
+
269
+ function printBookings(data) {
270
+ const rows = asArray(data);
271
+ if (!rows.length) {
272
+ process.stdout.write("No bookings found.\n");
273
+ return;
274
+ }
275
+ process.stdout.write(`${BOLD}Bookings${RESET}\n\n`);
276
+ for (const booking of rows.slice(0, 8)) {
277
+ const when = formatDate(booking.scheduled_at || booking.created_at);
278
+ process.stdout.write(` ${GOLD}${booking.title || booking.name || booking.guest_name || "Booking"}${RESET}\n`);
279
+ process.stdout.write(` ${DIM}${[booking.status, when, booking.email || booking.guest_email].filter(Boolean).join(" • ")}${RESET}\n`);
280
+ }
281
+ }
282
+
283
+ function printGenericResult(name, data) {
284
+ const rows = items(data);
285
+ if (rows.length) {
286
+ process.stdout.write(`${BOLD}${name}${RESET}\n\n`);
287
+ for (const row of rows.slice(0, 10)) {
288
+ process.stdout.write(` ${GOLD}${row.title || row.id || "Result"}${RESET}\n`);
289
+ if (row.subtitle) process.stdout.write(` ${DIM}${row.subtitle}${RESET}\n`);
290
+ if (row.link) process.stdout.write(` ${DIM}https://mangomagic.live${row.link}${RESET}\n`);
291
+ }
292
+ return;
293
+ }
294
+ const message = data?.message || data?.summary || data?.status;
295
+ process.stdout.write(message ? `${message}\n` : `${BOLD}Done.${RESET}\n`);
296
+ }
297
+
298
+ function printToolResult(name, data) {
299
+ switch (name) {
300
+ case "get_inbox":
301
+ return printInbox(data);
302
+ case "get_user_stats":
303
+ return printStats(data);
304
+ case "get_user_clips":
305
+ return printClips(data);
306
+ case "get_user_leads":
307
+ return printLeads(data);
308
+ case "get_upcoming_meetings":
309
+ return printMeetings(data);
310
+ case "get_bookings":
311
+ return printBookings(data);
312
+ case "get_user_episodes":
313
+ case "list_episodes":
314
+ case "search_user_episodes":
315
+ case "search_episodes":
316
+ return printEpisodeList(data);
317
+ default:
318
+ return printGenericResult(name, data);
319
+ }
320
+ }
321
+
96
322
  function printEpisode(data) {
97
323
  const ep = data?.episode || data?.data || data;
98
324
  if (!ep || typeof ep !== "object") {
@@ -166,12 +392,17 @@ export async function handleNaturalLanguage(text, actions, { allowModel = true }
166
392
  plan = await planWithKimi(text, {
167
393
  availableTools: [
168
394
  "create_talking_cards",
169
- "open_cards",
170
- "open_brand_doc",
395
+ "list_inbox",
396
+ "list_leads",
397
+ "analyze_guests",
398
+ "list_clips",
399
+ "account_stats",
400
+ "list_meetings",
401
+ "list_bookings",
171
402
  "show_tools",
172
403
  "mcp_config",
173
404
  "home",
174
- ...ALL_MCP_TOOL_CATALOG.map((tool) => tool.name),
405
+ ...CHAT_TOOL_NAMES,
175
406
  ],
176
407
  });
177
408
  } catch (err) {
@@ -210,6 +441,34 @@ ${DIM}${err?.message ?? err}${RESET}
210
441
  return actions.mcpConfig();
211
442
  case "home":
212
443
  return actions.home();
444
+ case "list_inbox": {
445
+ const data = await runCatalogTool("get_inbox", { limit: Math.max(1, Math.min(Number(args.limit || 5), 10)) });
446
+ return printInbox(data);
447
+ }
448
+ case "account_stats": {
449
+ const data = await runCatalogTool("get_user_stats", {});
450
+ return printStats(data);
451
+ }
452
+ case "list_clips": {
453
+ const data = await runCatalogTool("get_user_clips", { limit: Math.max(1, Math.min(Number(args.limit || 5), 10)) });
454
+ return printClips(data);
455
+ }
456
+ case "list_leads": {
457
+ const data = await runCatalogTool("get_user_leads", { limit: Math.max(1, Math.min(Number(args.limit || 8), 20)) });
458
+ return printLeads(data);
459
+ }
460
+ case "analyze_guests": {
461
+ const data = await runCatalogTool("get_user_leads", { limit: Math.max(1, Math.min(Number(args.limit || 10), 20)) });
462
+ return printGuestAnalysis(data);
463
+ }
464
+ case "list_meetings": {
465
+ const data = await runCatalogTool("get_upcoming_meetings", { limit: Math.max(1, Math.min(Number(args.limit || 5), 10)) });
466
+ return printMeetings(data);
467
+ }
468
+ case "list_bookings": {
469
+ const data = await runCatalogTool("get_bookings", { limit: Math.max(1, Math.min(Number(args.limit || 5), 10)) });
470
+ return printBookings(data);
471
+ }
213
472
  case "list_episodes": {
214
473
  const data = await apiCall("cli-list-episodes", { body: { limit: Math.max(1, Math.min(Number(args.limit || 5), 10)) } });
215
474
  return printEpisodeList(data);
@@ -235,10 +494,29 @@ ${DIM}${err?.message ?? err}${RESET}
235
494
  process.stdout.write("Which tool should I run?\n");
236
495
  return;
237
496
  }
497
+ if (String(args.toolName).startsWith("edge_")) {
498
+ process.stdout.write(`That is a developer/admin edge function. In chat mode I only run value-facing actions.\n`);
499
+ process.stdout.write(`Use ${GOLD}npx -y @mangomagic/cli tool ${args.toolName} '{"dryRun":true}'${RESET} if you want the raw operational tool.\n`);
500
+ return;
501
+ }
238
502
  const data = await runCatalogTool(args.toolName, args.args || {});
239
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
503
+ printToolResult(args.toolName, data);
240
504
  return;
241
505
  }
506
+ case "get_inbox":
507
+ case "get_user_stats":
508
+ case "get_user_clips":
509
+ case "get_user_leads":
510
+ case "get_upcoming_meetings":
511
+ case "get_bookings":
512
+ case "get_user_episodes": {
513
+ const data = await runCatalogTool(action, args);
514
+ return printToolResult(action, data);
515
+ }
516
+ case "qualify_leads": {
517
+ const data = await runCatalogTool("get_user_leads", { limit: Math.max(1, Math.min(Number(args.limit || 10), 20)) });
518
+ return printGuestAnalysis(data);
519
+ }
242
520
  case "answer":
243
521
  process.stdout.write(`${args.text || "I can help with MangoMagic episodes, talking cards, and MCP setup."}\n`);
244
522
  return;
package/src/index.mjs CHANGED
@@ -44,9 +44,10 @@ function sessionIntro() {
44
44
  ${BOLD}You can type naturally now.${RESET}
45
45
 
46
46
  Try:
47
- ${GOLD}what episodes do I have?${RESET}
48
47
  ${GOLD}create 3 talking cards about founder-led sales${RESET}
49
- ${GOLD}show me all of your tools${RESET}
48
+ ${GOLD}analyse my guests${RESET}
49
+ ${GOLD}show me my clips${RESET}
50
+ ${GOLD}what episodes do I have?${RESET}
50
51
 
51
52
  ${DIM}Type ${BOLD}exit${RESET}${DIM} to leave. Add ${BOLD}--home${RESET}${DIM} next time if you only want the menu.${RESET}
52
53
  `);
@@ -98,6 +99,12 @@ ${DIM}Core tools available now:${RESET}
98
99
  }
99
100
 
100
101
  process.stdout.write(`
102
+ ${DIM}Value actions that work in chat now:${RESET}
103
+ ${GOLD}analyse my guests${RESET}
104
+ ${GOLD}show me my clips${RESET}
105
+ ${GOLD}show me my emails${RESET}
106
+ ${GOLD}show me my stats${RESET}
107
+
101
108
  ${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).
102
109
  ${DIM}Run:${RESET} ${GOLD}${COMMAND_PREFIX} tool get_user_stats${RESET}
103
110
  ${DIM}Run with JSON:${RESET} ${GOLD}${COMMAND_PREFIX} tool get_user_episodes '{"limit":3}'${RESET}
@@ -120,14 +127,14 @@ ${DIM}Full list:${RESET} ${GOLD}${COMMAND_PREFIX} tools --all${RESET}
120
127
  }
121
128
 
122
129
  process.stdout.write(`
123
- ${DIM}Next high-value workflows to wire in:${RESET}
130
+ ${DIM}High-value workflows:${RESET}
124
131
  `);
125
132
  for (const workflow of NEXT_WORKFLOWS) {
126
133
  process.stdout.write(` ${GOLD}${workflow.name.padEnd(28)}${RESET} ${workflow.description}\n`);
127
134
  }
128
135
 
129
136
  process.stdout.write(`
130
- ${DIM}Use \`mangomagic mcp-config\` to connect an MCP client.${RESET}
137
+ ${DIM}Use \`${COMMAND_PREFIX} mcp-config\` to connect an MCP client.${RESET}
131
138
  `);
132
139
  }
133
140
 
@@ -91,14 +91,14 @@ export const QUICK_WINS = [
91
91
  export const NEXT_WORKFLOWS = [
92
92
  {
93
93
  name: "create_talking_cards",
94
- description: "Generate a batch of branded talking 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 and save them to the user's carousel library.",
95
95
  },
96
96
  {
97
97
  name: "sync_linkedin_brand_context",
98
- description: "Use LinkedIn profile context to draft or refresh the user's Brand Doc.",
98
+ description: "Use LinkedIn profile context to draft or refresh the user's Brand Doc. Coming next.",
99
99
  },
100
100
  {
101
101
  name: "publish_talking_card",
102
- description: "Publish an approved card to LinkedIn or prepare the post for manual approval.",
102
+ description: "Publish an approved card to LinkedIn or prepare the post for manual approval. Coming next.",
103
103
  },
104
104
  ];
package/src/tools/run.mjs CHANGED
@@ -2,6 +2,56 @@ import { apiCall } from "../api.mjs";
2
2
  import { ALL_MCP_TOOL_CATALOG } from "./catalog.mjs";
3
3
  import { functionNameFromEdgeTool } from "./edge-functions.mjs";
4
4
 
5
+ function asArray(data) {
6
+ if (Array.isArray(data)) return data;
7
+ if (Array.isArray(data?.data)) return data.data;
8
+ if (Array.isArray(data?.items)) return data.items;
9
+ if (Array.isArray(data?.results)) return data.results;
10
+ return [];
11
+ }
12
+
13
+ function leadName(lead) {
14
+ return [lead.first_name, lead.last_name].filter(Boolean).join(" ") || lead.title || lead.name || lead.email || lead.id || "Unnamed lead";
15
+ }
16
+
17
+ function qualifyFromLeads(result) {
18
+ const leads = asArray(result);
19
+ const recommendations = leads.map((lead) => {
20
+ const hasEmail = Boolean(lead.email || lead.metadata?.email);
21
+ const hasProfile = Boolean(lead.profile_url || lead.metadata?.profile_url);
22
+ const enrichmentStatus = lead.enrichment_status || lead.metadata?.enrichment_status || "unknown";
23
+ const score = (hasEmail ? 45 : 0) + (hasProfile ? 30 : 0) + (enrichmentStatus === "pending" ? 10 : 20);
24
+ return {
25
+ id: lead.id,
26
+ name: leadName(lead),
27
+ email: lead.email || lead.metadata?.email || null,
28
+ profileUrl: lead.profile_url || lead.metadata?.profile_url || null,
29
+ stage: lead.stage || lead.metadata?.stage || null,
30
+ enrichmentStatus,
31
+ score: Math.min(score, 100),
32
+ nextAction: enrichmentStatus === "pending" ? "Enrich this guest, then draft outreach." : "Draft outreach or invite to a recording.",
33
+ };
34
+ }).sort((a, b) => b.score - a.score);
35
+
36
+ return {
37
+ tool: "qualify_leads",
38
+ data: {
39
+ analysed: recommendations.length,
40
+ contactable: recommendations.filter((lead) => lead.email || lead.profileUrl).length,
41
+ needsEnrichment: recommendations.filter((lead) => lead.enrichmentStatus === "pending").length,
42
+ recommendations,
43
+ },
44
+ items: recommendations.slice(0, 10).map((lead) => ({
45
+ type: "lead_recommendation",
46
+ id: lead.id,
47
+ title: `${lead.name} (${lead.score}/100)`,
48
+ subtitle: lead.nextAction,
49
+ link: lead.id ? `/leads?id=${lead.id}` : undefined,
50
+ metadata: lead,
51
+ })),
52
+ };
53
+ }
54
+
5
55
  const BUILTIN_TOOL_HANDLERS = {
6
56
  list_episodes: async ({ limit = 10 }) => apiCall("cli-list-episodes", { body: { limit } }),
7
57
  get_episode: async ({ episode }) => apiCall("cli-get-episode", { body: { episode } }),
@@ -12,6 +62,15 @@ const BUILTIN_TOOL_HANDLERS = {
12
62
  count: Math.max(1, Math.min(Number(count || 3), 10)),
13
63
  },
14
64
  }),
65
+ qualify_leads: async ({ stage = "all", limit = 10 } = {}) => {
66
+ const leads = await apiCall("magic-assistant", {
67
+ body: {
68
+ toolName: "get_user_leads",
69
+ args: { stage, limit: Math.max(1, Math.min(Number(limit || 10), 20)) },
70
+ },
71
+ });
72
+ return qualifyFromLeads(leads);
73
+ },
15
74
  };
16
75
 
17
76
  export function findTool(name) {