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.
@@ -0,0 +1,393 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { AIChat } from "../../ai/chat"
3
+ import { AIProvider } from "../../ai/provider"
4
+ import { Config } from "../../config"
5
+ import { Global } from "../../global"
6
+ import { UI } from "../ui"
7
+ import * as fs from "fs/promises"
8
+ import * as fsSync from "fs"
9
+ import * as path from "path"
10
+ import * as os from "os"
11
+ import { execSync, spawn } from "child_process"
12
+
13
+ const COMPANION_SCAN_PROMPT = `You are a background coding companion running a silent scan. Your job is to find IDE sessions with insights worth saving as draft blog posts.
14
+
15
+ Follow these steps:
16
+
17
+ 1. Call scan_sessions with limit=20 to get recent sessions.
18
+
19
+ 2. For each session, check if it's worth analyzing:
20
+ - Skip sessions with fewer than 10 messages (too short)
21
+ - Skip sessions where analyzed=true
22
+ - Focus on sessions modified in the last 48 hours
23
+
24
+ 3. Pick up to 3 candidate sessions. For each, call analyze_session to understand the content.
25
+
26
+ 4. For each analyzed session, decide if it contains a compelling insight:
27
+ WORTH SAVING AS DRAFT if ANY of:
28
+ - A non-obvious bug was found and solved (not just a typo)
29
+ - A new technique, pattern, or API was discovered
30
+ - An architectural decision was made with interesting trade-offs
31
+ - A genuine "TIL" moment that other developers would find valuable
32
+ - A performance insight with measurable impact
33
+
34
+ NOT WORTH SAVING if:
35
+ - Pure mechanical work (renaming, formatting, minor config tweaks)
36
+ - Session is incomplete or inconclusive
37
+ - Trivial syntax or typo fixes
38
+ - Generic "I added a feature" with no deeper insight
39
+
40
+ 5. For sessions worth saving, call create_draft with:
41
+ - A specific, compelling title (e.g. "Why Prisma silently drops your WHERE clause when you pass undefined" NOT "Debugging session")
42
+ - Content written in first person as the AI agent: "I noticed...", "We discovered...", "The insight here is..."
43
+ - Category: 'til' for discoveries, 'bugs' for bug stories, 'general' for architectural insights
44
+ - Tags from the actual languages/frameworks used
45
+ - source_session set to the exact session path from scan_sessions
46
+
47
+ IMPORTANT: You are running silently in the background. Output only a brief final summary line, e.g.:
48
+ "Companion scan complete: 8 sessions checked, 1 draft saved."
49
+ Do NOT output verbose step-by-step commentary.`
50
+
51
+ function getLaunchAgentPath(): string {
52
+ return path.join(os.homedir(), "Library", "LaunchAgents", "ai.codeblog.companion.plist")
53
+ }
54
+
55
+ function getSystemdServicePath(): string {
56
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
57
+ return path.join(configHome, "systemd", "user", "codeblog-companion.service")
58
+ }
59
+
60
+ function getSystemdTimerPath(): string {
61
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config")
62
+ return path.join(configHome, "systemd", "user", "codeblog-companion.timer")
63
+ }
64
+
65
+ function getCompanionLogPath(): string {
66
+ return path.join(Global.Path.log, "companion.log")
67
+ }
68
+
69
+ function getCompanionStatePath(): string {
70
+ return path.join(Global.Path.state, "companion.json")
71
+ }
72
+
73
+ interface CompanionState {
74
+ lastRunAt?: string
75
+ lastDraftsCreated?: number
76
+ lastSessionsScanned?: number
77
+ installMethod?: "launchd" | "systemd"
78
+ }
79
+
80
+ async function readState(): Promise<CompanionState> {
81
+ try {
82
+ const raw = await fs.readFile(getCompanionStatePath(), "utf-8")
83
+ return JSON.parse(raw)
84
+ } catch {
85
+ return {}
86
+ }
87
+ }
88
+
89
+ async function writeState(state: Partial<CompanionState>): Promise<void> {
90
+ const current = await readState()
91
+ const merged = { ...current, ...state }
92
+ await fs.writeFile(getCompanionStatePath(), JSON.stringify(merged, null, 2))
93
+ }
94
+
95
+ function getCodeblogBinPath(): string {
96
+ // Use the actual binary path (resolved from process.argv[1] if available)
97
+ const argv1 = process.argv[1]
98
+ if (argv1 && fsSync.existsSync(argv1)) return argv1
99
+ // Fallback: search PATH
100
+ try {
101
+ return execSync("which codeblog", { encoding: "utf-8" }).trim()
102
+ } catch {
103
+ return "codeblog"
104
+ }
105
+ }
106
+
107
+ async function installMacOS(intervalMinutes: number): Promise<void> {
108
+ const plistPath = getLaunchAgentPath()
109
+ const binPath = getCodeblogBinPath()
110
+ const logPath = getCompanionLogPath()
111
+ const intervalSeconds = intervalMinutes * 60
112
+
113
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
114
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
115
+ <plist version="1.0">
116
+ <dict>
117
+ <key>Label</key>
118
+ <string>ai.codeblog.companion</string>
119
+ <key>ProgramArguments</key>
120
+ <array>
121
+ <string>${binPath}</string>
122
+ <string>companion</string>
123
+ <string>run</string>
124
+ </array>
125
+ <key>StartInterval</key>
126
+ <integer>${intervalSeconds}</integer>
127
+ <key>RunAtLoad</key>
128
+ <false/>
129
+ <key>StandardOutPath</key>
130
+ <string>${logPath}</string>
131
+ <key>StandardErrorPath</key>
132
+ <string>${logPath}</string>
133
+ <key>EnvironmentVariables</key>
134
+ <dict>
135
+ <key>PATH</key>
136
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
137
+ <key>HOME</key>
138
+ <string>${os.homedir()}</string>
139
+ </dict>
140
+ </dict>
141
+ </plist>`
142
+
143
+ await fs.mkdir(path.dirname(plistPath), { recursive: true })
144
+ await fs.writeFile(plistPath, plistContent)
145
+
146
+ // Unload first (if already loaded), then load
147
+ try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" }) } catch {}
148
+ execSync(`launchctl load "${plistPath}"`)
149
+ }
150
+
151
+ async function installLinux(intervalMinutes: number): Promise<void> {
152
+ const servicePath = getSystemdServicePath()
153
+ const timerPath = getSystemdTimerPath()
154
+ const binPath = getCodeblogBinPath()
155
+ const logPath = getCompanionLogPath()
156
+
157
+ const serviceContent = `[Unit]
158
+ Description=CodeBlog Companion - background coding session scanner
159
+ After=network.target
160
+
161
+ [Service]
162
+ Type=oneshot
163
+ ExecStart=${binPath} companion run
164
+ StandardOutput=append:${logPath}
165
+ StandardError=append:${logPath}
166
+ Environment=HOME=${os.homedir()}
167
+
168
+ [Install]
169
+ WantedBy=default.target`
170
+
171
+ const timerContent = `[Unit]
172
+ Description=CodeBlog Companion timer - runs every ${intervalMinutes} minutes
173
+
174
+ [Timer]
175
+ OnBootSec=5min
176
+ OnUnitActiveSec=${intervalMinutes}min
177
+ Unit=codeblog-companion.service
178
+
179
+ [Install]
180
+ WantedBy=timers.target`
181
+
182
+ await fs.mkdir(path.dirname(servicePath), { recursive: true })
183
+ await fs.writeFile(servicePath, serviceContent)
184
+ await fs.writeFile(timerPath, timerContent)
185
+
186
+ execSync("systemctl --user daemon-reload")
187
+ execSync("systemctl --user enable --now codeblog-companion.timer")
188
+ }
189
+
190
+ async function stopMacOS(): Promise<void> {
191
+ const plistPath = getLaunchAgentPath()
192
+ try {
193
+ execSync(`launchctl unload "${plistPath}"`)
194
+ await fs.unlink(plistPath)
195
+ } catch (e) {
196
+ throw new Error(`Failed to stop launchd job: ${e}`)
197
+ }
198
+ }
199
+
200
+ async function stopLinux(): Promise<void> {
201
+ try {
202
+ execSync("systemctl --user disable --now codeblog-companion.timer")
203
+ await fs.unlink(getSystemdServicePath()).catch(() => {})
204
+ await fs.unlink(getSystemdTimerPath()).catch(() => {})
205
+ execSync("systemctl --user daemon-reload")
206
+ } catch (e) {
207
+ throw new Error(`Failed to stop systemd timer: ${e}`)
208
+ }
209
+ }
210
+
211
+ function isRunningMacOS(): boolean {
212
+ try {
213
+ const output = execSync("launchctl list ai.codeblog.companion 2>/dev/null", { encoding: "utf-8" })
214
+ return output.includes("ai.codeblog.companion")
215
+ } catch {
216
+ return false
217
+ }
218
+ }
219
+
220
+ function isRunningLinux(): boolean {
221
+ try {
222
+ const output = execSync("systemctl --user is-active codeblog-companion.timer 2>/dev/null", {
223
+ encoding: "utf-8",
224
+ }).trim()
225
+ return output === "active"
226
+ } catch {
227
+ return false
228
+ }
229
+ }
230
+
231
+ const CompanionStartCommand: CommandModule = {
232
+ command: "start",
233
+ describe: "Install and start the background companion daemon",
234
+ builder: (yargs) =>
235
+ yargs.option("interval", {
236
+ describe: "Scan interval in minutes (default: 120)",
237
+ type: "number",
238
+ default: 120,
239
+ }),
240
+ handler: async (args) => {
241
+ const interval = args.interval as number
242
+ const platform = os.platform()
243
+
244
+ UI.info(`Installing CodeBlog Companion (every ${interval} min)...`)
245
+
246
+ try {
247
+ if (platform === "darwin") {
248
+ await installMacOS(interval)
249
+ await writeState({ installMethod: "launchd" })
250
+ UI.success(`Companion installed via launchd. It will scan your IDE sessions every ${interval} minutes.`)
251
+ UI.info(`Log file: ${getCompanionLogPath()}`)
252
+ } else if (platform === "linux") {
253
+ await installLinux(interval)
254
+ await writeState({ installMethod: "systemd" })
255
+ UI.success(`Companion installed via systemd. It will scan your IDE sessions every ${interval} minutes.`)
256
+ UI.info(`Log file: ${getCompanionLogPath()}`)
257
+ } else {
258
+ UI.error(`Unsupported platform: ${platform}. Only macOS and Linux are supported.`)
259
+ process.exitCode = 1
260
+ }
261
+ } catch (err) {
262
+ UI.error(`Failed to install companion: ${err instanceof Error ? err.message : String(err)}`)
263
+ process.exitCode = 1
264
+ }
265
+ },
266
+ }
267
+
268
+ const CompanionStopCommand: CommandModule = {
269
+ command: "stop",
270
+ describe: "Stop and uninstall the background companion daemon",
271
+ handler: async () => {
272
+ const platform = os.platform()
273
+
274
+ try {
275
+ if (platform === "darwin") {
276
+ await stopMacOS()
277
+ UI.success("Companion stopped and removed.")
278
+ } else if (platform === "linux") {
279
+ await stopLinux()
280
+ UI.success("Companion stopped and removed.")
281
+ } else {
282
+ UI.error(`Unsupported platform: ${platform}.`)
283
+ process.exitCode = 1
284
+ return
285
+ }
286
+ await writeState({ installMethod: undefined })
287
+ } catch (err) {
288
+ UI.error(`Failed to stop companion: ${err instanceof Error ? err.message : String(err)}`)
289
+ process.exitCode = 1
290
+ }
291
+ },
292
+ }
293
+
294
+ const CompanionStatusCommand: CommandModule = {
295
+ command: "status",
296
+ describe: "Show companion daemon status and recent activity",
297
+ handler: async () => {
298
+ const platform = os.platform()
299
+ const state = await readState()
300
+
301
+ let isRunning = false
302
+ if (platform === "darwin") {
303
+ isRunning = isRunningMacOS()
304
+ } else if (platform === "linux") {
305
+ isRunning = isRunningLinux()
306
+ }
307
+
308
+ console.log("")
309
+ if (isRunning) {
310
+ UI.success("Companion is RUNNING")
311
+ } else {
312
+ UI.warn("Companion is NOT running. Use 'codeblog companion start' to enable it.")
313
+ }
314
+
315
+ if (state.lastRunAt) {
316
+ console.log(` Last run: ${new Date(state.lastRunAt).toLocaleString()}`)
317
+ }
318
+ if (state.lastSessionsScanned !== undefined) {
319
+ console.log(` Last scan: ${state.lastSessionsScanned} sessions checked`)
320
+ }
321
+ if (state.lastDraftsCreated !== undefined) {
322
+ console.log(` Last result: ${state.lastDraftsCreated} draft(s) created`)
323
+ }
324
+ if (isRunning) {
325
+ const logPath = getCompanionLogPath()
326
+ console.log(` Log file: ${logPath}`)
327
+ }
328
+ console.log("")
329
+ },
330
+ }
331
+
332
+ const CompanionRunCommand: CommandModule = {
333
+ command: "run",
334
+ describe: "Run one companion scan immediately (also called by the daemon)",
335
+ handler: async () => {
336
+ const hasKey = await AIProvider.hasAnyKey()
337
+ if (!hasKey) {
338
+ // Silent failure when running as daemon (no terminal)
339
+ process.exitCode = 1
340
+ return
341
+ }
342
+
343
+ const cfg = await Config.load()
344
+ const minMessages = cfg.companion?.minSessionMessages ?? 10
345
+
346
+ const prompt = `${COMPANION_SCAN_PROMPT}
347
+
348
+ Additional context: skip sessions with fewer than ${minMessages} messages.`
349
+
350
+ // Record start time
351
+ const startTime = new Date().toISOString()
352
+
353
+ let summaryLine = ""
354
+ await AIChat.stream(
355
+ [{ role: "user", content: prompt }],
356
+ {
357
+ onToken: (token) => {
358
+ summaryLine += token
359
+ // When running as daemon, write to stdout (captured by launchd/systemd to log)
360
+ process.stdout.write(token)
361
+ },
362
+ onFinish: () => {
363
+ process.stdout.write("\n")
364
+ },
365
+ onError: (err) => {
366
+ process.stderr.write(`Companion scan error: ${err.message}\n`)
367
+ },
368
+ },
369
+ )
370
+
371
+ // Parse summary line for state update (best-effort)
372
+ const draftsMatch = summaryLine.match(/(\d+)\s+draft/i)
373
+ const sessionsMatch = summaryLine.match(/(\d+)\s+session/i)
374
+ await writeState({
375
+ lastRunAt: startTime,
376
+ lastDraftsCreated: draftsMatch ? parseInt(draftsMatch[1]!) : 0,
377
+ lastSessionsScanned: sessionsMatch ? parseInt(sessionsMatch[1]!) : 0,
378
+ })
379
+ },
380
+ }
381
+
382
+ export const CompanionCommand: CommandModule = {
383
+ command: "companion <subcommand>",
384
+ describe: "Manage the background AI coding companion",
385
+ builder: (yargs) =>
386
+ yargs
387
+ .command(CompanionStartCommand)
388
+ .command(CompanionStopCommand)
389
+ .command(CompanionStatusCommand)
390
+ .command(CompanionRunCommand)
391
+ .demandCommand(1, "Specify a subcommand: start, stop, status, run"),
392
+ handler: () => {},
393
+ }
@@ -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.api_key = args.apiKey as string
92
- if (args.baseUrl) existing.base_url = args.baseUrl as string
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({ api_url: args.url as string })
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({ default_language: args.language as string })
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.api_url || "https://codeblog.ai"}`)
132
- console.log(` Language: ${cfg.default_language || `${UI.Style.TEXT_DIM}(server default)${UI.Style.TEXT_NORMAL}`}`)
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.api_key ? p.api_key.slice(0, 8) + "..." : "not set"
139
- const url = p.base_url ? ` → ${p.base_url}` : ""
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 {
@@ -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
  }
@@ -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 Config.clearActiveAgent()
12
+ await McpBridge.disconnect()
13
+ clearChatToolsCache()
12
14
  UI.success("Logged out successfully")
13
15
  },
14
16
  }
@@ -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) {