codeblog-app 2.8.2 → 2.9.5
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/tools.ts +2 -0
- 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/companion.ts +393 -0
- 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 +71 -56
- package/src/global/index.ts +1 -1
- package/src/index.ts +4 -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
|
@@ -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,118 @@ 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 CompanionConfig {
|
|
31
|
+
enabled?: boolean
|
|
32
|
+
intervalMinutes?: number // default 120
|
|
33
|
+
minSessionMessages?: number // default 10, skip sessions shorter than this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CliConfig {
|
|
27
37
|
model?: string
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
active_agents?: Record<string, string>
|
|
38
|
+
defaultProvider?: string
|
|
39
|
+
defaultLanguage?: string
|
|
40
|
+
dailyReportHour?: number
|
|
32
41
|
providers?: Record<string, ProviderConfig>
|
|
33
|
-
|
|
42
|
+
featureFlags?: FeatureFlags
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CodeblogConfig {
|
|
46
|
+
serverUrl?: string
|
|
34
47
|
dailyReportHour?: number
|
|
48
|
+
auth?: AuthConfig
|
|
49
|
+
cli?: CliConfig
|
|
50
|
+
companion?: CompanionConfig
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
const defaults: CodeblogConfig = {
|
|
38
|
-
|
|
54
|
+
serverUrl: "https://codeblog.ai",
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
export const filepath = CONFIG_FILE
|
|
42
58
|
|
|
43
59
|
const FEATURE_FLAG_ENV: Record<keyof FeatureFlags, string> = {
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
aiProviderRegistryV2: "CODEBLOG_AI_PROVIDER_REGISTRY_V2",
|
|
61
|
+
aiOnboardingWizardV2: "CODEBLOG_AI_ONBOARDING_WIZARD_V2",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function deepMerge(target: Record<string, any>, source: Record<string, any>): Record<string, any> {
|
|
65
|
+
const result = { ...target }
|
|
66
|
+
for (const key of Object.keys(source)) {
|
|
67
|
+
const val = source[key]
|
|
68
|
+
if (val === undefined) {
|
|
69
|
+
delete result[key]
|
|
70
|
+
} else if (typeof val === "object" && !Array.isArray(val) && val !== null) {
|
|
71
|
+
result[key] = deepMerge((result[key] as Record<string, any>) || {}, val)
|
|
72
|
+
} else {
|
|
73
|
+
result[key] = val
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result
|
|
46
77
|
}
|
|
47
78
|
|
|
48
79
|
export async function load(): Promise<CodeblogConfig> {
|
|
49
80
|
const file = Bun.file(CONFIG_FILE)
|
|
50
81
|
const data = await file.json().catch(() => ({}))
|
|
51
|
-
return
|
|
82
|
+
return deepMerge(defaults, data) as CodeblogConfig
|
|
52
83
|
}
|
|
53
84
|
|
|
54
85
|
export async function save(config: Partial<CodeblogConfig>) {
|
|
55
86
|
const current = await load()
|
|
56
|
-
const merged =
|
|
87
|
+
const merged = deepMerge(current, config as Record<string, any>)
|
|
57
88
|
await writeFile(CONFIG_FILE, JSON.stringify(merged, null, 2))
|
|
58
89
|
await chmod(CONFIG_FILE, 0o600).catch(() => {})
|
|
59
90
|
}
|
|
60
91
|
|
|
61
|
-
|
|
92
|
+
// --- Auth helpers ---
|
|
93
|
+
|
|
94
|
+
export async function getActiveAgent(_username?: string) {
|
|
62
95
|
const cfg = await load()
|
|
63
|
-
|
|
64
|
-
return cfg.activeAgent || ""
|
|
96
|
+
return cfg.auth?.activeAgent || ""
|
|
65
97
|
}
|
|
66
98
|
|
|
67
|
-
export async function saveActiveAgent(agent: string,
|
|
99
|
+
export async function saveActiveAgent(agent: string, _username?: string) {
|
|
68
100
|
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 })
|
|
101
|
+
await save({ auth: { activeAgent: agent } })
|
|
91
102
|
}
|
|
92
103
|
|
|
93
|
-
export async function
|
|
94
|
-
|
|
104
|
+
export async function clearActiveAgent(_username?: string) {
|
|
105
|
+
await save({ auth: { activeAgent: undefined } })
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
// --- Server helpers ---
|
|
109
|
+
|
|
110
|
+
export async function url() {
|
|
111
|
+
return process.env.CODEBLOG_URL || (await load()).serverUrl || "https://codeblog.ai"
|
|
99
112
|
}
|
|
100
113
|
|
|
101
|
-
export async function
|
|
102
|
-
return process.env.
|
|
114
|
+
export async function key() {
|
|
115
|
+
return process.env.CODEBLOG_API_KEY || (await load()).auth?.apiKey || ""
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
export async function language() {
|
|
106
|
-
return process.env.CODEBLOG_LANGUAGE || (await load()).
|
|
119
|
+
return process.env.CODEBLOG_LANGUAGE || (await load()).cli?.defaultLanguage
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
export async function dailyReportHour(): Promise<number> {
|
|
@@ -111,6 +124,8 @@ export namespace Config {
|
|
|
111
124
|
return val !== undefined ? val : 22
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
// --- Feature flags ---
|
|
128
|
+
|
|
114
129
|
function parseBool(raw: string | undefined): boolean | undefined {
|
|
115
130
|
if (!raw) return undefined
|
|
116
131
|
const v = raw.trim().toLowerCase()
|
|
@@ -126,6 +141,6 @@ export namespace Config {
|
|
|
126
141
|
export async function featureEnabled(flag: keyof FeatureFlags): Promise<boolean> {
|
|
127
142
|
const env = parseBool(process.env[FEATURE_FLAG_ENV[flag]])
|
|
128
143
|
if (env !== undefined) return env
|
|
129
|
-
return !!(await load()).
|
|
144
|
+
return !!(await load()).cli?.featureFlags?.[flag]
|
|
130
145
|
}
|
|
131
146
|
}
|
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/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { ForumCommand } from "./cli/cmd/forum"
|
|
|
32
32
|
import { UninstallCommand } from "./cli/cmd/uninstall"
|
|
33
33
|
import { McpCommand } from "./cli/cmd/mcp"
|
|
34
34
|
import { DailyCommand } from "./cli/cmd/daily"
|
|
35
|
+
import { CompanionCommand } from "./cli/cmd/companion"
|
|
35
36
|
|
|
36
37
|
const VERSION = (await import("../package.json")).version
|
|
37
38
|
|
|
@@ -109,7 +110,8 @@ const cli = yargs(hideBin(process.argv))
|
|
|
109
110
|
" Scan & Publish:\n" +
|
|
110
111
|
" scan Scan local IDE sessions\n" +
|
|
111
112
|
" publish Auto-generate and publish a post\n" +
|
|
112
|
-
" daily Generate daily coding report (Day in Code)\n
|
|
113
|
+
" daily Generate daily coding report (Day in Code)\n" +
|
|
114
|
+
" companion Background AI companion (start/stop/status/run)\n\n" +
|
|
113
115
|
" Personal & Social:\n" +
|
|
114
116
|
" me Dashboard, posts, notifications, bookmarks, follow\n" +
|
|
115
117
|
" agent Manage agents (list, create, delete)\n" +
|
|
@@ -149,6 +151,7 @@ const cli = yargs(hideBin(process.argv))
|
|
|
149
151
|
.command({ ...UninstallCommand, describe: false })
|
|
150
152
|
.command({ ...McpCommand, describe: false })
|
|
151
153
|
.command({ ...DailyCommand, describe: false })
|
|
154
|
+
.command({ ...CompanionCommand, describe: false })
|
|
152
155
|
|
|
153
156
|
.fail((msg, err) => {
|
|
154
157
|
if (
|
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)
|
package/src/tui/routes/model.tsx
CHANGED
|
@@ -46,7 +46,7 @@ export function ModelPicker(props: { onDone: (model?: string) => void }) {
|
|
|
46
46
|
const cfg = await Config.load()
|
|
47
47
|
const resolved = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
|
|
48
48
|
setCurrent(resolved)
|
|
49
|
-
if (cfg.model !== resolved) await Config.save({ model: resolved })
|
|
49
|
+
if (cfg.cli?.model !== resolved) await Config.save({ cli: { model: resolved } })
|
|
50
50
|
|
|
51
51
|
setStatus("Fetching models from API...")
|
|
52
52
|
const all = await AIProvider.available()
|
|
@@ -130,7 +130,7 @@ export function ModelPicker(props: { onDone: (model?: string) => void }) {
|
|
|
130
130
|
const item = filtered().find((m) => m.id === id)
|
|
131
131
|
const saveId = item && item.provider === "openai-compatible" ? `openai-compatible/${id}` : id
|
|
132
132
|
const { Config } = await import("../../config")
|
|
133
|
-
await Config.save({ model: saveId })
|
|
133
|
+
await Config.save({ cli: { model: saveId } })
|
|
134
134
|
props.onDone(saveId)
|
|
135
135
|
} catch {
|
|
136
136
|
props.onDone()
|