claude-controller 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/bin/app-launcher.sh +22 -0
- package/bin/claude-sh +19 -0
- package/bin/controller +37 -0
- package/bin/native-app.py +102 -0
- package/bin/send +185 -0
- package/bin/start +75 -0
- package/config.sh +74 -0
- package/lib/checkpoint.sh +237 -0
- package/lib/executor.sh +183 -0
- package/lib/jobs.sh +333 -0
- package/lib/session.sh +78 -0
- package/lib/worktree.sh +122 -0
- package/package.json +61 -0
- package/postinstall.sh +30 -0
- package/service/controller.sh +503 -0
- package/web/auth.py +46 -0
- package/web/checkpoint.py +175 -0
- package/web/config.py +65 -0
- package/web/handler.py +780 -0
- package/web/jobs.py +228 -0
- package/web/server.py +16 -0
- package/web/static/app.js +2013 -0
- package/web/static/index.html +219 -0
- package/web/static/styles.css +1942 -0
- package/web/utils.py +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 choiwon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Controller
|
|
2
|
+
|
|
3
|
+
A shell wrapper that runs Claude Code CLI as a headless daemon. Provides FIFO pipe-based async task dispatch, Git Worktree isolation, automatic checkpointing/rewind, and a web dashboard for remote task management.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ Web Dashboard (Vanilla JS) │
|
|
10
|
+
│ https://claude.won-space.com <-> localhost:8420 │
|
|
11
|
+
└────────────────────┬────────────────────────────────────────────┘
|
|
12
|
+
│ REST API (Python http.server)
|
|
13
|
+
┌────────────────────▼────────────────────────────────────────────┐
|
|
14
|
+
│ Web Server (native-app.py) │
|
|
15
|
+
│ ┌──────────┐ ┌────────────┐ ┌──────────┐ ┌───────────────────┐│
|
|
16
|
+
│ │ handler │ │ jobs.py │ │ auth.py │ │ checkpoint.py ││
|
|
17
|
+
│ │ (REST) │ │ (FIFO I/O) │ │ (Bearer) │ │ (Rewind) ││
|
|
18
|
+
│ └──────────┘ └─────┬──────┘ └──────────┘ └───────────────────┘│
|
|
19
|
+
└─────────────────────┼───────────────────────────────────────────┘
|
|
20
|
+
│ JSON via FIFO (queue/controller.pipe)
|
|
21
|
+
┌─────────────────────▼───────────────────────────────────────────┐
|
|
22
|
+
│ Controller Daemon (service/controller.sh) │
|
|
23
|
+
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────────────┐ │
|
|
24
|
+
│ │ executor │ │ jobs.sh │ │ session │ │ worktree.sh │ │
|
|
25
|
+
│ │ (claude) │ │ (state) │ │ (conv.) │ │ (Git isolation) │ │
|
|
26
|
+
│ └──────────┘ └──────────┘ └───────────┘ └───────────────────┘ │
|
|
27
|
+
│ ┌───────────────────────────────────────────────────────────┐ │
|
|
28
|
+
│ │ checkpoint.sh (watch changes -> auto-commit -> rewind) │ │
|
|
29
|
+
│ └───────────────────────────────────────────────────────────┘ │
|
|
30
|
+
└──────────────────────┬──────────────────────────────────────────┘
|
|
31
|
+
│ claude -p --output-format stream-json
|
|
32
|
+
▼
|
|
33
|
+
Claude Code CLI (headless)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Project Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
controller/
|
|
40
|
+
├── bin/ # Entry points
|
|
41
|
+
│ ├── controller # Service control (start/stop/restart/status)
|
|
42
|
+
│ ├── send # CLI client — sends tasks to FIFO
|
|
43
|
+
│ ├── start # Launch service + TUI together
|
|
44
|
+
│ ├── claude-sh # Interactive shell mode entry point
|
|
45
|
+
│ ├── native-app.py # Web server with auto browser launch
|
|
46
|
+
│ └── app-launcher.sh # macOS app launcher
|
|
47
|
+
├── lib/ # Core modules (Bash)
|
|
48
|
+
│ ├── executor.sh # claude -p execution engine
|
|
49
|
+
│ ├── jobs.sh # Job registration / state / result management
|
|
50
|
+
│ ├── session.sh # Claude session ID tracking
|
|
51
|
+
│ ├── worktree.sh # Git Worktree create / remove / list
|
|
52
|
+
│ └── checkpoint.sh # Auto-checkpoint + Rewind
|
|
53
|
+
├── service/
|
|
54
|
+
│ └── controller.sh # FIFO listener → dispatch persistent daemon
|
|
55
|
+
├── web/ # HTTP REST API server (Python)
|
|
56
|
+
│ ├── server.py # Module entry point
|
|
57
|
+
│ ├── handler.py # REST API handler (GET/POST/DELETE)
|
|
58
|
+
│ ├── config.py # Paths / security / SSL settings
|
|
59
|
+
│ ├── auth.py # Token-based authentication
|
|
60
|
+
│ ├── jobs.py # Job CRUD + FIFO messaging
|
|
61
|
+
│ ├── checkpoint.py # Checkpoint queries + Rewind execution
|
|
62
|
+
│ ├── utils.py # Meta file parser, service status check
|
|
63
|
+
│ └── static/ # Web dashboard (Vanilla JS/CSS)
|
|
64
|
+
├── config.sh # Global config (paths, model, permissions, worktree)
|
|
65
|
+
├── data/ # Runtime data (settings.json, auth_token)
|
|
66
|
+
├── logs/ # Job output (.out) + metadata (.meta)
|
|
67
|
+
├── queue/ # FIFO pipe (controller.pipe)
|
|
68
|
+
├── sessions/ # Session history (history.log)
|
|
69
|
+
├── uploads/ # File upload storage
|
|
70
|
+
└── worktrees/ # Git Worktree storage
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Getting Started
|
|
74
|
+
|
|
75
|
+
### Prerequisites
|
|
76
|
+
|
|
77
|
+
- macOS / Linux
|
|
78
|
+
- Claude Code CLI (`claude` command or app-bundled binary)
|
|
79
|
+
- Python 3.8+
|
|
80
|
+
- `jq` (JSON processing)
|
|
81
|
+
- Git (required for Worktree features)
|
|
82
|
+
|
|
83
|
+
### Running
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Start the service only
|
|
87
|
+
bin/controller start
|
|
88
|
+
|
|
89
|
+
# Start service + TUI together
|
|
90
|
+
bin/start
|
|
91
|
+
|
|
92
|
+
# Start the web server (auto-opens browser)
|
|
93
|
+
python3 bin/native-app.py
|
|
94
|
+
|
|
95
|
+
# Check service status
|
|
96
|
+
bin/controller status
|
|
97
|
+
|
|
98
|
+
# Stop the service
|
|
99
|
+
bin/controller stop
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Sending Tasks via CLI
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Send a basic prompt
|
|
106
|
+
bin/send "Fix the bug in auth.py"
|
|
107
|
+
|
|
108
|
+
# Specify a working directory
|
|
109
|
+
bin/send --cwd /path/to/repo "Write test code"
|
|
110
|
+
|
|
111
|
+
# Run in an isolated Git Worktree
|
|
112
|
+
bin/send --worktree --repo /path/to/repo "Perform refactoring"
|
|
113
|
+
|
|
114
|
+
# Specify a custom task ID
|
|
115
|
+
bin/send --id my-task-1 "Write README"
|
|
116
|
+
|
|
117
|
+
# Check all task statuses
|
|
118
|
+
bin/send --status
|
|
119
|
+
|
|
120
|
+
# View task result
|
|
121
|
+
bin/send --result <task_id>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Key Features
|
|
125
|
+
|
|
126
|
+
### FIFO-Based Async Dispatch
|
|
127
|
+
|
|
128
|
+
The service daemon listens on a Named Pipe (`queue/controller.pipe`) for JSON messages and runs `claude -p` in the background. Supports duplicate prompt detection (3-second window), max concurrent job limits, and session modes (new/resume/fork/continue).
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"id": "task-1",
|
|
133
|
+
"prompt": "Fix the bug",
|
|
134
|
+
"cwd": "/path/to/project",
|
|
135
|
+
"worktree": "true",
|
|
136
|
+
"session": "resume:<session_id>",
|
|
137
|
+
"images": ["/path/to/screenshot.png"]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Git Worktree Isolation
|
|
142
|
+
|
|
143
|
+
Each task runs in an independent Git Worktree, enabling parallel work without affecting the main branch. A `controller/job-<id>` branch is automatically created and can be cleaned up after completion.
|
|
144
|
+
|
|
145
|
+
### Auto-Checkpoint & Rewind
|
|
146
|
+
|
|
147
|
+
Periodically monitors file changes in the Worktree and auto-commits when changes stabilize. If something goes wrong, you can `git reset --hard` to a specific checkpoint and resume work with a new prompt that includes the previous conversation context (Rewind).
|
|
148
|
+
|
|
149
|
+
### Session Management
|
|
150
|
+
|
|
151
|
+
Tracks Claude Code session IDs to enable conversation continuity:
|
|
152
|
+
|
|
153
|
+
- **new** — Start a fresh session
|
|
154
|
+
- **resume** — Continue an existing session (`--resume <session_id>`)
|
|
155
|
+
- **fork** — Branch from a previous session by injecting its context
|
|
156
|
+
- **continue** — Continue the most recent conversation (`--continue`)
|
|
157
|
+
|
|
158
|
+
## REST API
|
|
159
|
+
|
|
160
|
+
| Method | Endpoint | Description |
|
|
161
|
+
|--------|----------|-------------|
|
|
162
|
+
| GET | `/api/status` | Service running status |
|
|
163
|
+
| GET | `/api/jobs` | List all jobs |
|
|
164
|
+
| GET | `/api/jobs/:id/result` | Get job result |
|
|
165
|
+
| GET | `/api/jobs/:id/stream` | Poll real-time stream (offset-based) |
|
|
166
|
+
| GET | `/api/jobs/:id/checkpoints` | List checkpoints |
|
|
167
|
+
| GET | `/api/sessions` | List sessions (Claude Code native + job meta) |
|
|
168
|
+
| GET | `/api/session/:id/job` | Find job by session ID |
|
|
169
|
+
| GET | `/api/config` | Get configuration |
|
|
170
|
+
| GET | `/api/recent-dirs` | Get recent working directories |
|
|
171
|
+
| GET | `/api/dirs?path=` | Browse filesystem directories |
|
|
172
|
+
| POST | `/api/send` | Send a new task (via FIFO) |
|
|
173
|
+
| POST | `/api/upload` | Upload a file (base64) |
|
|
174
|
+
| POST | `/api/jobs/:id/rewind` | Rewind to a checkpoint |
|
|
175
|
+
| POST | `/api/service/start` | Start the service |
|
|
176
|
+
| POST | `/api/service/stop` | Stop the service |
|
|
177
|
+
| POST | `/api/config` | Save configuration |
|
|
178
|
+
| POST | `/api/auth/verify` | Verify auth token |
|
|
179
|
+
| DELETE | `/api/jobs/:id` | Delete a job |
|
|
180
|
+
| DELETE | `/api/jobs` | Bulk delete completed jobs |
|
|
181
|
+
|
|
182
|
+
## Security
|
|
183
|
+
|
|
184
|
+
Three-layer security model:
|
|
185
|
+
|
|
186
|
+
1. **Host Header Validation** — Only allows `localhost`, `127.0.0.1`, `[::1]` to prevent DNS Rebinding attacks.
|
|
187
|
+
2. **Origin Validation (CORS)** — Only accepts cross-origin requests from an allowed Origin list.
|
|
188
|
+
3. **Token Authentication** — Generates a random token on server startup. When `AUTH_REQUIRED=true`, all API requests must include an `Authorization: Bearer <token>` header.
|
|
189
|
+
|
|
190
|
+
### SSL/HTTPS
|
|
191
|
+
|
|
192
|
+
Generate local certificates with `mkcert` to enable HTTPS mode:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
mkcert -install
|
|
196
|
+
mkcert -cert-file certs/localhost+1.pem -key-file certs/localhost+1-key.pem localhost 127.0.0.1
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Configuration
|
|
200
|
+
|
|
201
|
+
Override settings via `data/settings.json` or environment variables:
|
|
202
|
+
|
|
203
|
+
| Setting | Default | Description |
|
|
204
|
+
|---------|---------|-------------|
|
|
205
|
+
| `skip_permissions` | `true` | Use `--dangerously-skip-permissions` flag |
|
|
206
|
+
| `model` | `""` | Claude model override (empty = default model) |
|
|
207
|
+
| `max_jobs` | `10` | Max concurrent background jobs |
|
|
208
|
+
| `target_repo` | `""` | Git repository path for Worktree creation |
|
|
209
|
+
| `base_branch` | `main` | Base branch for Worktree |
|
|
210
|
+
| `checkpoint_interval` | `5` | Checkpoint watch interval (seconds) |
|
|
211
|
+
| `append_system_prompt` | `""` | Additional system prompt text |
|
|
212
|
+
| `allowed_tools` | All tools | Tool allowlist for Claude |
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/cmux.app/Contents/Resources/bin:$PATH"
|
|
3
|
+
_src="$0"
|
|
4
|
+
while [[ -L "$_src" ]]; do
|
|
5
|
+
_dir="$(cd "$(dirname "$_src")" && pwd)"
|
|
6
|
+
_src="$(readlink "$_src")"
|
|
7
|
+
[[ "$_src" != /* ]] && _src="$_dir/$_src"
|
|
8
|
+
done
|
|
9
|
+
CONTROLLER_DIR="$(cd "$(dirname "$_src")/.." && pwd)"
|
|
10
|
+
cd "$CONTROLLER_DIR" || exit 1
|
|
11
|
+
source config.sh
|
|
12
|
+
mkdir -p "$LOGS_DIR" "$QUEUE_DIR" "$SESSIONS_DIR" "$WORKTREES_DIR"
|
|
13
|
+
|
|
14
|
+
# 서비스 시작
|
|
15
|
+
if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then true; else
|
|
16
|
+
rm -f "$FIFO_PATH" "$PID_FILE" 2>/dev/null
|
|
17
|
+
bash service/controller.sh start >> "$LOGS_DIR/service.log" 2>&1 &
|
|
18
|
+
sleep 2
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# 네이티브 앱 실행
|
|
22
|
+
exec /opt/homebrew/bin/python3 bin/native-app.py 2>> "$LOGS_DIR/app.log"
|
package/bin/claude-sh
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# claude-sh — Claude Shell Controller 진입점
|
|
4
|
+
#
|
|
5
|
+
# 사용법:
|
|
6
|
+
# claude-sh 인터랙티브 쉘 모드
|
|
7
|
+
# claude-sh "프롬프트" 단일 명령 실행
|
|
8
|
+
# claude-sh "프롬프트" & 백그라운드 실행
|
|
9
|
+
# echo "프롬프트" | claude-sh 파이프라인 입력
|
|
10
|
+
# ============================================================
|
|
11
|
+
|
|
12
|
+
_src="${BASH_SOURCE[0]}"
|
|
13
|
+
while [[ -L "$_src" ]]; do
|
|
14
|
+
_dir="$(cd "$(dirname "$_src")" && pwd)"
|
|
15
|
+
_src="$(readlink "$_src")"
|
|
16
|
+
[[ "$_src" != /* ]] && _src="$_dir/$_src"
|
|
17
|
+
done
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$_src")/.." && pwd)"
|
|
19
|
+
exec bash "${SCRIPT_DIR}/claude-shell.sh" "$@"
|
package/bin/controller
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Controller service entry point
|
|
3
|
+
# Usage:
|
|
4
|
+
# controller start — 서비스 시작
|
|
5
|
+
# controller stop — 서비스 중지
|
|
6
|
+
# controller restart — 재시작
|
|
7
|
+
# controller status — 상태 확인
|
|
8
|
+
|
|
9
|
+
_src="${BASH_SOURCE[0]}"
|
|
10
|
+
while [[ -L "$_src" ]]; do
|
|
11
|
+
_dir="$(cd "$(dirname "$_src")" && pwd)"
|
|
12
|
+
_src="$(readlink "$_src")"
|
|
13
|
+
[[ "$_src" != /* ]] && _src="$_dir/$_src"
|
|
14
|
+
done
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "$_src")/.." && pwd)"
|
|
16
|
+
source "${SCRIPT_DIR}/config.sh"
|
|
17
|
+
|
|
18
|
+
case "${1:-start}" in
|
|
19
|
+
start)
|
|
20
|
+
exec bash "${SCRIPT_DIR}/service/controller.sh" start
|
|
21
|
+
;;
|
|
22
|
+
stop)
|
|
23
|
+
exec bash "${SCRIPT_DIR}/service/controller.sh" stop
|
|
24
|
+
;;
|
|
25
|
+
restart)
|
|
26
|
+
bash "${SCRIPT_DIR}/service/controller.sh" stop
|
|
27
|
+
sleep 1
|
|
28
|
+
exec bash "${SCRIPT_DIR}/service/controller.sh" start
|
|
29
|
+
;;
|
|
30
|
+
status)
|
|
31
|
+
exec bash "${SCRIPT_DIR}/service/controller.sh" status
|
|
32
|
+
;;
|
|
33
|
+
*)
|
|
34
|
+
echo "사용법: controller {start|stop|restart|status}"
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Claude Controller — 웹 서버 실행 + 브라우저 자동 오픈
|
|
4
|
+
SSL/HTTPS 지원 + 토큰 인증
|
|
5
|
+
"""
|
|
6
|
+
import http.server
|
|
7
|
+
import os
|
|
8
|
+
import ssl
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import webbrowser
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
CONTROLLER_DIR = Path(__file__).resolve().parent.parent
|
|
16
|
+
SERVICE_SCRIPT = CONTROLLER_DIR / "service" / "controller.sh"
|
|
17
|
+
PID_FILE = CONTROLLER_DIR / "service" / "controller.pid"
|
|
18
|
+
LOGS_DIR = CONTROLLER_DIR / "logs"
|
|
19
|
+
PORT = int(os.environ.get("PORT", 8420))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_service_running():
|
|
23
|
+
if not PID_FILE.exists():
|
|
24
|
+
return False, None
|
|
25
|
+
try:
|
|
26
|
+
pid = int(PID_FILE.read_text().strip())
|
|
27
|
+
os.kill(pid, 0)
|
|
28
|
+
return True, pid
|
|
29
|
+
except (ValueError, ProcessLookupError, PermissionError, OSError):
|
|
30
|
+
return False, None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
for d in ["logs", "queue", "sessions", "uploads", "data", "certs"]:
|
|
35
|
+
(CONTROLLER_DIR / d).mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
# 서비스 시작
|
|
38
|
+
ok, pid = is_service_running()
|
|
39
|
+
if not ok:
|
|
40
|
+
log_fh = open(LOGS_DIR / "service.log", "a")
|
|
41
|
+
subprocess.Popen(["bash", str(SERVICE_SCRIPT), "start"],
|
|
42
|
+
stdout=log_fh, stderr=subprocess.STDOUT,
|
|
43
|
+
stdin=subprocess.DEVNULL, start_new_session=True, cwd=str(CONTROLLER_DIR))
|
|
44
|
+
log_fh.close()
|
|
45
|
+
for _ in range(30):
|
|
46
|
+
time.sleep(0.1)
|
|
47
|
+
ok, pid = is_service_running()
|
|
48
|
+
if ok:
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
# 웹 모듈 임포트
|
|
52
|
+
sys.path.insert(0, str(CONTROLLER_DIR / "web"))
|
|
53
|
+
from server import ControllerHandler
|
|
54
|
+
from config import SSL_CERT, SSL_KEY, PUBLIC_URL
|
|
55
|
+
from auth import generate_token
|
|
56
|
+
|
|
57
|
+
# 토큰 생성 (매 시작 시 새로 발급)
|
|
58
|
+
token = generate_token()
|
|
59
|
+
|
|
60
|
+
# SSL 인증서 확인
|
|
61
|
+
use_ssl = os.path.isfile(SSL_CERT) and os.path.isfile(SSL_KEY)
|
|
62
|
+
scheme = "https" if use_ssl else "http"
|
|
63
|
+
|
|
64
|
+
server = http.server.HTTPServer(("127.0.0.1", PORT), ControllerHandler)
|
|
65
|
+
|
|
66
|
+
if use_ssl:
|
|
67
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
68
|
+
ctx.load_cert_chain(SSL_CERT, SSL_KEY)
|
|
69
|
+
server.socket = ctx.wrap_socket(server.socket, server_side=True)
|
|
70
|
+
|
|
71
|
+
# 시작 배너
|
|
72
|
+
print(f"""
|
|
73
|
+
┌──────────────────────────────────────────────┐
|
|
74
|
+
│ Claude Controller │
|
|
75
|
+
├──────────────────────────────────────────────┤
|
|
76
|
+
│ API : {scheme}://localhost:{PORT:<24s}│
|
|
77
|
+
│ App : {PUBLIC_URL:<35s}│
|
|
78
|
+
│ SSL : {'ON' if use_ssl else 'OFF (HTTP 모드)':<35s}│
|
|
79
|
+
├──────────────────────────────────────────────┤
|
|
80
|
+
│ Auth Token (아래 토큰을 프론트엔드에 입력): │
|
|
81
|
+
│ {token:<43s}│
|
|
82
|
+
├──────────────────────────────────────────────┤
|
|
83
|
+
│ 종료 : Ctrl+C │
|
|
84
|
+
└──────────────────────────────────────────────┘
|
|
85
|
+
""")
|
|
86
|
+
|
|
87
|
+
if not use_ssl:
|
|
88
|
+
print(f" [참고] HTTPS를 사용하려면 mkcert로 인증서를 생성하세요:")
|
|
89
|
+
print(f" mkcert -install && mkcert -cert-file certs/localhost+1.pem \\")
|
|
90
|
+
print(f" -key-file certs/localhost+1-key.pem localhost 127.0.0.1\n")
|
|
91
|
+
|
|
92
|
+
webbrowser.open(PUBLIC_URL)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
server.serve_forever()
|
|
96
|
+
except KeyboardInterrupt:
|
|
97
|
+
print("\n 종료됨.")
|
|
98
|
+
server.server_close()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
main()
|
package/bin/send
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# send — Controller 서비스에 명령을 전송하는 클라이언트
|
|
4
|
+
# ============================================================
|
|
5
|
+
# Usage:
|
|
6
|
+
# send "Fix the bug in auth.py"
|
|
7
|
+
# send --cwd /path/to/repo "Fix the bug"
|
|
8
|
+
# send --id my-task-1 "Fix the bug"
|
|
9
|
+
# send --status
|
|
10
|
+
# send --result <job_id>
|
|
11
|
+
# send --stop
|
|
12
|
+
# ============================================================
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
_src="${BASH_SOURCE[0]}"
|
|
17
|
+
while [[ -L "$_src" ]]; do
|
|
18
|
+
_dir="$(cd "$(dirname "$_src")" && pwd)"
|
|
19
|
+
_src="$(readlink "$_src")"
|
|
20
|
+
[[ "$_src" != /* ]] && _src="$_dir/$_src"
|
|
21
|
+
done
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "$_src")" && pwd)"
|
|
23
|
+
source "${SCRIPT_DIR}/../config.sh"
|
|
24
|
+
source "${SCRIPT_DIR}/../lib/jobs.sh"
|
|
25
|
+
|
|
26
|
+
# ── 사용법 출력 ─────────────────────────────────────────────
|
|
27
|
+
usage() {
|
|
28
|
+
cat <<'USAGE'
|
|
29
|
+
사용법:
|
|
30
|
+
send "프롬프트" 프롬프트 전송
|
|
31
|
+
send --cwd <경로> "프롬프트" 작업 디렉토리 지정하여 전송
|
|
32
|
+
send --id <작업ID> "프롬프트" 사용자 지정 ID로 전송
|
|
33
|
+
send --worktree [--repo <경로>] "프롬프트" worktree에서 격리 실행
|
|
34
|
+
send --status 전체 작업 상태 조회
|
|
35
|
+
send --result <작업ID> 작업 결과 조회
|
|
36
|
+
send --stop 서비스 종료
|
|
37
|
+
send --help 이 도움말 출력
|
|
38
|
+
USAGE
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ── UUID 생성 ───────────────────────────────────────────────
|
|
42
|
+
generate_id() {
|
|
43
|
+
echo "$(date +%s)-$$-${RANDOM}"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# ── 서비스 실행 여부 확인 ───────────────────────────────────
|
|
47
|
+
check_service() {
|
|
48
|
+
if [[ ! -p "$FIFO_PATH" ]]; then
|
|
49
|
+
echo "[오류] 서비스가 실행 중이 아닙니다. (FIFO 없음: $FIFO_PATH)" >&2
|
|
50
|
+
echo " 먼저 서비스를 시작하세요." >&2
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ── 플래그 파싱 ─────────────────────────────────────────────
|
|
56
|
+
CWD=""
|
|
57
|
+
JOB_ID=""
|
|
58
|
+
ACTION="send" # send | status | result | stop
|
|
59
|
+
USE_WORKTREE=""
|
|
60
|
+
REPO_PATH=""
|
|
61
|
+
|
|
62
|
+
while [[ $# -gt 0 ]]; do
|
|
63
|
+
case "$1" in
|
|
64
|
+
--cwd)
|
|
65
|
+
CWD="$2"
|
|
66
|
+
shift 2
|
|
67
|
+
;;
|
|
68
|
+
--id)
|
|
69
|
+
JOB_ID="$2"
|
|
70
|
+
shift 2
|
|
71
|
+
;;
|
|
72
|
+
--worktree)
|
|
73
|
+
USE_WORKTREE="true"
|
|
74
|
+
shift
|
|
75
|
+
;;
|
|
76
|
+
--repo)
|
|
77
|
+
REPO_PATH="$2"
|
|
78
|
+
shift 2
|
|
79
|
+
;;
|
|
80
|
+
--status)
|
|
81
|
+
ACTION="status"
|
|
82
|
+
shift
|
|
83
|
+
;;
|
|
84
|
+
--result)
|
|
85
|
+
ACTION="result"
|
|
86
|
+
RESULT_JOB_ID="$2"
|
|
87
|
+
shift 2
|
|
88
|
+
;;
|
|
89
|
+
--stop)
|
|
90
|
+
ACTION="stop"
|
|
91
|
+
shift
|
|
92
|
+
;;
|
|
93
|
+
--help|-h)
|
|
94
|
+
usage
|
|
95
|
+
exit 0
|
|
96
|
+
;;
|
|
97
|
+
-*)
|
|
98
|
+
echo "[오류] 알 수 없는 옵션: $1" >&2
|
|
99
|
+
usage
|
|
100
|
+
exit 1
|
|
101
|
+
;;
|
|
102
|
+
*)
|
|
103
|
+
PROMPT="$1"
|
|
104
|
+
shift
|
|
105
|
+
;;
|
|
106
|
+
esac
|
|
107
|
+
done
|
|
108
|
+
|
|
109
|
+
# ── 액션 실행 ───────────────────────────────────────────────
|
|
110
|
+
case "$ACTION" in
|
|
111
|
+
|
|
112
|
+
status)
|
|
113
|
+
jobs_list
|
|
114
|
+
;;
|
|
115
|
+
|
|
116
|
+
result)
|
|
117
|
+
if [[ -z "${RESULT_JOB_ID:-}" ]]; then
|
|
118
|
+
echo "[오류] --result 에 작업 ID를 지정하세요." >&2
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
job_result "$RESULT_JOB_ID"
|
|
122
|
+
;;
|
|
123
|
+
|
|
124
|
+
stop)
|
|
125
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
126
|
+
echo "[오류] PID 파일을 찾을 수 없습니다. ($PID_FILE)" >&2
|
|
127
|
+
echo " 서비스가 실행 중이 아닌 것 같습니다." >&2
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
SERVICE_PID=$(cat "$PID_FILE")
|
|
131
|
+
if kill -0 "$SERVICE_PID" 2>/dev/null; then
|
|
132
|
+
kill -TERM "$SERVICE_PID"
|
|
133
|
+
echo "[완료] 서비스(PID: $SERVICE_PID)에 종료 신호를 전송했습니다."
|
|
134
|
+
else
|
|
135
|
+
echo "[알림] 서비스(PID: $SERVICE_PID)는 이미 종료된 상태입니다."
|
|
136
|
+
rm -f "$PID_FILE"
|
|
137
|
+
fi
|
|
138
|
+
;;
|
|
139
|
+
|
|
140
|
+
send)
|
|
141
|
+
if [[ -z "${PROMPT:-}" ]]; then
|
|
142
|
+
echo "[오류] 전송할 프롬프트를 입력하세요." >&2
|
|
143
|
+
usage
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
check_service
|
|
148
|
+
|
|
149
|
+
# ID가 없으면 자동 생성
|
|
150
|
+
[[ -z "$JOB_ID" ]] && JOB_ID=$(generate_id)
|
|
151
|
+
|
|
152
|
+
# CWD 기본값: 현재 디렉토리
|
|
153
|
+
[[ -z "$CWD" ]] && CWD="$(pwd)"
|
|
154
|
+
|
|
155
|
+
# JSON 페이로드 구성 (jq 사용 — 특수문자 안전 처리)
|
|
156
|
+
JQ_ARGS=(
|
|
157
|
+
--arg id "$JOB_ID"
|
|
158
|
+
--arg prompt "$PROMPT"
|
|
159
|
+
--arg cwd "$CWD"
|
|
160
|
+
)
|
|
161
|
+
JQ_EXPR='{id: $id, prompt: $prompt, cwd: $cwd}'
|
|
162
|
+
|
|
163
|
+
if [[ -n "$USE_WORKTREE" ]]; then
|
|
164
|
+
JQ_ARGS+=(--arg worktree "true")
|
|
165
|
+
JQ_EXPR='{id: $id, prompt: $prompt, cwd: $cwd, worktree: $worktree}'
|
|
166
|
+
if [[ -n "$REPO_PATH" ]]; then
|
|
167
|
+
JQ_ARGS+=(--arg repo "$REPO_PATH")
|
|
168
|
+
JQ_EXPR='{id: $id, prompt: $prompt, cwd: $cwd, worktree: $worktree, repo: $repo}'
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
PAYLOAD=$(jq -cn "${JQ_ARGS[@]}" "$JQ_EXPR")
|
|
173
|
+
|
|
174
|
+
# FIFO에 전송
|
|
175
|
+
echo "$PAYLOAD" > "$FIFO_PATH"
|
|
176
|
+
|
|
177
|
+
echo "[전송 완료] 작업이 서비스로 전달되었습니다."
|
|
178
|
+
echo " 작업 ID : $JOB_ID"
|
|
179
|
+
echo " 디렉토리: $CWD"
|
|
180
|
+
[[ -n "$USE_WORKTREE" ]] && echo " 워크트리: ON${REPO_PATH:+ (repo: $REPO_PATH)}"
|
|
181
|
+
display_prompt="${PROMPT:0:80}"
|
|
182
|
+
[[ ${#PROMPT} -gt 80 ]] && display_prompt="${display_prompt}..."
|
|
183
|
+
echo " 프롬프트: $display_prompt"
|
|
184
|
+
;;
|
|
185
|
+
esac
|
package/bin/start
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# start — Controller 서비스 + TUI 일괄 실행
|
|
4
|
+
# Usage: ./start [--repo /path/to/git/repo]
|
|
5
|
+
# ============================================================
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
|
|
8
|
+
_src="${BASH_SOURCE[0]}"
|
|
9
|
+
while [[ -L "$_src" ]]; do
|
|
10
|
+
_dir="$(cd "$(dirname "$_src")" && pwd)"
|
|
11
|
+
_src="$(readlink "$_src")"
|
|
12
|
+
[[ "$_src" != /* ]] && _src="$_dir/$_src"
|
|
13
|
+
done
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$_src")/.." && pwd)"
|
|
15
|
+
source "${SCRIPT_DIR}/config.sh"
|
|
16
|
+
|
|
17
|
+
# 옵션 파싱
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--repo)
|
|
21
|
+
export TARGET_REPO="$2"
|
|
22
|
+
shift 2
|
|
23
|
+
;;
|
|
24
|
+
*)
|
|
25
|
+
shift
|
|
26
|
+
;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
# 사전 검증
|
|
31
|
+
if [[ -z "$CLAUDE_BIN" ]]; then
|
|
32
|
+
echo "[오류] claude CLI를 찾을 수 없습니다."; exit 1
|
|
33
|
+
fi
|
|
34
|
+
if ! command -v python3 &>/dev/null; then
|
|
35
|
+
echo "[오류] python3가 필요합니다."; exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
mkdir -p "$LOGS_DIR" "$QUEUE_DIR" "$SESSIONS_DIR" "$WORKTREES_DIR"
|
|
39
|
+
|
|
40
|
+
echo ""
|
|
41
|
+
echo " ╔══════════════════════════════════════════╗"
|
|
42
|
+
echo " ║ Controller Service + TUI ║"
|
|
43
|
+
echo " ╚══════════════════════════════════════════╝"
|
|
44
|
+
echo ""
|
|
45
|
+
|
|
46
|
+
# 컨트롤러 서비스 시작
|
|
47
|
+
if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
|
48
|
+
echo " [서비스] 이미 실행 중 (PID: $(cat "$PID_FILE"))"
|
|
49
|
+
else
|
|
50
|
+
echo " [서비스] 컨트롤러 데몬 시작 중..."
|
|
51
|
+
bash "${SCRIPT_DIR}/service/controller.sh" start &
|
|
52
|
+
sleep 2
|
|
53
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
54
|
+
echo " [서비스] 시작됨 (PID: $(cat "$PID_FILE"))"
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo ""
|
|
59
|
+
echo " [TUI] 터미널 프로그램을 시작합니다..."
|
|
60
|
+
echo " [TUI] 종료: q 키"
|
|
61
|
+
echo ""
|
|
62
|
+
|
|
63
|
+
# Ctrl+C로 서비스도 함께 종료
|
|
64
|
+
cleanup_all() {
|
|
65
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
66
|
+
kill "$(cat "$PID_FILE")" 2>/dev/null
|
|
67
|
+
fi
|
|
68
|
+
echo ""
|
|
69
|
+
echo " 종료됨."
|
|
70
|
+
}
|
|
71
|
+
trap cleanup_all INT TERM
|
|
72
|
+
|
|
73
|
+
# TUI 포그라운드 실행
|
|
74
|
+
sleep 1
|
|
75
|
+
exec python3 "${SCRIPT_DIR}/bin/tui"
|