@cuylabs/cli
v0.2.0
Published
Cuylabs CLI for running and managing agents
Readme
@cuylabs/cli
A terminal-native AI coding agent with a full interactive TUI, built on @cuylabs/agent-code and @cuylabs/agent-core. Multi-provider (OpenAI, Anthropic, Google), slash commands, streaming, mid-turn intervention, tool approval, skill discovery, sub-agent delegation, persistent sessions, keyboard shortcuts, markdown rendering, and more.
Quick Start
1. Set an API key
You only need one provider key. The CLI loads a .env file from your current working directory — the project folder where you run cuylabs, not from inside packages/cli/.
Where does the .env go?
your-project/ ← cd here, then run `cuylabs`
├── .env ← put your API key here
├── src/
├── package.json
└── ...The CLI looks for .env in whatever directory you're in when you launch it (process.cwd()). You can also point to a specific file with --env-file.
Create your .env
An .env.example is included in apps/cli/. Copy it to your project:
# From your project directory
cp node_modules/@cuylabs/cli/.env.example .env
# Or just create one manually
cat > .env << 'EOF'
# Uncomment ONE provider key:
# OpenAI
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxx
# Anthropic
# ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxx
# Google (either variable works)
# GOOGLE_GENERATIVE_AI_API_KEY=AIzaxxxxxxxxxxxxxxxxxx
EOFOr set the key as a shell environment variable (no .env needed):
export OPENAI_API_KEY=sk-proj-...
cuylabsImportant: The
.envfile is loaded from your current directory, not frompackages/cli/. If you runcuylabsfrom/Users/you/my-project, it looks for/Users/you/my-project/.env.
Provider auto-detection
The CLI auto-detects which provider to use based on which key is present:
| Environment Variable | Provider |
|----------------------|----------|
| OPENAI_API_KEY | OpenAI |
| ANTHROPIC_API_KEY | Anthropic |
| GOOGLE_GENERATIVE_AI_API_KEY or GOOGLE_API_KEY | Google |
If multiple keys are set, the priority is: OpenAI > Anthropic > Google. Override with --provider.
2. Build
# From the agents-ts workspace root
pnpm build --filter @cuylabs/cli3. Launch
# Launch the interactive TUI (default command)
npx cuylabs
# Or with an initial prompt
npx cuylabs "refactor the auth module"
# Specify a model
npx cuylabs --model anthropic:claude-sonnet-4-20250514
# Resume your last session
npx cuylabs --continue # or -C
# Disable tool approval prompts
npx cuylabs --approval never
# Legacy plain-text chat mode
npx cuylabs chat
# One-shot prompt
npx cuylabs run "summarize this repo" --jsonRunning cuylabs with no subcommand launches the TUI — the full interactive terminal interface.
Install locally (from the monorepo)
If you're developing in the agents-ts repo, build and link the CLI so the cuylabs command uses your local code:
# From the agents-ts workspace root
pnpm install
pnpm build --filter @cuylabs/cli
# One-time: set up pnpm's global bin directory (if you haven't before)
pnpm setup
source ~/.zshrc # or restart your terminal
# Link the CLI globally so `cuylabs` resolves to your local build
cd apps/cli && pnpm link --global
cd -
# Now `cuylabs` runs your local version from anywhere
cuylabs --helpOr run it directly without linking — using pnpm exec from the workspace root:
pnpm --filter @cuylabs/cli exec cuylabsOr via the bin entry with Node:
node apps/cli/bin/cuylabs.jsInstall from npm (published release)
npm install -g @cuylabs/cli
cuylabsNote: This installs the latest published version from npm, not your local code. Use "Install locally" above if you're working on the CLI itself.
Choosing a Model
The CLI supports three providers and uses a provider:model syntax:
# Explicit provider:model
cuylabs --model openai:gpt-4o
cuylabs --model anthropic:claude-sonnet-4-20250514
cuylabs --model google:gemini-2.0-flash
# Provider/model also works
cuylabs --model anthropic/claude-sonnet-4-20250514
# Just the model name (provider inferred from your API key)
cuylabs --model gpt-4o
# Or set provider separately
cuylabs --provider anthropic --model claude-sonnet-4-20250514Defaults when no model is specified:
| Provider | Default Model |
|----------|---------------|
| OpenAI | gpt-4o |
| Anthropic | claude-sonnet-4-20250514 |
| Google | gemini-2.0-flash |
Switch models mid-session inside the TUI:
❯ /model anthropic:claude-sonnet-4-20250514
⚙ Model switched to: anthropic:claude-sonnet-4-20250514List all available models:
cuylabs models # grouped by provider
cuylabs models --remote # live fetch from models.dev
cuylabs models --json # machine-readable
cuylabs models --provider anthropicProvider Priority
--provider/--modelCLI flag (highest)- Config file (
~/.config/cuylabs/config.json) - Environment variable auto-detect (
OPENAI_API_KEY→ openai,ANTHROPIC_API_KEY→ anthropic, etc.)
The TUI
The TUI is an Ink-based (React for terminal) interactive interface. It's the default when you run cuylabs.
Layout
┌──────────────────────────────────────────┐
│ Cuylabs CLI │
│ Model: gpt-4o · Tools: 9 · cwd: /proj │
│ Type /help for commands, or start. │
│ │
│ ❯ refactor the auth module │
│ ● Looking at the auth module... │
│ ✓ 📖 read src/auth.ts │
│ ✓ ✏️ edit src/auth.ts │
│ Done. I've **refactored** the JWT... │
│ │
│ ❯ _ │
├──────────────────────────────────────────┤
│ ● ready gpt-4o 1.2k↑ 856↓ a3f8d2e1 │
└──────────────────────────────────────────┘Components:
| Area | What it does |
|------|-------------|
| Welcome Banner | Model, tool count, working directory — shown until first message |
| Message List | Scrolling conversation with role indicators (❯ user, ● assistant, ⚙ system), per-tool icons (📂📖✏️📝🔍⚡), tree-branch result summaries |
| Markdown Rendering | Completed messages render with full markdown: bold, italic, code, code blocks with language labels, headings, lists, blockquotes, links |
| Prompt Input | Text input with slash-command autocomplete hints. During streaming, input redirects the agent (intervention) |
| Status Bar | Agent state (ready/thinking/calling-tool), model name, token counters, session ID |
| Approval Overlay | Pops up when a tool needs permission — [Y] allow, [N] deny, [A] always allow |
Intervention (Mid-Turn Redirect)
While the agent is streaming (calling tools, generating text), type anything and press Enter to redirect it in-flight:
❯ refactor the auth module
● Reading auth module...
✓ read src/auth.ts
The auth module has three main... ← agent is streaming
⚡ fix only the JWT validation bug ← you type this
⚙ ⚡ Redirected: fix only the JWT validation bug
● Understood, focusing on JWT... ← agent adjustsThe intervention is injected at the next step boundary. The conversation stays coherent — interventions are persisted to session history.
Tool Approval
When the agent calls a tool classified as moderate or dangerous, an overlay appears:
╭──────────────────────────────────────────╮
│ ⚠ Approve tool: bash │
│ Run command: rm -rf dist/ │
│ {"command": "rm -rf dist/"} │
│ │
│ [Y] allow [N] deny [A] always [Esc] │
╰──────────────────────────────────────────╯- Y / Enter — allow this one call
- N / Esc — deny (the model sees the denial and can adjust)
- A — allow this tool+pattern for the rest of the session
Approval modes
Control when approval prompts appear with --approval:
cuylabs --approval auto # (default) ask for moderate + dangerous tools
cuylabs --approval always # ask for every tool call
cuylabs --approval never # skip all prompts (YOLO mode)Risk classification:
| Risk | Tools | Behavior |
|------|-------|----------|
| safe | read, grep, glob | Auto-allowed in auto mode |
| moderate | write, edit | Prompt in auto and always |
| dangerous | bash | Always prompt (unless never) |
You can also set this in your config file:
{
"approval": "auto"
}Keyboard Shortcuts
Global shortcuts that work alongside the text input:
| Shortcut | Action | |----------|--------| | Ctrl+L | Clear conversation | | Ctrl+K | Compact context (summarize older messages) | | Ctrl+N | Start a new session | | Esc | Deny approval / cancel |
Type /shortcuts in the TUI to see this list.
Note: Shortcuts are disabled during streaming and when the approval overlay is active.
Slash Commands
Type / in the TUI to see autocomplete suggestions. All commands:
| Command | Aliases | Description |
|---------|---------|-------------|
| /help | /h, /? | Show all available commands |
| /quit | /q, /exit | Exit the CLI |
| /clear | /c | Clear conversation history |
| /status | /s | Session info, model, token usage, context utilization % |
| /model [spec] | /m | Show or switch model (e.g. /model anthropic:claude-sonnet-4-20250514) |
| /tools | /t | List active tools |
| /compact | | Summarize older messages to free context window |
| /undo | | Revert file changes from the last agent turn |
| /diff | /d | Show file changes from the current turn |
| /sessions | | List saved sessions |
| /branch | /fork | Fork the current session into a new branch |
| /resume [id] | /r | Resume a previous session by ID prefix |
| /skills | /sk | List discovered skills |
| /agents | /sa | Show sub-agent tools and profiles |
| /shortcuts | /keys | Show keyboard shortcuts |
Skills
The CLI automatically discovers SKILL.md files from your project and loads them as tools the agent can use.
How it works
- On startup, the CLI scans for
SKILL.mdfiles in:.agents/directory (Cuylabs convention).claude/directory (Claude Code convention)- Nested up to 4 levels deep
- Skills are summarized in the agent's context (L1 — titles and descriptions)
- The agent can call the
skilltool to load full skill content on demand (L2 — body text) - The agent can call
skill_resourceto read associated files (scripts, examples, references)
Creating a skill
mkdir -p .agents/my-skill
cat > .agents/my-skill/SKILL.md << 'EOF'
---
name: deploy
description: Deploy the application to production
tags: [devops, deploy]
---
# Deploy Skill
## Steps
1. Run `pnpm build` to create the production build
2. Run `pnpm test` to verify all tests pass
3. Run `./scripts/deploy.sh` to push to production
## Notes
- Always run from the project root
- Requires AWS credentials in environment
EOFManaging skills
# List discovered skills in the TUI
/skills
# Disable skill discovery
cuylabs --skills falseSkills can also be set in your config:
{
"skills": true
}Sub-Agents
The agent can delegate tasks to specialized sub-agents — lightweight forks that run independently and return their results.
Built-in profiles
| Profile | Purpose | Max Steps |
|---------|---------|-----------|
| explorer | Fast read-only codebase search and exploration | 20 |
| coder | Full-power implementation — write, edit, fix, refactor | 40 |
| planner | Read-only analysis and step-by-step planning | 15 |
| runner | Execute commands (tests, builds, lint) and report results | 10 |
The model decides when to delegate based on the task. You don't need to trigger sub-agents manually — just ask complex multi-part questions and the agent will use invoke_agent when it makes sense.
How it works
- The agent gets an
invoke_agenttool that lists available profiles - When the agent calls
invoke_agent, a sub-agent is forked from the parent with the chosen profile - The sub-agent runs in its own session with its own turn budget
- Results are returned to the parent agent as a tool result
- Sub-agents share the same tools, cwd, and file system as the parent
Managing sub-agents
# Show available sub-agent tools in the TUI
/agents
# Disable sub-agent tools
cuylabs --sub-agents falseSessions
All sessions are automatically persisted to disk using JSONL file storage. Sessions are stored per-project in ~/.cuylabs/sessions/<project-hash>/.
Resume a session
# Resume the most recent session
cuylabs --continue
cuylabs -C
# Resume a specific session by ID
cuylabs --session <session-id>
# Resume from within the TUI
/resume # shows recent sessions
/resume a3f8d2e1 # switch to session matching prefixSession commands
# List all sessions for the current project
/sessions
# Fork the current conversation into a new branch
/branch
# Start a fresh session mid-conversation
# (Ctrl+N shortcut also works)How session persistence works
- Each session is a
.jsonlfile with append-only entries - Messages, tool calls, compaction summaries, and branches are all persisted
- Sessions are scoped to the project (identified by git root hash or directory path)
- Sub-agent sessions are linked to their parent via
parentSessionId - Session IDs are UUIDs — use
/resumewith a prefix to avoid typing the full ID
CLI Commands
The CLI also supports non-TUI modes for scripting and pipelines:
| Command | Description |
|---------|-------------|
| cuylabs [prompt] | Default — Launch interactive TUI |
| cuylabs tui [prompt] | Same as above (explicit) |
| cuylabs chat [prompt] | Legacy plain-text interactive mode (readline, no TUI) |
| cuylabs run [prompt] | Single prompt, then exit. Supports --json and stdin pipe |
| cuylabs models | List available models |
| cuylabs tools | List available tools and groups |
| cuylabs config <action> | path, show, or init for CLI configuration |
Examples
# TUI with a specific model and system prompt
cuylabs --model anthropic:claude-sonnet-4-20250514 --system @./AGENTS.md
# Resume last session with a follow-up
cuylabs -C "now add tests for that"
# Skip approval prompts (unattended mode)
cuylabs --approval never "fix all lint errors"
# One-shot with JSON output
cuylabs run "list the top 5 files by size" --json
# Pipe stdin
cat error.log | cuylabs run "explain this error"
# Read prompt from a file
cuylabs run @./prompt.md
# Disable tools (chat-only mode)
cuylabs --tools false
# Use specific tools only
cuylabs --tools read,grep,glob
# All tools except bash
cuylabs --tools all,-bash
# Use a tool group
cuylabs --tools read-only
cuylabs --tools safe
# Disable all agent features for a simple chat
cuylabs --tools false --skills false --sub-agents false --approval neverConfiguration
Config file
Default location: ~/.config/cuylabs/config.json (XDG compliant)
# Create a config template
cuylabs config init
# Show current config
cuylabs config show
# Print config path
cuylabs config pathConfig structure:
{
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"systemPrompt": "",
"cwd": "/path/to/project",
"tools": true,
"maxSteps": 50,
"temperature": 0.7,
"reasoningLevel": "medium",
"approval": "auto",
"skills": true,
"subAgents": true
}| Field | Type | Default | Description |
|-------|------|---------|-------------|
| provider | "openai" \| "anthropic" \| "google" | auto-detect | AI provider |
| model | string | provider default | Model ID |
| systemPrompt | string | "" | System prompt text |
| cwd | string | process.cwd() | Working directory |
| tools | boolean \| string | true | Tool spec (see below) |
| maxSteps | number | 50 | Max agent steps per turn |
| temperature | number | — | LLM temperature |
| reasoningLevel | string | — | Reasoning depth |
| approval | "auto" \| "always" \| "never" | "auto" | Tool approval mode |
| skills | boolean | true | Enable skill discovery |
| subAgents | boolean | true | Enable sub-agent delegation |
Environment variables
| Variable | Purpose |
|----------|---------|
| OPENAI_API_KEY | OpenAI provider |
| ANTHROPIC_API_KEY | Anthropic provider |
| GOOGLE_GENERATIVE_AI_API_KEY | Google provider (or GOOGLE_API_KEY) |
| CUYLABS_CONFIG / CUY_CONFIG | Custom config file path |
.env file resolution order
--env-file ./path/to/.env(explicit flag — highest priority)--cwd /some/dir→ looks for/some/dir/.env- Config file
cwdfield → looks for.envthere - Current working directory →
$(pwd)/.env(default)
# Use a specific .env file
cuylabs --env-file ~/secrets/cuylabs.env
# Set working directory (also changes where .env is loaded from)
cuylabs --cwd /path/to/projectAll CLI Options
cuylabs [prompt]
Options:
--model Model ID or provider:model (e.g. openai:gpt-4o)
--provider Force provider: openai, anthropic, google
--system System prompt string or @file path
--session Session ID for persistence
--continue, -C Resume the most recent session
--cwd Working directory for the agent
--env-file Path to .env file
--config Path to config JSON
--tools Tool spec (see below)
--approval Tool approval mode: auto, always, never (default: auto)
--skills Enable skill discovery (default: true)
--sub-agents Enable sub-agent delegation tools (default: true)
--temperature LLM temperature
--topP Top-P sampling
--maxOutputTokens Max output tokens
--maxSteps Max agent steps per turn
--reasoningLevel Reasoning depth (low, medium, high)
--verbose Show tool call details
--show-reasoning Stream reasoning/thinking text
--json (run only) JSON output instead of streamingTool specs
--tools true All tools (default)
--tools false No tools
--tools read-only Group: read, grep, glob
--tools safe Group: all except bash
--tools minimal Group: read, grep
--tools read,grep Specific tools by name
--tools all,-bash All minus bashArchitecture
cuylabs (CLI binary)
├── TUI layer (Ink / React for terminal)
│ ├── App — root layout, input routing, keyboard shortcuts
│ ├── MessageList — streaming conversation with markdown rendering
│ ├── MarkdownView — zero-dep markdown parser + Ink renderer
│ ├── PromptInput — text input with /command autocomplete hints
│ ├── StatusBar — model, tokens, state, session ID
│ ├── ApprovalOverlay — tool permission prompts (Y/N/A)
│ └── Commands — 15 slash commands via registry pattern
│
├── Hooks
│ ├── useAgent — bridges agent.chat() events → React state
│ ├── useKeyboard — Ctrl+L/K/N global shortcuts
│ └── useTerminal — terminal dimensions
│
├── ApprovalGateway — bridges middleware ↔ React state
│
├── @cuylabs/agent-code
│ ├── 6 coding tools: bash, read, edit, write, grep, glob
│ └── ToolsetBuilder (fluent tool selection)
│
└── @cuylabs/agent-core
├── Agent — streaming chat, sessions, forking
├── Middleware — approval, lifecycle hooks
├── Skills — SKILL.md discovery, registry, L1/L2 loading
├── Sub-Agents — invoke_agent tool, profiles, tracker
├── SessionManager — FileStorage (JSONL), branching, resume
├── Approval — risk classification, rules, remember
├── Intervention — mid-turn message injection
├── Context — token estimation, pruning, summarization
├── MCP — Model Context Protocol servers
└── Prompt Pipeline — model-aware system promptsHow the TUI works
cuylabsboots Ink (React for terminal) and mounts<App>setupAgent()creates the agent with middleware (approval), skills, sub-agent tools, and file-based session storage- User types a message →
useAgent.send(message)is called send()callsagent.chat(sessionId, message)— an async generator- Each
AgentEventfrom the generator updates React state:text-delta→ appends to the assistant message (raw during streaming, markdown on completion)tool-start/tool-result→ inline tool status with per-tool icons and result summariesstep-start/step-finish→ updates the status barintervention-applied→ adds a system note
- When the middleware triggers an approval request, the
ApprovalGatewaysets React state → theApprovalOverlayrenders → user presses Y/N/A → the Promise resolves → middleware continues - When the user types while streaming, it calls
agent.intervene()— injected at the next step boundary - Slash commands are dispatched locally (never sent to the LLM)
- All messages are persisted to
~/.cuylabs/sessions/<project-hash>/as JSONL
