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.
- package/dist/guide-content.js +41 -1
- package/dist/index.js +116 -10
- package/package.json +1 -1
package/dist/guide-content.js
CHANGED
|
@@ -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
|
|
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
|
|
233
|
-
2. Call **
|
|
234
|
-
3.
|
|
235
|
-
4.
|
|
236
|
-
5.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
8
|
|
240
|
-
|
|
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.
|
|
348
|
+
Start now. Load the guide and images in parallel, then detect the repo.`,
|
|
243
349
|
},
|
|
244
350
|
},
|
|
245
351
|
],
|