@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 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
+ ```
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from '../src/init.js';
4
+
5
+ const exitCode = await main(process.argv.slice(2));
6
+ process.exit(exitCode);
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
+ }