@cliphijack/santaclaude 1.0.11 → 1.0.12
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/santaclaude.js +31 -7
package/package.json
CHANGED
package/santaclaude.js
CHANGED
|
@@ -115,10 +115,19 @@ function readTail(file, maxBytes) {
|
|
|
115
115
|
return s.split('\n');
|
|
116
116
|
} catch (e) { return []; }
|
|
117
117
|
}
|
|
118
|
-
//
|
|
119
|
-
function
|
|
118
|
+
// 오프셋 이후 새로 늘어난 줄만 읽음(라이브 델타) — 완전한 줄까지만, 다음 오프셋 반환
|
|
119
|
+
function readDelta(file, from) {
|
|
120
|
+
try {
|
|
121
|
+
const size = fs.statSync(file).size; if (size <= from) return { lines: [], off: from };
|
|
122
|
+
const len = size - from; const fd = fs.openSync(file, 'r'); const buf = Buffer.alloc(len); fs.readSync(fd, buf, 0, len, from); fs.closeSync(fd);
|
|
123
|
+
const s = buf.toString('utf8'); const lastNl = s.lastIndexOf('\n'); if (lastNl < 0) return { lines: [], off: from }; // 완전한 줄 없음 → 다음 틱에
|
|
124
|
+
const complete = s.slice(0, lastNl);
|
|
125
|
+
return { lines: complete.split('\n'), off: from + Buffer.byteLength(complete, 'utf8') + 1 };
|
|
126
|
+
} catch (e) { return { lines: [], off: from }; }
|
|
127
|
+
}
|
|
128
|
+
// JSONL 줄들 → user/assistant 턴 (툴콜=칩, 첨부이미지=data URL)
|
|
129
|
+
function linesToTurns(lines) {
|
|
120
130
|
const out = []; let imgBudget = 8 * 1024 * 1024; // 전체 이미지 base64 합계 상한(모바일 보호)
|
|
121
|
-
const lines = readTail(file, 2 * 1024 * 1024); // 최근 ~2MB만 (통째로 X) — 형 지적: "꼭 다 열어야되나"
|
|
122
131
|
function grabImgs(c) { const out2 = []; if (!Array.isArray(c)) return out2; for (const b of c) { if (b && b.type === 'image' && b.source) { const s = b.source; if (s.type === 'base64' && s.data && imgBudget > s.data.length) { imgBudget -= s.data.length; out2.push('data:' + (s.media_type || 'image/png') + ';base64,' + s.data); } else if (s.type === 'url' && s.url) out2.push(String(s.url)); } } return out2; }
|
|
123
132
|
for (const line of lines) {
|
|
124
133
|
if (!line) continue; let o; try { o = JSON.parse(line); } catch (e) { continue; }
|
|
@@ -143,7 +152,11 @@ function parseTurns(file, maxTurns) {
|
|
|
143
152
|
out.push({ role: 'assistant', text: text.slice(0, 8000), tools, imgs });
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
|
-
return out
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
// 파일 꼬리(최근 2MB)만 읽어 최근 maxTurns개 (seed용)
|
|
158
|
+
function parseTurns(file, maxTurns) {
|
|
159
|
+
return linesToTurns(readTail(file, 2 * 1024 * 1024)).slice(-Math.max(10, Math.min(200, maxTurns || 60)));
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
async function post(api, p, body) {
|
|
@@ -262,6 +275,7 @@ async function run(conf) {
|
|
|
262
275
|
const session = String(pane).split(':')[0];
|
|
263
276
|
const claudeCmd = conf.claudeCmd || 'claude --dangerously-skip-permissions';
|
|
264
277
|
let warned = false; // 중복방지는 서버가 책임 (claim 시 next_fire 원자적 전진) — 클라 영구셋은 반복예약을 영구차단하므로 안 둠
|
|
278
|
+
let txState = { until: 0, off: 0, file: null, win: '' }; // 💬 루돌프톡 라이브: 무장시각·읽은 오프셋·세션파일·창
|
|
265
279
|
|
|
266
280
|
cleanOldImages();
|
|
267
281
|
console.log(`🛷 SantaClaude 커넥터 가동 — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
|
|
@@ -373,14 +387,16 @@ async function run(conf) {
|
|
|
373
387
|
if (rr > 0) args.push('-y', String(Math.max(10, Math.min(200, rr))));
|
|
374
388
|
if (args.length > 3) { try { execFileSync('tmux', args); console.log(` 📐 리사이즈 ${w} → 폭${c || '그대로'}${rr ? '×높이' + rr : ''}`); } catch (e) {} }
|
|
375
389
|
} else if (cmd.action === 'transcript') {
|
|
376
|
-
|
|
390
|
+
if (cmd.live) { txState.until = Date.now() + 45000; return; } // keepalive — 라이브 모드만 연장(seed 안 함, 초경량)
|
|
391
|
+
// 💬 seed: 전체내역(JSONL 꼬리) 읽어 웹에 전달 + 라이브 모드 무장(이후 델타는 화면틱에 묻어감)
|
|
377
392
|
const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
|
|
378
393
|
let cwd = os.homedir(); try { const pc = execFileSync('tmux', ['display-message', '-t', w, '-p', '#{pane_current_path}'], { encoding: 'utf8' }).trim(); if (pc) cwd = pc; } catch (e) {}
|
|
379
394
|
const file = findTranscript(cwd);
|
|
380
395
|
let turns = [], found = !!file;
|
|
381
396
|
if (file) turns = parseTurns(file, parseInt(cmd.n) || 60);
|
|
382
397
|
try { await post(api, '/api/transcript', { token, window: cmd.name || '', turns, found }); } catch (e) {}
|
|
383
|
-
|
|
398
|
+
txState = { until: Date.now() + 45000, off: file ? (function () { try { return fs.statSync(file).size; } catch (e) { return 0; } })() : 0, file: file, win: cmd.name || '' }; // seed 시점 파일끝=오프셋 기준
|
|
399
|
+
console.log(` 💬 대화내역 ${turns.length}턴 → 웹${found ? ' (라이브 ON)' : ' (트랜스크립트 못 찾음)'}`);
|
|
384
400
|
}
|
|
385
401
|
} catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
|
|
386
402
|
}
|
|
@@ -481,13 +497,21 @@ async function run(conf) {
|
|
|
481
497
|
// 화면 미러 — 변경됐을 때만 전송(claude 멈추면 0). 2.5초 체크라 응답 직후 빠르게 반영
|
|
482
498
|
let lastScr = '';
|
|
483
499
|
function sendScreen() { if (!ws || ws.readyState !== 1) return; let cur; try { cur = JSON.stringify(captureAll(session)); } catch (e) { return; } if (cur === lastScr) return; lastScr = cur; try { ws.send(JSON.stringify({ type: 'screen', screens: JSON.parse(cur) })); } catch (e) {} }
|
|
500
|
+
// 💬 루돌프톡 라이브 델타 — 화면틱에 묻어감. 라이브 무장 중 + JSONL 늘었을 때만 새 턴 전송(우리 서버 추가비용 0에 수렴)
|
|
501
|
+
function sendTxDelta() {
|
|
502
|
+
if (!ws || ws.readyState !== 1) return; if (Date.now() > txState.until || !txState.file) return;
|
|
503
|
+
let d; try { d = readDelta(txState.file, txState.off); } catch (e) { return; }
|
|
504
|
+
if (!d.lines.length) return; txState.off = d.off;
|
|
505
|
+
const turns = linesToTurns(d.lines); if (!turns.length) return;
|
|
506
|
+
try { ws.send(JSON.stringify({ type: 'txdelta', window: txState.win, turns })); console.log(` 💬+ 라이브 델타 ${turns.length}턴`); } catch (e) {}
|
|
507
|
+
}
|
|
484
508
|
|
|
485
509
|
// ── WebSocket 상시 연결 (폴링 제거 — DO가 예약/명령을 push) ──
|
|
486
510
|
const wsUrl = api.replace(/^http/, 'ws') + '/ws?token=' + encodeURIComponent(token);
|
|
487
511
|
let ws = null, hbIv = null, scrIv = null;
|
|
488
512
|
function connectWS() {
|
|
489
513
|
try { ws = new WebSocket(wsUrl); } catch (e) { console.error(' WebSocket 생성 실패:', e.message); setTimeout(connectWS, 5000); return; }
|
|
490
|
-
ws.addEventListener('open', () => { console.log(' 🔌 클라우드 연결됨 (WebSocket).'); lastScr = ''; sendHb(); sendScreen(); clearInterval(hbIv); clearInterval(scrIv); hbIv = setInterval(sendHb, Math.max(every, 15000)); scrIv = setInterval(sendScreen, 2500); });
|
|
514
|
+
ws.addEventListener('open', () => { console.log(' 🔌 클라우드 연결됨 (WebSocket).'); lastScr = ''; sendHb(); sendScreen(); clearInterval(hbIv); clearInterval(scrIv); hbIv = setInterval(sendHb, Math.max(every, 15000)); scrIv = setInterval(() => { sendScreen(); sendTxDelta(); }, 2500); });
|
|
491
515
|
ws.addEventListener('message', (e) => { let m; try { m = JSON.parse(typeof e.data === 'string' ? e.data : e.data.toString()); } catch { return; } if (m.type === 'job') onJob(m); else if (m.type === 'ctl') runControl(m.cmd); });
|
|
492
516
|
ws.addEventListener('close', () => { clearInterval(hbIv); clearInterval(scrIv); console.log(' 🔌 연결 끊김 — 5초 후 재연결'); setTimeout(connectWS, 5000); });
|
|
493
517
|
ws.addEventListener('error', () => { try { ws.close(); } catch (e) {} });
|