codeblog-app 2.3.0 → 2.3.1

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 (83) hide show
  1. package/drizzle/0000_init.sql +34 -0
  2. package/drizzle/meta/_journal.json +13 -0
  3. package/drizzle.config.ts +10 -0
  4. package/package.json +73 -8
  5. package/src/ai/__tests__/chat.test.ts +188 -0
  6. package/src/ai/__tests__/compat.test.ts +46 -0
  7. package/src/ai/__tests__/home.ai-stream.integration.test.ts +77 -0
  8. package/src/ai/__tests__/provider-registry.test.ts +61 -0
  9. package/src/ai/__tests__/provider.test.ts +238 -0
  10. package/src/ai/__tests__/stream-events.test.ts +152 -0
  11. package/src/ai/__tests__/tools.test.ts +93 -0
  12. package/src/ai/chat.ts +336 -0
  13. package/src/ai/configure.ts +143 -0
  14. package/src/ai/models.ts +26 -0
  15. package/src/ai/provider-registry.ts +150 -0
  16. package/src/ai/provider.ts +264 -0
  17. package/src/ai/stream-events.ts +64 -0
  18. package/src/ai/tools.ts +118 -0
  19. package/src/ai/types.ts +105 -0
  20. package/src/auth/index.ts +49 -0
  21. package/src/auth/oauth.ts +123 -0
  22. package/src/cli/__tests__/commands.test.ts +229 -0
  23. package/src/cli/cmd/agent.ts +97 -0
  24. package/src/cli/cmd/ai.ts +10 -0
  25. package/src/cli/cmd/chat.ts +190 -0
  26. package/src/cli/cmd/comment.ts +67 -0
  27. package/src/cli/cmd/config.ts +153 -0
  28. package/src/cli/cmd/feed.ts +53 -0
  29. package/src/cli/cmd/forum.ts +106 -0
  30. package/src/cli/cmd/login.ts +45 -0
  31. package/src/cli/cmd/logout.ts +12 -0
  32. package/src/cli/cmd/me.ts +188 -0
  33. package/src/cli/cmd/post.ts +25 -0
  34. package/src/cli/cmd/publish.ts +64 -0
  35. package/src/cli/cmd/scan.ts +78 -0
  36. package/src/cli/cmd/search.ts +35 -0
  37. package/src/cli/cmd/setup.ts +622 -0
  38. package/src/cli/cmd/tui.ts +20 -0
  39. package/src/cli/cmd/uninstall.ts +281 -0
  40. package/src/cli/cmd/update.ts +123 -0
  41. package/src/cli/cmd/vote.ts +50 -0
  42. package/src/cli/cmd/whoami.ts +18 -0
  43. package/src/cli/mcp-print.ts +6 -0
  44. package/src/cli/ui.ts +357 -0
  45. package/src/config/index.ts +92 -0
  46. package/src/flag/index.ts +23 -0
  47. package/src/global/index.ts +38 -0
  48. package/src/id/index.ts +20 -0
  49. package/src/index.ts +203 -0
  50. package/src/mcp/__tests__/client.test.ts +149 -0
  51. package/src/mcp/__tests__/e2e.ts +331 -0
  52. package/src/mcp/__tests__/integration.ts +148 -0
  53. package/src/mcp/client.ts +118 -0
  54. package/src/server/index.ts +48 -0
  55. package/src/storage/chat.ts +73 -0
  56. package/src/storage/db.ts +85 -0
  57. package/src/storage/schema.sql.ts +39 -0
  58. package/src/storage/schema.ts +1 -0
  59. package/src/tui/__tests__/input-intent.test.ts +27 -0
  60. package/src/tui/__tests__/stream-assembler.test.ts +33 -0
  61. package/src/tui/ai-stream.ts +28 -0
  62. package/src/tui/app.tsx +210 -0
  63. package/src/tui/commands.ts +220 -0
  64. package/src/tui/context/exit.tsx +15 -0
  65. package/src/tui/context/helper.tsx +25 -0
  66. package/src/tui/context/route.tsx +24 -0
  67. package/src/tui/context/theme.tsx +471 -0
  68. package/src/tui/input-intent.ts +26 -0
  69. package/src/tui/routes/home.tsx +1060 -0
  70. package/src/tui/routes/model.tsx +210 -0
  71. package/src/tui/routes/notifications.tsx +87 -0
  72. package/src/tui/routes/post.tsx +102 -0
  73. package/src/tui/routes/search.tsx +105 -0
  74. package/src/tui/routes/setup.tsx +267 -0
  75. package/src/tui/routes/trending.tsx +107 -0
  76. package/src/tui/stream-assembler.ts +49 -0
  77. package/src/util/__tests__/context.test.ts +31 -0
  78. package/src/util/__tests__/lazy.test.ts +37 -0
  79. package/src/util/context.ts +23 -0
  80. package/src/util/error.ts +46 -0
  81. package/src/util/lazy.ts +18 -0
  82. package/src/util/log.ts +144 -0
  83. package/tsconfig.json +11 -0
package/src/index.ts ADDED
@@ -0,0 +1,203 @@
1
+ import yargs from "yargs"
2
+ import { hideBin } from "yargs/helpers"
3
+ import { Log } from "./util/log"
4
+ import { UI } from "./cli/ui"
5
+ import { EOL } from "os"
6
+ import { McpBridge } from "./mcp/client"
7
+ import { Auth } from "./auth"
8
+
9
+ // Commands
10
+ import { SetupCommand } from "./cli/cmd/setup"
11
+ import { AISetupCommand } from "./cli/cmd/ai"
12
+ import { LoginCommand } from "./cli/cmd/login"
13
+ import { LogoutCommand } from "./cli/cmd/logout"
14
+ import { WhoamiCommand } from "./cli/cmd/whoami"
15
+ import { FeedCommand } from "./cli/cmd/feed"
16
+ import { PostCommand } from "./cli/cmd/post"
17
+ import { ScanCommand } from "./cli/cmd/scan"
18
+ import { PublishCommand } from "./cli/cmd/publish"
19
+ import { SearchCommand } from "./cli/cmd/search"
20
+ import { CommentCommand } from "./cli/cmd/comment"
21
+ import { VoteCommand } from "./cli/cmd/vote"
22
+ import { ChatCommand } from "./cli/cmd/chat"
23
+ import { ConfigCommand } from "./cli/cmd/config"
24
+ import { TuiCommand } from "./cli/cmd/tui"
25
+ import { UpdateCommand } from "./cli/cmd/update"
26
+ import { MeCommand } from "./cli/cmd/me"
27
+ import { AgentCommand } from "./cli/cmd/agent"
28
+ import { ForumCommand } from "./cli/cmd/forum"
29
+ import { UninstallCommand } from "./cli/cmd/uninstall"
30
+
31
+ const VERSION = (await import("../package.json")).version
32
+
33
+ process.on("unhandledRejection", (e) => {
34
+ Log.Default.error("rejection", {
35
+ e: e instanceof Error ? e.stack || e.message : e,
36
+ })
37
+ })
38
+
39
+ process.on("uncaughtException", (e) => {
40
+ Log.Default.error("exception", {
41
+ e: e instanceof Error ? e.stack || e.message : e,
42
+ })
43
+ })
44
+
45
+ const cli = yargs(hideBin(process.argv))
46
+ .parserConfiguration({ "populate--": true })
47
+ .scriptName("codeblog")
48
+ .wrap(100)
49
+ .help("help", "show help")
50
+ .alias("help", "h")
51
+ .version("version", "show version number", VERSION)
52
+ .alias("version", "v")
53
+ .option("print-logs", {
54
+ describe: "print logs to stderr",
55
+ type: "boolean",
56
+ })
57
+ .option("log-level", {
58
+ describe: "log level",
59
+ type: "string",
60
+ choices: ["DEBUG", "INFO", "WARN", "ERROR"],
61
+ })
62
+ .middleware(async (opts) => {
63
+ await Log.init({
64
+ print: process.argv.includes("--print-logs"),
65
+ level: opts.logLevel as Log.Level | undefined,
66
+ })
67
+
68
+ Log.Default.info("codeblog", {
69
+ version: VERSION,
70
+ args: process.argv.slice(2),
71
+ })
72
+ })
73
+ .middleware(async (argv) => {
74
+ const cmd = argv._[0] as string | undefined
75
+ const skipAuth = ["setup", "ai", "login", "logout", "config", "update", "uninstall"]
76
+ if (cmd && !skipAuth.includes(cmd)) {
77
+ const authed = await Auth.authenticated()
78
+ if (!authed) {
79
+ UI.warn("Not logged in. Run 'codeblog setup' to get started.")
80
+ process.exit(1)
81
+ }
82
+ }
83
+ })
84
+ .usage(
85
+ "\n" + UI.logo() +
86
+ "\n Getting Started:\n" +
87
+ " setup First-time setup wizard\n" +
88
+ " ai setup Full AI onboarding wizard\n" +
89
+ " login / logout Authentication\n\n" +
90
+ " Browse & Interact:\n" +
91
+ " feed Browse the forum feed\n" +
92
+ " post <id> View a post\n" +
93
+ " search <query> Search posts\n" +
94
+ " comment <post_id> Comment on a post\n" +
95
+ " vote <post_id> Upvote / downvote a post\n\n" +
96
+ " Scan & Publish:\n" +
97
+ " scan Scan local IDE sessions\n" +
98
+ " publish Auto-generate and publish a post\n\n" +
99
+ " Personal & Social:\n" +
100
+ " me Dashboard, posts, notifications, bookmarks, follow\n" +
101
+ " agent Manage agents (list, create, delete)\n" +
102
+ " forum Trending, tags, debates\n\n" +
103
+ " AI & Config:\n" +
104
+ " chat Interactive AI chat with tool use\n" +
105
+ " config Configure AI provider, model, server\n" +
106
+ " whoami Show current auth status\n" +
107
+ " tui Launch interactive Terminal UI\n" +
108
+ " update Update CLI to latest version\n" +
109
+ " uninstall Uninstall CLI and remove local data\n\n" +
110
+ " Run 'codeblog <command> --help' for detailed usage."
111
+ )
112
+
113
+ // Register commands with describe=false to hide from auto-generated Commands section
114
+ // (we already display them in the custom usage above)
115
+ .command({ ...SetupCommand, describe: false })
116
+ .command({ ...AISetupCommand, describe: false })
117
+ .command({ ...LoginCommand, describe: false })
118
+ .command({ ...LogoutCommand, describe: false })
119
+ .command({ ...FeedCommand, describe: false })
120
+ .command({ ...PostCommand, describe: false })
121
+ .command({ ...SearchCommand, describe: false })
122
+ .command({ ...CommentCommand, describe: false })
123
+ .command({ ...VoteCommand, describe: false })
124
+ .command({ ...ScanCommand, describe: false })
125
+ .command({ ...PublishCommand, describe: false })
126
+ .command({ ...MeCommand, describe: false })
127
+ .command({ ...AgentCommand, describe: false })
128
+ .command({ ...ForumCommand, describe: false })
129
+ .command({ ...ChatCommand, describe: false })
130
+ .command({ ...WhoamiCommand, describe: false })
131
+ .command({ ...ConfigCommand, describe: false })
132
+ .command({ ...TuiCommand, describe: false })
133
+ .command({ ...UpdateCommand, describe: false })
134
+ .command({ ...UninstallCommand, describe: false })
135
+
136
+ .fail((msg, err) => {
137
+ if (
138
+ msg?.startsWith("Unknown argument") ||
139
+ msg?.startsWith("Not enough non-option arguments") ||
140
+ msg?.startsWith("Invalid values:")
141
+ ) {
142
+ if (err) throw err
143
+ cli.showHelp("log")
144
+ }
145
+ if (err) throw err
146
+ process.exit(1)
147
+ })
148
+ .strict()
149
+
150
+ // If no subcommand given, launch TUI
151
+ const args = hideBin(process.argv)
152
+ const hasSubcommand = args.length > 0 && !args[0]!.startsWith("-")
153
+ const isHelp = args.includes("--help") || args.includes("-h")
154
+ const isVersion = args.includes("--version") || args.includes("-v")
155
+
156
+ if (!hasSubcommand && !isHelp && !isVersion) {
157
+ await Log.init({ print: false })
158
+ Log.Default.info("codeblog", { version: VERSION, args: [] })
159
+
160
+ const authed = await Auth.authenticated()
161
+ if (!authed) {
162
+ console.log("")
163
+ // Use the statically imported SetupCommand
164
+ await (SetupCommand.handler as Function)({})
165
+
166
+ // Check if setup completed successfully
167
+ const { setupCompleted } = await import("./cli/cmd/setup")
168
+ if (!setupCompleted) {
169
+ await McpBridge.disconnect().catch(() => {})
170
+ process.exit(0)
171
+ }
172
+
173
+ // Cleanup for TUI transition
174
+ await McpBridge.disconnect().catch(() => {})
175
+ if (process.stdin.isTTY && (process.stdin as any).setRawMode) {
176
+ (process.stdin as any).setRawMode(false)
177
+ }
178
+ process.stdout.write("\x1b[2J\x1b[H") // Clear screen
179
+ }
180
+
181
+ const { tui } = await import("./tui/app")
182
+ await tui({ onExit: async () => {} })
183
+ process.exit(0)
184
+ }
185
+
186
+ try {
187
+ await cli.parse()
188
+ } catch (e) {
189
+ Log.Default.error("fatal", {
190
+ name: e instanceof Error ? e.name : "unknown",
191
+ message: e instanceof Error ? e.message : String(e),
192
+ })
193
+ if (e instanceof Error) {
194
+ UI.error(e.message)
195
+ } else {
196
+ UI.error("Unexpected error, check log file at " + Log.file() + " for more details" + EOL)
197
+ console.error(String(e))
198
+ }
199
+ process.exitCode = 1
200
+ } finally {
201
+ await McpBridge.disconnect().catch(() => {})
202
+ process.exit()
203
+ }
@@ -0,0 +1,149 @@
1
+ import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test"
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // We test the McpBridge module by mocking the MCP SDK classes.
5
+ // The actual module spawns a subprocess, which we don't want in unit tests.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ // Mock the MCP SDK
9
+ const mockCallTool = mock((): Promise<{ content: Array<{ type: string; text?: string; data?: string }>; isError: boolean }> =>
10
+ Promise.resolve({
11
+ content: [{ type: "text", text: '{"ok":true}' }],
12
+ isError: false,
13
+ }),
14
+ )
15
+ const mockListTools = mock(() =>
16
+ Promise.resolve({ tools: [{ name: "test_tool", description: "A test tool" }] }),
17
+ )
18
+ const mockConnect = mock(() => Promise.resolve())
19
+ const mockGetServerVersion = mock(() => ({ name: "test-server", version: "1.0.0" }))
20
+ const mockClose = mock(() => Promise.resolve())
21
+
22
+ mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
23
+ Client: class MockClient {
24
+ callTool = mockCallTool
25
+ listTools = mockListTools
26
+ connect = mockConnect
27
+ getServerVersion = mockGetServerVersion
28
+ },
29
+ }))
30
+
31
+ mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({
32
+ StdioClientTransport: class MockTransport {
33
+ close = mockClose
34
+ },
35
+ }))
36
+
37
+ // Must import AFTER mocks are set up
38
+ const { McpBridge } = await import("../client")
39
+
40
+ describe("McpBridge", () => {
41
+ afterEach(async () => {
42
+ await McpBridge.disconnect()
43
+ mockCallTool.mockClear()
44
+ mockListTools.mockClear()
45
+ mockConnect.mockClear()
46
+ mockClose.mockClear()
47
+ })
48
+
49
+ test("callTool returns text content from MCP result", async () => {
50
+ const result = await McpBridge.callTool("test_tool", { key: "value" })
51
+ expect(result).toBe('{"ok":true}')
52
+ expect(mockCallTool).toHaveBeenCalledWith({
53
+ name: "test_tool",
54
+ arguments: { key: "value" },
55
+ })
56
+ })
57
+
58
+ test("callToolJSON parses JSON result", async () => {
59
+ const result = await McpBridge.callToolJSON("test_tool")
60
+ expect(result).toEqual({ ok: true })
61
+ })
62
+
63
+ test("callToolJSON falls back to raw text when JSON parse fails", async () => {
64
+ mockCallTool.mockImplementationOnce(() =>
65
+ Promise.resolve({
66
+ content: [{ type: "text", text: "not json" }],
67
+ isError: false,
68
+ }),
69
+ )
70
+ const result = await McpBridge.callToolJSON("test_tool")
71
+ expect(result).toBe("not json")
72
+ })
73
+
74
+ test("callTool throws on error result", async () => {
75
+ mockCallTool.mockImplementationOnce(() =>
76
+ Promise.resolve({
77
+ content: [{ type: "text", text: "Something went wrong" }],
78
+ isError: true,
79
+ }),
80
+ )
81
+ expect(McpBridge.callTool("failing_tool")).rejects.toThrow("Something went wrong")
82
+ })
83
+
84
+ test("callTool throws generic message when error has no text", async () => {
85
+ mockCallTool.mockImplementationOnce(() =>
86
+ Promise.resolve({
87
+ content: [],
88
+ isError: true,
89
+ }),
90
+ )
91
+ expect(McpBridge.callTool("failing_tool")).rejects.toThrow('MCP tool "failing_tool" returned an error')
92
+ })
93
+
94
+ test("callTool handles empty content array", async () => {
95
+ mockCallTool.mockImplementationOnce(() =>
96
+ Promise.resolve({
97
+ content: [],
98
+ isError: false,
99
+ }),
100
+ )
101
+ const result = await McpBridge.callTool("test_tool")
102
+ expect(result).toBe("")
103
+ })
104
+
105
+ test("callTool joins multiple text content items", async () => {
106
+ mockCallTool.mockImplementationOnce(() =>
107
+ Promise.resolve({
108
+ content: [
109
+ { type: "text", text: "line1" },
110
+ { type: "text", text: "line2" },
111
+ { type: "image", data: "..." },
112
+ ],
113
+ isError: false,
114
+ }),
115
+ )
116
+ const result = await McpBridge.callTool("test_tool")
117
+ expect(result).toBe("line1\nline2")
118
+ })
119
+
120
+ test("listTools delegates to MCP client", async () => {
121
+ const result = await McpBridge.listTools()
122
+ expect(result.tools).toHaveLength(1)
123
+ expect(result.tools[0]?.name).toBe("test_tool")
124
+ })
125
+
126
+ test("disconnect cleans up transport and client", async () => {
127
+ // First connect by making a call
128
+ await McpBridge.callTool("test_tool")
129
+ // Then disconnect
130
+ await McpBridge.disconnect()
131
+ // Verify close was called
132
+ expect(mockClose).toHaveBeenCalled()
133
+ })
134
+
135
+ test("connection is reused across multiple calls", async () => {
136
+ await McpBridge.callTool("test_tool")
137
+ await McpBridge.callTool("test_tool")
138
+ // connect should only be called once
139
+ expect(mockConnect).toHaveBeenCalledTimes(1)
140
+ })
141
+
142
+ test("default args is empty object", async () => {
143
+ await McpBridge.callTool("test_tool")
144
+ expect(mockCallTool).toHaveBeenCalledWith({
145
+ name: "test_tool",
146
+ arguments: {},
147
+ })
148
+ })
149
+ })
@@ -0,0 +1,331 @@
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
+ function firstLine(text: string): string {
58
+ return text.split("\n")[0] ?? ""
59
+ }
60
+
61
+ async function main() {
62
+ console.log("=== CodeBlog E2E Test — Full User Journey ===\n")
63
+
64
+ // 1. Status check
65
+ await test("1. codeblog_status", async () => {
66
+ const result = await McpBridge.callTool("codeblog_status")
67
+ assert(result.includes("CodeBlog MCP Server"), "should include server info")
68
+ assert(result.includes("Agent:"), "should include agent info (authenticated)")
69
+ console.log(` → ${firstLine(result)}`)
70
+ })
71
+
72
+ // 2. Scan IDE sessions
73
+ let sessionPath = ""
74
+ let sessionSource = ""
75
+ await test("2. scan_sessions", async () => {
76
+ const raw = await McpBridge.callTool("scan_sessions", { limit: 5 })
77
+ const sessions = JSON.parse(raw)
78
+ assert(Array.isArray(sessions), "should return array")
79
+ assert(sessions.length > 0, "should have at least 1 session")
80
+ sessionPath = sessions[0].path
81
+ sessionSource = sessions[0].source
82
+ console.log(` → Found ${sessions.length} sessions, first: [${sessionSource}] ${sessions[0].project}`)
83
+ })
84
+
85
+ // 3. Read a session
86
+ await test("3. read_session", async () => {
87
+ assert(sessionPath !== "", "need a session path from step 2")
88
+ const raw = await McpBridge.callTool("read_session", {
89
+ path: sessionPath,
90
+ source: sessionSource,
91
+ max_turns: 3,
92
+ })
93
+ assert(raw.length > 50, "should return session content")
94
+ console.log(` → Got ${raw.length} chars of session content`)
95
+ })
96
+
97
+ // 4. Analyze a session
98
+ await test("4. analyze_session", async () => {
99
+ const raw = await McpBridge.callTool("analyze_session", {
100
+ path: sessionPath,
101
+ source: sessionSource,
102
+ })
103
+ assert(raw.length > 50, "should return analysis")
104
+ console.log(` → Got ${raw.length} chars of analysis`)
105
+ })
106
+
107
+ // 5. Post to CodeBlog
108
+ await test("5. post_to_codeblog", async () => {
109
+ const raw = await McpBridge.callTool("post_to_codeblog", {
110
+ title: "[E2E Test] MCP Integration Test Post",
111
+ 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.",
112
+ source_session: sessionPath,
113
+ tags: ["e2e-test", "automated", "mcp"],
114
+ summary: "Automated test post — will be deleted",
115
+ category: "general",
116
+ })
117
+ // MCP returns text like "✅ Posted! View at: https://codeblog.ai/post/<id>"
118
+ // or JSON. Handle both.
119
+ try {
120
+ const result = JSON.parse(raw)
121
+ testPostId = result.id || result.post?.id || ""
122
+ } catch {
123
+ // Extract post ID from URL in text
124
+ const urlMatch = raw.match(/\/post\/([a-z0-9]+)/)
125
+ testPostId = urlMatch?.[1] || ""
126
+ }
127
+ assert(testPostId !== "", `should extract post ID from: ${raw.slice(0, 100)}`)
128
+ console.log(` → Created post: ${testPostId}`)
129
+ })
130
+
131
+ // 6. Browse posts
132
+ await test("6. browse_posts", async () => {
133
+ const raw = await McpBridge.callTool("browse_posts", { sort: "new", limit: 5 })
134
+ assert(raw.length > 10, "should return posts")
135
+ const result = JSON.parse(raw)
136
+ assert(result.posts || Array.isArray(result), "should be parseable")
137
+ const posts = result.posts || result
138
+ console.log(` → Got ${posts.length} posts`)
139
+ })
140
+
141
+ // 7. Search posts
142
+ await test("7. search_posts", async () => {
143
+ const raw = await McpBridge.callTool("search_posts", { query: "E2E Test", limit: 5 })
144
+ assert(raw.length > 5, "should return results")
145
+ console.log(` → Search returned ${raw.length} chars`)
146
+ })
147
+
148
+ // 8. Read the test post
149
+ await test("8. read_post", async () => {
150
+ assert(testPostId !== "", "need post ID from step 5")
151
+ const raw = await McpBridge.callTool("read_post", { post_id: testPostId })
152
+ assert(raw.length > 50, "should return post content")
153
+ assert(raw.includes("E2E Test") || raw.includes("e2e"), "should contain test post content")
154
+ console.log(` → Read post: ${raw.length} chars`)
155
+ })
156
+
157
+ // 9. Upvote the post
158
+ await test("9. vote_on_post (upvote)", async () => {
159
+ assert(testPostId !== "", "need post ID from step 5")
160
+ const raw = await McpBridge.callTool("vote_on_post", { post_id: testPostId, value: 1 })
161
+ console.log(` → ${raw.slice(0, 100)}`)
162
+ })
163
+
164
+ // 10. Comment on the post
165
+ await test("10. comment_on_post", async () => {
166
+ assert(testPostId !== "", "need post ID from step 5")
167
+ const raw = await McpBridge.callTool("comment_on_post", {
168
+ post_id: testPostId,
169
+ content: "This is an automated E2E test comment. Testing the comment system!",
170
+ })
171
+ // Extract comment ID from text or JSON
172
+ try {
173
+ const result = JSON.parse(raw)
174
+ testCommentId = result.id || result.comment?.id || ""
175
+ } catch {
176
+ const idMatch = raw.match(/Comment ID:\s*([a-z0-9]+)/)
177
+ testCommentId = idMatch?.[1] || ""
178
+ }
179
+ console.log(` → ${firstLine(raw).slice(0, 80)}`)
180
+ })
181
+
182
+ // 11. Edit the post
183
+ await test("11. edit_post", async () => {
184
+ assert(testPostId !== "", "need post ID from step 5")
185
+ const raw = await McpBridge.callTool("edit_post", {
186
+ post_id: testPostId,
187
+ title: "[E2E Test] MCP Integration Test Post (Edited)",
188
+ summary: "Automated test post — EDITED — will be deleted",
189
+ })
190
+ console.log(` → ${raw.slice(0, 100)}`)
191
+ })
192
+
193
+ // 12. Bookmark the post
194
+ await test("12. bookmark_post (toggle)", async () => {
195
+ assert(testPostId !== "", "need post ID from step 5")
196
+ const raw = await McpBridge.callTool("bookmark_post", {
197
+ action: "toggle",
198
+ post_id: testPostId,
199
+ })
200
+ console.log(` → ${raw.slice(0, 100)}`)
201
+ })
202
+
203
+ // 13. List bookmarks
204
+ await test("13. bookmark_post (list)", async () => {
205
+ const raw = await McpBridge.callTool("bookmark_post", { action: "list" })
206
+ assert(raw.length > 0, "should return bookmarks")
207
+ console.log(` → ${raw.slice(0, 100)}`)
208
+ })
209
+
210
+ // 14. Browse by tag
211
+ await test("14. browse_by_tag (trending)", async () => {
212
+ const raw = await McpBridge.callTool("browse_by_tag", { action: "trending", limit: 5 })
213
+ assert(raw.length > 0, "should return trending tags")
214
+ console.log(` → ${raw.slice(0, 100)}`)
215
+ })
216
+
217
+ await test("15. browse_by_tag (posts)", async () => {
218
+ const raw = await McpBridge.callTool("browse_by_tag", { action: "posts", tag: "e2e-test", limit: 5 })
219
+ console.log(` → ${raw.slice(0, 100)}`)
220
+ })
221
+
222
+ // 16. Trending topics
223
+ await test("16. trending_topics", async () => {
224
+ const raw = await McpBridge.callTool("trending_topics")
225
+ assert(raw.includes("Trending"), "should include trending info")
226
+ console.log(` → ${firstLine(raw)}`)
227
+ })
228
+
229
+ // 17. Explore and engage
230
+ await test("17. explore_and_engage (browse)", async () => {
231
+ const raw = await McpBridge.callTool("explore_and_engage", { action: "browse", limit: 3 })
232
+ assert(raw.length > 0, "should return content")
233
+ console.log(` → ${firstLine(raw)}`)
234
+ })
235
+
236
+ // 18. My posts
237
+ await test("18. my_posts", async () => {
238
+ const raw = await McpBridge.callTool("my_posts", { limit: 5 })
239
+ assert(raw.length > 0, "should return my posts")
240
+ console.log(` → ${raw.slice(0, 100)}`)
241
+ })
242
+
243
+ // 19. My dashboard
244
+ await test("19. my_dashboard", async () => {
245
+ const raw = await McpBridge.callTool("my_dashboard")
246
+ assert(raw.length > 0, "should return dashboard data")
247
+ console.log(` → ${raw.slice(0, 100)}`)
248
+ })
249
+
250
+ // 20. My notifications
251
+ await test("20. my_notifications (list)", async () => {
252
+ const raw = await McpBridge.callTool("my_notifications", { action: "list", limit: 5 })
253
+ console.log(` → ${raw.slice(0, 100)}`)
254
+ })
255
+
256
+ // 21. Manage agents
257
+ await test("21. manage_agents (list)", async () => {
258
+ const raw = await McpBridge.callTool("manage_agents", { action: "list" })
259
+ assert(raw.length > 0, "should return agents")
260
+ console.log(` → ${raw.slice(0, 100)}`)
261
+ })
262
+
263
+ // 22. Follow agent / user
264
+ await test("22. follow_agent (list_following)", async () => {
265
+ const raw = await McpBridge.callTool("follow_agent", { action: "list_following", limit: 5 })
266
+ console.log(` → ${raw.slice(0, 100)}`)
267
+ })
268
+
269
+ // 23. Join debate
270
+ await test("23. join_debate (list)", async () => {
271
+ const raw = await McpBridge.callTool("join_debate", { action: "list" })
272
+ console.log(` → ${raw.slice(0, 80)}`)
273
+ })
274
+
275
+ // 24. Weekly digest (dry run)
276
+ await test("24. weekly_digest (dry_run)", async () => {
277
+ const raw = await McpBridge.callTool("weekly_digest", { dry_run: true })
278
+ assert(raw.length > 0, "should return digest preview")
279
+ console.log(` → ${firstLine(raw)}`)
280
+ })
281
+
282
+ // 25. Auto post (dry run)
283
+ await test("25. auto_post (dry_run)", async () => {
284
+ const raw = await McpBridge.callTool("auto_post", { dry_run: true })
285
+ assert(raw.length > 0, "should return post preview")
286
+ console.log(` → ${firstLine(raw).slice(0, 100)}`)
287
+ })
288
+
289
+ // 26. Remove vote
290
+ await test("26. vote_on_post (remove)", async () => {
291
+ assert(testPostId !== "", "need post ID")
292
+ const raw = await McpBridge.callTool("vote_on_post", { post_id: testPostId, value: 0 })
293
+ console.log(` → ${raw.slice(0, 100)}`)
294
+ })
295
+
296
+ // 27. Unbookmark
297
+ await test("27. bookmark_post (unbookmark)", async () => {
298
+ assert(testPostId !== "", "need post ID")
299
+ const raw = await McpBridge.callTool("bookmark_post", {
300
+ action: "toggle",
301
+ post_id: testPostId,
302
+ })
303
+ console.log(` → ${raw.slice(0, 100)}`)
304
+ })
305
+
306
+ // 28. Delete the test post (cleanup)
307
+ await test("28. delete_post (cleanup)", async () => {
308
+ assert(testPostId !== "", "need post ID to delete")
309
+ const raw = await McpBridge.callTool("delete_post", {
310
+ post_id: testPostId,
311
+ confirm: true,
312
+ })
313
+ console.log(` → ${raw.slice(0, 100)}`)
314
+ })
315
+
316
+ // Disconnect
317
+ await McpBridge.disconnect()
318
+
319
+ console.log("\n=== Summary ===")
320
+ console.log(`Passed: ${passed}/${passed + failed}`)
321
+ console.log(`Failed: ${failed}/${passed + failed}`)
322
+
323
+ if (failed > 0) {
324
+ process.exit(1)
325
+ }
326
+ }
327
+
328
+ main().catch((err) => {
329
+ console.error("Fatal:", err)
330
+ process.exit(1)
331
+ })