forkfeed-mcp 1.2.0 → 1.3.1
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 +21 -37
- package/dist/index.js +106 -64
- package/package.json +1 -1
package/dist/guide-content.js
CHANGED
|
@@ -14,7 +14,7 @@ Turn GitHub commits into swipeable card content. One fork per repo, one feed per
|
|
|
14
14
|
|
|
15
15
|
1. Call **forkfeed_guide** and **forkfeed_commits()** in parallel
|
|
16
16
|
2. Show commits table, ask which ONE to process
|
|
17
|
-
3. Call **forkfeed_commits(sha)** to get diff, stats, and
|
|
17
|
+
3. Call **forkfeed_commits(sha)** to get diff, stats, and suggested scene images
|
|
18
18
|
4. Generate simplified content JSON (see format below)
|
|
19
19
|
5. Call **forkfeed_build** with the content (push defaults to true)
|
|
20
20
|
|
|
@@ -22,26 +22,19 @@ Turn GitHub commits into swipeable card content. One fork per repo, one feed per
|
|
|
22
22
|
|
|
23
23
|
## Simplified content format (for forkfeed_build)
|
|
24
24
|
|
|
25
|
-
The builder
|
|
25
|
+
The builder auto-detects repo info (owner, repo, GitHub URL), auto-assigns card backgrounds, and fetches existing feed IDs from the server. You only provide creative content.
|
|
26
26
|
|
|
27
27
|
\`\`\`json
|
|
28
28
|
{
|
|
29
|
-
"
|
|
30
|
-
"repo": "repo-name",
|
|
31
|
-
"sha": "7char",
|
|
29
|
+
"sha": "7char-sha",
|
|
32
30
|
"feedTitle": "Max 60 chars headline",
|
|
33
31
|
"feedDescription": "Mon DD: one-line impact summary",
|
|
34
32
|
"forkTitle": "Project Name",
|
|
35
33
|
"forkDescription": "What the project IS",
|
|
36
|
-
"actionUrl": "https://github.com/owner/repo",
|
|
37
|
-
"existingFeedIds": [],
|
|
38
|
-
"images": {
|
|
39
|
-
"bg": ["bg10", "bg11", "bg20", "bg25", "bg9", "bg12", "bg30", "bg18"]
|
|
40
|
-
},
|
|
41
34
|
"cards": [
|
|
42
35
|
{
|
|
43
36
|
"variants": [
|
|
44
|
-
{ "blocks": [{ "img": "img47"
|
|
37
|
+
{ "blocks": [{ "img": "img47" }, { "title": "Section heading" }, { "text": "Paragraph content..." }] },
|
|
45
38
|
{ "blocks": [{ "title": "Another section" }, { "text": "More content..." }] }
|
|
46
39
|
]
|
|
47
40
|
}
|
|
@@ -49,6 +42,8 @@ The builder handles UUIDs, full image URLs, cover variants, type wrappers, feedI
|
|
|
49
42
|
}
|
|
50
43
|
\`\`\`
|
|
51
44
|
|
|
45
|
+
Only 5 fields + cards. Everything else is auto-populated.
|
|
46
|
+
|
|
52
47
|
### Block types (inferred from fields)
|
|
53
48
|
|
|
54
49
|
| Write this | Becomes | Notes |
|
|
@@ -59,19 +54,27 @@ The builder handles UUIDs, full image URLs, cover variants, type wrappers, feedI
|
|
|
59
54
|
| \`{ "text": "Paragraph..." }\` | CONTENT_TEXT | 50-200 words, use \\\\n\\\\n for breaks |
|
|
60
55
|
| \`{ "code": "const x = 1", "lang": "typescript" }\` | CONTENT_CODE | Real code from diff, 5-20 lines, strip +/- prefixes |
|
|
61
56
|
| \`{ "subtext": "Pro tip..." }\` | CONTENT_SUBTEXT | Aside or protip |
|
|
62
|
-
| \`{ "name": "Chad", "subtitle": "10x Engineer", "avatar": "img98", "source": "linkedin" }\` | CONTENT_SOCIAL | Cards 2-3 only. source: x, linkedin, threads, etc. |
|
|
57
|
+
| \`{ "name": "Chad", "subtitle": "10x Engineer", "avatar": "img98", "source": "linkedin" }\` | CONTENT_SOCIAL | Cards 2-3 only. avatar can be image ID or URL. source: x, linkedin, threads, etc. |
|
|
63
58
|
| \`{ "question": "What does X do?", "options": [{"label": "A", "correct": false}, {"label": "B", "correct": true}], "explanation": "Because..." }\` | CONTENT_QUIZ | Card 7 only. 4 options recommended, min 2, one correct, always include explanation |
|
|
64
59
|
| \`{ "label": "Google it", "action": "url", "target": "https://google.com/search?q=topic" }\` | CONTENT_BUTTON | Card 5 only, MUST be last block in every detail variant |
|
|
65
60
|
|
|
66
61
|
### What the builder does automatically
|
|
62
|
+
- Detects repo owner/name from git remote
|
|
63
|
+
- Generates fork/feed IDs from convention (tfip-{owner}-{repo}-{sha})
|
|
64
|
+
- Constructs GitHub action URL
|
|
65
|
+
- Fetches existing feed IDs from server (for incremental updates)
|
|
66
|
+
- Assigns 8 unique card backgrounds from preference table + commit tags
|
|
67
67
|
- Generates UUID v4 for each card ID
|
|
68
68
|
- Resolves short image IDs (img47, bg10) to full CDN URLs
|
|
69
69
|
- Adds FULL_IMAGE cover variant with fixed title/subtitle per card type
|
|
70
|
-
- Sets backgroundSrc
|
|
71
|
-
- Sets feedId and order on each card
|
|
72
|
-
- Builds fork and feed objects with correct IDs and metadata
|
|
70
|
+
- Sets backgroundSrc, feedId, and order on each card
|
|
73
71
|
- Validates the assembled manifest before pushing
|
|
74
72
|
|
|
73
|
+
### Optional overrides
|
|
74
|
+
- \`bgOverride\`: array of 8 background IDs to override auto-assignment
|
|
75
|
+
- \`coverOverride\`: array of 8 cover image IDs (defaults to backgrounds)
|
|
76
|
+
- \`cwd\`: working directory if not process.cwd()
|
|
77
|
+
|
|
75
78
|
---
|
|
76
79
|
|
|
77
80
|
## The 8 cards
|
|
@@ -104,7 +107,7 @@ Each card = array of detail variants (cover is auto-generated). Provide 1+ varia
|
|
|
104
107
|
## Key rules
|
|
105
108
|
|
|
106
109
|
- Exactly 8 cards in the cards array (one per section above)
|
|
107
|
-
-
|
|
110
|
+
- Use short image IDs from forkfeed_commits output (img47, not full URLs)
|
|
108
111
|
- 12-20 unique scene images (img*) across the feed, no duplicate within same card
|
|
109
112
|
- Cards 0-5: code must be REAL from the diff (never fabricated)
|
|
110
113
|
- Card 6: synthesized code IS allowed
|
|
@@ -115,32 +118,13 @@ Each card = array of detail variants (cover is auto-generated). Provide 1+ varia
|
|
|
115
118
|
- No em dashes, smart quotes, or non-ASCII characters
|
|
116
119
|
- Feed title max 60 chars
|
|
117
120
|
|
|
118
|
-
### Images
|
|
119
|
-
|
|
120
|
-
Images are pre-filtered by **forkfeed_commits(sha)**. Pick from the suggested list.
|
|
121
|
-
|
|
122
|
-
**BG preferences:** ELI5: bg10/bg27 | Roast: bg11/bg1 | Decoded: bg20/bg11 | LinkedIn: bg25/bg24 | Stats: bg9/bg3 | Learning: match tech | Alts: bg25/bg30 | Quiz: bg18/bg5
|
|
123
|
-
|
|
124
121
|
### Tone
|
|
125
122
|
Casual, cheeky, technically accurate. Use "you," contractions, short paragraphs, occasional swearing. Humor is the default.
|
|
126
123
|
|
|
127
124
|
### Incremental updates
|
|
128
|
-
**forkfeed_commits()** (list mode) shows which commits have published feeds
|
|
125
|
+
**forkfeed_commits()** (list mode) shows which commits have published feeds. The builder auto-fetches existing feed IDs, so you don't need to track them.
|
|
129
126
|
|
|
130
|
-
- **New commit**:
|
|
127
|
+
- **New commit**: Just generate content. The builder handles feed ID preservation.
|
|
131
128
|
- **Existing commit**: Warn the user. Only regenerate if they confirm.
|
|
132
129
|
- **One commit per run**: Never process multiple commits at once.
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## Advanced: direct push with full manifest (forkfeed_push)
|
|
137
|
-
|
|
138
|
-
If you need full control, you can build the manifest manually and use forkfeed_push directly. The full manifest requires:
|
|
139
|
-
- Fork/feed objects with all fields (mode, scrollDirection, engagement, etc.)
|
|
140
|
-
- FULL_IMAGE cover variant[0] per card with fixed titles/subtitles
|
|
141
|
-
- CONTENT variants with type field, full image URLs, backgroundSrc
|
|
142
|
-
- UUID v4 card IDs, feedId and order on each card
|
|
143
|
-
|
|
144
|
-
ID conventions: Fork: \`tfip-{owner}-{repo}\`, Feed: \`tfip-{owner}-{repo}-{7char-sha}\`, Card: UUID v4
|
|
145
|
-
All IDs must match \`/^[a-z0-9-]+$/\` (except card UUIDs).
|
|
146
130
|
`.trim();
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import QRCode from 'qrcode';
|
|
10
10
|
import { GUIDE_CONTENT } from './guide-content.js';
|
|
11
|
-
import { IMAGE_CATALOG,
|
|
11
|
+
import { IMAGE_CATALOG, resolveImageId } from './image-catalog.js';
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
const APP_SERVER_URL = (process.env.APP_SERVER_URL || 'https://api.forkfeed.link').replace(/\/+$/, '');
|
|
14
14
|
const TOKEN = process.env.FORKFEED_TOKEN || '';
|
|
@@ -136,11 +136,12 @@ async function getRepoInfo(cwd) {
|
|
|
136
136
|
catch { /* no remote */ }
|
|
137
137
|
return { owner: null, repo: basename(cwd), isLocal: true };
|
|
138
138
|
}
|
|
139
|
+
const SEP = '\x1e'; // ASCII record separator (safe in commit messages)
|
|
139
140
|
async function getCommitList(cwd, count = 15) {
|
|
140
|
-
const fmt =
|
|
141
|
+
const fmt = `%H${SEP}%h${SEP}%s${SEP}%an${SEP}%aI`;
|
|
141
142
|
const raw = await gitExec(['log', '--no-merges', `-${count}`, `--format=${fmt}`], cwd);
|
|
142
143
|
return raw.trim().split('\n').filter(Boolean).map((line) => {
|
|
143
|
-
const [sha, shortSha, message, author, date] = line.split(
|
|
144
|
+
const [sha, shortSha, message, author, date] = line.split(SEP);
|
|
144
145
|
return { sha, shortSha, message, author, date: date?.slice(0, 10) || '' };
|
|
145
146
|
});
|
|
146
147
|
}
|
|
@@ -159,11 +160,11 @@ function truncateDiff(diff, maxLinesPerFile = 200) {
|
|
|
159
160
|
}
|
|
160
161
|
async function getCommitDetail(cwd, sha) {
|
|
161
162
|
const [meta, stats, diff] = await Promise.all([
|
|
162
|
-
gitExec(['show', sha,
|
|
163
|
+
gitExec(['show', sha, `--format=%H${SEP}%h${SEP}%s${SEP}%an${SEP}%aI`, '--no-patch'], cwd),
|
|
163
164
|
gitExec(['diff', `${sha}^..${sha}`, '--stat'], cwd),
|
|
164
165
|
gitExec(['diff', `${sha}^..${sha}`, '-U3'], cwd),
|
|
165
166
|
]);
|
|
166
|
-
const [fullSha, shortSha, message, author, date] = meta.trim().split(
|
|
167
|
+
const [fullSha, shortSha, message, author, date] = meta.trim().split(SEP);
|
|
167
168
|
return {
|
|
168
169
|
sha: fullSha,
|
|
169
170
|
shortSha,
|
|
@@ -209,6 +210,48 @@ function detectTags(message, filePaths) {
|
|
|
209
210
|
tags.add('general');
|
|
210
211
|
return [...tags];
|
|
211
212
|
}
|
|
213
|
+
function parseFilePathsFromStats(stats) {
|
|
214
|
+
const matches = stats.match(/^\s*(.+?)\s+\|/gm) || [];
|
|
215
|
+
return matches.map((m) => m.trim().replace(/\s+\|$/, ''));
|
|
216
|
+
}
|
|
217
|
+
const BG_PREFS = [
|
|
218
|
+
['bg10', 'bg27'], // 0: ELI5
|
|
219
|
+
['bg11', 'bg1'], // 1: Roast
|
|
220
|
+
['bg20', 'bg11'], // 2: Decoded
|
|
221
|
+
['bg25', 'bg24'], // 3: LinkedIn
|
|
222
|
+
['bg9', 'bg3'], // 4: Stats
|
|
223
|
+
['bg12', 'bg13', 'bg14', 'bg15', 'bg17'], // 5: Learning (tech-match)
|
|
224
|
+
['bg25', 'bg30'], // 6: Alternatives
|
|
225
|
+
['bg18', 'bg5'], // 7: Quiz
|
|
226
|
+
];
|
|
227
|
+
// Map language tags to preferred Learning card (index 5) backgrounds
|
|
228
|
+
const LANG_BG = {
|
|
229
|
+
javascript: 'bg12', typescript: 'bg12',
|
|
230
|
+
python: 'bg13',
|
|
231
|
+
rust: 'bg14',
|
|
232
|
+
deploy: 'bg15',
|
|
233
|
+
// bg17 = Frontend (fallback for language tag without specific match)
|
|
234
|
+
};
|
|
235
|
+
function assignBackgrounds(tags) {
|
|
236
|
+
const used = new Set();
|
|
237
|
+
// For card 5 (Learning), reorder prefs based on detected language tags
|
|
238
|
+
const learningPrefs = [...BG_PREFS[5]];
|
|
239
|
+
for (const tag of tags) {
|
|
240
|
+
const preferred = LANG_BG[tag];
|
|
241
|
+
if (preferred && learningPrefs.includes(preferred)) {
|
|
242
|
+
// Move matched bg to front
|
|
243
|
+
learningPrefs.splice(learningPrefs.indexOf(preferred), 1);
|
|
244
|
+
learningPrefs.unshift(preferred);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return BG_PREFS.map((prefs, i) => {
|
|
249
|
+
const candidates = i === 5 ? learningPrefs : prefs;
|
|
250
|
+
const pick = candidates.find((bg) => !used.has(bg)) || candidates[0];
|
|
251
|
+
used.add(pick);
|
|
252
|
+
return pick;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
212
255
|
function filterImagesByTags(tags) {
|
|
213
256
|
const tagSet = new Set(tags);
|
|
214
257
|
function score(entry) {
|
|
@@ -219,13 +262,13 @@ function filterImagesByTags(tags) {
|
|
|
219
262
|
.map((i) => ({ ...i, score: score(i) }))
|
|
220
263
|
.sort((a, b) => b.score - a.score)
|
|
221
264
|
.slice(0, 30)
|
|
222
|
-
.map((i) => `${i.id} | ${i.name} | ${i.tags}
|
|
265
|
+
.map((i) => `${i.id} | ${i.name} | ${i.tags}`);
|
|
223
266
|
const backgrounds = IMAGE_CATALOG
|
|
224
267
|
.filter((i) => i.id.startsWith('bg'))
|
|
225
268
|
.map((i) => ({ ...i, score: score(i) }))
|
|
226
269
|
.sort((a, b) => b.score - a.score)
|
|
227
270
|
.slice(0, 10)
|
|
228
|
-
.map((i) => `${i.id} | ${i.name} | ${i.tags}
|
|
271
|
+
.map((i) => `${i.id} | ${i.name} | ${i.tags}`);
|
|
229
272
|
return { scenes, backgrounds };
|
|
230
273
|
}
|
|
231
274
|
async function fetchStatusData() {
|
|
@@ -260,10 +303,11 @@ function inferBlock(block) {
|
|
|
260
303
|
};
|
|
261
304
|
}
|
|
262
305
|
if ('name' in block && 'avatar' in block) {
|
|
306
|
+
const avatar = block.avatar;
|
|
263
307
|
return {
|
|
264
308
|
type: 'CONTENT_SOCIAL',
|
|
265
309
|
name: block.name,
|
|
266
|
-
avatarSrc: resolveImageId(
|
|
310
|
+
avatarSrc: avatar.startsWith('http') ? avatar : resolveImageId(avatar),
|
|
267
311
|
source: block.source,
|
|
268
312
|
...(block.subtitle != null ? { subtitle: block.subtitle } : {}),
|
|
269
313
|
};
|
|
@@ -277,9 +321,10 @@ function inferBlock(block) {
|
|
|
277
321
|
};
|
|
278
322
|
}
|
|
279
323
|
if ('img' in block) {
|
|
324
|
+
const img = block.img;
|
|
280
325
|
return {
|
|
281
326
|
type: 'CONTENT_IMAGE',
|
|
282
|
-
imageSrc: resolveImageId(
|
|
327
|
+
imageSrc: img.startsWith('http') ? img : resolveImageId(img),
|
|
283
328
|
sizing: (block.sizing || 'wide'),
|
|
284
329
|
};
|
|
285
330
|
}
|
|
@@ -303,41 +348,66 @@ function inferBlock(block) {
|
|
|
303
348
|
}
|
|
304
349
|
// ── Build input schema ───────────────────────────────────────────────
|
|
305
350
|
const buildInputSchema = z.object({
|
|
306
|
-
owner: z.string().min(1),
|
|
307
|
-
repo: z.string().min(1),
|
|
308
351
|
sha: z.string().min(1),
|
|
309
352
|
feedTitle: z.string().min(1).max(60),
|
|
310
353
|
feedDescription: z.string().min(1),
|
|
311
354
|
forkTitle: z.string().min(1),
|
|
312
355
|
forkDescription: z.string().min(1),
|
|
313
|
-
actionUrl: z.string().optional(),
|
|
314
|
-
existingFeedIds: z.array(z.string()).optional(),
|
|
315
|
-
images: z.object({
|
|
316
|
-
bg: z.array(z.string()).length(8),
|
|
317
|
-
cover: z.array(z.string()).length(8).optional(),
|
|
318
|
-
}),
|
|
319
356
|
cards: z.array(z.object({
|
|
320
357
|
variants: z.array(z.object({
|
|
321
358
|
blocks: z.array(z.record(z.string(), z.unknown())).min(1),
|
|
322
359
|
})).min(1),
|
|
323
360
|
})).length(8),
|
|
361
|
+
cwd: z.string().optional().describe('Working directory (defaults to process.cwd())'),
|
|
362
|
+
bgOverride: z.array(z.string()).length(8).optional().describe('Override auto-assigned background IDs'),
|
|
363
|
+
coverOverride: z.array(z.string()).length(8).optional().describe('Override cover image IDs (defaults to backgrounds)'),
|
|
324
364
|
});
|
|
325
365
|
// ── Manifest builder ─────────────────────────────────────────────────
|
|
326
|
-
function buildManifest(input) {
|
|
327
|
-
const
|
|
328
|
-
|
|
366
|
+
async function buildManifest(input) {
|
|
367
|
+
const cwd = input.cwd || process.cwd();
|
|
368
|
+
// Auto-detect repo info
|
|
369
|
+
const repoInfo = await getRepoInfo(cwd);
|
|
370
|
+
const owner = repoInfo.owner || 'local';
|
|
371
|
+
const repo = repoInfo.repo;
|
|
372
|
+
const sha7 = input.sha.slice(0, 7);
|
|
373
|
+
const forkId = `tfip-${owner}-${repo}`;
|
|
374
|
+
const feedId = `tfip-${owner}-${repo}-${sha7}`;
|
|
375
|
+
// Auto-assign backgrounds (or use override)
|
|
376
|
+
let bgs;
|
|
377
|
+
if (input.bgOverride) {
|
|
378
|
+
bgs = input.bgOverride;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const meta = await gitExec(['show', input.sha, '--format=%s', '--no-patch'], cwd);
|
|
382
|
+
const statOutput = await gitExec(['diff', `${input.sha}^..${input.sha}`, '--stat'], cwd);
|
|
383
|
+
const filePaths = parseFilePathsFromStats(statOutput);
|
|
384
|
+
const tags = detectTags(meta.trim(), filePaths);
|
|
385
|
+
bgs = assignBackgrounds(tags);
|
|
386
|
+
}
|
|
387
|
+
// Auto-fetch existing feed IDs (best-effort)
|
|
388
|
+
let existingFeedIds = [];
|
|
389
|
+
if (TOKEN) {
|
|
390
|
+
try {
|
|
391
|
+
const status = await fetchStatusData();
|
|
392
|
+
existingFeedIds = (status.feeds || []).map((f) => f.externalFeedId);
|
|
393
|
+
}
|
|
394
|
+
catch { /* best-effort */ }
|
|
395
|
+
}
|
|
329
396
|
// Resolve image IDs
|
|
330
|
-
const bgUrls =
|
|
331
|
-
const coverUrls = input.
|
|
332
|
-
? input.
|
|
397
|
+
const bgUrls = bgs.map(resolveImageId);
|
|
398
|
+
const coverUrls = input.coverOverride
|
|
399
|
+
? input.coverOverride.map(resolveImageId)
|
|
333
400
|
: bgUrls;
|
|
401
|
+
const actionUrl = repoInfo.owner
|
|
402
|
+
? `https://github.com/${repoInfo.owner}/${repoInfo.repo}`
|
|
403
|
+
: undefined;
|
|
334
404
|
const fork = {
|
|
335
405
|
_id: forkId,
|
|
336
406
|
title: input.forkTitle,
|
|
337
407
|
description: input.forkDescription,
|
|
338
408
|
imageSrc: coverUrls[0],
|
|
339
|
-
feedIds: [...
|
|
340
|
-
...(
|
|
409
|
+
feedIds: [...existingFeedIds, feedId],
|
|
410
|
+
...(actionUrl ? { actionLabel: 'View on GitHub', actionUrl } : {}),
|
|
341
411
|
};
|
|
342
412
|
const feed = {
|
|
343
413
|
_id: feedId,
|
|
@@ -507,7 +577,7 @@ server.tool('forkfeed_commits', 'Read git commits from the current repo. Without
|
|
|
507
577
|
lines.push(`| ${i + 1} | ${c.shortSha} | ${c.message.slice(0, 60)} | ${c.author} | ${c.date} | ${pub} |`);
|
|
508
578
|
}
|
|
509
579
|
if (existingFeedIds.length > 0) {
|
|
510
|
-
lines.push('', `
|
|
580
|
+
lines.push('', `Published feeds: ${existingFeedIds.length} (builder auto-includes them)`);
|
|
511
581
|
}
|
|
512
582
|
if (statusWarning)
|
|
513
583
|
lines.push(statusWarning);
|
|
@@ -522,8 +592,7 @@ server.tool('forkfeed_commits', 'Read git commits from the current repo. Without
|
|
|
522
592
|
return { content: [{ type: 'text', text: `Failed to read commit ${sha}: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
523
593
|
}
|
|
524
594
|
// Extract file paths from stats for tag detection
|
|
525
|
-
const
|
|
526
|
-
const filePaths = filePathMatches.map((m) => m.trim().replace(/\s+\|$/, ''));
|
|
595
|
+
const filePaths = parseFilePathsFromStats(detail.stats);
|
|
527
596
|
const tags = detectTags(detail.message, filePaths);
|
|
528
597
|
const images = filterImagesByTags(tags);
|
|
529
598
|
const lines = [
|
|
@@ -538,40 +607,13 @@ server.tool('forkfeed_commits', 'Read git commits from the current repo. Without
|
|
|
538
607
|
'## Diff',
|
|
539
608
|
detail.diff,
|
|
540
609
|
'',
|
|
541
|
-
`## Suggested Images (tags: ${tags.join(', ')})`,
|
|
610
|
+
`## Suggested Scene Images (tags: ${tags.join(', ')})`,
|
|
611
|
+
'Use short IDs (e.g. img47) in card blocks. Backgrounds are auto-assigned by the builder.',
|
|
542
612
|
'',
|
|
543
|
-
'Scene images (for covers + inline):',
|
|
544
613
|
...images.scenes,
|
|
545
|
-
'',
|
|
546
|
-
'Background images (for card backgrounds):',
|
|
547
|
-
...images.backgrounds,
|
|
548
614
|
].filter(Boolean);
|
|
549
615
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
550
616
|
});
|
|
551
|
-
// ── Tool: forkfeed_images ──────────────────────────────────────────────
|
|
552
|
-
server.tool('forkfeed_images', 'Get the IT Scenes image catalog (200 scene images + 30 backgrounds). Use this to pick images that match your content by tags and name. Call after forkfeed_guide.', {}, async () => {
|
|
553
|
-
const scenes = IMAGE_CATALOG.filter((i) => i.id.startsWith('img'));
|
|
554
|
-
const bgs = IMAGE_CATALOG.filter((i) => i.id.startsWith('bg'));
|
|
555
|
-
const lines = [
|
|
556
|
-
'# IT Scenes Image Catalog',
|
|
557
|
-
'',
|
|
558
|
-
`Base URL: ${IMAGE_BASE_URL}`,
|
|
559
|
-
'Prepend base URL to all filenames below to get the full image URL.',
|
|
560
|
-
'',
|
|
561
|
-
'Match images to content by tags and name. Use img* for covers and inline images, bg* for card backgrounds.',
|
|
562
|
-
'',
|
|
563
|
-
'Tags: deploy, git, disaster, debug, hype, victory, beginner, language, lifestyle, workplace, sarcastic, general',
|
|
564
|
-
'',
|
|
565
|
-
'## Scene Images (covers + inline)',
|
|
566
|
-
'',
|
|
567
|
-
...scenes.map((i) => `${i.id} | ${i.name} | ${i.tags} | ${i.file}`),
|
|
568
|
-
'',
|
|
569
|
-
'## Background Images',
|
|
570
|
-
'',
|
|
571
|
-
...bgs.map((i) => `${i.id} | ${i.name} | ${i.tags} | ${i.file}`),
|
|
572
|
-
];
|
|
573
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
574
|
-
});
|
|
575
617
|
// ── Tool: forkfeed_push ────────────────────────────────────────────────
|
|
576
618
|
server.tool('forkfeed_push', 'Push a generated manifest (forks, feeds, cards) to forkfeed. The manifest JSON must conform to the structure described in forkfeed_guide.', {
|
|
577
619
|
manifest: z
|
|
@@ -603,15 +645,15 @@ server.tool('forkfeed_push', 'Push a generated manifest (forks, feeds, cards) to
|
|
|
603
645
|
return pushManifestToServer(parsed.data);
|
|
604
646
|
});
|
|
605
647
|
// ── Tool: forkfeed_build ──────────────────────────────────────────────
|
|
606
|
-
server.tool('forkfeed_build', 'Build a forkfeed manifest from simplified content and push it.
|
|
607
|
-
content: buildInputSchema.describe('Simplified content:
|
|
648
|
+
server.tool('forkfeed_build', 'Build a forkfeed manifest from simplified content and push it. Auto-detects repo info, assigns backgrounds, fetches existing feeds. Use this instead of forkfeed_push.', {
|
|
649
|
+
content: buildInputSchema.describe('Simplified content: sha, titles, descriptions, and 8 cards with blocks'),
|
|
608
650
|
push: z.boolean().optional().describe('Push immediately after building (default: true)'),
|
|
609
651
|
}, async ({ content, push }) => {
|
|
610
652
|
const shouldPush = push !== false;
|
|
611
653
|
// Build the full manifest from simplified input
|
|
612
654
|
let manifest;
|
|
613
655
|
try {
|
|
614
|
-
manifest = buildManifest(content);
|
|
656
|
+
manifest = await buildManifest(content);
|
|
615
657
|
}
|
|
616
658
|
catch (err) {
|
|
617
659
|
return {
|
|
@@ -665,7 +707,7 @@ server.tool('forkfeed_status', 'Check your current forkfeed content: which forks
|
|
|
665
707
|
content: [
|
|
666
708
|
{
|
|
667
709
|
type: 'text',
|
|
668
|
-
text: 'No forks published yet. Use forkfeed_guide to learn how to generate content, then push it with
|
|
710
|
+
text: 'No forks published yet. Use forkfeed_guide to learn how to generate content, then push it with forkfeed_build.',
|
|
669
711
|
},
|
|
670
712
|
],
|
|
671
713
|
};
|
|
@@ -708,9 +750,9 @@ server.prompt('forkfeed', 'Turn GitHub commits into swipeable forkfeed content.
|
|
|
708
750
|
text: `Turn the commits in this repo into forkfeed content. Follow these steps exactly:
|
|
709
751
|
|
|
710
752
|
1. Call **forkfeed_guide** and **forkfeed_commits()** in parallel (no arguments for commits = list mode).
|
|
711
|
-
2. Show the commits table from forkfeed_commits. It already indicates which commits have published feeds
|
|
712
|
-
3. Call **forkfeed_commits** with the selected commit SHA. This returns the diff, file stats, and
|
|
713
|
-
4. Generate the simplified content JSON
|
|
753
|
+
2. Show the commits table from forkfeed_commits. It already indicates which commits have published feeds. Ask which ONE commit to process. One commit at a time, never more. Do NOT ask about image style.
|
|
754
|
+
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
|
+
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.
|
|
714
756
|
5. Call **forkfeed_build** with the simplified content (push defaults to true).
|
|
715
757
|
|
|
716
758
|
Start now.`,
|