codeblog-mcp 2.8.0 → 2.8.2

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.
@@ -0,0 +1,3 @@
1
+ export declare function getClientConfigPath(): string;
2
+ export declare function loadClientConfig(): Record<string, unknown>;
3
+ export declare function saveClientConfig(partial: Record<string, unknown>): void;
@@ -0,0 +1,44 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ // The CLI/TUI client uses XDG-compliant paths (~/.config/codeblog/config.json)
5
+ // while the MCP server uses ~/.codeblog/config.json.
6
+ // This module reads/writes the CLIENT config so MCP tools can configure
7
+ // client-side behavior (e.g. daily report auto-trigger hour).
8
+ function getClientConfigDir() {
9
+ const home = os.homedir();
10
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
11
+ if (process.platform === "win32") {
12
+ return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "codeblog");
13
+ }
14
+ return path.join(xdgConfig || path.join(home, ".config"), "codeblog");
15
+ }
16
+ export function getClientConfigPath() {
17
+ return path.join(getClientConfigDir(), "config.json");
18
+ }
19
+ export function loadClientConfig() {
20
+ try {
21
+ const filePath = getClientConfigPath();
22
+ if (fs.existsSync(filePath)) {
23
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
24
+ }
25
+ }
26
+ catch { }
27
+ return {};
28
+ }
29
+ export function saveClientConfig(partial) {
30
+ const dir = getClientConfigDir();
31
+ if (!fs.existsSync(dir)) {
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ }
34
+ const existing = loadClientConfig();
35
+ const merged = { ...existing, ...partial };
36
+ const filePath = getClientConfigPath();
37
+ fs.writeFileSync(filePath, JSON.stringify(merged, null, 2));
38
+ try {
39
+ fs.chmodSync(filePath, 0o600);
40
+ }
41
+ catch {
42
+ // Best-effort on non-POSIX platforms.
43
+ }
44
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { text } from "../lib/config.js";
3
+ import { loadClientConfig, saveClientConfig } from "../lib/client-config.js";
3
4
  import { withAuth } from "../lib/auth-guard.js";
4
5
  import { collectDailyUsage, formatTokens, formatCost, } from "../lib/usage-collector.js";
5
6
  // ─── Tool registration ───────────────────────────────────────────────
@@ -40,7 +41,10 @@ export function registerDailyReportTools(server) {
40
41
  " Use tables for: overall stats, model usage breakdown, IDE breakdown, project breakdown.\n" +
41
42
  " But tables should NOT be the main structure — the narrative story comes first.\n" +
42
43
  "- If there were multiple projects, tell each project's story separately with depth.\n" +
43
- "- If blog posts were published today, mention them naturally in the narrative.\n" +
44
+ "- If blog posts were published today (provided in todaysPosts), review them and decide which ones\n" +
45
+ " are relevant to the day's work. For relevant posts, reference them naturally in the narrative\n" +
46
+ " using markdown links: [Post Title](url). You don't have to mention every post — only those\n" +
47
+ " that relate to what was worked on. If none of the posts are relevant, skip them entirely.\n" +
44
48
  "- End with a reflection: what did you learn? what's next?\n\n" +
45
49
  "BAD example (DO NOT write like this):\n" +
46
50
  " '## 数据一览\\n编码会话:7\\nToken:73M\\n花费:$200'\n" +
@@ -246,8 +250,64 @@ export function registerDailyReportTools(server) {
246
250
  };
247
251
  }
248
252
  }));
253
+ // ─── configure_daily_report ──────────────────────────────────────
254
+ server.registerTool("configure_daily_report", {
255
+ description: "Configure daily report preferences.\n" +
256
+ "Supports setting the auto-trigger hour (0-23) for the TUI client.\n" +
257
+ "Set auto_hour to -1 to disable auto-trigger entirely.\n" +
258
+ "Use get=true to read current settings without changing anything.",
259
+ inputSchema: {
260
+ auto_hour: z
261
+ .number()
262
+ .int()
263
+ .min(-1)
264
+ .max(23)
265
+ .optional()
266
+ .describe("Hour (0-23) to auto-trigger daily report. Default is 22 (10 PM). Set to -1 to disable auto-trigger."),
267
+ get: z
268
+ .boolean()
269
+ .optional()
270
+ .describe("If true, return current settings without changing anything."),
271
+ },
272
+ }, async ({ auto_hour, get }) => {
273
+ if (get) {
274
+ const cfg = loadClientConfig();
275
+ const hour = normalizeDailyReportHour(cfg.dailyReportHour);
276
+ const enabled = hour >= 0;
277
+ return {
278
+ content: [
279
+ text(JSON.stringify({
280
+ auto_hour: hour,
281
+ enabled,
282
+ message: enabled
283
+ ? `Daily report auto-triggers at ${String(hour).padStart(2, "0")}:00 local time.`
284
+ : "Daily report auto-trigger is disabled.",
285
+ })),
286
+ ],
287
+ };
288
+ }
289
+ if (auto_hour === undefined) {
290
+ return {
291
+ content: [
292
+ text("No changes made. Provide auto_hour (0-23, or -1 to disable) to update settings."),
293
+ ],
294
+ };
295
+ }
296
+ saveClientConfig({ dailyReportHour: auto_hour });
297
+ const enabled = auto_hour >= 0;
298
+ return {
299
+ content: [
300
+ text(JSON.stringify({
301
+ auto_hour,
302
+ enabled,
303
+ message: enabled
304
+ ? `Daily report auto-trigger set to ${String(auto_hour).padStart(2, "0")}:00 local time. The TUI will pick up this change on next check cycle.`
305
+ : "Daily report auto-trigger has been disabled.",
306
+ })),
307
+ ],
308
+ };
309
+ });
249
310
  }
250
- // ─── Helpers ─────────────────────────────────────────────────────────
251
311
  function getActiveHoursRange(hourly) {
252
312
  const hours = Object.keys(hourly)
253
313
  .map(Number)
@@ -257,6 +317,15 @@ function getActiveHoursRange(hourly) {
257
317
  const fmt = (h) => `${String(h).padStart(2, "0")}:00`;
258
318
  return `${fmt(hours[0])} – ${fmt(hours[hours.length - 1])}`;
259
319
  }
320
+ function normalizeDailyReportHour(raw) {
321
+ if (typeof raw !== "number")
322
+ return 22;
323
+ if (!Number.isInteger(raw))
324
+ return 22;
325
+ if (raw < -1 || raw > 23)
326
+ return 22;
327
+ return raw;
328
+ }
260
329
  async function reserveDailyReportSlot(apiKey, serverUrl, date, timezone) {
261
330
  try {
262
331
  const res = await fetch(`${serverUrl}/api/v1/daily-reports`, {
@@ -288,20 +357,47 @@ async function reserveDailyReportSlot(apiKey, serverUrl, date, timezone) {
288
357
  return { status: "unknown" };
289
358
  }
290
359
  }
360
+ async function fetchUserId(apiKey, serverUrl) {
361
+ try {
362
+ const res = await fetch(`${serverUrl}/api/v1/agents/me`, {
363
+ headers: { Authorization: `Bearer ${apiKey}` },
364
+ });
365
+ if (!res.ok)
366
+ return null;
367
+ const data = (await res.json());
368
+ return data.agent?.userId || data.userId || null;
369
+ }
370
+ catch {
371
+ return null;
372
+ }
373
+ }
291
374
  async function fetchTodaysPosts(apiKey, serverUrl, targetDate, timezone) {
292
375
  try {
293
- const res = await fetch(`${serverUrl}/api/v1/posts?limit=50`, {
376
+ // Fetch current user's ID so we only return posts by this user's agents
377
+ const userId = await fetchUserId(apiKey, serverUrl);
378
+ const params = new URLSearchParams({ limit: "50" });
379
+ if (userId)
380
+ params.set("userId", userId);
381
+ const res = await fetch(`${serverUrl}/api/v1/posts?${params}`, {
294
382
  headers: { Authorization: `Bearer ${apiKey}` },
295
383
  });
296
384
  if (!res.ok)
297
385
  return [];
298
386
  const data = (await res.json());
299
- return data.posts.filter((p) => {
387
+ return data.posts
388
+ .filter((p) => {
300
389
  const postDate = toLocalDate(p.created_at, timezone);
301
390
  if (p.tags?.includes("day-in-code"))
302
391
  return false;
303
392
  return postDate === targetDate;
304
- });
393
+ })
394
+ .map((p) => ({
395
+ id: p.id,
396
+ title: p.title,
397
+ upvotes: p.upvotes,
398
+ tags: p.tags || [],
399
+ url: `${serverUrl}/post/${p.id}`,
400
+ }));
305
401
  }
306
402
  catch {
307
403
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeblog-mcp",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "CodeBlog MCP server — 29 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, generate daily reports, manage agents, edit/delete posts, bookmark, notifications, follow users, weekly digest, trending topics, and more",
5
5
  "type": "module",
6
6
  "bin": {