@cliphijack/santaclaude 0.5.0 → 0.7.0

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 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -45,11 +45,33 @@ function spawnClaude(session, cmd, cwd) {
45
45
  try { execFileSync('sh', ['-c', 'sleep 0.3']); } catch (e) {}
46
46
  execFileSync('tmux', ['send-keys', '-t', session, 'Enter']);
47
47
  }
48
- // 살아있는 tmux 세션 목록
49
- function listSessions() {
50
- try { return execFileSync('tmux', ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' }).trim().split('\n').filter(Boolean); }
48
+ // 세션(작업장) 안에 새 윈도우(탭)로 claude 실행 — 프로젝트별 분리
49
+ function spawnWindow(session, name, cmd, cwd) {
50
+ const a = ['new-window', '-t', session, '-n', name];
51
+ if (cwd) a.push('-c', cwd);
52
+ execFileSync('tmux', a);
53
+ try { execFileSync('sh', ['-c', 'sleep 1']); } catch (e) {}
54
+ execFileSync('tmux', ['send-keys', '-t', session + ':' + name, '-l', cmd]);
55
+ try { execFileSync('sh', ['-c', 'sleep 0.3']); } catch (e) {}
56
+ execFileSync('tmux', ['send-keys', '-t', session + ':' + name, 'Enter']);
57
+ }
58
+ function windowExists(session, name) {
59
+ try { return execFileSync('tmux', ['list-windows', '-t', session, '-F', '#{window_name}'], { encoding: 'utf8' }).trim().split('\n').indexOf(name) >= 0; }
60
+ catch (e) { return false; }
61
+ }
62
+ // 작업장(세션) 안의 윈도우(루돌프) 목록
63
+ function listWindows(session) {
64
+ try { return execFileSync('tmux', ['list-windows', '-t', session, '-F', '#{window_name}'], { encoding: 'utf8' }).trim().split('\n').filter(Boolean); }
51
65
  catch (e) { return []; }
52
66
  }
67
+ // 각 루돌프(탭) 화면 캡처 — 웹 미러용 (마지막 ~30줄)
68
+ function captureWindow(session, name) {
69
+ try { return execFileSync('tmux', ['capture-pane', '-t', session + ':' + name, '-p', '-S', '-30'], { encoding: 'utf8' }).replace(/\n+$/, '').slice(-4000); }
70
+ catch (e) { return ''; }
71
+ }
72
+ function captureAll(session) {
73
+ const o = {}; for (const w of listWindows(session)) o[w] = captureWindow(session, w); return o;
74
+ }
53
75
 
54
76
  async function post(api, p, body) {
55
77
  const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
@@ -77,43 +99,40 @@ async function run(conf) {
77
99
  console.warn(` ⚠️ tmux "${session}" 세션 없음 (--no-spawn) — 직접 띄워줘.`);
78
100
  }
79
101
 
102
+ // 루돌프 = 작업장(session) 안의 윈도우(탭). 별도 세션 아님.
80
103
  function runControl(cmd) {
81
104
  try {
82
105
  if (cmd.action === 'create') {
83
- if (paneExists(cmd.name)) { console.log(` 🦌 "${cmd.name}" 이미 있어 새로 안 띄움`); return; }
106
+ if (!paneExists(session)) spawnClaude(session, claudeCmd); // 작업장 세션 없으면 먼저 띄움
107
+ if (windowExists(session, cmd.name)) { console.log(` 🦌 "${cmd.name}" 탭 이미 있어 — 안 만듦`); return; }
84
108
  let cwd = cmd.cwd ? String(cmd.cwd).trim() : '';
85
109
  if (cwd) {
86
110
  if (cwd === '~' || cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(1)); // ~ 확장
87
111
  try { fs.mkdirSync(cwd, { recursive: true }); } // 폴더 없으면 새로 생성
88
112
  catch (e) { console.warn(` ⚠️ 폴더 생성 실패(${cwd}): ${e.message} — 홈에서 띄움`); cwd = ''; }
89
113
  }
90
- spawnClaude(cmd.name, claudeCmd, cwd || undefined);
91
- console.log(` 🦌 새 루돌프 "${cmd.name}"${cwd ? ' @' + cwd + ' (폴더 준비됨)' : ''} 띄움 (tmux attach -t ${cmd.name} 로그인 1회)`);
114
+ spawnWindow(session, cmd.name, claudeCmd, cwd || undefined);
115
+ console.log(` 🦌 새 루돌프 "${session}:${cmd.name}"${cwd ? ' @' + cwd + ' (폴더 준비됨)' : ''} 추가 (tmux attach -t ${session} Ctrl+b 창번호로 이동, 첫 로그인 1회)`);
92
116
  } else if (cmd.action === 'kill') {
93
- execFileSync('tmux', ['kill-session', '-t', cmd.name]);
94
- console.log(` 💤 루돌프 "${cmd.name}" 닫음`);
117
+ execFileSync('tmux', ['kill-window', '-t', session + ':' + cmd.name]);
118
+ console.log(` 💤 루돌프 "${session}:${cmd.name}" 닫음`);
95
119
  }
96
120
  } catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
97
121
  }
98
122
 
99
123
  async function tick() {
100
124
  try {
101
- post(api, '/api/heartbeat', { token, pane, sessions: listSessions() }).catch(() => {});
125
+ post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session) }).catch(() => {}); // 보고 = 윈도우 목록 + 각 탭 화면 미러
102
126
  post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
103
127
  const d = await post(api, '/api/jobs/claim', { token });
104
128
  for (const j of (d.jobs || [])) {
105
- // 잡의 대상 세션 우선, 없으면 기본 pane. 대상 세션이 없으면 기본 pane으로 폴백
106
- let tpane = (j.target && String(j.target).trim()) || pane;
107
- if (!paneExists(tpane)) {
108
- if (conf.spawn !== false && tpane !== pane) {
109
- try { spawnClaude(String(tpane).split(':')[0], claudeCmd); console.log(` 🦌 대상 "${tpane}" 세션이 없어서 새로 띄웠어 (tmux attach 로 로그인 1회).`); }
110
- catch (e) { tpane = pane; }
111
- } else if (!paneExists(tpane)) { tpane = pane; }
112
- }
113
- if (!paneExists(tpane)) { if (!warned) { console.warn(` ⚠️ 주입할 세션 "${tpane}" 없음 — 보류`); warned = true; } continue; }
129
+ if (!paneExists(session)) { if (!warned) { console.warn(` ⚠️ 작업장 "${session}" 없음 보류`); warned = true; } continue; }
114
130
  warned = false;
115
- console.log(`[발사] ${new Date().toISOString()}${tpane}: ${String(j.message).slice(0, 60)}`);
116
- inject(tpane, '[SantaClaude] ' + j.message);
131
+ // 대상 윈도우 지정 session:target, 없으면 기본 pane(활성창)
132
+ const tname = j.target && String(j.target).trim();
133
+ const tgt = (tname && windowExists(session, tname)) ? (session + ':' + tname) : pane;
134
+ console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
135
+ try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
117
136
  }
118
137
  } catch (e) { console.error('[폴링 오류]', e.message); }
119
138
  }