@cliphijack/santaclaude 0.9.1 → 0.9.3

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 +39 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -37,7 +37,7 @@ function inject(pane, message) {
37
37
  }
38
38
  // tmux 세션 새로 만들고 그 안에 claude 자동 실행 (cwd 지정 시 그 폴더에서 = 프로젝트별 분리)
39
39
  function spawnClaude(session, cmd, cwd) {
40
- const a = ['new-session', '-d', '-s', session];
40
+ const a = ['new-session', '-d', '-s', session, '-x', '220', '-y', '50']; // 크게 — 더 많은 줄 보이게
41
41
  if (cwd) a.push('-c', cwd);
42
42
  execFileSync('tmux', a);
43
43
  try { execFileSync('sh', ['-c', 'sleep 1']); } catch (e) {} // 셸 초기화 대기
@@ -47,6 +47,7 @@ function spawnClaude(session, cmd, cwd) {
47
47
  }
48
48
  // 한 세션(작업장) 안에 새 윈도우(탭)로 claude 실행 — 프로젝트별 분리
49
49
  function spawnWindow(session, name, cmd, cwd) {
50
+ try { execFileSync('tmux', ['resize-window', '-t', session, '-x', '220', '-y', '50']); } catch (e) {} // 세션 크게
50
51
  const a = ['new-window', '-t', session, '-n', name];
51
52
  if (cwd) a.push('-c', cwd);
52
53
  execFileSync('tmux', a);
@@ -67,12 +68,20 @@ function listWindows(session) {
67
68
  // 각 루돌프(탭) 화면 캡처 — 웹 미러용 (마지막 ~30줄)
68
69
  function captureWindow(session, name) {
69
70
  // -e: ANSI 색상 보존(웹에서 터미널 색 재현), -S -40: 최근 40줄
70
- try { return execFileSync('tmux', ['capture-pane', '-t', session + ':' + name, '-p', '-e', '-S', '-200'], { encoding: 'utf8' }).replace(/\n+$/, '').slice(-16000); }
71
+ try { return execFileSync('tmux', ['capture-pane', '-t', session + ':' + name, '-p', '-e', '-S', '-500'], { encoding: 'utf8' }).replace(/\n+$/, '').slice(-60000); }
71
72
  catch (e) { return ''; }
72
73
  }
73
74
  function captureAll(session) {
74
75
  const o = {}; for (const w of listWindows(session)) o[w] = captureWindow(session, w); return o;
75
76
  }
77
+ // 받은 이미지 24시간 지난 건 로컬에서 청소
78
+ function cleanOldImages() {
79
+ try {
80
+ const dir = path.join(os.homedir(), '.santaclaude', 'img'); if (!fs.existsSync(dir)) return;
81
+ const now = Date.now();
82
+ for (const f of fs.readdirSync(dir)) { const fp = path.join(dir, f); try { if (now - fs.statSync(fp).mtimeMs > 86400000) fs.unlinkSync(fp); } catch (e) {} }
83
+ } catch (e) {}
84
+ }
76
85
 
77
86
  async function post(api, p, body) {
78
87
  const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
@@ -87,8 +96,10 @@ async function run(conf) {
87
96
  const claudeCmd = conf.claudeCmd || 'claude --dangerously-skip-permissions';
88
97
  let warned = false; // 중복방지는 서버가 책임 (claim 시 next_fire 원자적 전진) — 클라 영구셋은 반복예약을 영구차단하므로 안 둠
89
98
 
99
+ cleanOldImages();
90
100
  console.log(`🛷 SantaClaude 커넥터 가동 — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
91
101
  if (paneExists(pane)) {
102
+ try { execFileSync('tmux', ['resize-window', '-t', session, '-x', '220', '-y', '50']); } catch (e) {} // 기존 세션도 크게(더 많은 줄)
92
103
  console.log(` ✅ tmux "${session}" 세션에 붙음 (기존 claude에 주입).`);
93
104
  } else if (conf.spawn !== false) {
94
105
  console.log(` 🦌 "${session}" 세션이 없어서 claude를 새로 띄울게…`);
@@ -101,8 +112,33 @@ async function run(conf) {
101
112
  }
102
113
 
103
114
  // 루돌프 = 작업장(session) 안의 윈도우(탭). 별도 세션 아님.
104
- function runControl(cmd) {
115
+ async function runControl(cmd) {
105
116
  try {
117
+ if (cmd.action === 'image') {
118
+ // 웹이 올린 이미지를 받아 로컬에 저장하고 claude에 경로 주입
119
+ const dir = path.join(os.homedir(), '.santaclaude', 'img');
120
+ try { fs.mkdirSync(dir, { recursive: true }); } catch (e) {}
121
+ const paths = [];
122
+ for (const im of (cmd.imgs || [])) {
123
+ try {
124
+ const r = await fetch(api + '/api/img?token=' + encodeURIComponent(token) + '&id=' + encodeURIComponent(im.id));
125
+ if (!r.ok) continue;
126
+ const j = await r.json();
127
+ const ext = String(j.mime || '').includes('png') ? 'png' : String(j.mime || '').includes('webp') ? 'webp' : 'jpg';
128
+ const fp = path.join(dir, Date.now() + '_' + paths.length + '.' + ext);
129
+ fs.writeFileSync(fp, Buffer.from(j.data, 'base64'));
130
+ paths.push(fp);
131
+ } catch (e) {}
132
+ }
133
+ if (paths.length) {
134
+ const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
135
+ const msg = (cmd.note ? cmd.note + ' ' : '') + '첨부 이미지 ' + paths.length + '장 봐줘: ' + paths.join(' ');
136
+ execFileSync('tmux', ['send-keys', '-t', w, '-l', msg]);
137
+ setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
138
+ console.log(` 🖼️ 이미지 ${paths.length}장 → ${w}`);
139
+ }
140
+ return;
141
+ }
106
142
  if (cmd.action === 'create') {
107
143
  if (!paneExists(session)) spawnClaude(session, claudeCmd); // 작업장 세션 없으면 먼저 띄움
108
144
  if (windowExists(session, cmd.name)) { console.log(` 🦌 "${cmd.name}" 탭 이미 있어 — 안 만듦`); return; }