mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat: implement Claudeman - Claude session manager with Ralph Loop
Claudeman manages multiple Claude CLI sessions as subprocesses with: - Session management (start/stop/list/logs) - Priority-based task queue with dependency support - Ralph Loop for autonomous task assignment and completion detection - Time-aware loops for extended work sessions - JSON file persistence to ~/.claudeman/state.json Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"permissions": {
|
||||
"defaultMode": "bypassPermissions"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,65 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What This Repository Is
|
||||
|
||||
This is a **Claude Code configuration template** containing reusable CLAUDE.md and settings files. Copy these to new projects to bootstrap Claude Code configuration.
|
||||
|
||||
### Files to Copy
|
||||
- `CLAUDE.md` → project root (then customize the Project Overview section)
|
||||
- `.claude/settings.json` → `.claude/settings.json`
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
<!-- Customize this section when using in a real project -->
|
||||
- **Project Name**: claudeman (Claude Code config template)
|
||||
- **Description**: Reusable Claude Code configuration files
|
||||
- **Last Updated**: 2026-01-18
|
||||
|
||||
---
|
||||
|
||||
## Work Principles
|
||||
|
||||
### Autonomy
|
||||
Full permissions granted via `.claude/settings.json`. Act decisively - read, write, edit, execute freely.
|
||||
|
||||
### Git Discipline
|
||||
- Commit after every meaningful change - never batch unrelated work
|
||||
- Use conventional commits: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`
|
||||
- Commit message = what changed + why (not how)
|
||||
|
||||
### Planning Mode
|
||||
**Automatically enter planning mode** when:
|
||||
- Multi-file changes (3+ files)
|
||||
- Architectural decisions
|
||||
- New feature implementation
|
||||
- Refactoring existing functionality
|
||||
|
||||
**Skip planning mode** for: single-file fixes, typo corrections, simple config changes.
|
||||
|
||||
---
|
||||
|
||||
## Ralph Loop (Autonomous Work)
|
||||
|
||||
Start with `/ralph-loop`, cancel with `/cancel-ralph`.
|
||||
|
||||
Ralph loops enable persistent autonomous work. When active, continue iterating until completion criteria are met.
|
||||
|
||||
### Core Behaviors
|
||||
1. Work incrementally - one sub-task at a time
|
||||
2. Commit after each completion
|
||||
3. Self-correct: implement → test → fix failures → lint → fix errors → commit
|
||||
4. Only output completion phrase when ALL requirements done AND tests pass
|
||||
|
||||
### Time-Aware Loops
|
||||
When given a minimum duration, track elapsed time and self-generate additional tasks if primary work completes early. Only output completion phrase when both tasks AND time requirements are met.
|
||||
|
||||
---
|
||||
|
||||
## Session Log
|
||||
|
||||
| Date | Tasks Completed | Files Changed | Notes |
|
||||
|------|-----------------|---------------|-------|
|
||||
| 2026-01-18 | Improve CLAUDE.md template | CLAUDE.md | Made more concise |
|
||||
@@ -0,0 +1,203 @@
|
||||
# Claudeman
|
||||
|
||||
A Claude Code session manager with an autonomous Ralph Loop for task assignment and monitoring.
|
||||
|
||||
## Features
|
||||
|
||||
- **Session Management**: Spawn and manage multiple Claude CLI sessions as subprocesses
|
||||
- **Task Queue**: Priority-based task queue with dependency support
|
||||
- **Ralph Loop**: Autonomous control loop that assigns tasks to idle sessions and monitors completion
|
||||
- **Time-Aware Loops**: Set minimum durations for extended autonomous work sessions
|
||||
- **State Persistence**: All state persisted to `~/.claudeman/state.json`
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm link # Optional: make 'claudeman' available globally
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Start a Claude session
|
||||
claudeman start --dir /path/to/project
|
||||
|
||||
# Add tasks to the queue
|
||||
claudeman task add "Fix the bug in auth.ts"
|
||||
claudeman task add "Add tests for the API" --priority 5
|
||||
|
||||
# Start the Ralph loop to process tasks
|
||||
claudeman ralph start
|
||||
|
||||
# Check status
|
||||
claudeman status
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Session Management
|
||||
|
||||
```bash
|
||||
# Start a new session
|
||||
claudeman session start [--dir <path>]
|
||||
claudeman start [--dir <path>] # shorthand
|
||||
|
||||
# Stop a session
|
||||
claudeman session stop <session-id>
|
||||
|
||||
# List sessions
|
||||
claudeman session list
|
||||
claudeman list # shorthand
|
||||
|
||||
# View session output
|
||||
claudeman session logs <session-id>
|
||||
claudeman session logs <session-id> --errors # stderr
|
||||
```
|
||||
|
||||
### Task Management
|
||||
|
||||
```bash
|
||||
# Add a task
|
||||
claudeman task add "<prompt>" [options]
|
||||
--dir <path> Working directory
|
||||
--priority <n> Priority (higher = processed first)
|
||||
--completion <phrase> Custom completion phrase to detect
|
||||
--timeout <ms> Task timeout in milliseconds
|
||||
|
||||
# List tasks
|
||||
claudeman task list
|
||||
claudeman task list --status pending
|
||||
|
||||
# View task details
|
||||
claudeman task status <task-id>
|
||||
|
||||
# Remove a task
|
||||
claudeman task remove <task-id>
|
||||
|
||||
# Clear tasks
|
||||
claudeman task clear # completed tasks
|
||||
claudeman task clear --failed # failed tasks
|
||||
claudeman task clear --all # all tasks
|
||||
```
|
||||
|
||||
### Ralph Loop
|
||||
|
||||
```bash
|
||||
# Start the autonomous loop
|
||||
claudeman ralph start
|
||||
claudeman ralph start --min-hours 4 # run for at least 4 hours
|
||||
claudeman ralph start --no-auto-generate # disable auto task generation
|
||||
|
||||
# Stop the loop
|
||||
claudeman ralph stop
|
||||
|
||||
# Check loop status
|
||||
claudeman ralph status
|
||||
```
|
||||
|
||||
### Utility
|
||||
|
||||
```bash
|
||||
# Overall status
|
||||
claudeman status
|
||||
|
||||
# Reset all state (stops sessions, clears tasks)
|
||||
claudeman reset --force
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Claudeman CLI │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Ralph Loop Controller │
|
||||
│ - Monitors all sessions │
|
||||
│ - Assigns tasks from queue │
|
||||
│ - Detects completion/failure │
|
||||
│ - Self-generates follow-up tasks │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Session Manager │ Task Queue │
|
||||
│ - Spawn claude processes │ - Priority queue │
|
||||
│ - Track stdin/stdout/stderr │ - Task definitions │
|
||||
│ - Health monitoring │ - Dependencies │
|
||||
│ - Graceful shutdown │ - Status tracking │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ State Store (JSON file persistence) │
|
||||
│ - Sessions, tasks, logs │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Completion Detection
|
||||
|
||||
The Ralph Loop detects task completion by looking for:
|
||||
|
||||
1. **Promise tags**: `<promise>COMPLETE</promise>` or custom phrases
|
||||
2. **Common indicators**: "Task completed successfully", "All tasks done", "✓ Complete"
|
||||
|
||||
When creating tasks, you can specify a custom completion phrase:
|
||||
|
||||
```bash
|
||||
claudeman task add "Refactor the auth module" --completion "AUTH_REFACTOR_DONE"
|
||||
```
|
||||
|
||||
The session output will be scanned for `<promise>AUTH_REFACTOR_DONE</promise>`.
|
||||
|
||||
## Time-Aware Loops
|
||||
|
||||
For extended autonomous work sessions:
|
||||
|
||||
```bash
|
||||
claudeman ralph start --min-hours 8
|
||||
```
|
||||
|
||||
When the minimum duration hasn't been reached and all tasks are complete, the Ralph Loop will auto-generate follow-up tasks like:
|
||||
|
||||
- Review and optimize recently changed code
|
||||
- Add tests for uncovered code paths
|
||||
- Update documentation
|
||||
- Check for security vulnerabilities
|
||||
- Run linting and fix issues
|
||||
|
||||
## State File
|
||||
|
||||
All state is persisted to `~/.claudeman/state.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessions": { ... },
|
||||
"tasks": { ... },
|
||||
"ralphLoop": {
|
||||
"status": "running",
|
||||
"startedAt": 1234567890,
|
||||
"minDurationMs": 14400000,
|
||||
"tasksCompleted": 5,
|
||||
"tasksGenerated": 2
|
||||
},
|
||||
"config": {
|
||||
"pollIntervalMs": 1000,
|
||||
"defaultTimeoutMs": 300000,
|
||||
"maxConcurrentSessions": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Run in development mode
|
||||
npm run dev -- start
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Clean build artifacts
|
||||
npm run clean
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- Claude CLI (`claude`) installed and available in PATH
|
||||
Generated
+644
@@ -0,0 +1,644 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claudeman",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"claudeman": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
||||
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
||||
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
||||
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
||||
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
||||
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
||||
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
||||
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
||||
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
||||
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
|
||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.2",
|
||||
"@esbuild/android-arm": "0.27.2",
|
||||
"@esbuild/android-arm64": "0.27.2",
|
||||
"@esbuild/android-x64": "0.27.2",
|
||||
"@esbuild/darwin-arm64": "0.27.2",
|
||||
"@esbuild/darwin-x64": "0.27.2",
|
||||
"@esbuild/freebsd-arm64": "0.27.2",
|
||||
"@esbuild/freebsd-x64": "0.27.2",
|
||||
"@esbuild/linux-arm": "0.27.2",
|
||||
"@esbuild/linux-arm64": "0.27.2",
|
||||
"@esbuild/linux-ia32": "0.27.2",
|
||||
"@esbuild/linux-loong64": "0.27.2",
|
||||
"@esbuild/linux-mips64el": "0.27.2",
|
||||
"@esbuild/linux-ppc64": "0.27.2",
|
||||
"@esbuild/linux-riscv64": "0.27.2",
|
||||
"@esbuild/linux-s390x": "0.27.2",
|
||||
"@esbuild/linux-x64": "0.27.2",
|
||||
"@esbuild/netbsd-arm64": "0.27.2",
|
||||
"@esbuild/netbsd-x64": "0.27.2",
|
||||
"@esbuild/openbsd-arm64": "0.27.2",
|
||||
"@esbuild/openbsd-x64": "0.27.2",
|
||||
"@esbuild/openharmony-arm64": "0.27.2",
|
||||
"@esbuild/sunos-x64": "0.27.2",
|
||||
"@esbuild/win32-arm64": "0.27.2",
|
||||
"@esbuild/win32-ia32": "0.27.2",
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
|
||||
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "1.0.0",
|
||||
"description": "Claude Code session manager with autonomous Ralph Loop",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"claudeman": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
"cli",
|
||||
"session-manager",
|
||||
"automation"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"tsx": "^4.15.0",
|
||||
"typescript": "^5.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { getSessionManager } from './session-manager.js';
|
||||
import { getTaskQueue } from './task-queue.js';
|
||||
import { getRalphLoop } from './ralph-loop.js';
|
||||
import { getStore } from './state-store.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('claudeman')
|
||||
.description('Claude Code session manager with autonomous Ralph Loop')
|
||||
.version('1.0.0');
|
||||
|
||||
// ============ Session Commands ============
|
||||
|
||||
const sessionCmd = program
|
||||
.command('session')
|
||||
.alias('s')
|
||||
.description('Manage Claude sessions');
|
||||
|
||||
sessionCmd
|
||||
.command('start')
|
||||
.description('Start a new Claude session')
|
||||
.option('-d, --dir <path>', 'Working directory', process.cwd())
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const manager = getSessionManager();
|
||||
const session = await manager.createSession(options.dir);
|
||||
console.log(chalk.green(`✓ Session started: ${session.id}`));
|
||||
console.log(` Working directory: ${session.workingDir}`);
|
||||
console.log(` PID: ${session.pid}`);
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`✗ Failed to start session: ${(err as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('stop <id>')
|
||||
.description('Stop a session')
|
||||
.action(async (id) => {
|
||||
try {
|
||||
const manager = getSessionManager();
|
||||
await manager.stopSession(id);
|
||||
console.log(chalk.green(`✓ Session stopped: ${id}`));
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`✗ Failed to stop session: ${(err as Error).message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('list')
|
||||
.alias('ls')
|
||||
.description('List all sessions')
|
||||
.action(() => {
|
||||
const manager = getSessionManager();
|
||||
const sessions = manager.getAllSessions();
|
||||
const stored = manager.getStoredSessions();
|
||||
|
||||
if (sessions.length === 0 && Object.keys(stored).length === 0) {
|
||||
console.log(chalk.yellow('No sessions found'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\nActive Sessions:'));
|
||||
if (sessions.length === 0) {
|
||||
console.log(' (none)');
|
||||
} else {
|
||||
for (const session of sessions) {
|
||||
const status = session.status === 'idle'
|
||||
? chalk.green('idle')
|
||||
: session.status === 'busy'
|
||||
? chalk.yellow('busy')
|
||||
: chalk.red(session.status);
|
||||
console.log(` ${chalk.cyan(session.id.slice(0, 8))} ${status} ${session.workingDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
const stoppedSessions = Object.values(stored).filter((s) => s.status === 'stopped');
|
||||
if (stoppedSessions.length > 0) {
|
||||
console.log(chalk.bold('\nStopped Sessions:'));
|
||||
for (const session of stoppedSessions) {
|
||||
console.log(` ${chalk.gray(session.id.slice(0, 8))} ${chalk.gray('stopped')} ${session.workingDir}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
|
||||
sessionCmd
|
||||
.command('logs <id>')
|
||||
.description('View session output')
|
||||
.option('-e, --errors', 'Show stderr instead of stdout')
|
||||
.action((id, options) => {
|
||||
const manager = getSessionManager();
|
||||
const output = options.errors
|
||||
? manager.getSessionError(id)
|
||||
: manager.getSessionOutput(id);
|
||||
|
||||
if (output === null) {
|
||||
console.log(chalk.yellow(`Session ${id} not found or not active`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (output === '') {
|
||||
console.log(chalk.gray('(no output)'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(output);
|
||||
});
|
||||
|
||||
// ============ Task Commands ============
|
||||
|
||||
const taskCmd = program
|
||||
.command('task')
|
||||
.alias('t')
|
||||
.description('Manage tasks');
|
||||
|
||||
taskCmd
|
||||
.command('add <prompt>')
|
||||
.description('Add a new task')
|
||||
.option('-d, --dir <path>', 'Working directory', process.cwd())
|
||||
.option('-p, --priority <n>', 'Priority (higher = first)', '0')
|
||||
.option('-c, --completion <phrase>', 'Completion phrase to detect')
|
||||
.option('--timeout <ms>', 'Timeout in milliseconds')
|
||||
.action((prompt, options) => {
|
||||
const queue = getTaskQueue();
|
||||
const task = queue.addTask({
|
||||
prompt,
|
||||
workingDir: options.dir,
|
||||
priority: parseInt(options.priority, 10),
|
||||
completionPhrase: options.completion,
|
||||
timeoutMs: options.timeout ? parseInt(options.timeout, 10) : undefined,
|
||||
});
|
||||
console.log(chalk.green(`✓ Task added: ${task.id}`));
|
||||
console.log(` Prompt: ${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}`);
|
||||
console.log(` Priority: ${task.priority}`);
|
||||
});
|
||||
|
||||
taskCmd
|
||||
.command('list')
|
||||
.alias('ls')
|
||||
.description('List all tasks')
|
||||
.option('-s, --status <status>', 'Filter by status (pending, running, completed, failed)')
|
||||
.action((options) => {
|
||||
const queue = getTaskQueue();
|
||||
let tasks = queue.getAllTasks();
|
||||
|
||||
if (options.status) {
|
||||
tasks = tasks.filter((t) => t.status === options.status);
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
console.log(chalk.yellow('No tasks found'));
|
||||
return;
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
pending: chalk.gray,
|
||||
running: chalk.yellow,
|
||||
completed: chalk.green,
|
||||
failed: chalk.red,
|
||||
};
|
||||
|
||||
console.log(chalk.bold('\nTasks:'));
|
||||
for (const task of tasks) {
|
||||
const color = statusColors[task.status];
|
||||
const prompt = task.prompt.slice(0, 40) + (task.prompt.length > 40 ? '...' : '');
|
||||
console.log(` ${chalk.cyan(task.id.slice(0, 8))} ${color(task.status.padEnd(10))} [${task.priority}] ${prompt}`);
|
||||
}
|
||||
|
||||
const counts = queue.getCount();
|
||||
console.log(chalk.bold('\nSummary:'));
|
||||
console.log(` Pending: ${counts.pending}, Running: ${counts.running}, Completed: ${counts.completed}, Failed: ${counts.failed}`);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
taskCmd
|
||||
.command('status <id>')
|
||||
.description('Show task details')
|
||||
.action((id) => {
|
||||
const queue = getTaskQueue();
|
||||
const task = queue.getTask(id);
|
||||
|
||||
if (!task) {
|
||||
console.log(chalk.red(`Task ${id} not found`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\nTask Details:'));
|
||||
console.log(` ID: ${task.id}`);
|
||||
console.log(` Status: ${task.status}`);
|
||||
console.log(` Priority: ${task.priority}`);
|
||||
console.log(` Prompt: ${task.prompt}`);
|
||||
console.log(` Working Dir: ${task.workingDir}`);
|
||||
if (task.assignedSessionId) {
|
||||
console.log(` Session: ${task.assignedSessionId}`);
|
||||
}
|
||||
if (task.error) {
|
||||
console.log(` Error: ${chalk.red(task.error)}`);
|
||||
}
|
||||
if (task.output) {
|
||||
console.log(chalk.bold('\nOutput:'));
|
||||
console.log(task.output.slice(0, 500) + (task.output.length > 500 ? '...' : ''));
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
|
||||
taskCmd
|
||||
.command('remove <id>')
|
||||
.alias('rm')
|
||||
.description('Remove a task')
|
||||
.action((id) => {
|
||||
const queue = getTaskQueue();
|
||||
if (queue.removeTask(id)) {
|
||||
console.log(chalk.green(`✓ Task removed: ${id}`));
|
||||
} else {
|
||||
console.log(chalk.red(`Task ${id} not found`));
|
||||
}
|
||||
});
|
||||
|
||||
taskCmd
|
||||
.command('clear')
|
||||
.description('Clear completed/failed tasks')
|
||||
.option('-a, --all', 'Clear all tasks')
|
||||
.option('-f, --failed', 'Clear only failed tasks')
|
||||
.action((options) => {
|
||||
const queue = getTaskQueue();
|
||||
let count: number;
|
||||
|
||||
if (options.all) {
|
||||
count = queue.clearAll();
|
||||
console.log(chalk.green(`✓ Cleared ${count} tasks`));
|
||||
} else if (options.failed) {
|
||||
count = queue.clearFailed();
|
||||
console.log(chalk.green(`✓ Cleared ${count} failed tasks`));
|
||||
} else {
|
||||
count = queue.clearCompleted();
|
||||
console.log(chalk.green(`✓ Cleared ${count} completed tasks`));
|
||||
}
|
||||
});
|
||||
|
||||
// ============ Ralph Loop Commands ============
|
||||
|
||||
const ralphCmd = program
|
||||
.command('ralph')
|
||||
.alias('r')
|
||||
.description('Control the Ralph autonomous loop');
|
||||
|
||||
ralphCmd
|
||||
.command('start')
|
||||
.description('Start the Ralph loop')
|
||||
.option('-m, --min-hours <hours>', 'Minimum duration in hours')
|
||||
.option('--no-auto-generate', 'Disable auto-generating follow-up tasks')
|
||||
.action(async (options) => {
|
||||
const loop = getRalphLoop({
|
||||
autoGenerateTasks: options.autoGenerate,
|
||||
});
|
||||
|
||||
if (options.minHours) {
|
||||
loop.setMinDuration(parseFloat(options.minHours));
|
||||
}
|
||||
|
||||
if (loop.isRunning()) {
|
||||
console.log(chalk.yellow('Ralph loop is already running'));
|
||||
return;
|
||||
}
|
||||
|
||||
loop.on('taskAssigned', (taskId, sessionId) => {
|
||||
console.log(chalk.cyan(`→ Task ${taskId.slice(0, 8)} assigned to session ${sessionId.slice(0, 8)}`));
|
||||
});
|
||||
|
||||
loop.on('taskCompleted', (taskId) => {
|
||||
console.log(chalk.green(`✓ Task ${taskId.slice(0, 8)} completed`));
|
||||
});
|
||||
|
||||
loop.on('taskFailed', (taskId, error) => {
|
||||
console.log(chalk.red(`✗ Task ${taskId.slice(0, 8)} failed: ${error}`));
|
||||
});
|
||||
|
||||
loop.on('stopped', () => {
|
||||
console.log(chalk.yellow('\nRalph loop stopped'));
|
||||
printStats(loop.getStats());
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
await loop.start();
|
||||
console.log(chalk.green('✓ Ralph loop started'));
|
||||
if (options.minHours) {
|
||||
console.log(` Minimum duration: ${options.minHours} hours`);
|
||||
}
|
||||
console.log(chalk.gray(' Press Ctrl+C to stop\n'));
|
||||
|
||||
// Keep process running
|
||||
process.on('SIGINT', () => {
|
||||
console.log(chalk.yellow('\nStopping Ralph loop...'));
|
||||
loop.stop();
|
||||
});
|
||||
});
|
||||
|
||||
ralphCmd
|
||||
.command('stop')
|
||||
.description('Stop the Ralph loop')
|
||||
.action(() => {
|
||||
const loop = getRalphLoop();
|
||||
if (!loop.isRunning()) {
|
||||
console.log(chalk.yellow('Ralph loop is not running'));
|
||||
return;
|
||||
}
|
||||
loop.stop();
|
||||
console.log(chalk.green('✓ Ralph loop stopped'));
|
||||
});
|
||||
|
||||
ralphCmd
|
||||
.command('status')
|
||||
.description('Show Ralph loop status')
|
||||
.action(() => {
|
||||
const loop = getRalphLoop();
|
||||
const stats = loop.getStats();
|
||||
printStats(stats);
|
||||
});
|
||||
|
||||
function printStats(stats: ReturnType<ReturnType<typeof getRalphLoop>['getStats']>) {
|
||||
const statusColor =
|
||||
stats.status === 'running' ? chalk.green :
|
||||
stats.status === 'paused' ? chalk.yellow :
|
||||
chalk.gray;
|
||||
|
||||
console.log(chalk.bold('\nRalph Loop Status:'));
|
||||
console.log(` Status: ${statusColor(stats.status)}`);
|
||||
console.log(` Elapsed: ${stats.elapsedHours.toFixed(2)} hours`);
|
||||
if (stats.minDurationMs) {
|
||||
const minHours = stats.minDurationMs / (1000 * 60 * 60);
|
||||
console.log(` Min Duration: ${minHours.toFixed(2)} hours (${stats.minDurationReached ? 'reached' : 'not reached'})`);
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\nTasks:'));
|
||||
console.log(` Pending: ${stats.pending}`);
|
||||
console.log(` Running: ${stats.running}`);
|
||||
console.log(` Completed: ${stats.completed} (${stats.tasksCompleted} this session)`);
|
||||
console.log(` Failed: ${stats.failed}`);
|
||||
console.log(` Generated: ${stats.tasksGenerated}`);
|
||||
|
||||
console.log(chalk.bold('\nSessions:'));
|
||||
console.log(` Active: ${stats.activeSessions}`);
|
||||
console.log(` Idle: ${stats.idleSessions}`);
|
||||
console.log(` Busy: ${stats.busySessions}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// ============ Utility Commands ============
|
||||
|
||||
program
|
||||
.command('status')
|
||||
.description('Show overall status')
|
||||
.action(() => {
|
||||
const manager = getSessionManager();
|
||||
const queue = getTaskQueue();
|
||||
const loop = getRalphLoop();
|
||||
|
||||
const sessions = manager.getAllSessions();
|
||||
const taskCounts = queue.getCount();
|
||||
const loopStatus = loop.status;
|
||||
|
||||
console.log(chalk.bold('\nClaudeman Status'));
|
||||
console.log('─'.repeat(40));
|
||||
|
||||
console.log(chalk.bold('\nSessions:'));
|
||||
console.log(` Active: ${sessions.length}`);
|
||||
console.log(` Idle: ${sessions.filter((s) => s.isIdle()).length}`);
|
||||
console.log(` Busy: ${sessions.filter((s) => s.isBusy()).length}`);
|
||||
|
||||
console.log(chalk.bold('\nTasks:'));
|
||||
console.log(` Total: ${taskCounts.total}`);
|
||||
console.log(` Pending: ${taskCounts.pending}`);
|
||||
console.log(` Running: ${taskCounts.running}`);
|
||||
console.log(` Completed: ${taskCounts.completed}`);
|
||||
console.log(` Failed: ${taskCounts.failed}`);
|
||||
|
||||
const statusColor =
|
||||
loopStatus === 'running' ? chalk.green :
|
||||
loopStatus === 'paused' ? chalk.yellow :
|
||||
chalk.gray;
|
||||
console.log(chalk.bold('\nRalph Loop:'));
|
||||
console.log(` Status: ${statusColor(loopStatus)}`);
|
||||
console.log('');
|
||||
});
|
||||
|
||||
program
|
||||
.command('reset')
|
||||
.description('Reset all state')
|
||||
.option('-f, --force', 'Skip confirmation')
|
||||
.action(async (options) => {
|
||||
if (!options.force) {
|
||||
console.log(chalk.yellow('This will stop all sessions and clear all state.'));
|
||||
console.log(chalk.yellow('Use --force to confirm.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const manager = getSessionManager();
|
||||
const store = getStore();
|
||||
|
||||
await manager.stopAllSessions();
|
||||
store.reset();
|
||||
|
||||
console.log(chalk.green('✓ All state reset'));
|
||||
});
|
||||
|
||||
// Shorthand commands at root level
|
||||
program
|
||||
.command('start')
|
||||
.description('Start a new session (shorthand)')
|
||||
.option('-d, --dir <path>', 'Working directory', process.cwd())
|
||||
.action(async (options) => {
|
||||
const manager = getSessionManager();
|
||||
const session = await manager.createSession(options.dir);
|
||||
console.log(chalk.green(`✓ Session started: ${session.id}`));
|
||||
});
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.alias('ls')
|
||||
.description('List all sessions (shorthand)')
|
||||
.action(() => {
|
||||
const manager = getSessionManager();
|
||||
const sessions = manager.getAllSessions();
|
||||
const stored = manager.getStoredSessions();
|
||||
|
||||
if (sessions.length === 0 && Object.keys(stored).length === 0) {
|
||||
console.log(chalk.yellow('No sessions found'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\nActive Sessions:'));
|
||||
if (sessions.length === 0) {
|
||||
console.log(' (none)');
|
||||
} else {
|
||||
for (const session of sessions) {
|
||||
const status = session.status === 'idle'
|
||||
? chalk.green('idle')
|
||||
: session.status === 'busy'
|
||||
? chalk.yellow('busy')
|
||||
: chalk.red(session.status);
|
||||
console.log(` ${chalk.cyan(session.id.slice(0, 8))} ${status} ${session.workingDir}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
|
||||
export { program };
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { program } from './cli.js';
|
||||
|
||||
// Handle uncaught errors
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Uncaught exception:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('Unhandled rejection:', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Run CLI
|
||||
program.parse();
|
||||
@@ -0,0 +1,397 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { getSessionManager, SessionManager } from './session-manager.js';
|
||||
import { getTaskQueue, TaskQueue } from './task-queue.js';
|
||||
import { getStore, StateStore } from './state-store.js';
|
||||
import { Session } from './session.js';
|
||||
import { Task } from './task.js';
|
||||
import { RalphLoopStatus } from './types.js';
|
||||
|
||||
export interface RalphLoopEvents {
|
||||
started: () => void;
|
||||
stopped: () => void;
|
||||
taskAssigned: (taskId: string, sessionId: string) => void;
|
||||
taskCompleted: (taskId: string) => void;
|
||||
taskFailed: (taskId: string, error: string) => void;
|
||||
error: (error: Error) => void;
|
||||
}
|
||||
|
||||
export interface RalphLoopOptions {
|
||||
pollIntervalMs?: number;
|
||||
minDurationMs?: number;
|
||||
autoGenerateTasks?: boolean;
|
||||
}
|
||||
|
||||
export class RalphLoop extends EventEmitter {
|
||||
private sessionManager: SessionManager;
|
||||
private taskQueue: TaskQueue;
|
||||
private store: StateStore;
|
||||
private pollIntervalMs: number;
|
||||
private minDurationMs: number | null;
|
||||
private autoGenerateTasks: boolean;
|
||||
private loopTimer: NodeJS.Timeout | null = null;
|
||||
private _status: RalphLoopStatus = 'stopped';
|
||||
private startedAt: number | null = null;
|
||||
private tasksCompleted: number = 0;
|
||||
private tasksGenerated: number = 0;
|
||||
|
||||
constructor(options: RalphLoopOptions = {}) {
|
||||
super();
|
||||
this.sessionManager = getSessionManager();
|
||||
this.taskQueue = getTaskQueue();
|
||||
this.store = getStore();
|
||||
|
||||
const config = this.store.getConfig();
|
||||
this.pollIntervalMs = options.pollIntervalMs ?? config.pollIntervalMs;
|
||||
this.minDurationMs = options.minDurationMs ?? null;
|
||||
this.autoGenerateTasks = options.autoGenerateTasks ?? true;
|
||||
|
||||
// Load state from store
|
||||
const savedState = this.store.getRalphLoopState();
|
||||
if (savedState.status === 'running') {
|
||||
// If we crashed while running, reset to stopped
|
||||
this._status = 'stopped';
|
||||
this.store.setRalphLoopState({ status: 'stopped' });
|
||||
}
|
||||
|
||||
this.setupEventHandlers();
|
||||
}
|
||||
|
||||
private setupEventHandlers(): void {
|
||||
this.sessionManager.on('sessionCompletion', (sessionId: string, phrase: string) => {
|
||||
this.handleSessionCompletion(sessionId, phrase);
|
||||
});
|
||||
|
||||
this.sessionManager.on('sessionError', (sessionId: string, error: string) => {
|
||||
this.handleSessionError(sessionId, error);
|
||||
});
|
||||
|
||||
this.sessionManager.on('sessionStopped', (sessionId: string) => {
|
||||
this.handleSessionStopped(sessionId);
|
||||
});
|
||||
}
|
||||
|
||||
get status(): RalphLoopStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this._status === 'running';
|
||||
}
|
||||
|
||||
getElapsedMs(): number {
|
||||
if (!this.startedAt) {
|
||||
return 0;
|
||||
}
|
||||
return Date.now() - this.startedAt;
|
||||
}
|
||||
|
||||
getElapsedHours(): number {
|
||||
return this.getElapsedMs() / (1000 * 60 * 60);
|
||||
}
|
||||
|
||||
isMinDurationReached(): boolean {
|
||||
if (!this.minDurationMs) {
|
||||
return true;
|
||||
}
|
||||
return this.getElapsedMs() >= this.minDurationMs;
|
||||
}
|
||||
|
||||
getStats() {
|
||||
const taskCounts = this.taskQueue.getCount();
|
||||
return {
|
||||
status: this._status,
|
||||
elapsedMs: this.getElapsedMs(),
|
||||
elapsedHours: this.getElapsedHours(),
|
||||
minDurationMs: this.minDurationMs,
|
||||
minDurationReached: this.isMinDurationReached(),
|
||||
tasksCompleted: this.tasksCompleted,
|
||||
tasksGenerated: this.tasksGenerated,
|
||||
...taskCounts,
|
||||
activeSessions: this.sessionManager.getSessionCount(),
|
||||
idleSessions: this.sessionManager.getIdleSessions().length,
|
||||
busySessions: this.sessionManager.getBusySessions().length,
|
||||
};
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this._status === 'running') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._status = 'running';
|
||||
this.startedAt = Date.now();
|
||||
this.tasksCompleted = 0;
|
||||
this.tasksGenerated = 0;
|
||||
|
||||
this.store.setRalphLoopState({
|
||||
status: 'running',
|
||||
startedAt: this.startedAt,
|
||||
minDurationMs: this.minDurationMs,
|
||||
tasksCompleted: 0,
|
||||
tasksGenerated: 0,
|
||||
});
|
||||
|
||||
this.emit('started');
|
||||
this.runLoop();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this._status === 'stopped') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._status = 'stopped';
|
||||
|
||||
if (this.loopTimer) {
|
||||
clearTimeout(this.loopTimer);
|
||||
this.loopTimer = null;
|
||||
}
|
||||
|
||||
this.store.setRalphLoopState({
|
||||
status: 'stopped',
|
||||
lastCheckAt: Date.now(),
|
||||
});
|
||||
|
||||
this.emit('stopped');
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
if (this._status !== 'running') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._status = 'paused';
|
||||
|
||||
if (this.loopTimer) {
|
||||
clearTimeout(this.loopTimer);
|
||||
this.loopTimer = null;
|
||||
}
|
||||
|
||||
this.store.setRalphLoopState({ status: 'paused' });
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
if (this._status !== 'paused') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._status = 'running';
|
||||
this.store.setRalphLoopState({ status: 'running' });
|
||||
this.runLoop();
|
||||
}
|
||||
|
||||
private runLoop(): void {
|
||||
if (this._status !== 'running') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tick()
|
||||
.catch((err) => {
|
||||
this.emit('error', err);
|
||||
})
|
||||
.finally(() => {
|
||||
if (this._status === 'running') {
|
||||
this.loopTimer = setTimeout(() => this.runLoop(), this.pollIntervalMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async tick(): Promise<void> {
|
||||
this.store.setRalphLoopState({ lastCheckAt: Date.now() });
|
||||
|
||||
// Check for timed out tasks
|
||||
await this.checkTimeouts();
|
||||
|
||||
// Assign tasks to idle sessions
|
||||
await this.assignTasks();
|
||||
|
||||
// Check if we should auto-generate tasks
|
||||
if (this.autoGenerateTasks && this.shouldGenerateTasks()) {
|
||||
await this.generateFollowUpTasks();
|
||||
}
|
||||
|
||||
// Check if we're done
|
||||
if (this.shouldStop()) {
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async assignTasks(): Promise<void> {
|
||||
const idleSessions = this.sessionManager.getIdleSessions();
|
||||
|
||||
for (const session of idleSessions) {
|
||||
const task = this.taskQueue.next();
|
||||
if (!task) {
|
||||
break;
|
||||
}
|
||||
|
||||
await this.assignTaskToSession(task, session);
|
||||
}
|
||||
}
|
||||
|
||||
private async assignTaskToSession(task: Task, session: Session): Promise<void> {
|
||||
try {
|
||||
task.assign(session.id);
|
||||
session.assignTask(task.id);
|
||||
this.taskQueue.updateTask(task);
|
||||
|
||||
// Send the prompt to the session
|
||||
await session.sendInput(task.prompt);
|
||||
|
||||
this.emit('taskAssigned', task.id, session.id);
|
||||
} catch (err) {
|
||||
task.fail((err as Error).message);
|
||||
session.clearTask();
|
||||
this.taskQueue.updateTask(task);
|
||||
this.emit('taskFailed', task.id, (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkTimeouts(): Promise<void> {
|
||||
for (const task of this.taskQueue.getRunningTasks()) {
|
||||
if (task.isTimedOut()) {
|
||||
task.fail('Task timed out');
|
||||
this.taskQueue.updateTask(task);
|
||||
|
||||
const session = this.sessionManager.getSession(task.assignedSessionId!);
|
||||
if (session) {
|
||||
session.clearTask();
|
||||
}
|
||||
|
||||
this.emit('taskFailed', task.id, 'Task timed out');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleSessionCompletion(sessionId: string, phrase: string): void {
|
||||
const session = this.sessionManager.getSession(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = session.currentTaskId;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const task = this.taskQueue.getTask(taskId);
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Append output and check for completion
|
||||
task.appendOutput(session.getOutput());
|
||||
|
||||
if (task.checkCompletion(session.getOutput()) || phrase) {
|
||||
task.complete();
|
||||
this.taskQueue.updateTask(task);
|
||||
session.clearTask();
|
||||
this.tasksCompleted++;
|
||||
this.store.setRalphLoopState({ tasksCompleted: this.tasksCompleted });
|
||||
this.emit('taskCompleted', task.id);
|
||||
}
|
||||
}
|
||||
|
||||
private handleSessionError(sessionId: string, error: string): void {
|
||||
const session = this.sessionManager.getSession(sessionId);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = session.currentTaskId;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const task = this.taskQueue.getTask(taskId);
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
|
||||
task.setError(error);
|
||||
// Don't fail the task immediately on stderr - some tools write to stderr normally
|
||||
}
|
||||
|
||||
private handleSessionStopped(sessionId: string): void {
|
||||
const task = this.taskQueue.getRunningTaskForSession(sessionId);
|
||||
if (task) {
|
||||
task.fail('Session stopped unexpectedly');
|
||||
this.taskQueue.updateTask(task);
|
||||
this.emit('taskFailed', task.id, 'Session stopped unexpectedly');
|
||||
}
|
||||
}
|
||||
|
||||
private shouldGenerateTasks(): boolean {
|
||||
// Generate tasks if:
|
||||
// 1. No pending tasks
|
||||
// 2. Min duration not reached
|
||||
// 3. We have idle sessions
|
||||
const counts = this.taskQueue.getCount();
|
||||
return (
|
||||
counts.pending === 0 &&
|
||||
!this.isMinDurationReached() &&
|
||||
this.sessionManager.getIdleSessions().length > 0
|
||||
);
|
||||
}
|
||||
|
||||
private async generateFollowUpTasks(): Promise<void> {
|
||||
// This is a placeholder for auto-generating follow-up tasks
|
||||
// In a real implementation, this could:
|
||||
// - Analyze completed tasks to find optimization opportunities
|
||||
// - Generate tasks for code cleanup, tests, documentation
|
||||
// - Use Claude to suggest improvements
|
||||
|
||||
const suggestions = [
|
||||
'Review and optimize recently changed code',
|
||||
'Add tests for uncovered code paths',
|
||||
'Update documentation for changed APIs',
|
||||
'Check for security vulnerabilities',
|
||||
'Run linting and fix any issues',
|
||||
];
|
||||
|
||||
// Only generate one task at a time
|
||||
const suggestion = suggestions[this.tasksGenerated % suggestions.length];
|
||||
const defaultDir = process.cwd();
|
||||
|
||||
this.taskQueue.addTask({
|
||||
prompt: suggestion,
|
||||
workingDir: defaultDir,
|
||||
priority: -1, // Lower priority than user-added tasks
|
||||
});
|
||||
|
||||
this.tasksGenerated++;
|
||||
this.store.setRalphLoopState({ tasksGenerated: this.tasksGenerated });
|
||||
}
|
||||
|
||||
private shouldStop(): boolean {
|
||||
const counts = this.taskQueue.getCount();
|
||||
|
||||
// Don't stop if there are pending or running tasks
|
||||
if (counts.pending > 0 || counts.running > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't stop if min duration not reached and auto-generate is on
|
||||
if (!this.isMinDurationReached() && this.autoGenerateTasks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All tasks done and conditions met
|
||||
return true;
|
||||
}
|
||||
|
||||
setMinDuration(hours: number): void {
|
||||
this.minDurationMs = hours * 60 * 60 * 1000;
|
||||
this.store.setRalphLoopState({ minDurationMs: this.minDurationMs });
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let loopInstance: RalphLoop | null = null;
|
||||
|
||||
export function getRalphLoop(options?: RalphLoopOptions): RalphLoop {
|
||||
if (!loopInstance) {
|
||||
loopInstance = new RalphLoop(options);
|
||||
}
|
||||
return loopInstance;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Session } from './session.js';
|
||||
import { getStore } from './state-store.js';
|
||||
import { SessionState } from './types.js';
|
||||
|
||||
export interface SessionManagerEvents {
|
||||
sessionStarted: (session: Session) => void;
|
||||
sessionStopped: (sessionId: string) => void;
|
||||
sessionError: (sessionId: string, error: string) => void;
|
||||
sessionOutput: (sessionId: string, output: string) => void;
|
||||
sessionCompletion: (sessionId: string, phrase: string) => void;
|
||||
}
|
||||
|
||||
export class SessionManager extends EventEmitter {
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
private store = getStore();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadFromStore();
|
||||
}
|
||||
|
||||
private loadFromStore(): void {
|
||||
const storedSessions = this.store.getSessions();
|
||||
// Note: We don't restore actual processes, just the state
|
||||
// Dead sessions are marked as stopped
|
||||
for (const [id, state] of Object.entries(storedSessions)) {
|
||||
if (state.status !== 'stopped') {
|
||||
state.status = 'stopped';
|
||||
state.pid = null;
|
||||
this.store.setSession(id, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createSession(workingDir: string): Promise<Session> {
|
||||
const config = this.store.getConfig();
|
||||
|
||||
if (this.sessions.size >= config.maxConcurrentSessions) {
|
||||
throw new Error(`Maximum concurrent sessions (${config.maxConcurrentSessions}) reached`);
|
||||
}
|
||||
|
||||
const session = new Session({ workingDir });
|
||||
|
||||
// Set up event forwarding
|
||||
session.on('output', (data) => {
|
||||
this.emit('sessionOutput', session.id, data);
|
||||
this.updateSessionState(session);
|
||||
});
|
||||
|
||||
session.on('error', (data) => {
|
||||
this.emit('sessionError', session.id, data);
|
||||
this.updateSessionState(session);
|
||||
});
|
||||
|
||||
session.on('completion', (phrase) => {
|
||||
this.emit('sessionCompletion', session.id, phrase);
|
||||
});
|
||||
|
||||
session.on('exit', () => {
|
||||
this.emit('sessionStopped', session.id);
|
||||
this.updateSessionState(session);
|
||||
});
|
||||
|
||||
await session.start();
|
||||
|
||||
this.sessions.set(session.id, session);
|
||||
this.store.setSession(session.id, session.toState());
|
||||
|
||||
this.emit('sessionStarted', session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async stopSession(id: string): Promise<void> {
|
||||
const session = this.sessions.get(id);
|
||||
if (!session) {
|
||||
// Update store to mark as stopped if it exists there
|
||||
const storedSession = this.store.getSession(id);
|
||||
if (storedSession) {
|
||||
storedSession.status = 'stopped';
|
||||
storedSession.pid = null;
|
||||
this.store.setSession(id, storedSession);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await session.stop();
|
||||
this.sessions.delete(id);
|
||||
this.updateSessionState(session);
|
||||
}
|
||||
|
||||
async stopAllSessions(): Promise<void> {
|
||||
const stopPromises = Array.from(this.sessions.keys()).map((id) =>
|
||||
this.stopSession(id)
|
||||
);
|
||||
await Promise.all(stopPromises);
|
||||
}
|
||||
|
||||
getSession(id: string): Session | undefined {
|
||||
return this.sessions.get(id);
|
||||
}
|
||||
|
||||
getAllSessions(): Session[] {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
|
||||
getIdleSessions(): Session[] {
|
||||
return this.getAllSessions().filter((s) => s.isIdle());
|
||||
}
|
||||
|
||||
getBusySessions(): Session[] {
|
||||
return this.getAllSessions().filter((s) => s.isBusy());
|
||||
}
|
||||
|
||||
getSessionCount(): number {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
hasSession(id: string): boolean {
|
||||
return this.sessions.has(id);
|
||||
}
|
||||
|
||||
private updateSessionState(session: Session): void {
|
||||
this.store.setSession(session.id, session.toState());
|
||||
}
|
||||
|
||||
getStoredSessions(): Record<string, SessionState> {
|
||||
return this.store.getSessions();
|
||||
}
|
||||
|
||||
async sendToSession(sessionId: string, input: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Session ${sessionId} not found`);
|
||||
}
|
||||
await session.sendInput(input);
|
||||
}
|
||||
|
||||
getSessionOutput(sessionId: string): string | null {
|
||||
const session = this.sessions.get(sessionId);
|
||||
return session?.getOutput() ?? null;
|
||||
}
|
||||
|
||||
getSessionError(sessionId: string): string | null {
|
||||
const session = this.sessions.get(sessionId);
|
||||
return session?.getError() ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let managerInstance: SessionManager | null = null;
|
||||
|
||||
export function getSessionManager(): SessionManager {
|
||||
if (!managerInstance) {
|
||||
managerInstance = new SessionManager();
|
||||
}
|
||||
return managerInstance;
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
import { spawn, ChildProcess } from 'node:child_process';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SessionState, SessionStatus, SessionConfig } from './types.js';
|
||||
|
||||
export interface SessionEvents {
|
||||
output: (data: string) => void;
|
||||
error: (data: string) => void;
|
||||
exit: (code: number | null) => void;
|
||||
completion: (phrase: string) => void;
|
||||
}
|
||||
|
||||
export class Session extends EventEmitter {
|
||||
readonly id: string;
|
||||
readonly workingDir: string;
|
||||
readonly createdAt: number;
|
||||
|
||||
private process: ChildProcess | null = null;
|
||||
private _status: SessionStatus = 'idle';
|
||||
private _currentTaskId: string | null = null;
|
||||
private _outputBuffer: string = '';
|
||||
private _errorBuffer: string = '';
|
||||
private _lastActivityAt: number;
|
||||
|
||||
constructor(config: Partial<SessionConfig> & { workingDir: string }) {
|
||||
super();
|
||||
this.id = config.id || uuidv4();
|
||||
this.workingDir = config.workingDir;
|
||||
this.createdAt = config.createdAt || Date.now();
|
||||
this._lastActivityAt = this.createdAt;
|
||||
}
|
||||
|
||||
get status(): SessionStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
get currentTaskId(): string | null {
|
||||
return this._currentTaskId;
|
||||
}
|
||||
|
||||
get pid(): number | null {
|
||||
return this.process?.pid ?? null;
|
||||
}
|
||||
|
||||
get outputBuffer(): string {
|
||||
return this._outputBuffer;
|
||||
}
|
||||
|
||||
get errorBuffer(): string {
|
||||
return this._errorBuffer;
|
||||
}
|
||||
|
||||
get lastActivityAt(): number {
|
||||
return this._lastActivityAt;
|
||||
}
|
||||
|
||||
isIdle(): boolean {
|
||||
return this._status === 'idle';
|
||||
}
|
||||
|
||||
isBusy(): boolean {
|
||||
return this._status === 'busy';
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this._status === 'idle' || this._status === 'busy';
|
||||
}
|
||||
|
||||
toState(): SessionState {
|
||||
return {
|
||||
id: this.id,
|
||||
pid: this.pid,
|
||||
status: this._status,
|
||||
workingDir: this.workingDir,
|
||||
currentTaskId: this._currentTaskId,
|
||||
createdAt: this.createdAt,
|
||||
lastActivityAt: this._lastActivityAt,
|
||||
};
|
||||
}
|
||||
|
||||
start(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.process) {
|
||||
reject(new Error('Session already started'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Spawn claude CLI with --print flag for non-interactive mode
|
||||
this.process = spawn('claude', ['--print'], {
|
||||
cwd: this.workingDir,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env },
|
||||
});
|
||||
|
||||
this._status = 'idle';
|
||||
this._lastActivityAt = Date.now();
|
||||
|
||||
this.process.stdout?.on('data', (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
this._outputBuffer += text;
|
||||
this._lastActivityAt = Date.now();
|
||||
this.emit('output', text);
|
||||
this.checkForCompletion(text);
|
||||
});
|
||||
|
||||
this.process.stderr?.on('data', (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
this._errorBuffer += text;
|
||||
this._lastActivityAt = Date.now();
|
||||
this.emit('error', text);
|
||||
});
|
||||
|
||||
this.process.on('error', (err) => {
|
||||
this._status = 'error';
|
||||
this.emit('error', err.message);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
this._status = 'stopped';
|
||||
this.process = null;
|
||||
this.emit('exit', code);
|
||||
});
|
||||
|
||||
// Give process time to start
|
||||
setTimeout(() => {
|
||||
if (this.process && !this.process.killed) {
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
this._status = 'error';
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (!this.process) {
|
||||
this._status = 'stopped';
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
this.process = null;
|
||||
this._status = 'stopped';
|
||||
this._currentTaskId = null;
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.process.once('exit', cleanup);
|
||||
|
||||
// Try graceful shutdown first
|
||||
this.process.kill('SIGTERM');
|
||||
|
||||
// Force kill after timeout
|
||||
setTimeout(() => {
|
||||
if (this.process && !this.process.killed) {
|
||||
this.process.kill('SIGKILL');
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
async sendInput(input: string): Promise<void> {
|
||||
if (!this.process || !this.process.stdin) {
|
||||
throw new Error('Session not started or stdin not available');
|
||||
}
|
||||
|
||||
this._status = 'busy';
|
||||
this._lastActivityAt = Date.now();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.process!.stdin!.write(input + '\n', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
assignTask(taskId: string): void {
|
||||
this._currentTaskId = taskId;
|
||||
this._status = 'busy';
|
||||
this._outputBuffer = '';
|
||||
this._errorBuffer = '';
|
||||
this._lastActivityAt = Date.now();
|
||||
}
|
||||
|
||||
clearTask(): void {
|
||||
this._currentTaskId = null;
|
||||
this._status = 'idle';
|
||||
this._lastActivityAt = Date.now();
|
||||
}
|
||||
|
||||
getOutput(): string {
|
||||
return this._outputBuffer;
|
||||
}
|
||||
|
||||
getError(): string {
|
||||
return this._errorBuffer;
|
||||
}
|
||||
|
||||
clearBuffers(): void {
|
||||
this._outputBuffer = '';
|
||||
this._errorBuffer = '';
|
||||
}
|
||||
|
||||
private checkForCompletion(text: string): void {
|
||||
// Check for completion phrase pattern: <promise>...</promise>
|
||||
const promiseMatch = text.match(/<promise>([^<]+)<\/promise>/);
|
||||
if (promiseMatch) {
|
||||
this.emit('completion', promiseMatch[1]);
|
||||
}
|
||||
|
||||
// Also check for common completion indicators
|
||||
const completionIndicators = [
|
||||
/Task completed successfully/i,
|
||||
/All tasks done/i,
|
||||
/✓ Complete/i,
|
||||
];
|
||||
|
||||
for (const pattern of completionIndicators) {
|
||||
if (pattern.test(text)) {
|
||||
this.emit('completion', 'auto-detected');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { AppState, createInitialState } from './types.js';
|
||||
|
||||
export class StateStore {
|
||||
private state: AppState;
|
||||
private filePath: string;
|
||||
|
||||
constructor(filePath?: string) {
|
||||
this.filePath = filePath || join(homedir(), '.claudeman', 'state.json');
|
||||
this.state = this.load();
|
||||
this.state.config.stateFilePath = this.filePath;
|
||||
}
|
||||
|
||||
private ensureDir(): void {
|
||||
const dir = dirname(this.filePath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
private load(): AppState {
|
||||
try {
|
||||
if (existsSync(this.filePath)) {
|
||||
const data = readFileSync(this.filePath, 'utf-8');
|
||||
const parsed = JSON.parse(data) as Partial<AppState>;
|
||||
// Merge with initial state to ensure all fields exist
|
||||
const initial = createInitialState();
|
||||
return {
|
||||
...initial,
|
||||
...parsed,
|
||||
sessions: { ...parsed.sessions },
|
||||
tasks: { ...parsed.tasks },
|
||||
ralphLoop: { ...initial.ralphLoop, ...parsed.ralphLoop },
|
||||
config: { ...initial.config, ...parsed.config },
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load state, using initial state:', err);
|
||||
}
|
||||
return createInitialState();
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.ensureDir();
|
||||
writeFileSync(this.filePath, JSON.stringify(this.state, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
getState(): AppState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
getSessions() {
|
||||
return this.state.sessions;
|
||||
}
|
||||
|
||||
getSession(id: string) {
|
||||
return this.state.sessions[id] || null;
|
||||
}
|
||||
|
||||
setSession(id: string, session: AppState['sessions'][string]) {
|
||||
this.state.sessions[id] = session;
|
||||
this.save();
|
||||
}
|
||||
|
||||
removeSession(id: string) {
|
||||
delete this.state.sessions[id];
|
||||
this.save();
|
||||
}
|
||||
|
||||
getTasks() {
|
||||
return this.state.tasks;
|
||||
}
|
||||
|
||||
getTask(id: string) {
|
||||
return this.state.tasks[id] || null;
|
||||
}
|
||||
|
||||
setTask(id: string, task: AppState['tasks'][string]) {
|
||||
this.state.tasks[id] = task;
|
||||
this.save();
|
||||
}
|
||||
|
||||
removeTask(id: string) {
|
||||
delete this.state.tasks[id];
|
||||
this.save();
|
||||
}
|
||||
|
||||
getRalphLoopState() {
|
||||
return this.state.ralphLoop;
|
||||
}
|
||||
|
||||
setRalphLoopState(ralphLoop: Partial<AppState['ralphLoop']>) {
|
||||
this.state.ralphLoop = { ...this.state.ralphLoop, ...ralphLoop };
|
||||
this.save();
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return this.state.config;
|
||||
}
|
||||
|
||||
setConfig(config: Partial<AppState['config']>) {
|
||||
this.state.config = { ...this.state.config, ...config };
|
||||
this.save();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.state = createInitialState();
|
||||
this.state.config.stateFilePath = this.filePath;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let storeInstance: StateStore | null = null;
|
||||
|
||||
export function getStore(filePath?: string): StateStore {
|
||||
if (!storeInstance) {
|
||||
storeInstance = new StateStore(filePath);
|
||||
}
|
||||
return storeInstance;
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Task, CreateTaskOptions } from './task.js';
|
||||
import { getStore } from './state-store.js';
|
||||
import { TaskState } from './types.js';
|
||||
|
||||
export interface TaskQueueEvents {
|
||||
taskAdded: (task: Task) => void;
|
||||
taskRemoved: (taskId: string) => void;
|
||||
taskUpdated: (task: Task) => void;
|
||||
}
|
||||
|
||||
export class TaskQueue extends EventEmitter {
|
||||
private tasks: Map<string, Task> = new Map();
|
||||
private store = getStore();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadFromStore();
|
||||
}
|
||||
|
||||
private loadFromStore(): void {
|
||||
const storedTasks = this.store.getTasks();
|
||||
for (const [id, state] of Object.entries(storedTasks)) {
|
||||
const task = Task.fromState(state);
|
||||
this.tasks.set(id, task);
|
||||
}
|
||||
}
|
||||
|
||||
addTask(options: CreateTaskOptions): Task {
|
||||
const task = new Task(options);
|
||||
this.tasks.set(task.id, task);
|
||||
this.store.setTask(task.id, task.toState());
|
||||
this.emit('taskAdded', task);
|
||||
return task;
|
||||
}
|
||||
|
||||
getTask(id: string): Task | undefined {
|
||||
return this.tasks.get(id);
|
||||
}
|
||||
|
||||
removeTask(id: string): boolean {
|
||||
const removed = this.tasks.delete(id);
|
||||
if (removed) {
|
||||
this.store.removeTask(id);
|
||||
this.emit('taskRemoved', id);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
updateTask(task: Task): void {
|
||||
this.tasks.set(task.id, task);
|
||||
this.store.setTask(task.id, task.toState());
|
||||
this.emit('taskUpdated', task);
|
||||
}
|
||||
|
||||
getAllTasks(): Task[] {
|
||||
return Array.from(this.tasks.values());
|
||||
}
|
||||
|
||||
getPendingTasks(): Task[] {
|
||||
return this.getAllTasks()
|
||||
.filter((t) => t.isPending())
|
||||
.sort((a, b) => {
|
||||
// Sort by priority (higher first), then by creation time (older first)
|
||||
if (a.priority !== b.priority) {
|
||||
return b.priority - a.priority;
|
||||
}
|
||||
return a.createdAt - b.createdAt;
|
||||
});
|
||||
}
|
||||
|
||||
getRunningTasks(): Task[] {
|
||||
return this.getAllTasks().filter((t) => t.isRunning());
|
||||
}
|
||||
|
||||
getCompletedTasks(): Task[] {
|
||||
return this.getAllTasks().filter((t) => t.isCompleted());
|
||||
}
|
||||
|
||||
getFailedTasks(): Task[] {
|
||||
return this.getAllTasks().filter((t) => t.isFailed());
|
||||
}
|
||||
|
||||
hasNext(): boolean {
|
||||
return this.getNextAvailable() !== null;
|
||||
}
|
||||
|
||||
getNextAvailable(): Task | null {
|
||||
const pending = this.getPendingTasks();
|
||||
|
||||
for (const task of pending) {
|
||||
if (this.areDependenciesSatisfied(task)) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
next(): Task | null {
|
||||
return this.getNextAvailable();
|
||||
}
|
||||
|
||||
private areDependenciesSatisfied(task: Task): boolean {
|
||||
for (const depId of task.dependencies) {
|
||||
const dep = this.tasks.get(depId);
|
||||
if (!dep || !dep.isCompleted()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getTasksBySession(sessionId: string): Task[] {
|
||||
return this.getAllTasks().filter((t) => t.assignedSessionId === sessionId);
|
||||
}
|
||||
|
||||
getRunningTaskForSession(sessionId: string): Task | null {
|
||||
return this.getAllTasks().find(
|
||||
(t) => t.isRunning() && t.assignedSessionId === sessionId
|
||||
) || null;
|
||||
}
|
||||
|
||||
getCount(): { total: number; pending: number; running: number; completed: number; failed: number } {
|
||||
const tasks = this.getAllTasks();
|
||||
return {
|
||||
total: tasks.length,
|
||||
pending: tasks.filter((t) => t.isPending()).length,
|
||||
running: tasks.filter((t) => t.isRunning()).length,
|
||||
completed: tasks.filter((t) => t.isCompleted()).length,
|
||||
failed: tasks.filter((t) => t.isFailed()).length,
|
||||
};
|
||||
}
|
||||
|
||||
clearCompleted(): number {
|
||||
let count = 0;
|
||||
for (const task of this.getAllTasks()) {
|
||||
if (task.isCompleted()) {
|
||||
this.removeTask(task.id);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
clearFailed(): number {
|
||||
let count = 0;
|
||||
for (const task of this.getAllTasks()) {
|
||||
if (task.isFailed()) {
|
||||
this.removeTask(task.id);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
clearAll(): number {
|
||||
const count = this.tasks.size;
|
||||
for (const id of this.tasks.keys()) {
|
||||
this.store.removeTask(id);
|
||||
}
|
||||
this.tasks.clear();
|
||||
return count;
|
||||
}
|
||||
|
||||
getStoredTasks(): Record<string, TaskState> {
|
||||
return this.store.getTasks();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let queueInstance: TaskQueue | null = null;
|
||||
|
||||
export function getTaskQueue(): TaskQueue {
|
||||
if (!queueInstance) {
|
||||
queueInstance = new TaskQueue();
|
||||
}
|
||||
return queueInstance;
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { TaskDefinition, TaskState, TaskStatus } from './types.js';
|
||||
|
||||
export interface CreateTaskOptions {
|
||||
prompt: string;
|
||||
workingDir?: string;
|
||||
priority?: number;
|
||||
dependencies?: string[];
|
||||
completionPhrase?: string;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export class Task {
|
||||
readonly id: string;
|
||||
readonly prompt: string;
|
||||
readonly workingDir: string;
|
||||
readonly priority: number;
|
||||
readonly dependencies: string[];
|
||||
readonly completionPhrase: string | undefined;
|
||||
readonly timeoutMs: number | undefined;
|
||||
readonly createdAt: number;
|
||||
|
||||
private _status: TaskStatus = 'pending';
|
||||
private _assignedSessionId: string | null = null;
|
||||
private _startedAt: number | null = null;
|
||||
private _completedAt: number | null = null;
|
||||
private _output: string = '';
|
||||
private _error: string | null = null;
|
||||
|
||||
constructor(options: CreateTaskOptions, id?: string) {
|
||||
this.id = id || uuidv4();
|
||||
this.prompt = options.prompt;
|
||||
this.workingDir = options.workingDir || process.cwd();
|
||||
this.priority = options.priority ?? 0;
|
||||
this.dependencies = options.dependencies || [];
|
||||
this.completionPhrase = options.completionPhrase;
|
||||
this.timeoutMs = options.timeoutMs;
|
||||
this.createdAt = Date.now();
|
||||
}
|
||||
|
||||
get status(): TaskStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
get assignedSessionId(): string | null {
|
||||
return this._assignedSessionId;
|
||||
}
|
||||
|
||||
get startedAt(): number | null {
|
||||
return this._startedAt;
|
||||
}
|
||||
|
||||
get completedAt(): number | null {
|
||||
return this._completedAt;
|
||||
}
|
||||
|
||||
get output(): string {
|
||||
return this._output;
|
||||
}
|
||||
|
||||
get error(): string | null {
|
||||
return this._error;
|
||||
}
|
||||
|
||||
isPending(): boolean {
|
||||
return this._status === 'pending';
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this._status === 'running';
|
||||
}
|
||||
|
||||
isCompleted(): boolean {
|
||||
return this._status === 'completed';
|
||||
}
|
||||
|
||||
isFailed(): boolean {
|
||||
return this._status === 'failed';
|
||||
}
|
||||
|
||||
isDone(): boolean {
|
||||
return this._status === 'completed' || this._status === 'failed';
|
||||
}
|
||||
|
||||
toDefinition(): TaskDefinition {
|
||||
return {
|
||||
id: this.id,
|
||||
prompt: this.prompt,
|
||||
workingDir: this.workingDir,
|
||||
priority: this.priority,
|
||||
dependencies: this.dependencies,
|
||||
completionPhrase: this.completionPhrase,
|
||||
timeoutMs: this.timeoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
toState(): TaskState {
|
||||
return {
|
||||
...this.toDefinition(),
|
||||
status: this._status,
|
||||
assignedSessionId: this._assignedSessionId,
|
||||
createdAt: this.createdAt,
|
||||
startedAt: this._startedAt,
|
||||
completedAt: this._completedAt,
|
||||
output: this._output,
|
||||
error: this._error,
|
||||
};
|
||||
}
|
||||
|
||||
static fromState(state: TaskState): Task {
|
||||
const task = new Task(
|
||||
{
|
||||
prompt: state.prompt,
|
||||
workingDir: state.workingDir,
|
||||
priority: state.priority,
|
||||
dependencies: state.dependencies,
|
||||
completionPhrase: state.completionPhrase,
|
||||
timeoutMs: state.timeoutMs,
|
||||
},
|
||||
state.id
|
||||
);
|
||||
task._status = state.status;
|
||||
task._assignedSessionId = state.assignedSessionId;
|
||||
task._startedAt = state.startedAt;
|
||||
task._completedAt = state.completedAt;
|
||||
task._output = state.output;
|
||||
task._error = state.error;
|
||||
return task;
|
||||
}
|
||||
|
||||
assign(sessionId: string): void {
|
||||
if (this._status !== 'pending') {
|
||||
throw new Error(`Cannot assign task ${this.id}: status is ${this._status}`);
|
||||
}
|
||||
this._status = 'running';
|
||||
this._assignedSessionId = sessionId;
|
||||
this._startedAt = Date.now();
|
||||
}
|
||||
|
||||
appendOutput(output: string): void {
|
||||
this._output += output;
|
||||
}
|
||||
|
||||
setError(error: string): void {
|
||||
this._error = error;
|
||||
}
|
||||
|
||||
complete(): void {
|
||||
this._status = 'completed';
|
||||
this._completedAt = Date.now();
|
||||
}
|
||||
|
||||
fail(error?: string): void {
|
||||
this._status = 'failed';
|
||||
this._completedAt = Date.now();
|
||||
if (error) {
|
||||
this._error = error;
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._status = 'pending';
|
||||
this._assignedSessionId = null;
|
||||
this._startedAt = null;
|
||||
this._completedAt = null;
|
||||
this._output = '';
|
||||
this._error = null;
|
||||
}
|
||||
|
||||
checkCompletion(output: string): boolean {
|
||||
if (this.completionPhrase) {
|
||||
// Check for exact completion phrase in promise tags
|
||||
const pattern = new RegExp(`<promise>${this.completionPhrase}</promise>`);
|
||||
return pattern.test(output);
|
||||
}
|
||||
// Default: check for any promise tag completion
|
||||
return /<promise>[^<]+<\/promise>/.test(output);
|
||||
}
|
||||
|
||||
isTimedOut(): boolean {
|
||||
if (!this.timeoutMs || !this._startedAt) {
|
||||
return false;
|
||||
}
|
||||
return Date.now() - this._startedAt > this.timeoutMs;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
export type SessionStatus = 'idle' | 'busy' | 'stopped' | 'error';
|
||||
export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed';
|
||||
export type RalphLoopStatus = 'stopped' | 'running' | 'paused';
|
||||
|
||||
export interface SessionConfig {
|
||||
id: string;
|
||||
workingDir: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface SessionState {
|
||||
id: string;
|
||||
pid: number | null;
|
||||
status: SessionStatus;
|
||||
workingDir: string;
|
||||
currentTaskId: string | null;
|
||||
createdAt: number;
|
||||
lastActivityAt: number;
|
||||
}
|
||||
|
||||
export interface TaskDefinition {
|
||||
id: string;
|
||||
prompt: string;
|
||||
workingDir: string;
|
||||
priority: number;
|
||||
dependencies: string[];
|
||||
completionPhrase?: string;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface TaskState {
|
||||
id: string;
|
||||
prompt: string;
|
||||
workingDir: string;
|
||||
priority: number;
|
||||
dependencies: string[];
|
||||
completionPhrase?: string;
|
||||
timeoutMs?: number;
|
||||
status: TaskStatus;
|
||||
assignedSessionId: string | null;
|
||||
createdAt: number;
|
||||
startedAt: number | null;
|
||||
completedAt: number | null;
|
||||
output: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface RalphLoopState {
|
||||
status: RalphLoopStatus;
|
||||
startedAt: number | null;
|
||||
minDurationMs: number | null;
|
||||
tasksCompleted: number;
|
||||
tasksGenerated: number;
|
||||
lastCheckAt: number | null;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
sessions: Record<string, SessionState>;
|
||||
tasks: Record<string, TaskState>;
|
||||
ralphLoop: RalphLoopState;
|
||||
config: AppConfig;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
pollIntervalMs: number;
|
||||
defaultTimeoutMs: number;
|
||||
maxConcurrentSessions: number;
|
||||
stateFilePath: string;
|
||||
}
|
||||
|
||||
export interface SessionOutput {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number | null;
|
||||
}
|
||||
|
||||
export interface TaskAssignment {
|
||||
sessionId: string;
|
||||
taskId: string;
|
||||
assignedAt: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: AppConfig = {
|
||||
pollIntervalMs: 1000,
|
||||
defaultTimeoutMs: 300000, // 5 minutes
|
||||
maxConcurrentSessions: 5,
|
||||
stateFilePath: '',
|
||||
};
|
||||
|
||||
export function createInitialState(): AppState {
|
||||
return {
|
||||
sessions: {},
|
||||
tasks: {},
|
||||
ralphLoop: {
|
||||
status: 'stopped',
|
||||
startedAt: null,
|
||||
minDurationMs: null,
|
||||
tasksCompleted: 0,
|
||||
tasksGenerated: 0,
|
||||
lastCheckAt: null,
|
||||
},
|
||||
config: { ...DEFAULT_CONFIG },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user