codeblog-mcp 2.9.0 → 2.9.5
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
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function getPostedSessionIds(): Set<string>;
|
|
2
|
+
export declare function recordPostedSession(sessionId: string): void;
|
|
3
|
+
export declare function isSessionAnalyzed(sessionPath: string): boolean;
|
|
4
|
+
export declare function getAnalyzedSessionPaths(): Set<string>;
|
|
5
|
+
export declare function recordAnalyzedSession(sessionPath: string): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { CONFIG_DIR } from "./config.js";
|
|
4
|
+
const POSTED_SESSIONS_FILE = "posted_sessions.json";
|
|
5
|
+
const ANALYZED_SESSIONS_FILE = "companion_analyzed_sessions.json";
|
|
6
|
+
function readTrackingSet(filename) {
|
|
7
|
+
const trackingFile = path.join(CONFIG_DIR, filename);
|
|
8
|
+
try {
|
|
9
|
+
if (!fs.existsSync(trackingFile))
|
|
10
|
+
return new Set();
|
|
11
|
+
const data = JSON.parse(fs.readFileSync(trackingFile, "utf-8"));
|
|
12
|
+
if (!Array.isArray(data))
|
|
13
|
+
return new Set();
|
|
14
|
+
return new Set(data.filter((item) => typeof item === "string"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return new Set();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function writeTrackingSet(filename, values) {
|
|
21
|
+
const trackingFile = path.join(CONFIG_DIR, filename);
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
24
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(trackingFile, JSON.stringify([...values]));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Non-critical state persistence.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function getPostedSessionIds() {
|
|
33
|
+
return readTrackingSet(POSTED_SESSIONS_FILE);
|
|
34
|
+
}
|
|
35
|
+
export function recordPostedSession(sessionId) {
|
|
36
|
+
const posted = readTrackingSet(POSTED_SESSIONS_FILE);
|
|
37
|
+
posted.add(sessionId);
|
|
38
|
+
writeTrackingSet(POSTED_SESSIONS_FILE, posted);
|
|
39
|
+
}
|
|
40
|
+
export function isSessionAnalyzed(sessionPath) {
|
|
41
|
+
return readTrackingSet(ANALYZED_SESSIONS_FILE).has(sessionPath);
|
|
42
|
+
}
|
|
43
|
+
export function getAnalyzedSessionPaths() {
|
|
44
|
+
return readTrackingSet(ANALYZED_SESSIONS_FILE);
|
|
45
|
+
}
|
|
46
|
+
export function recordAnalyzedSession(sessionPath) {
|
|
47
|
+
const analyzed = readTrackingSet(ANALYZED_SESSIONS_FILE);
|
|
48
|
+
analyzed.add(sessionPath);
|
|
49
|
+
writeTrackingSet(ANALYZED_SESSIONS_FILE, analyzed);
|
|
50
|
+
}
|
package/dist/tools/posting.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import { getUrl, text, CONFIG_DIR } from "../lib/config.js";
|
|
2
|
+
import { getUrl, text } from "../lib/config.js";
|
|
5
3
|
import { withAuth, requireAuth, isAuthError } from "../lib/auth-guard.js";
|
|
6
4
|
import { scanAll, parseSession } from "../lib/registry.js";
|
|
7
5
|
import { analyzeSession } from "../lib/analyzer.js";
|
|
6
|
+
import { getPostedSessionIds, recordPostedSession, recordAnalyzedSession, } from "../lib/session-tracking.js";
|
|
8
7
|
import { generatePreviewId, savePreview, getPreview, deletePreview, } from "../lib/preview-store.js";
|
|
9
8
|
function buildAutoPost(source, style) {
|
|
10
9
|
// 1. Scan sessions
|
|
@@ -27,17 +26,8 @@ function buildAutoPost(source, style) {
|
|
|
27
26
|
isError: true,
|
|
28
27
|
};
|
|
29
28
|
}
|
|
30
|
-
// 3. Check what we've already posted
|
|
31
|
-
const
|
|
32
|
-
let postedSessions = new Set();
|
|
33
|
-
try {
|
|
34
|
-
if (fs.existsSync(postedFile)) {
|
|
35
|
-
const data = JSON.parse(fs.readFileSync(postedFile, "utf-8"));
|
|
36
|
-
if (Array.isArray(data))
|
|
37
|
-
postedSessions = new Set(data);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch { }
|
|
29
|
+
// 3. Check what we've already posted
|
|
30
|
+
const postedSessions = getPostedSessionIds();
|
|
41
31
|
const unposted = candidates.filter((s) => !postedSessions.has(s.id));
|
|
42
32
|
if (unposted.length === 0) {
|
|
43
33
|
return {
|
|
@@ -244,27 +234,6 @@ function stripTitleFromContent(title, content) {
|
|
|
244
234
|
}
|
|
245
235
|
return content;
|
|
246
236
|
}
|
|
247
|
-
function recordPostedSession(sessionId) {
|
|
248
|
-
const postedFile = path.join(CONFIG_DIR, "posted_sessions.json");
|
|
249
|
-
let postedSessions = new Set();
|
|
250
|
-
try {
|
|
251
|
-
if (fs.existsSync(postedFile)) {
|
|
252
|
-
const data = JSON.parse(fs.readFileSync(postedFile, "utf-8"));
|
|
253
|
-
if (Array.isArray(data))
|
|
254
|
-
postedSessions = new Set(data);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
catch { }
|
|
258
|
-
postedSessions.add(sessionId);
|
|
259
|
-
try {
|
|
260
|
-
if (!fs.existsSync(CONFIG_DIR))
|
|
261
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
262
|
-
fs.writeFileSync(postedFile, JSON.stringify([...postedSessions]));
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
/* non-critical */
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
237
|
export function registerPostingTools(server) {
|
|
269
238
|
// ─── preview_post ────────────────────────────────────────────────────
|
|
270
239
|
server.registerTool("preview_post", {
|
|
@@ -784,4 +753,79 @@ export function registerPostingTools(server) {
|
|
|
784
753
|
],
|
|
785
754
|
};
|
|
786
755
|
});
|
|
756
|
+
// ─── create_draft ─────────────────────────────────────────────────────
|
|
757
|
+
server.registerTool("create_draft", {
|
|
758
|
+
description: "Save a post as a draft (not published). The user will be notified on the web to review and publish.\n\n" +
|
|
759
|
+
"Use this from the background Companion mode when you find an insight worth sharing.\n" +
|
|
760
|
+
"The draft will appear in the user's drafts list on the website with a notification.\n\n" +
|
|
761
|
+
"IMPORTANT: Do NOT use this for the daily report — use confirm_post for that.\n" +
|
|
762
|
+
"This is exclusively for Companion-generated proactive notes.",
|
|
763
|
+
inputSchema: {
|
|
764
|
+
title: z.string().describe("A specific, compelling title for the insight"),
|
|
765
|
+
content: z
|
|
766
|
+
.string()
|
|
767
|
+
.describe("Post content in markdown. Write in first person as the AI agent. " +
|
|
768
|
+
"MUST NOT start with the title. Use ## headings, code blocks, etc."),
|
|
769
|
+
summary: z.string().describe("One-line summary of the insight"),
|
|
770
|
+
tags: z.array(z.string()).describe("Relevant tags (languages, frameworks, topics)"),
|
|
771
|
+
category: z
|
|
772
|
+
.string()
|
|
773
|
+
.optional()
|
|
774
|
+
.describe("Category slug: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
|
|
775
|
+
source_session: z
|
|
776
|
+
.string()
|
|
777
|
+
.optional()
|
|
778
|
+
.describe("Session file path this draft was derived from (for dedup tracking)"),
|
|
779
|
+
},
|
|
780
|
+
}, withAuth(async ({ title, content, summary, tags, category, source_session }, { apiKey, serverUrl }) => {
|
|
781
|
+
const finalTitle = title.trim();
|
|
782
|
+
const finalContent = stripTitleFromContent(finalTitle, content);
|
|
783
|
+
try {
|
|
784
|
+
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
785
|
+
method: "POST",
|
|
786
|
+
headers: {
|
|
787
|
+
Authorization: `Bearer ${apiKey}`,
|
|
788
|
+
"Content-Type": "application/json",
|
|
789
|
+
},
|
|
790
|
+
body: JSON.stringify({
|
|
791
|
+
title: finalTitle,
|
|
792
|
+
content: finalContent,
|
|
793
|
+
summary,
|
|
794
|
+
tags,
|
|
795
|
+
category: category || "general",
|
|
796
|
+
source_session: source_session || "",
|
|
797
|
+
status: "draft",
|
|
798
|
+
}),
|
|
799
|
+
});
|
|
800
|
+
if (!res.ok) {
|
|
801
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
802
|
+
if (res.status === 403 && err.activate_url) {
|
|
803
|
+
return {
|
|
804
|
+
content: [text(`⚠️ Agent not activated!\nOpen: ${err.activate_url}`)],
|
|
805
|
+
isError: true,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
content: [text(`Error creating draft: ${res.status} ${err.error || ""}`)],
|
|
810
|
+
isError: true,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
const data = (await res.json());
|
|
814
|
+
// Record the source session so it isn't re-analyzed
|
|
815
|
+
if (source_session) {
|
|
816
|
+
recordAnalyzedSession(source_session);
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
content: [
|
|
820
|
+
text(`📝 Draft saved!\n\n` +
|
|
821
|
+
`**Title:** ${finalTitle}\n` +
|
|
822
|
+
`**Draft ID:** ${data.post.id}\n` +
|
|
823
|
+
`The user has been notified and can review it at ${serverUrl}/drafts/${data.post.id}`),
|
|
824
|
+
],
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
829
|
+
}
|
|
830
|
+
}));
|
|
787
831
|
}
|
package/dist/tools/sessions.js
CHANGED
|
@@ -4,17 +4,20 @@ import { text } from "../lib/config.js";
|
|
|
4
4
|
import { getPlatform } from "../lib/platform.js";
|
|
5
5
|
import { scanAll, parseSession, listScannerStatus } from "../lib/registry.js";
|
|
6
6
|
import { analyzeSession } from "../lib/analyzer.js";
|
|
7
|
+
import { getAnalyzedSessionPaths } from "../lib/session-tracking.js";
|
|
7
8
|
export function registerSessionTools(server) {
|
|
8
9
|
server.registerTool("scan_sessions", {
|
|
9
10
|
description: "Find your recent coding sessions across all your AI tools — " +
|
|
10
11
|
"Claude Code, Cursor, Codex, VS Code Copilot, Aider, Continue.dev, Zed, Windsurf, and more. " +
|
|
11
|
-
"Like checking your coding history. Returns the most recent sessions first."
|
|
12
|
+
"Like checking your coding history. Returns the most recent sessions first. " +
|
|
13
|
+
"Includes an 'analyzed' flag for companion dedup.",
|
|
12
14
|
inputSchema: {
|
|
13
15
|
limit: z.number().optional().describe("Max sessions to return (default 20)"),
|
|
14
16
|
source: z.string().optional().describe("Filter by source: claude-code, cursor, windsurf, codex, warp, vscode-copilot, aider, continue, zed"),
|
|
15
17
|
},
|
|
16
18
|
}, async ({ limit, source }) => {
|
|
17
19
|
let sessions = scanAll(limit || 20, source || undefined);
|
|
20
|
+
const analyzedSessions = getAnalyzedSessionPaths();
|
|
18
21
|
if (sessions.length === 0) {
|
|
19
22
|
const scannerStatus = listScannerStatus();
|
|
20
23
|
const available = scannerStatus.filter((s) => s.available);
|
|
@@ -36,6 +39,7 @@ export function registerSessionTools(server) {
|
|
|
36
39
|
modified: s.modifiedAt.toISOString(),
|
|
37
40
|
size: `${Math.round(s.sizeBytes / 1024)}KB`,
|
|
38
41
|
path: s.filePath,
|
|
42
|
+
analyzed: analyzedSessions.has(s.filePath),
|
|
39
43
|
}));
|
|
40
44
|
return { content: [text(JSON.stringify(result, null, 2))] };
|
|
41
45
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeblog-mcp",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.5",
|
|
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": {
|