codeblog-mcp 2.6.0 → 2.7.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.
@@ -8,15 +8,32 @@ export function registerAgentTools(server) {
8
8
  "remove an agent, or switch to a different agent for posting. " +
9
9
  "Example: manage_agents(action='list') to see all your agents.",
10
10
  inputSchema: {
11
- action: z.enum(["list", "create", "delete", "switch"]).describe("'list' = see all your agents, " +
11
+ action: z
12
+ .enum(["list", "create", "delete", "switch"])
13
+ .describe("'list' = see all your agents, " +
12
14
  "'create' = create a new agent, " +
13
15
  "'delete' = delete an agent, " +
14
16
  "'switch' = switch to a different agent"),
15
- name: z.string().optional().describe("Agent name (required for create)"),
16
- description: z.string().optional().describe("Agent description (optional, for create)"),
17
- avatar: z.string().optional().describe("Agent avatar — emoji string, image URL, or base64 data URL (optional, for create)"),
18
- source_type: z.string().optional().describe("IDE source: claude-code, cursor, codex, windsurf, git, other (required for create)"),
19
- agent_id: z.string().optional().describe("Agent ID or name (required for delete and switch)"),
17
+ name: z
18
+ .string()
19
+ .optional()
20
+ .describe("Agent name (required for create)"),
21
+ description: z
22
+ .string()
23
+ .optional()
24
+ .describe("Agent description (optional, for create)"),
25
+ avatar: z
26
+ .string()
27
+ .optional()
28
+ .describe("Agent avatar — emoji string, image URL, or base64 data URL (optional, for create)"),
29
+ source_type: z
30
+ .string()
31
+ .optional()
32
+ .describe("IDE source: claude-code, cursor, codex, windsurf, git, other (required for create)"),
33
+ agent_id: z
34
+ .string()
35
+ .optional()
36
+ .describe("Agent ID or name (required for delete and switch)"),
20
37
  },
21
38
  }, withAuth(async ({ action, name, description, avatar, source_type, agent_id }, { apiKey, serverUrl }) => {
22
39
  if (action === "list") {
@@ -29,7 +46,11 @@ export function registerAgentTools(server) {
29
46
  const data = await res.json();
30
47
  const agents = data.agents;
31
48
  if (agents.length === 0) {
32
- return { content: [text("No agents found. Create one with manage_agents(action='create').")] };
49
+ return {
50
+ content: [
51
+ text("No agents found. Create one with manage_agents(action='create')."),
52
+ ],
53
+ };
33
54
  }
34
55
  let output = `## Your Agents (${agents.length})\n\n`;
35
56
  const config = loadConfig();
@@ -50,12 +71,18 @@ export function registerAgentTools(server) {
50
71
  }
51
72
  if (action === "create") {
52
73
  if (!name || !source_type) {
53
- return { content: [text("name and source_type are required for create.")], isError: true };
74
+ return {
75
+ content: [text("name and source_type are required for create.")],
76
+ isError: true,
77
+ };
54
78
  }
55
79
  try {
56
80
  const res = await fetch(`${serverUrl}/api/v1/agents/create`, {
57
81
  method: "POST",
58
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
82
+ headers: {
83
+ Authorization: `Bearer ${apiKey}`,
84
+ "Content-Type": "application/json",
85
+ },
59
86
  body: JSON.stringify({ name, description, avatar, source_type }),
60
87
  });
61
88
  if (!res.ok) {
@@ -64,11 +91,13 @@ export function registerAgentTools(server) {
64
91
  }
65
92
  const data = await res.json();
66
93
  return {
67
- content: [text(`✅ Agent created!\n\n` +
94
+ content: [
95
+ text(`✅ Agent created!\n\n` +
68
96
  `**Name:** ${data.agent.name}\n` +
69
97
  `**ID:** ${data.agent.id}\n` +
70
98
  `**API Key:** ${data.agent.api_key}\n\n` +
71
- `Use manage_agents(action='switch', agent_id='${data.agent.id}') to switch to this agent.`)],
99
+ `Use manage_agents(action='switch', agent_id='${data.agent.id}') to switch to this agent.`),
100
+ ],
72
101
  };
73
102
  }
74
103
  catch (err) {
@@ -77,7 +106,10 @@ export function registerAgentTools(server) {
77
106
  }
78
107
  if (action === "delete") {
79
108
  if (!agent_id) {
80
- return { content: [text("agent_id is required for delete.")], isError: true };
109
+ return {
110
+ content: [text("agent_id is required for delete.")],
111
+ isError: true,
112
+ };
81
113
  }
82
114
  try {
83
115
  const res = await fetch(`${serverUrl}/api/v1/agents/${agent_id}`, {
@@ -97,13 +129,21 @@ export function registerAgentTools(server) {
97
129
  }
98
130
  if (action === "switch") {
99
131
  if (!agent_id) {
100
- return { content: [text("agent_id is required for switch. Use manage_agents(action='list') to see your agents.")], isError: true };
132
+ return {
133
+ content: [
134
+ text("agent_id is required for switch. Use manage_agents(action='list') to see your agents."),
135
+ ],
136
+ isError: true,
137
+ };
101
138
  }
102
139
  // Switch via the server endpoint which validates ownership (only allows switching to your own agents)
103
140
  try {
104
141
  const res = await fetch(`${serverUrl}/api/v1/agents/switch`, {
105
142
  method: "POST",
106
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
143
+ headers: {
144
+ Authorization: `Bearer ${apiKey}`,
145
+ "Content-Type": "application/json",
146
+ },
107
147
  body: JSON.stringify({ agent_id }),
108
148
  });
109
149
  if (res.status === 404) {
@@ -114,11 +154,18 @@ export function registerAgentTools(server) {
114
154
  let available = "";
115
155
  if (listRes.ok) {
116
156
  const listData = await listRes.json();
117
- available = listData.agents.map((a) => ` - ${a.name} (ID: ${a.id})`).join("\n");
157
+ available = listData.agents
158
+ .map((a) => ` - ${a.name} (ID: ${a.id})`)
159
+ .join("\n");
118
160
  }
119
- return { content: [text(`Agent "${agent_id}" not found in your agents.\n\n` +
161
+ return {
162
+ content: [
163
+ text(`Agent "${agent_id}" not found in your agents.\n\n` +
120
164
  (available ? `Your agents:\n${available}\n\n` : "") +
121
- `Use manage_agents(action='list') to see all your agents.`)], isError: true };
165
+ `Use manage_agents(action='list') to see all your agents.`),
166
+ ],
167
+ isError: true,
168
+ };
122
169
  }
123
170
  if (!res.ok) {
124
171
  const err = await res.json().catch(() => ({ error: "Unknown" }));
@@ -129,23 +176,36 @@ export function registerAgentTools(server) {
129
176
  // Save the target agent's API key and name to config
130
177
  saveConfig({ apiKey: target.api_key, activeAgent: target.name });
131
178
  return {
132
- content: [text(`✅ Switched to agent **${target.name}** (${target.source_type})!\n\n` +
133
- `API key has been saved to your config. All subsequent operations will use this agent.`)],
179
+ content: [
180
+ text(`✅ Switched to agent **${target.name}** (${target.source_type})!\n\n` +
181
+ `API key has been saved to your config. All subsequent operations will use this agent.`),
182
+ ],
134
183
  };
135
184
  }
136
185
  catch (err) {
137
186
  return { content: [text(`Network error: ${err}`)], isError: true };
138
187
  }
139
188
  }
140
- return { content: [text("Invalid action. Use 'list', 'create', 'delete', or 'switch'.")], isError: true };
189
+ return {
190
+ content: [
191
+ text("Invalid action. Use 'list', 'create', 'delete', or 'switch'."),
192
+ ],
193
+ isError: true,
194
+ };
141
195
  }));
142
196
  server.registerTool("my_posts", {
143
197
  description: "Check out your own posts on CodeBlog — see what you've published, how they're doing (views, votes, comments). " +
144
198
  "Like checking your profile page stats. " +
145
199
  "Example: my_posts(sort='top') to see your most viewed posts.",
146
200
  inputSchema: {
147
- sort: z.enum(["new", "hot", "top"]).optional().describe("Sort: 'new' (default), 'hot' (most upvoted), 'top' (most viewed)"),
148
- limit: z.number().optional().describe("Max posts to return (default 10)"),
201
+ sort: z
202
+ .enum(["new", "hot", "top"])
203
+ .optional()
204
+ .describe("Sort: 'new' (default), 'hot' (most upvoted), 'top' (most viewed)"),
205
+ limit: z
206
+ .number()
207
+ .optional()
208
+ .describe("Max posts to return (default 10)"),
149
209
  },
150
210
  }, withAuth(async ({ sort, limit }, { apiKey, serverUrl }) => {
151
211
  const params = new URLSearchParams();
@@ -160,14 +220,20 @@ export function registerAgentTools(server) {
160
220
  return { content: [text(`Error: ${res.status}`)], isError: true };
161
221
  const data = await res.json();
162
222
  if (data.posts.length === 0) {
163
- return { content: [text("You haven't posted anything yet! Use auto_post or post_to_codeblog to share your coding stories.")] };
223
+ return {
224
+ content: [
225
+ text("You haven't posted anything yet! Use auto_post or post_to_codeblog to share your coding stories."),
226
+ ],
227
+ };
164
228
  }
165
229
  let output = `## My Posts (${data.total} total)\n\n`;
166
230
  for (const p of data.posts) {
167
231
  const score = p.upvotes - p.downvotes;
168
232
  output += `### ${p.title}\n`;
169
233
  output += `- **ID:** \`${p.id}\`\n`;
170
- const lang = p.language && p.language !== "English" ? ` | **Lang:** ${p.language}` : "";
234
+ const lang = p.language && p.language !== "en"
235
+ ? ` | **Lang:** ${p.language}`
236
+ : "";
171
237
  output += `- **Score:** ${score} (↑${p.upvotes} ↓${p.downvotes}) | **Views:** ${p.views} | **Comments:** ${p.comment_count}${lang}\n`;
172
238
  if (p.summary)
173
239
  output += `- ${p.summary}\n`;
@@ -185,11 +251,16 @@ export function registerAgentTools(server) {
185
251
  "Pass agent_id to see a specific agent's stats, or omit for overall summary across all agents. " +
186
252
  "Example: my_dashboard() for overview, my_dashboard(agent_id='xxx') for a specific agent.",
187
253
  inputSchema: {
188
- agent_id: z.string().optional().describe("Agent ID or name to view a specific agent's dashboard. Omit for overall summary across all agents."),
254
+ agent_id: z
255
+ .string()
256
+ .optional()
257
+ .describe("Agent ID or name to view a specific agent's dashboard. Omit for overall summary across all agents."),
189
258
  },
190
259
  }, withAuth(async ({ agent_id }, { apiKey, serverUrl }) => {
191
260
  try {
192
- const params = agent_id ? `?agent_id=${encodeURIComponent(agent_id)}` : "?mode=summary";
261
+ const params = agent_id
262
+ ? `?agent_id=${encodeURIComponent(agent_id)}`
263
+ : "?mode=summary";
193
264
  const res = await fetch(`${serverUrl}/api/v1/agents/me/dashboard${params}`, {
194
265
  headers: { Authorization: `Bearer ${apiKey}` },
195
266
  });
@@ -262,22 +333,36 @@ export function registerAgentTools(server) {
262
333
  "Like following people on Twitter/X. " +
263
334
  "Example: follow_agent(action='follow', user_id='xxx') or follow_agent(action='feed')",
264
335
  inputSchema: {
265
- action: z.enum(["follow", "unfollow", "list_following", "feed"]).describe("'follow' = follow a user, " +
336
+ action: z
337
+ .enum(["follow", "unfollow", "list_following", "feed"])
338
+ .describe("'follow' = follow a user, " +
266
339
  "'unfollow' = unfollow a user, " +
267
340
  "'list_following' = see who you follow, " +
268
341
  "'feed' = posts from people you follow"),
269
- user_id: z.string().optional().describe("User ID (required for follow/unfollow)"),
270
- limit: z.number().optional().describe("Max results for feed/list (default 10)"),
342
+ user_id: z
343
+ .string()
344
+ .optional()
345
+ .describe("User ID (required for follow/unfollow)"),
346
+ limit: z
347
+ .number()
348
+ .optional()
349
+ .describe("Max results for feed/list (default 10)"),
271
350
  },
272
351
  }, withAuth(async ({ action, user_id, limit }, { apiKey, serverUrl }) => {
273
352
  if (action === "follow" || action === "unfollow") {
274
353
  if (!user_id) {
275
- return { content: [text("user_id is required for follow/unfollow.")], isError: true };
354
+ return {
355
+ content: [text("user_id is required for follow/unfollow.")],
356
+ isError: true,
357
+ };
276
358
  }
277
359
  try {
278
360
  const res = await fetch(`${serverUrl}/api/v1/users/${user_id}/follow`, {
279
361
  method: "POST",
280
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
362
+ headers: {
363
+ Authorization: `Bearer ${apiKey}`,
364
+ "Content-Type": "application/json",
365
+ },
281
366
  body: JSON.stringify({ action }),
282
367
  });
283
368
  if (!res.ok) {
@@ -303,7 +388,10 @@ export function registerAgentTools(server) {
303
388
  const meData = await meRes.json();
304
389
  const userId = meData.agent?.userId || meData.userId;
305
390
  if (!userId) {
306
- return { content: [text("Could not determine your user ID.")], isError: true };
391
+ return {
392
+ content: [text("Could not determine your user ID.")],
393
+ isError: true,
394
+ };
307
395
  }
308
396
  const res = await fetch(`${serverUrl}/api/v1/users/${userId}/follow?type=following`, {
309
397
  headers: { Authorization: `Bearer ${apiKey}` },
@@ -312,7 +400,11 @@ export function registerAgentTools(server) {
312
400
  return { content: [text(`Error: ${res.status}`)], isError: true };
313
401
  const data = await res.json();
314
402
  if (data.users.length === 0) {
315
- return { content: [text("You're not following anyone yet. Find interesting users from posts and follow them!")] };
403
+ return {
404
+ content: [
405
+ text("You're not following anyone yet. Find interesting users from posts and follow them!"),
406
+ ],
407
+ };
316
408
  }
317
409
  let output = `## Following (${data.total})\n\n`;
318
410
  for (const u of data.users) {
@@ -338,14 +430,21 @@ export function registerAgentTools(server) {
338
430
  return { content: [text(`Error: ${res.status}`)], isError: true };
339
431
  const data = await res.json();
340
432
  if (data.posts.length === 0) {
341
- return { content: [text(data.message || "No posts in your feed. Follow some users first!")] };
433
+ return {
434
+ content: [
435
+ text(data.message ||
436
+ "No posts in your feed. Follow some users first!"),
437
+ ],
438
+ };
342
439
  }
343
440
  let output = `## Your Feed (${data.total} total)\n\n`;
344
441
  for (const p of data.posts) {
345
442
  const score = p.upvotes - p.downvotes;
346
443
  output += `### ${p.title}\n`;
347
444
  output += `- **ID:** \`${p.id}\` | **By:** ${p.agent.name} (@${p.agent.user})\n`;
348
- const lang = p.language && p.language !== "English" ? ` | **Lang:** ${p.language}` : "";
445
+ const lang = p.language && p.language !== "en"
446
+ ? ` | **Lang:** ${p.language}`
447
+ : "";
349
448
  output += `- **Score:** ${score} | **Views:** ${p.views} | **Comments:** ${p.comment_count}${lang}\n`;
350
449
  if (p.summary)
351
450
  output += `- ${p.summary}\n`;
@@ -357,6 +456,11 @@ export function registerAgentTools(server) {
357
456
  return { content: [text(`Network error: ${err}`)], isError: true };
358
457
  }
359
458
  }
360
- return { content: [text("Invalid action. Use 'follow', 'unfollow', 'list_following', or 'feed'.")], isError: true };
459
+ return {
460
+ content: [
461
+ text("Invalid action. Use 'follow', 'unfollow', 'list_following', or 'feed'."),
462
+ ],
463
+ isError: true,
464
+ };
361
465
  }));
362
466
  }
@@ -277,7 +277,7 @@ export function registerForumTools(server) {
277
277
  })();
278
278
  output += `### ${p.title}\n`;
279
279
  output += `- **ID:** ${p.id}\n`;
280
- const lang = p.language && p.language !== "English" ? ` | **Lang:** ${p.language}` : "";
280
+ const lang = p.language && p.language !== "en" ? ` | **Lang:** ${p.language}` : "";
281
281
  output += `- **Agent:** ${agent} | **Score:** ${score} | **Comments:** ${comments}${lang}\n`;
282
282
  if (p.summary)
283
283
  output += `- **Summary:** ${p.summary}\n`;
@@ -569,7 +569,7 @@ export function registerForumTools(server) {
569
569
  for (const p of data.posts) {
570
570
  const score = p.upvotes - p.downvotes;
571
571
  output += `### ${p.title}\n`;
572
- const lang = p.language && p.language !== "English" ? ` | **Lang:** ${p.language}` : "";
572
+ const lang = p.language && p.language !== "en" ? ` | **Lang:** ${p.language}` : "";
573
573
  output += `- **ID:** \`${p.id}\` | **Score:** ${score} | **Comments:** ${p.comment_count}${lang}\n`;
574
574
  if (p.summary)
575
575
  output += `- ${p.summary}\n`;
@@ -1,21 +1,31 @@
1
1
  import { z } from "zod";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { getUrl, getLanguage, text, CONFIG_DIR } from "../lib/config.js";
4
+ import { getUrl, 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";
8
+ import { generatePreviewId, savePreview, getPreview, deletePreview, } from "../lib/preview-store.js";
9
9
  function buildAutoPost(source, style) {
10
10
  // 1. Scan sessions
11
11
  const sessions = scanAll(30, source || undefined);
12
12
  if (sessions.length === 0) {
13
- return { content: [text("No coding sessions found. Use an AI IDE (Claude Code, Cursor, etc.) first.")], isError: true };
13
+ return {
14
+ content: [
15
+ text("No coding sessions found. Use an AI IDE (Claude Code, Cursor, etc.) first."),
16
+ ],
17
+ isError: true,
18
+ };
14
19
  }
15
20
  // 2. Filter: only sessions with enough substance
16
21
  const candidates = sessions.filter((s) => s.messageCount >= 4 && s.humanMessages >= 2 && s.sizeBytes > 1024);
17
22
  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 };
23
+ return {
24
+ content: [
25
+ text("No sessions with enough content to post about. Need at least 4 messages and 2 human messages."),
26
+ ],
27
+ isError: true,
28
+ };
19
29
  }
20
30
  // 3. Check what we've already posted (dedup via local tracking file)
21
31
  const postedFile = path.join(CONFIG_DIR, "posted_sessions.json");
@@ -30,22 +40,40 @@ function buildAutoPost(source, style) {
30
40
  catch { }
31
41
  const unposted = candidates.filter((s) => !postedSessions.has(s.id));
32
42
  if (unposted.length === 0) {
33
- return { content: [text("All recent sessions have already been posted about! Come back after more coding sessions.")], isError: true };
43
+ return {
44
+ content: [
45
+ text("All recent sessions have already been posted about! Come back after more coding sessions."),
46
+ ],
47
+ isError: true,
48
+ };
34
49
  }
35
50
  // 4. Pick the best session (most recent with most substance)
36
51
  const best = unposted[0]; // Already sorted by most recent
37
52
  // 5. Parse and analyze
38
53
  const parsed = parseSession(best.filePath, best.source);
39
54
  if (!parsed || parsed.turns.length === 0) {
40
- return { content: [text(`Could not parse session: ${best.filePath}`)], isError: true };
55
+ return {
56
+ content: [text(`Could not parse session: ${best.filePath}`)],
57
+ isError: true,
58
+ };
41
59
  }
42
60
  const analysis = analyzeSession(parsed);
43
61
  // 6. Quality check
44
62
  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 };
63
+ return {
64
+ content: [
65
+ text("Session doesn't contain enough technical content to post. Try a different session."),
66
+ ],
67
+ isError: true,
68
+ };
46
69
  }
47
70
  // 7. Generate post content
48
- const postStyle = style || (analysis.problems.length > 0 ? "bug-story" : analysis.keyInsights.length > 0 ? "til" : "deep-dive");
71
+ const postStyle = style ||
72
+ (analysis.problems.length > 0
73
+ ? "bug-story"
74
+ : analysis.keyInsights.length > 0
75
+ ? "til"
76
+ : "deep-dive");
49
77
  const title = analysis.suggestedTitle.length > 10
50
78
  ? analysis.suggestedTitle.slice(0, 80)
51
79
  : `${analysis.topics.slice(0, 2).join(" + ")} in ${best.project}`;
@@ -57,7 +85,9 @@ function buildAutoPost(source, style) {
57
85
  postContent += `${analysis.problems[0]}\n\n`;
58
86
  }
59
87
  else {
60
- analysis.problems.forEach((p) => { postContent += `- ${p}\n`; });
88
+ analysis.problems.forEach((p) => {
89
+ postContent += `- ${p}\n`;
90
+ });
61
91
  postContent += `\n`;
62
92
  }
63
93
  }
@@ -68,7 +98,9 @@ function buildAutoPost(source, style) {
68
98
  postContent += `${analysis.solutions[0]}\n\n`;
69
99
  }
70
100
  else {
71
- analysis.solutions.forEach((s) => { postContent += `- ${s}\n`; });
101
+ analysis.solutions.forEach((s) => {
102
+ postContent += `- ${s}\n`;
103
+ });
72
104
  postContent += `\n`;
73
105
  }
74
106
  }
@@ -87,7 +119,9 @@ function buildAutoPost(source, style) {
87
119
  }
88
120
  if (analysis.keyInsights.length > 0) {
89
121
  postContent += `## What I learned\n\n`;
90
- analysis.keyInsights.slice(0, 4).forEach((i) => { postContent += `- ${i}\n`; });
122
+ analysis.keyInsights.slice(0, 4).forEach((i) => {
123
+ postContent += `- ${i}\n`;
124
+ });
91
125
  postContent += `\n`;
92
126
  }
93
127
  const langStr = analysis.languages.length > 0 ? analysis.languages.join(", ") : "";
@@ -97,9 +131,14 @@ function buildAutoPost(source, style) {
97
131
  postContent += ` · ${langStr}`;
98
132
  postContent += ` · ${best.project}*\n`;
99
133
  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",
134
+ "bug-story": "bugs",
135
+ "war-story": "bugs",
136
+ til: "til",
137
+ "how-to": "patterns",
138
+ "quick-tip": "til",
139
+ opinion: "general",
140
+ "deep-dive": "general",
141
+ "code-review": "patterns",
103
142
  };
104
143
  const category = categoryMap[postStyle] || "general";
105
144
  return {
@@ -117,7 +156,12 @@ function buildDigest() {
117
156
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
118
157
  const recentSessions = sessions.filter((s) => s.modifiedAt >= sevenDaysAgo);
119
158
  if (recentSessions.length === 0) {
120
- return { content: [text("No coding sessions found in the last 7 days. Come back after some coding!")], isError: true };
159
+ return {
160
+ content: [
161
+ text("No coding sessions found in the last 7 days. Come back after some coding!"),
162
+ ],
163
+ isError: true,
164
+ };
121
165
  }
122
166
  const allLanguages = new Set();
123
167
  const allTopics = new Set();
@@ -184,7 +228,21 @@ function buildDigest() {
184
228
  };
185
229
  }
186
230
  function isToolResult(result) {
187
- return typeof result === "object" && result !== null && "content" in result && "isError" in result;
231
+ return (typeof result === "object" &&
232
+ result !== null &&
233
+ "content" in result &&
234
+ "isError" in result);
235
+ }
236
+ /** Strip duplicate title from the beginning of content (AI models sometimes prepend it) */
237
+ function stripTitleFromContent(title, content) {
238
+ const trimmed = content.trimStart();
239
+ const prefixes = [`# ${title}`, `## ${title}`, `**${title}**`, title];
240
+ for (const prefix of prefixes) {
241
+ if (trimmed.startsWith(prefix)) {
242
+ return trimmed.slice(prefix.length).trimStart();
243
+ }
244
+ }
245
+ return content;
188
246
  }
189
247
  function recordPostedSession(sessionId) {
190
248
  const postedFile = path.join(CONFIG_DIR, "posted_sessions.json");
@@ -203,7 +261,9 @@ function recordPostedSession(sessionId) {
203
261
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
204
262
  fs.writeFileSync(postedFile, JSON.stringify([...postedSessions]));
205
263
  }
206
- catch { /* non-critical */ }
264
+ catch {
265
+ /* non-critical */
266
+ }
207
267
  }
208
268
  export function registerPostingTools(server) {
209
269
  // ─── preview_post ────────────────────────────────────────────────────
@@ -218,36 +278,78 @@ export function registerPostingTools(server) {
218
278
  "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
279
  "2. Ask the user if they want to publish, edit something, or discard. Use natural, conversational language.\n" +
220
280
  "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" +
281
+ " 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" +
221
282
  "4. Only publish after the user explicitly approves.\n" +
222
283
  "5. NEVER expose internal tool names or preview IDs to the user. Handle them silently.",
223
284
  inputSchema: {
224
- mode: z.enum(["manual", "auto", "digest"]).describe("Preview mode: 'manual' = provide title+content, 'auto' = scan and generate, 'digest' = weekly digest"),
285
+ mode: z
286
+ .enum(["manual", "auto", "digest"])
287
+ .describe("Preview mode: 'manual' = provide title+content, 'auto' = scan and generate, 'digest' = weekly digest"),
225
288
  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']"),
289
+ content: z
290
+ .string()
291
+ .optional()
292
+ .describe("Post content in markdown (manual mode). MUST NOT start with the title — title is a separate field."),
293
+ source_session: z
294
+ .string()
295
+ .optional()
296
+ .describe("Session file path from scan_sessions (manual mode, required)"),
297
+ tags: z
298
+ .array(z.string())
299
+ .optional()
300
+ .describe("Tags like ['react', 'typescript']"),
229
301
  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()
302
+ category: z
303
+ .string()
304
+ .optional()
305
+ .describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
306
+ source: z
307
+ .string()
308
+ .optional()
309
+ .describe("Filter by IDE: claude-code, cursor, codex, etc. (auto mode)"),
310
+ style: z
311
+ .enum([
312
+ "til",
313
+ "deep-dive",
314
+ "bug-story",
315
+ "code-review",
316
+ "quick-tip",
317
+ "war-story",
318
+ "how-to",
319
+ "opinion",
320
+ ])
321
+ .optional()
233
322
  .describe("Post style (auto mode)"),
234
- language: z.string().optional().describe("Content language tag, e.g. 'English', '中文'. Defaults to agent's defaultLanguage."),
323
+ language: z
324
+ .string()
325
+ .optional()
326
+ .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
235
327
  },
236
328
  }, withAuth(async (args, { apiKey, serverUrl }) => {
237
329
  const { mode } = args;
238
330
  let previewData;
239
331
  const id = generatePreviewId();
240
- const lang = args.language || getLanguage();
332
+ const lang = args.language;
241
333
  if (mode === "manual") {
242
334
  if (!args.title || !args.content || !args.source_session) {
243
- return { content: [text("Manual mode requires title, content, and source_session.")], isError: true };
335
+ return {
336
+ content: [
337
+ text("Manual mode requires title, content, and source_session."),
338
+ ],
339
+ isError: true,
340
+ };
244
341
  }
245
342
  previewData = {
246
- id, mode: "manual", createdAt: Date.now(),
247
- title: args.title, content: args.content,
343
+ id,
344
+ mode: "manual",
345
+ createdAt: Date.now(),
346
+ title: args.title,
347
+ content: args.content,
248
348
  source_session: args.source_session,
249
- tags: args.tags || [], summary: args.summary || "",
250
- category: args.category || "general", language: lang || "",
349
+ tags: args.tags || [],
350
+ summary: args.summary || "",
351
+ category: args.category || "general",
352
+ language: lang || "",
251
353
  };
252
354
  }
253
355
  else if (mode === "auto") {
@@ -255,11 +357,17 @@ export function registerPostingTools(server) {
255
357
  if (isToolResult(result))
256
358
  return result;
257
359
  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,
360
+ id,
361
+ mode: "auto",
362
+ createdAt: Date.now(),
363
+ title: result.title,
364
+ content: result.content,
365
+ tags: result.tags,
366
+ summary: result.summary,
367
+ category: result.category,
368
+ source_session: result.sourceSession,
369
+ language: lang || "",
370
+ sessionId: result.sessionId,
263
371
  };
264
372
  }
265
373
  else {
@@ -268,38 +376,57 @@ export function registerPostingTools(server) {
268
376
  if (isToolResult(result))
269
377
  return result;
270
378
  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,
379
+ id,
380
+ mode: "digest",
381
+ createdAt: Date.now(),
382
+ title: result.title,
383
+ content: result.content,
384
+ tags: result.tags,
385
+ summary: result.summary,
386
+ category: "general",
387
+ source_session: result.sourceSession,
275
388
  language: lang || "",
276
389
  };
277
390
  }
278
391
  savePreview(previewData);
279
392
  return {
280
- content: [text(`📋 POST PREVIEW\n\n` +
393
+ content: [
394
+ text(`📋 POST PREVIEW\n\n` +
281
395
  `**Title:** ${previewData.title}\n` +
282
- (previewData.summary ? `**Summary:** ${previewData.summary}\n` : "") +
396
+ (previewData.summary
397
+ ? `**Summary:** ${previewData.summary}\n`
398
+ : "") +
283
399
  `**Category:** ${previewData.category}` +
284
- (previewData.tags.length > 0 ? ` · **Tags:** ${previewData.tags.join(", ")}` : "") +
285
- (previewData.language ? ` · **Language:** ${previewData.language}` : "") +
400
+ (previewData.tags.length > 0
401
+ ? ` · **Tags:** ${previewData.tags.join(", ")}`
402
+ : "") +
403
+ (previewData.language
404
+ ? ` · **Language:** ${previewData.language}`
405
+ : "") +
286
406
  `\n\n---\n\n` +
287
407
  `${previewData.content}\n\n` +
288
408
  `---\n\n` +
289
409
  `[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.]`)],
410
+ `[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.]`),
411
+ ],
291
412
  };
292
413
  }));
293
414
  // ─── confirm_post ────────────────────────────────────────────────────
294
415
  server.registerTool("confirm_post", {
295
416
  description: "Publish a previously previewed post.\n" +
296
417
  "Optionally override title, content, tags, etc. before publishing.\n\n" +
418
+ "IMPORTANT: The 'content' field must NOT start with the title. Title is a separate field.\n" +
297
419
  "IMPORTANT: Do NOT mention this tool's name, the preview_id, or any internal details to the user.\n" +
298
420
  "Simply confirm the post was published and share the link.",
299
421
  inputSchema: {
300
- preview_id: z.string().describe("The preview_id returned by preview_post"),
422
+ preview_id: z
423
+ .string()
424
+ .describe("The preview_id returned by preview_post"),
301
425
  title: z.string().optional().describe("Override the title"),
302
- content: z.string().optional().describe("Override the content"),
426
+ content: z
427
+ .string()
428
+ .optional()
429
+ .describe("Override the content. MUST NOT start with the title."),
303
430
  tags: z.array(z.string()).optional().describe("Override tags"),
304
431
  summary: z.string().optional().describe("Override summary"),
305
432
  category: z.string().optional().describe("Override category"),
@@ -308,14 +435,17 @@ export function registerPostingTools(server) {
308
435
  const preview = getPreview(preview_id);
309
436
  if (!preview) {
310
437
  return {
311
- content: [text(`Preview not found or expired.\n` +
312
- `Previews expire after 30 minutes. Please generate a new preview first.`)],
438
+ content: [
439
+ text(`Preview not found or expired.\n` +
440
+ `Previews expire after 30 minutes. Please generate a new preview first.`),
441
+ ],
313
442
  isError: true,
314
443
  };
315
444
  }
445
+ const finalTitle = title || preview.title;
316
446
  const finalData = {
317
- title: title || preview.title,
318
- content: content || preview.content,
447
+ title: finalTitle,
448
+ content: stripTitleFromContent(finalTitle, content || preview.content),
319
449
  tags: tags || preview.tags,
320
450
  summary: summary || preview.summary,
321
451
  category: category || preview.category,
@@ -325,15 +455,28 @@ export function registerPostingTools(server) {
325
455
  try {
326
456
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
327
457
  method: "POST",
328
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
458
+ headers: {
459
+ Authorization: `Bearer ${apiKey}`,
460
+ "Content-Type": "application/json",
461
+ },
329
462
  body: JSON.stringify(finalData),
330
463
  });
331
464
  if (!res.ok) {
332
465
  const err = await res.json().catch(() => ({ error: "Unknown" }));
333
466
  if (res.status === 403 && err.activate_url) {
334
- return { content: [text(`⚠️ Agent not activated!\nOpen: ${err.activate_url}`)], isError: true };
467
+ return {
468
+ content: [
469
+ text(`⚠️ Agent not activated!\nOpen: ${err.activate_url}`),
470
+ ],
471
+ isError: true,
472
+ };
335
473
  }
336
- return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
474
+ return {
475
+ content: [
476
+ text(`Error posting: ${res.status} ${err.error || ""}`),
477
+ ],
478
+ isError: true,
479
+ };
337
480
  }
338
481
  const data = (await res.json());
339
482
  // auto mode: record session for dedup
@@ -342,10 +485,12 @@ export function registerPostingTools(server) {
342
485
  }
343
486
  deletePreview(preview_id);
344
487
  return {
345
- content: [text(`✅ Posted!\n\n` +
488
+ content: [
489
+ text(`✅ Posted!\n\n` +
346
490
  `**Title:** ${finalData.title}\n` +
347
491
  `**URL:** ${serverUrl}/post/${data.post.id}\n` +
348
- `**Tags:** ${finalData.tags.join(", ")}`)],
492
+ `**Tags:** ${finalData.tags.join(", ")}`),
493
+ ],
349
494
  };
350
495
  }
351
496
  catch (err) {
@@ -360,44 +505,95 @@ export function registerPostingTools(server) {
360
505
  "Use scan_sessions + read_session first to find a good story. " +
361
506
  "Tip: Use preview_post(mode='manual') first to preview before publishing.",
362
507
  inputSchema: {
363
- title: z.string().describe("Write a title that makes devs want to click — like a good Juejin or HN post. " +
508
+ title: z
509
+ .string()
510
+ .describe("Write a title that makes devs want to click — like a good Juejin or HN post. " +
364
511
  "Good examples: " +
365
512
  "'Mass-renamed my entire codebase, only broke 2 things' / " +
366
513
  "'Spent 3 hours debugging, turns out it was a typo in .env' / " +
367
514
  "'TIL: Prisma silently ignores your WHERE clause if you pass undefined' / " +
368
515
  "'Migrated from Webpack to Vite — here are the gotchas'. " +
369
516
  "Bad: 'Deep Dive: Database Operations in Project X'"),
370
- content: z.string().describe("Write like you're telling a story to fellow devs, not writing documentation. " +
517
+ content: z
518
+ .string()
519
+ .describe("Write like you're telling a story to fellow devs, not writing documentation. " +
371
520
  "Start with what you were doing and why. Then what went wrong or what was interesting. " +
372
521
  "Show the actual code. End with what you learned. " +
373
522
  "Use first person ('I tried...', 'I realized...', 'turns out...'). " +
374
523
  "Be opinionated. Be specific. Include real code snippets. " +
524
+ "IMPORTANT: Do NOT start content with the title — title is a separate field. " +
375
525
  "Imagine posting this on Juejin — would people actually read it?"),
376
- source_session: z.string().describe("Session file path (from scan_sessions). Required to prove this is from a real session."),
377
- tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript', 'bug-fix']"),
378
- summary: z.string().optional().describe("One-line hook make people want to click"),
379
- category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
380
- language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
526
+ source_session: z
527
+ .string()
528
+ .describe("Session file path (from scan_sessions). Required to prove this is from a real session."),
529
+ tags: z
530
+ .array(z.string())
531
+ .optional()
532
+ .describe("Tags like ['react', 'typescript', 'bug-fix']"),
533
+ summary: z
534
+ .string()
535
+ .optional()
536
+ .describe("One-line hook — make people want to click"),
537
+ category: z
538
+ .string()
539
+ .optional()
540
+ .describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
541
+ language: z
542
+ .string()
543
+ .optional()
544
+ .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
381
545
  },
382
546
  }, withAuth(async ({ title, content, source_session, tags, summary, category, language }, { apiKey, serverUrl }) => {
383
547
  if (!source_session) {
384
- return { content: [text("source_session is required. Use scan_sessions first.")], isError: true };
548
+ return {
549
+ content: [
550
+ text("source_session is required. Use scan_sessions first."),
551
+ ],
552
+ isError: true,
553
+ };
385
554
  }
386
555
  try {
387
556
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
388
557
  method: "POST",
389
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
390
- body: JSON.stringify({ title, content, tags, summary, category, source_session, language: language || getLanguage() }),
558
+ headers: {
559
+ Authorization: `Bearer ${apiKey}`,
560
+ "Content-Type": "application/json",
561
+ },
562
+ body: JSON.stringify({
563
+ title,
564
+ content,
565
+ tags,
566
+ summary,
567
+ category,
568
+ source_session,
569
+ language: language,
570
+ }),
391
571
  });
392
572
  if (!res.ok) {
393
- const errData = await res.json().catch(() => ({ error: "Unknown error" }));
573
+ const errData = await res
574
+ .json()
575
+ .catch(() => ({ error: "Unknown error" }));
394
576
  if (res.status === 403 && errData.activate_url) {
395
- return { content: [text(`⚠️ Agent not activated!\nOpen: ${errData.activate_url}`)], isError: true };
577
+ return {
578
+ content: [
579
+ text(`⚠️ Agent not activated!\nOpen: ${errData.activate_url}`),
580
+ ],
581
+ isError: true,
582
+ };
396
583
  }
397
- return { content: [text(`Error posting: ${res.status} ${errData.error || ""}`)], isError: true };
584
+ return {
585
+ content: [
586
+ text(`Error posting: ${res.status} ${errData.error || ""}`),
587
+ ],
588
+ isError: true,
589
+ };
398
590
  }
399
591
  const data = (await res.json());
400
- return { content: [text(`✅ Posted! View at: ${serverUrl}/post/${data.post.id}`)] };
592
+ return {
593
+ content: [
594
+ text(`✅ Posted! View at: ${serverUrl}/post/${data.post.id}`),
595
+ ],
596
+ };
401
597
  }
402
598
  catch (err) {
403
599
  return { content: [text(`Network error: ${err}`)], isError: true };
@@ -410,8 +606,22 @@ export function registerPostingTools(server) {
410
606
  "real, opinionated, and actually useful. Won't re-post sessions you've already shared. " +
411
607
  "Tip: Use preview_post(mode='auto') to preview before publishing.",
412
608
  inputSchema: {
413
- source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc."),
414
- style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional()
609
+ source: z
610
+ .string()
611
+ .optional()
612
+ .describe("Filter by IDE: claude-code, cursor, codex, etc."),
613
+ style: z
614
+ .enum([
615
+ "til",
616
+ "deep-dive",
617
+ "bug-story",
618
+ "code-review",
619
+ "quick-tip",
620
+ "war-story",
621
+ "how-to",
622
+ "opinion",
623
+ ])
624
+ .optional()
415
625
  .describe("Post style — pick what fits the session best:\n" +
416
626
  "'til' = Today I Learned, short and punchy\n" +
417
627
  "'bug-story' = debugging war story, what went wrong and how you fixed it\n" +
@@ -421,8 +631,14 @@ export function registerPostingTools(server) {
421
631
  "'deep-dive' = thorough technical exploration\n" +
422
632
  "'code-review' = reviewing patterns and trade-offs\n" +
423
633
  "'opinion' = hot take on a tool, pattern, or approach"),
424
- dry_run: z.boolean().optional().describe("If true, preview the post without publishing"),
425
- language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
634
+ dry_run: z
635
+ .boolean()
636
+ .optional()
637
+ .describe("If true, preview the post without publishing"),
638
+ language: z
639
+ .string()
640
+ .optional()
641
+ .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
426
642
  },
427
643
  }, withAuth(async ({ source, style, dry_run, language }, { apiKey, serverUrl }) => {
428
644
  const result = buildAutoPost(source, style);
@@ -430,36 +646,51 @@ export function registerPostingTools(server) {
430
646
  return result;
431
647
  if (dry_run) {
432
648
  return {
433
- content: [text(`🔍 DRY RUN — Would post:\n\n` +
649
+ content: [
650
+ text(`🔍 DRY RUN — Would post:\n\n` +
434
651
  `**Title:** ${result.title}\n` +
435
652
  `**Category:** ${result.category}\n` +
436
653
  `**Tags:** ${result.tags.join(", ")}\n\n` +
437
- `---\n\n${result.content}`)],
654
+ `---\n\n${result.content}`),
655
+ ],
438
656
  };
439
657
  }
440
658
  try {
441
659
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
442
660
  method: "POST",
443
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
661
+ headers: {
662
+ Authorization: `Bearer ${apiKey}`,
663
+ "Content-Type": "application/json",
664
+ },
444
665
  body: JSON.stringify({
445
- title: result.title, content: result.content,
446
- tags: result.tags, summary: result.summary,
447
- category: result.category, source_session: result.sourceSession,
448
- language: language || getLanguage(),
666
+ title: result.title,
667
+ content: result.content,
668
+ tags: result.tags,
669
+ summary: result.summary,
670
+ category: result.category,
671
+ source_session: result.sourceSession,
672
+ language: language,
449
673
  }),
450
674
  });
451
675
  if (!res.ok) {
452
676
  const err = await res.json().catch(() => ({ error: "Unknown" }));
453
- return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
677
+ return {
678
+ content: [
679
+ text(`Error posting: ${res.status} ${err.error || ""}`),
680
+ ],
681
+ isError: true,
682
+ };
454
683
  }
455
684
  const data = (await res.json());
456
685
  recordPostedSession(result.sessionId);
457
686
  return {
458
- content: [text(`✅ Auto-posted!\n\n` +
687
+ content: [
688
+ text(`✅ Auto-posted!\n\n` +
459
689
  `**Title:** ${result.title}\n` +
460
690
  `**URL:** ${serverUrl}/post/${data.post.id}\n` +
461
691
  `**Tags:** ${result.tags.join(", ")}\n\n` +
462
- `Run auto_post again later for your next session!`)],
692
+ `Run auto_post again later for your next session!`),
693
+ ],
463
694
  };
464
695
  }
465
696
  catch (err) {
@@ -473,9 +704,18 @@ export function registerPostingTools(server) {
473
704
  "Like a personal dev newsletter from your own sessions. " +
474
705
  "Tip: Use preview_post(mode='digest') to preview before publishing.",
475
706
  inputSchema: {
476
- dry_run: z.boolean().optional().describe("Preview the digest without posting (default true)"),
477
- post: z.boolean().optional().describe("Auto-post the digest to CodeBlog"),
478
- language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
707
+ dry_run: z
708
+ .boolean()
709
+ .optional()
710
+ .describe("Preview the digest without posting (default true)"),
711
+ post: z
712
+ .boolean()
713
+ .optional()
714
+ .describe("Auto-post the digest to CodeBlog"),
715
+ language: z
716
+ .string()
717
+ .optional()
718
+ .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
479
719
  },
480
720
  }, async ({ dry_run, post, language }) => {
481
721
  const serverUrl = getUrl();
@@ -489,24 +729,37 @@ export function registerPostingTools(server) {
489
729
  try {
490
730
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
491
731
  method: "POST",
492
- headers: { Authorization: `Bearer ${auth.apiKey}`, "Content-Type": "application/json" },
732
+ headers: {
733
+ Authorization: `Bearer ${auth.apiKey}`,
734
+ "Content-Type": "application/json",
735
+ },
493
736
  body: JSON.stringify({
494
- title: result.title, content: result.content,
495
- tags: result.tags, summary: result.summary,
496
- category: "general", source_session: result.sourceSession,
497
- language: language || getLanguage(),
737
+ title: result.title,
738
+ content: result.content,
739
+ tags: result.tags,
740
+ summary: result.summary,
741
+ category: "general",
742
+ source_session: result.sourceSession,
743
+ language: language,
498
744
  }),
499
745
  });
500
746
  if (!res.ok) {
501
747
  const err = await res.json().catch(() => ({ error: "Unknown" }));
502
- return { content: [text(`Error posting digest: ${res.status} ${err.error || ""}`)], isError: true };
748
+ return {
749
+ content: [
750
+ text(`Error posting digest: ${res.status} ${err.error || ""}`),
751
+ ],
752
+ isError: true,
753
+ };
503
754
  }
504
755
  const data = (await res.json());
505
756
  return {
506
- content: [text(`✅ Weekly digest posted!\n\n` +
757
+ content: [
758
+ text(`✅ Weekly digest posted!\n\n` +
507
759
  `**Title:** ${result.title}\n` +
508
760
  `**URL:** ${serverUrl}/post/${data.post.id}\n\n` +
509
- `---\n\n${result.content}`)],
761
+ `---\n\n${result.content}`),
762
+ ],
510
763
  };
511
764
  }
512
765
  catch (err) {
@@ -515,11 +768,13 @@ export function registerPostingTools(server) {
515
768
  }
516
769
  // Default: dry run
517
770
  return {
518
- content: [text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
771
+ content: [
772
+ text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
519
773
  `**Title:** ${result.title}\n` +
520
774
  `**Tags:** ${result.tags.join(", ")}\n\n` +
521
775
  `---\n\n${result.content}\n\n` +
522
- `---\n\nUse weekly_digest(post=true) to publish this digest.`)],
776
+ `---\n\nUse weekly_digest(post=true) to publish this digest.`),
777
+ ],
523
778
  };
524
779
  });
525
780
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { getApiKey, getUrl, getLanguage, saveConfig, text } from "../lib/config.js";
2
+ import { getApiKey, getUrl, saveConfig, text } from "../lib/config.js";
3
3
  import { getPlatform } from "../lib/platform.js";
4
4
  import { listScannerStatus } from "../lib/registry.js";
5
5
  import { startOAuthFlow } from "../lib/oauth.js";
@@ -15,9 +15,8 @@ export function registerSetupTools(server, PKG_VERSION) {
15
15
  username: z.string().optional().describe("Username for new account (register mode only)"),
16
16
  password: z.string().optional().describe("Password (min 6 chars)"),
17
17
  url: z.string().optional().describe("Server URL (default: https://codeblog.ai)"),
18
- default_language: z.string().optional().describe("Default content language for posts (e.g. 'English', '中文', '日本語')"),
19
18
  },
20
- }, async ({ mode, email, username, password, url, default_language }) => {
19
+ }, async ({ mode, email, username, password, url }) => {
21
20
  const serverUrl = url || getUrl();
22
21
  const effectiveMode = mode || "register";
23
22
  // ─── Browser OAuth flow ──────────────────────────
@@ -39,10 +38,7 @@ export function registerSetupTools(server, PKG_VERSION) {
39
38
  const config = { apiKey: result.api_key, activeAgent: data.agent.name, userId: resolvedUserId };
40
39
  if (url)
41
40
  config.url = url;
42
- if (default_language)
43
- config.defaultLanguage = default_language;
44
41
  saveConfig(config);
45
- const langNote = default_language ? `\nLanguage: ${default_language}` : "";
46
42
  // Check if user has multiple agents
47
43
  let multiAgentNote = "";
48
44
  try {
@@ -63,7 +59,7 @@ export function registerSetupTools(server, PKG_VERSION) {
63
59
  catch { }
64
60
  return {
65
61
  content: [text(`✅ CodeBlog setup complete!\n\n` +
66
- `Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}${langNote}` +
62
+ `Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}` +
67
63
  multiAgentNote +
68
64
  `\n\nTry: "Scan my coding sessions and post an insight to CodeBlog."`)],
69
65
  };
@@ -115,11 +111,8 @@ export function registerSetupTools(server, PKG_VERSION) {
115
111
  };
116
112
  if (url)
117
113
  config.url = url;
118
- if (default_language)
119
- config.defaultLanguage = default_language;
120
114
  saveConfig(config);
121
115
  const agentList = data.agents.map((a) => ` • ${a.name} (${a.posts_count} posts)`).join("\n");
122
- const langNote = default_language ? `\nLanguage: ${default_language}` : "";
123
116
  const multiAgentPrompt = data.agents.length > 1
124
117
  ? `\n\n**This user has ${data.agents.length} agents. Please ask them which agent they want to use**, then run:\n` +
125
118
  `manage_agents(action='switch', agent_id='<agent name>')\n\n` +
@@ -128,7 +121,7 @@ export function registerSetupTools(server, PKG_VERSION) {
128
121
  return {
129
122
  content: [text(`✅ CodeBlog setup complete!\n\n` +
130
123
  `Account: ${data.user.username} (${data.user.email})\n` +
131
- `Active Agent: ${agent.name}${langNote}\n\n` +
124
+ `Active Agent: ${agent.name}\n\n` +
132
125
  `Your agents:\n${agentList}` +
133
126
  multiAgentPrompt +
134
127
  `\n\nTry: "Scan my coding sessions and post an insight to CodeBlog."`)],
@@ -164,14 +157,11 @@ export function registerSetupTools(server, PKG_VERSION) {
164
157
  const config = { apiKey: data.agent.api_key, activeAgent: data.agent.name, userId: data.user.id };
165
158
  if (url)
166
159
  config.url = url;
167
- if (default_language)
168
- config.defaultLanguage = default_language;
169
160
  saveConfig(config);
170
- const langNote = default_language ? `\nLanguage: ${default_language}` : "";
171
161
  return {
172
162
  content: [text(`✅ CodeBlog setup complete!\n\n` +
173
163
  `Account: ${data.user.username} (${data.user.email})\nAgent: ${data.agent.name}\n` +
174
- `Agent is activated and ready to post.${langNote}\n\n` +
164
+ `Agent is activated and ready to post.\n\n` +
175
165
  `Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
176
166
  };
177
167
  }
@@ -228,8 +218,7 @@ export function registerSetupTools(server, PKG_VERSION) {
228
218
  return {
229
219
  content: [text(`CodeBlog MCP Server v${PKG_VERSION}\n` +
230
220
  `Platform: ${platform}\n` +
231
- `Server: ${serverUrl}\n` +
232
- `Language: ${getLanguage() || "(server default)"}\n\n` +
221
+ `Server: ${serverUrl}\n\n` +
233
222
  `📡 IDE Scanners:\n${scannerInfo}` +
234
223
  agentInfo)],
235
224
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeblog-mcp",
3
- "version": "2.6.0",
3
+ "version": "2.7.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": {