@ai-skills.ai/agentskills 0.2.2
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/README.md +32 -0
- package/bin/agentskills.js +6 -0
- package/package.json +24 -0
- package/src/init.js +262 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @ai-skills.ai/agentskills
|
|
2
|
+
|
|
3
|
+
Official installer CLI for AI Skills.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @ai-skills.ai/agentskills init --api-key <key>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Install a single skill:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @ai-skills.ai/agentskills init --api-key <key> --skill douyin-traffic-dashboard
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Where `<key>` is your API key from [ai-skills.ai](https://ai-skills.ai) dashboard.
|
|
18
|
+
|
|
19
|
+
The CLI will:
|
|
20
|
+
|
|
21
|
+
1. Persist `AISKILLS_API_KEY` to `~/.ai-skills/env.sh` (mode `0600`) and source it from your shell config
|
|
22
|
+
2. Refresh the current runtime environment from that user-level API key before installation
|
|
23
|
+
3. Run `npx skills add <registry> --skill '*'` to install all skills, or `--skill <skillId>` for a single skill
|
|
24
|
+
4. Let the downstream `skills` CLI prompt for target platforms/agents (e.g. Claude Code, Codex)
|
|
25
|
+
|
|
26
|
+
**Note:** Your API key is never placed on the command line. The installer relies on the user environment that `init` has just persisted and synchronized.
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm test
|
|
32
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-skills.ai/agentskills",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "AI Skills installer CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentskills": "bin/agentskills.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"check": "node --test"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/init.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export const API_KEY_ENV_NAME = 'AISKILLS_API_KEY';
|
|
7
|
+
export const MANAGED_ENV_DIR = '.ai-skills';
|
|
8
|
+
export const MANAGED_ENV_FILE = 'env.sh';
|
|
9
|
+
|
|
10
|
+
// Hardcoded registry — same for all users
|
|
11
|
+
export const DEFAULT_REGISTRY_URL = 'https://github.com/allinherog-star/ai-skills';
|
|
12
|
+
|
|
13
|
+
// Security constants
|
|
14
|
+
const SPAWN_TIMEOUT_MS = 120_000; // 2 minutes max for npx install
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parses CLI arguments for the init command.
|
|
18
|
+
* Accepts --api-key (required) and ignores unknown flags silently.
|
|
19
|
+
*/
|
|
20
|
+
export function parseCliArgs(argv) {
|
|
21
|
+
const args = Array.from(argv);
|
|
22
|
+
const command = args[0] || '';
|
|
23
|
+
let apiKey = '';
|
|
24
|
+
let skill = '';
|
|
25
|
+
|
|
26
|
+
for (let i = 1; i < args.length; i += 1) {
|
|
27
|
+
const current = args[i];
|
|
28
|
+
if (current === '--api-key') {
|
|
29
|
+
apiKey = args[i + 1] || '';
|
|
30
|
+
i += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (current.startsWith('--api-key=')) {
|
|
34
|
+
apiKey = current.slice('--api-key='.length);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (current === '--skill') {
|
|
38
|
+
skill = args[i + 1] || '';
|
|
39
|
+
i += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (current.startsWith('--skill=')) {
|
|
43
|
+
skill = current.slice('--skill='.length);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { command, apiKey, skill };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validates that a URL uses HTTPS.
|
|
52
|
+
* Security: Prevents man-in-the-middle attacks by enforcing TLS.
|
|
53
|
+
*/
|
|
54
|
+
export function requireHttpsUrl(urlString, context = 'URL') {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(urlString);
|
|
57
|
+
if (url.protocol !== 'https:') {
|
|
58
|
+
throw new Error(`${context} must use HTTPS`);
|
|
59
|
+
}
|
|
60
|
+
return url;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof TypeError) {
|
|
63
|
+
throw new Error(`${context} must be a valid URL`);
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validates install flags to prevent command injection.
|
|
71
|
+
* Only allows specific safe flags for `npx skills add`.
|
|
72
|
+
*/
|
|
73
|
+
export function validateInstallFlags(flags) {
|
|
74
|
+
if (!Array.isArray(flags)) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const ALLOWED_FLAGS = new Set([
|
|
78
|
+
'--force',
|
|
79
|
+
'-f',
|
|
80
|
+
'--dry-run',
|
|
81
|
+
'--verbose',
|
|
82
|
+
'-v',
|
|
83
|
+
]);
|
|
84
|
+
return flags.filter(flag => {
|
|
85
|
+
if (typeof flag !== 'string') return false;
|
|
86
|
+
// Block any flag that looks like shell injection
|
|
87
|
+
if (flag.includes(';') || flag.includes('&&') || flag.includes('||')) return false;
|
|
88
|
+
if (flag.includes('$(...)') || flag.includes('`')) return false;
|
|
89
|
+
return ALLOWED_FLAGS.has(flag);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeInstallSkill(skill) {
|
|
94
|
+
if (typeof skill !== 'string') {
|
|
95
|
+
return '*';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const normalized = skill.trim();
|
|
99
|
+
if (!normalized) {
|
|
100
|
+
return '*';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (normalized === '*') {
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!/^[a-z0-9-]{1,128}$/.test(normalized)) {
|
|
108
|
+
throw new Error('Invalid skill id');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sanitizes a string for safe shell embedding in single-quoted strings.
|
|
116
|
+
*/
|
|
117
|
+
export function shellEscape(value) {
|
|
118
|
+
if (typeof value !== 'string') {
|
|
119
|
+
throw new Error('shellEscape requires a string');
|
|
120
|
+
}
|
|
121
|
+
let escaped = value.replace(/\x00/g, '');
|
|
122
|
+
escaped = escaped.replace(/\\/g, '\\\\');
|
|
123
|
+
escaped = escaped.replace(/'/g, "'\\''");
|
|
124
|
+
return escaped;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ensureLine(filePath, line, fsImpl) {
|
|
128
|
+
const existing = fsImpl.existsSync(filePath) ? fsImpl.readFileSync(filePath, 'utf8') : '';
|
|
129
|
+
if (existing.includes(line)) return;
|
|
130
|
+
const next = existing && !existing.endsWith('\n') ? `${existing}\n` : existing;
|
|
131
|
+
fsImpl.writeFileSync(filePath, `${next}${line}\n`, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function persistApiKey(apiKey, options = {}) {
|
|
135
|
+
const platform = options.platform || process.platform;
|
|
136
|
+
const homedir = options.homedir || os.homedir;
|
|
137
|
+
const fsImpl = options.fsImpl || fs;
|
|
138
|
+
const spawnSyncImpl = options.spawnSyncImpl || spawnSync;
|
|
139
|
+
|
|
140
|
+
if (!apiKey) {
|
|
141
|
+
throw new Error('Missing API key');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Security: Validate API key format (basic sanity check)
|
|
145
|
+
if (!/^[\x20-\x7E]+$/.test(apiKey)) {
|
|
146
|
+
throw new Error('Invalid API key format');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (platform === 'win32') {
|
|
150
|
+
const result = spawnSyncImpl('setx', [API_KEY_ENV_NAME, apiKey], {
|
|
151
|
+
stdio: 'ignore',
|
|
152
|
+
windowsHide: true,
|
|
153
|
+
timeout: 5000,
|
|
154
|
+
});
|
|
155
|
+
if (result?.error) throw result.error;
|
|
156
|
+
if ((result?.status || 0) !== 0) {
|
|
157
|
+
throw new Error(`Failed to persist ${API_KEY_ENV_NAME} with setx`);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const home = homedir();
|
|
163
|
+
const managedDir = path.join(home, MANAGED_ENV_DIR);
|
|
164
|
+
const envFile = path.join(managedDir, MANAGED_ENV_FILE);
|
|
165
|
+
const sourceLine = `[ -f "$HOME/${MANAGED_ENV_DIR}/${MANAGED_ENV_FILE}" ] && . "$HOME/${MANAGED_ENV_DIR}/${MANAGED_ENV_FILE}"`;
|
|
166
|
+
|
|
167
|
+
fsImpl.mkdirSync(managedDir, { recursive: true });
|
|
168
|
+
fsImpl.writeFileSync(envFile, `export ${API_KEY_ENV_NAME}='${shellEscape(apiKey)}'\n`, {
|
|
169
|
+
encoding: 'utf8',
|
|
170
|
+
mode: 0o600,
|
|
171
|
+
});
|
|
172
|
+
fsImpl.chmodSync(envFile, 0o600);
|
|
173
|
+
|
|
174
|
+
['.zshrc', '.bashrc', '.bash_profile', '.profile'].forEach((fileName) => {
|
|
175
|
+
ensureLine(path.join(home, fileName), sourceLine, fsImpl);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function runSkillsInstall(options = {}) {
|
|
180
|
+
const {
|
|
181
|
+
registryUrl = DEFAULT_REGISTRY_URL,
|
|
182
|
+
installFlags = [],
|
|
183
|
+
skill = '*',
|
|
184
|
+
spawnSyncImpl = options.spawnSyncImpl || spawnSync,
|
|
185
|
+
platform = options.platform || process.platform,
|
|
186
|
+
envObject = options.envObject || process.env,
|
|
187
|
+
} = options;
|
|
188
|
+
|
|
189
|
+
const validatedFlags = validateInstallFlags(installFlags);
|
|
190
|
+
const normalizedSkill = normalizeInstallSkill(skill);
|
|
191
|
+
|
|
192
|
+
// Always pre-select the requested skill set; validatedFlags adds user-supplied safe ones
|
|
193
|
+
const flags = ['--skill', normalizedSkill, ...validatedFlags];
|
|
194
|
+
|
|
195
|
+
requireHttpsUrl(registryUrl, 'registry-url');
|
|
196
|
+
|
|
197
|
+
const binary = platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
198
|
+
const args = ['skills', 'add', registryUrl, ...flags];
|
|
199
|
+
|
|
200
|
+
// Install inherits the already-prepared runtime env instead of treating
|
|
201
|
+
// the API key as an install-only override.
|
|
202
|
+
const result = spawnSyncImpl(binary, args, {
|
|
203
|
+
stdio: 'inherit',
|
|
204
|
+
env: { ...envObject },
|
|
205
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
206
|
+
windowsHide: true,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (result?.error) {
|
|
210
|
+
throw result.error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result?.status || 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function runInitCommand(apiKey, options = {}) {
|
|
217
|
+
const logger = options.logger || console;
|
|
218
|
+
persistApiKey(apiKey, options);
|
|
219
|
+
process.env[API_KEY_ENV_NAME] = apiKey;
|
|
220
|
+
const installEnv = {
|
|
221
|
+
...(options.envObject || process.env),
|
|
222
|
+
[API_KEY_ENV_NAME]: apiKey,
|
|
223
|
+
};
|
|
224
|
+
logger.log(`Persisted ${API_KEY_ENV_NAME}`);
|
|
225
|
+
return runSkillsInstall({
|
|
226
|
+
spawnSyncImpl: options.spawnSyncImpl,
|
|
227
|
+
platform: options.platform,
|
|
228
|
+
envObject: installEnv,
|
|
229
|
+
skill: options.skill,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function renderUsage() {
|
|
234
|
+
return [
|
|
235
|
+
'Usage:',
|
|
236
|
+
' npx @ai-skills.ai/agentskills init --api-key <key>',
|
|
237
|
+
'',
|
|
238
|
+
'Options:',
|
|
239
|
+
' --api-key <key> API key from ai-skills.ai dashboard',
|
|
240
|
+
' --skill <skillId> Install one specific skill instead of the full bundle',
|
|
241
|
+
].join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function main(argv = process.argv.slice(2), options = {}) {
|
|
245
|
+
const logger = options.logger || console;
|
|
246
|
+
const { command, apiKey, skill } = parseCliArgs(argv);
|
|
247
|
+
|
|
248
|
+
if (command !== 'init' || !apiKey) {
|
|
249
|
+
logger.error(renderUsage());
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
return await runInitCommand(apiKey, {
|
|
255
|
+
...options,
|
|
256
|
+
skill: options.skill ?? skill,
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
}
|