Include npm test scripts and vitest commands for running single test files and pattern matching. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Claudeman is a Claude Code session manager with a web interface and autonomous Ralph Loop. It spawns Claude CLI processes via PTY, streams output in real-time via SSE, and supports scheduled/timed runs.
Tech Stack: TypeScript, Node.js, Fastify, Server-Sent Events, node-pty
Commands
npm run build # Compile TypeScript + copy static files to dist/web/
npm run dev # Run with tsx (no build needed)
npm run clean # Remove dist/
npm link # Make 'claudeman' globally available
claudeman web # Start web interface on port 3000
claudeman web -p 8080 # Custom port
# Testing
npm run test # Run all tests once
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage report
npx vitest run test/session.test.ts # Run single test file
npx vitest run -t "should create session" # Run tests matching pattern
Architecture
src/
├── index.ts # CLI entry point (commander)
├── cli.ts # CLI command implementations
├── session.ts # Core: PTY wrapper for Claude CLI
├── session-manager.ts # Manages multiple sessions
├── respawn-controller.ts # Auto-respawn state machine
├── ralph-loop.ts # Autonomous task assignment
├── task.ts / task-queue.ts # Priority queue with dependencies
├── state-store.ts # Persistence to ~/.claudeman/state.json
├── types.ts # All TypeScript interfaces
├── web/
│ ├── server.ts # Fastify REST API + SSE
│ └── public/ # Static frontend files
└── templates/
└── claude-md.ts # CLAUDE.md generator for new cases
Data Flow
- Session spawns
claude -p --dangerously-skip-permissionsvianode-pty - PTY output is buffered, ANSI stripped, and parsed for JSON messages
- WebServer broadcasts events to SSE clients at
/api/events - State persists to
~/.claudeman/state.jsonvia StateStore
Key Components
-
Session (
src/session.ts): Wraps Claude CLI as PTY subprocess. Two modes:runPrompt(prompt)for one-shot execution,startInteractive()for persistent terminal. Emitsoutput,terminal,message,completion,exit,idle,workingevents. Maintains terminal buffer for reconnections. Includes buffer management for long-running sessions (12-24+ hours) with automatic trimming. -
RespawnController (
src/respawn-controller.ts): State machine that keeps interactive sessions productive. Detects idle → sends update prompt →/clear→/init→ repeats. Configurable timeouts and prompts. -
RalphLoop (
src/ralph-loop.ts): Autonomous task assignment controller. Monitors sessions for idle state, assigns tasks from queue, detects completion via<promise>PHRASE</promise>markers. Supports time-aware loops with minimum duration. -
WebServer (
src/web/server.ts): Fastify server with REST API + SSE. Manages sessions, scheduled runs, respawn controllers, and case directories. Broadcasts all events to connected clients.
Session Modes
One-Shot Mode (runPrompt(prompt)):
- Execute a single prompt and receive completion event
- Used for scheduled runs and quick API calls
- Session exits after prompt completes
Interactive Mode (startInteractive()):
- Persistent PTY terminal with full Claude CLI access
- Supports terminal resize for proper formatting
- Terminal buffer persisted for client reconnections
- Works with RespawnController for autonomous cycling
Code Patterns
Claude Message Parsing
Claude CLI outputs newline-delimited JSON. Strip ANSI codes before parsing:
const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, '');
const msg = JSON.parse(cleanLine) as ClaudeMessage;
// msg.type: 'system' | 'assistant' | 'user' | 'result'
// msg.message?.content: Array<{ type: 'text', text: string }>
// msg.total_cost_usd: number (on result messages)
PTY Spawn Modes
One-shot mode (prompt execution):
pty.spawn('claude', ['-p', '--dangerously-skip-permissions', prompt], { ... })
Interactive mode (persistent terminal):
pty.spawn('claude', ['--dangerously-skip-permissions'], { ... })
Idle Detection
Session detects idle by watching for prompt character (❯ or \u276f) and waiting 2 seconds without activity. RespawnController uses the same patterns plus spinner characters to detect working state.
Long-Running Session Support
Sessions are optimized for 12-24+ hour runs with automatic buffer management:
Buffer Limits:
- Terminal buffer: 5MB max, trims to 4MB when exceeded
- Text output: 2MB max, trims to 1.5MB when exceeded
- Messages: 1000 max, keeps most recent 800 when exceeded
Performance Optimizations:
- Server-side terminal batching at 60fps (16ms intervals)
- Client-side requestAnimationFrame batching for smooth rendering
- Buffer statistics available via session details for monitoring
Buffer Stats Response:
{
bufferStats: {
terminalBufferSize: number; // Current terminal buffer size in bytes
textOutputSize: number; // Current text output size in bytes
messageCount: number; // Number of parsed messages
maxTerminalBuffer: number; // Max allowed terminal buffer
maxTextOutput: number; // Max allowed text output
maxMessages: number; // Max allowed messages
}
}
Respawn Controller State Machine
WATCHING → SENDING_UPDATE → WAITING_UPDATE → SENDING_CLEAR → WAITING_CLEAR → SENDING_INIT → WAITING_INIT → WATCHING
Default config (RespawnConfig in src/types.ts):
idleTimeoutMs: 5000 (5s after prompt)updatePrompt: "update all the docs and CLAUDE.md"interStepDelayMs: 1000 (1s between steps)
SSE Event Catalog
All events are broadcast to clients connected to /api/events. Event format: { type: string, sessionId?: string, data: any }.
Session Events:
| Event | Data | Description |
|---|---|---|
session:created |
{ session } |
New session created |
session:deleted |
{ id } |
Session removed |
session:output |
{ id, data } |
Parsed output line (ANSI stripped) |
session:terminal |
{ id, data } |
Raw terminal data with ANSI codes |
session:message |
{ id, message } |
Parsed Claude JSON message |
session:running |
{ id, prompt } |
Prompt execution started |
session:interactive |
{ id } |
Interactive mode started |
session:completion |
{ id, result, cost } |
Prompt completed, includes cost |
session:exit |
{ id, code } |
Session process exited |
session:idle |
{ id } |
Session is idle (prompt detected) |
session:working |
{ id } |
Session is working (activity detected) |
session:updated |
{ session } |
Session state updated |
session:error |
{ id, error } |
Session error occurred |
Respawn Controller Events:
| Event | Data | Description |
|---|---|---|
respawn:started |
{ sessionId, status } |
Respawn controller started |
respawn:stopped |
{ sessionId } |
Respawn controller stopped |
respawn:stateChanged |
{ sessionId, state, prevState } |
State machine transition |
respawn:cycleStarted |
{ sessionId, cycleNumber } |
New update cycle starting |
respawn:cycleCompleted |
{ sessionId, cycleNumber } |
Update cycle finished |
respawn:stepSent |
{ sessionId, step, input } |
Command sent (update/clear/init) |
respawn:stepCompleted |
{ sessionId, step } |
Command completed |
respawn:configUpdated |
{ sessionId, config } |
Configuration changed |
respawn:log |
{ sessionId, message } |
Debug/info log message |
respawn:error |
{ sessionId, error } |
Error occurred |
Scheduled Run Events:
| Event | Data | Description |
|---|---|---|
scheduled:created |
{ run } |
New scheduled run created |
scheduled:updated |
{ run } |
Status/timer update |
scheduled:log |
{ id, log } |
Scheduled run log entry |
scheduled:completed |
{ run } |
Scheduled run finished |
scheduled:stopped |
{ run } |
Scheduled run stopped by user |
Case Events:
| Event | Data | Description |
|---|---|---|
case:created |
{ name, path } |
New case directory created |
Init Event:
| Event | Data | Description |
|---|---|---|
init |
{ sessions, scheduledRuns, respawnStatus, timestamp } |
Full state sent on SSE connection |
API Endpoints
Session Management
GET /api/sessions # List all sessions (includes buffer stats)
POST /api/sessions # Create session { workingDir }
GET /api/sessions/:id # Get single session details
DELETE /api/sessions/:id # Stop and remove a session (kills process + children)
DELETE /api/sessions # Kill all sessions at once
GET /api/sessions/:id/output # Get session output buffer
GET /api/sessions/:id/terminal # Get terminal buffer (raw ANSI)
Session Operations
POST /api/sessions/:id/run # Run prompt { prompt } (one-shot mode)
POST /api/sessions/:id/interactive # Start interactive terminal mode
POST /api/sessions/:id/input # Send input to interactive session { input }
POST /api/sessions/:id/resize # Resize terminal { cols, rows }
POST /api/sessions/:id/interactive-respawn # Start interactive + respawn controller
Respawn Controller
GET /api/sessions/:id/respawn # Get respawn controller state
POST /api/sessions/:id/respawn/start # Start respawn controller { config? }
POST /api/sessions/:id/respawn/stop # Stop respawn controller
PUT /api/sessions/:id/respawn/config # Update config { idleTimeoutMs, updatePrompt, interStepDelayMs }
Scheduled Runs
GET /api/scheduled # List all scheduled runs
POST /api/scheduled # Create { prompt, workingDir, durationMinutes }
GET /api/scheduled/:id # Get scheduled run details
DELETE /api/scheduled/:id # Cancel scheduled run
Cases & Quick Start
GET /api/cases # List case directories
POST /api/cases # Create case { name, description }
GET /api/cases/:name # Get case details
POST /api/quick-start # Quick start { caseName? } - creates case + interactive session
POST /api/run # Quick run { prompt, workingDir } (no session management)
Quick Start Response:
{
success: boolean;
sessionId?: string; // ID of the created session
casePath?: string; // Full path to the case directory
caseName?: string; // Name of the case
error?: string; // Error message if success is false
}
Events
GET /api/events # SSE stream (real-time events)
GET /api/status # Full state snapshot (sessions + scheduled + respawn)