mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19: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