codeblog-app 0.2.0 → 0.4.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.2.0",
4
+ "version": "0.4.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,35 @@
56
56
  "typescript": "5.8.2"
57
57
  },
58
58
  "dependencies": {
59
+ "@ai-sdk/amazon-bedrock": "^4.0.60",
60
+ "@ai-sdk/anthropic": "^3.0.44",
61
+ "@ai-sdk/azure": "^3.0.30",
62
+ "@ai-sdk/cerebras": "^2.0.33",
63
+ "@ai-sdk/cohere": "^3.0.21",
64
+ "@ai-sdk/deepinfra": "^2.0.34",
65
+ "@ai-sdk/gateway": "^3.0.46",
66
+ "@ai-sdk/google": "^3.0.29",
67
+ "@ai-sdk/google-vertex": "^4.0.58",
68
+ "@ai-sdk/groq": "^3.0.24",
69
+ "@ai-sdk/mistral": "^3.0.20",
70
+ "@ai-sdk/openai": "^3.0.29",
71
+ "@ai-sdk/openai-compatible": "^2.0.30",
72
+ "@ai-sdk/perplexity": "^3.0.19",
73
+ "@ai-sdk/togetherai": "^2.0.33",
74
+ "@ai-sdk/vercel": "^2.0.32",
75
+ "@ai-sdk/xai": "^3.0.56",
76
+ "@openrouter/ai-sdk-provider": "^2.2.3",
77
+ "@opentui/core": "^0.1.79",
78
+ "@opentui/solid": "^0.1.79",
79
+ "ai": "^6.0.86",
59
80
  "drizzle-orm": "1.0.0-beta.12-a5629fb",
81
+ "fuzzysort": "^3.1.0",
60
82
  "hono": "4.10.7",
83
+ "ink": "^6.7.0",
61
84
  "open": "10.1.2",
85
+ "react": "^19.2.4",
86
+ "remeda": "^2.33.6",
87
+ "solid-js": "^1.9.11",
62
88
  "xdg-basedir": "5.1.0",
63
89
  "yargs": "18.0.0",
64
90
  "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,311 @@
1
+ import { createAnthropic } from "@ai-sdk/anthropic"
2
+ import { createOpenAI } from "@ai-sdk/openai"
3
+ import { createGoogleGenerativeAI } from "@ai-sdk/google"
4
+ import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
5
+ import { createAzure } from "@ai-sdk/azure"
6
+ import { createGoogleGenerativeAI as createVertex } from "@ai-sdk/google"
7
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
8
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider"
9
+ import { createXai } from "@ai-sdk/xai"
10
+ import { createMistral } from "@ai-sdk/mistral"
11
+ import { createGroq } from "@ai-sdk/groq"
12
+ import { createDeepInfra } from "@ai-sdk/deepinfra"
13
+ import { createCerebras } from "@ai-sdk/cerebras"
14
+ import { createCohere } from "@ai-sdk/cohere"
15
+ import { createGateway } from "@ai-sdk/gateway"
16
+ import { createTogetherAI } from "@ai-sdk/togetherai"
17
+ import { createPerplexity } from "@ai-sdk/perplexity"
18
+ import { createVercel } from "@ai-sdk/vercel"
19
+ import { type LanguageModel, type Provider as SDK } from "ai"
20
+ import { Config } from "../config"
21
+ import { Log } from "../util/log"
22
+ import { Global } from "../global"
23
+ import path from "path"
24
+
25
+ const log = Log.create({ service: "ai-provider" })
26
+
27
+ export namespace AIProvider {
28
+ // ---------------------------------------------------------------------------
29
+ // Bundled providers — same mapping as opencode
30
+ // ---------------------------------------------------------------------------
31
+ const BUNDLED_PROVIDERS: Record<string, (options: any) => SDK> = {
32
+ "@ai-sdk/amazon-bedrock": createAmazonBedrock,
33
+ "@ai-sdk/anthropic": createAnthropic,
34
+ "@ai-sdk/azure": createAzure,
35
+ "@ai-sdk/google": createGoogleGenerativeAI,
36
+ "@ai-sdk/google-vertex": createVertex as any,
37
+ "@ai-sdk/openai": createOpenAI,
38
+ "@ai-sdk/openai-compatible": createOpenAICompatible,
39
+ "@openrouter/ai-sdk-provider": createOpenRouter as any,
40
+ "@ai-sdk/xai": createXai,
41
+ "@ai-sdk/mistral": createMistral,
42
+ "@ai-sdk/groq": createGroq,
43
+ "@ai-sdk/deepinfra": createDeepInfra,
44
+ "@ai-sdk/cerebras": createCerebras,
45
+ "@ai-sdk/cohere": createCohere,
46
+ "@ai-sdk/gateway": createGateway,
47
+ "@ai-sdk/togetherai": createTogetherAI,
48
+ "@ai-sdk/perplexity": createPerplexity,
49
+ "@ai-sdk/vercel": createVercel,
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Provider env key mapping
54
+ // ---------------------------------------------------------------------------
55
+ const PROVIDER_ENV: Record<string, string[]> = {
56
+ anthropic: ["ANTHROPIC_API_KEY"],
57
+ openai: ["OPENAI_API_KEY"],
58
+ google: ["GOOGLE_GENERATIVE_AI_API_KEY", "GOOGLE_API_KEY"],
59
+ "amazon-bedrock": ["AWS_ACCESS_KEY_ID"],
60
+ azure: ["AZURE_API_KEY", "AZURE_OPENAI_API_KEY"],
61
+ xai: ["XAI_API_KEY"],
62
+ mistral: ["MISTRAL_API_KEY"],
63
+ groq: ["GROQ_API_KEY"],
64
+ deepinfra: ["DEEPINFRA_API_KEY"],
65
+ cerebras: ["CEREBRAS_API_KEY"],
66
+ cohere: ["COHERE_API_KEY"],
67
+ togetherai: ["TOGETHER_AI_API_KEY", "TOGETHERAI_API_KEY"],
68
+ perplexity: ["PERPLEXITY_API_KEY"],
69
+ openrouter: ["OPENROUTER_API_KEY"],
70
+ "openai-compatible": ["OPENAI_COMPATIBLE_API_KEY"],
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Provider → npm package mapping
75
+ // ---------------------------------------------------------------------------
76
+ const PROVIDER_NPM: Record<string, string> = {
77
+ anthropic: "@ai-sdk/anthropic",
78
+ openai: "@ai-sdk/openai",
79
+ google: "@ai-sdk/google",
80
+ "amazon-bedrock": "@ai-sdk/amazon-bedrock",
81
+ azure: "@ai-sdk/azure",
82
+ "google-vertex": "@ai-sdk/google-vertex",
83
+ xai: "@ai-sdk/xai",
84
+ mistral: "@ai-sdk/mistral",
85
+ groq: "@ai-sdk/groq",
86
+ deepinfra: "@ai-sdk/deepinfra",
87
+ cerebras: "@ai-sdk/cerebras",
88
+ cohere: "@ai-sdk/cohere",
89
+ gateway: "@ai-sdk/gateway",
90
+ togetherai: "@ai-sdk/togetherai",
91
+ perplexity: "@ai-sdk/perplexity",
92
+ vercel: "@ai-sdk/vercel",
93
+ openrouter: "@openrouter/ai-sdk-provider",
94
+ "openai-compatible": "@ai-sdk/openai-compatible",
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Model info type
99
+ // ---------------------------------------------------------------------------
100
+ export interface ModelInfo {
101
+ id: string
102
+ providerID: string
103
+ name: string
104
+ contextWindow: number
105
+ outputTokens: number
106
+ npm?: string
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Built-in model list (fallback when models.dev is unavailable)
111
+ // ---------------------------------------------------------------------------
112
+ export const BUILTIN_MODELS: Record<string, ModelInfo> = {
113
+ "claude-sonnet-4-20250514": { id: "claude-sonnet-4-20250514", providerID: "anthropic", name: "Claude Sonnet 4", contextWindow: 200000, outputTokens: 16384 },
114
+ "claude-3-5-haiku-20241022": { id: "claude-3-5-haiku-20241022", providerID: "anthropic", name: "Claude 3.5 Haiku", contextWindow: 200000, outputTokens: 8192 },
115
+ "gpt-4o": { id: "gpt-4o", providerID: "openai", name: "GPT-4o", contextWindow: 128000, outputTokens: 16384 },
116
+ "gpt-4o-mini": { id: "gpt-4o-mini", providerID: "openai", name: "GPT-4o Mini", contextWindow: 128000, outputTokens: 16384 },
117
+ "o3-mini": { id: "o3-mini", providerID: "openai", name: "o3-mini", contextWindow: 200000, outputTokens: 100000 },
118
+ "gemini-2.5-flash": { id: "gemini-2.5-flash", providerID: "google", name: "Gemini 2.5 Flash", contextWindow: 1048576, outputTokens: 65536 },
119
+ "gemini-2.5-pro": { id: "gemini-2.5-pro", providerID: "google", name: "Gemini 2.5 Pro", contextWindow: 1048576, outputTokens: 65536 },
120
+ "grok-3": { id: "grok-3", providerID: "xai", name: "Grok 3", contextWindow: 131072, outputTokens: 16384 },
121
+ "grok-3-mini": { id: "grok-3-mini", providerID: "xai", name: "Grok 3 Mini", contextWindow: 131072, outputTokens: 16384 },
122
+ "mistral-large-latest": { id: "mistral-large-latest", providerID: "mistral", name: "Mistral Large", contextWindow: 128000, outputTokens: 8192 },
123
+ "codestral-latest": { id: "codestral-latest", providerID: "mistral", name: "Codestral", contextWindow: 256000, outputTokens: 8192 },
124
+ "llama-3.3-70b-versatile": { id: "llama-3.3-70b-versatile", providerID: "groq", name: "Llama 3.3 70B (Groq)", contextWindow: 128000, outputTokens: 32768 },
125
+ "deepseek-chat": { id: "deepseek-chat", providerID: "deepinfra", name: "DeepSeek V3", contextWindow: 64000, outputTokens: 8192 },
126
+ "command-a-03-2025": { id: "command-a-03-2025", providerID: "cohere", name: "Command A", contextWindow: 256000, outputTokens: 16384 },
127
+ "sonar-pro": { id: "sonar-pro", providerID: "perplexity", name: "Sonar Pro", contextWindow: 200000, outputTokens: 8192 },
128
+ }
129
+
130
+ export const DEFAULT_MODEL = "claude-sonnet-4-20250514"
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // models.dev dynamic loading (same as opencode)
134
+ // ---------------------------------------------------------------------------
135
+ let modelsDevCache: Record<string, any> | null = null
136
+
137
+ async function fetchModelsDev(): Promise<Record<string, any>> {
138
+ if (modelsDevCache) return modelsDevCache
139
+ const cachePath = path.join(Global.Path.cache, "models.json")
140
+ const file = Bun.file(cachePath)
141
+ const cached = await file.json().catch(() => null)
142
+ if (cached) {
143
+ modelsDevCache = cached
144
+ return cached
145
+ }
146
+ try {
147
+ const resp = await fetch("https://models.dev/api.json", { signal: AbortSignal.timeout(5000) })
148
+ if (resp.ok) {
149
+ const data = await resp.json()
150
+ modelsDevCache = data as Record<string, any>
151
+ await Bun.write(file, JSON.stringify(data)).catch(() => {})
152
+ return modelsDevCache!
153
+ }
154
+ } catch {
155
+ log.info("models.dev fetch failed, using builtin models")
156
+ }
157
+ return {}
158
+ }
159
+
160
+ // Refresh models.dev in background
161
+ if (typeof globalThis.setTimeout !== "undefined") {
162
+ fetchModelsDev().catch(() => {})
163
+ setInterval(() => fetchModelsDev().catch(() => {}), 60 * 60 * 1000).unref?.()
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Get API key for a provider
168
+ // ---------------------------------------------------------------------------
169
+ export async function getApiKey(providerID: string): Promise<string | undefined> {
170
+ const envKeys = PROVIDER_ENV[providerID] || []
171
+ for (const key of envKeys) {
172
+ if (process.env[key]) return process.env[key]
173
+ }
174
+ const cfg = await Config.load() as Record<string, unknown>
175
+ const providers = (cfg.providers || {}) as Record<string, { api_key?: string }>
176
+ return providers[providerID]?.api_key
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // List all available providers with their models
181
+ // ---------------------------------------------------------------------------
182
+ export async function listProviders(): Promise<Record<string, { name: string; models: string[]; hasKey: boolean }>> {
183
+ const result: Record<string, { name: string; models: string[]; hasKey: boolean }> = {}
184
+ const modelsDev = await fetchModelsDev()
185
+
186
+ // From models.dev
187
+ for (const [providerID, provider] of Object.entries(modelsDev)) {
188
+ const p = provider as any
189
+ if (!p.models || typeof p.models !== "object") continue
190
+ const key = await getApiKey(providerID)
191
+ result[providerID] = {
192
+ name: p.name || providerID,
193
+ models: Object.keys(p.models),
194
+ hasKey: !!key,
195
+ }
196
+ }
197
+
198
+ // Ensure builtin providers are always listed
199
+ for (const model of Object.values(BUILTIN_MODELS)) {
200
+ if (!result[model.providerID]) {
201
+ const key = await getApiKey(model.providerID)
202
+ result[model.providerID] = { name: model.providerID, models: [], hasKey: !!key }
203
+ }
204
+ if (!result[model.providerID].models.includes(model.id)) {
205
+ result[model.providerID].models.push(model.id)
206
+ }
207
+ }
208
+
209
+ return result
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Get a LanguageModel instance
214
+ // ---------------------------------------------------------------------------
215
+ const sdkCache = new Map<string, SDK>()
216
+
217
+ export async function getModel(modelID?: string): Promise<LanguageModel> {
218
+ const id = modelID || (await getConfiguredModel()) || DEFAULT_MODEL
219
+
220
+ // Try builtin first
221
+ const builtin = BUILTIN_MODELS[id]
222
+ if (builtin) {
223
+ const apiKey = await getApiKey(builtin.providerID)
224
+ if (!apiKey) throw noKeyError(builtin.providerID)
225
+ return getLanguageModel(builtin.providerID, id, apiKey)
226
+ }
227
+
228
+ // Try models.dev
229
+ const modelsDev = await fetchModelsDev()
230
+ for (const [providerID, provider] of Object.entries(modelsDev)) {
231
+ const p = provider as any
232
+ if (p.models?.[id]) {
233
+ const apiKey = await getApiKey(providerID)
234
+ if (!apiKey) throw noKeyError(providerID)
235
+ const npm = p.models[id].provider?.npm || p.npm || "@ai-sdk/openai-compatible"
236
+ return getLanguageModel(providerID, id, apiKey, npm, p.api)
237
+ }
238
+ }
239
+
240
+ // Try provider/model format
241
+ if (id.includes("/")) {
242
+ const [providerID, ...rest] = id.split("/")
243
+ const mid = rest.join("/")
244
+ const apiKey = await getApiKey(providerID)
245
+ if (!apiKey) throw noKeyError(providerID)
246
+ return getLanguageModel(providerID, mid, apiKey)
247
+ }
248
+
249
+ throw new Error(`Unknown model: ${id}. Run: codeblog config --list`)
250
+ }
251
+
252
+ function getLanguageModel(providerID: string, modelID: string, apiKey: string, npm?: string, baseURL?: string): LanguageModel {
253
+ const pkg = npm || PROVIDER_NPM[providerID] || "@ai-sdk/openai-compatible"
254
+ const cacheKey = `${providerID}:${pkg}:${apiKey.slice(0, 8)}`
255
+
256
+ log.info("loading model", { provider: providerID, model: modelID, pkg })
257
+
258
+ let sdk = sdkCache.get(cacheKey)
259
+ if (!sdk) {
260
+ const createFn = BUNDLED_PROVIDERS[pkg]
261
+ if (!createFn) throw new Error(`No bundled provider for ${pkg}. Provider ${providerID} not supported.`)
262
+ const opts: Record<string, unknown> = { apiKey }
263
+ if (baseURL) opts.baseURL = baseURL
264
+ if (providerID === "openrouter") {
265
+ opts.headers = { "HTTP-Referer": "https://codeblog.ai/", "X-Title": "codeblog" }
266
+ }
267
+ if (providerID === "cerebras") {
268
+ opts.headers = { "X-Cerebras-3rd-Party-Integration": "codeblog" }
269
+ }
270
+ sdk = createFn(opts)
271
+ sdkCache.set(cacheKey, sdk)
272
+ }
273
+
274
+ // OpenAI uses responses API
275
+ if (providerID === "openai" && "responses" in (sdk as any)) {
276
+ return (sdk as any).responses(modelID)
277
+ }
278
+ return (sdk as any).languageModel?.(modelID) ?? (sdk as any)(modelID)
279
+ }
280
+
281
+ function noKeyError(providerID: string): Error {
282
+ const envKeys = PROVIDER_ENV[providerID] || []
283
+ const envHint = envKeys[0] || `${providerID.toUpperCase().replace(/-/g, "_")}_API_KEY`
284
+ return new Error(`No API key for ${providerID}. Set ${envHint} or run: codeblog config --provider ${providerID} --api-key <key>`)
285
+ }
286
+
287
+ async function getConfiguredModel(): Promise<string | undefined> {
288
+ const cfg = await Config.load() as Record<string, unknown>
289
+ return cfg.model as string | undefined
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // List available models with key status (for codeblog config --list)
294
+ // ---------------------------------------------------------------------------
295
+ export async function available(): Promise<Array<{ model: ModelInfo; hasKey: boolean }>> {
296
+ const result: Array<{ model: ModelInfo; hasKey: boolean }> = []
297
+ for (const model of Object.values(BUILTIN_MODELS)) {
298
+ const key = await getApiKey(model.providerID)
299
+ result.push({ model, hasKey: !!key })
300
+ }
301
+ return result
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Parse provider/model format
306
+ // ---------------------------------------------------------------------------
307
+ export function parseModel(model: string) {
308
+ const [providerID, ...rest] = model.split("/")
309
+ return { providerID, modelID: rest.join("/") }
310
+ }
311
+ }
@@ -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
+ }