codeblog-app 2.8.2 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -356,7 +356,7 @@ function isOfficialOpenAIBase(baseURL: string): boolean {
356
356
  }
357
357
  }
358
358
 
359
- async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: string): Promise<{ ok: boolean; detail: string; detectedApi?: Config.ModelApi }> {
359
+ async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: string): Promise<{ ok: boolean; detail: string; detectedApi?: Config.ApiType }> {
360
360
  try {
361
361
  if (choice.api === "anthropic") {
362
362
  const clean = baseURL.replace(/\/+$/, "")
@@ -383,7 +383,7 @@ async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: stri
383
383
  const detected = await probe(baseURL, key)
384
384
  if (detected === "anthropic") return { ok: true, detail: "Detected Anthropic API format", detectedApi: "anthropic" }
385
385
  if (detected === "openai") {
386
- const detectedApi: Config.ModelApi =
386
+ const detectedApi: Config.ApiType =
387
387
  choice.providerID === "openai" && isOfficialOpenAIBase(baseURL)
388
388
  ? "openai"
389
389
  : "openai-compatible"
@@ -392,7 +392,7 @@ async function verifyEndpoint(choice: ProviderChoice, baseURL: string, key: stri
392
392
 
393
393
  const models = await fetchOpenAIModels(baseURL, key)
394
394
  if (models.length > 0) {
395
- const detectedApi: Config.ModelApi =
395
+ const detectedApi: Config.ApiType =
396
396
  choice.providerID === "openai" && isOfficialOpenAIBase(baseURL)
397
397
  ? "openai"
398
398
  : "openai-compatible"
@@ -501,18 +501,20 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
501
501
 
502
502
  const proxyURL = `${(await Config.url()).replace(/\/+$/, "")}/api/v1/ai-credit/chat`
503
503
  const cfg = await Config.load()
504
- const providers = cfg.providers || {}
504
+ const providers = cfg.cli?.providers || {}
505
505
  providers["codeblog"] = {
506
- api_key: "proxy",
507
- base_url: proxyURL,
508
- api: "openai-compatible",
509
- compat_profile: "openai-compatible",
506
+ apiKey: "proxy",
507
+ baseUrl: proxyURL,
508
+ apiType: "openai-compatible",
509
+ compatProfile: "openai-compatible",
510
510
  }
511
511
 
512
512
  await Config.save({
513
- providers,
514
- default_provider: "codeblog",
515
- model: `codeblog/${balance.model}`,
513
+ cli: {
514
+ providers,
515
+ defaultProvider: "codeblog",
516
+ model: `codeblog/${balance.model}`,
517
+ },
516
518
  })
517
519
 
518
520
  UI.success(`AI configured: CodeBlog Credit (${balance.model})`)
@@ -567,7 +569,7 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
567
569
  }
568
570
 
569
571
  let verified = false
570
- let detectedApi: Config.ModelApi | undefined
572
+ let detectedApi: Config.ApiType | undefined
571
573
 
572
574
  while (!verified) {
573
575
  await shimmerLine("Verifying endpoint...", 900)
@@ -589,17 +591,17 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
589
591
  return
590
592
  }
591
593
  const cfg = await Config.load()
592
- const providers = cfg.providers || {}
594
+ const providers = cfg.cli?.providers || {}
593
595
  const resolvedApi = detectedApi || provider.api
594
596
  const resolvedCompat = provider.providerID === "openai-compatible" && resolvedApi === "openai"
595
597
  ? "openai-compatible"
596
598
  : resolvedApi
597
599
  const providerConfig: Config.ProviderConfig = {
598
- api_key: key,
599
- api: resolvedApi,
600
- compat_profile: resolvedCompat,
600
+ apiKey: key,
601
+ apiType: resolvedApi,
602
+ compatProfile: resolvedCompat,
601
603
  }
602
- if (baseURL) providerConfig.base_url = baseURL
604
+ if (baseURL) providerConfig.baseUrl = baseURL
603
605
  providers[provider.providerID] = providerConfig
604
606
 
605
607
  const model = provider.providerID === "openai-compatible" && !selectedModel.includes("/")
@@ -607,9 +609,11 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
607
609
  : selectedModel
608
610
 
609
611
  await Config.save({
610
- providers,
611
- default_provider: provider.providerID,
612
- model,
612
+ cli: {
613
+ providers,
614
+ defaultProvider: provider.providerID,
615
+ model,
616
+ },
613
617
  })
614
618
 
615
619
  UI.success(`AI configured: ${provider.name} (${model})`)
@@ -763,11 +767,6 @@ async function agentSelectionPrompt(): Promise<void> {
763
767
  await Auth.set({ type: "apikey", value: switchData.agent.api_key, username: auth.username })
764
768
  await Config.saveActiveAgent(switchData.agent.name, auth.username)
765
769
 
766
- // Sync to MCP config
767
- try {
768
- await McpBridge.callTool("codeblog_setup", { api_key: switchData.agent.api_key })
769
- } catch {}
770
-
771
770
  UI.success(`Active agent: ${switchData.agent.name}`)
772
771
  } else {
773
772
  UI.error("Failed to switch agent. You can switch later with: codeblog agent switch")
@@ -864,11 +863,6 @@ async function agentCreationWizard(): Promise<void> {
864
863
  await Auth.set({ type: "apikey", value: result.api_key, username: auth?.username })
865
864
  await Config.saveActiveAgent(result.name, auth?.username)
866
865
 
867
- // Sync to MCP config
868
- try {
869
- await McpBridge.callTool("codeblog_setup", { api_key: result.api_key })
870
- } catch {}
871
-
872
866
  console.log("")
873
867
  UI.success(`Your agent "${emoji} ${name}" is ready! It'll represent you on CodeBlog.`)
874
868
  } catch (err) {
@@ -0,0 +1,222 @@
1
+ import fs from "fs/promises"
2
+ import os from "os"
3
+ import path from "path"
4
+ import { describe, test, expect, beforeAll, beforeEach, afterAll } from "bun:test"
5
+
6
+ describe("Config unified read/write", () => {
7
+ const testHome = path.join(os.tmpdir(), `codeblog-config-test-${process.pid}-${Date.now()}`)
8
+ const configDir = path.join(testHome, ".codeblog")
9
+ const configFile = path.join(configDir, "config.json")
10
+
11
+ let Config: (typeof import("../../config"))["Config"]
12
+ let Auth: (typeof import("../../auth"))["Auth"]
13
+
14
+ beforeAll(async () => {
15
+ process.env.CODEBLOG_TEST_HOME = testHome
16
+ await fs.mkdir(configDir, { recursive: true })
17
+ await fs.writeFile(configFile, "{}\n")
18
+ ;({ Config } = await import("../../config"))
19
+ ;({ Auth } = await import("../../auth"))
20
+ })
21
+
22
+ beforeEach(async () => {
23
+ await fs.writeFile(configFile, "{}\n")
24
+ })
25
+
26
+ afterAll(async () => {
27
+ await fs.rm(testHome, { recursive: true, force: true })
28
+ delete process.env.CODEBLOG_TEST_HOME
29
+ })
30
+
31
+ // --- Config.save / Config.load ---
32
+
33
+ test("save and load serverUrl at top level", async () => {
34
+ await Config.save({ serverUrl: "https://test.codeblog.ai" })
35
+ const cfg = await Config.load()
36
+ expect(cfg.serverUrl).toBe("https://test.codeblog.ai")
37
+ })
38
+
39
+ test("save and load dailyReportHour at top level", async () => {
40
+ await Config.save({ dailyReportHour: 18 })
41
+ const cfg = await Config.load()
42
+ expect(cfg.dailyReportHour).toBe(18)
43
+ })
44
+
45
+ test("save auth nested fields", async () => {
46
+ await Config.save({ auth: { apiKey: "cbk_test123", activeAgent: "my-agent", userId: "user123" } })
47
+ const cfg = await Config.load()
48
+ expect(cfg.auth?.apiKey).toBe("cbk_test123")
49
+ expect(cfg.auth?.activeAgent).toBe("my-agent")
50
+ expect(cfg.auth?.userId).toBe("user123")
51
+ })
52
+
53
+ test("save cli nested fields", async () => {
54
+ await Config.save({
55
+ cli: {
56
+ model: "anthropic/claude-sonnet-4",
57
+ defaultProvider: "anthropic",
58
+ providers: {
59
+ anthropic: { apiKey: "sk-ant-test", apiType: "anthropic", compatProfile: "anthropic" },
60
+ },
61
+ },
62
+ })
63
+ const cfg = await Config.load()
64
+ expect(cfg.cli?.model).toBe("anthropic/claude-sonnet-4")
65
+ expect(cfg.cli?.defaultProvider).toBe("anthropic")
66
+ expect(cfg.cli?.providers?.anthropic?.apiKey).toBe("sk-ant-test")
67
+ })
68
+
69
+ // --- Deep merge ---
70
+
71
+ test("deep merge preserves other sections when saving one", async () => {
72
+ await Config.save({ auth: { apiKey: "cbk_first" } })
73
+ await Config.save({ cli: { model: "gpt-5.2" } })
74
+ const cfg = await Config.load()
75
+ expect(cfg.auth?.apiKey).toBe("cbk_first")
76
+ expect(cfg.cli?.model).toBe("gpt-5.2")
77
+ })
78
+
79
+ test("deep merge preserves nested fields within same section", async () => {
80
+ await Config.save({ auth: { apiKey: "cbk_key", userId: "u1" } })
81
+ await Config.save({ auth: { activeAgent: "agent1" } })
82
+ const cfg = await Config.load()
83
+ expect(cfg.auth?.apiKey).toBe("cbk_key")
84
+ expect(cfg.auth?.userId).toBe("u1")
85
+ expect(cfg.auth?.activeAgent).toBe("agent1")
86
+ })
87
+
88
+ test("undefined deletes fields (for Auth.remove)", async () => {
89
+ await Config.save({ auth: { apiKey: "cbk_key", userId: "u1", activeAgent: "a1" } })
90
+ await Config.save({ auth: { apiKey: undefined, userId: undefined, activeAgent: undefined } })
91
+ const cfg = await Config.load()
92
+ expect(cfg.auth?.apiKey).toBeUndefined()
93
+ expect(cfg.auth?.userId).toBeUndefined()
94
+ expect(cfg.auth?.activeAgent).toBeUndefined()
95
+ })
96
+
97
+ test("undefined in auth does not affect cli", async () => {
98
+ await Config.save({ auth: { apiKey: "cbk_key" }, cli: { model: "gpt-5.2" } })
99
+ await Config.save({ auth: { apiKey: undefined } })
100
+ const cfg = await Config.load()
101
+ expect(cfg.auth?.apiKey).toBeUndefined()
102
+ expect(cfg.cli?.model).toBe("gpt-5.2")
103
+ })
104
+
105
+ // --- Auth proxy ---
106
+
107
+ test("Auth.set writes to config.auth", async () => {
108
+ await Auth.set({ type: "apikey", value: "cbk_authtest", username: "testuser" })
109
+ const cfg = await Config.load()
110
+ expect(cfg.auth?.apiKey).toBe("cbk_authtest")
111
+ expect(cfg.auth?.username).toBe("testuser")
112
+ })
113
+
114
+ test("Auth.get reads from config.auth", async () => {
115
+ await Config.save({ auth: { apiKey: "cbk_gettest", username: "user2" } })
116
+ const token = await Auth.get()
117
+ expect(token).not.toBeNull()
118
+ expect(token!.type).toBe("apikey")
119
+ expect(token!.value).toBe("cbk_gettest")
120
+ expect(token!.username).toBe("user2")
121
+ })
122
+
123
+ test("Auth.get returns null when no apiKey", async () => {
124
+ const token = await Auth.get()
125
+ expect(token).toBeNull()
126
+ })
127
+
128
+ test("Auth.remove clears auth but preserves cli", async () => {
129
+ await Config.save({ auth: { apiKey: "cbk_rm", userId: "u1" }, cli: { model: "gpt-5.2" } })
130
+ await Auth.remove()
131
+ const cfg = await Config.load()
132
+ expect(cfg.auth?.apiKey).toBeUndefined()
133
+ expect(cfg.auth?.userId).toBeUndefined()
134
+ expect(cfg.cli?.model).toBe("gpt-5.2")
135
+ })
136
+
137
+ test("Auth.authenticated returns true/false correctly", async () => {
138
+ expect(await Auth.authenticated()).toBe(false)
139
+ await Auth.set({ type: "apikey", value: "cbk_auth" })
140
+ expect(await Auth.authenticated()).toBe(true)
141
+ await Auth.remove()
142
+ expect(await Auth.authenticated()).toBe(false)
143
+ })
144
+
145
+ test("Auth.header returns correct Authorization header", async () => {
146
+ await Auth.set({ type: "apikey", value: "cbk_hdr" })
147
+ const header = await Auth.header()
148
+ expect(header).toEqual({ Authorization: "Bearer cbk_hdr" })
149
+ })
150
+
151
+ test("Auth.header returns empty object when not authenticated", async () => {
152
+ const header = await Auth.header()
153
+ expect(header).toEqual({})
154
+ })
155
+
156
+ // --- Config helper functions ---
157
+
158
+ test("Config.url reads serverUrl", async () => {
159
+ await Config.save({ serverUrl: "https://custom.codeblog.ai" })
160
+ const url = await Config.url()
161
+ expect(url).toBe("https://custom.codeblog.ai")
162
+ })
163
+
164
+ test("Config.url returns default when not set", async () => {
165
+ const url = await Config.url()
166
+ expect(url).toBe("https://codeblog.ai")
167
+ })
168
+
169
+ test("Config.key reads auth.apiKey", async () => {
170
+ await Config.save({ auth: { apiKey: "cbk_keytest" } })
171
+ const key = await Config.key()
172
+ expect(key).toBe("cbk_keytest")
173
+ })
174
+
175
+ test("Config.getActiveAgent reads auth.activeAgent", async () => {
176
+ await Config.save({ auth: { activeAgent: "my-bot" } })
177
+ const agent = await Config.getActiveAgent()
178
+ expect(agent).toBe("my-bot")
179
+ })
180
+
181
+ test("Config.saveActiveAgent writes auth.activeAgent", async () => {
182
+ await Config.saveActiveAgent("new-bot")
183
+ const cfg = await Config.load()
184
+ expect(cfg.auth?.activeAgent).toBe("new-bot")
185
+ })
186
+
187
+ test("Config.clearActiveAgent removes auth.activeAgent", async () => {
188
+ await Config.save({ auth: { activeAgent: "old-bot", apiKey: "cbk_keep" } })
189
+ await Config.clearActiveAgent()
190
+ const cfg = await Config.load()
191
+ expect(cfg.auth?.activeAgent).toBeUndefined()
192
+ expect(cfg.auth?.apiKey).toBe("cbk_keep")
193
+ })
194
+
195
+ test("Config.dailyReportHour returns saved value or default 22", async () => {
196
+ expect(await Config.dailyReportHour()).toBe(22)
197
+ await Config.save({ dailyReportHour: 8 })
198
+ expect(await Config.dailyReportHour()).toBe(8)
199
+ })
200
+
201
+ // --- JSON file format ---
202
+
203
+ test("config file is valid JSON with correct structure", async () => {
204
+ await Config.save({
205
+ serverUrl: "https://codeblog.ai",
206
+ dailyReportHour: 22,
207
+ auth: { apiKey: "cbk_json", activeAgent: "bot", userId: "u1" },
208
+ cli: { model: "gpt-5.2", providers: { openai: { apiKey: "sk-test" } } },
209
+ })
210
+ const raw = await fs.readFile(configFile, "utf-8")
211
+ const parsed = JSON.parse(raw)
212
+ expect(parsed.serverUrl).toBe("https://codeblog.ai")
213
+ expect(parsed.dailyReportHour).toBe(22)
214
+ expect(parsed.auth.apiKey).toBe("cbk_json")
215
+ expect(parsed.cli.model).toBe("gpt-5.2")
216
+ expect(parsed.cli.providers.openai.apiKey).toBe("sk-test")
217
+ // No old flat fields
218
+ expect(parsed.api_url).toBeUndefined()
219
+ expect(parsed.apiKey).toBeUndefined()
220
+ expect(parsed.providers).toBeUndefined()
221
+ })
222
+ })
@@ -5,105 +5,111 @@ import { Global } from "../global"
5
5
  const CONFIG_FILE = path.join(Global.Path.config, "config.json")
6
6
 
7
7
  export namespace Config {
8
- export type ModelApi = "anthropic" | "openai" | "google" | "openai-compatible"
8
+ export type ApiType = "anthropic" | "openai" | "google" | "openai-compatible"
9
9
  export type CompatProfile = "anthropic" | "openai" | "openai-compatible" | "google"
10
10
 
11
- export interface FeatureFlags {
12
- ai_provider_registry_v2?: boolean
13
- ai_onboarding_wizard_v2?: boolean
11
+ export interface AuthConfig {
12
+ apiKey?: string
13
+ activeAgent?: string
14
+ userId?: string
15
+ username?: string
14
16
  }
15
17
 
16
18
  export interface ProviderConfig {
17
- api_key: string
18
- base_url?: string
19
- api?: ModelApi
20
- compat_profile?: CompatProfile
19
+ apiKey: string
20
+ baseUrl?: string
21
+ apiType?: ApiType
22
+ compatProfile?: CompatProfile
21
23
  }
22
24
 
23
- export interface CodeblogConfig {
24
- api_url: string
25
- api_key?: string
26
- token?: string
25
+ export interface FeatureFlags {
26
+ aiProviderRegistryV2?: boolean
27
+ aiOnboardingWizardV2?: boolean
28
+ }
29
+
30
+ export interface CliConfig {
27
31
  model?: string
28
- default_provider?: string
29
- default_language?: string
30
- activeAgent?: string
31
- active_agents?: Record<string, string>
32
+ defaultProvider?: string
33
+ defaultLanguage?: string
34
+ dailyReportHour?: number
32
35
  providers?: Record<string, ProviderConfig>
33
- feature_flags?: FeatureFlags
36
+ featureFlags?: FeatureFlags
37
+ }
38
+
39
+ export interface CodeblogConfig {
40
+ serverUrl?: string
34
41
  dailyReportHour?: number
42
+ auth?: AuthConfig
43
+ cli?: CliConfig
35
44
  }
36
45
 
37
46
  const defaults: CodeblogConfig = {
38
- api_url: "https://codeblog.ai",
47
+ serverUrl: "https://codeblog.ai",
39
48
  }
40
49
 
41
50
  export const filepath = CONFIG_FILE
42
51
 
43
52
  const FEATURE_FLAG_ENV: Record<keyof FeatureFlags, string> = {
44
- ai_provider_registry_v2: "CODEBLOG_AI_PROVIDER_REGISTRY_V2",
45
- ai_onboarding_wizard_v2: "CODEBLOG_AI_ONBOARDING_WIZARD_V2",
53
+ aiProviderRegistryV2: "CODEBLOG_AI_PROVIDER_REGISTRY_V2",
54
+ aiOnboardingWizardV2: "CODEBLOG_AI_ONBOARDING_WIZARD_V2",
55
+ }
56
+
57
+ function deepMerge(target: Record<string, any>, source: Record<string, any>): Record<string, any> {
58
+ const result = { ...target }
59
+ for (const key of Object.keys(source)) {
60
+ const val = source[key]
61
+ if (val === undefined) {
62
+ delete result[key]
63
+ } else if (typeof val === "object" && !Array.isArray(val) && val !== null) {
64
+ result[key] = deepMerge((result[key] as Record<string, any>) || {}, val)
65
+ } else {
66
+ result[key] = val
67
+ }
68
+ }
69
+ return result
46
70
  }
47
71
 
48
72
  export async function load(): Promise<CodeblogConfig> {
49
73
  const file = Bun.file(CONFIG_FILE)
50
74
  const data = await file.json().catch(() => ({}))
51
- return { ...defaults, ...data }
75
+ return deepMerge(defaults, data) as CodeblogConfig
52
76
  }
53
77
 
54
78
  export async function save(config: Partial<CodeblogConfig>) {
55
79
  const current = await load()
56
- const merged = { ...current, ...config }
80
+ const merged = deepMerge(current, config as Record<string, any>)
57
81
  await writeFile(CONFIG_FILE, JSON.stringify(merged, null, 2))
58
82
  await chmod(CONFIG_FILE, 0o600).catch(() => {})
59
83
  }
60
84
 
61
- export async function getActiveAgent(username?: string) {
85
+ // --- Auth helpers ---
86
+
87
+ export async function getActiveAgent(_username?: string) {
62
88
  const cfg = await load()
63
- if (username) return cfg.active_agents?.[username] || ""
64
- return cfg.activeAgent || ""
89
+ return cfg.auth?.activeAgent || ""
65
90
  }
66
91
 
67
- export async function saveActiveAgent(agent: string, username?: string) {
92
+ export async function saveActiveAgent(agent: string, _username?: string) {
68
93
  if (!agent.trim()) return
69
- if (!username) {
70
- await save({ activeAgent: agent })
71
- return
72
- }
73
- const cfg = await load()
74
- await save({
75
- active_agents: {
76
- ...(cfg.active_agents || {}),
77
- [username]: agent,
78
- },
79
- })
80
- }
81
-
82
- export async function clearActiveAgent(username?: string) {
83
- if (!username) {
84
- await save({ activeAgent: "", active_agents: {} })
85
- return
86
- }
87
- const cfg = await load()
88
- const map = { ...(cfg.active_agents || {}) }
89
- delete map[username]
90
- await save({ active_agents: map })
94
+ await save({ auth: { activeAgent: agent } })
91
95
  }
92
96
 
93
- export async function url() {
94
- return process.env.CODEBLOG_URL || (await load()).api_url || "https://codeblog.ai"
97
+ export async function clearActiveAgent(_username?: string) {
98
+ await save({ auth: { activeAgent: undefined } })
95
99
  }
96
100
 
97
- export async function key() {
98
- return process.env.CODEBLOG_API_KEY || (await load()).api_key || ""
101
+ // --- Server helpers ---
102
+
103
+ export async function url() {
104
+ return process.env.CODEBLOG_URL || (await load()).serverUrl || "https://codeblog.ai"
99
105
  }
100
106
 
101
- export async function token() {
102
- return process.env.CODEBLOG_TOKEN || (await load()).token || ""
107
+ export async function key() {
108
+ return process.env.CODEBLOG_API_KEY || (await load()).auth?.apiKey || ""
103
109
  }
104
110
 
105
111
  export async function language() {
106
- return process.env.CODEBLOG_LANGUAGE || (await load()).default_language
112
+ return process.env.CODEBLOG_LANGUAGE || (await load()).cli?.defaultLanguage
107
113
  }
108
114
 
109
115
  export async function dailyReportHour(): Promise<number> {
@@ -111,6 +117,8 @@ export namespace Config {
111
117
  return val !== undefined ? val : 22
112
118
  }
113
119
 
120
+ // --- Feature flags ---
121
+
114
122
  function parseBool(raw: string | undefined): boolean | undefined {
115
123
  if (!raw) return undefined
116
124
  const v = raw.trim().toLowerCase()
@@ -126,6 +134,6 @@ export namespace Config {
126
134
  export async function featureEnabled(flag: keyof FeatureFlags): Promise<boolean> {
127
135
  const env = parseBool(process.env[FEATURE_FLAG_ENV[flag]])
128
136
  if (env !== undefined) return env
129
- return !!(await load()).feature_flags?.[flag]
137
+ return !!(await load()).cli?.featureFlags?.[flag]
130
138
  }
131
139
  }
@@ -12,7 +12,7 @@ const localappdata = process.env.LOCALAPPDATA || path.join(home, "AppData", "Loc
12
12
 
13
13
  const data = win ? path.join(localappdata, app) : path.join(xdgData || path.join(home, ".local", "share"), app)
14
14
  const cache = win ? path.join(localappdata, app, "cache") : path.join(xdgCache || path.join(home, ".cache"), app)
15
- const config = win ? path.join(appdata, app) : path.join(xdgConfig || path.join(home, ".config"), app)
15
+ const config = path.join(home, `.${app}`)
16
16
  const state = win ? path.join(localappdata, app, "state") : path.join(xdgState || path.join(home, ".local", "state"), app)
17
17
 
18
18
  export namespace Global {
package/src/tui/app.tsx CHANGED
@@ -195,13 +195,13 @@ function App() {
195
195
  const { Config } = await import("../config")
196
196
  const cfg = await Config.load()
197
197
  const model = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
198
- if (cfg.model !== model) await Config.save({ model })
198
+ if (cfg.cli?.model !== model) await Config.save({ cli: { model } })
199
199
  setModelName(model)
200
200
  const info = AIProvider.BUILTIN_MODELS[model]
201
201
  setAiProvider(info?.providerID || model.split("/")[0] || "ai")
202
202
 
203
203
  // Fetch credit balance if using codeblog provider
204
- if (cfg.default_provider === "codeblog") {
204
+ if (cfg.cli?.defaultProvider === "codeblog") {
205
205
  try {
206
206
  const { fetchCreditBalance } = await import("../ai/codeblog-provider")
207
207
  const balance = await fetchCreditBalance()
@@ -56,7 +56,7 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
56
56
  }
57
57
 
58
58
  const saveId = picked.providerID === "openai-compatible" ? `openai-compatible/${picked.id}` : picked.id
59
- await Config.save({ model: saveId })
59
+ await Config.save({ cli: { model: saveId } })
60
60
  deps.onAIConfigured()
61
61
  deps.showMsg(`Model switched to ${saveId}`, deps.colors.success)
62
62
  }},
@@ -72,7 +72,11 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
72
72
  { name: "/logout", description: "Sign out of CodeBlog", action: async () => {
73
73
  try {
74
74
  const { Auth } = await import("../auth")
75
+ const { McpBridge } = await import("../mcp/client")
76
+ const { clearChatToolsCache } = await import("../ai/tools")
75
77
  await Auth.remove()
78
+ await McpBridge.disconnect()
79
+ clearChatToolsCache()
76
80
  deps.showMsg("Logged out.", deps.colors.text)
77
81
  deps.onLogout()
78
82
  } catch (err) { deps.showMsg(`Logout failed: ${err instanceof Error ? err.message : String(err)}`, deps.colors.error) }
@@ -306,13 +306,13 @@ export function Home(props: {
306
306
  const current = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
307
307
  const currentBuiltin = AIProvider.BUILTIN_MODELS[current]
308
308
  const currentProvider =
309
- cfg.default_provider ||
309
+ cfg.cli?.defaultProvider ||
310
310
  (current.includes("/") ? current.split("/")[0] : currentBuiltin?.providerID) ||
311
311
  "openai"
312
- const providerCfg = cfg.providers?.[currentProvider]
313
- const providerApi = providerCfg?.api || providerCfg?.compat_profile || (currentProvider === "openai" ? "openai" : "openai-compatible")
314
- const providerKey = providerCfg?.api_key
315
- const providerBase = providerCfg?.base_url || (currentProvider === "openai" ? "https://api.openai.com" : "")
312
+ const providerCfg = cfg.cli?.providers?.[currentProvider]
313
+ const providerApi = providerCfg?.apiType || providerCfg?.compatProfile || (currentProvider === "openai" ? "openai" : "openai-compatible")
314
+ const providerKey = providerCfg?.apiKey
315
+ const providerBase = providerCfg?.baseUrl || (currentProvider === "openai" ? "https://api.openai.com" : "")
316
316
 
317
317
  const remote = await (async () => {
318
318
  if (!providerKey || !providerBase) return [] as string[]
@@ -382,7 +382,7 @@ export function Home(props: {
382
382
  async function pickModel(id: string) {
383
383
  try {
384
384
  const { Config } = await import("../../config")
385
- await Config.save({ model: id })
385
+ await Config.save({ cli: { model: id } })
386
386
  props.onAIConfigured()
387
387
  showMsg(`Set model to ${id}`, theme.colors.success)
388
388
  } catch (err) {
@@ -958,18 +958,20 @@ export function Home(props: {
958
958
 
959
959
  const proxyURL = `${(await Config.url()).replace(/\/+$/, "")}/api/v1/ai-credit/chat`
960
960
  const cfg = await Config.load()
961
- const providers = cfg.providers || {}
961
+ const providers = cfg.cli?.providers || {}
962
962
  providers["codeblog"] = {
963
- api_key: "proxy",
964
- base_url: proxyURL,
965
- api: "openai-compatible",
966
- compat_profile: "openai-compatible",
963
+ apiKey: "proxy",
964
+ baseUrl: proxyURL,
965
+ apiType: "openai-compatible",
966
+ compatProfile: "openai-compatible",
967
967
  }
968
968
 
969
969
  await Config.save({
970
- providers,
971
- default_provider: "codeblog",
972
- model: `codeblog/${balance.model}`,
970
+ cli: {
971
+ providers,
972
+ defaultProvider: "codeblog",
973
+ model: `codeblog/${balance.model}`,
974
+ },
973
975
  })
974
976
 
975
977
  const msg = claim.already_claimed
@@ -1006,17 +1008,17 @@ export function Home(props: {
1006
1008
 
1007
1009
  const { Config } = await import("../../config")
1008
1010
  const cfg = await Config.load()
1009
- const providers = cfg.providers || {}
1011
+ const providers = cfg.cli?.providers || {}
1010
1012
  const resolvedApi = verify.detectedApi || choice.api
1011
1013
  const resolvedCompat = choice.providerID === "openai-compatible" && resolvedApi === "openai"
1012
1014
  ? "openai-compatible" as const
1013
1015
  : resolvedApi
1014
- const providerConfig: { api_key: string; base_url?: string; api: typeof resolvedApi; compat_profile: typeof resolvedCompat } = {
1015
- api_key: key,
1016
- api: resolvedApi,
1017
- compat_profile: resolvedCompat,
1016
+ const providerConfig: { apiKey: string; baseUrl?: string; apiType: typeof resolvedApi; compatProfile: typeof resolvedCompat } = {
1017
+ apiKey: key,
1018
+ apiType: resolvedApi,
1019
+ compatProfile: resolvedCompat,
1018
1020
  }
1019
- if (baseURL) providerConfig.base_url = baseURL
1021
+ if (baseURL) providerConfig.baseUrl = baseURL
1020
1022
  providers[choice.providerID] = providerConfig
1021
1023
 
1022
1024
  const saveModel = choice.providerID === "openai-compatible" && !model.includes("/")
@@ -1024,9 +1026,11 @@ export function Home(props: {
1024
1026
  : model
1025
1027
 
1026
1028
  await Config.save({
1027
- providers,
1028
- default_provider: choice.providerID,
1029
- model: saveModel,
1029
+ cli: {
1030
+ providers,
1031
+ defaultProvider: choice.providerID,
1032
+ model: saveModel,
1033
+ },
1030
1034
  })
1031
1035
 
1032
1036
  showMsg(`✓ AI configured: ${choice.name} (${saveModel})`, theme.colors.success)