Files
fidoca/README.md
2026-09-27 23:45:07 +02:00

18 KiB
Raw Permalink Blame History

Fido Project Overview

Fido is a minimal, interactive terminal coding assistant. It connects an OpenRouter-hosted model to a deliberately small set of workspace tools, then presents the conversation and tool activity in a full-screen terminal UI.

The project is written in modern JavaScript as native ES modules. It runs directly from source with Node.js; there is no transpiler, bundler, or JSX build step.

What Fido Can Do

  • Inspect and search a project without leaving the terminal.
  • List directories, find files by name, search file contents, and read files in chunks.
  • Apply precise file edits, create files and directories, and remove files or directory trees.
  • Run shell commands after explicit user approval.
  • Search the web and extract web pages through Tavily when configured.
  • Ask the user clarifying questions or request help from a human expert.
  • Stream model reasoning and tool notifications into a scrollable terminal interface.
  • Navigate project files from the prompt and insert whole-file, single-line, or line-range mentions.
  • Keep mutation and conversation state isolated per workspace, outside repositories when installed.
  • Compact large tool payloads between turns to reduce token usage and avoid retaining stale file contents.

Runtime Architecture

bin/fido
  ├─ loads Fido configuration
  ├─ selects per-workspace state
  └─ starts src/fido.js
         └─ runRepl()
              ├─ builds the system prompt
              ├─ drives LLMOrbiter + OpenRouter
              ├─ executes tools from src/tools/
              └─ drives the Ink UI through src/ui/

model tool call
  └─ tool registry
       ├─ workspace path validation
       ├─ filesystem, shell, or Tavily operation
       └─ structured result returned to the model and shown in the UI

Main Modules

Module Responsibility
bin/fido Installed/source launcher; loads user configuration and creates per-workspace state paths.
src/fido.js Process entry point; checks for the required API key and starts the REPL.
src/repl.js Owns conversation state, provider turns, cancellation, tool events, and transcript compaction.
src/turn-state.js Closed enum of connection states plus the pure (state, event) -> state reducer that drives the command-bar label.
src/config.js Loads .env files and resolves the model, workspace root, and state locations.
src/system-prompt.js Builds the workspace-aware model instructions sent at session start.
src/commands.js Implements local slash commands without spending a model turn.
src/slash-commands.js Defines and resolves the built-in command registry.
src/tools/index.js Registers the model-facing tools and their UI notification renderers.
src/tools/*.js Implements individual filesystem, shell, web, and interaction tools.
src/sandbox.js Resolves relative tool paths and rejects absolute paths or lexical traversal outside the workspace.
src/dir-listing.js Shared .gitignore-aware directory walking and pagination.
src/glob.js Small shared glob dialect used by name and content searches.
src/lines.js Shared line splitting with editor-compatible trailing-newline behavior.
src/compact.js Shrinks old patch arguments and invalidates file reads made stale by later patches.
src/undo.js Persists a newest-first log describing mutations made by tools.
src/suspend.js Saves and restores manually suspended conversation snapshots.
src/transcript.js Formats assistant, user, tool, error, command, and recalled-message output.
src/file-browser.js Provides testable file-browser navigation and mention-formatting helpers.
src/ui/index.js Exposes an imperative UI singleton to the REPL and tools.
src/ui/store.js External, subscriber-based state store for prompts, history, status, and usage totals.
src/ui/app.js Root Ink application and fixed terminal layout.
src/ui/main-input.js Main input, slash-command completion, and Ctrl+O file/line browser.

Conversation Flow

  1. Configuration resolves the workspace root using the first positional argument, then ROOT_DIR, then the current directory.
  2. The REPL loads undo and suspend state and mounts the Ink UI.
  3. It builds a system prompt containing the workspace root, a short root listing, tool contracts, and operating rules.
  4. The user submits a normal message or a local slash command.
  5. Normal input is sent to LLMOrbiter, which uses the OpenRouter provider and advertises the registered tools to the model.
  6. A provider turn can iterate up to 20 times, allowing the model to call tools and continue reasoning before returning a final response.
  7. Tool calls and results are rendered in the transcript; the structured result is also returned to the model.
  8. Between user turns, Fido compacts large patch arguments and replaces pre-patch file reads that are no longer current.
  9. The resulting message array remains in memory for the session. It can be saved explicitly with /suspend.

Model-Facing Tools

All currently registered tools are defined in src/tools/index.js.

Tool Purpose and notable behavior
list_files Lists one directory, optionally recursively, with .gitignore support and pagination. Dot-directories are hidden unless requested.
find_files Recursively finds files or directories by a simple name glob. Slashless patterns also match basenames.
read_file Reads a workspace file as line-based content and supports zero-indexed seek/limit chunking.
ripgrep Performs a pure-JavaScript regular-expression content search. It skips binary files, files over 2 MiB, ignored paths, and hidden directories by default. Long matching lines are capped.
patch Applies ordered create/overwrite or exact first-match search/replace operations. Supports skiplines, CRLF adaptation, partial-progress reporting, and undo records.
mkdir Recursively creates a directory and records its creation.
rm Deletes a file while retaining its contents in the undo log.
rmdir Recursively deletes a directory. Its undo entry records the path but not the removed contents.
bash_exec Runs bash -c with the workspace as its initial working directory. Requires explicit approval, closes stdin, disables interactive/color behavior, limits output, and kills the process tree at timeout.
javascript_execute Runs a JavaScript snippet in a vm2 sandbox on a worker thread, terminated at timeout. Returns module.exports (sync or a promise) and captured console output. require is restricted to workspace files — no host modules, no node_modules, no process/fs. fetch is off unless allow_network=true is explicitly approved by the user.
websearch Uses Tavily to return a synthesized answer and ranked web results. It fails when no Tavily key is configured.
browse_url Uses Tavily to extract one or more web pages, optionally focused by a relevance query. It fails when no Tavily key is configured.
ask_user Prompts the user for clarification or a decision, with up to three suggested choices and a free-text option.
ask_an_expert Displays a copyable question for a human expert and waits for the user to paste the answer back. It does not contact an external expert directly.

The web tools remain registered even when TAVILY_API_KEY is absent; configuration is validated lazily when one is invoked.

Built-In Commands

Command Behavior
/paste Temporarily hands the terminal to $EDITOR for multiline input, then sends the resulting text to the model.
/suspend [description] Saves the current message array as a timestamped JSON snapshot. A description is requested if omitted.
/recall Lets the user select a saved snapshot. The old conversation is restored under a newly built system prompt.
/undo Displays the most recent mutation-log entries. It currently inspects the log; it does not execute a rollback operation.
/reasoning [level] Shows or sets provider reasoning effort to minimal, low, medium, or high. Omitting the level leaves the model default in effect.
/quit Ends the session.

Typing / in the main input opens inline command completion. All recognized slash commands are handled locally and do not consume a model turn.

Terminal UI

The UI is a React application rendered by Ink in the terminal's alternate screen.

  • Header: model, workspace, reasoning setting, and history-scroll state.
  • History: bottom-anchored transcript with streaming reasoning and line-granular scrolling.
  • Command bar: main input, local command completion, approval prompts, selections, confirmations, and the Ctrl+O file browser.
  • Footer: cumulative model calls, input/output/cache tokens, estimated cost, and key hints.
  • History controls: PgUp/PgDn, mouse wheel, and End to return to the live tail.
  • Cancellation: Ctrl+C at the main prompt exits; during a model turn it aborts the provider request and restores the draft.

The store retains the latest 1,000 logical history entries, while the view mounts only the lines that fit in the current terminal. Mouse reporting can be disabled with FIDO_NO_MOUSE=1.

Connection States

While a turn runs, the command bar shows an animated spinner plus one of five states. The state is a closed enum (src/turn-state.js) and every transition is driven by an observable provider event rather than a timer, so the label always names what the stream is currently doing.

State Label Entered when
idle (the › (idle) placeholder) Session start, after a turn completes, after cancel, after an error.
connecting connecting… The turn started and the request is in flight, but no delta has arrived yet.
thinking thinking… Every reasoning delta.
working working… Every tool-call delta.
streaming streaming… Every content delta.

A turn therefore reads as connecting → streaming for a plain answer, connecting → thinking → streaming when the model reasons first, and connecting → thinking → working → streaming when it also calls a tool. Each delta wins immediately, so a later reasoning delta can move the label back from streaming to thinking; the label tracks the newest activity rather than the furthest point reached. Cancel and error are terminal: both fall back to idle so the prompt always returns.

The REPL (src/repl.js) is the only module that decides a transition, and it does so through a pure (state, event) -> state reducer, so the machine is unit-testable without React, the store, or a live provider. Text supplied by the provider's onStatus is treated as a non-authoritative detail shown next to the state label, never as a replacement for it.

File Mentions

Press Ctrl+O in the main prompt to open the file browser.

  • Arrow keys move the selection.
  • Enter opens a directory or enters a file's line selector.
  • Tab inserts a whole-file mention from the browser.
  • Backspace moves to the parent directory.
  • Esc leaves an overlay.
  • In line mode, v starts or ends a range, g/G jump to the first/last line, and Enter inserts the mention.

Mentions are references, not embedded file contents:

@README.md
@src/fido.js:12
@src/fido.js:12-40

The browser hides ignored and dot-directory entries by default. Mentions use forward-slash workspace-relative paths and one-based line numbers; the model maps those numbers to read_file's zero-based seek.

Safety and Trust Boundaries

Fido has useful path and workflow safeguards, but it is not an operating-system sandbox.

  • Dedicated filesystem tools require relative paths and reject lexical paths that resolve outside ROOT_DIR.
  • Direct filesystem access can still follow filesystem semantics such as symlinks; path checks should not be treated as a hard kernel-enforced isolation boundary.
  • bash_exec pins only the shell's starting directory. The shell can use absolute paths or change directories, so interactive approval is the primary protection around shell execution.
  • javascript_execute runs code in a vm2 sandbox on a worker thread. NodeVM ignores vm2's own timeout option, so the parent enforces a hard wall-clock limit and terminates the thread — this is what stops while (true) {}. The sandbox gets no process, no built-in modules and no node_modules; require resolves only against the workspace root.
  • javascript_execute never enables the network implicitly. fetch and the WHATWG classes are injected only when allow_network=true is passed and the user approves; declining runs the snippet offline rather than failing the call. Approved network access still permits SSRF (localhost, internal services, cloud metadata endpoints), so the approval prompt is the only guard.
  • Users can approve one command or all shell commands for the rest of that Fido session.
  • Network and extracted web content is explicitly treated as untrusted third-party input.
  • The model is instructed to inspect files before patching and to prefer targeted replacements over whole-file rewrites.
  • API keys are loaded from environment files that are excluded by .gitignore; installed configuration lives under FIDO_HOME.
  • Tool notifications shorten large arguments, but full model tool results remain part of the provider conversation.

State and Persistence

Installed layout

The installer defaults to FIDO_HOME=~/.fido:

~/.fido/
  app/                    # this repository
  llmorbiter/             # bundled local dependency
  bin/fido                # launcher
  .env                    # API keys and configuration
  state/
    <workspace-slug>-<checksum>/
      undo.json
      suspends/
        <timestamp>-<description>.json

The workspace slug is derived from the canonical workspace path and augmented with a checksum, keeping state isolated between projects and avoiding repository litter.

Source-checkout defaults

When run without the installed launcher, the defaults are:

UNDO_FILE=.fido-undo.json
SUSPEND_DIR=.fido-suspends

These paths are resolved from the selected workspace. They are covered by the repository's .fido-* ignore rule.

Normal conversations are not automatically persisted on exit. /suspend and /recall provide explicit manual snapshots.

Configuration

Copy .env.example to .env and set at least OPENROUTER_API_KEY.

Variable Required Default / meaning
OPENROUTER_API_KEY Yes Credential for the OpenRouter provider.
MODEL No xiaomi/mimo-v2.6-flash
TAVILY_API_KEY No Enables websearch and browse_url.
ROOT_DIR No Workspace root when no positional path is supplied.
UNDO_FILE No Undo-log file; the installed launcher overrides it per workspace.
SUSPEND_DIR No Suspend directory; the installed launcher overrides it per workspace.
COMPACT_DEBUG No Set to 1 to report transcript-compaction statistics.
FIDO_HOME No Installer/launcher home, default ~/.fido.
LLMORBITER_SRC No Installer override for the sibling llmorbiter checkout.
BASHRC No Installer shell RC file, default ~/.bashrc.
FIDO_SKIP_DEPS No Set to 1 to let the installer skip npm install.
FIDO_NO_MOUSE No Set to 1 to disable terminal mouse reporting.

For installed use, launcher-exported variables take precedence over workspace .env values. The source application also checks the repository's .env and then the current working directory's .env.

Installation and Development

Source checkout

Fido requires the sibling local dependency declared in package.json:

<parent-directory>/
  fido/
  llmorbiter/

From the Fido checkout:

npm install
cp .env.example .env
# Edit .env and set OPENROUTER_API_KEY.
npm start

Run against another workspace with:

npm start -- /path/to/project

Installed launcher

./install.sh
source ~/.bashrc
fido
# or
fido /path/to/project

The installer copies the app and llmorbiter into FIDO_HOME, installs dependencies, installs the launcher, and adds $FIDO_HOME/bin to PATH. Reinstalling preserves an existing $FIDO_HOME/.env. Use ./install.sh --uninstall to remove the install and its PATH block.

If llmorbiter is not a sibling checkout:

LLMORBITER_SRC=/path/to/llmorbiter ./install.sh

Tests

npm test
# or one file
node --test test/compact.test.js

The suite uses Node's built-in test runner and focuses on pure helper behavior: transcript compaction, file-browser paths and mention formatting, glob translation, line counting, mouse decoding, and UI/store rendering behavior. Unit tests do not make live model or Tavily requests.

Extending the Project

To add a model-facing tool:

  1. Create a module under src/tools/ using defineTool from llmorbiter.
  2. Validate arguments and return structured data or throw ToolError.
  3. Use resolveSafe for workspace-relative filesystem paths.
  4. Export a concise notifyCall and, when useful, a tailored notifyResult renderer.
  5. Register the tool and notification renderers in src/tools/index.js.
  6. Add its contract to src/system-prompt.js when the model needs extra usage rules.
  7. Add focused tests that do not require live network or provider calls.

The non-React REPL and tool modules should interact with rendering through the ui facade in src/ui/index.js; direct console writes would bypass the full-screen UI state.

Current Status and Planned Work

The current implementation does not include persistent todo plans or subagent delegation. PLAN.md is a proposed design for those features, including an update_plan tool, a right-side plan panel, a bounded read-only delegate tool, and separate subagent activity/status UI. Those capabilities should not be treated as available until their modules, registry entries, persistence, and tests are implemented.

Other practical limitations today include manual-only conversation snapshots, a view-only /undo command, no kernel-level sandbox, and no live provider integration tests in the standard suite.

License

Fido is released under the 0BSD License.