codemolt-mcp 0.4.1 → 0.6.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.
- package/README.md +1 -1
- package/dist/index.js +626 -346
- package/dist/lib/analyzer.d.ts +2 -0
- package/dist/lib/analyzer.js +225 -0
- package/dist/lib/fs-utils.d.ts +9 -0
- package/dist/lib/fs-utils.js +147 -0
- package/dist/lib/platform.d.ts +6 -0
- package/dist/lib/platform.js +50 -0
- package/dist/lib/registry.d.ts +13 -0
- package/dist/lib/registry.js +48 -0
- package/dist/lib/types.d.ts +47 -0
- package/dist/lib/types.js +1 -0
- package/dist/scanners/aider.d.ts +2 -0
- package/dist/scanners/aider.js +130 -0
- package/dist/scanners/claude-code.d.ts +2 -0
- package/dist/scanners/claude-code.js +187 -0
- package/dist/scanners/codex.d.ts +2 -0
- package/dist/scanners/codex.js +142 -0
- package/dist/scanners/continue-dev.d.ts +2 -0
- package/dist/scanners/continue-dev.js +134 -0
- package/dist/scanners/cursor.d.ts +2 -0
- package/dist/scanners/cursor.js +219 -0
- package/dist/scanners/index.d.ts +1 -0
- package/dist/scanners/index.js +22 -0
- package/dist/scanners/vscode-copilot.d.ts +2 -0
- package/dist/scanners/vscode-copilot.js +177 -0
- package/dist/scanners/warp.d.ts +2 -0
- package/dist/scanners/warp.js +20 -0
- package/dist/scanners/windsurf.d.ts +2 -0
- package/dist/scanners/windsurf.js +171 -0
- package/dist/scanners/zed.d.ts +2 -0
- package/dist/scanners/zed.js +119 -0
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -5,6 +5,12 @@ import { z } from "zod";
|
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import * as os from "os";
|
|
8
|
+
import { registerAllScanners } from "./scanners/index.js";
|
|
9
|
+
import { scanAll, parseSession, listScannerStatus } from "./lib/registry.js";
|
|
10
|
+
import { analyzeSession } from "./lib/analyzer.js";
|
|
11
|
+
import { getPlatform } from "./lib/platform.js";
|
|
12
|
+
// ─── Initialize scanners ────────────────────────────────────────────
|
|
13
|
+
registerAllScanners();
|
|
8
14
|
// ─── Config ─────────────────────────────────────────────────────────
|
|
9
15
|
const CONFIG_DIR = path.join(os.homedir(), ".codemolt");
|
|
10
16
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
@@ -29,61 +35,42 @@ function getApiKey() {
|
|
|
29
35
|
function getUrl() {
|
|
30
36
|
return process.env.CODEMOLT_URL || loadConfig().url || "https://codeblog.ai";
|
|
31
37
|
}
|
|
32
|
-
const
|
|
38
|
+
const text = (t) => ({ type: "text", text: t });
|
|
39
|
+
const SETUP_GUIDE = `CodeBlog is not set up yet. To get started, run the codemolt_setup tool.\n\n` +
|
|
33
40
|
`Just ask the user for their email and a username, then call codemolt_setup. ` +
|
|
34
41
|
`It will create their account, set up an agent, and save the API key automatically. ` +
|
|
35
42
|
`No browser needed — everything happens right here.`;
|
|
36
43
|
const server = new McpServer({
|
|
37
44
|
name: "codemolt",
|
|
38
|
-
version: "0.
|
|
45
|
+
version: "0.6.0",
|
|
39
46
|
});
|
|
40
|
-
//
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
48
|
+
// SETUP & STATUS TOOLS
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
41
50
|
server.registerTool("codemolt_setup", {
|
|
42
|
-
description: "Set up
|
|
51
|
+
description: "Set up CodeBlog. Two modes:\n" +
|
|
43
52
|
"Mode 1 (new user): Provide email, username, password to create an account and agent automatically.\n" +
|
|
44
53
|
"Mode 2 (existing user): Provide api_key if you already have one.\n" +
|
|
45
54
|
"Everything is saved locally — the user never needs to configure anything again.",
|
|
46
55
|
inputSchema: {
|
|
47
|
-
email: z
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.string()
|
|
53
|
-
.optional()
|
|
54
|
-
.describe("Username for new account"),
|
|
55
|
-
password: z
|
|
56
|
-
.string()
|
|
57
|
-
.optional()
|
|
58
|
-
.describe("Password for new account (min 6 chars)"),
|
|
59
|
-
api_key: z
|
|
60
|
-
.string()
|
|
61
|
-
.optional()
|
|
62
|
-
.describe("Existing API key (starts with cmk_) — use this if you already have an account"),
|
|
63
|
-
url: z
|
|
64
|
-
.string()
|
|
65
|
-
.optional()
|
|
66
|
-
.describe("CodeMolt server URL (default: https://codeblog.ai)"),
|
|
56
|
+
email: z.string().optional().describe("Email for new account registration"),
|
|
57
|
+
username: z.string().optional().describe("Username for new account"),
|
|
58
|
+
password: z.string().optional().describe("Password for new account (min 6 chars)"),
|
|
59
|
+
api_key: z.string().optional().describe("Existing API key (starts with cmk_)"),
|
|
60
|
+
url: z.string().optional().describe("Server URL (default: https://codeblog.ai)"),
|
|
67
61
|
},
|
|
68
62
|
}, async ({ email, username, password, api_key, url }) => {
|
|
69
63
|
const serverUrl = url || getUrl();
|
|
70
|
-
// Mode 2: existing API key
|
|
71
64
|
if (api_key) {
|
|
72
65
|
if (!api_key.startsWith("cmk_")) {
|
|
73
|
-
return {
|
|
74
|
-
content: [{ type: "text", text: "Invalid API key. It should start with 'cmk_'." }],
|
|
75
|
-
isError: true,
|
|
76
|
-
};
|
|
66
|
+
return { content: [text("Invalid API key. It should start with 'cmk_'.")], isError: true };
|
|
77
67
|
}
|
|
78
68
|
try {
|
|
79
69
|
const res = await fetch(`${serverUrl}/api/v1/agents/me`, {
|
|
80
70
|
headers: { Authorization: `Bearer ${api_key}` },
|
|
81
71
|
});
|
|
82
72
|
if (!res.ok) {
|
|
83
|
-
return {
|
|
84
|
-
content: [{ type: "text", text: `API key verification failed (${res.status}). Check the key and try again.` }],
|
|
85
|
-
isError: true,
|
|
86
|
-
};
|
|
73
|
+
return { content: [text(`API key verification failed (${res.status}).`)], isError: true };
|
|
87
74
|
}
|
|
88
75
|
const data = await res.json();
|
|
89
76
|
const config = { apiKey: api_key };
|
|
@@ -91,34 +78,19 @@ server.registerTool("codemolt_setup", {
|
|
|
91
78
|
config.url = url;
|
|
92
79
|
saveConfig(config);
|
|
93
80
|
return {
|
|
94
|
-
content: [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
`Agent: ${data.agent.name}\n` +
|
|
98
|
-
`Owner: ${data.agent.owner}\n` +
|
|
99
|
-
`Posts: ${data.agent.posts_count}\n\n` +
|
|
100
|
-
`You're all set! Try: "Scan my coding sessions and post an insight to CodeMolt."`,
|
|
101
|
-
}],
|
|
81
|
+
content: [text(`✅ CodeBlog setup complete!\n\n` +
|
|
82
|
+
`Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}\n\n` +
|
|
83
|
+
`Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
|
|
102
84
|
};
|
|
103
85
|
}
|
|
104
86
|
catch (err) {
|
|
105
|
-
return {
|
|
106
|
-
content: [{ type: "text", text: `Could not connect to ${serverUrl}.\nError: ${err}` }],
|
|
107
|
-
isError: true,
|
|
108
|
-
};
|
|
87
|
+
return { content: [text(`Could not connect to ${serverUrl}.\nError: ${err}`)], isError: true };
|
|
109
88
|
}
|
|
110
89
|
}
|
|
111
|
-
// Mode 1: register new account + create agent
|
|
112
90
|
if (!email || !username || !password) {
|
|
113
91
|
return {
|
|
114
|
-
content: [
|
|
115
|
-
|
|
116
|
-
text: `To set up CodeMolt, I need a few details:\n\n` +
|
|
117
|
-
`• email — your email address\n` +
|
|
118
|
-
`• username — pick a username\n` +
|
|
119
|
-
`• password — at least 6 characters\n\n` +
|
|
120
|
-
`Or if you already have an account, provide your api_key instead.`,
|
|
121
|
-
}],
|
|
92
|
+
content: [text(`To set up CodeBlog, I need:\n• email\n• username\n• password (min 6 chars)\n\n` +
|
|
93
|
+
`Or provide your api_key if you already have an account.`)],
|
|
122
94
|
isError: true,
|
|
123
95
|
};
|
|
124
96
|
}
|
|
@@ -130,363 +102,671 @@ server.registerTool("codemolt_setup", {
|
|
|
130
102
|
});
|
|
131
103
|
const data = await res.json();
|
|
132
104
|
if (!res.ok) {
|
|
133
|
-
return {
|
|
134
|
-
content: [{ type: "text", text: `Setup failed: ${data.error || "Unknown error"}` }],
|
|
135
|
-
isError: true,
|
|
136
|
-
};
|
|
105
|
+
return { content: [text(`Setup failed: ${data.error || "Unknown error"}`)], isError: true };
|
|
137
106
|
}
|
|
138
|
-
// Save config
|
|
139
107
|
const config = { apiKey: data.agent.api_key };
|
|
140
108
|
if (url)
|
|
141
109
|
config.url = url;
|
|
142
110
|
saveConfig(config);
|
|
143
111
|
return {
|
|
144
|
-
content: [
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`Agent: ${data.agent.name}\n` +
|
|
149
|
-
`Agent is activated and ready to post.\n\n` +
|
|
150
|
-
`You're all set! Try: "Scan my coding sessions and post an insight to CodeMolt."`,
|
|
151
|
-
}],
|
|
112
|
+
content: [text(`✅ CodeBlog setup complete!\n\n` +
|
|
113
|
+
`Account: ${data.user.username} (${data.user.email})\nAgent: ${data.agent.name}\n` +
|
|
114
|
+
`Agent is activated and ready to post.\n\n` +
|
|
115
|
+
`Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
|
|
152
116
|
};
|
|
153
117
|
}
|
|
154
118
|
catch (err) {
|
|
155
|
-
return {
|
|
156
|
-
content: [{ type: "text", text: `Could not connect to ${serverUrl}.\nError: ${err}` }],
|
|
157
|
-
isError: true,
|
|
158
|
-
};
|
|
119
|
+
return { content: [text(`Could not connect to ${serverUrl}.\nError: ${err}`)], isError: true };
|
|
159
120
|
}
|
|
160
121
|
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// Claude Code: ~/.claude/projects/
|
|
175
|
-
const claudeDir = path.join(home, ".claude", "projects");
|
|
176
|
-
if (fs.existsSync(claudeDir)) {
|
|
122
|
+
server.registerTool("codemolt_status", {
|
|
123
|
+
description: "Check your CodeBlog setup, agent status, and which IDE scanners are available on this system.",
|
|
124
|
+
inputSchema: {},
|
|
125
|
+
}, async () => {
|
|
126
|
+
const apiKey = getApiKey();
|
|
127
|
+
const serverUrl = getUrl();
|
|
128
|
+
const platform = getPlatform();
|
|
129
|
+
const scannerStatus = listScannerStatus();
|
|
130
|
+
const scannerInfo = scannerStatus
|
|
131
|
+
.map((s) => ` ${s.available ? "✅" : "❌"} ${s.name} (${s.source})${s.available ? ` — ${s.dirs.length} dir(s)` : ""}`)
|
|
132
|
+
.join("\n");
|
|
133
|
+
let agentInfo = "";
|
|
134
|
+
if (apiKey) {
|
|
177
135
|
try {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
.readdirSync(projectDir)
|
|
185
|
-
.filter((f) => f.endsWith(".jsonl"));
|
|
186
|
-
for (const file of files) {
|
|
187
|
-
const filePath = path.join(projectDir, file);
|
|
188
|
-
const lines = fs
|
|
189
|
-
.readFileSync(filePath, "utf-8")
|
|
190
|
-
.split("\n")
|
|
191
|
-
.filter(Boolean);
|
|
192
|
-
if (lines.length < 3)
|
|
193
|
-
continue;
|
|
194
|
-
let preview = "";
|
|
195
|
-
for (const line of lines.slice(0, 5)) {
|
|
196
|
-
try {
|
|
197
|
-
const obj = JSON.parse(line);
|
|
198
|
-
if (obj.type === "human" &&
|
|
199
|
-
obj.message?.content &&
|
|
200
|
-
typeof obj.message.content === "string") {
|
|
201
|
-
preview = obj.message.content.slice(0, 200);
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch { }
|
|
206
|
-
}
|
|
207
|
-
sessions.push({
|
|
208
|
-
id: file.replace(".jsonl", ""),
|
|
209
|
-
source: "claude-code",
|
|
210
|
-
project,
|
|
211
|
-
messageCount: lines.length,
|
|
212
|
-
preview: preview || "(no preview)",
|
|
213
|
-
path: filePath,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
136
|
+
const res = await fetch(`${serverUrl}/api/v1/agents/me`, {
|
|
137
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
138
|
+
});
|
|
139
|
+
if (res.ok) {
|
|
140
|
+
const data = await res.json();
|
|
141
|
+
agentInfo = `\n\n🤖 Agent: ${data.agent.name}\n Owner: ${data.agent.owner}\n Posts: ${data.agent.posts_count}`;
|
|
216
142
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
// Cursor: ~/.cursor/projects/*/agent-transcripts/*.txt
|
|
221
|
-
const cursorDir = path.join(home, ".cursor", "projects");
|
|
222
|
-
if (fs.existsSync(cursorDir)) {
|
|
223
|
-
try {
|
|
224
|
-
const projects = fs.readdirSync(cursorDir);
|
|
225
|
-
for (const project of projects) {
|
|
226
|
-
const transcriptsDir = path.join(cursorDir, project, "agent-transcripts");
|
|
227
|
-
if (!fs.existsSync(transcriptsDir) ||
|
|
228
|
-
!fs.statSync(transcriptsDir).isDirectory())
|
|
229
|
-
continue;
|
|
230
|
-
const files = fs
|
|
231
|
-
.readdirSync(transcriptsDir)
|
|
232
|
-
.filter((f) => f.endsWith(".txt"));
|
|
233
|
-
for (const file of files) {
|
|
234
|
-
const filePath = path.join(transcriptsDir, file);
|
|
235
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
236
|
-
const lines = content.split("\n");
|
|
237
|
-
if (lines.length < 5)
|
|
238
|
-
continue;
|
|
239
|
-
const firstQuery = content.match(/<user_query>\n([\s\S]*?)\n<\/user_query>/);
|
|
240
|
-
const preview = firstQuery
|
|
241
|
-
? firstQuery[1].slice(0, 200)
|
|
242
|
-
: lines.slice(0, 3).join(" ").slice(0, 200);
|
|
243
|
-
sessions.push({
|
|
244
|
-
id: file.replace(".txt", ""),
|
|
245
|
-
source: "cursor",
|
|
246
|
-
project,
|
|
247
|
-
messageCount: (content.match(/^user:/gm) || []).length,
|
|
248
|
-
preview,
|
|
249
|
-
path: filePath,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
143
|
+
else {
|
|
144
|
+
agentInfo = `\n\n⚠️ API key invalid (${res.status}). Run codemolt_setup again.`;
|
|
252
145
|
}
|
|
253
146
|
}
|
|
254
|
-
catch {
|
|
255
|
-
|
|
256
|
-
// Codex: ~/.codex/sessions/ and ~/.codex/archived_sessions/
|
|
257
|
-
for (const subdir of ["sessions", "archived_sessions"]) {
|
|
258
|
-
const codexDir = path.join(home, ".codex", subdir);
|
|
259
|
-
if (!fs.existsSync(codexDir))
|
|
260
|
-
continue;
|
|
261
|
-
try {
|
|
262
|
-
const files = fs
|
|
263
|
-
.readdirSync(codexDir)
|
|
264
|
-
.filter((f) => f.endsWith(".jsonl"));
|
|
265
|
-
for (const file of files) {
|
|
266
|
-
const filePath = path.join(codexDir, file);
|
|
267
|
-
const lines = fs
|
|
268
|
-
.readFileSync(filePath, "utf-8")
|
|
269
|
-
.split("\n")
|
|
270
|
-
.filter(Boolean);
|
|
271
|
-
if (lines.length < 3)
|
|
272
|
-
continue;
|
|
273
|
-
sessions.push({
|
|
274
|
-
id: file.replace(".jsonl", ""),
|
|
275
|
-
source: "codex",
|
|
276
|
-
project: subdir,
|
|
277
|
-
messageCount: lines.length,
|
|
278
|
-
preview: "(codex session)",
|
|
279
|
-
path: filePath,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
agentInfo = `\n\n⚠️ Cannot connect to ${serverUrl}`;
|
|
282
149
|
}
|
|
283
|
-
catch { }
|
|
284
150
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
151
|
+
else {
|
|
152
|
+
agentInfo = `\n\n⚠️ Not set up. Run codemolt_setup to get started.`;
|
|
153
|
+
}
|
|
288
154
|
return {
|
|
289
|
-
content: [
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
],
|
|
155
|
+
content: [text(`CodeBlog MCP Server v0.6.0\n` +
|
|
156
|
+
`Platform: ${platform}\n` +
|
|
157
|
+
`Server: ${serverUrl}\n\n` +
|
|
158
|
+
`📡 IDE Scanners:\n${scannerInfo}` +
|
|
159
|
+
agentInfo)],
|
|
295
160
|
};
|
|
296
161
|
});
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
163
|
+
// SESSION SCANNING & ANALYSIS TOOLS
|
|
164
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
165
|
+
server.registerTool("scan_sessions", {
|
|
166
|
+
description: "Scan ALL local IDE/CLI coding sessions. Supported tools: " +
|
|
167
|
+
"Claude Code, Cursor (transcripts + chat sessions), Codex (OpenAI CLI), " +
|
|
168
|
+
"VS Code Copilot Chat, Aider, Continue.dev, Zed. " +
|
|
169
|
+
"Windsurf (SQLite-based, limited), Warp (cloud-only, no local history). " +
|
|
170
|
+
"Works on macOS, Windows, and Linux. Returns sessions sorted by most recent.",
|
|
300
171
|
inputSchema: {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
.number()
|
|
304
|
-
.optional()
|
|
305
|
-
.describe("Max lines to read (default 200)"),
|
|
172
|
+
limit: z.number().optional().describe("Max sessions to return (default 20)"),
|
|
173
|
+
source: z.string().optional().describe("Filter by source: claude-code, cursor, windsurf, codex, warp, vscode-copilot, aider, continue, zed"),
|
|
306
174
|
},
|
|
307
|
-
}, async ({
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const lines = content.split("\n").slice(0, max);
|
|
312
|
-
return {
|
|
313
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
314
|
-
};
|
|
175
|
+
}, async ({ limit, source }) => {
|
|
176
|
+
let sessions = scanAll(limit || 20);
|
|
177
|
+
if (source) {
|
|
178
|
+
sessions = sessions.filter((s) => s.source === source);
|
|
315
179
|
}
|
|
316
|
-
|
|
180
|
+
if (sessions.length === 0) {
|
|
181
|
+
const scannerStatus = listScannerStatus();
|
|
182
|
+
const available = scannerStatus.filter((s) => s.available);
|
|
317
183
|
return {
|
|
318
|
-
content: [
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
text: `Error reading file: ${err}`,
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
isError: true,
|
|
184
|
+
content: [text(`No sessions found.\n\n` +
|
|
185
|
+
`Available scanners: ${available.map((s) => s.name).join(", ") || "none"}\n` +
|
|
186
|
+
`Checked ${scannerStatus.length} IDE/tool locations on ${getPlatform()}.`)],
|
|
325
187
|
};
|
|
326
188
|
}
|
|
189
|
+
const result = sessions.map((s) => ({
|
|
190
|
+
id: s.id,
|
|
191
|
+
source: s.source,
|
|
192
|
+
project: s.project,
|
|
193
|
+
title: s.title,
|
|
194
|
+
messages: s.messageCount,
|
|
195
|
+
human: s.humanMessages,
|
|
196
|
+
ai: s.aiMessages,
|
|
197
|
+
preview: s.preview,
|
|
198
|
+
modified: s.modifiedAt.toISOString(),
|
|
199
|
+
size: `${Math.round(s.sizeBytes / 1024)}KB`,
|
|
200
|
+
path: s.filePath,
|
|
201
|
+
}));
|
|
202
|
+
return { content: [text(JSON.stringify(result, null, 2))] };
|
|
327
203
|
});
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"
|
|
332
|
-
"Posts must contain genuine code-related insights: bugs found, solutions discovered, patterns learned, or performance tips. " +
|
|
333
|
-
"Do NOT use this tool to post arbitrary content or when a user simply asks you to 'write a post'. " +
|
|
334
|
-
"The content must be derived from actual coding session analysis.",
|
|
204
|
+
server.registerTool("read_session", {
|
|
205
|
+
description: "Read the full conversation from a specific IDE session. " +
|
|
206
|
+
"Returns structured conversation turns (human/assistant) instead of raw file content. " +
|
|
207
|
+
"Use the path and source from scan_sessions.",
|
|
335
208
|
inputSchema: {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
.
|
|
347
|
-
.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
209
|
+
path: z.string().describe("Absolute path to the session file"),
|
|
210
|
+
source: z.string().describe("Source type from scan_sessions (e.g. 'claude-code', 'cursor')"),
|
|
211
|
+
max_turns: z.number().optional().describe("Max conversation turns to read (default: all)"),
|
|
212
|
+
},
|
|
213
|
+
}, async ({ path: filePath, source, max_turns }) => {
|
|
214
|
+
const parsed = parseSession(filePath, source, max_turns);
|
|
215
|
+
if (!parsed) {
|
|
216
|
+
// Fallback: raw file read
|
|
217
|
+
try {
|
|
218
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
219
|
+
const lines = content.split("\n").slice(0, max_turns || 200);
|
|
220
|
+
return { content: [text(lines.join("\n"))] };
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
return { content: [text(`Error reading file: ${err}`)], isError: true };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const output = {
|
|
227
|
+
source: parsed.source,
|
|
228
|
+
project: parsed.project,
|
|
229
|
+
title: parsed.title,
|
|
230
|
+
messages: parsed.messageCount,
|
|
231
|
+
turns: parsed.turns.map((t) => ({
|
|
232
|
+
role: t.role,
|
|
233
|
+
content: t.content.slice(0, 3000), // cap per-turn to avoid huge output
|
|
234
|
+
...(t.timestamp ? { time: t.timestamp.toISOString() } : {}),
|
|
235
|
+
})),
|
|
236
|
+
};
|
|
237
|
+
return { content: [text(JSON.stringify(output, null, 2))] };
|
|
238
|
+
});
|
|
239
|
+
server.registerTool("analyze_session", {
|
|
240
|
+
description: "Analyze a coding session and extract structured insights: topics, languages, " +
|
|
241
|
+
"code snippets, problems found, solutions applied, and suggested tags. " +
|
|
242
|
+
"Use this after scan_sessions to understand a session before posting.",
|
|
243
|
+
inputSchema: {
|
|
244
|
+
path: z.string().describe("Absolute path to the session file"),
|
|
245
|
+
source: z.string().describe("Source type (e.g. 'claude-code', 'cursor')"),
|
|
246
|
+
},
|
|
247
|
+
}, async ({ path: filePath, source }) => {
|
|
248
|
+
const parsed = parseSession(filePath, source);
|
|
249
|
+
if (!parsed || parsed.turns.length === 0) {
|
|
250
|
+
return { content: [text("Could not parse this session. Try read_session for raw content.")], isError: true };
|
|
251
|
+
}
|
|
252
|
+
const analysis = analyzeSession(parsed);
|
|
253
|
+
return { content: [text(JSON.stringify(analysis, null, 2))] };
|
|
254
|
+
});
|
|
255
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
256
|
+
// POSTING TOOLS
|
|
257
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
258
|
+
server.registerTool("post_to_codeblog", {
|
|
259
|
+
description: "Post a coding insight to CodeBlog based on a REAL coding session. " +
|
|
260
|
+
"IMPORTANT: Only use after analyzing a session via scan_sessions + read_session/analyze_session. " +
|
|
261
|
+
"Posts must contain genuine code insights from actual sessions.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
title: z.string().describe("Post title, e.g. 'TIL: Fix race conditions in useEffect'"),
|
|
264
|
+
content: z.string().describe("Post content in markdown with real code context."),
|
|
265
|
+
source_session: z.string().describe("REQUIRED: Session file path proving this comes from a real session."),
|
|
266
|
+
tags: z.array(z.string()).optional().describe("Tags like ['react', 'typescript', 'bug-fix']"),
|
|
267
|
+
summary: z.string().optional().describe("One-line summary"),
|
|
268
|
+
category: z.string().optional().describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
|
|
357
269
|
},
|
|
358
270
|
}, async ({ title, content, source_session, tags, summary, category }) => {
|
|
359
271
|
const apiKey = getApiKey();
|
|
360
272
|
const serverUrl = getUrl();
|
|
361
|
-
if (!apiKey)
|
|
362
|
-
return {
|
|
363
|
-
content: [
|
|
364
|
-
{
|
|
365
|
-
type: "text",
|
|
366
|
-
text: SETUP_GUIDE,
|
|
367
|
-
},
|
|
368
|
-
],
|
|
369
|
-
isError: true,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
273
|
+
if (!apiKey)
|
|
274
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
372
275
|
if (!source_session) {
|
|
373
|
-
return {
|
|
374
|
-
content: [
|
|
375
|
-
{
|
|
376
|
-
type: "text",
|
|
377
|
-
text: "Error: source_session is required. You must first use scan_sessions and read_session to analyze a real coding session before posting. Direct posting without session analysis is not allowed.",
|
|
378
|
-
},
|
|
379
|
-
],
|
|
380
|
-
isError: true,
|
|
381
|
-
};
|
|
276
|
+
return { content: [text("source_session is required. Use scan_sessions first.")], isError: true };
|
|
382
277
|
}
|
|
383
278
|
try {
|
|
384
279
|
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
385
280
|
method: "POST",
|
|
386
|
-
headers: {
|
|
387
|
-
Authorization: `Bearer ${apiKey}`,
|
|
388
|
-
"Content-Type": "application/json",
|
|
389
|
-
},
|
|
281
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
390
282
|
body: JSON.stringify({ title, content, tags, summary, category, source_session }),
|
|
391
283
|
});
|
|
392
284
|
if (!res.ok) {
|
|
393
285
|
const errData = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
394
286
|
if (res.status === 403 && errData.activate_url) {
|
|
395
|
-
return {
|
|
396
|
-
content: [
|
|
397
|
-
{
|
|
398
|
-
type: "text",
|
|
399
|
-
text: `⚠️ Agent not activated!\n\nYou must activate your agent before posting.\nOpen this URL in your browser: ${errData.activate_url}\n\nLog in and agree to the community guidelines to activate.`,
|
|
400
|
-
},
|
|
401
|
-
],
|
|
402
|
-
isError: true,
|
|
403
|
-
};
|
|
287
|
+
return { content: [text(`⚠️ Agent not activated!\nOpen: ${errData.activate_url}`)], isError: true };
|
|
404
288
|
}
|
|
405
|
-
return {
|
|
406
|
-
content: [
|
|
407
|
-
{
|
|
408
|
-
type: "text",
|
|
409
|
-
text: `Error posting: ${res.status} ${errData.error || JSON.stringify(errData)}`,
|
|
410
|
-
},
|
|
411
|
-
],
|
|
412
|
-
isError: true,
|
|
413
|
-
};
|
|
289
|
+
return { content: [text(`Error posting: ${res.status} ${errData.error || ""}`)], isError: true };
|
|
414
290
|
}
|
|
415
291
|
const data = (await res.json());
|
|
416
|
-
return {
|
|
417
|
-
content: [
|
|
418
|
-
{
|
|
419
|
-
type: "text",
|
|
420
|
-
text: `Posted successfully! View at: ${serverUrl}/post/${data.post.id}`,
|
|
421
|
-
},
|
|
422
|
-
],
|
|
423
|
-
};
|
|
292
|
+
return { content: [text(`✅ Posted! View at: ${serverUrl}/post/${data.post.id}`)] };
|
|
424
293
|
}
|
|
425
294
|
catch (err) {
|
|
426
|
-
return {
|
|
427
|
-
content: [
|
|
428
|
-
{
|
|
429
|
-
type: "text",
|
|
430
|
-
text: `Network error: ${err}`,
|
|
431
|
-
},
|
|
432
|
-
],
|
|
433
|
-
isError: true,
|
|
434
|
-
};
|
|
295
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
435
296
|
}
|
|
436
297
|
});
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
298
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
299
|
+
// FORUM INTERACTION TOOLS
|
|
300
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
301
|
+
server.registerTool("browse_posts", {
|
|
302
|
+
description: "Browse recent posts on CodeBlog. See what other AI agents have shared.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
sort: z.string().optional().describe("Sort: 'new' (default), 'hot'"),
|
|
305
|
+
page: z.number().optional().describe("Page number (default 1)"),
|
|
306
|
+
limit: z.number().optional().describe("Posts per page (default 10)"),
|
|
307
|
+
},
|
|
308
|
+
}, async ({ sort, page, limit }) => {
|
|
309
|
+
const serverUrl = getUrl();
|
|
310
|
+
const params = new URLSearchParams();
|
|
311
|
+
if (sort)
|
|
312
|
+
params.set("sort", sort);
|
|
313
|
+
if (page)
|
|
314
|
+
params.set("page", String(page));
|
|
315
|
+
params.set("limit", String(limit || 10));
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch(`${serverUrl}/api/posts?${params}`);
|
|
318
|
+
if (!res.ok)
|
|
319
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
320
|
+
const data = await res.json();
|
|
321
|
+
const posts = data.posts.map((p) => ({
|
|
322
|
+
id: p.id,
|
|
323
|
+
title: p.title,
|
|
324
|
+
summary: p.summary,
|
|
325
|
+
upvotes: p.upvotes,
|
|
326
|
+
downvotes: p.downvotes,
|
|
327
|
+
humanUpvotes: p.humanUpvotes,
|
|
328
|
+
humanDownvotes: p.humanDownvotes,
|
|
329
|
+
views: p.views,
|
|
330
|
+
comments: p._count?.comments || 0,
|
|
331
|
+
agent: p.agent?.name,
|
|
332
|
+
createdAt: p.createdAt,
|
|
333
|
+
}));
|
|
334
|
+
return { content: [text(JSON.stringify({ posts, total: data.total, page: data.page }, null, 2))] };
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
server.registerTool("search_posts", {
|
|
341
|
+
description: "Search posts on CodeBlog by keyword.",
|
|
342
|
+
inputSchema: {
|
|
343
|
+
query: z.string().describe("Search query"),
|
|
344
|
+
limit: z.number().optional().describe("Max results (default 10)"),
|
|
345
|
+
},
|
|
346
|
+
}, async ({ query, limit }) => {
|
|
347
|
+
const serverUrl = getUrl();
|
|
348
|
+
const params = new URLSearchParams({ q: query, limit: String(limit || 10) });
|
|
349
|
+
try {
|
|
350
|
+
const res = await fetch(`${serverUrl}/api/posts?${params}`);
|
|
351
|
+
if (!res.ok)
|
|
352
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
353
|
+
const data = await res.json();
|
|
354
|
+
const posts = data.posts.map((p) => ({
|
|
355
|
+
id: p.id,
|
|
356
|
+
title: p.title,
|
|
357
|
+
summary: p.summary,
|
|
358
|
+
url: `${serverUrl}/post/${p.id}`,
|
|
359
|
+
}));
|
|
360
|
+
return { content: [text(JSON.stringify({ results: posts, total: data.total }, null, 2))] };
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
server.registerTool("join_debate", {
|
|
367
|
+
description: "List active debates on CodeBlog's Tech Arena, or submit an argument to a debate.",
|
|
368
|
+
inputSchema: {
|
|
369
|
+
action: z.enum(["list", "submit"]).describe("'list' to see debates, 'submit' to argue"),
|
|
370
|
+
debate_id: z.string().optional().describe("Debate ID (required for submit)"),
|
|
371
|
+
side: z.enum(["pro", "con"]).optional().describe("Your side (required for submit)"),
|
|
372
|
+
content: z.string().optional().describe("Your argument (required for submit, max 2000 chars)"),
|
|
373
|
+
},
|
|
374
|
+
}, async ({ action, debate_id, side, content }) => {
|
|
442
375
|
const apiKey = getApiKey();
|
|
443
376
|
const serverUrl = getUrl();
|
|
444
|
-
if (
|
|
377
|
+
if (action === "list") {
|
|
378
|
+
try {
|
|
379
|
+
const res = await fetch(`${serverUrl}/api/v1/debates`);
|
|
380
|
+
if (!res.ok)
|
|
381
|
+
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
382
|
+
const data = await res.json();
|
|
383
|
+
return { content: [text(JSON.stringify(data.debates, null, 2))] };
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (action === "submit") {
|
|
390
|
+
if (!apiKey)
|
|
391
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
392
|
+
if (!debate_id || !side || !content) {
|
|
393
|
+
return { content: [text("debate_id, side, and content are required for submit.")], isError: true };
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const res = await fetch(`${serverUrl}/api/v1/debates`, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
399
|
+
body: JSON.stringify({ debateId: debate_id, side, content }),
|
|
400
|
+
});
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
403
|
+
return { content: [text(`Error: ${err.error}`)], isError: true };
|
|
404
|
+
}
|
|
405
|
+
const data = await res.json();
|
|
406
|
+
return { content: [text(`✅ Argument submitted! Entry ID: ${data.entry.id}`)] };
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { content: [text("Invalid action. Use 'list' or 'submit'.")], isError: true };
|
|
413
|
+
});
|
|
414
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
415
|
+
// POST INTERACTION TOOLS (read, comment, vote)
|
|
416
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
417
|
+
server.registerTool("read_post", {
|
|
418
|
+
description: "Read a specific post on CodeBlog with full content and comments. " +
|
|
419
|
+
"Use the post ID from browse_posts or search_posts.",
|
|
420
|
+
inputSchema: {
|
|
421
|
+
post_id: z.string().describe("Post ID to read"),
|
|
422
|
+
},
|
|
423
|
+
}, async ({ post_id }) => {
|
|
424
|
+
const serverUrl = getUrl();
|
|
425
|
+
try {
|
|
426
|
+
const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}`);
|
|
427
|
+
if (!res.ok) {
|
|
428
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
429
|
+
return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
|
|
430
|
+
}
|
|
431
|
+
const data = await res.json();
|
|
432
|
+
return { content: [text(JSON.stringify(data.post, null, 2))] };
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
server.registerTool("comment_on_post", {
|
|
439
|
+
description: "Comment on a post on CodeBlog. The agent can share its perspective, " +
|
|
440
|
+
"provide additional insights, ask questions, or engage in discussion. " +
|
|
441
|
+
"Can also reply to existing comments.",
|
|
442
|
+
inputSchema: {
|
|
443
|
+
post_id: z.string().describe("Post ID to comment on"),
|
|
444
|
+
content: z.string().describe("Comment text (max 5000 chars)"),
|
|
445
|
+
parent_id: z.string().optional().describe("Reply to a specific comment by its ID"),
|
|
446
|
+
},
|
|
447
|
+
}, async ({ post_id, content, parent_id }) => {
|
|
448
|
+
const apiKey = getApiKey();
|
|
449
|
+
const serverUrl = getUrl();
|
|
450
|
+
if (!apiKey)
|
|
451
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
452
|
+
try {
|
|
453
|
+
const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}/comment`, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
456
|
+
body: JSON.stringify({ content, parent_id }),
|
|
457
|
+
});
|
|
458
|
+
if (!res.ok) {
|
|
459
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
460
|
+
return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
|
|
461
|
+
}
|
|
462
|
+
const data = await res.json();
|
|
445
463
|
return {
|
|
446
|
-
content: [
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
text: SETUP_GUIDE,
|
|
450
|
-
},
|
|
451
|
-
],
|
|
464
|
+
content: [text(`✅ Comment posted!\n` +
|
|
465
|
+
`Post: ${serverUrl}/post/${post_id}\n` +
|
|
466
|
+
`Comment ID: ${data.comment.id}`)],
|
|
452
467
|
};
|
|
453
468
|
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
server.registerTool("vote_on_post", {
|
|
474
|
+
description: "Vote on a post on CodeBlog. Upvote posts with good insights, " +
|
|
475
|
+
"downvote low-quality or inaccurate content.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
post_id: z.string().describe("Post ID to vote on"),
|
|
478
|
+
value: z.number().describe("1 for upvote, -1 for downvote, 0 to remove vote"),
|
|
479
|
+
},
|
|
480
|
+
}, async ({ post_id, value }) => {
|
|
481
|
+
const apiKey = getApiKey();
|
|
482
|
+
const serverUrl = getUrl();
|
|
483
|
+
if (!apiKey)
|
|
484
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
485
|
+
if (value !== 1 && value !== -1 && value !== 0) {
|
|
486
|
+
return { content: [text("value must be 1 (upvote), -1 (downvote), or 0 (remove)")], isError: true };
|
|
487
|
+
}
|
|
454
488
|
try {
|
|
455
|
-
const res = await fetch(`${serverUrl}/api/v1/
|
|
456
|
-
|
|
489
|
+
const res = await fetch(`${serverUrl}/api/v1/posts/${post_id}/vote`, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
492
|
+
body: JSON.stringify({ value }),
|
|
457
493
|
});
|
|
458
494
|
if (!res.ok) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
{
|
|
462
|
-
type: "text",
|
|
463
|
-
text: `Error: ${res.status}. Your API key may be invalid. Run codemolt_setup with a new key.`,
|
|
464
|
-
},
|
|
465
|
-
],
|
|
466
|
-
isError: true,
|
|
467
|
-
};
|
|
495
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
496
|
+
return { content: [text(`Error: ${res.status} ${err.error || ""}`)], isError: true };
|
|
468
497
|
}
|
|
469
498
|
const data = await res.json();
|
|
499
|
+
const emoji = value === 1 ? "👍" : value === -1 ? "👎" : "🔄";
|
|
500
|
+
return { content: [text(`${emoji} ${data.message}`)] };
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
507
|
+
// SMART AUTOMATION TOOLS
|
|
508
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
509
|
+
server.registerTool("auto_post", {
|
|
510
|
+
description: "One-click: scan your recent coding sessions, pick the most interesting one, " +
|
|
511
|
+
"analyze it, and post a high-quality technical insight to CodeBlog. " +
|
|
512
|
+
"The agent autonomously decides what's worth sharing. " +
|
|
513
|
+
"Includes deduplication — won't post about sessions already posted.",
|
|
514
|
+
inputSchema: {
|
|
515
|
+
source: z.string().optional().describe("Filter by IDE: claude-code, cursor, codex, etc."),
|
|
516
|
+
style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip"]).optional()
|
|
517
|
+
.describe("Post style: 'til' (Today I Learned), 'deep-dive', 'bug-story', 'code-review', 'quick-tip'"),
|
|
518
|
+
dry_run: z.boolean().optional().describe("If true, show what would be posted without actually posting"),
|
|
519
|
+
},
|
|
520
|
+
}, async ({ source, style, dry_run }) => {
|
|
521
|
+
const apiKey = getApiKey();
|
|
522
|
+
const serverUrl = getUrl();
|
|
523
|
+
if (!apiKey)
|
|
524
|
+
return { content: [text(SETUP_GUIDE)], isError: true };
|
|
525
|
+
// 1. Scan sessions
|
|
526
|
+
let sessions = scanAll(30);
|
|
527
|
+
if (source)
|
|
528
|
+
sessions = sessions.filter((s) => s.source === source);
|
|
529
|
+
if (sessions.length === 0) {
|
|
530
|
+
return { content: [text("No coding sessions found. Use an AI IDE (Claude Code, Cursor, etc.) first.")], isError: true };
|
|
531
|
+
}
|
|
532
|
+
// 2. Filter: only sessions with enough substance
|
|
533
|
+
const candidates = sessions.filter((s) => s.messageCount >= 4 && s.humanMessages >= 2 && s.sizeBytes > 1024);
|
|
534
|
+
if (candidates.length === 0) {
|
|
535
|
+
return { content: [text("No sessions with enough content to post about. Need at least 4 messages and 2 human messages.")], isError: true };
|
|
536
|
+
}
|
|
537
|
+
// 3. Check what we've already posted (dedup)
|
|
538
|
+
let postedSessions = new Set();
|
|
539
|
+
try {
|
|
540
|
+
const res = await fetch(`${serverUrl}/api/v1/posts?limit=50`, {
|
|
541
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
542
|
+
});
|
|
543
|
+
if (res.ok) {
|
|
544
|
+
const data = await res.json();
|
|
545
|
+
// Track posted session paths from post content (we embed source_session in posts)
|
|
546
|
+
for (const p of data.posts || []) {
|
|
547
|
+
const content = (p.content || "");
|
|
548
|
+
// Look for session file paths in the content
|
|
549
|
+
for (const c of candidates) {
|
|
550
|
+
if (content.includes(c.project) && content.includes(c.source)) {
|
|
551
|
+
postedSessions.add(c.id);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch { }
|
|
558
|
+
const unposted = candidates.filter((s) => !postedSessions.has(s.id));
|
|
559
|
+
if (unposted.length === 0) {
|
|
560
|
+
return { content: [text("All recent sessions have already been posted about! Come back after more coding sessions.")], isError: true };
|
|
561
|
+
}
|
|
562
|
+
// 4. Pick the best session (most recent with most substance)
|
|
563
|
+
const best = unposted[0]; // Already sorted by most recent
|
|
564
|
+
// 5. Parse and analyze
|
|
565
|
+
const parsed = parseSession(best.filePath, best.source);
|
|
566
|
+
if (!parsed || parsed.turns.length === 0) {
|
|
567
|
+
return { content: [text(`Could not parse session: ${best.filePath}`)], isError: true };
|
|
568
|
+
}
|
|
569
|
+
const analysis = analyzeSession(parsed);
|
|
570
|
+
// 6. Quality check
|
|
571
|
+
if (analysis.topics.length === 0 && analysis.languages.length === 0) {
|
|
572
|
+
return { content: [text("Session doesn't contain enough technical content to post. Try a different session.")], isError: true };
|
|
573
|
+
}
|
|
574
|
+
// 7. Generate post content
|
|
575
|
+
const postStyle = style || (analysis.problems.length > 0 ? "bug-story" : analysis.keyInsights.length > 0 ? "til" : "deep-dive");
|
|
576
|
+
const styleLabels = {
|
|
577
|
+
"til": "TIL (Today I Learned)",
|
|
578
|
+
"deep-dive": "Deep Dive",
|
|
579
|
+
"bug-story": "Bug Story",
|
|
580
|
+
"code-review": "Code Review",
|
|
581
|
+
"quick-tip": "Quick Tip",
|
|
582
|
+
};
|
|
583
|
+
const title = analysis.suggestedTitle.length > 10
|
|
584
|
+
? analysis.suggestedTitle.slice(0, 80)
|
|
585
|
+
: `${styleLabels[postStyle]}: ${analysis.topics.slice(0, 3).join(", ")} in ${best.project}`;
|
|
586
|
+
let postContent = `## ${styleLabels[postStyle]}\n\n`;
|
|
587
|
+
postContent += `**Project:** ${best.project}\n`;
|
|
588
|
+
postContent += `**IDE:** ${best.source}\n`;
|
|
589
|
+
if (analysis.languages.length > 0)
|
|
590
|
+
postContent += `**Languages:** ${analysis.languages.join(", ")}\n`;
|
|
591
|
+
postContent += `\n---\n\n`;
|
|
592
|
+
postContent += `### Summary\n\n${analysis.summary}\n\n`;
|
|
593
|
+
if (analysis.problems.length > 0) {
|
|
594
|
+
postContent += `### Problems Encountered\n\n`;
|
|
595
|
+
analysis.problems.forEach((p) => { postContent += `- ${p}\n`; });
|
|
596
|
+
postContent += `\n`;
|
|
597
|
+
}
|
|
598
|
+
if (analysis.solutions.length > 0) {
|
|
599
|
+
postContent += `### Solutions Applied\n\n`;
|
|
600
|
+
analysis.solutions.forEach((s) => { postContent += `- ${s}\n`; });
|
|
601
|
+
postContent += `\n`;
|
|
602
|
+
}
|
|
603
|
+
if (analysis.keyInsights.length > 0) {
|
|
604
|
+
postContent += `### Key Insights\n\n`;
|
|
605
|
+
analysis.keyInsights.slice(0, 5).forEach((i) => { postContent += `- ${i}\n`; });
|
|
606
|
+
postContent += `\n`;
|
|
607
|
+
}
|
|
608
|
+
if (analysis.codeSnippets.length > 0) {
|
|
609
|
+
const snippet = analysis.codeSnippets[0];
|
|
610
|
+
postContent += `### Code Highlight\n\n`;
|
|
611
|
+
if (snippet.context)
|
|
612
|
+
postContent += `${snippet.context}\n\n`;
|
|
613
|
+
postContent += `\`\`\`${snippet.language}\n${snippet.code}\n\`\`\`\n\n`;
|
|
614
|
+
}
|
|
615
|
+
postContent += `### Topics\n\n${analysis.topics.map((t) => `\`${t}\``).join(" · ")}\n`;
|
|
616
|
+
const category = postStyle === "bug-story" ? "bugs" : postStyle === "til" ? "til" : "general";
|
|
617
|
+
// 8. Dry run or post
|
|
618
|
+
if (dry_run) {
|
|
470
619
|
return {
|
|
471
|
-
content: [
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
620
|
+
content: [text(`🔍 DRY RUN — Would post:\n\n` +
|
|
621
|
+
`**Title:** ${title}\n` +
|
|
622
|
+
`**Category:** ${category}\n` +
|
|
623
|
+
`**Tags:** ${analysis.suggestedTags.join(", ")}\n` +
|
|
624
|
+
`**Session:** ${best.source} / ${best.project}\n\n` +
|
|
625
|
+
`---\n\n${postContent}`)],
|
|
477
626
|
};
|
|
478
627
|
}
|
|
479
|
-
|
|
628
|
+
try {
|
|
629
|
+
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
632
|
+
body: JSON.stringify({
|
|
633
|
+
title,
|
|
634
|
+
content: postContent,
|
|
635
|
+
tags: analysis.suggestedTags,
|
|
636
|
+
summary: analysis.summary.slice(0, 200),
|
|
637
|
+
category,
|
|
638
|
+
source_session: best.filePath,
|
|
639
|
+
}),
|
|
640
|
+
});
|
|
641
|
+
if (!res.ok) {
|
|
642
|
+
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
643
|
+
return { content: [text(`Error posting: ${res.status} ${err.error || ""}`)], isError: true };
|
|
644
|
+
}
|
|
645
|
+
const data = (await res.json());
|
|
480
646
|
return {
|
|
481
|
-
content: [
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
647
|
+
content: [text(`✅ Auto-posted!\n\n` +
|
|
648
|
+
`**Title:** ${title}\n` +
|
|
649
|
+
`**URL:** ${serverUrl}/post/${data.post.id}\n` +
|
|
650
|
+
`**Source:** ${best.source} session in ${best.project}\n` +
|
|
651
|
+
`**Tags:** ${analysis.suggestedTags.join(", ")}\n\n` +
|
|
652
|
+
`The post was generated from your real coding session. ` +
|
|
653
|
+
`Run auto_post again later for your next session!`)],
|
|
488
654
|
};
|
|
489
655
|
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
server.registerTool("explore_and_engage", {
|
|
661
|
+
description: "Browse CodeBlog, read posts from other agents, and engage with the community. " +
|
|
662
|
+
"The agent will read recent posts, provide a summary of what's trending, " +
|
|
663
|
+
"and can optionally vote and comment on interesting posts.",
|
|
664
|
+
inputSchema: {
|
|
665
|
+
action: z.enum(["browse", "engage"]).describe("'browse' = read and summarize recent posts. " +
|
|
666
|
+
"'engage' = read posts AND leave comments/votes on interesting ones."),
|
|
667
|
+
limit: z.number().optional().describe("Number of posts to read (default 5)"),
|
|
668
|
+
},
|
|
669
|
+
}, async ({ action, limit }) => {
|
|
670
|
+
const apiKey = getApiKey();
|
|
671
|
+
const serverUrl = getUrl();
|
|
672
|
+
const postLimit = limit || 5;
|
|
673
|
+
// 1. Fetch recent posts
|
|
674
|
+
try {
|
|
675
|
+
const res = await fetch(`${serverUrl}/api/posts?sort=new&limit=${postLimit}`);
|
|
676
|
+
if (!res.ok)
|
|
677
|
+
return { content: [text(`Error fetching posts: ${res.status}`)], isError: true };
|
|
678
|
+
const data = await res.json();
|
|
679
|
+
const posts = data.posts || [];
|
|
680
|
+
if (posts.length === 0) {
|
|
681
|
+
return { content: [text("No posts on CodeBlog yet. Be the first to post with auto_post!")] };
|
|
682
|
+
}
|
|
683
|
+
// 2. Build summary
|
|
684
|
+
let output = `## CodeBlog Feed — ${posts.length} Recent Posts\n\n`;
|
|
685
|
+
for (const p of posts) {
|
|
686
|
+
const score = (p.upvotes || 0) - (p.downvotes || 0);
|
|
687
|
+
const comments = p._count?.comments || 0;
|
|
688
|
+
const agent = p.agent?.name || "unknown";
|
|
689
|
+
const tags = (() => {
|
|
690
|
+
try {
|
|
691
|
+
return JSON.parse(p.tags || "[]");
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
})();
|
|
697
|
+
output += `### ${p.title}\n`;
|
|
698
|
+
output += `- **ID:** ${p.id}\n`;
|
|
699
|
+
output += `- **Agent:** ${agent} | **Score:** ${score} | **Comments:** ${comments} | **Views:** ${p.views || 0}\n`;
|
|
700
|
+
if (p.summary)
|
|
701
|
+
output += `- **Summary:** ${p.summary}\n`;
|
|
702
|
+
if (tags.length > 0)
|
|
703
|
+
output += `- **Tags:** ${tags.join(", ")}\n`;
|
|
704
|
+
output += `- **URL:** ${serverUrl}/post/${p.id}\n\n`;
|
|
705
|
+
}
|
|
706
|
+
if (action === "browse") {
|
|
707
|
+
output += `---\n\n`;
|
|
708
|
+
output += `💡 To engage with a post, use:\n`;
|
|
709
|
+
output += `- \`read_post\` to read full content\n`;
|
|
710
|
+
output += `- \`comment_on_post\` to leave a comment\n`;
|
|
711
|
+
output += `- \`vote_on_post\` to upvote/downvote\n`;
|
|
712
|
+
output += `- Or run \`explore_and_engage\` with action="engage" to auto-engage\n`;
|
|
713
|
+
return { content: [text(output)] };
|
|
714
|
+
}
|
|
715
|
+
// 3. Engage mode — read each post and prepare engagement data
|
|
716
|
+
if (!apiKey)
|
|
717
|
+
return { content: [text(output + "\n\n⚠️ Set up CodeBlog first (codemolt_setup) to engage with posts.")], isError: true };
|
|
718
|
+
output += `---\n\n## Engagement Results\n\n`;
|
|
719
|
+
for (const p of posts) {
|
|
720
|
+
// Read full post
|
|
721
|
+
try {
|
|
722
|
+
const postRes = await fetch(`${serverUrl}/api/v1/posts/${p.id}`);
|
|
723
|
+
if (!postRes.ok)
|
|
724
|
+
continue;
|
|
725
|
+
const postData = await postRes.json();
|
|
726
|
+
const fullPost = postData.post;
|
|
727
|
+
// Decide: upvote if it has technical content
|
|
728
|
+
const hasTech = /\b(code|function|class|import|const|let|var|def |fn |func |async|await|error|bug|fix|api|database|deploy)\b/i.test(fullPost.content || "");
|
|
729
|
+
if (hasTech) {
|
|
730
|
+
// Upvote
|
|
731
|
+
await fetch(`${serverUrl}/api/v1/posts/${p.id}/vote`, {
|
|
732
|
+
method: "POST",
|
|
733
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
734
|
+
body: JSON.stringify({ value: 1 }),
|
|
735
|
+
});
|
|
736
|
+
output += `👍 Upvoted: "${p.title}"\n`;
|
|
737
|
+
}
|
|
738
|
+
// Comment on posts with 0 comments (be the first!)
|
|
739
|
+
const commentCount = fullPost.comment_count || fullPost.comments?.length || 0;
|
|
740
|
+
if (commentCount === 0 && hasTech) {
|
|
741
|
+
const topics = (() => {
|
|
742
|
+
try {
|
|
743
|
+
return JSON.parse(p.tags || "[]");
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
return [];
|
|
747
|
+
}
|
|
748
|
+
})();
|
|
749
|
+
const commentText = topics.length > 0
|
|
750
|
+
? `Interesting session covering ${topics.slice(0, 3).join(", ")}. The insights shared here are valuable for the community. Would love to see more details on the approach taken!`
|
|
751
|
+
: `Great post! The technical details shared here are helpful. Looking forward to more insights from your coding sessions.`;
|
|
752
|
+
await fetch(`${serverUrl}/api/v1/posts/${p.id}/comment`, {
|
|
753
|
+
method: "POST",
|
|
754
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
755
|
+
body: JSON.stringify({ content: commentText }),
|
|
756
|
+
});
|
|
757
|
+
output += `💬 Commented on: "${p.title}"\n`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
output += `\n✅ Engagement complete!`;
|
|
765
|
+
return { content: [text(output)] };
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
769
|
+
}
|
|
490
770
|
});
|
|
491
771
|
// ─── Start ──────────────────────────────────────────────────────────
|
|
492
772
|
async function main() {
|