mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
docs: add JSDoc comments to session, screen-manager, and state-store
- Add comprehensive module-level documentation - Document main classes with usage examples - Add parameter and return type documentation to key methods - Document constants and buffer limits Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+55
-2
@@ -1,3 +1,19 @@
|
||||
/**
|
||||
* @fileoverview GNU Screen session manager for persistent Claude sessions.
|
||||
*
|
||||
* This module provides the ScreenManager class which creates and manages
|
||||
* GNU Screen sessions that wrap Claude CLI processes. Screen provides:
|
||||
*
|
||||
* - **Persistence**: Sessions survive server restarts and disconnects
|
||||
* - **Ghost recovery**: Orphaned screens are discovered and reattached on startup
|
||||
* - **Resource tracking**: Memory, CPU, and child process stats per session
|
||||
* - **Reliable input**: `screen -X stuff` bypasses PTY for programmatic commands
|
||||
*
|
||||
* Screen sessions are named `claudeman-{sessionId}` and stored in ~/.claudeman/screens.json.
|
||||
*
|
||||
* @module screen-manager
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { spawn, execSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
@@ -5,11 +21,36 @@ import { dirname, join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { ScreenSession, ProcessStats, ScreenSessionWithStats } from './types.js';
|
||||
|
||||
/** Path to persisted screen session metadata */
|
||||
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
|
||||
|
||||
// Pre-compiled regex for screen list parsing
|
||||
/** Pre-compiled regex for parsing `screen -ls` output */
|
||||
const SCREEN_PATTERN = /(\d+)\.(claudeman-([a-f0-9-]+))/g;
|
||||
|
||||
/**
|
||||
* Manages GNU Screen sessions that wrap Claude CLI or shell processes.
|
||||
*
|
||||
* The ScreenManager maintains a registry of screen sessions, creates new ones,
|
||||
* kills them using a 4-strategy approach, and discovers orphaned "ghost" screens
|
||||
* from previous runs.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const manager = new ScreenManager();
|
||||
*
|
||||
* // Create a screen session for Claude
|
||||
* const screen = await manager.createScreen(sessionId, '/project', 'claude');
|
||||
*
|
||||
* // Send input to the screen
|
||||
* manager.sendInput(sessionId, '/clear\r');
|
||||
*
|
||||
* // Kill when done
|
||||
* await manager.killScreen(sessionId);
|
||||
* ```
|
||||
*
|
||||
* @fires ScreenManager#screenCreated - New screen session created
|
||||
* @fires ScreenManager#screenKilled - Screen session terminated
|
||||
*/
|
||||
export class ScreenManager extends EventEmitter {
|
||||
private screens: Map<string, ScreenSession> = new Map();
|
||||
private statsInterval: NodeJS.Timeout | null = null;
|
||||
@@ -50,7 +91,19 @@ export class ScreenManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new GNU screen session
|
||||
/**
|
||||
* Creates a new GNU Screen session wrapping Claude CLI or a shell.
|
||||
*
|
||||
* The screen is created in detached mode and automatically starts the
|
||||
* appropriate command based on the mode parameter.
|
||||
*
|
||||
* @param sessionId - Unique session identifier (used in screen name)
|
||||
* @param workingDir - Working directory for the screen session
|
||||
* @param mode - 'claude' for Claude CLI or 'shell' for bash
|
||||
* @param name - Optional display name for the session
|
||||
* @returns The created screen session metadata
|
||||
* @throws {Error} If screen creation fails
|
||||
*/
|
||||
async createScreen(sessionId: string, workingDir: string, mode: 'claude' | 'shell', name?: string): Promise<ScreenSession> {
|
||||
const screenName = `claudeman-${sessionId.slice(0, 8)}`;
|
||||
|
||||
|
||||
+211
-18
@@ -1,3 +1,20 @@
|
||||
/**
|
||||
* @fileoverview Core PTY session wrapper for Claude CLI interactions.
|
||||
*
|
||||
* This module provides the Session class which manages a PTY (pseudo-terminal)
|
||||
* process running the Claude CLI. It supports three operation modes:
|
||||
*
|
||||
* 1. **One-shot mode** (`runPrompt`): Execute a single prompt and get JSON response
|
||||
* 2. **Interactive mode** (`startInteractive`): Start an interactive Claude session
|
||||
* 3. **Shell mode**: Run a plain bash shell for debugging/testing
|
||||
*
|
||||
* The session can optionally run inside a GNU Screen session for persistence
|
||||
* across disconnects. It tracks tokens, costs, background tasks, and supports
|
||||
* auto-clear/auto-compact functionality when token limits are approached.
|
||||
*
|
||||
* @module session
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as pty from 'node-pty';
|
||||
@@ -10,18 +27,29 @@ import { ScreenManager } from './screen-manager.js';
|
||||
export type { BackgroundTask } from './task-tracker.js';
|
||||
export type { InnerLoopState, InnerTodoItem } from './types.js';
|
||||
|
||||
// Maximum terminal buffer size in characters (default 5MB of text)
|
||||
// ============================================================================
|
||||
// Buffer Size Constants
|
||||
// ============================================================================
|
||||
|
||||
/** Maximum terminal buffer size in characters (5MB) */
|
||||
const MAX_TERMINAL_BUFFER_SIZE = 5 * 1024 * 1024;
|
||||
// When trimming, keep the most recent portion (4MB)
|
||||
|
||||
/** When trimming terminal buffer, keep the most recent portion (4MB) */
|
||||
const TERMINAL_BUFFER_TRIM_SIZE = 4 * 1024 * 1024;
|
||||
// Maximum text output buffer size (2MB)
|
||||
|
||||
/** Maximum text output buffer size (2MB) - ANSI-stripped text */
|
||||
const MAX_TEXT_OUTPUT_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
/** When trimming text output, keep the most recent portion (1.5MB) */
|
||||
const TEXT_OUTPUT_TRIM_SIZE = 1.5 * 1024 * 1024;
|
||||
// Maximum number of Claude messages to keep in memory
|
||||
|
||||
/** Maximum number of Claude JSON messages to keep in memory */
|
||||
const MAX_MESSAGES = 1000;
|
||||
// Maximum line buffer size (64KB) - prevents unbounded growth for long lines
|
||||
|
||||
/** Maximum line buffer size (64KB) - prevents unbounded growth for long lines */
|
||||
const MAX_LINE_BUFFER_SIZE = 64 * 1024;
|
||||
// Line buffer flush interval (100ms) - forces processing of partial lines
|
||||
|
||||
/** Line buffer flush interval (100ms) - forces processing of partial lines */
|
||||
const LINE_BUFFER_FLUSH_INTERVAL = 100;
|
||||
|
||||
// Filter out terminal focus escape sequences (focus in/out reports)
|
||||
@@ -32,10 +60,18 @@ const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
||||
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
||||
|
||||
/**
|
||||
* Represents a JSON message from Claude CLI's stream-json output format.
|
||||
* Messages are newline-delimited JSON objects parsed from PTY output.
|
||||
*/
|
||||
export interface ClaudeMessage {
|
||||
/** Message type indicating the role or purpose */
|
||||
type: 'system' | 'assistant' | 'user' | 'result';
|
||||
/** Optional subtype for further classification */
|
||||
subtype?: string;
|
||||
/** Claude's internal session identifier */
|
||||
session_id?: string;
|
||||
/** Message content with optional token usage */
|
||||
message?: {
|
||||
content: Array<{ type: string; text?: string }>;
|
||||
usage?: {
|
||||
@@ -43,37 +79,93 @@ export interface ClaudeMessage {
|
||||
output_tokens: number;
|
||||
};
|
||||
};
|
||||
/** Final result text (on result messages) */
|
||||
result?: string;
|
||||
/** Whether this message represents an error */
|
||||
is_error?: boolean;
|
||||
/** Total cost in USD (on result messages) */
|
||||
total_cost_usd?: number;
|
||||
/** Total duration in milliseconds (on result messages) */
|
||||
duration_ms?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event signatures emitted by the Session class.
|
||||
* Subscribe using `session.on('eventName', handler)`.
|
||||
*/
|
||||
export interface SessionEvents {
|
||||
/** Processed text output (ANSI stripped) */
|
||||
output: (data: string) => void;
|
||||
/** Parsed JSON message from Claude CLI */
|
||||
message: (msg: ClaudeMessage) => void;
|
||||
/** Error output from the session */
|
||||
error: (data: string) => void;
|
||||
/** Session process exited */
|
||||
exit: (code: number | null) => void;
|
||||
/** One-shot prompt completed with result and cost */
|
||||
completion: (result: string, cost: number) => void;
|
||||
terminal: (data: string) => void; // Raw terminal data
|
||||
clearTerminal: () => void; // Signal client to clear terminal (after screen attach)
|
||||
// Background task events
|
||||
/** Raw terminal data (includes ANSI codes) */
|
||||
terminal: (data: string) => void;
|
||||
/** Signal to clear terminal display (after screen attach) */
|
||||
clearTerminal: () => void;
|
||||
/** New background task started */
|
||||
taskCreated: (task: BackgroundTask) => void;
|
||||
/** Background task status changed */
|
||||
taskUpdated: (task: BackgroundTask) => void;
|
||||
/** Background task finished successfully */
|
||||
taskCompleted: (task: BackgroundTask) => void;
|
||||
/** Background task failed with error */
|
||||
taskFailed: (task: BackgroundTask, error: string) => void;
|
||||
// Auto-clear event
|
||||
/** Auto-clear triggered due to token threshold */
|
||||
autoClear: (data: { tokens: number; threshold: number }) => void;
|
||||
// Auto-compact event
|
||||
/** Auto-compact triggered due to token threshold */
|
||||
autoCompact: (data: { tokens: number; threshold: number; prompt?: string }) => void;
|
||||
// Inner loop tracking events (Claude Code running inside this session)
|
||||
/** Inner loop (Ralph Wiggum) state changed */
|
||||
innerLoopUpdate: (state: InnerLoopState) => void;
|
||||
/** Inner loop todo list updated */
|
||||
innerTodoUpdate: (todos: InnerTodoItem[]) => void;
|
||||
/** Inner loop completion phrase detected */
|
||||
innerCompletionDetected: (phrase: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session operation mode.
|
||||
* - `'claude'`: Runs Claude CLI for AI interactions (default)
|
||||
* - `'shell'`: Runs a plain bash shell for debugging/testing
|
||||
*/
|
||||
export type SessionMode = 'claude' | 'shell';
|
||||
|
||||
/**
|
||||
* Core session class that wraps a PTY process running Claude CLI or a shell.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create and start an interactive Claude session
|
||||
* const session = new Session({
|
||||
* workingDir: '/path/to/project',
|
||||
* screenManager: screenManager,
|
||||
* useScreen: true
|
||||
* });
|
||||
* await session.startInteractive();
|
||||
*
|
||||
* // Listen for events
|
||||
* session.on('terminal', (data) => console.log(data));
|
||||
* session.on('message', (msg) => console.log('Claude:', msg));
|
||||
*
|
||||
* // Send input
|
||||
* session.write('Hello Claude!\r');
|
||||
*
|
||||
* // Stop when done
|
||||
* await session.stop();
|
||||
* ```
|
||||
*
|
||||
* @fires Session#terminal - Raw terminal output
|
||||
* @fires Session#message - Parsed Claude JSON message
|
||||
* @fires Session#completion - One-shot prompt completed
|
||||
* @fires Session#exit - Process exited
|
||||
* @fires Session#autoClear - Token threshold reached, clearing context
|
||||
* @fires Session#autoCompact - Token threshold reached, compacting context
|
||||
*/
|
||||
export class Session extends EventEmitter {
|
||||
readonly id: string;
|
||||
readonly workingDir: string;
|
||||
@@ -364,7 +456,23 @@ export class Session extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
// Start an interactive Claude Code session (full terminal)
|
||||
/**
|
||||
* Starts an interactive Claude CLI session with full terminal support.
|
||||
*
|
||||
* This spawns Claude CLI with `--dangerously-skip-permissions` flag in
|
||||
* interactive mode. If screen wrapping is enabled, the session runs inside
|
||||
* a GNU Screen session for persistence across disconnects.
|
||||
*
|
||||
* @throws {Error} If a process is already running in this session
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const session = new Session({ workingDir: '/project', useScreen: true });
|
||||
* await session.startInteractive();
|
||||
* session.on('terminal', (data) => process.stdout.write(data));
|
||||
* session.write('help me with this code\r');
|
||||
* ```
|
||||
*/
|
||||
async startInteractive(): Promise<void> {
|
||||
if (this.ptyProcess) {
|
||||
throw new Error('Session already has a running process');
|
||||
@@ -515,7 +623,21 @@ export class Session extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
// Start a plain shell session (bash/zsh without Claude)
|
||||
/**
|
||||
* Starts a plain shell session (bash/zsh) without Claude CLI.
|
||||
*
|
||||
* Useful for debugging, testing, or when you just need a terminal.
|
||||
* Uses the user's default shell from $SHELL or falls back to /bin/bash.
|
||||
*
|
||||
* @throws {Error} If a process is already running in this session
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const session = new Session({ workingDir: '/project', mode: 'shell' });
|
||||
* await session.startShell();
|
||||
* session.write('ls -la\r');
|
||||
* ```
|
||||
*/
|
||||
async startShell(): Promise<void> {
|
||||
if (this.ptyProcess) {
|
||||
throw new Error('Session already has a running process');
|
||||
@@ -629,6 +751,25 @@ export class Session extends EventEmitter {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a one-shot prompt and returns the result.
|
||||
*
|
||||
* This spawns Claude CLI with `--output-format stream-json` to get
|
||||
* structured JSON output. The promise resolves when Claude completes
|
||||
* the response.
|
||||
*
|
||||
* @param prompt - The prompt text to send to Claude
|
||||
* @returns Promise resolving to the result text and total cost in USD
|
||||
* @throws {Error} If a process is already running in this session
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const session = new Session({ workingDir: '/project' });
|
||||
* const { result, cost } = await session.runPrompt('Explain this code');
|
||||
* console.log(`Response: ${result}`);
|
||||
* console.log(`Cost: $${cost.toFixed(4)}`);
|
||||
* ```
|
||||
*/
|
||||
async runPrompt(prompt: string): Promise<{ result: string; cost: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.ptyProcess) {
|
||||
@@ -930,15 +1071,43 @@ export class Session extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Send input to the PTY (for interactive sessions)
|
||||
/**
|
||||
* Sends input directly to the PTY process.
|
||||
*
|
||||
* For interactive sessions, this is how you send user input to Claude.
|
||||
* Remember to include `\r` (carriage return) to simulate pressing Enter.
|
||||
*
|
||||
* @param data - The input data to send (text, escape sequences, etc.)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* session.write('hello world'); // Text only, no Enter
|
||||
* session.write('\r'); // Enter key
|
||||
* session.write('ls -la\r'); // Command with Enter
|
||||
* ```
|
||||
*/
|
||||
write(data: string): void {
|
||||
if (this.ptyProcess) {
|
||||
this.ptyProcess.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
// Send input via screen -X stuff (for programmatic input like respawn controller)
|
||||
// This bypasses PTY and sends directly to screen, more reliable for Enter key
|
||||
/**
|
||||
* Sends input via GNU Screen's `screen -X stuff` command.
|
||||
*
|
||||
* More reliable than direct PTY write for programmatic input, especially
|
||||
* with Claude CLI which uses Ink (React for terminals). Text and Enter
|
||||
* are sent as separate commands internally.
|
||||
*
|
||||
* @param data - Input data with optional `\r` for Enter
|
||||
* @returns true if input was sent, false if no screen session or PTY
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* session.writeViaScreen('/clear\r'); // Send /clear command
|
||||
* session.writeViaScreen('/init\r'); // Send /init command
|
||||
* ```
|
||||
*/
|
||||
writeViaScreen(data: string): boolean {
|
||||
if (this._screenManager && this._screenSession) {
|
||||
return this._screenManager.sendInput(this.id, data);
|
||||
@@ -951,7 +1120,14 @@ export class Session extends EventEmitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resize the PTY
|
||||
/**
|
||||
* Resizes the PTY terminal dimensions.
|
||||
*
|
||||
* Call this when the frontend terminal is resized to keep PTY in sync.
|
||||
*
|
||||
* @param cols - Number of columns (width in characters)
|
||||
* @param rows - Number of rows (height in lines)
|
||||
*/
|
||||
resize(cols: number, rows: number): void {
|
||||
if (this.ptyProcess) {
|
||||
this.ptyProcess.resize(cols, rows);
|
||||
@@ -972,6 +1148,23 @@ export class Session extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the session and cleans up resources.
|
||||
*
|
||||
* This kills the PTY process and optionally the associated GNU Screen
|
||||
* session. All buffers are cleared and the session is marked as stopped.
|
||||
*
|
||||
* @param killScreen - Whether to also kill the screen session (default: true)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Stop and kill everything
|
||||
* await session.stop();
|
||||
*
|
||||
* // Stop but keep screen running for later reattachment
|
||||
* await session.stop(false);
|
||||
* ```
|
||||
*/
|
||||
async stop(killScreen: boolean = true): Promise<void> {
|
||||
// Clear activity timeout to prevent memory leak
|
||||
if (this.activityTimeout) {
|
||||
|
||||
+38
-1
@@ -1,11 +1,48 @@
|
||||
/**
|
||||
* @fileoverview Persistent JSON state storage for Claudeman.
|
||||
*
|
||||
* This module provides the StateStore class which persists application state
|
||||
* to `~/.claudeman/state.json` with debounced writes to prevent excessive disk I/O.
|
||||
*
|
||||
* State is split into two files:
|
||||
* - `state.json`: Main app state (sessions, tasks, config)
|
||||
* - `state-inner.json`: Inner loop state (todos, Ralph loop state per session)
|
||||
*
|
||||
* The separation reduces write frequency since inner loop state changes rapidly
|
||||
* during Ralph Wiggum loops.
|
||||
*
|
||||
* @module state-store
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { AppState, createInitialState, InnerSessionState, createInitialInnerSessionState } from './types.js';
|
||||
|
||||
// Debounce delay for batching state writes (ms)
|
||||
/** Debounce delay for batching state writes (ms) */
|
||||
const SAVE_DEBOUNCE_MS = 500;
|
||||
|
||||
/**
|
||||
* Persistent JSON state storage with debounced writes.
|
||||
*
|
||||
* State is automatically loaded on construction and saved with 500ms
|
||||
* debouncing to batch rapid updates into single disk writes.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const store = new StateStore();
|
||||
*
|
||||
* // Read state
|
||||
* const sessions = store.getState().sessions;
|
||||
*
|
||||
* // Modify and save
|
||||
* store.getState().sessions[id] = sessionState;
|
||||
* store.save(); // Debounced - won't write immediately
|
||||
*
|
||||
* // Force immediate write
|
||||
* store.saveNow();
|
||||
* ```
|
||||
*/
|
||||
export class StateStore {
|
||||
private state: AppState;
|
||||
private filePath: string;
|
||||
|
||||
Reference in New Issue
Block a user