forkfeed-mcp 1.3.2 → 1.3.4

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.
@@ -79,18 +79,18 @@ Only 5 fields + cards. Everything else is auto-populated.
79
79
 
80
80
  ## The 8 cards
81
81
 
82
- Each card = array of detail variants (cover is auto-generated). Provide 1+ variants per card.
82
+ Each card = array of detail variants (cover is auto-generated). Provide 1+ variants per card. Quality over quantity: pick only the most impactful, funny, or educational content.
83
83
 
84
84
  | # | Section | Detail variants | Block pattern | Tone |
85
85
  |---|---------|----------------|---------------|------|
86
- | 0 | Explain like I'm 5 | 3-6 | img -> title -> text | Playful, zero jargon |
87
- | 1 | The roast | 3-6 | img -> title -> text, optional subtext | Maximum cheekiness, swearing ok |
88
- | 2 | Commit message, decoded | 2-4 | social -> title -> text -> optional code | Escalating absurdity |
89
- | 3 | The LinkedIn post | 2-4 | social -> title -> text -> subtext | Peak LinkedIn parody |
90
- | 4 | Statistics | 3-5 | img -> title -> code | Deadpan data humor |
91
- | 5 | Learning moment | 3-8 | img -> title -> text -> optional code -> button(last) | Teach with personality |
92
- | 6 | Alternatives | 3-6 | img -> title -> text -> optional code | Constructive snark |
93
- | 7 | Quiz | 10-15 | title -> quiz (no img blocks) | Quiz show energy |
86
+ | 0 | Explain like I'm 5 | 2-3 | img -> title -> text | Playful, zero jargon |
87
+ | 1 | The roast | 2-3 | img -> title -> text, optional subtext | Maximum cheekiness, swearing ok |
88
+ | 2 | Commit message, decoded | 2-3 | social -> title -> text -> optional code | Escalating absurdity |
89
+ | 3 | The LinkedIn post | 2-3 | social -> title -> text -> subtext | Peak LinkedIn parody |
90
+ | 4 | Statistics | 2-3 | img -> title -> code | Deadpan data humor |
91
+ | 5 | Learning moment | 2-3 | img -> title -> text -> optional code -> button(last) | Teach with personality |
92
+ | 6 | Alternatives | 2-3 | img -> title -> text -> optional code | Constructive snark |
93
+ | 7 | Quiz | 4-6 | title -> quiz (no img blocks) | Quiz show energy |
94
94
 
95
95
  ### Card 2 personas (Decoded)
96
96
  - "What you meant" (source: "x" or "threads")
@@ -108,7 +108,7 @@ Each card = array of detail variants (cover is auto-generated). Provide 1+ varia
108
108
 
109
109
  - Exactly 8 cards in the cards array (one per section above)
110
110
  - Use short image IDs from forkfeed_commits output (img47, not full URLs)
111
- - 12-20 unique scene images (img*) across the feed, no duplicate within same card
111
+ - 8-12 unique scene images (img*) across the feed, no duplicate within same card
112
112
  - Cards 0-5: code must be REAL from the diff (never fabricated)
113
113
  - Card 6: synthesized code IS allowed
114
114
  - Card 7: no img blocks, quiz blocks only (with title per variant)
package/dist/index.js CHANGED
@@ -63,13 +63,14 @@ const manifestSchema = z.object({
63
63
  feeds: z.array(feedSchema).min(1, 'Manifest must have at least one feed'),
64
64
  cards: z.array(cardSchema).min(1, 'Manifest must have at least one card'),
65
65
  });
66
- /** Cross-reference checks that Zod can't express. Returns error string or null. */
67
- function crossValidate(manifest) {
66
+ /** Cross-reference checks that Zod can't express. Returns error string or null.
67
+ * knownFeedIds: feed IDs that exist server-side but aren't in this manifest (skip fork ref check for these). */
68
+ function crossValidate(manifest, knownFeedIds) {
68
69
  const feedIds = new Set(manifest.feeds.map((f) => f._id));
69
70
  const errors = [];
70
71
  for (const fork of manifest.forks) {
71
72
  for (const fid of fork.feedIds) {
72
- if (!feedIds.has(fid))
73
+ if (!feedIds.has(fid) && !knownFeedIds?.has(fid))
73
74
  errors.push(`Fork "${fork._id}" references feed "${fid}" which is not in feeds array`);
74
75
  }
75
76
  }
@@ -437,7 +438,7 @@ async function buildManifest(input) {
437
438
  variants: [coverVariant, ...detailVariants],
438
439
  };
439
440
  });
440
- return { forks: [fork], feeds: [feed], cards };
441
+ return { manifest: { forks: [fork], feeds: [feed], cards }, existingFeedIds };
441
442
  }
442
443
  async function pushManifestToServer(manifest) {
443
444
  if (!TOKEN) {
@@ -481,31 +482,32 @@ async function pushManifestToServer(manifest) {
481
482
  const url = `https://forkfeed.link/fork/${forkId}`;
482
483
  try {
483
484
  const qr = await QRCode.toString(url, { type: 'utf8', errorCorrectionLevel: 'L' });
484
- qrBlock = ['', 'Scan to open in forkfeed:', '', qr, url].join('\n');
485
+ qrBlock = ['', 'Scan to open in forkfeed:', '', qr].join('\n');
485
486
  }
486
487
  catch {
487
- qrBlock = `\nLink: ${url}`;
488
+ qrBlock = '';
488
489
  }
489
490
  }
490
- return {
491
- content: [{
492
- type: 'text',
493
- text: [
494
- 'Content pushed successfully!',
495
- '',
496
- `Uploaded: ${data.uploaded?.feeds || 0} feeds, ${data.uploaded?.cards || 0} cards`,
497
- '',
498
- 'Forks:',
499
- forkSummary || ' (none)',
500
- '',
501
- saveError || `Manifest saved to ${filepath}`,
502
- '',
503
- 'Your content is now live in the forkfeed app. It starts as private.',
504
- 'To make it public, change visibility in the app (requires admin approval).',
505
- qrBlock,
506
- ].join('\n'),
507
- }],
508
- };
491
+ const blocks = [{
492
+ type: 'text',
493
+ text: [
494
+ 'Content pushed successfully!',
495
+ '',
496
+ `Uploaded: ${data.uploaded?.feeds || 0} feeds, ${data.uploaded?.cards || 0} cards`,
497
+ '',
498
+ 'Forks:',
499
+ forkSummary || ' (none)',
500
+ '',
501
+ saveError || `Manifest saved to ${filepath}`,
502
+ '',
503
+ 'Your content is now live in the forkfeed app. It starts as private.',
504
+ 'To make it public, change visibility in the app (requires admin approval).',
505
+ ].join('\n'),
506
+ }];
507
+ if (qrBlock) {
508
+ blocks.push({ type: 'text', text: qrBlock });
509
+ }
510
+ return { content: blocks };
509
511
  }
510
512
  catch (err) {
511
513
  return {
@@ -652,8 +654,9 @@ server.tool('forkfeed_build', 'Build a forkfeed manifest from simplified content
652
654
  const shouldPush = push !== false;
653
655
  // Build the full manifest from simplified input
654
656
  let manifest;
657
+ let existingFeedIds;
655
658
  try {
656
- manifest = await buildManifest(content);
659
+ ({ manifest, existingFeedIds } = await buildManifest(content));
657
660
  }
658
661
  catch (err) {
659
662
  return {
@@ -672,7 +675,7 @@ server.tool('forkfeed_build', 'Build a forkfeed manifest from simplified content
672
675
  isError: true,
673
676
  };
674
677
  }
675
- const crossErr = crossValidate(parsed.data);
678
+ const crossErr = crossValidate(parsed.data, new Set(existingFeedIds));
676
679
  if (crossErr) {
677
680
  return {
678
681
  content: [{ type: 'text', text: `Built manifest cross-reference errors:\n${crossErr}` }],
@@ -754,6 +757,7 @@ server.prompt('forkfeed', 'Turn GitHub commits into swipeable forkfeed content.
754
757
  3. Call **forkfeed_commits** with the selected commit SHA. This returns the diff, file stats, and suggested scene images. Do NOT run git commands yourself.
755
758
  4. Generate the simplified content JSON: sha, feedTitle, feedDescription, forkTitle, forkDescription, and 8 cards with blocks. Use short image IDs (img47) for inline images. Do NOT provide owner, repo, backgrounds, existingFeedIds, UUIDs, covers, or type wrappers. The builder auto-detects and generates all of that.
756
759
  5. Call **forkfeed_build** with the simplified content (push defaults to true).
760
+ 6. Display the QR code block from the push result exactly as returned, so the user can scan it.
757
761
 
758
762
  Start now.`,
759
763
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forkfeed-mcp",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "MCP server for pushing GitHub commits to forkfeed",
5
5
  "type": "module",
6
6
  "bin": {