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

314 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```text
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:
```text
@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`:
```text
~/.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:
```text
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`:
```text
<parent-directory>/
fido/
llmorbiter/
```
From the Fido checkout:
```sh
npm install
cp .env.example .env
# Edit .env and set OPENROUTER_API_KEY.
npm start
```
Run against another workspace with:
```sh
npm start -- /path/to/project
```
### Installed launcher
```sh
./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:
```sh
LLMORBITER_SRC=/path/to/llmorbiter ./install.sh
```
### Tests
```sh
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](README.md#license).