@cliphijack/santaclaude 1.0.10 → 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 +22 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "1.0.10",
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
@@ -106,30 +106,41 @@ function findTranscript(cwd) {
106
106
  return files[0]; // 가장 최근 수정 = 활성 세션
107
107
  } catch (e) { return null; }
108
108
  }
109
- // JSONL user/assistant (툴콜은 칩으로 요약). 최근 maxTurns개.
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개.
110
119
  function parseTurns(file, maxTurns) {
111
- const out = [];
112
- let lines; try { lines = fs.readFileSync(file, 'utf8').split('\n'); } catch (e) { return out; }
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; }
113
123
  for (const line of lines) {
114
124
  if (!line) continue; let o; try { o = JSON.parse(line); } catch (e) { continue; }
115
125
  if (o.type !== 'user' && o.type !== 'assistant') continue;
116
126
  const m = o.message; if (!m) continue; const c = m.content;
117
127
  if (o.type === 'user') {
118
- let text = '';
128
+ let text = ''; const imgs = grabImgs(c);
119
129
  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) });
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 });
124
135
  } else {
125
- let text = '', tools = [];
136
+ let text = '', tools = []; const imgs = grabImgs(c);
126
137
  if (typeof c === 'string') text = c;
127
138
  else if (Array.isArray(c)) for (const b of c) {
128
139
  if (b && b.type === 'text' && b.text) text += (text ? '\n' : '') + b.text;
129
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) : '')); }
130
141
  }
131
- text = String(text || '').trim(); if (!text && !tools.length) continue;
132
- out.push({ role: 'assistant', text: text.slice(0, 8000), tools });
142
+ text = String(text || '').trim(); if (!text && !tools.length && !imgs.length) continue;
143
+ out.push({ role: 'assistant', text: text.slice(0, 8000), tools, imgs });
133
144
  }
134
145
  }
135
146
  return out.slice(-Math.max(10, Math.min(200, maxTurns || 60)));