diff --git a/QUICKSTART.md b/QUICKSTART.md index ba3c7da8e..3b12ddd4d 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -8,7 +8,7 @@ Run the full product locally. - **Node.js:** `~24` (Node 24.x). The repo enforces this through `package.json#engines`. - **pnpm:** `10.33.x`. The repo pins `pnpm@10.33.2` through `packageManager`; use Corepack so the pinned version is selected automatically. -- **OS:** macOS, Linux, and WSL2 are the primary paths. Windows native is supported; see [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md) for common setup gotchas. +- **OS:** macOS, Linux, and WSL2 are the primary paths. If your agent CLIs run inside WSL2, use the [`WSL2 setup guide`](docs/wsl-setup.md). Windows native is supported; see [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md) for common PowerShell setup gotchas. - **Optional local agent CLI:** Claude Code, Codex, Devin for Terminal, Gemini CLI, OpenCode, Cursor Agent, Qwen, Qoder CLI, GitHub Copilot CLI, etc. If none are installed, use the BYOK API mode from Settings. ### Local agent CLI and PATH diff --git a/README.md b/README.md index 69574fda3..5aa38a32c 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,10 @@ od mcp install # | pi | vibe | hermes | cline | kimi | trae | opencode ``` +> **WSL2 users:** If your coding-agent CLIs run inside WSL2, follow the +> [`WSL2 setup guide`](docs/wsl-setup.md) first. Linux's `/usr/bin/od` can +> shadow Open Design's `od` command. + Then, inside the agent: ``` @@ -343,7 +347,7 @@ corepack enable && pnpm install pnpm tools-dev run web ``` -Node `~24`, pnpm `10.33.x`. Windows users, see [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md). Full quickstart, env vars, Nix flake, and packaged build flow → [`QUICKSTART.md`](QUICKSTART.md). +Node `~24`, pnpm `10.33.x`. WSL2 users, see [`docs/wsl-setup.md`](docs/wsl-setup.md); native Windows users, see [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md). Full quickstart, env vars, Nix flake, and packaged build flow → [`QUICKSTART.md`](QUICKSTART.md). ### A full workflow — from brief to artifact diff --git a/apps/daemon/src/codex-config-normalize.ts b/apps/daemon/src/codex-config-normalize.ts index 6a0418cc2..13a06376a 100644 --- a/apps/daemon/src/codex-config-normalize.ts +++ b/apps/daemon/src/codex-config-normalize.ts @@ -24,8 +24,14 @@ // intentionally scoped: only standalone `service_tier` key lines are touched; // everything else in config.toml is preserved verbatim. // -// The normalization is idempotent: if the file is absent or every service_tier -// value is already valid, it is left unchanged. +// The normalizer also removes nested `[features.*]` tables. Current Codex CLI +// configs model `[features]` as a map of boolean flags; a nested table makes a +// flag value a TOML map and the CLI exits with: +// +// invalid type: map, expected a boolean in `features` +// +// The normalization is idempotent: if the file is absent, or if no invalid +// service_tier value or nested features table is present, it is left unchanged. import { randomBytes } from 'node:crypto'; import { rename, readFile, unlink, writeFile } from 'node:fs/promises'; @@ -61,6 +67,51 @@ export function resolveCodexConfigPath( */ const VALID_SERVICE_TIERS = new Set(['fast', 'flex']); +function splitLinesPreservingEndings(content: string): string[] { + const lines = content.match(/[^\r\n]*(?:\r\n|\n|\r|$)/g) ?? []; + if (lines.at(-1) === '') lines.pop(); + return lines; +} + +function tableHeaderName(line: string): string | null { + const withoutLineEnding = line.replace(/\r\n$|\n$|\r$/, ''); + const trimmed = withoutLineEnding.trim(); + const arrayHeader = trimmed.match(/^\[\[([^\]\r\n]+)\]\][^\S\r\n]*(?:#.*)?$/); + const arrayHeaderName = arrayHeader?.[1]; + if (arrayHeaderName) return arrayHeaderName.trim(); + const tableHeader = trimmed.match(/^\[([^\]\r\n]+)\][^\S\r\n]*(?:#.*)?$/); + const tableName = tableHeader?.[1]; + if (tableName) return tableName.trim(); + return null; +} + +function removeNestedFeaturesTables(content: string): string | null { + const lines = splitLinesPreservingEndings(content); + let changed = false; + let droppingNestedFeaturesTable = false; + const kept: string[] = []; + + for (const line of lines) { + const headerName = tableHeaderName(line); + if (headerName) { + droppingNestedFeaturesTable = headerName.startsWith('features.'); + if (droppingNestedFeaturesTable) { + changed = true; + continue; + } + } + + if (droppingNestedFeaturesTable) { + changed = true; + continue; + } + + kept.push(line); + } + + return changed ? kept.join('') : null; +} + /** * Normalize the `service_tier` field in a config.toml string. * @@ -68,6 +119,9 @@ const VALID_SERVICE_TIERS = new Set(['fast', 'flex']); * {@link VALID_SERVICE_TIERS} has its entire line removed (so the Codex CLI * uses its built-in default tier). Valid values are left verbatim. * + * Any nested `[features.*]` table is also removed because current Codex CLI + * configs expect `[features]` entries to be booleans, not maps. + * * Returns `null` when nothing needed to change, otherwise the patched content. */ export function normalizeCodexConfigContent(content: string): string | null { @@ -94,7 +148,7 @@ export function normalizeCodexConfigContent(content: string): string | null { /^([^\S\r\n]*)service_tier([^\S\r\n]*=[^\S\r\n]*)(["'])([^"'\r\n]*)\3([^\S\r\n]*(?:#[^\r\n]*)?)(\r?\n|$)/gm; let changed = false; - const patched = content.replace( + const serviceTierPatched = content.replace( pattern, (match: string, _indent, _eq, _quote, value: string) => { if (VALID_SERVICE_TIERS.has(value)) { @@ -107,7 +161,12 @@ export function normalizeCodexConfigContent(content: string): string | null { }, ); - return changed ? patched : null; + const featuresPatched = removeNestedFeaturesTables(serviceTierPatched); + if (featuresPatched !== null) { + changed = true; + } + + return changed ? (featuresPatched ?? serviceTierPatched) : null; } /** diff --git a/apps/daemon/tests/codex-config-normalize.test.ts b/apps/daemon/tests/codex-config-normalize.test.ts index 54ee889bb..fd7d53309 100644 --- a/apps/daemon/tests/codex-config-normalize.test.ts +++ b/apps/daemon/tests/codex-config-normalize.test.ts @@ -195,6 +195,49 @@ describe('normalizeCodexConfigContent', () => { expect(normalizeCodexConfigContent(input)).toBeNull(); }); + it('removes nested features tables that make Codex parse features as maps (#4648)', () => { + const input = [ + '[features]', + 'hide_spawn_agent_metadata = false', + '[features.multi_agent_v2]', + 'hide_spawn_agent_metadata = false', + 'max_concurrent_threads_per_session = 10000', + 'enabled = false', + '', + '[model]', + 'model = "gpt-5.5"', + ].join('\n'); + + const result = normalizeCodexConfigContent(input); + + expect(result).toBe([ + '[features]', + 'hide_spawn_agent_metadata = false', + '[model]', + 'model = "gpt-5.5"', + ].join('\n')); + }); + + it('preserves unrelated dotted tables while removing nested features tables', () => { + const input = [ + '[profiles.default]', + 'model = "gpt-5.5"', + '[features.multi_agent_v2]', + 'enabled = false', + '[projects."/tmp/open-design"]', + 'trust_level = "trusted"', + ].join('\n'); + + const result = normalizeCodexConfigContent(input); + + expect(result).toBe([ + '[profiles.default]', + 'model = "gpt-5.5"', + '[projects."/tmp/open-design"]', + 'trust_level = "trusted"', + ].join('\n')); + }); + // ------------------------------------------------------------------------- // CRLF regression: config.toml files written by the Codex app on Windows use // CRLF endings. Removing the stale line must preserve ALL surrounding \r\n @@ -216,6 +259,17 @@ describe('normalizeCodexConfigContent', () => { // No naked LF introduced. expect(result).not.toMatch(/(? { + const crlfContent = '[features]\r\nhide_spawn_agent_metadata = false\r\n[features.multi_agent_v2]\r\nenabled = false\r\n[model]\r\nmodel = "gpt-5.5"\r\n'; + const result = normalizeCodexConfigContent(crlfContent); + + expect(result).not.toBeNull(); + expect(result).not.toContain('[features.multi_agent_v2]'); + expect(result).toBe('[features]\r\nhide_spawn_agent_metadata = false\r\n[model]\r\nmodel = "gpt-5.5"\r\n'); + expect(result).not.toMatch(/\r(?!\n)/); + expect(result).not.toMatch(/(? { expect(after).toContain('model = "gpt-5.5"'); }); + it('removes nested features tables from config.toml (#4648 regression)', async () => { + const configPath = join(tmpDir, 'config.toml'); + writeFileSync( + configPath, + [ + '[features]', + 'hide_spawn_agent_metadata = false', + '[features.multi_agent_v2]', + 'hide_spawn_agent_metadata = false', + 'max_concurrent_threads_per_session = 10000', + 'enabled = false', + '[model]', + 'model = "gpt-5.5"', + ].join('\n'), + 'utf8', + ); + + await normalizeCodexConfigFile({ CODEX_HOME: tmpDir }); + + const after = readFileSync(configPath, 'utf8'); + expect(after).toContain('[features]'); + expect(after).toContain('hide_spawn_agent_metadata = false'); + expect(after).not.toContain('[features.multi_agent_v2]'); + expect(after).not.toContain('max_concurrent_threads_per_session'); + expect(after).toContain('[model]'); + expect(after).toContain('model = "gpt-5.5"'); + }); + it('does not modify config.toml when service_tier is already valid', async () => { const configPath = join(tmpDir, 'config.toml'); const original = `service_tier = "fast"\n`; diff --git a/docs/windows-troubleshooting.md b/docs/windows-troubleshooting.md index e1b96d0ac..5206a10b7 100644 --- a/docs/windows-troubleshooting.md +++ b/docs/windows-troubleshooting.md @@ -2,7 +2,7 @@ Open Design runs on Windows natively, but the path is less travelled than macOS, Linux, or WSL2. This guide covers the most common errors you will hit on a fresh Windows machine and the exact fix for each. -> **Tip:** If you already have WSL2 set up, that is the smoothest path on Windows. This guide is for native Windows (PowerShell). +> **Tip:** If your coding-agent CLIs run inside WSL2, use the dedicated [`WSL2 setup guide`](wsl-setup.md). This guide is for native Windows (PowerShell). --- diff --git a/docs/wsl-setup.md b/docs/wsl-setup.md new file mode 100644 index 000000000..fb471472c --- /dev/null +++ b/docs/wsl-setup.md @@ -0,0 +1,166 @@ +# WSL2 Setup Guide + +Use this guide when your coding-agent CLIs run inside WSL2. In that setup, +install and run Open Design from WSL as well so the agent CLI, `od` command, +daemon, Node modules, and credentials all come from the same Linux environment. + +For native Windows PowerShell setup, use +[`docs/windows-troubleshooting.md`](windows-troubleshooting.md) instead. + +## Recommended shape + +- Clone Open Design inside WSL2. +- Install Node `~24` and the repo-pinned pnpm (`10.33.2`) inside WSL2. +- Put a WSL-native `od` wrapper before `/usr/bin` on `PATH`. +- Start the daemon from WSL with `od --no-open`. +- Install MCP entries from the same WSL shell. + +Do not assume the Windows desktop app's daemon is the right daemon for WSL +agent clients. WSL2 networking and Windows credential stores can make that path +ambiguous. A WSL-started daemon keeps the MCP clients and Open Design in the +same environment. + +## 1. Install from source in WSL + +```bash +git clone https://github.com/nexu-io/open-design.git ~/tools/open-design +cd ~/tools/open-design + +node --version # should print v24.x.x +corepack enable +corepack pnpm --version # should print 10.33.2 +pnpm install +``` + +If you use `mise`, trust and install the repo toolchain before `pnpm install`: + +```bash +mise trust +mise install +``` + +## 2. Fix the `od` command collision + +Linux already ships `/usr/bin/od` (octal dump). If that binary wins on `PATH`, +commands such as `od mcp install claude` fail with file-not-found messages for +`mcp`, `install`, and the agent name. + +Check what your shell resolves: + +```bash +type -a od +``` + +If `/usr/bin/od` appears before Open Design, create a wrapper in `~/.local/bin` +and make sure that directory is first on `PATH`: + +```bash +mkdir -p ~/.local/bin + +cat > ~/.local/bin/od <<'EOF' +#!/usr/bin/env bash +repo="$HOME/tools/open-design" +cd "$repo" || exit 127 + +if command -v mise >/dev/null 2>&1; then + exec mise exec -- pnpm exec od "$@" +fi + +exec corepack pnpm exec od "$@" +EOF + +chmod +x ~/.local/bin/od +export PATH="$HOME/.local/bin:$PATH" +hash -r +type -a od +``` + +Expected first result: + +```text +od is /home//.local/bin/od +``` + +`od.exe` is not a reliable workaround from WSL. It may resolve to a Windows +coreutils binary instead of Open Design, especially on machines with Windows +coreutils installed. + +## 3. Start the daemon from WSL + +Run the daemon from the same WSL environment that your agent CLIs use: + +```bash +cd ~/tools/open-design +od --no-open +``` + +In another WSL terminal, verify it is reachable: + +```bash +curl -sSf http://127.0.0.1:7456 >/dev/null && echo "Open Design daemon is reachable" +``` + +Leave the daemon terminal running while using MCP integrations. + +## 4. Install MCP entries + +From WSL, run the installer for each agent CLI you use: + +```bash +od mcp install claude +od mcp install opencode +od mcp install codex +od mcp install antigravity +od mcp install copilot +``` + +The installer writes to the agent config locations for the current WSL user, +for example `~/.claude.json`, `~/.config/opencode/opencode.json`, +`~/.codex/config.toml`, `~/.gemini/antigravity/mcp_config.json`, and +`~/.copilot/mcp-config.json`. + +## Native module mismatch after changing Node versions + +If dependencies were installed under Node 22 and Open Design later runs under +Node 24, native modules such as `better-sqlite3` can fail with a +`NODE_MODULE_VERSION` mismatch. + +Reinstall under the active Node 24 runtime: + +```bash +cd ~/tools/open-design +rm -rf node_modules +pnpm store prune +pnpm install +``` + +Then verify the native module loads: + +```bash +pnpm --filter @open-design/daemon exec node -e "require('better-sqlite3')" +``` + +## Codex config parse failures + +If Codex fails before MCP install or a direct `codex` run with: + +```text +invalid type: map, expected a boolean +in `features` +``` + +check `~/.codex/config.toml` for nested feature tables such as: + +```toml +[features.multi_agent_v2] +hide_spawn_agent_metadata = false +max_concurrent_threads_per_session = 10000 +enabled = false +``` + +Current Codex CLI versions expect `[features]` values to be booleans. Remove or +comment out the nested `[features.*]` block, then retry the command. + +Open Design also normalizes this shape before daemon-launched Codex runs, but +manual cleanup may still be needed when Codex itself is invoked directly before +Open Design gets a chance to patch the config.