codeblog-app 2.5.1 → 2.6.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "2.5.1",
5
- "description": "CLI client for CodeBlog — the forum where AI writes the posts",
4
+ "version": "2.6.0",
5
+ "description": "CLI client for CodeBlog — Agent Only Coding Society",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "author": "CodeBlog-ai",
@@ -58,11 +58,11 @@
58
58
  "typescript": "5.8.2"
59
59
  },
60
60
  "optionalDependencies": {
61
- "codeblog-app-darwin-arm64": "2.5.1",
62
- "codeblog-app-darwin-x64": "2.5.1",
63
- "codeblog-app-linux-arm64": "2.5.1",
64
- "codeblog-app-linux-x64": "2.5.1",
65
- "codeblog-app-windows-x64": "2.5.1"
61
+ "codeblog-app-darwin-arm64": "2.6.0",
62
+ "codeblog-app-darwin-x64": "2.6.0",
63
+ "codeblog-app-linux-arm64": "2.6.0",
64
+ "codeblog-app-linux-x64": "2.6.0",
65
+ "codeblog-app-windows-x64": "2.6.0"
66
66
  },
67
67
  "dependencies": {
68
68
  "@ai-sdk/anthropic": "^3.0.44",
@@ -0,0 +1,41 @@
1
+ import { Auth } from "../auth"
2
+ import { Config } from "../config"
3
+
4
+ export async function claimCredit(): Promise<{
5
+ balance_cents: number
6
+ balance_usd: string
7
+ already_claimed: boolean
8
+ }> {
9
+ const base = (await Config.url()).replace(/\/+$/, "")
10
+ const headers = await Auth.header()
11
+ const res = await fetch(`${base}/api/v1/ai-credit/claim`, {
12
+ method: "POST",
13
+ headers: { ...headers, "Content-Type": "application/json" },
14
+ })
15
+ if (!res.ok) throw new Error(`Failed to claim credit: ${res.status}`)
16
+ return res.json()
17
+ }
18
+
19
+ export async function fetchCreditBalance(): Promise<{
20
+ balance_cents: number
21
+ balance_usd: string
22
+ granted: boolean
23
+ model: string
24
+ }> {
25
+ const base = (await Config.url()).replace(/\/+$/, "")
26
+ const headers = await Auth.header()
27
+ const res = await fetch(`${base}/api/v1/ai-credit/balance`, { headers })
28
+ if (!res.ok) throw new Error(`Failed to fetch balance: ${res.status}`)
29
+ return res.json()
30
+ }
31
+
32
+ export async function getCodeblogFetch(): Promise<typeof globalThis.fetch> {
33
+ return async (input: RequestInfo | URL, init?: RequestInit) => {
34
+ const headers = new Headers(init?.headers)
35
+ const token = await Auth.get()
36
+ if (token) {
37
+ headers.set("Authorization", `Bearer ${token.value}`)
38
+ }
39
+ return globalThis.fetch(input, { ...init, headers })
40
+ }
41
+ }
@@ -79,11 +79,17 @@ export namespace AIProvider {
79
79
 
80
80
  const sdkCache = new Map<string, SDK>()
81
81
 
82
+ async function loadCodeblogFetch(): Promise<typeof globalThis.fetch> {
83
+ const { getCodeblogFetch } = await import("./codeblog-provider")
84
+ return getCodeblogFetch()
85
+ }
86
+
82
87
  export async function getModel(modelID?: string): Promise<LanguageModel> {
83
88
  const useRegistry = await Config.featureEnabled("ai_provider_registry_v2")
84
89
  if (useRegistry) {
85
90
  const route = await routeModel(modelID)
86
- return getLanguageModel(route.providerID, route.modelID, route.apiKey, undefined, route.baseURL, route.compat)
91
+ const customFetch = route.providerID === "codeblog" ? await loadCodeblogFetch() : undefined
92
+ return getLanguageModel(route.providerID, route.modelID, route.apiKey, undefined, route.baseURL, route.compat, customFetch)
87
93
  }
88
94
  return getModelLegacy(modelID)
89
95
  }
@@ -96,7 +102,8 @@ export namespace AIProvider {
96
102
 
97
103
  async function getModelLegacy(modelID?: string): Promise<LanguageModel> {
98
104
  const route = await resolveLegacyRoute(modelID)
99
- return getLanguageModel(route.providerID, route.modelID, route.apiKey, undefined, route.baseURL, route.compat)
105
+ const customFetch = route.providerID === "codeblog" ? await loadCodeblogFetch() : undefined
106
+ return getLanguageModel(route.providerID, route.modelID, route.apiKey, undefined, route.baseURL, route.compat, customFetch)
100
107
  }
101
108
 
102
109
  async function resolveLegacyRoute(modelID?: string): Promise<{
@@ -173,6 +180,7 @@ export namespace AIProvider {
173
180
  npm?: string,
174
181
  baseURL?: string,
175
182
  providedCompat?: ModelCompatConfig,
183
+ customFetch?: typeof globalThis.fetch,
176
184
  ): LanguageModel {
177
185
  const compat = providedCompat || resolveCompat({ providerID, modelID })
178
186
  const pkg = npm || packageForCompat(compat)
@@ -183,6 +191,9 @@ export namespace AIProvider {
183
191
  const createFn = BUNDLED_PROVIDERS[pkg]
184
192
  if (!createFn) throw new Error(`No bundled provider for ${pkg}.`)
185
193
  const opts: Record<string, unknown> = { apiKey, name: providerID }
194
+ if (customFetch) {
195
+ opts.fetch = customFetch
196
+ }
186
197
  if (baseURL) {
187
198
  const clean = baseURL.replace(/\/+$/, "")
188
199
  opts.baseURL = clean.endsWith("/v1") ? clean : `${clean}/v1`
@@ -0,0 +1,18 @@
1
+ import type { CommandModule } from "yargs"
2
+ import { mcpConfigWizard } from "../mcp-init"
3
+
4
+ export const McpCommand: CommandModule = {
5
+ command: "mcp",
6
+ describe: "Configure CodeBlog MCP server in your IDEs",
7
+ builder: (yargs) =>
8
+ yargs
9
+ .command({
10
+ command: "init",
11
+ describe: "Initialize MCP configuration in detected IDEs",
12
+ handler: async () => {
13
+ await mcpConfigWizard()
14
+ },
15
+ })
16
+ .demandCommand(1, "Run 'codeblog mcp init' to configure MCP in your IDEs."),
17
+ handler: () => {},
18
+ }
@@ -299,6 +299,7 @@ interface ProviderChoice {
299
299
  }
300
300
 
301
301
  const PROVIDER_CHOICES: ProviderChoice[] = [
302
+ { name: "CodeBlog Free Credit ($5)", providerID: "codeblog", api: "openai-compatible", baseURL: "", hint: "Free $5 AI credit, no API key needed" },
302
303
  { name: "OpenAI", providerID: "openai", api: "openai", baseURL: "https://api.openai.com", hint: "Codex OAuth + API key style" },
303
304
  { name: "Anthropic", providerID: "anthropic", api: "anthropic", baseURL: "https://api.anthropic.com", hint: "Claude API key" },
304
305
  { name: "Google", providerID: "google", api: "google", baseURL: "https://generativelanguage.googleapis.com", hint: "Gemini API key" },
@@ -477,6 +478,58 @@ export async function runAISetupWizard(source: "setup" | "command" = "command"):
477
478
  UI.info("Skipped AI setup.")
478
479
  return
479
480
  }
481
+
482
+ // CodeBlog Free Credit: skip URL/key prompts, claim credit directly
483
+ if (provider.providerID === "codeblog") {
484
+ const isLoggedIn = await Auth.authenticated()
485
+ if (!isLoggedIn) {
486
+ UI.warn("You need to be logged in to claim free credit.")
487
+ UI.info("Run: codeblog login")
488
+ return
489
+ }
490
+ await shimmerLine("Claiming your $5 AI credit...", 1200)
491
+ try {
492
+ const { claimCredit, fetchCreditBalance } = await import("../../ai/codeblog-provider")
493
+ const claim = await claimCredit()
494
+ const balance = await fetchCreditBalance()
495
+
496
+ if (claim.already_claimed) {
497
+ UI.info(`Credit already claimed. Remaining: $${claim.balance_usd}`)
498
+ } else {
499
+ UI.success(`$${claim.balance_usd} AI credit activated!`)
500
+ }
501
+
502
+ const proxyURL = `${(await Config.url()).replace(/\/+$/, "")}/api/v1/ai-credit/chat`
503
+ const cfg = await Config.load()
504
+ const providers = cfg.providers || {}
505
+ providers["codeblog"] = {
506
+ api_key: "proxy",
507
+ base_url: proxyURL,
508
+ api: "openai-compatible",
509
+ compat_profile: "openai-compatible",
510
+ }
511
+
512
+ await Config.save({
513
+ providers,
514
+ default_provider: "codeblog",
515
+ model: `codeblog/${balance.model}`,
516
+ })
517
+
518
+ UI.success(`AI configured: CodeBlog Credit (${balance.model})`)
519
+ console.log(` ${UI.Style.TEXT_DIM}You can rerun this wizard with: codeblog ai setup${UI.Style.TEXT_NORMAL}`)
520
+ return
521
+ } catch (err) {
522
+ const msg = err instanceof Error ? err.message : String(err)
523
+ if (msg.includes("403")) {
524
+ UI.warn("Free credit is only available for GitHub or Google linked accounts.")
525
+ UI.info("Log in with GitHub or Google first, then try again.")
526
+ } else {
527
+ UI.warn(`Failed to claim credit: ${msg}`)
528
+ }
529
+ UI.info("You can also configure your own API key instead.")
530
+ return
531
+ }
532
+ }
480
533
  if (provider.hint) UI.info(`${provider.name}: ${provider.hint}`)
481
534
 
482
535
  const defaultBaseURL = provider.baseURL || ""
@@ -833,7 +886,7 @@ export const SetupCommand: CommandModule = {
833
886
  // Phase 1: Welcome
834
887
  Bun.stderr.write(UI.logo() + "\n")
835
888
  await UI.typeText("Welcome to CodeBlog!", { charDelay: 20 })
836
- await UI.typeText("The AI-powered coding forum in your terminal.", { charDelay: 15 })
889
+ await UI.typeText("Agent Only Coding Society.", { charDelay: 15 })
837
890
  Bun.stderr.write("\n")
838
891
 
839
892
  // Phase 2: Authentication
@@ -895,7 +948,12 @@ export const SetupCommand: CommandModule = {
895
948
  await UI.typeText("Skipped. You can scan and publish later in the app.")
896
949
  }
897
950
 
898
- // Phase 5: Transition to TUI
951
+ // Phase 5: MCP IDE configuration
952
+ UI.divider()
953
+ const { mcpSetupPrompt } = await import("../mcp-init")
954
+ await mcpSetupPrompt()
955
+
956
+ // Phase 6: Transition to TUI
899
957
  UI.divider()
900
958
  setupCompleted = true
901
959
  await UI.typeText("All set! Launching CodeBlog...", { charDelay: 20 })
@@ -12,10 +12,12 @@ export const UpdateCommand: CommandModule = {
12
12
  default: false,
13
13
  }),
14
14
  handler: async (args) => {
15
+ Bun.stderr.write(UI.logo() + "\n")
16
+
15
17
  const pkg = await import("../../../package.json")
16
18
  const current = pkg.version
17
19
 
18
- UI.info(`Current version: v${current}`)
20
+ UI.info(`Current version: ${UI.Style.TEXT_NORMAL_BOLD}v${current}${UI.Style.TEXT_NORMAL}`)
19
21
  UI.info("Checking for updates...")
20
22
 
21
23
  const checkController = new AbortController()
@@ -44,18 +46,45 @@ export const UpdateCommand: CommandModule = {
44
46
  const latest = data.version
45
47
 
46
48
  if (current === latest && !args.force) {
47
- UI.success(`Already on latest version v${current}`)
49
+ UI.success(`Already on latest version ${UI.Style.TEXT_NORMAL_BOLD}v${current}${UI.Style.TEXT_NORMAL}`)
50
+ console.log("")
51
+ await promptLaunch()
48
52
  return
49
53
  }
50
54
 
51
- UI.info(`Updating v${current} → v${latest}...`)
55
+ UI.info(`Updating ${UI.Style.TEXT_DIM}v${current}${UI.Style.TEXT_NORMAL}${UI.Style.TEXT_NORMAL_BOLD}v${latest}${UI.Style.TEXT_NORMAL}...`)
52
56
 
53
57
  try {
54
58
  await performUpdate(latest)
55
- UI.success(`Updated to v${latest}!`)
59
+ console.log("")
60
+ UI.success(`Updated to ${UI.Style.TEXT_NORMAL_BOLD}v${latest}${UI.Style.TEXT_NORMAL}!`)
61
+ console.log("")
62
+ await promptLaunch()
56
63
  } catch (e) {
57
64
  UI.error(e instanceof Error ? e.message : String(e))
58
65
  process.exitCode = 1
59
66
  }
60
67
  },
61
68
  }
69
+
70
+ async function promptLaunch() {
71
+ if (!process.stdin.isTTY) return
72
+
73
+ const key = await UI.waitEnter("Press Enter to launch codeblog (or Esc to exit)")
74
+ if (key === "escape") return
75
+
76
+ // Re-launch without subcommand args so it enters TUI.
77
+ // In compiled binary: process.argv = ["/path/to/codeblog", "update"]
78
+ // In dev mode: process.argv = ["bun", "src/index.ts", "update"]
79
+ // We strip everything after the entry script to drop "update".
80
+ const entry = process.argv.findIndex((a) => a.endsWith("index.ts") || a.endsWith("index.js"))
81
+ const cmd = entry >= 0
82
+ ? process.argv.slice(0, entry + 1)
83
+ : [process.argv[0]!]
84
+
85
+ const proc = Bun.spawn(cmd, {
86
+ stdio: ["inherit", "inherit", "inherit"],
87
+ })
88
+ const code = await proc.exited
89
+ process.exit(code)
90
+ }
@@ -0,0 +1,317 @@
1
+ import * as fs from "fs"
2
+ import * as path from "path"
3
+ import { UI } from "./ui"
4
+
5
+ const home = process.env.HOME || process.env.USERPROFILE || "~"
6
+
7
+ interface IdeTarget {
8
+ name: string
9
+ id: string
10
+ configPath: string
11
+ detect: () => boolean
12
+ read: () => Record<string, unknown>
13
+ write: (config: Record<string, unknown>) => void
14
+ }
15
+
16
+ const MCP_SERVER_ENTRY = {
17
+ command: "npx",
18
+ args: ["-y", "codeblog-mcp@latest"],
19
+ }
20
+
21
+ function ensureDir(filePath: string) {
22
+ const dir = path.dirname(filePath)
23
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
24
+ }
25
+
26
+ function readJson(filePath: string): Record<string, unknown> {
27
+ try {
28
+ if (!fs.existsSync(filePath)) return {}
29
+ const raw = fs.readFileSync(filePath, "utf-8").trim()
30
+ if (!raw) return {}
31
+ return JSON.parse(raw)
32
+ } catch {
33
+ return {}
34
+ }
35
+ }
36
+
37
+ function writeJson(filePath: string, data: Record<string, unknown>) {
38
+ ensureDir(filePath)
39
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n")
40
+ }
41
+
42
+ function readToml(filePath: string): string {
43
+ try {
44
+ if (!fs.existsSync(filePath)) return ""
45
+ return fs.readFileSync(filePath, "utf-8")
46
+ } catch {
47
+ return ""
48
+ }
49
+ }
50
+
51
+ // Cursor: ~/.cursor/mcp.json
52
+ function cursorTarget(): IdeTarget {
53
+ const configPath = path.join(home, ".cursor", "mcp.json")
54
+ return {
55
+ name: "Cursor",
56
+ id: "cursor",
57
+ configPath,
58
+ detect: () => fs.existsSync(path.join(home, ".cursor")),
59
+ read: () => readJson(configPath),
60
+ write: (config) => writeJson(configPath, config),
61
+ }
62
+ }
63
+
64
+ // Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
65
+ function claudeDesktopTarget(): IdeTarget {
66
+ const configPath = process.platform === "darwin"
67
+ ? path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json")
68
+ : process.platform === "win32"
69
+ ? path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json")
70
+ : path.join(home, ".config", "Claude", "claude_desktop_config.json")
71
+ return {
72
+ name: "Claude Desktop",
73
+ id: "claude-desktop",
74
+ configPath,
75
+ detect: () => fs.existsSync(path.dirname(configPath)),
76
+ read: () => readJson(configPath),
77
+ write: (config) => writeJson(configPath, config),
78
+ }
79
+ }
80
+
81
+ // Claude Code: ~/.claude.json
82
+ function claudeCodeTarget(): IdeTarget {
83
+ const configPath = path.join(home, ".claude.json")
84
+ return {
85
+ name: "Claude Code",
86
+ id: "claude-code",
87
+ configPath,
88
+ detect: () => fs.existsSync(path.join(home, ".claude")),
89
+ read: () => readJson(configPath),
90
+ write: (config) => writeJson(configPath, config),
91
+ }
92
+ }
93
+
94
+ // Windsurf: ~/.codeium/windsurf/mcp_config.json
95
+ function windsurfTarget(): IdeTarget {
96
+ const configPath = path.join(home, ".codeium", "windsurf", "mcp_config.json")
97
+ return {
98
+ name: "Windsurf",
99
+ id: "windsurf",
100
+ configPath,
101
+ detect: () => fs.existsSync(path.join(home, ".codeium", "windsurf")),
102
+ read: () => readJson(configPath),
103
+ write: (config) => writeJson(configPath, config),
104
+ }
105
+ }
106
+
107
+ // Codex CLI: ~/.codex/config.toml (TOML format, special handling)
108
+ function codexTarget(): IdeTarget {
109
+ const configPath = path.join(home, ".codex", "config.toml")
110
+ return {
111
+ name: "Codex CLI",
112
+ id: "codex",
113
+ configPath,
114
+ detect: () => fs.existsSync(path.join(home, ".codex")),
115
+ read: () => {
116
+ const content = readToml(configPath)
117
+ if (content.includes("[mcp_servers.codeblog]")) return { _has_codeblog: true }
118
+ return {}
119
+ },
120
+ write: () => {
121
+ ensureDir(configPath)
122
+ let content = readToml(configPath)
123
+ if (content.includes("[mcp_servers.codeblog]")) return
124
+ const block = [
125
+ "",
126
+ "[mcp_servers.codeblog]",
127
+ `command = "npx"`,
128
+ `args = ["-y", "codeblog-mcp@latest"]`,
129
+ "",
130
+ ].join("\n")
131
+ content = content.trimEnd() + "\n" + block
132
+ fs.writeFileSync(configPath, content)
133
+ },
134
+ }
135
+ }
136
+
137
+ // VS Code (Copilot): user settings or .vscode/mcp.json — we use global settings.json
138
+ function vscodeTarget(): IdeTarget {
139
+ const configPath = process.platform === "darwin"
140
+ ? path.join(home, "Library", "Application Support", "Code", "User", "settings.json")
141
+ : process.platform === "win32"
142
+ ? path.join(process.env.APPDATA || "", "Code", "User", "settings.json")
143
+ : path.join(home, ".config", "Code", "User", "settings.json")
144
+ return {
145
+ name: "VS Code (Copilot)",
146
+ id: "vscode",
147
+ configPath,
148
+ detect: () => fs.existsSync(path.dirname(configPath)),
149
+ read: () => readJson(configPath),
150
+ write: (config) => writeJson(configPath, config),
151
+ }
152
+ }
153
+
154
+ const ALL_TARGETS = [
155
+ cursorTarget,
156
+ claudeDesktopTarget,
157
+ claudeCodeTarget,
158
+ windsurfTarget,
159
+ codexTarget,
160
+ vscodeTarget,
161
+ ]
162
+
163
+ function alreadyConfigured(target: IdeTarget): boolean {
164
+ const config = target.read()
165
+ if (target.id === "codex") return !!config._has_codeblog
166
+ if (target.id === "vscode") {
167
+ const mcp = config.mcp as Record<string, unknown> | undefined
168
+ const servers = mcp?.servers as Record<string, unknown> | undefined
169
+ return !!servers?.codeblog
170
+ }
171
+ const servers = config.mcpServers as Record<string, unknown> | undefined
172
+ return !!servers?.codeblog
173
+ }
174
+
175
+ function injectConfig(target: IdeTarget) {
176
+ if (target.id === "codex") {
177
+ target.write({})
178
+ return
179
+ }
180
+
181
+ if (target.id === "vscode") {
182
+ const config = target.read()
183
+ const mcp = (config.mcp || {}) as Record<string, unknown>
184
+ const servers = (mcp.servers || {}) as Record<string, unknown>
185
+ servers.codeblog = { type: "stdio", ...MCP_SERVER_ENTRY }
186
+ mcp.servers = servers
187
+ config.mcp = mcp
188
+ target.write(config)
189
+ return
190
+ }
191
+
192
+ // Standard mcpServers format (Cursor, Claude Desktop, Claude Code, Windsurf)
193
+ const config = target.read()
194
+ const servers = (config.mcpServers || {}) as Record<string, unknown>
195
+ servers.codeblog = { ...MCP_SERVER_ENTRY }
196
+ config.mcpServers = servers
197
+ target.write(config)
198
+ }
199
+
200
+ function detectTargets(): IdeTarget[] {
201
+ const detected: IdeTarget[] = []
202
+ for (const factory of ALL_TARGETS) {
203
+ const target = factory()
204
+ if (target.detect()) detected.push(target)
205
+ }
206
+ return detected
207
+ }
208
+
209
+ function buildLabels(targets: IdeTarget[]): string[] {
210
+ return targets.map((t) => {
211
+ const configured = alreadyConfigured(t)
212
+ return configured
213
+ ? `${t.name} ${UI.Style.TEXT_SUCCESS}(already configured)${UI.Style.TEXT_NORMAL}`
214
+ : t.name
215
+ })
216
+ }
217
+
218
+ function unconfiguredCount(targets: IdeTarget[]): number {
219
+ return targets.filter((t) => !alreadyConfigured(t)).length
220
+ }
221
+
222
+ async function configureSelected(targets: IdeTarget[], indices: number[]): Promise<void> {
223
+ let configured = 0
224
+ let skipped = 0
225
+
226
+ for (const i of indices) {
227
+ const target = targets[i]!
228
+ if (alreadyConfigured(target)) {
229
+ console.log(` ${UI.Style.TEXT_DIM}${target.name}: already configured, skipping${UI.Style.TEXT_NORMAL}`)
230
+ skipped++
231
+ continue
232
+ }
233
+ try {
234
+ injectConfig(target)
235
+ console.log(` ${UI.Style.TEXT_SUCCESS}✓${UI.Style.TEXT_NORMAL} ${target.name}: configured ${UI.Style.TEXT_DIM}(${target.configPath})${UI.Style.TEXT_NORMAL}`)
236
+ configured++
237
+ } catch (err) {
238
+ UI.warn(`${target.name}: failed — ${err instanceof Error ? err.message : String(err)}`)
239
+ }
240
+ }
241
+
242
+ console.log("")
243
+ if (configured > 0) {
244
+ UI.success(`Configured ${configured} IDE${configured > 1 ? "s" : ""}!${skipped > 0 ? ` (${skipped} already configured)` : ""}`)
245
+ await UI.typeText("Restart your IDE(s) for the MCP configuration to take effect.", { charDelay: 8 })
246
+ } else if (skipped > 0) {
247
+ UI.info("All selected IDEs were already configured.")
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Standalone wizard for `codeblog mcp init`.
253
+ */
254
+ export async function mcpConfigWizard(): Promise<void> {
255
+ const detected = detectTargets()
256
+
257
+ if (detected.length === 0) {
258
+ UI.info("No supported IDE detected on this machine.")
259
+ console.log(` ${UI.Style.TEXT_DIM}Supported: Cursor, Claude Desktop, Claude Code, Windsurf, Codex CLI, VS Code${UI.Style.TEXT_NORMAL}`)
260
+ return
261
+ }
262
+
263
+ console.log("")
264
+ await UI.typeText("Configure CodeBlog MCP in your IDEs.", { charDelay: 10 })
265
+ await UI.typeText("This lets your AI coding agent access CodeBlog tools directly.", { charDelay: 8 })
266
+ console.log("")
267
+
268
+ const indices = await UI.multiSelect(" Select IDEs to configure", buildLabels(detected))
269
+
270
+ if (indices.length === 0) {
271
+ console.log("")
272
+ UI.info("Skipped. You can run this again with: codeblog mcp init")
273
+ return
274
+ }
275
+
276
+ console.log("")
277
+ await configureSelected(detected, indices)
278
+ }
279
+
280
+ /**
281
+ * Lightweight prompt for the setup wizard — asks first, then shows multi-select.
282
+ */
283
+ export async function mcpSetupPrompt(): Promise<void> {
284
+ const detected = detectTargets()
285
+ if (detected.length === 0) return
286
+
287
+ const pending = unconfiguredCount(detected)
288
+ if (pending === 0) {
289
+ UI.success("CodeBlog MCP is already configured in all your IDEs!")
290
+ return
291
+ }
292
+
293
+ const names = detected.filter((t) => !alreadyConfigured(t)).map((t) => t.name)
294
+ await UI.typeText("One more thing — we can set up CodeBlog MCP in your other IDEs.", { charDelay: 10 })
295
+ await UI.typeText(`Detected: ${names.join(", ")}`, { charDelay: 6 })
296
+ console.log("")
297
+
298
+ const choice = await UI.waitEnter("Press Enter to configure, or Esc to skip")
299
+
300
+ if (choice === "escape") {
301
+ console.log("")
302
+ await UI.typeText("No problem! You can configure later with: codeblog mcp init", { charDelay: 8 })
303
+ return
304
+ }
305
+
306
+ console.log("")
307
+ const indices = await UI.multiSelect(" Select IDEs to configure", buildLabels(detected))
308
+
309
+ if (indices.length === 0) {
310
+ console.log("")
311
+ UI.info("Skipped. You can configure later with: codeblog mcp init")
312
+ return
313
+ }
314
+
315
+ console.log("")
316
+ await configureSelected(detected, indices)
317
+ }