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.
Files changed (2) hide show
  1. package/index.mjs +331 -0
  2. 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
+ }
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "sra-skills",
3
+ "version": "0.1.0",
4
+ "description": "SRA agent skills installer — manage AI skill repos",
5
+ "bin": {
6
+ "sra": "./index.mjs",
7
+ "sra-skills": "./index.mjs"
8
+ },
9
+ "type": "module"
10
+ }