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/web/utils.py ADDED
@@ -0,0 +1,109 @@
1
+ """
2
+ Controller Service — 유틸리티 함수
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+
10
+ from config import PID_FILE, CLAUDE_PROJECTS_DIR
11
+
12
+
13
+ def parse_meta_file(filepath):
14
+ """쉘 source 가능한 .meta 파일을 딕셔너리로 파싱한다."""
15
+ data = {}
16
+ try:
17
+ with open(filepath, "r") as f:
18
+ for line in f:
19
+ line = line.strip()
20
+ if not line or line.startswith("#"):
21
+ continue
22
+ match = re.match(r"^(\w+)=(.*)$", line)
23
+ if match:
24
+ key = match.group(1)
25
+ val = match.group(2)
26
+ if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
27
+ val = val[1:-1]
28
+ data[key] = val
29
+ except (OSError, IOError):
30
+ pass
31
+ return data
32
+
33
+
34
+ def is_service_running():
35
+ """서비스 PID 파일을 읽고 프로세스 생존 여부를 확인한다."""
36
+ if not PID_FILE.exists():
37
+ return False, None
38
+ try:
39
+ pid = int(PID_FILE.read_text().strip())
40
+ os.kill(pid, 0)
41
+ return True, pid
42
+ except (ValueError, ProcessLookupError, PermissionError, OSError):
43
+ return False, None
44
+
45
+
46
+ def cwd_to_project_dir(cwd: str) -> str:
47
+ """CWD 경로를 Claude Code 프로젝트 디렉토리 이름으로 변환한다."""
48
+ return re.sub(r'[^a-zA-Z0-9]', '-', os.path.normpath(cwd))
49
+
50
+
51
+ def scan_claude_sessions(project_dir: Path, limit=100):
52
+ """Claude Code 프로젝트 디렉토리에서 세션 JSONL 파일을 스캔한다.
53
+ 각 파일에서 첫 user 메시지(프롬프트/CWD/타임스탬프)와 slug(세션 이름)을 추출한다."""
54
+ sessions = {}
55
+ if not project_dir.exists():
56
+ return sessions
57
+
58
+ jsonl_files = sorted(
59
+ project_dir.glob("*.jsonl"),
60
+ key=lambda f: f.stat().st_mtime,
61
+ reverse=True,
62
+ )[:limit]
63
+
64
+ for jf in jsonl_files:
65
+ sid = jf.stem
66
+ prompt = ""
67
+ cwd = ""
68
+ timestamp = ""
69
+ slug = ""
70
+ try:
71
+ with open(jf, "r") as f:
72
+ found_user = False
73
+ for line_no, line in enumerate(f):
74
+ if line_no >= 30:
75
+ break
76
+ try:
77
+ obj = json.loads(line)
78
+ except json.JSONDecodeError:
79
+ continue
80
+ if obj.get("slug"):
81
+ slug = obj["slug"]
82
+ if not found_user and obj.get("type") == "user":
83
+ msg = obj.get("message", {}).get("content", "")
84
+ if isinstance(msg, list):
85
+ msg = " ".join(
86
+ p.get("text", "") for p in msg
87
+ if isinstance(p, dict) and p.get("type") == "text"
88
+ )
89
+ prompt = (msg or "")[:200]
90
+ cwd = obj.get("cwd", "")
91
+ timestamp = obj.get("timestamp", "")
92
+ found_user = True
93
+ if found_user and slug:
94
+ break
95
+ except OSError:
96
+ continue
97
+
98
+ sessions[sid] = {
99
+ "session_id": sid,
100
+ "job_id": None,
101
+ "prompt": prompt,
102
+ "timestamp": timestamp,
103
+ "status": "done",
104
+ "cwd": cwd,
105
+ "cost_usd": None,
106
+ "slug": slug,
107
+ }
108
+
109
+ return sessions