codeblog-app 1.6.5 → 2.0.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.
- package/package.json +4 -18
- 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 +90 -0
- package/src/ai/chat.ts +14 -14
- package/src/ai/provider.ts +24 -250
- package/src/ai/tools.ts +46 -281
- package/src/auth/oauth.ts +7 -0
- package/src/cli/__tests__/commands.test.ts +225 -0
- package/src/cli/__tests__/setup.test.ts +57 -0
- package/src/cli/cmd/agent.ts +102 -0
- package/src/cli/cmd/chat.ts +1 -1
- package/src/cli/cmd/comment.ts +47 -16
- package/src/cli/cmd/feed.ts +18 -30
- package/src/cli/cmd/forum.ts +123 -0
- package/src/cli/cmd/login.ts +9 -2
- package/src/cli/cmd/me.ts +202 -0
- package/src/cli/cmd/post.ts +6 -88
- package/src/cli/cmd/publish.ts +44 -23
- package/src/cli/cmd/scan.ts +45 -34
- package/src/cli/cmd/search.ts +8 -70
- package/src/cli/cmd/setup.ts +160 -62
- package/src/cli/cmd/vote.ts +29 -14
- package/src/cli/cmd/whoami.ts +7 -36
- package/src/cli/ui.ts +50 -0
- package/src/index.ts +80 -59
- 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/api/agents.ts +0 -103
- package/src/api/bookmarks.ts +0 -25
- package/src/api/client.ts +0 -96
- package/src/api/debates.ts +0 -35
- package/src/api/feed.ts +0 -25
- package/src/api/notifications.ts +0 -31
- package/src/api/posts.ts +0 -116
- package/src/api/search.ts +0 -29
- package/src/api/tags.ts +0 -13
- package/src/api/trending.ts +0 -38
- package/src/api/users.ts +0 -8
- package/src/cli/cmd/agents.ts +0 -77
- package/src/cli/cmd/ai-publish.ts +0 -118
- package/src/cli/cmd/bookmark.ts +0 -27
- package/src/cli/cmd/bookmarks.ts +0 -42
- package/src/cli/cmd/dashboard.ts +0 -59
- package/src/cli/cmd/debate.ts +0 -89
- package/src/cli/cmd/delete.ts +0 -35
- package/src/cli/cmd/edit.ts +0 -42
- package/src/cli/cmd/explore.ts +0 -63
- package/src/cli/cmd/follow.ts +0 -34
- package/src/cli/cmd/myposts.ts +0 -50
- package/src/cli/cmd/notifications.ts +0 -65
- package/src/cli/cmd/tags.ts +0 -58
- package/src/cli/cmd/trending.ts +0 -64
- package/src/cli/cmd/weekly-digest.ts +0 -117
- package/src/publisher/index.ts +0 -139
- package/src/scanner/__tests__/analyzer.test.ts +0 -67
- package/src/scanner/__tests__/fs-utils.test.ts +0 -50
- package/src/scanner/__tests__/platform.test.ts +0 -27
- package/src/scanner/__tests__/registry.test.ts +0 -56
- package/src/scanner/aider.ts +0 -96
- package/src/scanner/analyzer.ts +0 -237
- package/src/scanner/claude-code.ts +0 -188
- package/src/scanner/codex.ts +0 -127
- package/src/scanner/continue-dev.ts +0 -95
- package/src/scanner/cursor.ts +0 -299
- package/src/scanner/fs-utils.ts +0 -123
- package/src/scanner/index.ts +0 -26
- package/src/scanner/platform.ts +0 -44
- package/src/scanner/registry.ts +0 -68
- package/src/scanner/types.ts +0 -62
- package/src/scanner/vscode-copilot.ts +0 -125
- package/src/scanner/warp.ts +0 -19
- package/src/scanner/windsurf.ts +0 -147
- package/src/scanner/zed.ts +0 -88
|
@@ -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,57 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test"
|
|
2
|
+
|
|
3
|
+
// Import the pure extraction functions directly — no mocks needed
|
|
4
|
+
const { extractApiKey, extractUsername } = await import("../cmd/setup")
|
|
5
|
+
|
|
6
|
+
describe("Setup — extractApiKey", () => {
|
|
7
|
+
test("extracts API key from registration response", () => {
|
|
8
|
+
const text =
|
|
9
|
+
"✅ CodeBlog setup complete!\n\n" +
|
|
10
|
+
"Account: alice (alice@example.com)\nAgent: alice-agent\n" +
|
|
11
|
+
"Agent is activated and ready to post.\n\n" +
|
|
12
|
+
"API-KEY: cbk_abc123xyz\n\n" +
|
|
13
|
+
'Try: "Scan my coding sessions and post an insight to CodeBlog."'
|
|
14
|
+
expect(extractApiKey(text)).toBe("cbk_abc123xyz")
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("extracts API key from api_key verification response", () => {
|
|
18
|
+
const text =
|
|
19
|
+
"✅ CodeBlog setup complete!\n\n" +
|
|
20
|
+
"Agent: bob-agent\nOwner: bob\nPosts: 5\n\n" +
|
|
21
|
+
"API-KEY: cbk_existing_key_999\n\n" +
|
|
22
|
+
'Try: "Scan my coding sessions and post an insight to CodeBlog."'
|
|
23
|
+
expect(extractApiKey(text)).toBe("cbk_existing_key_999")
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("returns null when no API-KEY line present", () => {
|
|
27
|
+
const text = "✅ CodeBlog setup complete!\n\nAgent: test-agent\n"
|
|
28
|
+
expect(extractApiKey(text)).toBeNull()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("handles API-KEY with extra whitespace", () => {
|
|
32
|
+
const text = "API-KEY: cbk_spaced_key \nsome other line"
|
|
33
|
+
expect(extractApiKey(text)).toBe("cbk_spaced_key")
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("Setup — extractUsername", () => {
|
|
38
|
+
test("extracts username from Account line (registration)", () => {
|
|
39
|
+
const text = "Account: alice (alice@example.com)\nAgent: alice-agent\n"
|
|
40
|
+
expect(extractUsername(text)).toBe("alice")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("extracts username from Owner line (api_key verification)", () => {
|
|
44
|
+
const text = "Agent: bob-agent\nOwner: bob\nPosts: 5\n"
|
|
45
|
+
expect(extractUsername(text)).toBe("bob")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("prefers Account over Owner when both present", () => {
|
|
49
|
+
const text = "Account: alice (alice@example.com)\nOwner: bob\n"
|
|
50
|
+
expect(extractUsername(text)).toBe("alice")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("returns null when neither Account nor Owner present", () => {
|
|
54
|
+
const text = "✅ CodeBlog setup complete!\nAgent: test-agent\n"
|
|
55
|
+
expect(extractUsername(text)).toBeNull()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { McpBridge } from "../../mcp/client"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
|
|
5
|
+
export const AgentCommand: CommandModule = {
|
|
6
|
+
command: "agent",
|
|
7
|
+
describe: "Manage your CodeBlog agents",
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.command({
|
|
11
|
+
command: "list",
|
|
12
|
+
aliases: ["ls"],
|
|
13
|
+
describe: "List all your agents",
|
|
14
|
+
handler: async () => {
|
|
15
|
+
try {
|
|
16
|
+
const text = await McpBridge.callTool("manage_agents", { action: "list" })
|
|
17
|
+
console.log("")
|
|
18
|
+
for (const line of text.split("\n")) {
|
|
19
|
+
console.log(` ${line}`)
|
|
20
|
+
}
|
|
21
|
+
console.log("")
|
|
22
|
+
} catch (err) {
|
|
23
|
+
UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
24
|
+
process.exitCode = 1
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
.command({
|
|
29
|
+
command: "create",
|
|
30
|
+
describe: "Create a new agent",
|
|
31
|
+
builder: (y) =>
|
|
32
|
+
y
|
|
33
|
+
.option("name", {
|
|
34
|
+
alias: "n",
|
|
35
|
+
describe: "Agent name",
|
|
36
|
+
type: "string",
|
|
37
|
+
demandOption: true,
|
|
38
|
+
})
|
|
39
|
+
.option("source", {
|
|
40
|
+
alias: "s",
|
|
41
|
+
describe: "IDE source: claude-code, cursor, codex, windsurf, git, other",
|
|
42
|
+
type: "string",
|
|
43
|
+
demandOption: true,
|
|
44
|
+
})
|
|
45
|
+
.option("description", {
|
|
46
|
+
alias: "d",
|
|
47
|
+
describe: "Agent description",
|
|
48
|
+
type: "string",
|
|
49
|
+
}),
|
|
50
|
+
handler: async (args) => {
|
|
51
|
+
try {
|
|
52
|
+
const mcpArgs: Record<string, unknown> = {
|
|
53
|
+
action: "create",
|
|
54
|
+
name: args.name,
|
|
55
|
+
source_type: args.source,
|
|
56
|
+
}
|
|
57
|
+
if (args.description) mcpArgs.description = args.description
|
|
58
|
+
|
|
59
|
+
const text = await McpBridge.callTool("manage_agents", mcpArgs)
|
|
60
|
+
console.log("")
|
|
61
|
+
for (const line of text.split("\n")) {
|
|
62
|
+
console.log(` ${line}`)
|
|
63
|
+
}
|
|
64
|
+
console.log("")
|
|
65
|
+
} catch (err) {
|
|
66
|
+
UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
67
|
+
process.exitCode = 1
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
.command({
|
|
72
|
+
command: "delete <agent_id>",
|
|
73
|
+
describe: "Delete an agent",
|
|
74
|
+
builder: (y) =>
|
|
75
|
+
y.positional("agent_id", {
|
|
76
|
+
describe: "Agent ID to delete",
|
|
77
|
+
type: "string",
|
|
78
|
+
demandOption: true,
|
|
79
|
+
}),
|
|
80
|
+
handler: async (args) => {
|
|
81
|
+
const answer = await UI.input(` Are you sure you want to delete agent ${args.agent_id}? (y/n) [n]: `)
|
|
82
|
+
if (answer.toLowerCase() !== "y") {
|
|
83
|
+
UI.info("Cancelled.")
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const text = await McpBridge.callTool("manage_agents", {
|
|
88
|
+
action: "delete",
|
|
89
|
+
agent_id: args.agent_id,
|
|
90
|
+
})
|
|
91
|
+
console.log("")
|
|
92
|
+
console.log(` ${text}`)
|
|
93
|
+
console.log("")
|
|
94
|
+
} catch (err) {
|
|
95
|
+
UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
96
|
+
process.exitCode = 1
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
.demandCommand(1, "Run `codeblog agent --help` to see available subcommands"),
|
|
101
|
+
handler: () => {},
|
|
102
|
+
}
|
package/src/cli/cmd/chat.ts
CHANGED
|
@@ -91,7 +91,7 @@ export const ChatCommand: CommandModule = {
|
|
|
91
91
|
|
|
92
92
|
// Handle commands
|
|
93
93
|
if (input.startsWith("/")) {
|
|
94
|
-
const cmd = input.split(" ")[0]
|
|
94
|
+
const cmd = input.split(" ")[0]!
|
|
95
95
|
const rest = input.slice(cmd.length).trim()
|
|
96
96
|
|
|
97
97
|
if (cmd === "/exit" || cmd === "/quit" || cmd === "/q") {
|
package/src/cli/cmd/comment.ts
CHANGED
|
@@ -1,38 +1,69 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
|
-
import {
|
|
2
|
+
import { McpBridge } from "../../mcp/client"
|
|
3
3
|
import { UI } from "../ui"
|
|
4
4
|
|
|
5
5
|
export const CommentCommand: CommandModule = {
|
|
6
|
-
command: "comment <
|
|
6
|
+
command: "comment <post_id>",
|
|
7
7
|
describe: "Comment on a post",
|
|
8
8
|
builder: (yargs) =>
|
|
9
9
|
yargs
|
|
10
|
-
.positional("
|
|
10
|
+
.positional("post_id", {
|
|
11
11
|
describe: "Post ID to comment on",
|
|
12
12
|
type: "string",
|
|
13
13
|
demandOption: true,
|
|
14
14
|
})
|
|
15
|
-
.option("
|
|
16
|
-
alias: "
|
|
17
|
-
describe: "
|
|
15
|
+
.option("reply", {
|
|
16
|
+
alias: "r",
|
|
17
|
+
describe: "Reply to a specific comment by its ID",
|
|
18
18
|
type: "string",
|
|
19
19
|
}),
|
|
20
20
|
handler: async (args) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.")
|
|
28
49
|
return
|
|
29
50
|
}
|
|
30
51
|
|
|
31
52
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
const text = await McpBridge.callTool("comment_on_post", mcpArgs)
|
|
60
|
+
console.log("")
|
|
61
|
+
for (const line of text.split("\n")) {
|
|
62
|
+
console.log(` ${line}`)
|
|
63
|
+
}
|
|
64
|
+
console.log("")
|
|
34
65
|
} catch (err) {
|
|
35
|
-
UI.error(`
|
|
66
|
+
UI.error(`Comment failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
36
67
|
process.exitCode = 1
|
|
37
68
|
}
|
|
38
69
|
},
|
package/src/cli/cmd/feed.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
|
-
import {
|
|
2
|
+
import { McpBridge } from "../../mcp/client"
|
|
3
3
|
import { UI } from "../ui"
|
|
4
4
|
|
|
5
5
|
export const FeedCommand: CommandModule = {
|
|
@@ -20,47 +20,35 @@ export const FeedCommand: CommandModule = {
|
|
|
20
20
|
.option("tag", {
|
|
21
21
|
describe: "Filter by tag",
|
|
22
22
|
type: "string",
|
|
23
|
+
})
|
|
24
|
+
.option("sort", {
|
|
25
|
+
describe: "Sort: new, hot, top",
|
|
26
|
+
type: "string",
|
|
27
|
+
default: "new",
|
|
23
28
|
}),
|
|
24
29
|
handler: async (args) => {
|
|
25
30
|
try {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
if (result.posts.length === 0) {
|
|
33
|
-
UI.info("No posts found.")
|
|
34
|
-
return
|
|
31
|
+
const mcpArgs: Record<string, unknown> = {
|
|
32
|
+
limit: args.limit,
|
|
33
|
+
page: args.page,
|
|
34
|
+
sort: args.sort,
|
|
35
35
|
}
|
|
36
|
+
if (args.tag) mcpArgs.tag = args.tag
|
|
37
|
+
|
|
38
|
+
const text = await McpBridge.callTool("browse_posts", mcpArgs)
|
|
36
39
|
|
|
37
40
|
const tagFilter = args.tag ? ` ${UI.Style.TEXT_INFO}#${args.tag}${UI.Style.TEXT_NORMAL}` : ""
|
|
38
41
|
console.log("")
|
|
39
42
|
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Posts${UI.Style.TEXT_NORMAL}${tagFilter} ${UI.Style.TEXT_DIM}page ${args.page}${UI.Style.TEXT_NORMAL}`)
|
|
40
43
|
console.log("")
|
|
41
44
|
|
|
42
|
-
for (const
|
|
43
|
-
|
|
44
|
-
const scoreColor = score > 0 ? UI.Style.TEXT_SUCCESS : score < 0 ? UI.Style.TEXT_DANGER : UI.Style.TEXT_DIM
|
|
45
|
-
const votes = `${scoreColor}${score > 0 ? "+" : ""}${score}${UI.Style.TEXT_NORMAL}`
|
|
46
|
-
const comments = `${UI.Style.TEXT_DIM}💬 ${post.comment_count}${UI.Style.TEXT_NORMAL}`
|
|
47
|
-
const tags = post.tags.slice(0, 4).map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")
|
|
48
|
-
const author = `${UI.Style.TEXT_DIM}${post.author.name}${UI.Style.TEXT_NORMAL}`
|
|
49
|
-
const lang = post.language && post.language !== "English" ? ` ${UI.Style.TEXT_INFO}[${post.language}]${UI.Style.TEXT_NORMAL}` : ""
|
|
50
|
-
const date = new Date(post.created_at).toLocaleDateString()
|
|
51
|
-
|
|
52
|
-
console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${post.title}${UI.Style.TEXT_NORMAL}${lang}`)
|
|
53
|
-
if (post.summary) {
|
|
54
|
-
console.log(` ${UI.Style.TEXT_DIM}${post.summary.slice(0, 100)}${UI.Style.TEXT_NORMAL}`)
|
|
55
|
-
}
|
|
56
|
-
console.log(` ${comments} ${tags} ${author} ${UI.Style.TEXT_DIM}${date}${UI.Style.TEXT_NORMAL}`)
|
|
57
|
-
console.log(` ${UI.Style.TEXT_DIM}${post.id}${UI.Style.TEXT_NORMAL}`)
|
|
58
|
-
console.log("")
|
|
45
|
+
for (const line of text.split("\n")) {
|
|
46
|
+
console.log(` ${line}`)
|
|
59
47
|
}
|
|
48
|
+
console.log("")
|
|
60
49
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
50
|
+
console.log(` ${UI.Style.TEXT_DIM}Next page: codeblog feed --page ${(args.page as number) + 1}${UI.Style.TEXT_NORMAL}`)
|
|
51
|
+
console.log("")
|
|
64
52
|
} catch (err) {
|
|
65
53
|
UI.error(`Failed to fetch feed: ${err instanceof Error ? err.message : String(err)}`)
|
|
66
54
|
process.exitCode = 1
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { McpBridge } from "../../mcp/client"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
|
|
5
|
+
export const ForumCommand: CommandModule = {
|
|
6
|
+
command: "forum",
|
|
7
|
+
describe: "Discover: trending, tags, debates",
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.command({
|
|
11
|
+
command: "trending",
|
|
12
|
+
aliases: ["hot"],
|
|
13
|
+
describe: "Top posts, most discussed, active agents, trending tags",
|
|
14
|
+
handler: async () => {
|
|
15
|
+
try {
|
|
16
|
+
const text = await McpBridge.callTool("trending_topics")
|
|
17
|
+
console.log("")
|
|
18
|
+
for (const line of text.split("\n")) {
|
|
19
|
+
console.log(` ${line}`)
|
|
20
|
+
}
|
|
21
|
+
console.log("")
|
|
22
|
+
} catch (err) {
|
|
23
|
+
UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
24
|
+
process.exitCode = 1
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
.command({
|
|
29
|
+
command: "tags",
|
|
30
|
+
describe: "Browse trending tags or posts by tag",
|
|
31
|
+
builder: (y) =>
|
|
32
|
+
y.option("tag", {
|
|
33
|
+
alias: "t",
|
|
34
|
+
describe: "Filter posts by this tag",
|
|
35
|
+
type: "string",
|
|
36
|
+
}),
|
|
37
|
+
handler: async (args) => {
|
|
38
|
+
try {
|
|
39
|
+
if (args.tag) {
|
|
40
|
+
const text = await McpBridge.callTool("browse_by_tag", {
|
|
41
|
+
action: "posts",
|
|
42
|
+
tag: args.tag,
|
|
43
|
+
})
|
|
44
|
+
console.log("")
|
|
45
|
+
for (const line of text.split("\n")) {
|
|
46
|
+
console.log(` ${line}`)
|
|
47
|
+
}
|
|
48
|
+
console.log("")
|
|
49
|
+
} else {
|
|
50
|
+
const text = await McpBridge.callTool("browse_by_tag", { action: "trending" })
|
|
51
|
+
console.log("")
|
|
52
|
+
for (const line of text.split("\n")) {
|
|
53
|
+
console.log(` ${line}`)
|
|
54
|
+
}
|
|
55
|
+
console.log("")
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
59
|
+
process.exitCode = 1
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
.command({
|
|
64
|
+
command: "debates",
|
|
65
|
+
aliases: ["debate"],
|
|
66
|
+
describe: "Tech Arena: list or create debates",
|
|
67
|
+
builder: (y) =>
|
|
68
|
+
y
|
|
69
|
+
.option("create", {
|
|
70
|
+
describe: "Create a new debate",
|
|
71
|
+
type: "boolean",
|
|
72
|
+
default: false,
|
|
73
|
+
})
|
|
74
|
+
.option("title", {
|
|
75
|
+
describe: "Debate title (for create)",
|
|
76
|
+
type: "string",
|
|
77
|
+
})
|
|
78
|
+
.option("pro", {
|
|
79
|
+
describe: "Pro side label (for create)",
|
|
80
|
+
type: "string",
|
|
81
|
+
})
|
|
82
|
+
.option("con", {
|
|
83
|
+
describe: "Con side label (for create)",
|
|
84
|
+
type: "string",
|
|
85
|
+
}),
|
|
86
|
+
handler: async (args) => {
|
|
87
|
+
try {
|
|
88
|
+
if (args.create) {
|
|
89
|
+
if (!args.title || !args.pro || !args.con) {
|
|
90
|
+
UI.error("--title, --pro, and --con are required for creating a debate.")
|
|
91
|
+
process.exitCode = 1
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const text = await McpBridge.callTool("join_debate", {
|
|
95
|
+
action: "create",
|
|
96
|
+
title: args.title,
|
|
97
|
+
pro_label: args.pro,
|
|
98
|
+
con_label: args.con,
|
|
99
|
+
})
|
|
100
|
+
console.log("")
|
|
101
|
+
for (const line of text.split("\n")) {
|
|
102
|
+
console.log(` ${line}`)
|
|
103
|
+
}
|
|
104
|
+
console.log("")
|
|
105
|
+
} else {
|
|
106
|
+
const text = await McpBridge.callTool("join_debate", { action: "list" })
|
|
107
|
+
console.log("")
|
|
108
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Tech Arena — Active Debates${UI.Style.TEXT_NORMAL}`)
|
|
109
|
+
console.log("")
|
|
110
|
+
for (const line of text.split("\n")) {
|
|
111
|
+
console.log(` ${line}`)
|
|
112
|
+
}
|
|
113
|
+
console.log("")
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
UI.error(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
117
|
+
process.exitCode = 1
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
.demandCommand(1, "Run `codeblog forum --help` to see available subcommands"),
|
|
122
|
+
handler: () => {},
|
|
123
|
+
}
|