codeblog-app 2.2.4 → 2.3.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 (69) 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 -179
  6. package/src/ai/__tests__/provider.test.ts +0 -198
  7. package/src/ai/__tests__/tools.test.ts +0 -93
  8. package/src/ai/chat.ts +0 -224
  9. package/src/ai/configure.ts +0 -134
  10. package/src/ai/provider.ts +0 -302
  11. package/src/ai/tools.ts +0 -114
  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 -97
  16. package/src/cli/cmd/chat.ts +0 -190
  17. package/src/cli/cmd/comment.ts +0 -67
  18. package/src/cli/cmd/config.ts +0 -153
  19. package/src/cli/cmd/feed.ts +0 -53
  20. package/src/cli/cmd/forum.ts +0 -106
  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 -188
  24. package/src/cli/cmd/post.ts +0 -25
  25. package/src/cli/cmd/publish.ts +0 -64
  26. package/src/cli/cmd/scan.ts +0 -78
  27. package/src/cli/cmd/search.ts +0 -35
  28. package/src/cli/cmd/setup.ts +0 -352
  29. package/src/cli/cmd/tui.ts +0 -20
  30. package/src/cli/cmd/uninstall.ts +0 -281
  31. package/src/cli/cmd/update.ts +0 -123
  32. package/src/cli/cmd/vote.ts +0 -50
  33. package/src/cli/cmd/whoami.ts +0 -18
  34. package/src/cli/mcp-print.ts +0 -6
  35. package/src/cli/ui.ts +0 -250
  36. package/src/config/index.ts +0 -54
  37. package/src/flag/index.ts +0 -23
  38. package/src/global/index.ts +0 -38
  39. package/src/id/index.ts +0 -20
  40. package/src/index.ts +0 -200
  41. package/src/mcp/__tests__/client.test.ts +0 -149
  42. package/src/mcp/__tests__/e2e.ts +0 -327
  43. package/src/mcp/__tests__/integration.ts +0 -148
  44. package/src/mcp/client.ts +0 -148
  45. package/src/server/index.ts +0 -48
  46. package/src/storage/chat.ts +0 -71
  47. package/src/storage/db.ts +0 -85
  48. package/src/storage/schema.sql.ts +0 -39
  49. package/src/storage/schema.ts +0 -1
  50. package/src/tui/app.tsx +0 -165
  51. package/src/tui/commands.ts +0 -186
  52. package/src/tui/context/exit.tsx +0 -15
  53. package/src/tui/context/helper.tsx +0 -25
  54. package/src/tui/context/route.tsx +0 -24
  55. package/src/tui/context/theme.tsx +0 -470
  56. package/src/tui/routes/home.tsx +0 -660
  57. package/src/tui/routes/model.tsx +0 -210
  58. package/src/tui/routes/notifications.tsx +0 -87
  59. package/src/tui/routes/post.tsx +0 -102
  60. package/src/tui/routes/search.tsx +0 -105
  61. package/src/tui/routes/setup.tsx +0 -255
  62. package/src/tui/routes/trending.tsx +0 -107
  63. package/src/util/__tests__/context.test.ts +0 -31
  64. package/src/util/__tests__/lazy.test.ts +0 -37
  65. package/src/util/context.ts +0 -23
  66. package/src/util/error.ts +0 -46
  67. package/src/util/lazy.ts +0 -18
  68. package/src/util/log.ts +0 -142
  69. package/tsconfig.json +0 -11
package/package.json CHANGED
@@ -1,9 +1,7 @@
1
1
  {
2
- "$schema": "https://json.schemastore.org/package.json",
3
2
  "name": "codeblog-app",
4
- "version": "2.2.4",
3
+ "version": "2.3.0",
5
4
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
- "type": "module",
7
5
  "license": "MIT",
8
6
  "author": "CodeBlog-ai",
9
7
  "homepage": "https://github.com/CodeBlog-ai/codeblog-app",
@@ -11,75 +9,14 @@
11
9
  "type": "git",
12
10
  "url": "https://github.com/CodeBlog-ai/codeblog-app"
13
11
  },
14
- "bugs": {
15
- "url": "https://github.com/CodeBlog-ai/codeblog-app/issues"
16
- },
17
- "keywords": [
18
- "codeblog",
19
- "cli",
20
- "ai",
21
- "coding",
22
- "forum",
23
- "ide",
24
- "scanner",
25
- "claude",
26
- "cursor",
27
- "windsurf",
28
- "codex",
29
- "copilot"
30
- ],
31
- "scripts": {
32
- "typecheck": "tsc --noEmit",
33
- "test": "bun test --timeout 30000",
34
- "dev": "bun run --watch ./src/index.ts"
35
- },
36
12
  "bin": {
37
- "codeblog": "./bin/codeblog"
38
- },
39
- "files": [
40
- "bin",
41
- "src",
42
- "drizzle",
43
- "drizzle.config.ts",
44
- "tsconfig.json",
45
- "package.json",
46
- "README.md"
47
- ],
48
- "exports": {
49
- "./*": "./src/*.ts"
50
- },
51
- "devDependencies": {
52
- "@tsconfig/bun": "1.0.9",
53
- "@types/bun": "1.3.9",
54
- "@types/yargs": "17.0.33",
55
- "drizzle-kit": "1.0.0-beta.12-a5629fb",
56
- "typescript": "5.8.2"
13
+ "codeblog": "bin/codeblog"
57
14
  },
58
15
  "optionalDependencies": {
59
- "codeblog-app-darwin-arm64": "2.2.4",
60
- "codeblog-app-darwin-x64": "2.2.4",
61
- "codeblog-app-linux-arm64": "2.2.4",
62
- "codeblog-app-linux-x64": "2.2.4",
63
- "codeblog-app-windows-x64": "2.2.4"
64
- },
65
- "dependencies": {
66
- "@ai-sdk/anthropic": "^3.0.44",
67
- "@ai-sdk/google": "^3.0.29",
68
- "@ai-sdk/openai": "^3.0.29",
69
- "@ai-sdk/openai-compatible": "^2.0.30",
70
- "@modelcontextprotocol/sdk": "^1.26.0",
71
- "@opentui/core": "^0.1.79",
72
- "@opentui/solid": "^0.1.79",
73
- "ai": "^6.0.86",
74
- "codeblog-mcp": "^2.1.4",
75
- "drizzle-orm": "1.0.0-beta.12-a5629fb",
76
- "fuzzysort": "^3.1.0",
77
- "hono": "4.10.7",
78
- "open": "10.1.2",
79
- "remeda": "^2.33.6",
80
- "solid-js": "^1.9.11",
81
- "xdg-basedir": "5.1.0",
82
- "yargs": "18.0.0",
83
- "zod": "4.1.8"
16
+ "codeblog-app-darwin-arm64": "2.3.0",
17
+ "codeblog-app-darwin-x64": "2.3.0",
18
+ "codeblog-app-linux-arm64": "2.3.0",
19
+ "codeblog-app-linux-x64": "2.3.0",
20
+ "codeblog-app-windows-x64": "2.3.0"
84
21
  }
85
- }
22
+ }
@@ -1,34 +0,0 @@
1
- CREATE TABLE IF NOT EXISTS `published_sessions` (
2
- `id` text PRIMARY KEY NOT NULL,
3
- `session_id` text NOT NULL,
4
- `source` text NOT NULL,
5
- `post_id` text NOT NULL,
6
- `file_path` text NOT NULL,
7
- `published_at` integer DEFAULT (unixepoch()) NOT NULL
8
- );
9
-
10
- CREATE TABLE IF NOT EXISTS `cached_posts` (
11
- `id` text PRIMARY KEY NOT NULL,
12
- `title` text NOT NULL,
13
- `content` text NOT NULL,
14
- `summary` text,
15
- `tags` text DEFAULT '[]' NOT NULL,
16
- `upvotes` integer DEFAULT 0 NOT NULL,
17
- `downvotes` integer DEFAULT 0 NOT NULL,
18
- `author_name` text NOT NULL,
19
- `fetched_at` integer DEFAULT (unixepoch()) NOT NULL
20
- );
21
-
22
- CREATE TABLE IF NOT EXISTS `notifications_cache` (
23
- `id` text PRIMARY KEY NOT NULL,
24
- `type` text NOT NULL,
25
- `message` text NOT NULL,
26
- `read` integer DEFAULT 0 NOT NULL,
27
- `post_id` text,
28
- `created_at` integer DEFAULT (unixepoch()) NOT NULL
29
- );
30
-
31
- CREATE INDEX IF NOT EXISTS `idx_published_sessions_source` ON `published_sessions` (`source`);
32
- CREATE INDEX IF NOT EXISTS `idx_published_sessions_session_id` ON `published_sessions` (`session_id`);
33
- CREATE INDEX IF NOT EXISTS `idx_cached_posts_fetched_at` ON `cached_posts` (`fetched_at`);
34
- CREATE INDEX IF NOT EXISTS `idx_notifications_read` ON `notifications_cache` (`read`);
@@ -1,13 +0,0 @@
1
- {
2
- "version": "7",
3
- "dialect": "sqlite",
4
- "entries": [
5
- {
6
- "idx": 0,
7
- "version": "7",
8
- "when": 1739520000000,
9
- "tag": "0000_init",
10
- "breakpoints": true
11
- }
12
- ]
13
- }
package/drizzle.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { Config } from "drizzle-kit"
2
-
3
- export default {
4
- schema: "./src/storage/schema.sql.ts",
5
- out: "./drizzle",
6
- dialect: "sqlite",
7
- dbCredentials: {
8
- url: process.env.DATABASE_URL || "~/.codeblog/data/codeblog.db",
9
- },
10
- } satisfies Config
@@ -1,179 +0,0 @@
1
- import { describe, test, expect, mock, beforeEach } from "bun:test"
2
-
3
- // Mock the MCP bridge for chat tests
4
- const mockCallToolJSON = mock((name: string, args: Record<string, unknown>) =>
5
- Promise.resolve({ ok: true, tool: name }),
6
- )
7
-
8
- const mockListTools = mock(() =>
9
- Promise.resolve({ tools: [] }),
10
- )
11
-
12
- mock.module("../../mcp/client", () => ({
13
- McpBridge: {
14
- callTool: mock((name: string, args: Record<string, unknown>) =>
15
- Promise.resolve(JSON.stringify({ ok: true, tool: name })),
16
- ),
17
- callToolJSON: mockCallToolJSON,
18
- listTools: mockListTools,
19
- disconnect: mock(() => Promise.resolve()),
20
- },
21
- }))
22
-
23
- // Each call to streamText must return a FRESH async generator
24
- function makeStreamResult() {
25
- return {
26
- fullStream: (async function* () {
27
- yield { type: "text-delta", textDelta: "Hello " }
28
- yield { type: "text-delta", textDelta: "World" }
29
- })(),
30
- }
31
- }
32
-
33
- function makeToolCallStreamResult() {
34
- return {
35
- fullStream: (async function* () {
36
- yield { type: "tool-call", toolName: "scan_sessions", args: { limit: 5 } }
37
- yield { type: "tool-result", toolName: "scan_sessions", result: { sessions: [] } }
38
- yield { type: "text-delta", textDelta: "Done scanning." }
39
- })(),
40
- }
41
- }
42
-
43
- let streamFactory = () => makeStreamResult()
44
-
45
- mock.module("ai", () => ({
46
- streamText: () => streamFactory(),
47
- stepCountIs: (n: number) => ({ type: "step-count", count: n }),
48
- tool: (config: any) => config,
49
- jsonSchema: (schema: any) => schema,
50
- }))
51
-
52
- mock.module("../provider", () => ({
53
- AIProvider: {
54
- getModel: mock(() => Promise.resolve({ id: "test-model" })),
55
- DEFAULT_MODEL: "test-model",
56
- },
57
- }))
58
-
59
- const { AIChat } = await import("../chat")
60
-
61
- describe("AIChat", () => {
62
- beforeEach(() => {
63
- mockCallToolJSON.mockClear()
64
- streamFactory = () => makeStreamResult()
65
- })
66
-
67
- // ---------------------------------------------------------------------------
68
- // Message interface
69
- // ---------------------------------------------------------------------------
70
-
71
- test("Message type accepts user, assistant, system roles", () => {
72
- const messages: AIChat.Message[] = [
73
- { role: "user", content: "hello" },
74
- { role: "assistant", content: "hi" },
75
- { role: "system", content: "you are a bot" },
76
- ]
77
- expect(messages).toHaveLength(3)
78
- })
79
-
80
- // ---------------------------------------------------------------------------
81
- // stream()
82
- // ---------------------------------------------------------------------------
83
-
84
- test("stream calls onToken for each text delta", async () => {
85
- const tokens: string[] = []
86
- const result = await AIChat.stream(
87
- [{ role: "user", content: "test" }],
88
- {
89
- onToken: (t) => tokens.push(t),
90
- onFinish: () => {},
91
- },
92
- )
93
- expect(tokens).toEqual(["Hello ", "World"])
94
- expect(result).toBe("Hello World")
95
- })
96
-
97
- test("stream calls onFinish with full text", async () => {
98
- let finished = ""
99
- await AIChat.stream(
100
- [{ role: "user", content: "test" }],
101
- {
102
- onFinish: (text) => { finished = text },
103
- },
104
- )
105
- expect(finished).toBe("Hello World")
106
- })
107
-
108
- test("stream filters out system messages from history", async () => {
109
- await AIChat.stream(
110
- [
111
- { role: "system", content: "ignored" },
112
- { role: "user", content: "hello" },
113
- { role: "assistant", content: "hi" },
114
- { role: "user", content: "bye" },
115
- ],
116
- { onFinish: () => {} },
117
- )
118
- // Should not throw — system messages are filtered
119
- })
120
-
121
- // ---------------------------------------------------------------------------
122
- // stream() with tool calls
123
- // ---------------------------------------------------------------------------
124
-
125
- test("stream dispatches onToolCall and onToolResult callbacks", async () => {
126
- streamFactory = () => makeToolCallStreamResult()
127
-
128
- const toolCalls: Array<{ name: string; args: unknown }> = []
129
- const toolResults: Array<{ name: string; result: unknown }> = []
130
- const tokens: string[] = []
131
-
132
- await AIChat.stream(
133
- [{ role: "user", content: "scan my sessions" }],
134
- {
135
- onToken: (t) => tokens.push(t),
136
- onToolCall: (name, args) => toolCalls.push({ name, args }),
137
- onToolResult: (name, result) => toolResults.push({ name, result }),
138
- onFinish: () => {},
139
- },
140
- )
141
-
142
- expect(toolCalls).toEqual([{ name: "scan_sessions", args: { limit: 5 } }])
143
- expect(toolResults).toEqual([{ name: "scan_sessions", result: { sessions: [] } }])
144
- expect(tokens).toEqual(["Done scanning."])
145
- })
146
-
147
- // ---------------------------------------------------------------------------
148
- // stream() error handling
149
- // ---------------------------------------------------------------------------
150
-
151
- test("stream calls onError when error event is received", async () => {
152
- streamFactory = () => ({
153
- fullStream: (async function* () {
154
- yield { type: "error", error: new Error("test error") }
155
- })(),
156
- })
157
-
158
- const errors: Error[] = []
159
- await AIChat.stream(
160
- [{ role: "user", content: "test" }],
161
- {
162
- onError: (err) => errors.push(err),
163
- onFinish: () => {},
164
- },
165
- )
166
-
167
- expect(errors).toHaveLength(1)
168
- expect(errors[0]!.message).toBe("test error")
169
- })
170
-
171
- // ---------------------------------------------------------------------------
172
- // generate()
173
- // ---------------------------------------------------------------------------
174
-
175
- test("generate returns the full response text", async () => {
176
- const result = await AIChat.generate("test prompt")
177
- expect(result).toBe("Hello World")
178
- })
179
- })
@@ -1,198 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test"
2
- import { AIProvider } from "../provider"
3
-
4
- describe("AIProvider", () => {
5
- const originalEnv = { ...process.env }
6
-
7
- beforeEach(() => {
8
- // Clean up env vars before each test
9
- delete process.env.ANTHROPIC_API_KEY
10
- delete process.env.ANTHROPIC_AUTH_TOKEN
11
- delete process.env.OPENAI_API_KEY
12
- delete process.env.GOOGLE_GENERATIVE_AI_API_KEY
13
- delete process.env.GOOGLE_API_KEY
14
- delete process.env.OPENAI_COMPATIBLE_API_KEY
15
- delete process.env.ANTHROPIC_BASE_URL
16
- delete process.env.OPENAI_BASE_URL
17
- delete process.env.OPENAI_API_BASE
18
- delete process.env.GOOGLE_API_BASE_URL
19
- delete process.env.OPENAI_COMPATIBLE_BASE_URL
20
- })
21
-
22
- afterEach(() => {
23
- // Restore original env
24
- for (const key of Object.keys(process.env)) {
25
- if (!(key in originalEnv)) delete process.env[key]
26
- }
27
- for (const [key, val] of Object.entries(originalEnv)) {
28
- if (val !== undefined) process.env[key] = val
29
- }
30
- })
31
-
32
- // ---------------------------------------------------------------------------
33
- // BUILTIN_MODELS
34
- // ---------------------------------------------------------------------------
35
-
36
- test("BUILTIN_MODELS has 7 models", () => {
37
- expect(Object.keys(AIProvider.BUILTIN_MODELS)).toHaveLength(7)
38
- })
39
-
40
- test("each model has required fields", () => {
41
- for (const [id, model] of Object.entries(AIProvider.BUILTIN_MODELS)) {
42
- expect(model.id).toBe(id)
43
- expect(model.providerID).toBeTruthy()
44
- expect(model.name).toBeTruthy()
45
- expect(model.contextWindow).toBeGreaterThan(0)
46
- expect(model.outputTokens).toBeGreaterThan(0)
47
- }
48
- })
49
-
50
- test("DEFAULT_MODEL is a valid builtin model", () => {
51
- expect(AIProvider.BUILTIN_MODELS[AIProvider.DEFAULT_MODEL]).toBeDefined()
52
- })
53
-
54
- test("all providers are covered: anthropic, openai, google", () => {
55
- const providerIDs = new Set(Object.values(AIProvider.BUILTIN_MODELS).map((m) => m.providerID))
56
- expect(providerIDs.has("anthropic")).toBe(true)
57
- expect(providerIDs.has("openai")).toBe(true)
58
- expect(providerIDs.has("google")).toBe(true)
59
- })
60
-
61
- // ---------------------------------------------------------------------------
62
- // getApiKey
63
- // ---------------------------------------------------------------------------
64
-
65
- test("getApiKey returns env var when set", async () => {
66
- process.env.ANTHROPIC_API_KEY = "sk-ant-test123"
67
- const key = await AIProvider.getApiKey("anthropic")
68
- expect(key).toBe("sk-ant-test123")
69
- })
70
-
71
- test("getApiKey checks secondary env var", async () => {
72
- process.env.ANTHROPIC_AUTH_TOKEN = "token-test"
73
- const key = await AIProvider.getApiKey("anthropic")
74
- expect(key).toBe("token-test")
75
- })
76
-
77
- test("getApiKey returns undefined when no key set", async () => {
78
- const key = await AIProvider.getApiKey("anthropic")
79
- // May return undefined or a config value — just check it doesn't crash
80
- expect(key === undefined || typeof key === "string").toBe(true)
81
- })
82
-
83
- test("getApiKey works for openai", async () => {
84
- process.env.OPENAI_API_KEY = "sk-openai-test"
85
- const key = await AIProvider.getApiKey("openai")
86
- expect(key).toBe("sk-openai-test")
87
- })
88
-
89
- test("getApiKey works for google", async () => {
90
- process.env.GOOGLE_GENERATIVE_AI_API_KEY = "aiza-test"
91
- const key = await AIProvider.getApiKey("google")
92
- expect(key).toBe("aiza-test")
93
- })
94
-
95
- // ---------------------------------------------------------------------------
96
- // getBaseUrl
97
- // ---------------------------------------------------------------------------
98
-
99
- test("getBaseUrl returns env var when set", async () => {
100
- process.env.ANTHROPIC_BASE_URL = "https://custom.api.com"
101
- const url = await AIProvider.getBaseUrl("anthropic")
102
- expect(url).toBe("https://custom.api.com")
103
- })
104
-
105
- test("getBaseUrl returns undefined when no env var set", async () => {
106
- const url = await AIProvider.getBaseUrl("anthropic")
107
- expect(url === undefined || typeof url === "string").toBe(true)
108
- })
109
-
110
- // ---------------------------------------------------------------------------
111
- // hasAnyKey
112
- // ---------------------------------------------------------------------------
113
-
114
- test("hasAnyKey returns true when any key is set", async () => {
115
- process.env.OPENAI_API_KEY = "sk-test"
116
- const has = await AIProvider.hasAnyKey()
117
- expect(has).toBe(true)
118
- })
119
-
120
- // ---------------------------------------------------------------------------
121
- // parseModel
122
- // ---------------------------------------------------------------------------
123
-
124
- test("parseModel splits provider/model", () => {
125
- const result = AIProvider.parseModel("anthropic/claude-sonnet-4-20250514")
126
- expect(result.providerID).toBe("anthropic")
127
- expect(result.modelID).toBe("claude-sonnet-4-20250514")
128
- })
129
-
130
- test("parseModel handles nested slashes", () => {
131
- const result = AIProvider.parseModel("openai/gpt-4o/latest")
132
- expect(result.providerID).toBe("openai")
133
- expect(result.modelID).toBe("gpt-4o/latest")
134
- })
135
-
136
- test("parseModel handles no slash", () => {
137
- const result = AIProvider.parseModel("gpt-4o")
138
- expect(result.providerID).toBe("gpt-4o")
139
- expect(result.modelID).toBe("")
140
- })
141
-
142
- // ---------------------------------------------------------------------------
143
- // available
144
- // ---------------------------------------------------------------------------
145
-
146
- test("available returns at least builtin models with hasKey status", async () => {
147
- const models = await AIProvider.available()
148
- expect(models.length).toBeGreaterThanOrEqual(7)
149
- for (const entry of models) {
150
- expect(entry.model).toBeDefined()
151
- expect(typeof entry.hasKey).toBe("boolean")
152
- }
153
- // The first 7 should always be builtins
154
- const builtinCount = models.filter((m) => AIProvider.BUILTIN_MODELS[m.model.id]).length
155
- expect(builtinCount).toBe(7)
156
- })
157
-
158
- test("available includes openai-compatible remote models when configured", async () => {
159
- // With a valid key and base URL from config, remote models should be fetched.
160
- // This test verifies that available() attempts to include openai-compatible models.
161
- // Use localhost to ensure fast failure (connection refused) instead of DNS timeout.
162
- process.env.OPENAI_COMPATIBLE_API_KEY = "sk-test"
163
- process.env.OPENAI_COMPATIBLE_BASE_URL = "http://127.0.0.1:1"
164
- const models = await AIProvider.available()
165
- // Should still return at least builtins even if remote fetch fails
166
- expect(models.length).toBeGreaterThanOrEqual(7)
167
- })
168
-
169
- // ---------------------------------------------------------------------------
170
- // getModel
171
- // ---------------------------------------------------------------------------
172
-
173
- test("getModel throws when no API key for builtin model", async () => {
174
- expect(AIProvider.getModel("gpt-4o")).rejects.toThrow("No API key for openai")
175
- })
176
-
177
- test("getModel falls back to provider with base_url for unknown model", async () => {
178
- // When a provider with base_url is configured, unknown models get sent there
179
- // instead of throwing. This test verifies the fallback behavior.
180
- // If no provider has a base_url, it would throw.
181
- const result = AIProvider.getModel("nonexistent-model-xyz")
182
- // Either resolves (provider with base_url available) or rejects
183
- const settled = await Promise.allSettled([result])
184
- expect(settled[0]!.status).toBeDefined()
185
- })
186
-
187
- // ---------------------------------------------------------------------------
188
- // listProviders
189
- // ---------------------------------------------------------------------------
190
-
191
- test("listProviders returns provider info", async () => {
192
- process.env.OPENAI_API_KEY = "sk-test"
193
- const providers = await AIProvider.listProviders()
194
- expect(providers.openai).toBeDefined()
195
- expect(providers.openai!.hasKey).toBe(true)
196
- expect(providers.openai!.models.length).toBeGreaterThan(0)
197
- })
198
- })
@@ -1,93 +0,0 @@
1
- import { describe, test, expect, mock } from "bun:test"
2
-
3
- // Mock MCP bridge so tests don't need a running MCP server
4
- const MOCK_MCP_TOOLS = [
5
- {
6
- name: "scan_sessions",
7
- description: "Scan IDE sessions for coding activity",
8
- inputSchema: { type: "object", properties: { limit: { type: "number" } } },
9
- },
10
- {
11
- name: "my_dashboard",
12
- description: "Show the user dashboard with stats",
13
- inputSchema: {},
14
- },
15
- {
16
- name: "browse_posts",
17
- description: "Browse posts on the forum with filters",
18
- inputSchema: { type: "object", properties: { page: { type: "number" }, tag: { type: "string" } } },
19
- },
20
- ]
21
-
22
- mock.module("../../mcp/client", () => ({
23
- McpBridge: {
24
- listTools: mock(() => Promise.resolve({ tools: MOCK_MCP_TOOLS })),
25
- callToolJSON: mock((name: string) => Promise.resolve({ ok: true, tool: name })),
26
- },
27
- }))
28
-
29
- mock.module("ai", () => ({
30
- tool: (config: any) => config,
31
- jsonSchema: (schema: any) => schema,
32
- }))
33
-
34
- const { getChatTools, TOOL_LABELS, clearChatToolsCache } = await import("../tools")
35
-
36
- describe("AI Tools (dynamic MCP discovery)", () => {
37
- let chatTools: Record<string, any>
38
-
39
- test("getChatTools() discovers tools from MCP server", async () => {
40
- clearChatToolsCache()
41
- chatTools = await getChatTools()
42
- const names = Object.keys(chatTools)
43
- expect(names.length).toBe(MOCK_MCP_TOOLS.length)
44
- expect(names).toContain("scan_sessions")
45
- expect(names).toContain("my_dashboard")
46
- expect(names).toContain("browse_posts")
47
- })
48
-
49
- test("each tool has inputSchema and execute", () => {
50
- for (const [name, t] of Object.entries(chatTools)) {
51
- const tool = t as any
52
- expect(tool.inputSchema).toBeDefined()
53
- expect(tool.execute).toBeDefined()
54
- expect(typeof tool.execute).toBe("function")
55
- }
56
- })
57
-
58
- test("each tool has a description", () => {
59
- for (const [name, t] of Object.entries(chatTools)) {
60
- const tool = t as any
61
- expect(tool.description).toBeDefined()
62
- expect(typeof tool.description).toBe("string")
63
- expect(tool.description.length).toBeGreaterThan(10)
64
- }
65
- })
66
-
67
- test("normalizeToolSchema adds type:object to empty schemas", async () => {
68
- // my_dashboard has empty inputSchema {} — should be normalized
69
- const dashboard = chatTools["my_dashboard"] as any
70
- expect(dashboard.inputSchema.type).toBe("object")
71
- expect(dashboard.inputSchema.properties).toEqual({})
72
- })
73
-
74
- // ---------------------------------------------------------------------------
75
- // TOOL_LABELS tests (static fallback map)
76
- // ---------------------------------------------------------------------------
77
-
78
- test("TOOL_LABELS values are non-empty strings", () => {
79
- for (const [key, label] of Object.entries(TOOL_LABELS)) {
80
- expect(label.length).toBeGreaterThan(0)
81
- }
82
- })
83
-
84
- // ---------------------------------------------------------------------------
85
- // Caching
86
- // ---------------------------------------------------------------------------
87
-
88
- test("getChatTools() returns cached result on second call", async () => {
89
- const tools1 = await getChatTools()
90
- const tools2 = await getChatTools()
91
- expect(tools1).toBe(tools2) // same reference
92
- })
93
- })