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/config.sh
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# Controller Service — Configuration
|
|
4
|
+
# ============================================================
|
|
5
|
+
|
|
6
|
+
# Claude CLI 경로
|
|
7
|
+
# 1) 환경변수 CLAUDE_BIN이 있으면 사용
|
|
8
|
+
# 2) PATH에서 claude를 찾음
|
|
9
|
+
# 3) macOS 앱 기본 경로
|
|
10
|
+
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo "/Applications/cmux.app/Contents/Resources/bin/claude")}"
|
|
11
|
+
|
|
12
|
+
# 기본 출력 형식 (stream-json: 실시간 토큰 스트리밍)
|
|
13
|
+
DEFAULT_OUTPUT_FORMAT="${DEFAULT_OUTPUT_FORMAT:-stream-json}"
|
|
14
|
+
|
|
15
|
+
# 모든 도구 권한 허용
|
|
16
|
+
DEFAULT_ALLOWED_TOOLS="${DEFAULT_ALLOWED_TOOLS:-Bash,Read,Write,Edit,Glob,Grep,Agent,NotebookEdit,WebFetch,WebSearch}"
|
|
17
|
+
|
|
18
|
+
# 모델 설정
|
|
19
|
+
DEFAULT_MODEL="${DEFAULT_MODEL:-}"
|
|
20
|
+
|
|
21
|
+
# 디렉토리 경로
|
|
22
|
+
CONTROLLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
LOGS_DIR="${CONTROLLER_DIR}/logs"
|
|
24
|
+
SESSIONS_DIR="${CONTROLLER_DIR}/sessions"
|
|
25
|
+
QUEUE_DIR="${CONTROLLER_DIR}/queue"
|
|
26
|
+
|
|
27
|
+
# FIFO 파이프 경로 — 외부에서 이 파이프에 쓰면 서비스가 수신
|
|
28
|
+
FIFO_PATH="${CONTROLLER_DIR}/queue/controller.pipe"
|
|
29
|
+
|
|
30
|
+
# PID 파일
|
|
31
|
+
PID_FILE="${CONTROLLER_DIR}/service/controller.pid"
|
|
32
|
+
|
|
33
|
+
# 최대 동시 백그라운드 작업 수
|
|
34
|
+
MAX_BACKGROUND_JOBS="${MAX_BACKGROUND_JOBS:-10}"
|
|
35
|
+
|
|
36
|
+
# 시스템 프롬프트 추가
|
|
37
|
+
APPEND_SYSTEM_PROMPT="${APPEND_SYSTEM_PROMPT:-}"
|
|
38
|
+
|
|
39
|
+
# 작업 디렉토리 — claude -p 실행 시 --cwd
|
|
40
|
+
WORKING_DIR="${WORKING_DIR:-}"
|
|
41
|
+
|
|
42
|
+
# ── Worktree 설정 ─────────────────────────────────────────
|
|
43
|
+
# 대상 Git 저장소 (worktree 생성 원본)
|
|
44
|
+
TARGET_REPO="${TARGET_REPO:-}"
|
|
45
|
+
|
|
46
|
+
# 워크트리 기준 브랜치
|
|
47
|
+
BASE_BRANCH="${BASE_BRANCH:-main}"
|
|
48
|
+
|
|
49
|
+
# 워크트리 저장 디렉토리
|
|
50
|
+
WORKTREES_DIR="${CONTROLLER_DIR}/worktrees"
|
|
51
|
+
|
|
52
|
+
# ── 권한 설정 ──────────────────────────────────────────────
|
|
53
|
+
# true로 설정 시 --dangerously-skip-permissions 사용 (모든 도구 무제한 허용)
|
|
54
|
+
SKIP_PERMISSIONS="${SKIP_PERMISSIONS:-true}"
|
|
55
|
+
|
|
56
|
+
# ── Checkpoint 설정 ────────────────────────────────────────
|
|
57
|
+
# 체크포인트 감시 주기 (초) — 이 간격으로 worktree 변경을 확인
|
|
58
|
+
CHECKPOINT_INTERVAL="${CHECKPOINT_INTERVAL:-5}"
|
|
59
|
+
|
|
60
|
+
# ── settings.json 오버라이드 ────────────────────────────────
|
|
61
|
+
# data/settings.json이 존재하면 해당 값으로 기본값을 덮어씀
|
|
62
|
+
SETTINGS_FILE="${CONTROLLER_DIR}/data/settings.json"
|
|
63
|
+
if [[ -f "$SETTINGS_FILE" ]] && command -v jq &>/dev/null; then
|
|
64
|
+
_s() { jq -r "$1 // empty" "$SETTINGS_FILE" 2>/dev/null; }
|
|
65
|
+
_v=$(_s '.skip_permissions'); [[ -n "$_v" ]] && SKIP_PERMISSIONS="$_v"
|
|
66
|
+
_v=$(_s '.allowed_tools'); [[ -n "$_v" ]] && DEFAULT_ALLOWED_TOOLS="$_v"
|
|
67
|
+
_v=$(_s '.model'); [[ -n "$_v" ]] && DEFAULT_MODEL="$_v"
|
|
68
|
+
_v=$(_s '.max_jobs'); [[ -n "$_v" ]] && MAX_BACKGROUND_JOBS="$_v"
|
|
69
|
+
_v=$(_s '.append_system_prompt'); [[ -n "$_v" ]] && APPEND_SYSTEM_PROMPT="$_v"
|
|
70
|
+
_v=$(_s '.target_repo'); [[ -n "$_v" ]] && TARGET_REPO="$_v"
|
|
71
|
+
_v=$(_s '.base_branch'); [[ -n "$_v" ]] && BASE_BRANCH="$_v"
|
|
72
|
+
_v=$(_s '.checkpoint_interval'); [[ -n "$_v" ]] && CHECKPOINT_INTERVAL="$_v"
|
|
73
|
+
unset -f _s; unset _v
|
|
74
|
+
fi
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# Checkpoint 관리 모듈
|
|
4
|
+
# Worktree에서 실행 중인 job의 파일 변경을 자동 커밋(checkpoint)하고
|
|
5
|
+
# 특정 체크포인트로 되돌리는(rewind) 기능을 제공합니다.
|
|
6
|
+
#
|
|
7
|
+
# 의존: jobs.sh (_get_meta_field), config.sh (LOGS_DIR)
|
|
8
|
+
# ============================================================
|
|
9
|
+
|
|
10
|
+
# ── 설정 ────────────────────────────────────────────────────
|
|
11
|
+
CHECKPOINT_INTERVAL="${CHECKPOINT_INTERVAL:-5}"
|
|
12
|
+
CHECKPOINT_PREFIX="ckpt"
|
|
13
|
+
|
|
14
|
+
# ── 체크포인트 워처 루프 ────────────────────────────────────
|
|
15
|
+
# 백그라운드에서 worktree를 감시하며 파일 변경이 안정화되면 자동 커밋.
|
|
16
|
+
# 호출 시 반드시 & (백그라운드)로 실행할 것.
|
|
17
|
+
#
|
|
18
|
+
# Usage: checkpoint_watcher_loop <worktree_path> <job_id> <meta_file> &
|
|
19
|
+
checkpoint_watcher_loop() {
|
|
20
|
+
local wt_path="$1"
|
|
21
|
+
local job_id="$2"
|
|
22
|
+
local meta_file="$3"
|
|
23
|
+
local turn=0
|
|
24
|
+
local prev_hash=""
|
|
25
|
+
|
|
26
|
+
cd "$wt_path" 2>/dev/null || return 1
|
|
27
|
+
|
|
28
|
+
# worktree 로컬 git 설정 (커밋용)
|
|
29
|
+
git config user.email "checkpoint@controller.local" 2>/dev/null
|
|
30
|
+
git config user.name "Controller Checkpoint" 2>/dev/null
|
|
31
|
+
|
|
32
|
+
# 초기 상태에 untracked 파일이 있으면 커밋
|
|
33
|
+
git add -A 2>/dev/null
|
|
34
|
+
if ! git diff --cached --quiet 2>/dev/null; then
|
|
35
|
+
git commit -m "${CHECKPOINT_PREFIX}:${job_id}:0:init" --no-verify 2>/dev/null || true
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
while true; do
|
|
39
|
+
sleep "$CHECKPOINT_INTERVAL"
|
|
40
|
+
|
|
41
|
+
# job 상태 확인 — running이 아니면 최종 커밋 후 종료
|
|
42
|
+
local status=""
|
|
43
|
+
[[ -f "$meta_file" ]] && status=$(grep '^STATUS=' "$meta_file" 2>/dev/null | tail -1 | sed 's/^STATUS=//')
|
|
44
|
+
if [[ "$status" != "running" ]]; then
|
|
45
|
+
_ckpt_commit_if_dirty "$wt_path" "$job_id" "$turn" "final"
|
|
46
|
+
break
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# 현재 변경사항 해시 (diff + untracked files)
|
|
50
|
+
local curr_hash
|
|
51
|
+
curr_hash=$( { git diff 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null; } | md5 2>/dev/null || echo "none" )
|
|
52
|
+
|
|
53
|
+
# 빈 해시 = 변경 없음
|
|
54
|
+
local empty_hash
|
|
55
|
+
empty_hash=$(echo -n | md5 2>/dev/null || echo "none")
|
|
56
|
+
if [[ "$curr_hash" == "$empty_hash" ]]; then
|
|
57
|
+
prev_hash=""
|
|
58
|
+
continue
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# 안정화 대기: 이전 체크와 해시가 다르면 아직 변경 진행 중
|
|
62
|
+
if [[ "$curr_hash" != "$prev_hash" ]]; then
|
|
63
|
+
prev_hash="$curr_hash"
|
|
64
|
+
continue
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# 2회 연속 동일 해시 = 변경 완료, 커밋 수행
|
|
68
|
+
(( turn++ )) || true
|
|
69
|
+
_ckpt_commit_if_dirty "$wt_path" "$job_id" "$turn" ""
|
|
70
|
+
prev_hash=""
|
|
71
|
+
done
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ── 내부: 변경이 있으면 커밋 ────────────────────────────────
|
|
75
|
+
_ckpt_commit_if_dirty() {
|
|
76
|
+
local wt_path="$1" job_id="$2" turn="$3" suffix="$4"
|
|
77
|
+
|
|
78
|
+
cd "$wt_path" 2>/dev/null || return 1
|
|
79
|
+
|
|
80
|
+
git add -A 2>/dev/null
|
|
81
|
+
|
|
82
|
+
if ! git diff --cached --quiet 2>/dev/null; then
|
|
83
|
+
local msg="${CHECKPOINT_PREFIX}:${job_id}:${turn}"
|
|
84
|
+
[[ -n "$suffix" ]] && msg="${msg}:${suffix}"
|
|
85
|
+
|
|
86
|
+
# 변경 파일 목록 (최대 5개)
|
|
87
|
+
local changed
|
|
88
|
+
changed=$(git diff --cached --name-only 2>/dev/null | head -5 | tr '\n' ', ')
|
|
89
|
+
[[ -n "$changed" ]] && msg="${msg} [${changed%,}]"
|
|
90
|
+
|
|
91
|
+
git commit -m "$msg" --no-verify 2>/dev/null || true
|
|
92
|
+
fi
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ── 체크포인트 목록 조회 ────────────────────────────────────
|
|
96
|
+
# worktree의 git log에서 이 job의 checkpoint 커밋들을 JSON 배열로 반환.
|
|
97
|
+
#
|
|
98
|
+
# Usage: checkpoint_list <worktree_path> <job_id>
|
|
99
|
+
# Output: JSON array
|
|
100
|
+
checkpoint_list() {
|
|
101
|
+
local wt_path="$1"
|
|
102
|
+
local job_id="$2"
|
|
103
|
+
|
|
104
|
+
# worktree 유효성 확인
|
|
105
|
+
if [[ ! -d "$wt_path" ]] || ! (cd "$wt_path" && git rev-parse --git-dir >/dev/null 2>&1); then
|
|
106
|
+
echo "[]"
|
|
107
|
+
return
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
cd "$wt_path" 2>/dev/null || { echo "[]"; return; }
|
|
111
|
+
|
|
112
|
+
local result="["
|
|
113
|
+
local first=true
|
|
114
|
+
|
|
115
|
+
while IFS='|' read -r hash ts msg; do
|
|
116
|
+
[[ -z "$hash" ]] && continue
|
|
117
|
+
|
|
118
|
+
# 턴 번호 추출
|
|
119
|
+
local turn_num
|
|
120
|
+
turn_num=$(echo "$msg" | sed -n "s/.*${CHECKPOINT_PREFIX}:${job_id}:\([0-9]*\).*/\1/p")
|
|
121
|
+
[[ -z "$turn_num" ]] && turn_num=0
|
|
122
|
+
|
|
123
|
+
# 변경 파일 수
|
|
124
|
+
local file_count
|
|
125
|
+
file_count=$(git diff-tree --no-commit-id --name-only -r "$hash" 2>/dev/null | wc -l | tr -d ' ')
|
|
126
|
+
|
|
127
|
+
# 변경 파일 목록
|
|
128
|
+
local file_list
|
|
129
|
+
file_list=$(git diff-tree --no-commit-id --name-only -r "$hash" 2>/dev/null | head -10 | jq -R . 2>/dev/null | jq -s . 2>/dev/null)
|
|
130
|
+
[[ -z "$file_list" ]] && file_list="[]"
|
|
131
|
+
|
|
132
|
+
[[ "$first" == "true" ]] && first=false || result="${result},"
|
|
133
|
+
result="${result}{\"hash\":\"${hash}\",\"turn\":${turn_num},\"timestamp\":\"${ts}\",\"message\":$(echo "$msg" | jq -R .),\"files_changed\":${file_count},\"files\":${file_list}}"
|
|
134
|
+
done < <(git log --format='%H|%aI|%s' --grep="${CHECKPOINT_PREFIX}:${job_id}:" 2>/dev/null)
|
|
135
|
+
|
|
136
|
+
result="${result}]"
|
|
137
|
+
echo "$result"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ── 대화 컨텍스트 추출 ──────────────────────────────────────
|
|
141
|
+
# stream-json .out 파일에서 assistant 텍스트 + tool_use 요약을 추출.
|
|
142
|
+
# 최대 max_chars 글자까지만 반환하여 프롬프트 크기를 제한.
|
|
143
|
+
#
|
|
144
|
+
# Usage: checkpoint_extract_context <out_file> [max_chars]
|
|
145
|
+
# Output: plain text
|
|
146
|
+
checkpoint_extract_context() {
|
|
147
|
+
local out_file="$1"
|
|
148
|
+
local max_chars="${2:-4000}"
|
|
149
|
+
|
|
150
|
+
[[ -f "$out_file" ]] || return 0
|
|
151
|
+
|
|
152
|
+
# jq로 한 번에 추출 (성능 + 안정성)
|
|
153
|
+
jq -r '
|
|
154
|
+
if .type == "assistant" then
|
|
155
|
+
[.message.content[]? |
|
|
156
|
+
if .type == "text" then "[\(.type)] \(.text[0:300])"
|
|
157
|
+
elif .type == "tool_use" then "[tool: \(.name)] \(.input | tostring[0:150])"
|
|
158
|
+
else empty end
|
|
159
|
+
] | join("\n")
|
|
160
|
+
elif .type == "result" then
|
|
161
|
+
"[result] \(.result[0:300] // "")"
|
|
162
|
+
else empty end
|
|
163
|
+
' "$out_file" 2>/dev/null | head -c "$max_chars"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# ── Rewind 실행 ──────────────────────────────────────────────
|
|
167
|
+
# 1. 실행 중인 job 종료
|
|
168
|
+
# 2. worktree를 지정된 checkpoint로 git reset --hard
|
|
169
|
+
# 3. 대화 컨텍스트 추출
|
|
170
|
+
# 4. 새 프롬프트 생성 (FIFO 전송은 호출자가 담당)
|
|
171
|
+
#
|
|
172
|
+
# Usage: checkpoint_rewind <job_id> <checkpoint_hash> <new_prompt>
|
|
173
|
+
# Output: 생성된 full_prompt (stdout), 실패 시 exit 1
|
|
174
|
+
checkpoint_rewind() {
|
|
175
|
+
local job_id="$1"
|
|
176
|
+
local ckpt_hash="$2"
|
|
177
|
+
local new_prompt="$3"
|
|
178
|
+
|
|
179
|
+
local meta_file="${LOGS_DIR}/job_${job_id}.meta"
|
|
180
|
+
local out_file="${LOGS_DIR}/job_${job_id}.out"
|
|
181
|
+
|
|
182
|
+
# meta 파일 확인
|
|
183
|
+
if [[ ! -f "$meta_file" ]]; then
|
|
184
|
+
echo "[오류] Job #${job_id}를 찾을 수 없습니다." >&2
|
|
185
|
+
return 1
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# 실행 중이면 종료
|
|
189
|
+
local status
|
|
190
|
+
status=$(_get_meta_field "$meta_file" "STATUS")
|
|
191
|
+
if [[ "$status" == "running" ]]; then
|
|
192
|
+
job_kill "$job_id" >/dev/null 2>&1
|
|
193
|
+
sleep 1
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# worktree 경로 확인
|
|
197
|
+
local wt_path
|
|
198
|
+
wt_path=$(_get_meta_field "$meta_file" "WORKTREE")
|
|
199
|
+
if [[ -z "$wt_path" || ! -d "$wt_path" ]]; then
|
|
200
|
+
echo "[오류] 워크트리를 찾을 수 없습니다." >&2
|
|
201
|
+
return 1
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
# checkpoint 커밋 유효성 확인
|
|
205
|
+
if ! (cd "$wt_path" && git cat-file -t "$ckpt_hash" >/dev/null 2>&1); then
|
|
206
|
+
echo "[오류] 유효하지 않은 체크포인트: $ckpt_hash" >&2
|
|
207
|
+
return 1
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
# 체크포인트로 reset
|
|
211
|
+
(cd "$wt_path" && git reset --hard "$ckpt_hash" 2>/dev/null) || {
|
|
212
|
+
echo "[오류] git reset 실패" >&2
|
|
213
|
+
return 1
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# 대화 컨텍스트 추출
|
|
217
|
+
local context=""
|
|
218
|
+
if [[ -f "$out_file" ]]; then
|
|
219
|
+
context=$(checkpoint_extract_context "$out_file" 4000)
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# 새 프롬프트 구성
|
|
223
|
+
if [[ -n "$context" ]]; then
|
|
224
|
+
cat <<REWIND_PROMPT
|
|
225
|
+
[이전 작업 컨텍스트 — 아래는 이전 세션에서 수행된 작업 요약입니다]
|
|
226
|
+
${context}
|
|
227
|
+
|
|
228
|
+
[Rewind 지시사항]
|
|
229
|
+
파일 상태가 위 작업 중간의 체크포인트 시점으로 복원되었습니다.
|
|
230
|
+
이전 작업 내용을 참고하되, 이어서 다음을 수행하세요:
|
|
231
|
+
|
|
232
|
+
${new_prompt}
|
|
233
|
+
REWIND_PROMPT
|
|
234
|
+
else
|
|
235
|
+
echo "$new_prompt"
|
|
236
|
+
fi
|
|
237
|
+
}
|
package/lib/executor.sh
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# 실행 엔진 — 서비스 데몬 패턴
|
|
4
|
+
# FIFO로 수신된 JSON 메시지를 파싱하여 claude -p 를 백그라운드로 실행합니다.
|
|
5
|
+
# 모든 실행은 비동기이며, 결과는 $LOGS_DIR/job_<id>.out 에 저장됩니다.
|
|
6
|
+
# ============================================================
|
|
7
|
+
|
|
8
|
+
# ── claude -p 실행 (내부 전용) ─────────────────────────────
|
|
9
|
+
# JSON에서 파싱한 개별 값을 받아 claude CLI를 호출합니다.
|
|
10
|
+
# eval 대신 인자 배열을 직접 사용하여 글로브 확장·인젝션 방지
|
|
11
|
+
_run_claude() {
|
|
12
|
+
local prompt="$1"
|
|
13
|
+
local cwd="$2"
|
|
14
|
+
local out_file="$3"
|
|
15
|
+
local session_mode="${4:-}"
|
|
16
|
+
local session_id="${5:-}"
|
|
17
|
+
|
|
18
|
+
local args=()
|
|
19
|
+
args+=(-p "$prompt")
|
|
20
|
+
args+=(--output-format json)
|
|
21
|
+
|
|
22
|
+
# 모든 도구 권한 허용
|
|
23
|
+
if [[ -n "${DEFAULT_ALLOWED_TOOLS:-}" ]]; then
|
|
24
|
+
args+=(--allowedTools "$DEFAULT_ALLOWED_TOOLS")
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# 모델 지정
|
|
28
|
+
if [[ -n "${DEFAULT_MODEL:-}" ]]; then
|
|
29
|
+
args+=(--model "$DEFAULT_MODEL")
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# 시스템 프롬프트 추가
|
|
33
|
+
if [[ -n "${APPEND_SYSTEM_PROMPT:-}" ]]; then
|
|
34
|
+
args+=(--append-system-prompt "$APPEND_SYSTEM_PROMPT")
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# 작업 디렉토리 — JSON에서 받은 cwd 우선, 없으면 글로벌 WORKING_DIR
|
|
38
|
+
local effective_cwd="${cwd:-${WORKING_DIR:-}}"
|
|
39
|
+
if [[ -n "$effective_cwd" ]]; then
|
|
40
|
+
args+=(--cwd "$effective_cwd")
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# 세션 이어가기 플래그
|
|
44
|
+
case "${session_mode}" in
|
|
45
|
+
continue)
|
|
46
|
+
args+=(--continue)
|
|
47
|
+
;;
|
|
48
|
+
resume)
|
|
49
|
+
if [[ -n "$session_id" ]]; then
|
|
50
|
+
args+=(--resume "$session_id")
|
|
51
|
+
fi
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
# 직접 실행 — 배열로 안전하게 호출
|
|
56
|
+
"$CLAUDE_BIN" "${args[@]}" > "$out_file" 2>&1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# ── JSON 기반 실행 (서비스 데몬의 진입점) ──────────────────
|
|
60
|
+
# FIFO에서 수신한 JSON 문자열을 파싱하여 백그라운드로 claude -p 실행
|
|
61
|
+
#
|
|
62
|
+
# JSON 필드:
|
|
63
|
+
# id (필수) — 작업 식별자. 외부에서 부여한 고유 ID
|
|
64
|
+
# prompt (필수) — claude에 전달할 프롬프트
|
|
65
|
+
# cwd (선택) — 작업 디렉토리. 미지정 시 WORKING_DIR 사용
|
|
66
|
+
# session (선택) — "continue" | "resume:<session_id>"
|
|
67
|
+
#
|
|
68
|
+
# 반환: stdout에 job_id 출력
|
|
69
|
+
execute_from_json() {
|
|
70
|
+
local json_string="$1"
|
|
71
|
+
|
|
72
|
+
# ── JSON 유효성 검사 ──
|
|
73
|
+
if ! echo "$json_string" | jq empty 2>/dev/null; then
|
|
74
|
+
echo "[오류] 유효하지 않은 JSON: $json_string" >&2
|
|
75
|
+
return 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ── 필수 필드 파싱 ──
|
|
79
|
+
local job_id prompt cwd session_raw images_json
|
|
80
|
+
job_id=$(echo "$json_string" | jq -r '.id // empty')
|
|
81
|
+
prompt=$(echo "$json_string" | jq -r '.prompt // empty')
|
|
82
|
+
cwd=$(echo "$json_string" | jq -r '.cwd // empty')
|
|
83
|
+
session_raw=$(echo "$json_string" | jq -r '.session // empty')
|
|
84
|
+
images_json=$(echo "$json_string" | jq -r '.images // empty')
|
|
85
|
+
|
|
86
|
+
if [[ -z "$job_id" ]]; then
|
|
87
|
+
echo "[오류] JSON에 'id' 필드가 없습니다." >&2
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if [[ -z "$prompt" ]]; then
|
|
92
|
+
echo "[오류] JSON에 'prompt' 필드가 없습니다." >&2
|
|
93
|
+
return 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ── 첨부 이미지가 있으면 프롬프트에 경로 삽입 ──
|
|
97
|
+
if [[ -n "$images_json" && "$images_json" != "null" ]]; then
|
|
98
|
+
local img_count
|
|
99
|
+
img_count=$(echo "$images_json" | jq -r 'length')
|
|
100
|
+
if [[ "$img_count" -gt 0 ]]; then
|
|
101
|
+
local img_lines=""
|
|
102
|
+
for i in $(seq 0 $(( img_count - 1 ))); do
|
|
103
|
+
local img_path
|
|
104
|
+
img_path=$(echo "$images_json" | jq -r ".[$i]")
|
|
105
|
+
img_lines="${img_lines}
|
|
106
|
+
- ${img_path}"
|
|
107
|
+
done
|
|
108
|
+
prompt="[첨부 파일 — Read 도구로 확인하세요]${img_lines}
|
|
109
|
+
|
|
110
|
+
${prompt}"
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# ── 세션 모드 분리 (예: "resume:abc-123") ──
|
|
115
|
+
local session_mode="" session_id=""
|
|
116
|
+
if [[ -n "$session_raw" ]]; then
|
|
117
|
+
case "$session_raw" in
|
|
118
|
+
resume:*)
|
|
119
|
+
session_mode="resume"
|
|
120
|
+
session_id="${session_raw#resume:}"
|
|
121
|
+
;;
|
|
122
|
+
continue)
|
|
123
|
+
session_mode="continue"
|
|
124
|
+
;;
|
|
125
|
+
*)
|
|
126
|
+
echo "[경고] 알 수 없는 session 값: $session_raw" >&2
|
|
127
|
+
;;
|
|
128
|
+
esac
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# ── 최대 동시 작업 수 확인 ──
|
|
132
|
+
local running_count=0
|
|
133
|
+
for meta_file in "${LOGS_DIR}"/job_*.meta; do
|
|
134
|
+
[[ -f "$meta_file" ]] || continue
|
|
135
|
+
local STATUS=""
|
|
136
|
+
STATUS=$(_get_meta_field "$meta_file" "STATUS")
|
|
137
|
+
if [[ "$STATUS" == "running" ]]; then
|
|
138
|
+
(( running_count++ )) || true
|
|
139
|
+
fi
|
|
140
|
+
done
|
|
141
|
+
|
|
142
|
+
if [[ $running_count -ge $MAX_BACKGROUND_JOBS ]]; then
|
|
143
|
+
echo "[오류] 최대 동시 작업 수($MAX_BACKGROUND_JOBS)에 도달. job_id=$job_id 거부됨" >&2
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# ── Job 등록 (jobs.sh 모듈 사용) ──
|
|
148
|
+
# job_register는 내부 카운터 ID를 반환하지만,
|
|
149
|
+
# 외부 ID(job_id)를 파일명에 직접 사용하여 추적 일관성 확보
|
|
150
|
+
local internal_id
|
|
151
|
+
internal_id=$(job_register "$prompt")
|
|
152
|
+
|
|
153
|
+
# 외부 ID ↔ 내부 ID 매핑 저장
|
|
154
|
+
echo "$job_id" > "${LOGS_DIR}/job_${internal_id}.ext_id"
|
|
155
|
+
|
|
156
|
+
local out_file="${LOGS_DIR}/job_${internal_id}.out"
|
|
157
|
+
|
|
158
|
+
# ── 서브쉘에서 백그라운드 실행 ──
|
|
159
|
+
(
|
|
160
|
+
_run_claude "$prompt" "$cwd" "$out_file" "$session_mode" "$session_id"
|
|
161
|
+
local exit_code=$?
|
|
162
|
+
|
|
163
|
+
# 세션 ID 추출 (JSON 출력에서)
|
|
164
|
+
if [[ -f "$out_file" ]]; then
|
|
165
|
+
local sid
|
|
166
|
+
sid=$(jq -r '.session_id // empty' "$out_file" 2>/dev/null)
|
|
167
|
+
[[ -n "$sid" ]] && job_set_session "$internal_id" "$sid"
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# 완료 상태 갱신
|
|
171
|
+
if [[ $exit_code -eq 0 ]]; then
|
|
172
|
+
job_mark_done "$internal_id"
|
|
173
|
+
else
|
|
174
|
+
job_mark_failed "$internal_id"
|
|
175
|
+
fi
|
|
176
|
+
) &
|
|
177
|
+
|
|
178
|
+
local bg_pid=$!
|
|
179
|
+
job_set_pid "$internal_id" "$bg_pid"
|
|
180
|
+
|
|
181
|
+
# 호출자(서비스 데몬)에게 내부 ID 반환
|
|
182
|
+
echo "$internal_id"
|
|
183
|
+
}
|