Skip to content

Repository files navigation

juno

A terminal coding agent you drive from your shell. juno streams a live LLM turn into an Ink transcript, lets the model call workspace-jailed tools, and gates every risky call behind an interactive permission prompt — built ground-up in TypeScript + React, no build step.

NodeTypeScriptReact + InktestsCIlicense

 juno v0.1.0
Claude Fable 5 (subscription) · ~/src/acme-api
/ commands · ? shortcuts
▌ add a rate-limit guard to the login handler
● I'll add a token-bucket check before the handler runs, then
wire it into the router.
⚙ edit_file src/routes/auth.ts
╭─ ⚠ permission required ───────────────────────────╮
│ edit_file risk: risky │
│ │
│ + import { rateLimit } from './mw/limit' │
│ + router.post('/login', limit(5,'1m'), │
│ - loginHandler); │
│ + loginHandler); │
│ │
│ [y] allow [a] always [d] deny [!] bypass │
╰────────────────────────────────────────────────────╯
Claude Fable 5 (subscription) · ~/src/acme-api · ctx 48.5k (5%) · medium

(Illustration of the streaming transcript and a live permission prompt — the box is rendered by Ink at runtime, colorized by risk tier.)


What it is

juno is a from-scratch reimplementation of a terminal AI coding agent — the loop that takes a user message, streams a model's thinking and tool calls into a live TUI, runs those tools against your files under an explicit permission gate, feeds the results back, and repeats until the turn ends. It is a single-runtime TypeScript + React application rendered with Ink: .ts/.tsx run directly under tsx on Node 22+ — no Python, no compile step, no cross-language surface.

What this demonstrates

The engineering worth looking at:

  • A frozen event seam. Every model backend translates its wire format into one normalized AgentEvent discriminated union (src/core/events.ts); a pure eventToAction maps each event 1:1 onto a reducer action (src/core/reducer.ts). The UI never sees a provider-specific shape — swapping backends changes nothing downstream.
  • A turn coordinator (src/agent/turnRunner.ts) that drives one submission to completion, loops on tool_use, runs each call through an executor that owns the permission round-trip, and re-enters the results — with an abort path that always settles parked permission prompts so nothing hangs.
  • A workspace-jail sandbox (src/tools/fileTools.ts): every file path is realpath-resolved against the working directory and any .. escape, absolute-out-of-root path, or symlink pointing outside is rejected before the syscall.
  • Per-tool risk classification (safe / risky / dangerous) with a headless, pure policy (src/permissions/policy.ts): reads auto-allow, writes prompt, the shell is always prompted, deny beats allow, and a dangerous call can never be satisfied by an ordinary remembered rule.
  • A multi-backend model layer behind one ModelClient interface (src/providers/) — HTTP APIs plus Claude and Codex subscription CLI transports normalized to the same stream (see below).
  • MCP integration (src/services/mcpManager.ts) — external Model Context Protocol servers discovered at startup, their tools surfaced through the same risk gate.
  • A privacy-by-default stance: the OpenRouter transport tags every request with a no-train directive (data_collection: 'deny'), and the subscription backend runs render-only with shell, network, and sub-agent tools hard-denied.

Feature highlights

  • Multi-provider model layer. One ModelClient seam, four transport families:
    • Anthropic Messages API — streaming SSE against /v1/messages (anthropicClient.ts).
    • OpenAI-compatible / OpenRouter — chat-completions streaming, OpenRouter carrying the no-train directive (openaiCompatClient.ts).
    • Subscription CLI seam — spawns the claude CLI headless (claude -p --output-format stream-json) on the logged-in Max subscription (no API key), translating its NDJSON into the sameAgentEvent stream, with server-side session reuse across turns (claudeCliClient.ts).
    • Codex subscription seam — spawns codex exec --json, preserves resumable thread ids, classifies stalls and context overflows, and can bridge Juno-managed subagents and MCP tools under the same permission policy (codexCliClient.ts).
  • Streaming TUI. Assistant text and extended-thinking stream token-by-token; finished turns commit into an Ink <Static> region so they are never redrawn. A model picker, slash-command palette, and a responsive status strip (model, cwd, context-window gauge, effort, cost) round it out.
  • Observatory orchestration workspace. Press Down from an empty composer or run /agents to enter a dedicated alternate-screen surface: a responsive agent rail beside the selected agent's ordered prose, reasoning, tool, steering, permission, and lifecycle stream. Narrow terminals use a one-pane drill-in; available steer, cancel, and permission actions appear only when the live runner can actually perform them.
  • Risk-tiered tool approval. An interactive prompt shows the tool, its risk tint, and a colorized unified diff for file writes; y/a/d decide, ! is an explicit dangerous bypass, and always-allow patterns are remembered.
  • Tool suite. Five workspace-jailed file tools (read_file, list_files, grep, write_file, edit_file), an on-demand skill loader, a depth-limited spawn_subagent, a dangerous-tier run_shell, a preset-bound parent-only run_verification, a bounded session-memory tier, and any MCP server tools — assembled per session.
  • Sessions & resume. Committed turns persist to ~/.config/juno/sessions/ (JSON snapshot + append-only JSONL log); a /resume palette lists past sessions newest-first and rehydrates the transcript.
  • Durable bounded background agents. Detached children persist task state and write-through output, queue FIFO above the concurrency cap, surface elapsed and last-activity timing, and reconcile unfinished work honestly after a restart.

Architecture at a glance

 provider adapters normalized stream UI
───────────────── ───────────────── ──
anthropicClient ┐
openaiCompat ├─► AgentEvent ─► turnRunner ─► reducer ─► Ink <Static>
claudeCliClient ┘ (events.ts) (loops on (state) transcript
mcp tools ───────┘ tool_use) ─► executor ─► permission gate

Quickstart

Requires Node.js 22+ (the package is ESM-only).

npm install
npm start # launch the TUI (tsx src/cli.ts)
npm run dev # launch with file-watch reload
npm start -- --cwd ./my-project # pin tools and providers to one project root

--help / --version go through tsx directly:

npx tsx src/cli.ts --help

The default backend reuses a logged-in claude CLI session and needs no API key. Codex subscription models require an installed, logged-in codex CLI. HTTP transports read ANTHROPIC_API_KEY, OPENAI_API_KEY, or OPENROUTER_API_KEY at call time. Pick a model in the TUI or with JUNO_MODEL=<id> npm start.

Interactive launches open with a short, skippable ASCII orbit sequence after startup is genuinely ready. Press any key to enter immediately, or set JUNO_NO_INTRO=1 to disable it.

Launch from the project directory you want Juno to control, or pass --cwd explicitly. The path is canonicalized before startup and becomes the shared root for providers, native tools, agents, hooks, and verification. Choosing a broad directory such as ~/src intentionally places its sibling projects in scope.

Codex-managed Juno tools

Codex can reach Juno's managed process sessions and structured verification through the in-process MCP bridge. Because codex exec is headless, it cannot pause an MCP call for Juno's interactive permission overlay. Safe calls continue to follow the normal policy; calls that would prompt fail closed unless their exact tool name is explicitly preauthorized:

JUNO_CODEX_BRIDGE_ALLOW=start_process,poll_process,write_process_stdin,terminate_process,run_verification \
juno --cwd ./my-project

The allowlist accepts exact names only—unknown names and wildcards are ignored—and never overrides a configured Juno deny rule. Grant only the capabilities needed for the session. JUNO_CODEX_SPAWN_BRIDGE=1 separately enables Juno subagents for a Codex parent; a managed-tool grant does not silently enable delegation.

Configuration

Settings resolve built-in defaults → ~/.config/juno/config.json → environment variables (last wins). External MCP servers are registered under mcpServers, keyed by id; each tool's risk is classified per tool so only tools you deliberately mark safe are auto-allowed:

{
"defaultModel": "claude-fable-5",
"backgroundAgentMaxConcurrent": 3,
"backgroundAgentTimeoutMs": 1800000,
"codexIdleTimeoutMs": 180000,
"codexStaleStreamMs": 300000,
"mcpServers": {
"docs": {
"command": ["my-docs-mcp", "--stdio"],
"toolRisk": { "search_docs": "safe", "get_doc": "safe" }
}
}
}

Here mcp__docs__search_docs and mcp__docs__get_doc auto-allow (reads), while any write tool the server exposes falls through to the prompt-gated default. Extra background agents remain visibly queued until a slot opens; an executing child that exceeds its wall-clock limit is aborted and settled as an error. The Codex values are silence guards, not per-item command deadlines. Corresponding env overrides are JUNO_BACKGROUND_AGENT_MAX_CONCURRENT, JUNO_BACKGROUND_AGENT_TIMEOUT_MS, JUNO_CODEX_IDLE_TIMEOUT_MS, and JUNO_CODEX_STALE_STREAM_MS; the common overrides remain JUNO_PROVIDER, JUNO_MODEL, JUNO_CWD, and JUNO_MAX_CONTEXT.

maxToolCalls is an optional per-turn iteration guard. It is intentionally unset by default because subscription and raw-API workloads have different budgets; set a positive integer in config.json or JUNO_MAX_TOOL_CALLS=<n> when a hard tool-loop ceiling is appropriate.

Diagnostic traces and replay

Session tracing is deliberately off by default. Enable it with "trace": true in config.json or JUNO_TRACE=1. Juno then writes versioned NDJSON under ~/.config/juno/traces/, at the single reducer dispatch funnel. Each line carries a monotonic sequence, timestamp, session id, turn id, and the exact action shape accepted by the reducer.

Raw user prompts are replaced with their character count. Tool arguments/results are depth-, collection-, and string-bounded, and secret-looking keys are redacted; resumed transcripts are not copied into traces. Model output remains diagnostic content but is string-bounded, so treat the directory as private user data. Serialization and append I/O run behind a bounded asynchronous queue; tracing is fail-soft and never changes a turn outcome. Graceful shutdown and session changes flush/close their recorder. Startup retains the 20 newest .ndjson files (including the new session); hard process termination can leave only the final line incomplete, which the line-oriented reader reports without hiding other records.

replayTraceFile / replayTraceNdjson in src/services/sessionTrace.ts provide the first executable replay seam: records are validated and folded through the pure reducer. Issues are classified as trace (NDJSON/envelope/ordering), action (unknown action version), or reducer (application failure). Full provider/tool selftest playback is intentionally follow-up work; this seam replays state evolution without performing model calls, tools, permissions, or terminal rendering.

Testing

npm test# vitest, run once
npm run typecheck # tsc --noEmit (strict)
npm run verify:polish # 16 responsive Observatory frames + focused tests + real-PTY selftest

The suite covers the reducer, permission policy, provider adapters, workspace jail, Ink components, and end-to-end PTY behavior. The polish gate writes inspectable frames and a machine-readable report under .polish/; the PTY lane writes its real terminal framebuffer and scrollback evidence under .selftest/.

How it was built

juno was produced by an autonomous multi-agent build system — a "forge" that planned the port, decomposed it into sealed seams, and had agents implement, test, and review each one against a frozen contract. The tight event/reducer seams and the exhaustively-tested contracts throughout are a direct consequence of that build discipline.

Status

Active development. The architecture and test coverage are mature; the surface is still moving (new providers and tools land regularly), and the version reflects that (0.1.0).

License

MIT © 2026 Aiden Angel

About

Terminal AI agent harness built from scratch in TypeScript + React/Ink — multi-provider model layer (Anthropic, OpenAI-compatible, subscription CLIs), MCP client, permission-gated tools, streaming TUI

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages