@cliphijack/santaclaude 1.0.24 → 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 +50 -2
package/package.json
CHANGED
package/santaclaude.js
CHANGED
|
@@ -259,6 +259,45 @@ function collectTok24h() {
|
|
|
259
259
|
let LEAGUE_ON = false, _tokCache = 0, _tokAt = 0;
|
|
260
260
|
function tok24h() { const now = Date.now(); if (LEAGUE_ON && now - _tokAt > 5 * 60 * 1000) { _tokCache = collectTok24h(); _tokAt = now; } return LEAGUE_ON ? _tokCache : 0; }
|
|
261
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
|
+
|
|
262
301
|
async function post(api, p, body) {
|
|
263
302
|
const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
264
303
|
return r.json();
|
|
@@ -380,6 +419,7 @@ async function run(conf) {
|
|
|
380
419
|
cleanOldImages();
|
|
381
420
|
try { execFileSync('tmux', ['set-option', '-g', 'history-limit', '50000'], { stdio: 'ignore' }); } catch (e) {} // 새로 띄우는 루돌프 pane은 스크롤백 5만줄(기존 pane은 유지)
|
|
382
421
|
setTimeout(() => { try { fs.writeFileSync(_GOOD, VER); } catch (e) {} }, 60000); // 60초+ 살면 이 버전을 안정버전으로 기록(롤백 기준)
|
|
422
|
+
refreshTn(); setInterval(refreshTn, 30000); // 📡 테일스케일·Taildrop 감지(30초)
|
|
383
423
|
setTimeout(checkUpdate, 90000); setInterval(checkUpdate, 3 * 3600 * 1000); // 자동 업데이트 체크: 시작 90초 후 + 3시간마다
|
|
384
424
|
console.log(`🛷 SantaClaude 커넥터 가동 v${VER} — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
|
|
385
425
|
if (paneExists(pane)) {
|
|
@@ -437,6 +477,14 @@ async function run(conf) {
|
|
|
437
477
|
setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
|
|
438
478
|
return;
|
|
439
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
|
+
}
|
|
440
488
|
if (cmd.action === 'image') {
|
|
441
489
|
// 웹이 올린 파일을 받아 로컬에 저장하고 claude에 경로 주입 (이미지·txt·mp3·mp4 등 전부)
|
|
442
490
|
const dir = path.join(os.homedir(), '.santaclaude', 'img');
|
|
@@ -614,7 +662,7 @@ async function run(conf) {
|
|
|
614
662
|
console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
|
|
615
663
|
try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
|
|
616
664
|
}
|
|
617
|
-
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) {} } }
|
|
618
666
|
// 화면 미러 — 변경됐을 때만 전송(claude 멈추면 0). 2.5초 체크라 응답 직후 빠르게 반영
|
|
619
667
|
let lastScr = '';
|
|
620
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) {} }
|
|
@@ -640,7 +688,7 @@ async function run(conf) {
|
|
|
640
688
|
// 폴링 폴백 (node<21 = WebSocket 미지원) — 워커가 DO 백엔드라 폴링이어도 KV write 0
|
|
641
689
|
async function pollTick() {
|
|
642
690
|
try {
|
|
643
|
-
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(() => {});
|
|
644
692
|
post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
|
|
645
693
|
const d = await post(api, '/api/jobs/claim', { token });
|
|
646
694
|
for (const j of (d.jobs || [])) onJob(j);
|