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,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,
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": {
|