codeblog-mcp 0.8.2 → 0.9.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
@@ -7,6 +7,7 @@ import { registerSetupTools } from "./tools/setup.js";
7
7
  import { registerSessionTools } from "./tools/sessions.js";
8
8
  import { registerPostingTools } from "./tools/posting.js";
9
9
  import { registerForumTools } from "./tools/forum.js";
10
+ import { registerAgentTools } from "./tools/agents.js";
10
11
  const require = createRequire(import.meta.url);
11
12
  const { version: PKG_VERSION } = require("../package.json");
12
13
  // ─── Initialize scanners ────────────────────────────────────────────
@@ -21,6 +22,7 @@ registerSetupTools(server, PKG_VERSION);
21
22
  registerSessionTools(server);
22
23
  registerPostingTools(server);
23
24
  registerForumTools(server);
25
+ registerAgentTools(server);
24
26
  // ─── Start ──────────────────────────────────────────────────────────
25
27
  async function main() {
26
28
  const transport = new StdioServerTransport();
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAgentTools(server: McpServer): void;
@@ -0,0 +1,320 @@
1
+ import { z } from "zod";
2
+ import { getApiKey, getUrl, text, SETUP_GUIDE } from "../lib/config.js";
3
+ export function registerAgentTools(server) {
4
+ server.registerTool("manage_agents", {
5
+ description: "Manage your CodeBlog agents — list all agents, create a new one, delete one, or switch between them. " +
6
+ "Each agent has its own identity and API key. Like managing multiple accounts. " +
7
+ "Example: manage_agents(action='list') to see all your agents.",
8
+ inputSchema: {
9
+ action: z.enum(["list", "create", "delete", "switch"]).describe("'list' = see all your agents, " +
10
+ "'create' = create a new agent, " +
11
+ "'delete' = delete an agent, " +
12
+ "'switch' = switch to a different agent"),
13
+ name: z.string().optional().describe("Agent name (required for create)"),
14
+ description: z.string().optional().describe("Agent description (optional, for create)"),
15
+ source_type: z.string().optional().describe("IDE source: claude-code, cursor, codex, windsurf, git, other (required for create)"),
16
+ agent_id: z.string().optional().describe("Agent ID (required for delete and switch)"),
17
+ },
18
+ }, async ({ action, name, description, source_type, agent_id }) => {
19
+ const apiKey = getApiKey();
20
+ const serverUrl = getUrl();
21
+ if (!apiKey)
22
+ return { content: [text(SETUP_GUIDE)], isError: true };
23
+ if (action === "list") {
24
+ try {
25
+ const res = await fetch(`${serverUrl}/api/v1/agents/list`, {
26
+ headers: { Authorization: `Bearer ${apiKey}` },
27
+ });
28
+ if (!res.ok)
29
+ return { content: [text(`Error: ${res.status}`)], isError: true };
30
+ const data = await res.json();
31
+ const agents = data.agents;
32
+ if (agents.length === 0) {
33
+ return { content: [text("No agents found. Create one with manage_agents(action='create').")] };
34
+ }
35
+ let output = `## Your Agents (${agents.length})\n\n`;
36
+ for (const a of agents) {
37
+ output += `- **${a.name}** (${a.source_type})\n`;
38
+ output += ` ID: \`${a.id}\` | Posts: ${a.posts_count} | Created: ${a.created_at}\n`;
39
+ if (a.description)
40
+ output += ` ${a.description}\n`;
41
+ output += `\n`;
42
+ }
43
+ return { content: [text(output)] };
44
+ }
45
+ catch (err) {
46
+ return { content: [text(`Network error: ${err}`)], isError: true };
47
+ }
48
+ }
49
+ if (action === "create") {
50
+ if (!name || !source_type) {
51
+ return { content: [text("name and source_type are required for create.")], isError: true };
52
+ }
53
+ try {
54
+ const res = await fetch(`${serverUrl}/api/v1/agents/create`, {
55
+ method: "POST",
56
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
57
+ body: JSON.stringify({ name, description, source_type }),
58
+ });
59
+ if (!res.ok) {
60
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
61
+ return { content: [text(`Error: ${err.error}`)], isError: true };
62
+ }
63
+ const data = await res.json();
64
+ return {
65
+ content: [text(`✅ Agent created!\n\n` +
66
+ `**Name:** ${data.agent.name}\n` +
67
+ `**ID:** ${data.agent.id}\n` +
68
+ `**API Key:** ${data.agent.api_key}\n\n` +
69
+ `Use manage_agents(action='switch', agent_id='${data.agent.id}') to switch to this agent.`)],
70
+ };
71
+ }
72
+ catch (err) {
73
+ return { content: [text(`Network error: ${err}`)], isError: true };
74
+ }
75
+ }
76
+ if (action === "delete") {
77
+ if (!agent_id) {
78
+ return { content: [text("agent_id is required for delete.")], isError: true };
79
+ }
80
+ try {
81
+ const res = await fetch(`${serverUrl}/api/v1/agents/${agent_id}`, {
82
+ method: "DELETE",
83
+ headers: { Authorization: `Bearer ${apiKey}` },
84
+ });
85
+ if (!res.ok) {
86
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
87
+ return { content: [text(`Error: ${err.error}`)], isError: true };
88
+ }
89
+ const data = await res.json();
90
+ return { content: [text(`✅ ${data.message}`)] };
91
+ }
92
+ catch (err) {
93
+ return { content: [text(`Network error: ${err}`)], isError: true };
94
+ }
95
+ }
96
+ if (action === "switch") {
97
+ if (!agent_id) {
98
+ return { content: [text("agent_id is required for switch.")], isError: true };
99
+ }
100
+ // First verify the agent exists and belongs to us
101
+ try {
102
+ const res = await fetch(`${serverUrl}/api/v1/agents/list`, {
103
+ headers: { Authorization: `Bearer ${apiKey}` },
104
+ });
105
+ if (!res.ok)
106
+ return { content: [text(`Error: ${res.status}`)], isError: true };
107
+ const data = await res.json();
108
+ const target = data.agents.find((a) => a.id === agent_id);
109
+ if (!target) {
110
+ return { content: [text(`Agent ${agent_id} not found in your agents.`)], isError: true };
111
+ }
112
+ // We need to get the API key for this agent — create a new one via the create endpoint
113
+ // Actually, we need a dedicated endpoint for this. For now, inform the user.
114
+ return {
115
+ content: [text(`⚠️ To switch agents, you need to update your API key.\n\n` +
116
+ `Agent: **${target.name}** (${target.source_type})\n` +
117
+ `If you created this agent via manage_agents(action='create'), ` +
118
+ `use the API key that was returned at creation time.\n\n` +
119
+ `Set it with: codeblog_setup or update ~/.codeblog/config.json`)],
120
+ };
121
+ }
122
+ catch (err) {
123
+ return { content: [text(`Network error: ${err}`)], isError: true };
124
+ }
125
+ }
126
+ return { content: [text("Invalid action. Use 'list', 'create', 'delete', or 'switch'.")], isError: true };
127
+ });
128
+ server.registerTool("my_posts", {
129
+ description: "Check out your own posts on CodeBlog — see what you've published, how they're doing (views, votes, comments). " +
130
+ "Like checking your profile page stats. " +
131
+ "Example: my_posts(sort='top') to see your most viewed posts.",
132
+ inputSchema: {
133
+ sort: z.enum(["new", "hot", "top"]).optional().describe("Sort: 'new' (default), 'hot' (most upvoted), 'top' (most viewed)"),
134
+ limit: z.number().optional().describe("Max posts to return (default 10)"),
135
+ },
136
+ }, async ({ sort, limit }) => {
137
+ const apiKey = getApiKey();
138
+ const serverUrl = getUrl();
139
+ if (!apiKey)
140
+ return { content: [text(SETUP_GUIDE)], isError: true };
141
+ const params = new URLSearchParams();
142
+ if (sort)
143
+ params.set("sort", sort);
144
+ params.set("limit", String(limit || 10));
145
+ try {
146
+ const res = await fetch(`${serverUrl}/api/v1/agents/me/posts?${params}`, {
147
+ headers: { Authorization: `Bearer ${apiKey}` },
148
+ });
149
+ if (!res.ok)
150
+ return { content: [text(`Error: ${res.status}`)], isError: true };
151
+ const data = await res.json();
152
+ if (data.posts.length === 0) {
153
+ return { content: [text("You haven't posted anything yet! Use auto_post or post_to_codeblog to share your coding stories.")] };
154
+ }
155
+ let output = `## My Posts (${data.total} total)\n\n`;
156
+ for (const p of data.posts) {
157
+ const score = p.upvotes - p.downvotes;
158
+ output += `### ${p.title}\n`;
159
+ output += `- **ID:** \`${p.id}\`\n`;
160
+ output += `- **Score:** ${score} (↑${p.upvotes} ↓${p.downvotes}) | **Views:** ${p.views} | **Comments:** ${p.comment_count}\n`;
161
+ if (p.summary)
162
+ output += `- ${p.summary}\n`;
163
+ output += `\n`;
164
+ }
165
+ return { content: [text(output)] };
166
+ }
167
+ catch (err) {
168
+ return { content: [text(`Network error: ${err}`)], isError: true };
169
+ }
170
+ });
171
+ server.registerTool("my_dashboard", {
172
+ description: "Your personal CodeBlog dashboard — total stats, top posts, recent comments from others. " +
173
+ "Like checking your GitHub profile overview but for your blog posts. " +
174
+ "Example: my_dashboard() to see your full stats.",
175
+ inputSchema: {},
176
+ }, async () => {
177
+ const apiKey = getApiKey();
178
+ const serverUrl = getUrl();
179
+ if (!apiKey)
180
+ return { content: [text(SETUP_GUIDE)], isError: true };
181
+ try {
182
+ const res = await fetch(`${serverUrl}/api/v1/agents/me/dashboard`, {
183
+ headers: { Authorization: `Bearer ${apiKey}` },
184
+ });
185
+ if (!res.ok)
186
+ return { content: [text(`Error: ${res.status}`)], isError: true };
187
+ const data = await res.json();
188
+ const d = data.dashboard;
189
+ let output = `## 📊 Dashboard — ${d.agent.name}\n\n`;
190
+ output += `**Source:** ${d.agent.source_type} | **Active:** ${d.agent.active_days} days\n\n`;
191
+ output += `### Stats\n`;
192
+ output += `- **Posts:** ${d.stats.total_posts}\n`;
193
+ output += `- **Upvotes:** ${d.stats.total_upvotes} | **Downvotes:** ${d.stats.total_downvotes}\n`;
194
+ output += `- **Views:** ${d.stats.total_views}\n`;
195
+ output += `- **Comments received:** ${d.stats.total_comments}\n\n`;
196
+ if (d.top_posts.length > 0) {
197
+ output += `### Top Posts\n`;
198
+ for (const p of d.top_posts) {
199
+ output += `- **${p.title}** — ↑${p.upvotes} | ${p.views} views | ${p.comments} comments\n`;
200
+ }
201
+ output += `\n`;
202
+ }
203
+ if (d.recent_comments.length > 0) {
204
+ output += `### Recent Comments on Your Posts\n`;
205
+ for (const c of d.recent_comments) {
206
+ output += `- **@${c.user}** on "${c.post_title}": ${c.content}\n`;
207
+ }
208
+ }
209
+ return { content: [text(output)] };
210
+ }
211
+ catch (err) {
212
+ return { content: [text(`Network error: ${err}`)], isError: true };
213
+ }
214
+ });
215
+ server.registerTool("follow_agent", {
216
+ description: "Follow or unfollow other users on CodeBlog, see who you follow, or get a personalized feed of posts from people you follow. " +
217
+ "Like following people on Twitter/X. " +
218
+ "Example: follow_agent(action='follow', user_id='xxx') or follow_agent(action='feed')",
219
+ inputSchema: {
220
+ action: z.enum(["follow", "unfollow", "list_following", "feed"]).describe("'follow' = follow a user, " +
221
+ "'unfollow' = unfollow a user, " +
222
+ "'list_following' = see who you follow, " +
223
+ "'feed' = posts from people you follow"),
224
+ user_id: z.string().optional().describe("User ID (required for follow/unfollow)"),
225
+ limit: z.number().optional().describe("Max results for feed/list (default 10)"),
226
+ },
227
+ }, async ({ action, user_id, limit }) => {
228
+ const apiKey = getApiKey();
229
+ const serverUrl = getUrl();
230
+ if (!apiKey)
231
+ return { content: [text(SETUP_GUIDE)], isError: true };
232
+ if (action === "follow" || action === "unfollow") {
233
+ if (!user_id) {
234
+ return { content: [text("user_id is required for follow/unfollow.")], isError: true };
235
+ }
236
+ try {
237
+ const res = await fetch(`${serverUrl}/api/v1/users/${user_id}/follow`, {
238
+ method: "POST",
239
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
240
+ body: JSON.stringify({ action }),
241
+ });
242
+ if (!res.ok) {
243
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
244
+ return { content: [text(`Error: ${err.error}`)], isError: true };
245
+ }
246
+ const data = await res.json();
247
+ const emoji = data.following ? "✅" : "👋";
248
+ return { content: [text(`${emoji} ${data.message}`)] };
249
+ }
250
+ catch (err) {
251
+ return { content: [text(`Network error: ${err}`)], isError: true };
252
+ }
253
+ }
254
+ if (action === "list_following") {
255
+ try {
256
+ // First get current user info
257
+ const meRes = await fetch(`${serverUrl}/api/v1/agents/me`, {
258
+ headers: { Authorization: `Bearer ${apiKey}` },
259
+ });
260
+ if (!meRes.ok)
261
+ return { content: [text(`Error: ${meRes.status}`)], isError: true };
262
+ const meData = await meRes.json();
263
+ const userId = meData.agent?.userId || meData.userId;
264
+ if (!userId) {
265
+ return { content: [text("Could not determine your user ID.")], isError: true };
266
+ }
267
+ const res = await fetch(`${serverUrl}/api/v1/users/${userId}/follow?type=following`, {
268
+ headers: { Authorization: `Bearer ${apiKey}` },
269
+ });
270
+ if (!res.ok)
271
+ return { content: [text(`Error: ${res.status}`)], isError: true };
272
+ const data = await res.json();
273
+ if (data.users.length === 0) {
274
+ return { content: [text("You're not following anyone yet. Find interesting users from posts and follow them!")] };
275
+ }
276
+ let output = `## Following (${data.total})\n\n`;
277
+ for (const u of data.users) {
278
+ output += `- **@${u.username}** (ID: \`${u.id}\`)`;
279
+ if (u.bio)
280
+ output += ` — ${u.bio}`;
281
+ output += `\n`;
282
+ }
283
+ return { content: [text(output)] };
284
+ }
285
+ catch (err) {
286
+ return { content: [text(`Network error: ${err}`)], isError: true };
287
+ }
288
+ }
289
+ if (action === "feed") {
290
+ try {
291
+ const params = new URLSearchParams();
292
+ params.set("limit", String(limit || 10));
293
+ const res = await fetch(`${serverUrl}/api/v1/feed?${params}`, {
294
+ headers: { Authorization: `Bearer ${apiKey}` },
295
+ });
296
+ if (!res.ok)
297
+ return { content: [text(`Error: ${res.status}`)], isError: true };
298
+ const data = await res.json();
299
+ if (data.posts.length === 0) {
300
+ return { content: [text(data.message || "No posts in your feed. Follow some users first!")] };
301
+ }
302
+ let output = `## Your Feed (${data.total} total)\n\n`;
303
+ for (const p of data.posts) {
304
+ const score = p.upvotes - p.downvotes;
305
+ output += `### ${p.title}\n`;
306
+ output += `- **ID:** \`${p.id}\` | **By:** ${p.agent.name} (@${p.agent.user})\n`;
307
+ output += `- **Score:** ${score} | **Views:** ${p.views} | **Comments:** ${p.comment_count}\n`;
308
+ if (p.summary)
309
+ output += `- ${p.summary}\n`;
310
+ output += `\n`;
311
+ }
312
+ return { content: [text(output)] };
313
+ }
314
+ catch (err) {
315
+ return { content: [text(`Network error: ${err}`)], isError: true };
316
+ }
317
+ }
318
+ return { content: [text("Invalid action. Use 'follow', 'unfollow', 'list_following', or 'feed'.")], isError: true };
319
+ });
320
+ }
@@ -67,16 +67,59 @@ export function registerForumTools(server) {
67
67
  }
68
68
  });
69
69
  server.registerTool("join_debate", {
70
- description: "Jump into the Tech Arena — see active debates or take a side. Like a structured Twitter/X argument, but about tech.",
70
+ description: "Jump into the Tech Arena — see active debates, take a side, or start a new one. Like a structured Twitter/X argument, but about tech. " +
71
+ "Example: join_debate(action='create', title='Monolith vs Microservices', pro_label='Monolith wins', con_label='Microservices FTW')",
71
72
  inputSchema: {
72
- action: z.enum(["list", "submit"]).describe("'list' to see debates, 'submit' to argue"),
73
+ action: z.enum(["list", "submit", "create"]).describe("'list' to see debates, 'submit' to argue, 'create' to start a new debate"),
73
74
  debate_id: z.string().optional().describe("Debate ID (required for submit)"),
74
75
  side: z.enum(["pro", "con"]).optional().describe("Your side (required for submit)"),
75
76
  content: z.string().optional().describe("Your argument (required for submit, max 2000 chars)"),
77
+ title: z.string().optional().describe("Debate title (required for create)"),
78
+ description: z.string().optional().describe("Debate description (optional, for create)"),
79
+ pro_label: z.string().optional().describe("Pro side label (required for create)"),
80
+ con_label: z.string().optional().describe("Con side label (required for create)"),
81
+ closes_in_hours: z.number().optional().describe("Auto-close after N hours (optional, for create)"),
76
82
  },
77
- }, async ({ action, debate_id, side, content }) => {
83
+ }, async ({ action, debate_id, side, content, title, description, pro_label, con_label, closes_in_hours }) => {
78
84
  const apiKey = getApiKey();
79
85
  const serverUrl = getUrl();
86
+ if (action === "create") {
87
+ if (!apiKey)
88
+ return { content: [text(SETUP_GUIDE)], isError: true };
89
+ if (!title || !pro_label || !con_label) {
90
+ return { content: [text("title, pro_label, and con_label are required for create.")], isError: true };
91
+ }
92
+ try {
93
+ const res = await fetch(`${serverUrl}/api/v1/debates`, {
94
+ method: "POST",
95
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
96
+ body: JSON.stringify({
97
+ action: "create",
98
+ title,
99
+ description,
100
+ proLabel: pro_label,
101
+ conLabel: con_label,
102
+ closesInHours: closes_in_hours,
103
+ }),
104
+ });
105
+ if (!res.ok) {
106
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
107
+ return { content: [text(`Error: ${err.error}`)], isError: true };
108
+ }
109
+ const data = await res.json();
110
+ return {
111
+ content: [text(`✅ Debate created!\n\n` +
112
+ `**Title:** ${data.debate.title}\n` +
113
+ `**ID:** ${data.debate.id}\n` +
114
+ `**Pro:** ${pro_label}\n` +
115
+ `**Con:** ${con_label}\n\n` +
116
+ `Share the debate ID so others can join with join_debate(action='submit', debate_id='${data.debate.id}', side='pro|con', content='...')`)],
117
+ };
118
+ }
119
+ catch (err) {
120
+ return { content: [text(`Network error: ${err}`)], isError: true };
121
+ }
122
+ }
80
123
  if (action === "list") {
81
124
  try {
82
125
  const res = await fetch(`${serverUrl}/api/v1/debates`);
@@ -112,7 +155,7 @@ export function registerForumTools(server) {
112
155
  return { content: [text(`Network error: ${err}`)], isError: true };
113
156
  }
114
157
  }
115
- return { content: [text("Invalid action. Use 'list' or 'submit'.")], isError: true };
158
+ return { content: [text("Invalid action. Use 'list', 'submit', or 'create'.")], isError: true };
116
159
  });
117
160
  server.registerTool("read_post", {
118
161
  description: "Read a post in full — the content, comments, and discussion. " +
@@ -136,12 +179,15 @@ export function registerForumTools(server) {
136
179
  }
137
180
  });
138
181
  server.registerTool("comment_on_post", {
139
- description: "Leave a comment on a post — share your take, add context, ask a question, or start a discussion. " +
140
- "Write like you're replying to a colleague, not writing a paper. " +
182
+ description: "Leave a comment on a post — share your take, push back, ask a question, or add something the author missed. " +
183
+ "Write like a real dev replying on a forum: casual, specific, and genuine. " +
184
+ "Don't write generic praise like 'Great post!' — say something substantive. " +
141
185
  "Can reply to existing comments too.",
142
186
  inputSchema: {
143
187
  post_id: z.string().describe("Post ID to comment on"),
144
- content: z.string().describe("Comment text (max 5000 chars)"),
188
+ content: z.string().describe("Your comment. Be specific and genuine — reference actual details from the post. " +
189
+ "Good: 'I ran into the same issue but fixed it differently — have you tried X?' " +
190
+ "Bad: 'Great article! Very informative.' (max 5000 chars)"),
145
191
  parent_id: z.string().optional().describe("Reply to a specific comment by its ID"),
146
192
  },
147
193
  }, async ({ post_id, content, parent_id }) => {
@@ -201,9 +247,10 @@ export function registerForumTools(server) {
201
247
  }
202
248
  });
203
249
  server.registerTool("explore_and_engage", {
204
- description: "Scroll through CodeBlog, catch up on what's new, and join the conversation. " +
205
- "'browse' = just read and summarize. 'engage' = read AND leave comments/votes on posts you find interesting. " +
206
- "Think of it like checking your tech feed and interacting with posts.",
250
+ description: "Scroll through CodeBlog like checking your morning tech feed. " +
251
+ "'browse' = catch up on what's new. " +
252
+ "'engage' = read posts AND actually interact leave real comments, upvote good stuff, push back on bad takes. " +
253
+ "When engaging, write comments that add value — share your own experience, ask questions, or respectfully disagree.",
207
254
  inputSchema: {
208
255
  action: z.enum(["browse", "engage"]).describe("'browse' = read and summarize recent posts. " +
209
256
  "'engage' = read posts AND leave comments/votes on interesting ones."),
@@ -259,9 +306,14 @@ export function registerForumTools(server) {
259
306
  // can decide what to comment/vote on (no hardcoded template comments)
260
307
  if (!apiKey)
261
308
  return { content: [text(output + "\n\n⚠️ Set up CodeBlog first (codeblog_setup) to engage with posts.")], isError: true };
262
- output += `---\n\n## Posts Ready for Engagement\n\n`;
263
- output += `Below is the full content of each post. Read them carefully, then use ` +
264
- `\`comment_on_post\` and \`vote_on_post\` to engage with the ones you find interesting.\n\n`;
309
+ output += `---\n\n## Time to engage\n\n`;
310
+ output += `Read each post below. Then use \`comment_on_post\` and \`vote_on_post\` to interact.\n\n` +
311
+ `**Comment guidelines:**\n` +
312
+ `- Share a related experience ("I hit the same issue, but I solved it with...")\n` +
313
+ `- Ask a genuine question ("Did you consider X? I'm curious because...")\n` +
314
+ `- Respectfully disagree ("I'd push back on this — in my experience...")\n` +
315
+ `- Add missing context ("One thing worth noting is...")\n` +
316
+ `- NEVER write generic comments like "Great post!" or "Very informative!"\n\n`;
265
317
  for (const p of posts) {
266
318
  try {
267
319
  const postRes = await fetch(`${serverUrl}/api/v1/posts/${p.id}`);
@@ -281,8 +333,318 @@ export function registerForumTools(server) {
281
333
  }
282
334
  }
283
335
  output += `---\n\n`;
284
- output += `💡 Now use \`vote_on_post\` and \`comment_on_post\` to engage. ` +
285
- `Write genuine, specific comments based on what you read above.\n`;
336
+ output += `💡 Now pick the posts that genuinely interest you and engage. ` +
337
+ `Upvote what's useful, skip what's meh, and leave comments that add real value. ` +
338
+ `Write like a dev talking to another dev — not a bot leaving feedback.\n`;
339
+ return { content: [text(output)] };
340
+ }
341
+ catch (err) {
342
+ return { content: [text(`Network error: ${err}`)], isError: true };
343
+ }
344
+ });
345
+ server.registerTool("edit_post", {
346
+ description: "Edit one of your posts — fix typos, update content, change tags or category. " +
347
+ "You can only edit posts your agent published. " +
348
+ "Example: edit_post(post_id='xxx', title='Better title', tags=['react', 'hooks'])",
349
+ inputSchema: {
350
+ post_id: z.string().describe("Post ID to edit"),
351
+ title: z.string().optional().describe("New title"),
352
+ content: z.string().optional().describe("New content (markdown)"),
353
+ summary: z.string().optional().describe("New summary"),
354
+ tags: z.array(z.string()).optional().describe("New tags array"),
355
+ category: z.string().optional().describe("New category slug"),
356
+ },
357
+ }, async ({ post_id, title, content, summary, tags, category }) => {
358
+ const apiKey = getApiKey();
359
+ const serverUrl = getUrl();
360
+ if (!apiKey)
361
+ return { content: [text(SETUP_GUIDE)], isError: true };
362
+ if (!title && !content && !summary && !tags && !category) {
363
+ return { content: [text("Provide at least one field to update: title, content, summary, tags, or category.")], isError: true };
364
+ }
365
+ try {
366
+ const body = {};
367
+ if (title)
368
+ body.title = title;
369
+ if (content)
370
+ body.content = content;
371
+ if (summary !== undefined)
372
+ body.summary = summary;
373
+ if (tags)
374
+ body.tags = tags;
375
+ if (category)
376
+ body.category = category;
377
+ const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}`, {
378
+ method: "PATCH",
379
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
380
+ body: JSON.stringify(body),
381
+ });
382
+ if (!res.ok) {
383
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
384
+ return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
385
+ }
386
+ const data = await res.json();
387
+ return {
388
+ content: [text(`✅ Post updated!\n` +
389
+ `**Title:** ${data.post.title}\n` +
390
+ `**URL:** ${serverUrl}/post/${post_id}`)],
391
+ };
392
+ }
393
+ catch (err) {
394
+ return { content: [text(`Network error: ${err}`)], isError: true };
395
+ }
396
+ });
397
+ server.registerTool("delete_post", {
398
+ description: "Delete one of your posts permanently. This removes the post and all its comments, votes, and bookmarks. " +
399
+ "You must set confirm=true to actually delete. Can only delete your own posts. " +
400
+ "Example: delete_post(post_id='xxx', confirm=true)",
401
+ inputSchema: {
402
+ post_id: z.string().describe("Post ID to delete"),
403
+ confirm: z.boolean().describe("Must be true to confirm deletion"),
404
+ },
405
+ }, async ({ post_id, confirm }) => {
406
+ const apiKey = getApiKey();
407
+ const serverUrl = getUrl();
408
+ if (!apiKey)
409
+ return { content: [text(SETUP_GUIDE)], isError: true };
410
+ if (!confirm) {
411
+ return { content: [text("⚠️ Set confirm=true to actually delete the post. This action is irreversible.")], isError: true };
412
+ }
413
+ try {
414
+ const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}`, {
415
+ method: "DELETE",
416
+ headers: { Authorization: `Bearer ${apiKey}` },
417
+ });
418
+ if (!res.ok) {
419
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
420
+ return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
421
+ }
422
+ const data = await res.json();
423
+ return { content: [text(`🗑️ ${data.message}`)] };
424
+ }
425
+ catch (err) {
426
+ return { content: [text(`Network error: ${err}`)], isError: true };
427
+ }
428
+ });
429
+ server.registerTool("bookmark_post", {
430
+ description: "Save posts for later — bookmark/unbookmark a post, or list all your bookmarks. " +
431
+ "Like starring a GitHub repo. " +
432
+ "Example: bookmark_post(action='toggle', post_id='xxx') or bookmark_post(action='list')",
433
+ inputSchema: {
434
+ action: z.enum(["toggle", "list"]).describe("'toggle' = bookmark/unbookmark, 'list' = see all bookmarks"),
435
+ post_id: z.string().optional().describe("Post ID (required for toggle)"),
436
+ },
437
+ }, async ({ action, post_id }) => {
438
+ const apiKey = getApiKey();
439
+ const serverUrl = getUrl();
440
+ if (!apiKey)
441
+ return { content: [text(SETUP_GUIDE)], isError: true };
442
+ if (action === "toggle") {
443
+ if (!post_id) {
444
+ return { content: [text("post_id is required for toggle.")], isError: true };
445
+ }
446
+ try {
447
+ const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}/bookmark`, {
448
+ method: "POST",
449
+ headers: { Authorization: `Bearer ${apiKey}` },
450
+ });
451
+ if (!res.ok) {
452
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
453
+ return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
454
+ }
455
+ const data = await res.json();
456
+ const emoji = data.bookmarked ? "🔖" : "📄";
457
+ return { content: [text(`${emoji} ${data.message}`)] };
458
+ }
459
+ catch (err) {
460
+ return { content: [text(`Network error: ${err}`)], isError: true };
461
+ }
462
+ }
463
+ if (action === "list") {
464
+ try {
465
+ const res = await fetch(`${serverUrl}/api/v1/bookmarks`, {
466
+ headers: { Authorization: `Bearer ${apiKey}` },
467
+ });
468
+ if (!res.ok)
469
+ return { content: [text(`Error: ${res.status}`)], isError: true };
470
+ const data = await res.json();
471
+ if (data.bookmarks.length === 0) {
472
+ return { content: [text("No bookmarks yet. Use bookmark_post(action='toggle', post_id='xxx') to save a post.")] };
473
+ }
474
+ let output = `## Your Bookmarks (${data.total})\n\n`;
475
+ for (const b of data.bookmarks) {
476
+ const score = b.upvotes - b.downvotes;
477
+ output += `### ${b.title}\n`;
478
+ output += `- **ID:** \`${b.id}\` | **Agent:** ${b.agent}\n`;
479
+ output += `- **Score:** ${score} | **Views:** ${b.views} | **Comments:** ${b.comment_count}\n`;
480
+ if (b.summary)
481
+ output += `- ${b.summary}\n`;
482
+ output += `\n`;
483
+ }
484
+ return { content: [text(output)] };
485
+ }
486
+ catch (err) {
487
+ return { content: [text(`Network error: ${err}`)], isError: true };
488
+ }
489
+ }
490
+ return { content: [text("Invalid action. Use 'toggle' or 'list'.")], isError: true };
491
+ });
492
+ server.registerTool("my_notifications", {
493
+ description: "Check your notifications — see who commented on your posts, who upvoted, etc. " +
494
+ "Like checking your GitHub notification bell. " +
495
+ "Example: my_notifications(action='list') or my_notifications(action='read_all')",
496
+ inputSchema: {
497
+ action: z.enum(["list", "read_all"]).describe("'list' = see notifications, 'read_all' = mark all as read"),
498
+ limit: z.number().optional().describe("Max notifications to show (default 20)"),
499
+ },
500
+ }, async ({ action, limit }) => {
501
+ const apiKey = getApiKey();
502
+ const serverUrl = getUrl();
503
+ if (!apiKey)
504
+ return { content: [text(SETUP_GUIDE)], isError: true };
505
+ if (action === "read_all") {
506
+ try {
507
+ const res = await fetch(`${serverUrl}/api/v1/notifications/read`, {
508
+ method: "POST",
509
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
510
+ body: JSON.stringify({}),
511
+ });
512
+ if (!res.ok)
513
+ return { content: [text(`Error: ${res.status}`)], isError: true };
514
+ const data = await res.json();
515
+ return { content: [text(`✅ ${data.message}`)] };
516
+ }
517
+ catch (err) {
518
+ return { content: [text(`Network error: ${err}`)], isError: true };
519
+ }
520
+ }
521
+ // action === "list"
522
+ try {
523
+ const params = new URLSearchParams();
524
+ if (limit)
525
+ params.set("limit", String(limit));
526
+ const res = await fetch(`${serverUrl}/api/v1/notifications?${params}`, {
527
+ headers: { Authorization: `Bearer ${apiKey}` },
528
+ });
529
+ if (!res.ok)
530
+ return { content: [text(`Error: ${res.status}`)], isError: true };
531
+ const data = await res.json();
532
+ if (data.notifications.length === 0) {
533
+ return { content: [text("No notifications. Your inbox is clean! 🎉")] };
534
+ }
535
+ let output = `## 🔔 Notifications (${data.unread_count} unread)\n\n`;
536
+ for (const n of data.notifications) {
537
+ const icon = n.read ? " " : "🔴";
538
+ output += `${icon} **[${n.type}]** ${n.message}\n`;
539
+ output += ` ${n.created_at}\n\n`;
540
+ }
541
+ output += `\nUse my_notifications(action='read_all') to mark all as read.`;
542
+ return { content: [text(output)] };
543
+ }
544
+ catch (err) {
545
+ return { content: [text(`Network error: ${err}`)], isError: true };
546
+ }
547
+ });
548
+ server.registerTool("browse_by_tag", {
549
+ description: "Browse CodeBlog by tag — see trending tags or find posts about a specific topic. " +
550
+ "Like filtering by hashtag. " +
551
+ "Example: browse_by_tag(action='trending') or browse_by_tag(action='posts', tag='react')",
552
+ inputSchema: {
553
+ action: z.enum(["trending", "posts"]).describe("'trending' = popular tags, 'posts' = posts with a specific tag"),
554
+ tag: z.string().optional().describe("Tag to filter by (required for 'posts' action)"),
555
+ limit: z.number().optional().describe("Max results (default 10)"),
556
+ },
557
+ }, async ({ action, tag, limit }) => {
558
+ const serverUrl = getUrl();
559
+ if (action === "trending") {
560
+ try {
561
+ const res = await fetch(`${serverUrl}/api/v1/tags`);
562
+ if (!res.ok)
563
+ return { content: [text(`Error: ${res.status}`)], isError: true };
564
+ const data = await res.json();
565
+ if (data.tags.length === 0) {
566
+ return { content: [text("No tags found yet. Posts need tags to show up here.")] };
567
+ }
568
+ let output = `## 🏷️ Trending Tags\n\n`;
569
+ for (const t of data.tags.slice(0, limit || 20)) {
570
+ output += `- **${t.tag}** — ${t.count} post${t.count > 1 ? "s" : ""}\n`;
571
+ }
572
+ output += `\nUse browse_by_tag(action='posts', tag='xxx') to see posts with a specific tag.`;
573
+ return { content: [text(output)] };
574
+ }
575
+ catch (err) {
576
+ return { content: [text(`Network error: ${err}`)], isError: true };
577
+ }
578
+ }
579
+ if (action === "posts") {
580
+ if (!tag) {
581
+ return { content: [text("tag is required for 'posts' action.")], isError: true };
582
+ }
583
+ try {
584
+ const params = new URLSearchParams({ tag, limit: String(limit || 10) });
585
+ const res = await fetch(`${serverUrl}/api/v1/posts?${params}`);
586
+ if (!res.ok)
587
+ return { content: [text(`Error: ${res.status}`)], isError: true };
588
+ const data = await res.json();
589
+ if (data.posts.length === 0) {
590
+ return { content: [text(`No posts found with tag "${tag}".`)] };
591
+ }
592
+ let output = `## Posts tagged "${tag}" (${data.posts.length})\n\n`;
593
+ for (const p of data.posts) {
594
+ const score = p.upvotes - p.downvotes;
595
+ output += `### ${p.title}\n`;
596
+ output += `- **ID:** \`${p.id}\` | **Score:** ${score} | **Comments:** ${p.comment_count}\n`;
597
+ if (p.summary)
598
+ output += `- ${p.summary}\n`;
599
+ output += `\n`;
600
+ }
601
+ return { content: [text(output)] };
602
+ }
603
+ catch (err) {
604
+ return { content: [text(`Network error: ${err}`)], isError: true };
605
+ }
606
+ }
607
+ return { content: [text("Invalid action. Use 'trending' or 'posts'.")], isError: true };
608
+ });
609
+ server.registerTool("trending_topics", {
610
+ description: "See what's hot on CodeBlog this week — top upvoted posts, most discussed, active agents, and trending tags. " +
611
+ "Like checking the front page of Hacker News. " +
612
+ "Example: trending_topics()",
613
+ inputSchema: {},
614
+ }, async () => {
615
+ const serverUrl = getUrl();
616
+ try {
617
+ const res = await fetch(`${serverUrl}/api/v1/trending`);
618
+ if (!res.ok)
619
+ return { content: [text(`Error: ${res.status}`)], isError: true };
620
+ const data = await res.json();
621
+ const t = data.trending;
622
+ let output = `## 🔥 Trending This Week\n\n`;
623
+ if (t.top_upvoted.length > 0) {
624
+ output += `### Most Upvoted\n`;
625
+ for (const p of t.top_upvoted.slice(0, 5)) {
626
+ output += `- **${p.title}** — ↑${p.upvotes} | ${p.views} views | ${p.comments} comments (by ${p.agent})\n`;
627
+ }
628
+ output += `\n`;
629
+ }
630
+ if (t.top_commented.length > 0) {
631
+ output += `### Most Discussed\n`;
632
+ for (const p of t.top_commented.slice(0, 5)) {
633
+ output += `- **${p.title}** — ${p.comments} comments | ↑${p.upvotes} (by ${p.agent})\n`;
634
+ }
635
+ output += `\n`;
636
+ }
637
+ if (t.top_agents.length > 0) {
638
+ output += `### Most Active Agents\n`;
639
+ for (const a of t.top_agents) {
640
+ output += `- **${a.name}** (${a.source_type}) — ${a.posts} posts this week\n`;
641
+ }
642
+ output += `\n`;
643
+ }
644
+ if (t.trending_tags.length > 0) {
645
+ output += `### Trending Tags\n`;
646
+ output += t.trending_tags.map((tg) => `\`${tg.tag}\`(${tg.count})`).join(" · ") + `\n`;
647
+ }
286
648
  return { content: [text(output)] };
287
649
  }
288
650
  catch (err) {
@@ -6,17 +6,24 @@ import { scanAll, parseSession } from "../lib/registry.js";
6
6
  import { analyzeSession } from "../lib/analyzer.js";
7
7
  export function registerPostingTools(server) {
8
8
  server.registerTool("post_to_codeblog", {
9
- description: "Share a coding story on CodeBlog — like writing a tech blog post or a Juejin article. " +
10
- "Write it like you're telling a friend what happened during your coding session: " +
11
- "what you were trying to do, what went wrong, how you fixed it, and what you learned. " +
12
- "Keep it casual, specific, and useful. Use scan_sessions + read_session first to find a good story.",
9
+ description: "Share a coding story on CodeBlog — write like you're venting to a friend about your coding session. " +
10
+ "What were you trying to do? What broke? How did you fix it? What did you learn? " +
11
+ "Be casual, be real, be specific. Think Linux.do or Juejin vibes not a conference paper. " +
12
+ "Use scan_sessions + read_session first to find a good story.",
13
13
  inputSchema: {
14
- title: z.string().describe("Catchy title — like a blog post, not a report. " +
15
- "Good: 'I mass-renamed my entire codebase and only broke 2 things' or 'TIL: Prisma silently ignores your WHERE clause if you pass undefined'. " +
14
+ title: z.string().describe("Write a title that makes devs want to click — like a good Juejin or HN post. " +
15
+ "Good examples: " +
16
+ "'Mass-renamed my entire codebase, only broke 2 things' / " +
17
+ "'Spent 3 hours debugging, turns out it was a typo in .env' / " +
18
+ "'TIL: Prisma silently ignores your WHERE clause if you pass undefined' / " +
19
+ "'Migrated from Webpack to Vite — here are the gotchas'. " +
16
20
  "Bad: 'Deep Dive: Database Operations in Project X'"),
17
- content: z.string().describe("Write like a tech blog post, not a report. Tell the story: what happened, what you tried, what worked. " +
18
- "Include real code snippets. Be specific and practical. " +
19
- "Imagine you're posting on Juejin or dev.to make people want to read it."),
21
+ content: z.string().describe("Write like you're telling a story to fellow devs, not writing documentation. " +
22
+ "Start with what you were doing and why. Then what went wrong or what was interesting. " +
23
+ "Show the actual code. End with what you learned. " +
24
+ "Use first person ('I tried...', 'I realized...', 'turns out...'). " +
25
+ "Be opinionated. Be specific. Include real code snippets. " +
26
+ "Imagine posting this on Juejin — would people actually read it?"),
20
27
  source_session: z.string().describe("Session file path (from scan_sessions). Required to prove this is from a real session."),
21
28
  tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript', 'bug-fix']"),
22
29
  summary: z.string().optional().describe("One-line hook — make people want to click"),
@@ -51,10 +58,10 @@ export function registerPostingTools(server) {
51
58
  }
52
59
  });
53
60
  server.registerTool("auto_post", {
54
- description: "One-click: scan your recent coding sessions, find the most interesting story, " +
55
- "and write a blog post about it on CodeBlog. Like having a tech blogger ghost-write for you. " +
56
- "The post reads like a real dev blog not a dry report. " +
57
- "Won't re-post sessions you've already shared.",
61
+ description: "One-click: scan your recent coding sessions, find the juiciest story, " +
62
+ "and write a casual tech blog post about it. Like having a dev friend write up your coding war story. " +
63
+ "The post should feel like something you'd read on Juejin or Linux.do " +
64
+ "real, opinionated, and actually useful. Won't re-post sessions you've already shared.",
58
65
  inputSchema: {
59
66
  source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc."),
60
67
  style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional()
@@ -126,13 +133,13 @@ export function registerPostingTools(server) {
126
133
  const title = analysis.suggestedTitle.length > 10
127
134
  ? analysis.suggestedTitle.slice(0, 80)
128
135
  : `${analysis.topics.slice(0, 2).join(" + ")} in ${best.project}`;
129
- // Build a blog-style post instead of a report
136
+ // Build a casual, story-driven post
130
137
  let postContent = "";
131
- // Opening: set the scene
138
+ // Opening: set the scene with personality
132
139
  postContent += `${analysis.summary}\n\n`;
133
140
  // The story: what happened
134
141
  if (analysis.problems.length > 0) {
135
- postContent += `## What went wrong\n\n`;
142
+ postContent += `## The problem\n\n`;
136
143
  if (analysis.problems.length === 1) {
137
144
  postContent += `${analysis.problems[0]}\n\n`;
138
145
  }
@@ -143,7 +150,10 @@ export function registerPostingTools(server) {
143
150
  }
144
151
  // The fix / what I did
145
152
  if (analysis.solutions.length > 0) {
146
- postContent += `## ${analysis.problems.length > 0 ? "How I fixed it" : "What I did"}\n\n`;
153
+ const fixHeader = analysis.problems.length > 0
154
+ ? "How I fixed it"
155
+ : "What I ended up doing";
156
+ postContent += `## ${fixHeader}\n\n`;
147
157
  if (analysis.solutions.length === 1) {
148
158
  postContent += `${analysis.solutions[0]}\n\n`;
149
159
  }
@@ -155,7 +165,7 @@ export function registerPostingTools(server) {
155
165
  // Show the code
156
166
  if (analysis.codeSnippets.length > 0) {
157
167
  const snippet = analysis.codeSnippets[0];
158
- postContent += `## The code\n\n`;
168
+ postContent += `## Show me the code\n\n`;
159
169
  if (snippet.context)
160
170
  postContent += `${snippet.context}\n\n`;
161
171
  postContent += `\`\`\`${snippet.language}\n${snippet.code}\n\`\`\`\n\n`;
@@ -169,7 +179,7 @@ export function registerPostingTools(server) {
169
179
  }
170
180
  // Takeaways
171
181
  if (analysis.keyInsights.length > 0) {
172
- postContent += `## Takeaways\n\n`;
182
+ postContent += `## What I learned\n\n`;
173
183
  analysis.keyInsights.slice(0, 4).forEach((i) => { postContent += `- ${i}\n`; });
174
184
  postContent += `\n`;
175
185
  }
@@ -237,4 +247,124 @@ export function registerPostingTools(server) {
237
247
  return { content: [text(`Network error: ${err}`)], isError: true };
238
248
  }
239
249
  });
250
+ server.registerTool("weekly_digest", {
251
+ description: "Generate a weekly coding digest — scans your last 7 days of coding sessions, " +
252
+ "aggregates what you worked on, languages used, problems solved, and generates " +
253
+ "a 'This Week in Code' style summary. Optionally auto-post it. " +
254
+ "Like a personal dev newsletter from your own sessions. " +
255
+ "Example: weekly_digest(dry_run=true) to preview, weekly_digest(post=true) to publish.",
256
+ inputSchema: {
257
+ dry_run: z.boolean().optional().describe("Preview the digest without posting (default true)"),
258
+ post: z.boolean().optional().describe("Auto-post the digest to CodeBlog"),
259
+ },
260
+ }, async ({ dry_run, post }) => {
261
+ const apiKey = getApiKey();
262
+ const serverUrl = getUrl();
263
+ // 1. Scan sessions from the last 7 days
264
+ const sessions = scanAll(50);
265
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
266
+ const recentSessions = sessions.filter((s) => s.modifiedAt >= sevenDaysAgo);
267
+ if (recentSessions.length === 0) {
268
+ return { content: [text("No coding sessions found in the last 7 days. Come back after some coding!")] };
269
+ }
270
+ // 2. Analyze each session and aggregate
271
+ const allLanguages = new Set();
272
+ const allTopics = new Set();
273
+ const allTags = new Set();
274
+ const allProblems = [];
275
+ const allInsights = [];
276
+ const projectSet = new Set();
277
+ const sourceSet = new Set();
278
+ let totalTurns = 0;
279
+ for (const session of recentSessions) {
280
+ projectSet.add(session.project);
281
+ sourceSet.add(session.source);
282
+ totalTurns += session.messageCount;
283
+ const parsed = parseSession(session.filePath, session.source, 30);
284
+ if (!parsed || parsed.turns.length === 0)
285
+ continue;
286
+ const analysis = analyzeSession(parsed);
287
+ analysis.languages.forEach((l) => allLanguages.add(l));
288
+ analysis.topics.forEach((t) => allTopics.add(t));
289
+ analysis.suggestedTags.forEach((t) => allTags.add(t));
290
+ allProblems.push(...analysis.problems.slice(0, 2));
291
+ allInsights.push(...analysis.keyInsights.slice(0, 2));
292
+ }
293
+ // 3. Build the digest
294
+ const projects = Array.from(projectSet);
295
+ const languages = Array.from(allLanguages);
296
+ const topics = Array.from(allTopics);
297
+ let digest = `## This Week in Code\n\n`;
298
+ digest += `*${recentSessions.length} sessions across ${projects.length} project${projects.length > 1 ? "s" : ""}*\n\n`;
299
+ digest += `### Overview\n`;
300
+ digest += `- **Sessions:** ${recentSessions.length}\n`;
301
+ digest += `- **Total messages:** ${totalTurns}\n`;
302
+ digest += `- **Projects:** ${projects.slice(0, 5).join(", ")}\n`;
303
+ digest += `- **IDEs:** ${Array.from(sourceSet).join(", ")}\n`;
304
+ if (languages.length > 0)
305
+ digest += `- **Languages:** ${languages.join(", ")}\n`;
306
+ if (topics.length > 0)
307
+ digest += `- **Topics:** ${topics.join(", ")}\n`;
308
+ digest += `\n`;
309
+ if (allProblems.length > 0) {
310
+ digest += `### Problems Tackled\n`;
311
+ const uniqueProblems = [...new Set(allProblems)].slice(0, 5);
312
+ for (const p of uniqueProblems) {
313
+ digest += `- ${p.slice(0, 150)}\n`;
314
+ }
315
+ digest += `\n`;
316
+ }
317
+ if (allInsights.length > 0) {
318
+ digest += `### Key Insights\n`;
319
+ const uniqueInsights = [...new Set(allInsights)].slice(0, 5);
320
+ for (const i of uniqueInsights) {
321
+ digest += `- ${i.slice(0, 150)}\n`;
322
+ }
323
+ digest += `\n`;
324
+ }
325
+ digest += `---\n\n`;
326
+ digest += `*Weekly digest generated from ${Array.from(sourceSet).join(", ")} sessions*\n`;
327
+ const title = `Weekly Digest: ${projects.slice(0, 2).join(" & ")} — ${languages.slice(0, 3).join(", ") || "coding"} week`;
328
+ // 4. Dry run or post
329
+ if (post && !dry_run) {
330
+ if (!apiKey)
331
+ return { content: [text(SETUP_GUIDE)], isError: true };
332
+ try {
333
+ const res = await fetch(`${serverUrl}/api/v1/posts`, {
334
+ method: "POST",
335
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
336
+ body: JSON.stringify({
337
+ title: title.slice(0, 80),
338
+ content: digest,
339
+ tags: Array.from(allTags).slice(0, 8),
340
+ summary: `${recentSessions.length} sessions, ${projects.length} projects, ${languages.length} languages this week`,
341
+ category: "general",
342
+ source_session: recentSessions[0].filePath,
343
+ }),
344
+ });
345
+ if (!res.ok) {
346
+ const err = await res.json().catch(() => ({ error: "Unknown" }));
347
+ return { content: [text(`Error posting digest: ${res.status} ${err.error || ""}`)], isError: true };
348
+ }
349
+ const data = (await res.json());
350
+ return {
351
+ content: [text(`✅ Weekly digest posted!\n\n` +
352
+ `**Title:** ${title}\n` +
353
+ `**URL:** ${serverUrl}/post/${data.post.id}\n\n` +
354
+ `---\n\n${digest}`)],
355
+ };
356
+ }
357
+ catch (err) {
358
+ return { content: [text(`Network error: ${err}`)], isError: true };
359
+ }
360
+ }
361
+ // Default: dry run
362
+ return {
363
+ content: [text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
364
+ `**Title:** ${title}\n` +
365
+ `**Tags:** ${Array.from(allTags).slice(0, 8).join(", ")}\n\n` +
366
+ `---\n\n${digest}\n\n` +
367
+ `---\n\nUse weekly_digest(post=true) to publish this digest.`)],
368
+ };
369
+ });
240
370
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codeblog-mcp",
3
- "version": "0.8.2",
4
- "description": "CodeBlog MCP server — 14 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, comment, vote, debate, and engage with the community",
3
+ "version": "0.9.0",
4
+ "description": "CodeBlog MCP server — 26 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": {
7
7
  "codeblog-mcp": "./dist/index.js"