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 +7 -7
- package/src/ai/__tests__/provider-registry.test.ts +37 -0
- package/src/ai/__tests__/provider.test.ts +5 -4
- package/src/ai/configure.ts +7 -6
- package/src/ai/models.ts +41 -0
- package/src/ai/provider-registry.ts +2 -2
- package/src/ai/provider.ts +3 -3
- package/src/auth/oauth.ts +28 -10
- package/src/cli/cmd/chat.ts +1 -1
- package/src/cli/cmd/config.ts +3 -2
- package/src/cli/cmd/logout.ts +2 -0
- package/src/cli/cmd/setup.ts +13 -3
- package/src/cli/cmd/update.ts +17 -1
- package/src/config/index.ts +33 -0
- package/src/index.ts +9 -0
- package/src/tui/app.tsx +61 -47
- package/src/tui/commands.ts +6 -2
- package/src/tui/routes/home.tsx +18 -25
- package/src/tui/routes/model.tsx +5 -2
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.
|
|
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.
|
|
62
|
-
"codeblog-app-darwin-x64": "2.3.
|
|
63
|
-
"codeblog-app-linux-arm64": "2.3.
|
|
64
|
-
"codeblog-app-linux-x64": "2.3.
|
|
65
|
-
"codeblog-app-windows-x64": "2.3.
|
|
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.
|
|
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
|
|
71
|
-
|
|
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
|
-
//
|
|
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(
|
|
190
|
+
expect(builtinCount).toBe(8)
|
|
190
191
|
})
|
|
191
192
|
|
|
192
193
|
test("available includes openai-compatible remote models when configured", async () => {
|
package/src/ai/configure.ts
CHANGED
|
@@ -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
|
|
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 = "
|
|
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 {
|
|
132
|
-
|
|
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,
|
|
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
|
|
118
|
+
const requestedModel = normalizeModelID(inputModel) || resolveModelFromConfig(cfg)
|
|
119
119
|
const loaded = await loadProviders(cfg)
|
|
120
120
|
const providers = loaded.providers
|
|
121
121
|
|
package/src/ai/provider.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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")
|
package/src/cli/cmd/chat.ts
CHANGED
|
@@ -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-
|
|
15
|
+
describe: "Model to use (e.g. claude-sonnet-4-20250514, gpt-5.2)",
|
|
16
16
|
type: "string",
|
|
17
17
|
})
|
|
18
18
|
.option("prompt", {
|
package/src/cli/cmd/config.ts
CHANGED
|
@@ -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-
|
|
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
|
|
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("")
|
package/src/cli/cmd/logout.ts
CHANGED
|
@@ -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
|
}
|
package/src/cli/cmd/setup.ts
CHANGED
|
@@ -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-
|
|
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
|
|
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-
|
|
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"])
|
package/src/cli/cmd/update.ts
CHANGED
|
@@ -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 })
|
package/src/config/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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>
|
package/src/tui/commands.ts
CHANGED
|
@@ -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<
|
|
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 () => {
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -60,7 +60,7 @@ export function Home(props: {
|
|
|
60
60
|
hasAI: boolean
|
|
61
61
|
aiProvider: string
|
|
62
62
|
modelName: string
|
|
63
|
-
onLogin: () => Promise<
|
|
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
|
|
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
|
|
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 (
|
|
680
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
package/src/tui/routes/model.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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`)
|