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.
- package/package.json +7 -7
- package/src/ai/__tests__/compat.test.ts +1 -1
- package/src/ai/__tests__/provider-registry.test.ts +47 -33
- package/src/ai/__tests__/provider.test.ts +5 -5
- package/src/ai/codeblog-provider.ts +1 -1
- package/src/ai/configure.ts +19 -19
- package/src/ai/models.ts +4 -4
- package/src/ai/provider-registry.ts +4 -4
- package/src/ai/provider.ts +12 -12
- package/src/ai/types.ts +3 -3
- package/src/auth/index.ts +7 -18
- package/src/auth/oauth.ts +0 -7
- package/src/cli/cmd/config.ts +12 -12
- package/src/cli/cmd/login.ts +0 -7
- package/src/cli/cmd/logout.ts +4 -2
- package/src/cli/cmd/setup.ts +24 -30
- package/src/config/__tests__/config.test.ts +222 -0
- package/src/config/index.ts +64 -56
- package/src/global/index.ts +1 -1
- package/src/tui/app.tsx +2 -2
- package/src/tui/commands.ts +5 -1
- package/src/tui/routes/home.tsx +27 -23
- package/src/tui/routes/model.tsx +2 -2
package/src/cli/cmd/setup.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
506
|
+
apiKey: "proxy",
|
|
507
|
+
baseUrl: proxyURL,
|
|
508
|
+
apiType: "openai-compatible",
|
|
509
|
+
compatProfile: "openai-compatible",
|
|
510
510
|
}
|
|
511
511
|
|
|
512
512
|
await Config.save({
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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.
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
600
|
+
apiKey: key,
|
|
601
|
+
apiType: resolvedApi,
|
|
602
|
+
compatProfile: resolvedCompat,
|
|
601
603
|
}
|
|
602
|
-
if (baseURL) providerConfig.
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
})
|
package/src/config/index.ts
CHANGED
|
@@ -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
|
|
8
|
+
export type ApiType = "anthropic" | "openai" | "google" | "openai-compatible"
|
|
9
9
|
export type CompatProfile = "anthropic" | "openai" | "openai-compatible" | "google"
|
|
10
10
|
|
|
11
|
-
export interface
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
apiKey: string
|
|
20
|
+
baseUrl?: string
|
|
21
|
+
apiType?: ApiType
|
|
22
|
+
compatProfile?: CompatProfile
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export interface
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
export interface FeatureFlags {
|
|
26
|
+
aiProviderRegistryV2?: boolean
|
|
27
|
+
aiOnboardingWizardV2?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CliConfig {
|
|
27
31
|
model?: string
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
active_agents?: Record<string, string>
|
|
32
|
+
defaultProvider?: string
|
|
33
|
+
defaultLanguage?: string
|
|
34
|
+
dailyReportHour?: number
|
|
32
35
|
providers?: Record<string, ProviderConfig>
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
85
|
+
// --- Auth helpers ---
|
|
86
|
+
|
|
87
|
+
export async function getActiveAgent(_username?: string) {
|
|
62
88
|
const cfg = await load()
|
|
63
|
-
|
|
64
|
-
return cfg.activeAgent || ""
|
|
89
|
+
return cfg.auth?.activeAgent || ""
|
|
65
90
|
}
|
|
66
91
|
|
|
67
|
-
export async function saveActiveAgent(agent: string,
|
|
92
|
+
export async function saveActiveAgent(agent: string, _username?: string) {
|
|
68
93
|
if (!agent.trim()) return
|
|
69
|
-
|
|
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
|
|
94
|
-
|
|
97
|
+
export async function clearActiveAgent(_username?: string) {
|
|
98
|
+
await save({ auth: { activeAgent: undefined } })
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
102
|
-
return process.env.
|
|
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()).
|
|
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()).
|
|
137
|
+
return !!(await load()).cli?.featureFlags?.[flag]
|
|
130
138
|
}
|
|
131
139
|
}
|
package/src/global/index.ts
CHANGED
|
@@ -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 =
|
|
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.
|
|
204
|
+
if (cfg.cli?.defaultProvider === "codeblog") {
|
|
205
205
|
try {
|
|
206
206
|
const { fetchCreditBalance } = await import("../ai/codeblog-provider")
|
|
207
207
|
const balance = await fetchCreditBalance()
|
package/src/tui/commands.ts
CHANGED
|
@@ -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) }
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -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.
|
|
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?.
|
|
314
|
-
const providerKey = providerCfg?.
|
|
315
|
-
const providerBase = providerCfg?.
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
963
|
+
apiKey: "proxy",
|
|
964
|
+
baseUrl: proxyURL,
|
|
965
|
+
apiType: "openai-compatible",
|
|
966
|
+
compatProfile: "openai-compatible",
|
|
967
967
|
}
|
|
968
968
|
|
|
969
969
|
await Config.save({
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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: {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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.
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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)
|