@cliphijack/santaclaude 1.0.9 → 1.0.11

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 +63 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -92,6 +92,60 @@ 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
+ // 파일 꼬리(최근 maxBytes)만 읽음 — 수MB JSONL 통째로 안 열고 끝부분만(첫 잘린 줄 버림)
110
+ function readTail(file, maxBytes) {
111
+ try {
112
+ const size = fs.statSync(file).size; const start = Math.max(0, size - maxBytes); const len = size - start;
113
+ const fd = fs.openSync(file, 'r'); const buf = Buffer.alloc(len); fs.readSync(fd, buf, 0, len, start); fs.closeSync(fd);
114
+ let s = buf.toString('utf8'); if (start > 0) { const nl = s.indexOf('\n'); if (nl >= 0) s = s.slice(nl + 1); } // 중간부터 읽었으면 깨진 첫 줄 버림
115
+ return s.split('\n');
116
+ } catch (e) { return []; }
117
+ }
118
+ // JSONL → user/assistant 턴 (툴콜=칩, 첨부이미지=data URL). 파일 꼬리만 읽고 최근 maxTurns개.
119
+ function parseTurns(file, maxTurns) {
120
+ const out = []; let imgBudget = 8 * 1024 * 1024; // 전체 이미지 base64 합계 상한(모바일 보호)
121
+ const lines = readTail(file, 2 * 1024 * 1024); // 최근 ~2MB만 (통째로 X) — 형 지적: "꼭 다 열어야되나"
122
+ function grabImgs(c) { const out2 = []; if (!Array.isArray(c)) return out2; for (const b of c) { if (b && b.type === 'image' && b.source) { const s = b.source; if (s.type === 'base64' && s.data && imgBudget > s.data.length) { imgBudget -= s.data.length; out2.push('data:' + (s.media_type || 'image/png') + ';base64,' + s.data); } else if (s.type === 'url' && s.url) out2.push(String(s.url)); } } return out2; }
123
+ for (const line of lines) {
124
+ if (!line) continue; let o; try { o = JSON.parse(line); } catch (e) { continue; }
125
+ if (o.type !== 'user' && o.type !== 'assistant') continue;
126
+ const m = o.message; if (!m) continue; const c = m.content;
127
+ if (o.type === 'user') {
128
+ let text = ''; const imgs = grabImgs(c);
129
+ if (typeof c === 'string') text = c;
130
+ else if (Array.isArray(c)) { const t = c.filter((b) => b && b.type === 'text').map((b) => b.text); if (!t.length && !imgs.length) continue; text = t.join('\n'); } // 텍스트도 이미지도 없으면(tool_result만) 스킵
131
+ text = String(text || '').trim();
132
+ if (text && /^<(command-name|local-command|command-message|command-args)/.test(text)) continue; // 슬래시명령 메타 스킵
133
+ if (!text && !imgs.length) continue;
134
+ out.push({ role: 'user', text: text.slice(0, 8000), imgs });
135
+ } else {
136
+ let text = '', tools = []; const imgs = grabImgs(c);
137
+ if (typeof c === 'string') text = c;
138
+ else if (Array.isArray(c)) for (const b of c) {
139
+ if (b && b.type === 'text' && b.text) text += (text ? '\n' : '') + b.text;
140
+ 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) : '')); }
141
+ }
142
+ text = String(text || '').trim(); if (!text && !tools.length && !imgs.length) continue;
143
+ out.push({ role: 'assistant', text: text.slice(0, 8000), tools, imgs });
144
+ }
145
+ }
146
+ return out.slice(-Math.max(10, Math.min(200, maxTurns || 60)));
147
+ }
148
+
95
149
  async function post(api, p, body) {
96
150
  const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
97
151
  return r.json();
@@ -318,6 +372,15 @@ async function run(conf) {
318
372
  if (c > 0) args.push('-x', String(Math.max(20, Math.min(400, c))));
319
373
  if (rr > 0) args.push('-y', String(Math.max(10, Math.min(200, rr))));
320
374
  if (args.length > 3) { try { execFileSync('tmux', args); console.log(` 📐 리사이즈 ${w} → 폭${c || '그대로'}${rr ? '×높이' + rr : ''}`); } catch (e) {} }
375
+ } else if (cmd.action === 'transcript') {
376
+ // 💬 그 루돌프의 claude 대화 전체내역(JSONL) 읽어 웹에 전달 — 우리 서버엔 안 쌓음(TTL 릴레이)
377
+ const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
378
+ 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) {}
379
+ const file = findTranscript(cwd);
380
+ let turns = [], found = !!file;
381
+ if (file) turns = parseTurns(file, parseInt(cmd.n) || 60);
382
+ try { await post(api, '/api/transcript', { token, window: cmd.name || '', turns, found }); } catch (e) {}
383
+ console.log(` 💬 대화내역 ${turns.length}턴 → 웹${found ? '' : ' (트랜스크립트 못 찾음)'}`);
321
384
  }
322
385
  } catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
323
386
  }