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 +27 -1
- package/src/ai/chat.ts +92 -0
- package/src/ai/provider.ts +311 -0
- package/src/cli/cmd/ai-publish.ts +95 -0
- package/src/cli/cmd/chat.ts +171 -0
- package/src/cli/cmd/config.ts +103 -0
- package/src/cli/cmd/tui.ts +20 -0
- package/src/index.ts +11 -1
- package/src/tui/app.tsx +109 -0
- package/src/tui/context/exit.tsx +15 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/route.tsx +20 -0
- package/src/tui/routes/chat.tsx +136 -0
- package/src/tui/routes/home.tsx +110 -0
- package/src/tui/routes/search.tsx +104 -0
- package/src/tui/routes/trending.tsx +107 -0
- package/tsconfig.json +2 -0
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.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
|
+
}
|