@cliphijack/santaclaude 1.0.12 → 1.0.14

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 +18 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -125,38 +125,38 @@ function readDelta(file, from) {
125
125
  return { lines: complete.split('\n'), off: from + Buffer.byteLength(complete, 'utf8') + 1 };
126
126
  } catch (e) { return { lines: [], off: from }; }
127
127
  }
128
- // JSONL 줄들 → user/assistant 턴 (툴콜=칩, 첨부이미지=data URL)
129
- function linesToTurns(lines) {
130
- const out = []; let imgBudget = 8 * 1024 * 1024; // 전체 이미지 base64 합계 상한(모바일 보호)
131
- 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; }
128
+ // JSONL 줄들 → user/assistant 턴 (툴콜=칩, 이미지). withImgs=false면 이미지 데이터 빼고 개수만(imgn) → seed 초경량·즉시
129
+ function linesToTurns(lines, withImgs) {
130
+ const out = []; let imgBudget = 6 * 1024 * 1024; // 이미지 base64 합계 상한(모바일 보호)
131
+ function grabImgs(c) { let urls = [], n = 0; if (!Array.isArray(c)) return { urls, n }; for (const b of c) { if (b && b.type === 'image' && b.source) { n++; if (withImgs) { const s = b.source; if (s.type === 'base64' && s.data && imgBudget > s.data.length) { imgBudget -= s.data.length; urls.push('data:' + (s.media_type || 'image/png') + ';base64,' + s.data); } else if (s.type === 'url' && s.url) urls.push(String(s.url)); } } } return { urls, n }; }
132
132
  for (const line of lines) {
133
133
  if (!line) continue; let o; try { o = JSON.parse(line); } catch (e) { continue; }
134
134
  if (o.type !== 'user' && o.type !== 'assistant') continue;
135
135
  const m = o.message; if (!m) continue; const c = m.content;
136
136
  if (o.type === 'user') {
137
- let text = ''; const imgs = grabImgs(c);
137
+ let text = ''; const gi = grabImgs(c);
138
138
  if (typeof c === 'string') text = c;
139
- 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만) 스킵
139
+ else if (Array.isArray(c)) { const t = c.filter((b) => b && b.type === 'text').map((b) => b.text); if (!t.length && !gi.n) continue; text = t.join('\n'); } // 텍스트도 이미지도 없으면(tool_result만) 스킵
140
140
  text = String(text || '').trim();
141
141
  if (text && /^<(command-name|local-command|command-message|command-args)/.test(text)) continue; // 슬래시명령 메타 스킵
142
- if (!text && !imgs.length) continue;
143
- out.push({ role: 'user', text: text.slice(0, 8000), imgs });
142
+ if (!text && !gi.n) continue;
143
+ out.push({ role: 'user', text: text.slice(0, 8000), imgs: gi.urls, imgn: gi.n });
144
144
  } else {
145
- let text = '', tools = []; const imgs = grabImgs(c);
145
+ let text = '', tools = []; const gi = grabImgs(c);
146
146
  if (typeof c === 'string') text = c;
147
147
  else if (Array.isArray(c)) for (const b of c) {
148
148
  if (b && b.type === 'text' && b.text) text += (text ? '\n' : '') + b.text;
149
149
  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) : '')); }
150
150
  }
151
- text = String(text || '').trim(); if (!text && !tools.length && !imgs.length) continue;
152
- out.push({ role: 'assistant', text: text.slice(0, 8000), tools, imgs });
151
+ text = String(text || '').trim(); if (!text && !tools.length && !gi.n) continue;
152
+ out.push({ role: 'assistant', text: text.slice(0, 8000), tools, imgs: gi.urls, imgn: gi.n });
153
153
  }
154
154
  }
155
155
  return out;
156
156
  }
157
- // 파일 꼬리(최근 2MB)만 읽어 최근 maxTurns개 (seed)
157
+ // 파일 꼬리(최근 2MB)만 읽어 최근 maxTurns개 (seed=텍스트만, 이미지 데이터 X → 초경량 즉시표시)
158
158
  function parseTurns(file, maxTurns) {
159
- return linesToTurns(readTail(file, 2 * 1024 * 1024)).slice(-Math.max(10, Math.min(200, maxTurns || 60)));
159
+ return linesToTurns(readTail(file, 2 * 1024 * 1024), false).slice(-Math.max(10, Math.min(200, maxTurns || 60)));
160
160
  }
161
161
 
162
162
  async function post(api, p, body) {
@@ -377,15 +377,15 @@ async function run(conf) {
377
377
  execFileSync('tmux', ['send-keys', '-t', w, String(cmd.key || '')]);
378
378
  console.log(` ⌨️ 키 → ${w}: ${cmd.key}`);
379
379
  } else if (cmd.action === 'resize') {
380
- // 뷰어 폭에 맞춰 tmux 윈도우 리사이즈 claude TUI가 그 폭으로 reflow(모바일 가로스크롤 해소)
381
- // 보통 cols만 옴 = 폭만 맞추고 높이는 그대로(스크롤백으로 세로 내역 풍부하게). rows 오면 같이 적용.
382
- if (clientsAttached(session)) { console.log(' 📐 리사이즈 건너뜀 — 사람이 tmux에 붙어있음(고객 화면 보호)'); return; } // 사람이 직접 보고있으면 reflow로 화면 망가뜨리지 않음
380
+ // 📐 창에맞춤 토글: fit=강제 reflow(사람 붙어있어도) / restore=window-size latest로 원복( 터미널 따라가기)
383
381
  const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
382
+ if (cmd.restore) { try { execFileSync('tmux', ['set-window-option', '-t', w, 'window-size', 'latest']); console.log(' 📐 창에맞춤 OFF → 터미널 크기로 복원(window-size latest)'); } catch (e) {} return; }
383
+ if (!cmd.fit && clientsAttached(session)) { console.log(' 📐 리사이즈 스킵(사람 attach + 창맞춤 off=보호)'); return; } // fit OFF + 사람 붙음 = 보호
384
384
  const c = parseInt(cmd.cols) || 0, rr = parseInt(cmd.rows) || 0;
385
385
  const args = ['resize-window', '-t', w];
386
386
  if (c > 0) args.push('-x', String(Math.max(20, Math.min(400, c))));
387
387
  if (rr > 0) args.push('-y', String(Math.max(10, Math.min(200, rr))));
388
- if (args.length > 3) { try { execFileSync('tmux', args); console.log(` 📐 리사이즈 ${w} → 폭${c || '그대로'}${rr ? '×높이' + rr : ''}`); } catch (e) {} }
388
+ if (args.length > 3) { try { execFileSync('tmux', args); console.log(` 📐 리사이즈 ${w} → 폭${c || '그대로'}${rr ? '×' + rr : ''}${cmd.fit ? ' (창맞춤)' : ''}`); } catch (e) {} }
389
389
  } else if (cmd.action === 'transcript') {
390
390
  if (cmd.live) { txState.until = Date.now() + 45000; return; } // keepalive — 라이브 모드만 연장(seed 안 함, 초경량)
391
391
  // 💬 seed: 전체내역(JSONL 꼬리) 읽어 웹에 전달 + 라이브 모드 무장(이후 델타는 화면틱에 묻어감)
@@ -502,7 +502,7 @@ async function run(conf) {
502
502
  if (!ws || ws.readyState !== 1) return; if (Date.now() > txState.until || !txState.file) return;
503
503
  let d; try { d = readDelta(txState.file, txState.off); } catch (e) { return; }
504
504
  if (!d.lines.length) return; txState.off = d.off;
505
- const turns = linesToTurns(d.lines); if (!turns.length) return;
505
+ const turns = linesToTurns(d.lines, true); if (!turns.length) return; // 델타는 새 턴 1~2개라 이미지 포함 OK(가벼움)
506
506
  try { ws.send(JSON.stringify({ type: 'txdelta', window: txState.win, turns })); console.log(` 💬+ 라이브 델타 ${turns.length}턴`); } catch (e) {}
507
507
  }
508
508