sra-skills 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/index.mjs +331 -0
- package/package.json +10 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync,
|
|
4
|
+
symlinkSync, unlinkSync, lstatSync, readlinkSync } from 'fs';
|
|
5
|
+
import { join, basename, resolve } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
|
|
8
|
+
const HOME = homedir();
|
|
9
|
+
const SRA_HOME = join(HOME, '.sra');
|
|
10
|
+
const REPOS_DIR = join(SRA_HOME, 'repos');
|
|
11
|
+
const MANIFEST = join(SRA_HOME, 'manifest.json');
|
|
12
|
+
const AGENTS_SKILLS = join(HOME, '.agents', 'skills');
|
|
13
|
+
|
|
14
|
+
// --- Tool registry ---
|
|
15
|
+
|
|
16
|
+
const TOOLS = {
|
|
17
|
+
claude: {
|
|
18
|
+
detect: () => existsSync(join(HOME, '.claude')),
|
|
19
|
+
skillsDir: join(HOME, '.claude', 'skills'),
|
|
20
|
+
setup(repoPath) {
|
|
21
|
+
// Commands
|
|
22
|
+
const cmdsDir = join(repoPath, 'adapters', 'claude', 'commands');
|
|
23
|
+
const targetCmds = join(HOME, '.claude', 'commands');
|
|
24
|
+
if (existsSync(cmdsDir)) {
|
|
25
|
+
mkdirSync(targetCmds, { recursive: true });
|
|
26
|
+
for (const f of readdirSync(cmdsDir).filter(f => f.endsWith('.md'))) {
|
|
27
|
+
symlinkSafe(join(cmdsDir, f), join(targetCmds, f));
|
|
28
|
+
console.log(` [claude] → commands/${f}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Agents
|
|
32
|
+
const agentsDir = join(repoPath, 'adapters', 'claude', 'agents');
|
|
33
|
+
const targetAgents = join(HOME, '.claude', 'agents');
|
|
34
|
+
if (existsSync(agentsDir)) {
|
|
35
|
+
mkdirSync(targetAgents, { recursive: true });
|
|
36
|
+
for (const f of readdirSync(agentsDir).filter(f => f.endsWith('.md'))) {
|
|
37
|
+
symlinkSafe(join(agentsDir, f), join(targetAgents, f));
|
|
38
|
+
console.log(` [claude] → agents/${f}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// MCP servers
|
|
42
|
+
const mcpBase = join(repoPath, 'mcp');
|
|
43
|
+
if (existsSync(mcpBase)) {
|
|
44
|
+
for (const dir of readdirSync(mcpBase)) {
|
|
45
|
+
if (dir === 'shared') continue;
|
|
46
|
+
const serverPy = join(mcpBase, dir, 'server.py');
|
|
47
|
+
if (!existsSync(serverPy)) continue;
|
|
48
|
+
const name = dir.replace(/-server$/, '');
|
|
49
|
+
try {
|
|
50
|
+
execSync(`claude mcp remove -s user "${dir}" 2>/dev/null; claude mcp remove -s user "${name}" 2>/dev/null; claude mcp add -s user "${name}" python3 "${serverPy}"`, { stdio: 'pipe' });
|
|
51
|
+
console.log(` [claude] → MCP: ${name}`);
|
|
52
|
+
} catch { console.log(` [claude] ⚠ MCP ${name} registration failed`); }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
teardown(repoPath) {
|
|
57
|
+
const resolvedRepo = resolve(repoPath);
|
|
58
|
+
for (const subdir of ['commands', 'agents']) {
|
|
59
|
+
const dir = join(HOME, '.claude', subdir);
|
|
60
|
+
if (!existsSync(dir)) continue;
|
|
61
|
+
for (const f of readdirSync(dir)) {
|
|
62
|
+
const link = join(dir, f);
|
|
63
|
+
try {
|
|
64
|
+
if (lstatSync(link).isSymbolicLink()) {
|
|
65
|
+
const target = resolve(readlinkSync(link));
|
|
66
|
+
if (target.startsWith(resolvedRepo)) unlinkSync(link);
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const mcpBase = join(repoPath, 'mcp');
|
|
72
|
+
if (existsSync(mcpBase)) {
|
|
73
|
+
for (const dir of readdirSync(mcpBase)) {
|
|
74
|
+
if (dir === 'shared') continue;
|
|
75
|
+
const name = dir.replace(/-server$/, '');
|
|
76
|
+
try { execSync(`claude mcp remove -s user "${name}" 2>/dev/null`, { stdio: 'pipe' }); } catch {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
codex: {
|
|
82
|
+
detect: () => existsSync(join(HOME, '.codex')),
|
|
83
|
+
skillsDir: null,
|
|
84
|
+
},
|
|
85
|
+
cursor: {
|
|
86
|
+
detect: () => existsSync(join(HOME, '.cursor')),
|
|
87
|
+
skillsDir: null,
|
|
88
|
+
},
|
|
89
|
+
openclaw: {
|
|
90
|
+
detect: () => { try { execSync('which openclaw', { stdio: 'pipe' }); return true; } catch { return false; } },
|
|
91
|
+
skillsDir: null,
|
|
92
|
+
setup(repoPath) {
|
|
93
|
+
const configPath = join(HOME, '.openclaw', 'openclaw.json');
|
|
94
|
+
if (!existsSync(configPath)) return;
|
|
95
|
+
try {
|
|
96
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
97
|
+
const extra = config?.skills?.load?.extraDirs || [];
|
|
98
|
+
if (!extra.includes(AGENTS_SKILLS)) {
|
|
99
|
+
extra.push(AGENTS_SKILLS);
|
|
100
|
+
config.skills = config.skills || {};
|
|
101
|
+
config.skills.load = config.skills.load || {};
|
|
102
|
+
config.skills.load.extraDirs = extra;
|
|
103
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
104
|
+
console.log(` [openclaw] Added ${AGENTS_SKILLS} to extraDirs`);
|
|
105
|
+
}
|
|
106
|
+
} catch (e) { console.log(` [openclaw] ⚠ Could not update config: ${e.message}`); }
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// --- Core functions ---
|
|
112
|
+
|
|
113
|
+
function readManifest() {
|
|
114
|
+
if (!existsSync(MANIFEST)) return { repos: {}, tools: [] };
|
|
115
|
+
return JSON.parse(readFileSync(MANIFEST, 'utf8'));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeManifest(m) {
|
|
119
|
+
mkdirSync(SRA_HOME, { recursive: true });
|
|
120
|
+
writeFileSync(MANIFEST, JSON.stringify(m, null, 2) + '\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function detectTools() {
|
|
124
|
+
return Object.entries(TOOLS).filter(([, t]) => t.detect()).map(([name]) => name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseToolFlag(args) {
|
|
128
|
+
const idx = args.indexOf('--tool');
|
|
129
|
+
if (idx >= 0 && args[idx + 1]) {
|
|
130
|
+
const val = args[idx + 1];
|
|
131
|
+
args.splice(idx, 2);
|
|
132
|
+
return val === 'all' ? Object.keys(TOOLS) : [val];
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function discoverSkills(repoPath) {
|
|
138
|
+
const skillsDir = join(repoPath, 'skills');
|
|
139
|
+
if (!existsSync(skillsDir)) return [];
|
|
140
|
+
return readdirSync(skillsDir)
|
|
141
|
+
.filter(name => existsSync(join(skillsDir, name, 'SKILL.md')))
|
|
142
|
+
.map(name => ({ name, path: join(skillsDir, name) }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function symlinkSafe(target, linkPath) {
|
|
146
|
+
try { unlinkSync(linkPath); } catch {}
|
|
147
|
+
symlinkSync(target, linkPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function linkSkills(skills, tools) {
|
|
151
|
+
mkdirSync(AGENTS_SKILLS, { recursive: true });
|
|
152
|
+
for (const { name, path } of skills) {
|
|
153
|
+
symlinkSafe(path, join(AGENTS_SKILLS, name));
|
|
154
|
+
}
|
|
155
|
+
for (const toolName of tools) {
|
|
156
|
+
const tool = TOOLS[toolName];
|
|
157
|
+
if (!tool?.skillsDir) continue;
|
|
158
|
+
mkdirSync(tool.skillsDir, { recursive: true });
|
|
159
|
+
for (const { name } of skills) {
|
|
160
|
+
symlinkSafe(join(AGENTS_SKILLS, name), join(tool.skillsDir, name));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
skills.forEach(s => console.log(` → ${s.name}`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function unlinkSkills(skills, tools) {
|
|
167
|
+
for (const { name } of skills) {
|
|
168
|
+
const link = join(AGENTS_SKILLS, name);
|
|
169
|
+
try { if (lstatSync(link).isSymbolicLink()) unlinkSync(link); } catch {}
|
|
170
|
+
for (const toolName of tools) {
|
|
171
|
+
const tool = TOOLS[toolName];
|
|
172
|
+
if (!tool?.skillsDir) continue;
|
|
173
|
+
const tl = join(tool.skillsDir, name);
|
|
174
|
+
try { if (lstatSync(tl).isSymbolicLink()) unlinkSync(tl); } catch {}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getVersion(repoPath) {
|
|
180
|
+
try {
|
|
181
|
+
return execSync('git describe --tags --always', { cwd: repoPath, encoding: 'utf8' }).trim();
|
|
182
|
+
} catch { return 'unknown'; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function repoNameFromUrl(url) {
|
|
186
|
+
return basename(url).replace(/\.git$/, '');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function mergeCredentials(repoPath) {
|
|
190
|
+
const tmplPath = join(repoPath, 'config', 'credentials.template.json');
|
|
191
|
+
if (!existsSync(tmplPath)) return;
|
|
192
|
+
const credDir = join(HOME, '.config', 'sra');
|
|
193
|
+
const credPath = join(credDir, 'credentials.json');
|
|
194
|
+
mkdirSync(credDir, { recursive: true });
|
|
195
|
+
const template = JSON.parse(readFileSync(tmplPath, 'utf8'));
|
|
196
|
+
const existing = existsSync(credPath) ? JSON.parse(readFileSync(credPath, 'utf8')) : {};
|
|
197
|
+
let added = 0;
|
|
198
|
+
for (const [key, value] of Object.entries(template)) {
|
|
199
|
+
if (!(key in existing)) { existing[key] = value; added++; }
|
|
200
|
+
}
|
|
201
|
+
writeFileSync(credPath, JSON.stringify(existing, null, 2) + '\n');
|
|
202
|
+
if (added > 0) console.log(` Merged ${added} new credential key(s)`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function runToolSetup(repoPath, tools) {
|
|
206
|
+
for (const name of tools) {
|
|
207
|
+
const tool = TOOLS[name];
|
|
208
|
+
if (tool?.setup) tool.setup(repoPath);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- Commands ---
|
|
213
|
+
|
|
214
|
+
const rawArgs = process.argv.slice(2);
|
|
215
|
+
const cmd = rawArgs[0];
|
|
216
|
+
const cmdArgs = rawArgs.slice(1);
|
|
217
|
+
|
|
218
|
+
if (cmd === 'add') {
|
|
219
|
+
const isLocal = cmdArgs.includes('--local');
|
|
220
|
+
const tools = parseToolFlag(cmdArgs) || detectTools();
|
|
221
|
+
const nameIdx = cmdArgs.indexOf('--name');
|
|
222
|
+
let url, name, repoPath;
|
|
223
|
+
|
|
224
|
+
if (isLocal) {
|
|
225
|
+
cmdArgs.splice(cmdArgs.indexOf('--local'), 1);
|
|
226
|
+
const localPath = cmdArgs.find(a => !a.startsWith('--'));
|
|
227
|
+
repoPath = resolve(localPath);
|
|
228
|
+
name = nameIdx >= 0 ? cmdArgs[nameIdx + 1] : basename(repoPath);
|
|
229
|
+
url = null;
|
|
230
|
+
mkdirSync(REPOS_DIR, { recursive: true });
|
|
231
|
+
const repoLink = join(REPOS_DIR, name);
|
|
232
|
+
if (repoPath !== repoLink) symlinkSafe(repoPath, repoLink);
|
|
233
|
+
} else {
|
|
234
|
+
url = cmdArgs.find(a => !a.startsWith('--'));
|
|
235
|
+
if (!url) { console.error('Usage: sra add <git-url> [--tool <tool>] [--name <name>]'); process.exit(1); }
|
|
236
|
+
name = nameIdx >= 0 ? cmdArgs[nameIdx + 1] : repoNameFromUrl(url);
|
|
237
|
+
repoPath = join(REPOS_DIR, name);
|
|
238
|
+
mkdirSync(REPOS_DIR, { recursive: true });
|
|
239
|
+
if (existsSync(repoPath)) {
|
|
240
|
+
console.log(`Repo "${name}" exists, updating...`);
|
|
241
|
+
execSync('git pull', { cwd: repoPath, stdio: 'inherit' });
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`Cloning ${url}...`);
|
|
244
|
+
execSync(`git clone --depth 1 "${url}" "${repoPath}"`, { stdio: 'inherit' });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const skills = discoverSkills(repoPath);
|
|
249
|
+
console.log(`Found ${skills.length} skills, tools: [${tools.join(', ')}]`);
|
|
250
|
+
linkSkills(skills, tools);
|
|
251
|
+
mergeCredentials(repoPath);
|
|
252
|
+
runToolSetup(repoPath, tools);
|
|
253
|
+
|
|
254
|
+
const m = readManifest();
|
|
255
|
+
m.repos[name] = { path: repoPath, ...(url && { url }), version: getVersion(repoPath),
|
|
256
|
+
updated_at: new Date().toISOString() };
|
|
257
|
+
m.tools = tools;
|
|
258
|
+
writeManifest(m);
|
|
259
|
+
console.log(`Done. ${name}: ${m.repos[name].version}`);
|
|
260
|
+
|
|
261
|
+
} else if (cmd === 'remove') {
|
|
262
|
+
const name = cmdArgs[0];
|
|
263
|
+
if (!name) { console.error('Usage: sra remove <repo-name>'); process.exit(1); }
|
|
264
|
+
const m = readManifest();
|
|
265
|
+
const repo = m.repos[name];
|
|
266
|
+
if (!repo) { console.error(`Repo "${name}" not found`); process.exit(1); }
|
|
267
|
+
|
|
268
|
+
const tools = m.tools || detectTools();
|
|
269
|
+
const skills = discoverSkills(repo.path);
|
|
270
|
+
unlinkSkills(skills, tools);
|
|
271
|
+
for (const t of tools) { if (TOOLS[t]?.teardown) TOOLS[t].teardown(repo.path); }
|
|
272
|
+
// Only remove the symlink in repos/, not the actual source repo
|
|
273
|
+
const repoLink = join(REPOS_DIR, name);
|
|
274
|
+
try {
|
|
275
|
+
if (lstatSync(repoLink).isSymbolicLink()) unlinkSync(repoLink);
|
|
276
|
+
else execSync(`rm -rf "${repoLink}"`);
|
|
277
|
+
} catch {}
|
|
278
|
+
delete m.repos[name];
|
|
279
|
+
writeManifest(m);
|
|
280
|
+
console.log(`Removed ${name} (${skills.length} skills unlinked)`);
|
|
281
|
+
|
|
282
|
+
} else if (cmd === 'update') {
|
|
283
|
+
const m = readManifest();
|
|
284
|
+
const tools = parseToolFlag(cmdArgs) || m.tools || detectTools();
|
|
285
|
+
const target = cmdArgs.find(a => !a.startsWith('--'));
|
|
286
|
+
const targets = target ? [target] : Object.keys(m.repos);
|
|
287
|
+
for (const name of targets) {
|
|
288
|
+
const repo = m.repos[name];
|
|
289
|
+
if (!repo) { console.error(`Repo "${name}" not found`); continue; }
|
|
290
|
+
console.log(`Updating ${name}...`);
|
|
291
|
+
try { execSync('git pull', { cwd: repo.path, stdio: 'inherit' }); } catch {}
|
|
292
|
+
const skills = discoverSkills(repo.path);
|
|
293
|
+
linkSkills(skills, tools);
|
|
294
|
+
runToolSetup(repo.path, tools);
|
|
295
|
+
repo.version = getVersion(repo.path);
|
|
296
|
+
repo.updated_at = new Date().toISOString();
|
|
297
|
+
}
|
|
298
|
+
m.tools = tools;
|
|
299
|
+
writeManifest(m);
|
|
300
|
+
|
|
301
|
+
} else if (cmd === 'list') {
|
|
302
|
+
const m = readManifest();
|
|
303
|
+
for (const [name, info] of Object.entries(m.repos)) {
|
|
304
|
+
const skills = discoverSkills(info.path);
|
|
305
|
+
console.log(`${name} (${info.version || 'unknown'}):`);
|
|
306
|
+
skills.forEach(s => console.log(` ${s.name}`));
|
|
307
|
+
}
|
|
308
|
+
if (m.tools?.length) console.log(`\nTools: ${m.tools.join(', ')}`);
|
|
309
|
+
|
|
310
|
+
} else if (cmd === 'version') {
|
|
311
|
+
const m = readManifest();
|
|
312
|
+
for (const [name, info] of Object.entries(m.repos)) {
|
|
313
|
+
console.log(`${name}: ${info.version || 'unknown'}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
} else {
|
|
317
|
+
console.log(`Usage: sra <add|remove|update|list|version> [--tool <tool>] [args]
|
|
318
|
+
|
|
319
|
+
Commands:
|
|
320
|
+
add <git-url> Clone repo, discover skills, symlink, configure tools
|
|
321
|
+
add --local <path> Register existing repo directory
|
|
322
|
+
remove <repo-name> Unlink skills, remove repo
|
|
323
|
+
update [repo-name] Git pull, re-link, re-configure
|
|
324
|
+
list Show installed repos and skills
|
|
325
|
+
version Show repo versions
|
|
326
|
+
|
|
327
|
+
Options:
|
|
328
|
+
--tool <name> Target tool (claude|codex|cursor|openclaw|all)
|
|
329
|
+
Default: auto-detect installed tools
|
|
330
|
+
--name <name> Override repo name (for add)`);
|
|
331
|
+
}
|