forkfeed-mcp 1.0.12 → 1.0.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.
@@ -205,11 +205,51 @@ Always call **forkfeed_status** before generating content. It lists published fe
205
205
  - **Existing commit**: Warn the user that this commit already has a feed. Only regenerate if they confirm. The old cards will be replaced.
206
206
  - **One commit per run**: Never process multiple commits at once. If the user wants more, they run the command again.
207
207
 
208
+ ### Parallel card generation
209
+
210
+ Generate all 8 cards simultaneously using the Agent tool. This is critical for speed.
211
+
212
+ **Planning phase** (main context, before launching agents):
213
+ 1. Assign images upfront: pick 8 unique covers (img*), 8 unique backgrounds (bg*), and split the remaining scene images into 8 non-overlapping pools for inline CONTENT_IMAGE use per card.
214
+ 2. Pre-generate 8 card UUIDs via \`node -e "..."\`.
215
+ 3. Output the skeleton (fork, feed, image assignments, UUIDs) so it's visible.
216
+
217
+ **Agent prompts** (launch ALL 8 in a single message):
218
+ Each agent generates exactly one card. The agent prompt must include:
219
+ - Card number and section name (e.g., "Card 1: The roast")
220
+ - The section rules from this guide: variant count range, block pattern, tone
221
+ - The full commit diff and stats
222
+ - Assigned cover imageSrc (img*), backgroundSrc (bg*), and the pool of available inline images for this card
223
+ - The card UUID (\`_id\`) and feed ID (\`feedId\`)
224
+ - "Return ONLY the raw JSON card object, no markdown, no explanation"
225
+
226
+ Example agent prompt structure:
227
+ \`\`\`
228
+ Generate Card 2 (Commit message, decoded) for forkfeed.
229
+
230
+ Card JSON structure: { "_id": "{uuid}", "feedId": "{feedId}", "order": 2, "variants": [...] }
231
+
232
+ Rules: 2-4 detail variants. Pattern: SOCIAL -> TITLE -> TEXT -> optional CODE. Escalating absurdity.
233
+ Personas: "What you meant" (source: "x"), "Corporate speak" (source: "linkedin").
234
+
235
+ Cover: { "type": "FULL_IMAGE", "imageSrc": "{assigned-cover}", "title": "...", "subtitle": "..." }
236
+ Background for all CONTENT variants: "{assigned-bg}"
237
+ Available inline images: {pool}
238
+
239
+ Commit diff:
240
+ {diff}
241
+
242
+ Return ONLY the JSON object. No markdown fences, no explanation.
243
+ \`\`\`
244
+
245
+ **Assembly** (main context, after agents complete):
246
+ Collect 8 card JSONs, combine with fork and feed into the manifest, validate against the checklist, and push.
247
+
208
248
  ---
209
249
 
210
250
  ## Phase 4: Push
211
251
 
212
- After generating the manifest JSON, call the **forkfeed_push** tool with it:
252
+ After assembling the manifest, call the **forkfeed_push** tool with it:
213
253
  \`\`\`
214
254
  forkfeed_push({ manifest: { forks: [...], feeds: [...], cards: [...] } })
215
255
  \`\`\`
package/dist/index.js CHANGED
@@ -9,6 +9,86 @@ import { GUIDE_CONTENT } from './guide-content.js';
9
9
  import { IMAGE_CATALOG } from './image-catalog.js';
10
10
  const APP_SERVER_URL = (process.env.APP_SERVER_URL || 'https://api.forkfeed.link').replace(/\/+$/, '');
11
11
  const TOKEN = process.env.FORKFEED_TOKEN || '';
12
+ // ── Manifest schemas ──────────────────────────────────────────────────
13
+ const forkSchema = z.object({
14
+ _id: z.string().min(1, 'Fork _id is required'),
15
+ title: z.string().min(1, 'Fork title is required'),
16
+ description: z.string(),
17
+ imageSrc: z.string(),
18
+ feedIds: z.array(z.string().min(1)).min(1, 'Fork must reference at least one feed'),
19
+ actionLabel: z.string().optional(),
20
+ actionUrl: z.string().optional(),
21
+ }).passthrough();
22
+ const feedSchema = z.object({
23
+ _id: z.string().min(1, 'Feed _id is required'),
24
+ title: z.string().min(1, 'Feed title is required').max(60, 'Feed title max 60 chars'),
25
+ description: z.string().optional(),
26
+ imageSrc: z.string(),
27
+ mode: z.string(),
28
+ scrollDirection: z.string(),
29
+ engagement: z.boolean().optional(),
30
+ }).passthrough();
31
+ const sizingEnum = z.enum(['automatic', 'wide', 'portrait', 'square', 'small_portrait']);
32
+ const socialSourceEnum = z.enum(['x', 'linkedin', 'instagram', 'facebook', 'threads', 'bluesky']);
33
+ const buttonActionEnum = z.enum(['url', 'fork', 'feed', 'user']);
34
+ const quizOptionSchema = z.object({
35
+ label: z.string().min(1),
36
+ correct: z.boolean(),
37
+ }).passthrough();
38
+ const contentBlockSchema = z.discriminatedUnion('type', [
39
+ z.object({ type: z.literal('CONTENT_IMAGE'), imageSrc: z.string().min(1), sizing: sizingEnum }).passthrough(),
40
+ z.object({ type: z.literal('CONTENT_TITLE'), title: z.string().min(1) }).passthrough(),
41
+ z.object({ type: z.literal('CONTENT_TEXT'), text: z.string().min(1) }).passthrough(),
42
+ z.object({ type: z.literal('CONTENT_CODE'), code: z.string().min(1), language: z.string().optional() }).passthrough(),
43
+ z.object({ type: z.literal('CONTENT_SOCIAL'), name: z.string().min(1), avatarSrc: z.string(), source: socialSourceEnum }).passthrough(),
44
+ z.object({ type: z.literal('CONTENT_SUBTEXT'), text: z.string().min(1) }).passthrough(),
45
+ z.object({ type: z.literal('CONTENT_QUIZ'), question: z.string().min(1), options: z.array(quizOptionSchema).min(2), explanation: z.string().optional() }).passthrough(),
46
+ z.object({ type: z.literal('CONTENT_BUTTON'), label: z.string().min(1), action: buttonActionEnum, target: z.string().min(1) }).passthrough(),
47
+ ]);
48
+ const variantSchema = z.discriminatedUnion('type', [
49
+ z.object({ type: z.literal('FULL_IMAGE'), imageSrc: z.string().min(1, 'FULL_IMAGE requires imageSrc'), title: z.string().optional(), subtitle: z.string().optional() }).passthrough(),
50
+ z.object({ type: z.literal('FULL_VIDEO'), videoSrc: z.string().min(1), title: z.string().optional() }).passthrough(),
51
+ z.object({ type: z.literal('CONTENT'), blocks: z.array(contentBlockSchema).min(1, 'CONTENT variant needs at least one block'), backgroundSrc: z.string().optional() }).passthrough(),
52
+ ]);
53
+ const cardSchema = z.object({
54
+ _id: z.string().min(1, 'Card _id is required'),
55
+ feedId: z.string().min(1, 'Card feedId is required'),
56
+ order: z.number().int().min(0),
57
+ variants: z.array(variantSchema).min(2, 'Card needs at least 2 variants (cover + detail)'),
58
+ }).passthrough();
59
+ const manifestSchema = z.object({
60
+ forks: z.array(forkSchema).min(1, 'Manifest must have at least one fork'),
61
+ feeds: z.array(feedSchema).min(1, 'Manifest must have at least one feed'),
62
+ cards: z.array(cardSchema).min(1, 'Manifest must have at least one card'),
63
+ });
64
+ /** Cross-reference checks that Zod can't express. Returns error string or null. */
65
+ function crossValidate(manifest) {
66
+ const feedIds = new Set(manifest.feeds.map((f) => f._id));
67
+ const errors = [];
68
+ for (const fork of manifest.forks) {
69
+ for (const fid of fork.feedIds) {
70
+ if (!feedIds.has(fid))
71
+ errors.push(`Fork "${fork._id}" references feed "${fid}" which is not in feeds array`);
72
+ }
73
+ }
74
+ for (const card of manifest.cards) {
75
+ if (!feedIds.has(card.feedId))
76
+ errors.push(`Card "${card._id}" references feed "${card.feedId}" which is not in feeds array`);
77
+ for (let vi = 0; vi < card.variants.length; vi++) {
78
+ const v = card.variants[vi];
79
+ if (v.type !== 'CONTENT')
80
+ continue;
81
+ for (const block of v.blocks) {
82
+ if (block.type === 'CONTENT_QUIZ') {
83
+ const hasCorrect = block.options.some((o) => o.correct === true);
84
+ if (!hasCorrect)
85
+ errors.push(`Card "${card._id}" variants[${vi}]: CONTENT_QUIZ must have at least one correct option`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return errors.length > 0 ? errors.join('\n') : null;
91
+ }
12
92
  // ── Helpers ────────────────────────────────────────────────────────────
13
93
  function authHeaders() {
14
94
  return {
@@ -67,6 +147,24 @@ server.tool('forkfeed_push', 'Push a generated manifest (forks, feeds, cards) to
67
147
  isError: true,
68
148
  };
69
149
  }
150
+ // Validate manifest structure before pushing
151
+ const parsed = manifestSchema.safeParse(manifest);
152
+ if (!parsed.success) {
153
+ const issues = parsed.error.issues
154
+ .map((i) => ` ${i.path.join('.')}: ${i.message}`)
155
+ .join('\n');
156
+ return {
157
+ content: [{ type: 'text', text: `Manifest validation failed:\n${issues}\n\nFix the issues above and try again.` }],
158
+ isError: true,
159
+ };
160
+ }
161
+ const crossErr = crossValidate(parsed.data);
162
+ if (crossErr) {
163
+ return {
164
+ content: [{ type: 'text', text: `Manifest cross-reference errors:\n${crossErr}\n\nFix the issues above and try again.` }],
165
+ isError: true,
166
+ };
167
+ }
70
168
  // Save manifest locally before uploading (best-effort, doesn't block push)
71
169
  const forkId = manifest.forks?.[0]?._id;
72
170
  const filename = `${forkId || `manifest-${Date.now()}`}.json`;
@@ -229,17 +327,25 @@ server.prompt('forkfeed', 'Turn GitHub commits into swipeable forkfeed content.
229
327
  type: 'text',
230
328
  text: `Turn the commits in this repo into forkfeed content. Follow these steps exactly:
231
329
 
232
- 1. Call **forkfeed_guide** to get the full content generation guide. Read it carefully.
233
- 2. Call **forkfeed_images** to get the IT Scenes image catalog (200 scenes + 30 backgrounds).
234
- 3. Call **forkfeed_status** to check for existing content. Note which feeds (commits) are already published.
235
- 4. Detect the current repo from the working directory. Use git to get commit data.
236
- 5. Ask which ONE commit to process (default: latest). Only one commit at a time, never more. Do NOT ask about image style (always use IT Scenes). Show a table of recent commits with a column indicating whether each already has a published feed (match the 7-char SHA from forkfeed_status feed IDs against the commit SHAs). If the user selects a commit that already has a feed, warn and ask for confirmation before regenerating.
237
- 6. Fetch the diff, analyze it, and generate the 8-card manifest following the guide exactly. Include ALL existing feed IDs in the fork's feedIds array (see "Incremental updates" in the guide).
238
- 7. Match images from the catalog to card content by tags and semantic similarity.
239
- 8. Validate the manifest against the checklist in the guide.
240
- 9. Call **forkfeed_push** with the complete manifest to publish it.
330
+ 1. Call **forkfeed_guide** and **forkfeed_images** in parallel to load the content guide and image catalog.
331
+ 2. Call **forkfeed_status** to check for existing content.
332
+ 3. Detect the repo from the working directory. Show recent commits in a table with a column indicating whether each already has a published feed (match 7-char SHA from status feed IDs). Ask which ONE commit to process (default: latest). One commit at a time, never more. Do NOT ask about image style.
333
+ 4. Fetch the commit diff and stats via git.
334
+ 5. Plan the manifest skeleton (output this before generating cards):
335
+ - Fork: ID, title, description, imageSrc
336
+ - Feed: ID, title, description, imageSrc, mode: "sequential", scrollDirection: "vertical", engagement: true
337
+ - Assign 8 unique cover images (img*) and 8 unique backgrounds (bg*) to cards 0-7
338
+ - Pre-generate 8 card UUIDs
339
+ - Split remaining scene images into 8 non-overlapping pools for inline CONTENT_IMAGE use
340
+ - Include ALL existing feed IDs in the fork's feedIds array (see "Incremental updates" in guide)
341
+ 6. Generate all 8 cards IN PARALLEL using the Agent tool. Launch ALL 8 agents in a SINGLE message.
342
+ Each agent prompt must include: the card number, section type and rules (from the guide), the full commit diff, assigned cover/background/inline images, the card UUID, and the feed ID.
343
+ Each agent must return ONLY the raw card JSON object: { "_id": "...", "feedId": "...", "order": N, "variants": [...] }
344
+ See "Parallel card generation" in the guide for details.
345
+ 7. Assemble the full manifest from the skeleton + 8 card JSON results. Validate against the checklist.
346
+ 8. Call **forkfeed_push** with the complete manifest.
241
347
 
242
- Start now. Detect the repo and check for existing content.`,
348
+ Start now. Load the guide and images in parallel, then detect the repo.`,
243
349
  },
244
350
  },
245
351
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forkfeed-mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "MCP server for pushing GitHub commits to forkfeed",
5
5
  "type": "module",
6
6
  "bin": {