user-majico-mcp 0.7.5

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.
Files changed (66) hide show
  1. package/README.md +144 -0
  2. package/dist/agent-bootstrap.d.ts +8 -0
  3. package/dist/agent-bootstrap.js +21 -0
  4. package/dist/auth-prompt.d.ts +13 -0
  5. package/dist/auth-prompt.js +15 -0
  6. package/dist/client-disconnect.d.ts +4 -0
  7. package/dist/client-disconnect.js +17 -0
  8. package/dist/credentials.d.ts +40 -0
  9. package/dist/credentials.js +60 -0
  10. package/dist/http-handler.d.ts +6 -0
  11. package/dist/http-handler.js +28 -0
  12. package/dist/index.d.ts +12 -0
  13. package/dist/index.js +22 -0
  14. package/dist/present-interactive.d.ts +92 -0
  15. package/dist/present-interactive.js +479 -0
  16. package/dist/present-types.d.ts +17 -0
  17. package/dist/present-types.js +1 -0
  18. package/dist/project-relevance.d.ts +27 -0
  19. package/dist/project-relevance.js +95 -0
  20. package/dist/project-selection-hints.d.ts +16 -0
  21. package/dist/project-selection-hints.js +32 -0
  22. package/dist/project-selection.d.ts +25 -0
  23. package/dist/project-selection.js +42 -0
  24. package/dist/recommended-external-skills.d.ts +39 -0
  25. package/dist/recommended-external-skills.js +319 -0
  26. package/dist/sdk-surface.d.ts +10 -0
  27. package/dist/sdk-surface.js +24 -0
  28. package/dist/server-branding.d.ts +17 -0
  29. package/dist/server-branding.js +40 -0
  30. package/dist/server.d.ts +3 -0
  31. package/dist/server.js +24 -0
  32. package/dist/svg-preview-block.d.ts +14 -0
  33. package/dist/svg-preview-block.js +82 -0
  34. package/dist/tools/constants.d.ts +47 -0
  35. package/dist/tools/constants.js +95 -0
  36. package/dist/tools/dispatch-branding-studio.d.ts +5 -0
  37. package/dist/tools/dispatch-branding-studio.js +197 -0
  38. package/dist/tools/dispatch-pulse-blog.d.ts +3 -0
  39. package/dist/tools/dispatch-pulse-blog.js +158 -0
  40. package/dist/tools/dispatch-research-assets.d.ts +3 -0
  41. package/dist/tools/dispatch-research-assets.js +132 -0
  42. package/dist/tools/handle-tool-call-pre.d.ts +4 -0
  43. package/dist/tools/handle-tool-call-pre.js +47 -0
  44. package/dist/tools/handle-tool-call.d.ts +3 -0
  45. package/dist/tools/handle-tool-call.js +56 -0
  46. package/dist/tools/tool-call-helpers.d.ts +22 -0
  47. package/dist/tools/tool-call-helpers.js +104 -0
  48. package/dist/tools/tool-catalog.d.ts +14 -0
  49. package/dist/tools/tool-catalog.js +54 -0
  50. package/dist/tools/tool-definitions-blog-git.d.ts +2 -0
  51. package/dist/tools/tool-definitions-blog-git.js +207 -0
  52. package/dist/tools/tool-definitions-branding.d.ts +2 -0
  53. package/dist/tools/tool-definitions-branding.js +197 -0
  54. package/dist/tools/tool-definitions-research-aliases.d.ts +2 -0
  55. package/dist/tools/tool-definitions-research-aliases.js +340 -0
  56. package/dist/tools/tool-definitions-studio-pulse.d.ts +2 -0
  57. package/dist/tools/tool-definitions-studio-pulse.js +235 -0
  58. package/dist/tools/tool-matrix.d.ts +12 -0
  59. package/dist/tools/tool-matrix.js +36 -0
  60. package/dist/tools.d.ts +5 -0
  61. package/dist/tools.js +3 -0
  62. package/dist/ui-ux-skills.d.ts +33 -0
  63. package/dist/ui-ux-skills.js +157 -0
  64. package/dist/user-pick-policy.d.ts +18 -0
  65. package/dist/user-pick-policy.js +21 -0
  66. package/package.json +71 -0
@@ -0,0 +1,479 @@
1
+ import { svgPreviewBlock } from "./svg-preview-block.js";
2
+ const REELDEMO_PROJECT_ID = "252e664f-4a92-467d-b07e-6447ab96f3ba";
3
+ const REELDEMO_BRAND = {
4
+ bg: "#0B0D10",
5
+ accent: "#FF6A1A",
6
+ signal: "#7C8F43",
7
+ };
8
+ const PALETTE_PREVIEW_LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
9
+ const DEFAULT_HEADING_FONT = "Space Grotesk";
10
+ const DEFAULT_BODY_FONT = "Inter";
11
+ const PALETTE_CARD_WIDTH = 320;
12
+ const PALETTE_CARD_HEIGHT = 200;
13
+ const MAX_PALETTE_OPTIONS = 3;
14
+ import { AUTO_PICK_ALLOWED_WHEN, AUTO_PICK_DEFAULT_POLICY, USER_SELECTION_JSON_META, } from "./user-pick-policy.js";
15
+ const PALETTE_USER_SELECTION_WARNING = [
16
+ "**Default — wait for user:** Present options and ask before calling `select_palette`.",
17
+ AUTO_PICK_DEFAULT_POLICY,
18
+ `**Exception:** If the user prompt explicitly delegates the choice (${AUTO_PICK_ALLOWED_WHEN}), call \`select_palette\` with \`userDelegatedPick: true\` (same turn is OK).`,
19
+ ].join(" ");
20
+ const LOGO_USER_SELECTION_WARNING = [
21
+ "**Default — wait for user:** Present numbered logos and ask before calling `select_logo`.",
22
+ AUTO_PICK_DEFAULT_POLICY,
23
+ `**Exception:** If the user prompt explicitly delegates the choice (${AUTO_PICK_ALLOWED_WHEN}), call \`select_logo\` with \`userDelegatedPick: true\` (same turn is OK).`,
24
+ ].join(" ");
25
+ function escapeXml(value) {
26
+ return value.replace(/[<>&"']/g, (c) => {
27
+ switch (c) {
28
+ case "<":
29
+ return "&lt;";
30
+ case ">":
31
+ return "&gt;";
32
+ case "&":
33
+ return "&amp;";
34
+ case '"':
35
+ return "&quot;";
36
+ case "'":
37
+ return "&apos;";
38
+ default:
39
+ return c;
40
+ }
41
+ });
42
+ }
43
+ function sanitizeHex(hex, fallback) {
44
+ if (!hex?.trim())
45
+ return fallback;
46
+ const safe = hex.trim().replace(/[<>"']/g, "");
47
+ return /^#[0-9a-fA-F]{3,8}$/.test(safe) ? safe : fallback;
48
+ }
49
+ function hexLuminance(hex) {
50
+ const h = hex.replace("#", "");
51
+ if (h.length < 6)
52
+ return 0.5;
53
+ const r = Number.parseInt(h.slice(0, 2), 16) / 255;
54
+ const g = Number.parseInt(h.slice(2, 4), 16) / 255;
55
+ const b = Number.parseInt(h.slice(4, 6), 16) / 255;
56
+ return 0.2126 * r + 0.5872 * g + 0.114 * b;
57
+ }
58
+ function colorDistance(hex1, hex2) {
59
+ const parse = (hex) => {
60
+ const h = hex.replace("#", "");
61
+ if (h.length < 6)
62
+ return [0, 0, 0];
63
+ return [
64
+ Number.parseInt(h.slice(0, 2), 16),
65
+ Number.parseInt(h.slice(2, 4), 16),
66
+ Number.parseInt(h.slice(4, 6), 16),
67
+ ];
68
+ };
69
+ const [r1, g1, b1] = parse(hex1);
70
+ const [r2, g2, b2] = parse(hex2);
71
+ return Math.hypot(r1 - r2, g1 - g2, b1 - b2);
72
+ }
73
+ function tokensFromOption(option) {
74
+ const dark = option.previewTokens?.dark;
75
+ if (dark) {
76
+ return {
77
+ bg: sanitizeHex(dark.bg, "#0B0D10"),
78
+ bgMuted: sanitizeHex(dark.bgMuted, "#1A1D24"),
79
+ text: sanitizeHex(dark.text, "#E8EAED"),
80
+ accent: sanitizeHex(dark.accent, "#FF6A1A"),
81
+ };
82
+ }
83
+ const sw = option.swatches?.dark ?? option.swatches?.light ?? [];
84
+ return {
85
+ bg: sanitizeHex(sw[0], "#0B0D10"),
86
+ bgMuted: sanitizeHex(sw[3] ?? sw[0], "#1A1D24"),
87
+ text: sanitizeHex(sw[2], "#E8EAED"),
88
+ accent: sanitizeHex(sw[1], "#FF6A1A"),
89
+ };
90
+ }
91
+ export function pickPaletteOptionsForDisplay(options, max = MAX_PALETTE_OPTIONS) {
92
+ if (options.length <= max)
93
+ return options;
94
+ return [...options]
95
+ .sort((a, b) => {
96
+ const lumA = hexLuminance(tokensFromOption(a).bg);
97
+ const lumB = hexLuminance(tokensFromOption(b).bg);
98
+ return lumA - lumB;
99
+ })
100
+ .slice(0, max);
101
+ }
102
+ export function scoreReeldemoPaletteMatch(option) {
103
+ const tokens = tokensFromOption(option);
104
+ return (colorDistance(tokens.bg, REELDEMO_BRAND.bg) +
105
+ colorDistance(tokens.accent, REELDEMO_BRAND.accent) * 0.6 +
106
+ colorDistance(tokens.bgMuted, REELDEMO_BRAND.signal) * 0.3);
107
+ }
108
+ export function isReeldemoPaletteContext(ctx) {
109
+ if (ctx.projectId === REELDEMO_PROJECT_ID)
110
+ return true;
111
+ return (ctx.repoHints ?? []).some((h) => h.includes("reeldemo"));
112
+ }
113
+ export function buildPalettePreviewCardSvg(args) {
114
+ const width = args.width ?? PALETTE_CARD_WIDTH;
115
+ const height = args.height ?? PALETTE_CARD_HEIGHT;
116
+ const { bg, bgMuted, text, accent } = args.tokens;
117
+ const headingFont = escapeXml(args.headingFont);
118
+ const bodyFont = escapeXml(args.bodyFont);
119
+ const label = escapeXml(args.label.slice(0, 40));
120
+ const swatchW = 68;
121
+ const swatchH = 28;
122
+ const swatches = [
123
+ { color: bg, name: "bg" },
124
+ { color: bgMuted, name: "surface" },
125
+ { color: text, name: "text" },
126
+ { color: accent, name: "accent" },
127
+ ]
128
+ .map((s, i) => `<rect x="${12 + i * (swatchW + 6)}" y="12" width="${swatchW}" height="${swatchH}" rx="4" fill="${s.color}"/>
129
+ <text x="${12 + i * (swatchW + 6) + swatchW / 2}" y="${12 + swatchH + 10}" text-anchor="middle" font-family="Inter,system-ui,sans-serif" font-size="8" fill="${text}" opacity="0.7">${s.name}</text>`)
130
+ .join("\n ");
131
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
132
+ <rect width="${width}" height="${height}" rx="8" fill="${bg}"/>
133
+ <rect x="1" y="1" width="${width - 2}" height="${height - 2}" rx="7" fill="none" stroke="${bgMuted}" stroke-width="1"/>
134
+ ${swatches}
135
+ <text x="16" y="78" font-family="${headingFont},system-ui,sans-serif" font-size="18" font-weight="600" fill="${accent}">Palette ${args.index}</text>
136
+ <text x="16" y="96" font-family="${bodyFont},system-ui,sans-serif" font-size="10" fill="${text}" opacity="0.85">${label}</text>
137
+ <text x="16" y="124" font-family="${bodyFont},system-ui,sans-serif" font-size="11" fill="${text}" opacity="0.9">${PALETTE_PREVIEW_LOREM}</text>
138
+ <text x="16" y="178" font-family="Inter,system-ui,sans-serif" font-size="9" fill="${text}" opacity="0.55">${headingFont} / ${bodyFont}</text>
139
+ </svg>`;
140
+ }
141
+ function normalizeBase(url) {
142
+ return url.replace(/\/$/, "");
143
+ }
144
+ /** Bind-all / non-browser hosts must never appear in chat preview links. */
145
+ function isNonBrowserHost(hostname) {
146
+ return (hostname === "0.0.0.0" ||
147
+ hostname === "::" ||
148
+ hostname === "[::]");
149
+ }
150
+ function isBrowserReachableOrigin(url) {
151
+ if (!url?.trim())
152
+ return false;
153
+ try {
154
+ return !isNonBrowserHost(new URL(url.trim()).hostname);
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ }
160
+ /**
161
+ * Prefer API-provided picker URL when it is browser-reachable; otherwise rebuild
162
+ * from the session public base (staging k8s often mints http://0.0.0.0:3000/…).
163
+ */
164
+ function resolvePickerUrl(fromApi, rebuilt) {
165
+ const api = fromApi?.trim();
166
+ if (api && isBrowserReachableOrigin(api))
167
+ return api;
168
+ return rebuilt;
169
+ }
170
+ /**
171
+ * Rewrite absolute URLs whose host is bind-all onto the session public base,
172
+ * preserving path + query.
173
+ */
174
+ export function rewritePreviewUrlOntoPublicBase(url, publicBaseUrl) {
175
+ if (!url?.trim() || !isBrowserReachableOrigin(publicBaseUrl))
176
+ return url;
177
+ try {
178
+ const parsed = new URL(url);
179
+ if (!isNonBrowserHost(parsed.hostname))
180
+ return url;
181
+ const base = new URL(normalizeBase(publicBaseUrl));
182
+ return `${base.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
183
+ }
184
+ catch {
185
+ return url;
186
+ }
187
+ }
188
+ const PLAN_MODE_PREVIEW_NOTE = [
189
+ "**Cursor Plan mode:** inline previews often do not render — open the browser picker link below.",
190
+ "**Agent mode:** PNG preview cards should appear below each option when supported.",
191
+ ].join(" ");
192
+ function logoPickerUrl(base, projectId, previewToken, candidateId) {
193
+ const origin = normalizeBase(base);
194
+ const params = new URLSearchParams({ project: projectId, cursor: "1" });
195
+ const token = previewToken?.trim();
196
+ if (token)
197
+ params.set("t", token);
198
+ if (candidateId)
199
+ params.set("candidate", candidateId);
200
+ return `${origin}/mcp/preview/logo-picker?${params.toString()}`;
201
+ }
202
+ function studioLogoUrl(base, projectId, previewToken, candidateId) {
203
+ return logoPickerUrl(base, projectId, previewToken, candidateId);
204
+ }
205
+ function palettePickerUrl(base, projectId, previewToken, optionId) {
206
+ const origin = normalizeBase(base);
207
+ const params = new URLSearchParams({ project: projectId, cursor: "1" });
208
+ const token = previewToken?.trim();
209
+ if (token)
210
+ params.set("t", token);
211
+ if (optionId) {
212
+ params.set("option", optionId);
213
+ return `${origin}/mcp/preview/palette?${params.toString()}`;
214
+ }
215
+ return `${origin}/mcp/preview/palette-picker?${params.toString()}`;
216
+ }
217
+ function previewUrl(base, projectId, candidateId) {
218
+ return `${normalizeBase(base)}/api/mcp/projects/${encodeURIComponent(projectId)}/logos/${encodeURIComponent(candidateId)}/preview`;
219
+ }
220
+ function swatchRowSvg(colors, label, width = 200, height = 56) {
221
+ const cellW = Math.floor((width - 8) / Math.max(colors.length, 1));
222
+ const rects = colors
223
+ .map((hex, i) => {
224
+ const safe = String(hex).replace(/[<>"']/g, "");
225
+ return `<rect x="${4 + i * cellW}" y="20" width="${cellW - 2}" height="32" rx="4" fill="${safe}"/>`;
226
+ })
227
+ .join("");
228
+ const safeLabel = label.replace(/[<>&]/g, "").slice(0, 24);
229
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
230
+ <text x="4" y="14" font-family="Inter,system-ui,sans-serif" font-size="10" fill="#475569">${safeLabel}</text>
231
+ ${rects}
232
+ </svg>`;
233
+ }
234
+ function lockedPaletteSvg(tokens, mode) {
235
+ const t = tokens[mode];
236
+ if (!t)
237
+ return null;
238
+ const colors = [t.bg, t.accent, t.text, t.bgMuted].filter(Boolean);
239
+ if (colors.length === 0)
240
+ return null;
241
+ return swatchRowSvg(colors, `${mode} theme`);
242
+ }
243
+ export async function presentLogoCandidates(data, ctx) {
244
+ const previewToken = data.previewToken ?? null;
245
+ const projectId = data.projectId?.trim() || ctx.projectId;
246
+ const previewPickerUrl = resolvePickerUrl(data.previewPickerUrl, logoPickerUrl(ctx.publicBaseUrl, projectId, previewToken));
247
+ const browserLogoUrl = previewPickerUrl;
248
+ const candidates = (data.candidates ?? []).map((c, i) => ({
249
+ index: i + 1,
250
+ id: c.id,
251
+ kind: c.kind,
252
+ templateId: c.templateId ?? null,
253
+ userRating: c.userRating ?? null,
254
+ previewUrl: rewritePreviewUrlOntoPublicBase(previewUrl(ctx.publicBaseUrl, projectId, c.id), ctx.publicBaseUrl),
255
+ browserPickUrl: rewritePreviewUrlOntoPublicBase(studioLogoUrl(ctx.publicBaseUrl, projectId, previewToken, c.id), ctx.publicBaseUrl),
256
+ selectHint: `After user confirms or delegates: reply ${i + 1} with userConfirmed: true, or call select_logo with candidateId "${c.id}" and userConfirmed/userDelegatedPick: true`,
257
+ }));
258
+ const summary = {
259
+ ...USER_SELECTION_JSON_META,
260
+ selectedLogoTemplateId: data.selectedLogoTemplateId,
261
+ logoSvg: data.logoSvg,
262
+ logoFavorites: data.logoFavorites,
263
+ shortlistCount: data.shortlistCount,
264
+ emptyReason: data.emptyReason ?? null,
265
+ guidance: data.guidance ?? null,
266
+ browserLogoUrl,
267
+ previewPickerUrl,
268
+ previewToken,
269
+ agentInstructions: candidates.length === 0
270
+ ? "No distinct generated logo candidates. Do not invent generic marks — run logo generation, then list_logo_candidates again."
271
+ : "Show each numbered logo image below. Default: wait for user pick (1-N). If user prompt explicitly asks you to choose, select_logo with userDelegatedPick: true.",
272
+ userSelectionWarning: LOGO_USER_SELECTION_WARNING,
273
+ candidates,
274
+ };
275
+ if (candidates.length === 0) {
276
+ return [
277
+ {
278
+ type: "text",
279
+ text: [
280
+ `# Logo candidates (0)`,
281
+ "",
282
+ data.guidance ??
283
+ "No generated logo candidates yet. Run logo generation, then call list_logo_candidates again.",
284
+ "",
285
+ PLAN_MODE_PREVIEW_NOTE,
286
+ "",
287
+ `[Open logo picker](${previewPickerUrl})`,
288
+ "",
289
+ JSON.stringify(summary, null, 2),
290
+ ].join("\n"),
291
+ },
292
+ ];
293
+ }
294
+ const content = [
295
+ {
296
+ type: "text",
297
+ text: [
298
+ `# Logo candidates (${candidates.length})`,
299
+ "",
300
+ LOGO_USER_SELECTION_WARNING,
301
+ "",
302
+ PLAN_MODE_PREVIEW_NOTE,
303
+ "",
304
+ `[Open logo picker](${previewPickerUrl})`,
305
+ "",
306
+ `Studio (full canvas): ${normalizeBase(ctx.publicBaseUrl)}/canvas?project=${encodeURIComponent(projectId)}`,
307
+ "",
308
+ JSON.stringify(summary, null, 2),
309
+ ].join("\n"),
310
+ },
311
+ ];
312
+ for (const c of candidates) {
313
+ const raw = data.candidates[c.index - 1]?.previewSvg;
314
+ content.push({
315
+ type: "text",
316
+ text: [
317
+ `## Option ${c.index} — \`${c.id.slice(0, 8)}…\``,
318
+ `Preview: ${c.previewUrl}`,
319
+ `Browser: ${c.browserPickUrl}`,
320
+ c.selectHint,
321
+ ].join("\n"),
322
+ });
323
+ if (raw?.trim())
324
+ content.push(await svgPreviewBlock(raw));
325
+ }
326
+ return content;
327
+ }
328
+ export async function presentPaletteOptions(data, ctx) {
329
+ const previewToken = data.previewToken ?? null;
330
+ const projectId = data.projectId?.trim() || ctx.projectId;
331
+ const projectName = data.projectName?.trim() || null;
332
+ const previewPickerUrl = resolvePickerUrl(data.previewPickerUrl, palettePickerUrl(ctx.publicBaseUrl, projectId, previewToken));
333
+ const headingFont = data.headingFont?.trim() || DEFAULT_HEADING_FONT;
334
+ const bodyFont = data.bodyFont?.trim() || DEFAULT_BODY_FONT;
335
+ const allOptions = data.options ?? [];
336
+ const displayed = pickPaletteOptionsForDisplay(allOptions);
337
+ const reeldemoCtx = isReeldemoPaletteContext(ctx);
338
+ const reeldemoBest = reeldemoCtx && displayed.length > 0
339
+ ? displayed.reduce((best, o, i) => {
340
+ const score = scoreReeldemoPaletteMatch(o);
341
+ return score < best.score
342
+ ? { index: i + 1, optionId: o.optionId, score }
343
+ : best;
344
+ }, { index: 1, optionId: displayed[0].optionId, score: Infinity })
345
+ : null;
346
+ const options = displayed.map((o, i) => ({
347
+ index: i + 1,
348
+ optionId: o.optionId,
349
+ label: o.label,
350
+ isSelected: o.isSelected,
351
+ swatches: o.swatches,
352
+ previewTokens: o.previewTokens,
353
+ browserPickUrl: rewritePreviewUrlOntoPublicBase(palettePickerUrl(ctx.publicBaseUrl, projectId, previewToken, o.optionId), ctx.publicBaseUrl),
354
+ selectHint: `After user confirms or delegates: reply **${i + 1}** with userConfirmed: true, or call \`select_palette\` with optionId \`${o.optionId}\` and userConfirmed/userDelegatedPick: true`,
355
+ reeldemoFitAdvisory: reeldemoBest?.optionId === o.optionId && reeldemoCtx ? true : undefined,
356
+ }));
357
+ const reeldemoHint = reeldemoBest && reeldemoCtx
358
+ ? `**Reeldemo fit (advisory only):** Option **${reeldemoBest.index}** is closest to dark studio (Pitch Black \`${REELDEMO_BRAND.bg}\`, Ember Orange \`${REELDEMO_BRAND.accent}\`, Acid Moss \`${REELDEMO_BRAND.signal}\`). Mention to the user — only call \`select_palette\` after they pick or explicitly delegate (e.g. "auto-select the Reeldemo match").`
359
+ : null;
360
+ const content = [
361
+ {
362
+ type: "text",
363
+ text: [
364
+ `# Palette options (${options.length} of ${allOptions.length} shown)`,
365
+ "",
366
+ PALETTE_USER_SELECTION_WARNING,
367
+ "",
368
+ PLAN_MODE_PREVIEW_NOTE,
369
+ "",
370
+ data.agentInstructions ??
371
+ "Present the browser palette picker link first. Default: wait for user pick 1–3. If user prompt explicitly asks you to choose, select_palette with userDelegatedPick: true.",
372
+ "",
373
+ reeldemoHint,
374
+ "",
375
+ `[Open palette picker](${previewPickerUrl})`,
376
+ "",
377
+ JSON.stringify({
378
+ ...USER_SELECTION_JSON_META,
379
+ userSelectionWarning: PALETTE_USER_SELECTION_WARNING,
380
+ previewPickerUrl,
381
+ projectId,
382
+ ...(projectName ? { projectName } : {}),
383
+ previewToken,
384
+ selectedOptionId: data.selectedOptionId,
385
+ paletteTokens: data.paletteTokens,
386
+ headingFont,
387
+ bodyFont,
388
+ options,
389
+ totalOptions: allOptions.length,
390
+ displayedCount: options.length,
391
+ }, null, 2),
392
+ ]
393
+ .filter(Boolean)
394
+ .join("\n"),
395
+ },
396
+ ];
397
+ if (options.length > 0) {
398
+ for (const o of options) {
399
+ const source = displayed[o.index - 1];
400
+ const tokens = tokensFromOption(source);
401
+ const cardSvg = buildPalettePreviewCardSvg({
402
+ label: o.label,
403
+ index: o.index,
404
+ tokens,
405
+ headingFont,
406
+ bodyFont,
407
+ });
408
+ const reeldemoTag = o.reeldemoFitAdvisory
409
+ ? " _(advisory Reeldemo fit — pick only after user confirms or delegates)_"
410
+ : "";
411
+ content.push({
412
+ type: "text",
413
+ text: [
414
+ `## Option ${o.index} — \`${o.optionId}\`${reeldemoTag}`,
415
+ o.label,
416
+ "",
417
+ `[Pick option ${o.index} in browser](${o.browserPickUrl})`,
418
+ o.selectHint,
419
+ ].join("\n"),
420
+ });
421
+ content.push(await svgPreviewBlock(cardSvg));
422
+ }
423
+ }
424
+ else if (data.paletteTokens) {
425
+ content.push({
426
+ type: "text",
427
+ text: "## Current palette (locked — no alternates in snapshot)",
428
+ });
429
+ const light = lockedPaletteSvg(data.paletteTokens, "light");
430
+ const dark = lockedPaletteSvg(data.paletteTokens, "dark");
431
+ if (light)
432
+ content.push(await svgPreviewBlock(light));
433
+ if (dark)
434
+ content.push(await svgPreviewBlock(dark));
435
+ }
436
+ return content;
437
+ }
438
+ export function presentTweetDrafts(data) {
439
+ const drafts = data.drafts ?? [];
440
+ const content = [
441
+ {
442
+ type: "text",
443
+ text: [
444
+ "# Tweet drafts",
445
+ "",
446
+ data.agentInstructions ??
447
+ "Present drafts numbered. Wait for the user before select_tweet_draft.",
448
+ "",
449
+ JSON.stringify(data, null, 2),
450
+ ].join("\n"),
451
+ },
452
+ ];
453
+ drafts.forEach((d, i) => {
454
+ content.push({
455
+ type: "text",
456
+ text: `## Draft ${i + 1} — \`${d.draftId}\`\n\n${d.text}\n\nReply ${i + 1} or select_tweet_draft with draftId "${d.draftId}"`,
457
+ });
458
+ });
459
+ return content;
460
+ }
461
+ export function presentCursorHandoff(data) {
462
+ const h = data.handoff;
463
+ return [
464
+ {
465
+ type: "text",
466
+ text: [
467
+ "# Cursor handoff",
468
+ h ? `**Event:** ${h.event}` : "",
469
+ `**Pending:** ${data.pending}`,
470
+ h?.browserUrl ? `**Browser:** ${h.browserUrl}` : "",
471
+ h?.chatPrompt ? `\n${h.chatPrompt}` : "",
472
+ "",
473
+ JSON.stringify(data, null, 2),
474
+ ]
475
+ .filter(Boolean)
476
+ .join("\n"),
477
+ },
478
+ ];
479
+ }
@@ -0,0 +1,17 @@
1
+ export type McpContentBlock = {
2
+ type: "text";
3
+ text: string;
4
+ } | {
5
+ type: "image";
6
+ data: string;
7
+ mimeType: string;
8
+ annotations?: {
9
+ audience?: Array<"user" | "assistant">;
10
+ priority?: number;
11
+ };
12
+ };
13
+ export type PresentContext = {
14
+ projectId: string;
15
+ publicBaseUrl: string;
16
+ repoHints?: string[];
17
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ export declare const RELEVANT_PROJECTS_CAP = 5;
2
+ export declare const RELEVANT_PROJECTS_FLOOR = 3;
3
+ export type RankableProject = {
4
+ id: string;
5
+ name: string;
6
+ slug?: string;
7
+ description?: string;
8
+ hasBrandData: boolean;
9
+ hasCanvasData: boolean;
10
+ };
11
+ export type RankedProject = RankableProject & {
12
+ relevanceScore: number;
13
+ };
14
+ export type ProjectRelevanceContext = {
15
+ consumerRepoPath?: string;
16
+ requestContext?: string;
17
+ };
18
+ /**
19
+ * Tokenize free-text context (user request, repo folder, etc.) for relevance scoring.
20
+ */
21
+ export declare function tokenizeRelevanceContext(text?: string): string[];
22
+ /** Rank projects by name/description match against repo path and request context. */
23
+ export declare function rankProjectsByRelevance(projects: RankableProject[], context: ProjectRelevanceContext): RankedProject[];
24
+ /**
25
+ * Pick the 3–5 most relevant projects for user selection (or fewer when the user has less).
26
+ */
27
+ export declare function pickRelevantProjects(projects: RankableProject[], context: ProjectRelevanceContext): RankedProject[];
@@ -0,0 +1,95 @@
1
+ import { inferRepoNameHints } from "./project-selection-hints.js";
2
+ export const RELEVANT_PROJECTS_CAP = 5;
3
+ export const RELEVANT_PROJECTS_FLOOR = 3;
4
+ const STOPWORDS = new Set([
5
+ "the",
6
+ "and",
7
+ "for",
8
+ "with",
9
+ "from",
10
+ "this",
11
+ "that",
12
+ "majico",
13
+ "project",
14
+ "brand",
15
+ "using",
16
+ "into",
17
+ ]);
18
+ /**
19
+ * Tokenize free-text context (user request, repo folder, etc.) for relevance scoring.
20
+ */
21
+ export function tokenizeRelevanceContext(text) {
22
+ if (!text?.trim())
23
+ return [];
24
+ const normalized = text
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, " ")
27
+ .trim();
28
+ if (!normalized)
29
+ return [];
30
+ const tokens = normalized
31
+ .split(/\s+/)
32
+ .filter((token) => token.length > 2 && !STOPWORDS.has(token));
33
+ return [...new Set(tokens)];
34
+ }
35
+ function buildContextTokens(context) {
36
+ return [
37
+ ...inferRepoNameHints(context.consumerRepoPath),
38
+ ...tokenizeRelevanceContext(context.requestContext),
39
+ ];
40
+ }
41
+ function scoreProject(project, tokens) {
42
+ if (tokens.length === 0)
43
+ return 0;
44
+ const haystacks = [
45
+ project.name.toLowerCase(),
46
+ project.slug?.toLowerCase() ?? "",
47
+ project.description?.toLowerCase() ?? "",
48
+ ];
49
+ let score = 0;
50
+ for (const token of tokens) {
51
+ for (const [index, haystack] of haystacks.entries()) {
52
+ if (!haystack.includes(token))
53
+ continue;
54
+ if (index === 0)
55
+ score += 10;
56
+ else if (index === 1)
57
+ score += 4;
58
+ else
59
+ score += 6;
60
+ }
61
+ }
62
+ const phrase = tokens.slice(0, 4).join(" ");
63
+ if (phrase.length > 4 && project.name.toLowerCase().includes(phrase)) {
64
+ score += 12;
65
+ }
66
+ return score;
67
+ }
68
+ /** Rank projects by name/description match against repo path and request context. */
69
+ export function rankProjectsByRelevance(projects, context) {
70
+ const tokens = buildContextTokens(context);
71
+ return projects
72
+ .map((project) => ({
73
+ ...project,
74
+ relevanceScore: scoreProject(project, tokens),
75
+ }))
76
+ .sort((a, b) => {
77
+ if (b.relevanceScore !== a.relevanceScore) {
78
+ return b.relevanceScore - a.relevanceScore;
79
+ }
80
+ return a.name.localeCompare(b.name);
81
+ });
82
+ }
83
+ /**
84
+ * Pick the 3–5 most relevant projects for user selection (or fewer when the user has less).
85
+ */
86
+ export function pickRelevantProjects(projects, context) {
87
+ const ranked = rankProjectsByRelevance(projects, context);
88
+ if (ranked.length <= RELEVANT_PROJECTS_CAP)
89
+ return ranked;
90
+ const withSignal = ranked.filter((project) => project.relevanceScore > 0);
91
+ if (withSignal.length >= RELEVANT_PROJECTS_FLOOR) {
92
+ return withSignal.slice(0, RELEVANT_PROJECTS_CAP);
93
+ }
94
+ return ranked.slice(0, RELEVANT_PROJECTS_CAP);
95
+ }
@@ -0,0 +1,16 @@
1
+ /** Repo-name hint helpers for MCP project selection. */
2
+ export type ProjectSummary = {
3
+ id: string;
4
+ name: string;
5
+ slug?: string;
6
+ description?: string;
7
+ hasBrandData: boolean;
8
+ hasCanvasData: boolean;
9
+ };
10
+ /**
11
+ * Derive searchable tokens from a consumer repo path or folder name.
12
+ * e.g. `/path/reeldemo-ableton` → ["reeldemo", "ableton"]
13
+ */
14
+ export declare function inferRepoNameHints(repoPath?: string): string[];
15
+ export declare function projectNameMatchesHints(projectName: string, hints: string[]): boolean;
16
+ export declare function findSuggestedProject(projects: ProjectSummary[], hints: string[]): ProjectSummary | undefined;
@@ -0,0 +1,32 @@
1
+ /** Repo-name hint helpers for MCP project selection. */
2
+ /**
3
+ * Derive searchable tokens from a consumer repo path or folder name.
4
+ * e.g. `/path/reeldemo-ableton` → ["reeldemo", "ableton"]
5
+ */
6
+ export function inferRepoNameHints(repoPath) {
7
+ if (!repoPath?.trim())
8
+ return [];
9
+ const normalized = repoPath
10
+ .replace(/\\/g, "/")
11
+ .split("/")
12
+ .filter(Boolean)
13
+ .pop()
14
+ ?.toLowerCase()
15
+ .replace(/[^a-z0-9]+/g, " ")
16
+ .trim();
17
+ if (!normalized)
18
+ return [];
19
+ const tokens = normalized.split(/\s+/).filter((t) => t.length > 2);
20
+ return [...new Set(tokens)];
21
+ }
22
+ export function projectNameMatchesHints(projectName, hints) {
23
+ if (hints.length === 0)
24
+ return true;
25
+ const normalized = projectName.toLowerCase();
26
+ return hints.some((hint) => normalized.includes(hint));
27
+ }
28
+ export function findSuggestedProject(projects, hints) {
29
+ if (hints.length === 0)
30
+ return undefined;
31
+ return projects.find((project) => projectNameMatchesHints(project.name, hints));
32
+ }