codeblog-app 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/package.json +8 -71
  2. package/drizzle/0000_init.sql +0 -34
  3. package/drizzle/meta/_journal.json +0 -13
  4. package/drizzle.config.ts +0 -10
  5. package/src/ai/__tests__/chat.test.ts +0 -110
  6. package/src/ai/__tests__/provider.test.ts +0 -184
  7. package/src/ai/__tests__/tools.test.ts +0 -90
  8. package/src/ai/chat.ts +0 -169
  9. package/src/ai/configure.ts +0 -134
  10. package/src/ai/provider.ts +0 -238
  11. package/src/ai/tools.ts +0 -336
  12. package/src/auth/index.ts +0 -47
  13. package/src/auth/oauth.ts +0 -94
  14. package/src/cli/__tests__/commands.test.ts +0 -225
  15. package/src/cli/cmd/agent.ts +0 -102
  16. package/src/cli/cmd/chat.ts +0 -190
  17. package/src/cli/cmd/comment.ts +0 -70
  18. package/src/cli/cmd/config.ts +0 -153
  19. package/src/cli/cmd/feed.ts +0 -57
  20. package/src/cli/cmd/forum.ts +0 -123
  21. package/src/cli/cmd/login.ts +0 -45
  22. package/src/cli/cmd/logout.ts +0 -12
  23. package/src/cli/cmd/me.ts +0 -202
  24. package/src/cli/cmd/post.ts +0 -29
  25. package/src/cli/cmd/publish.ts +0 -70
  26. package/src/cli/cmd/scan.ts +0 -80
  27. package/src/cli/cmd/search.ts +0 -40
  28. package/src/cli/cmd/setup.ts +0 -273
  29. package/src/cli/cmd/tui.ts +0 -20
  30. package/src/cli/cmd/update.ts +0 -78
  31. package/src/cli/cmd/vote.ts +0 -50
  32. package/src/cli/cmd/whoami.ts +0 -21
  33. package/src/cli/ui.ts +0 -195
  34. package/src/config/index.ts +0 -54
  35. package/src/flag/index.ts +0 -23
  36. package/src/global/index.ts +0 -38
  37. package/src/id/index.ts +0 -20
  38. package/src/index.ts +0 -197
  39. package/src/mcp/__tests__/client.test.ts +0 -149
  40. package/src/mcp/__tests__/e2e.ts +0 -327
  41. package/src/mcp/__tests__/integration.ts +0 -148
  42. package/src/mcp/client.ts +0 -148
  43. package/src/server/index.ts +0 -48
  44. package/src/storage/chat.ts +0 -92
  45. package/src/storage/db.ts +0 -85
  46. package/src/storage/schema.sql.ts +0 -39
  47. package/src/storage/schema.ts +0 -1
  48. package/src/tui/app.tsx +0 -163
  49. package/src/tui/commands.ts +0 -187
  50. package/src/tui/context/exit.tsx +0 -15
  51. package/src/tui/context/helper.tsx +0 -25
  52. package/src/tui/context/route.tsx +0 -24
  53. package/src/tui/context/theme.tsx +0 -470
  54. package/src/tui/routes/home.tsx +0 -508
  55. package/src/tui/routes/model.tsx +0 -209
  56. package/src/tui/routes/notifications.tsx +0 -85
  57. package/src/tui/routes/post.tsx +0 -108
  58. package/src/tui/routes/search.tsx +0 -104
  59. package/src/tui/routes/setup.tsx +0 -255
  60. package/src/tui/routes/trending.tsx +0 -107
  61. package/src/util/__tests__/context.test.ts +0 -31
  62. package/src/util/__tests__/lazy.test.ts +0 -37
  63. package/src/util/context.ts +0 -23
  64. package/src/util/error.ts +0 -46
  65. package/src/util/lazy.ts +0 -18
  66. package/src/util/log.ts +0 -142
  67. package/tsconfig.json +0 -11
@@ -1,327 +0,0 @@
1
- /**
2
- * Full E2E test: test ALL MCP tools as a real user would.
3
- * This script walks through the entire user journey:
4
- *
5
- * 1. Status check
6
- * 2. Scan IDE sessions
7
- * 3. Read a session
8
- * 4. Analyze a session
9
- * 5. Post to CodeBlog
10
- * 6. Browse posts
11
- * 7. Search posts
12
- * 8. Read a specific post
13
- * 9. Upvote a post
14
- * 10. Comment on a post
15
- * 11. Edit the post
16
- * 12. Bookmark the post
17
- * 13. Browse by tag
18
- * 14. Trending topics
19
- * 15. Explore and engage
20
- * 16. My posts
21
- * 17. My dashboard
22
- * 18. My notifications
23
- * 19. Manage agents
24
- * 20. Follow a user
25
- * 21. Join debate
26
- * 22. Weekly digest (dry run)
27
- * 23. Delete the test post
28
- * 24. Unbookmark
29
- *
30
- * Usage: bun run src/mcp/__tests__/e2e.ts
31
- */
32
-
33
- import { McpBridge } from "../client"
34
-
35
- let testPostId = ""
36
- let testCommentId = ""
37
- let passed = 0
38
- let failed = 0
39
-
40
- async function test(name: string, fn: () => Promise<void>) {
41
- try {
42
- await fn()
43
- console.log(` ✓ ${name}`)
44
- passed++
45
- } catch (err) {
46
- const msg = err instanceof Error ? err.message : String(err)
47
- console.log(` ✗ ${name}`)
48
- console.log(` Error: ${msg.slice(0, 200)}`)
49
- failed++
50
- }
51
- }
52
-
53
- function assert(condition: boolean, msg: string) {
54
- if (!condition) throw new Error(`Assertion failed: ${msg}`)
55
- }
56
-
57
- async function main() {
58
- console.log("=== CodeBlog E2E Test — Full User Journey ===\n")
59
-
60
- // 1. Status check
61
- await test("1. codeblog_status", async () => {
62
- const result = await McpBridge.callTool("codeblog_status")
63
- assert(result.includes("CodeBlog MCP Server"), "should include server info")
64
- assert(result.includes("Agent:"), "should include agent info (authenticated)")
65
- console.log(` → ${result.split("\n")[0]}`)
66
- })
67
-
68
- // 2. Scan IDE sessions
69
- let sessionPath = ""
70
- let sessionSource = ""
71
- await test("2. scan_sessions", async () => {
72
- const raw = await McpBridge.callTool("scan_sessions", { limit: 5 })
73
- const sessions = JSON.parse(raw)
74
- assert(Array.isArray(sessions), "should return array")
75
- assert(sessions.length > 0, "should have at least 1 session")
76
- sessionPath = sessions[0].path
77
- sessionSource = sessions[0].source
78
- console.log(` → Found ${sessions.length} sessions, first: [${sessionSource}] ${sessions[0].project}`)
79
- })
80
-
81
- // 3. Read a session
82
- await test("3. read_session", async () => {
83
- assert(sessionPath !== "", "need a session path from step 2")
84
- const raw = await McpBridge.callTool("read_session", {
85
- path: sessionPath,
86
- source: sessionSource,
87
- max_turns: 3,
88
- })
89
- assert(raw.length > 50, "should return session content")
90
- console.log(` → Got ${raw.length} chars of session content`)
91
- })
92
-
93
- // 4. Analyze a session
94
- await test("4. analyze_session", async () => {
95
- const raw = await McpBridge.callTool("analyze_session", {
96
- path: sessionPath,
97
- source: sessionSource,
98
- })
99
- assert(raw.length > 50, "should return analysis")
100
- console.log(` → Got ${raw.length} chars of analysis`)
101
- })
102
-
103
- // 5. Post to CodeBlog
104
- await test("5. post_to_codeblog", async () => {
105
- const raw = await McpBridge.callTool("post_to_codeblog", {
106
- title: "[E2E Test] MCP Integration Test Post",
107
- content: "This is an automated test post from the E2E test suite.\n\n## Test Content\n\n```typescript\nconsole.log('Hello from E2E test!')\n```\n\nThis post will be deleted after testing.",
108
- source_session: sessionPath,
109
- tags: ["e2e-test", "automated", "mcp"],
110
- summary: "Automated test post — will be deleted",
111
- category: "general",
112
- })
113
- // MCP returns text like "✅ Posted! View at: https://codeblog.ai/post/<id>"
114
- // or JSON. Handle both.
115
- try {
116
- const result = JSON.parse(raw)
117
- testPostId = result.id || result.post?.id || ""
118
- } catch {
119
- // Extract post ID from URL in text
120
- const urlMatch = raw.match(/\/post\/([a-z0-9]+)/)
121
- testPostId = urlMatch?.[1] || ""
122
- }
123
- assert(testPostId !== "", `should extract post ID from: ${raw.slice(0, 100)}`)
124
- console.log(` → Created post: ${testPostId}`)
125
- })
126
-
127
- // 6. Browse posts
128
- await test("6. browse_posts", async () => {
129
- const raw = await McpBridge.callTool("browse_posts", { sort: "new", limit: 5 })
130
- assert(raw.length > 10, "should return posts")
131
- const result = JSON.parse(raw)
132
- assert(result.posts || Array.isArray(result), "should be parseable")
133
- const posts = result.posts || result
134
- console.log(` → Got ${posts.length} posts`)
135
- })
136
-
137
- // 7. Search posts
138
- await test("7. search_posts", async () => {
139
- const raw = await McpBridge.callTool("search_posts", { query: "E2E Test", limit: 5 })
140
- assert(raw.length > 5, "should return results")
141
- console.log(` → Search returned ${raw.length} chars`)
142
- })
143
-
144
- // 8. Read the test post
145
- await test("8. read_post", async () => {
146
- assert(testPostId !== "", "need post ID from step 5")
147
- const raw = await McpBridge.callTool("read_post", { post_id: testPostId })
148
- assert(raw.length > 50, "should return post content")
149
- assert(raw.includes("E2E Test") || raw.includes("e2e"), "should contain test post content")
150
- console.log(` → Read post: ${raw.length} chars`)
151
- })
152
-
153
- // 9. Upvote the post
154
- await test("9. vote_on_post (upvote)", async () => {
155
- assert(testPostId !== "", "need post ID from step 5")
156
- const raw = await McpBridge.callTool("vote_on_post", { post_id: testPostId, value: 1 })
157
- console.log(` → ${raw.slice(0, 100)}`)
158
- })
159
-
160
- // 10. Comment on the post
161
- await test("10. comment_on_post", async () => {
162
- assert(testPostId !== "", "need post ID from step 5")
163
- const raw = await McpBridge.callTool("comment_on_post", {
164
- post_id: testPostId,
165
- content: "This is an automated E2E test comment. Testing the comment system!",
166
- })
167
- // Extract comment ID from text or JSON
168
- try {
169
- const result = JSON.parse(raw)
170
- testCommentId = result.id || result.comment?.id || ""
171
- } catch {
172
- const idMatch = raw.match(/Comment ID:\s*([a-z0-9]+)/)
173
- testCommentId = idMatch?.[1] || ""
174
- }
175
- console.log(` → ${raw.split("\n")[0].slice(0, 80)}`)
176
- })
177
-
178
- // 11. Edit the post
179
- await test("11. edit_post", async () => {
180
- assert(testPostId !== "", "need post ID from step 5")
181
- const raw = await McpBridge.callTool("edit_post", {
182
- post_id: testPostId,
183
- title: "[E2E Test] MCP Integration Test Post (Edited)",
184
- summary: "Automated test post — EDITED — will be deleted",
185
- })
186
- console.log(` → ${raw.slice(0, 100)}`)
187
- })
188
-
189
- // 12. Bookmark the post
190
- await test("12. bookmark_post (toggle)", async () => {
191
- assert(testPostId !== "", "need post ID from step 5")
192
- const raw = await McpBridge.callTool("bookmark_post", {
193
- action: "toggle",
194
- post_id: testPostId,
195
- })
196
- console.log(` → ${raw.slice(0, 100)}`)
197
- })
198
-
199
- // 13. List bookmarks
200
- await test("13. bookmark_post (list)", async () => {
201
- const raw = await McpBridge.callTool("bookmark_post", { action: "list" })
202
- assert(raw.length > 0, "should return bookmarks")
203
- console.log(` → ${raw.slice(0, 100)}`)
204
- })
205
-
206
- // 14. Browse by tag
207
- await test("14. browse_by_tag (trending)", async () => {
208
- const raw = await McpBridge.callTool("browse_by_tag", { action: "trending", limit: 5 })
209
- assert(raw.length > 0, "should return trending tags")
210
- console.log(` → ${raw.slice(0, 100)}`)
211
- })
212
-
213
- await test("15. browse_by_tag (posts)", async () => {
214
- const raw = await McpBridge.callTool("browse_by_tag", { action: "posts", tag: "e2e-test", limit: 5 })
215
- console.log(` → ${raw.slice(0, 100)}`)
216
- })
217
-
218
- // 16. Trending topics
219
- await test("16. trending_topics", async () => {
220
- const raw = await McpBridge.callTool("trending_topics")
221
- assert(raw.includes("Trending"), "should include trending info")
222
- console.log(` → ${raw.split("\n")[0]}`)
223
- })
224
-
225
- // 17. Explore and engage
226
- await test("17. explore_and_engage (browse)", async () => {
227
- const raw = await McpBridge.callTool("explore_and_engage", { action: "browse", limit: 3 })
228
- assert(raw.length > 0, "should return content")
229
- console.log(` → ${raw.split("\n")[0]}`)
230
- })
231
-
232
- // 18. My posts
233
- await test("18. my_posts", async () => {
234
- const raw = await McpBridge.callTool("my_posts", { limit: 5 })
235
- assert(raw.length > 0, "should return my posts")
236
- console.log(` → ${raw.slice(0, 100)}`)
237
- })
238
-
239
- // 19. My dashboard
240
- await test("19. my_dashboard", async () => {
241
- const raw = await McpBridge.callTool("my_dashboard")
242
- assert(raw.length > 0, "should return dashboard data")
243
- console.log(` → ${raw.slice(0, 100)}`)
244
- })
245
-
246
- // 20. My notifications
247
- await test("20. my_notifications (list)", async () => {
248
- const raw = await McpBridge.callTool("my_notifications", { action: "list", limit: 5 })
249
- console.log(` → ${raw.slice(0, 100)}`)
250
- })
251
-
252
- // 21. Manage agents
253
- await test("21. manage_agents (list)", async () => {
254
- const raw = await McpBridge.callTool("manage_agents", { action: "list" })
255
- assert(raw.length > 0, "should return agents")
256
- console.log(` → ${raw.slice(0, 100)}`)
257
- })
258
-
259
- // 22. Follow agent / user
260
- await test("22. follow_agent (list_following)", async () => {
261
- const raw = await McpBridge.callTool("follow_agent", { action: "list_following", limit: 5 })
262
- console.log(` → ${raw.slice(0, 100)}`)
263
- })
264
-
265
- // 23. Join debate
266
- await test("23. join_debate (list)", async () => {
267
- const raw = await McpBridge.callTool("join_debate", { action: "list" })
268
- console.log(` → ${raw.slice(0, 80)}`)
269
- })
270
-
271
- // 24. Weekly digest (dry run)
272
- await test("24. weekly_digest (dry_run)", async () => {
273
- const raw = await McpBridge.callTool("weekly_digest", { dry_run: true })
274
- assert(raw.length > 0, "should return digest preview")
275
- console.log(` → ${raw.split("\n")[0]}`)
276
- })
277
-
278
- // 25. Auto post (dry run)
279
- await test("25. auto_post (dry_run)", async () => {
280
- const raw = await McpBridge.callTool("auto_post", { dry_run: true })
281
- assert(raw.length > 0, "should return post preview")
282
- console.log(` → ${raw.split("\n")[0].slice(0, 100)}`)
283
- })
284
-
285
- // 26. Remove vote
286
- await test("26. vote_on_post (remove)", async () => {
287
- assert(testPostId !== "", "need post ID")
288
- const raw = await McpBridge.callTool("vote_on_post", { post_id: testPostId, value: 0 })
289
- console.log(` → ${raw.slice(0, 100)}`)
290
- })
291
-
292
- // 27. Unbookmark
293
- await test("27. bookmark_post (unbookmark)", async () => {
294
- assert(testPostId !== "", "need post ID")
295
- const raw = await McpBridge.callTool("bookmark_post", {
296
- action: "toggle",
297
- post_id: testPostId,
298
- })
299
- console.log(` → ${raw.slice(0, 100)}`)
300
- })
301
-
302
- // 28. Delete the test post (cleanup)
303
- await test("28. delete_post (cleanup)", async () => {
304
- assert(testPostId !== "", "need post ID to delete")
305
- const raw = await McpBridge.callTool("delete_post", {
306
- post_id: testPostId,
307
- confirm: true,
308
- })
309
- console.log(` → ${raw.slice(0, 100)}`)
310
- })
311
-
312
- // Disconnect
313
- await McpBridge.disconnect()
314
-
315
- console.log("\n=== Summary ===")
316
- console.log(`Passed: ${passed}/${passed + failed}`)
317
- console.log(`Failed: ${failed}/${passed + failed}`)
318
-
319
- if (failed > 0) {
320
- process.exit(1)
321
- }
322
- }
323
-
324
- main().catch((err) => {
325
- console.error("Fatal:", err)
326
- process.exit(1)
327
- })
@@ -1,148 +0,0 @@
1
- /**
2
- * Integration test: verify all 26 MCP tools are accessible via McpBridge.
3
- *
4
- * This script:
5
- * 1. Connects to the MCP server (spawns codeblog-mcp subprocess)
6
- * 2. Lists all available tools
7
- * 3. Tests calling each tool that can be safely invoked without side effects
8
- * 4. Reports results
9
- *
10
- * Usage: bun run src/mcp/__tests__/integration.ts
11
- */
12
-
13
- import { McpBridge } from "../client"
14
-
15
- const EXPECTED_TOOLS = [
16
- "scan_sessions",
17
- "read_session",
18
- "analyze_session",
19
- "post_to_codeblog",
20
- "auto_post",
21
- "weekly_digest",
22
- "browse_posts",
23
- "search_posts",
24
- "read_post",
25
- "comment_on_post",
26
- "vote_on_post",
27
- "edit_post",
28
- "delete_post",
29
- "bookmark_post",
30
- "browse_by_tag",
31
- "trending_topics",
32
- "explore_and_engage",
33
- "join_debate",
34
- "my_notifications",
35
- "manage_agents",
36
- "my_posts",
37
- "my_dashboard",
38
- "follow_agent",
39
- "codeblog_status",
40
- "codeblog_setup",
41
- ]
42
-
43
- // Tools that are safe to call without side effects (read-only)
44
- const SAFE_TOOLS: Record<string, Record<string, unknown>> = {
45
- codeblog_status: {},
46
- scan_sessions: { limit: 3 },
47
- browse_posts: { sort: "new", limit: 2 },
48
- search_posts: { query: "test", limit: 2 },
49
- browse_by_tag: { action: "trending", limit: 3 },
50
- trending_topics: {},
51
- explore_and_engage: { action: "browse", limit: 2 },
52
- join_debate: { action: "list" },
53
- my_notifications: { action: "list", limit: 2 },
54
- manage_agents: { action: "list" },
55
- my_posts: { limit: 2 },
56
- my_dashboard: {},
57
- follow_agent: { action: "list_following", limit: 2 },
58
- bookmark_post: { action: "list" },
59
- }
60
-
61
- async function main() {
62
- console.log("=== MCP Integration Test ===\n")
63
-
64
- // Step 1: List tools
65
- console.log("1. Listing MCP tools...")
66
- let tools: Array<{ name: string; description?: string }>
67
- try {
68
- const result = await McpBridge.listTools()
69
- tools = result.tools
70
- console.log(` ✓ Found ${tools.length} tools\n`)
71
- } catch (err) {
72
- console.error(` ✗ Failed to list tools: ${err instanceof Error ? err.message : err}`)
73
- await McpBridge.disconnect()
74
- process.exit(1)
75
- return
76
- }
77
-
78
- // Step 2: Check expected tools
79
- console.log("2. Checking expected tools...")
80
- const toolNames = tools.map((t) => t.name)
81
- let missing = 0
82
- for (const expected of EXPECTED_TOOLS) {
83
- if (toolNames.includes(expected)) {
84
- console.log(` ✓ ${expected}`)
85
- } else {
86
- console.log(` ✗ MISSING: ${expected}`)
87
- missing++
88
- }
89
- }
90
-
91
- const extra = toolNames.filter((t) => !EXPECTED_TOOLS.includes(t))
92
- if (extra.length > 0) {
93
- console.log(`\n Extra tools not in expected list: ${extra.join(", ")}`)
94
- }
95
- console.log(`\n Expected: ${EXPECTED_TOOLS.length}, Found: ${toolNames.length}, Missing: ${missing}\n`)
96
-
97
- // Step 3: Call safe tools
98
- console.log("3. Testing safe tool calls...")
99
- let passed = 0
100
- let failed = 0
101
-
102
- for (const [name, args] of Object.entries(SAFE_TOOLS)) {
103
- if (!toolNames.includes(name)) {
104
- console.log(` ⊘ ${name} — skipped (not available)`)
105
- continue
106
- }
107
-
108
- try {
109
- const result = await McpBridge.callTool(name, args)
110
- const preview = result.slice(0, 80).replace(/\n/g, " ")
111
- console.log(` ✓ ${name} — ${preview}${result.length > 80 ? "..." : ""}`)
112
- passed++
113
- } catch (err) {
114
- const msg = err instanceof Error ? err.message : String(err)
115
- // Auth errors are expected in test environment
116
- if (msg.includes("auth") || msg.includes("API key") || msg.includes("token") || msg.includes("401") || msg.includes("unauthorized") || msg.includes("Unauthorized")) {
117
- console.log(` ⊘ ${name} — auth required (expected in test env)`)
118
- passed++ // Count as pass — tool is reachable, just needs auth
119
- } else {
120
- console.log(` ✗ ${name} — ${msg}`)
121
- failed++
122
- }
123
- }
124
- }
125
-
126
- console.log(`\n Passed: ${passed}, Failed: ${failed}\n`)
127
-
128
- // Cleanup
129
- console.log("4. Disconnecting...")
130
- await McpBridge.disconnect()
131
- console.log(" ✓ Disconnected\n")
132
-
133
- console.log("=== Summary ===")
134
- console.log(`Tools found: ${toolNames.length}`)
135
- console.log(`Tools tested: ${passed + failed}/${Object.keys(SAFE_TOOLS).length}`)
136
- console.log(`Tests passed: ${passed}`)
137
- console.log(`Tests failed: ${failed}`)
138
- console.log(`Missing expected tools: ${missing}`)
139
-
140
- if (failed > 0 || missing > 0) {
141
- process.exit(1)
142
- }
143
- }
144
-
145
- main().catch((err) => {
146
- console.error("Fatal error:", err)
147
- process.exit(1)
148
- })
package/src/mcp/client.ts DELETED
@@ -1,148 +0,0 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js"
2
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
3
- import { resolve, dirname } from "path"
4
- import { Log } from "../util/log"
5
-
6
- const log = Log.create({ service: "mcp" })
7
-
8
- const CONNECTION_TIMEOUT_MS = 30_000
9
-
10
- let client: Client | null = null
11
- let transport: StdioClientTransport | null = null
12
- let connecting: Promise<Client> | null = null
13
-
14
- function getMcpBinaryPath(): string {
15
- try {
16
- const resolved = require.resolve("codeblog-mcp/dist/index.js")
17
- return resolved
18
- } catch {
19
- return resolve(dirname(new URL(import.meta.url).pathname), "../../node_modules/codeblog-mcp/dist/index.js")
20
- }
21
- }
22
-
23
- function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
24
- return new Promise<T>((resolve, reject) => {
25
- const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
26
- promise.then(
27
- (v) => { clearTimeout(timer); resolve(v) },
28
- (e) => { clearTimeout(timer); reject(e) },
29
- )
30
- })
31
- }
32
-
33
- async function connect(): Promise<Client> {
34
- if (client) return client
35
-
36
- // If another caller is already connecting, reuse that promise
37
- if (connecting) return connecting
38
-
39
- connecting = (async (): Promise<Client> => {
40
- const mcpPath = getMcpBinaryPath()
41
- log.debug("connecting", { path: mcpPath })
42
-
43
- const env: Record<string, string> = {}
44
- for (const [k, v] of Object.entries(process.env)) {
45
- if (v !== undefined) env[k] = v
46
- }
47
-
48
- const t = new StdioClientTransport({
49
- command: "node",
50
- args: [mcpPath],
51
- env,
52
- stderr: "pipe",
53
- })
54
-
55
- const c = new Client({ name: "codeblog-cli", version: "2.0.0" })
56
-
57
- try {
58
- await withTimeout(c.connect(t), CONNECTION_TIMEOUT_MS, "MCP connection")
59
- } catch (err) {
60
- // Clean up on failure so next call can retry
61
- await t.close().catch(() => {})
62
- throw err
63
- }
64
-
65
- log.debug("connected", {
66
- server: c.getServerVersion()?.name,
67
- version: c.getServerVersion()?.version,
68
- })
69
-
70
- // Only assign to module-level vars after successful connection
71
- transport = t
72
- client = c
73
- return c
74
- })()
75
-
76
- try {
77
- return await connecting
78
- } catch (err) {
79
- // Reset connecting so next call can retry
80
- connecting = null
81
- throw err
82
- }
83
- }
84
-
85
- export namespace McpBridge {
86
- /**
87
- * Call an MCP tool by name with arguments.
88
- * Returns the text content from the tool result.
89
- */
90
- export async function callTool(
91
- name: string,
92
- args: Record<string, unknown> = {},
93
- ): Promise<string> {
94
- const c = await connect()
95
- const result = await c.callTool({ name, arguments: args })
96
-
97
- if (result.isError) {
98
- const text = extractText(result)
99
- throw new Error(text || `MCP tool "${name}" returned an error`)
100
- }
101
-
102
- return extractText(result)
103
- }
104
-
105
- /**
106
- * Call an MCP tool and parse the result as JSON.
107
- */
108
- export async function callToolJSON<T = unknown>(
109
- name: string,
110
- args: Record<string, unknown> = {},
111
- ): Promise<T> {
112
- const text = await callTool(name, args)
113
- try {
114
- return JSON.parse(text) as T
115
- } catch {
116
- return text as unknown as T
117
- }
118
- }
119
-
120
- /**
121
- * List all available MCP tools.
122
- */
123
- export async function listTools() {
124
- const c = await connect()
125
- return c.listTools()
126
- }
127
-
128
- /**
129
- * Disconnect the MCP client and kill the subprocess.
130
- */
131
- export async function disconnect(): Promise<void> {
132
- connecting = null
133
- if (transport) {
134
- await transport.close().catch(() => {})
135
- transport = null
136
- }
137
- client = null
138
- }
139
- }
140
-
141
- function extractText(result: unknown): string {
142
- const r = result as { content?: Array<{ type: string; text?: string }> }
143
- if (!r.content || !Array.isArray(r.content)) return ""
144
- return r.content
145
- .filter((c) => c.type === "text" && c.text)
146
- .map((c) => c.text!)
147
- .join("\n")
148
- }
@@ -1,48 +0,0 @@
1
- import { Hono } from "hono"
2
- import { Log } from "../util/log"
3
-
4
- const log = Log.create({ service: "server" })
5
-
6
- export namespace Server {
7
- let instance: ReturnType<typeof Bun.serve> | null = null
8
-
9
- export function start(app: Hono, port: number): ReturnType<typeof Bun.serve> {
10
- if (instance) {
11
- log.warn("server already running, stopping previous instance")
12
- instance.stop()
13
- }
14
- instance = Bun.serve({ port, fetch: app.fetch })
15
- log.info("server started", { port })
16
- return instance
17
- }
18
-
19
- export function stop() {
20
- if (instance) {
21
- instance.stop()
22
- instance = null
23
- log.info("server stopped")
24
- }
25
- }
26
-
27
- export function running(): boolean {
28
- return instance !== null
29
- }
30
-
31
- export function createCallbackServer(onCallback: (params: URLSearchParams) => Promise<string>): {
32
- app: Hono
33
- port: number
34
- } {
35
- const port = 19823
36
- const app = new Hono()
37
-
38
- app.get("/callback", async (c) => {
39
- const params = new URL(c.req.url).searchParams
40
- const html = await onCallback(params)
41
- return c.html(html)
42
- })
43
-
44
- app.get("/health", (c) => c.json({ ok: true }))
45
-
46
- return { app, port }
47
- }
48
- }