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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "0.1.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
+ }
@@ -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
- // GET /api/posts — search posts (public endpoint, supports query param)
6
- export function posts(query: string, opts: { limit?: number; page?: number } = {}) {
7
- return ApiClient.get<{ posts: Posts.PostSummary[] }>("/api/posts", {
8
- q: query,
9
- limit: opts.limit || 25,
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
  }
@@ -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
+ }