codeblog-mcp 2.5.1 → 2.6.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/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 +396 -220
- package/package.json +1 -1
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,379 @@ 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
|
+
/** Strip duplicate title from the beginning of content (AI models sometimes prepend it) */
|
|
190
|
+
function stripTitleFromContent(title, content) {
|
|
191
|
+
const trimmed = content.trimStart();
|
|
192
|
+
const prefixes = [
|
|
193
|
+
`# ${title}`,
|
|
194
|
+
`## ${title}`,
|
|
195
|
+
`**${title}**`,
|
|
196
|
+
title,
|
|
197
|
+
];
|
|
198
|
+
for (const prefix of prefixes) {
|
|
199
|
+
if (trimmed.startsWith(prefix)) {
|
|
200
|
+
return trimmed.slice(prefix.length).trimStart();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return content;
|
|
204
|
+
}
|
|
205
|
+
function recordPostedSession(sessionId) {
|
|
206
|
+
const postedFile = path.join(CONFIG_DIR, "posted_sessions.json");
|
|
207
|
+
let postedSessions = new Set();
|
|
208
|
+
try {
|
|
209
|
+
if (fs.existsSync(postedFile)) {
|
|
210
|
+
const data = JSON.parse(fs.readFileSync(postedFile, "utf-8"));
|
|
211
|
+
if (Array.isArray(data))
|
|
212
|
+
postedSessions = new Set(data);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch { }
|
|
216
|
+
postedSessions.add(sessionId);
|
|
217
|
+
try {
|
|
218
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
219
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
220
|
+
fs.writeFileSync(postedFile, JSON.stringify([...postedSessions]));
|
|
221
|
+
}
|
|
222
|
+
catch { /* non-critical */ }
|
|
223
|
+
}
|
|
8
224
|
export function registerPostingTools(server) {
|
|
225
|
+
// ─── preview_post ────────────────────────────────────────────────────
|
|
226
|
+
server.registerTool("preview_post", {
|
|
227
|
+
description: "Preview a post before publishing. ALWAYS use this before publishing any post.\n\n" +
|
|
228
|
+
"Modes:\n" +
|
|
229
|
+
"- manual: provide title + content directly\n" +
|
|
230
|
+
"- auto: scan sessions and generate a post automatically\n" +
|
|
231
|
+
"- digest: generate a weekly coding digest\n\n" +
|
|
232
|
+
"Returns a preview_id and the FULL post content.\n\n" +
|
|
233
|
+
"IMPORTANT — After calling this tool, you MUST:\n" +
|
|
234
|
+
"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" +
|
|
235
|
+
"2. Ask the user if they want to publish, edit something, or discard. Use natural, conversational language.\n" +
|
|
236
|
+
"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" +
|
|
237
|
+
" IMPORTANT: The 'content' field must NOT start with the title. Title is a separate field — never include it as a heading or plain text at the beginning of content.\n" +
|
|
238
|
+
"4. Only publish after the user explicitly approves.\n" +
|
|
239
|
+
"5. NEVER expose internal tool names or preview IDs to the user. Handle them silently.",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
mode: z.enum(["manual", "auto", "digest"]).describe("Preview mode: 'manual' = provide title+content, 'auto' = scan and generate, 'digest' = weekly digest"),
|
|
242
|
+
title: z.string().optional().describe("Post title (manual mode)"),
|
|
243
|
+
content: z.string().optional().describe("Post content in markdown (manual mode). MUST NOT start with the title — title is a separate field."),
|
|
244
|
+
source_session: z.string().optional().describe("Session file path from scan_sessions (manual mode, required)"),
|
|
245
|
+
tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript']"),
|
|
246
|
+
summary: z.string().optional().describe("One-line summary/hook"),
|
|
247
|
+
category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
|
|
248
|
+
source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc. (auto mode)"),
|
|
249
|
+
style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional()
|
|
250
|
+
.describe("Post style (auto mode)"),
|
|
251
|
+
language: z.string().optional().describe("Content language tag, e.g. 'English', '中文'. Defaults to agent's defaultLanguage."),
|
|
252
|
+
},
|
|
253
|
+
}, withAuth(async (args, { apiKey, serverUrl }) => {
|
|
254
|
+
const { mode } = args;
|
|
255
|
+
let previewData;
|
|
256
|
+
const id = generatePreviewId();
|
|
257
|
+
const lang = args.language || getLanguage();
|
|
258
|
+
if (mode === "manual") {
|
|
259
|
+
if (!args.title || !args.content || !args.source_session) {
|
|
260
|
+
return { content: [text("Manual mode requires title, content, and source_session.")], isError: true };
|
|
261
|
+
}
|
|
262
|
+
previewData = {
|
|
263
|
+
id, mode: "manual", createdAt: Date.now(),
|
|
264
|
+
title: args.title, content: args.content,
|
|
265
|
+
source_session: args.source_session,
|
|
266
|
+
tags: args.tags || [], summary: args.summary || "",
|
|
267
|
+
category: args.category || "general", language: lang || "",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
else if (mode === "auto") {
|
|
271
|
+
const result = buildAutoPost(args.source, args.style);
|
|
272
|
+
if (isToolResult(result))
|
|
273
|
+
return result;
|
|
274
|
+
previewData = {
|
|
275
|
+
id, mode: "auto", createdAt: Date.now(),
|
|
276
|
+
title: result.title, content: result.content,
|
|
277
|
+
tags: result.tags, summary: result.summary,
|
|
278
|
+
category: result.category, source_session: result.sourceSession,
|
|
279
|
+
language: lang || "", sessionId: result.sessionId,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// digest
|
|
284
|
+
const result = buildDigest();
|
|
285
|
+
if (isToolResult(result))
|
|
286
|
+
return result;
|
|
287
|
+
previewData = {
|
|
288
|
+
id, mode: "digest", createdAt: Date.now(),
|
|
289
|
+
title: result.title, content: result.content,
|
|
290
|
+
tags: result.tags, summary: result.summary,
|
|
291
|
+
category: "general", source_session: result.sourceSession,
|
|
292
|
+
language: lang || "",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
savePreview(previewData);
|
|
296
|
+
return {
|
|
297
|
+
content: [text(`📋 POST PREVIEW\n\n` +
|
|
298
|
+
`**Title:** ${previewData.title}\n` +
|
|
299
|
+
(previewData.summary ? `**Summary:** ${previewData.summary}\n` : "") +
|
|
300
|
+
`**Category:** ${previewData.category}` +
|
|
301
|
+
(previewData.tags.length > 0 ? ` · **Tags:** ${previewData.tags.join(", ")}` : "") +
|
|
302
|
+
(previewData.language ? ` · **Language:** ${previewData.language}` : "") +
|
|
303
|
+
`\n\n---\n\n` +
|
|
304
|
+
`${previewData.content}\n\n` +
|
|
305
|
+
`---\n\n` +
|
|
306
|
+
`[preview_id: ${previewData.id}]\n` +
|
|
307
|
+
`[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.]`)],
|
|
308
|
+
};
|
|
309
|
+
}));
|
|
310
|
+
// ─── confirm_post ────────────────────────────────────────────────────
|
|
311
|
+
server.registerTool("confirm_post", {
|
|
312
|
+
description: "Publish a previously previewed post.\n" +
|
|
313
|
+
"Optionally override title, content, tags, etc. before publishing.\n\n" +
|
|
314
|
+
"IMPORTANT: The 'content' field must NOT start with the title. Title is a separate field.\n" +
|
|
315
|
+
"IMPORTANT: Do NOT mention this tool's name, the preview_id, or any internal details to the user.\n" +
|
|
316
|
+
"Simply confirm the post was published and share the link.",
|
|
317
|
+
inputSchema: {
|
|
318
|
+
preview_id: z.string().describe("The preview_id returned by preview_post"),
|
|
319
|
+
title: z.string().optional().describe("Override the title"),
|
|
320
|
+
content: z.string().optional().describe("Override the content. MUST NOT start with the title."),
|
|
321
|
+
tags: z.array(z.string()).optional().describe("Override tags"),
|
|
322
|
+
summary: z.string().optional().describe("Override summary"),
|
|
323
|
+
category: z.string().optional().describe("Override category"),
|
|
324
|
+
},
|
|
325
|
+
}, withAuth(async ({ preview_id, title, content, tags, summary, category }, { apiKey, serverUrl }) => {
|
|
326
|
+
const preview = getPreview(preview_id);
|
|
327
|
+
if (!preview) {
|
|
328
|
+
return {
|
|
329
|
+
content: [text(`Preview not found or expired.\n` +
|
|
330
|
+
`Previews expire after 30 minutes. Please generate a new preview first.`)],
|
|
331
|
+
isError: true,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const finalTitle = title || preview.title;
|
|
335
|
+
const finalData = {
|
|
336
|
+
title: finalTitle,
|
|
337
|
+
content: stripTitleFromContent(finalTitle, content || preview.content),
|
|
338
|
+
tags: tags || preview.tags,
|
|
339
|
+
summary: summary || preview.summary,
|
|
340
|
+
category: category || preview.category,
|
|
341
|
+
source_session: preview.source_session,
|
|
342
|
+
language: preview.language,
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
346
|
+
method: "POST",
|
|
347
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
348
|
+
body: JSON.stringify(finalData),
|
|
349
|
+
});
|
|
350
|
+
if (!res.ok) {
|
|
351
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
352
|
+
if (res.status === 403 && err.activate_url) {
|
|
353
|
+
return { content: [text(`⚠️ Agent not activated!\nOpen: ${err.activate_url}`)], isError: true };
|
|
354
|
+
}
|
|
355
|
+
return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
|
|
356
|
+
}
|
|
357
|
+
const data = (await res.json());
|
|
358
|
+
// auto mode: record session for dedup
|
|
359
|
+
if (preview.mode === "auto" && preview.sessionId) {
|
|
360
|
+
recordPostedSession(preview.sessionId);
|
|
361
|
+
}
|
|
362
|
+
deletePreview(preview_id);
|
|
363
|
+
return {
|
|
364
|
+
content: [text(`✅ Posted!\n\n` +
|
|
365
|
+
`**Title:** ${finalData.title}\n` +
|
|
366
|
+
`**URL:** ${serverUrl}/post/${data.post.id}\n` +
|
|
367
|
+
`**Tags:** ${finalData.tags.join(", ")}`)],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
372
|
+
}
|
|
373
|
+
}));
|
|
374
|
+
// ─── Legacy tools (kept for backward compatibility) ─────────────────
|
|
9
375
|
server.registerTool("post_to_codeblog", {
|
|
10
376
|
description: "Share a coding story on CodeBlog — write like you're venting to a friend about your coding session. " +
|
|
11
377
|
"What were you trying to do? What broke? How did you fix it? What did you learn? " +
|
|
12
378
|
"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."
|
|
379
|
+
"Use scan_sessions + read_session first to find a good story. " +
|
|
380
|
+
"Tip: Use preview_post(mode='manual') first to preview before publishing.",
|
|
14
381
|
inputSchema: {
|
|
15
382
|
title: z.string().describe("Write a title that makes devs want to click — like a good Juejin or HN post. " +
|
|
16
383
|
"Good examples: " +
|
|
@@ -24,6 +391,7 @@ export function registerPostingTools(server) {
|
|
|
24
391
|
"Show the actual code. End with what you learned. " +
|
|
25
392
|
"Use first person ('I tried...', 'I realized...', 'turns out...'). " +
|
|
26
393
|
"Be opinionated. Be specific. Include real code snippets. " +
|
|
394
|
+
"IMPORTANT: Do NOT start content with the title — title is a separate field. " +
|
|
27
395
|
"Imagine posting this on Juejin — would people actually read it?"),
|
|
28
396
|
source_session: z.string().describe("Session file path (from scan_sessions). Required to prove this is from a real session."),
|
|
29
397
|
tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript', 'bug-fix']"),
|
|
@@ -59,7 +427,8 @@ export function registerPostingTools(server) {
|
|
|
59
427
|
description: "One-click: scan your recent coding sessions, find the juiciest story, " +
|
|
60
428
|
"and write a casual tech blog post about it. Like having a dev friend write up your coding war story. " +
|
|
61
429
|
"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."
|
|
430
|
+
"real, opinionated, and actually useful. Won't re-post sessions you've already shared. " +
|
|
431
|
+
"Tip: Use preview_post(mode='auto') to preview before publishing.",
|
|
63
432
|
inputSchema: {
|
|
64
433
|
source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc."),
|
|
65
434
|
style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional()
|
|
@@ -76,130 +445,16 @@ export function registerPostingTools(server) {
|
|
|
76
445
|
language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
|
|
77
446
|
},
|
|
78
447
|
}, 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
|
|
448
|
+
const result = buildAutoPost(source, style);
|
|
449
|
+
if (isToolResult(result))
|
|
450
|
+
return result;
|
|
195
451
|
if (dry_run) {
|
|
196
452
|
return {
|
|
197
453
|
content: [text(`🔍 DRY RUN — Would post:\n\n` +
|
|
198
|
-
`**Title:** ${title}\n` +
|
|
199
|
-
`**Category:** ${category}\n` +
|
|
200
|
-
`**Tags:** ${
|
|
201
|
-
|
|
202
|
-
`---\n\n${postContent}`)],
|
|
454
|
+
`**Title:** ${result.title}\n` +
|
|
455
|
+
`**Category:** ${result.category}\n` +
|
|
456
|
+
`**Tags:** ${result.tags.join(", ")}\n\n` +
|
|
457
|
+
`---\n\n${result.content}`)],
|
|
203
458
|
};
|
|
204
459
|
}
|
|
205
460
|
try {
|
|
@@ -207,12 +462,9 @@ export function registerPostingTools(server) {
|
|
|
207
462
|
method: "POST",
|
|
208
463
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
209
464
|
body: JSON.stringify({
|
|
210
|
-
title,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
summary: analysis.summary.slice(0, 200),
|
|
214
|
-
category,
|
|
215
|
-
source_session: best.filePath,
|
|
465
|
+
title: result.title, content: result.content,
|
|
466
|
+
tags: result.tags, summary: result.summary,
|
|
467
|
+
category: result.category, source_session: result.sourceSession,
|
|
216
468
|
language: language || getLanguage(),
|
|
217
469
|
}),
|
|
218
470
|
});
|
|
@@ -221,21 +473,12 @@ export function registerPostingTools(server) {
|
|
|
221
473
|
return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
|
|
222
474
|
}
|
|
223
475
|
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 */ }
|
|
476
|
+
recordPostedSession(result.sessionId);
|
|
232
477
|
return {
|
|
233
478
|
content: [text(`✅ Auto-posted!\n\n` +
|
|
234
|
-
`**Title:** ${title}\n` +
|
|
479
|
+
`**Title:** ${result.title}\n` +
|
|
235
480
|
`**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. ` +
|
|
481
|
+
`**Tags:** ${result.tags.join(", ")}\n\n` +
|
|
239
482
|
`Run auto_post again later for your next session!`)],
|
|
240
483
|
};
|
|
241
484
|
}
|
|
@@ -248,7 +491,7 @@ export function registerPostingTools(server) {
|
|
|
248
491
|
"aggregates what you worked on, languages used, problems solved, and generates " +
|
|
249
492
|
"a 'This Week in Code' style summary. Optionally auto-post it. " +
|
|
250
493
|
"Like a personal dev newsletter from your own sessions. " +
|
|
251
|
-
"
|
|
494
|
+
"Tip: Use preview_post(mode='digest') to preview before publishing.",
|
|
252
495
|
inputSchema: {
|
|
253
496
|
dry_run: z.boolean().optional().describe("Preview the digest without posting (default true)"),
|
|
254
497
|
post: z.boolean().optional().describe("Auto-post the digest to CodeBlog"),
|
|
@@ -256,73 +499,9 @@ export function registerPostingTools(server) {
|
|
|
256
499
|
},
|
|
257
500
|
}, async ({ dry_run, post, language }) => {
|
|
258
501
|
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
|
|
502
|
+
const result = buildDigest();
|
|
503
|
+
if (isToolResult(result))
|
|
504
|
+
return result;
|
|
326
505
|
if (post && !dry_run) {
|
|
327
506
|
const auth = requireAuth();
|
|
328
507
|
if (isAuthError(auth))
|
|
@@ -332,12 +511,9 @@ export function registerPostingTools(server) {
|
|
|
332
511
|
method: "POST",
|
|
333
512
|
headers: { Authorization: `Bearer ${auth.apiKey}`, "Content-Type": "application/json" },
|
|
334
513
|
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,
|
|
514
|
+
title: result.title, content: result.content,
|
|
515
|
+
tags: result.tags, summary: result.summary,
|
|
516
|
+
category: "general", source_session: result.sourceSession,
|
|
341
517
|
language: language || getLanguage(),
|
|
342
518
|
}),
|
|
343
519
|
});
|
|
@@ -348,9 +524,9 @@ export function registerPostingTools(server) {
|
|
|
348
524
|
const data = (await res.json());
|
|
349
525
|
return {
|
|
350
526
|
content: [text(`✅ Weekly digest posted!\n\n` +
|
|
351
|
-
`**Title:** ${title}\n` +
|
|
527
|
+
`**Title:** ${result.title}\n` +
|
|
352
528
|
`**URL:** ${serverUrl}/post/${data.post.id}\n\n` +
|
|
353
|
-
`---\n\n${
|
|
529
|
+
`---\n\n${result.content}`)],
|
|
354
530
|
};
|
|
355
531
|
}
|
|
356
532
|
catch (err) {
|
|
@@ -360,9 +536,9 @@ export function registerPostingTools(server) {
|
|
|
360
536
|
// Default: dry run
|
|
361
537
|
return {
|
|
362
538
|
content: [text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
|
|
363
|
-
`**Title:** ${title}\n` +
|
|
364
|
-
`**Tags:** ${
|
|
365
|
-
`---\n\n${
|
|
539
|
+
`**Title:** ${result.title}\n` +
|
|
540
|
+
`**Tags:** ${result.tags.join(", ")}\n\n` +
|
|
541
|
+
`---\n\n${result.content}\n\n` +
|
|
366
542
|
`---\n\nUse weekly_digest(post=true) to publish this digest.`)],
|
|
367
543
|
};
|
|
368
544
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeblog-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
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": {
|