claude-controller 0.1.2 → 0.3.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.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/bin/autoloop.sh +382 -0
  3. package/bin/ctl +1189 -0
  4. package/bin/native-app.py +6 -3
  5. package/bin/watchdog.sh +357 -0
  6. package/cognitive/__init__.py +14 -0
  7. package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
  9. package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
  10. package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
  11. package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
  12. package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
  13. package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
  14. package/cognitive/dispatcher.py +192 -0
  15. package/cognitive/evaluator.py +289 -0
  16. package/cognitive/goal_engine.py +232 -0
  17. package/cognitive/learning.py +189 -0
  18. package/cognitive/orchestrator.py +303 -0
  19. package/cognitive/planner.py +207 -0
  20. package/cognitive/prompts/analyst.md +31 -0
  21. package/cognitive/prompts/coder.md +22 -0
  22. package/cognitive/prompts/reviewer.md +33 -0
  23. package/cognitive/prompts/tester.md +21 -0
  24. package/cognitive/prompts/writer.md +25 -0
  25. package/config.sh +6 -1
  26. package/dag/__init__.py +5 -0
  27. package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/dag/__pycache__/graph.cpython-314.pyc +0 -0
  29. package/dag/graph.py +222 -0
  30. package/lib/jobs.sh +12 -1
  31. package/package.json +11 -5
  32. package/postinstall.sh +1 -1
  33. package/service/controller.sh +43 -11
  34. package/web/audit.py +122 -0
  35. package/web/checkpoint.py +80 -0
  36. package/web/config.py +2 -5
  37. package/web/handler.py +634 -473
  38. package/web/handler_fs.py +153 -0
  39. package/web/handler_goals.py +203 -0
  40. package/web/handler_jobs.py +372 -0
  41. package/web/handler_memory.py +203 -0
  42. package/web/handler_sessions.py +132 -0
  43. package/web/jobs.py +585 -13
  44. package/web/personas.py +419 -0
  45. package/web/pipeline.py +981 -0
  46. package/web/presets.py +506 -0
  47. package/web/projects.py +246 -0
  48. package/web/static/api.js +141 -0
  49. package/web/static/app.js +25 -1937
  50. package/web/static/attachments.js +144 -0
  51. package/web/static/base.css +497 -0
  52. package/web/static/context.js +204 -0
  53. package/web/static/dirs.js +246 -0
  54. package/web/static/form.css +763 -0
  55. package/web/static/goals.css +363 -0
  56. package/web/static/goals.js +300 -0
  57. package/web/static/i18n.js +625 -0
  58. package/web/static/index.html +215 -13
  59. package/web/static/{styles.css → jobs.css} +746 -1141
  60. package/web/static/jobs.js +1270 -0
  61. package/web/static/memoryview.js +117 -0
  62. package/web/static/personas.js +228 -0
  63. package/web/static/pipeline.css +338 -0
  64. package/web/static/pipelines.js +487 -0
  65. package/web/static/presets.js +244 -0
  66. package/web/static/send.js +135 -0
  67. package/web/static/settings-style.css +291 -0
  68. package/web/static/settings.js +81 -0
  69. package/web/static/stream.js +534 -0
  70. package/web/static/utils.js +131 -0
  71. package/web/webhook.py +210 -0
@@ -0,0 +1,204 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Context Management — 세션 컨텍스트 (new/resume/fork)
3
+ ═══════════════════════════════════════════════ */
4
+
5
+ let _contextMode = 'new';
6
+ let _contextSessionId = null;
7
+ let _contextSessionPrompt = null;
8
+
9
+ function setContextMode(mode) {
10
+ _contextMode = mode;
11
+ _contextSessionId = null;
12
+ _contextSessionPrompt = null;
13
+ _updateContextUI();
14
+ _closeSessionPicker();
15
+ }
16
+
17
+ function clearContext() { setContextMode('new'); }
18
+
19
+ function _updateContextUI() {
20
+ const newBtn = document.getElementById('ctxNew');
21
+ const resumeBtn = document.getElementById('ctxResume');
22
+ const forkBtn = document.getElementById('ctxFork');
23
+ const label = document.getElementById('ctxSessionLabel');
24
+ const promptInfo = document.getElementById('promptSessionInfo');
25
+
26
+ [newBtn, resumeBtn, forkBtn].forEach(b => b.classList.remove('active'));
27
+ label.classList.remove('visible');
28
+ label.textContent = '';
29
+
30
+ if (_contextMode === 'new') {
31
+ newBtn.classList.add('active');
32
+ if (promptInfo) promptInfo.textContent = '';
33
+ } else if (_contextMode === 'resume') {
34
+ resumeBtn.classList.add('active');
35
+ const sid = _contextSessionId ? _contextSessionId.slice(0, 8) : '';
36
+ if (sid) {
37
+ const promptSnippet = _contextSessionPrompt ? _contextSessionPrompt.slice(0, 24) : '';
38
+ label.textContent = promptSnippet ? `${sid}… ${promptSnippet}` : `${sid}…`;
39
+ label.classList.add('visible');
40
+ }
41
+ if (promptInfo) promptInfo.textContent = sid ? `resume:${sid}` : 'resume';
42
+ } else if (_contextMode === 'fork') {
43
+ forkBtn.classList.add('active');
44
+ const sid = _contextSessionId ? _contextSessionId.slice(0, 8) : '';
45
+ if (sid) {
46
+ const promptSnippet = _contextSessionPrompt ? _contextSessionPrompt.slice(0, 24) : '';
47
+ label.textContent = promptSnippet ? `${sid}… ${promptSnippet}` : `${sid}…`;
48
+ label.classList.add('visible');
49
+ }
50
+ if (promptInfo) promptInfo.textContent = sid ? `fork:${sid}` : 'fork';
51
+ }
52
+ }
53
+
54
+ function _formatCwdShort(cwd) {
55
+ if (!cwd) return '';
56
+ const parts = cwd.replace(/\/$/, '').split('/');
57
+ return parts[parts.length - 1] || cwd;
58
+ }
59
+
60
+ function _renderSessionItem(s) {
61
+ const id = s.session_id || s.id || '';
62
+ const hint = escapeHtml(truncate(s.prompt || s.last_prompt || id, 80));
63
+ const cwdShort = _formatCwdShort(s.cwd || s.project || '');
64
+ const cwdBadge = cwdShort
65
+ ? `<span class="session-cwd-badge" title="${escapeHtml(s.cwd || s.project || '')}">${escapeHtml(cwdShort)}</span>`
66
+ : '';
67
+ const ts = s.updated_at || s.timestamp || '';
68
+ const timeStr = ts ? formatTime(ts) : '';
69
+ return `<div class="session-item" onclick="_selectSession('${escapeHtml(id)}', '${escapeHtml((s.prompt || '').replace(/'/g, "\\'"))}', '${escapeHtml((s.cwd || '').replace(/'/g, "\\'"))}')">
70
+ <div class="session-item-prompt">${hint}</div>
71
+ <div class="session-item-meta">${cwdBadge}<span class="session-item-id">${escapeHtml(id.slice(0, 12))}</span>${timeStr ? `<span class="session-item-time">${timeStr}</span>` : ''}</div>
72
+ </div>`;
73
+ }
74
+
75
+ function _renderSessionList(sessions, grouped) {
76
+ const list = document.getElementById('sessionPickerList');
77
+ if (sessions.length === 0) {
78
+ list.innerHTML = '<div style="padding:12px;text-align:center;color:var(--text-muted);font-size:0.75rem;">세션이 없습니다</div>';
79
+ return;
80
+ }
81
+
82
+ if (grouped && Object.keys(grouped).length > 0) {
83
+ let html = '';
84
+ for (const [project, items] of Object.entries(grouped)) {
85
+ const label = project || '(프로젝트 미지정)';
86
+ html += `<div class="session-group-label">${escapeHtml(label)}</div>`;
87
+ html += items.map(s => _renderSessionItem(s)).join('');
88
+ }
89
+ list.innerHTML = html;
90
+ } else {
91
+ list.innerHTML = sessions.map(s => _renderSessionItem(s)).join('');
92
+ }
93
+ }
94
+
95
+ // 세션 검색 필터링용 캐시
96
+ let _sessionPickerSessions = [];
97
+ let _sessionPickerGrouped = {};
98
+
99
+ function _filterSessions(query) {
100
+ if (!query) {
101
+ _renderSessionList(_sessionPickerSessions, _sessionPickerGrouped);
102
+ return;
103
+ }
104
+ const q = query.toLowerCase();
105
+ const filtered = _sessionPickerSessions.filter(s => {
106
+ const prompt = (s.prompt || s.last_prompt || '').toLowerCase();
107
+ const id = (s.session_id || s.id || '').toLowerCase();
108
+ const cwd = (s.cwd || s.project || '').toLowerCase();
109
+ return prompt.includes(q) || id.includes(q) || cwd.includes(q);
110
+ });
111
+ _renderSessionList(filtered, null);
112
+ }
113
+
114
+ async function openSessionPicker(mode) {
115
+ _contextMode = mode;
116
+ _updateContextUI();
117
+
118
+ const picker = document.getElementById('sessionPicker');
119
+ const list = document.getElementById('sessionPickerList');
120
+ const title = document.getElementById('sessionPickerTitle');
121
+ const filterBar = document.getElementById('sessionFilterBar');
122
+ const filterProject = document.getElementById('sessionFilterProject');
123
+ const searchInput = document.getElementById('sessionSearchInput');
124
+
125
+ title.textContent = mode === 'resume' ? 'Resume 세션 선택' : 'Fork 세션 선택';
126
+ picker.classList.add('open');
127
+ list.innerHTML = '<div style="padding:12px;text-align:center;"><span class="spinner"></span></div>';
128
+ searchInput.value = '';
129
+
130
+ try {
131
+ const currentCwd = document.getElementById('cwdInput').value.trim();
132
+ let url = '/api/sessions';
133
+ if (currentCwd) url += `?cwd=${encodeURIComponent(currentCwd)}`;
134
+ const data = await apiFetch(url);
135
+
136
+ const sessions = Array.isArray(data) ? data : (data.sessions || []);
137
+ const grouped = data.grouped || {};
138
+ _sessionPickerSessions = sessions;
139
+ _sessionPickerGrouped = grouped;
140
+
141
+ if (currentCwd && sessions.length > 0) {
142
+ filterBar.style.display = 'flex';
143
+ const cwdShort = _formatCwdShort(currentCwd);
144
+ filterProject.textContent = cwdShort;
145
+ filterProject.title = currentCwd;
146
+ } else {
147
+ filterBar.style.display = 'none';
148
+ }
149
+
150
+ _renderSessionList(sessions, grouped);
151
+ } catch (err) {
152
+ list.innerHTML = `<div style="padding:12px;color:var(--red);">세션 목록 로딩 실패: ${err.message}</div>`;
153
+ }
154
+ }
155
+
156
+ function _toggleProjectFilter() {
157
+ const btn = document.getElementById('sessionFilterBtn');
158
+ const isActive = btn.classList.toggle('active');
159
+ const currentCwd = document.getElementById('cwdInput').value.trim();
160
+
161
+ if (isActive && currentCwd) {
162
+ openSessionPicker(_contextMode);
163
+ } else {
164
+ const list = document.getElementById('sessionPickerList');
165
+ list.innerHTML = '<div style="padding:12px;text-align:center;"><span class="spinner"></span></div>';
166
+ apiFetch('/api/sessions').then(data => {
167
+ const sessions = Array.isArray(data) ? data : (data.sessions || []);
168
+ const grouped = data.grouped || {};
169
+ _sessionPickerSessions = sessions;
170
+ _sessionPickerGrouped = grouped;
171
+ _renderSessionList(sessions, grouped);
172
+ }).catch(() => {});
173
+ }
174
+ }
175
+
176
+ function _selectSession(sid, prompt, cwd) {
177
+ _contextSessionId = sid;
178
+ _contextSessionPrompt = prompt || null;
179
+ _updateContextUI();
180
+ _closeSessionPicker();
181
+
182
+ if (cwd) {
183
+ addRecentDir(cwd);
184
+ selectRecentDir(cwd, true);
185
+ }
186
+
187
+ const modeLabel = _contextMode === 'resume' ? 'Resume' : 'Fork';
188
+ showToast(`${modeLabel}: ${sid.slice(0, 8)}...`);
189
+ }
190
+
191
+ function _closeSessionPicker() {
192
+ const picker = document.getElementById('sessionPicker');
193
+ if (picker) picker.classList.remove('open');
194
+ }
195
+
196
+ // 세션 피커 외부 클릭 시 닫기
197
+ document.addEventListener('click', function(e) {
198
+ const picker = document.getElementById('sessionPicker');
199
+ if (!picker || !picker.classList.contains('open')) return;
200
+ const toolbar = document.querySelector('.ctx-toolbar-wrap');
201
+ if (toolbar && !toolbar.contains(e.target)) {
202
+ _closeSessionPicker();
203
+ }
204
+ });
@@ -0,0 +1,246 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Directory — 최근 디렉토리, 인라인 디렉토리 브라우저
3
+ ═══════════════════════════════════════════════ */
4
+
5
+ const MAX_RECENT_DIRS = 8;
6
+ let _recentDirsCache = [];
7
+ let dirBrowserCurrentPath = '';
8
+ let dirBrowserOpen = false;
9
+
10
+ // ── Recent Directories ──
11
+
12
+ function getRecentDirs() {
13
+ return _recentDirsCache;
14
+ }
15
+
16
+ async function loadRecentDirs() {
17
+ try {
18
+ const res = await apiFetch('/api/recent-dirs');
19
+ _recentDirsCache = Array.isArray(res) ? res : [];
20
+ } catch { _recentDirsCache = []; }
21
+ renderRecentDirs();
22
+ }
23
+
24
+ function _saveRecentDirs() {
25
+ apiFetch('/api/recent-dirs', {
26
+ method: 'POST',
27
+ body: JSON.stringify({ dirs: _recentDirsCache })
28
+ }).catch(() => {});
29
+ }
30
+
31
+ function addRecentDir(path) {
32
+ if (!path) return;
33
+ _recentDirsCache = _recentDirsCache.filter(d => d !== path);
34
+ _recentDirsCache.unshift(path);
35
+ if (_recentDirsCache.length > MAX_RECENT_DIRS) _recentDirsCache = _recentDirsCache.slice(0, MAX_RECENT_DIRS);
36
+ _saveRecentDirs();
37
+ renderRecentDirs();
38
+ }
39
+
40
+ function removeRecentDir(path) {
41
+ _recentDirsCache = _recentDirsCache.filter(d => d !== path);
42
+ _saveRecentDirs();
43
+ if (document.getElementById('cwdInput').value === path) {
44
+ clearDirSelection();
45
+ }
46
+ renderRecentDirs();
47
+ }
48
+
49
+ function renderRecentDirs() {
50
+ const container = document.getElementById('recentDirs');
51
+ const dirs = _recentDirsCache;
52
+ const currentCwd = document.getElementById('cwdInput').value;
53
+
54
+ if (dirs.length === 0) {
55
+ container.innerHTML = '';
56
+ return;
57
+ }
58
+
59
+ let html = '<span class="recent-dirs-label">최근</span>';
60
+ html += dirs.map(dir => {
61
+ const parts = dir.replace(/\/+$/, '').split('/');
62
+ const name = parts[parts.length - 1] || dir;
63
+ const isActive = dir === currentCwd ? ' active' : '';
64
+ const escapedDir = dir.replace(/'/g, "\\'");
65
+ return `<span class="recent-chip${isActive}" onclick="selectRecentDir('${escapedDir}')" title="${dir}">
66
+ <span class="recent-chip-name">${name}</span>
67
+ <button class="recent-chip-remove" onclick="event.stopPropagation(); removeRecentDir('${escapedDir}')" title="제거">
68
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
69
+ </button>
70
+ </span>`;
71
+ }).join('');
72
+
73
+ container.innerHTML = html;
74
+ }
75
+
76
+ function selectRecentDir(path, force) {
77
+ const current = document.getElementById('cwdInput').value;
78
+ if (!force && current === path) {
79
+ clearDirSelection();
80
+ return;
81
+ }
82
+ document.getElementById('cwdInput').value = path;
83
+ const text = document.getElementById('dirPickerText');
84
+ text.textContent = path;
85
+ document.getElementById('dirPickerDisplay').classList.add('has-value');
86
+ document.getElementById('dirPickerClear').classList.add('visible');
87
+ renderRecentDirs();
88
+ if (dirBrowserOpen) {
89
+ browseTo(path);
90
+ } else {
91
+ dirBrowserCurrentPath = path;
92
+ }
93
+ }
94
+
95
+ // ── Inline Directory Browser ──
96
+
97
+ function toggleDirBrowser() {
98
+ if (dirBrowserOpen) {
99
+ closeDirBrowser();
100
+ } else {
101
+ openDirBrowser();
102
+ }
103
+ }
104
+
105
+ function openDirBrowser() {
106
+ const panel = document.getElementById('dirBrowserPanel');
107
+ const chevron = document.getElementById('dirPickerChevron');
108
+ const currentCwd = document.getElementById('cwdInput').value;
109
+ const startPath = currentCwd || '~';
110
+
111
+ panel.classList.add('open');
112
+ if (chevron) chevron.style.transform = 'rotate(180deg)';
113
+ dirBrowserOpen = true;
114
+ browseTo(startPath);
115
+ }
116
+
117
+ function closeDirBrowser() {
118
+ const panel = document.getElementById('dirBrowserPanel');
119
+ const chevron = document.getElementById('dirPickerChevron');
120
+ if (panel) panel.classList.remove('open');
121
+ if (chevron) chevron.style.transform = '';
122
+ dirBrowserOpen = false;
123
+ }
124
+
125
+ async function browseTo(path) {
126
+ const list = document.getElementById('dirList');
127
+ const breadcrumb = document.getElementById('dirBreadcrumb');
128
+ const currentDisplay = document.getElementById('dirCurrentPath');
129
+
130
+ list.innerHTML = '<div class="dir-modal-loading"><span class="spinner"></span> 불러오는 중...</div>';
131
+
132
+ try {
133
+ const data = await apiFetch(`/api/dirs?path=${encodeURIComponent(path)}`);
134
+ dirBrowserCurrentPath = data.current;
135
+ currentDisplay.textContent = data.current;
136
+ currentDisplay.title = data.current;
137
+
138
+ document.getElementById('cwdInput').value = data.current;
139
+ document.getElementById('dirPickerText').textContent = data.current;
140
+ document.getElementById('dirPickerDisplay').classList.add('has-value');
141
+ document.getElementById('dirPickerClear').classList.add('visible');
142
+ renderRecentDirs();
143
+
144
+ renderBreadcrumb(data.current, breadcrumb);
145
+
146
+ const dirs = data.entries.filter(e => e.type === 'dir');
147
+ if (dirs.length === 0) {
148
+ list.innerHTML = '<div class="dir-modal-loading" style="color:var(--text-muted);">하위 디렉토리가 없습니다</div>';
149
+ return;
150
+ }
151
+
152
+ list.innerHTML = dirs.map(entry => {
153
+ const isParent = entry.name === '..';
154
+ const icon = isParent
155
+ ? '<svg class="dir-item-icon is-parent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>'
156
+ : '<svg class="dir-item-icon is-dir" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
157
+ const label = isParent ? '상위 디렉토리' : entry.name;
158
+ return `<div class="dir-item" onclick="browseTo('${entry.path.replace(/'/g, "\\'")}')">
159
+ ${icon}
160
+ <span class="dir-item-name is-dir">${label}</span>
161
+ </div>`;
162
+ }).join('');
163
+
164
+ } catch (err) {
165
+ list.innerHTML = `<div class="dir-modal-loading" style="color:var(--red);">불러오기 실패: ${err.message}</div>`;
166
+ }
167
+ }
168
+
169
+ function renderBreadcrumb(fullPath, container) {
170
+ const parts = fullPath.split('/').filter(Boolean);
171
+ let html = `<span class="breadcrumb-seg" onclick="browseTo('/')">/</span>`;
172
+ let accumulated = '';
173
+ for (const part of parts) {
174
+ accumulated += '/' + part;
175
+ const p = accumulated;
176
+ html += `<span class="breadcrumb-sep">/</span><span class="breadcrumb-seg" onclick="browseTo('${p.replace(/'/g, "\\'")}')">${part}</span>`;
177
+ }
178
+ container.innerHTML = html;
179
+ }
180
+
181
+ function selectCurrentDir() {
182
+ if (!dirBrowserCurrentPath) return;
183
+ document.getElementById('cwdInput').value = dirBrowserCurrentPath;
184
+
185
+ const text = document.getElementById('dirPickerText');
186
+ text.textContent = dirBrowserCurrentPath;
187
+ document.getElementById('dirPickerDisplay').classList.add('has-value');
188
+ document.getElementById('dirPickerClear').classList.add('visible');
189
+
190
+ addRecentDir(dirBrowserCurrentPath);
191
+ closeDirBrowser();
192
+ renderRecentDirs();
193
+ }
194
+
195
+ function clearDirSelection() {
196
+ document.getElementById('cwdInput').value = '';
197
+ const text = document.getElementById('dirPickerText');
198
+ text.textContent = t('select_directory');
199
+ document.getElementById('dirPickerDisplay').classList.remove('has-value');
200
+ document.getElementById('dirPickerClear').classList.remove('visible');
201
+ renderRecentDirs();
202
+ }
203
+
204
+ // ── 디렉토리 생성 ──
205
+
206
+ function showCreateDirInput() {
207
+ const row = document.getElementById('dirCreateRow');
208
+ const input = document.getElementById('dirCreateInput');
209
+ row.style.display = 'flex';
210
+ input.value = '';
211
+ input.placeholder = t('new_folder_name');
212
+ input.focus();
213
+ }
214
+
215
+ function hideCreateDirInput() {
216
+ document.getElementById('dirCreateRow').style.display = 'none';
217
+ }
218
+
219
+ async function createDir() {
220
+ const input = document.getElementById('dirCreateInput');
221
+ const name = input.value.trim();
222
+ if (!name) return;
223
+ if (!dirBrowserCurrentPath) return;
224
+
225
+ try {
226
+ await apiFetch('/api/mkdir', {
227
+ method: 'POST',
228
+ headers: { 'Content-Type': 'application/json' },
229
+ body: JSON.stringify({ parent: dirBrowserCurrentPath, name }),
230
+ });
231
+ hideCreateDirInput();
232
+ browseTo(dirBrowserCurrentPath);
233
+ } catch (err) {
234
+ input.setCustomValidity(err.message);
235
+ input.reportValidity();
236
+ setTimeout(() => input.setCustomValidity(''), 2000);
237
+ }
238
+ }
239
+
240
+ // ESC 키로 패널 닫기
241
+ document.addEventListener('keydown', function(e) {
242
+ if (e.key === 'Escape') {
243
+ hideCreateDirInput();
244
+ closeDirBrowser();
245
+ }
246
+ });