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/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.
|
|
4
|
+
"version": "2.9.0",
|
|
5
5
|
"description": "CLI client for CodeBlog — Agent Only Coding Society",
|
|
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.
|
|
62
|
-
"codeblog-app-darwin-x64": "2.
|
|
63
|
-
"codeblog-app-linux-arm64": "2.
|
|
64
|
-
"codeblog-app-linux-x64": "2.
|
|
65
|
-
"codeblog-app-windows-x64": "2.
|
|
61
|
+
"codeblog-app-darwin-arm64": "2.9.0",
|
|
62
|
+
"codeblog-app-darwin-x64": "2.9.0",
|
|
63
|
+
"codeblog-app-linux-arm64": "2.9.0",
|
|
64
|
+
"codeblog-app-linux-x64": "2.9.0",
|
|
65
|
+
"codeblog-app-windows-x64": "2.9.0"
|
|
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.
|
|
76
|
+
"codeblog-mcp": "2.9.0",
|
|
77
77
|
"drizzle-orm": "1.0.0-beta.12-a5629fb",
|
|
78
78
|
"fuzzysort": "^3.1.0",
|
|
79
79
|
"hono": "4.10.7",
|
|
@@ -16,7 +16,7 @@ describe("AI Compat", () => {
|
|
|
16
16
|
const compat = resolveCompat({
|
|
17
17
|
providerID: "openai-compatible",
|
|
18
18
|
modelID: "claude-sonnet-4-20250514",
|
|
19
|
-
providerConfig: {
|
|
19
|
+
providerConfig: { apiKey: "x", apiType: "anthropic", compatProfile: "anthropic" },
|
|
20
20
|
})
|
|
21
21
|
expect(compat.api).toBe("anthropic")
|
|
22
22
|
expect(compat.compatProfile).toBe("anthropic")
|
|
@@ -11,25 +11,29 @@ describe("provider-registry", () => {
|
|
|
11
11
|
|
|
12
12
|
test("routes explicit provider/model first", async () => {
|
|
13
13
|
const route = await routeModel("openai/gpt-4o", {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
serverUrl: "https://codeblog.ai",
|
|
15
|
+
cli: {
|
|
16
|
+
providers: {
|
|
17
|
+
openai: { apiKey: "sk-openai" },
|
|
18
|
+
},
|
|
17
19
|
},
|
|
18
20
|
})
|
|
19
21
|
expect(route.providerID).toBe("openai")
|
|
20
22
|
expect(route.modelID).toBe("gpt-4o")
|
|
21
23
|
})
|
|
22
24
|
|
|
23
|
-
test("routes by
|
|
25
|
+
test("routes by defaultProvider for unknown model", async () => {
|
|
24
26
|
const route = await routeModel("deepseek-chat", {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
serverUrl: "https://codeblog.ai",
|
|
28
|
+
cli: {
|
|
29
|
+
defaultProvider: "openai-compatible",
|
|
30
|
+
providers: {
|
|
31
|
+
"openai-compatible": {
|
|
32
|
+
apiKey: "sk-compat",
|
|
33
|
+
baseUrl: "https://api.deepseek.com",
|
|
34
|
+
apiType: "openai-compatible",
|
|
35
|
+
compatProfile: "openai-compatible",
|
|
36
|
+
},
|
|
33
37
|
},
|
|
34
38
|
},
|
|
35
39
|
})
|
|
@@ -39,20 +43,24 @@ describe("provider-registry", () => {
|
|
|
39
43
|
|
|
40
44
|
test("unknown model throws deterministic actionable error", async () => {
|
|
41
45
|
await expect(routeModel("unknown-model-x", {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
serverUrl: "https://codeblog.ai",
|
|
47
|
+
cli: {
|
|
48
|
+
providers: {
|
|
49
|
+
openai: { apiKey: "sk-openai" },
|
|
50
|
+
},
|
|
45
51
|
},
|
|
46
52
|
})).rejects.toThrow('Unknown model "unknown-model-x"')
|
|
47
53
|
})
|
|
48
54
|
|
|
49
55
|
test("multi-provider routing is deterministic by prefix", async () => {
|
|
50
56
|
const route = await routeModel("gpt-4o-mini", {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
serverUrl: "https://codeblog.ai",
|
|
58
|
+
cli: {
|
|
59
|
+
defaultProvider: "openai-compatible",
|
|
60
|
+
providers: {
|
|
61
|
+
openai: { apiKey: "sk-openai" },
|
|
62
|
+
"openai-compatible": { apiKey: "sk-compat", baseUrl: "https://api.deepseek.com" },
|
|
63
|
+
},
|
|
56
64
|
},
|
|
57
65
|
})
|
|
58
66
|
expect(route.providerID).toBe("openai")
|
|
@@ -61,11 +69,13 @@ describe("provider-registry", () => {
|
|
|
61
69
|
|
|
62
70
|
test("legacy model id is normalized to stable default", async () => {
|
|
63
71
|
const route = await routeModel(undefined, {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
serverUrl: "https://codeblog.ai",
|
|
73
|
+
cli: {
|
|
74
|
+
defaultProvider: "openai",
|
|
75
|
+
model: "4.0Ultra",
|
|
76
|
+
providers: {
|
|
77
|
+
openai: { apiKey: "sk-openai" },
|
|
78
|
+
},
|
|
69
79
|
},
|
|
70
80
|
})
|
|
71
81
|
expect(route.providerID).toBe("openai")
|
|
@@ -74,10 +84,12 @@ describe("provider-registry", () => {
|
|
|
74
84
|
|
|
75
85
|
test("missing model uses provider-specific default", async () => {
|
|
76
86
|
const route = await routeModel(undefined, {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
serverUrl: "https://codeblog.ai",
|
|
88
|
+
cli: {
|
|
89
|
+
defaultProvider: "openai",
|
|
90
|
+
providers: {
|
|
91
|
+
openai: { apiKey: "sk-openai" },
|
|
92
|
+
},
|
|
81
93
|
},
|
|
82
94
|
})
|
|
83
95
|
expect(route.providerID).toBe("openai")
|
|
@@ -86,10 +98,12 @@ describe("provider-registry", () => {
|
|
|
86
98
|
|
|
87
99
|
test("missing model on openai-compatible keeps provider prefix", async () => {
|
|
88
100
|
const route = await routeModel(undefined, {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
serverUrl: "https://codeblog.ai",
|
|
102
|
+
cli: {
|
|
103
|
+
defaultProvider: "openai-compatible",
|
|
104
|
+
providers: {
|
|
105
|
+
"openai-compatible": { apiKey: "sk-compat", baseUrl: "https://example.com/v1", apiType: "openai-compatible" },
|
|
106
|
+
},
|
|
93
107
|
},
|
|
94
108
|
})
|
|
95
109
|
expect(route.providerID).toBe("openai-compatible")
|
|
@@ -7,7 +7,7 @@ import { Config } from "../../config"
|
|
|
7
7
|
describe("AIProvider", () => {
|
|
8
8
|
const originalEnv = { ...process.env }
|
|
9
9
|
const testHome = path.join(os.tmpdir(), `codeblog-provider-test-${process.pid}-${Date.now()}`)
|
|
10
|
-
const configFile = path.join(testHome, ".
|
|
10
|
+
const configFile = path.join(testHome, ".codeblog", "config.json")
|
|
11
11
|
const xdgData = path.join(testHome, ".local", "share")
|
|
12
12
|
const xdgCache = path.join(testHome, ".cache")
|
|
13
13
|
const xdgConfig = path.join(testHome, ".config")
|
|
@@ -207,7 +207,7 @@ describe("AIProvider", () => {
|
|
|
207
207
|
|
|
208
208
|
test("getModel throws when no API key for builtin model", async () => {
|
|
209
209
|
const load = Config.load
|
|
210
|
-
Config.load = async () => ({
|
|
210
|
+
Config.load = async () => ({ serverUrl: "https://codeblog.ai" })
|
|
211
211
|
try {
|
|
212
212
|
await expect(AIProvider.getModel("gpt-4o")).rejects.toThrow("No API key for openai")
|
|
213
213
|
} finally {
|
|
@@ -215,10 +215,10 @@ describe("AIProvider", () => {
|
|
|
215
215
|
}
|
|
216
216
|
})
|
|
217
217
|
|
|
218
|
-
test("getModel falls back to provider with
|
|
219
|
-
// When a provider with
|
|
218
|
+
test("getModel falls back to provider with baseUrl for unknown model", async () => {
|
|
219
|
+
// When a provider with baseUrl is configured, unknown models get sent there
|
|
220
220
|
// instead of throwing. This test verifies the fallback behavior.
|
|
221
|
-
// If no provider has a
|
|
221
|
+
// If no provider has a baseUrl, it would throw.
|
|
222
222
|
const result = AIProvider.getModel("nonexistent-model-xyz")
|
|
223
223
|
// Either resolves (provider with base_url available) or rejects
|
|
224
224
|
const settled = await Promise.allSettled([result])
|
|
@@ -45,7 +45,7 @@ type FetchFn = (
|
|
|
45
45
|
|
|
46
46
|
export async function getCodeblogFetch(): Promise<FetchFn> {
|
|
47
47
|
const cfg = await Config.load()
|
|
48
|
-
const proxyURL = (cfg.providers?.codeblog?.
|
|
48
|
+
const proxyURL = (cfg.cli?.providers?.codeblog?.baseUrl || `${(await Config.url()).replace(/\/+$/, "")}/api/v1/ai-credit/chat`).replace(/\/+$/, "")
|
|
49
49
|
|
|
50
50
|
return async (input, init) => {
|
|
51
51
|
const headers = new Headers(init?.headers)
|
package/src/ai/configure.ts
CHANGED
|
@@ -90,29 +90,29 @@ export async function saveProvider(url: string, key: string): Promise<{ provider
|
|
|
90
90
|
process.env[envBase] = url
|
|
91
91
|
|
|
92
92
|
const cfg = await Config.load()
|
|
93
|
-
const providers = cfg.providers || {}
|
|
93
|
+
const providers = cfg.cli?.providers || {}
|
|
94
94
|
providers[provider] = {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
apiKey: key,
|
|
96
|
+
baseUrl: url,
|
|
97
|
+
apiType: detected === "anthropic" ? "anthropic" : "openai-compatible",
|
|
98
|
+
compatProfile: detected === "anthropic" ? "anthropic" : "openai-compatible",
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Auto-set model if not already configured
|
|
102
|
-
const
|
|
103
|
-
if (!cfg.model) {
|
|
102
|
+
const cliUpdate: Record<string, unknown> = { providers, defaultProvider: provider }
|
|
103
|
+
if (!cfg.cli?.model) {
|
|
104
104
|
const { defaultModelForProvider } = await import("./models")
|
|
105
105
|
if (detected === "anthropic") {
|
|
106
|
-
|
|
106
|
+
cliUpdate.model = defaultModelForProvider("anthropic")
|
|
107
107
|
} else {
|
|
108
108
|
// For openai-compatible with custom URL, try to fetch available models
|
|
109
109
|
const model = await fetchFirstModel(url, key)
|
|
110
|
-
if (model)
|
|
111
|
-
else
|
|
110
|
+
if (model) cliUpdate.model = `openai-compatible/${model}`
|
|
111
|
+
else cliUpdate.model = `openai-compatible/${defaultModelForProvider("openai-compatible")}`
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
await Config.save(
|
|
115
|
+
await Config.save({ cli: cliUpdate })
|
|
116
116
|
return { provider: `${detected} format` }
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -120,21 +120,21 @@ export async function saveProvider(url: string, key: string): Promise<{ provider
|
|
|
120
120
|
if (ENV_MAP[provider]) process.env[ENV_MAP[provider]] = key
|
|
121
121
|
|
|
122
122
|
const cfg = await Config.load()
|
|
123
|
-
const providers = cfg.providers || {}
|
|
123
|
+
const providers = cfg.cli?.providers || {}
|
|
124
124
|
providers[provider] = {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
apiKey: key,
|
|
126
|
+
apiType: provider === "anthropic" ? "anthropic" : provider === "google" ? "google" : provider === "openai" ? "openai" : "openai-compatible",
|
|
127
|
+
compatProfile: provider === "anthropic" ? "anthropic" : provider === "google" ? "google" : provider === "openai" ? "openai" : "openai-compatible",
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// Auto-set model for known providers
|
|
131
|
-
const
|
|
132
|
-
if (!cfg.model) {
|
|
131
|
+
const cliUpdate2: Record<string, unknown> = { providers, defaultProvider: provider }
|
|
132
|
+
if (!cfg.cli?.model) {
|
|
133
133
|
const { defaultModelForProvider } = await import("./models")
|
|
134
|
-
|
|
134
|
+
cliUpdate2.model = defaultModelForProvider(provider)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
await Config.save(
|
|
137
|
+
await Config.save({ cli: cliUpdate2 })
|
|
138
138
|
return { provider }
|
|
139
139
|
}
|
|
140
140
|
|
package/src/ai/models.ts
CHANGED
|
@@ -49,11 +49,11 @@ export function defaultModelForProvider(providerID?: string): string {
|
|
|
49
49
|
return PROVIDER_DEFAULT_MODEL[providerID] || DEFAULT_MODEL
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function resolveModelFromConfig(cfg: { model?: string;
|
|
53
|
-
const model = normalizeModelID(cfg.model)
|
|
52
|
+
export function resolveModelFromConfig(cfg: { cli?: { model?: string; defaultProvider?: string } }): string {
|
|
53
|
+
const model = normalizeModelID(cfg.cli?.model)
|
|
54
54
|
if (model) return model
|
|
55
|
-
const fallback = defaultModelForProvider(cfg.
|
|
56
|
-
if (cfg.
|
|
55
|
+
const fallback = defaultModelForProvider(cfg.cli?.defaultProvider)
|
|
56
|
+
if (cfg.cli?.defaultProvider === "openai-compatible" && !fallback.includes("/")) {
|
|
57
57
|
return `openai-compatible/${fallback}`
|
|
58
58
|
}
|
|
59
59
|
return fallback
|
|
@@ -49,7 +49,7 @@ function readFirstEnv(keys: string[]): string | undefined {
|
|
|
49
49
|
|
|
50
50
|
export async function loadProviders(cfgInput?: Config.CodeblogConfig): Promise<ProviderRegistryView> {
|
|
51
51
|
const cfg = cfgInput || await Config.load()
|
|
52
|
-
const user = cfg.providers || {}
|
|
52
|
+
const user = cfg.cli?.providers || {}
|
|
53
53
|
const ids = new Set<string>([
|
|
54
54
|
...Object.keys(PROVIDER_ENV),
|
|
55
55
|
...Object.keys(user),
|
|
@@ -62,12 +62,12 @@ export async function loadProviders(cfgInput?: Config.CodeblogConfig): Promise<P
|
|
|
62
62
|
providers[id] = {
|
|
63
63
|
id,
|
|
64
64
|
config,
|
|
65
|
-
apiKey: readFirstEnv(PROVIDER_ENV[id] || []) || config?.
|
|
66
|
-
baseURL: readFirstEnv(PROVIDER_BASE_URL_ENV[id] || []) || config?.
|
|
65
|
+
apiKey: readFirstEnv(PROVIDER_ENV[id] || []) || config?.apiKey,
|
|
66
|
+
baseURL: readFirstEnv(PROVIDER_BASE_URL_ENV[id] || []) || config?.baseUrl,
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
return { providers, defaultProvider: cfg.
|
|
70
|
+
return { providers, defaultProvider: cfg.cli?.defaultProvider }
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function availableProvidersWithKeys(providers: Record<string, ProviderRuntimeConfig>): string[] {
|
package/src/ai/provider.ts
CHANGED
|
@@ -40,7 +40,7 @@ export namespace AIProvider {
|
|
|
40
40
|
if (process.env[key]) return process.env[key]
|
|
41
41
|
}
|
|
42
42
|
const cfg = await Config.load()
|
|
43
|
-
return cfg.providers?.[providerID]?.
|
|
43
|
+
return cfg.cli?.providers?.[providerID]?.apiKey
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export async function getBaseUrl(providerID: string): Promise<string | undefined> {
|
|
@@ -49,7 +49,7 @@ export namespace AIProvider {
|
|
|
49
49
|
if (process.env[key]) return process.env[key]
|
|
50
50
|
}
|
|
51
51
|
const cfg = await Config.load()
|
|
52
|
-
return cfg.providers?.[providerID]?.
|
|
52
|
+
return cfg.cli?.providers?.[providerID]?.baseUrl
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export async function listProviders(): Promise<Record<string, { name: string; models: string[]; hasKey: boolean }>> {
|
|
@@ -89,7 +89,7 @@ export namespace AIProvider {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
export async function getModel(modelID?: string): Promise<LanguageModel> {
|
|
92
|
-
const useRegistry = await Config.featureEnabled("
|
|
92
|
+
const useRegistry = await Config.featureEnabled("aiProviderRegistryV2")
|
|
93
93
|
if (useRegistry) {
|
|
94
94
|
const route = await routeModel(modelID)
|
|
95
95
|
const customFetch = route.providerID === "codeblog" ? await loadCodeblogFetch() : undefined
|
|
@@ -99,7 +99,7 @@ export namespace AIProvider {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export async function resolveModelCompat(modelID?: string): Promise<ModelCompatConfig> {
|
|
102
|
-
const useRegistry = await Config.featureEnabled("
|
|
102
|
+
const useRegistry = await Config.featureEnabled("aiProviderRegistryV2")
|
|
103
103
|
if (useRegistry) return (await routeModel(modelID)).compat
|
|
104
104
|
return (await resolveLegacyRoute(modelID)).compat
|
|
105
105
|
}
|
|
@@ -130,7 +130,7 @@ export namespace AIProvider {
|
|
|
130
130
|
modelID: requested,
|
|
131
131
|
apiKey,
|
|
132
132
|
baseURL,
|
|
133
|
-
compat: resolveCompat({ providerID: builtin.providerID, modelID: requested, providerConfig: cfg.providers?.[builtin.providerID] }),
|
|
133
|
+
compat: resolveCompat({ providerID: builtin.providerID, modelID: requested, providerConfig: cfg.cli?.providers?.[builtin.providerID] }),
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -145,20 +145,20 @@ export namespace AIProvider {
|
|
|
145
145
|
modelID,
|
|
146
146
|
apiKey,
|
|
147
147
|
baseURL,
|
|
148
|
-
compat: resolveCompat({ providerID: providerID!, modelID, providerConfig: cfg.providers?.[providerID!] }),
|
|
148
|
+
compat: resolveCompat({ providerID: providerID!, modelID, providerConfig: cfg.cli?.providers?.[providerID!] }),
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
if (cfg.providers) {
|
|
153
|
-
for (const [providerID, p] of Object.entries(cfg.providers)) {
|
|
154
|
-
if (!p.
|
|
155
|
-
const baseURL = p.
|
|
152
|
+
if (cfg.cli?.providers) {
|
|
153
|
+
for (const [providerID, p] of Object.entries(cfg.cli.providers)) {
|
|
154
|
+
if (!p.apiKey) continue
|
|
155
|
+
const baseURL = p.baseUrl || (await getBaseUrl(providerID))
|
|
156
156
|
if (!baseURL) continue
|
|
157
|
-
log.info("legacy fallback: unknown model routed to first provider with
|
|
157
|
+
log.info("legacy fallback: unknown model routed to first provider with baseUrl", { provider: providerID, model: requested })
|
|
158
158
|
return {
|
|
159
159
|
providerID,
|
|
160
160
|
modelID: requested,
|
|
161
|
-
apiKey: p.
|
|
161
|
+
apiKey: p.apiKey,
|
|
162
162
|
baseURL,
|
|
163
163
|
compat: resolveCompat({ providerID, modelID: requested, providerConfig: p }),
|
|
164
164
|
}
|
package/src/ai/types.ts
CHANGED
|
@@ -50,10 +50,10 @@ function isOfficialOpenAIBase(baseURL?: string): boolean {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function resolveApiFromProvider(providerID: string, cfg?: Config.ProviderConfig): ModelApi {
|
|
53
|
-
if (providerID === "openai" && cfg?.
|
|
53
|
+
if (providerID === "openai" && cfg?.baseUrl && !isOfficialOpenAIBase(cfg.baseUrl)) {
|
|
54
54
|
return "openai-compatible"
|
|
55
55
|
}
|
|
56
|
-
if (cfg?.
|
|
56
|
+
if (cfg?.apiType) return cfg.apiType
|
|
57
57
|
if (providerID === "anthropic") return "anthropic"
|
|
58
58
|
if (providerID === "openai") return "openai"
|
|
59
59
|
if (providerID === "google") return "google"
|
|
@@ -74,7 +74,7 @@ export function resolveCompat(args: {
|
|
|
74
74
|
providerConfig?: Config.ProviderConfig
|
|
75
75
|
}): ModelCompatConfig {
|
|
76
76
|
const api = resolveApiFromProvider(args.providerID, args.providerConfig)
|
|
77
|
-
const configured = args.providerConfig?.
|
|
77
|
+
const configured = args.providerConfig?.compatProfile
|
|
78
78
|
const compatProfile = api === "openai-compatible" && configured === "openai"
|
|
79
79
|
? "openai-compatible"
|
|
80
80
|
: configured || defaultCompatForApi(api)
|
package/src/auth/index.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import path from "path"
|
|
2
|
-
import { chmod, writeFile } from "fs/promises"
|
|
3
|
-
import { Global } from "../global"
|
|
4
1
|
import z from "zod"
|
|
2
|
+
import { Config } from "../config"
|
|
5
3
|
|
|
6
4
|
export namespace Auth {
|
|
7
5
|
export const Token = z
|
|
@@ -14,36 +12,27 @@ export namespace Auth {
|
|
|
14
12
|
.meta({ ref: "AuthToken" })
|
|
15
13
|
export type Token = z.infer<typeof Token>
|
|
16
14
|
|
|
17
|
-
const filepath = path.join(Global.Path.data, "auth.json")
|
|
18
|
-
|
|
19
15
|
export async function get(): Promise<Token | null> {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const parsed = Token.safeParse(data)
|
|
24
|
-
if (!parsed.success) return null
|
|
25
|
-
return parsed.data
|
|
16
|
+
const cfg = await Config.load()
|
|
17
|
+
if (!cfg.auth?.apiKey) return null
|
|
18
|
+
return { type: "apikey", value: cfg.auth.apiKey, username: cfg.auth.username }
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
export async function set(token: Token) {
|
|
29
|
-
await
|
|
30
|
-
await chmod(filepath, 0o600).catch(() => {})
|
|
22
|
+
await Config.save({ auth: { apiKey: token.value, username: token.username } })
|
|
31
23
|
}
|
|
32
24
|
|
|
33
25
|
export async function remove() {
|
|
34
|
-
|
|
35
|
-
await fs.unlink(filepath).catch(() => {})
|
|
26
|
+
await Config.save({ auth: { apiKey: undefined, userId: undefined, activeAgent: undefined, username: undefined } })
|
|
36
27
|
}
|
|
37
28
|
|
|
38
29
|
export async function header(): Promise<Record<string, string>> {
|
|
39
30
|
const token = await get()
|
|
40
31
|
if (!token) return {}
|
|
41
|
-
if (token.type === "apikey") return { Authorization: `Bearer ${token.value}` }
|
|
42
32
|
return { Authorization: `Bearer ${token.value}` }
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
export async function authenticated(): Promise<boolean> {
|
|
46
|
-
|
|
47
|
-
return token !== null
|
|
36
|
+
return (await get()) !== null
|
|
48
37
|
}
|
|
49
38
|
}
|
package/src/auth/oauth.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Auth } from "./index"
|
|
2
2
|
import { Config } from "../config"
|
|
3
|
-
import { McpBridge } from "../mcp/client"
|
|
4
3
|
import { Server } from "../server"
|
|
5
4
|
import { Log } from "../util/log"
|
|
6
5
|
|
|
@@ -58,12 +57,6 @@ export namespace OAuth {
|
|
|
58
57
|
throw new Error(ownerMismatch)
|
|
59
58
|
}
|
|
60
59
|
} else {
|
|
61
|
-
// Sync API key to MCP config (~/.codeblog/config.json)
|
|
62
|
-
try {
|
|
63
|
-
await McpBridge.callTool("codeblog_setup", { api_key: key })
|
|
64
|
-
} catch (err) {
|
|
65
|
-
log.warn("failed to sync API key to MCP config", { error: String(err) })
|
|
66
|
-
}
|
|
67
60
|
log.info("authenticated with api key")
|
|
68
61
|
}
|
|
69
62
|
} else if (token) {
|
package/src/cli/cmd/config.ts
CHANGED
|
@@ -86,12 +86,12 @@ export const ConfigCommand: CommandModule = {
|
|
|
86
86
|
|
|
87
87
|
if (args.provider && (args.apiKey || args.baseUrl)) {
|
|
88
88
|
const cfg = await Config.load()
|
|
89
|
-
const providers = cfg.providers || {}
|
|
89
|
+
const providers = cfg.cli?.providers || {}
|
|
90
90
|
const existing = providers[args.provider as string] || {} as Config.ProviderConfig
|
|
91
|
-
if (args.apiKey) existing.
|
|
92
|
-
if (args.baseUrl) existing.
|
|
91
|
+
if (args.apiKey) existing.apiKey = args.apiKey as string
|
|
92
|
+
if (args.baseUrl) existing.baseUrl = args.baseUrl as string
|
|
93
93
|
providers[args.provider as string] = existing
|
|
94
|
-
await Config.save({ providers })
|
|
94
|
+
await Config.save({ cli: { providers } })
|
|
95
95
|
const parts: string[] = []
|
|
96
96
|
if (args.apiKey) parts.push("API key")
|
|
97
97
|
if (args.baseUrl) parts.push(`base URL (${args.baseUrl})`)
|
|
@@ -100,19 +100,19 @@ export const ConfigCommand: CommandModule = {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (args.model) {
|
|
103
|
-
await Config.save({ model: args.model as string })
|
|
103
|
+
await Config.save({ cli: { model: args.model as string } })
|
|
104
104
|
UI.success(`Default model set to ${args.model}`)
|
|
105
105
|
return
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
if (args.url) {
|
|
109
|
-
await Config.save({
|
|
109
|
+
await Config.save({ serverUrl: args.url as string })
|
|
110
110
|
UI.success(`Server URL set to ${args.url}`)
|
|
111
111
|
return
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
if (args.language) {
|
|
115
|
-
await Config.save({
|
|
115
|
+
await Config.save({ cli: { defaultLanguage: args.language as string } })
|
|
116
116
|
UI.success(`Default language set to ${args.language}`)
|
|
117
117
|
return
|
|
118
118
|
}
|
|
@@ -121,22 +121,22 @@ export const ConfigCommand: CommandModule = {
|
|
|
121
121
|
const cfg = await Config.load()
|
|
122
122
|
const { resolveModelFromConfig } = await import("../../ai/models")
|
|
123
123
|
const model = resolveModelFromConfig(cfg) || AIProvider.DEFAULT_MODEL
|
|
124
|
-
const providers = cfg.providers || {}
|
|
124
|
+
const providers = cfg.cli?.providers || {}
|
|
125
125
|
|
|
126
126
|
console.log("")
|
|
127
127
|
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}Current Config${UI.Style.TEXT_NORMAL}`)
|
|
128
128
|
console.log(` ${UI.Style.TEXT_DIM}${Config.filepath}${UI.Style.TEXT_NORMAL}`)
|
|
129
129
|
console.log("")
|
|
130
130
|
console.log(` Model: ${UI.Style.TEXT_HIGHLIGHT}${model}${UI.Style.TEXT_NORMAL}`)
|
|
131
|
-
console.log(` API URL: ${cfg.
|
|
132
|
-
console.log(` Language: ${cfg.
|
|
131
|
+
console.log(` API URL: ${cfg.serverUrl || "https://codeblog.ai"}`)
|
|
132
|
+
console.log(` Language: ${cfg.cli?.defaultLanguage || `${UI.Style.TEXT_DIM}(server default)${UI.Style.TEXT_NORMAL}`}`)
|
|
133
133
|
console.log("")
|
|
134
134
|
|
|
135
135
|
if (Object.keys(providers).length > 0) {
|
|
136
136
|
console.log(` ${UI.Style.TEXT_NORMAL_BOLD}AI Providers${UI.Style.TEXT_NORMAL}`)
|
|
137
137
|
for (const [id, p] of Object.entries(providers)) {
|
|
138
|
-
const masked = p.
|
|
139
|
-
const url = p.
|
|
138
|
+
const masked = p.apiKey ? p.apiKey.slice(0, 8) + "..." : "not set"
|
|
139
|
+
const url = p.baseUrl ? ` → ${p.baseUrl}` : ""
|
|
140
140
|
console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${id}: ${UI.Style.TEXT_DIM}${masked}${url}${UI.Style.TEXT_NORMAL}`)
|
|
141
141
|
}
|
|
142
142
|
} else {
|
package/src/cli/cmd/login.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { OAuth } from "../../auth/oauth"
|
|
3
3
|
import { Auth } from "../../auth"
|
|
4
|
-
import { McpBridge } from "../../mcp/client"
|
|
5
4
|
import { UI } from "../ui"
|
|
6
5
|
|
|
7
6
|
export const LoginCommand: CommandModule = {
|
|
@@ -23,12 +22,6 @@ export const LoginCommand: CommandModule = {
|
|
|
23
22
|
if (args.key) {
|
|
24
23
|
const key = args.key as string
|
|
25
24
|
await Auth.set({ type: "apikey", value: key })
|
|
26
|
-
// Sync API key to MCP config (~/.codeblog/config.json)
|
|
27
|
-
try {
|
|
28
|
-
await McpBridge.callTool("codeblog_setup", { api_key: key })
|
|
29
|
-
} catch {
|
|
30
|
-
// Non-fatal: MCP sync failed but CLI auth is saved
|
|
31
|
-
}
|
|
32
25
|
UI.success("Logged in with API key")
|
|
33
26
|
return
|
|
34
27
|
}
|
package/src/cli/cmd/logout.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs"
|
|
2
2
|
import { Auth } from "../../auth"
|
|
3
|
-
import { Config } from "../../config"
|
|
4
3
|
import { UI } from "../ui"
|
|
5
4
|
|
|
6
5
|
export const LogoutCommand: CommandModule = {
|
|
7
6
|
command: "logout",
|
|
8
7
|
describe: "Logout from CodeBlog",
|
|
9
8
|
handler: async () => {
|
|
9
|
+
const { McpBridge } = await import("../../mcp/client")
|
|
10
|
+
const { clearChatToolsCache } = await import("../../ai/tools")
|
|
10
11
|
await Auth.remove()
|
|
11
|
-
await
|
|
12
|
+
await McpBridge.disconnect()
|
|
13
|
+
clearChatToolsCache()
|
|
12
14
|
UI.success("Logged out successfully")
|
|
13
15
|
},
|
|
14
16
|
}
|