ronds_ai 0.1.6 → 0.1.7

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/bin/ronds_ai.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { runCodeRecord, saveCliError } = require('../lib/code_record');
4
4
  const { runDoctor } = require('../lib/doctor');
5
5
  const { deployHooks } = require('../lib/hooks_deploy');
6
+ const { runSkillsInstall } = require('../lib/skills_install');
6
7
 
7
8
  const SUPPORTED_SOURCES = new Set(['claude', 'cursor']);
8
9
 
@@ -12,10 +13,12 @@ function printUsage() {
12
13
  ' ronds_ai record <tool>',
13
14
  ' ronds_ai doctor <tool>',
14
15
  ' ronds_ai hooks deploy',
16
+ ' ronds_ai skills install <name> [--tool claude,codex,cursor] [--scope project|global] [--project-dir <path>] [--force]',
15
17
  '',
16
18
  'Supported tools:',
17
19
  ' claude',
18
20
  ' cursor',
21
+ ' codex',
19
22
  '',
20
23
  'Examples:',
21
24
  ' npx ronds_ai@latest record claude',
@@ -23,6 +26,9 @@ function printUsage() {
23
26
  ' npx ronds_ai@latest doctor claude',
24
27
  ' npx ronds_ai@latest doctor cursor',
25
28
  ' npx ronds_ai@latest hooks deploy',
29
+ ' npx ronds_ai@latest skills install demo-skill',
30
+ ' npx ronds_ai@latest skills install demo-skill --tool claude --scope global',
31
+ ' npx ronds_ai@latest skills install demo-skill --tool codex,cursor --scope project',
26
32
  ].join('\n'));
27
33
  process.stderr.write('\n');
28
34
  }
@@ -38,6 +44,86 @@ async function runHooksCommand(args) {
38
44
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
39
45
  }
40
46
 
47
+ function parseSkillsInstallArgs(args) {
48
+ const options = {
49
+ tool: '',
50
+ scope: '',
51
+ projectDir: '',
52
+ force: false,
53
+ };
54
+ const positional = [];
55
+
56
+ for (let index = 0; index < args.length; index += 1) {
57
+ const value = args[index];
58
+
59
+ if (value === '--force') {
60
+ options.force = true;
61
+ continue;
62
+ }
63
+
64
+ if (value === '--tool') {
65
+ index += 1;
66
+ options.tool = args[index] || '';
67
+ continue;
68
+ }
69
+
70
+ if (value.startsWith('--tool=')) {
71
+ options.tool = value.slice('--tool='.length);
72
+ continue;
73
+ }
74
+
75
+ if (value === '--scope') {
76
+ index += 1;
77
+ options.scope = args[index] || '';
78
+ continue;
79
+ }
80
+
81
+ if (value.startsWith('--scope=')) {
82
+ options.scope = value.slice('--scope='.length);
83
+ continue;
84
+ }
85
+
86
+ if (value === '--project-dir') {
87
+ index += 1;
88
+ options.projectDir = args[index] || '';
89
+ continue;
90
+ }
91
+
92
+ if (value.startsWith('--project-dir=')) {
93
+ options.projectDir = value.slice('--project-dir='.length);
94
+ continue;
95
+ }
96
+
97
+ if (value.startsWith('--')) {
98
+ throw new Error(`Unsupported option: ${value}`);
99
+ }
100
+
101
+ positional.push(value);
102
+ }
103
+
104
+ const [name] = positional;
105
+ if (!name) {
106
+ throw new Error('Missing skill name');
107
+ }
108
+
109
+ return {
110
+ name,
111
+ options,
112
+ };
113
+ }
114
+
115
+ async function runSkillsCommand(args) {
116
+ const [action, ...rest] = args;
117
+
118
+ if (action !== 'install') {
119
+ throw new Error('Unsupported skills command');
120
+ }
121
+
122
+ const { name, options } = parseSkillsInstallArgs(rest);
123
+ const result = await runSkillsInstall(name, options);
124
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
125
+ }
126
+
41
127
  async function run() {
42
128
  const [, , command, ...args] = process.argv;
43
129
 
@@ -71,6 +157,11 @@ async function run() {
71
157
  return;
72
158
  }
73
159
 
160
+ if (command === 'skills') {
161
+ await runSkillsCommand(args);
162
+ return;
163
+ }
164
+
74
165
  throw new Error(`Unsupported command: ${command || ''}`);
75
166
  }
76
167
 
@@ -0,0 +1,168 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const { execFileSync } = require('child_process');
7
+ const { buildInstallTargets, normalizeScope, normalizeTools } = require('./skills_targets');
8
+ const { promptForScope, promptForTools } = require('./skills_prompt');
9
+
10
+ const SKILL_DOWNLOAD_BASE_URL = 'https://aihub.ronds.com/api/api/v1/skills/download';
11
+ const MAX_REDIRECTS = 5;
12
+
13
+ function streamToFile(url, destinationPath, redirectCount = 0) {
14
+ return new Promise((resolve, reject) => {
15
+ const transport = String(url).startsWith('http://') ? http : https;
16
+ const request = transport.get(url, (response) => {
17
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
18
+ response.resume();
19
+ if (redirectCount >= MAX_REDIRECTS) {
20
+ reject(new Error('Too many redirects while downloading skill archive'));
21
+ return;
22
+ }
23
+
24
+ const nextUrl = new URL(response.headers.location, url).toString();
25
+ streamToFile(nextUrl, destinationPath, redirectCount + 1).then(resolve, reject);
26
+ return;
27
+ }
28
+
29
+ if (response.statusCode !== 200) {
30
+ response.resume();
31
+ reject(new Error(`Skill download failed with status ${response.statusCode || 0}`));
32
+ return;
33
+ }
34
+
35
+ const fileStream = fs.createWriteStream(destinationPath);
36
+ response.pipe(fileStream);
37
+
38
+ fileStream.on('finish', () => {
39
+ fileStream.close(() => resolve(destinationPath));
40
+ });
41
+
42
+ fileStream.on('error', (error) => {
43
+ fileStream.close(() => reject(error));
44
+ });
45
+ });
46
+
47
+ request.on('error', reject);
48
+ });
49
+ }
50
+
51
+ async function downloadSkillArchive(name, workingDir) {
52
+ const archivePath = path.join(workingDir, `${name}.zip`);
53
+ const url = `${SKILL_DOWNLOAD_BASE_URL}/${encodeURIComponent(name)}`;
54
+ await streamToFile(url, archivePath);
55
+ return {
56
+ url,
57
+ archivePath,
58
+ };
59
+ }
60
+
61
+ function extractArchive(archivePath, outputDir) {
62
+ fs.mkdirSync(outputDir, { recursive: true });
63
+ execFileSync('unzip', ['-q', archivePath, '-d', outputDir], {
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ });
66
+ }
67
+
68
+ function findSkillRoot(extractDir) {
69
+ const directSkillMd = path.join(extractDir, 'SKILL.md');
70
+ if (fs.existsSync(directSkillMd)) {
71
+ return extractDir;
72
+ }
73
+
74
+ const entries = fs.readdirSync(extractDir, { withFileTypes: true });
75
+ const candidateDirs = entries
76
+ .filter((entry) => entry.isDirectory())
77
+ .map((entry) => path.join(extractDir, entry.name))
78
+ .filter((entryPath) => fs.existsSync(path.join(entryPath, 'SKILL.md')));
79
+
80
+ if (candidateDirs.length === 1) {
81
+ return candidateDirs[0];
82
+ }
83
+
84
+ if (candidateDirs.length > 1) {
85
+ throw new Error('Downloaded archive contains multiple skill roots');
86
+ }
87
+
88
+ throw new Error('Downloaded archive does not contain SKILL.md');
89
+ }
90
+
91
+ function copySkill(sourceDir, destinationDir, force) {
92
+ if (fs.existsSync(destinationDir)) {
93
+ if (!force) {
94
+ throw new Error(`Skill already exists at ${destinationDir}. Use --force to overwrite.`);
95
+ }
96
+
97
+ fs.rmSync(destinationDir, { recursive: true, force: true });
98
+ }
99
+
100
+ fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
101
+ fs.cpSync(sourceDir, destinationDir, { recursive: true });
102
+ }
103
+
104
+ async function resolveInstallOptions(options) {
105
+ let tools = normalizeTools(options.tool);
106
+ if (tools.length === 0) {
107
+ tools = normalizeTools(await promptForTools());
108
+ }
109
+
110
+ if (tools.length === 0) {
111
+ throw new Error('No skills tool selected');
112
+ }
113
+
114
+ let scope = normalizeScope(options.scope);
115
+ if (!scope) {
116
+ scope = normalizeScope(await promptForScope());
117
+ }
118
+
119
+ if (!scope) {
120
+ throw new Error('No skills scope selected');
121
+ }
122
+
123
+ return {
124
+ tools,
125
+ scope,
126
+ projectDir: options.projectDir ? path.resolve(options.projectDir) : process.cwd(),
127
+ force: Boolean(options.force),
128
+ };
129
+ }
130
+
131
+ async function runSkillsInstall(name, rawOptions = {}) {
132
+ const skillName = String(name || '').trim();
133
+ if (!skillName) {
134
+ throw new Error('Missing skill name');
135
+ }
136
+
137
+ const options = await resolveInstallOptions(rawOptions);
138
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ronds-ai-skill-'));
139
+
140
+ try {
141
+ const download = await downloadSkillArchive(skillName, tempRoot);
142
+ const extractDir = path.join(tempRoot, 'extracted');
143
+ extractArchive(download.archivePath, extractDir);
144
+ const skillRoot = findSkillRoot(extractDir);
145
+ const targets = buildInstallTargets(options.tools, options.scope, options.projectDir, skillName);
146
+
147
+ for (const target of targets) {
148
+ copySkill(skillRoot, target.destination, options.force);
149
+ }
150
+
151
+ return {
152
+ name: skillName,
153
+ scope: options.scope,
154
+ projectDir: options.scope === 'project' ? options.projectDir : '',
155
+ archiveUrl: download.url,
156
+ installedTargets: targets.map((target) => ({
157
+ tools: target.tools,
158
+ destination: target.destination,
159
+ })),
160
+ };
161
+ } finally {
162
+ fs.rmSync(tempRoot, { recursive: true, force: true });
163
+ }
164
+ }
165
+
166
+ module.exports = {
167
+ runSkillsInstall,
168
+ };
@@ -0,0 +1,87 @@
1
+ const readline = require('readline/promises');
2
+
3
+ const TOOL_CHOICES = ['claude', 'codex', 'cursor'];
4
+ const SCOPE_CHOICES = ['project', 'global'];
5
+
6
+ function normalizeCsvInput(raw) {
7
+ return String(raw || '')
8
+ .split(',')
9
+ .map((item) => item.trim().toLowerCase())
10
+ .filter(Boolean);
11
+ }
12
+
13
+ function normalizeChoiceInput(raw) {
14
+ const values = normalizeCsvInput(raw);
15
+ if (values.length === 0) {
16
+ return [];
17
+ }
18
+
19
+ return values.flatMap((value) => {
20
+ if (/^\d+$/.test(value)) {
21
+ const index = Number(value) - 1;
22
+ return TOOL_CHOICES[index] ? [TOOL_CHOICES[index]] : [];
23
+ }
24
+
25
+ return [value];
26
+ });
27
+ }
28
+
29
+ async function promptForTools() {
30
+ const rl = readline.createInterface({
31
+ input: process.stdin,
32
+ output: process.stdout,
33
+ });
34
+
35
+ try {
36
+ process.stderr.write([
37
+ 'Select tools to install:',
38
+ ' 1. claude',
39
+ ' 2. codex',
40
+ ' 3. cursor',
41
+ 'Enter comma-separated names or numbers:',
42
+ ].join('\n'));
43
+ process.stderr.write('\n');
44
+
45
+ const answer = await rl.question('> ');
46
+ return normalizeChoiceInput(answer);
47
+ } finally {
48
+ rl.close();
49
+ }
50
+ }
51
+
52
+ async function promptForScope() {
53
+ const rl = readline.createInterface({
54
+ input: process.stdin,
55
+ output: process.stdout,
56
+ });
57
+
58
+ try {
59
+ process.stderr.write([
60
+ 'Select install scope:',
61
+ ' 1. project',
62
+ ' 2. global',
63
+ 'Enter project or global:',
64
+ ].join('\n'));
65
+ process.stderr.write('\n');
66
+
67
+ const answer = String(await rl.question('> ')).trim().toLowerCase();
68
+ if (answer === '1') {
69
+ return 'project';
70
+ }
71
+ if (answer === '2') {
72
+ return 'global';
73
+ }
74
+ return answer;
75
+ } finally {
76
+ rl.close();
77
+ }
78
+ }
79
+
80
+ module.exports = {
81
+ SCOPE_CHOICES,
82
+ TOOL_CHOICES,
83
+ normalizeChoiceInput,
84
+ normalizeCsvInput,
85
+ promptForScope,
86
+ promptForTools,
87
+ };
@@ -0,0 +1,81 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const { SCOPE_CHOICES, TOOL_CHOICES, normalizeCsvInput } = require('./skills_prompt');
4
+
5
+ function normalizeTools(rawTools) {
6
+ const values = Array.isArray(rawTools)
7
+ ? rawTools.flatMap((item) => normalizeCsvInput(item))
8
+ : normalizeCsvInput(rawTools);
9
+ const unique = [];
10
+
11
+ for (const tool of values) {
12
+ if (!TOOL_CHOICES.includes(tool)) {
13
+ throw new Error(`Unsupported skills tool: ${tool}`);
14
+ }
15
+
16
+ if (!unique.includes(tool)) {
17
+ unique.push(tool);
18
+ }
19
+ }
20
+
21
+ return unique;
22
+ }
23
+
24
+ function normalizeScope(rawScope) {
25
+ const scope = String(rawScope || '').trim().toLowerCase();
26
+ if (!scope) {
27
+ return '';
28
+ }
29
+
30
+ if (!SCOPE_CHOICES.includes(scope)) {
31
+ throw new Error(`Unsupported skills scope: ${rawScope}`);
32
+ }
33
+
34
+ return scope;
35
+ }
36
+
37
+ function resolveToolBaseDir(tool, scope, projectDir) {
38
+ const baseDir = scope === 'global' ? os.homedir() : path.resolve(projectDir || process.cwd());
39
+
40
+ if (tool === 'claude') {
41
+ return path.join(baseDir, '.claude', 'skills');
42
+ }
43
+
44
+ if (tool === 'codex' || tool === 'cursor') {
45
+ return path.join(baseDir, '.agents', 'skills');
46
+ }
47
+
48
+ throw new Error(`Unsupported skills tool: ${tool}`);
49
+ }
50
+
51
+ function buildInstallTargets(tools, scope, projectDir, skillName) {
52
+ const deduped = new Map();
53
+
54
+ for (const tool of tools) {
55
+ const baseDir = resolveToolBaseDir(tool, scope, projectDir);
56
+ const destination = path.join(baseDir, skillName);
57
+ const existing = deduped.get(destination);
58
+
59
+ if (existing) {
60
+ existing.tools.push(tool);
61
+ continue;
62
+ }
63
+
64
+ deduped.set(destination, {
65
+ tool,
66
+ tools: [tool],
67
+ scope,
68
+ baseDir,
69
+ destination,
70
+ });
71
+ }
72
+
73
+ return Array.from(deduped.values());
74
+ }
75
+
76
+ module.exports = {
77
+ buildInstallTargets,
78
+ normalizeScope,
79
+ normalizeTools,
80
+ resolveToolBaseDir,
81
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ronds_ai",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "CLI for reporting AI code edit events.",
5
5
  "bin": {
6
6
  "ronds_ai": "bin/ronds_ai.js"