@cliphijack/santaclaude 1.0.8 → 1.0.10
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 +59 -3
package/package.json
CHANGED
package/santaclaude.js
CHANGED
|
@@ -29,6 +29,8 @@ function parseArgs(argv) {
|
|
|
29
29
|
function loadConf() { try { return JSON.parse(fs.readFileSync(CONF, 'utf8')); } catch { return {}; } }
|
|
30
30
|
function saveConf(c) { fs.writeFileSync(CONF, JSON.stringify(c, null, 2)); }
|
|
31
31
|
function paneExists(pane) { try { execFileSync('tmux', ['has-session', '-t', pane.split(':')[0]], { stdio: 'ignore' }); return true; } catch { return false; } }
|
|
32
|
+
// 사람이 직접 이 세션에 붙어있나(tmux attach 등). 붙어있으면 리사이즈 금지 — 고객 화면 망가뜨리면 안 됨
|
|
33
|
+
function clientsAttached(session) { try { return execFileSync('tmux', ['list-clients', '-t', session, '-F', '#{client_name}'], { encoding: 'utf8' }).trim().length > 0; } catch (e) { return false; } }
|
|
32
34
|
|
|
33
35
|
function inject(pane, message) {
|
|
34
36
|
const msg = String(message);
|
|
@@ -54,7 +56,7 @@ function spawnClaude(session, cmd, cwd) {
|
|
|
54
56
|
}
|
|
55
57
|
// 한 세션(작업장) 안에 새 윈도우(탭)로 claude 실행 — 프로젝트별 분리
|
|
56
58
|
function spawnWindow(session, name, cmd, cwd) {
|
|
57
|
-
try { execFileSync('tmux', ['resize-window', '-t', session, '-x', '220', '-y', '50']); } catch (e) {} // 세션
|
|
59
|
+
if (!clientsAttached(session)) { try { execFileSync('tmux', ['resize-window', '-t', session, '-x', '220', '-y', '50']); } catch (e) {} } // 사람 안 붙어있을 때만 크게(고객 세션 보호)
|
|
58
60
|
const a = ['new-window', '-t', session, '-n', name];
|
|
59
61
|
if (cwd) a.push('-c', cwd);
|
|
60
62
|
execFileSync('tmux', a);
|
|
@@ -90,6 +92,49 @@ function cleanOldImages() {
|
|
|
90
92
|
} catch (e) {}
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
// 💬 대화내역 — claude는 전체 대화를 ~/.claude/projects/<cwd인코딩>/<세션>.jsonl 에 저장. 그 활성 세션 파일 찾기.
|
|
96
|
+
function findTranscript(cwd) {
|
|
97
|
+
try {
|
|
98
|
+
const base = path.join(os.homedir(), '.claude', 'projects'); if (!fs.existsSync(base)) return null;
|
|
99
|
+
let dirs = [];
|
|
100
|
+
if (cwd) { const enc = cwd.replace(/[/.]/g, '-'); const cand = path.join(base, enc); if (fs.existsSync(cand)) dirs = [cand]; } // claude는 cwd의 / · . 를 - 로 인코딩
|
|
101
|
+
if (!dirs.length) dirs = fs.readdirSync(base).map((d) => path.join(base, d)).filter((d) => { try { return fs.statSync(d).isDirectory(); } catch (e) { return false; } });
|
|
102
|
+
let files = [];
|
|
103
|
+
for (const d of dirs) { try { for (const f of fs.readdirSync(d)) if (f.endsWith('.jsonl')) files.push(path.join(d, f)); } catch (e) {} }
|
|
104
|
+
if (!files.length) return null;
|
|
105
|
+
files.sort((a, b) => { try { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; } catch (e) { return 0; } });
|
|
106
|
+
return files[0]; // 가장 최근 수정 = 활성 세션
|
|
107
|
+
} catch (e) { return null; }
|
|
108
|
+
}
|
|
109
|
+
// JSONL → user/assistant 턴 (툴콜은 칩으로 요약). 최근 maxTurns개.
|
|
110
|
+
function parseTurns(file, maxTurns) {
|
|
111
|
+
const out = [];
|
|
112
|
+
let lines; try { lines = fs.readFileSync(file, 'utf8').split('\n'); } catch (e) { return out; }
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
if (!line) continue; let o; try { o = JSON.parse(line); } catch (e) { continue; }
|
|
115
|
+
if (o.type !== 'user' && o.type !== 'assistant') continue;
|
|
116
|
+
const m = o.message; if (!m) continue; const c = m.content;
|
|
117
|
+
if (o.type === 'user') {
|
|
118
|
+
let text = '';
|
|
119
|
+
if (typeof c === 'string') text = c;
|
|
120
|
+
else if (Array.isArray(c)) { const t = c.filter((b) => b && b.type === 'text').map((b) => b.text); if (!t.length) continue; text = t.join('\n'); } // tool_result만이면 사용자 발화 아님 → 스킵
|
|
121
|
+
text = String(text || '').trim(); if (!text) continue;
|
|
122
|
+
if (/^<(command-name|local-command|command-message|command-args)/.test(text)) continue; // 슬래시명령 메타 스킵
|
|
123
|
+
out.push({ role: 'user', text: text.slice(0, 8000) });
|
|
124
|
+
} else {
|
|
125
|
+
let text = '', tools = [];
|
|
126
|
+
if (typeof c === 'string') text = c;
|
|
127
|
+
else if (Array.isArray(c)) for (const b of c) {
|
|
128
|
+
if (b && b.type === 'text' && b.text) text += (text ? '\n' : '') + b.text;
|
|
129
|
+
else if (b && b.type === 'tool_use') { const inp = b.input || {}; const hint = inp.command ? String(inp.command) : (inp.file_path ? String(inp.file_path).split('/').pop() : (inp.path ? String(inp.path) : '')); tools.push((b.name || 'tool') + (hint ? ': ' + hint.slice(0, 70) : '')); }
|
|
130
|
+
}
|
|
131
|
+
text = String(text || '').trim(); if (!text && !tools.length) continue;
|
|
132
|
+
out.push({ role: 'assistant', text: text.slice(0, 8000), tools });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out.slice(-Math.max(10, Math.min(200, maxTurns || 60)));
|
|
136
|
+
}
|
|
137
|
+
|
|
93
138
|
async function post(api, p, body) {
|
|
94
139
|
const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
95
140
|
return r.json();
|
|
@@ -210,8 +255,9 @@ async function run(conf) {
|
|
|
210
255
|
cleanOldImages();
|
|
211
256
|
console.log(`🛷 SantaClaude 커넥터 가동 — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
|
|
212
257
|
if (paneExists(pane)) {
|
|
213
|
-
|
|
214
|
-
|
|
258
|
+
// 기존(고객) 세션엔 사람이 안 붙어있을 때만 리사이즈 — 직접 쓰고 있으면 화면 절대 안 건드림
|
|
259
|
+
if (!clientsAttached(session)) { try { execFileSync('tmux', ['resize-window', '-t', session, '-x', '220', '-y', '50']); } catch (e) {} }
|
|
260
|
+
console.log(` ✅ tmux "${session}" 세션에 붙음 (기존 claude에 주입${clientsAttached(session) ? ', 사람 사용중→사이즈 안 건드림' : ''}).`);
|
|
215
261
|
} else if (conf.spawn !== false) {
|
|
216
262
|
console.log(` 🦌 "${session}" 세션이 없어서 claude를 새로 띄울게…`);
|
|
217
263
|
try {
|
|
@@ -308,12 +354,22 @@ async function run(conf) {
|
|
|
308
354
|
} else if (cmd.action === 'resize') {
|
|
309
355
|
// 웹 뷰어 폭에 맞춰 tmux 윈도우 리사이즈 — claude TUI가 그 폭으로 reflow(모바일 가로스크롤 해소)
|
|
310
356
|
// 보통 cols만 옴 = 폭만 맞추고 높이는 그대로(스크롤백으로 세로 내역 풍부하게). rows 오면 같이 적용.
|
|
357
|
+
if (clientsAttached(session)) { console.log(' 📐 리사이즈 건너뜀 — 사람이 tmux에 붙어있음(고객 화면 보호)'); return; } // 사람이 직접 보고있으면 reflow로 화면 망가뜨리지 않음
|
|
311
358
|
const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
|
|
312
359
|
const c = parseInt(cmd.cols) || 0, rr = parseInt(cmd.rows) || 0;
|
|
313
360
|
const args = ['resize-window', '-t', w];
|
|
314
361
|
if (c > 0) args.push('-x', String(Math.max(20, Math.min(400, c))));
|
|
315
362
|
if (rr > 0) args.push('-y', String(Math.max(10, Math.min(200, rr))));
|
|
316
363
|
if (args.length > 3) { try { execFileSync('tmux', args); console.log(` 📐 리사이즈 ${w} → 폭${c || '그대로'}${rr ? '×높이' + rr : ''}`); } catch (e) {} }
|
|
364
|
+
} else if (cmd.action === 'transcript') {
|
|
365
|
+
// 💬 그 루돌프의 claude 대화 전체내역(JSONL) 읽어 웹에 전달 — 우리 서버엔 안 쌓음(TTL 릴레이)
|
|
366
|
+
const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
|
|
367
|
+
let cwd = os.homedir(); try { const pc = execFileSync('tmux', ['display-message', '-t', w, '-p', '#{pane_current_path}'], { encoding: 'utf8' }).trim(); if (pc) cwd = pc; } catch (e) {}
|
|
368
|
+
const file = findTranscript(cwd);
|
|
369
|
+
let turns = [], found = !!file;
|
|
370
|
+
if (file) turns = parseTurns(file, parseInt(cmd.n) || 60);
|
|
371
|
+
try { await post(api, '/api/transcript', { token, window: cmd.name || '', turns, found }); } catch (e) {}
|
|
372
|
+
console.log(` 💬 대화내역 ${turns.length}턴 → 웹${found ? '' : ' (트랜스크립트 못 찾음)'}`);
|
|
317
373
|
}
|
|
318
374
|
} catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
|
|
319
375
|
}
|