codeblog-app 1.6.4 → 2.0.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.
Files changed (76) hide show
  1. package/package.json +4 -18
  2. package/src/ai/__tests__/chat.test.ts +110 -0
  3. package/src/ai/__tests__/provider.test.ts +184 -0
  4. package/src/ai/__tests__/tools.test.ts +90 -0
  5. package/src/ai/chat.ts +81 -50
  6. package/src/ai/provider.ts +24 -250
  7. package/src/ai/tools.ts +46 -281
  8. package/src/auth/oauth.ts +7 -0
  9. package/src/cli/__tests__/commands.test.ts +225 -0
  10. package/src/cli/__tests__/setup.test.ts +57 -0
  11. package/src/cli/cmd/agent.ts +102 -0
  12. package/src/cli/cmd/chat.ts +1 -1
  13. package/src/cli/cmd/comment.ts +47 -16
  14. package/src/cli/cmd/feed.ts +18 -30
  15. package/src/cli/cmd/forum.ts +123 -0
  16. package/src/cli/cmd/login.ts +9 -2
  17. package/src/cli/cmd/me.ts +202 -0
  18. package/src/cli/cmd/post.ts +6 -88
  19. package/src/cli/cmd/publish.ts +44 -23
  20. package/src/cli/cmd/scan.ts +45 -34
  21. package/src/cli/cmd/search.ts +8 -70
  22. package/src/cli/cmd/setup.ts +160 -62
  23. package/src/cli/cmd/vote.ts +29 -14
  24. package/src/cli/cmd/whoami.ts +7 -36
  25. package/src/cli/ui.ts +50 -0
  26. package/src/index.ts +80 -59
  27. package/src/mcp/__tests__/client.test.ts +149 -0
  28. package/src/mcp/__tests__/e2e.ts +327 -0
  29. package/src/mcp/__tests__/integration.ts +148 -0
  30. package/src/mcp/client.ts +148 -0
  31. package/src/api/agents.ts +0 -103
  32. package/src/api/bookmarks.ts +0 -25
  33. package/src/api/client.ts +0 -96
  34. package/src/api/debates.ts +0 -35
  35. package/src/api/feed.ts +0 -25
  36. package/src/api/notifications.ts +0 -31
  37. package/src/api/posts.ts +0 -116
  38. package/src/api/search.ts +0 -29
  39. package/src/api/tags.ts +0 -13
  40. package/src/api/trending.ts +0 -38
  41. package/src/api/users.ts +0 -8
  42. package/src/cli/cmd/agents.ts +0 -77
  43. package/src/cli/cmd/ai-publish.ts +0 -118
  44. package/src/cli/cmd/bookmark.ts +0 -27
  45. package/src/cli/cmd/bookmarks.ts +0 -42
  46. package/src/cli/cmd/dashboard.ts +0 -59
  47. package/src/cli/cmd/debate.ts +0 -89
  48. package/src/cli/cmd/delete.ts +0 -35
  49. package/src/cli/cmd/edit.ts +0 -42
  50. package/src/cli/cmd/explore.ts +0 -63
  51. package/src/cli/cmd/follow.ts +0 -34
  52. package/src/cli/cmd/myposts.ts +0 -50
  53. package/src/cli/cmd/notifications.ts +0 -65
  54. package/src/cli/cmd/tags.ts +0 -58
  55. package/src/cli/cmd/trending.ts +0 -64
  56. package/src/cli/cmd/weekly-digest.ts +0 -117
  57. package/src/publisher/index.ts +0 -139
  58. package/src/scanner/__tests__/analyzer.test.ts +0 -67
  59. package/src/scanner/__tests__/fs-utils.test.ts +0 -50
  60. package/src/scanner/__tests__/platform.test.ts +0 -27
  61. package/src/scanner/__tests__/registry.test.ts +0 -56
  62. package/src/scanner/aider.ts +0 -96
  63. package/src/scanner/analyzer.ts +0 -237
  64. package/src/scanner/claude-code.ts +0 -188
  65. package/src/scanner/codex.ts +0 -127
  66. package/src/scanner/continue-dev.ts +0 -95
  67. package/src/scanner/cursor.ts +0 -299
  68. package/src/scanner/fs-utils.ts +0 -123
  69. package/src/scanner/index.ts +0 -26
  70. package/src/scanner/platform.ts +0 -44
  71. package/src/scanner/registry.ts +0 -68
  72. package/src/scanner/types.ts +0 -62
  73. package/src/scanner/vscode-copilot.ts +0 -125
  74. package/src/scanner/warp.ts +0 -19
  75. package/src/scanner/windsurf.ts +0 -147
  76. package/src/scanner/zed.ts +0 -88
package/src/ai/tools.ts CHANGED
@@ -1,30 +1,10 @@
1
- import { tool } from "ai"
1
+ import { tool as _rawTool } from "ai"
2
2
  import { z } from "zod"
3
- import { AIProvider } from "./provider"
3
+ import { McpBridge } from "../mcp/client"
4
4
 
5
- // ---------------------------------------------------------------------------
6
- // API helper authenticated requests to CodeBlog server
7
- // ---------------------------------------------------------------------------
8
- async function api(method: string, path: string, body?: unknown) {
9
- const { Config } = await import("../config")
10
- const { Auth } = await import("../auth")
11
- const base = await Config.url()
12
- const headers: Record<string, string> = { "Content-Type": "application/json" }
13
- const auth = await Auth.header()
14
- Object.assign(headers, auth)
15
- const cfg = await Config.load()
16
- if (cfg.api_key && !headers["Authorization"]) headers["Authorization"] = `Bearer ${cfg.api_key}`
17
- const res = await fetch(`${base}${path}`, {
18
- method,
19
- headers,
20
- body: body ? JSON.stringify(body) : undefined,
21
- })
22
- if (!res.ok) {
23
- const err = await res.text().catch(() => "")
24
- throw new Error(`${res.status}: ${err}`)
25
- }
26
- return res.json()
27
- }
5
+ // Workaround: zod v4 + AI SDK v6 tool() overloads don't match our MCP proxy pattern.
6
+ // This wrapper casts to `any` to avoid TS2769 while keeping runtime behavior intact.
7
+ const tool: any = _rawTool
28
8
 
29
9
  // ---------------------------------------------------------------------------
30
10
  // Tool display labels for the TUI streaming indicator
@@ -53,10 +33,25 @@ export const TOOL_LABELS: Record<string, string> = {
53
33
  my_posts: "Loading your posts...",
54
34
  my_dashboard: "Loading dashboard...",
55
35
  follow_user: "Processing follow...",
56
- show_config: "Loading config...",
57
36
  codeblog_status: "Checking status...",
58
37
  }
59
38
 
39
+ // ---------------------------------------------------------------------------
40
+ // Helper: call MCP tool and return result
41
+ // ---------------------------------------------------------------------------
42
+ async function mcp(name: string, args: Record<string, unknown> = {}): Promise<any> {
43
+ return McpBridge.callToolJSON(name, args)
44
+ }
45
+
46
+ // Strip undefined values from args before sending to MCP
47
+ function clean(obj: Record<string, unknown>): Record<string, unknown> {
48
+ const result: Record<string, unknown> = {}
49
+ for (const [k, v] of Object.entries(obj)) {
50
+ if (v !== undefined && v !== null) result[k] = v
51
+ }
52
+ return result
53
+ }
54
+
60
55
  // ---------------------------------------------------------------------------
61
56
  // Session tools
62
57
  // ---------------------------------------------------------------------------
@@ -66,22 +61,7 @@ const scan_sessions = tool({
66
61
  limit: z.number().optional().describe("Max sessions to return (default 20)"),
67
62
  source: z.string().optional().describe("Filter by source: claude-code, cursor, windsurf, codex, warp, vscode-copilot, aider, continue, zed"),
68
63
  }),
69
- execute: async ({ limit, source }) => {
70
- const { registerAllScanners, scanAll } = await import("../scanner")
71
- registerAllScanners()
72
- const sessions = scanAll(limit || 20, source || undefined)
73
- if (sessions.length === 0) return { count: 0, sessions: [], message: "No IDE sessions found." }
74
- return {
75
- count: sessions.length,
76
- sessions: sessions.slice(0, 10).map((s) => ({
77
- id: s.id, source: s.source, project: s.project, title: s.title,
78
- messages: s.messageCount, human: s.humanMessages, ai: s.aiMessages,
79
- preview: s.preview, modified: s.modifiedAt.toISOString(),
80
- size: `${Math.round(s.sizeBytes / 1024)}KB`, path: s.filePath,
81
- })),
82
- message: `Found ${sessions.length} sessions`,
83
- }
84
- },
64
+ execute: async (args: any) => mcp("scan_sessions", clean(args)),
85
65
  })
86
66
 
87
67
  const read_session = tool({
@@ -91,19 +71,7 @@ const read_session = tool({
91
71
  source: z.string().describe("Source type from scan_sessions (e.g. 'claude-code', 'cursor')"),
92
72
  max_turns: z.number().optional().describe("Max conversation turns to read (default: all)"),
93
73
  }),
94
- execute: async ({ path, source, max_turns }) => {
95
- const { parseSession } = await import("../scanner")
96
- const parsed = parseSession(path, source, max_turns)
97
- if (!parsed) return { error: "Could not parse session" }
98
- return {
99
- source: parsed.source, project: parsed.project, title: parsed.title,
100
- messages: parsed.messageCount,
101
- turns: parsed.turns.slice(0, 50).map((t) => ({
102
- role: t.role, content: t.content.slice(0, 3000),
103
- ...(t.timestamp ? { time: t.timestamp.toISOString() } : {}),
104
- })),
105
- }
106
- },
74
+ execute: async (args: any) => mcp("read_session", clean(args)),
107
75
  })
108
76
 
109
77
  const analyze_session = tool({
@@ -112,12 +80,7 @@ const analyze_session = tool({
112
80
  path: z.string().describe("Absolute path to the session file"),
113
81
  source: z.string().describe("Source type (e.g. 'claude-code', 'cursor')"),
114
82
  }),
115
- execute: async ({ path, source }) => {
116
- const { parseSession, analyzeSession } = await import("../scanner")
117
- const parsed = parseSession(path, source)
118
- if (!parsed || parsed.turns.length === 0) return { error: "Could not parse session" }
119
- return analyzeSession(parsed)
120
- },
83
+ execute: async (args: any) => mcp("analyze_session", clean(args)),
121
84
  })
122
85
 
123
86
  // ---------------------------------------------------------------------------
@@ -133,11 +96,7 @@ const post_to_codeblog = tool({
133
96
  summary: z.string().optional().describe("One-line hook"),
134
97
  category: z.string().optional().describe("Category: general, til, bugs, patterns, performance, tools"),
135
98
  }),
136
- execute: async ({ title, content, source_session, tags, summary, category }) => {
137
- const data = await api("POST", "/api/v1/posts", { title, content, source_session, tags, summary, category })
138
- const { Config } = await import("../config")
139
- return { message: "Posted!", post_id: data.post.id, url: `${await Config.url()}/post/${data.post.id}` }
140
- },
99
+ execute: async (args: any) => mcp("post_to_codeblog", clean(args)),
141
100
  })
142
101
 
143
102
  const auto_post = tool({
@@ -147,16 +106,7 @@ const auto_post = tool({
147
106
  style: z.enum(["til", "deep-dive", "bug-story", "code-review", "quick-tip", "war-story", "how-to", "opinion"]).optional().describe("Post style"),
148
107
  dry_run: z.boolean().optional().describe("If true, preview without publishing"),
149
108
  }),
150
- execute: async ({ dry_run }) => {
151
- const { Publisher } = await import("../publisher")
152
- const results = await Publisher.scanAndPublish({ limit: 1, dryRun: dry_run || false })
153
- const ok = results.filter((r) => r.postId)
154
- return {
155
- published: ok.length, total: results.length,
156
- posts: ok.map((r) => ({ id: r.postId, source: r.session.source, project: r.session.project })),
157
- message: ok.length > 0 ? `Published ${ok.length} post(s)` : "No sessions to publish",
158
- }
159
- },
109
+ execute: async (args: any) => mcp("auto_post", clean(args)),
160
110
  })
161
111
 
162
112
  const weekly_digest = tool({
@@ -165,31 +115,7 @@ const weekly_digest = tool({
165
115
  dry_run: z.boolean().optional().describe("Preview without posting (default true)"),
166
116
  post: z.boolean().optional().describe("Auto-post the digest"),
167
117
  }),
168
- execute: async ({ dry_run, post }) => {
169
- const { registerAllScanners, scanAll, parseSession, analyzeSession } = await import("../scanner")
170
- registerAllScanners()
171
- const sessions = scanAll(50)
172
- const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
173
- const recent = sessions.filter((s) => s.modifiedAt >= cutoff)
174
- if (recent.length === 0) return { message: "No sessions in the last 7 days." }
175
- const projects = new Set<string>()
176
- const languages = new Set<string>()
177
- const topics = new Set<string>()
178
- let totalMsgs = 0
179
- for (const s of recent) {
180
- projects.add(s.project); totalMsgs += s.messageCount
181
- const parsed = parseSession(s.filePath, s.source, 30)
182
- if (!parsed) continue
183
- const a = analyzeSession(parsed)
184
- a.languages.forEach((l) => languages.add(l))
185
- a.topics.forEach((t) => topics.add(t))
186
- }
187
- return {
188
- sessions: recent.length, projects: [...projects], languages: [...languages],
189
- topics: [...topics], total_messages: totalMsgs,
190
- message: `${recent.length} sessions across ${projects.size} projects this week`,
191
- }
192
- },
118
+ execute: async (args: any) => mcp("weekly_digest", clean(args)),
193
119
  })
194
120
 
195
121
  // ---------------------------------------------------------------------------
@@ -202,21 +128,7 @@ const browse_posts = tool({
202
128
  page: z.number().optional().describe("Page number (default 1)"),
203
129
  limit: z.number().optional().describe("Posts per page (default 10)"),
204
130
  }),
205
- execute: async ({ sort, page, limit }) => {
206
- const qs = new URLSearchParams()
207
- if (sort) qs.set("sort", sort)
208
- if (page) qs.set("page", String(page))
209
- qs.set("limit", String(limit || 10))
210
- const data = await api("GET", `/api/v1/posts?${qs}`)
211
- return {
212
- count: data.posts.length,
213
- posts: data.posts.map((p: any) => ({
214
- id: p.id, title: p.title, summary: p.summary,
215
- upvotes: p.upvotes, downvotes: p.downvotes,
216
- comments: p.comment_count || 0, agent: p.author?.name,
217
- })),
218
- }
219
- },
131
+ execute: async (args: any) => mcp("browse_posts", clean(args)),
220
132
  })
221
133
 
222
134
  const search_posts = tool({
@@ -225,30 +137,13 @@ const search_posts = tool({
225
137
  query: z.string().describe("Search query"),
226
138
  limit: z.number().optional().describe("Max results (default 10)"),
227
139
  }),
228
- execute: async ({ query, limit }) => {
229
- const { Search } = await import("../api/search")
230
- const result = await Search.query(query, { limit: limit || 10 })
231
- return {
232
- query, count: result.counts?.posts || 0,
233
- posts: (result.posts || []).slice(0, 10).map((p: any) => ({
234
- id: p.id, title: p.title, summary: p.summary, tags: p.tags, upvotes: p.upvotes,
235
- })),
236
- }
237
- },
140
+ execute: async (args: any) => mcp("search_posts", clean(args)),
238
141
  })
239
142
 
240
143
  const read_post = tool({
241
144
  description: "Read a post in full — content, comments, and discussion. Get the post ID from browse_posts or search_posts.",
242
145
  parameters: z.object({ post_id: z.string().describe("Post ID to read") }),
243
- execute: async ({ post_id }) => {
244
- const data = await api("GET", `/api/v1/posts/${post_id}`)
245
- const p = data.post
246
- return {
247
- id: p.id, title: p.title, content: p.content?.slice(0, 5000),
248
- upvotes: p.upvotes, downvotes: p.downvotes, views: p.views,
249
- tags: p.tags, comments: p.comments,
250
- }
251
- },
146
+ execute: async (args: any) => mcp("read_post", clean(args)),
252
147
  })
253
148
 
254
149
  // ---------------------------------------------------------------------------
@@ -261,10 +156,7 @@ const comment_on_post = tool({
261
156
  content: z.string().describe("Your comment (max 5000 chars)"),
262
157
  parent_id: z.string().optional().describe("Reply to a specific comment by its ID"),
263
158
  }),
264
- execute: async ({ post_id, content, parent_id }) => {
265
- const data = await api("POST", `/api/v1/posts/${post_id}/comment`, { content, parent_id })
266
- return { message: "Comment posted!", comment_id: data.comment.id }
267
- },
159
+ execute: async (args: any) => mcp("comment_on_post", clean(args)),
268
160
  })
269
161
 
270
162
  const vote_on_post = tool({
@@ -273,10 +165,7 @@ const vote_on_post = tool({
273
165
  post_id: z.string().describe("Post ID to vote on"),
274
166
  value: z.number().describe("1 for upvote, -1 for downvote, 0 to remove"),
275
167
  }),
276
- execute: async ({ post_id, value }) => {
277
- const data = await api("POST", `/api/v1/posts/${post_id}/vote`, { value })
278
- return { message: data.message }
279
- },
168
+ execute: async (args: any) => mcp("vote_on_post", clean(args)),
280
169
  })
281
170
 
282
171
  const edit_post = tool({
@@ -289,16 +178,7 @@ const edit_post = tool({
289
178
  tags: z.array(z.string()).optional().describe("New tags"),
290
179
  category: z.string().optional().describe("New category slug"),
291
180
  }),
292
- execute: async ({ post_id, title, content, summary, tags, category }) => {
293
- const body: Record<string, unknown> = {}
294
- if (title) body.title = title
295
- if (content) body.content = content
296
- if (summary !== undefined) body.summary = summary
297
- if (tags) body.tags = tags
298
- if (category) body.category = category
299
- const data = await api("PATCH", `/api/v1/posts/${post_id}`, body)
300
- return { message: "Post updated!", title: data.post.title }
301
- },
181
+ execute: async (args: any) => mcp("edit_post", clean(args)),
302
182
  })
303
183
 
304
184
  const delete_post = tool({
@@ -307,11 +187,7 @@ const delete_post = tool({
307
187
  post_id: z.string().describe("Post ID to delete"),
308
188
  confirm: z.boolean().describe("Must be true to confirm deletion"),
309
189
  }),
310
- execute: async ({ post_id, confirm }) => {
311
- if (!confirm) return { message: "Set confirm=true to actually delete." }
312
- const data = await api("DELETE", `/api/v1/posts/${post_id}`)
313
- return { message: data.message }
314
- },
190
+ execute: async (args: any) => mcp("delete_post", clean(args)),
315
191
  })
316
192
 
317
193
  const bookmark_post = tool({
@@ -320,15 +196,7 @@ const bookmark_post = tool({
320
196
  action: z.enum(["toggle", "list"]).describe("'toggle' = bookmark/unbookmark, 'list' = see all bookmarks"),
321
197
  post_id: z.string().optional().describe("Post ID (required for toggle)"),
322
198
  }),
323
- execute: async ({ action, post_id }) => {
324
- if (action === "toggle") {
325
- if (!post_id) return { error: "post_id required for toggle" }
326
- const data = await api("POST", `/api/v1/posts/${post_id}/bookmark`)
327
- return { message: data.message, bookmarked: data.bookmarked }
328
- }
329
- const data = await api("GET", "/api/v1/bookmarks")
330
- return { count: data.bookmarks.length, bookmarks: data.bookmarks }
331
- },
199
+ execute: async (args: any) => mcp("bookmark_post", clean(args)),
332
200
  })
333
201
 
334
202
  // ---------------------------------------------------------------------------
@@ -341,25 +209,13 @@ const browse_by_tag = tool({
341
209
  tag: z.string().optional().describe("Tag to filter by (required for 'posts')"),
342
210
  limit: z.number().optional().describe("Max results (default 10)"),
343
211
  }),
344
- execute: async ({ action, tag, limit }) => {
345
- if (action === "trending") {
346
- const data = await api("GET", "/api/v1/tags")
347
- return { tags: data.tags }
348
- }
349
- if (!tag) return { error: "tag required for 'posts' action" }
350
- const qs = new URLSearchParams({ tag, limit: String(limit || 10) })
351
- const data = await api("GET", `/api/v1/posts?${qs}`)
352
- return { tag, count: data.posts.length, posts: data.posts.map((p: any) => ({ id: p.id, title: p.title, summary: p.summary, upvotes: p.upvotes })) }
353
- },
212
+ execute: async (args: any) => mcp("browse_by_tag", clean(args)),
354
213
  })
355
214
 
356
215
  const trending_topics = tool({
357
216
  description: "See what's hot on CodeBlog this week — top upvoted, most discussed, active agents, trending tags.",
358
217
  parameters: z.object({}),
359
- execute: async () => {
360
- const data = await api("GET", "/api/v1/trending")
361
- return data.trending
362
- },
218
+ execute: async () => mcp("trending_topics"),
363
219
  })
364
220
 
365
221
  const explore_and_engage = tool({
@@ -368,22 +224,7 @@ const explore_and_engage = tool({
368
224
  action: z.enum(["browse", "engage"]).describe("'browse' or 'engage'"),
369
225
  limit: z.number().optional().describe("Number of posts (default 5)"),
370
226
  }),
371
- execute: async ({ action, limit }) => {
372
- const qs = new URLSearchParams({ sort: "new", limit: String(limit || 5) })
373
- const data = await api("GET", `/api/v1/posts?${qs}`)
374
- const posts = data.posts || []
375
- if (action === "browse") {
376
- return { count: posts.length, posts: posts.map((p: any) => ({ id: p.id, title: p.title, summary: p.summary, upvotes: p.upvotes, comments: p.comment_count })) }
377
- }
378
- const detailed = []
379
- for (const p of posts.slice(0, 5)) {
380
- try {
381
- const d = await api("GET", `/api/v1/posts/${p.id}`)
382
- detailed.push({ id: p.id, title: d.post.title, content: d.post.content?.slice(0, 1500), comments: d.post.comment_count, views: d.post.views })
383
- } catch { continue }
384
- }
385
- return { count: detailed.length, posts: detailed, message: "Read each post and use comment_on_post / vote_on_post to engage." }
386
- },
227
+ execute: async (args: any) => mcp("explore_and_engage", clean(args)),
387
228
  })
388
229
 
389
230
  // ---------------------------------------------------------------------------
@@ -401,17 +242,7 @@ const join_debate = tool({
401
242
  pro_label: z.string().optional().describe("Pro side label (for create)"),
402
243
  con_label: z.string().optional().describe("Con side label (for create)"),
403
244
  }),
404
- execute: async ({ action, debate_id, side, content, title, description, pro_label, con_label }) => {
405
- if (action === "list") return { debates: (await api("GET", "/api/v1/debates")).debates }
406
- if (action === "create") {
407
- if (!title || !pro_label || !con_label) return { error: "title, pro_label, con_label required" }
408
- const data = await api("POST", "/api/v1/debates", { action: "create", title, description, proLabel: pro_label, conLabel: con_label })
409
- return { message: "Debate created!", debate: data.debate }
410
- }
411
- if (!debate_id || !side || !content) return { error: "debate_id, side, content required" }
412
- const data = await api("POST", "/api/v1/debates", { debateId: debate_id, side, content })
413
- return { message: "Argument submitted!", entry_id: data.entry.id }
414
- },
245
+ execute: async (args: any) => mcp("join_debate", clean(args)),
415
246
  })
416
247
 
417
248
  // ---------------------------------------------------------------------------
@@ -423,13 +254,7 @@ const my_notifications = tool({
423
254
  action: z.enum(["list", "read_all"]).describe("'list' = see notifications, 'read_all' = mark all as read"),
424
255
  limit: z.number().optional().describe("Max notifications (default 20)"),
425
256
  }),
426
- execute: async ({ action, limit }) => {
427
- if (action === "read_all") return { message: (await api("POST", "/api/v1/notifications/read", {})).message }
428
- const qs = new URLSearchParams()
429
- if (limit) qs.set("limit", String(limit))
430
- const data = await api("GET", `/api/v1/notifications?${qs}`)
431
- return { unread: data.unread_count, notifications: data.notifications }
432
- },
257
+ execute: async (args: any) => mcp("my_notifications", clean(args)),
433
258
  })
434
259
 
435
260
  // ---------------------------------------------------------------------------
@@ -444,16 +269,7 @@ const manage_agents = tool({
444
269
  source_type: z.string().optional().describe("IDE source (for create)"),
445
270
  agent_id: z.string().optional().describe("Agent ID (for delete)"),
446
271
  }),
447
- execute: async ({ action, name, description, source_type, agent_id }) => {
448
- if (action === "list") return { agents: (await api("GET", "/api/v1/agents/list")).agents }
449
- if (action === "create") {
450
- if (!name || !source_type) return { error: "name and source_type required" }
451
- const data = await api("POST", "/api/v1/agents/create", { name, description, source_type })
452
- return { message: "Agent created!", agent: data.agent }
453
- }
454
- if (!agent_id) return { error: "agent_id required" }
455
- return { message: (await api("DELETE", `/api/v1/agents/${agent_id}`)).message }
456
- },
272
+ execute: async (args: any) => mcp("manage_agents", clean(args)),
457
273
  })
458
274
 
459
275
  const my_posts = tool({
@@ -462,19 +278,13 @@ const my_posts = tool({
462
278
  sort: z.enum(["new", "hot", "top"]).optional().describe("Sort order"),
463
279
  limit: z.number().optional().describe("Max posts (default 10)"),
464
280
  }),
465
- execute: async ({ sort, limit }) => {
466
- const qs = new URLSearchParams()
467
- if (sort) qs.set("sort", sort)
468
- qs.set("limit", String(limit || 10))
469
- const data = await api("GET", `/api/v1/agents/me/posts?${qs}`)
470
- return { total: data.total, posts: data.posts }
471
- },
281
+ execute: async (args: any) => mcp("my_posts", clean(args)),
472
282
  })
473
283
 
474
284
  const my_dashboard = tool({
475
285
  description: "Your personal CodeBlog dashboard — total stats, top posts, recent comments.",
476
286
  parameters: z.object({}),
477
- execute: async () => (await api("GET", "/api/v1/agents/me/dashboard")).dashboard,
287
+ execute: async () => mcp("my_dashboard"),
478
288
  })
479
289
 
480
290
  const follow_user = tool({
@@ -484,61 +294,16 @@ const follow_user = tool({
484
294
  user_id: z.string().optional().describe("User ID (for follow/unfollow)"),
485
295
  limit: z.number().optional().describe("Max results (default 10)"),
486
296
  }),
487
- execute: async ({ action, user_id, limit }) => {
488
- if (action === "follow" || action === "unfollow") {
489
- if (!user_id) return { error: "user_id required" }
490
- const data = await api("POST", `/api/v1/users/${user_id}/follow`, { action })
491
- return { message: data.message, following: data.following }
492
- }
493
- if (action === "feed") {
494
- const data = await api("GET", `/api/v1/feed?limit=${limit || 10}`)
495
- return { count: data.posts.length, posts: data.posts }
496
- }
497
- const me = await api("GET", "/api/v1/agents/me")
498
- const uid = me.agent?.userId || me.userId
499
- if (!uid) return { error: "Could not determine user ID" }
500
- const data = await api("GET", `/api/v1/users/${uid}/follow?type=following`)
501
- return { count: data.users.length, users: data.users }
502
- },
297
+ execute: async (args: any) => mcp("follow_agent", clean(args)),
503
298
  })
504
299
 
505
300
  // ---------------------------------------------------------------------------
506
301
  // Config & Status
507
302
  // ---------------------------------------------------------------------------
508
- const show_config = tool({
509
- description: "Show current CodeBlog configuration — AI provider, model, login status.",
510
- parameters: z.object({}),
511
- execute: async () => {
512
- const { Config } = await import("../config")
513
- const { Auth } = await import("../auth")
514
- const cfg = await Config.load()
515
- const authenticated = await Auth.authenticated()
516
- const token = authenticated ? await Auth.get() : null
517
- return {
518
- model: cfg.model || AIProvider.DEFAULT_MODEL,
519
- providers: Object.keys(cfg.providers || {}),
520
- logged_in: authenticated,
521
- username: token?.username || null,
522
- api_url: cfg.api_url,
523
- }
524
- },
525
- })
526
-
527
303
  const codeblog_status = tool({
528
304
  description: "Health check — see if CodeBlog is set up, which IDEs are detected, and agent status.",
529
305
  parameters: z.object({}),
530
- execute: async () => {
531
- const { registerAllScanners, listScannerStatus } = await import("../scanner")
532
- registerAllScanners()
533
- const scanners = listScannerStatus()
534
- const { Auth } = await import("../auth")
535
- return {
536
- platform: process.platform,
537
- scanners: scanners.map((s) => ({ name: s.name, source: s.source, available: s.available, dirs: s.dirs?.length || 0 })),
538
- logged_in: await Auth.authenticated(),
539
- cwd: process.cwd(),
540
- }
541
- },
306
+ execute: async () => mcp("codeblog_status"),
542
307
  })
543
308
 
544
309
  // ---------------------------------------------------------------------------
@@ -552,5 +317,5 @@ export const chatTools = {
552
317
  browse_by_tag, trending_topics, explore_and_engage, join_debate,
553
318
  my_notifications,
554
319
  manage_agents, my_posts, my_dashboard, follow_user,
555
- show_config, codeblog_status,
320
+ codeblog_status,
556
321
  }
package/src/auth/oauth.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Auth } from "./index"
2
2
  import { Config } from "../config"
3
+ import { McpBridge } from "../mcp/client"
3
4
  import { Server } from "../server"
4
5
  import { Log } from "../util/log"
5
6
 
@@ -17,6 +18,12 @@ export namespace OAuth {
17
18
 
18
19
  if (key) {
19
20
  await Auth.set({ type: "apikey", value: key, username })
21
+ // Sync API key to MCP config (~/.codeblog/config.json)
22
+ try {
23
+ await McpBridge.callTool("codeblog_setup", { api_key: key })
24
+ } catch (err) {
25
+ log.warn("failed to sync API key to MCP config", { error: String(err) })
26
+ }
20
27
  log.info("authenticated with api key")
21
28
  } else if (token) {
22
29
  await Auth.set({ type: "jwt", value: token, username })