virgil-cli
Virgil in your terminal. npx virgil → an AI agent that does real work on your machine — runs commands, reads/writes/edits files, fetches the web — talking to the Virgil brain over HTTPS. No account signup, no app store. Tokens (25 free, then top up) are metered server-side per device.
npm i -g virgil-cli # or: npx virgil
virgil # interactive
virgil "summarise every .md in this folder" # one-shot
Architecture
terminal (this CLI) server (:8095 behind /vx)
┌───────────────────┐ POST /vx/agent ┌────────────────────────┐
│ REPL / TUI │ {mode:"cli", │ DeepSeek + Virgil │
│ agent loop ──────┼──► messages:[…]} │ system prompt + TOOLS │
│ local tools: │◄── {assistant:{ │ token paywall / device │
│ run_shell, │ tool_calls}, │ remember/finish handled │
│ read/write/edit, │ tokens} │ here (finish = -1 token)│
│ list_dir, web │ └────────────────────────┘
└───────────────────┘
The brain, tools contract, identity and token metering already live on the server (virgil_agent_server.py, /agent with mode:"cli"). The CLI is a thin client: it runs the tool calls the brain asks for, locally, and feeds results back until the brain calls finish.
What is already built (CORE — do not modify)
src/types.ts — all shared contracts (Tool, ToolResult, ToolContext, Message, AgentResponse, VirgilConfig).
src/api.ts — agentStep(messages, cfg) and getCredits(cfg).
src/loop.ts — the agent loop and the LoopUI interface the TUI must implement.
src/index.ts — assembly + REPL orchestration + slash commands.
Deliverables
Worker A — local tools (src/tools/)
Implement each Tool (see src/types.ts) and export a registry:
import type { ToolRegistry } from "../types.js";
export const registry: ToolRegistry = { run_shell, read_file, write_file, edit_file, list_dir, web_fetch };
Tool names must match the server exactly: run_shell, read_file, write_file, edit_file, list_dir, web_fetch.
run_shell | {command, cwd?} | run via shell in cwd ?? ctx.cwd; capture stdout+stderr+exit; needsConfirm = true |
read_file | {path} | return file contents (bound length); resolve relative to ctx.cwd |
write_file | {path, content} | overwrite/create; needsConfirm = true; preview = "write: " |
edit_file | {path, old_string, new_string} | replace the unique occurrence; error if 0 or >1 matches; needsConfirm = true |
list_dir | {path} | list entries (mark dirs); resolve relative to ctx.cwd |
web_fetch | {url} | fetch, strip to readable text, bound to ~4000 chars |
Rules: return {ok, output} (never throw for expected failures — put the message in output, ok:false). Every tool result string bounded (~4000 chars). Path traversal is fine (user's own machine) but resolve relative paths against ctx.cwd. preview(args) returns one short line for the UI. run_shell/write_file/edit_file set needsConfirm → true (destructive); read_file/list_dir/web_fetch are read-only (no confirm).
Worker B — config + TUI (src/config.ts, src/tui.ts)
src/config.ts exports:
export async function loadConfig(): Promise<VirgilConfig>
export async function login(cfg: VirgilConfig): Promise<void>
export async function showCredits(cfg: VirgilConfig): Promise<void>
- Config file:
~/.virgil/config.json. On first run create { device, baseUrl, token }.
device = "d-" + 24 random hex chars.
- Defaults:
baseUrl = "https://api.nm-streetwear.space/vx", token = "virgil-ext-8528".
login: print the device id and open https://api.nm-streetwear.space/vx top-up / SumUp page in the browser (best-effort open/xdg-start/start); explain 25 free tokens, then tiers.
src/tui.ts exports:
export function makeUI(): LoopUI
export function prompt(): Promise<string | null>
export function banner(cfg: VirgilConfig, tokens: number | null): void
- Smooth like Claude Code: a spinner during
thinkingStart/Stop; assistant text rendered cleanly (basic markdown/inline code is a plus); each tool call shown compactly (e.g. ⏺ run_shell ls -la) with a dim, bounded result; confirm(prompt) shows the pending action and reads y/N (default No); noTokens prints the tiers and how to top up. Use Node built-ins + at most one tiny color lib (e.g. picocolors) — keep install light.
Conventions (both workers)
- TypeScript, ESM,
"module": "NodeNext" → import local files with the .js extension (./types.js), even though the source is .ts.
strict: true must pass. Run npm run build (tsc) — zero errors.
- Do not edit
src/types.ts, src/api.ts, src/loop.ts, src/index.ts.
- Keep dependencies minimal.