coxpit 3.0.0 → 3.0.1

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": "3.0.0",
3
+ "version": "3.0.1",
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": "MIT",
package/src/board.ts CHANGED
@@ -610,7 +610,11 @@ function humanize(e){
610
610
  if (kind === 'stderr') return { k:'stderr', t:payload };
611
611
  try{
612
612
  const o = JSON.parse(payload);
613
- if (o.type === 'system') return { k:'session', t:'started · '+(o.model||o.subtype||'') };
613
+ if (o.type === 'system'){
614
+ if (o.subtype === 'init' || !o.subtype) return { k:'session', t:'started'+(o.model?' · '+o.model:'') };
615
+ if (o.subtype === 'permission_denied') return { k:'denied', t:'⛔ '+(o.tool_name||o.tool||'tool use')+' blocked — attach the Terminal to approve, or widen COXPIT_AGENT_PERM' };
616
+ return null; // thinking_tokens 등 스트림 잡음
617
+ }
614
618
  if (o.type === 'user') return null; // tool 결과 회신 — 노이즈
615
619
  if (o.type === 'assistant' && o.message){
616
620
  const parts = [];
@@ -628,7 +632,17 @@ function humanize(e){
628
632
  if (o.type === 'result') return { k:'done', t:o.result || 'finished' };
629
633
  if (kind === 'meta') return { k:'start', t:'worktree '+String(o.worktree||'').split('/').slice(-2).join('/') };
630
634
  return { k:kind, t:payload.slice(0,140) };
631
- }catch{ return { k:kind, t:payload }; }
635
+ }catch{
636
+ // 파싱 실패(과거에 잘려 저장된 이벤트 등) — JSON 잔해를 그대로 보여주지 않는다:
637
+ // text 조각만 구제하고, 없으면 생략.
638
+ if (payload.trim().startsWith('{')){
639
+ const texts = [];
640
+ const re = /"text":"((?:[^"\\\\]|\\\\.)*)"/g; let m;
641
+ while ((m = re.exec(payload)) && texts.length < 2) texts.push(m[1].replace(/\\\\n/g,' ').slice(0,140));
642
+ return texts.length ? { k:'said', t:texts.join(' · ') } : null;
643
+ }
644
+ return { k:kind, t:payload };
645
+ }
632
646
  }
633
647
  function humanLines(events){
634
648
  const out = [];
@@ -155,8 +155,12 @@ async function runAgentChild(runId: number, machine: MachineTarget, wtPath: stri
155
155
  const s = line.trim();
156
156
  if (!s) return;
157
157
  let kind = 'log';
158
+ let stored = s;
158
159
  try {
159
- const obj = JSON.parse(s) as { type?: string; result?: string; session_id?: string };
160
+ const obj = JSON.parse(s) as {
161
+ type?: string; subtype?: string; model?: string; result?: string; session_id?: string;
162
+ message?: { content?: Array<{ type?: string; text?: string; name?: string; input?: Record<string, unknown> }> };
163
+ };
160
164
  if (obj.type) kind = obj.type;
161
165
  // steer(--resume) 용 세션 키 캡처
162
166
  if (obj.type === 'system' && typeof obj.session_id === 'string') {
@@ -164,8 +168,28 @@ async function runAgentChild(runId: number, machine: MachineTarget, wtPath: stri
164
168
  }
165
169
  // result 이벤트의 사람이 읽는 요약만 뽑아 둔다(없으면 원본 라인).
166
170
  if (obj.type === 'result') lastResult = typeof obj.result === 'string' ? obj.result : s;
167
- } catch { /* 비-JSON 로그 라인 */ }
168
- void recordEvent(runId, kind, s.slice(0, 2000));
171
+ // 2000자 초과 이벤트는 자르면 JSON 깨져 잔해가 화면에 노출된다 —
172
+ // 저장 전에 "요지만 남긴" 유효 JSON 으로 압축한다.
173
+ if (s.length > 2000) {
174
+ if (obj.type === 'assistant' && obj.message) {
175
+ const content = (obj.message.content ?? [])
176
+ .filter((c) => c.type === 'text' || c.type === 'tool_use')
177
+ .map((c) => c.type === 'text'
178
+ ? { type: 'text', text: (c.text ?? '').slice(0, 600) }
179
+ : { type: 'tool_use', name: c.name, input: compactInput(c.input) });
180
+ stored = JSON.stringify({ type: 'assistant', message: { content } }).slice(0, 2000);
181
+ } else if (obj.type === 'user') {
182
+ stored = JSON.stringify({ type: 'user' }); // tool 결과 회신 — 표시 안 함
183
+ } else if (obj.type === 'system') {
184
+ stored = JSON.stringify({ type: 'system', subtype: obj.subtype, model: obj.model });
185
+ } else if (obj.type === 'result') {
186
+ stored = JSON.stringify({ type: 'result', result: (obj.result ?? '').slice(0, 1500) });
187
+ } else {
188
+ stored = s.slice(0, 2000);
189
+ }
190
+ }
191
+ } catch { stored = s.slice(0, 2000); /* 비-JSON 로그 라인 */ }
192
+ void recordEvent(runId, kind, stored.slice(0, 2000));
169
193
  });
170
194
  }
171
195
  if (child.stderr) {
@@ -192,6 +216,16 @@ async function runAgentChild(runId: number, machine: MachineTarget, wtPath: stri
192
216
  void notifySettle(runId, status, filesChanged, exitSummary);
193
217
  }
194
218
 
219
+ /** tool_use input 을 표시용 핵심 필드만 남긴다(이벤트 압축용). */
220
+ function compactInput(input?: Record<string, unknown>): Record<string, string> {
221
+ const out: Record<string, string> = {};
222
+ if (!input) return out;
223
+ for (const k of ['file_path', 'command', 'path', 'pattern', 'url']) {
224
+ if (typeof input[k] === 'string') out[k] = (input[k] as string).slice(0, 200);
225
+ }
226
+ return out;
227
+ }
228
+
195
229
  /** run 정착 웹훅(선택) — COXPIT_WEBHOOK_URL 로 JSON POST. 실패는 무해. */
196
230
  async function notifySettle(runId: number, status: string, filesChanged: number, exitSummary: string): Promise<void> {
197
231
  if (!config.webhookUrl) return;
package/src/server.ts CHANGED
@@ -32,7 +32,7 @@ export async function buildServer(): Promise<FastifyInstance> {
32
32
  app.addHook('onRequest', authGate);
33
33
 
34
34
  // 무인증 헬스(외부 감시용)
35
- app.get('/api/health', async () => ({ ok: true, name: 'coxpit', version: '3.0.0' }));
35
+ app.get('/api/health', async () => ({ ok: true, name: 'coxpit', version: '3.0.1' }));
36
36
 
37
37
  // 플릿 보드(단일 페이지). 인증 게이트 적용됨.
38
38
  app.get('/', async (_req, reply) => reply.type('text/html').send(BOARD_HTML));
@@ -491,7 +491,7 @@ export async function buildServer(): Promise<FastifyInstance> {
491
491
  // 라이브 스트림 좌석 — 오케스트레이터가 run/event 를 여기로 broadcast.
492
492
  app.get('/ws', { websocket: true }, (socket) => {
493
493
  addSink(socket);
494
- socket.send(JSON.stringify({ type: 'hello', name: 'coxpit-fleet', version: '3.0.0' }));
494
+ socket.send(JSON.stringify({ type: 'hello', name: 'coxpit-fleet', version: '3.0.1' }));
495
495
  socket.on('close', () => removeSink(socket));
496
496
  });
497
497