@mangomagic/cli 0.1.11 → 0.1.13
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 +1 -1
- package/src/chat/natural-language.mjs +322 -12
- package/src/index.mjs +34 -13
- package/src/tools/catalog.mjs +5 -5
- package/src/ui/splash.mjs +18 -9
package/package.json
CHANGED
|
@@ -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, " ");
|
|
@@ -20,21 +32,66 @@ function localPlan(text) {
|
|
|
20
32
|
if (!t) return { action: "answer", args: { text: "" } };
|
|
21
33
|
|
|
22
34
|
if (/^(exit|quit|bye)$/i.test(t)) return { action: "exit", args: {} };
|
|
35
|
+
if (/^(hi|hey|hello|yo)\??$/i.test(t)) {
|
|
36
|
+
return {
|
|
37
|
+
action: "answer",
|
|
38
|
+
args: {
|
|
39
|
+
text: "Hey. I'm here in the terminal now. Ask for episodes, talking cards, tools, or MCP setup.",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (t.includes("understand me")) {
|
|
44
|
+
return {
|
|
45
|
+
action: "answer",
|
|
46
|
+
args: {
|
|
47
|
+
text: "Yes. Type at the mango> prompt and I will route natural language to MangoMagic tools.",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
23
51
|
if (t.includes("brand doc") || t.includes("brand colour") || t.includes("brand color")) {
|
|
24
52
|
return { action: "open_brand_doc", args: {} };
|
|
25
53
|
}
|
|
26
54
|
if (t.includes("talking card") || t.includes("carousel") || /\bcards?\b/.test(t)) {
|
|
27
55
|
return { action: "create_talking_cards", args: { focus: raw, count: inferCount(raw) } };
|
|
28
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
|
+
}
|
|
29
78
|
if (t.includes("mcp") && (t.includes("config") || t.includes("connect") || t.includes("claude") || t.includes("cursor") || t.includes("codex"))) {
|
|
30
79
|
return { action: "mcp_config", args: {} };
|
|
31
80
|
}
|
|
32
|
-
if (t.includes("tool") || t.includes("what can"))
|
|
33
|
-
|
|
34
|
-
return { action: "list_episodes", args: { limit: 5 } };
|
|
81
|
+
if (t.includes("tool") || t.includes("what can")) {
|
|
82
|
+
return { action: "show_tools", args: { all: /\b(all|every|full|complete)\b/.test(t) } };
|
|
35
83
|
}
|
|
36
84
|
const searchMatch = raw.match(/search(?: my)? episodes? (?:for|about)\s+(.+)/i);
|
|
37
85
|
if (searchMatch?.[1]) return { action: "search_episodes", args: { query: searchMatch[1] } };
|
|
86
|
+
if (
|
|
87
|
+
t.includes("latest episode") ||
|
|
88
|
+
t.includes("recent episode") ||
|
|
89
|
+
t === "episodes" ||
|
|
90
|
+
t.includes("list episodes") ||
|
|
91
|
+
(/\bepisodes?\b/.test(t) && /\b(what|which|have|got|my|show)\b/.test(t))
|
|
92
|
+
) {
|
|
93
|
+
return { action: "list_episodes", args: { limit: 5 } };
|
|
94
|
+
}
|
|
38
95
|
const getMatch = raw.match(/(?:get|show|open)(?: episode)?\s+([a-z0-9][a-z0-9_-]{5,})/i);
|
|
39
96
|
if (getMatch?.[1]) return { action: "get_episode", args: { episode: getMatch[1] } };
|
|
40
97
|
if (t.includes("home") || t.includes("help")) return { action: "home", args: {} };
|
|
@@ -56,6 +113,28 @@ function asArray(data) {
|
|
|
56
113
|
return [];
|
|
57
114
|
}
|
|
58
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
|
+
|
|
59
138
|
function printEpisodeList(data) {
|
|
60
139
|
const rows = asArray(data);
|
|
61
140
|
if (!rows.length) {
|
|
@@ -69,6 +148,177 @@ function printEpisodeList(data) {
|
|
|
69
148
|
}
|
|
70
149
|
}
|
|
71
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
|
+
|
|
72
322
|
function printEpisode(data) {
|
|
73
323
|
const ep = data?.episode || data?.data || data;
|
|
74
324
|
if (!ep || typeof ep !== "object") {
|
|
@@ -142,12 +392,17 @@ export async function handleNaturalLanguage(text, actions, { allowModel = true }
|
|
|
142
392
|
plan = await planWithKimi(text, {
|
|
143
393
|
availableTools: [
|
|
144
394
|
"create_talking_cards",
|
|
145
|
-
"
|
|
146
|
-
"
|
|
395
|
+
"list_inbox",
|
|
396
|
+
"list_leads",
|
|
397
|
+
"analyze_guests",
|
|
398
|
+
"list_clips",
|
|
399
|
+
"account_stats",
|
|
400
|
+
"list_meetings",
|
|
401
|
+
"list_bookings",
|
|
147
402
|
"show_tools",
|
|
148
403
|
"mcp_config",
|
|
149
404
|
"home",
|
|
150
|
-
...
|
|
405
|
+
...CHAT_TOOL_NAMES,
|
|
151
406
|
],
|
|
152
407
|
});
|
|
153
408
|
} catch (err) {
|
|
@@ -181,11 +436,39 @@ ${DIM}${err?.message ?? err}${RESET}
|
|
|
181
436
|
case "open_brand_doc":
|
|
182
437
|
return actions.openPath("/carousels/doc");
|
|
183
438
|
case "show_tools":
|
|
184
|
-
return actions.tools();
|
|
439
|
+
return actions.tools({ all: Boolean(args.all) });
|
|
185
440
|
case "mcp_config":
|
|
186
441
|
return actions.mcpConfig();
|
|
187
442
|
case "home":
|
|
188
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
|
+
}
|
|
189
472
|
case "list_episodes": {
|
|
190
473
|
const data = await apiCall("cli-list-episodes", { body: { limit: Math.max(1, Math.min(Number(args.limit || 5), 10)) } });
|
|
191
474
|
return printEpisodeList(data);
|
|
@@ -211,10 +494,29 @@ ${DIM}${err?.message ?? err}${RESET}
|
|
|
211
494
|
process.stdout.write("Which tool should I run?\n");
|
|
212
495
|
return;
|
|
213
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
|
+
}
|
|
214
502
|
const data = await runCatalogTool(args.toolName, args.args || {});
|
|
215
|
-
|
|
503
|
+
printToolResult(args.toolName, data);
|
|
216
504
|
return;
|
|
217
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
|
+
}
|
|
218
520
|
case "answer":
|
|
219
521
|
process.stdout.write(`${args.text || "I can help with MangoMagic episodes, talking cards, and MCP setup."}\n`);
|
|
220
522
|
return;
|
|
@@ -223,17 +525,25 @@ ${DIM}${err?.message ?? err}${RESET}
|
|
|
223
525
|
}
|
|
224
526
|
}
|
|
225
527
|
|
|
226
|
-
export async function chat(actions) {
|
|
528
|
+
export async function chat(actions, { showHeader = true } = {}) {
|
|
227
529
|
const rl = readline.createInterface({ input, output });
|
|
228
|
-
|
|
530
|
+
if (showHeader) {
|
|
531
|
+
process.stdout.write(`
|
|
229
532
|
${BOLD}MangoMagic Chat${RESET}
|
|
230
533
|
Type what you want. Try: ${GOLD}create talking cards about AI adoption${RESET}
|
|
231
534
|
Type ${DIM}exit${RESET} to leave.
|
|
232
535
|
|
|
233
536
|
`);
|
|
537
|
+
}
|
|
234
538
|
try {
|
|
235
539
|
while (true) {
|
|
236
|
-
|
|
540
|
+
let text;
|
|
541
|
+
try {
|
|
542
|
+
text = await rl.question(`${GOLD}mango>${RESET} `);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
if (err?.message === "readline was closed") break;
|
|
545
|
+
throw err;
|
|
546
|
+
}
|
|
237
547
|
const result = await handleNaturalLanguage(text, actions);
|
|
238
548
|
if (result === "exit") break;
|
|
239
549
|
process.stdout.write("\n");
|
package/src/index.mjs
CHANGED
|
@@ -35,7 +35,21 @@ ${BOLD}mangomagic${RESET} ${DIM}— sign into MangoMagic and bring your account
|
|
|
35
35
|
Token cache: ${credentialsPath()}
|
|
36
36
|
|
|
37
37
|
If \`mangomagic\` is not installed globally, use:
|
|
38
|
-
${GOLD}${COMMAND_PREFIX}
|
|
38
|
+
${GOLD}${COMMAND_PREFIX} login${RESET}
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sessionIntro() {
|
|
43
|
+
process.stdout.write(`
|
|
44
|
+
${BOLD}You can type naturally now.${RESET}
|
|
45
|
+
|
|
46
|
+
Try:
|
|
47
|
+
${GOLD}create 3 talking cards about founder-led sales${RESET}
|
|
48
|
+
${GOLD}analyse my guests${RESET}
|
|
49
|
+
${GOLD}show me my clips${RESET}
|
|
50
|
+
${GOLD}what episodes do I have?${RESET}
|
|
51
|
+
|
|
52
|
+
${DIM}Type ${BOLD}exit${RESET}${DIM} to leave. Add ${BOLD}--home${RESET}${DIM} next time if you only want the menu.${RESET}
|
|
39
53
|
`);
|
|
40
54
|
}
|
|
41
55
|
|
|
@@ -46,8 +60,9 @@ ${BOLD}What can MangoMagic do now?${RESET}
|
|
|
46
60
|
${DIM}Fast wins:${RESET}
|
|
47
61
|
`);
|
|
48
62
|
for (const item of QUICK_WINS) {
|
|
49
|
-
process.stdout.write(` ${
|
|
50
|
-
process.stdout.write(` ${
|
|
63
|
+
process.stdout.write(` ${BOLD}${item.title}${RESET}\n`);
|
|
64
|
+
process.stdout.write(` ${GOLD}${item.command}${RESET}\n`);
|
|
65
|
+
process.stdout.write(` ${DIM}${item.description}${RESET}\n\n`);
|
|
51
66
|
}
|
|
52
67
|
process.stdout.write(`
|
|
53
68
|
${DIM}Try this in Claude, Cursor, or Codex after MCP is connected:${RESET}
|
|
@@ -84,10 +99,16 @@ ${DIM}Core tools available now:${RESET}
|
|
|
84
99
|
}
|
|
85
100
|
|
|
86
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
|
+
|
|
87
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).
|
|
88
|
-
${DIM}Run:${RESET} ${GOLD}
|
|
89
|
-
${DIM}Run with JSON:${RESET} ${GOLD}
|
|
90
|
-
${DIM}Full list:${RESET} ${GOLD}
|
|
109
|
+
${DIM}Run:${RESET} ${GOLD}${COMMAND_PREFIX} tool get_user_stats${RESET}
|
|
110
|
+
${DIM}Run with JSON:${RESET} ${GOLD}${COMMAND_PREFIX} tool get_user_episodes '{"limit":3}'${RESET}
|
|
111
|
+
${DIM}Full list:${RESET} ${GOLD}${COMMAND_PREFIX} tools --all${RESET}
|
|
91
112
|
`);
|
|
92
113
|
|
|
93
114
|
if (all) {
|
|
@@ -106,14 +127,14 @@ ${DIM}Full list:${RESET} ${GOLD}mangomagic tools --all${RESET}
|
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
process.stdout.write(`
|
|
109
|
-
${DIM}
|
|
130
|
+
${DIM}High-value workflows:${RESET}
|
|
110
131
|
`);
|
|
111
132
|
for (const workflow of NEXT_WORKFLOWS) {
|
|
112
133
|
process.stdout.write(` ${GOLD}${workflow.name.padEnd(28)}${RESET} ${workflow.description}\n`);
|
|
113
134
|
}
|
|
114
135
|
|
|
115
136
|
process.stdout.write(`
|
|
116
|
-
${DIM}Use
|
|
137
|
+
${DIM}Use \`${COMMAND_PREFIX} mcp-config\` to connect an MCP client.${RESET}
|
|
117
138
|
`);
|
|
118
139
|
}
|
|
119
140
|
|
|
@@ -150,15 +171,15 @@ function splashMode() {
|
|
|
150
171
|
|
|
151
172
|
async function enterChatAfterLogin(startChat) {
|
|
152
173
|
if (!startChat) return;
|
|
153
|
-
|
|
154
|
-
await chat(actions());
|
|
174
|
+
sessionIntro();
|
|
175
|
+
await chat(actions(), { showHeader: false });
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
async function login({ openInBrowser = true, startChat = false } = {}) {
|
|
158
179
|
if (loadToken()) {
|
|
159
|
-
process.stdout.write(`${DIM}You're already signed in.
|
|
180
|
+
process.stdout.write(`${DIM}You're already signed in. Continuing in MangoMagic.${RESET}\n`);
|
|
160
181
|
await playSplash({ mode: splashMode(), greetingLine: "Welcome back to MangoMagic." });
|
|
161
|
-
home();
|
|
182
|
+
if (!startChat) home();
|
|
162
183
|
await enterChatAfterLogin(startChat);
|
|
163
184
|
return;
|
|
164
185
|
}
|
|
@@ -180,7 +201,7 @@ ${BOLD}Sign in to MangoMagic${RESET}
|
|
|
180
201
|
saveToken(creds);
|
|
181
202
|
process.stdout.write("\n\n");
|
|
182
203
|
await playSplash({ mode: splashMode(), greetingLine: "You're in. Welcome to MangoMagic." });
|
|
183
|
-
home();
|
|
204
|
+
if (!startChat) home();
|
|
184
205
|
await enterChatAfterLogin(startChat);
|
|
185
206
|
});
|
|
186
207
|
}
|
package/src/tools/catalog.mjs
CHANGED
|
@@ -69,9 +69,9 @@ export const QUICK_WINS = [
|
|
|
69
69
|
status: "ready",
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
|
-
command: `${COMMAND_PREFIX}
|
|
72
|
+
command: `${COMMAND_PREFIX} login`,
|
|
73
73
|
title: "Talk To MangoMagic",
|
|
74
|
-
description: "
|
|
74
|
+
description: "Enter chat mode, type natural language, and let the CLI choose the right MangoMagic action.",
|
|
75
75
|
status: "ready",
|
|
76
76
|
},
|
|
77
77
|
{
|
|
@@ -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
|
|
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/ui/splash.mjs
CHANGED
|
@@ -35,6 +35,7 @@ const WHITEC = [255, 255, 255];
|
|
|
35
35
|
const DIMC = [110, 78, 12];
|
|
36
36
|
|
|
37
37
|
const RESET = "\x1b[0m";
|
|
38
|
+
const ESC = "\x1b[";
|
|
38
39
|
|
|
39
40
|
const rgb = (c) => `\x1b[38;2;${c[0]};${c[1]};${c[2]}m`;
|
|
40
41
|
const lerp = (a, b, t) => [
|
|
@@ -217,9 +218,15 @@ export async function playSplash({ mode = "static", greetingLine = null } = {})
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
const fps = 14;
|
|
220
|
-
const total = mode === "loop" ? Number.MAX_SAFE_INTEGER : Math.floor(
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
const total = mode === "loop" ? Number.MAX_SAFE_INTEGER : Math.floor(1.15 * fps);
|
|
222
|
+
const useAltScreen = process.stdout.isTTY && process.env.MANGOMAGIC_SPLASH_INLINE !== "1";
|
|
223
|
+
const renderFrame = (body) => {
|
|
224
|
+
process.stdout.write(`${ESC}H${ESC}2J`);
|
|
225
|
+
process.stdout.write(body);
|
|
226
|
+
process.stdout.write("\n\n" + tag + greet + "\n");
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
process.stdout.write(useAltScreen ? `${ESC}?1049h${ESC}?25l` : `${ESC}?25l`);
|
|
223
230
|
let interrupted = false;
|
|
224
231
|
const onSig = () => { interrupted = true; };
|
|
225
232
|
process.on("SIGINT", onSig);
|
|
@@ -229,15 +236,17 @@ export async function playSplash({ mode = "static", greetingLine = null } = {})
|
|
|
229
236
|
if (Math.random() < 0.08 && shooters.length < 2) shooters.push(new Shooter(width, height));
|
|
230
237
|
for (const sh of shooters) sh.life += 1;
|
|
231
238
|
shooters = shooters.filter(s => s.life < 6);
|
|
232
|
-
|
|
233
|
-
process.stdout.write(render(width, height, iconTop, wordTop, stars, shooters, f));
|
|
234
|
-
process.stdout.write("\n\n" + tag + greet + "\n");
|
|
239
|
+
renderFrame(render(width, height, iconTop, wordTop, stars, shooters, f));
|
|
235
240
|
await sleep(1000 / fps);
|
|
236
241
|
}
|
|
237
242
|
} finally {
|
|
238
243
|
process.off("SIGINT", onSig);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
if (useAltScreen) {
|
|
245
|
+
process.stdout.write(`${ESC}?25h${ESC}?1049l`);
|
|
246
|
+
process.stdout.write(renderSettled() + "\n\n" + tag + greet + "\n");
|
|
247
|
+
} else {
|
|
248
|
+
renderFrame(renderSettled());
|
|
249
|
+
process.stdout.write(`${ESC}?25h`);
|
|
250
|
+
}
|
|
242
251
|
}
|
|
243
252
|
}
|