codeblog-app 2.2.6 → 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.
- package/package.json +8 -71
- package/drizzle/0000_init.sql +0 -34
- package/drizzle/meta/_journal.json +0 -13
- package/drizzle.config.ts +0 -10
- package/src/ai/__tests__/chat.test.ts +0 -179
- package/src/ai/__tests__/provider.test.ts +0 -198
- package/src/ai/__tests__/tools.test.ts +0 -93
- package/src/ai/chat.ts +0 -224
- package/src/ai/configure.ts +0 -134
- package/src/ai/provider.ts +0 -302
- package/src/ai/tools.ts +0 -114
- package/src/auth/index.ts +0 -47
- package/src/auth/oauth.ts +0 -108
- package/src/cli/__tests__/commands.test.ts +0 -225
- package/src/cli/cmd/agent.ts +0 -97
- package/src/cli/cmd/chat.ts +0 -190
- package/src/cli/cmd/comment.ts +0 -67
- package/src/cli/cmd/config.ts +0 -153
- package/src/cli/cmd/feed.ts +0 -53
- package/src/cli/cmd/forum.ts +0 -106
- package/src/cli/cmd/login.ts +0 -45
- package/src/cli/cmd/logout.ts +0 -12
- package/src/cli/cmd/me.ts +0 -188
- package/src/cli/cmd/post.ts +0 -25
- package/src/cli/cmd/publish.ts +0 -64
- package/src/cli/cmd/scan.ts +0 -78
- package/src/cli/cmd/search.ts +0 -35
- package/src/cli/cmd/setup.ts +0 -352
- package/src/cli/cmd/tui.ts +0 -20
- package/src/cli/cmd/uninstall.ts +0 -281
- package/src/cli/cmd/update.ts +0 -123
- package/src/cli/cmd/vote.ts +0 -50
- package/src/cli/cmd/whoami.ts +0 -18
- package/src/cli/mcp-print.ts +0 -6
- package/src/cli/ui.ts +0 -250
- package/src/config/index.ts +0 -55
- package/src/flag/index.ts +0 -23
- package/src/global/index.ts +0 -38
- package/src/id/index.ts +0 -20
- package/src/index.ts +0 -200
- package/src/mcp/__tests__/client.test.ts +0 -149
- package/src/mcp/__tests__/e2e.ts +0 -327
- package/src/mcp/__tests__/integration.ts +0 -148
- package/src/mcp/client.ts +0 -148
- package/src/server/index.ts +0 -48
- package/src/storage/chat.ts +0 -71
- package/src/storage/db.ts +0 -85
- package/src/storage/schema.sql.ts +0 -39
- package/src/storage/schema.ts +0 -1
- package/src/tui/app.tsx +0 -184
- package/src/tui/commands.ts +0 -186
- package/src/tui/context/exit.tsx +0 -15
- package/src/tui/context/helper.tsx +0 -25
- package/src/tui/context/route.tsx +0 -24
- package/src/tui/context/theme.tsx +0 -470
- package/src/tui/routes/home.tsx +0 -660
- package/src/tui/routes/model.tsx +0 -210
- package/src/tui/routes/notifications.tsx +0 -87
- package/src/tui/routes/post.tsx +0 -102
- package/src/tui/routes/search.tsx +0 -105
- package/src/tui/routes/setup.tsx +0 -255
- package/src/tui/routes/trending.tsx +0 -107
- package/src/util/__tests__/context.test.ts +0 -31
- package/src/util/__tests__/lazy.test.ts +0 -37
- package/src/util/context.ts +0 -23
- package/src/util/error.ts +0 -46
- package/src/util/lazy.ts +0 -18
- package/src/util/log.ts +0 -142
- package/tsconfig.json +0 -11
package/src/ai/tools.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { tool, jsonSchema } from "ai"
|
|
2
|
-
import { McpBridge } from "../mcp/client"
|
|
3
|
-
import { Log } from "../util/log"
|
|
4
|
-
|
|
5
|
-
const log = Log.create({ service: "ai-tools" })
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Tool display labels for the TUI streaming indicator.
|
|
9
|
-
// Kept as a static fallback — new tools added to MCP will show their name
|
|
10
|
-
// as-is if not listed here, which is acceptable.
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
export const TOOL_LABELS: Record<string, string> = {
|
|
13
|
-
scan_sessions: "Scanning IDE sessions...",
|
|
14
|
-
read_session: "Reading session...",
|
|
15
|
-
analyze_session: "Analyzing session...",
|
|
16
|
-
post_to_codeblog: "Publishing post...",
|
|
17
|
-
auto_post: "Auto-posting...",
|
|
18
|
-
weekly_digest: "Generating weekly digest...",
|
|
19
|
-
browse_posts: "Browsing posts...",
|
|
20
|
-
search_posts: "Searching posts...",
|
|
21
|
-
read_post: "Reading post...",
|
|
22
|
-
comment_on_post: "Posting comment...",
|
|
23
|
-
vote_on_post: "Voting...",
|
|
24
|
-
edit_post: "Editing post...",
|
|
25
|
-
delete_post: "Deleting post...",
|
|
26
|
-
bookmark_post: "Bookmarking...",
|
|
27
|
-
browse_by_tag: "Browsing tags...",
|
|
28
|
-
trending_topics: "Loading trending...",
|
|
29
|
-
explore_and_engage: "Exploring posts...",
|
|
30
|
-
join_debate: "Loading debates...",
|
|
31
|
-
my_notifications: "Checking notifications...",
|
|
32
|
-
manage_agents: "Managing agents...",
|
|
33
|
-
my_posts: "Loading your posts...",
|
|
34
|
-
my_dashboard: "Loading dashboard...",
|
|
35
|
-
follow_user: "Processing follow...",
|
|
36
|
-
codeblog_setup: "Configuring CodeBlog...",
|
|
37
|
-
codeblog_status: "Checking status...",
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Helper: call MCP tool and return result
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
async function mcp(name: string, args: Record<string, unknown> = {}): Promise<any> {
|
|
44
|
-
return McpBridge.callToolJSON(name, args)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Strip undefined/null values from args before sending to MCP
|
|
48
|
-
function clean(obj: Record<string, unknown>): Record<string, unknown> {
|
|
49
|
-
const result: Record<string, unknown> = {}
|
|
50
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
51
|
-
if (v !== undefined && v !== null) result[k] = v
|
|
52
|
-
}
|
|
53
|
-
return result
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Schema normalization: ensure all JSON schemas are valid tool input schemas.
|
|
58
|
-
// Some MCP tools have empty inputSchema ({}) which produces schemas without
|
|
59
|
-
// "type": "object", causing providers like DeepSeek/Qwen to reject them.
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
function normalizeToolSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
|
62
|
-
const normalized = { ...schema }
|
|
63
|
-
if (!normalized.type) normalized.type = "object"
|
|
64
|
-
if (normalized.type === "object" && !normalized.properties) normalized.properties = {}
|
|
65
|
-
return normalized
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// Dynamic tool discovery from MCP server
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
let _cached: Record<string, any> | null = null
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Build AI SDK tools dynamically from the MCP server's listTools() response.
|
|
75
|
-
* Results are cached after the first successful call.
|
|
76
|
-
*/
|
|
77
|
-
export async function getChatTools(): Promise<Record<string, any>> {
|
|
78
|
-
if (_cached) return _cached
|
|
79
|
-
|
|
80
|
-
const { tools: mcpTools } = await McpBridge.listTools()
|
|
81
|
-
log.info("discovered MCP tools", { count: mcpTools.length, names: mcpTools.map((t) => t.name) })
|
|
82
|
-
|
|
83
|
-
const tools: Record<string, any> = {}
|
|
84
|
-
|
|
85
|
-
for (const t of mcpTools) {
|
|
86
|
-
const name = t.name
|
|
87
|
-
const rawSchema = (t.inputSchema ?? {}) as Record<string, unknown>
|
|
88
|
-
|
|
89
|
-
tools[name] = tool({
|
|
90
|
-
description: t.description || name,
|
|
91
|
-
inputSchema: jsonSchema(normalizeToolSchema(rawSchema)),
|
|
92
|
-
execute: async (args: any) => {
|
|
93
|
-
log.info("execute tool", { name, args })
|
|
94
|
-
const result = await mcp(name, clean(args))
|
|
95
|
-
const resultStr = typeof result === "string" ? result : JSON.stringify(result)
|
|
96
|
-
log.info("execute tool result", { name, resultType: typeof result, resultLength: resultStr.length, resultPreview: resultStr.slice(0, 300) })
|
|
97
|
-
// Truncate very large tool results to avoid overwhelming the LLM context
|
|
98
|
-
if (resultStr.length > 8000) {
|
|
99
|
-
log.info("truncating large tool result", { name, originalLength: resultStr.length })
|
|
100
|
-
return resultStr.slice(0, 8000) + "\n...(truncated)"
|
|
101
|
-
}
|
|
102
|
-
return resultStr
|
|
103
|
-
},
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
_cached = tools
|
|
108
|
-
return tools
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Clear the cached tools (useful for testing or reconnection). */
|
|
112
|
-
export function clearChatToolsCache(): void {
|
|
113
|
-
_cached = null
|
|
114
|
-
}
|
package/src/auth/index.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import path from "path"
|
|
2
|
-
import { Global } from "../global"
|
|
3
|
-
import z from "zod"
|
|
4
|
-
|
|
5
|
-
export namespace Auth {
|
|
6
|
-
export const Token = z
|
|
7
|
-
.object({
|
|
8
|
-
type: z.enum(["jwt", "apikey"]),
|
|
9
|
-
value: z.string(),
|
|
10
|
-
expires: z.number().optional(),
|
|
11
|
-
username: z.string().optional(),
|
|
12
|
-
})
|
|
13
|
-
.meta({ ref: "AuthToken" })
|
|
14
|
-
export type Token = z.infer<typeof Token>
|
|
15
|
-
|
|
16
|
-
const filepath = path.join(Global.Path.data, "auth.json")
|
|
17
|
-
|
|
18
|
-
export async function get(): Promise<Token | null> {
|
|
19
|
-
const file = Bun.file(filepath)
|
|
20
|
-
const data = await file.json().catch(() => null)
|
|
21
|
-
if (!data) return null
|
|
22
|
-
const parsed = Token.safeParse(data)
|
|
23
|
-
if (!parsed.success) return null
|
|
24
|
-
return parsed.data
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function set(token: Token) {
|
|
28
|
-
await Bun.write(Bun.file(filepath, { mode: 0o600 }), JSON.stringify(token, null, 2))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function remove() {
|
|
32
|
-
const fs = await import("fs/promises")
|
|
33
|
-
await fs.unlink(filepath).catch(() => {})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function header(): Promise<Record<string, string>> {
|
|
37
|
-
const token = await get()
|
|
38
|
-
if (!token) return {}
|
|
39
|
-
if (token.type === "apikey") return { Authorization: `Bearer ${token.value}` }
|
|
40
|
-
return { Authorization: `Bearer ${token.value}` }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function authenticated(): Promise<boolean> {
|
|
44
|
-
const token = await get()
|
|
45
|
-
return token !== null
|
|
46
|
-
}
|
|
47
|
-
}
|
package/src/auth/oauth.ts
DELETED
|
@@ -1,108 +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
|
-
Server.start(wrapped, port)
|
|
95
|
-
|
|
96
|
-
const authUrl = `${base}/auth/cli?port=${port}`
|
|
97
|
-
log.info("opening browser", { url: authUrl })
|
|
98
|
-
if (options?.onUrl) options.onUrl(authUrl)
|
|
99
|
-
open(authUrl)
|
|
100
|
-
|
|
101
|
-
// Timeout after 5 minutes
|
|
102
|
-
setTimeout(() => {
|
|
103
|
-
Server.stop()
|
|
104
|
-
reject(new Error("OAuth login timed out"))
|
|
105
|
-
}, 5 * 60 * 1000)
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,225 +0,0 @@
|
|
|
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
|
-
})
|
package/src/cli/cmd/agent.ts
DELETED
|
@@ -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
|
-
}
|