codeblog-app 2.3.1 → 2.3.3

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": "2.3.1",
4
+ "version": "2.3.3",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -58,11 +58,11 @@
58
58
  "typescript": "5.8.2"
59
59
  },
60
60
  "optionalDependencies": {
61
- "codeblog-app-darwin-arm64": "2.3.1",
62
- "codeblog-app-darwin-x64": "2.3.1",
63
- "codeblog-app-linux-arm64": "2.3.1",
64
- "codeblog-app-linux-x64": "2.3.1",
65
- "codeblog-app-windows-x64": "2.3.1"
61
+ "codeblog-app-darwin-arm64": "2.3.3",
62
+ "codeblog-app-darwin-x64": "2.3.3",
63
+ "codeblog-app-linux-arm64": "2.3.3",
64
+ "codeblog-app-linux-x64": "2.3.3",
65
+ "codeblog-app-windows-x64": "2.3.3"
66
66
  },
67
67
  "dependencies": {
68
68
  "@ai-sdk/anthropic": "^3.0.44",
@@ -73,7 +73,7 @@
73
73
  "@opentui/core": "^0.1.79",
74
74
  "@opentui/solid": "^0.1.79",
75
75
  "ai": "^6.0.86",
76
- "codeblog-mcp": "2.2.0",
76
+ "codeblog-mcp": "2.2.1",
77
77
  "drizzle-orm": "1.0.0-beta.12-a5629fb",
78
78
  "fuzzysort": "^3.1.0",
79
79
  "hono": "4.10.7",
@@ -58,4 +58,41 @@ describe("provider-registry", () => {
58
58
  expect(route.providerID).toBe("openai")
59
59
  expect(route.modelID).toBe("gpt-4o-mini")
60
60
  })
61
+
62
+ test("legacy model id is normalized to stable default", async () => {
63
+ const route = await routeModel(undefined, {
64
+ api_url: "https://codeblog.ai",
65
+ default_provider: "openai",
66
+ model: "4.0Ultra",
67
+ providers: {
68
+ openai: { api_key: "sk-openai" },
69
+ },
70
+ })
71
+ expect(route.providerID).toBe("openai")
72
+ expect(route.modelID).toBe("gpt-5.2")
73
+ })
74
+
75
+ test("missing model uses provider-specific default", async () => {
76
+ const route = await routeModel(undefined, {
77
+ api_url: "https://codeblog.ai",
78
+ default_provider: "openai",
79
+ providers: {
80
+ openai: { api_key: "sk-openai" },
81
+ },
82
+ })
83
+ expect(route.providerID).toBe("openai")
84
+ expect(route.modelID).toBe("gpt-5.2")
85
+ })
86
+
87
+ test("missing model on openai-compatible keeps provider prefix", async () => {
88
+ const route = await routeModel(undefined, {
89
+ api_url: "https://codeblog.ai",
90
+ default_provider: "openai-compatible",
91
+ providers: {
92
+ "openai-compatible": { api_key: "sk-compat", base_url: "https://example.com/v1", api: "openai-compatible" },
93
+ },
94
+ })
95
+ expect(route.providerID).toBe("openai-compatible")
96
+ expect(route.modelID).toBe("gpt-5.2")
97
+ })
61
98
  })
@@ -67,8 +67,9 @@ describe("AIProvider", () => {
67
67
  // BUILTIN_MODELS
68
68
  // ---------------------------------------------------------------------------
69
69
 
70
- test("BUILTIN_MODELS has 7 models", () => {
71
- expect(Object.keys(AIProvider.BUILTIN_MODELS)).toHaveLength(7)
70
+ test("BUILTIN_MODELS has 8 models", () => {
71
+ // Includes GPT-5.2 as the OpenAI default.
72
+ expect(Object.keys(AIProvider.BUILTIN_MODELS)).toHaveLength(8)
72
73
  })
73
74
 
74
75
  test("each model has required fields", () => {
@@ -184,9 +185,9 @@ describe("AIProvider", () => {
184
185
  expect(entry.model).toBeDefined()
185
186
  expect(typeof entry.hasKey).toBe("boolean")
186
187
  }
187
- // The first 7 should always be builtins
188
+ // Built-ins should always be present.
188
189
  const builtinCount = models.filter((m) => AIProvider.BUILTIN_MODELS[m.model.id]).length
189
- expect(builtinCount).toBe(7)
190
+ expect(builtinCount).toBe(8)
190
191
  })
191
192
 
192
193
  test("available includes openai-compatible remote models when configured", async () => {
@@ -57,9 +57,9 @@ async function fetchFirstModel(base: string, key: string): Promise<string | null
57
57
  const data = await r.json() as { data?: Array<{ id: string }> }
58
58
  if (!data.data || data.data.length === 0) return null
59
59
 
60
- // Prefer capable models: claude-sonnet > gpt-4o > claude-opus > first available
60
+ // Prefer stable defaults first; avoid niche/legacy IDs unless explicitly chosen.
61
61
  const ids = data.data.map((m) => m.id)
62
- const preferred = [/^claude-sonnet-4/, /^gpt-4o$/, /^claude-opus-4/, /^gpt-4o-mini$/, /^gemini-2\.5-flash$/]
62
+ const preferred = [/^gpt-5\.2$/, /^claude-sonnet-4(?:-5)?/, /^gpt-5(?:\.|$|-)/, /^gpt-4o$/, /^claude-opus-4/, /^gpt-4o-mini$/, /^gemini-2\.5-flash$/]
63
63
  for (const pattern of preferred) {
64
64
  const match = ids.find((id) => pattern.test(id))
65
65
  if (match) return match
@@ -101,12 +101,14 @@ export async function saveProvider(url: string, key: string): Promise<{ provider
101
101
  // Auto-set model if not already configured
102
102
  const update: Record<string, unknown> = { providers, default_provider: provider }
103
103
  if (!cfg.model) {
104
+ const { defaultModelForProvider } = await import("./models")
104
105
  if (detected === "anthropic") {
105
- update.model = "claude-sonnet-4-20250514"
106
+ update.model = defaultModelForProvider("anthropic")
106
107
  } else {
107
108
  // For openai-compatible with custom URL, try to fetch available models
108
109
  const model = await fetchFirstModel(url, key)
109
110
  if (model) update.model = `openai-compatible/${model}`
111
+ else update.model = `openai-compatible/${defaultModelForProvider("openai-compatible")}`
110
112
  }
111
113
  }
112
114
 
@@ -128,9 +130,8 @@ export async function saveProvider(url: string, key: string): Promise<{ provider
128
130
  // Auto-set model for known providers
129
131
  const update: Record<string, unknown> = { providers, default_provider: provider }
130
132
  if (!cfg.model) {
131
- const { AIProvider } = await import("./provider")
132
- const models = Object.values(AIProvider.BUILTIN_MODELS).filter((m) => m.providerID === provider)
133
- if (models.length > 0) update.model = models[0]!.id
133
+ const { defaultModelForProvider } = await import("./models")
134
+ update.model = defaultModelForProvider(provider)
134
135
  }
135
136
 
136
137
  await Config.save(update)
package/src/ai/models.ts CHANGED
@@ -9,6 +9,7 @@ export interface ModelInfo {
9
9
  export const BUILTIN_MODELS: Record<string, ModelInfo> = {
10
10
  "claude-sonnet-4-20250514": { id: "claude-sonnet-4-20250514", providerID: "anthropic", name: "Claude Sonnet 4", contextWindow: 200000, outputTokens: 16384 },
11
11
  "claude-3-5-haiku-20241022": { id: "claude-3-5-haiku-20241022", providerID: "anthropic", name: "Claude 3.5 Haiku", contextWindow: 200000, outputTokens: 8192 },
12
+ "gpt-5.2": { id: "gpt-5.2", providerID: "openai", name: "GPT-5.2", contextWindow: 400000, outputTokens: 128000 },
12
13
  "gpt-4o": { id: "gpt-4o", providerID: "openai", name: "GPT-4o", contextWindow: 128000, outputTokens: 16384 },
13
14
  "gpt-4o-mini": { id: "gpt-4o-mini", providerID: "openai", name: "GPT-4o Mini", contextWindow: 128000, outputTokens: 16384 },
14
15
  "o3-mini": { id: "o3-mini", providerID: "openai", name: "o3-mini", contextWindow: 200000, outputTokens: 100000 },
@@ -17,6 +18,46 @@ export const BUILTIN_MODELS: Record<string, ModelInfo> = {
17
18
  }
18
19
 
19
20
  export const DEFAULT_MODEL = "claude-sonnet-4-20250514"
21
+ const LEGACY_MODEL_MAP: Record<string, string> = {
22
+ "4.0Ultra": "gpt-5.2",
23
+ "4.0ultra": "gpt-5.2",
24
+ }
25
+
26
+ const PROVIDER_DEFAULT_MODEL: Record<string, string> = {
27
+ anthropic: "claude-sonnet-4-20250514",
28
+ openai: "gpt-5.2",
29
+ google: "gemini-2.5-flash",
30
+ "openai-compatible": "gpt-5.2",
31
+ }
32
+
33
+ export function normalizeModelID(modelID?: string): string | undefined {
34
+ if (!modelID) return undefined
35
+ const trimmed = modelID.trim()
36
+ if (!trimmed) return undefined
37
+ if (LEGACY_MODEL_MAP[trimmed]) return LEGACY_MODEL_MAP[trimmed]
38
+ if (!trimmed.includes("/")) return trimmed
39
+ const [providerID, ...rest] = trimmed.split("/")
40
+ const raw = rest.join("/")
41
+ if (!raw) return trimmed
42
+ const mapped = LEGACY_MODEL_MAP[raw]
43
+ if (!mapped) return trimmed
44
+ return `${providerID}/${mapped}`
45
+ }
46
+
47
+ export function defaultModelForProvider(providerID?: string): string {
48
+ if (!providerID) return DEFAULT_MODEL
49
+ return PROVIDER_DEFAULT_MODEL[providerID] || DEFAULT_MODEL
50
+ }
51
+
52
+ export function resolveModelFromConfig(cfg: { model?: string; default_provider?: string }): string {
53
+ const model = normalizeModelID(cfg.model)
54
+ if (model) return model
55
+ const fallback = defaultModelForProvider(cfg.default_provider)
56
+ if (cfg.default_provider === "openai-compatible" && !fallback.includes("/")) {
57
+ return `openai-compatible/${fallback}`
58
+ }
59
+ return fallback
60
+ }
20
61
 
21
62
  export function inferProviderByModelPrefix(modelID: string): string | undefined {
22
63
  if (modelID.startsWith("claude-")) return "anthropic"
@@ -1,6 +1,6 @@
1
1
  import { Config } from "../config"
2
2
  import { Log } from "../util/log"
3
- import { BUILTIN_MODELS, DEFAULT_MODEL, inferProviderByModelPrefix } from "./models"
3
+ import { BUILTIN_MODELS, inferProviderByModelPrefix, resolveModelFromConfig, normalizeModelID } from "./models"
4
4
  import { type ModelCompatConfig, resolveCompat } from "./types"
5
5
 
6
6
  const log = Log.create({ service: "ai-provider-registry" })
@@ -115,7 +115,7 @@ function routeViaProvider(
115
115
 
116
116
  export async function routeModel(inputModel?: string, cfgInput?: Config.CodeblogConfig): Promise<ModelRoute> {
117
117
  const cfg = cfgInput || await Config.load()
118
- const requestedModel = inputModel || cfg.model || DEFAULT_MODEL
118
+ const requestedModel = normalizeModelID(inputModel) || resolveModelFromConfig(cfg)
119
119
  const loaded = await loadProviders(cfg)
120
120
  const providers = loaded.providers
121
121
 
@@ -5,7 +5,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
5
5
  import { type LanguageModel, type Provider as SDK } from "ai"
6
6
  import { Config } from "../config"
7
7
  import { Log } from "../util/log"
8
- import { BUILTIN_MODELS as CORE_MODELS, DEFAULT_MODEL as CORE_DEFAULT_MODEL, type ModelInfo as CoreModelInfo } from "./models"
8
+ import { BUILTIN_MODELS as CORE_MODELS, DEFAULT_MODEL as CORE_DEFAULT_MODEL, type ModelInfo as CoreModelInfo, resolveModelFromConfig, normalizeModelID } from "./models"
9
9
  import { loadProviders, PROVIDER_BASE_URL_ENV, PROVIDER_ENV, routeModel } from "./provider-registry"
10
10
  import { patchRequestByCompat, resolveCompat, type ModelApi, type ModelCompatConfig } from "./types"
11
11
 
@@ -106,7 +106,7 @@ export namespace AIProvider {
106
106
  baseURL?: string
107
107
  compat: ModelCompatConfig
108
108
  }> {
109
- const requested = modelID || (await getConfiguredModel()) || DEFAULT_MODEL
109
+ const requested = normalizeModelID(modelID) || (await getConfiguredModel()) || DEFAULT_MODEL
110
110
  const cfg = await Config.load()
111
111
 
112
112
  const builtin = BUILTIN_MODELS[requested]
@@ -227,7 +227,7 @@ export namespace AIProvider {
227
227
 
228
228
  async function getConfiguredModel(): Promise<string | undefined> {
229
229
  const cfg = await Config.load()
230
- return cfg.model
230
+ return resolveModelFromConfig(cfg)
231
231
  }
232
232
 
233
233
  export async function hasAnyKey(): Promise<boolean> {
package/src/auth/oauth.ts CHANGED
@@ -17,28 +17,46 @@ export namespace OAuth {
17
17
  const username = params.get("username") || undefined
18
18
 
19
19
  if (key) {
20
+ let ownerMismatch = ""
20
21
  await Auth.set({ type: "apikey", value: key, username })
21
- // Sync API key to MCP config (~/.codeblog/config.json)
22
- try {
23
- await McpBridge.callTool("codeblog_setup", { api_key: key })
24
- } catch (err) {
25
- log.warn("failed to sync API key to MCP config", { error: String(err) })
26
- }
27
22
  // Fetch agent name and save to CLI config
28
23
  try {
29
24
  const meRes = await fetch(`${base}/api/v1/agents/me`, {
30
25
  headers: { Authorization: `Bearer ${key}` },
31
26
  })
32
27
  if (meRes.ok) {
33
- const meData = await meRes.json() as { agent?: { name?: string } }
34
- if (meData.agent?.name) {
35
- await Config.save({ activeAgent: meData.agent.name })
28
+ const meData = await meRes.json() as { agent?: { name?: string; owner?: string | null } }
29
+ const name = meData.agent?.name?.trim()
30
+ const owner = meData.agent?.owner || ""
31
+ if (username && owner && owner !== username) {
32
+ ownerMismatch = `API key belongs to @${owner}, not @${username}`
33
+ log.warn("api key owner mismatch", { username, owner, agent: name || "" })
34
+ } else if (name) {
35
+ await Config.saveActiveAgent(name, username)
36
36
  }
37
37
  }
38
38
  } catch (err) {
39
39
  log.warn("failed to fetch agent info", { error: String(err) })
40
40
  }
41
- log.info("authenticated with api key")
41
+ if (ownerMismatch) {
42
+ if (token) {
43
+ await Auth.set({ type: "jwt", value: token, username })
44
+ log.warn("fallback to jwt auth due api key owner mismatch", { username })
45
+ log.info("authenticated with jwt")
46
+ }
47
+ else {
48
+ await Auth.remove()
49
+ throw new Error(ownerMismatch)
50
+ }
51
+ } else {
52
+ // Sync API key to MCP config (~/.codeblog/config.json)
53
+ try {
54
+ await McpBridge.callTool("codeblog_setup", { api_key: key })
55
+ } catch (err) {
56
+ log.warn("failed to sync API key to MCP config", { error: String(err) })
57
+ }
58
+ log.info("authenticated with api key")
59
+ }
42
60
  } else if (token) {
43
61
  await Auth.set({ type: "jwt", value: token, username })
44
62
  log.info("authenticated with jwt")
@@ -12,7 +12,7 @@ export const ChatCommand: CommandModule = {
12
12
  yargs
13
13
  .option("model", {
14
14
  alias: "m",
15
- describe: "Model to use (e.g. claude-sonnet-4-20250514, gpt-4o)",
15
+ describe: "Model to use (e.g. claude-sonnet-4-20250514, gpt-5.2)",
16
16
  type: "string",
17
17
  })
18
18
  .option("prompt", {
@@ -18,7 +18,7 @@ export const ConfigCommand: CommandModule = {
18
18
  })
19
19
  .option("model", {
20
20
  alias: "m",
21
- describe: "Set default AI model (e.g. claude-sonnet-4-20250514, gpt-4o)",
21
+ describe: "Set default AI model (e.g. claude-sonnet-4-20250514, gpt-5.2)",
22
22
  type: "string",
23
23
  })
24
24
  .option("url", {
@@ -119,7 +119,8 @@ export const ConfigCommand: CommandModule = {
119
119
 
120
120
  // Show current config
121
121
  const cfg = await Config.load()
122
- const model = cfg.model || AIProvider.DEFAULT_MODEL
122
+ const { resolveModelFromConfig } = await import("../../ai/models")
123
+ const model = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
123
124
  const providers = cfg.providers || {}
124
125
 
125
126
  console.log("")
@@ -1,5 +1,6 @@
1
1
  import type { CommandModule } from "yargs"
2
2
  import { Auth } from "../../auth"
3
+ import { Config } from "../../config"
3
4
  import { UI } from "../ui"
4
5
 
5
6
  export const LogoutCommand: CommandModule = {
@@ -7,6 +8,7 @@ export const LogoutCommand: CommandModule = {
7
8
  describe: "Logout from CodeBlog",
8
9
  handler: async () => {
9
10
  await Auth.remove()
11
+ await Config.clearActiveAgent()
10
12
  UI.success("Logged out successfully")
11
13
  },
12
14
  }
@@ -336,6 +336,16 @@ async function fetchOpenAIModels(baseURL: string, key: string): Promise<string[]
336
336
  }
337
337
  }
338
338
 
339
+ function pickPreferredRemoteModel(models: string[]): string | undefined {
340
+ if (models.length === 0) return undefined
341
+ const preferred = [/^gpt-5\.2$/, /^claude-sonnet-4(?:-5)?/, /^gpt-5(?:\.|$|-)/, /^gpt-4o$/, /^gpt-4o-mini$/, /^gemini-2\.5-flash$/]
342
+ for (const pattern of preferred) {
343
+ const found = models.find((id) => pattern.test(id))
344
+ if (found) return found
345
+ }
346
+ return models[0]
347
+ }
348
+
339
349
  function isOfficialOpenAIBase(baseURL: string): boolean {
340
350
  try {
341
351
  const u = new URL(baseURL)
@@ -413,10 +423,10 @@ async function chooseModel(choice: ProviderChoice, mode: WizardMode, baseURL: st
413
423
 
414
424
  if (mode === "quick") {
415
425
  if (choice.providerID === "anthropic") return "claude-sonnet-4-20250514"
416
- if (choice.providerID === "openai" && !openaiCustom) return "gpt-4o-mini"
426
+ if (choice.providerID === "openai" && !openaiCustom) return "gpt-5.2"
417
427
  if (choice.providerID === "google") return "gemini-2.5-flash"
418
428
  const remote = await fetchOpenAIModels(baseURL, key)
419
- return remote[0] || "gpt-4o-mini"
429
+ return pickPreferredRemoteModel(remote) || "gpt-5.2"
420
430
  }
421
431
 
422
432
  let options = builtin
@@ -426,7 +436,7 @@ async function chooseModel(choice: ProviderChoice, mode: WizardMode, baseURL: st
426
436
  }
427
437
  if (options.length === 0) {
428
438
  const typed = await UI.input(` Model ID: `)
429
- return typed.trim() || "gpt-4o-mini"
439
+ return typed.trim() || "gpt-5.2"
430
440
  }
431
441
 
432
442
  const idx = await UI.select(" Choose a model", [...options, "Custom model id"])
@@ -112,8 +112,24 @@ export const UpdateCommand: CommandModule = {
112
112
  await fs.chmod(bin, 0o755)
113
113
  }
114
114
  if (os === "darwin") {
115
+ await Bun.spawn(["codesign", "--remove-signature", bin], { stdout: "ignore", stderr: "ignore" }).exited
115
116
  const cs = Bun.spawn(["codesign", "--sign", "-", "--force", bin], { stdout: "ignore", stderr: "ignore" })
116
- await cs.exited
117
+ if ((await cs.exited) !== 0) {
118
+ await fs.rm(tmp, { recursive: true, force: true })
119
+ UI.error("Update installed but macOS code signing failed. Please reinstall with install.sh.")
120
+ process.exitCode = 1
121
+ return
122
+ }
123
+ const verify = Bun.spawn(["codesign", "--verify", "--deep", "--strict", bin], {
124
+ stdout: "ignore",
125
+ stderr: "ignore",
126
+ })
127
+ if ((await verify.exited) !== 0) {
128
+ await fs.rm(tmp, { recursive: true, force: true })
129
+ UI.error("Update installed but signature verification failed. Please reinstall with install.sh.")
130
+ process.exitCode = 1
131
+ return
132
+ }
117
133
  }
118
134
 
119
135
  await fs.rm(tmp, { recursive: true, force: true })
@@ -28,6 +28,7 @@ export namespace Config {
28
28
  default_provider?: string
29
29
  default_language?: string
30
30
  activeAgent?: string
31
+ active_agents?: Record<string, string>
31
32
  providers?: Record<string, ProviderConfig>
32
33
  feature_flags?: FeatureFlags
33
34
  }
@@ -56,6 +57,38 @@ export namespace Config {
56
57
  await chmod(CONFIG_FILE, 0o600).catch(() => {})
57
58
  }
58
59
 
60
+ export async function getActiveAgent(username?: string) {
61
+ const cfg = await load()
62
+ if (username) return cfg.active_agents?.[username] || ""
63
+ return cfg.activeAgent || ""
64
+ }
65
+
66
+ export async function saveActiveAgent(agent: string, username?: string) {
67
+ if (!agent.trim()) return
68
+ if (!username) {
69
+ await save({ activeAgent: agent })
70
+ return
71
+ }
72
+ const cfg = await load()
73
+ await save({
74
+ active_agents: {
75
+ ...(cfg.active_agents || {}),
76
+ [username]: agent,
77
+ },
78
+ })
79
+ }
80
+
81
+ export async function clearActiveAgent(username?: string) {
82
+ if (!username) {
83
+ await save({ activeAgent: "", active_agents: {} })
84
+ return
85
+ }
86
+ const cfg = await load()
87
+ const map = { ...(cfg.active_agents || {}) }
88
+ delete map[username]
89
+ await save({ active_agents: map })
90
+ }
91
+
59
92
  export async function url() {
60
93
  return process.env.CODEBLOG_URL || (await load()).api_url || "https://codeblog.ai"
61
94
  }
package/src/index.ts CHANGED
@@ -30,6 +30,14 @@ import { UninstallCommand } from "./cli/cmd/uninstall"
30
30
 
31
31
  const VERSION = (await import("../package.json")).version
32
32
 
33
+ function resetTerminalModes() {
34
+ if (!process.stdout.isTTY) return
35
+ // Disable mouse reporting, bracketed paste, focus tracking, and modifyOtherKeys.
36
+ process.stdout.write("\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?2004l\x1b[?1004l\x1b[>4m\x1b[0m")
37
+ }
38
+
39
+ resetTerminalModes()
40
+
33
41
  process.on("unhandledRejection", (e) => {
34
42
  Log.Default.error("rejection", {
35
43
  e: e instanceof Error ? e.stack || e.message : e,
@@ -198,6 +206,7 @@ try {
198
206
  }
199
207
  process.exitCode = 1
200
208
  } finally {
209
+ resetTerminalModes()
201
210
  await McpBridge.disconnect().catch(() => {})
202
211
  process.exit()
203
212
  }
package/src/tui/app.tsx CHANGED
@@ -75,15 +75,66 @@ function App() {
75
75
  const [aiProvider, setAiProvider] = createSignal("")
76
76
  const [modelName, setModelName] = createSignal("")
77
77
 
78
+ async function refreshAuth() {
79
+ try {
80
+ const { Auth } = await import("../auth")
81
+ const token = await Auth.get()
82
+ const authenticated = token !== null
83
+ const username = token?.username || ""
84
+ setLoggedIn(authenticated)
85
+ setUsername(username)
86
+ if (!authenticated) {
87
+ setActiveAgent("")
88
+ return
89
+ }
90
+ const { Config } = await import("../config")
91
+ const cached = await Config.getActiveAgent(username || undefined)
92
+ if (cached) setActiveAgent(cached)
93
+ if (!token?.value) {
94
+ if (!cached) setActiveAgent("")
95
+ return
96
+ }
97
+ try {
98
+ const base = await Config.url()
99
+ const res = await fetch(`${base}/api/v1/agents/me`, {
100
+ headers: { Authorization: `Bearer ${token.value}` },
101
+ })
102
+ if (!res.ok) {
103
+ if (!cached) setActiveAgent("")
104
+ return
105
+ }
106
+ const data = await res.json() as { agent?: { name?: string; owner?: string | null } }
107
+ const name = data.agent?.name?.trim()
108
+ const owner = data.agent?.owner || ""
109
+ if (username && owner && owner !== username) {
110
+ setActiveAgent("")
111
+ await Config.clearActiveAgent(username)
112
+ return
113
+ }
114
+ if (!name) {
115
+ setActiveAgent("")
116
+ await Config.clearActiveAgent(username || undefined)
117
+ return
118
+ }
119
+ setActiveAgent(name)
120
+ await Config.saveActiveAgent(name, username || undefined)
121
+ } catch {
122
+ if (!cached) setActiveAgent("")
123
+ }
124
+ } catch {}
125
+ }
126
+
78
127
  async function refreshAI() {
79
128
  try {
80
129
  const { AIProvider } = await import("../ai/provider")
130
+ const { resolveModelFromConfig } = await import("../ai/models")
81
131
  const has = await AIProvider.hasAnyKey()
82
132
  setHasAI(has)
83
133
  if (has) {
84
134
  const { Config } = await import("../config")
85
135
  const cfg = await Config.load()
86
- const model = cfg.model || AIProvider.DEFAULT_MODEL
136
+ const model = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
137
+ if (cfg.model !== model) await Config.save({ model })
87
138
  setModelName(model)
88
139
  const info = AIProvider.BUILTIN_MODELS[model]
89
140
  setAiProvider(info?.providerID || model.split("/")[0] || "ai")
@@ -93,46 +144,7 @@ function App() {
93
144
 
94
145
  onMount(async () => {
95
146
  renderer.setTerminalTitle("CodeBlog")
96
-
97
- // Check auth status
98
- try {
99
- const { Auth } = await import("../auth")
100
- const authenticated = await Auth.authenticated()
101
- setLoggedIn(authenticated)
102
- if (authenticated) {
103
- const token = await Auth.get()
104
- if (token?.username) setUsername(token.username)
105
- }
106
- } catch {}
107
-
108
- // Get active agent
109
- try {
110
- const { Config } = await import("../config")
111
- const cfg = await Config.load()
112
- if (cfg.activeAgent) {
113
- setActiveAgent(cfg.activeAgent)
114
- } else if (loggedIn()) {
115
- // If logged in but no activeAgent cached, fetch from API
116
- const { Auth } = await import("../auth")
117
- const tok = await Auth.get()
118
- if (tok?.type === "apikey" && tok.value) {
119
- try {
120
- const base = await Config.url()
121
- const res = await fetch(`${base}/api/v1/agents/me`, {
122
- headers: { Authorization: `Bearer ${tok.value}` },
123
- })
124
- if (res.ok) {
125
- const data = await res.json() as { agent?: { name?: string } }
126
- if (data.agent?.name) {
127
- setActiveAgent(data.agent.name)
128
- await Config.save({ activeAgent: data.agent.name })
129
- }
130
- }
131
- } catch {}
132
- }
133
- }
134
- } catch {}
135
-
147
+ await refreshAuth()
136
148
  await refreshAI()
137
149
  })
138
150
 
@@ -166,13 +178,15 @@ function App() {
166
178
  try {
167
179
  const { OAuth } = await import("../auth/oauth")
168
180
  await OAuth.login()
169
- const { Auth } = await import("../auth")
170
- setLoggedIn(true)
171
- const token = await Auth.get()
172
- if (token?.username) setUsername(token.username)
173
- } catch {}
181
+ await refreshAuth()
182
+ return { ok: true }
183
+ } catch (err) {
184
+ const msg = err instanceof Error ? err.message : String(err)
185
+ await refreshAuth()
186
+ return { ok: false, error: `Login failed: ${msg}` }
187
+ }
174
188
  }}
175
- onLogout={() => { setLoggedIn(false); setUsername("") }}
189
+ onLogout={() => { setLoggedIn(false); setUsername(""); setActiveAgent("") }}
176
190
  onAIConfigured={refreshAI}
177
191
  />
178
192
  </Match>
@@ -11,7 +11,7 @@ export interface CommandDeps {
11
11
  showMsg: (text: string, color?: string) => void
12
12
  openModelPicker: () => Promise<void>
13
13
  exit: () => void
14
- onLogin: () => Promise<void>
14
+ onLogin: () => Promise<{ ok: boolean; error?: string }>
15
15
  onLogout: () => void
16
16
  onAIConfigured: () => void
17
17
  clearChat: () => void
@@ -62,7 +62,11 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
62
62
  }},
63
63
  { name: "/login", description: "Sign in to CodeBlog", action: async () => {
64
64
  deps.showMsg("Opening browser for login...", deps.colors.primary)
65
- await deps.onLogin()
65
+ const result = await deps.onLogin()
66
+ if (!result.ok) {
67
+ deps.showMsg(result.error || "Login failed", deps.colors.error)
68
+ return
69
+ }
66
70
  deps.showMsg("Logged in!", deps.colors.success)
67
71
  }},
68
72
  { name: "/logout", description: "Sign out of CodeBlog", action: async () => {
@@ -60,7 +60,7 @@ export function Home(props: {
60
60
  hasAI: boolean
61
61
  aiProvider: string
62
62
  modelName: string
63
- onLogin: () => Promise<void>
63
+ onLogin: () => Promise<{ ok: boolean; error?: string }>
64
64
  onLogout: () => void
65
65
  onAIConfigured: () => void
66
66
  }) {
@@ -141,6 +141,13 @@ export function Home(props: {
141
141
  let modelPreload: Promise<ModelOption[]> | undefined
142
142
  const keyLog = process.env.CODEBLOG_DEBUG_KEYS === "1" ? Log.create({ service: "tui-key" }) : undefined
143
143
  const toHex = (value: string) => Array.from(value).map((ch) => ch.charCodeAt(0).toString(16).padStart(2, "0")).join(" ")
144
+ const chars = (evt: { sequence: string; name: string; ctrl: boolean; meta: boolean }) => {
145
+ if (evt.ctrl || evt.meta) return ""
146
+ const seq = (evt.sequence || "").replace(/[\x00-\x1f\x7f]/g, "")
147
+ if (seq) return seq
148
+ if (evt.name.length === 1) return evt.name
149
+ return ""
150
+ }
144
151
 
145
152
  function tone(color: string): ChatMsg["tone"] {
146
153
  if (color === theme.colors.success) return "success"
@@ -181,8 +188,9 @@ export function Home(props: {
181
188
  setModelLoading(true)
182
189
  const { AIProvider } = await import("../../ai/provider")
183
190
  const { Config } = await import("../../config")
191
+ const { resolveModelFromConfig } = await import("../../ai/models")
184
192
  const cfg = await Config.load()
185
- const current = cfg.model || AIProvider.DEFAULT_MODEL
193
+ const current = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
186
194
  const currentBuiltin = AIProvider.BUILTIN_MODELS[current]
187
195
  const currentProvider =
188
196
  cfg.default_provider ||
@@ -434,8 +442,9 @@ export function Home(props: {
434
442
  const { AIChat } = await import("../../ai/chat")
435
443
  const { AIProvider } = await import("../../ai/provider")
436
444
  const { Config } = await import("../../config")
445
+ const { resolveModelFromConfig } = await import("../../ai/models")
437
446
  const cfg = await Config.load()
438
- const mid = cfg.model || AIProvider.DEFAULT_MODEL
447
+ const mid = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
439
448
  const allMsgs = [...prev, userMsg]
440
449
  .filter((m): m is ChatMsg & { role: "user" | "assistant" } => m.role === "user" || m.role === "assistant")
441
450
  .map((m) => ({ role: m.role, content: m.modelContent || m.content }))
@@ -661,6 +670,7 @@ export function Home(props: {
661
670
  (submitKey && seq.startsWith("\x1b[") && seq.endsWith("~")) ||
662
671
  (submitKey && raw.includes(";13")) ||
663
672
  (submitKey && seq.includes(";13"))
673
+ const print = chars(evt)
664
674
  const newlineKey =
665
675
  evt.name === "linefeed" ||
666
676
  newlineFromRaw ||
@@ -676,21 +686,15 @@ export function Home(props: {
676
686
  if (submitKey || newlineKey) { handleSubmit(); evt.preventDefault(); return }
677
687
  if (evt.name === "escape") { setAiMode(""); evt.preventDefault(); return }
678
688
  if (evt.name === "backspace") { setAiUrl((s) => s.slice(0, -1)); evt.preventDefault(); return }
679
- if (evt.sequence && evt.sequence.length >= 1 && !evt.ctrl && !evt.meta) {
680
- const clean = evt.sequence.replace(/[\x00-\x1f\x7f]/g, "")
681
- if (clean) { setAiUrl((s) => s + clean); evt.preventDefault(); return }
682
- }
683
- if (evt.name === "space") { evt.preventDefault(); return }
689
+ if (print === " " || evt.name === "space") { evt.preventDefault(); return }
690
+ if (print) { setAiUrl((s) => s + print); evt.preventDefault(); return }
684
691
  return
685
692
  }
686
693
  if (aiMode() === "key") {
687
694
  if (submitKey || newlineKey) { handleSubmit(); evt.preventDefault(); return }
688
695
  if (evt.name === "escape") { setAiMode("url"); setAiKey(""); showMsg("Paste your API URL (or press Enter to skip):", theme.colors.primary); evt.preventDefault(); return }
689
696
  if (evt.name === "backspace") { setAiKey((s) => s.slice(0, -1)); evt.preventDefault(); return }
690
- if (evt.sequence && evt.sequence.length >= 1 && !evt.ctrl && !evt.meta) {
691
- const clean = evt.sequence.replace(/[\x00-\x1f\x7f]/g, "")
692
- if (clean) { setAiKey((s) => s + clean); evt.preventDefault(); return }
693
- }
697
+ if (print) { setAiKey((s) => s + print); evt.preventDefault(); return }
694
698
  if (evt.name === "space") { setAiKey((s) => s + " "); evt.preventDefault(); return }
695
699
  return
696
700
  }
@@ -734,15 +738,7 @@ export function Home(props: {
734
738
  evt.preventDefault()
735
739
  return
736
740
  }
737
- if (evt.sequence && evt.sequence.length >= 1 && !evt.ctrl && !evt.meta) {
738
- const clean = evt.sequence.replace(/[\x00-\x1f\x7f]/g, "")
739
- if (clean) {
740
- setModelQuery((q) => q + clean)
741
- setModelIdx(0)
742
- evt.preventDefault()
743
- return
744
- }
745
- }
741
+ if (print) { setModelQuery((q) => q + print); setModelIdx(0); evt.preventDefault(); return }
746
742
  if (evt.name === "space") {
747
743
  setModelQuery((q) => q + " ")
748
744
  setModelIdx(0)
@@ -781,10 +777,7 @@ export function Home(props: {
781
777
  if (submitKey && !shift && !evt.ctrl && !evt.meta && !modifiedSubmitFromRaw) { handleSubmit(); evt.preventDefault(); return }
782
778
  if (newlineKey) { setInput((s) => s + "\n"); evt.preventDefault(); return }
783
779
  if (evt.name === "backspace") { setInput((s) => s.slice(0, -1)); setSelectedIdx(0); evt.preventDefault(); return }
784
- if (evt.sequence && evt.sequence.length >= 1 && !evt.ctrl && !evt.meta) {
785
- const clean = evt.sequence.replace(/[\x00-\x1f\x7f]/g, "")
786
- if (clean) { setInput((s) => s + clean); setSelectedIdx(0); evt.preventDefault(); return }
787
- }
780
+ if (print) { setInput((s) => s + print); setSelectedIdx(0); evt.preventDefault(); return }
788
781
  if (evt.name === "space") { setInput((s) => s + " "); evt.preventDefault(); return }
789
782
  }, { release: true })
790
783
 
@@ -42,8 +42,11 @@ export function ModelPicker(props: { onDone: (model?: string) => void }) {
42
42
  try {
43
43
  const { AIProvider } = await import("../../ai/provider")
44
44
  const { Config } = await import("../../config")
45
+ const { resolveModelFromConfig } = await import("../../ai/models")
45
46
  const cfg = await Config.load()
46
- setCurrent(cfg.model || AIProvider.DEFAULT_MODEL)
47
+ const resolved = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
48
+ setCurrent(resolved)
49
+ if (cfg.model !== resolved) await Config.save({ model: resolved })
47
50
 
48
51
  setStatus("Fetching models from API...")
49
52
  const all = await AIProvider.available()
@@ -54,7 +57,7 @@ export function ModelPicker(props: { onDone: (model?: string) => void }) {
54
57
  }))
55
58
  if (items.length > 0) {
56
59
  setModels(items)
57
- const modelId = cfg.model || AIProvider.DEFAULT_MODEL
60
+ const modelId = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
58
61
  const curIdx = items.findIndex((m) => m.id === modelId || `${m.provider}/${m.id}` === modelId)
59
62
  if (curIdx >= 0) setIdx(curIdx)
60
63
  setStatus(`${items.length} models loaded`)