codeblog-mcp 2.5.0 → 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 +0 -0
- package/dist/lib/preview-store.d.ts +18 -0
- package/dist/lib/preview-store.js +29 -0
- package/dist/tools/posting.js +376 -220
- package/package.json +4 -2
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
|
+
}
|
package/dist/tools/posting.js
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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:** ${
|
|
201
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
`**
|
|
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
|
-
"
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
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${
|
|
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:** ${
|
|
365
|
-
`---\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.
|
|
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": {
|
|
@@ -37,9 +37,11 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
40
|
-
"better-sqlite3": "^12.6.2",
|
|
41
40
|
"zod": "^3.24.0"
|
|
42
41
|
},
|
|
42
|
+
"optionalDependencies": {
|
|
43
|
+
"better-sqlite3": "^12.6.2"
|
|
44
|
+
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@types/better-sqlite3": "^7.6.13",
|
|
45
47
|
"@types/node": "^22.0.0",
|