@cliphijack/santaclaude 1.0.26 → 1.0.28

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/santaclaude.js +36 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -263,6 +263,26 @@ 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 에 토큰 있나
267
+ // 🔮 노션 피드 — 부모페이지("클로드세션") 하위(세션 페이지·DB)를 읽어 웹에 보여줌. 커넥터(유저 PC)가 직접 호출 → 서버 무부담
268
+ let _njCache = null;
269
+ function refreshNotion() {
270
+ let env = ''; try { env = fs.readFileSync(path.join(os.homedir(), '.santaclaude', '.env'), 'utf8'); } catch (e) { _njCache = null; return; }
271
+ const tok = (env.match(/^NOTION_TOKEN=(.+)$/m) || [])[1];
272
+ const parent = (env.match(/^NOTION_PARENT_PAGE=(.+)$/m) || [])[1];
273
+ if (!tok) { _njCache = null; return; }
274
+ if (!parent) { _njCache = { noParent: true, items: [] }; return; }
275
+ fetch('https://api.notion.com/v1/blocks/' + parent.trim() + '/children?page_size=50', { headers: { Authorization: 'Bearer ' + tok.trim(), 'Notion-Version': '2022-06-28' } })
276
+ .then((r) => r.json()).then((j) => {
277
+ if (!j || j.object === 'error') { _njCache = { err: (j && j.message) || 'notion error', items: [] }; return; }
278
+ const items = [];
279
+ for (const b of (j.results || [])) {
280
+ if (b.type === 'child_page') items.push({ t: 'page', id: (b.id || '').replace(/-/g, ''), title: (b.child_page && b.child_page.title) || '(제목 없음)' });
281
+ else if (b.type === 'child_database') items.push({ t: 'db', id: (b.id || '').replace(/-/g, ''), title: (b.child_database && b.child_database.title) || '(DB)' });
282
+ }
283
+ _njCache = { items: items.slice(0, 30) };
284
+ }).catch(() => { _njCache = null; });
285
+ }
266
286
  function startShareServer(ip) { // 테일넷 IP에만 바인드 → 테일넷 피어만 접근. 경로 토큰 + basename으로 보호
267
287
  if (_shareServer || !ip) return;
268
288
  try { _shareToken = require('crypto').randomBytes(8).toString('hex'); } catch (e) { _shareToken = String(Date.now()); }
@@ -422,6 +442,7 @@ async function run(conf) {
422
442
  try { execFileSync('tmux', ['set-option', '-g', 'history-limit', '50000'], { stdio: 'ignore' }); } catch (e) {} // 새로 띄우는 루돌프 pane은 스크롤백 5만줄(기존 pane은 유지)
423
443
  setTimeout(() => { try { fs.writeFileSync(_GOOD, VER); } catch (e) {} }, 60000); // 60초+ 살면 이 버전을 안정버전으로 기록(롤백 기준)
424
444
  refreshTn(); setInterval(refreshTn, 30000); // 📡 테일스케일·Taildrop 감지(30초)
445
+ refreshNotion(); setInterval(refreshNotion, 45000); // 🔮 노션 피드(45초)
425
446
  setTimeout(checkUpdate, 90000); setInterval(checkUpdate, 3 * 3600 * 1000); // 자동 업데이트 체크: 시작 90초 후 + 3시간마다
426
447
  console.log(`🛷 SantaClaude 커넥터 가동 v${VER} — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
427
448
  if (paneExists(pane)) {
@@ -480,6 +501,19 @@ async function run(conf) {
480
501
  setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
481
502
  return;
482
503
  }
504
+ if (cmd.action === 'notion-setup') { // 🔮 노션 연결 — 토큰·부모페이지를 PC ~/.santaclaude/.env 에만 기록(우리 서버 저장 X)
505
+ const ntok = String(cmd.ntoken || '').trim(); if (!ntok) return;
506
+ let pid = String(cmd.parent || '').replace(/-/g, '').match(/[0-9a-fA-F]{32}/); pid = pid ? pid[0] : '';
507
+ try {
508
+ const dir = path.join(os.homedir(), '.santaclaude'); fs.mkdirSync(dir, { recursive: true });
509
+ const ep = path.join(dir, '.env'); let env = ''; try { env = fs.readFileSync(ep, 'utf8'); } catch (e) {}
510
+ env = env.split('\n').filter((l) => l && !/^NOTION_(TOKEN|PARENT_PAGE)=/.test(l)).join('\n'); if (env && !env.endsWith('\n')) env += '\n';
511
+ env += 'NOTION_TOKEN=' + ntok + '\n'; if (pid) env += 'NOTION_PARENT_PAGE=' + pid + '\n';
512
+ fs.writeFileSync(ep, env, { mode: 0o600 });
513
+ console.log(' 🔮 노션 연결 저장 → ~/.santaclaude/.env' + (pid ? ' (부모페이지 포함)' : ''));
514
+ } catch (e) { console.warn(' 노션 저장 실패: ' + (e.message || '')); }
515
+ return;
516
+ }
483
517
  if (cmd.action === 'taildrop') { // 📲 PC→폰 Taildrop 푸시 (C). 공유폴더 파일을 지정 피어로
484
518
  const fn = path.basename(String(cmd.file || '')); const peer = String(cmd.peer || '').replace(/[^A-Za-z0-9._-]/g, '');
485
519
  if (!fn || !peer) return;
@@ -676,7 +710,7 @@ async function run(conf) {
676
710
  console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
677
711
  try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
678
712
  }
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) {} } }
713
+ 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(), feed: _njCache } })); } catch (e) {} } }
680
714
  // 화면 미러 — 변경됐을 때만 전송(claude 멈추면 0). 2.5초 체크라 응답 직후 빠르게 반영
681
715
  let lastScr = '';
682
716
  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) {} }
@@ -702,7 +736,7 @@ async function run(conf) {
702
736
  // 폴링 폴백 (node<21 = WebSocket 미지원) — 워커가 DO 백엔드라 폴링이어도 KV write 0
703
737
  async function pollTick() {
704
738
  try {
705
- post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined, tn: _tnCache }).catch(() => {});
739
+ post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined, tn: _tnCache, notion: { set: notionSet(), feed: _njCache } }).catch(() => {});
706
740
  post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
707
741
  const d = await post(api, '/api/jobs/claim', { token });
708
742
  for (const j of (d.jobs || [])) onJob(j);