@cliphijack/santaclaude 0.1.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.
- package/package.json +10 -0
- package/santaclaude.js +96 -0
package/package.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cliphijack/santaclaude",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
|
|
6
|
+
"bin": { "santaclaude": "./santaclaude.js" },
|
|
7
|
+
"engines": { "node": ">=18" },
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"files": ["santaclaude.js"]
|
|
10
|
+
}
|
package/santaclaude.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux pane)에 발사.
|
|
4
|
+
* 의존성 0. node 18+ (global fetch).
|
|
5
|
+
*
|
|
6
|
+
* santaclaude connect --token <TOKEN> --pane <tmux:pane> [--api <url>] [--every <sec>]
|
|
7
|
+
* 토큰·pane을 ~/.santaclaude.json 에 저장하고 데몬 시작
|
|
8
|
+
* santaclaude 저장된 설정으로 데몬 시작
|
|
9
|
+
* santaclaude status 설정 + 클라우드 연결 확인
|
|
10
|
+
*
|
|
11
|
+
* 동작: --every초마다 /api/jobs/claim 폴링 → due 잡을 tmux send-keys로 주입(메시지+Enter 분리).
|
|
12
|
+
* 매 폴링 /api/heartbeat 로 살아있음 보고(대시보드 🦌 깨어있음 표시용).
|
|
13
|
+
*/
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const CONF = path.join(os.homedir(), '.santaclaude.json');
|
|
20
|
+
const DEFAULT_API = 'https://santaclaude.app';
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const o = {}; for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (a.startsWith('--')) { const k = a.slice(2); const v = (argv[i + 1] && !argv[i + 1].startsWith('--')) ? argv[++i] : true; o[k] = v; }
|
|
26
|
+
else o._ = (o._ || []).concat(a);
|
|
27
|
+
} return o;
|
|
28
|
+
}
|
|
29
|
+
function loadConf() { try { return JSON.parse(fs.readFileSync(CONF, 'utf8')); } catch { return {}; } }
|
|
30
|
+
function saveConf(c) { fs.writeFileSync(CONF, JSON.stringify(c, null, 2)); }
|
|
31
|
+
function paneExists(pane) { try { execFileSync('tmux', ['has-session', '-t', pane.split(':')[0]], { stdio: 'ignore' }); return true; } catch { return false; } }
|
|
32
|
+
|
|
33
|
+
function inject(pane, message) {
|
|
34
|
+
const oneLine = String(message).replace(/[\r\n]+/g, ' ');
|
|
35
|
+
execFileSync('tmux', ['send-keys', '-t', pane, '-l', oneLine]);
|
|
36
|
+
setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', pane, 'Enter']); } catch (e) {} }, 350);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function post(api, p, body) {
|
|
40
|
+
const r = await fetch(api + p, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
41
|
+
return r.json();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function run(conf) {
|
|
45
|
+
const { token, pane, api = DEFAULT_API } = conf;
|
|
46
|
+
const every = Math.max(5, Number(conf.every) || 20) * 1000;
|
|
47
|
+
if (!token || !pane) { console.error('설정 없음. 먼저: santaclaude connect --token <T> --pane <pane>'); process.exit(1); }
|
|
48
|
+
let warned = false; // 중복방지는 서버가 책임 (claim 시 next_fire 원자적 전진) — 클라 영구셋은 반복예약을 영구차단하므로 안 둠
|
|
49
|
+
|
|
50
|
+
console.log(`🛷 SantaClaude 커넥터 가동 — pane=${pane} · ${every / 1000}s 폴링 · ${api}`);
|
|
51
|
+
if (!paneExists(pane)) console.warn(` ⚠️ tmux pane "${pane}" 안 보임 — 떠 있는지 확인 (그래도 폴링은 계속)`);
|
|
52
|
+
|
|
53
|
+
async function tick() {
|
|
54
|
+
try {
|
|
55
|
+
post(api, '/api/heartbeat', { token, pane }).catch(() => {});
|
|
56
|
+
const d = await post(api, '/api/jobs/claim', { token });
|
|
57
|
+
for (const j of (d.jobs || [])) {
|
|
58
|
+
if (!paneExists(pane)) { if (!warned) { console.warn(` ⚠️ pane "${pane}" 없음 — 주입 보류`); warned = true; } continue; }
|
|
59
|
+
warned = false;
|
|
60
|
+
console.log(`[발사] ${new Date().toISOString()} → ${pane}: ${String(j.message).slice(0, 60)}`);
|
|
61
|
+
inject(pane, '[SantaClaude] ' + j.message);
|
|
62
|
+
}
|
|
63
|
+
} catch (e) { console.error('[폴링 오류]', e.message); }
|
|
64
|
+
}
|
|
65
|
+
await tick();
|
|
66
|
+
const iv = setInterval(tick, every);
|
|
67
|
+
process.on('SIGINT', () => { clearInterval(iv); console.log('\n🛷 커넥터 종료. 너의 루돌프들은 자러 간다.'); process.exit(0); });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
const a = parseArgs(process.argv.slice(2));
|
|
72
|
+
const cmd = (a._ && a._[0]) || 'run';
|
|
73
|
+
|
|
74
|
+
if (cmd === 'connect') {
|
|
75
|
+
const token = a.token, pane = a.pane;
|
|
76
|
+
if (!token || !pane) { console.error('사용: santaclaude connect --token <TOKEN> --pane <tmux:pane>'); process.exit(1); }
|
|
77
|
+
const conf = { token, pane, api: a.api || DEFAULT_API, every: a.every ? Number(a.every) : 20 };
|
|
78
|
+
saveConf(conf);
|
|
79
|
+
console.log(`✅ 페어링 저장 → ${CONF}`);
|
|
80
|
+
return run(conf);
|
|
81
|
+
}
|
|
82
|
+
if (cmd === 'status') {
|
|
83
|
+
const c = loadConf();
|
|
84
|
+
if (!c.token) { console.log('❌ 페어링 안 됨. santaclaude connect 먼저.'); return; }
|
|
85
|
+
console.log(`설정: pane=${c.pane} · api=${c.api || DEFAULT_API} · ${c.every || 20}s`);
|
|
86
|
+
console.log(`pane 존재: ${paneExists(c.pane) ? '✅' : '❌'}`);
|
|
87
|
+
try { const r = await fetch((c.api || DEFAULT_API) + '/api/ping?token=' + encodeURIComponent(c.token)); const d = await r.json(); console.log(`클라우드: ${d.ok ? '✅ 연결' : '❌'} · 예약 ${d.count ?? '?'}개`); }
|
|
88
|
+
catch (e) { console.log('클라우드: ❌ ' + e.message); }
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// 기본: 저장된 설정으로 실행
|
|
92
|
+
const c = loadConf();
|
|
93
|
+
if (!c.token) { console.error('페어링 필요: santaclaude connect --token <TOKEN> --pane <tmux:pane>'); process.exit(1); }
|
|
94
|
+
return run(c);
|
|
95
|
+
}
|
|
96
|
+
main();
|