coxpit 6.0.0 → 6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coxpit",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Self-hosted cockpit for running a fleet of AI coding agents across your own machines — parallel worktree runs, live board, compare & merge, web terminal, design capture.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/src/agentstate.ts CHANGED
@@ -22,7 +22,7 @@ const HOOK_COOLDOWN_MS = 60_000; // run 하나가 웹훅을 때릴 수 있는
22
22
 
23
23
  interface Pattern { id: string; re: RegExp }
24
24
 
25
- // ⚠️ 휴리스틱이고 provider TUI 화면에 종속적이다. "에이전트의 의도"를 안다고 주장하지 않는다 —
25
+ // [!] 휴리스틱이고 provider TUI 화면에 종속적이다. "에이전트의 의도"를 안다고 주장하지 않는다 —
26
26
  // 말하는 것은 "화면에 입력/승인 프롬프트가 떠 있다" 하나뿐이다. CLI 가 화면을 바꾸면 이 표만
27
27
  // 고치고, provider 를 늘리는 일은 행을 늘리는 일이다. 확신이 없으면 넣지 않는다:
28
28
  // 빗나간 idle 은 받아들일 수 있고, 지어낸 waiting 은 받아들일 수 없다.
@@ -90,13 +90,38 @@ interface Tracker {
90
90
  lastByteAt: number;
91
91
  timer: ReturnType<typeof setTimeout> | null; // tracker 당 정확히 하나
92
92
  lastHookAt: number; // 웹훅 쿨다운 — tracker 와 함께 살고 함께 죽는다
93
+ spotted: Set<number>; // B3 수동 포트 감지 — 출력에서 본 LISTEN 포트(감지만, 행동 없음)
94
+ }
95
+
96
+ // B3: 출력에서 "listening on :PORT" / "http(s)://host:PORT" 만 보수적으로 집는다.
97
+ // A2 와 같은 규율 — 못 맞히면 빈손이고, 포트를 지어내지 않는다. 1..65535 만 인정.
98
+ const PORT_PATTERNS: RegExp[] = [
99
+ /\blisten(?:ing)?\b[^0-9]{0,20}:(\d{2,5})\b/gi, // "Listening on :3000", "listening at port 8080" 근처의 :NNNN
100
+ /\bhttps?:\/\/[a-z0-9.\-]+:(\d{2,5})\b/gi, // http://localhost:3000
101
+ /\b(?:0\.0\.0\.0|127\.0\.0\.1|localhost)\b[^0-9]{0,4}:(\d{2,5})\b/gi,
102
+ ];
103
+
104
+ function spotPorts(t: Tracker, chunk: string): void {
105
+ for (const re of PORT_PATTERNS) {
106
+ re.lastIndex = 0;
107
+ let m: RegExpExecArray | null;
108
+ while ((m = re.exec(chunk))) {
109
+ const p = Number(m[1]);
110
+ if (p >= 1 && p <= 65535) t.spotted.add(p);
111
+ }
112
+ }
113
+ // 무한정 쌓지 않는다 — 최근 것 위주로 상한
114
+ if (t.spotted.size > 24) {
115
+ const keep = [...t.spotted].slice(-24);
116
+ t.spotted = new Set(keep);
117
+ }
93
118
  }
94
119
 
95
120
  /**
96
121
  * 주의 환기의 서버 쪽 절반(spec v5.28 A5) — 코크핏이 아예 닫혀 있을 때 유일하게 남는 신호다.
97
122
  * orchestrator 의 notifySettle 과 같은 모양으로 POST 하고, 실패는 무해하게 삼킨다.
98
123
  *
99
- * ⚠️ **상태만 보낸다.** detail 도, tail 조각도 절대 태우지 않는다 — 터미널 출력은 시크릿을
124
+ * [!] **상태만 보낸다.** detail 도, tail 조각도 절대 태우지 않는다 — 터미널 출력은 시크릿을
100
125
  * 그대로 뱉을 수 있고 웹훅 엔드포인트는 coxpit 의 신뢰 경계 **밖**이다(꼬리는 인증된 /ws 허브에만).
101
126
  */
102
127
  async function postHook(runId: number, state: AgentState): Promise<void> {
@@ -173,7 +198,7 @@ function onQuiet(runId: number): void {
173
198
  export function attach(runId: number): void {
174
199
  const cur = trackers.get(runId);
175
200
  if (cur) { cur.refs++; return; }
176
- trackers.set(runId, { refs: 1, state: 'unknown', since: Date.now(), tail: '', lastByteAt: 0, timer: null, lastHookAt: 0 });
201
+ trackers.set(runId, { refs: 1, state: 'unknown', since: Date.now(), tail: '', lastByteAt: 0, timer: null, lastHookAt: 0, spotted: new Set() });
177
202
  }
178
203
 
179
204
  /** 출력 청크 — tail 에 붙이고 시각을 찍고 working. 미러 중복이 들어와도 해롭지 않다. */
@@ -183,10 +208,17 @@ export function feed(runId: number, chunk: string): void {
183
208
  t.tail += chunk;
184
209
  if (t.tail.length > TAIL_MAX) t.tail = t.tail.slice(t.tail.length - TAIL_MAX);
185
210
  t.lastByteAt = Date.now();
211
+ spotPorts(t, chunk); // B3 수동 포트 감지 — 감지만, 행동 없음
186
212
  setState(runId, t, 'working');
187
213
  arm(runId, t, ACTIVE_MS);
188
214
  }
189
215
 
216
+ /** B3: 이 run 의 출력에서 감지한 LISTEN 포트 목록(감지만; 코크핏이 원클릭 대상으로 제안). */
217
+ export function spottedPorts(runId: number): number[] {
218
+ const t = trackers.get(runId);
219
+ return t ? [...t.spotted] : [];
220
+ }
221
+
190
222
  /**
191
223
  * 사람이 터미널에 입력했다 — waiting 을 즉시 지운다.
192
224
  * TUI 는 다시 그리므로 이미 답한 프롬프트 문구가 tail 에 남는다. 입력 신호는 "사람이 응답했다"는
package/src/board.ts CHANGED
@@ -1928,23 +1928,29 @@ $('archList').addEventListener('click', async (e)=>{
1928
1928
  }catch{ toast('could not open task', 'error'); }
1929
1929
  });
1930
1930
 
1931
- /* ── Reclaim orphaned worktrees ── cleaned/failed run worktrees are disk debt
1932
- (~180MB each — node_modules lives inside). Only shown when there's something
1933
- to reclaim; active/successful work is never listed by the server. */
1931
+ /* ── Reclaim orphaned worktrees ── finished run worktrees are disk debt
1932
+ (~180MB each — node_modules lives inside). Active work is never listed by the
1933
+ server. v6.0 T6b: settled done worktrees are listed too, but any whose change
1934
+ set is unmerged AND un-exported is flagged (reclaimRisk) — this button reclaims
1935
+ only the safe ones, so the count here is the SAFE count. Pick a flagged one
1936
+ deliberately in the cockpit's worktree sheet. */
1934
1937
  let reclaimN = 0;
1935
1938
  async function reclaimRefresh(){
1936
1939
  try{
1937
1940
  const j = await fetch('/api/worktrees').then(x=>x.json());
1938
- reclaimN = (j.items||[]).length;
1939
- const mb = Math.round((j.totalKb||0)/1024);
1940
- $('reclaimHint').textContent = reclaimN ? (reclaimN+' · ~'+mb+'MB') : '';
1941
+ const items = (j.items||[]);
1942
+ const safe = items.filter(w=>!w.reclaimRisk);
1943
+ reclaimN = safe.length;
1944
+ const mb = Math.round(safe.reduce((s,w)=>s+(w.sizeKb||0),0)/1024);
1945
+ const flagged = items.length - safe.length;
1946
+ $('reclaimHint').textContent = reclaimN ? (reclaimN+' · ~'+mb+'MB'+(flagged?(' (+'+flagged+' flagged)'):'')) : '';
1941
1947
  $('reclaimBtn').hidden = reclaimN===0;
1942
1948
  }catch{ $('reclaimBtn').hidden = true; }
1943
1949
  }
1944
1950
  $('reclaimBtn').addEventListener('click', async ()=>{
1945
1951
  if (!reclaimN) return;
1946
- const ok = await confirmUI('remove '+reclaimN+' cleaned/failed run worktree'+(reclaimN===1?'':'s')+'?', {
1947
- sub:'active work is untouched — only closed tasks and failed/error/stopped runs are reclaimed',
1952
+ const ok = await confirmUI('remove '+reclaimN+' finished run worktree'+(reclaimN===1?'':'s')+'?', {
1953
+ sub:'active work is untouched, and worktrees holding unmerged, un-exported changes are left alone tick those one by one in the cockpit',
1948
1954
  okLabel:'Reclaim', danger:true });
1949
1955
  if (!ok) return;
1950
1956
  try{