Chapter 4: Command System
Table of Contents
- Introduction: Commands vs Tools
- Command Type System
- Command Registration
- Command Lookup and Filtering Pipeline
- Deep Dive: Representative Commands
- Skill-Based Commands
- Plugin Commands
- Availability and Enablement
- Hands-On: Implement a Slash Command
- Key Takeaways
- Hands-On Build: Query Loop (Agentic Loop)
Introduction: Commands vs Tools
Claude Code exposes two fundamentally different extensibility surfaces: Tools and Commands.
| Dimension | Tools | Commands |
|---|---|---|
| Invoked by | The AI model | The user (typing /) |
| Syntax | JSON in API call | /command-name [args] |
| Definition | Tool class with inputSchema | Command object with typed fields |
| Purpose | Extend what Claude can do | Extend what users can trigger |
| Chapter | Chapter 3 | This chapter |
Commands are the slash-command interface users interact with directly. When you type /compact, /diff, or /review pr-123, you're invoking a command. The model never calls commands — it calls tools. This distinction keeps the system clean: commands are for human-facing workflows; tools are for agent capabilities.
User types /compact
│
▼
processSlashCommand() ← REPL input handler
│
├─ type: 'prompt' → getPromptForCommand() → inject into conversation
├─ type: 'local' → load().call() → execute in process, return text
└─ type: 'local-jsx' → load().call() → render Ink componentCommand Type System
The type definitions live in src/types/command.ts. The top-level union is:
// src/types/command.ts:205-206
export type Command = CommandBase &
(PromptCommand | LocalCommand | LocalJSXCommand)Every command starts with CommandBase shared fields, then specializes into one of three execution models.
PromptCommand
// src/types/command.ts:25-57
export type PromptCommand = {
type: 'prompt'
progressMessage: string
contentLength: number
argNames?: string[]
allowedTools?: string[]
model?: string
source: SettingSource | 'builtin' | 'mcp' | 'plugin' | 'bundled'
pluginInfo?: { ... }
disableNonInteractive?: boolean
hooks?: HooksSettings
skillRoot?: string
context?: 'inline' | 'fork'
agent?: string
effort?: EffortValue
paths?: string[]
getPromptForCommand(
args: string,
context: ToolUseContext,
): Promise<ContentBlockParam[]>
}Key fields:
getPromptForCommand— returns an array ofContentBlockParamblocks that get injected into the conversation as the user's next message. This is how a command like/reviewexpands into a long detailed prompt.context: 'inline' | 'fork'—inline(default) expands the prompt in the current conversation;forkspawns a sub-agent with a separate token budget.source— tracks where the command came from (builtin,mcp,plugin,bundled, or aSettingSourcelikeuserSettings/projectSettings).paths— glob patterns: the command is hidden until the model has touched matching files (useful for file-type-specific skills).
sequenceDiagram
participant U as User
participant REPL
participant Cmd as PromptCommand
participant API as Anthropic API
U->>REPL: /review 123
REPL->>Cmd: getPromptForCommand("123", ctx)
Cmd-->>REPL: ContentBlockParam[]
REPL->>API: inject as user message
API-->>REPL: model response
REPL-->>U: streaming outputLocalCommand
// src/types/command.ts:74-78
type LocalCommand = {
type: 'local'
supportsNonInteractive: boolean
load: () => Promise<LocalCommandModule>
}And the module it loads:
// src/types/command.ts:62-72
export type LocalCommandModule = {
call: LocalCommandCall
}
export type LocalCommandCall = (
args: string,
context: LocalJSXCommandContext,
) => Promise<LocalCommandResult>The load() pattern is lazy loading — the heavy implementation module is not imported at startup. It's only fetched when the command is actually invoked. This keeps startup time fast even with 50+ commands registered.
LocalCommandResult can be:
// src/types/command.ts:16-23
export type LocalCommandResult =
| { type: 'text'; value: string }
| { type: 'compact'; compactionResult: CompactionResult; displayText?: string }
| { type: 'skip' }LocalJSXCommand
// src/types/command.ts:144-152
type LocalJSXCommand = {
type: 'local-jsx'
load: () => Promise<LocalJSXCommandModule>
}Same lazy load() pattern, but the implementation returns a React node (rendered by Ink in the terminal). Commands like /diff, /memory, /config, and /doctor use this type to render interactive terminal UIs.
CommandBase Fields
All three types share CommandBase:
// src/types/command.ts:175-203
export type CommandBase = {
availability?: CommandAvailability[] // auth gating
description: string
hasUserSpecifiedDescription?: boolean
isEnabled?: () => boolean // feature-flag gating
isHidden?: boolean // hide from typeahead
name: string
aliases?: string[]
isMcp?: boolean
argumentHint?: string // gray hint in autocomplete
whenToUse?: string // model-facing usage guidance
version?: string
disableModelInvocation?: boolean // block AI from calling this
userInvocable?: boolean
loadedFrom?: 'commands_DEPRECATED' | 'skills' | 'plugin' | 'managed' | 'bundled' | 'mcp'
kind?: 'workflow'
immediate?: boolean // bypass queue
isSensitive?: boolean // redact args from history
userFacingName?: () => string
}Notable fields:
isEnabled— a thunk that evaluates at call-time. Used for feature-flag and environment-variable gating. Returningfalsehides the command from typeahead.availability— static auth requirement:'claude-ai'(OAuth subscribers) or'console'(API key users). Evaluated inmeetsAvailabilityRequirement()on everygetCommands()call, not memoized, so auth changes during a session take effect immediately.aliases— additional names (e.g.,/configis also/settings).whenToUse— free-text guidance for the model's SkillTool. Not shown to users.disableModelInvocation— prevents the command from appearing in model-callable skill listings.
Command Registration
The entire registration flow lives in src/commands.ts (754 lines). It has three distinct layers.
Static Imports
The file begins with ~50 static import statements, one per built-in command:
// src/commands.ts:2-57
import compact from './commands/compact/index.js'
import config from './commands/config/index.js'
import diff from './commands/diff/index.js'
import doctor from './commands/doctor/index.js'
import memory from './commands/memory/index.js'
// ...40+ moreThese are always loaded at module initialization. Each index.ts is tiny (< 15 lines) — it just defines the command metadata object and the load() thunk. The heavy implementation is only pulled in when the command runs.
Feature-Flag-Gated Commands (DCE)
Commands gated by feature() (Bun's dead code elimination):
// src/commands.ts:62-122
const proactive =
feature('PROACTIVE') || feature('KAIROS')
? require('./commands/proactive.js').default
: null
const briefCommand =
feature('KAIROS') || feature('KAIROS_BRIEF')
? require('./commands/brief.js').default
: null
const bridge = feature('BRIDGE_MODE')
? require('./commands/bridge/index.js').default
: null
// ...more feature-gated commandsfeature('FLAG_NAME') is evaluated at build time by Bun's bundler. If the flag is false, the require() call (and the entire referenced module) is eliminated from the production bundle — true dead code elimination (DCE). This keeps the published binary lean.
At runtime, the result is either the command object or null. The COMMANDS() array then spreads only non-null values:
// src/commands.ts:320-330
...(proactive ? [proactive] : []),
...(briefCommand ? [briefCommand] : []),
...(bridge ? [bridge] : []),USER_TYPE Conditional Loading
// src/commands.ts:48-52
const agentsPlatform =
process.env.USER_TYPE === 'ant'
? require('./commands/agents-platform/index.js').default
: nullAnt-internal commands check process.env.USER_TYPE === 'ant' at runtime. They're only visible to Anthropic employees. The entire INTERNAL_ONLY_COMMANDS array is similarly gated:
// src/commands.ts:343-346
...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO
? INTERNAL_ONLY_COMMANDS
: []),Dynamic Sources: Skills, Plugins, Workflows
The memoized loadAllCommands() function assembles all dynamic command sources in parallel:
// src/commands.ts:449-469
const loadAllCommands = memoize(async (cwd: string): Promise<Command[]> => {
const [
{ skillDirCommands, pluginSkills, bundledSkills, builtinPluginSkills },
pluginCommands,
workflowCommands,
] = await Promise.all([
getSkills(cwd),
getPluginCommands(),
getWorkflowCommands ? getWorkflowCommands(cwd) : Promise.resolve([]),
])
return [
...bundledSkills,
...builtinPluginSkills,
...skillDirCommands,
...workflowCommands,
...pluginCommands,
...pluginSkills,
...COMMANDS(), // built-in commands come last
]
})The priority order matters for name conflict resolution: bundled skills first, plugin skills last before built-ins. Built-ins always win because they're appended last and findCommand() returns the first match — actually bundled skills win when they share a name with built-ins because they appear earlier in the array.
graph TD
A[getCommands cwd] --> B[loadAllCommands memoized by cwd]
B --> C1[getBundledSkills]
B --> C2[getBuiltinPluginSkillCommands]
B --> C3[getSkillDirCommands from .claude/skills/]
B --> C4[getWorkflowCommands]
B --> C5[getPluginCommands]
B --> C6[getPluginSkills]
B --> C7[COMMANDS built-in array]
C1 & C2 & C3 & C4 & C5 & C6 & C7 --> D[allCommands]
D --> E[meetsAvailabilityRequirement filter]
E --> F[isCommandEnabled filter]
F --> G[getDynamicSkills dedup]
G --> H[Final command list]Command Lookup and Filtering Pipeline
getCommands() is not just the loader — it applies two runtime filters on every call:
// src/commands.ts:476-517
export async function getCommands(cwd: string): Promise<Command[]> {
const allCommands = await loadAllCommands(cwd)
const dynamicSkills = getDynamicSkills()
const baseCommands = allCommands.filter(
_ => meetsAvailabilityRequirement(_) && isCommandEnabled(_),
)
// dedupe and insert dynamic skills
...
}meetsAvailabilityRequirement— checkscmd.availabilityagainst current auth state. Not memoized because auth can change mid-session (after/login).isCommandEnabled— delegates tocmd.isEnabled?.() ?? true. If the command has noisEnabled, it defaults to enabled.- Dynamic skills — discovered during file I/O operations (e.g., when the model reads a file that triggers a path-matched skill) are merged in last, deduped by name.
Finding a command by name uses findCommand():
// src/commands.ts:688-698
export function findCommand(
commandName: string,
commands: Command[],
): Command | undefined {
return commands.find(
_ =>
_.name === commandName ||
getCommandName(_) === commandName ||
_.aliases?.includes(commandName),
)
}It checks name, userFacingName(), and aliases. This is why /settings resolves to the config command (which declares aliases: ['settings']).
Deep Dive: Representative Commands
compact — LocalCommand with isEnabled guard
// src/commands/compact/index.ts:1-15
import type { Command } from '../../commands.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
const compact = {
type: 'local',
name: 'compact',
description:
'Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]',
isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT),
supportsNonInteractive: true,
argumentHint: '<optional custom summarization instructions>',
load: () => import('./compact.js'),
} satisfies CommandThe implementation (compact.ts) is ~288 lines, which would significantly slow startup if eagerly loaded. The load() thunk defers it until the user actually runs /compact.
The isEnabled guard checks DISABLE_COMPACT env var, allowing operators to disable the feature without code changes.
The call() implementation (in compact.ts:40) accepts optional custom summarization instructions and orchestrates multiple compaction strategies:
- Session memory compaction (if no custom instructions)
- Reactive compaction (feature-flagged path)
- Traditional
compactConversation()with microcompact pre-pass
diff — LocalJSXCommand
// src/commands/diff/index.ts:1-8
export default {
type: 'local-jsx',
name: 'diff',
description: 'View uncommitted changes and per-turn diffs',
load: () => import('./diff.js'),
} satisfies CommandThe minimal index file — pure metadata. The diff.js implementation renders an Ink component that shows git diffs with interactive navigation. The local-jsx type tells the REPL dispatcher to render the returned React node in the terminal rather than printing text.
review — PromptCommand
// src/commands/review.ts:33-44
const review: Command = {
type: 'prompt',
name: 'review',
description: 'Review a pull request',
progressMessage: 'reviewing pull request',
contentLength: 0,
source: 'builtin',
async getPromptForCommand(args): Promise<ContentBlockParam[]> {
return [{ type: 'text', text: LOCAL_REVIEW_PROMPT(args) }]
},
}The getPromptForCommand returns the filled template as a content block. The REPL injects it as the user's next message to the model. Notice contentLength: 0 — the actual content length is computed dynamically by the template function. This is a simplification for built-in commands; skill-based commands compute the token estimate from their Markdown file size.
The same file exports ultrareview as a local-jsx command — it uses the same name-space but completely different execution path (renders permission UI then forks to a remote agent).
insights — Lazy PromptCommand Shim
// src/commands.ts:190-202
const usageReport: Command = {
type: 'prompt',
name: 'insights',
description: 'Generate a report analyzing your Claude Code sessions',
contentLength: 0,
progressMessage: 'analyzing your sessions',
source: 'builtin',
async getPromptForCommand(args, context) {
const real = (await import('./commands/insights.js')).default
if (real.type !== 'prompt') throw new Error('unreachable')
return real.getPromptForCommand(args, context)
},
}The comment explains why: insights.ts is 113KB (3200 lines). The shim is a PromptCommand whose getPromptForCommand does a dynamic import() on first call. This is the same lazy-loading pattern as LocalCommand.load(), applied to a PromptCommand. The module is only fetched when /insights is first run.
Skill-Based Commands
Skills are user-defined commands written as Markdown files. They're the primary extension mechanism for end users.
Loading from .claude/skills/
The loader (src/skills/loadSkillsDir.ts) searches multiple directories in priority order:
// src/skills/loadSkillsDir.ts:78-94
export function getSkillsPath(
source: SettingSource | 'plugin',
dir: 'skills' | 'commands',
): string {
switch (source) {
case 'policySettings':
return join(getManagedFilePath(), '.claude', dir)
case 'userSettings':
return join(getClaudeConfigHomeDir(), dir)
case 'projectSettings':
return `.claude/${dir}`
case 'plugin':
return 'plugin'
}
}Sources in priority order:
policySettings— managed/enterprise policy (highest priority)userSettings—~/.claude/skills/(global user skills)projectSettings—.claude/skills/in the project directory
Each .md file in these directories becomes a PromptCommand. The filename (without extension) becomes the command name. Frontmatter controls metadata:
---
description: Summarize all changes since the last release
whenToUse: Use when you need a release summary for changelogs
allowedTools: Bash, Read
context: inline
---
Please summarize all changes since the last git tag...The frontmatter parser (src/utils/frontmatterParser.ts) extracts these fields and maps them to PromptCommand fields. getPromptForCommand returns the file body (after frontmatter) as the prompt content.
Bundled Skills
Bundled skills are skills compiled into the binary — no external file needed. They're registered programmatically using registerBundledSkill():
// src/skills/bundledSkills.ts:53-60
export function registerBundledSkill(definition: BundledSkillDefinition): void {
// ...handles file extraction and skillRoot setup
bundledSkills.push(/* Command object */)
}The BundledSkillDefinition type is similar to PromptCommand but with an additional files field — a map of relative paths to content. On first invocation, these files are extracted to disk so the model can Read them via tool calls. This is how built-in skills can ship reference documentation.
In commands.ts, bundled skills are collected via getBundledSkills() and placed first in the assembled command list, giving them highest priority.
Plugin Commands
Plugins can inject both PromptCommand and LocalCommand/LocalJSXCommand entries. The loading flow:
Plugin installed at ~/.claude/plugins/<name>/
│
▼
loadAllPluginsCacheOnly() ← reads plugin manifests
│
▼
walkPluginMarkdown() ← finds .md files in plugin commands/ dir
│
▼
buildCommandFromMarkdown() ← creates PromptCommand for each file
│
▼
getPluginCommands() ← returns all plugin commands, memoizedPlugin commands get their source set to 'plugin' and carry pluginInfo with the manifest:
// PromptCommand fields for plugin commands
source: 'plugin',
pluginInfo: {
pluginManifest: PluginManifest,
repository: string,
}The formatDescriptionWithSource() function (at src/commands.ts:728) prefixes plugin commands in the autocomplete display:
// src/commands.ts:737-740
if (cmd.source === 'plugin') {
const pluginName = cmd.pluginInfo?.pluginManifest.name
if (pluginName) {
return `(${pluginName}) ${cmd.description}`
}
}This labels plugin commands visually, so users know where a command comes from.
Plugin skills (from plugins/<name>/skills/) go through a similar path but via getPluginSkills(). They're placed in the pluginSkills group, which appears before built-in commands in the merged list.
Availability and Enablement
The system uses two orthogonal gating mechanisms:
graph LR
A[Command] -->|static check| B{meetsAvailabilityRequirement}
B -->|pass| C{isCommandEnabled}
B -->|fail| X[Hidden]
C -->|true| D[Shown to user]
C -->|false| Xavailability (static, auth-based):
// src/types/command.ts:169-172
export type CommandAvailability =
| 'claude-ai' // claude.ai OAuth subscriber
| 'console' // Console API key userCommands without availability are shown to everyone. Commands with availability require the user to match at least one of the listed auth types.
isEnabled (dynamic, feature-flag-based):
// src/commands/compact/index.ts:9
isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT),This is a zero-argument function evaluated on every getCommands() call. Use it for:
- Environment variable toggles
- GrowthBook feature flags
- Platform-specific conditions
The isCommandEnabled() helper provides a safe default:
// src/types/command.ts:214-216
export function isCommandEnabled(cmd: CommandBase): boolean {
return cmd.isEnabled?.() ?? true
}Hands-On: Implement a Slash Command
Let's implement a /stats command that shows statistics about the current directory. We'll create both a simplified demo and walk through where to place it in the real codebase.
File Structure
src/commands/
└── stats/
├── index.ts ← command metadata (type, name, load)
└── stats.ts ← implementation (call function)Step 1: Define the Command Metadata
// src/commands/stats/index.ts
import type { Command } from '../../commands.js'
const stats = {
type: 'local',
name: 'stats',
description: 'Show file statistics for the current directory',
supportsNonInteractive: true,
argumentHint: '[path]',
load: () => import('./stats.js'),
} satisfies Command
export default statsKey decisions:
type: 'local'— returns text, no React UI neededsupportsNonInteractive: true— works in--printmodeload: () => import('./stats.js')— lazy load the implementation
Step 2: Implement the Command
// src/commands/stats/stats.ts
import { readdir, stat } from 'fs/promises'
import { join } from 'path'
import type { LocalCommandCall } from '../../types/command.js'
export const call: LocalCommandCall = async (args, context) => {
const targetPath = args.trim() || context.options.cwd || process.cwd()
try {
const entries = await readdir(targetPath, { withFileTypes: true })
const files = entries.filter(e => e.isFile())
const dirs = entries.filter(e => e.isDirectory())
const fileSizes = await Promise.all(
files.map(f =>
stat(join(targetPath, f.name)).then(s => s.size)
)
)
const totalBytes = fileSizes.reduce((a, b) => a + b, 0)
const avgBytes = files.length > 0 ? Math.round(totalBytes / files.length) : 0
return {
type: 'text',
value: [
`Directory: ${targetPath}`,
`Files: ${files.length}`,
`Subdirectories: ${dirs.length}`,
`Total size: ${(totalBytes / 1024).toFixed(1)} KB`,
`Average file size: ${avgBytes} bytes`,
].join('\n'),
}
} catch (err) {
return {
type: 'text',
value: `Error reading directory: ${err instanceof Error ? err.message : String(err)}`,
}
}
}Step 3: Register in commands.ts
Add the import and include in the COMMANDS() array:
// src/commands.ts — add near other imports
import stats from './commands/stats/index.js'
// Inside the COMMANDS() memoized array
const COMMANDS = memoize((): Command[] => [
// ...existing commands...
stats, // ← add here
])Step 4: Test
# In development
bun run dev
# In Claude Code REPL
/stats
/stats src/commandsChoosing the Right Command Type
| Use Case | Type | Reason |
|---|---|---|
| Show text output | local | Simple, works in non-interactive |
| Render interactive UI | local-jsx | Need Ink components |
| Expand to a model prompt | prompt | Let Claude handle the reasoning |
| User-written skill | prompt (via .md file) | Markdown frontmatter system |
See the full working implementation in examples/04-command-system/slash-command.ts.
Key Takeaways
Three command types —
PromptCommand(expand to prompt),LocalCommand(run in process, return text),LocalJSXCommand(run in process, render Ink UI). All lazy-load their implementations viaload().Registration is layered — static imports for built-ins,
feature()DCE for experimental commands,USER_TYPEfor internal commands, and async parallel loading for dynamic sources (skills, plugins, workflows).getCommands()is the gating point — it applies bothmeetsAvailabilityRequirementandisCommandEnabledfilters on every call. Auth changes take effect immediately because this is not memoized.Skills are just Markdown files — place a
.mdfile in~/.claude/skills/or.claude/skills/with optional frontmatter, and it becomes a/command-nameautomatically.Plugins extend commands — plugins can inject both prompt-based and local commands through manifest-declared files. They appear prefixed with their plugin name in the autocomplete.
Lazy loading is critical — every non-trivial command uses
load: () => import('./impl.js'). Theindex.tsfile is a ~10-line metadata declaration; the real code loads on first use.sourceandloadedFromtrack provenance — the system knows exactly where each command came from, enabling correct display labels, model invocation filtering, and bridge safety checks.
Hands-On Build: Query Loop (Agentic Loop)
This is the pivotal milestone of the entire tutorial. After completing this section, your mini-claude will achieve its first complete closed loop — input a question, the AI autonomously calls tools, and returns an answer. The type system, tool registry, API client, and system prompt from previous chapters all converge here.
11.1 Project Structure Update
demo/
├── main.ts
├── query.ts # ← NEW: query loop (Agentic Loop)
├── Tool.ts # Chapter 2
├── tools.ts # Chapter 2
├── context.ts # Chapter 3
├── services/api/ # Chapter 3
├── utils/
│ └── messages.ts # ← NEW: message conversion utilities
└── types/ # Chapter 111.2 query.ts Core Logic: The Agentic Loop
Open demo/query.ts — this is the most important file in this chapter. It implements the Agentic Loop, the AI's autonomous reasoning cycle.
Pseudocode Overview
while (turn < maxTurns) {
1. Message history → API format (messagesToAPIParams)
2. Stream API call (streamMessage)
3. Collect response (text + tool calls)
4. No tool calls? → exit loop
5. Execute tools (read-only concurrent, read-write sequential)
6. Tool results → append as user message to history
7. Continue loop
}Each iteration is called a "turn." If the model returns plain text (no tool calls), it considers the task complete and the loop terminates. If it returns tool calls, the tools are executed, results appended to the message history, and the API is called again — the model sees the tool results and can continue reasoning or decide to finish.
This is what "Agentic" means: the model doesn't answer in one shot. It autonomously decides what information it needs, which tools to call, and when to stop.
11.3 Tool Orchestration Strategy
query.ts employs intelligent orchestration for tool execution:
- Read-only tools (Read, Grep, etc., where
isReadOnly() === true) →Promise.allconcurrent execution - Read-write tools (Bash, etc., where
isReadOnly() === false) → sequential execution
Why this design?
- Concurrent reads are safe — multiple Read calls executing simultaneously don't interfere with each other
- Write operations need ordering guarantees — two Bash commands executing concurrently could produce race conditions (e.g., one creates a file while another writes to the same file)
This mirrors the processToolCalls() logic in the real Claude Code's src/query.ts: tools marked isConcurrencySafe run concurrently, the rest run sequentially.
11.4 utils/messages.ts: Message Conversion Utilities
Open demo/utils/messages.ts — the second new file in this chapter.
The API expects message formats (MessageParam) that differ from our internal Message type. This file provides three conversion functions:
messagesToAPIParams()— converts internalMessage[]to the API'sMessageParam[], handling role mapping and content block formattingcreateToolResultBlock()— constructs atool_resultcontent block, wrapping tool execution results in the format the API expectsextractToolUseBlocks()— extracts alltool_useblocks from an assistant message's content array, used to determine whether the loop should continue
These functions may seem simple, but they are the critical glue layer that keeps the Agentic Loop running correctly — without proper message format conversion, the API cannot understand tool results and the loop breaks.
11.5 Running the Demo
cd demo
bun run main.ts # Run without API key (shows tool registry and system prompt)
ANTHROPIC_API_KEY=sk-xxx bun run main.ts # Full Agentic Loop experienceWith an API key, expected output looks like:
Agentic Loop Demo:
────────────────────────────────────
[Tool Call] Read({"file_path":"/.../package.json"})
[Tool Result] ✅ Read: {"name":"mini-claude","version":"0.1.0"...
OK, from package.json I can see: the project name is mini-claude, version 0.1.0.
────────────────────────────────────
Loop turns: 1
Token usage: xxx input / xxx outputYou'll see the AI autonomously deciding to call the Read tool to read a file, then organizing its answer from the retrieved information — this is the complete Agentic Loop in action.
11.6 Mapping to Real Claude Code
| Demo File | Real File | What's Simplified |
|---|---|---|
query.ts | src/query.ts + src/QueryEngine.ts | No context compression, no retry, no recovery |
utils/messages.ts | src/utils/messages.ts | No attachments, no tombstone, no snip |
These simplifications preserve the core pattern: call API in a loop → execute tools → append results → call again, while omitting production concerns like error recovery and context window management.
What Comes Next
Chapter 5 will flesh out tool implementations (FileWriteTool, FileEditTool, GlobTool), enabling mini-claude to create and edit files.
What's Next: Chapter 5 covers the Ink rendering layer — how LocalJSXCommand implementations produce interactive terminal UIs and how the REPL manages the React tree.