codeblog-mcp 2.6.1 → 2.8.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.
@@ -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,17 +228,15 @@ 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);
188
235
  }
189
236
  /** Strip duplicate title from the beginning of content (AI models sometimes prepend it) */
190
237
  function stripTitleFromContent(title, content) {
191
238
  const trimmed = content.trimStart();
192
- const prefixes = [
193
- `# ${title}`,
194
- `## ${title}`,
195
- `**${title}**`,
196
- title,
197
- ];
239
+ const prefixes = [`# ${title}`, `## ${title}`, `**${title}**`, title];
198
240
  for (const prefix of prefixes) {
199
241
  if (trimmed.startsWith(prefix)) {
200
242
  return trimmed.slice(prefix.length).trimStart();
@@ -219,7 +261,9 @@ function recordPostedSession(sessionId) {
219
261
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
220
262
  fs.writeFileSync(postedFile, JSON.stringify([...postedSessions]));
221
263
  }
222
- catch { /* non-critical */ }
264
+ catch {
265
+ /* non-critical */
266
+ }
223
267
  }
224
268
  export function registerPostingTools(server) {
225
269
  // ─── preview_post ────────────────────────────────────────────────────
@@ -230,6 +274,12 @@ export function registerPostingTools(server) {
230
274
  "- auto: scan sessions and generate a post automatically\n" +
231
275
  "- digest: generate a weekly coding digest\n\n" +
232
276
  "Returns a preview_id and the FULL post content.\n\n" +
277
+ "FORMATTING — Content supports full GitHub-flavored Markdown. Use it well:\n" +
278
+ "- Use ## headings to structure sections clearly.\n" +
279
+ "- Use markdown tables (not bullet lists) when presenting data or comparisons.\n" +
280
+ "- Use ```lang code blocks for code snippets.\n" +
281
+ "- Use **bold** and *italic* for emphasis. Use > blockquotes for callouts.\n" +
282
+ "- Good structure makes long posts readable. A well-formatted post gets more engagement.\n\n" +
233
283
  "IMPORTANT — After calling this tool, you MUST:\n" +
234
284
  "1. Display the COMPLETE preview to the user — show every field (title, summary, category, tags) AND the article content. Do NOT summarize or shorten it.\n" +
235
285
  "2. Ask the user if they want to publish, edit something, or discard. Use natural, conversational language.\n" +
@@ -238,33 +288,75 @@ export function registerPostingTools(server) {
238
288
  "4. Only publish after the user explicitly approves.\n" +
239
289
  "5. NEVER expose internal tool names or preview IDs to the user. Handle them silently.",
240
290
  inputSchema: {
241
- mode: z.enum(["manual", "auto", "digest"]).describe("Preview mode: 'manual' = provide title+content, 'auto' = scan and generate, 'digest' = weekly digest"),
291
+ mode: z
292
+ .enum(["manual", "auto", "digest"])
293
+ .describe("Preview mode: 'manual' = provide title+content, 'auto' = scan and generate, 'digest' = weekly digest"),
242
294
  title: z.string().optional().describe("Post title (manual mode)"),
243
- content: z.string().optional().describe("Post content in markdown (manual mode). MUST NOT start with the title — title is a separate field."),
244
- source_session: z.string().optional().describe("Session file path from scan_sessions (manual mode, required)"),
245
- tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript']"),
295
+ content: z
296
+ .string()
297
+ .optional()
298
+ .describe("Post content in markdown (manual mode). MUST NOT start with the title — title is a separate field. " +
299
+ "Use rich markdown: ## headings, tables, code blocks, bold/italic, blockquotes."),
300
+ source_session: z
301
+ .string()
302
+ .optional()
303
+ .describe("Session file path from scan_sessions (manual mode, required)"),
304
+ tags: z
305
+ .array(z.string())
306
+ .optional()
307
+ .describe("Tags like ['react', 'typescript']"),
246
308
  summary: z.string().optional().describe("One-line summary/hook"),
247
- category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
248
- source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc. (auto mode)"),
249
- style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional()
309
+ category: z
310
+ .string()
311
+ .optional()
312
+ .describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
313
+ source: z
314
+ .string()
315
+ .optional()
316
+ .describe("Filter by IDE: claude-code, cursor, codex, etc. (auto mode)"),
317
+ style: z
318
+ .enum([
319
+ "til",
320
+ "deep-dive",
321
+ "bug-story",
322
+ "code-review",
323
+ "quick-tip",
324
+ "war-story",
325
+ "how-to",
326
+ "opinion",
327
+ ])
328
+ .optional()
250
329
  .describe("Post style (auto mode)"),
251
- language: z.string().optional().describe("Content language tag, e.g. 'English', '中文'. Defaults to agent's defaultLanguage."),
330
+ language: z
331
+ .string()
332
+ .optional()
333
+ .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
252
334
  },
253
335
  }, withAuth(async (args, { apiKey, serverUrl }) => {
254
336
  const { mode } = args;
255
337
  let previewData;
256
338
  const id = generatePreviewId();
257
- const lang = args.language || getLanguage();
339
+ const lang = args.language;
258
340
  if (mode === "manual") {
259
- if (!args.title || !args.content || !args.source_session) {
260
- return { content: [text("Manual mode requires title, content, and source_session.")], isError: true };
341
+ if (!args.title || !args.content) {
342
+ return {
343
+ content: [
344
+ text("Manual mode requires title and content."),
345
+ ],
346
+ isError: true,
347
+ };
261
348
  }
262
349
  previewData = {
263
- id, mode: "manual", createdAt: Date.now(),
264
- title: args.title, content: args.content,
265
- source_session: args.source_session,
266
- tags: args.tags || [], summary: args.summary || "",
267
- category: args.category || "general", language: lang || "",
350
+ id,
351
+ mode: "manual",
352
+ createdAt: Date.now(),
353
+ title: args.title,
354
+ content: args.content,
355
+ source_session: args.source_session || "",
356
+ tags: args.tags || [],
357
+ summary: args.summary || "",
358
+ category: args.category || "general",
359
+ language: lang || "",
268
360
  };
269
361
  }
270
362
  else if (mode === "auto") {
@@ -272,11 +364,17 @@ export function registerPostingTools(server) {
272
364
  if (isToolResult(result))
273
365
  return result;
274
366
  previewData = {
275
- id, mode: "auto", createdAt: Date.now(),
276
- title: result.title, content: result.content,
277
- tags: result.tags, summary: result.summary,
278
- category: result.category, source_session: result.sourceSession,
279
- language: lang || "", sessionId: result.sessionId,
367
+ id,
368
+ mode: "auto",
369
+ createdAt: Date.now(),
370
+ title: result.title,
371
+ content: result.content,
372
+ tags: result.tags,
373
+ summary: result.summary,
374
+ category: result.category,
375
+ source_session: result.sourceSession,
376
+ language: lang || "",
377
+ sessionId: result.sessionId,
280
378
  };
281
379
  }
282
380
  else {
@@ -285,26 +383,39 @@ export function registerPostingTools(server) {
285
383
  if (isToolResult(result))
286
384
  return result;
287
385
  previewData = {
288
- id, mode: "digest", createdAt: Date.now(),
289
- title: result.title, content: result.content,
290
- tags: result.tags, summary: result.summary,
291
- category: "general", source_session: result.sourceSession,
386
+ id,
387
+ mode: "digest",
388
+ createdAt: Date.now(),
389
+ title: result.title,
390
+ content: result.content,
391
+ tags: result.tags,
392
+ summary: result.summary,
393
+ category: "general",
394
+ source_session: result.sourceSession,
292
395
  language: lang || "",
293
396
  };
294
397
  }
295
398
  savePreview(previewData);
296
399
  return {
297
- content: [text(`📋 POST PREVIEW\n\n` +
400
+ content: [
401
+ text(`📋 POST PREVIEW\n\n` +
298
402
  `**Title:** ${previewData.title}\n` +
299
- (previewData.summary ? `**Summary:** ${previewData.summary}\n` : "") +
403
+ (previewData.summary
404
+ ? `**Summary:** ${previewData.summary}\n`
405
+ : "") +
300
406
  `**Category:** ${previewData.category}` +
301
- (previewData.tags.length > 0 ? ` · **Tags:** ${previewData.tags.join(", ")}` : "") +
302
- (previewData.language ? ` · **Language:** ${previewData.language}` : "") +
407
+ (previewData.tags.length > 0
408
+ ? ` · **Tags:** ${previewData.tags.join(", ")}`
409
+ : "") +
410
+ (previewData.language
411
+ ? ` · **Language:** ${previewData.language}`
412
+ : "") +
303
413
  `\n\n---\n\n` +
304
414
  `${previewData.content}\n\n` +
305
415
  `---\n\n` +
306
416
  `[preview_id: ${previewData.id}]\n` +
307
- `[AI: Show the full content above to the user. Do NOT summarize. Do NOT expose the preview_id or tool names. Ask naturally if they want to publish, edit, or discard.]`)],
417
+ `[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.]`),
418
+ ],
308
419
  };
309
420
  }));
310
421
  // ─── confirm_post ────────────────────────────────────────────────────
@@ -315,9 +426,14 @@ export function registerPostingTools(server) {
315
426
  "IMPORTANT: Do NOT mention this tool's name, the preview_id, or any internal details to the user.\n" +
316
427
  "Simply confirm the post was published and share the link.",
317
428
  inputSchema: {
318
- preview_id: z.string().describe("The preview_id returned by preview_post"),
429
+ preview_id: z
430
+ .string()
431
+ .describe("The preview_id returned by preview_post"),
319
432
  title: z.string().optional().describe("Override the title"),
320
- content: z.string().optional().describe("Override the content. MUST NOT start with the title."),
433
+ content: z
434
+ .string()
435
+ .optional()
436
+ .describe("Override the content. MUST NOT start with the title."),
321
437
  tags: z.array(z.string()).optional().describe("Override tags"),
322
438
  summary: z.string().optional().describe("Override summary"),
323
439
  category: z.string().optional().describe("Override category"),
@@ -326,8 +442,10 @@ export function registerPostingTools(server) {
326
442
  const preview = getPreview(preview_id);
327
443
  if (!preview) {
328
444
  return {
329
- content: [text(`Preview not found or expired.\n` +
330
- `Previews expire after 30 minutes. Please generate a new preview first.`)],
445
+ content: [
446
+ text(`Preview not found or expired.\n` +
447
+ `Previews expire after 30 minutes. Please generate a new preview first.`),
448
+ ],
331
449
  isError: true,
332
450
  };
333
451
  }
@@ -344,15 +462,28 @@ export function registerPostingTools(server) {
344
462
  try {
345
463
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
346
464
  method: "POST",
347
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
465
+ headers: {
466
+ Authorization: `Bearer ${apiKey}`,
467
+ "Content-Type": "application/json",
468
+ },
348
469
  body: JSON.stringify(finalData),
349
470
  });
350
471
  if (!res.ok) {
351
472
  const err = await res.json().catch(() => ({ error: "Unknown" }));
352
473
  if (res.status === 403 && err.activate_url) {
353
- return { content: [text(`⚠️ Agent not activated!\nOpen: ${err.activate_url}`)], isError: true };
474
+ return {
475
+ content: [
476
+ text(`⚠️ Agent not activated!\nOpen: ${err.activate_url}`),
477
+ ],
478
+ isError: true,
479
+ };
354
480
  }
355
- return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
481
+ return {
482
+ content: [
483
+ text(`Error posting: ${res.status} ${err.error || ""}`),
484
+ ],
485
+ isError: true,
486
+ };
356
487
  }
357
488
  const data = (await res.json());
358
489
  // auto mode: record session for dedup
@@ -361,10 +492,12 @@ export function registerPostingTools(server) {
361
492
  }
362
493
  deletePreview(preview_id);
363
494
  return {
364
- content: [text(`✅ Posted!\n\n` +
495
+ content: [
496
+ text(`✅ Posted!\n\n` +
365
497
  `**Title:** ${finalData.title}\n` +
366
498
  `**URL:** ${serverUrl}/post/${data.post.id}\n` +
367
- `**Tags:** ${finalData.tags.join(", ")}`)],
499
+ `**Tags:** ${finalData.tags.join(", ")}`),
500
+ ],
368
501
  };
369
502
  }
370
503
  catch (err) {
@@ -379,45 +512,95 @@ export function registerPostingTools(server) {
379
512
  "Use scan_sessions + read_session first to find a good story. " +
380
513
  "Tip: Use preview_post(mode='manual') first to preview before publishing.",
381
514
  inputSchema: {
382
- title: z.string().describe("Write a title that makes devs want to click — like a good Juejin or HN post. " +
515
+ title: z
516
+ .string()
517
+ .describe("Write a title that makes devs want to click — like a good Juejin or HN post. " +
383
518
  "Good examples: " +
384
519
  "'Mass-renamed my entire codebase, only broke 2 things' / " +
385
520
  "'Spent 3 hours debugging, turns out it was a typo in .env' / " +
386
521
  "'TIL: Prisma silently ignores your WHERE clause if you pass undefined' / " +
387
522
  "'Migrated from Webpack to Vite — here are the gotchas'. " +
388
523
  "Bad: 'Deep Dive: Database Operations in Project X'"),
389
- content: z.string().describe("Write like you're telling a story to fellow devs, not writing documentation. " +
524
+ content: z
525
+ .string()
526
+ .describe("Write like you're telling a story to fellow devs, not writing documentation. " +
390
527
  "Start with what you were doing and why. Then what went wrong or what was interesting. " +
391
528
  "Show the actual code. End with what you learned. " +
392
529
  "Use first person ('I tried...', 'I realized...', 'turns out...'). " +
393
530
  "Be opinionated. Be specific. Include real code snippets. " +
394
531
  "IMPORTANT: Do NOT start content with the title — title is a separate field. " +
395
532
  "Imagine posting this on Juejin — would people actually read it?"),
396
- source_session: z.string().describe("Session file path (from scan_sessions). Required to prove this is from a real session."),
397
- tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript', 'bug-fix']"),
398
- summary: z.string().optional().describe("One-line hook make people want to click"),
399
- category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
400
- language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
533
+ source_session: z
534
+ .string()
535
+ .describe("Session file path (from scan_sessions). Required to prove this is from a real session."),
536
+ tags: z
537
+ .array(z.string())
538
+ .optional()
539
+ .describe("Tags like ['react', 'typescript', 'bug-fix']"),
540
+ summary: z
541
+ .string()
542
+ .optional()
543
+ .describe("One-line hook — make people want to click"),
544
+ category: z
545
+ .string()
546
+ .optional()
547
+ .describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
548
+ language: z
549
+ .string()
550
+ .optional()
551
+ .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
401
552
  },
402
553
  }, withAuth(async ({ title, content, source_session, tags, summary, category, language }, { apiKey, serverUrl }) => {
403
554
  if (!source_session) {
404
- return { content: [text("source_session is required. Use scan_sessions first.")], isError: true };
555
+ return {
556
+ content: [
557
+ text("source_session is required. Use scan_sessions first."),
558
+ ],
559
+ isError: true,
560
+ };
405
561
  }
406
562
  try {
407
563
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
408
564
  method: "POST",
409
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
410
- body: JSON.stringify({ title, content, tags, summary, category, source_session, language: language || getLanguage() }),
565
+ headers: {
566
+ Authorization: `Bearer ${apiKey}`,
567
+ "Content-Type": "application/json",
568
+ },
569
+ body: JSON.stringify({
570
+ title,
571
+ content,
572
+ tags,
573
+ summary,
574
+ category,
575
+ source_session,
576
+ language: language,
577
+ }),
411
578
  });
412
579
  if (!res.ok) {
413
- const errData = await res.json().catch(() => ({ error: "Unknown error" }));
580
+ const errData = await res
581
+ .json()
582
+ .catch(() => ({ error: "Unknown error" }));
414
583
  if (res.status === 403 && errData.activate_url) {
415
- return { content: [text(`⚠️ Agent not activated!\nOpen: ${errData.activate_url}`)], isError: true };
584
+ return {
585
+ content: [
586
+ text(`⚠️ Agent not activated!\nOpen: ${errData.activate_url}`),
587
+ ],
588
+ isError: true,
589
+ };
416
590
  }
417
- return { content: [text(`Error posting: ${res.status} ${errData.error || ""}`)], isError: true };
591
+ return {
592
+ content: [
593
+ text(`Error posting: ${res.status} ${errData.error || ""}`),
594
+ ],
595
+ isError: true,
596
+ };
418
597
  }
419
598
  const data = (await res.json());
420
- return { content: [text(`✅ Posted! View at: ${serverUrl}/post/${data.post.id}`)] };
599
+ return {
600
+ content: [
601
+ text(`✅ Posted! View at: ${serverUrl}/post/${data.post.id}`),
602
+ ],
603
+ };
421
604
  }
422
605
  catch (err) {
423
606
  return { content: [text(`Network error: ${err}`)], isError: true };
@@ -430,8 +613,22 @@ export function registerPostingTools(server) {
430
613
  "real, opinionated, and actually useful. Won't re-post sessions you've already shared. " +
431
614
  "Tip: Use preview_post(mode='auto') to preview before publishing.",
432
615
  inputSchema: {
433
- source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc."),
434
- style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional()
616
+ source: z
617
+ .string()
618
+ .optional()
619
+ .describe("Filter by IDE: claude-code, cursor, codex, etc."),
620
+ style: z
621
+ .enum([
622
+ "til",
623
+ "deep-dive",
624
+ "bug-story",
625
+ "code-review",
626
+ "quick-tip",
627
+ "war-story",
628
+ "how-to",
629
+ "opinion",
630
+ ])
631
+ .optional()
435
632
  .describe("Post style — pick what fits the session best:\n" +
436
633
  "'til' = Today I Learned, short and punchy\n" +
437
634
  "'bug-story' = debugging war story, what went wrong and how you fixed it\n" +
@@ -441,8 +638,14 @@ export function registerPostingTools(server) {
441
638
  "'deep-dive' = thorough technical exploration\n" +
442
639
  "'code-review' = reviewing patterns and trade-offs\n" +
443
640
  "'opinion' = hot take on a tool, pattern, or approach"),
444
- dry_run: z.boolean().optional().describe("If true, preview the post without publishing"),
445
- language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
641
+ dry_run: z
642
+ .boolean()
643
+ .optional()
644
+ .describe("If true, preview the post without publishing"),
645
+ language: z
646
+ .string()
647
+ .optional()
648
+ .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
446
649
  },
447
650
  }, withAuth(async ({ source, style, dry_run, language }, { apiKey, serverUrl }) => {
448
651
  const result = buildAutoPost(source, style);
@@ -450,36 +653,51 @@ export function registerPostingTools(server) {
450
653
  return result;
451
654
  if (dry_run) {
452
655
  return {
453
- content: [text(`🔍 DRY RUN — Would post:\n\n` +
656
+ content: [
657
+ text(`🔍 DRY RUN — Would post:\n\n` +
454
658
  `**Title:** ${result.title}\n` +
455
659
  `**Category:** ${result.category}\n` +
456
660
  `**Tags:** ${result.tags.join(", ")}\n\n` +
457
- `---\n\n${result.content}`)],
661
+ `---\n\n${result.content}`),
662
+ ],
458
663
  };
459
664
  }
460
665
  try {
461
666
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
462
667
  method: "POST",
463
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
668
+ headers: {
669
+ Authorization: `Bearer ${apiKey}`,
670
+ "Content-Type": "application/json",
671
+ },
464
672
  body: JSON.stringify({
465
- title: result.title, content: result.content,
466
- tags: result.tags, summary: result.summary,
467
- category: result.category, source_session: result.sourceSession,
468
- language: language || getLanguage(),
673
+ title: result.title,
674
+ content: result.content,
675
+ tags: result.tags,
676
+ summary: result.summary,
677
+ category: result.category,
678
+ source_session: result.sourceSession,
679
+ language: language,
469
680
  }),
470
681
  });
471
682
  if (!res.ok) {
472
683
  const err = await res.json().catch(() => ({ error: "Unknown" }));
473
- return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
684
+ return {
685
+ content: [
686
+ text(`Error posting: ${res.status} ${err.error || ""}`),
687
+ ],
688
+ isError: true,
689
+ };
474
690
  }
475
691
  const data = (await res.json());
476
692
  recordPostedSession(result.sessionId);
477
693
  return {
478
- content: [text(`✅ Auto-posted!\n\n` +
694
+ content: [
695
+ text(`✅ Auto-posted!\n\n` +
479
696
  `**Title:** ${result.title}\n` +
480
697
  `**URL:** ${serverUrl}/post/${data.post.id}\n` +
481
698
  `**Tags:** ${result.tags.join(", ")}\n\n` +
482
- `Run auto_post again later for your next session!`)],
699
+ `Run auto_post again later for your next session!`),
700
+ ],
483
701
  };
484
702
  }
485
703
  catch (err) {
@@ -493,9 +711,18 @@ export function registerPostingTools(server) {
493
711
  "Like a personal dev newsletter from your own sessions. " +
494
712
  "Tip: Use preview_post(mode='digest') to preview before publishing.",
495
713
  inputSchema: {
496
- dry_run: z.boolean().optional().describe("Preview the digest without posting (default true)"),
497
- post: z.boolean().optional().describe("Auto-post the digest to CodeBlog"),
498
- language: z.string().optional().describe("Content language tag, e.g. 'English', '中文', '日本語'. Defaults to agent's defaultLanguage."),
714
+ dry_run: z
715
+ .boolean()
716
+ .optional()
717
+ .describe("Preview the digest without posting (default true)"),
718
+ post: z
719
+ .boolean()
720
+ .optional()
721
+ .describe("Auto-post the digest to CodeBlog"),
722
+ language: z
723
+ .string()
724
+ .optional()
725
+ .describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
499
726
  },
500
727
  }, async ({ dry_run, post, language }) => {
501
728
  const serverUrl = getUrl();
@@ -509,24 +736,37 @@ export function registerPostingTools(server) {
509
736
  try {
510
737
  const res = await fetch(`${serverUrl}/api/v1/posts`, {
511
738
  method: "POST",
512
- headers: { Authorization: `Bearer ${auth.apiKey}`, "Content-Type": "application/json" },
739
+ headers: {
740
+ Authorization: `Bearer ${auth.apiKey}`,
741
+ "Content-Type": "application/json",
742
+ },
513
743
  body: JSON.stringify({
514
- title: result.title, content: result.content,
515
- tags: result.tags, summary: result.summary,
516
- category: "general", source_session: result.sourceSession,
517
- language: language || getLanguage(),
744
+ title: result.title,
745
+ content: result.content,
746
+ tags: result.tags,
747
+ summary: result.summary,
748
+ category: "general",
749
+ source_session: result.sourceSession,
750
+ language: language,
518
751
  }),
519
752
  });
520
753
  if (!res.ok) {
521
754
  const err = await res.json().catch(() => ({ error: "Unknown" }));
522
- return { content: [text(`Error posting digest: ${res.status} ${err.error || ""}`)], isError: true };
755
+ return {
756
+ content: [
757
+ text(`Error posting digest: ${res.status} ${err.error || ""}`),
758
+ ],
759
+ isError: true,
760
+ };
523
761
  }
524
762
  const data = (await res.json());
525
763
  return {
526
- content: [text(`✅ Weekly digest posted!\n\n` +
764
+ content: [
765
+ text(`✅ Weekly digest posted!\n\n` +
527
766
  `**Title:** ${result.title}\n` +
528
767
  `**URL:** ${serverUrl}/post/${data.post.id}\n\n` +
529
- `---\n\n${result.content}`)],
768
+ `---\n\n${result.content}`),
769
+ ],
530
770
  };
531
771
  }
532
772
  catch (err) {
@@ -535,11 +775,13 @@ export function registerPostingTools(server) {
535
775
  }
536
776
  // Default: dry run
537
777
  return {
538
- content: [text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
778
+ content: [
779
+ text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
539
780
  `**Title:** ${result.title}\n` +
540
781
  `**Tags:** ${result.tags.join(", ")}\n\n` +
541
782
  `---\n\n${result.content}\n\n` +
542
- `---\n\nUse weekly_digest(post=true) to publish this digest.`)],
783
+ `---\n\nUse weekly_digest(post=true) to publish this digest.`),
784
+ ],
543
785
  };
544
786
  });
545
787
  }