@cliphijack/santaclaude 0.5.0 → 0.6.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 +31 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -45,9 +45,23 @@ 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
  }
53
67
 
@@ -77,43 +91,40 @@ async function run(conf) {
77
91
  console.warn(` ⚠️ tmux "${session}" 세션 없음 (--no-spawn) — 직접 띄워줘.`);
78
92
  }
79
93
 
94
+ // 루돌프 = 작업장(session) 안의 윈도우(탭). 별도 세션 아님.
80
95
  function runControl(cmd) {
81
96
  try {
82
97
  if (cmd.action === 'create') {
83
- if (paneExists(cmd.name)) { console.log(` 🦌 "${cmd.name}" 이미 있어 새로 안 띄움`); return; }
98
+ if (!paneExists(session)) spawnClaude(session, claudeCmd); // 작업장 세션 없으면 먼저 띄움
99
+ if (windowExists(session, cmd.name)) { console.log(` 🦌 "${cmd.name}" 탭 이미 있어 — 안 만듦`); return; }
84
100
  let cwd = cmd.cwd ? String(cmd.cwd).trim() : '';
85
101
  if (cwd) {
86
102
  if (cwd === '~' || cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(1)); // ~ 확장
87
103
  try { fs.mkdirSync(cwd, { recursive: true }); } // 폴더 없으면 새로 생성
88
104
  catch (e) { console.warn(` ⚠️ 폴더 생성 실패(${cwd}): ${e.message} — 홈에서 띄움`); cwd = ''; }
89
105
  }
90
- spawnClaude(cmd.name, claudeCmd, cwd || undefined);
91
- console.log(` 🦌 새 루돌프 "${cmd.name}"${cwd ? ' @' + cwd + ' (폴더 준비됨)' : ''} 띄움 (tmux attach -t ${cmd.name} 로그인 1회)`);
106
+ spawnWindow(session, cmd.name, claudeCmd, cwd || undefined);
107
+ console.log(` 🦌 새 루돌프 "${session}:${cmd.name}"${cwd ? ' @' + cwd + ' (폴더 준비됨)' : ''} 추가 (tmux attach -t ${session} Ctrl+b 창번호로 이동, 첫 로그인 1회)`);
92
108
  } else if (cmd.action === 'kill') {
93
- execFileSync('tmux', ['kill-session', '-t', cmd.name]);
94
- console.log(` 💤 루돌프 "${cmd.name}" 닫음`);
109
+ execFileSync('tmux', ['kill-window', '-t', session + ':' + cmd.name]);
110
+ console.log(` 💤 루돌프 "${session}:${cmd.name}" 닫음`);
95
111
  }
96
112
  } catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
97
113
  }
98
114
 
99
115
  async function tick() {
100
116
  try {
101
- post(api, '/api/heartbeat', { token, pane, sessions: listSessions() }).catch(() => {});
117
+ post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session) }).catch(() => {}); // 보고 = 작업장 안 윈도우(루돌프)들
102
118
  post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
103
119
  const d = await post(api, '/api/jobs/claim', { token });
104
120
  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; }
121
+ if (!paneExists(session)) { if (!warned) { console.warn(` ⚠️ 작업장 "${session}" 없음 보류`); warned = true; } continue; }
114
122
  warned = false;
115
- console.log(`[발사] ${new Date().toISOString()}${tpane}: ${String(j.message).slice(0, 60)}`);
116
- inject(tpane, '[SantaClaude] ' + j.message);
123
+ // 대상 윈도우 지정 session:target, 없으면 기본 pane(활성창)
124
+ const tname = j.target && String(j.target).trim();
125
+ const tgt = (tname && windowExists(session, tname)) ? (session + ':' + tname) : pane;
126
+ console.log(`[발사] ${new Date().toISOString()} → ${tgt}: ${String(j.message).slice(0, 60)}`);
127
+ try { inject(tgt, '[SantaClaude] ' + j.message); } catch (e) { console.warn(` 주입 실패(${tgt}): ${e.message}`); }
117
128
  }
118
129
  } catch (e) { console.error('[폴링 오류]', e.message); }
119
130
  }