@cliphijack/santaclaude 1.0.23 → 1.0.25
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 +54 -4
package/package.json
CHANGED
package/santaclaude.js
CHANGED
|
@@ -52,9 +52,11 @@ function checkUpdate() {
|
|
|
52
52
|
const target = String(j.target || '');
|
|
53
53
|
if (!target || target === VER) return; // 목표=현재면 그대로 (target≠VER이면 업/다운 둘다 = 롤백 가능)
|
|
54
54
|
if (process.env.SC_SUP === '1') {
|
|
55
|
-
console.log(` ⬆️ 새 버전 ${target} (현재 ${VER}) —
|
|
55
|
+
console.log(` ⬆️ 새 버전 ${target} (현재 ${VER}) — 미리 받는 중(무중단 교체)…`);
|
|
56
56
|
try { fs.writeFileSync(_TGT, target); } catch (e) {}
|
|
57
|
-
|
|
57
|
+
// 🔄 프리페치: 교체 전에 새 버전을 npx 캐시에 미리 받아둔다(status는 ping 한 번 하고 종료=데몬 안 띄움) → 재시작 순간 빠름
|
|
58
|
+
try { require('child_process').spawn('npx', ['-y', '@cliphijack/santaclaude@' + target, 'status'], { stdio: 'ignore', detached: true }).unref(); } catch (e) {}
|
|
59
|
+
setTimeout(() => { console.log(' 🔄 교체 — 1~2초 끊겼다 복귀(루돌프·예약은 계속, 빠진 잡은 서버가 보관)'); process.exit(75); }, 5000);
|
|
58
60
|
} else console.log(` ⬆️ 새 버전 ${target} 나옴 (현재 ${VER}) — 재시작 권장: npx -y @cliphijack/santaclaude@latest`);
|
|
59
61
|
}).catch(() => {});
|
|
60
62
|
}
|
|
@@ -257,6 +259,45 @@ function collectTok24h() {
|
|
|
257
259
|
let LEAGUE_ON = false, _tokCache = 0, _tokAt = 0;
|
|
258
260
|
function tok24h() { const now = Date.now(); if (LEAGUE_ON && now - _tokAt > 5 * 60 * 1000) { _tokCache = collectTok24h(); _tokAt = now; } return LEAGUE_ON ? _tokCache : 0; }
|
|
259
261
|
|
|
262
|
+
// 📡 테일스케일 — 18MB 우회. A)미니 파일서버(PC→폰 직다운) C)Taildrop 푸시(PC→폰) + 받은파일(폰→PC). 비동기 캐시
|
|
263
|
+
const SHARE_PORT = 8799;
|
|
264
|
+
const SHARE_DIRS = [path.join(os.homedir(), '.santaclaude', 'outbox'), path.join(os.homedir(), '.santaclaude', 'share'), path.join(os.homedir(), 'Downloads')];
|
|
265
|
+
let _tnCache = null, _shareToken = null, _shareServer = null;
|
|
266
|
+
function startShareServer(ip) { // 테일넷 IP에만 바인드 → 테일넷 피어만 접근. 경로 토큰 + basename으로 보호
|
|
267
|
+
if (_shareServer || !ip) return;
|
|
268
|
+
try { _shareToken = require('crypto').randomBytes(8).toString('hex'); } catch (e) { _shareToken = String(Date.now()); }
|
|
269
|
+
const http = require('http');
|
|
270
|
+
_shareServer = http.createServer((req, res) => {
|
|
271
|
+
try {
|
|
272
|
+
const parts = decodeURIComponent((req.url || '').split('?')[0]).split('/').filter(Boolean);
|
|
273
|
+
if (parts[0] !== _shareToken) { res.writeHead(403); return res.end('no'); }
|
|
274
|
+
const fname = path.basename(parts.slice(1).join('/')); if (!fname) { res.writeHead(404); return res.end('no'); }
|
|
275
|
+
for (const d of SHARE_DIRS) { const fp = path.join(d, fname); if (fp.startsWith(d)) { try { if (fs.statSync(fp).isFile()) { res.writeHead(200, { 'Content-Disposition': 'attachment; filename="' + encodeURIComponent(fname) + '"', 'Content-Length': fs.statSync(fp).size, 'Content-Type': 'application/octet-stream' }); return fs.createReadStream(fp).pipe(res); } } catch (x) {} } }
|
|
276
|
+
res.writeHead(404); res.end('no');
|
|
277
|
+
} catch (e) { try { res.writeHead(500); res.end(); } catch (x) {} }
|
|
278
|
+
});
|
|
279
|
+
_shareServer.on('error', () => { try { _shareServer.close(); } catch (x) {} _shareServer = null; });
|
|
280
|
+
try { _shareServer.listen(SHARE_PORT, ip); } catch (e) { _shareServer = null; }
|
|
281
|
+
}
|
|
282
|
+
function refreshTn() {
|
|
283
|
+
const { execFile } = require('child_process');
|
|
284
|
+
execFile('tailscale', ['ip', '-4'], { timeout: 4000 }, (e, out) => {
|
|
285
|
+
if (e || !out) { _tnCache = null; return; }
|
|
286
|
+
const ip = String(out).trim().split('\n')[0]; if (!ip) { _tnCache = null; return; }
|
|
287
|
+
let host = ''; try { host = String(execFileSync('hostname', [], { encoding: 'utf8' })).trim(); } catch (x) {}
|
|
288
|
+
startShareServer(ip);
|
|
289
|
+
execFile('tailscale', ['status', '--json'], { timeout: 4000, maxBuffer: 8e6 }, (e2, sout) => {
|
|
290
|
+
let peers = []; try { const j = JSON.parse(sout || '{}'); peers = Object.values(j.Peer || {}).map((p) => ({ name: p.HostName || (p.DNSName || '').split('.')[0] || '', target: (p.DNSName || p.HostName || '').replace(/\.$/, ''), os: p.OS || '' })).filter((p) => p.name).slice(0, 25); } catch (x) {}
|
|
291
|
+
execFile('tailscale', ['file', 'get', '--conflict=rename', path.join(os.homedir(), 'Downloads')], { timeout: 6000 }, () => { // Linux: 대기중 Taildrop 받기(best-effort)
|
|
292
|
+
const drops = [], dir = path.join(os.homedir(), 'Downloads'), now = Date.now();
|
|
293
|
+
try { for (const f of fs.readdirSync(dir)) { if (f.startsWith('.')) continue; const fp = path.join(dir, f); try { const st = fs.statSync(fp); if (st.isFile() && now - st.mtimeMs < 25 * 3600 * 1000) drops.push({ name: f.slice(0, 120), size: st.size, mt: Math.round(st.mtimeMs) }); } catch (x) {} } } catch (x) {}
|
|
294
|
+
drops.sort((a, b) => b.mt - a.mt);
|
|
295
|
+
_tnCache = { ip, host, port: SHARE_PORT, token: _shareToken, peers, drops: drops.slice(0, 15) };
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
260
301
|
async function post(api, p, body) {
|
|
261
302
|
const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
262
303
|
return r.json();
|
|
@@ -378,6 +419,7 @@ async function run(conf) {
|
|
|
378
419
|
cleanOldImages();
|
|
379
420
|
try { execFileSync('tmux', ['set-option', '-g', 'history-limit', '50000'], { stdio: 'ignore' }); } catch (e) {} // 새로 띄우는 루돌프 pane은 스크롤백 5만줄(기존 pane은 유지)
|
|
380
421
|
setTimeout(() => { try { fs.writeFileSync(_GOOD, VER); } catch (e) {} }, 60000); // 60초+ 살면 이 버전을 안정버전으로 기록(롤백 기준)
|
|
422
|
+
refreshTn(); setInterval(refreshTn, 30000); // 📡 테일스케일·Taildrop 감지(30초)
|
|
381
423
|
setTimeout(checkUpdate, 90000); setInterval(checkUpdate, 3 * 3600 * 1000); // 자동 업데이트 체크: 시작 90초 후 + 3시간마다
|
|
382
424
|
console.log(`🛷 SantaClaude 커넥터 가동 v${VER} — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
|
|
383
425
|
if (paneExists(pane)) {
|
|
@@ -435,6 +477,14 @@ async function run(conf) {
|
|
|
435
477
|
setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
|
|
436
478
|
return;
|
|
437
479
|
}
|
|
480
|
+
if (cmd.action === 'taildrop') { // 📲 PC→폰 Taildrop 푸시 (C). 공유폴더 파일을 지정 피어로
|
|
481
|
+
const fn = path.basename(String(cmd.file || '')); const peer = String(cmd.peer || '').replace(/[^A-Za-z0-9._-]/g, '');
|
|
482
|
+
if (!fn || !peer) return;
|
|
483
|
+
let fp = ''; for (const d of SHARE_DIRS) { const c = path.join(d, fn); try { if (fs.statSync(c).isFile()) { fp = c; break; } } catch (e) {} }
|
|
484
|
+
if (!fp) { console.log(' 📲 Taildrop: 파일 못 찾음 — ' + fn); return; }
|
|
485
|
+
try { execFileSync('tailscale', ['file', 'cp', fp, peer + ':'], { timeout: 120000 }); console.log(` 📲 Taildrop → ${peer}: ${fn}`); } catch (e) { console.log(' 📲 Taildrop 실패: ' + (e.message || '')); }
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
438
488
|
if (cmd.action === 'image') {
|
|
439
489
|
// 웹이 올린 파일을 받아 로컬에 저장하고 claude에 경로 주입 (이미지·txt·mp3·mp4 등 전부)
|
|
440
490
|
const dir = path.join(os.homedir(), '.santaclaude', 'img');
|
|
@@ -612,7 +662,7 @@ async function run(conf) {
|
|
|
612
662
|
console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
|
|
613
663
|
try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
|
|
614
664
|
}
|
|
615
|
-
function sendHb() { if (ws && ws.readyState === 1) { try { ws.send(JSON.stringify({ type: 'hb', pane, sessions: listWindows(session), token: LEAGUE_ON ? token : undefined, tok: LEAGUE_ON ? tok24h() : undefined })); } catch (e) {} } }
|
|
665
|
+
function sendHb() { if (ws && ws.readyState === 1) { try { ws.send(JSON.stringify({ type: 'hb', pane, sessions: listWindows(session), token: LEAGUE_ON ? token : undefined, tok: LEAGUE_ON ? tok24h() : undefined, tn: _tnCache })); } catch (e) {} } }
|
|
616
666
|
// 화면 미러 — 변경됐을 때만 전송(claude 멈추면 0). 2.5초 체크라 응답 직후 빠르게 반영
|
|
617
667
|
let lastScr = '';
|
|
618
668
|
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) {} }
|
|
@@ -638,7 +688,7 @@ async function run(conf) {
|
|
|
638
688
|
// 폴링 폴백 (node<21 = WebSocket 미지원) — 워커가 DO 백엔드라 폴링이어도 KV write 0
|
|
639
689
|
async function pollTick() {
|
|
640
690
|
try {
|
|
641
|
-
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined }).catch(() => {});
|
|
691
|
+
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined, tn: _tnCache }).catch(() => {});
|
|
642
692
|
post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
|
|
643
693
|
const d = await post(api, '/api/jobs/claim', { token });
|
|
644
694
|
for (const j of (d.jobs || [])) onJob(j);
|