@cliphijack/santaclaude 0.9.4 → 0.9.6

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 +47 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliphijack/santaclaude",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "SantaClaude 커넥터 — 클라우드 예약을 내 로컬 Claude(tmux)에 발사",
6
6
  "bin": { "santaclaude": "./santaclaude.js" },
package/santaclaude.js CHANGED
@@ -123,7 +123,10 @@ async function run(conf) {
123
123
  // 그 창의 작업 폴더 (없으면 홈)
124
124
  let cwd = os.homedir();
125
125
  try { const pc = execFileSync('tmux', ['display-message', '-t', w, '-p', '#{pane_current_path}'], { encoding: 'utf8' }).trim(); if (pc) cwd = pc; } catch (e) {}
126
- const sdir = path.join(cwd, '.claude', 'skills', sk.name);
126
+ // 폴더 네임스페이스 santa-{퍼블리셔}-{스킬}: 충돌 방지 + 마켓 출처 식별. SKILL.md 파일명은 표준 고정
127
+ const pub = String(sk.publisher || 'me').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase() || 'me';
128
+ const folder = ('santa-' + pub + '-' + sk.name).replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 80);
129
+ const sdir = path.join(cwd, '.claude', 'skills', folder);
127
130
  let cnt = 0;
128
131
  for (const f of (sk.files || [])) {
129
132
  const safe = String(f.path || '').replace(/\\/g, '/').replace(/(^|\/)\.\.(\/|$)/g, '/').replace(/^\/+/, '');
@@ -133,7 +136,7 @@ async function run(conf) {
133
136
  try { fs.mkdirSync(path.dirname(fp), { recursive: true }); fs.writeFileSync(fp, String(f.content || '')); cnt++; } catch (e) {}
134
137
  }
135
138
  console.log(` 🧩 스킬 "${sk.name}" ${cnt}개 파일 이식 → ${sdir}`);
136
- const msg = sk.prompt && sk.prompt.trim() ? sk.prompt.trim() : ('스킬 "' + sk.name + '"이 .claude/skills/' + sk.name + '/ 에 이식됐어. SKILL.md 읽고 적용해줘.');
139
+ const msg = (sk.prompt && sk.prompt.trim() ? sk.prompt.trim() + ' ' : '') + '(스킬 "' + sk.name + '"이 .claude/skills/' + folder + '/ 에 이식됨 SKILL.md 읽고 적용)';
137
140
  execFileSync('tmux', ['send-keys', '-t', w, '-l', msg]);
138
141
  setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
139
142
  return;
@@ -183,13 +186,55 @@ async function run(conf) {
183
186
  execFileSync('tmux', ['send-keys', '-t', w, '-l', String(cmd.text || '')]);
184
187
  setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', w, 'Enter']); } catch (e) {} }, 300);
185
188
  console.log(` ⌨️ 입력 → ${w}: ${String(cmd.text || '').slice(0, 50)}`);
189
+ } else if (cmd.action === 'key') {
190
+ // 플로팅 컨트롤 — 특수키 그대로 전달 (Escape, C-c, Up 등)
191
+ const w = (cmd.name && windowExists(session, cmd.name)) ? (session + ':' + cmd.name) : pane;
192
+ execFileSync('tmux', ['send-keys', '-t', w, String(cmd.key || '')]);
193
+ console.log(` ⌨️ 키 → ${w}: ${cmd.key}`);
186
194
  }
187
195
  } catch (e) { console.warn(` ⚠️ 제어 실패(${cmd.action} ${cmd.name}): ${e.message}`); }
188
196
  }
189
197
 
198
+ // 스킬 클린등록 수거 — claude가 ~/.santaclaude/publish/<name>/ + READY 만들면 마켓에 자동 등록
199
+ const PUBDIR = path.join(os.homedir(), '.santaclaude', 'publish');
200
+ function readDirFiles(dir, base) {
201
+ let out = [];
202
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
203
+ if (e.name === 'READY') continue;
204
+ const fp = path.join(dir, e.name);
205
+ if (e.isDirectory()) out = out.concat(readDirFiles(fp, base));
206
+ else { try { out.push({ path: path.relative(base, fp).replace(/\\/g, '/'), content: fs.readFileSync(fp, 'utf8') }); } catch (e) {} }
207
+ }
208
+ return out;
209
+ }
210
+ async function scanPublish() {
211
+ try {
212
+ if (!fs.existsSync(PUBDIR)) return;
213
+ for (const name of fs.readdirSync(PUBDIR)) {
214
+ const d = path.join(PUBDIR, name);
215
+ try { if (!fs.statSync(d).isDirectory()) continue; } catch (e) { continue; }
216
+ if (!fs.existsSync(path.join(d, 'READY'))) continue; // claude가 다 끝냈다는 신호
217
+ const files = readDirFiles(d, d);
218
+ if (!files.length) { try { fs.rmSync(d, { recursive: true, force: true }); } catch (e) {} continue; }
219
+ const publisher = conf.publisher || (String(token).replace(/^sc_/, '').slice(0, 8)) || 'me';
220
+ try {
221
+ const r = await fetch(api + '/api/skill', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, name, publisher, files, public: true }) });
222
+ const j = await r.json();
223
+ if (j.ok) {
224
+ console.log(` 🏪 스킬 "${name}" 마켓 클린등록 완료`);
225
+ try { fs.rmSync(d, { recursive: true, force: true }); } catch (e) {}
226
+ execFileSync('tmux', ['send-keys', '-t', pane, '-l', '🏪 "' + name + '" 마켓에 클린 등록됐어!']);
227
+ setTimeout(() => { try { execFileSync('tmux', ['send-keys', '-t', pane, 'Enter']); } catch (e) {} }, 300);
228
+ } else { console.warn(` 스킬 등록 실패(${name}): ${j.error}`); try { fs.unlinkSync(path.join(d, 'READY')); } catch (e) {} }
229
+ } catch (e) {}
230
+ }
231
+ } catch (e) {}
232
+ }
233
+
190
234
  async function tick() {
191
235
  try {
192
236
  post(api, '/api/heartbeat', { token, pane, sessions: listWindows(session), screens: captureAll(session) }).catch(() => {}); // 보고 = 윈도우 목록 + 각 탭 화면 미러
237
+ scanPublish();
193
238
  post(api, '/api/control/claim', { token }).then((c) => { for (const cmd of (c && c.commands) || []) runControl(cmd); }).catch(() => {});
194
239
  const d = await post(api, '/api/jobs/claim', { token });
195
240
  for (const j of (d.jobs || [])) {