Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
10 changes: 6 additions & 4 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,25 @@ function resolveDashboardPort(
: undefined;
}

/** Resolution order: flag → REASONIX_DASHBOARD_HOST env → config.dashboard.host → undefined (server defaults to 127.0.0.1). */
/** Resolution order: flag → CARBONCODE_DASHBOARD_HOST env → legacy REASONIX_DASHBOARD_HOST env → config.dashboard.host → undefined (server defaults to 127.0.0.1). */
function resolveDashboardHost(
flagValue: string | undefined,
noConfig: boolean,
): string | undefined {
const fromFlag = flagValue?.trim();
if (fromFlag) return fromFlag;
const fromEnv = process.env.REASONIX_DASHBOARD_HOST?.trim();
const fromEnv =
process.env.CARBONCODE_DASHBOARD_HOST?.trim() ?? process.env.REASONIX_DASHBOARD_HOST?.trim();
if (fromEnv) return fromEnv;
if (noConfig) return undefined;
const fromCfg = readConfig().dashboard?.host;
return typeof fromCfg === "string" && fromCfg.trim() ? fromCfg.trim() : undefined;
}

/** Resolution order: REASONIX_DASHBOARD_TOKEN env → config.dashboard.token → undefined (server mints a fresh per-boot token). Min 16 chars; shorter values are dropped with a warning to avoid trivially-guessable tokens. */
/** Resolution order: CARBONCODE_DASHBOARD_TOKEN env → legacy REASONIX_DASHBOARD_TOKEN env → config.dashboard.token → undefined (server mints a fresh per-boot token). Min 16 chars; shorter values are dropped with a warning to avoid trivially-guessable tokens. */
function resolveDashboardToken(noConfig: boolean): string | undefined {
const fromEnv = process.env.REASONIX_DASHBOARD_TOKEN?.trim();
const fromEnv =
process.env.CARBONCODE_DASHBOARD_TOKEN?.trim() ?? process.env.REASONIX_DASHBOARD_TOKEN?.trim();
const fromCfg = noConfig ? undefined : readConfig().dashboard?.token?.trim();
const candidate = fromEnv || fromCfg;
if (!candidate) return undefined;
Expand Down
4 changes: 2 additions & 2 deletions src/cli/startup-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const marks: PhaseMark[] = [];
let dumped = false;

function envFlag(): boolean {
const v = process.env.REASONIX_PROFILE_STARTUP;
const v = process.env.CARBONCODE_PROFILE_STARTUP ?? process.env.REASONIX_PROFILE_STARTUP;
return v === "1" || v === "true" || v === "yes";
}

Expand All @@ -36,7 +36,7 @@ export function dumpStartupProfile(stream: NodeJS.WriteStream = process.stderr):
prev = m.t;
}
lines.push(
`─── ${Math.round(totalMs)}ms total · last phase ${marks[marks.length - 1]!.name} · set REASONIX_PROFILE_STARTUP=0 to silence`,
`─── ${Math.round(totalMs)}ms total · last phase ${marks[marks.length - 1]!.name} · set CARBONCODE_PROFILE_STARTUP=0 to silence`,
);
stream.write(`${lines.join("\n")}\n`);
}
Expand Down
13 changes: 8 additions & 5 deletions src/cli/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,10 @@ export interface AppProps {
* Throttle interval in ms. 50ms —20Hz —slow enough that cursor-up
* repaints on winpty/MINTTY/ConEmu/tmux don't leave half-drawn frames,
* fast enough that streaming text still reads as continuous. Override
* via `REASONIX_FLUSH_MS` if you want 60Hz on a terminal you trust.
* via `CARBONCODE_FLUSH_MS` if you want 60Hz on a terminal you trust.
*/
const FLUSH_INTERVAL_MS = (() => {
const raw = process.env.REASONIX_FLUSH_MS;
const raw = process.env.CARBONCODE_FLUSH_MS ?? process.env.REASONIX_FLUSH_MS;
if (!raw) return 50;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed < 16 || parsed > 1000) return 50;
Expand Down Expand Up @@ -393,7 +393,7 @@ export function App(props: AppProps): React.ReactElement {
[props.session],
);
const [themeName, setThemeName] = React.useState<ThemeName>(() =>
resolveThemePreference(loadTheme(), process.env.REASONIX_THEME),
resolveThemePreference(loadTheme(), process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME),
);
const statusBar = React.useMemo((): StatusBarConfig => {
const cfg = readConfig().statusBar ?? {};
Expand Down Expand Up @@ -2535,7 +2535,10 @@ function AppInner({
const handleQQThemePick = useCallback(
(target: ThemeChoice): string => {
saveTheme(target);
const active = resolveThemePreference(target, process.env.REASONIX_THEME);
const active = resolveThemePreference(
target,
process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME,
);
setThemeName(active);
return `theme saved: ${target}\nactive now: ${active}`;
},
Expand Down Expand Up @@ -4282,7 +4285,7 @@ function AppInner({
saveTheme(outcome.value);
const active = resolveThemePreference(
outcome.value,
process.env.REASONIX_THEME,
process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME,
);
setThemeName(active);
log.pushInfo(`theme saved: ${outcome.value}\n active now: ${active}`);
Expand Down
10 changes: 8 additions & 2 deletions src/cli/ui/Wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,19 @@ export function Wizard({
useEffect(() => onLanguageChange(() => setLanguageVersion((v) => v + 1)), []);

const [previewTheme, setPreviewTheme] = useState<ThemeName>(() =>
resolveThemePreference(initial?.theme ?? loadTheme(), process.env.REASONIX_THEME),
resolveThemePreference(
initial?.theme ?? loadTheme(),
process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME,
),
);

const [step, setStep] = useState<Step>("language");
const [data, setData] = useState<WizardData>(() => ({
language: getLanguage(),
theme: resolveThemePreference(initial?.theme ?? loadTheme(), process.env.REASONIX_THEME),
theme: resolveThemePreference(
initial?.theme ?? loadTheme(),
process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME,
),
apiKey: existingApiKey ?? "",
preset: initial?.preset ?? "auto",
selectedCatalog: deriveInitialCatalog(initial?.mcp ?? []),
Expand Down
6 changes: 4 additions & 2 deletions src/cli/ui/open-url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** Cross-platform URL opener; no-op under CI / when REASONIX_NO_OPEN is set. */
/** Cross-platform URL opener; no-op under CI / when CARBONCODE_NO_OPEN or legacy REASONIX_NO_OPEN is set. */

import { spawn } from "node:child_process";
import { platform } from "node:os";
Expand All @@ -10,7 +10,9 @@ export interface OpenUrlResult {

export function openUrl(url: string): OpenUrlResult {
if (process.env.CI) return { opened: false, reason: "ci" };
if (process.env.REASONIX_NO_OPEN) return { opened: false, reason: "disabled" };
if (process.env.CARBONCODE_NO_OPEN ?? process.env.REASONIX_NO_OPEN) {
return { opened: false, reason: "disabled" };
}

const os = platform();
let cmd: string;
Expand Down
5 changes: 4 additions & 1 deletion src/cli/ui/slash/handlers/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,10 @@ function estimateCost(userText: string, loop: import("@/loop.js").CacheFirstLoop
}

const feedback: SlashHandler = (_args, loop, ctx) => {
const themeName = resolveThemePreference(loadTheme(), process.env.REASONIX_THEME);
const themeName = resolveThemePreference(
loadTheme(),
process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME,
);
const diagnostic = buildFeedbackDiagnostic({
version: VERSION,
latestVersion: ctx.latestVersion ?? undefined,
Expand Down
5 changes: 4 additions & 1 deletion src/cli/ui/slash/handlers/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ const theme: SlashHandler = (args) => {
}

saveTheme(next);
const active = resolveThemePreference(next, process.env.REASONIX_THEME);
const active = resolveThemePreference(
next,
process.env.CARBONCODE_THEME ?? process.env.REASONIX_THEME,
);
return { info: `theme saved: ${next}\nactive on next launch: ${active}` };
};

Expand Down
2 changes: 1 addition & 1 deletion src/i18n/EN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ export const EN: TranslationSchema = {
footer: "↑↓ pick · ⏎ confirm · esc cancel",
currentPref: "current preference",
activeNow: "active now",
autoDesc: "use REASONIX_THEME or default",
autoDesc: "use CARBONCODE_THEME or default",
},
planFlow: {
approveCardTitle: "Approve plan",
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ export const zhCN: TranslationSchema = {
footer: "↑↓ 选择 · ⏎ 确认 · Esc 取消",
currentPref: "当前偏好",
activeNow: "当前生效",
autoDesc: "使用 REASONIX_THEME 或默认主题",
autoDesc: "使用 CARBONCODE_THEME 或默认主题",
},
planFlow: {
approveCardTitle: "确认计划",
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
9 changes: 9 additions & 0 deletions src/server/context.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
8 changes: 8 additions & 0 deletions tests/cli-bare-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {});
Expand All @@ -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<typeof vi.spyOn>;
Expand All @@ -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();
Expand All @@ -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 () => {
Expand Down
1 change: 1 addition & 0 deletions tests/helpers/codex-parity-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ function runCommand(
const child = spawn(command, [...args], {
cwd,
env: { ...process.env, CI: "1" },
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
});
const chunks: Buffer[] = [];
Expand Down
Loading