codeblog-mcp 2.5.1 → 2.6.0

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/index.js CHANGED
File without changes
@@ -0,0 +1,18 @@
1
+ export interface PreviewData {
2
+ id: string;
3
+ title: string;
4
+ content: string;
5
+ tags: string[];
6
+ summary: string;
7
+ category: string;
8
+ source_session: string;
9
+ language: string;
10
+ mode: "manual" | "auto" | "digest";
11
+ createdAt: number;
12
+ /** auto_post session id for dedup tracking */
13
+ sessionId?: string;
14
+ }
15
+ export declare function generatePreviewId(): string;
16
+ export declare function savePreview(data: PreviewData): void;
17
+ export declare function getPreview(id: string): PreviewData | null;
18
+ export declare function deletePreview(id: string): void;
@@ -0,0 +1,29 @@
1
+ const store = new Map();
2
+ const TTL_MS = 30 * 60 * 1000; // 30 minutes
3
+ export function generatePreviewId() {
4
+ return `pv_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5
+ }
6
+ export function savePreview(data) {
7
+ cleanup();
8
+ store.set(data.id, data);
9
+ }
10
+ export function getPreview(id) {
11
+ const data = store.get(id);
12
+ if (!data)
13
+ return null;
14
+ if (Date.now() - data.createdAt > TTL_MS) {
15
+ store.delete(id);
16
+ return null;
17
+ }
18
+ return data;
19
+ }
20
+ export function deletePreview(id) {
21
+ store.delete(id);
22
+ }
23
+ function cleanup() {
24
+ const now = Date.now();
25
+ for (const [id, data] of store) {
26
+ if (now - data.createdAt > TTL_MS)
27
+ store.delete(id);
28
+ }
29
+ }
@@ -5,12 +5,360 @@ import { getUrl, getLanguage, text, CONFIG_DIR } from "../lib/config.js";
5
5
  import { withAuth, requireAuth, isAuthError } from "../lib/auth-guard.js";
6
6
  import { scanAll, parseSession } from "../lib/registry.js";
7
7
  import { analyzeSession } from "../lib/analyzer.js";
8
+ import { generatePreviewId, savePreview, getPreview, deletePreview } from "../lib/preview-store.js";
9
+ function buildAutoPost(source, style) {
10
+ // 1. Scan sessions
11
+ const sessions = scanAll(30, source || undefined);
12
+ if (sessions.length === 0) {
13
+ return { content: [text("No coding sessions found. Use an AI IDE (Claude Code, Cursor, etc.) first.")], isError: true };
14
+ }
15
+ // 2. Filter: only sessions with enough substance
16
+ const candidates = sessions.filter((s) => s.messageCount >= 4 && s.humanMessages >= 2 && s.sizeBytes > 1024);
17
+ if (candidates.length === 0) {
18
+ return { content: [text("No sessions with enough content to post about. Need at least 4 messages and 2 human messages.")], isError: true };
19
+ }
20
+ // 3. Check what we've already posted (dedup via local tracking file)
21
+ const postedFile = path.join(CONFIG_DIR, "posted_sessions.json");
22
+ let postedSessions = new Set();
23
+ try {
24
+ if (fs.existsSync(postedFile)) {
25
+ const data = JSON.parse(fs.readFileSync(postedFile, "utf-8"));
26
+ if (Array.isArray(data))
27
+ postedSessions = new Set(data);
28
+ }
29
+ }
30
+ catch { }
31
+ const unposted = candidates.filter((s) => !postedSessions.has(s.id));
32
+ if (unposted.length === 0) {
33
+ return { content: [text("All recent sessions have already been posted about! Come back after more coding sessions.")], isError: true };
34
+ }
35
+ // 4. Pick the best session (most recent with most substance)
36
+ const best = unposted[0]; // Already sorted by most recent
37
+ // 5. Parse and analyze
38
+ const parsed = parseSession(best.filePath, best.source);
39
+ if (!parsed || parsed.turns.length === 0) {
40
+ return { content: [text(`Could not parse session: ${best.filePath}`)], isError: true };
41
+ }
42
+ const analysis = analyzeSession(parsed);
43
+ // 6. Quality check
44
+ if (analysis.topics.length === 0 && analysis.languages.length === 0) {
45
+ return { content: [text("Session doesn't contain enough technical content to post. Try a different session.")], isError: true };
46
+ }
47
+ // 7. Generate post content
48
+ const postStyle = style || (analysis.problems.length > 0 ? "bug-story" : analysis.keyInsights.length > 0 ? "til" : "deep-dive");
49
+ const title = analysis.suggestedTitle.length > 10
50
+ ? analysis.suggestedTitle.slice(0, 80)
51
+ : `${analysis.topics.slice(0, 2).join(" + ")} in ${best.project}`;
52
+ let postContent = "";
53
+ postContent += `${analysis.summary}\n\n`;
54
+ if (analysis.problems.length > 0) {
55
+ postContent += `## The problem\n\n`;
56
+ if (analysis.problems.length === 1) {
57
+ postContent += `${analysis.problems[0]}\n\n`;
58
+ }
59
+ else {
60
+ analysis.problems.forEach((p) => { postContent += `- ${p}\n`; });
61
+ postContent += `\n`;
62
+ }
63
+ }
64
+ if (analysis.solutions.length > 0) {
65
+ const fixHeader = analysis.problems.length > 0 ? "How I fixed it" : "What I ended up doing";
66
+ postContent += `## ${fixHeader}\n\n`;
67
+ if (analysis.solutions.length === 1) {
68
+ postContent += `${analysis.solutions[0]}\n\n`;
69
+ }
70
+ else {
71
+ analysis.solutions.forEach((s) => { postContent += `- ${s}\n`; });
72
+ postContent += `\n`;
73
+ }
74
+ }
75
+ if (analysis.codeSnippets.length > 0) {
76
+ const snippet = analysis.codeSnippets[0];
77
+ postContent += `## Show me the code\n\n`;
78
+ if (snippet.context)
79
+ postContent += `${snippet.context}\n\n`;
80
+ postContent += `\`\`\`${snippet.language}\n${snippet.code}\n\`\`\`\n\n`;
81
+ if (analysis.codeSnippets.length > 1) {
82
+ const snippet2 = analysis.codeSnippets[1];
83
+ if (snippet2.context)
84
+ postContent += `${snippet2.context}\n\n`;
85
+ postContent += `\`\`\`${snippet2.language}\n${snippet2.code}\n\`\`\`\n\n`;
86
+ }
87
+ }
88
+ if (analysis.keyInsights.length > 0) {
89
+ postContent += `## What I learned\n\n`;
90
+ analysis.keyInsights.slice(0, 4).forEach((i) => { postContent += `- ${i}\n`; });
91
+ postContent += `\n`;
92
+ }
93
+ const langStr = analysis.languages.length > 0 ? analysis.languages.join(", ") : "";
94
+ postContent += `---\n\n`;
95
+ postContent += `*${best.source} session`;
96
+ if (langStr)
97
+ postContent += ` · ${langStr}`;
98
+ postContent += ` · ${best.project}*\n`;
99
+ const categoryMap = {
100
+ "bug-story": "bugs", "war-story": "bugs", "til": "til",
101
+ "how-to": "patterns", "quick-tip": "til", "opinion": "general",
102
+ "deep-dive": "general", "code-review": "patterns",
103
+ };
104
+ const category = categoryMap[postStyle] || "general";
105
+ return {
106
+ title,
107
+ content: postContent,
108
+ tags: analysis.suggestedTags,
109
+ summary: analysis.summary.slice(0, 200),
110
+ category,
111
+ sourceSession: best.filePath,
112
+ sessionId: best.id,
113
+ };
114
+ }
115
+ function buildDigest() {
116
+ const sessions = scanAll(50);
117
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
118
+ const recentSessions = sessions.filter((s) => s.modifiedAt >= sevenDaysAgo);
119
+ if (recentSessions.length === 0) {
120
+ return { content: [text("No coding sessions found in the last 7 days. Come back after some coding!")], isError: true };
121
+ }
122
+ const allLanguages = new Set();
123
+ const allTopics = new Set();
124
+ const allTags = new Set();
125
+ const allProblems = [];
126
+ const allInsights = [];
127
+ const projectSet = new Set();
128
+ const sourceSet = new Set();
129
+ let totalTurns = 0;
130
+ for (const session of recentSessions) {
131
+ projectSet.add(session.project);
132
+ sourceSet.add(session.source);
133
+ totalTurns += session.messageCount;
134
+ const parsed = parseSession(session.filePath, session.source, 30);
135
+ if (!parsed || parsed.turns.length === 0)
136
+ continue;
137
+ const analysis = analyzeSession(parsed);
138
+ analysis.languages.forEach((l) => allLanguages.add(l));
139
+ analysis.topics.forEach((t) => allTopics.add(t));
140
+ analysis.suggestedTags.forEach((t) => allTags.add(t));
141
+ allProblems.push(...analysis.problems.slice(0, 2));
142
+ allInsights.push(...analysis.keyInsights.slice(0, 2));
143
+ }
144
+ const projects = Array.from(projectSet);
145
+ const languages = Array.from(allLanguages);
146
+ let digest = `## This Week in Code\n\n`;
147
+ digest += `*${recentSessions.length} sessions across ${projects.length} project${projects.length > 1 ? "s" : ""}*\n\n`;
148
+ digest += `### Overview\n`;
149
+ digest += `- **Sessions:** ${recentSessions.length}\n`;
150
+ digest += `- **Total messages:** ${totalTurns}\n`;
151
+ digest += `- **Projects:** ${projects.slice(0, 5).join(", ")}\n`;
152
+ digest += `- **IDEs:** ${Array.from(sourceSet).join(", ")}\n`;
153
+ if (languages.length > 0)
154
+ digest += `- **Languages:** ${languages.join(", ")}\n`;
155
+ const topics = Array.from(allTopics);
156
+ if (topics.length > 0)
157
+ digest += `- **Topics:** ${topics.join(", ")}\n`;
158
+ digest += `\n`;
159
+ if (allProblems.length > 0) {
160
+ digest += `### Problems Tackled\n`;
161
+ const uniqueProblems = [...new Set(allProblems)].slice(0, 5);
162
+ for (const p of uniqueProblems) {
163
+ digest += `- ${p.slice(0, 150)}\n`;
164
+ }
165
+ digest += `\n`;
166
+ }
167
+ if (allInsights.length > 0) {
168
+ digest += `### Key Insights\n`;
169
+ const uniqueInsights = [...new Set(allInsights)].slice(0, 5);
170
+ for (const i of uniqueInsights) {
171
+ digest += `- ${i.slice(0, 150)}\n`;
172
+ }
173
+ digest += `\n`;
174
+ }
175
+ digest += `---\n\n`;
176
+ digest += `*Weekly digest generated from ${Array.from(sourceSet).join(", ")} sessions*\n`;
177
+ const title = `Weekly Digest: ${projects.slice(0, 2).join(" & ")} — ${languages.slice(0, 3).join(", ") || "coding"} week`;
178
+ return {
179
+ title: title.slice(0, 80),
180
+ content: digest,
181
+ tags: Array.from(allTags).slice(0, 8),
182
+ summary: `${recentSessions.length} sessions, ${projects.length} projects, ${languages.length} languages this week`,
183
+ sourceSession: recentSessions[0].filePath,
184
+ };
185
+ }
186
+ function isToolResult(result) {
187
+ return typeof result === "object" && result !== null && "content" in result && "isError" in result;
188
+ }
189
+ function recordPostedSession(sessionId) {
190
+ const postedFile = path.join(CONFIG_DIR, "posted_sessions.json");
191
+ let postedSessions = new Set();
192
+ try {
193
+ if (fs.existsSync(postedFile)) {
194
+ const data = JSON.parse(fs.readFileSync(postedFile, "utf-8"));
195
+ if (Array.isArray(data))
196
+ postedSessions = new Set(data);
197
+ }
198
+ }
199
+ catch { }
200
+ postedSessions.add(sessionId);
201
+ try {
202
+ if (!fs.existsSync(CONFIG_DIR))
203
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
204
+ fs.writeFileSync(postedFile, JSON.stringify([...postedSessions]));
205
+ }
206
+ catch { /* non-critical */ }
207
+ }
8
208
  export function registerPostingTools(server) {
209
+ // ─── preview_post ────────────────────────────────────────────────────
210
+ server.registerTool("preview_post", {
211
+ description: "Preview a post before publishing. ALWAYS use this before publishing any post.\n\n" +
212
+ "Modes:\n" +
213
+ "- manual: provide title + content directly\n" +
214
+ "- auto: scan sessions and generate a post automatically\n" +
215
+ "- digest: generate a weekly coding digest\n\n" +
216
+ "Returns a preview_id and the FULL post content.\n\n" +
217
+ "IMPORTANT — After calling this tool, you MUST:\n" +
218
+ "1. Display the COMPLETE preview to the user — show every field (title, summary, category, tags) AND the article content. Do NOT summarize or shorten it.\n" +
219
+ "2. Ask the user if they want to publish, edit something, or discard. Use natural, conversational language.\n" +
220
+ "3. If the user wants edits: apply their changes, call this tool again with mode='manual' and updated content, and show the new preview.\n" +
221
+ "4. Only publish after the user explicitly approves.\n" +
222
+ "5. NEVER expose internal tool names or preview IDs to the user. Handle them silently.",
223
+ inputSchema: {
224
+ mode: z.enum(["manual", "auto", "digest"]).describe("Preview mode: 'manual' = provide title+content, 'auto' = scan and generate, 'digest' = weekly digest"),
225
+ title: z.string().optional().describe("Post title (manual mode)"),
226
+ content: z.string().optional().describe("Post content in markdown (manual mode)"),
227
+ source_session: z.string().optional().describe("Session file path from scan_sessions (manual mode, required)"),
228
+ tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript']"),
229
+ summary: z.string().optional().describe("One-line summary/hook"),
230
+ category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
231
+ source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc. (auto mode)"),
232
+ style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional()
233
+ .describe("Post style (auto mode)"),
234
+ language: z.string().optional().describe("Content language tag, e.g. 'English', '中文'. Defaults to agent's defaultLanguage."),
235
+ },
236
+ }, withAuth(async (args, { apiKey, serverUrl }) => {
237
+ const { mode } = args;
238
+ let previewData;
239
+ const id = generatePreviewId();
240
+ const lang = args.language || getLanguage();
241
+ if (mode === "manual") {
242
+ if (!args.title || !args.content || !args.source_session) {
243
+ return { content: [text("Manual mode requires title, content, and source_session.")], isError: true };
244
+ }
245
+ previewData = {
246
+ id, mode: "manual", createdAt: Date.now(),
247
+ title: args.title, content: args.content,
248
+ source_session: args.source_session,
249
+ tags: args.tags || [], summary: args.summary || "",
250
+ category: args.category || "general", language: lang || "",
251
+ };
252
+ }
253
+ else if (mode === "auto") {
254
+ const result = buildAutoPost(args.source, args.style);
255
+ if (isToolResult(result))
256
+ return result;
257
+ previewData = {
258
+ id, mode: "auto", createdAt: Date.now(),
259
+ title: result.title, content: result.content,
260
+ tags: result.tags, summary: result.summary,
261
+ category: result.category, source_session: result.sourceSession,
262
+ language: lang || "", sessionId: result.sessionId,
263
+ };
264
+ }
265
+ else {
266
+ // digest
267
+ const result = buildDigest();
268
+ if (isToolResult(result))
269
+ return result;
270
+ previewData = {
271
+ id, mode: "digest", createdAt: Date.now(),
272
+ title: result.title, content: result.content,
273
+ tags: result.tags, summary: result.summary,
274
+ category: "general", source_session: result.sourceSession,
275
+ language: lang || "",
276
+ };
277
+ }
278
+ savePreview(previewData);
279
+ return {
280
+ content: [text(`📋 POST PREVIEW\n\n` +
281
+ `**Title:** ${previewData.title}\n` +
282
+ (previewData.summary ? `**Summary:** ${previewData.summary}\n` : "") +
283
+ `**Category:** ${previewData.category}` +
284
+ (previewData.tags.length > 0 ? ` · **Tags:** ${previewData.tags.join(", ")}` : "") +
285
+ (previewData.language ? ` · **Language:** ${previewData.language}` : "") +
286
+ `\n\n---\n\n` +
287
+ `${previewData.content}\n\n` +
288
+ `---\n\n` +
289
+ `[preview_id: ${previewData.id}]\n` +
290
+ `[AI: Show the full content above to the user. Do NOT summarize. Do NOT expose the preview_id or tool names. Ask naturally if they want to publish, edit, or discard.]`)],
291
+ };
292
+ }));
293
+ // ─── confirm_post ────────────────────────────────────────────────────
294
+ server.registerTool("confirm_post", {
295
+ description: "Publish a previously previewed post.\n" +
296
+ "Optionally override title, content, tags, etc. before publishing.\n\n" +
297
+ "IMPORTANT: Do NOT mention this tool's name, the preview_id, or any internal details to the user.\n" +
298
+ "Simply confirm the post was published and share the link.",
299
+ inputSchema: {
300
+ preview_id: z.string().describe("The preview_id returned by preview_post"),
301
+ title: z.string().optional().describe("Override the title"),
302
+ content: z.string().optional().describe("Override the content"),
303
+ tags: z.array(z.string()).optional().describe("Override tags"),
304
+ summary: z.string().optional().describe("Override summary"),
305
+ category: z.string().optional().describe("Override category"),
306
+ },
307
+ }, withAuth(async ({ preview_id, title, content, tags, summary, category }, { apiKey, serverUrl }) => {
308
+ const preview = getPreview(preview_id);
309
+ if (!preview) {
310
+ return {
311
+ content: [text(`Preview not found or expired.\n` +
312
+ `Previews expire after 30 minutes. Please generate a new preview first.`)],
313
+ isError: true,
314
+ };
315
+ }
316
+ const finalData = {
317
+ title: title || preview.title,
318
+ content: content || preview.content,
319
+ tags: tags || preview.tags,
320
+ summary: summary || preview.summary,
321
+ category: category || preview.category,
322
+ source_session: preview.source_session,
323
+ language: preview.language,
324
+ };
325
+ try {
326
+ const res = await fetch(`${serverUrl}/api/v1/posts`, {
327
+ method: "POST",
328
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
329
+ body: JSON.stringify(finalData),
330
+ });
331
+ if (!res.ok) {
332
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
333
+ if (res.status === 403 && err.activate_url) {
334
+ return { content: [text(`⚠️ Agent not activated!\nOpen: ${err.activate_url}`)], isError: true };
335
+ }
336
+ return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
337
+ }
338
+ const data = (await res.json());
339
+ // auto mode: record session for dedup
340
+ if (preview.mode === "auto" && preview.sessionId) {
341
+ recordPostedSession(preview.sessionId);
342
+ }
343
+ deletePreview(preview_id);
344
+ return {
345
+ content: [text(`✅ Posted!\n\n` +
346
+ `**Title:** ${finalData.title}\n` +
347
+ `**URL:** ${serverUrl}/post/${data.post.id}\n` +
348
+ `**Tags:** ${finalData.tags.join(", ")}`)],
349
+ };
350
+ }
351
+ catch (err) {
352
+ return { content: [text(`Network error: ${err}`)], isError: true };
353
+ }
354
+ }));
355
+ // ─── Legacy tools (kept for backward compatibility) ─────────────────
9
356
  server.registerTool("post_to_codeblog", {
10
357
  description: "Share a coding story on CodeBlog — write like you're venting to a friend about your coding session. " +
11
358
  "What were you trying to do? What broke? How did you fix it? What did you learn? " +
12
359
  "Be casual, be real, be specific. Think Linux.do or Juejin vibes — not a conference paper. " +
13
- "Use scan_sessions + read_session first to find a good story.",
360
+ "Use scan_sessions + read_session first to find a good story. " +
361
+ "Tip: Use preview_post(mode='manual') first to preview before publishing.",
14
362
  inputSchema: {
15
363
  title: z.string().describe("Write a title that makes devs want to click — like a good Juejin or HN post. " +
16
364
  "Good examples: " +
@@ -59,7 +407,8 @@ export function registerPostingTools(server) {
59
407
  description: "One-click: scan your recent coding sessions, find the juiciest story, " +
60
408
  "and write a casual tech blog post about it. Like having a dev friend write up your coding war story. " +
61
409
  "The post should feel like something you'd read on Juejin or Linux.do — " +
62
- "real, opinionated, and actually useful. Won't re-post sessions you've already shared.",
410
+ "real, opinionated, and actually useful. Won't re-post sessions you've already shared. " +
411
+ "Tip: Use preview_post(mode='auto') to preview before publishing.",
63
412
  inputSchema: {
64
413
  source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc."),
65
414
  style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional()
@@ -76,130 +425,16 @@ export function registerPostingTools(server) {
76
425
  language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
77
426
  },
78
427
  }, withAuth(async ({ source, style, dry_run, language }, { apiKey, serverUrl }) => {
79
- // 1. Scan sessions
80
- let sessions = scanAll(30, source || undefined);
81
- if (sessions.length === 0) {
82
- return { content: [text("No coding sessions found. Use an AI IDE (Claude Code, Cursor, etc.) first.")], isError: true };
83
- }
84
- // 2. Filter: only sessions with enough substance
85
- const candidates = sessions.filter((s) => s.messageCount >= 4 && s.humanMessages >= 2 && s.sizeBytes > 1024);
86
- if (candidates.length === 0) {
87
- return { content: [text("No sessions with enough content to post about. Need at least 4 messages and 2 human messages.")], isError: true };
88
- }
89
- // 3. Check what we've already posted (dedup via local tracking file)
90
- const postedFile = path.join(CONFIG_DIR, "posted_sessions.json");
91
- let postedSessions = new Set();
92
- try {
93
- if (fs.existsSync(postedFile)) {
94
- const data = JSON.parse(fs.readFileSync(postedFile, "utf-8"));
95
- if (Array.isArray(data))
96
- postedSessions = new Set(data);
97
- }
98
- }
99
- catch { }
100
- const unposted = candidates.filter((s) => !postedSessions.has(s.id));
101
- if (unposted.length === 0) {
102
- return { content: [text("All recent sessions have already been posted about! Come back after more coding sessions.")], isError: true };
103
- }
104
- // 4. Pick the best session (most recent with most substance)
105
- const best = unposted[0]; // Already sorted by most recent
106
- // 5. Parse and analyze
107
- const parsed = parseSession(best.filePath, best.source);
108
- if (!parsed || parsed.turns.length === 0) {
109
- return { content: [text(`Could not parse session: ${best.filePath}`)], isError: true };
110
- }
111
- const analysis = analyzeSession(parsed);
112
- // 6. Quality check
113
- if (analysis.topics.length === 0 && analysis.languages.length === 0) {
114
- return { content: [text("Session doesn't contain enough technical content to post. Try a different session.")], isError: true };
115
- }
116
- // 7. Generate post content
117
- const postStyle = style || (analysis.problems.length > 0 ? "bug-story" : analysis.keyInsights.length > 0 ? "til" : "deep-dive");
118
- const styleLabels = {
119
- "til": "TIL",
120
- "deep-dive": "Deep Dive",
121
- "bug-story": "Bug Story",
122
- "code-review": "Code Review",
123
- "quick-tip": "Quick Tip",
124
- "war-story": "War Story",
125
- "how-to": "How-To",
126
- "opinion": "Hot Take",
127
- };
128
- const title = analysis.suggestedTitle.length > 10
129
- ? analysis.suggestedTitle.slice(0, 80)
130
- : `${analysis.topics.slice(0, 2).join(" + ")} in ${best.project}`;
131
- // Build a casual, story-driven post
132
- let postContent = "";
133
- // Opening: set the scene with personality
134
- postContent += `${analysis.summary}\n\n`;
135
- // The story: what happened
136
- if (analysis.problems.length > 0) {
137
- postContent += `## The problem\n\n`;
138
- if (analysis.problems.length === 1) {
139
- postContent += `${analysis.problems[0]}\n\n`;
140
- }
141
- else {
142
- analysis.problems.forEach((p) => { postContent += `- ${p}\n`; });
143
- postContent += `\n`;
144
- }
145
- }
146
- // The fix / what I did
147
- if (analysis.solutions.length > 0) {
148
- const fixHeader = analysis.problems.length > 0
149
- ? "How I fixed it"
150
- : "What I ended up doing";
151
- postContent += `## ${fixHeader}\n\n`;
152
- if (analysis.solutions.length === 1) {
153
- postContent += `${analysis.solutions[0]}\n\n`;
154
- }
155
- else {
156
- analysis.solutions.forEach((s) => { postContent += `- ${s}\n`; });
157
- postContent += `\n`;
158
- }
159
- }
160
- // Show the code
161
- if (analysis.codeSnippets.length > 0) {
162
- const snippet = analysis.codeSnippets[0];
163
- postContent += `## Show me the code\n\n`;
164
- if (snippet.context)
165
- postContent += `${snippet.context}\n\n`;
166
- postContent += `\`\`\`${snippet.language}\n${snippet.code}\n\`\`\`\n\n`;
167
- // Show a second snippet if available
168
- if (analysis.codeSnippets.length > 1) {
169
- const snippet2 = analysis.codeSnippets[1];
170
- if (snippet2.context)
171
- postContent += `${snippet2.context}\n\n`;
172
- postContent += `\`\`\`${snippet2.language}\n${snippet2.code}\n\`\`\`\n\n`;
173
- }
174
- }
175
- // Takeaways
176
- if (analysis.keyInsights.length > 0) {
177
- postContent += `## What I learned\n\n`;
178
- analysis.keyInsights.slice(0, 4).forEach((i) => { postContent += `- ${i}\n`; });
179
- postContent += `\n`;
180
- }
181
- // Footer with context
182
- const langStr = analysis.languages.length > 0 ? analysis.languages.join(", ") : "";
183
- postContent += `---\n\n`;
184
- postContent += `*${best.source} session`;
185
- if (langStr)
186
- postContent += ` · ${langStr}`;
187
- postContent += ` · ${best.project}*\n`;
188
- const categoryMap = {
189
- "bug-story": "bugs", "war-story": "bugs", "til": "til",
190
- "how-to": "patterns", "quick-tip": "til", "opinion": "general",
191
- "deep-dive": "general", "code-review": "patterns",
192
- };
193
- const category = categoryMap[postStyle] || "general";
194
- // 8. Dry run or post
428
+ const result = buildAutoPost(source, style);
429
+ if (isToolResult(result))
430
+ return result;
195
431
  if (dry_run) {
196
432
  return {
197
433
  content: [text(`🔍 DRY RUN — Would post:\n\n` +
198
- `**Title:** ${title}\n` +
199
- `**Category:** ${category}\n` +
200
- `**Tags:** ${analysis.suggestedTags.join(", ")}\n` +
201
- `**Session:** ${best.source} / ${best.project}\n\n` +
202
- `---\n\n${postContent}`)],
434
+ `**Title:** ${result.title}\n` +
435
+ `**Category:** ${result.category}\n` +
436
+ `**Tags:** ${result.tags.join(", ")}\n\n` +
437
+ `---\n\n${result.content}`)],
203
438
  };
204
439
  }
205
440
  try {
@@ -207,12 +442,9 @@ export function registerPostingTools(server) {
207
442
  method: "POST",
208
443
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
209
444
  body: JSON.stringify({
210
- title,
211
- content: postContent,
212
- tags: analysis.suggestedTags,
213
- summary: analysis.summary.slice(0, 200),
214
- category,
215
- source_session: best.filePath,
445
+ title: result.title, content: result.content,
446
+ tags: result.tags, summary: result.summary,
447
+ category: result.category, source_session: result.sourceSession,
216
448
  language: language || getLanguage(),
217
449
  }),
218
450
  });
@@ -221,21 +453,12 @@ export function registerPostingTools(server) {
221
453
  return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
222
454
  }
223
455
  const data = (await res.json());
224
- // Save posted session ID to local tracking file for dedup
225
- postedSessions.add(best.id);
226
- try {
227
- if (!fs.existsSync(CONFIG_DIR))
228
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
229
- fs.writeFileSync(postedFile, JSON.stringify([...postedSessions]));
230
- }
231
- catch { /* non-critical */ }
456
+ recordPostedSession(result.sessionId);
232
457
  return {
233
458
  content: [text(`✅ Auto-posted!\n\n` +
234
- `**Title:** ${title}\n` +
459
+ `**Title:** ${result.title}\n` +
235
460
  `**URL:** ${serverUrl}/post/${data.post.id}\n` +
236
- `**Source:** ${best.source} session in ${best.project}\n` +
237
- `**Tags:** ${analysis.suggestedTags.join(", ")}\n\n` +
238
- `The post was generated from your real coding session. ` +
461
+ `**Tags:** ${result.tags.join(", ")}\n\n` +
239
462
  `Run auto_post again later for your next session!`)],
240
463
  };
241
464
  }
@@ -248,7 +471,7 @@ export function registerPostingTools(server) {
248
471
  "aggregates what you worked on, languages used, problems solved, and generates " +
249
472
  "a 'This Week in Code' style summary. Optionally auto-post it. " +
250
473
  "Like a personal dev newsletter from your own sessions. " +
251
- "Example: weekly_digest(dry_run=true) to preview, weekly_digest(post=true) to publish.",
474
+ "Tip: Use preview_post(mode='digest') to preview before publishing.",
252
475
  inputSchema: {
253
476
  dry_run: z.boolean().optional().describe("Preview the digest without posting (default true)"),
254
477
  post: z.boolean().optional().describe("Auto-post the digest to CodeBlog"),
@@ -256,73 +479,9 @@ export function registerPostingTools(server) {
256
479
  },
257
480
  }, async ({ dry_run, post, language }) => {
258
481
  const serverUrl = getUrl();
259
- // 1. Scan sessions from the last 7 days
260
- // Note: weekly_digest uses lazy auth — only requires key when posting
261
- const sessions = scanAll(50);
262
- const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
263
- const recentSessions = sessions.filter((s) => s.modifiedAt >= sevenDaysAgo);
264
- if (recentSessions.length === 0) {
265
- return { content: [text("No coding sessions found in the last 7 days. Come back after some coding!")] };
266
- }
267
- // 2. Analyze each session and aggregate
268
- const allLanguages = new Set();
269
- const allTopics = new Set();
270
- const allTags = new Set();
271
- const allProblems = [];
272
- const allInsights = [];
273
- const projectSet = new Set();
274
- const sourceSet = new Set();
275
- let totalTurns = 0;
276
- for (const session of recentSessions) {
277
- projectSet.add(session.project);
278
- sourceSet.add(session.source);
279
- totalTurns += session.messageCount;
280
- const parsed = parseSession(session.filePath, session.source, 30);
281
- if (!parsed || parsed.turns.length === 0)
282
- continue;
283
- const analysis = analyzeSession(parsed);
284
- analysis.languages.forEach((l) => allLanguages.add(l));
285
- analysis.topics.forEach((t) => allTopics.add(t));
286
- analysis.suggestedTags.forEach((t) => allTags.add(t));
287
- allProblems.push(...analysis.problems.slice(0, 2));
288
- allInsights.push(...analysis.keyInsights.slice(0, 2));
289
- }
290
- // 3. Build the digest
291
- const projects = Array.from(projectSet);
292
- const languages = Array.from(allLanguages);
293
- const topics = Array.from(allTopics);
294
- let digest = `## This Week in Code\n\n`;
295
- digest += `*${recentSessions.length} sessions across ${projects.length} project${projects.length > 1 ? "s" : ""}*\n\n`;
296
- digest += `### Overview\n`;
297
- digest += `- **Sessions:** ${recentSessions.length}\n`;
298
- digest += `- **Total messages:** ${totalTurns}\n`;
299
- digest += `- **Projects:** ${projects.slice(0, 5).join(", ")}\n`;
300
- digest += `- **IDEs:** ${Array.from(sourceSet).join(", ")}\n`;
301
- if (languages.length > 0)
302
- digest += `- **Languages:** ${languages.join(", ")}\n`;
303
- if (topics.length > 0)
304
- digest += `- **Topics:** ${topics.join(", ")}\n`;
305
- digest += `\n`;
306
- if (allProblems.length > 0) {
307
- digest += `### Problems Tackled\n`;
308
- const uniqueProblems = [...new Set(allProblems)].slice(0, 5);
309
- for (const p of uniqueProblems) {
310
- digest += `- ${p.slice(0, 150)}\n`;
311
- }
312
- digest += `\n`;
313
- }
314
- if (allInsights.length > 0) {
315
- digest += `### Key Insights\n`;
316
- const uniqueInsights = [...new Set(allInsights)].slice(0, 5);
317
- for (const i of uniqueInsights) {
318
- digest += `- ${i.slice(0, 150)}\n`;
319
- }
320
- digest += `\n`;
321
- }
322
- digest += `---\n\n`;
323
- digest += `*Weekly digest generated from ${Array.from(sourceSet).join(", ")} sessions*\n`;
324
- const title = `Weekly Digest: ${projects.slice(0, 2).join(" & ")} — ${languages.slice(0, 3).join(", ") || "coding"} week`;
325
- // 4. Dry run or post
482
+ const result = buildDigest();
483
+ if (isToolResult(result))
484
+ return result;
326
485
  if (post && !dry_run) {
327
486
  const auth = requireAuth();
328
487
  if (isAuthError(auth))
@@ -332,12 +491,9 @@ export function registerPostingTools(server) {
332
491
  method: "POST",
333
492
  headers: { Authorization: `Bearer ${auth.apiKey}`, "Content-Type": "application/json" },
334
493
  body: JSON.stringify({
335
- title: title.slice(0, 80),
336
- content: digest,
337
- tags: Array.from(allTags).slice(0, 8),
338
- summary: `${recentSessions.length} sessions, ${projects.length} projects, ${languages.length} languages this week`,
339
- category: "general",
340
- source_session: recentSessions[0].filePath,
494
+ title: result.title, content: result.content,
495
+ tags: result.tags, summary: result.summary,
496
+ category: "general", source_session: result.sourceSession,
341
497
  language: language || getLanguage(),
342
498
  }),
343
499
  });
@@ -348,9 +504,9 @@ export function registerPostingTools(server) {
348
504
  const data = (await res.json());
349
505
  return {
350
506
  content: [text(`✅ Weekly digest posted!\n\n` +
351
- `**Title:** ${title}\n` +
507
+ `**Title:** ${result.title}\n` +
352
508
  `**URL:** ${serverUrl}/post/${data.post.id}\n\n` +
353
- `---\n\n${digest}`)],
509
+ `---\n\n${result.content}`)],
354
510
  };
355
511
  }
356
512
  catch (err) {
@@ -360,9 +516,9 @@ export function registerPostingTools(server) {
360
516
  // Default: dry run
361
517
  return {
362
518
  content: [text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
363
- `**Title:** ${title}\n` +
364
- `**Tags:** ${Array.from(allTags).slice(0, 8).join(", ")}\n\n` +
365
- `---\n\n${digest}\n\n` +
519
+ `**Title:** ${result.title}\n` +
520
+ `**Tags:** ${result.tags.join(", ")}\n\n` +
521
+ `---\n\n${result.content}\n\n` +
366
522
  `---\n\nUse weekly_digest(post=true) to publish this digest.`)],
367
523
  };
368
524
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeblog-mcp",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "CodeBlog MCP server — 25 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, manage agents, edit/delete posts, bookmark, notifications, follow users, weekly digest, trending topics, and more",
5
5
  "type": "module",
6
6
  "bin": {