@cliphijack/santaclaude 1.0.24 → 1.0.26
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 +68 -6
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();
|
|
@@ -317,7 +356,7 @@ description: 산타클로드 파일 보내기 — 만든·가진 파일(이미
|
|
|
317
356
|
|
|
318
357
|
## 규칙
|
|
319
358
|
- **파일명 그대로 유지**(확장자 포함) — 사용자가 그 이름으로 받는다.
|
|
320
|
-
-
|
|
359
|
+
- **크기 제한 사실상 없음** — 18MB 이하는 자동 릴레이, 큰 파일(영상 등)은 테일스케일이 켜져 있으면 **테일넷 직다운(📡)으로 자동 전환**(폰이 같은 테일넷이면 한도 없이 받음). 테일스케일이 없을 때만 18MB 초과 불가.
|
|
321
360
|
- **원본 그대로 복사** — 재인코딩·리사이즈·압축 금지.
|
|
322
361
|
- 옮기지 말고 **복사**(cp). 원본 작업물은 그대로 둔다.
|
|
323
362
|
- 보낸 뒤 사용자에게 "보냈어 — <파일명>" 한 줄 확인.
|
|
@@ -360,9 +399,11 @@ function installDefaultSkills(cwd) {
|
|
|
360
399
|
for (const [fn, content] of Object.entries(files)) {
|
|
361
400
|
try {
|
|
362
401
|
const fp = path.join(base, '.claude', 'skills', name, fn);
|
|
363
|
-
|
|
402
|
+
const existed = fs.existsSync(fp);
|
|
403
|
+
if (existed) { try { if (fs.readFileSync(fp, 'utf8') === content) continue; } catch (e) {} } // santa-* 는 시스템 스킬 — 내용 다르면(구버전) 갱신, 같으면 스킵
|
|
364
404
|
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
365
405
|
fs.writeFileSync(fp, content);
|
|
406
|
+
if (existed) console.log(` 🔄 기본 스킬 갱신: ${name}`);
|
|
366
407
|
} catch (e) {}
|
|
367
408
|
}
|
|
368
409
|
}
|
|
@@ -380,6 +421,7 @@ async function run(conf) {
|
|
|
380
421
|
cleanOldImages();
|
|
381
422
|
try { execFileSync('tmux', ['set-option', '-g', 'history-limit', '50000'], { stdio: 'ignore' }); } catch (e) {} // 새로 띄우는 루돌프 pane은 스크롤백 5만줄(기존 pane은 유지)
|
|
382
423
|
setTimeout(() => { try { fs.writeFileSync(_GOOD, VER); } catch (e) {} }, 60000); // 60초+ 살면 이 버전을 안정버전으로 기록(롤백 기준)
|
|
424
|
+
refreshTn(); setInterval(refreshTn, 30000); // 📡 테일스케일·Taildrop 감지(30초)
|
|
383
425
|
setTimeout(checkUpdate, 90000); setInterval(checkUpdate, 3 * 3600 * 1000); // 자동 업데이트 체크: 시작 90초 후 + 3시간마다
|
|
384
426
|
console.log(`🛷 SantaClaude 커넥터 가동 v${VER} — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
|
|
385
427
|
if (paneExists(pane)) {
|
|
@@ -396,7 +438,8 @@ async function run(conf) {
|
|
|
396
438
|
console.warn(` ⚠️ tmux "${session}" 세션 없음 (--no-spawn) — 직접 띄워줘.`);
|
|
397
439
|
}
|
|
398
440
|
// 기본 스킬 자동 설치 (메인 루돌프 작업 폴더에)
|
|
399
|
-
try {
|
|
441
|
+
try { installDefaultSkills(os.homedir()); } catch (e) {} // 글로벌(~/.claude/skills)에 기본스킬 설치·갱신 — 모든 세션 공용
|
|
442
|
+
try { let c = ''; try { const pc = execFileSync('tmux', ['display-message', '-t', pane, '-p', '#{pane_current_path}'], { encoding: 'utf8' }).trim(); if (pc && pc !== os.homedir()) c = pc; } catch (e) {} if (c) installDefaultSkills(c); } catch (e) {}
|
|
400
443
|
|
|
401
444
|
// 루돌프 = 작업장(session) 안의 윈도우(탭). 별도 세션 아님.
|
|
402
445
|
async function runControl(cmd) {
|
|
@@ -437,6 +480,14 @@ async function run(conf) {
|
|
|
437
480
|
setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
|
|
438
481
|
return;
|
|
439
482
|
}
|
|
483
|
+
if (cmd.action === 'taildrop') { // 📲 PC→폰 Taildrop 푸시 (C). 공유폴더 파일을 지정 피어로
|
|
484
|
+
const fn = path.basename(String(cmd.file || '')); const peer = String(cmd.peer || '').replace(/[^A-Za-z0-9._-]/g, '');
|
|
485
|
+
if (!fn || !peer) return;
|
|
486
|
+
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) {} }
|
|
487
|
+
if (!fp) { console.log(' 📲 Taildrop: 파일 못 찾음 — ' + fn); return; }
|
|
488
|
+
try { execFileSync('tailscale', ['file', 'cp', fp, peer + ':'], { timeout: 120000 }); console.log(` 📲 Taildrop → ${peer}: ${fn}`); } catch (e) { console.log(' 📲 Taildrop 실패: ' + (e.message || '')); }
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
440
491
|
if (cmd.action === 'image') {
|
|
441
492
|
// 웹이 올린 파일을 받아 로컬에 저장하고 claude에 경로 주입 (이미지·txt·mp3·mp4 등 전부)
|
|
442
493
|
const dir = path.join(os.homedir(), '.santaclaude', 'img');
|
|
@@ -589,7 +640,18 @@ async function run(conf) {
|
|
|
589
640
|
const fp = path.join(OUTBOXDIR, fn);
|
|
590
641
|
let st; try { st = fs.statSync(fp); } catch (e) { continue; }
|
|
591
642
|
if (!st.isFile()) continue;
|
|
592
|
-
if (st.size > MAXOUT) {
|
|
643
|
+
if (st.size > MAXOUT) { // 18MB 초과 → 테일스케일 있으면 share/로 옮겨 미니서버 직다운(📡), 없으면 보류
|
|
644
|
+
if (_tnCache && _tnCache.ip) {
|
|
645
|
+
try {
|
|
646
|
+
const shareDir = path.join(os.homedir(), '.santaclaude', 'share'); fs.mkdirSync(shareDir, { recursive: true });
|
|
647
|
+
fs.renameSync(fp, path.join(shareDir, fn)); // outbox 밖으로 → 재스캔 안 됨, 미니서버가 서빙
|
|
648
|
+
const mime = EXT_MIME[(fn.split('.').pop() || '').toLowerCase()] || 'application/octet-stream';
|
|
649
|
+
await post(api, '/api/outfile', { token, name: fn, mime, size: st.size, tn: true }).catch(() => {});
|
|
650
|
+
console.log(` 📡 "${fn}" ${(st.size / 1048576).toFixed(1)}MB → 테일넷 직다운(share/)`);
|
|
651
|
+
} catch (e) { console.warn(' big→테일넷 실패: ' + e.message); }
|
|
652
|
+
} else { console.warn(` ⚠️ "${fn}" ${(st.size / 1048576).toFixed(1)}MB > 18MB · 테일스케일 없어 못 보냄`); try { fs.renameSync(fp, fp + '.toobig'); } catch (e) {} }
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
593
655
|
const ext = (fn.split('.').pop() || '').toLowerCase();
|
|
594
656
|
const mime = EXT_MIME[ext] || 'application/octet-stream';
|
|
595
657
|
let data; try { data = fs.readFileSync(fp).toString('base64'); } catch (e) { continue; }
|
|
@@ -614,7 +676,7 @@ async function run(conf) {
|
|
|
614
676
|
console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
|
|
615
677
|
try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
|
|
616
678
|
}
|
|
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) {} } }
|
|
679
|
+
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
680
|
// 화면 미러 — 변경됐을 때만 전송(claude 멈추면 0). 2.5초 체크라 응답 직후 빠르게 반영
|
|
619
681
|
let lastScr = '';
|
|
620
682
|
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 +702,7 @@ async function run(conf) {
|
|
|
640
702
|
// 폴링 폴백 (node<21 = WebSocket 미지원) — 워커가 DO 백엔드라 폴링이어도 KV write 0
|
|
641
703
|
async function pollTick() {
|
|
642
704
|
try {
|
|
643
|
-
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined }).catch(() => {});
|
|
705
|
+
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined, tn: _tnCache }).catch(() => {});
|
|
644
706
|
post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
|
|
645
707
|
const d = await post(api, '/api/jobs/claim', { token });
|
|
646
708
|
for (const j of (d.jobs || [])) onJob(j);
|