@cliphijack/santaclaude 1.0.9 → 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 +52 -0
package/package.json
CHANGED
package/santaclaude.js
CHANGED
|
@@ -92,6 +92,49 @@ function cleanOldImages() {
|
|
|
92
92
|
} catch (e) {}
|
|
93
93
|
}
|
|
94
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
|
+
|
|
95
138
|
async function post(api, p, body) {
|
|
96
139
|
const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
97
140
|
return r.json();
|
|
@@ -318,6 +361,15 @@ async function run(conf) {
|
|
|
318
361
|
if (c > 0) args.push('-x', String(Math.max(20, Math.min(400, c))));
|
|
319
362
|
if (rr > 0) args.push('-y', String(Math.max(10, Math.min(200, rr))));
|
|
320
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 ? '' : ' (트랜스크립트 못 찾음)'}`);
|
|
321
373
|
}
|
|
322
374
|
} catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
|
|
323
375
|
}
|