codeblog-app 0.3.0 → 0.4.1
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 +21 -1
- package/src/ai/provider.ts +265 -71
- package/src/cli/cmd/chat.ts +6 -10
- package/src/cli/cmd/config.ts +18 -11
- package/src/cli/cmd/tui.ts +20 -0
- package/src/index.ts +4 -1
- package/src/tui/app.tsx +130 -0
- package/src/tui/context/exit.tsx +15 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/route.tsx +22 -0
- package/src/tui/routes/chat.tsx +180 -0
- package/src/tui/routes/home.tsx +115 -0
- package/src/tui/routes/notifications.tsx +85 -0
- package/src/tui/routes/post.tsx +107 -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.1",
|
|
5
5
|
"description": "CLI client for CodeBlog — the forum where AI writes the posts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -56,15 +56,35 @@
|
|
|
56
56
|
"typescript": "5.8.2"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@ai-sdk/amazon-bedrock": "^4.0.60",
|
|
59
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",
|
|
60
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",
|
|
61
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",
|
|
62
79
|
"ai": "^6.0.86",
|
|
63
80
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
81
|
+
"fuzzysort": "^3.1.0",
|
|
64
82
|
"hono": "4.10.7",
|
|
65
83
|
"ink": "^6.7.0",
|
|
66
84
|
"open": "10.1.2",
|
|
67
85
|
"react": "^19.2.4",
|
|
86
|
+
"remeda": "^2.33.6",
|
|
87
|
+
"solid-js": "^1.9.11",
|
|
68
88
|
"xdg-basedir": "5.1.0",
|
|
69
89
|
"yargs": "18.0.0",
|
|
70
90
|
"zod": "4.1.8"
|
package/src/ai/provider.ts
CHANGED
|
@@ -1,117 +1,311 @@
|
|
|
1
1
|
import { createAnthropic } from "@ai-sdk/anthropic"
|
|
2
2
|
import { createOpenAI } from "@ai-sdk/openai"
|
|
3
3
|
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
|
4
|
-
import {
|
|
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"
|
|
5
20
|
import { Config } from "../config"
|
|
6
21
|
import { Log } from "../util/log"
|
|
22
|
+
import { Global } from "../global"
|
|
23
|
+
import path from "path"
|
|
7
24
|
|
|
8
25
|
const log = Log.create({ service: "ai-provider" })
|
|
9
26
|
|
|
10
27
|
export namespace AIProvider {
|
|
11
|
-
|
|
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
|
+
}
|
|
12
96
|
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Model info type
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
13
100
|
export interface ModelInfo {
|
|
14
101
|
id: string
|
|
15
|
-
providerID:
|
|
102
|
+
providerID: string
|
|
16
103
|
name: string
|
|
17
104
|
contextWindow: number
|
|
18
105
|
outputTokens: number
|
|
106
|
+
npm?: string
|
|
19
107
|
}
|
|
20
108
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
},
|
|
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 },
|
|
57
128
|
}
|
|
58
129
|
|
|
59
130
|
export const DEFAULT_MODEL = "claude-sonnet-4-20250514"
|
|
60
131
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
66
196
|
}
|
|
67
|
-
const envKey = process.env[env[providerID]]
|
|
68
|
-
if (envKey) return envKey
|
|
69
197
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
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
|
|
73
210
|
}
|
|
74
211
|
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Get a LanguageModel instance
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
const sdkCache = new Map<string, SDK>()
|
|
216
|
+
|
|
75
217
|
export async function getModel(modelID?: string): Promise<LanguageModel> {
|
|
76
218
|
const id = modelID || (await getConfiguredModel()) || DEFAULT_MODEL
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
throw
|
|
83
|
-
|
|
84
|
-
)
|
|
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)
|
|
85
226
|
}
|
|
86
227
|
|
|
87
|
-
|
|
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
|
+
}
|
|
88
239
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
92
247
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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)
|
|
96
272
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
273
|
+
|
|
274
|
+
// OpenAI uses responses API
|
|
275
|
+
if (providerID === "openai" && "responses" in (sdk as any)) {
|
|
276
|
+
return (sdk as any).responses(modelID)
|
|
100
277
|
}
|
|
101
|
-
|
|
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>`)
|
|
102
285
|
}
|
|
103
286
|
|
|
104
287
|
async function getConfiguredModel(): Promise<string | undefined> {
|
|
105
|
-
const cfg = await Config.load()
|
|
106
|
-
return
|
|
288
|
+
const cfg = await Config.load() as Record<string, unknown>
|
|
289
|
+
return cfg.model as string | undefined
|
|
107
290
|
}
|
|
108
291
|
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// List available models with key status (for codeblog config --list)
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
109
295
|
export async function available(): Promise<Array<{ model: ModelInfo; hasKey: boolean }>> {
|
|
110
296
|
const result: Array<{ model: ModelInfo; hasKey: boolean }> = []
|
|
111
|
-
for (const model of Object.values(
|
|
297
|
+
for (const model of Object.values(BUILTIN_MODELS)) {
|
|
112
298
|
const key = await getApiKey(model.providerID)
|
|
113
299
|
result.push({ model, hasKey: !!key })
|
|
114
300
|
}
|
|
115
301
|
return result
|
|
116
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
|
+
}
|
|
117
311
|
}
|
package/src/cli/cmd/chat.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const ChatCommand: CommandModule = {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Interactive REPL
|
|
46
|
-
const modelInfo = AIProvider.
|
|
46
|
+
const modelInfo = AIProvider.BUILTIN_MODELS[modelID || AIProvider.DEFAULT_MODEL]
|
|
47
47
|
const modelName = modelInfo?.name || modelID || AIProvider.DEFAULT_MODEL
|
|
48
48
|
|
|
49
49
|
console.log("")
|
|
@@ -91,17 +91,13 @@ export const ChatCommand: CommandModule = {
|
|
|
91
91
|
|
|
92
92
|
if (cmd === "/model") {
|
|
93
93
|
if (rest) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.log(` ${UI.Style.TEXT_SUCCESS}Model: ${AIProvider.MODELS[rest].name}${UI.Style.TEXT_NORMAL}`)
|
|
97
|
-
} else {
|
|
98
|
-
console.log(` ${UI.Style.TEXT_DANGER}Unknown model: ${rest}${UI.Style.TEXT_NORMAL}`)
|
|
99
|
-
console.log(` ${UI.Style.TEXT_DIM}Available: ${Object.keys(AIProvider.MODELS).join(", ")}${UI.Style.TEXT_NORMAL}`)
|
|
100
|
-
}
|
|
94
|
+
currentModel = rest
|
|
95
|
+
console.log(` ${UI.Style.TEXT_SUCCESS}Model: ${rest}${UI.Style.TEXT_NORMAL}`)
|
|
101
96
|
} else {
|
|
102
|
-
const current = AIProvider.
|
|
97
|
+
const current = AIProvider.BUILTIN_MODELS[currentModel || AIProvider.DEFAULT_MODEL]
|
|
103
98
|
console.log(` ${UI.Style.TEXT_DIM}Current: ${current?.name || currentModel || AIProvider.DEFAULT_MODEL}${UI.Style.TEXT_NORMAL}`)
|
|
104
|
-
console.log(` ${UI.Style.TEXT_DIM}
|
|
99
|
+
console.log(` ${UI.Style.TEXT_DIM}Built-in: ${Object.keys(AIProvider.BUILTIN_MODELS).join(", ")}${UI.Style.TEXT_NORMAL}`)
|
|
100
|
+
console.log(` ${UI.Style.TEXT_DIM}Any model from models.dev works too (e.g. anthropic/claude-sonnet-4-20250514)${UI.Style.TEXT_NORMAL}`)
|
|
105
101
|
}
|
|
106
102
|
rl.prompt()
|
|
107
103
|
return
|
package/src/cli/cmd/config.ts
CHANGED
|
@@ -29,8 +29,24 @@ export const ConfigCommand: CommandModule = {
|
|
|
29
29
|
try {
|
|
30
30
|
if (args.list) {
|
|
31
31
|
const models = await AIProvider.available()
|
|
32
|
+
const providers = await AIProvider.listProviders()
|
|
33
|
+
|
|
34
|
+
console.log("")
|
|
35
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Providers${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${Object.keys(providers).length} from models.dev)${UI.Style.TEXT_NORMAL}`)
|
|
32
36
|
console.log("")
|
|
33
|
-
|
|
37
|
+
|
|
38
|
+
const configured = Object.entries(providers).filter(([, p]) => p.hasKey)
|
|
39
|
+
const unconfigured = Object.entries(providers).filter(([, p]) => !p.hasKey)
|
|
40
|
+
|
|
41
|
+
if (configured.length > 0) {
|
|
42
|
+
console.log(` ${UI.Style.TEXT_SUCCESS}Configured:${UI.Style.TEXT_NORMAL}`)
|
|
43
|
+
for (const [id, p] of configured) {
|
|
44
|
+
console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_NORMAL_BOLD}${p.name}${UI.Style.TEXT_NORMAL} ${UI.Style.TEXT_DIM}(${p.models.length} models)${UI.Style.TEXT_NORMAL}`)
|
|
45
|
+
}
|
|
46
|
+
console.log("")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Built-in Models${UI.Style.TEXT_NORMAL}`)
|
|
34
50
|
console.log("")
|
|
35
51
|
for (const { model, hasKey } of models) {
|
|
36
52
|
const status = hasKey ? `${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL}` : `${UI.Style.TEXT_DIM}✗${UI.Style.TEXT_NORMAL}`
|
|
@@ -40,17 +56,13 @@ export const ConfigCommand: CommandModule = {
|
|
|
40
56
|
console.log("")
|
|
41
57
|
console.log(` ${UI.Style.TEXT_DIM}✓ = API key configured, ✗ = needs key${UI.Style.TEXT_NORMAL}`)
|
|
42
58
|
console.log(` ${UI.Style.TEXT_DIM}Set key: codeblog config --provider anthropic --api-key sk-...${UI.Style.TEXT_NORMAL}`)
|
|
59
|
+
console.log(` ${UI.Style.TEXT_DIM}Any model from models.dev can be used with provider/model format${UI.Style.TEXT_NORMAL}`)
|
|
43
60
|
console.log("")
|
|
44
61
|
return
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
if (args.provider && args.apiKey) {
|
|
48
65
|
const provider = args.provider as string
|
|
49
|
-
if (!["anthropic", "openai", "google"].includes(provider)) {
|
|
50
|
-
UI.error("Provider must be: anthropic, openai, or google")
|
|
51
|
-
process.exitCode = 1
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
66
|
const cfg = await Config.load() as Record<string, unknown>
|
|
55
67
|
const providers = (cfg.providers || {}) as Record<string, Record<string, string>>
|
|
56
68
|
providers[provider] = { ...providers[provider], api_key: args.apiKey as string }
|
|
@@ -61,11 +73,6 @@ export const ConfigCommand: CommandModule = {
|
|
|
61
73
|
|
|
62
74
|
if (args.model) {
|
|
63
75
|
const model = args.model as string
|
|
64
|
-
if (!AIProvider.MODELS[model]) {
|
|
65
|
-
UI.error(`Unknown model: ${model}. Run: codeblog config --list`)
|
|
66
|
-
process.exitCode = 1
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
76
|
const cfg = await Config.load() as Record<string, unknown>
|
|
70
77
|
await Config.save({ ...cfg, model } as unknown as Config.CodeblogConfig)
|
|
71
78
|
UI.success(`Default model set to ${model}`)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CommandModule } from "yargs"
|
|
2
|
+
|
|
3
|
+
export const TuiCommand: CommandModule = {
|
|
4
|
+
command: "tui",
|
|
5
|
+
aliases: ["ui"],
|
|
6
|
+
describe: "Launch interactive TUI — browse feed, chat with AI, manage posts",
|
|
7
|
+
builder: (yargs) =>
|
|
8
|
+
yargs
|
|
9
|
+
.option("model", {
|
|
10
|
+
alias: "m",
|
|
11
|
+
describe: "Default AI model",
|
|
12
|
+
type: "string",
|
|
13
|
+
}),
|
|
14
|
+
handler: async (args) => {
|
|
15
|
+
const { tui } = await import("../../tui/app")
|
|
16
|
+
await tui({
|
|
17
|
+
onExit: async () => {},
|
|
18
|
+
})
|
|
19
|
+
},
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -30,8 +30,9 @@ import { DeleteCommand } from "./cli/cmd/delete"
|
|
|
30
30
|
import { ChatCommand } from "./cli/cmd/chat"
|
|
31
31
|
import { ConfigCommand } from "./cli/cmd/config"
|
|
32
32
|
import { AIPublishCommand } from "./cli/cmd/ai-publish"
|
|
33
|
+
import { TuiCommand } from "./cli/cmd/tui"
|
|
33
34
|
|
|
34
|
-
const VERSION = "0.
|
|
35
|
+
const VERSION = "0.4.1"
|
|
35
36
|
|
|
36
37
|
process.on("unhandledRejection", (e) => {
|
|
37
38
|
Log.Default.error("rejection", {
|
|
@@ -100,6 +101,8 @@ const cli = yargs(hideBin(process.argv))
|
|
|
100
101
|
// AI
|
|
101
102
|
.command(ChatCommand)
|
|
102
103
|
.command(ConfigCommand)
|
|
104
|
+
// TUI
|
|
105
|
+
.command(TuiCommand)
|
|
103
106
|
// Account
|
|
104
107
|
.command(NotificationsCommand)
|
|
105
108
|
.command(DashboardCommand)
|