Project: @nulz-rip/code v1.0.0 Nulz Code — free, bring-your-own-key agentic coding CLI. Multi-provider (Gemini, Claude, OpenRouter, OpenAI, Ollama + more), tools, scraping, automation.
Key deps: @google/genai, ink, ink-spinner, ink-text-input, react
Build: npm run build — compiles TypeScript via tsc (see tsconfig.json)
Dev/Run: npm run dev — runs via tsx src/cli.tsx (no build needed)
Production: npm start — runs compiled node dist/cli.js
Tests: npm test — runs node --import tsx --test test/*.test.ts (Node native test runner)
- Single test:
node --import tsx --test test/tools.test.ts
Lint: No explicit lint command configured. Add eslint if needed.
The codebase has two entry modes:
- Interactive TUI (
cli.tsx): Ink/React-based terminal UI with real-time streaming, approvals, and scrollback - Headless one-shot (
headless.ts): Scriptable mode vianulz -p "prompt", auto-approves tools, prints to stdout
The data flow in interactive mode:
User input → Chat component (cli.tsx:480)
→ Engine.send() (engine.ts:560)
→ Backend.stream() → LLM API
→ AsyncGenerator yields EngineEvents
→ UI renders blocks (banner, user, thinking, answer, tool, console, diff, todos, system)
→ Tool calls flow through confirm() callback for approval
→ runTool() / runCommandStream() (tools.ts)
→ Results pushed back to LLM as tool role messages
Key architectural patterns:
- Async generators for streaming: Backend.stream() yields chunks as they arrive from LLM APIs
- Checkpoint system for /rewind: Each turn records file snapshots before mutations; restoreSnapshot() reverts (tools.ts:351)
- Three backend implementations: Gemini (native), Anthropic (Messages API), OpenAI-compatible (covers everyone else)
- Single source of truth for tools: toolDeclarations[] in tools.ts, transformed via lowerCaseTypes() for OpenAI/Anthropic compatibility
- Session persistence: Saved per-folder under ~/.nulz/projects//.json with full history and checkpoints
- Tool labels: TOOL_LABELS map (cli.tsx:131) provides friendly verb headers for tool calls (Claude-style "Read", "Update", "Bash", etc.)
- Console cap: CONSOLE_CAP = 24000 chars limits run_command output in UI to prevent scrollback explosion
Main entry point (~1650 lines). Exports nothing; runs at import.
Key components:
App: Stage manager (boot → setup → trust → chat). Loads config, validates provider key on startup.Setup: Multi-step provider onboarding (provider → key → model → more). Saves to config after validation.Chat: The main chat UI. Manages:blocks: Transcript state (banner, user, thinking, answer, tool, console, diff, todos, system)engine: Engine instance persisted via useStatemode: "normal" | "bypass" | "plan" — toggled with shift+tabcheckpointsRef: Array of turn-level snapshots for /rewindsubmit(): Handles all input — user messages, slash commands, shell mode (!cmd)- Streaming buffering: flushBuf ref batches updates ~10/sec to preserve terminal selection
- Command queue: Messages typed while busy are queued and auto-sent when turn ends
- Console output suppression: run_command output is hidden in chat UI (model still receives it); only a summary appears on completion
Slash commands (COMMANDS array ~line 107, handled in handleCommand() ~line 854):
| Command | Description |
|---|---|
/provider |
switch AI provider |
/model |
pick a model |
/prefs |
preferred stack |
/resume |
resume a past session |
/rewind |
restore code + chat to an earlier point |
/compact |
summarize old turns to free context |
/verify |
adversarially check the recent changes |
/commit |
stage + commit changes (AI writes the message) |
/push |
push the current branch to its remote |
/plan |
toggle plan mode (read-only, proposes a plan) |
/init |
generate NULZ.md project context |
/theme |
change the accent color |
/status |
session info |
/copy |
copy last response |
/cost |
token usage |
/doctor |
environment check |
/update |
update Nulz |
/clear |
clear the screen |
/help |
all commands |
/exit |
quit |
Tool call approval: confirm() callback passed to engine.send(). Auto-approves safe commands (isSafeCommand) and remembered signatures (isRemembered). Renders Approve component when pending.
Core LLM orchestration. Exports Engine class, makeBackend(), types.
Key types:
Msg: user | assistant | tool — unified message formatToolCall: { id, name, args, thoughtSignature? }EngineEvent: thinking | answer | tool_call | tool_chunk | tool_result | usage | retry | todos | notice | errorBackendinterface: setModel(), stream() → AsyncGenerator<StreamYield, StreamReturn>
Engine class responsibilities:
- Maintains
messages[]array (conversation history) - Builds dynamic system prompt via
buildSystem(): base SYSTEM + env (cwd, platform, date, git info) + context (NULZ.md) + prefs + live todos + changed files reminder + plan mode block send(): Main turn loop (max 25 iterations). Handles:- Auto-compact when prompt tokens > 120k (AUTO_COMPACT_TOKENS)
- Retry with exponential backoff on 503/429/overload errors (isRetryable(), sleep())
- Tool call execution with confirm() gating
- Denial tracking (stops after 3 repeated denials of same tool)
compact(): Summarizes conversation, replaces history with summaryverify(): Adversarial check — one-off LLM call to try to break recent changes
System prompt (engine.ts:20) adapted from open-source projects; includes critical rules:
- NEVER generate/guess URLs unless confident they're for programming
- No emojis unless explicitly requested
- File edits require prior full read
- All commits must end with the Nulz footer (Co-Authored-By + Generated with Nulz Code URL)
- Commit messages require detailed body + footer verification via git log -1
Backends:
GeminiBackend: Uses @google/genai, functionDeclarations, supports thinking tokensAnthropicBackend: Native Messages API, streaming SSE parsing, tool_use/tool_result blocksOpenAICompatBackend: Generic OpenAI-compatible adapter, handles reasoning_content for thinking
Tool implementations and safety. Exports declarations, runTool, command streaming, checkpoint management.
toolDeclarations: Array of FunctionDeclaration (Gemini format) with 15 tools:
- File: read_file, write_file, edit_file, multi_edit
- Discovery: glob, grep, ls
- Shell: run_command, command_output (background tasks)
- Web: fetch_url, extract_text, web_search, download_file, browser_scrape
- Planning: todo_write
Key tool descriptions:
read_file: Returns text with line numbers (format: spaces+number+tab+content). Large files truncated; use offset/limit. Prefer over cat/head/tail.edit_file: Exact string replacement; old_string must match file exactly (no line-number prefix). Must be unique unless replace_all set.multi_edit: Apply several exact-string edits to ONE file atomically (all succeed or none). Each edit follows edit_file rules.grep: Search file contents by regex. Returns file:line:match. Prefer over running grep/rg through shell.run_command: Runs in cwd; output streams live. Quote paths with spaces. For long-running commands (dev servers, builds) set run_in_background:true.command_output: Check a background command started with run_in_background. Omit task_id to list all background tasks.todo_write: Plan and track multi-step work as a task list. Pass FULL list each call (replaces previous). Mark exactly one task in_progress before starting it.
Schema transformation:
openaiTools: Maps to OpenAI function format via lowerCaseTypes() (converts Type.OBJECT → object, etc.)anthropicTools: Maps to Anthropic input_schema format
Safety features:
dangerousToolsSet: write_file, edit_file, run_command, download_file — require approvalisSafeCommand(): Allows read-only git/npm/ls commands, blocks any shell chaining (|, ;, &&, etc.)commandSignature(): Extracts stable signature for "always allow" memory (e.g., "npm run", "git push")isRemembered(): Checks if signature is in allowed list, never allows chained commands
Edit safety:
readFilesMap tracks full vs partial readsedit_file/multi_editrequire prior full read (blocked if partial)applyEdits(): Atomic string replacement with uniqueness checking
Checkpoint system:
beginTurnRecording(): Starts new Map for this turnrecordPrior(): Captures file content before first mutation (null if didn't exist)getTurnRecording(): Returns snapshot object for checkpointrestoreSnapshot(): Reverts files to prior state (writes old content or deletes if null)
Command execution:
runCommandStream(): Spawns shell process, yields stdout/stderr chunks live, returns ToolResultstartBackground(): Spawns detached process, returns bg{id} handlebackgroundOutput(): Retrieves output from bgTasks Map
Provider registry and model listing. Exports PROVIDERS array, getProvider(), listModels().
ProviderDef interface: { id, label, kind: "gemini" | "anthropic" | "openai-compat", baseUrl?, needsKey, signupUrl? }
Current providers (15):
- gemini (kind: gemini)
- anthropic (kind: anthropic, baseUrl: https://api.anthropic.com/v1)
- openrouter, openai, groq, deepseek, kimi, xai, mistral, together, fireworks, perplexity, cerebras (kind: openai-compat)
- ollama, lmstudio (kind: openai-compat, needsKey: false, local)
Model listing:
listGemini(): Uses @google/genai models.list(), filters for generateContent supportlistAnthropic(): Falls back to hardcoded ANTHROPIC_FALLBACK if /models failslistOpenAICompat(): Generic /models endpoint fetch
User configuration management. Config lives at ~/.nulz/config.json.
NulzConfig interface:
- provider: string (active provider id)
- providers: Record<string, ProviderConfig> (per-provider apiKey, baseUrl, model)
- prefs?: Record<string, string> (stack preferences)
- allowedCommands?: string[] (remembered command signatures)
- trustedFolders?: string[] (folders user confirmed)
- theme?: string (accent color)
Exports:
loadConfig(): Reads config, migrates old shape, fills env vars (GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY)saveActiveProvider(),saveProviderConfig(),savePrefs(),saveAllowedCommand(),saveTrustedFolder(),saveTheme()isTrustedFolder(): Checks if cwd is in trusted listbuildPrefsText(): Formats prefs as system prompt section
Session persistence. Sessions saved under ~/.nulz/projects/<sha1(cwd)>/<id>.json.
Session interface: { id, cwd, title, createdAt, updatedAt, model, history: Msg[], blocks: any[], checkpoints?: any[] }
Exports:
newSessionId(): timestamp + random suffixsaveSession(): Writes JSON to project dirlistSessions(): Returns SessionMeta[] sorted by updatedAt descloadSession(): Returns full Session or nullrelativeTime(): Human-readable timestamps ("5m ago")
Project context loading. Checks for NULZ.md, nulz.md, or .nulz/context.md in cwd. Injects into system prompt if found.
UI components and theming. Ink/React components.
Themes: THEMES record with 15 named colors + custom hex support. ORANGE is the live accent binding (let export).
Components:
Logo,Welcome: ASCII banner with changelog from package.jsonSelect: Arrow-key menu with windowed scrolling, used for /model, /provider, etc.InputBox: Bordered prompt containerStatusLine: Bottom bar showing mode, provider, model, token countsApprove: Tool approval prompt with preview (diff for edits, command for shell)useTermSize(): Terminal resize handling
Utilities:
setTerminalTitle(): OSC sequence for window/tab titlelink(): OSC 8 clickable hyperlinks
Pure formatting helpers. No React dependencies, unit-testable.
Exports:
wrapText(): Hard-wraps strings to widthmarkdownLines(): Converts markdown to styled Line[] — handles code blocks (cyan), headers (accent, bold), lists (•), strips ** and ` inline
Non-interactive entry point. Runs when -p or --print flag present.
runHeadless(): Reads prompt from argv or stdin, auto-approves all tools, prints answer to stdout, tool noise to stderr. Supports --provider, --model, -q/--quiet.
Cross-platform clipboard operations.
copyToClipboard(): Uses clip (Win), pbcopy (Mac), xclip (Linux)readClipboard(): Best-effort read, returns empty string on failurereadClipboardImage(): Returns { base64, mime } for PNG images from clipboard (PowerShell/.NET on Windows, osascript on Mac, xclip on Linux)
Version checking and update commands.
checkForUpdate(): Fetches npm registry for @nulz-rip/code, compares versionsisNewer(): Semver comparisonupdateCmd(): Returnsbun add -g @nulz-rip/code@{version} --force
Cost estimation. PRICES record maps model prefixes to [input$/M, output$/M]. estimateCost() returns formatted string or null.
- Add declaration in
src/tools.tstoolDeclarations[]:
{
name: "my_tool",
description: "What it does",
parameters: {
type: Type.OBJECT,
properties: { arg1: { type: Type.STRING } },
required: ["arg1"],
},
}- Add implementation in
runTool()switch:
case "my_tool": {
// Validate, execute
return { ok: true, output: "result" };
}-
Mark dangerous if needed: Add to
dangerousToolsSet if it mutates state. -
Export from engine.ts if needed: Add to imports if engine needs direct access.
No other changes needed — schemas automatically flow to all backends via lowerCaseTypes().
- Add to COMMANDS[] in
src/cli.tsx(~line 107) with description:
{ cmd: "mycommand", desc: "does something" }- Handle in handleCommand() switch (~line 854):
case "mycommand":
// Do work
push({ kind: "system", text: "Result" });
return;For async work, set busy state and call setBusy(false) when done.
- Add to PROVIDERS[] in
src/providers.ts(~line 14):
{ id: "myprovider", label: "My Provider", kind: "openai-compat", baseUrl: "https://api.my.com/v1", needsKey: true, signupUrl: "..." }Use kind: "gemini" or "anthropic" only if implementing native adapters. Most new providers use "openai-compat".
- Test model listing: Ensure
/modelsendpoint works or provider will fail setup.
No backend code changes needed for OpenAI-compatible providers — they use OpenAICompatBackend automatically via makeBackend() in engine.ts.
Tests use Node native test runner (node:test) with tsx for TypeScript.
test/tools.test.ts: Tool execution, safety checks, checkpoint/restore, glob/greptest/misc.test.ts: Version comparison, config prefs, providers registry, session utils, pricing, retry logictest/markdown.test.ts: Text wrapping, markdown rendering
Run single test file: node --import tsx --test test/tools.test.ts
- TypeScript strict mode, ESM modules, React JSX (react-jsx transform)
- Functional React components with hooks, no classes
- Native Node.js APIs preferred over external deps
- Tools return
{ ok: boolean; output: string }shape - File edits require prior full read (enforced by readFiles Map)
- System prompt lives in engine.ts SYSTEM constant — keep in sync with actual behavior
- All tool approvals go through confirm() callback; dangerousTools Set gates mutations
- Background commands tracked in-memory only (not persisted to session)
- /rewind uses turn-level checkpoints with full file snapshots