@cliphijack/santaclaude 1.0.25 → 1.0.27
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 +34 -6
package/package.json
CHANGED
package/santaclaude.js
CHANGED
|
@@ -263,6 +263,7 @@ function tok24h() { const now = Date.now(); if (LEAGUE_ON && now - _tokAt > 5 *
|
|
|
263
263
|
const SHARE_PORT = 8799;
|
|
264
264
|
const SHARE_DIRS = [path.join(os.homedir(), '.santaclaude', 'outbox'), path.join(os.homedir(), '.santaclaude', 'share'), path.join(os.homedir(), 'Downloads')];
|
|
265
265
|
let _tnCache = null, _shareToken = null, _shareServer = null;
|
|
266
|
+
function notionSet() { try { return /^NOTION_TOKEN=.+/m.test(fs.readFileSync(path.join(os.homedir(), '.santaclaude', '.env'), 'utf8')); } catch (e) { return false; } } // ~/.santaclaude/.env 에 토큰 있나
|
|
266
267
|
function startShareServer(ip) { // 테일넷 IP에만 바인드 → 테일넷 피어만 접근. 경로 토큰 + basename으로 보호
|
|
267
268
|
if (_shareServer || !ip) return;
|
|
268
269
|
try { _shareToken = require('crypto').randomBytes(8).toString('hex'); } catch (e) { _shareToken = String(Date.now()); }
|
|
@@ -356,7 +357,7 @@ description: 산타클로드 파일 보내기 — 만든·가진 파일(이미
|
|
|
356
357
|
|
|
357
358
|
## 규칙
|
|
358
359
|
- **파일명 그대로 유지**(확장자 포함) — 사용자가 그 이름으로 받는다.
|
|
359
|
-
-
|
|
360
|
+
- **크기 제한 사실상 없음** — 18MB 이하는 자동 릴레이, 큰 파일(영상 등)은 테일스케일이 켜져 있으면 **테일넷 직다운(📡)으로 자동 전환**(폰이 같은 테일넷이면 한도 없이 받음). 테일스케일이 없을 때만 18MB 초과 불가.
|
|
360
361
|
- **원본 그대로 복사** — 재인코딩·리사이즈·압축 금지.
|
|
361
362
|
- 옮기지 말고 **복사**(cp). 원본 작업물은 그대로 둔다.
|
|
362
363
|
- 보낸 뒤 사용자에게 "보냈어 — <파일명>" 한 줄 확인.
|
|
@@ -399,9 +400,11 @@ function installDefaultSkills(cwd) {
|
|
|
399
400
|
for (const [fn, content] of Object.entries(files)) {
|
|
400
401
|
try {
|
|
401
402
|
const fp = path.join(base, '.claude', 'skills', name, fn);
|
|
402
|
-
|
|
403
|
+
const existed = fs.existsSync(fp);
|
|
404
|
+
if (existed) { try { if (fs.readFileSync(fp, 'utf8') === content) continue; } catch (e) {} } // santa-* 는 시스템 스킬 — 내용 다르면(구버전) 갱신, 같으면 스킵
|
|
403
405
|
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
404
406
|
fs.writeFileSync(fp, content);
|
|
407
|
+
if (existed) console.log(` 🔄 기본 스킬 갱신: ${name}`);
|
|
405
408
|
} catch (e) {}
|
|
406
409
|
}
|
|
407
410
|
}
|
|
@@ -436,7 +439,8 @@ async function run(conf) {
|
|
|
436
439
|
console.warn(` ⚠️ tmux "${session}" 세션 없음 (--no-spawn) — 직접 띄워줘.`);
|
|
437
440
|
}
|
|
438
441
|
// 기본 스킬 자동 설치 (메인 루돌프 작업 폴더에)
|
|
439
|
-
try {
|
|
442
|
+
try { installDefaultSkills(os.homedir()); } catch (e) {} // 글로벌(~/.claude/skills)에 기본스킬 설치·갱신 — 모든 세션 공용
|
|
443
|
+
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) {}
|
|
440
444
|
|
|
441
445
|
// 루돌프 = 작업장(session) 안의 윈도우(탭). 별도 세션 아님.
|
|
442
446
|
async function runControl(cmd) {
|
|
@@ -477,6 +481,19 @@ async function run(conf) {
|
|
|
477
481
|
setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
|
|
478
482
|
return;
|
|
479
483
|
}
|
|
484
|
+
if (cmd.action === 'notion-setup') { // 🔮 노션 연결 — 토큰·부모페이지를 PC ~/.santaclaude/.env 에만 기록(우리 서버 저장 X)
|
|
485
|
+
const ntok = String(cmd.ntoken || '').trim(); if (!ntok) return;
|
|
486
|
+
let pid = String(cmd.parent || '').replace(/-/g, '').match(/[0-9a-fA-F]{32}/); pid = pid ? pid[0] : '';
|
|
487
|
+
try {
|
|
488
|
+
const dir = path.join(os.homedir(), '.santaclaude'); fs.mkdirSync(dir, { recursive: true });
|
|
489
|
+
const ep = path.join(dir, '.env'); let env = ''; try { env = fs.readFileSync(ep, 'utf8'); } catch (e) {}
|
|
490
|
+
env = env.split('\n').filter((l) => l && !/^NOTION_(TOKEN|PARENT_PAGE)=/.test(l)).join('\n'); if (env && !env.endsWith('\n')) env += '\n';
|
|
491
|
+
env += 'NOTION_TOKEN=' + ntok + '\n'; if (pid) env += 'NOTION_PARENT_PAGE=' + pid + '\n';
|
|
492
|
+
fs.writeFileSync(ep, env, { mode: 0o600 });
|
|
493
|
+
console.log(' 🔮 노션 연결 저장 → ~/.santaclaude/.env' + (pid ? ' (부모페이지 포함)' : ''));
|
|
494
|
+
} catch (e) { console.warn(' 노션 저장 실패: ' + (e.message || '')); }
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
480
497
|
if (cmd.action === 'taildrop') { // 📲 PC→폰 Taildrop 푸시 (C). 공유폴더 파일을 지정 피어로
|
|
481
498
|
const fn = path.basename(String(cmd.file || '')); const peer = String(cmd.peer || '').replace(/[^A-Za-z0-9._-]/g, '');
|
|
482
499
|
if (!fn || !peer) return;
|
|
@@ -637,7 +654,18 @@ async function run(conf) {
|
|
|
637
654
|
const fp = path.join(OUTBOXDIR, fn);
|
|
638
655
|
let st; try { st = fs.statSync(fp); } catch (e) { continue; }
|
|
639
656
|
if (!st.isFile()) continue;
|
|
640
|
-
if (st.size > MAXOUT) {
|
|
657
|
+
if (st.size > MAXOUT) { // 18MB 초과 → 테일스케일 있으면 share/로 옮겨 미니서버 직다운(📡), 없으면 보류
|
|
658
|
+
if (_tnCache && _tnCache.ip) {
|
|
659
|
+
try {
|
|
660
|
+
const shareDir = path.join(os.homedir(), '.santaclaude', 'share'); fs.mkdirSync(shareDir, { recursive: true });
|
|
661
|
+
fs.renameSync(fp, path.join(shareDir, fn)); // outbox 밖으로 → 재스캔 안 됨, 미니서버가 서빙
|
|
662
|
+
const mime = EXT_MIME[(fn.split('.').pop() || '').toLowerCase()] || 'application/octet-stream';
|
|
663
|
+
await post(api, '/api/outfile', { token, name: fn, mime, size: st.size, tn: true }).catch(() => {});
|
|
664
|
+
console.log(` 📡 "${fn}" ${(st.size / 1048576).toFixed(1)}MB → 테일넷 직다운(share/)`);
|
|
665
|
+
} catch (e) { console.warn(' big→테일넷 실패: ' + e.message); }
|
|
666
|
+
} else { console.warn(` ⚠️ "${fn}" ${(st.size / 1048576).toFixed(1)}MB > 18MB · 테일스케일 없어 못 보냄`); try { fs.renameSync(fp, fp + '.toobig'); } catch (e) {} }
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
641
669
|
const ext = (fn.split('.').pop() || '').toLowerCase();
|
|
642
670
|
const mime = EXT_MIME[ext] || 'application/octet-stream';
|
|
643
671
|
let data; try { data = fs.readFileSync(fp).toString('base64'); } catch (e) { continue; }
|
|
@@ -662,7 +690,7 @@ async function run(conf) {
|
|
|
662
690
|
console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
|
|
663
691
|
try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
|
|
664
692
|
}
|
|
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) {} } }
|
|
693
|
+
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, notion: { set: notionSet() } })); } catch (e) {} } }
|
|
666
694
|
// 화면 미러 — 변경됐을 때만 전송(claude 멈추면 0). 2.5초 체크라 응답 직후 빠르게 반영
|
|
667
695
|
let lastScr = '';
|
|
668
696
|
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) {} }
|
|
@@ -688,7 +716,7 @@ async function run(conf) {
|
|
|
688
716
|
// 폴링 폴백 (node<21 = WebSocket 미지원) — 워커가 DO 백엔드라 폴링이어도 KV write 0
|
|
689
717
|
async function pollTick() {
|
|
690
718
|
try {
|
|
691
|
-
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined, tn: _tnCache }).catch(() => {});
|
|
719
|
+
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined, tn: _tnCache, notion: { set: notionSet() } }).catch(() => {});
|
|
692
720
|
post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
|
|
693
721
|
const d = await post(api, '/api/jobs/claim', { token });
|
|
694
722
|
for (const j of (d.jobs || [])) onJob(j);
|