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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "0.3.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"
@@ -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 { type LanguageModel } from "ai"
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
- export type ProviderID = "anthropic" | "openai" | "google"
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: ProviderID
102
+ providerID: string
16
103
  name: string
17
104
  contextWindow: number
18
105
  outputTokens: number
106
+ npm?: string
19
107
  }
20
108
 
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
- },
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
- 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",
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
- 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
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
- 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
- )
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
- log.info("loading model", { model: id, provider: info.providerID })
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
- if (info.providerID === "anthropic") {
90
- const provider = createAnthropic({ apiKey })
91
- return provider(id)
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
- if (info.providerID === "openai") {
94
- const provider = createOpenAI({ apiKey })
95
- return provider(id)
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
- if (info.providerID === "google") {
98
- const provider = createGoogleGenerativeAI({ apiKey })
99
- return provider(id)
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
- throw new Error(`Unsupported provider: ${info.providerID}`)
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 (cfg as Record<string, unknown>).model as string | undefined
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(MODELS)) {
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
  }
@@ -43,7 +43,7 @@ export const ChatCommand: CommandModule = {
43
43
  }
44
44
 
45
45
  // Interactive REPL
46
- const modelInfo = AIProvider.MODELS[modelID || AIProvider.DEFAULT_MODEL]
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
- if (AIProvider.MODELS[rest]) {
95
- currentModel = rest
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.MODELS[currentModel || AIProvider.DEFAULT_MODEL]
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}Available: ${Object.keys(AIProvider.MODELS).join(", ")}${UI.Style.TEXT_NORMAL}`)
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
@@ -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
- console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Available Models${UI.Style.TEXT_NORMAL}`)
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.3.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)