@cliphijack/santaclaude 0.1.0 → 0.3.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 +34 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -35,6 +35,14 @@ function inject(pane, message) {
35
35
  execFileSync('tmux', ['send-keys', '-t', pane, '-l', oneLine]);
36
36
  setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', pane, 'Enter']); } catch (e) {} }, 350);
37
37
  }
38
+ // 세션이 없을 때만 — tmux 세션 새로 만들고 그 안에 claude 자동 실행
39
+ function spawnClaude(session, cmd) {
40
+ execFileSync('tmux', ['new-session', '-d', '-s', session]);
41
+ try { execFileSync('sh', ['-c', 'sleep 1']); } catch (e) {} // 셸 초기화 대기
42
+ execFileSync('tmux', ['send-keys', '-t', session, '-l', cmd]);
43
+ try { execFileSync('sh', ['-c', 'sleep 0.3']); } catch (e) {}
44
+ execFileSync('tmux', ['send-keys', '-t', session, 'Enter']);
45
+ }
38
46
 
39
47
  async function post(api, p, body) {
40
48
  const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
@@ -45,20 +53,40 @@ async function run(conf) {
45
53
  const { token, pane, api = DEFAULT_API } = conf;
46
54
  const every = Math.max(5, Number(conf.every) || 20) * 1000;
47
55
  if (!token || !pane) { console.error('설정 없음. 먼저: santaclaude connect --token <T> --pane <pane>'); process.exit(1); }
56
+ const session = String(pane).split(':')[0];
57
+ const claudeCmd = conf.claudeCmd || 'claude --dangerously-skip-permissions';
48
58
  let warned = false; // 중복방지는 서버가 책임 (claim 시 next_fire 원자적 전진) — 클라 영구셋은 반복예약을 영구차단하므로 안 둠
49
59
 
50
60
  console.log(`🛷 SantaClaude 커넥터 가동 — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
51
- if (!paneExists(pane)) console.warn(` ⚠️ tmux pane "${pane}" 안 보임 — 떠 있는지 확인 (그래도 폴링은 계속)`);
61
+ if (paneExists(pane)) {
62
+ console.log(` ✅ tmux "${session}" 세션에 붙음 (기존 claude에 주입).`);
63
+ } else if (conf.spawn !== false) {
64
+ console.log(` 🦌 "${session}" 세션이 없어서 claude를 새로 띄울게…`);
65
+ try {
66
+ spawnClaude(session, claudeCmd);
67
+ console.log(` ✅ "${session}"에 claude 띄웠어. 처음이면 새 창에서 tmux attach -t ${session} 로 로그인 1회 해줘 (이후엔 자동).`);
68
+ } catch (e) { console.warn(` ⚠️ claude 자동실행 실패(${e.message}). tmux new -s ${session} 로 직접 띄워줘.`); }
69
+ } else {
70
+ console.warn(` ⚠️ tmux "${session}" 세션 없음 (--no-spawn) — 직접 띄워줘.`);
71
+ }
52
72
 
53
73
  async function tick() {
54
74
  try {
55
75
  post(api, '/api/heartbeat', { token, pane }).catch(() => {});
56
76
  const d = await post(api, '/api/jobs/claim', { token });
57
77
  for (const j of (d.jobs || [])) {
58
- if (!paneExists(pane)) { if (!warned) { console.warn(` ⚠️ pane "${pane}" 없음 주입 보류`); warned = true; } continue; }
78
+ // 잡의 대상 세션 우선, 없으면 기본 pane. 대상 세션이 없으면 기본 pane으로 폴백
79
+ let tpane = (j.target && String(j.target).trim()) || pane;
80
+ if (!paneExists(tpane)) {
81
+ if (conf.spawn !== false && tpane !== pane) {
82
+ try { spawnClaude(String(tpane).split(':')[0], claudeCmd); console.log(` 🦌 대상 "${tpane}" 세션이 없어서 새로 띄웠어 (tmux attach 로 로그인 1회).`); }
83
+ catch (e) { tpane = pane; }
84
+ } else if (!paneExists(tpane)) { tpane = pane; }
85
+ }
86
+ if (!paneExists(tpane)) { if (!warned) { console.warn(` ⚠️ 주입할 세션 "${tpane}" 없음 — 보류`); warned = true; } continue; }
59
87
  warned = false;
60
- console.log(`[발사] ${new Date().toISOString()} → ${pane}: ${String(j.message).slice(0, 60)}`);
61
- inject(pane, '[SantaClaude] ' + j.message);
88
+ console.log(`[발사] ${new Date().toISOString()} → ${tpane}: ${String(j.message).slice(0, 60)}`);
89
+ inject(tpane, '[SantaClaude] ' + j.message);
62
90
  }
63
91
  } catch (e) { console.error('[폴링 오류]', e.message); }
64
92
  }
@@ -75,6 +103,8 @@ async function main() {
75
103
  const token = a.token, pane = a.pane;
76
104
  if (!token || !pane) { console.error('사용: santaclaude connect --token <TOKEN> --pane <tmux:pane>'); process.exit(1); }
77
105
  const conf = { token, pane, api: a.api || DEFAULT_API, every: a.every ? Number(a.every) : 20 };
106
+ if (a['no-spawn']) conf.spawn = false;
107
+ if (a['claude-cmd']) conf.claudeCmd = a['claude-cmd'];
78
108
  saveConf(conf);
79
109
  console.log(`✅ 페어링 저장 → ${CONF}`);
80
110
  return run(conf);