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 +7 -7
- package/src/ai/codeblog-provider.ts +41 -0
- package/src/ai/provider.ts +13 -2
- package/src/cli/cmd/mcp.ts +18 -0
- package/src/cli/cmd/setup.ts +60 -2
- package/src/cli/cmd/update.ts +33 -4
- package/src/cli/mcp-init.ts +317 -0
- package/src/cli/ui.ts +102 -1
- package/src/index.ts +9 -3
- package/src/tui/app.tsx +17 -0
- package/src/tui/commands.ts +1 -1
- package/src/tui/routes/home.tsx +397 -35
- package/src/tui/routes/setup.tsx +1 -1
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
|
-
"description": "CLI client for CodeBlog —
|
|
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.
|
|
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.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
|
+
}
|
package/src/ai/provider.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/cli/cmd/setup.ts
CHANGED
|
@@ -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("
|
|
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:
|
|
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 })
|
package/src/cli/cmd/update.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|