From 8aae03d7c5d7adfa4382793e3eb4fcc9749d9738 Mon Sep 17 00:00:00 2001 From: mxito3 <1477311729@qq.com> Date: Fri, 22 May 2026 23:02:00 +0800 Subject: [PATCH 01/12] Fix Windows CI dashboard paths and npm spawn --- src/server/api/health.ts | 22 ++++++++++++++-------- src/server/api/memory.ts | 16 ++++++++-------- src/server/context.ts | 9 +++++++++ tests/helpers/codex-parity-harness.ts | 3 ++- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/server/api/health.ts b/src/server/api/health.ts index 6583d11..d43fcea 100644 --- a/src/server/api/health.ts +++ b/src/server/api/health.ts @@ -1,9 +1,7 @@ import { existsSync, readdirSync, statSync } from "node:fs"; -import { homedir } from "node:os"; import { join } from "node:path"; -import { listSessions } from "../../memory/session.js"; import { VERSION } from "../../version.js"; -import type { DashboardContext } from "../context.js"; +import { type DashboardContext, resolveCarboncodeHome } from "../context.js"; import type { ApiResult } from "../router.js"; interface DirStat { @@ -56,6 +54,17 @@ function dirSize(path: string): DirStat { return { path, exists: true, fileCount, totalBytes }; } +function countSessionFiles(path: string): number { + if (!existsSync(path)) return 0; + try { + return readdirSync(path).filter( + (file) => file.endsWith(".jsonl") && !file.endsWith(".events.jsonl"), + ).length; + } catch { + return 0; + } +} + export async function handleHealth( method: string, _rest: string[], @@ -65,8 +74,7 @@ export async function handleHealth( if (method !== "GET") { return { status: 405, body: { error: "GET only" } }; } - const home = homedir(); - const carboncodeHome = join(home, ".carboncode"); + const carboncodeHome = resolveCarboncodeHome(ctx.configPath); const sessionsStat = dirSize(join(carboncodeHome, "sessions")); const memoryStat = dirSize(join(carboncodeHome, "memory")); @@ -81,8 +89,6 @@ export async function handleHealth( } } - const sessions = listSessions(); - return { status: 200, body: { @@ -91,7 +97,7 @@ export async function handleHealth( carboncodeHome, sessions: { path: sessionsStat.path, - count: sessions.length, + count: countSessionFiles(sessionsStat.path), totalBytes: sessionsStat.totalBytes, }, memory: { diff --git a/src/server/api/memory.ts b/src/server/api/memory.ts index e244ce5..28871d0 100644 --- a/src/server/api/memory.ts +++ b/src/server/api/memory.ts @@ -10,26 +10,25 @@ import { unlinkSync, writeFileSync, } from "node:fs"; -import { homedir } from "node:os"; import { basename, dirname, join, resolve as resolvePath } from "node:path"; import { PROJECT_MEMORY_FILE, findProjectMemoryPath, resolveProjectMemoryWritePath, } from "../../memory/project.js"; -import type { DashboardContext } from "../context.js"; +import { type DashboardContext, resolveCarboncodeHome } from "../context.js"; import type { ApiResult } from "../router.js"; function projectHash(rootDir: string): string { return createHash("sha1").update(resolvePath(rootDir)).digest("hex").slice(0, 16); } -function globalMemoryDir(): string { - return join(homedir(), ".carboncode", "memory", "global"); +function globalMemoryDir(carboncodeHome: string): string { + return join(carboncodeHome, "memory", "global"); } -function projectMemoryDir(rootDir: string): string { - return join(homedir(), ".carboncode", "memory", projectHash(rootDir)); +function projectMemoryDir(carboncodeHome: string, rootDir: string): string { + return join(carboncodeHome, "memory", projectHash(rootDir)); } interface WriteBody { @@ -74,8 +73,9 @@ export async function handleMemory( ctx: DashboardContext, ): Promise { const cwd = ctx.getCurrentCwd?.(); - const globalDir = globalMemoryDir(); - const projectMemDir = cwd ? projectMemoryDir(cwd) : ""; + const carboncodeHome = resolveCarboncodeHome(ctx.configPath); + const globalDir = globalMemoryDir(carboncodeHome); + const projectMemDir = cwd ? projectMemoryDir(carboncodeHome, cwd) : ""; if (method === "GET" && rest.length === 0) { const existingProjectMemory = cwd ? findProjectMemoryPath(cwd) : null; diff --git a/src/server/context.ts b/src/server/context.ts index 83fa7c8..50faefa 100644 --- a/src/server/context.ts +++ b/src/server/context.ts @@ -1,11 +1,20 @@ /** Callbacks (not refs) so endpoints read live loop state per request, not a frozen closure. */ +import { homedir } from "node:os"; +import { basename, dirname, join } from "node:path"; import type { McpServerSummary } from "../cli/ui/slash/types.js"; import type { EditMode } from "../config.js"; import type { CacheFirstLoop } from "../loop.js"; import type { ToolRegistry } from "../tools.js"; import type { JobRegistry } from "../tools/jobs.js"; +export function resolveCarboncodeHome(configPath: string): string { + if (!configPath.trim()) return join(homedir(), ".carboncode"); + const configDir = dirname(configPath); + if (basename(configDir).toLowerCase() === ".carboncode") return configDir; + return join(configDir, ".carboncode"); +} + export interface DashboardContext { /** Caller resolves via `defaultConfigPath()`; module deliberately avoids `homedir()` so tests can redirect. */ configPath: string; diff --git a/tests/helpers/codex-parity-harness.ts b/tests/helpers/codex-parity-harness.ts index 67e127c..0f0d9b1 100644 --- a/tests/helpers/codex-parity-harness.ts +++ b/tests/helpers/codex-parity-harness.ts @@ -136,7 +136,8 @@ function runCommand( args: readonly string[], ): Promise { return new Promise((resolve, reject) => { - const child = spawn(command, [...args], { + const executable = process.platform === "win32" && command === "npm" ? "npm.cmd" : command; + const child = spawn(executable, [...args], { cwd, env: { ...process.env, CI: "1" }, stdio: ["ignore", "pipe", "pipe"], From a08eb343c5d4aff917538e477556ed0f925e33df Mon Sep 17 00:00:00 2001 From: mxito3 <1477311729@qq.com> Date: Fri, 22 May 2026 23:06:43 +0800 Subject: [PATCH 02/12] Bump version to 0.1.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce39a1d..9752b74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@carboncode/cli", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@carboncode/cli", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "dependencies": { "cli-highlight": "^2.1.11", diff --git a/package.json b/package.json index 4aeaec7..4eebdb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@carboncode/cli", - "version": "0.1.2", + "version": "0.1.3", "description": "Chinese-first DeepSeek-powered terminal coding agent for personal developer workflows.", "type": "module", "bin": { From 9d52567101443f81e2dc7f20c005aa4adff92363 Mon Sep 17 00:00:00 2001 From: yapie0 Date: Fri, 22 May 2026 23:47:30 +0800 Subject: [PATCH 03/12] Stabilize CLI routing test under heap reexec --- tests/cli-bare-routing.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/cli-bare-routing.test.ts b/tests/cli-bare-routing.test.ts index 8c99a98..9556d7f 100644 --- a/tests/cli-bare-routing.test.ts +++ b/tests/cli-bare-routing.test.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { writeConfig } from "../src/config.js"; +const HEAP_REEXEC_ENV = "REASONIX_HEAP_REEXEC"; const codeCommand = vi.fn(async () => {}); const chatCommand = vi.fn(async () => {}); const setupCommand = vi.fn(async () => {}); @@ -25,6 +26,7 @@ describe("bare CLI routing", () => { let cwd: string; const origHome = process.env.HOME; const origUserProfile = process.env.USERPROFILE; + const origHeapReexec = process.env[HEAP_REEXEC_ENV]; const origArgv = process.argv; const origCwd = process.cwd(); let stderr: ReturnType; @@ -38,6 +40,7 @@ describe("bare CLI routing", () => { cwd = realpathSync(mkdtempSync(join(tmpdir(), "carboncode-cli-cwd-"))); process.env.HOME = home; process.env.USERPROFILE = home; + process.env[HEAP_REEXEC_ENV] = "1"; process.chdir(cwd); codeCommand.mockClear(); chatCommand.mockClear(); @@ -63,6 +66,11 @@ describe("bare CLI routing", () => { } else { process.env.USERPROFILE = origUserProfile; } + if (origHeapReexec === undefined) { + delete process.env[HEAP_REEXEC_ENV]; + } else { + process.env[HEAP_REEXEC_ENV] = origHeapReexec; + } }); it("routes bare carboncode to code mode rooted at cwd", async () => { From 14da9ab920244ac782333e27f204d6498969f2ff Mon Sep 17 00:00:00 2001 From: yapie0 Date: Sat, 23 May 2026 00:50:57 +0800 Subject: [PATCH 04/12] Use npmjs registry in CI lockfile --- .github/workflows/ci.yml | 1 + package-lock.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d7f15f..9e24466 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: "22" + registry-url: https://registry.npmjs.org cache: npm - name: Install dependencies diff --git a/package-lock.json b/package-lock.json index 9752b74..9325900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2225,7 +2225,7 @@ }, "node_modules/@types/ws": { "version": "8.18.1", - "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", @@ -6891,7 +6891,7 @@ }, "node_modules/ws": { "version": "8.20.1", - "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.1.tgz", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { From 4f2dfbab1089fae19a23ad1980b9770850f20cbf Mon Sep 17 00:00:00 2001 From: yapie0 Date: Sat, 23 May 2026 00:54:05 +0800 Subject: [PATCH 05/12] Bump version to 0.1.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9325900..222ff0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@carboncode/cli", - "version": "0.1.3", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@carboncode/cli", - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "dependencies": { "cli-highlight": "^2.1.11", diff --git a/package.json b/package.json index 4eebdb8..db7733f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@carboncode/cli", - "version": "0.1.3", + "version": "0.1.4", "description": "Chinese-first DeepSeek-powered terminal coding agent for personal developer workflows.", "type": "module", "bin": { From 8b5570d9b054340ef56ba47d34c1153afbfe8816 Mon Sep 17 00:00:00 2001 From: yapie0 Date: Sat, 23 May 2026 11:43:42 +0800 Subject: [PATCH 06/12] Run parity harness npm command through shell on Windows --- tests/helpers/codex-parity-harness.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/helpers/codex-parity-harness.ts b/tests/helpers/codex-parity-harness.ts index 0f0d9b1..2bb03c6 100644 --- a/tests/helpers/codex-parity-harness.ts +++ b/tests/helpers/codex-parity-harness.ts @@ -136,10 +136,10 @@ function runCommand( args: readonly string[], ): Promise { return new Promise((resolve, reject) => { - const executable = process.platform === "win32" && command === "npm" ? "npm.cmd" : command; - const child = spawn(executable, [...args], { + const child = spawn(command, [...args], { cwd, env: { ...process.env, CI: "1" }, + shell: process.platform === "win32", stdio: ["ignore", "pipe", "pipe"], }); const chunks: Buffer[] = []; From 18e952049d286e81511105aea8cae61d1f8241d5 Mon Sep 17 00:00:00 2001 From: yapie0 Date: Sat, 23 May 2026 11:59:39 +0800 Subject: [PATCH 07/12] Bump version to 0.1.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 222ff0e..182f6dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@carboncode/cli", - "version": "0.1.4", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@carboncode/cli", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { "cli-highlight": "^2.1.11", diff --git a/package.json b/package.json index db7733f..7be76a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@carboncode/cli", - "version": "0.1.4", + "version": "0.1.5", "description": "Chinese-first DeepSeek-powered terminal coding agent for personal developer workflows.", "type": "module", "bin": { From 3d01e953838d76478c36662e34b5e4e55bd980e7 Mon Sep 17 00:00:00 2001 From: w287346141 <287346141@qq.com> Date: Sun, 24 May 2026 08:03:49 +0800 Subject: [PATCH 08/12] Localize setup preset descriptions --- src/cli/ui/Wizard.tsx | 7 +++---- src/i18n/EN.ts | 14 ++++++++++++++ src/i18n/types.ts | 1 + src/i18n/zh-CN.ts | 14 ++++++++++++++ tests/wizard.test.tsx | 20 +++++++++++++++++++- 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/cli/ui/Wizard.tsx b/src/cli/ui/Wizard.tsx index 9706f8b..101330a 100644 --- a/src/cli/ui/Wizard.tsx +++ b/src/cli/ui/Wizard.tsx @@ -36,7 +36,6 @@ import { import type { LanguageCode } from "../../i18n/types.js"; import { type CatalogEntry, MCP_CATALOG } from "../../mcp/catalog.js"; import { MultiSelect, type SelectItem, SingleSelect } from "./Select.js"; -import { PRESET_DESCRIPTIONS } from "./presets.js"; import { ThemeProvider, useTheme } from "./theme/context.js"; import { type ThemeName, listThemeNames } from "./theme/tokens.js"; @@ -705,11 +704,11 @@ function SummaryLine({ label, value }: { label: string; value: string }) { ); } -function presetItems(): SelectItem[] { +export function presetItems(): SelectItem[] { return (["auto", "flash", "pro"] as const).map((name) => ({ value: name as PresetName, - label: `${name} — ${PRESET_DESCRIPTIONS[name].headline}`, - hint: PRESET_DESCRIPTIONS[name].cost, + label: `${name} — ${t(`wizard.presetDescriptions.${name}.headline`)}`, + hint: t(`wizard.presetDescriptions.${name}.cost`), })); } diff --git a/src/i18n/EN.ts b/src/i18n/EN.ts index 0ac9539..c1c849f 100644 --- a/src/i18n/EN.ts +++ b/src/i18n/EN.ts @@ -451,6 +451,20 @@ export const EN: TranslationSchema = { "github-light": "GitHub light", "high-contrast": "Accessibility", }, + presetDescriptions: { + auto: { + headline: "flash → pro on hard turns", + cost: "default · ~96% turns stay on flash · pro kicks in only when needed", + }, + flash: { + headline: "v4-flash always", + cost: "cheapest · predictable · /pro still works for a one-turn bump", + }, + pro: { + headline: "v4-pro always", + cost: "~3× flash (5/31 discount) / ~12× full price · for hard multi-turn work", + }, + }, reviewLabelTheme: "Theme", presetTitle: "Pick a preset", mcpTitle: "Which MCP servers should Carbon Code wire up for you?", diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 8282c0a..afc06a0 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -306,6 +306,7 @@ export interface TranslationSchema { themeSampleHeading: string; themeFooter: string; themeCaption: Record; + presetDescriptions: Record<"auto" | "flash" | "pro", { headline: string; cost: string }>; reviewTitle: string; reviewLabelApiKey: string; reviewLabelLanguage: string; diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index 7afc2e3..bd4b7c0 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -438,6 +438,20 @@ export const zhCN: TranslationSchema = { "github-light": "GitHub 浅色", "high-contrast": "高对比度(无障碍)", }, + presetDescriptions: { + auto: { + headline: "困难轮次从 flash 升级到 pro", + cost: "默认 · 大多数轮次使用 flash · 需要时才启用 pro", + }, + flash: { + headline: "始终使用 v4-flash", + cost: "最便宜 · 可预测 · 仍可用 /pro 临时提升一轮", + }, + pro: { + headline: "始终使用 v4-pro", + cost: "约 3 倍 flash(5/31 折扣)/ 原价约 12 倍 · 适合困难的多轮工作", + }, + }, reviewLabelTheme: "主题", presetTitle: "选择预设", mcpTitle: "Carbon Code 要为你接入哪些 MCP 服务器?", diff --git a/tests/wizard.test.tsx b/tests/wizard.test.tsx index 552e2b4..1b2ff16 100644 --- a/tests/wizard.test.tsx +++ b/tests/wizard.test.tsx @@ -3,7 +3,7 @@ import { render } from "ink-testing-library"; import React from "react"; import { afterEach, describe, expect, it } from "vitest"; -import { Wizard, buildSpec, validateDeepSeekApiKey } from "../src/cli/ui/Wizard.js"; +import { Wizard, buildSpec, presetItems, validateDeepSeekApiKey } from "../src/cli/ui/Wizard.js"; import { setLanguageRuntime } from "../src/i18n/index.js"; import { parseMcpSpec } from "../src/mcp/spec.js"; @@ -66,6 +66,24 @@ describe("Wizard — first-launch language picker", () => { }); }); +describe("Wizard — localized preset descriptions", () => { + afterEach(() => { + setLanguageRuntime("EN"); + }); + + it("shows preset descriptions in zh-CN when runtime language is Simplified Chinese", () => { + setLanguageRuntime("zh-CN"); + const items = presetItems(); + + expect(items.map((item) => item.label)).toEqual([ + "auto — 困难轮次从 flash 升级到 pro", + "flash — 始终使用 v4-flash", + "pro — 始终使用 v4-pro", + ]); + expect(items.map((item) => item.hint).join("\n")).not.toContain("hard turns"); + }); +}); + describe("Wizard API-key validation", () => { it("accepts a key when DeepSeek auth check succeeds", async () => { const fetcher = async () => new Response(JSON.stringify({ data: [] }), { status: 200 }); From 3be88d76b4f1c037578cff9250938886a9814ac3 Mon Sep 17 00:00:00 2001 From: w287346141 <287346141@qq.com> Date: Sun, 24 May 2026 08:23:18 +0800 Subject: [PATCH 09/12] Localize setup MCP catalog descriptions --- src/cli/ui/Wizard.tsx | 22 ++++++++++++++++------ src/i18n/EN.ts | 21 +++++++++++++++++++++ src/i18n/types.ts | 4 ++++ src/i18n/zh-CN.ts | 21 +++++++++++++++++++++ tests/wizard.test.tsx | 30 +++++++++++++++++++++++++++++- 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/cli/ui/Wizard.tsx b/src/cli/ui/Wizard.tsx index 101330a..7e310d3 100644 --- a/src/cli/ui/Wizard.tsx +++ b/src/cli/ui/Wizard.tsx @@ -596,10 +596,10 @@ function McpArgsStep({ return ( - {entry.summary} - {entry.note ? ( + {mcpCatalogSummary(entry)} + {mcpCatalogNote(entry) ? ( - {entry.note} + {mcpCatalogNote(entry)} ) : null} @@ -712,11 +712,12 @@ export function presetItems(): SelectItem[] { })); } -function mcpItems(): SelectItem[] { +export function mcpItems(): SelectItem[] { return MCP_CATALOG.map((entry) => { - const hintParts: string[] = [entry.summary]; + const hintParts: string[] = [mcpCatalogSummary(entry)]; if (entry.userArgs) hintParts.push(t("wizard.mcpUserArgsHint", { arg: entry.userArgs })); - if (entry.note) hintParts.push(entry.note); + const note = mcpCatalogNote(entry); + if (note) hintParts.push(note); return { value: entry.name, label: entry.name, @@ -725,6 +726,15 @@ function mcpItems(): SelectItem[] { }); } +function mcpCatalogSummary(entry: CatalogEntry): string { + return t(`wizard.mcpCatalog.${entry.name}.summary`); +} + +function mcpCatalogNote(entry: CatalogEntry): string | undefined { + if (!entry.note) return undefined; + return t(`wizard.mcpCatalog.${entry.name}.note`); +} + function placeholderFor(entry: CatalogEntry): string { if (entry.name === "filesystem") return "e.g. /tmp/carboncode-sandbox"; if (entry.name === "sqlite") return "e.g. ./notes.sqlite"; diff --git a/src/i18n/EN.ts b/src/i18n/EN.ts index c1c849f..1ffc769 100644 --- a/src/i18n/EN.ts +++ b/src/i18n/EN.ts @@ -465,6 +465,27 @@ export const EN: TranslationSchema = { cost: "~3× flash (5/31 discount) / ~12× full price · for hard multi-turn work", }, }, + mcpCatalog: { + filesystem: { + summary: "read/write/search files inside a sandboxed directory", + note: "the directory is a hard sandbox — the server refuses access outside it", + }, + memory: { + summary: "persistent key-value memory across sessions", + }, + github: { + summary: "read issues, PRs, code search (needs GITHUB_PERSONAL_ACCESS_TOKEN)", + note: "set GITHUB_PERSONAL_ACCESS_TOKEN in your env before spawning", + }, + puppeteer: { + summary: "browser automation — take screenshots, click, type", + note: "downloads Chromium on first run (~200 MB)", + }, + everything: { + summary: "official test server — exercises every MCP feature", + note: "useful for debugging your Carbon Code setup", + }, + }, reviewLabelTheme: "Theme", presetTitle: "Pick a preset", mcpTitle: "Which MCP servers should Carbon Code wire up for you?", diff --git a/src/i18n/types.ts b/src/i18n/types.ts index afc06a0..8da2faa 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -307,6 +307,10 @@ export interface TranslationSchema { themeFooter: string; themeCaption: Record; presetDescriptions: Record<"auto" | "flash" | "pro", { headline: string; cost: string }>; + mcpCatalog: Record< + "filesystem" | "memory" | "github" | "puppeteer" | "everything", + { summary: string; note?: string } + >; reviewTitle: string; reviewLabelApiKey: string; reviewLabelLanguage: string; diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index bd4b7c0..ad0a118 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -452,6 +452,27 @@ export const zhCN: TranslationSchema = { cost: "约 3 倍 flash(5/31 折扣)/ 原价约 12 倍 · 适合困难的多轮工作", }, }, + mcpCatalog: { + filesystem: { + summary: "在沙箱目录内读取、写入和搜索文件", + note: "该目录是严格沙箱 — 服务器会拒绝访问目录外的路径", + }, + memory: { + summary: "跨会话保存持久化键值记忆", + }, + github: { + summary: "读取 issues、PR 和代码搜索(需要 GITHUB_PERSONAL_ACCESS_TOKEN)", + note: "启动前请在环境变量中设置 GITHUB_PERSONAL_ACCESS_TOKEN", + }, + puppeteer: { + summary: "浏览器自动化 — 截图、点击、输入", + note: "首次运行会下载 Chromium(约 200 MB)", + }, + everything: { + summary: "官方测试服务器 — 覆盖所有 MCP 功能", + note: "适合调试 Carbon Code 设置", + }, + }, reviewLabelTheme: "主题", presetTitle: "选择预设", mcpTitle: "Carbon Code 要为你接入哪些 MCP 服务器?", diff --git a/tests/wizard.test.tsx b/tests/wizard.test.tsx index 1b2ff16..2378b1a 100644 --- a/tests/wizard.test.tsx +++ b/tests/wizard.test.tsx @@ -3,7 +3,13 @@ import { render } from "ink-testing-library"; import React from "react"; import { afterEach, describe, expect, it } from "vitest"; -import { Wizard, buildSpec, presetItems, validateDeepSeekApiKey } from "../src/cli/ui/Wizard.js"; +import { + Wizard, + buildSpec, + mcpItems, + presetItems, + validateDeepSeekApiKey, +} from "../src/cli/ui/Wizard.js"; import { setLanguageRuntime } from "../src/i18n/index.js"; import { parseMcpSpec } from "../src/mcp/spec.js"; @@ -84,6 +90,28 @@ describe("Wizard — localized preset descriptions", () => { }); }); +describe("Wizard — localized MCP catalog descriptions", () => { + afterEach(() => { + setLanguageRuntime("EN"); + }); + + it("shows MCP catalog summaries and notes in zh-CN", () => { + setLanguageRuntime("zh-CN"); + const items = mcpItems(); + const filesystem = items.find((item) => item.value === "filesystem"); + const github = items.find((item) => item.value === "github"); + + expect(filesystem?.hint).toContain("在沙箱目录内读取、写入和搜索文件"); + expect(filesystem?.hint).toContain("需要你提供 "); + expect(filesystem?.hint).toContain("服务器会拒绝访问目录外的路径"); + expect(github?.hint).toContain("读取 issues、PR 和代码搜索"); + expect(github?.hint).toContain("启动前请在环境变量中设置 GITHUB_PERSONAL_ACCESS_TOKEN"); + expect(items.map((item) => item.hint).join("\n")).not.toContain( + "read/write/search files inside a sandboxed directory", + ); + }); +}); + describe("Wizard API-key validation", () => { it("accepts a key when DeepSeek auth check succeeds", async () => { const fetcher = async () => new Response(JSON.stringify({ data: [] }), { status: 200 }); From 79a6c6ec64a409c09f042c302ffabaab71adafcf Mon Sep 17 00:00:00 2001 From: w287346141 <287346141@qq.com> Date: Sun, 24 May 2026 08:47:51 +0800 Subject: [PATCH 10/12] Localize setup detected language hint --- src/cli/ui/Wizard.tsx | 2 +- src/i18n/EN.ts | 1 + src/i18n/types.ts | 1 + src/i18n/zh-CN.ts | 1 + tests/wizard.test.tsx | 2 ++ 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli/ui/Wizard.tsx b/src/cli/ui/Wizard.tsx index 7e310d3..5c41cb3 100644 --- a/src/cli/ui/Wizard.tsx +++ b/src/cli/ui/Wizard.tsx @@ -400,7 +400,7 @@ function LanguageStep({ const items: SelectItem[] = getSupportedLanguages().map((code) => ({ value: code, label: LANGUAGE_LABELS[code], - hint: code === detectSystemLanguage() ? "(detected)" : undefined, + hint: code === detectSystemLanguage() ? t("wizard.languageDetectedHint") : undefined, })); return ( diff --git a/src/i18n/EN.ts b/src/i18n/EN.ts index 1ffc769..1ba8199 100644 --- a/src/i18n/EN.ts +++ b/src/i18n/EN.ts @@ -426,6 +426,7 @@ export const EN: TranslationSchema = { wizard: { languageTitle: "Choose your language", languageSubtitle: "Detected from your system locale. Switch later via /language.", + languageDetectedHint: "(detected)", welcomeTitle: "Welcome to Carbon Code.", apiKeyPrompt: "Paste your DeepSeek API key to get started.", apiKeyGetOne: "Get one at: https://platform.deepseek.com/api_keys", diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 8da2faa..3e888d6 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -280,6 +280,7 @@ export interface TranslationSchema { wizard: { languageTitle: string; languageSubtitle: string; + languageDetectedHint: string; welcomeTitle: string; apiKeyPrompt: string; apiKeyGetOne: string; diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index ad0a118..bafa4ae 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -415,6 +415,7 @@ export const zhCN: TranslationSchema = { wizard: { languageTitle: "选择语言", languageSubtitle: "已根据系统语言自动选中。之后可用 /language 切换。", + languageDetectedHint: "(已检测)", welcomeTitle: "欢迎使用 Carbon Code。", apiKeyPrompt: "粘贴你的 DeepSeek API key 开始使用。", apiKeyGetOne: "在此获取:https://platform.deepseek.com/api_keys", diff --git a/tests/wizard.test.tsx b/tests/wizard.test.tsx index 2378b1a..4571f6a 100644 --- a/tests/wizard.test.tsx +++ b/tests/wizard.test.tsx @@ -68,6 +68,8 @@ describe("Wizard — first-launch language picker", () => { expect(out).toContain("选择语言"); expect(out).toContain("English"); expect(out).toContain("简体中文"); + expect(out).toContain("(已检测)"); + expect(out).not.toContain("(detected)"); unmount(); }); }); From 657b5f0327475465ba9e876bd9e45f6044cad828 Mon Sep 17 00:00:00 2001 From: w287346141 <287346141@qq.com> Date: Sun, 24 May 2026 09:11:51 +0800 Subject: [PATCH 11/12] Localize setup MCP argument placeholders --- src/cli/ui/Wizard.tsx | 6 +++--- src/i18n/EN.ts | 2 ++ src/i18n/types.ts | 2 ++ src/i18n/zh-CN.ts | 2 ++ tests/wizard.test.tsx | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/cli/ui/Wizard.tsx b/src/cli/ui/Wizard.tsx index 5c41cb3..59db5fb 100644 --- a/src/cli/ui/Wizard.tsx +++ b/src/cli/ui/Wizard.tsx @@ -735,9 +735,9 @@ function mcpCatalogNote(entry: CatalogEntry): string | undefined { return t(`wizard.mcpCatalog.${entry.name}.note`); } -function placeholderFor(entry: CatalogEntry): string { - if (entry.name === "filesystem") return "e.g. /tmp/carboncode-sandbox"; - if (entry.name === "sqlite") return "e.g. ./notes.sqlite"; +export function placeholderFor(entry: CatalogEntry): string { + if (entry.name === "filesystem") return t("wizard.mcpArgsFilesystemPlaceholder"); + if (entry.name === "sqlite") return t("wizard.mcpArgsSqlitePlaceholder"); return entry.userArgs ?? ""; } diff --git a/src/i18n/EN.ts b/src/i18n/EN.ts index 1ba8199..a9b7d33 100644 --- a/src/i18n/EN.ts +++ b/src/i18n/EN.ts @@ -500,6 +500,8 @@ export const EN: TranslationSchema = { mcpArgsRequiredParam: "Required parameter: ", mcpArgsEmpty: "{name} needs a value — got an empty string.", mcpArgsNotADir: "{path} exists but is not a directory.", + mcpArgsFilesystemPlaceholder: "e.g. /tmp/carboncode-sandbox", + mcpArgsSqlitePlaceholder: "e.g. ./notes.sqlite", reviewTitle: "Ready to save", reviewLabelApiKey: "API key", reviewLabelLanguage: "Language", diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 3e888d6..bc763c7 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -302,6 +302,8 @@ export interface TranslationSchema { mcpArgsRequiredParam: string; mcpArgsEmpty: string; mcpArgsNotADir: string; + mcpArgsFilesystemPlaceholder: string; + mcpArgsSqlitePlaceholder: string; themeTitle: string; themeSubtitle: string; themeSampleHeading: string; diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index bafa4ae..31519e8 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -486,6 +486,8 @@ export const zhCN: TranslationSchema = { mcpArgsRequiredParam: "必填参数:", mcpArgsEmpty: "{name} 需要一个值 — 不能为空。", mcpArgsNotADir: "{path} 存在但不是目录。", + mcpArgsFilesystemPlaceholder: "例如:/tmp/carboncode-sandbox", + mcpArgsSqlitePlaceholder: "例如:./notes.sqlite", reviewTitle: "确认保存", reviewLabelApiKey: "API key", reviewLabelLanguage: "语言", diff --git a/tests/wizard.test.tsx b/tests/wizard.test.tsx index 4571f6a..956dea9 100644 --- a/tests/wizard.test.tsx +++ b/tests/wizard.test.tsx @@ -7,6 +7,7 @@ import { Wizard, buildSpec, mcpItems, + placeholderFor, presetItems, validateDeepSeekApiKey, } from "../src/cli/ui/Wizard.js"; @@ -114,6 +115,38 @@ describe("Wizard — localized MCP catalog descriptions", () => { }); }); +describe("Wizard MCP argument placeholders", () => { + afterEach(() => { + setLanguageRuntime("EN"); + }); + + it("localizes filesystem and sqlite placeholders in zh-CN", () => { + setLanguageRuntime("zh-CN"); + + const filesystem = placeholderFor({ + name: "filesystem", + command: "npx", + args: [], + userArgs: "", + }); + const sqlite = placeholderFor({ name: "sqlite", command: "npx", args: [], userArgs: "" }); + + expect(filesystem).toContain("/tmp/carboncode-sandbox"); + expect(filesystem).not.toBe("e.g. /tmp/carboncode-sandbox"); + expect(sqlite).toContain("./notes.sqlite"); + expect(sqlite).not.toBe("e.g. ./notes.sqlite"); + }); + + it("keeps filesystem and sqlite placeholders in EN", () => { + expect( + placeholderFor({ name: "filesystem", command: "npx", args: [], userArgs: "" }), + ).toBe("e.g. /tmp/carboncode-sandbox"); + expect(placeholderFor({ name: "sqlite", command: "npx", args: [], userArgs: "" })).toBe( + "e.g. ./notes.sqlite", + ); + }); +}); + describe("Wizard API-key validation", () => { it("accepts a key when DeepSeek auth check succeeds", async () => { const fetcher = async () => new Response(JSON.stringify({ data: [] }), { status: 200 }); From cd1d53f24f58751e306ed973c3ecc560cdc241d8 Mon Sep 17 00:00:00 2001 From: w287346141 <287346141@qq.com> Date: Sun, 24 May 2026 10:40:09 +0800 Subject: [PATCH 12/12] Localize MCP argument step copy --- src/cli/ui/Wizard.tsx | 18 +++++++++++++++--- src/i18n/EN.ts | 2 ++ src/i18n/types.ts | 2 ++ src/i18n/zh-CN.ts | 2 ++ tests/wizard.test.tsx | 20 ++++++++++++++++++++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/cli/ui/Wizard.tsx b/src/cli/ui/Wizard.tsx index 59db5fb..a00ad57 100644 --- a/src/cli/ui/Wizard.tsx +++ b/src/cli/ui/Wizard.tsx @@ -548,6 +548,8 @@ function McpArgsStep({ }) { const [value, setValue] = useState(""); const [pendingCreate, setPendingCreate] = useState(null); + const summary = mcpArgsSummaryFor(entry); + const note = mcpArgsNoteFor(entry); useInput((input, key) => { if (!pendingCreate) return; @@ -596,10 +598,10 @@ function McpArgsStep({ return ( - {mcpCatalogSummary(entry)} - {mcpCatalogNote(entry) ? ( + {summary} + {note ? ( - {mcpCatalogNote(entry)} + {note} ) : null} @@ -735,6 +737,16 @@ function mcpCatalogNote(entry: CatalogEntry): string | undefined { return t(`wizard.mcpCatalog.${entry.name}.note`); } +export function mcpArgsSummaryFor(entry: CatalogEntry): string { + if (entry.name === "filesystem") return t("wizard.mcpArgsFilesystemSummary"); + return mcpCatalogSummary(entry); +} + +export function mcpArgsNoteFor(entry: CatalogEntry): string | undefined { + if (entry.name === "filesystem") return t("wizard.mcpArgsFilesystemNote"); + return mcpCatalogNote(entry); +} + export function placeholderFor(entry: CatalogEntry): string { if (entry.name === "filesystem") return t("wizard.mcpArgsFilesystemPlaceholder"); if (entry.name === "sqlite") return t("wizard.mcpArgsSqlitePlaceholder"); diff --git a/src/i18n/EN.ts b/src/i18n/EN.ts index a9b7d33..a2e003f 100644 --- a/src/i18n/EN.ts +++ b/src/i18n/EN.ts @@ -502,6 +502,8 @@ export const EN: TranslationSchema = { mcpArgsNotADir: "{path} exists but is not a directory.", mcpArgsFilesystemPlaceholder: "e.g. /tmp/carboncode-sandbox", mcpArgsSqlitePlaceholder: "e.g. ./notes.sqlite", + mcpArgsFilesystemSummary: "read/write/search files inside a sandboxed directory", + mcpArgsFilesystemNote: "the directory is a hard sandbox — the server refuses access outside it", reviewTitle: "Ready to save", reviewLabelApiKey: "API key", reviewLabelLanguage: "Language", diff --git a/src/i18n/types.ts b/src/i18n/types.ts index bc763c7..504ec05 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -304,6 +304,8 @@ export interface TranslationSchema { mcpArgsNotADir: string; mcpArgsFilesystemPlaceholder: string; mcpArgsSqlitePlaceholder: string; + mcpArgsFilesystemSummary: string; + mcpArgsFilesystemNote: string; themeTitle: string; themeSubtitle: string; themeSampleHeading: string; diff --git a/src/i18n/zh-CN.ts b/src/i18n/zh-CN.ts index 31519e8..8c959e1 100644 --- a/src/i18n/zh-CN.ts +++ b/src/i18n/zh-CN.ts @@ -488,6 +488,8 @@ export const zhCN: TranslationSchema = { mcpArgsNotADir: "{path} 存在但不是目录。", mcpArgsFilesystemPlaceholder: "例如:/tmp/carboncode-sandbox", mcpArgsSqlitePlaceholder: "例如:./notes.sqlite", + mcpArgsFilesystemSummary: "在沙盒目录内读写和搜索文件", + mcpArgsFilesystemNote: "该目录是严格沙盒,服务器会拒绝访问目录外的内容", reviewTitle: "确认保存", reviewLabelApiKey: "API key", reviewLabelLanguage: "语言", diff --git a/tests/wizard.test.tsx b/tests/wizard.test.tsx index 956dea9..c60531d 100644 --- a/tests/wizard.test.tsx +++ b/tests/wizard.test.tsx @@ -6,6 +6,8 @@ import { afterEach, describe, expect, it } from "vitest"; import { Wizard, buildSpec, + mcpArgsNoteFor, + mcpArgsSummaryFor, mcpItems, placeholderFor, presetItems, @@ -120,6 +122,24 @@ describe("Wizard MCP argument placeholders", () => { setLanguageRuntime("EN"); }); + it("localizes the filesystem argument step copy in zh-CN", () => { + setLanguageRuntime("zh-CN"); + const entry = { + name: "filesystem", + summary: "read/write/search files inside a sandboxed directory", + package: "@modelcontextprotocol/server-filesystem", + command: "npx", + args: [], + userArgs: "", + note: "the directory is a hard sandbox — the server refuses access outside it", + }; + + expect(mcpArgsSummaryFor(entry)).toContain("沙盒"); + expect(mcpArgsSummaryFor(entry)).not.toBe(entry.summary); + expect(mcpArgsNoteFor(entry)).toContain("目录外"); + expect(mcpArgsNoteFor(entry)).not.toBe(entry.note); + }); + it("localizes filesystem and sqlite placeholders in zh-CN", () => { setLanguageRuntime("zh-CN");