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 +1 -1
- package/src/board.ts +16 -2
- package/src/orchestrator.ts +37 -3
- package/src/server.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "3.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')
|
|
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{
|
|
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 = [];
|
package/src/orchestrator.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
168
|
-
|
|
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.
|
|
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.
|
|
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
|
|