An AI agent command-line interface written in TypeScript
Multi-model LLM access · streaming terminal UI · tool calling · permission control · session persistence
English | 简体中文
Features: streaming REPL with slash commands (+ autocomplete) · OpenAI / Anthropic / OpenAI-compatible providers with an interactive /connect onboarding wizard · built-in fs / bash / web tools with a permission gate · git integration (/commit drafts Conventional Commits messages, /diff shows a colored working-tree diff, repo status sent as a per-turn request-scoped reminder so it never breaks the prompt cache) · prompt caching on Anthropic providers (cache breakpoints on the stable prefix and the history tail; OpenAI-style protocols cache server-side automatically) with a status-bar hit rate · token thrift: re-reading an unchanged file answers with a one-line note, and auxiliary calls (summaries, titles) can run on a cheaper smallModel · plan mode with read-only research and plan approval · thinking spinner with dim reasoning preview · diff preview on write/edit approval · @file mentions · !cmd shell passthrough · custom slash commands from Markdown files · conversation compaction (/compact) · session persistence and resume with auto-generated titles (/resume, star -r) · subagent delegation for focused subtasks · lifecycle hooks (PreToolUse/PostToolUse/Stop shell commands from config) · file-write snapshots with /undo and checkpoint rollback with /rewind · TODO task tracking · background shell tasks with status-bar visibility (/tasks) · terminal bell on long turns and background-task completion · Markdown session export (/export) · /init + /doctor project scaffolding and environment checks · cost estimation · update notifier · --json NDJSON output for scripting.
- Node.js >= 20
- pnpm
npm install -g @cryer/star-cli
star # interactive REPL
star -p "hi" # non-interactive print modepnpm install
pnpm build
npm link # one-time: registers the `star` command globally
star # interactive REPL
star -p "hi" # non-interactive print modeWithout npm link you can run the bundle directly: node dist/main.js (re-run pnpm build after code changes; the linked star always points at dist/).
Config file: ~/.star-cli/config.toml (set the STAR_HOME environment variable to use a different home than ~/.star-cli; project-level override: .star/config.toml in cwd, sandboxed to safe keys only; CLI flags win over both).
defaultModel = "gpt" # name of the [[models]] block used by default
# smallModel = "haiku" # optional [[models]] block for cheap auxiliary calls
# (compaction summaries, session titles, completion checks);
# unset = the active main model does them
permissionMode = "ask" # ask | auto | readonly | yolo | plan
contextMaxTokens = 100000 # context window in tokens (per-model override below)
# compactThresholdTokens = 80000 # optional auto-compaction trigger in tokens; defaults to the
# context window and is clamped to it (top-level only — there is
# no per-model override). /compact always compacts, regardless.
# Max model round-trips ("steps") per turn between progress checkpoints — one
# step is a reply plus the tool calls it asked for; retries and auto-continue
# nudges don't count. Reaching the cap while tools still execute resets it
# with a notice instead of stopping the turn; 0 disables the cap entirely.
maxSteps = 100
contextCompaction = "summary" # summary | truncate — how over-budget history is compacted
streamMaxRetries = 5 # extra attempts per model request on transient failures
# (network error, 429/5xx, idle cutoff, empty reply); 0 disables
streamIdleTimeoutSec = 60 # seconds with no stream output before a stalled response is
# ended gracefully (some relays never close the stream)
streamFirstChunkTimeoutSec = 300 # seconds to wait for the very first content part — slow
# thinking-model endpoints can stay silent for minutes
maxAutoContinues = 2 # nudges for consecutive unproductive text-only replies before
# the turn is handed back; tool calls reset the count; 0 disables
doomLoopThreshold = 3 # consecutive identical tool calls before the loop refuses the
# repeat and tells the model to change approach; 0 disables
gitSnapshots = true # track the whole working tree each turn (internal repo outside
# your .git) so /undo and /redo also cover bash-made changes
# sessionBudgetUsd = 5 # optional per-session cost cap in USD; unset = no cap
notifyBell = true # ring the terminal bell when a long turn finishes (REPL only)
notifyBellThresholdSec = 10 # turns shorter than this stay silent
[permissions]
# persistent allow-rules, written automatically when you pick "a" (always) on a permission prompt.
# Syntax: <tool> or <tool(<pattern>)>, * is a glob wildcard. A bash rule is matched per command
# segment (the command line is split on shell separators): an allow rule must match EVERY segment,
# and any command substitution or file redirection disables auto-allow entirely.
allow = ["bash(npm test)", "read_file"]
# persistent deny-rules — checked after the hard safety rules; a deny matching ANY segment beats
# auto mode and allow-rules (yolo still bypasses everything)
deny = ["bash(rm -rf *)"]
[[providers]]
name = "openai"
protocol = "openai-compatible" # openai-compatible | anthropic | openai-responses
baseURL = "https://api.openai.com/v1"
apiKeyEnv = "OPENAI_API_KEY" # recommended: name of the env var holding the key — keys live in
# ~/.star-cli/.env (written by /connect), never in this file
# apiKey = "sk-..." # inline key instead of apiKeyEnv (not recommended)
# headers = { "X-Title" = "star-cli" } # extra HTTP headers sent with every request
# protocol = "openai-responses" # for relays exposing only /v1/responses
[[models]]
name = "gpt" # referenced by defaultModel and -m
provider = "openai" # name of a [[providers]] block
model = "gpt-4o" # model id sent to the API
# maxTokens = 8192 # cap on generated tokens per reply
# contextMaxTokens = 272000 # per-model context window — overrides the top-level
# contextMaxTokens for compaction and the ctx % in the status bar
# temperature = 1 # sampling temperature forwarded on every request. Unset = the AI
# SDK default applies (ai@4 sends 0 — it does NOT omit the field),
# so endpoints that mandate one explicit value (kimi-for-coding/k3
# accept only 1) MUST set it here.
# reasoningEffort = "high" # reasoning intensity for thinking models, forwarded verbatim as
# reasoning_effort on every request. Level naming varies by
# provider (low/medium/high are common; some add minimal/max...).
# Unset = server default. Ignored for anthropic. The /model
# picker also offers these levels after a model pick.
# vision = false # mark a text-only model: read_image/screenshot then decline with
# a text error instead of attaching images the endpoint would
# reject. Unset = images allowed. (Without it, a 4xx rejecting
# images outright still self-heals: the images are stripped from
# the history and the request retried once.)
# streamIdleTimeoutSec = 60 # per-model overrides of the global stream watchdog timeouts
# streamFirstChunkTimeoutSec = 600
# optional per-model pricing in USD per 1M tokens — enables the $ estimate in /cost and /usage.
# promptPrice prices input tokens (system prompt, history, @file contents, tool results — resent
# every turn, so the bulk of usage); completionPrice prices output tokens (the model's replies and
# tool calls — less volume, usually the pricier rate). BOTH fields are required together.
promptPrice = 2.5
completionPrice = 10
# cacheReadPrice = 0.25 # optional price for cache-read prompt tokens; unset = OpenAI-style
# cached tokens bill at promptPrice and Anthropic-style cache
# reads are not billed
[[providers]]
name = "claude"
protocol = "anthropic"
baseURL = "https://api.anthropic.com"
apiKeyEnv = "ANTHROPIC_API_KEY"
[[models]]
name = "sonnet"
provider = "claude"
model = "claude-sonnet-4-20250514"
[[hooks]]
event = "PostToolUse" # PreToolUse | PostToolUse | Stop
matcher = "edit_file|write_file" # optional regex on the tool name; matches all tools when omitted
command = "biome check --write ." # PreToolUse exiting with code 2 blocks the tool (stderr
# becomes its result); Stop fires on every turn end
# timeoutSec = 30 # optional per-hook timeout (default 30; on timeout the hook
# is allowed through with a warning)API keys resolve from the environment variable first (apiKeyEnv), then the apiKey field in the config file. On startup Star CLI also loads ~/.star-cli/.env (dotenv-style KEY=VALUE lines) into the environment without overwriting variables that are already set — this is where /connect stores the keys it collects, so config.toml only ever references the variable name, never the key itself.
The fastest way to set up a provider is the /connect wizard inside the REPL: pick a preset (OpenAI, Anthropic, Kimi/Moonshot, DeepSeek) or a custom endpoint, paste the API key (masked while typing), name a model, and it appends the [[providers]]/[[models]] blocks to config.toml (existing content and comments preserved), writes the key to ~/.star-cli/.env (as STAR_API_KEY_<NAME>, file mode 600 where the platform honors it), and optionally makes the new model the default — all without restarting. The written [[models]] block comes with contextMaxTokens = 128000 and temperature = 1 (a safe default — coding models like kimi-for-coding/k3 reject any other value; lower it only for models that allow it) plus zero placeholder prices — edit all three to the real limits and rates as needed.
Third-party relays usually front one of the standard wire formats: set protocol to openai-responses when the relay exposes the Responses API (/responses), otherwise openai-compatible (/chat/completions).
Many relays buffer a whole generation before flushing — tens of seconds to minutes can pass without a single SSE chunk, which looks like a dead stream. Star CLI timestamps the raw response bytes of every request: as long as bytes keep flowing (including SSE heartbeat comments the SDK itself swallows) the stream watchdog is extended instead of cutting the relay off mid-generation, bounded by a 10-minute cap on total part silence so a heartbeat-only zombie can't hang a turn. If a relay still triggers frequent idle timeouts, raise the model's own watchdogs:
[[providers]]
name = "kimi"
protocol = "openai-responses"
baseURL = "https://api.kimi.com/coding/v1" # official Kimi coding endpoint
apiKeyEnv = "STAR_API_KEY_KIMI"
[[models]]
name = "kimi"
provider = "kimi"
model = "kimi-for-coding"
temperature = 1 # mandatory here — the endpoint rejects any other value
contextMaxTokens = 128000
# raise these when the relay buffers long generations:
# streamIdleTimeoutSec = 120 # default 60
# streamFirstChunkTimeoutSec = 600 # default 300STAR_HOME— overrides~/.star-clias the data home (config, sessions,.env, memory, debug log).STAR_DEBUG— any non-empty value enables stream diagnostics appended to~/.star-cli/debug.log, one JSON object per line.STAR_NO_NOTIFY=1— disables the terminal bell without touching the config.
star start the interactive REPL
star -p "prompt" non-interactive print mode (pipe-friendly)
star -p "prompt" --image x.png attach an image (png/jpg/jpeg/gif/webp, max 5MB; repeatable; anything past 2000px on the longest side is downsampled)
star -p "prompt" --json NDJSON event stream on stdout (text/tool/usage/error lines)
star -m gpt pick a model
star --permission-mode auto ask | auto | readonly | yolo | plan
star -r <sessionId> resume a previous session (full or short id)
star -r list sessions for the current directory
star -c continue the most recent session for the current directory
star --clear-sessions delete stored sessions for the current directory
star --clear-sessions all delete every stored session
| Command | Description |
|---|---|
/help |
list commands, grouped by category |
/model [name] |
switch models — no argument opens an interactive picker (context window, pricing, current marker) |
/resume [id|--all] |
resume a session by id — no argument opens an interactive picker of recent sessions (message count, relative time, first-message preview; --all: every directory, with cwd shown) |
/fork |
branch the current session into a copy and switch to it — explore another direction without losing the original |
/search <query> |
full-text search across every stored session (messages, tool calls and results), with snippets — resume a hit with /resume <id> |
/new |
start a new session with a clean context (the old session stays on disk) |
/clear-sessions [--all] |
delete stored sessions: this directory by default, every session with --all (the current session is kept) |
/todo |
show TODO list |
/tasks |
list background tasks (id, status, runtime, exit code) |
/cost |
show API token usage and estimated $ cost (needs per-model pricing in config) |
/usage |
token usage dashboard across all sessions: totals, per-day bar chart, per-model breakdown with $ estimate |
/config |
show resolved config |
/permission [mode] |
set the permission mode — no argument opens an interactive picker; ask / auto / readonly / yolo are saved to config, plan stays session-only |
/connect |
interactive provider onboarding wizard: pick a preset or custom endpoint, paste the API key (masked), name a model — appends [[providers]]/[[models]] to the config, stores the key in ~/.star-cli/.env (never in config.toml), and can set the new model as default + switch to it immediately |
/plan |
toggle plan mode: read-only research, then approve the generated plan before it executes (session-only) |
/memory [add <text>] |
show the long-term memory file (~/.star-cli/MEMORY.md, injected into every session's system prompt), or append a line to it — see Long-term memory |
/compact |
compact conversation history now — always runs, even below the auto-compact threshold |
/export [path] |
export the current session to a Markdown file |
/undo |
undo the last conversation turn: revert its file changes (write_file/edit_file) and retract its messages — earlier turns are never touched (previews the message count and per-file revert diffs, then asks for confirmation) |
/rewind [n] |
list file-change checkpoints, or rewind to just before checkpoint n: restore every file changed since then and retract the matching conversation messages (asks for confirmation first) |
/init [force] |
scan the project and generate an AGENTS.md (LLM-polished when a model is available) |
/doctor |
environment self-check (Node, shell, config, API key status, sessions dir writability) |
/skills |
list available skills (project scope overrides user scope) |
/commit [instructions] |
analyze uncommitted changes and let the agent stage + commit them with a Conventional Commits message (git add/commit runs through the normal permission gate) |
/diff |
show uncommitted changes client-side: git status --short plus a colored staged/unstaged diff (no model call; huge diffs are truncated at 2000 lines) |
/copy [all] |
copy the last assistant reply to the clipboard (all: the whole conversation as plain text) |
/clear |
clear the screen |
/exit |
quit |
/q |
quit (alias of /exit) |
Keys: ESC / Ctrl+C interrupts the current stream — the partial reply generated so far stays on screen (and in the session history) with a dim [interrupted] marker; on a permission prompt: y allow, n deny, a always allow — the generated allow-rule (e.g. bash(npm test)) is saved to permissions.allow in the config file and survives restarts. Write/edit prompts include a colored diff preview of the pending change. Shift+Tab cycles the permission mode for the session (ask → auto → readonly → plan; not saved to config). Input editing: arrow keys move the cursor, Ctrl+A/Ctrl+E jump to start/end, Ctrl+U/Ctrl+K delete before/after the cursor, Ctrl+W deletes the previous word, up/down recall history (persisted to ~/.star-cli/history across sessions, capped at 50 entries, consecutive duplicates collapsed, slash commands and key-repeat garbage never recorded); while a recalled entry is unmodified up/down keep switching entries, but once you edit the text — or are typing your own multi-line input — up/down move the cursor between lines instead. Ctrl+R starts a reverse history search: type to filter (case-insensitive, newest match first), Ctrl+R/↑ for older matches, ↓ for newer, Enter to accept the match into the input, ESC/Ctrl+C/Ctrl+G to cancel and restore what you had. Multi-line input: use Ctrl+J (or Alt+Enter), or end the line with \ and press Enter — pasted multi-line text is kept intact. Pasting a large block (10+ lines or 500+ characters in one go) collapses it into a dim [pasted #N: L lines] placeholder so the input box stays compact; the full text is restored into what gets submitted, and Backspace removes the placeholder as a unit. A huge paste that the terminal delivers in several chunks is reassembled (via bracketed paste where the terminal supports it) and still collapses into a single placeholder. Typing / shows slash-command suggestions with their usage — prefix matches first, then fuzzy subsequence matches; ↑/↓ to highlight, Tab (or → at end of input) to complete, ESC to dismiss, and an unknown command name gets a Did you mean: … hint. Typing @ completes file paths relative to the working directory with the same keys; directories end in / so Tab descends, and node_modules, .git, dist and .starignore entries are skipped. Alt+V pastes an image from the clipboard as an attachment (each one shows as [image attached: clipboard.png] above the input; uses PowerShell on Windows, pngpaste/osascript on macOS, xclip on Linux). Images larger than 2000px on the longest side are automatically downsampled to fit (keeping the aspect ratio, via platform-native tools — no extra installs on Windows/macOS; ImageMagick or ffmpeg on Linux); when resizing isn't possible the original is kept and a warning is shown. Ctrl+V also works where the terminal passes it through — Windows Terminal binds Ctrl+V to its own paste, so use Alt+V there. While a turn is streaming you can keep typing: submitted prompts (and ! bangs) queue up as dimmed entries and auto-send in order once the turn finishes; ESC during a turn aborts it and clears the queue (the partial reply is kept, marked [interrupted]). Pressing ESC twice within half a second while idle retracts your last prompt and restores its text into the input for editing.
The status bar shows the working directory (full path on wide terminals), the git branch, the model, the permission mode, context usage as a percentage of the model's context window (its own contextMaxTokens when set, else the top-level one), the session prompt-cache hit rate when the provider reports cache usage (cache: 42%, or cache: Not provided when it never does), the session cost when the model has pricing configured, and total tokens. Model replies render as markdown (headings, emphasis, code blocks, tables) directly in the terminal. Decorative icons use emoji on terminals known to render them double-width (Windows Terminal, VS Code, Kitty, …) and fall back to plain symbols elsewhere so the display never desyncs; force a style with STAR_ICONS=emoji|plain.
Prefix input with ! to run a command locally without involving the model:
!git status
Output renders as a tool card and is injected into the conversation so the model can see it afterwards. Dangerous commands are still blocked, and ESC / Ctrl+C aborts execution.
The model can run long shell commands in the background via bash with run_in_background: true (same permission gate as foreground commands). While anything runs in the background, the status bar shows bg: N; when a task finishes, fails, times out, or is stopped, a system message reports the outcome. /tasks lists every task with status, runtime, and exit code, and the model can inspect or stop tasks with the task_list / task_output / task_kill tools. Remaining tasks are killed when the REPL exits.
The REPL rings the terminal bell (BEL) so you can switch windows while the agent works: once when a turn finishes after taking longer than notifyBellThresholdSec (default 10s), and once whenever a background task completes — the moment you are least likely to be watching. It only rings on a TTY, never for interrupted (ESC / Ctrl+C) turns, and never in print mode (-p). Disable it with notifyBell = false in the config or STAR_NO_NOTIFY=1 in the environment.
Setting sessionBudgetUsd in the config caps spend per REPL session (the active model needs promptPrice/completionPrice so the cost can be computed). When a finished turn pushes the session cost past 80% of the cap, a one-time warning appears; past 100%, further prompts are blocked with an error while slash commands keep working — raise the limit in the config or start fresh with /new.
Plan mode (/plan, Shift+Tab cycling, or --permission-mode plan) makes the agent research read-only before touching anything: the model only sees read-level tools (read_file / glob / grep / web_* / todo …), write/exec tools are hidden entirely, and the system prompt instructs it to end with a concrete step-by-step plan. When the plan is ready, an approval prompt appears — y restores the previous permission mode and tells the agent to execute the plan, n / ESC stays in plan mode so you can keep refining. Plan mode is session-scoped and never written to the config file; /plan again toggles back.
[[hooks]] entries in the config run your own shell commands at agent lifecycle points (a simplified take on Claude Code hooks):
[[hooks]]
event = "PreToolUse" # before a tool runs
matcher = "edit_file|write_file" # optional regex on the tool name; omit to match every tool
command = "node scripts/check.js"
[[hooks]]
event = "Stop" # once per finished turn
command = "notify-send 'turn done'"- PreToolUse runs before the tool executes. Exit code
0lets it run; exit code2blocks it and the hook's stderr is returned to the model as the tool result; any other non-zero exit lets the tool run and shows stderr as a warning (a system message in the REPL,[hook] …on stderr in print mode). - PostToolUse runs after a tool succeeds (never after an error result). A non-zero exit only produces a warning — nothing is blocked. Typical use: formatters and lint autofix (
biome check --write .). - Stop runs once when a turn finishes (after the final assistant reply).
matcheris ignored here since there is no tool; failures only warn.
Hook processes run in the working directory with a timeout (30s default, timeoutSec per hook) and receive STAR_HOOK_EVENT, STAR_CWD, STAR_SESSION_ID, plus STAR_TOOL_NAME and STAR_TOOL_INPUT (JSON of the tool arguments) for tool events. A timed-out hook is treated as failed — for PreToolUse that means allow-with-warning, so a stuck hook can never lock the agent.
Security: hooks are commands you configured, so they run in every permission mode and do not go through the permission gate — treat the config file as trusted code. Hooks only fire for events that actually happen: in readonly/plan mode write/exec tools never run, so their PreToolUse/PostToolUse hooks never fire either. A failing hook can never crash the agent.
Inside a git repository, the agent's system prompt automatically carries a short git context block — current branch, number of uncommitted files, and the last 3 commits — refreshed at the start of every turn (REPL and print mode alike; any git failure is silently ignored).
Two slash commands build on top of it:
/commit [instructions]collectsgit status, the staged + unstaged diff (truncated to 2000 lines), and the last 5 commits, then asks the agent to draft a Conventional Commits message matching the repo's history and rungit add/git commitvia the bash tool — so the usual permission prompt still applies. Outside a repo or with a clean tree it just says so without calling the model./diffis purely client-side: it rendersgit status --shortand the staged/unstaged diff with the same colors as the write/edit approval preview, truncated at 2000 lines when larger.
Drop Markdown files into .star/commands/<name>.md (project) or ~/.star-cli/commands/<name>.md (global) to define your own commands — /review src/ then sends the file's contents (with $ARGUMENTS replaced by your arguments) to the model as a prompt. An optional first line <!-- description: does a thing --> sets the description shown in /help and autocomplete. Names must match [a-z0-9-]+; built-in commands win on conflicts.
<!-- description: review code for issues -->
Review the following code and list concrete issues: $ARGUMENTSThe REPL also checks npm for a newer release on startup (async, non-blocking; disable with STAR_NO_UPDATE_CHECK=1).
Skills are reusable instruction packs the agent loads on demand. Drop a SKILL.md into .star/skills/<name>/ (project) or ~/.star-cli/skills/<name>/ (user); project skills override user skills of the same name. Each SKILL.md starts with frontmatter (name optional, defaults to the directory name; description required), followed by the instructions body:
---
description: Review code for common issues
---
Check error handling, naming, and test coverage. Report concrete findings with file:line references.Available skills are listed in the system prompt (name + description only, so they cost almost no tokens until used). When a request matches one, the agent calls the read-level skill tool to load the full body — supporting files next to the SKILL.md (scripts, templates, examples) are resolved relative to the skill's directory. Skills added mid-session are picked up on the next turn; /skills lists what's currently visible. Bodies are capped at 32KB.
~/.star-cli/MEMORY.md is user-curated long-term memory, injected into the system prompt at the start of every turn (next to the project AGENTS.md, capped at 32KB) so preferences and conventions survive across sessions and projects. It is only ever written when you say so: /memory add <text> appends a bullet, and the agent's remember tool (write permission level, so it goes through the permission gate in ask mode) is contractually restricted to explicit "remember this" requests — nothing is recorded autonomously. /memory with no argument shows the current content and path; editing the file directly works too, and changes are picked up on the next turn.
Prefix a path with @ in any prompt to attach its content (REPL and print mode alike):
star -p "summarize @README.md and @src/main.tsx"
Image files (@screenshot.png — png, jpg, jpeg, gif, webp, up to 5MB) are sent to the model as image input instead of inlined text, in both the REPL and print mode; anything over 2000px on the longest side is downsampled first. In print mode you can also attach images with the repeatable --image <path> flag:
star -p "what's wrong in this UI?" --image screenshot.png --image mockup.jpg
Invalid --image paths (unsupported type, missing, unreadable, or oversized) abort print mode with a non-zero exit.
If a provider still rejects a request because an image is too large, the agent automatically removes the oversized images from the conversation (replaced with a [image removed: too large for the model] placeholder, also in the stored session) and retries the request once, with a notice in the UI.
A directory mention (@src/agent/, with or without the trailing slash) inlines an indented tree of the directory instead of file contents — handy for giving the model project structure without pasting tree output. Listings honor .starignore and skip sensitive files, and are capped at 200 entries / 10 levels deep with a ... (truncated, N more entries) marker when larger.
Missing, binary, oversized (>100KB for text, >5MB for images), or sensitive files (.env, private keys) are skipped with a note. The chat history keeps your original @path text, so resumed sessions don't carry the injected bulk.
A .starignore file in the working directory hides paths from the agent: the glob and grep tools skip matching files (ignored directories are pruned entirely), and @file completion filters them too. Syntax mirrors .gitignore basics — one pattern per line, # comments, bare patterns match basenames (*.log), patterns with / match relative paths (build/**), a trailing / marks directory-only (private/). Negation (!) is not supported, and an explicitly requested file (e.g. grep with an exact path) is still read. Changes are picked up immediately.
read_file, read_image (attach an image file to the conversation mid-session — screenshots, mockups; png/jpg/gif/webp, auto-downsampled past 2000px), screenshot (capture the primary display and see it — visual verification of a running program, e.g. a dev server's rendered page; platform-native capture with no dependencies, DPI-aware on Windows so scaled displays come out pixel-exact, headless sessions get a clear error), write_file, edit_file, glob, grep, bash, web_fetch, web_search (DuckDuckGo, no API key needed), todo_read, todo_write, task_list, task_output, task_kill, subagent, skill, remember — each declares a permission level (read / write / exec) enforced by the permission gate. Hard safety rules (dangerous shell commands, paths outside the working directory, secret files like .env / private keys) are denied in ask / auto / readonly and cannot be overridden by allow-rules.
The subagent tool (exec level, so it is hidden in plan mode and denied in readonly) spawns a child agent loop with the same built-in tools to handle a focused, self-contained subtask — research, exploration, or an isolated change — and returns the child's final report as the tool result. Subagents run one level deep (a subagent cannot spawn further subagents), share the parent's permission mode and confirmation prompt, and their conversation is not persisted to the session.
Permission modes: ask (reads allowed, writes/exec ask) · auto (everything allowed except the hard-denied rules above) · readonly (read-only) · yolo (allow everything, never ask — all safety checks disabled, use at your own risk) · plan (read-only research with a plan-approval flow, see above — session-only). Switch at runtime with /permission (persisted to the config file), per session with /plan or Shift+Tab, or at startup with --permission-mode. On top of the modes, the [permissions] config block holds persistent allow and deny rule lists (same bash(npm test *) syntax); deny rules are evaluated right after the hard safety rules and beat both auto mode and allow-rules, while yolo bypasses everything.
Every successful write_file / edit_file first snapshots the file's previous content (in-memory, last 50 writes per session); /undo reverts the last turn's snapshots — restoring old content, or deleting files the turn created — but only after showing a preview of the retraction and per-file revert diffs and getting a y/n confirmation.
Each snapshot is also a numbered checkpoint: /rewind lists every file change of the session (id, time, tool, file), and /rewind <n> rolls the session back to just before checkpoint n — every file changed since then is restored in reverse order (files created in the meantime are deleted), the conversation is retracted to the turn that made the change, and the undo stack drops the rewound entries. Because this is destructive, the command first shows how many file changes and messages will be affected and asks for y/n confirmation.
Checkpoints are persisted under ~/.star-cli/sessions/<id>/checkpoints/ (an index.json plus one content file per checkpoint, created lazily on the first file write), so rewinding still works after resuming a session with /resume or star -r. /undo stays the fine-grained counterpart: it only ever touches the last turn.
Sessions persist under ~/.star-cli/sessions/<id>/ (messages as JSONL + meta.json). /resume with no argument opens an interactive picker of this directory's sessions (most recently active first, message counts, relative times, a preview of the first message, and a current marker on the live session; /resume --all covers every directory with each session's cwd shown), while /resume <id> and star -r <id> jump straight to one — both accept the short id shown in the list. /fork branches the live session: every message is copied into a new session titled Fork of … which becomes the active one, so you can explore a different direction while the original stays intact (checkpoints and rewind history are not carried over). /search <query> full-text-searches every stored session — message text plus tool-call arguments and results — and lists matching sessions with a snippet, ready for /resume <id>. star -c (--continue) jumps straight back into the most recently active session for the current directory, in the REPL and in print mode alike; when the directory has no sessions it says so and starts a fresh one. -r and -c are mutually exclusive, and a bare star -r prints the session list instead of erroring. Sessions are created lazily — opening the REPL and exiting without chatting leaves nothing on disk, and print mode (-p) doesn't create a session unless resuming with -r or -c. Token usage is accumulated in meta.json, so /cost reflects resumed history too. /usage is the global counterpart of /cost (which stays session-scoped): it aggregates every session's meta.json across all directories into a dashboard — grand totals, a per-day bar chart of the last 14 days with usage, and a per-model breakdown with a $ estimate for models that have pricing configured (models without pricing are listed but excluded from the total). Per-day numbers come from a usageByDay bucket map recorded from now on; usage recorded before that field existed only has grand totals and shows up as a single "earlier usage" line. /new abandons the in-memory conversation and attaches a fresh lazy session store (the old session stays on disk); deletion is a separate explicit action — /clear-sessions removes the sessions recorded for the current directory, /clear-sessions --all wipes every session directory (including ones with an unreadable meta.json), and both keep the live session; star --clear-sessions [all] does the same non-interactively and exits.
After the first turn of a session (the first user message gets its first assistant reply), a lightweight background LLM request — same model, capped at 20 output tokens — summarizes the first user message into a short title (≤50 chars, matching the message's language) and stores it in meta.json, where the /resume list picks it up. Title generation is fire-and-forget: it never blocks the REPL or print-mode output, it runs only when the session has no title yet (resumed sessions keep their existing title; untitled older sessions get one after their next turn), and if the request fails the title simply stays empty — untitled sessions show as (无标题) in the list. An unfinished request is abandoned when the process exits. It applies wherever a session is attached, including print mode when resuming with -r/-c.
pnpm build # tsup bundle to dist/
pnpm test # vitest unit tests (no network)
pnpm typecheck # tsc --noEmit
pnpm lint # biome
pnpm test:pipeline # layered pipeline incl. optional live LLM smoke (needs an API key)CI runs lint, typecheck, tests, and build on Ubuntu + Windows against Node 20/22 (.github/workflows/ci.yml).
Architecture: src/cli (Ink UI), src/agent (main loop), src/llm (Vercel AI SDK provider layer), src/tools, src/context (token budget + compaction), src/permissions, src/session, src/config.
Terminal-Bench 2.0, 20 randomly sampled tasks, same model for both agents:
| Agent | Model | Pass rate | Avg tokens | Avg duration |
|---|---|---|---|---|
| Star CLI 0.3.9 | Kimi K3 | 17/20 (85%) | 129.2K | 414s |
| terminus-2 2.0.0 | Kimi K3 | 9/20 (45%) | 49.5K | 484s |