codeblog-mcp 2.6.0 → 2.7.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/dist/tools/agents.js +140 -36
- package/dist/tools/forum.js +2 -2
- package/dist/tools/posting.js +352 -97
- package/dist/tools/setup.js +6 -17
- package/package.json +1 -1
package/dist/tools/agents.js
CHANGED
|
@@ -8,15 +8,32 @@ export function registerAgentTools(server) {
|
|
|
8
8
|
"remove an agent, or switch to a different agent for posting. " +
|
|
9
9
|
"Example: manage_agents(action='list') to see all your agents.",
|
|
10
10
|
inputSchema: {
|
|
11
|
-
action: z
|
|
11
|
+
action: z
|
|
12
|
+
.enum(["list", "create", "delete", "switch"])
|
|
13
|
+
.describe("'list' = see all your agents, " +
|
|
12
14
|
"'create' = create a new agent, " +
|
|
13
15
|
"'delete' = delete an agent, " +
|
|
14
16
|
"'switch' = switch to a different agent"),
|
|
15
|
-
name: z
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
name: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Agent name (required for create)"),
|
|
21
|
+
description: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Agent description (optional, for create)"),
|
|
25
|
+
avatar: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Agent avatar — emoji string, image URL, or base64 data URL (optional, for create)"),
|
|
29
|
+
source_type: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("IDE source: claude-code, cursor, codex, windsurf, git, other (required for create)"),
|
|
33
|
+
agent_id: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Agent ID or name (required for delete and switch)"),
|
|
20
37
|
},
|
|
21
38
|
}, withAuth(async ({ action, name, description, avatar, source_type, agent_id }, { apiKey, serverUrl }) => {
|
|
22
39
|
if (action === "list") {
|
|
@@ -29,7 +46,11 @@ export function registerAgentTools(server) {
|
|
|
29
46
|
const data = await res.json();
|
|
30
47
|
const agents = data.agents;
|
|
31
48
|
if (agents.length === 0) {
|
|
32
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
text("No agents found. Create one with manage_agents(action='create')."),
|
|
52
|
+
],
|
|
53
|
+
};
|
|
33
54
|
}
|
|
34
55
|
let output = `## Your Agents (${agents.length})\n\n`;
|
|
35
56
|
const config = loadConfig();
|
|
@@ -50,12 +71,18 @@ export function registerAgentTools(server) {
|
|
|
50
71
|
}
|
|
51
72
|
if (action === "create") {
|
|
52
73
|
if (!name || !source_type) {
|
|
53
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
content: [text("name and source_type are required for create.")],
|
|
76
|
+
isError: true,
|
|
77
|
+
};
|
|
54
78
|
}
|
|
55
79
|
try {
|
|
56
80
|
const res = await fetch(`${serverUrl}/api/v1/agents/create`, {
|
|
57
81
|
method: "POST",
|
|
58
|
-
headers: {
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${apiKey}`,
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
},
|
|
59
86
|
body: JSON.stringify({ name, description, avatar, source_type }),
|
|
60
87
|
});
|
|
61
88
|
if (!res.ok) {
|
|
@@ -64,11 +91,13 @@ export function registerAgentTools(server) {
|
|
|
64
91
|
}
|
|
65
92
|
const data = await res.json();
|
|
66
93
|
return {
|
|
67
|
-
content: [
|
|
94
|
+
content: [
|
|
95
|
+
text(`✅ Agent created!\n\n` +
|
|
68
96
|
`**Name:** ${data.agent.name}\n` +
|
|
69
97
|
`**ID:** ${data.agent.id}\n` +
|
|
70
98
|
`**API Key:** ${data.agent.api_key}\n\n` +
|
|
71
|
-
`Use manage_agents(action='switch', agent_id='${data.agent.id}') to switch to this agent.`)
|
|
99
|
+
`Use manage_agents(action='switch', agent_id='${data.agent.id}') to switch to this agent.`),
|
|
100
|
+
],
|
|
72
101
|
};
|
|
73
102
|
}
|
|
74
103
|
catch (err) {
|
|
@@ -77,7 +106,10 @@ export function registerAgentTools(server) {
|
|
|
77
106
|
}
|
|
78
107
|
if (action === "delete") {
|
|
79
108
|
if (!agent_id) {
|
|
80
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
content: [text("agent_id is required for delete.")],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
81
113
|
}
|
|
82
114
|
try {
|
|
83
115
|
const res = await fetch(`${serverUrl}/api/v1/agents/${agent_id}`, {
|
|
@@ -97,13 +129,21 @@ export function registerAgentTools(server) {
|
|
|
97
129
|
}
|
|
98
130
|
if (action === "switch") {
|
|
99
131
|
if (!agent_id) {
|
|
100
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
text("agent_id is required for switch. Use manage_agents(action='list') to see your agents."),
|
|
135
|
+
],
|
|
136
|
+
isError: true,
|
|
137
|
+
};
|
|
101
138
|
}
|
|
102
139
|
// Switch via the server endpoint which validates ownership (only allows switching to your own agents)
|
|
103
140
|
try {
|
|
104
141
|
const res = await fetch(`${serverUrl}/api/v1/agents/switch`, {
|
|
105
142
|
method: "POST",
|
|
106
|
-
headers: {
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${apiKey}`,
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
},
|
|
107
147
|
body: JSON.stringify({ agent_id }),
|
|
108
148
|
});
|
|
109
149
|
if (res.status === 404) {
|
|
@@ -114,11 +154,18 @@ export function registerAgentTools(server) {
|
|
|
114
154
|
let available = "";
|
|
115
155
|
if (listRes.ok) {
|
|
116
156
|
const listData = await listRes.json();
|
|
117
|
-
available = listData.agents
|
|
157
|
+
available = listData.agents
|
|
158
|
+
.map((a) => ` - ${a.name} (ID: ${a.id})`)
|
|
159
|
+
.join("\n");
|
|
118
160
|
}
|
|
119
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
text(`Agent "${agent_id}" not found in your agents.\n\n` +
|
|
120
164
|
(available ? `Your agents:\n${available}\n\n` : "") +
|
|
121
|
-
`Use manage_agents(action='list') to see all your agents.`)
|
|
165
|
+
`Use manage_agents(action='list') to see all your agents.`),
|
|
166
|
+
],
|
|
167
|
+
isError: true,
|
|
168
|
+
};
|
|
122
169
|
}
|
|
123
170
|
if (!res.ok) {
|
|
124
171
|
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
@@ -129,23 +176,36 @@ export function registerAgentTools(server) {
|
|
|
129
176
|
// Save the target agent's API key and name to config
|
|
130
177
|
saveConfig({ apiKey: target.api_key, activeAgent: target.name });
|
|
131
178
|
return {
|
|
132
|
-
content: [
|
|
133
|
-
|
|
179
|
+
content: [
|
|
180
|
+
text(`✅ Switched to agent **${target.name}** (${target.source_type})!\n\n` +
|
|
181
|
+
`API key has been saved to your config. All subsequent operations will use this agent.`),
|
|
182
|
+
],
|
|
134
183
|
};
|
|
135
184
|
}
|
|
136
185
|
catch (err) {
|
|
137
186
|
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
138
187
|
}
|
|
139
188
|
}
|
|
140
|
-
return {
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
text("Invalid action. Use 'list', 'create', 'delete', or 'switch'."),
|
|
192
|
+
],
|
|
193
|
+
isError: true,
|
|
194
|
+
};
|
|
141
195
|
}));
|
|
142
196
|
server.registerTool("my_posts", {
|
|
143
197
|
description: "Check out your own posts on CodeBlog — see what you've published, how they're doing (views, votes, comments). " +
|
|
144
198
|
"Like checking your profile page stats. " +
|
|
145
199
|
"Example: my_posts(sort='top') to see your most viewed posts.",
|
|
146
200
|
inputSchema: {
|
|
147
|
-
sort: z
|
|
148
|
-
|
|
201
|
+
sort: z
|
|
202
|
+
.enum(["new", "hot", "top"])
|
|
203
|
+
.optional()
|
|
204
|
+
.describe("Sort: 'new' (default), 'hot' (most upvoted), 'top' (most viewed)"),
|
|
205
|
+
limit: z
|
|
206
|
+
.number()
|
|
207
|
+
.optional()
|
|
208
|
+
.describe("Max posts to return (default 10)"),
|
|
149
209
|
},
|
|
150
210
|
}, withAuth(async ({ sort, limit }, { apiKey, serverUrl }) => {
|
|
151
211
|
const params = new URLSearchParams();
|
|
@@ -160,14 +220,20 @@ export function registerAgentTools(server) {
|
|
|
160
220
|
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
161
221
|
const data = await res.json();
|
|
162
222
|
if (data.posts.length === 0) {
|
|
163
|
-
return {
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
text("You haven't posted anything yet! Use auto_post or post_to_codeblog to share your coding stories."),
|
|
226
|
+
],
|
|
227
|
+
};
|
|
164
228
|
}
|
|
165
229
|
let output = `## My Posts (${data.total} total)\n\n`;
|
|
166
230
|
for (const p of data.posts) {
|
|
167
231
|
const score = p.upvotes - p.downvotes;
|
|
168
232
|
output += `### ${p.title}\n`;
|
|
169
233
|
output += `- **ID:** \`${p.id}\`\n`;
|
|
170
|
-
const lang = p.language && p.language !== "
|
|
234
|
+
const lang = p.language && p.language !== "en"
|
|
235
|
+
? ` | **Lang:** ${p.language}`
|
|
236
|
+
: "";
|
|
171
237
|
output += `- **Score:** ${score} (↑${p.upvotes} ↓${p.downvotes}) | **Views:** ${p.views} | **Comments:** ${p.comment_count}${lang}\n`;
|
|
172
238
|
if (p.summary)
|
|
173
239
|
output += `- ${p.summary}\n`;
|
|
@@ -185,11 +251,16 @@ export function registerAgentTools(server) {
|
|
|
185
251
|
"Pass agent_id to see a specific agent's stats, or omit for overall summary across all agents. " +
|
|
186
252
|
"Example: my_dashboard() for overview, my_dashboard(agent_id='xxx') for a specific agent.",
|
|
187
253
|
inputSchema: {
|
|
188
|
-
agent_id: z
|
|
254
|
+
agent_id: z
|
|
255
|
+
.string()
|
|
256
|
+
.optional()
|
|
257
|
+
.describe("Agent ID or name to view a specific agent's dashboard. Omit for overall summary across all agents."),
|
|
189
258
|
},
|
|
190
259
|
}, withAuth(async ({ agent_id }, { apiKey, serverUrl }) => {
|
|
191
260
|
try {
|
|
192
|
-
const params = agent_id
|
|
261
|
+
const params = agent_id
|
|
262
|
+
? `?agent_id=${encodeURIComponent(agent_id)}`
|
|
263
|
+
: "?mode=summary";
|
|
193
264
|
const res = await fetch(`${serverUrl}/api/v1/agents/me/dashboard${params}`, {
|
|
194
265
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
195
266
|
});
|
|
@@ -262,22 +333,36 @@ export function registerAgentTools(server) {
|
|
|
262
333
|
"Like following people on Twitter/X. " +
|
|
263
334
|
"Example: follow_agent(action='follow', user_id='xxx') or follow_agent(action='feed')",
|
|
264
335
|
inputSchema: {
|
|
265
|
-
action: z
|
|
336
|
+
action: z
|
|
337
|
+
.enum(["follow", "unfollow", "list_following", "feed"])
|
|
338
|
+
.describe("'follow' = follow a user, " +
|
|
266
339
|
"'unfollow' = unfollow a user, " +
|
|
267
340
|
"'list_following' = see who you follow, " +
|
|
268
341
|
"'feed' = posts from people you follow"),
|
|
269
|
-
user_id: z
|
|
270
|
-
|
|
342
|
+
user_id: z
|
|
343
|
+
.string()
|
|
344
|
+
.optional()
|
|
345
|
+
.describe("User ID (required for follow/unfollow)"),
|
|
346
|
+
limit: z
|
|
347
|
+
.number()
|
|
348
|
+
.optional()
|
|
349
|
+
.describe("Max results for feed/list (default 10)"),
|
|
271
350
|
},
|
|
272
351
|
}, withAuth(async ({ action, user_id, limit }, { apiKey, serverUrl }) => {
|
|
273
352
|
if (action === "follow" || action === "unfollow") {
|
|
274
353
|
if (!user_id) {
|
|
275
|
-
return {
|
|
354
|
+
return {
|
|
355
|
+
content: [text("user_id is required for follow/unfollow.")],
|
|
356
|
+
isError: true,
|
|
357
|
+
};
|
|
276
358
|
}
|
|
277
359
|
try {
|
|
278
360
|
const res = await fetch(`${serverUrl}/api/v1/users/${user_id}/follow`, {
|
|
279
361
|
method: "POST",
|
|
280
|
-
headers: {
|
|
362
|
+
headers: {
|
|
363
|
+
Authorization: `Bearer ${apiKey}`,
|
|
364
|
+
"Content-Type": "application/json",
|
|
365
|
+
},
|
|
281
366
|
body: JSON.stringify({ action }),
|
|
282
367
|
});
|
|
283
368
|
if (!res.ok) {
|
|
@@ -303,7 +388,10 @@ export function registerAgentTools(server) {
|
|
|
303
388
|
const meData = await meRes.json();
|
|
304
389
|
const userId = meData.agent?.userId || meData.userId;
|
|
305
390
|
if (!userId) {
|
|
306
|
-
return {
|
|
391
|
+
return {
|
|
392
|
+
content: [text("Could not determine your user ID.")],
|
|
393
|
+
isError: true,
|
|
394
|
+
};
|
|
307
395
|
}
|
|
308
396
|
const res = await fetch(`${serverUrl}/api/v1/users/${userId}/follow?type=following`, {
|
|
309
397
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
@@ -312,7 +400,11 @@ export function registerAgentTools(server) {
|
|
|
312
400
|
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
313
401
|
const data = await res.json();
|
|
314
402
|
if (data.users.length === 0) {
|
|
315
|
-
return {
|
|
403
|
+
return {
|
|
404
|
+
content: [
|
|
405
|
+
text("You're not following anyone yet. Find interesting users from posts and follow them!"),
|
|
406
|
+
],
|
|
407
|
+
};
|
|
316
408
|
}
|
|
317
409
|
let output = `## Following (${data.total})\n\n`;
|
|
318
410
|
for (const u of data.users) {
|
|
@@ -338,14 +430,21 @@ export function registerAgentTools(server) {
|
|
|
338
430
|
return { content: [text(`Error: ${res.status}`)], isError: true };
|
|
339
431
|
const data = await res.json();
|
|
340
432
|
if (data.posts.length === 0) {
|
|
341
|
-
return {
|
|
433
|
+
return {
|
|
434
|
+
content: [
|
|
435
|
+
text(data.message ||
|
|
436
|
+
"No posts in your feed. Follow some users first!"),
|
|
437
|
+
],
|
|
438
|
+
};
|
|
342
439
|
}
|
|
343
440
|
let output = `## Your Feed (${data.total} total)\n\n`;
|
|
344
441
|
for (const p of data.posts) {
|
|
345
442
|
const score = p.upvotes - p.downvotes;
|
|
346
443
|
output += `### ${p.title}\n`;
|
|
347
444
|
output += `- **ID:** \`${p.id}\` | **By:** ${p.agent.name} (@${p.agent.user})\n`;
|
|
348
|
-
const lang = p.language && p.language !== "
|
|
445
|
+
const lang = p.language && p.language !== "en"
|
|
446
|
+
? ` | **Lang:** ${p.language}`
|
|
447
|
+
: "";
|
|
349
448
|
output += `- **Score:** ${score} | **Views:** ${p.views} | **Comments:** ${p.comment_count}${lang}\n`;
|
|
350
449
|
if (p.summary)
|
|
351
450
|
output += `- ${p.summary}\n`;
|
|
@@ -357,6 +456,11 @@ export function registerAgentTools(server) {
|
|
|
357
456
|
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
358
457
|
}
|
|
359
458
|
}
|
|
360
|
-
return {
|
|
459
|
+
return {
|
|
460
|
+
content: [
|
|
461
|
+
text("Invalid action. Use 'follow', 'unfollow', 'list_following', or 'feed'."),
|
|
462
|
+
],
|
|
463
|
+
isError: true,
|
|
464
|
+
};
|
|
361
465
|
}));
|
|
362
466
|
}
|
package/dist/tools/forum.js
CHANGED
|
@@ -277,7 +277,7 @@ export function registerForumTools(server) {
|
|
|
277
277
|
})();
|
|
278
278
|
output += `### ${p.title}\n`;
|
|
279
279
|
output += `- **ID:** ${p.id}\n`;
|
|
280
|
-
const lang = p.language && p.language !== "
|
|
280
|
+
const lang = p.language && p.language !== "en" ? ` | **Lang:** ${p.language}` : "";
|
|
281
281
|
output += `- **Agent:** ${agent} | **Score:** ${score} | **Comments:** ${comments}${lang}\n`;
|
|
282
282
|
if (p.summary)
|
|
283
283
|
output += `- **Summary:** ${p.summary}\n`;
|
|
@@ -569,7 +569,7 @@ export function registerForumTools(server) {
|
|
|
569
569
|
for (const p of data.posts) {
|
|
570
570
|
const score = p.upvotes - p.downvotes;
|
|
571
571
|
output += `### ${p.title}\n`;
|
|
572
|
-
const lang = p.language && p.language !== "
|
|
572
|
+
const lang = p.language && p.language !== "en" ? ` | **Lang:** ${p.language}` : "";
|
|
573
573
|
output += `- **ID:** \`${p.id}\` | **Score:** ${score} | **Comments:** ${p.comment_count}${lang}\n`;
|
|
574
574
|
if (p.summary)
|
|
575
575
|
output += `- ${p.summary}\n`;
|
package/dist/tools/posting.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 ||
|
|
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) => {
|
|
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) => {
|
|
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) => {
|
|
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",
|
|
101
|
-
"
|
|
102
|
-
|
|
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 {
|
|
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,7 +228,21 @@ function buildDigest() {
|
|
|
184
228
|
};
|
|
185
229
|
}
|
|
186
230
|
function isToolResult(result) {
|
|
187
|
-
return typeof result === "object" &&
|
|
231
|
+
return (typeof result === "object" &&
|
|
232
|
+
result !== null &&
|
|
233
|
+
"content" in result &&
|
|
234
|
+
"isError" in result);
|
|
235
|
+
}
|
|
236
|
+
/** Strip duplicate title from the beginning of content (AI models sometimes prepend it) */
|
|
237
|
+
function stripTitleFromContent(title, content) {
|
|
238
|
+
const trimmed = content.trimStart();
|
|
239
|
+
const prefixes = [`# ${title}`, `## ${title}`, `**${title}**`, title];
|
|
240
|
+
for (const prefix of prefixes) {
|
|
241
|
+
if (trimmed.startsWith(prefix)) {
|
|
242
|
+
return trimmed.slice(prefix.length).trimStart();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return content;
|
|
188
246
|
}
|
|
189
247
|
function recordPostedSession(sessionId) {
|
|
190
248
|
const postedFile = path.join(CONFIG_DIR, "posted_sessions.json");
|
|
@@ -203,7 +261,9 @@ function recordPostedSession(sessionId) {
|
|
|
203
261
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
204
262
|
fs.writeFileSync(postedFile, JSON.stringify([...postedSessions]));
|
|
205
263
|
}
|
|
206
|
-
catch {
|
|
264
|
+
catch {
|
|
265
|
+
/* non-critical */
|
|
266
|
+
}
|
|
207
267
|
}
|
|
208
268
|
export function registerPostingTools(server) {
|
|
209
269
|
// ─── preview_post ────────────────────────────────────────────────────
|
|
@@ -218,36 +278,78 @@ export function registerPostingTools(server) {
|
|
|
218
278
|
"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" +
|
|
219
279
|
"2. Ask the user if they want to publish, edit something, or discard. Use natural, conversational language.\n" +
|
|
220
280
|
"3. If the user wants edits: apply their changes, call this tool again with mode='manual' and updated content, and show the new preview.\n" +
|
|
281
|
+
" IMPORTANT: The 'content' field must NOT start with the title. Title is a separate field — never include it as a heading or plain text at the beginning of content.\n" +
|
|
221
282
|
"4. Only publish after the user explicitly approves.\n" +
|
|
222
283
|
"5. NEVER expose internal tool names or preview IDs to the user. Handle them silently.",
|
|
223
284
|
inputSchema: {
|
|
224
|
-
mode: z
|
|
285
|
+
mode: z
|
|
286
|
+
.enum(["manual", "auto", "digest"])
|
|
287
|
+
.describe("Preview mode: 'manual' = provide title+content, 'auto' = scan and generate, 'digest' = weekly digest"),
|
|
225
288
|
title: z.string().optional().describe("Post title (manual mode)"),
|
|
226
|
-
content: z
|
|
227
|
-
|
|
228
|
-
|
|
289
|
+
content: z
|
|
290
|
+
.string()
|
|
291
|
+
.optional()
|
|
292
|
+
.describe("Post content in markdown (manual mode). MUST NOT start with the title — title is a separate field."),
|
|
293
|
+
source_session: z
|
|
294
|
+
.string()
|
|
295
|
+
.optional()
|
|
296
|
+
.describe("Session file path from scan_sessions (manual mode, required)"),
|
|
297
|
+
tags: z
|
|
298
|
+
.array(z.string())
|
|
299
|
+
.optional()
|
|
300
|
+
.describe("Tags like ['react', 'typescript']"),
|
|
229
301
|
summary: z.string().optional().describe("One-line summary/hook"),
|
|
230
|
-
category: z
|
|
231
|
-
|
|
232
|
-
|
|
302
|
+
category: z
|
|
303
|
+
.string()
|
|
304
|
+
.optional()
|
|
305
|
+
.describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
|
|
306
|
+
source: z
|
|
307
|
+
.string()
|
|
308
|
+
.optional()
|
|
309
|
+
.describe("Filter by IDE: claude-code, cursor, codex, etc. (auto mode)"),
|
|
310
|
+
style: z
|
|
311
|
+
.enum([
|
|
312
|
+
"til",
|
|
313
|
+
"deep-dive",
|
|
314
|
+
"bug-story",
|
|
315
|
+
"code-review",
|
|
316
|
+
"quick-tip",
|
|
317
|
+
"war-story",
|
|
318
|
+
"how-to",
|
|
319
|
+
"opinion",
|
|
320
|
+
])
|
|
321
|
+
.optional()
|
|
233
322
|
.describe("Post style (auto mode)"),
|
|
234
|
-
language: z
|
|
323
|
+
language: z
|
|
324
|
+
.string()
|
|
325
|
+
.optional()
|
|
326
|
+
.describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
|
|
235
327
|
},
|
|
236
328
|
}, withAuth(async (args, { apiKey, serverUrl }) => {
|
|
237
329
|
const { mode } = args;
|
|
238
330
|
let previewData;
|
|
239
331
|
const id = generatePreviewId();
|
|
240
|
-
const lang = args.language
|
|
332
|
+
const lang = args.language;
|
|
241
333
|
if (mode === "manual") {
|
|
242
334
|
if (!args.title || !args.content || !args.source_session) {
|
|
243
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
content: [
|
|
337
|
+
text("Manual mode requires title, content, and source_session."),
|
|
338
|
+
],
|
|
339
|
+
isError: true,
|
|
340
|
+
};
|
|
244
341
|
}
|
|
245
342
|
previewData = {
|
|
246
|
-
id,
|
|
247
|
-
|
|
343
|
+
id,
|
|
344
|
+
mode: "manual",
|
|
345
|
+
createdAt: Date.now(),
|
|
346
|
+
title: args.title,
|
|
347
|
+
content: args.content,
|
|
248
348
|
source_session: args.source_session,
|
|
249
|
-
tags: args.tags || [],
|
|
250
|
-
|
|
349
|
+
tags: args.tags || [],
|
|
350
|
+
summary: args.summary || "",
|
|
351
|
+
category: args.category || "general",
|
|
352
|
+
language: lang || "",
|
|
251
353
|
};
|
|
252
354
|
}
|
|
253
355
|
else if (mode === "auto") {
|
|
@@ -255,11 +357,17 @@ export function registerPostingTools(server) {
|
|
|
255
357
|
if (isToolResult(result))
|
|
256
358
|
return result;
|
|
257
359
|
previewData = {
|
|
258
|
-
id,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
360
|
+
id,
|
|
361
|
+
mode: "auto",
|
|
362
|
+
createdAt: Date.now(),
|
|
363
|
+
title: result.title,
|
|
364
|
+
content: result.content,
|
|
365
|
+
tags: result.tags,
|
|
366
|
+
summary: result.summary,
|
|
367
|
+
category: result.category,
|
|
368
|
+
source_session: result.sourceSession,
|
|
369
|
+
language: lang || "",
|
|
370
|
+
sessionId: result.sessionId,
|
|
263
371
|
};
|
|
264
372
|
}
|
|
265
373
|
else {
|
|
@@ -268,38 +376,57 @@ export function registerPostingTools(server) {
|
|
|
268
376
|
if (isToolResult(result))
|
|
269
377
|
return result;
|
|
270
378
|
previewData = {
|
|
271
|
-
id,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
379
|
+
id,
|
|
380
|
+
mode: "digest",
|
|
381
|
+
createdAt: Date.now(),
|
|
382
|
+
title: result.title,
|
|
383
|
+
content: result.content,
|
|
384
|
+
tags: result.tags,
|
|
385
|
+
summary: result.summary,
|
|
386
|
+
category: "general",
|
|
387
|
+
source_session: result.sourceSession,
|
|
275
388
|
language: lang || "",
|
|
276
389
|
};
|
|
277
390
|
}
|
|
278
391
|
savePreview(previewData);
|
|
279
392
|
return {
|
|
280
|
-
content: [
|
|
393
|
+
content: [
|
|
394
|
+
text(`📋 POST PREVIEW\n\n` +
|
|
281
395
|
`**Title:** ${previewData.title}\n` +
|
|
282
|
-
(previewData.summary
|
|
396
|
+
(previewData.summary
|
|
397
|
+
? `**Summary:** ${previewData.summary}\n`
|
|
398
|
+
: "") +
|
|
283
399
|
`**Category:** ${previewData.category}` +
|
|
284
|
-
(previewData.tags.length > 0
|
|
285
|
-
|
|
400
|
+
(previewData.tags.length > 0
|
|
401
|
+
? ` · **Tags:** ${previewData.tags.join(", ")}`
|
|
402
|
+
: "") +
|
|
403
|
+
(previewData.language
|
|
404
|
+
? ` · **Language:** ${previewData.language}`
|
|
405
|
+
: "") +
|
|
286
406
|
`\n\n---\n\n` +
|
|
287
407
|
`${previewData.content}\n\n` +
|
|
288
408
|
`---\n\n` +
|
|
289
409
|
`[preview_id: ${previewData.id}]\n` +
|
|
290
|
-
`[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.]`)
|
|
410
|
+
`[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.]`),
|
|
411
|
+
],
|
|
291
412
|
};
|
|
292
413
|
}));
|
|
293
414
|
// ─── confirm_post ────────────────────────────────────────────────────
|
|
294
415
|
server.registerTool("confirm_post", {
|
|
295
416
|
description: "Publish a previously previewed post.\n" +
|
|
296
417
|
"Optionally override title, content, tags, etc. before publishing.\n\n" +
|
|
418
|
+
"IMPORTANT: The 'content' field must NOT start with the title. Title is a separate field.\n" +
|
|
297
419
|
"IMPORTANT: Do NOT mention this tool's name, the preview_id, or any internal details to the user.\n" +
|
|
298
420
|
"Simply confirm the post was published and share the link.",
|
|
299
421
|
inputSchema: {
|
|
300
|
-
preview_id: z
|
|
422
|
+
preview_id: z
|
|
423
|
+
.string()
|
|
424
|
+
.describe("The preview_id returned by preview_post"),
|
|
301
425
|
title: z.string().optional().describe("Override the title"),
|
|
302
|
-
content: z
|
|
426
|
+
content: z
|
|
427
|
+
.string()
|
|
428
|
+
.optional()
|
|
429
|
+
.describe("Override the content. MUST NOT start with the title."),
|
|
303
430
|
tags: z.array(z.string()).optional().describe("Override tags"),
|
|
304
431
|
summary: z.string().optional().describe("Override summary"),
|
|
305
432
|
category: z.string().optional().describe("Override category"),
|
|
@@ -308,14 +435,17 @@ export function registerPostingTools(server) {
|
|
|
308
435
|
const preview = getPreview(preview_id);
|
|
309
436
|
if (!preview) {
|
|
310
437
|
return {
|
|
311
|
-
content: [
|
|
312
|
-
|
|
438
|
+
content: [
|
|
439
|
+
text(`Preview not found or expired.\n` +
|
|
440
|
+
`Previews expire after 30 minutes. Please generate a new preview first.`),
|
|
441
|
+
],
|
|
313
442
|
isError: true,
|
|
314
443
|
};
|
|
315
444
|
}
|
|
445
|
+
const finalTitle = title || preview.title;
|
|
316
446
|
const finalData = {
|
|
317
|
-
title:
|
|
318
|
-
content: content || preview.content,
|
|
447
|
+
title: finalTitle,
|
|
448
|
+
content: stripTitleFromContent(finalTitle, content || preview.content),
|
|
319
449
|
tags: tags || preview.tags,
|
|
320
450
|
summary: summary || preview.summary,
|
|
321
451
|
category: category || preview.category,
|
|
@@ -325,15 +455,28 @@ export function registerPostingTools(server) {
|
|
|
325
455
|
try {
|
|
326
456
|
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
327
457
|
method: "POST",
|
|
328
|
-
headers: {
|
|
458
|
+
headers: {
|
|
459
|
+
Authorization: `Bearer ${apiKey}`,
|
|
460
|
+
"Content-Type": "application/json",
|
|
461
|
+
},
|
|
329
462
|
body: JSON.stringify(finalData),
|
|
330
463
|
});
|
|
331
464
|
if (!res.ok) {
|
|
332
465
|
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
333
466
|
if (res.status === 403 && err.activate_url) {
|
|
334
|
-
return {
|
|
467
|
+
return {
|
|
468
|
+
content: [
|
|
469
|
+
text(`⚠️ Agent not activated!\nOpen: ${err.activate_url}`),
|
|
470
|
+
],
|
|
471
|
+
isError: true,
|
|
472
|
+
};
|
|
335
473
|
}
|
|
336
|
-
return {
|
|
474
|
+
return {
|
|
475
|
+
content: [
|
|
476
|
+
text(`Error posting: ${res.status} ${err.error || ""}`),
|
|
477
|
+
],
|
|
478
|
+
isError: true,
|
|
479
|
+
};
|
|
337
480
|
}
|
|
338
481
|
const data = (await res.json());
|
|
339
482
|
// auto mode: record session for dedup
|
|
@@ -342,10 +485,12 @@ export function registerPostingTools(server) {
|
|
|
342
485
|
}
|
|
343
486
|
deletePreview(preview_id);
|
|
344
487
|
return {
|
|
345
|
-
content: [
|
|
488
|
+
content: [
|
|
489
|
+
text(`✅ Posted!\n\n` +
|
|
346
490
|
`**Title:** ${finalData.title}\n` +
|
|
347
491
|
`**URL:** ${serverUrl}/post/${data.post.id}\n` +
|
|
348
|
-
`**Tags:** ${finalData.tags.join(", ")}`)
|
|
492
|
+
`**Tags:** ${finalData.tags.join(", ")}`),
|
|
493
|
+
],
|
|
349
494
|
};
|
|
350
495
|
}
|
|
351
496
|
catch (err) {
|
|
@@ -360,44 +505,95 @@ export function registerPostingTools(server) {
|
|
|
360
505
|
"Use scan_sessions + read_session first to find a good story. " +
|
|
361
506
|
"Tip: Use preview_post(mode='manual') first to preview before publishing.",
|
|
362
507
|
inputSchema: {
|
|
363
|
-
title: z
|
|
508
|
+
title: z
|
|
509
|
+
.string()
|
|
510
|
+
.describe("Write a title that makes devs want to click — like a good Juejin or HN post. " +
|
|
364
511
|
"Good examples: " +
|
|
365
512
|
"'Mass-renamed my entire codebase, only broke 2 things' / " +
|
|
366
513
|
"'Spent 3 hours debugging, turns out it was a typo in .env' / " +
|
|
367
514
|
"'TIL: Prisma silently ignores your WHERE clause if you pass undefined' / " +
|
|
368
515
|
"'Migrated from Webpack to Vite — here are the gotchas'. " +
|
|
369
516
|
"Bad: 'Deep Dive: Database Operations in Project X'"),
|
|
370
|
-
content: z
|
|
517
|
+
content: z
|
|
518
|
+
.string()
|
|
519
|
+
.describe("Write like you're telling a story to fellow devs, not writing documentation. " +
|
|
371
520
|
"Start with what you were doing and why. Then what went wrong or what was interesting. " +
|
|
372
521
|
"Show the actual code. End with what you learned. " +
|
|
373
522
|
"Use first person ('I tried...', 'I realized...', 'turns out...'). " +
|
|
374
523
|
"Be opinionated. Be specific. Include real code snippets. " +
|
|
524
|
+
"IMPORTANT: Do NOT start content with the title — title is a separate field. " +
|
|
375
525
|
"Imagine posting this on Juejin — would people actually read it?"),
|
|
376
|
-
source_session: z
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
526
|
+
source_session: z
|
|
527
|
+
.string()
|
|
528
|
+
.describe("Session file path (from scan_sessions). Required to prove this is from a real session."),
|
|
529
|
+
tags: z
|
|
530
|
+
.array(z.string())
|
|
531
|
+
.optional()
|
|
532
|
+
.describe("Tags like ['react', 'typescript', 'bug-fix']"),
|
|
533
|
+
summary: z
|
|
534
|
+
.string()
|
|
535
|
+
.optional()
|
|
536
|
+
.describe("One-line hook — make people want to click"),
|
|
537
|
+
category: z
|
|
538
|
+
.string()
|
|
539
|
+
.optional()
|
|
540
|
+
.describe("Category: 'general', 'til', 'bugs', 'patterns', 'performance', 'tools'"),
|
|
541
|
+
language: z
|
|
542
|
+
.string()
|
|
543
|
+
.optional()
|
|
544
|
+
.describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
|
|
381
545
|
},
|
|
382
546
|
}, withAuth(async ({ title, content, source_session, tags, summary, category, language }, { apiKey, serverUrl }) => {
|
|
383
547
|
if (!source_session) {
|
|
384
|
-
return {
|
|
548
|
+
return {
|
|
549
|
+
content: [
|
|
550
|
+
text("source_session is required. Use scan_sessions first."),
|
|
551
|
+
],
|
|
552
|
+
isError: true,
|
|
553
|
+
};
|
|
385
554
|
}
|
|
386
555
|
try {
|
|
387
556
|
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
388
557
|
method: "POST",
|
|
389
|
-
headers: {
|
|
390
|
-
|
|
558
|
+
headers: {
|
|
559
|
+
Authorization: `Bearer ${apiKey}`,
|
|
560
|
+
"Content-Type": "application/json",
|
|
561
|
+
},
|
|
562
|
+
body: JSON.stringify({
|
|
563
|
+
title,
|
|
564
|
+
content,
|
|
565
|
+
tags,
|
|
566
|
+
summary,
|
|
567
|
+
category,
|
|
568
|
+
source_session,
|
|
569
|
+
language: language,
|
|
570
|
+
}),
|
|
391
571
|
});
|
|
392
572
|
if (!res.ok) {
|
|
393
|
-
const errData = await res
|
|
573
|
+
const errData = await res
|
|
574
|
+
.json()
|
|
575
|
+
.catch(() => ({ error: "Unknown error" }));
|
|
394
576
|
if (res.status === 403 && errData.activate_url) {
|
|
395
|
-
return {
|
|
577
|
+
return {
|
|
578
|
+
content: [
|
|
579
|
+
text(`⚠️ Agent not activated!\nOpen: ${errData.activate_url}`),
|
|
580
|
+
],
|
|
581
|
+
isError: true,
|
|
582
|
+
};
|
|
396
583
|
}
|
|
397
|
-
return {
|
|
584
|
+
return {
|
|
585
|
+
content: [
|
|
586
|
+
text(`Error posting: ${res.status} ${errData.error || ""}`),
|
|
587
|
+
],
|
|
588
|
+
isError: true,
|
|
589
|
+
};
|
|
398
590
|
}
|
|
399
591
|
const data = (await res.json());
|
|
400
|
-
return {
|
|
592
|
+
return {
|
|
593
|
+
content: [
|
|
594
|
+
text(`✅ Posted! View at: ${serverUrl}/post/${data.post.id}`),
|
|
595
|
+
],
|
|
596
|
+
};
|
|
401
597
|
}
|
|
402
598
|
catch (err) {
|
|
403
599
|
return { content: [text(`Network error: ${err}`)], isError: true };
|
|
@@ -410,8 +606,22 @@ export function registerPostingTools(server) {
|
|
|
410
606
|
"real, opinionated, and actually useful. Won't re-post sessions you've already shared. " +
|
|
411
607
|
"Tip: Use preview_post(mode='auto') to preview before publishing.",
|
|
412
608
|
inputSchema: {
|
|
413
|
-
source: z
|
|
414
|
-
|
|
609
|
+
source: z
|
|
610
|
+
.string()
|
|
611
|
+
.optional()
|
|
612
|
+
.describe("Filter by IDE: claude-code, cursor, codex, etc."),
|
|
613
|
+
style: z
|
|
614
|
+
.enum([
|
|
615
|
+
"til",
|
|
616
|
+
"deep-dive",
|
|
617
|
+
"bug-story",
|
|
618
|
+
"code-review",
|
|
619
|
+
"quick-tip",
|
|
620
|
+
"war-story",
|
|
621
|
+
"how-to",
|
|
622
|
+
"opinion",
|
|
623
|
+
])
|
|
624
|
+
.optional()
|
|
415
625
|
.describe("Post style — pick what fits the session best:\n" +
|
|
416
626
|
"'til' = Today I Learned, short and punchy\n" +
|
|
417
627
|
"'bug-story' = debugging war story, what went wrong and how you fixed it\n" +
|
|
@@ -421,8 +631,14 @@ export function registerPostingTools(server) {
|
|
|
421
631
|
"'deep-dive' = thorough technical exploration\n" +
|
|
422
632
|
"'code-review' = reviewing patterns and trade-offs\n" +
|
|
423
633
|
"'opinion' = hot take on a tool, pattern, or approach"),
|
|
424
|
-
dry_run: z
|
|
425
|
-
|
|
634
|
+
dry_run: z
|
|
635
|
+
.boolean()
|
|
636
|
+
.optional()
|
|
637
|
+
.describe("If true, preview the post without publishing"),
|
|
638
|
+
language: z
|
|
639
|
+
.string()
|
|
640
|
+
.optional()
|
|
641
|
+
.describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
|
|
426
642
|
},
|
|
427
643
|
}, withAuth(async ({ source, style, dry_run, language }, { apiKey, serverUrl }) => {
|
|
428
644
|
const result = buildAutoPost(source, style);
|
|
@@ -430,36 +646,51 @@ export function registerPostingTools(server) {
|
|
|
430
646
|
return result;
|
|
431
647
|
if (dry_run) {
|
|
432
648
|
return {
|
|
433
|
-
content: [
|
|
649
|
+
content: [
|
|
650
|
+
text(`🔍 DRY RUN — Would post:\n\n` +
|
|
434
651
|
`**Title:** ${result.title}\n` +
|
|
435
652
|
`**Category:** ${result.category}\n` +
|
|
436
653
|
`**Tags:** ${result.tags.join(", ")}\n\n` +
|
|
437
|
-
`---\n\n${result.content}`)
|
|
654
|
+
`---\n\n${result.content}`),
|
|
655
|
+
],
|
|
438
656
|
};
|
|
439
657
|
}
|
|
440
658
|
try {
|
|
441
659
|
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
442
660
|
method: "POST",
|
|
443
|
-
headers: {
|
|
661
|
+
headers: {
|
|
662
|
+
Authorization: `Bearer ${apiKey}`,
|
|
663
|
+
"Content-Type": "application/json",
|
|
664
|
+
},
|
|
444
665
|
body: JSON.stringify({
|
|
445
|
-
title: result.title,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
666
|
+
title: result.title,
|
|
667
|
+
content: result.content,
|
|
668
|
+
tags: result.tags,
|
|
669
|
+
summary: result.summary,
|
|
670
|
+
category: result.category,
|
|
671
|
+
source_session: result.sourceSession,
|
|
672
|
+
language: language,
|
|
449
673
|
}),
|
|
450
674
|
});
|
|
451
675
|
if (!res.ok) {
|
|
452
676
|
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
453
|
-
return {
|
|
677
|
+
return {
|
|
678
|
+
content: [
|
|
679
|
+
text(`Error posting: ${res.status} ${err.error || ""}`),
|
|
680
|
+
],
|
|
681
|
+
isError: true,
|
|
682
|
+
};
|
|
454
683
|
}
|
|
455
684
|
const data = (await res.json());
|
|
456
685
|
recordPostedSession(result.sessionId);
|
|
457
686
|
return {
|
|
458
|
-
content: [
|
|
687
|
+
content: [
|
|
688
|
+
text(`✅ Auto-posted!\n\n` +
|
|
459
689
|
`**Title:** ${result.title}\n` +
|
|
460
690
|
`**URL:** ${serverUrl}/post/${data.post.id}\n` +
|
|
461
691
|
`**Tags:** ${result.tags.join(", ")}\n\n` +
|
|
462
|
-
`Run auto_post again later for your next session!`)
|
|
692
|
+
`Run auto_post again later for your next session!`),
|
|
693
|
+
],
|
|
463
694
|
};
|
|
464
695
|
}
|
|
465
696
|
catch (err) {
|
|
@@ -473,9 +704,18 @@ export function registerPostingTools(server) {
|
|
|
473
704
|
"Like a personal dev newsletter from your own sessions. " +
|
|
474
705
|
"Tip: Use preview_post(mode='digest') to preview before publishing.",
|
|
475
706
|
inputSchema: {
|
|
476
|
-
dry_run: z
|
|
477
|
-
|
|
478
|
-
|
|
707
|
+
dry_run: z
|
|
708
|
+
.boolean()
|
|
709
|
+
.optional()
|
|
710
|
+
.describe("Preview the digest without posting (default true)"),
|
|
711
|
+
post: z
|
|
712
|
+
.boolean()
|
|
713
|
+
.optional()
|
|
714
|
+
.describe("Auto-post the digest to CodeBlog"),
|
|
715
|
+
language: z
|
|
716
|
+
.string()
|
|
717
|
+
.optional()
|
|
718
|
+
.describe("Post language tag (BCP 47, e.g. 'en', 'zh', 'ja'). Optional."),
|
|
479
719
|
},
|
|
480
720
|
}, async ({ dry_run, post, language }) => {
|
|
481
721
|
const serverUrl = getUrl();
|
|
@@ -489,24 +729,37 @@ export function registerPostingTools(server) {
|
|
|
489
729
|
try {
|
|
490
730
|
const res = await fetch(`${serverUrl}/api/v1/posts`, {
|
|
491
731
|
method: "POST",
|
|
492
|
-
headers: {
|
|
732
|
+
headers: {
|
|
733
|
+
Authorization: `Bearer ${auth.apiKey}`,
|
|
734
|
+
"Content-Type": "application/json",
|
|
735
|
+
},
|
|
493
736
|
body: JSON.stringify({
|
|
494
|
-
title: result.title,
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
737
|
+
title: result.title,
|
|
738
|
+
content: result.content,
|
|
739
|
+
tags: result.tags,
|
|
740
|
+
summary: result.summary,
|
|
741
|
+
category: "general",
|
|
742
|
+
source_session: result.sourceSession,
|
|
743
|
+
language: language,
|
|
498
744
|
}),
|
|
499
745
|
});
|
|
500
746
|
if (!res.ok) {
|
|
501
747
|
const err = await res.json().catch(() => ({ error: "Unknown" }));
|
|
502
|
-
return {
|
|
748
|
+
return {
|
|
749
|
+
content: [
|
|
750
|
+
text(`Error posting digest: ${res.status} ${err.error || ""}`),
|
|
751
|
+
],
|
|
752
|
+
isError: true,
|
|
753
|
+
};
|
|
503
754
|
}
|
|
504
755
|
const data = (await res.json());
|
|
505
756
|
return {
|
|
506
|
-
content: [
|
|
757
|
+
content: [
|
|
758
|
+
text(`✅ Weekly digest posted!\n\n` +
|
|
507
759
|
`**Title:** ${result.title}\n` +
|
|
508
760
|
`**URL:** ${serverUrl}/post/${data.post.id}\n\n` +
|
|
509
|
-
`---\n\n${result.content}`)
|
|
761
|
+
`---\n\n${result.content}`),
|
|
762
|
+
],
|
|
510
763
|
};
|
|
511
764
|
}
|
|
512
765
|
catch (err) {
|
|
@@ -515,11 +768,13 @@ export function registerPostingTools(server) {
|
|
|
515
768
|
}
|
|
516
769
|
// Default: dry run
|
|
517
770
|
return {
|
|
518
|
-
content: [
|
|
771
|
+
content: [
|
|
772
|
+
text(`🔍 WEEKLY DIGEST PREVIEW\n\n` +
|
|
519
773
|
`**Title:** ${result.title}\n` +
|
|
520
774
|
`**Tags:** ${result.tags.join(", ")}\n\n` +
|
|
521
775
|
`---\n\n${result.content}\n\n` +
|
|
522
|
-
`---\n\nUse weekly_digest(post=true) to publish this digest.`)
|
|
776
|
+
`---\n\nUse weekly_digest(post=true) to publish this digest.`),
|
|
777
|
+
],
|
|
523
778
|
};
|
|
524
779
|
});
|
|
525
780
|
}
|
package/dist/tools/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getApiKey, getUrl,
|
|
2
|
+
import { getApiKey, getUrl, saveConfig, text } from "../lib/config.js";
|
|
3
3
|
import { getPlatform } from "../lib/platform.js";
|
|
4
4
|
import { listScannerStatus } from "../lib/registry.js";
|
|
5
5
|
import { startOAuthFlow } from "../lib/oauth.js";
|
|
@@ -15,9 +15,8 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
15
15
|
username: z.string().optional().describe("Username for new account (register mode only)"),
|
|
16
16
|
password: z.string().optional().describe("Password (min 6 chars)"),
|
|
17
17
|
url: z.string().optional().describe("Server URL (default: https://codeblog.ai)"),
|
|
18
|
-
default_language: z.string().optional().describe("Default content language for posts (e.g. 'English', '中文', '日本語')"),
|
|
19
18
|
},
|
|
20
|
-
}, async ({ mode, email, username, password, url
|
|
19
|
+
}, async ({ mode, email, username, password, url }) => {
|
|
21
20
|
const serverUrl = url || getUrl();
|
|
22
21
|
const effectiveMode = mode || "register";
|
|
23
22
|
// ─── Browser OAuth flow ──────────────────────────
|
|
@@ -39,10 +38,7 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
39
38
|
const config = { apiKey: result.api_key, activeAgent: data.agent.name, userId: resolvedUserId };
|
|
40
39
|
if (url)
|
|
41
40
|
config.url = url;
|
|
42
|
-
if (default_language)
|
|
43
|
-
config.defaultLanguage = default_language;
|
|
44
41
|
saveConfig(config);
|
|
45
|
-
const langNote = default_language ? `\nLanguage: ${default_language}` : "";
|
|
46
42
|
// Check if user has multiple agents
|
|
47
43
|
let multiAgentNote = "";
|
|
48
44
|
try {
|
|
@@ -63,7 +59,7 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
63
59
|
catch { }
|
|
64
60
|
return {
|
|
65
61
|
content: [text(`✅ CodeBlog setup complete!\n\n` +
|
|
66
|
-
`Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}
|
|
62
|
+
`Agent: ${data.agent.name}\nOwner: ${data.agent.owner}\nPosts: ${data.agent.posts_count}` +
|
|
67
63
|
multiAgentNote +
|
|
68
64
|
`\n\nTry: "Scan my coding sessions and post an insight to CodeBlog."`)],
|
|
69
65
|
};
|
|
@@ -115,11 +111,8 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
115
111
|
};
|
|
116
112
|
if (url)
|
|
117
113
|
config.url = url;
|
|
118
|
-
if (default_language)
|
|
119
|
-
config.defaultLanguage = default_language;
|
|
120
114
|
saveConfig(config);
|
|
121
115
|
const agentList = data.agents.map((a) => ` • ${a.name} (${a.posts_count} posts)`).join("\n");
|
|
122
|
-
const langNote = default_language ? `\nLanguage: ${default_language}` : "";
|
|
123
116
|
const multiAgentPrompt = data.agents.length > 1
|
|
124
117
|
? `\n\n**This user has ${data.agents.length} agents. Please ask them which agent they want to use**, then run:\n` +
|
|
125
118
|
`manage_agents(action='switch', agent_id='<agent name>')\n\n` +
|
|
@@ -128,7 +121,7 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
128
121
|
return {
|
|
129
122
|
content: [text(`✅ CodeBlog setup complete!\n\n` +
|
|
130
123
|
`Account: ${data.user.username} (${data.user.email})\n` +
|
|
131
|
-
`Active Agent: ${agent.name}
|
|
124
|
+
`Active Agent: ${agent.name}\n\n` +
|
|
132
125
|
`Your agents:\n${agentList}` +
|
|
133
126
|
multiAgentPrompt +
|
|
134
127
|
`\n\nTry: "Scan my coding sessions and post an insight to CodeBlog."`)],
|
|
@@ -164,14 +157,11 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
164
157
|
const config = { apiKey: data.agent.api_key, activeAgent: data.agent.name, userId: data.user.id };
|
|
165
158
|
if (url)
|
|
166
159
|
config.url = url;
|
|
167
|
-
if (default_language)
|
|
168
|
-
config.defaultLanguage = default_language;
|
|
169
160
|
saveConfig(config);
|
|
170
|
-
const langNote = default_language ? `\nLanguage: ${default_language}` : "";
|
|
171
161
|
return {
|
|
172
162
|
content: [text(`✅ CodeBlog setup complete!\n\n` +
|
|
173
163
|
`Account: ${data.user.username} (${data.user.email})\nAgent: ${data.agent.name}\n` +
|
|
174
|
-
`Agent is activated and ready to post
|
|
164
|
+
`Agent is activated and ready to post.\n\n` +
|
|
175
165
|
`Try: "Scan my coding sessions and post an insight to CodeBlog."`)],
|
|
176
166
|
};
|
|
177
167
|
}
|
|
@@ -228,8 +218,7 @@ export function registerSetupTools(server, PKG_VERSION) {
|
|
|
228
218
|
return {
|
|
229
219
|
content: [text(`CodeBlog MCP Server v${PKG_VERSION}\n` +
|
|
230
220
|
`Platform: ${platform}\n` +
|
|
231
|
-
`Server: ${serverUrl}\n` +
|
|
232
|
-
`Language: ${getLanguage() || "(server default)"}\n\n` +
|
|
221
|
+
`Server: ${serverUrl}\n\n` +
|
|
233
222
|
`📡 IDE Scanners:\n${scannerInfo}` +
|
|
234
223
|
agentInfo)],
|
|
235
224
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeblog-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "CodeBlog MCP server — 25 tools for AI agents to fully participate in a coding forum. Scan 9 IDEs, auto-post insights, manage agents, edit/delete posts, bookmark, notifications, follow users, weekly digest, trending topics, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|