- TypeScript 99.1%
- JavaScript 0.9%
| .nut/chat | ||
| docs | ||
| effect@a46592aed9 | ||
| pi@f10993bc7f | ||
| scripts | ||
| src | ||
| test | ||
| .gitignore | ||
| .gitmodules | ||
| bun.lock | ||
| bunfig.toml | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Nut
An endless coding chat built with Effect v4 and pi-durable. Based on Victor Taelin's OptChat specification. The pinned specification is in docs/optchat.md.
Each turn starts with a fresh model context. A bounded view of the whole history supplies memory. The agent can open summaries with zoom to read the original messages.
Status: working local CLI with automated tests. No live provider calls or cache-cost measurements have been verified. See the differences below before using it for important work.
Setup
Requires Bun 1.4.2+ and macOS or Linux. Node.js is not required. The directory lock uses a Unix socket.
git submodule update --init --recursive
bun install --frozen-lockfile
bun run check
bun run test
Installation runs bun run setup. Setup copies the pinned Pi AI source to .cache/pi-ai and downloads its checksum-verified model catalog. It does not change pi/. Run setup again after changing the submodule revision. If install scripts are disabled, run bun run setup yourself.
The project pins effect, @effect/platform-bun, and @effect/vitest to 4.0.2. Pi is pinned by the Git submodule. Dependencies are locked in bun.lock. Source files use some node: compatibility APIs supplied by Bun; they do not start Node.js. Run the application from source: Pi's OAuth loaders use runtime imports that a standalone bundle does not preserve automatically.
Run
export ANTHROPIC_API_KEY=...
bun start
Defaults:
- Chat directory:
<working-directory>/.nut/chat(project-local history, summaries, and durable state). - Agent and compactor:
anthropic/claude-sonnet-4-5. - Working directory: the current directory.
- Instructions:
AGENTS.mdin the working directory, if present.
bun start --chat /path/to/chat --cwd /path/to/project
bun start --model openai/gpt-5-mini --compactor anthropic/claude-sonnet-4-5
bun start --prompt 'Inspect the project and suggest the next task.'
OpenAI supports either OPENAI_API_KEY (API billing) or ChatGPT subscription login. NUT_CHAT, NUT_MODEL, NUT_COMPACTOR, and NUT_AUTH_DIR set defaults through Effect Config. CLI flags take priority.
Memory defaults to .nut/chat inside --cwd (or the launch directory). No Git-root search is performed: launch from the project root or pass --cwd consistently to reuse its memory. Different project directories have separate histories. Add .nut/ to each project’s .gitignore; memory contains plaintext messages and tool output. This repository already ignores it.
--chat overrides NUT_CHAT, which overrides the project-local default. Existing ~/.nut/chat history is left untouched; use --chat ~/.nut/chat to reopen it. Credentials and saved model/thinking preferences remain global in ~/.nut/auth and ~/.nut/settings.json.
Tool execution and recovered work use the current session's --cwd (or launch directory), independently of --chat. New agent configuration does not persist cwd. Older chats remain readable: saved cwd is ignored and cleared from current agent state, without rewriting historical records, message IDs, summaries, or zoom data. This does not redact paths already present in messages, tool arguments/output, or old runtime records.
ChatGPT subscription
-
Stop any running Nut process that uses the same auth directory.
-
Run the login command in an interactive terminal:
bun start login --provider openai-codex -
Open the displayed URL in your browser.
-
Sign in with your ChatGPT account.
-
If the local callback does not finish, paste the full final redirect URL into the terminal.
-
Select Codex for both the main agent and compactor:
bun start --model openai-codex/gpt-5.5 --compactor openai-codex/gpt-5.5
No Anthropic or OpenAI API key is needed for this command. Both models consume your subscription's usage allowance. Model availability and limits depend on your account. The defaults remain Anthropic unless you set the flags or environment variables.
bun start auth-status
bun start logout
Login defaults to Pi's openai-codex provider: its Codex OAuth client and ChatGPT backend, including browser and device-code login choices. This is distinct from the newer direct-token openai flow, which uses a different OAuth client and the public Responses API. Enterprise policy may allow one and not the other; live enterprise login has not been verified here. Pi owns OAuth, account metadata, and refresh for both paths.
To use the newer flow instead, run bun start login --provider openai, then select openai/... models. Existing openai credentials are preserved. logout also defaults to openai-codex; auth-status lists all providers unless --provider is supplied. Browser callbacks use port 1455; stop other pending logins if it is occupied.
Credentials live in ~/.nut/auth/auth.json, separate from chat history. The directory has mode 0700; the file has mode 0600. Writes use an fsynced temporary file and atomic rename. A stable installation ID is stored beside the credentials. OAuth metadata and refreshed tokens survive restarts. Nut serializes refreshes and waits for an active refresh to save its tokens before closing.
Only one Nut process can own an auth directory at a time. Stop the chat before login, logout, or auth-status. Use --auth-dir PATH or NUT_AUTH_DIR for a different private directory, and use the same setting for login and chat. Nut does not read or change Pi's or Codex's own credential files.
For the openai provider, saved OAuth credentials take priority over OPENAI_API_KEY. Codex credentials are separate and never fall back to this API key. A failed refresh does not silently switch to API billing. After logout, a set OPENAI_API_KEY becomes active again. Logout removes local credentials; it does not revoke the account grant remotely.
Credentials are plaintext secrets. Do not add the auth directory to Git or share it. A corrupt credential file causes an error rather than being replaced. Tests use fake tokens and mocked login/refresh flows; live account login has not been verified.
Model selection
bun start models: list the registered Anthropic, OpenAI, and Codex models, with credential status./models [filter]: searchable agent-model picker in the TUI; type to filter, use arrows and Enter, or Escape to cancel. Shows models with configured credentials, not a guarantee of account entitlement. Plain mode prints the catalog./model provider/model-id: select an agent model directly./modelopens the picker in the TUI or shows the selection in plain mode./compactor: separate compactor-model picker./compactor provider/model-idselects directly./thinkingand/compactor-thinking: separate thinking pickers; append a level for direct selection (e.g./thinking high). Levels:off,minimal,low,medium,high,xhigh. Pi maps these to provider-specific options; non-reasoning models may ignore them and providers may map unsupported levels.- Selections are saved globally to
~/.nut/settings.json(override withNUT_SETTINGS). Agent and compactor choices are independent. Model precedence: CLI flags >NUT_MODEL/NUT_COMPACTOR> saved settings > built-in defaults.--thinkingand--compactor-thinkingoverride the corresponding saved level for one launch without rewriting it. - Agent model/thinking changes require an idle queue; finish work or
/cancelfirst. Compactor changes affect new summary requests, not in-flight summaries. - If no settings have been saved yet, start once with authenticated
--modeland--compactorchoices, then use the pickers to save them. Subsequent launches need no model flags.
Multiple provider logins
Credentials coexist for anthropic, openai, and openai-codex. One credential per provider is supported, not multiple accounts within one provider. OAuth is the default; API-key login is available where Pi's provider supports it:
bun start login --provider openai-codex
bun start login --provider anthropic # Claude subscription OAuth
bun start login --provider openai --auth-method api_key
bun start auth-status # all providers, no token values
bun start logout --provider openai # leaves other logins intact
API-key prompts hide input. Credentials remain in the private auth directory, never the settings file. Stop chat before running auth commands (the credential store has an exclusive lock). /auth inside chat shows configured providers and login instructions. Existing environment-variable API keys still work.
Pi terminal UI
Interactive terminals open the Pi TUI by default. It uses @earendil-works/pi-tui components, not Pi coding-agent's separate session loop. The durable harness and memory tree still own all model work.
- Multiline editor with input history and bracketed paste.
- Markdown replies and working/thinking status.
- Tool cards with running, done, and error states.
- Command and file-path completion with Tab.
- Terminal scrollback, resize handling, and recent messages on startup.
| Key | Action |
|---|---|
| Enter | Send a message |
| Alt+Enter | Add a line; Shift+Enter also works in supported terminals |
| Tab | Complete a command or file path |
| Esc | Dismiss completion, or cancel active and queued work |
| Ctrl+O | Expand or collapse retained tool output |
| Ctrl+D | Quit when the editor is empty; wait for accepted work |
| Ctrl+C | Suspend saved work and restore the terminal |
File completion inserts a path; it does not attach the file. The agent can read it with read. The TUI shows thinking status, not reasoning text. Tool previews are bounded; the memory log remains the durable record.
Use bun start --plain for the previous line-based interface. Piped input and --prompt also use plain mode. The summary view is available through /view instead of filling the TUI at startup.
Terminal commands
| Command | Action |
|---|---|
/view |
Print the current summary view |
/zoom ID N |
Open a summary; N=1 returns the original message |
/date ID |
Print the local date and time of a message |
/tools |
List the tools available to the agent |
/cancel |
Cancel active and queued work; keep user input in memory |
/quit |
Stop reading input and wait for accepted turns to finish |
/help |
Print help |
Input sent during a run is saved in a durable queue. It starts a fresh turn after the current run. EOF also waits for accepted turns. Ctrl-C closes the process and suspends durable work. Restarting the same chat resumes it. Use /cancel to cancel work instead.
Available tools
| Tool | Action |
|---|---|
read |
Read text files; large output can be truncated |
write |
Create or replace a file |
edit |
Replace exact text in a file |
bash |
Run a shell command |
zoom |
Open a memory summary or original message |
date |
Get a memory message's local date and time |
Use /tools in chat or bun start tools without credentials. Web search, browser control, MCP tools, and subagents are not connected.
The tools are not sandboxed. read, write, edit, and bash run with your account's permissions. The working directory is not a security boundary.
Browse and import
These commands use the same directory lock. Stop the chat process first.
bun start view --chat /path/to/chat
bun start browse --chat /path/to/chat --output memory.html
bun start import --chat /path/to/chat --input old-notes.txt
bun start import --chat /path/to/chat --input history.jsonl
Text files become one note. JSONL imports use one record per line:
{"kind":"user","text":"Keep the original algorithm.","date":"2026-01-02T03:04:05.000Z"}
{"kind":"talk","text":"Saved it in src/algorithm.ts."}
Valid kinds: user, talk, tool, echo, note. Dates are optional. Nut assigns permanent message IDs. Import validates all records before writing. Importing the same file contents again does not duplicate messages. Changed file contents are a new import.
The HTML export contains the view, every original message, and every tree level. Links connect summaries to children and original messages. It escapes message text and does not run scripts.
Storage and recovery
chat/
lock Unix socket; one writer
main/YYYY-MM-DD.jsonl Original memory messages
tree/YYYY-MM-DD.jsonl Binary summary nodes
runtime/ pi-durable entries, tasks, queue, and checkpoints
Each memory record uses one append write followed by fsync. Invalid JSON lines are reported and skipped. A missing final newline is added before future writes. Valid records with invalid schemas, duplicate IDs, or missing message ranges stop loading; Nut does not silently renumber history.
A raw runtime entry has a stable source ID in the memory log. Startup scans raw entries, including history hidden by resets. This repairs a crash between a runtime commit and memory projection without duplicate messages or replies.
The pinned Pi JSONL backend flushes sidecars with fsync: true, but does not normally flush its main commit marker. src/runtime-storage.ts also flushes that marker before publication. Unsafe interrupted tools are not automatically replayed. Pi reports them as interrupted.
Back up the entire chat directory. Nut does not make Git commits or backups for you. The files contain private messages, tool output, and possibly credentials copied into either. They are not encrypted. This implementation does not claim host-power-loss protection beyond the file fsync behavior; directory entries are not separately fsynced.
Effect architecture
src/app.ts:AppConfig,NutModels,MemoryStore, andChatAgentservices. Layers own resources withEffect.acquireRelease. Errors are schema-backed tagged errors.src/cli.ts: Effect workflows, input streams, scoped fibers, andBunRuntime.runMain.src/tui.ts: Pi editor, Markdown transcript, and committed tool activity. Its lifetime belongs to the CLI scope.src/auth.ts: scoped Effect authentication service and browser-login terminal interface.src/credentials.ts: private, locked, atomic credential storage for Pi's OAuth callbacks.src/schema.ts: persisted record and import schemas.src/memory.ts: incremental view and binary tree. Node workers useEffect.retrywith a fixedSchedule.spaceddelay.src/compactor.ts: named Effect workflow around the model SDK. Byte-limit correction stays in one conversation.src/durable.ts: Promise adapter for Pi's durable API, fresh contexts, recovery, and memory projection.src/cache.ts: complete-line view splits and provider payload policy.
The memory engine and Pi adapter retain Promise interfaces at their integration boundary. The Effect service layer owns their lifetime. Shutdown closes the agent before releasing the memory lock.
Spec coverage and differences
Implemented:
- Append-only, word-for-word memory records, except capped ordinary tool results.
- Pure binary summaries; short sources become free nodes.
- Ordered leaf compression with summarized context and concurrent merges.
- 512-byte summary target, five size attempts, shortest result retained.
- Eight worker slots and fixed 10-second retries, with the first failure reported once per node.
- Incremental 128,000-byte view; oldest-relative-to-size merge order; no splits.
- Wait for summaries before new turns; capture the view before logging new input.
- Fresh model context per turn; stable system prompt and tools.
- Anthropic view cache marks near 50k, 80k, and 100k characters, plus automatic request-end caching.
- Tool results capped to 30,000 characters with head and tail retained.
Deliberate differences and limits:
- Reasoning:
main/andtree/never contain reasoning. Pi's separateruntime/log does store reasoning and signatures for recovery and within-turn replay. It retains old turns too. - Busy input: queued for a fresh turn, not injected at a tool boundary. Multiple queued inputs are separate turns, not one batch.
- OpenAI caching: uses implicit caching,
store: false, encrypted reasoning replay, andall_turnsreasoning context. Per-blockprompt_cache_breakpointfields are not added. Live API compatibility and cache usage still need measurement. - Full-message zoom: exempt from the tool-result cap, so large original messages remain retrievable. Built-in coding tools can apply their own output limits before Nut's cap.
- No ordinary compaction: Pi's threshold and overflow compaction are disabled. A single very long tool loop can exhaust the model context and fail. Start a new turn to use the summary view.
- Local process: no remote attachment server, optional subagents, computer tools, or automatic backups. Run in a persistent terminal session on an always-on machine if needed.
- Scale: history and tree indexes are held in RAM. Large-history performance has not been benchmarked.
- Terminal presentation: the TUI redraws content for usability. Use
--plainfor the spec's line-based interface.
Tests use faux providers. They cover tree invariants, retries, UTF-8, malformed log tails, fresh turns, tool execution, reasoning exclusion, cancellation, restart, a SIGKILL recovery window, Effect layer cleanup, private credential storage, OAuth metadata, and concurrent token refresh.
bun audit currently reports one high-severity advisory for braces in the linked Pi development-tool chain (shx → shelljs → fast-glob → micromatch → braces). Nut does not call that tool chain. No forced dependency downgrade has been applied.
The interactive footer shows current-run input/output tokens, cache reads/writes, latest agent prompt cache-hit percentage, and catalog-estimated cost (with compactor cost broken out). Totals include completed agent and summary responses, including summary length retries, and update when responses finish—not token-by-token. They reset on restart; restored history is not charged again in this display. Provider usage may be unavailable for failed/interrupted requests. Catalog estimates are not invoices or subscription quota/charges (including Codex subscriptions).
Startup context and lock errors
Interactive chat startup shows a current memory-summary table, user instructions,
working directory and tool names instead of replaying raw history. /context
refreshes this preview; /view shows just the memory table. This is the context
for a fresh turn, not an exact provider request: summaries can finish afterward,
and recovered in-flight work may retain an earlier snapshot.
The credential directory has one process owner to protect OAuth token refreshes.
If startup says Credential store is already open, close the other Nut session
before retrying. Do not delete an active lock. Flags override settings for that
run only; use /models and /compactor to save choices for flag-free startup.