@cliphijack/santaclaude 0.6.0 → 0.8.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 +15 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -64,6 +64,14 @@ function listWindows(session) {
64
64
  try { return execFileSync('tmux', ['list-windows', '-t', session, '-F', '#{window_name}'], { encoding: 'utf8' }).trim().split('\n').filter(Boolean); }
65
65
  catch (e) { return []; }
66
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
+ }
67
75
 
68
76
  async function post(api, p, body) {
69
77
  const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
@@ -108,13 +116,19 @@ async function run(conf) {
108
116
  } else if (cmd.action === 'kill') {
109
117
  execFileSync('tmux', ['kill-window', '-t', session + ':' + cmd.name]);
110
118
  console.log(` 💤 루돌프 "${session}:${cmd.name}" 탭 닫음`);
119
+ } else if (cmd.action === 'input') {
120
+ // 웹에서 친 입력을 해당 탭(루돌프)에 주입 — 양방향 웹 터미널
121
+ const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
122
+ execFileSync('tmux', ['send-keys', '-t', w, '-l', String(cmd.text || '')]);
123
+ setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
124
+ console.log(` ⌨️ 입력 → ${w}: ${String(cmd.text || '').slice(0, 50)}`);
111
125
  }
112
126
  } catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
113
127
  }
114
128
 
115
129
  async function tick() {
116
130
  try {
117
- post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session) }).catch(() => {}); // 보고 = 작업장 윈도우(루돌프)들
131
+ post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session) }).catch(() => {}); // 보고 = 윈도우 목록 + 각 탭 화면 미러
118
132
  post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
119
133
  const d = await post(api, '/api/jobs/claim', { token });
120
134
  for (const j of (d.jobs || [])) {