codeblog-app 2.3.1 → 2.3.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 (83) hide show
  1. package/package.json +8 -73
  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 -188
  6. package/src/ai/__tests__/compat.test.ts +0 -46
  7. package/src/ai/__tests__/home.ai-stream.integration.test.ts +0 -77
  8. package/src/ai/__tests__/provider-registry.test.ts +0 -61
  9. package/src/ai/__tests__/provider.test.ts +0 -238
  10. package/src/ai/__tests__/stream-events.test.ts +0 -152
  11. package/src/ai/__tests__/tools.test.ts +0 -93
  12. package/src/ai/chat.ts +0 -336
  13. package/src/ai/configure.ts +0 -143
  14. package/src/ai/models.ts +0 -26
  15. package/src/ai/provider-registry.ts +0 -150
  16. package/src/ai/provider.ts +0 -264
  17. package/src/ai/stream-events.ts +0 -64
  18. package/src/ai/tools.ts +0 -118
  19. package/src/ai/types.ts +0 -105
  20. package/src/auth/index.ts +0 -49
  21. package/src/auth/oauth.ts +0 -123
  22. package/src/cli/__tests__/commands.test.ts +0 -229
  23. package/src/cli/cmd/agent.ts +0 -97
  24. package/src/cli/cmd/ai.ts +0 -10
  25. package/src/cli/cmd/chat.ts +0 -190
  26. package/src/cli/cmd/comment.ts +0 -67
  27. package/src/cli/cmd/config.ts +0 -153
  28. package/src/cli/cmd/feed.ts +0 -53
  29. package/src/cli/cmd/forum.ts +0 -106
  30. package/src/cli/cmd/login.ts +0 -45
  31. package/src/cli/cmd/logout.ts +0 -12
  32. package/src/cli/cmd/me.ts +0 -188
  33. package/src/cli/cmd/post.ts +0 -25
  34. package/src/cli/cmd/publish.ts +0 -64
  35. package/src/cli/cmd/scan.ts +0 -78
  36. package/src/cli/cmd/search.ts +0 -35
  37. package/src/cli/cmd/setup.ts +0 -622
  38. package/src/cli/cmd/tui.ts +0 -20
  39. package/src/cli/cmd/uninstall.ts +0 -281
  40. package/src/cli/cmd/update.ts +0 -123
  41. package/src/cli/cmd/vote.ts +0 -50
  42. package/src/cli/cmd/whoami.ts +0 -18
  43. package/src/cli/mcp-print.ts +0 -6
  44. package/src/cli/ui.ts +0 -357
  45. package/src/config/index.ts +0 -92
  46. package/src/flag/index.ts +0 -23
  47. package/src/global/index.ts +0 -38
  48. package/src/id/index.ts +0 -20
  49. package/src/index.ts +0 -203
  50. package/src/mcp/__tests__/client.test.ts +0 -149
  51. package/src/mcp/__tests__/e2e.ts +0 -331
  52. package/src/mcp/__tests__/integration.ts +0 -148
  53. package/src/mcp/client.ts +0 -118
  54. package/src/server/index.ts +0 -48
  55. package/src/storage/chat.ts +0 -73
  56. package/src/storage/db.ts +0 -85
  57. package/src/storage/schema.sql.ts +0 -39
  58. package/src/storage/schema.ts +0 -1
  59. package/src/tui/__tests__/input-intent.test.ts +0 -27
  60. package/src/tui/__tests__/stream-assembler.test.ts +0 -33
  61. package/src/tui/ai-stream.ts +0 -28
  62. package/src/tui/app.tsx +0 -210
  63. package/src/tui/commands.ts +0 -220
  64. package/src/tui/context/exit.tsx +0 -15
  65. package/src/tui/context/helper.tsx +0 -25
  66. package/src/tui/context/route.tsx +0 -24
  67. package/src/tui/context/theme.tsx +0 -471
  68. package/src/tui/input-intent.ts +0 -26
  69. package/src/tui/routes/home.tsx +0 -1060
  70. package/src/tui/routes/model.tsx +0 -210
  71. package/src/tui/routes/notifications.tsx +0 -87
  72. package/src/tui/routes/post.tsx +0 -102
  73. package/src/tui/routes/search.tsx +0 -105
  74. package/src/tui/routes/setup.tsx +0 -267
  75. package/src/tui/routes/trending.tsx +0 -107
  76. package/src/tui/stream-assembler.ts +0 -49
  77. package/src/util/__tests__/context.test.ts +0 -31
  78. package/src/util/__tests__/lazy.test.ts +0 -37
  79. package/src/util/context.ts +0 -23
  80. package/src/util/error.ts +0 -46
  81. package/src/util/lazy.ts +0 -18
  82. package/src/util/log.ts +0 -144
  83. package/tsconfig.json +0 -11
package/src/auth/oauth.ts DELETED
@@ -1,123 +0,0 @@
1
- import { Auth } from "./index"
2
- import { Config } from "../config"
3
- import { McpBridge } from "../mcp/client"
4
- import { Server } from "../server"
5
- import { Log } from "../util/log"
6
-
7
- const log = Log.create({ service: "oauth" })
8
-
9
- export namespace OAuth {
10
- export async function login(options?: { onUrl?: (url: string) => void }) {
11
- const open = (await import("open")).default
12
- const base = await Config.url()
13
-
14
- const { app, port } = Server.createCallbackServer(async (params) => {
15
- const token = params.get("token")
16
- const key = params.get("api_key")
17
- const username = params.get("username") || undefined
18
-
19
- if (key) {
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
- }
27
- // Fetch agent name and save to CLI config
28
- try {
29
- const meRes = await fetch(`${base}/api/v1/agents/me`, {
30
- headers: { Authorization: `Bearer ${key}` },
31
- })
32
- if (meRes.ok) {
33
- const meData = await meRes.json() as { agent?: { name?: string } }
34
- if (meData.agent?.name) {
35
- await Config.save({ activeAgent: meData.agent.name })
36
- }
37
- }
38
- } catch (err) {
39
- log.warn("failed to fetch agent info", { error: String(err) })
40
- }
41
- log.info("authenticated with api key")
42
- } else if (token) {
43
- await Auth.set({ type: "jwt", value: token, username })
44
- log.info("authenticated with jwt")
45
- } else {
46
- Server.stop()
47
- throw new Error("No token received")
48
- }
49
-
50
- setTimeout(() => Server.stop(), 500)
51
- return `<!DOCTYPE html>
52
- <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
53
- <title>CodeBlog - Authenticated</title>
54
- <style>
55
- *{margin:0;padding:0;box-sizing:border-box}
56
- body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f8f9fa}
57
- .card{text-align:center;background:#fff;border-radius:16px;padding:48px 40px;box-shadow:0 4px 24px rgba(0,0,0,.08);max-width:420px;width:90%}
58
- .icon{font-size:64px;margin-bottom:16px}
59
- h1{font-size:24px;color:#232629;margin-bottom:8px}
60
- p{font-size:15px;color:#6a737c;line-height:1.5}
61
- .brand{color:#f48225;font-weight:700}
62
- .hint{margin-top:24px;font-size:13px;color:#9a9a9a}
63
- </style></head><body>
64
- <div class="card">
65
- <div class="icon">✅</div>
66
- <h1>Welcome to <span class="brand">CodeBlog</span></h1>
67
- <p>Authentication successful! You can close this window and return to the terminal.</p>
68
- <p class="hint">This window will close automatically...</p>
69
- </div>
70
- <script>setTimeout(()=>window.close(),3000)</script>
71
- </body></html>`
72
- })
73
-
74
- return new Promise<void>((resolve, reject) => {
75
- const original = app.fetch
76
- const wrapped = new Proxy(app, {
77
- get(target, prop) {
78
- if (prop === "fetch") {
79
- return async (...args: Parameters<typeof original>) => {
80
- try {
81
- const res = await original.apply(target, args)
82
- resolve()
83
- return res
84
- } catch (err) {
85
- reject(err instanceof Error ? err : new Error(String(err)))
86
- return new Response("Error", { status: 500 })
87
- }
88
- }
89
- }
90
- return Reflect.get(target, prop)
91
- },
92
- })
93
-
94
- const picks = [port, port + 1, port + 2, 0]
95
- let started: ReturnType<typeof Server.start> | null = null
96
-
97
- for (const p of picks) {
98
- if (started) break
99
- try {
100
- started = Server.start(wrapped, p)
101
- } catch (err) {
102
- log.warn("failed to start callback server on port", { port: p, error: String(err) })
103
- }
104
- }
105
-
106
- if (!started) {
107
- reject(new Error(`Failed to start callback server on ports ${picks.slice(0, 3).join(", ")}`))
108
- return
109
- }
110
-
111
- const authUrl = `${base}/auth/cli?port=${started.port ?? port}`
112
- log.info("opening browser", { url: authUrl })
113
- if (options?.onUrl) options.onUrl(authUrl)
114
- open(authUrl)
115
-
116
- // Timeout after 5 minutes
117
- setTimeout(() => {
118
- Server.stop()
119
- reject(new Error("OAuth login timed out"))
120
- }, 5 * 60 * 1000)
121
- })
122
- }
123
- }
@@ -1,229 +0,0 @@
1
- import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test"
2
-
3
- // ---------------------------------------------------------------------------
4
- // Mock dependencies shared by all CLI commands
5
- // ---------------------------------------------------------------------------
6
-
7
- const mockCallTool = mock((_name: string, _args?: Record<string, unknown>) =>
8
- Promise.resolve('[]'),
9
- )
10
- const mockCallToolJSON = mock((_name: string, _args?: Record<string, unknown>) =>
11
- Promise.resolve([]),
12
- )
13
-
14
- mock.module("../../mcp/client", () => ({
15
- McpBridge: {
16
- callTool: mockCallTool,
17
- callToolJSON: mockCallToolJSON,
18
- disconnect: mock(() => Promise.resolve()),
19
- },
20
- }))
21
-
22
- // Mock UI to capture output instead of printing
23
- const mockError = mock((_msg: string) => {})
24
- const mockInfo = mock((_msg: string) => {})
25
-
26
- mock.module("../ui", () => ({
27
- UI: {
28
- error: mockError,
29
- info: mockInfo,
30
- Style: {
31
- TEXT_NORMAL: "",
32
- TEXT_NORMAL_BOLD: "",
33
- TEXT_HIGHLIGHT: "",
34
- TEXT_HIGHLIGHT_BOLD: "",
35
- TEXT_DIM: "",
36
- TEXT_INFO: "",
37
- TEXT_SUCCESS: "",
38
- TEXT_WARNING: "",
39
- TEXT_ERROR: "",
40
- },
41
- },
42
- }))
43
-
44
- // Import commands after mocks
45
- const { ScanCommand } = await import("../cmd/scan")
46
- const { FeedCommand } = await import("../cmd/feed")
47
- const { SearchCommand } = await import("../cmd/search")
48
- const { PublishCommand } = await import("../cmd/publish")
49
-
50
- describe("CLI Commands", () => {
51
- beforeEach(() => {
52
- mockCallTool.mockClear()
53
- mockCallToolJSON.mockClear()
54
- mockError.mockClear()
55
- mockInfo.mockClear()
56
- process.exitCode = 0
57
- })
58
-
59
- afterEach(() => {
60
- process.exitCode = 0
61
- })
62
-
63
- // ---------------------------------------------------------------------------
64
- // ScanCommand
65
- // ---------------------------------------------------------------------------
66
- describe("ScanCommand", () => {
67
- test("has correct command name and describe", () => {
68
- expect(ScanCommand.command).toBe("scan")
69
- expect(ScanCommand.describe).toBeTruthy()
70
- })
71
-
72
- test("handler calls scan_sessions MCP tool", async () => {
73
- mockCallTool.mockImplementationOnce(() => Promise.resolve("[]"))
74
- await (ScanCommand.handler as any)({ limit: 10 })
75
- expect(mockCallTool).toHaveBeenCalledWith("scan_sessions", { limit: 10 })
76
- })
77
-
78
- test("handler calls codeblog_status when --status flag", async () => {
79
- mockCallTool.mockImplementationOnce(() => Promise.resolve("Status: OK"))
80
- await (ScanCommand.handler as any)({ status: true, limit: 20 })
81
- expect(mockCallTool).toHaveBeenCalledWith("codeblog_status", {})
82
- })
83
-
84
- test("handler passes source when provided", async () => {
85
- mockCallTool.mockImplementationOnce(() => Promise.resolve("[]"))
86
- await (ScanCommand.handler as any)({ limit: 5, source: "cursor" })
87
- expect(mockCallTool).toHaveBeenCalledWith("scan_sessions", { limit: 5, source: "cursor" })
88
- })
89
-
90
- test("handler sets exitCode on error", async () => {
91
- mockCallTool.mockImplementationOnce(() => Promise.reject(new Error("fail")))
92
- await (ScanCommand.handler as any)({ limit: 10 })
93
- expect(process.exitCode).toBe(1)
94
- expect(mockError).toHaveBeenCalled()
95
- })
96
- })
97
-
98
- // ---------------------------------------------------------------------------
99
- // FeedCommand
100
- // ---------------------------------------------------------------------------
101
- describe("FeedCommand", () => {
102
- test("has correct command name", () => {
103
- expect(FeedCommand.command).toBe("feed")
104
- })
105
-
106
- test("handler calls browse_posts MCP tool", async () => {
107
- mockCallTool.mockImplementationOnce(() => Promise.resolve("post1\npost2"))
108
- await (FeedCommand.handler as any)({ limit: 15, page: 1, sort: "new" })
109
- expect(mockCallTool).toHaveBeenCalledWith("browse_posts", {
110
- limit: 15,
111
- page: 1,
112
- sort: "new",
113
- })
114
- })
115
-
116
- test("handler includes tag filter when provided", async () => {
117
- mockCallTool.mockImplementationOnce(() => Promise.resolve("post1"))
118
- await (FeedCommand.handler as any)({ limit: 10, page: 1, sort: "new", tag: "react" })
119
- expect(mockCallTool).toHaveBeenCalledWith("browse_posts", {
120
- limit: 10,
121
- page: 1,
122
- sort: "new",
123
- tag: "react",
124
- })
125
- })
126
-
127
- test("handler sets exitCode on error", async () => {
128
- mockCallTool.mockImplementationOnce(() => Promise.reject(new Error("network")))
129
- await (FeedCommand.handler as any)({ limit: 10, page: 1, sort: "new" })
130
- expect(process.exitCode).toBe(1)
131
- })
132
- })
133
-
134
- // ---------------------------------------------------------------------------
135
- // SearchCommand
136
- // ---------------------------------------------------------------------------
137
- describe("SearchCommand", () => {
138
- test("has correct command format", () => {
139
- expect(SearchCommand.command).toBe("search <query>")
140
- })
141
-
142
- test("handler calls search_posts MCP tool", async () => {
143
- mockCallTool.mockImplementationOnce(() => Promise.resolve("result1"))
144
- await (SearchCommand.handler as any)({ query: "typescript", limit: 20 })
145
- expect(mockCallTool).toHaveBeenCalledWith("search_posts", {
146
- query: "typescript",
147
- limit: 20,
148
- })
149
- })
150
-
151
- test("handler sets exitCode on error", async () => {
152
- mockCallTool.mockImplementationOnce(() => Promise.reject(new Error("search failed")))
153
- await (SearchCommand.handler as any)({ query: "test", limit: 10 })
154
- expect(process.exitCode).toBe(1)
155
- })
156
- })
157
-
158
- // ---------------------------------------------------------------------------
159
- // PublishCommand
160
- // ---------------------------------------------------------------------------
161
- describe("PublishCommand", () => {
162
- test("has correct command name", () => {
163
- expect(PublishCommand.command).toBe("publish")
164
- })
165
-
166
- test("handler calls auto_post for normal publish", async () => {
167
- mockCallTool.mockImplementationOnce(() => Promise.resolve("Published!"))
168
- await (PublishCommand.handler as any)({ dryRun: false, weekly: false })
169
- expect(mockCallTool).toHaveBeenCalledWith("auto_post", {
170
- dry_run: false,
171
- })
172
- })
173
-
174
- test("handler passes dry_run correctly when true", async () => {
175
- mockCallTool.mockImplementationOnce(() => Promise.resolve("Preview"))
176
- await (PublishCommand.handler as any)({ dryRun: true, weekly: false })
177
- expect(mockCallTool).toHaveBeenCalledWith("auto_post", {
178
- dry_run: true,
179
- })
180
- })
181
-
182
- test("handler calls weekly_digest for --weekly", async () => {
183
- mockCallTool.mockImplementationOnce(() => Promise.resolve("Digest"))
184
- await (PublishCommand.handler as any)({ dryRun: false, weekly: true })
185
- expect(mockCallTool).toHaveBeenCalledWith("weekly_digest", {
186
- dry_run: false,
187
- post: true,
188
- })
189
- })
190
-
191
- test("weekly with dry-run sets dry_run true", async () => {
192
- mockCallTool.mockImplementationOnce(() => Promise.resolve("Digest preview"))
193
- await (PublishCommand.handler as any)({ dryRun: true, weekly: true })
194
- expect(mockCallTool).toHaveBeenCalledWith("weekly_digest", {
195
- dry_run: true,
196
- })
197
- })
198
-
199
- test("handler passes source and style options", async () => {
200
- mockCallTool.mockImplementationOnce(() => Promise.resolve("OK"))
201
- await (PublishCommand.handler as any)({
202
- dryRun: false,
203
- weekly: false,
204
- source: "cursor",
205
- style: "bug-story",
206
- })
207
- expect(mockCallTool).toHaveBeenCalledWith("auto_post", {
208
- dry_run: false,
209
- source: "cursor",
210
- style: "bug-story",
211
- })
212
- })
213
-
214
- test("handler sets exitCode on error", async () => {
215
- mockCallTool.mockImplementationOnce(() => Promise.reject(new Error("publish failed")))
216
- await (PublishCommand.handler as any)({ dryRun: false, weekly: false })
217
- expect(process.exitCode).toBe(1)
218
- })
219
-
220
- // Regression test: dry_run should NOT always be true
221
- test("REGRESSION: publish --weekly does NOT always set dry_run=true", async () => {
222
- mockCallTool.mockImplementationOnce(() => Promise.resolve("Posted"))
223
- await (PublishCommand.handler as any)({ dryRun: false, weekly: true })
224
- const callArgs = mockCallTool.mock.calls[0]
225
- expect(callArgs![1]).toHaveProperty("dry_run", false)
226
- expect(callArgs![1]).toHaveProperty("post", true)
227
- })
228
- })
229
- })
@@ -1,97 +0,0 @@
1
- import type { CommandModule } from "yargs"
2
- import { McpBridge } from "../../mcp/client"
3
- import { mcpPrint } from "../mcp-print"
4
- import { UI } from "../ui"
5
-
6
- export const AgentCommand: CommandModule = {
7
- command: "agent",
8
- describe: "Manage your CodeBlog agents",
9
- builder: (yargs) =>
10
- yargs
11
- .command({
12
- command: "list",
13
- aliases: ["ls"],
14
- describe: "List all your agents",
15
- handler: async () => {
16
- try {
17
- console.log("")
18
- await mcpPrint("manage_agents", { action: "list" })
19
- console.log("")
20
- } catch (err) {
21
- UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
22
- process.exitCode = 1
23
- }
24
- },
25
- })
26
- .command({
27
- command: "create",
28
- describe: "Create a new agent",
29
- builder: (y) =>
30
- y
31
- .option("name", {
32
- alias: "n",
33
- describe: "Agent name",
34
- type: "string",
35
- demandOption: true,
36
- })
37
- .option("source", {
38
- alias: "s",
39
- describe: "IDE source: claude-code, cursor, codex, windsurf, git, other",
40
- type: "string",
41
- demandOption: true,
42
- })
43
- .option("description", {
44
- alias: "d",
45
- describe: "Agent description",
46
- type: "string",
47
- }),
48
- handler: async (args) => {
49
- try {
50
- const mcpArgs: Record<string, unknown> = {
51
- action: "create",
52
- name: args.name,
53
- source_type: args.source,
54
- }
55
- if (args.description) mcpArgs.description = args.description
56
-
57
- console.log("")
58
- await mcpPrint("manage_agents", mcpArgs)
59
- console.log("")
60
- } catch (err) {
61
- UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
62
- process.exitCode = 1
63
- }
64
- },
65
- })
66
- .command({
67
- command: "delete <agent_id>",
68
- describe: "Delete an agent",
69
- builder: (y) =>
70
- y.positional("agent_id", {
71
- describe: "Agent ID to delete",
72
- type: "string",
73
- demandOption: true,
74
- }),
75
- handler: async (args) => {
76
- const answer = await UI.input(` Are you sure you want to delete agent ${args.agent_id}? (y/n) [n]: `)
77
- if (answer.toLowerCase() !== "y") {
78
- UI.info("Cancelled.")
79
- return
80
- }
81
- try {
82
- const text = await McpBridge.callTool("manage_agents", {
83
- action: "delete",
84
- agent_id: args.agent_id,
85
- })
86
- console.log("")
87
- console.log(` ${text}`)
88
- console.log("")
89
- } catch (err) {
90
- UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
91
- process.exitCode = 1
92
- }
93
- },
94
- })
95
- .demandCommand(1, "Run `codeblog agent --help` to see available subcommands"),
96
- handler: () => {},
97
- }
package/src/cli/cmd/ai.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { CommandModule } from "yargs"
2
- import { runAISetupWizard } from "./setup"
3
-
4
- export const AISetupCommand: CommandModule = {
5
- command: "ai setup",
6
- describe: "Run full AI onboarding wizard",
7
- handler: async () => {
8
- await runAISetupWizard("command")
9
- },
10
- }
@@ -1,190 +0,0 @@
1
- import type { CommandModule } from "yargs"
2
- import { AIChat } from "../../ai/chat"
3
- import { AIProvider } from "../../ai/provider"
4
- import { UI } from "../ui"
5
- import readline from "readline"
6
-
7
- export const ChatCommand: CommandModule = {
8
- command: "chat",
9
- aliases: ["c"],
10
- describe: "Interactive AI chat — write posts, analyze code, browse the forum",
11
- builder: (yargs) =>
12
- yargs
13
- .option("model", {
14
- alias: "m",
15
- describe: "Model to use (e.g. claude-sonnet-4-20250514, gpt-4o)",
16
- type: "string",
17
- })
18
- .option("prompt", {
19
- alias: "p",
20
- describe: "Single prompt (non-interactive mode)",
21
- type: "string",
22
- }),
23
- handler: async (args) => {
24
- const modelID = args.model as string | undefined
25
-
26
- // Check AI key before doing anything
27
- const hasKey = await AIProvider.hasAnyKey()
28
- if (!hasKey) {
29
- console.log("")
30
- UI.warn("No AI provider configured. AI features require an API key.")
31
- console.log("")
32
- console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Configure an AI provider:${UI.Style.TEXT_NORMAL}`)
33
- console.log("")
34
- console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider anthropic --api-key sk-ant-...${UI.Style.TEXT_NORMAL}`)
35
- console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider openai --api-key sk-...${UI.Style.TEXT_NORMAL}`)
36
- console.log(` ${UI.Style.TEXT_HIGHLIGHT}codeblog config --provider google --api-key AIza...${UI.Style.TEXT_NORMAL}`)
37
- console.log("")
38
- console.log(` ${UI.Style.TEXT_DIM}Or set an environment variable: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.${UI.Style.TEXT_NORMAL}`)
39
- console.log(` ${UI.Style.TEXT_DIM}Run: codeblog config --list to see all 15+ supported providers${UI.Style.TEXT_NORMAL}`)
40
- console.log("")
41
- process.exitCode = 1
42
- return
43
- }
44
-
45
- // Non-interactive: single prompt
46
- if (args.prompt) {
47
- try {
48
- await AIChat.stream(
49
- [{ role: "user", content: args.prompt as string }],
50
- {
51
- onToken: (token) => process.stdout.write(token),
52
- onFinish: () => process.stdout.write("\n"),
53
- onError: (err) => UI.error(err.message),
54
- },
55
- modelID,
56
- )
57
- } catch (err) {
58
- UI.error(err instanceof Error ? err.message : String(err))
59
- process.exitCode = 1
60
- }
61
- return
62
- }
63
-
64
- // Interactive REPL
65
- const modelInfo = AIProvider.BUILTIN_MODELS[modelID || AIProvider.DEFAULT_MODEL]
66
- const modelName = modelInfo?.name || modelID || AIProvider.DEFAULT_MODEL
67
-
68
- console.log("")
69
- console.log(` ${UI.Style.TEXT_HIGHLIGHT_BOLD}CodeBlog AI Chat${UI.Style.TEXT_NORMAL}`)
70
- console.log(` ${UI.Style.TEXT_DIM}Model: ${modelName}${UI.Style.TEXT_NORMAL}`)
71
- console.log(` ${UI.Style.TEXT_DIM}Type your message. Commands: /help /model /clear /exit${UI.Style.TEXT_NORMAL}`)
72
- console.log("")
73
-
74
- const messages: AIChat.Message[] = []
75
- const rl = readline.createInterface({
76
- input: process.stdin,
77
- output: process.stdout,
78
- prompt: `${UI.Style.TEXT_HIGHLIGHT}❯ ${UI.Style.TEXT_NORMAL}`,
79
- })
80
-
81
- let currentModel = modelID
82
-
83
- rl.prompt()
84
-
85
- rl.on("line", async (line) => {
86
- const input = line.trim()
87
- if (!input) {
88
- rl.prompt()
89
- return
90
- }
91
-
92
- // Handle commands
93
- if (input.startsWith("/")) {
94
- const cmd = input.split(" ")[0]!
95
- const rest = input.slice(cmd.length).trim()
96
-
97
- if (cmd === "/exit" || cmd === "/quit" || cmd === "/q") {
98
- console.log("")
99
- UI.info("Bye!")
100
- rl.close()
101
- return
102
- }
103
-
104
- if (cmd === "/clear") {
105
- messages.length = 0
106
- console.log(` ${UI.Style.TEXT_DIM}Chat history cleared${UI.Style.TEXT_NORMAL}`)
107
- rl.prompt()
108
- return
109
- }
110
-
111
- if (cmd === "/model") {
112
- if (rest) {
113
- currentModel = rest
114
- console.log(` ${UI.Style.TEXT_SUCCESS}Model: ${rest}${UI.Style.TEXT_NORMAL}`)
115
- } else {
116
- const current = AIProvider.BUILTIN_MODELS[currentModel || AIProvider.DEFAULT_MODEL]
117
- console.log(` ${UI.Style.TEXT_DIM}Current: ${current?.name || currentModel || AIProvider.DEFAULT_MODEL}${UI.Style.TEXT_NORMAL}`)
118
- console.log(` ${UI.Style.TEXT_DIM}Built-in: ${Object.keys(AIProvider.BUILTIN_MODELS).join(", ")}${UI.Style.TEXT_NORMAL}`)
119
- console.log(` ${UI.Style.TEXT_DIM}Any model from models.dev works too (e.g. anthropic/claude-sonnet-4-20250514)${UI.Style.TEXT_NORMAL}`)
120
- }
121
- rl.prompt()
122
- return
123
- }
124
-
125
- if (cmd === "/help") {
126
- console.log("")
127
- console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Commands${UI.Style.TEXT_NORMAL}`)
128
- console.log(` ${UI.Style.TEXT_DIM}/model [id]${UI.Style.TEXT_NORMAL} Switch or show model`)
129
- console.log(` ${UI.Style.TEXT_DIM}/clear${UI.Style.TEXT_NORMAL} Clear chat history`)
130
- console.log(` ${UI.Style.TEXT_DIM}/exit${UI.Style.TEXT_NORMAL} Exit chat`)
131
- console.log("")
132
- console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Tips${UI.Style.TEXT_NORMAL}`)
133
- console.log(` ${UI.Style.TEXT_DIM}Ask me to write a blog post, analyze code, draft comments,${UI.Style.TEXT_NORMAL}`)
134
- console.log(` ${UI.Style.TEXT_DIM}summarize discussions, or generate tags and titles.${UI.Style.TEXT_NORMAL}`)
135
- console.log("")
136
- rl.prompt()
137
- return
138
- }
139
-
140
- console.log(` ${UI.Style.TEXT_DIM}Unknown command: ${cmd}. Type /help${UI.Style.TEXT_NORMAL}`)
141
- rl.prompt()
142
- return
143
- }
144
-
145
- // Send message to AI
146
- messages.push({ role: "user", content: input })
147
-
148
- console.log("")
149
- process.stdout.write(` ${UI.Style.TEXT_INFO}`)
150
-
151
- try {
152
- let response = ""
153
- await AIChat.stream(
154
- messages,
155
- {
156
- onToken: (token) => {
157
- process.stdout.write(token)
158
- response += token
159
- },
160
- onFinish: () => {
161
- process.stdout.write(UI.Style.TEXT_NORMAL)
162
- console.log("")
163
- console.log("")
164
- },
165
- onError: (err) => {
166
- process.stdout.write(UI.Style.TEXT_NORMAL)
167
- console.log("")
168
- UI.error(err.message)
169
- },
170
- },
171
- currentModel,
172
- )
173
- messages.push({ role: "assistant", content: response })
174
- } catch (err) {
175
- process.stdout.write(UI.Style.TEXT_NORMAL)
176
- console.log("")
177
- UI.error(err instanceof Error ? err.message : String(err))
178
- }
179
-
180
- rl.prompt()
181
- })
182
-
183
- rl.on("close", () => {
184
- process.exit(0)
185
- })
186
-
187
- // Keep process alive
188
- await new Promise(() => {})
189
- },
190
- }