@cliphijack/santaclaude 1.0.14 → 1.0.16
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 -2
package/package.json
CHANGED
package/santaclaude.js
CHANGED
|
@@ -80,6 +80,35 @@ function captureWindow(session, name) {
|
|
|
80
80
|
try { return execFileSync('tmux', ['capture-pane', '-t', session + ':' + name, '-p', '-e', '-S', '-500'], { encoding: 'utf8' }).replace(/\n+$/, '').slice(-60000); }
|
|
81
81
|
catch (e) { return ''; }
|
|
82
82
|
}
|
|
83
|
+
// 🔍 로컬 스킬 발견 — ~/.claude/skills + 각 루돌프 프로젝트 .claude/skills 스캔 (등록 누를 때만 1회)
|
|
84
|
+
function parseFrontmatter(md) {
|
|
85
|
+
const m = String(md).match(/^\s*---\s*\n([\s\S]*?)\n---/); let name = '', desc = '';
|
|
86
|
+
if (m) { const nm = m[1].match(/^name:\s*(.+)$/m); const dm = m[1].match(/^description:\s*(.+)$/m); if (nm) name = nm[1].trim(); if (dm) desc = dm[1].trim(); }
|
|
87
|
+
return { name, desc };
|
|
88
|
+
}
|
|
89
|
+
function scanSkillDir(dir, scope, sessionName, out, seen) {
|
|
90
|
+
let entries = []; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return; }
|
|
91
|
+
for (const e of entries) {
|
|
92
|
+
if (!e.isDirectory()) continue;
|
|
93
|
+
const sd = path.join(dir, e.name);
|
|
94
|
+
let md = ''; try { md = fs.readFileSync(path.join(sd, 'SKILL.md'), 'utf8'); } catch (err) { continue; } // SKILL.md 없으면 스킬 아님
|
|
95
|
+
const fm = parseFrontmatter(md);
|
|
96
|
+
const name = String(fm.name || e.name).slice(0, 80); const key = name.toLowerCase();
|
|
97
|
+
if (seen.has(key)) continue; seen.add(key);
|
|
98
|
+
out.push({ name, desc: String(fm.desc || '').slice(0, 200), scope, dir: sd, session: sessionName || '' });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function collectLocalSkills(session) {
|
|
102
|
+
const out = [], seen = new Set();
|
|
103
|
+
scanSkillDir(path.join(os.homedir(), '.claude', 'skills'), '전역 (~/.claude/skills)', '', out, seen);
|
|
104
|
+
try {
|
|
105
|
+
for (const w of listWindows(session)) {
|
|
106
|
+
let cwd = ''; try { cwd = execFileSync('tmux', ['display-message', '-t', session + ':' + w, '-p', '#{pane_current_path}'], { encoding: 'utf8' }).trim(); } catch (e) {}
|
|
107
|
+
if (cwd && cwd !== os.homedir()) scanSkillDir(path.join(cwd, '.claude', 'skills'), '프로젝트: ' + w, w, out, seen);
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {}
|
|
110
|
+
return out.slice(0, 200);
|
|
111
|
+
}
|
|
83
112
|
function captureAll(session) {
|
|
84
113
|
const o = {}; for (const w of listWindows(session)) o[w] = captureWindow(session, w); return o;
|
|
85
114
|
}
|
|
@@ -158,6 +187,32 @@ function linesToTurns(lines, withImgs) {
|
|
|
158
187
|
function parseTurns(file, maxTurns) {
|
|
159
188
|
return linesToTurns(readTail(file, 2 * 1024 * 1024), false).slice(-Math.max(10, Math.min(200, maxTurns || 60)));
|
|
160
189
|
}
|
|
190
|
+
// 🔥 열일 루돌프 — 최근 24h 토큰 합산(로컬 JSONL의 usage). opt-in(리그 등판) 때만 보고. 숫자만, 대화내용 0
|
|
191
|
+
function collectTok24h() {
|
|
192
|
+
const cutoff = Date.now() - 24 * 3600 * 1000;
|
|
193
|
+
let files = [];
|
|
194
|
+
try {
|
|
195
|
+
const base = path.join(os.homedir(), '.claude', 'projects'); if (!fs.existsSync(base)) return 0;
|
|
196
|
+
for (const d of fs.readdirSync(base)) {
|
|
197
|
+
const dp = path.join(base, d); try { if (!fs.statSync(dp).isDirectory()) continue; } catch (e) { continue; }
|
|
198
|
+
for (const f of fs.readdirSync(dp)) { if (!f.endsWith('.jsonl')) continue; const fp = path.join(dp, f); try { if (fs.statSync(fp).mtimeMs > cutoff) files.push({ fp, m: fs.statSync(fp).mtimeMs }); } catch (e) {} }
|
|
199
|
+
}
|
|
200
|
+
} catch (e) { return 0; }
|
|
201
|
+
files.sort((a, b) => b.m - a.m); files = files.slice(0, 25); // 최근 수정 25개만(부담 방지)
|
|
202
|
+
let tok = 0;
|
|
203
|
+
for (const { fp } of files) {
|
|
204
|
+
for (const line of readTail(fp, 6 * 1024 * 1024)) {
|
|
205
|
+
if (!line) continue; let o; try { o = JSON.parse(line); } catch (e) { continue; }
|
|
206
|
+
if (o.type !== 'assistant') continue;
|
|
207
|
+
const ts = o.timestamp ? Date.parse(o.timestamp) : 0; if (ts && ts < cutoff) continue;
|
|
208
|
+
const u = o.message && o.message.usage; if (!u) continue;
|
|
209
|
+
tok += (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return tok;
|
|
213
|
+
}
|
|
214
|
+
let LEAGUE_ON = false, _tokCache = 0, _tokAt = 0;
|
|
215
|
+
function tok24h() { const now = Date.now(); if (LEAGUE_ON && now - _tokAt > 5 * 60 * 1000) { _tokCache = collectTok24h(); _tokAt = now; } return LEAGUE_ON ? _tokCache : 0; }
|
|
161
216
|
|
|
162
217
|
async function post(api, p, body) {
|
|
163
218
|
const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
@@ -298,6 +353,17 @@ async function run(conf) {
|
|
|
298
353
|
// 루돌프 = 작업장(session) 안의 윈도우(탭). 별도 세션 아님.
|
|
299
354
|
async function runControl(cmd) {
|
|
300
355
|
try {
|
|
356
|
+
if (cmd.action === 'scan-skills') {
|
|
357
|
+
const skills = collectLocalSkills(session);
|
|
358
|
+
try { await fetch(api + '/api/localskills', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, skills }) }); } catch (e) {}
|
|
359
|
+
console.log(' 🔍 로컬 스킬 ' + skills.length + '개 발견 → 보고');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (cmd.action === 'league') { // 🔥 열일 루돌프 리그 opt-in 토글 (켜질 때만 24h 토큰 보고)
|
|
363
|
+
LEAGUE_ON = !!cmd.on; _tokAt = 0; // 캐시 무효화 → 다음 hb에 즉시 반영
|
|
364
|
+
console.log(' 🔥 열일 리그 등판: ' + (LEAGUE_ON ? 'ON' : 'OFF'));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
301
367
|
if (cmd.action === 'skill') {
|
|
302
368
|
// 마켓 스킬 이식 — 그 루돌프 프로젝트 폴더 .claude/skills/<name>/ 에 설치 + claude에 적용 주입
|
|
303
369
|
const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
|
|
@@ -493,7 +559,7 @@ async function run(conf) {
|
|
|
493
559
|
console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
|
|
494
560
|
try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
|
|
495
561
|
}
|
|
496
|
-
function sendHb() { if (ws && ws.readyState === 1) { try { ws.send(JSON.stringify({ type: 'hb', pane, sessions: listWindows(session) })); } catch (e) {} } }
|
|
562
|
+
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) {} } }
|
|
497
563
|
// 화면 미러 — 변경됐을 때만 전송(claude 멈추면 0). 2.5초 체크라 응답 직후 빠르게 반영
|
|
498
564
|
let lastScr = '';
|
|
499
565
|
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) {} }
|
|
@@ -519,7 +585,7 @@ async function run(conf) {
|
|
|
519
585
|
// 폴링 폴백 (node<21 = WebSocket 미지원) — 워커가 DO 백엔드라 폴링이어도 KV write 0
|
|
520
586
|
async function pollTick() {
|
|
521
587
|
try {
|
|
522
|
-
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session) }).catch(() => {});
|
|
588
|
+
post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session), tok: LEAGUE_ON ? tok24h() : undefined }).catch(() => {});
|
|
523
589
|
post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
|
|
524
590
|
const d = await post(api, '/api/jobs/claim', { token });
|
|
525
591
|
for (const j of (d.jobs || [])) onJob(j);
|