Skip to content
Merged
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@carboncode/cli",
"version": "0.1.2",
"version": "0.1.5",
"description": "Chinese-first DeepSeek-powered terminal coding agent for personal developer workflows.",
"type": "module",
"bin": {
Expand Down
49 changes: 35 additions & 14 deletions src/cli/ui/Wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -401,7 +400,7 @@ function LanguageStep({
const items: SelectItem<LanguageCode>[] = getSupportedLanguages().map((code) => ({
value: code,
label: LANGUAGE_LABELS[code],
hint: code === detectSystemLanguage() ? "(detected)" : undefined,
hint: code === detectSystemLanguage() ? t("wizard.languageDetectedHint") : undefined,
}));
return (
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1}>
Expand Down Expand Up @@ -549,6 +548,8 @@ function McpArgsStep({
}) {
const [value, setValue] = useState("");
const [pendingCreate, setPendingCreate] = useState<string | null>(null);
const summary = mcpArgsSummaryFor(entry);
const note = mcpArgsNoteFor(entry);

useInput((input, key) => {
if (!pendingCreate) return;
Expand Down Expand Up @@ -597,10 +598,10 @@ function McpArgsStep({
return (
<StepFrame title={t("wizard.mcpArgsTitle", { name: entry.name })} step={2} total={3}>
<Box flexDirection="column">
<Text>{entry.summary}</Text>
{entry.note ? (
<Text>{summary}</Text>
{note ? (
<Box marginTop={1}>
<Text dimColor>{entry.note}</Text>
<Text dimColor>{note}</Text>
</Box>
) : null}
<Box marginTop={1}>
Expand Down Expand Up @@ -705,19 +706,20 @@ function SummaryLine({ label, value }: { label: string; value: string }) {
);
}

function presetItems(): SelectItem<PresetName>[] {
export function presetItems(): SelectItem<PresetName>[] {
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`),
}));
}

function mcpItems(): SelectItem<string>[] {
export function mcpItems(): SelectItem<string>[] {
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,
Expand All @@ -726,9 +728,28 @@ function mcpItems(): SelectItem<string>[] {
});
}

function placeholderFor(entry: CatalogEntry): string {
if (entry.name === "filesystem") return "e.g. /tmp/carboncode-sandbox";
if (entry.name === "sqlite") return "e.g. ./notes.sqlite";
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`);
}

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");
return entry.userArgs ?? "";
}

Expand Down
40 changes: 40 additions & 0 deletions src/i18n/EN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -451,6 +452,41 @@ 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",
},
},
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?",
Expand All @@ -464,6 +500,10 @@ 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",
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",
Expand Down
10 changes: 10 additions & 0 deletions src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export interface TranslationSchema {
wizard: {
languageTitle: string;
languageSubtitle: string;
languageDetectedHint: string;
welcomeTitle: string;
apiKeyPrompt: string;
apiKeyGetOne: string;
Expand All @@ -301,11 +302,20 @@ export interface TranslationSchema {
mcpArgsRequiredParam: string;
mcpArgsEmpty: string;
mcpArgsNotADir: string;
mcpArgsFilesystemPlaceholder: string;
mcpArgsSqlitePlaceholder: string;
mcpArgsFilesystemSummary: string;
mcpArgsFilesystemNote: string;
themeTitle: string;
themeSubtitle: string;
themeSampleHeading: string;
themeFooter: string;
themeCaption: Record<string, string>;
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;
Expand Down
40 changes: 40 additions & 0 deletions src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -438,6 +439,41 @@ 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 倍 · 适合困难的多轮工作",
},
},
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 服务器?",
Expand All @@ -450,6 +486,10 @@ export const zhCN: TranslationSchema = {
mcpArgsRequiredParam: "必填参数:",
mcpArgsEmpty: "{name} 需要一个值 — 不能为空。",
mcpArgsNotADir: "{path} 存在但不是目录。",
mcpArgsFilesystemPlaceholder: "例如:/tmp/carboncode-sandbox",
mcpArgsSqlitePlaceholder: "例如:./notes.sqlite",
mcpArgsFilesystemSummary: "在沙盒目录内读写和搜索文件",
mcpArgsFilesystemNote: "该目录是严格沙盒,服务器会拒绝访问目录外的内容",
reviewTitle: "确认保存",
reviewLabelApiKey: "API key",
reviewLabelLanguage: "语言",
Expand Down
22 changes: 14 additions & 8 deletions src/server/api/health.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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[],
Expand All @@ -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"));
Expand All @@ -81,8 +89,6 @@ export async function handleHealth(
}
}

const sessions = listSessions();

return {
status: 200,
body: {
Expand All @@ -91,7 +97,7 @@ export async function handleHealth(
carboncodeHome,
sessions: {
path: sessionsStat.path,
count: sessions.length,
count: countSessionFiles(sessionsStat.path),
totalBytes: sessionsStat.totalBytes,
},
memory: {
Expand Down
16 changes: 8 additions & 8 deletions src/server/api/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -74,8 +73,9 @@ export async function handleMemory(
ctx: DashboardContext,
): Promise<ApiResult> {
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;
Expand Down
Loading