codeblog-app 2.1.2 → 2.1.4
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.
- package/drizzle/0000_init.sql +34 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/package.json +71 -8
- package/src/ai/__tests__/chat.test.ts +110 -0
- package/src/ai/__tests__/provider.test.ts +184 -0
- package/src/ai/__tests__/tools.test.ts +54 -0
- package/src/ai/chat.ts +170 -0
- package/src/ai/configure.ts +134 -0
- package/src/ai/provider.ts +238 -0
- package/src/ai/tools.ts +91 -0
- package/src/auth/index.ts +47 -0
- package/src/auth/oauth.ts +94 -0
- package/src/cli/__tests__/commands.test.ts +225 -0
- package/src/cli/cmd/agent.ts +97 -0
- package/src/cli/cmd/chat.ts +190 -0
- package/src/cli/cmd/comment.ts +67 -0
- package/src/cli/cmd/config.ts +153 -0
- package/src/cli/cmd/feed.ts +53 -0
- package/src/cli/cmd/forum.ts +106 -0
- package/src/cli/cmd/login.ts +45 -0
- package/src/cli/cmd/logout.ts +12 -0
- package/src/cli/cmd/me.ts +188 -0
- package/src/cli/cmd/post.ts +25 -0
- package/src/cli/cmd/publish.ts +64 -0
- package/src/cli/cmd/scan.ts +78 -0
- package/src/cli/cmd/search.ts +35 -0
- package/src/cli/cmd/setup.ts +273 -0
- package/src/cli/cmd/tui.ts +20 -0
- package/src/cli/cmd/uninstall.ts +156 -0
- package/src/cli/cmd/update.ts +123 -0
- package/src/cli/cmd/vote.ts +50 -0
- package/src/cli/cmd/whoami.ts +18 -0
- package/src/cli/mcp-print.ts +6 -0
- package/src/cli/ui.ts +195 -0
- package/src/config/index.ts +54 -0
- package/src/flag/index.ts +23 -0
- package/src/global/index.ts +38 -0
- package/src/id/index.ts +20 -0
- package/src/index.ts +200 -0
- package/src/mcp/__tests__/client.test.ts +149 -0
- package/src/mcp/__tests__/e2e.ts +327 -0
- package/src/mcp/__tests__/integration.ts +148 -0
- package/src/mcp/client.ts +148 -0
- package/src/server/index.ts +48 -0
- package/src/storage/chat.ts +71 -0
- package/src/storage/db.ts +85 -0
- package/src/storage/schema.sql.ts +39 -0
- package/src/storage/schema.ts +1 -0
- package/src/tui/app.tsx +179 -0
- package/src/tui/commands.ts +187 -0
- package/src/tui/context/exit.tsx +15 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/route.tsx +24 -0
- package/src/tui/context/theme.tsx +470 -0
- package/src/tui/routes/home.tsx +508 -0
- package/src/tui/routes/model.tsx +207 -0
- package/src/tui/routes/notifications.tsx +87 -0
- package/src/tui/routes/post.tsx +102 -0
- package/src/tui/routes/search.tsx +105 -0
- package/src/tui/routes/setup.tsx +255 -0
- package/src/tui/routes/trending.tsx +107 -0
- package/src/util/__tests__/context.test.ts +31 -0
- package/src/util/__tests__/lazy.test.ts +37 -0
- package/src/util/context.ts +23 -0
- package/src/util/error.ts +46 -0
- package/src/util/lazy.ts +18 -0
- package/src/util/log.ts +142 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, test, expect, mock, beforeEach } 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 = undefined as any
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// ScanCommand
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
describe("ScanCommand", () => {
|
|
63
|
+
test("has correct command name and describe", () => {
|
|
64
|
+
expect(ScanCommand.command).toBe("scan")
|
|
65
|
+
expect(ScanCommand.describe).toBeTruthy()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("handler calls scan_sessions MCP tool", async () => {
|
|
69
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("[]"))
|
|
70
|
+
await (ScanCommand.handler as any)({ limit: 10 })
|
|
71
|
+
expect(mockCallTool).toHaveBeenCalledWith("scan_sessions", { limit: 10 })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("handler calls codeblog_status when --status flag", async () => {
|
|
75
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("Status: OK"))
|
|
76
|
+
await (ScanCommand.handler as any)({ status: true, limit: 20 })
|
|
77
|
+
expect(mockCallTool).toHaveBeenCalledWith("codeblog_status")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("handler passes source when provided", async () => {
|
|
81
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("[]"))
|
|
82
|
+
await (ScanCommand.handler as any)({ limit: 5, source: "cursor" })
|
|
83
|
+
expect(mockCallTool).toHaveBeenCalledWith("scan_sessions", { limit: 5, source: "cursor" })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("handler sets exitCode on error", async () => {
|
|
87
|
+
mockCallTool.mockImplementationOnce(() => Promise.reject(new Error("fail")))
|
|
88
|
+
await (ScanCommand.handler as any)({ limit: 10 })
|
|
89
|
+
expect(process.exitCode).toBe(1)
|
|
90
|
+
expect(mockError).toHaveBeenCalled()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// FeedCommand
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
describe("FeedCommand", () => {
|
|
98
|
+
test("has correct command name", () => {
|
|
99
|
+
expect(FeedCommand.command).toBe("feed")
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test("handler calls browse_posts MCP tool", async () => {
|
|
103
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("post1\npost2"))
|
|
104
|
+
await (FeedCommand.handler as any)({ limit: 15, page: 1, sort: "new" })
|
|
105
|
+
expect(mockCallTool).toHaveBeenCalledWith("browse_posts", {
|
|
106
|
+
limit: 15,
|
|
107
|
+
page: 1,
|
|
108
|
+
sort: "new",
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test("handler includes tag filter when provided", async () => {
|
|
113
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("post1"))
|
|
114
|
+
await (FeedCommand.handler as any)({ limit: 10, page: 1, sort: "new", tag: "react" })
|
|
115
|
+
expect(mockCallTool).toHaveBeenCalledWith("browse_posts", {
|
|
116
|
+
limit: 10,
|
|
117
|
+
page: 1,
|
|
118
|
+
sort: "new",
|
|
119
|
+
tag: "react",
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("handler sets exitCode on error", async () => {
|
|
124
|
+
mockCallTool.mockImplementationOnce(() => Promise.reject(new Error("network")))
|
|
125
|
+
await (FeedCommand.handler as any)({ limit: 10, page: 1, sort: "new" })
|
|
126
|
+
expect(process.exitCode).toBe(1)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// SearchCommand
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
describe("SearchCommand", () => {
|
|
134
|
+
test("has correct command format", () => {
|
|
135
|
+
expect(SearchCommand.command).toBe("search <query>")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("handler calls search_posts MCP tool", async () => {
|
|
139
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("result1"))
|
|
140
|
+
await (SearchCommand.handler as any)({ query: "typescript", limit: 20 })
|
|
141
|
+
expect(mockCallTool).toHaveBeenCalledWith("search_posts", {
|
|
142
|
+
query: "typescript",
|
|
143
|
+
limit: 20,
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test("handler sets exitCode on error", async () => {
|
|
148
|
+
mockCallTool.mockImplementationOnce(() => Promise.reject(new Error("search failed")))
|
|
149
|
+
await (SearchCommand.handler as any)({ query: "test", limit: 10 })
|
|
150
|
+
expect(process.exitCode).toBe(1)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// PublishCommand
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
describe("PublishCommand", () => {
|
|
158
|
+
test("has correct command name", () => {
|
|
159
|
+
expect(PublishCommand.command).toBe("publish")
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test("handler calls auto_post for normal publish", async () => {
|
|
163
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("Published!"))
|
|
164
|
+
await (PublishCommand.handler as any)({ dryRun: false, weekly: false })
|
|
165
|
+
expect(mockCallTool).toHaveBeenCalledWith("auto_post", {
|
|
166
|
+
dry_run: false,
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test("handler passes dry_run correctly when true", async () => {
|
|
171
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("Preview"))
|
|
172
|
+
await (PublishCommand.handler as any)({ dryRun: true, weekly: false })
|
|
173
|
+
expect(mockCallTool).toHaveBeenCalledWith("auto_post", {
|
|
174
|
+
dry_run: true,
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test("handler calls weekly_digest for --weekly", async () => {
|
|
179
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("Digest"))
|
|
180
|
+
await (PublishCommand.handler as any)({ dryRun: false, weekly: true })
|
|
181
|
+
expect(mockCallTool).toHaveBeenCalledWith("weekly_digest", {
|
|
182
|
+
dry_run: false,
|
|
183
|
+
post: true,
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test("weekly with dry-run sets dry_run true", async () => {
|
|
188
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("Digest preview"))
|
|
189
|
+
await (PublishCommand.handler as any)({ dryRun: true, weekly: true })
|
|
190
|
+
expect(mockCallTool).toHaveBeenCalledWith("weekly_digest", {
|
|
191
|
+
dry_run: true,
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test("handler passes source and style options", async () => {
|
|
196
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("OK"))
|
|
197
|
+
await (PublishCommand.handler as any)({
|
|
198
|
+
dryRun: false,
|
|
199
|
+
weekly: false,
|
|
200
|
+
source: "cursor",
|
|
201
|
+
style: "bug-story",
|
|
202
|
+
})
|
|
203
|
+
expect(mockCallTool).toHaveBeenCalledWith("auto_post", {
|
|
204
|
+
dry_run: false,
|
|
205
|
+
source: "cursor",
|
|
206
|
+
style: "bug-story",
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test("handler sets exitCode on error", async () => {
|
|
211
|
+
mockCallTool.mockImplementationOnce(() => Promise.reject(new Error("publish failed")))
|
|
212
|
+
await (PublishCommand.handler as any)({ dryRun: false, weekly: false })
|
|
213
|
+
expect(process.exitCode).toBe(1)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Regression test: dry_run should NOT always be true
|
|
217
|
+
test("REGRESSION: publish --weekly does NOT always set dry_run=true", async () => {
|
|
218
|
+
mockCallTool.mockImplementationOnce(() => Promise.resolve("Posted"))
|
|
219
|
+
await (PublishCommand.handler as any)({ dryRun: false, weekly: true })
|
|
220
|
+
const callArgs = mockCallTool.mock.calls[0]
|
|
221
|
+
expect(callArgs![1]).toHaveProperty("dry_run", false)
|
|
222
|
+
expect(callArgs![1]).toHaveProperty("post", true)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { mcpPrint } from "../mcp-print"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
|
|
5
|
+
export const CommentCommand: CommandModule = {
|
|
6
|
+
command: "comment <post_id>",
|
|
7
|
+
describe: "Comment on a post",
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.positional("post_id", {
|
|
11
|
+
describe: "Post ID to comment on",
|
|
12
|
+
type: "string",
|
|
13
|
+
demandOption: true,
|
|
14
|
+
})
|
|
15
|
+
.option("reply", {
|
|
16
|
+
alias: "r",
|
|
17
|
+
describe: "Reply to a specific comment by its ID",
|
|
18
|
+
type: "string",
|
|
19
|
+
}),
|
|
20
|
+
handler: async (args) => {
|
|
21
|
+
const postId = args.post_id as string
|
|
22
|
+
|
|
23
|
+
console.log("")
|
|
24
|
+
console.log(` ${UI.Style.TEXT_DIM}Write your comment (end with an empty line):${UI.Style.TEXT_NORMAL}`)
|
|
25
|
+
console.log("")
|
|
26
|
+
|
|
27
|
+
// Read multiline input
|
|
28
|
+
const lines: string[] = []
|
|
29
|
+
const readline = require("readline")
|
|
30
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
31
|
+
|
|
32
|
+
const content = await new Promise<string>((resolve) => {
|
|
33
|
+
rl.on("line", (line: string) => {
|
|
34
|
+
if (line === "" && lines.length > 0) {
|
|
35
|
+
rl.close()
|
|
36
|
+
resolve(lines.join("\n"))
|
|
37
|
+
} else {
|
|
38
|
+
lines.push(line)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
rl.on("close", () => {
|
|
42
|
+
resolve(lines.join("\n"))
|
|
43
|
+
})
|
|
44
|
+
rl.prompt()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (!content.trim()) {
|
|
48
|
+
UI.warn("Empty comment, skipped.")
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const mcpArgs: Record<string, unknown> = {
|
|
54
|
+
post_id: postId,
|
|
55
|
+
content: content.trim(),
|
|
56
|
+
}
|
|
57
|
+
if (args.reply) mcpArgs.parent_id = args.reply
|
|
58
|
+
|
|
59
|
+
console.log("")
|
|
60
|
+
await mcpPrint("comment_on_post", mcpArgs)
|
|
61
|
+
console.log("")
|
|
62
|
+
} catch (err) {
|
|
63
|
+
UI.error(`Comment failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
64
|
+
process.exitCode = 1
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|