Skip to content

Latest commit

 

History

History
345 lines (268 loc) · 15.9 KB

File metadata and controls

345 lines (268 loc) · 15.9 KB

Project context

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

Commands

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.

Architecture Overview

The codebase has two entry modes:

  1. Interactive TUI (cli.tsx): Ink/React-based terminal UI with real-time streaming, approvals, and scrollback
  2. Headless one-shot (headless.ts): Scriptable mode via nulz -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

Module-by-Module Breakdown

src/cli.tsx

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 useState
    • mode: "normal" | "bypass" | "plan" — toggled with shift+tab
    • checkpointsRef: Array of turn-level snapshots for /rewind
    • submit(): 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.

src/engine.ts

Core LLM orchestration. Exports Engine class, makeBackend(), types.

Key types:

  • Msg: user | assistant | tool — unified message format
  • ToolCall: { id, name, args, thoughtSignature? }
  • EngineEvent: thinking | answer | tool_call | tool_chunk | tool_result | usage | retry | todos | notice | error
  • Backend interface: 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 summary
  • verify(): 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 tokens
  • AnthropicBackend: Native Messages API, streaming SSE parsing, tool_use/tool_result blocks
  • OpenAICompatBackend: Generic OpenAI-compatible adapter, handles reasoning_content for thinking

src/tools.ts

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:

  • dangerousTools Set: write_file, edit_file, run_command, download_file — require approval
  • isSafeCommand(): 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:

  • readFiles Map tracks full vs partial reads
  • edit_file / multi_edit require prior full read (blocked if partial)
  • applyEdits(): Atomic string replacement with uniqueness checking

Checkpoint system:

  • beginTurnRecording(): Starts new Map for this turn
  • recordPrior(): Captures file content before first mutation (null if didn't exist)
  • getTurnRecording(): Returns snapshot object for checkpoint
  • restoreSnapshot(): Reverts files to prior state (writes old content or deletes if null)

Command execution:

  • runCommandStream(): Spawns shell process, yields stdout/stderr chunks live, returns ToolResult
  • startBackground(): Spawns detached process, returns bg{id} handle
  • backgroundOutput(): Retrieves output from bgTasks Map

src/providers.ts

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 support
  • listAnthropic(): Falls back to hardcoded ANTHROPIC_FALLBACK if /models fails
  • listOpenAICompat(): Generic /models endpoint fetch

src/config.ts

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 list
  • buildPrefsText(): Formats prefs as system prompt section

src/session.ts

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 suffix
  • saveSession(): Writes JSON to project dir
  • listSessions(): Returns SessionMeta[] sorted by updatedAt desc
  • loadSession(): Returns full Session or null
  • relativeTime(): Human-readable timestamps ("5m ago")

src/context.ts

Project context loading. Checks for NULZ.md, nulz.md, or .nulz/context.md in cwd. Injects into system prompt if found.

src/ui.tsx

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.json
  • Select: Arrow-key menu with windowed scrolling, used for /model, /provider, etc.
  • InputBox: Bordered prompt container
  • StatusLine: Bottom bar showing mode, provider, model, token counts
  • Approve: Tool approval prompt with preview (diff for edits, command for shell)
  • useTermSize(): Terminal resize handling

Utilities:

  • setTerminalTitle(): OSC sequence for window/tab title
  • link(): OSC 8 clickable hyperlinks

src/markdown.ts

Pure formatting helpers. No React dependencies, unit-testable.

Exports:

  • wrapText(): Hard-wraps strings to width
  • markdownLines(): Converts markdown to styled Line[] — handles code blocks (cyan), headers (accent, bold), lists (•), strips ** and ` inline

src/headless.ts

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.

src/clipboard.ts

Cross-platform clipboard operations.

  • copyToClipboard(): Uses clip (Win), pbcopy (Mac), xclip (Linux)
  • readClipboard(): Best-effort read, returns empty string on failure
  • readClipboardImage(): Returns { base64, mime } for PNG images from clipboard (PowerShell/.NET on Windows, osascript on Mac, xclip on Linux)

src/update.ts

Version checking and update commands.

  • checkForUpdate(): Fetches npm registry for @nulz-rip/code, compares versions
  • isNewer(): Semver comparison
  • updateCmd(): Returns bun add -g @nulz-rip/code@{version} --force

src/pricing.ts

Cost estimation. PRICES record maps model prefixes to [input$/M, output$/M]. estimateCost() returns formatted string or null.

How to Extend

Add a Tool

  1. Add declaration in src/tools.ts toolDeclarations[]:
{
  name: "my_tool",
  description: "What it does",
  parameters: {
    type: Type.OBJECT,
    properties: { arg1: { type: Type.STRING } },
    required: ["arg1"],
  },
}
  1. Add implementation in runTool() switch:
case "my_tool": {
  // Validate, execute
  return { ok: true, output: "result" };
}
  1. Mark dangerous if needed: Add to dangerousTools Set if it mutates state.

  2. 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 a Slash Command

  1. Add to COMMANDS[] in src/cli.tsx (~line 107) with description:
{ cmd: "mycommand", desc: "does something" }
  1. 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 a Provider

  1. 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".

  1. Test model listing: Ensure /models endpoint works or provider will fail setup.

No backend code changes needed for OpenAI-compatible providers — they use OpenAICompatBackend automatically via makeBackend() in engine.ts.

Test Structure

Tests use Node native test runner (node:test) with tsx for TypeScript.

  • test/tools.test.ts: Tool execution, safety checks, checkpoint/restore, glob/grep
  • test/misc.test.ts: Version comparison, config prefs, providers registry, session utils, pricing, retry logic
  • test/markdown.test.ts: Text wrapping, markdown rendering

Run single test file: node --import tsx --test test/tools.test.ts

Key Conventions

  • 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