codeblog-app 0.1.0 → 0.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 +7 -1
- package/src/ai/chat.ts +92 -0
- package/src/ai/provider.ts +117 -0
- package/src/api/agents.ts +68 -0
- package/src/api/bookmarks.ts +25 -0
- package/src/api/debates.ts +35 -0
- package/src/api/notifications.ts +7 -0
- package/src/api/search.ts +22 -6
- package/src/api/users.ts +8 -0
- package/src/cli/cmd/agents.ts +77 -0
- package/src/cli/cmd/ai-publish.ts +95 -0
- package/src/cli/cmd/bookmarks.ts +42 -0
- package/src/cli/cmd/chat.ts +175 -0
- package/src/cli/cmd/config.ts +96 -0
- package/src/cli/cmd/dashboard.ts +26 -13
- package/src/cli/cmd/debate.ts +89 -0
- package/src/cli/cmd/delete.ts +35 -0
- package/src/cli/cmd/edit.ts +42 -0
- package/src/cli/cmd/follow.ts +34 -0
- package/src/cli/cmd/myposts.ts +50 -0
- package/src/cli/cmd/notifications.ts +37 -5
- package/src/cli/cmd/search.ts +65 -12
- package/src/index.ts +22 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "codeblog-app",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -56,9 +56,15 @@
|
|
|
56
56
|
"typescript": "5.8.2"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@ai-sdk/anthropic": "^3.0.44",
|
|
60
|
+
"@ai-sdk/google": "^3.0.29",
|
|
61
|
+
"@ai-sdk/openai": "^3.0.29",
|
|
62
|
+
"ai": "^6.0.86",
|
|
59
63
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
60
64
|
"hono": "4.10.7",
|
|
65
|
+
"ink": "^6.7.0",
|
|
61
66
|
"open": "10.1.2",
|
|
67
|
+
"react": "^19.2.4",
|
|
62
68
|
"xdg-basedir": "5.1.0",
|
|
63
69
|
"yargs": "18.0.0",
|
|
64
70
|
"zod": "4.1.8"
|
package/src/ai/chat.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { streamText, type CoreMessage } from "ai"
|
|
2
|
+
import { AIProvider } from "./provider"
|
|
3
|
+
import { Log } from "../util/log"
|
|
4
|
+
|
|
5
|
+
const log = Log.create({ service: "ai-chat" })
|
|
6
|
+
|
|
7
|
+
export namespace AIChat {
|
|
8
|
+
export interface Message {
|
|
9
|
+
role: "user" | "assistant" | "system"
|
|
10
|
+
content: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StreamCallbacks {
|
|
14
|
+
onToken?: (token: string) => void
|
|
15
|
+
onFinish?: (text: string) => void
|
|
16
|
+
onError?: (error: Error) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SYSTEM_PROMPT = `You are CodeBlog AI — an assistant for the CodeBlog developer forum (codeblog.ai).
|
|
20
|
+
|
|
21
|
+
You help developers:
|
|
22
|
+
- Write engaging blog posts from their coding sessions
|
|
23
|
+
- Analyze code and explain technical concepts
|
|
24
|
+
- Draft comments and debate arguments
|
|
25
|
+
- Summarize posts and discussions
|
|
26
|
+
- Generate tags and titles for posts
|
|
27
|
+
|
|
28
|
+
Write casually like a dev talking to another dev. Be specific, opinionated, and genuine.
|
|
29
|
+
Use code examples when relevant. Think Juejin / HN / Linux.do vibes — not a conference paper.`
|
|
30
|
+
|
|
31
|
+
export async function stream(messages: Message[], callbacks: StreamCallbacks, modelID?: string) {
|
|
32
|
+
const model = await AIProvider.getModel(modelID)
|
|
33
|
+
|
|
34
|
+
log.info("streaming", { model: modelID || AIProvider.DEFAULT_MODEL, messages: messages.length })
|
|
35
|
+
|
|
36
|
+
const coreMessages: CoreMessage[] = messages.map((m) => ({
|
|
37
|
+
role: m.role,
|
|
38
|
+
content: m.content,
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
const result = streamText({
|
|
42
|
+
model,
|
|
43
|
+
system: SYSTEM_PROMPT,
|
|
44
|
+
messages: coreMessages,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
let full = ""
|
|
48
|
+
for await (const chunk of result.textStream) {
|
|
49
|
+
full += chunk
|
|
50
|
+
callbacks.onToken?.(chunk)
|
|
51
|
+
}
|
|
52
|
+
callbacks.onFinish?.(full)
|
|
53
|
+
return full
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function generate(prompt: string, modelID?: string): Promise<string> {
|
|
57
|
+
let result = ""
|
|
58
|
+
await stream([{ role: "user", content: prompt }], { onFinish: (text) => (result = text) }, modelID)
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function analyzeAndPost(sessionContent: string, modelID?: string): Promise<{ title: string; content: string; tags: string[]; summary: string }> {
|
|
63
|
+
const prompt = `Analyze this coding session and write a blog post about it.
|
|
64
|
+
|
|
65
|
+
The post should:
|
|
66
|
+
- Have a catchy, dev-friendly title (like HN or Juejin)
|
|
67
|
+
- Tell a story: what you were doing, what went wrong/right, what you learned
|
|
68
|
+
- Include relevant code snippets
|
|
69
|
+
- Be casual and genuine, written in first person
|
|
70
|
+
- End with key takeaways
|
|
71
|
+
|
|
72
|
+
Also provide:
|
|
73
|
+
- 3-8 relevant tags (lowercase, hyphenated)
|
|
74
|
+
- A one-line summary/hook
|
|
75
|
+
|
|
76
|
+
Session content:
|
|
77
|
+
${sessionContent.slice(0, 50000)}
|
|
78
|
+
|
|
79
|
+
Respond in this exact JSON format:
|
|
80
|
+
{
|
|
81
|
+
"title": "...",
|
|
82
|
+
"content": "... (markdown)",
|
|
83
|
+
"tags": ["tag1", "tag2"],
|
|
84
|
+
"summary": "..."
|
|
85
|
+
}`
|
|
86
|
+
|
|
87
|
+
const raw = await generate(prompt, modelID)
|
|
88
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/)
|
|
89
|
+
if (!jsonMatch) throw new Error("AI did not return valid JSON")
|
|
90
|
+
return JSON.parse(jsonMatch[0])
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createAnthropic } from "@ai-sdk/anthropic"
|
|
2
|
+
import { createOpenAI } from "@ai-sdk/openai"
|
|
3
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
|
4
|
+
import { type LanguageModel } from "ai"
|
|
5
|
+
import { Config } from "../config"
|
|
6
|
+
import { Log } from "../util/log"
|
|
7
|
+
|
|
8
|
+
const log = Log.create({ service: "ai-provider" })
|
|
9
|
+
|
|
10
|
+
export namespace AIProvider {
|
|
11
|
+
export type ProviderID = "anthropic" | "openai" | "google"
|
|
12
|
+
|
|
13
|
+
export interface ModelInfo {
|
|
14
|
+
id: string
|
|
15
|
+
providerID: ProviderID
|
|
16
|
+
name: string
|
|
17
|
+
contextWindow: number
|
|
18
|
+
outputTokens: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const MODELS: Record<string, ModelInfo> = {
|
|
22
|
+
"claude-sonnet-4-20250514": {
|
|
23
|
+
id: "claude-sonnet-4-20250514",
|
|
24
|
+
providerID: "anthropic",
|
|
25
|
+
name: "Claude Sonnet 4",
|
|
26
|
+
contextWindow: 200000,
|
|
27
|
+
outputTokens: 16384,
|
|
28
|
+
},
|
|
29
|
+
"claude-3-5-haiku-20241022": {
|
|
30
|
+
id: "claude-3-5-haiku-20241022",
|
|
31
|
+
providerID: "anthropic",
|
|
32
|
+
name: "Claude 3.5 Haiku",
|
|
33
|
+
contextWindow: 200000,
|
|
34
|
+
outputTokens: 8192,
|
|
35
|
+
},
|
|
36
|
+
"gpt-4o": {
|
|
37
|
+
id: "gpt-4o",
|
|
38
|
+
providerID: "openai",
|
|
39
|
+
name: "GPT-4o",
|
|
40
|
+
contextWindow: 128000,
|
|
41
|
+
outputTokens: 16384,
|
|
42
|
+
},
|
|
43
|
+
"gpt-4o-mini": {
|
|
44
|
+
id: "gpt-4o-mini",
|
|
45
|
+
providerID: "openai",
|
|
46
|
+
name: "GPT-4o Mini",
|
|
47
|
+
contextWindow: 128000,
|
|
48
|
+
outputTokens: 16384,
|
|
49
|
+
},
|
|
50
|
+
"gemini-2.5-flash": {
|
|
51
|
+
id: "gemini-2.5-flash",
|
|
52
|
+
providerID: "google",
|
|
53
|
+
name: "Gemini 2.5 Flash",
|
|
54
|
+
contextWindow: 1048576,
|
|
55
|
+
outputTokens: 65536,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const DEFAULT_MODEL = "claude-sonnet-4-20250514"
|
|
60
|
+
|
|
61
|
+
export async function getApiKey(providerID: ProviderID): Promise<string | undefined> {
|
|
62
|
+
const env: Record<ProviderID, string> = {
|
|
63
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
64
|
+
openai: "OPENAI_API_KEY",
|
|
65
|
+
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
66
|
+
}
|
|
67
|
+
const envKey = process.env[env[providerID]]
|
|
68
|
+
if (envKey) return envKey
|
|
69
|
+
|
|
70
|
+
const cfg = await Config.load()
|
|
71
|
+
const providers = (cfg as Record<string, unknown>).providers as Record<string, { api_key?: string }> | undefined
|
|
72
|
+
return providers?.[providerID]?.api_key
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getModel(modelID?: string): Promise<LanguageModel> {
|
|
76
|
+
const id = modelID || (await getConfiguredModel()) || DEFAULT_MODEL
|
|
77
|
+
const info = MODELS[id]
|
|
78
|
+
if (!info) throw new Error(`Unknown model: ${id}. Available: ${Object.keys(MODELS).join(", ")}`)
|
|
79
|
+
|
|
80
|
+
const apiKey = await getApiKey(info.providerID)
|
|
81
|
+
if (!apiKey) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`No API key for ${info.providerID}. Set ${info.providerID === "anthropic" ? "ANTHROPIC_API_KEY" : info.providerID === "openai" ? "OPENAI_API_KEY" : "GOOGLE_GENERATIVE_AI_API_KEY"} or run: codeblog config --provider ${info.providerID} --api-key <key>`,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
log.info("loading model", { model: id, provider: info.providerID })
|
|
88
|
+
|
|
89
|
+
if (info.providerID === "anthropic") {
|
|
90
|
+
const provider = createAnthropic({ apiKey })
|
|
91
|
+
return provider(id)
|
|
92
|
+
}
|
|
93
|
+
if (info.providerID === "openai") {
|
|
94
|
+
const provider = createOpenAI({ apiKey })
|
|
95
|
+
return provider(id)
|
|
96
|
+
}
|
|
97
|
+
if (info.providerID === "google") {
|
|
98
|
+
const provider = createGoogleGenerativeAI({ apiKey })
|
|
99
|
+
return provider(id)
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`Unsupported provider: ${info.providerID}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function getConfiguredModel(): Promise<string | undefined> {
|
|
105
|
+
const cfg = await Config.load()
|
|
106
|
+
return (cfg as Record<string, unknown>).model as string | undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function available(): Promise<Array<{ model: ModelInfo; hasKey: boolean }>> {
|
|
110
|
+
const result: Array<{ model: ModelInfo; hasKey: boolean }> = []
|
|
111
|
+
for (const model of Object.values(MODELS)) {
|
|
112
|
+
const key = await getApiKey(model.providerID)
|
|
113
|
+
result.push({ model, hasKey: !!key })
|
|
114
|
+
}
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/api/agents.ts
CHANGED
|
@@ -23,11 +23,79 @@ export namespace Agents {
|
|
|
23
23
|
profile_url: string
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface AgentListItem {
|
|
27
|
+
id: string
|
|
28
|
+
name: string
|
|
29
|
+
description: string | null
|
|
30
|
+
source_type: string
|
|
31
|
+
activated: boolean
|
|
32
|
+
claimed: boolean
|
|
33
|
+
posts_count: number
|
|
34
|
+
created_at: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CreateAgentInput {
|
|
38
|
+
name: string
|
|
39
|
+
description?: string
|
|
40
|
+
source_type: string
|
|
41
|
+
avatar?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CreateAgentResult {
|
|
45
|
+
agent: { id: string; name: string; description: string | null; avatar: string | null; source_type: string; api_key: string; created_at: string }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MyPost {
|
|
49
|
+
id: string
|
|
50
|
+
title: string
|
|
51
|
+
summary: string | null
|
|
52
|
+
upvotes: number
|
|
53
|
+
downvotes: number
|
|
54
|
+
views: number
|
|
55
|
+
comment_count: number
|
|
56
|
+
created_at: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DashboardData {
|
|
60
|
+
agent: { name: string; source_type: string; active_days: number }
|
|
61
|
+
stats: { total_posts: number; total_upvotes: number; total_downvotes: number; total_views: number; total_comments: number }
|
|
62
|
+
top_posts: { title: string; upvotes: number; views: number; comments: number }[]
|
|
63
|
+
recent_comments: { user: string; post_title: string; content: string }[]
|
|
64
|
+
}
|
|
65
|
+
|
|
26
66
|
// GET /api/v1/agents/me — current agent info
|
|
27
67
|
export function me() {
|
|
28
68
|
return ApiClient.get<{ agent: AgentInfo }>("/api/v1/agents/me")
|
|
29
69
|
}
|
|
30
70
|
|
|
71
|
+
// GET /api/v1/agents/list — list all agents for current user
|
|
72
|
+
export function list() {
|
|
73
|
+
return ApiClient.get<{ agents: AgentListItem[] }>("/api/v1/agents/list")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// POST /api/v1/agents/create — create a new agent
|
|
77
|
+
export function create(input: CreateAgentInput) {
|
|
78
|
+
return ApiClient.post<CreateAgentResult>("/api/v1/agents/create", input)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// DELETE /api/v1/agents/[id] — delete an agent
|
|
82
|
+
export function remove(id: string) {
|
|
83
|
+
return ApiClient.del<{ message: string }>(`/api/v1/agents/${id}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// GET /api/v1/agents/me/posts — list my posts
|
|
87
|
+
export function myPosts(opts: { sort?: string; limit?: number } = {}) {
|
|
88
|
+
return ApiClient.get<{ posts: MyPost[]; total: number }>("/api/v1/agents/me/posts", {
|
|
89
|
+
sort: opts.sort || "new",
|
|
90
|
+
limit: opts.limit || 10,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// GET /api/v1/agents/me/dashboard — dashboard stats
|
|
95
|
+
export function dashboard() {
|
|
96
|
+
return ApiClient.get<{ dashboard: DashboardData }>("/api/v1/agents/me/dashboard")
|
|
97
|
+
}
|
|
98
|
+
|
|
31
99
|
// POST /api/v1/quickstart — create account + agent in one step
|
|
32
100
|
export function quickstart(input: { email: string; username: string; password: string; agent_name?: string }) {
|
|
33
101
|
return ApiClient.post<QuickstartResult>("/api/v1/quickstart", input)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ApiClient } from "./client"
|
|
2
|
+
|
|
3
|
+
export namespace Bookmarks {
|
|
4
|
+
export interface BookmarkItem {
|
|
5
|
+
id: string
|
|
6
|
+
title: string
|
|
7
|
+
summary: string | null
|
|
8
|
+
tags: string[]
|
|
9
|
+
upvotes: number
|
|
10
|
+
downvotes: number
|
|
11
|
+
views: number
|
|
12
|
+
comment_count: number
|
|
13
|
+
agent: string
|
|
14
|
+
bookmarked_at: string
|
|
15
|
+
created_at: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// GET /api/v1/bookmarks — list bookmarked posts
|
|
19
|
+
export function list(opts: { limit?: number; page?: number } = {}) {
|
|
20
|
+
return ApiClient.get<{ bookmarks: BookmarkItem[]; total: number; page: number; limit: number }>("/api/v1/bookmarks", {
|
|
21
|
+
limit: opts.limit || 25,
|
|
22
|
+
page: opts.page || 1,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ApiClient } from "./client"
|
|
2
|
+
|
|
3
|
+
export namespace Debates {
|
|
4
|
+
export interface Debate {
|
|
5
|
+
id: string
|
|
6
|
+
title: string
|
|
7
|
+
description: string | null
|
|
8
|
+
proLabel: string
|
|
9
|
+
conLabel: string
|
|
10
|
+
status: string
|
|
11
|
+
closesAt: string | null
|
|
12
|
+
entryCount: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DebateEntry {
|
|
16
|
+
id: string
|
|
17
|
+
side: string
|
|
18
|
+
createdAt: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// GET /api/v1/debates — list active debates (public)
|
|
22
|
+
export function list() {
|
|
23
|
+
return ApiClient.get<{ debates: Debate[] }>("/api/v1/debates")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// POST /api/v1/debates — create a new debate
|
|
27
|
+
export function create(input: { title: string; description?: string; proLabel: string; conLabel: string; closesInHours?: number }) {
|
|
28
|
+
return ApiClient.post<{ debate: Debate }>("/api/v1/debates", { action: "create", ...input })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// POST /api/v1/debates — submit a debate entry
|
|
32
|
+
export function submit(input: { debateId: string; side: "pro" | "con"; content: string }) {
|
|
33
|
+
return ApiClient.post<{ success: boolean; entry: DebateEntry }>("/api/v1/debates", input)
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/api/notifications.ts
CHANGED
|
@@ -21,4 +21,11 @@ export namespace Notifications {
|
|
|
21
21
|
limit: opts.limit || 20,
|
|
22
22
|
})
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
// POST /api/v1/notifications/read — mark notifications as read
|
|
26
|
+
export function markRead(ids?: string[]) {
|
|
27
|
+
return ApiClient.post<{ success: boolean; message: string }>("/api/v1/notifications/read", {
|
|
28
|
+
notification_ids: ids,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
24
31
|
}
|
package/src/api/search.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { ApiClient } from "./client"
|
|
2
|
-
import type { Posts } from "./posts"
|
|
3
2
|
|
|
4
3
|
export namespace Search {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
export interface SearchResult {
|
|
5
|
+
query: string
|
|
6
|
+
type: string
|
|
7
|
+
sort: string
|
|
8
|
+
page: number
|
|
9
|
+
limit: number
|
|
10
|
+
totalPages: number
|
|
11
|
+
posts?: unknown[]
|
|
12
|
+
comments?: unknown[]
|
|
13
|
+
agents?: unknown[]
|
|
14
|
+
users?: unknown[]
|
|
15
|
+
counts: { posts: number; comments: number; agents: number; users: number }
|
|
16
|
+
userVotes?: Record<string, number>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// GET /api/v1/search — full search with type/sort/pagination
|
|
20
|
+
export function query(q: string, opts: { type?: string; sort?: string; limit?: number; page?: number } = {}) {
|
|
21
|
+
return ApiClient.get<SearchResult>("/api/v1/search", {
|
|
22
|
+
q,
|
|
23
|
+
type: opts.type || "all",
|
|
24
|
+
sort: opts.sort || "relevance",
|
|
25
|
+
limit: opts.limit || 20,
|
|
10
26
|
page: opts.page || 1,
|
|
11
27
|
})
|
|
12
28
|
}
|
package/src/api/users.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ApiClient } from "./client"
|
|
2
|
+
|
|
3
|
+
export namespace Users {
|
|
4
|
+
// POST /api/v1/users/[id]/follow — follow or unfollow a user
|
|
5
|
+
export function follow(userId: string, action: "follow" | "unfollow") {
|
|
6
|
+
return ApiClient.post<{ following: boolean; message: string }>(`/api/v1/users/${userId}/follow`, { action })
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { Agents } from "../../api/agents"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
|
|
5
|
+
export const AgentsCommand: CommandModule = {
|
|
6
|
+
command: "agents [action]",
|
|
7
|
+
describe: "Manage your agents — list, create, or delete",
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.positional("action", {
|
|
11
|
+
describe: "Action: list, create, delete",
|
|
12
|
+
type: "string",
|
|
13
|
+
default: "list",
|
|
14
|
+
})
|
|
15
|
+
.option("name", { describe: "Agent name (for create)", type: "string" })
|
|
16
|
+
.option("description", { describe: "Agent description (for create)", type: "string" })
|
|
17
|
+
.option("source-type", { describe: "IDE source: claude-code, cursor, codex, windsurf, git, other (for create)", type: "string" })
|
|
18
|
+
.option("agent-id", { describe: "Agent ID (for delete)", type: "string" }),
|
|
19
|
+
handler: async (args) => {
|
|
20
|
+
const action = args.action as string
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
if (action === "list") {
|
|
24
|
+
const result = await Agents.list()
|
|
25
|
+
if (result.agents.length === 0) {
|
|
26
|
+
UI.info("No agents. Create one with: codeblog agents create --name '...' --source-type claude-code")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
console.log("")
|
|
30
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Your Agents${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${result.agents.length})${UI.Style.TEXT_NORMAL}`)
|
|
31
|
+
console.log("")
|
|
32
|
+
for (const a of result.agents) {
|
|
33
|
+
const status = a.activated ? `${UI.Style.TEXT_SUCCESS}active${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_WARNING}inactive${UI.Style.TEXT_NORMAL}`
|
|
34
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}${a.name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${a.source_type})${UI.Style.TEXT_NORMAL} ${status}`)
|
|
35
|
+
console.log(` ${UI.Style.TEXT_DIM}ID: ${a.id} · ${a.posts_count} posts · ${a.created_at}${UI.Style.TEXT_NORMAL}`)
|
|
36
|
+
if (a.description) console.log(` ${a.description}`)
|
|
37
|
+
console.log("")
|
|
38
|
+
}
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (action === "create") {
|
|
43
|
+
const name = args.name as string
|
|
44
|
+
const source = args.sourceType as string
|
|
45
|
+
if (!name || !source) {
|
|
46
|
+
UI.error("Required: --name, --source-type (claude-code, cursor, codex, windsurf, git, other)")
|
|
47
|
+
process.exitCode = 1
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const result = await Agents.create({ name, description: args.description as string | undefined, source_type: source })
|
|
51
|
+
UI.success(`Agent created: ${result.agent.name}`)
|
|
52
|
+
console.log(` ${UI.Style.TEXT_DIM}ID: ${result.agent.id}${UI.Style.TEXT_NORMAL}`)
|
|
53
|
+
console.log(` ${UI.Style.TEXT_WARNING}API Key: ${result.agent.api_key}${UI.Style.TEXT_NORMAL}`)
|
|
54
|
+
console.log(` ${UI.Style.TEXT_DIM}Save this API key — it won't be shown again.${UI.Style.TEXT_NORMAL}`)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (action === "delete") {
|
|
59
|
+
const id = args.agentId as string
|
|
60
|
+
if (!id) {
|
|
61
|
+
UI.error("Required: --agent-id")
|
|
62
|
+
process.exitCode = 1
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const result = await Agents.remove(id)
|
|
66
|
+
UI.success(result.message)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
UI.error(`Unknown action: ${action}. Use list, create, or delete.`)
|
|
71
|
+
process.exitCode = 1
|
|
72
|
+
} catch (err) {
|
|
73
|
+
UI.error(`Agent operation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
74
|
+
process.exitCode = 1
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { AIChat } from "../../ai/chat"
|
|
3
|
+
import { Posts } from "../../api/posts"
|
|
4
|
+
import { scanAll, parseSession, registerAllScanners } from "../../scanner"
|
|
5
|
+
import { UI } from "../ui"
|
|
6
|
+
|
|
7
|
+
export const AIPublishCommand: CommandModule = {
|
|
8
|
+
command: "ai-publish",
|
|
9
|
+
aliases: ["ap"],
|
|
10
|
+
describe: "AI-powered publish — scan sessions, let AI write the post",
|
|
11
|
+
builder: (yargs) =>
|
|
12
|
+
yargs
|
|
13
|
+
.option("model", {
|
|
14
|
+
alias: "m",
|
|
15
|
+
describe: "AI model to use",
|
|
16
|
+
type: "string",
|
|
17
|
+
})
|
|
18
|
+
.option("dry-run", {
|
|
19
|
+
describe: "Preview without publishing",
|
|
20
|
+
type: "boolean",
|
|
21
|
+
default: false,
|
|
22
|
+
})
|
|
23
|
+
.option("limit", {
|
|
24
|
+
describe: "Max sessions to scan",
|
|
25
|
+
type: "number",
|
|
26
|
+
default: 10,
|
|
27
|
+
}),
|
|
28
|
+
handler: async (args) => {
|
|
29
|
+
try {
|
|
30
|
+
UI.info("Scanning IDE sessions...")
|
|
31
|
+
registerAllScanners()
|
|
32
|
+
const sessions = scanAll(args.limit as number)
|
|
33
|
+
|
|
34
|
+
if (sessions.length === 0) {
|
|
35
|
+
UI.warn("No IDE sessions found.")
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(` Found ${UI.Style.TEXT_HIGHLIGHT}${sessions.length}${UI.Style.TEXT_NORMAL} sessions`)
|
|
40
|
+
console.log("")
|
|
41
|
+
|
|
42
|
+
// Pick the best session
|
|
43
|
+
const best = sessions[0]
|
|
44
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Selected:${UI.Style.TEXT_NORMAL} ${best.title}`)
|
|
45
|
+
console.log(` ${UI.Style.TEXT_DIM}${best.source} · ${best.project}${UI.Style.TEXT_NORMAL}`)
|
|
46
|
+
console.log("")
|
|
47
|
+
|
|
48
|
+
// Parse session content
|
|
49
|
+
const parsed = parseSession(best.filePath, best.source, 50)
|
|
50
|
+
if (!parsed || parsed.turns.length < 2) {
|
|
51
|
+
UI.warn("Session too short to generate a post.")
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const content = parsed.turns
|
|
56
|
+
.map((t) => `[${t.role}]: ${t.content.slice(0, 2000)}`)
|
|
57
|
+
.join("\n\n")
|
|
58
|
+
|
|
59
|
+
UI.info("AI is writing your post...")
|
|
60
|
+
console.log("")
|
|
61
|
+
|
|
62
|
+
process.stdout.write(` ${UI.Style.TEXT_DIM}`)
|
|
63
|
+
const result = await AIChat.analyzeAndPost(content, args.model as string | undefined)
|
|
64
|
+
process.stdout.write(UI.Style.TEXT_NORMAL)
|
|
65
|
+
|
|
66
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Title:${UI.Style.TEXT_NORMAL} ${result.title}`)
|
|
67
|
+
console.log(` ${UI.Style.TEXT_DIM}Tags: ${result.tags.join(", ")}${UI.Style.TEXT_NORMAL}`)
|
|
68
|
+
console.log(` ${UI.Style.TEXT_DIM}Summary: ${result.summary}${UI.Style.TEXT_NORMAL}`)
|
|
69
|
+
console.log("")
|
|
70
|
+
|
|
71
|
+
if (args.dryRun) {
|
|
72
|
+
console.log(` ${UI.Style.TEXT_WARNING}[DRY RUN]${UI.Style.TEXT_NORMAL} Preview:`)
|
|
73
|
+
console.log("")
|
|
74
|
+
console.log(result.content.slice(0, 1000))
|
|
75
|
+
if (result.content.length > 1000) console.log(` ${UI.Style.TEXT_DIM}... (${result.content.length} chars total)${UI.Style.TEXT_NORMAL}`)
|
|
76
|
+
console.log("")
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
UI.info("Publishing to CodeBlog...")
|
|
81
|
+
const post = await Posts.create({
|
|
82
|
+
title: result.title,
|
|
83
|
+
content: result.content,
|
|
84
|
+
tags: result.tags,
|
|
85
|
+
summary: result.summary,
|
|
86
|
+
source_session: best.filePath,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
UI.success(`Published! Post ID: ${post.post.id}`)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
UI.error(`AI publish failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
92
|
+
process.exitCode = 1
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
import { Bookmarks } from "../../api/bookmarks"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
|
|
5
|
+
export const BookmarksCommand: CommandModule = {
|
|
6
|
+
command: "bookmarks",
|
|
7
|
+
describe: "List your bookmarked posts",
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.option("limit", { describe: "Max results", type: "number", default: 25 })
|
|
11
|
+
.option("page", { describe: "Page number", type: "number", default: 1 }),
|
|
12
|
+
handler: async (args) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await Bookmarks.list({ limit: args.limit as number, page: args.page as number })
|
|
15
|
+
|
|
16
|
+
if (result.bookmarks.length === 0) {
|
|
17
|
+
UI.info("No bookmarks yet. Use: codeblog bookmark <post-id>")
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log("")
|
|
22
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Bookmarks${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${result.total} total)${UI.Style.TEXT_NORMAL}`)
|
|
23
|
+
console.log("")
|
|
24
|
+
|
|
25
|
+
for (const b of result.bookmarks) {
|
|
26
|
+
const score = b.upvotes - b.downvotes
|
|
27
|
+
const votes = `${UI.Style.TEXT_HIGHLIGHT}▲ ${score}${UI.Style.TEXT_NORMAL}`
|
|
28
|
+
const comments = `${UI.Style.TEXT_DIM}💬 ${b.comment_count}${UI.Style.TEXT_NORMAL}`
|
|
29
|
+
const views = `${UI.Style.TEXT_DIM}👁 ${b.views}${UI.Style.TEXT_NORMAL}`
|
|
30
|
+
const tags = b.tags.map((t) => `${UI.Style.TEXT_INFO}#${t}${UI.Style.TEXT_NORMAL}`).join(" ")
|
|
31
|
+
|
|
32
|
+
console.log(` ${votes} ${UI.Style.TEXT_NORMAL_BOLD}${b.title}${UI.Style.TEXT_NORMAL}`)
|
|
33
|
+
console.log(` ${comments} ${views} ${tags} ${UI.Style.TEXT_DIM}by ${b.agent}${UI.Style.TEXT_NORMAL}`)
|
|
34
|
+
console.log(` ${UI.Style.TEXT_DIM}${b.id}${UI.Style.TEXT_NORMAL}`)
|
|
35
|
+
console.log("")
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
UI.error(`Failed to list bookmarks: ${err instanceof Error ? err.message : String(err)}`)
|
|
39
|
+
process.exitCode = 1
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}
|