@areca-cms/skills 0.0.0-stage → 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/dist/cli.js ADDED
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { join, relative, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { parseArgs } from 'node:util';
6
+ import { cancel, confirm, groupMultiselect, intro, isCancel, log, multiselect, note, outro, select, text, } from '@clack/prompts';
7
+ import { AGENTS, findAgent, GROUP_LABELS, mcpServerName, validateConnectorUrl, } from './agents.js';
8
+ import { exportZips, installSkills, removeSkills, resolveTargets } from './install.js';
9
+ import { configureMcp } from './mcp-config.js';
10
+ import { loadSkills } from './skills.js';
11
+ const USAGE = `Usage: npx @areca-cms/skills [command] [options]
12
+
13
+ Commands:
14
+ add (default) Install the Areca skills into your agents
15
+ mcp Add the Areca MCP server to your agents' project config
16
+ remove Remove the Areca skills
17
+ export Write one .zip per skill (Claude app, ChatGPT uploads)
18
+ list Show supported agents and bundled skills
19
+
20
+ Options:
21
+ -a, --agent <id> Agent id, repeatable or comma separated (see list)
22
+ -s, --skill <name> Only this skill (areca-sdk, areca-mcp); default all
23
+ -g, --global Install into user-level folders instead of the project
24
+ --mcp-url <url> Connector URL (<cms>/api/mcp/<site-slug>) to register
25
+ --out <dir> Zip folder for chat apps and export (default: areca-skills)
26
+ -y, --yes No prompts; requires --agent for add/mcp/remove
27
+ -h, --help Show this help`;
28
+ class Cancelled extends Error {
29
+ }
30
+ export async function run(argv, cwd = process.cwd()) {
31
+ let parsed;
32
+ try {
33
+ parsed = parseArgs({
34
+ args: argv,
35
+ allowPositionals: true,
36
+ options: {
37
+ agent: { type: 'string', short: 'a', multiple: true },
38
+ skill: { type: 'string', short: 's', multiple: true },
39
+ global: { type: 'boolean', short: 'g' },
40
+ 'mcp-url': { type: 'string' },
41
+ url: { type: 'string' },
42
+ out: { type: 'string' },
43
+ yes: { type: 'boolean', short: 'y' },
44
+ help: { type: 'boolean', short: 'h' },
45
+ },
46
+ });
47
+ }
48
+ catch (error) {
49
+ console.error(error instanceof Error ? error.message : error);
50
+ console.error(USAGE);
51
+ return 1;
52
+ }
53
+ const { values, positionals } = parsed;
54
+ const command = positionals[0] ?? 'add';
55
+ if (values.help || command === 'help') {
56
+ console.log(USAGE);
57
+ return 0;
58
+ }
59
+ const options = {
60
+ agents: splitList(values.agent),
61
+ skills: splitList(values.skill),
62
+ global: values.global === true,
63
+ mcpUrl: values['mcp-url'] ?? values.url,
64
+ out: values.out ?? 'areca-skills',
65
+ yes: values.yes === true || !process.stdin.isTTY,
66
+ };
67
+ try {
68
+ const unknown = options.agents.filter((id) => !findAgent(id));
69
+ if (unknown.length)
70
+ throw new Error(`Unknown agent: ${unknown.join(', ')}. Run "list" to see ids.`);
71
+ if (options.mcpUrl) {
72
+ const problem = validateConnectorUrl(options.mcpUrl);
73
+ if (problem)
74
+ throw new Error(problem);
75
+ }
76
+ switch (command) {
77
+ case 'add':
78
+ return await add(options, cwd);
79
+ case 'mcp':
80
+ return await mcp(options, cwd);
81
+ case 'remove':
82
+ return await remove(options, cwd);
83
+ case 'export':
84
+ return exportCommand(options, cwd);
85
+ case 'list':
86
+ return list();
87
+ default:
88
+ console.error(`Unknown command: ${command}\n\n${USAGE}`);
89
+ return 1;
90
+ }
91
+ }
92
+ catch (error) {
93
+ if (error instanceof Cancelled) {
94
+ cancel('Cancelled.');
95
+ return 0;
96
+ }
97
+ console.error(error instanceof Error ? error.message : error);
98
+ return 1;
99
+ }
100
+ }
101
+ async function add(options, cwd) {
102
+ const interactive = !options.yes;
103
+ if (interactive)
104
+ intro('Areca agent skills');
105
+ const agents = await chooseAgents(options, cwd, 'Which agents should get the Areca skills?');
106
+ const skills = await chooseSkills(options);
107
+ const coding = agents.filter((agent) => agent.group !== 'chat');
108
+ const chat = agents.filter((agent) => agent.group === 'chat');
109
+ let scope = options.global ? 'global' : 'project';
110
+ if (interactive && coding.length && !options.global) {
111
+ scope = unwrap(await select({
112
+ message: 'Install for',
113
+ options: [
114
+ { value: 'project', label: 'This project', hint: 'commit them so the whole team gets them' },
115
+ { value: 'global', label: 'Me, in every project', hint: 'user-level folders' },
116
+ ],
117
+ }));
118
+ }
119
+ const targets = resolveTargets(coding, scope, cwd);
120
+ const written = installSkills(skills, targets);
121
+ for (const target of targets) {
122
+ log.success(`${target.label} (${target.agents.join(', ')})`);
123
+ }
124
+ if (chat.length) {
125
+ const files = exportZips(skills, resolve(cwd, options.out));
126
+ log.success(`Zips: ${files.map((file) => relative(cwd, file)).join(', ')}`);
127
+ }
128
+ let mcpResults = [];
129
+ if (coding.length) {
130
+ const url = await chooseMcpUrl(options, cwd, interactive);
131
+ if (url)
132
+ mcpResults = registerMcp(coding, url, scope, cwd);
133
+ }
134
+ const after = [...new Set(agents.map((agent) => agent.after).filter(Boolean))];
135
+ const name = mcpResults.length ? mcpServerName(options.mcpUrl ?? '') : '<name>';
136
+ const steps = [
137
+ `Installed ${skills.map((skill) => skill.name).join(' + ')} (${written.length} folder${written.length === 1 ? '' : 's'}).`,
138
+ ...after.map((line) => line.replaceAll('<name>', name)),
139
+ ];
140
+ if (interactive)
141
+ outro(steps.join('\n'));
142
+ else
143
+ console.log(steps.join('\n'));
144
+ return 0;
145
+ }
146
+ async function mcp(options, cwd) {
147
+ const interactive = !options.yes;
148
+ if (interactive)
149
+ intro('Areca MCP server');
150
+ const agents = (await chooseAgents(options, cwd, 'Add the Areca MCP server to which agents?')).filter((agent) => agent.group !== 'chat');
151
+ const url = await chooseMcpUrl({ ...options, yes: options.yes }, cwd, interactive, true);
152
+ if (!url)
153
+ throw new Error('Pass --url <cms>/api/mcp/<site-slug>');
154
+ registerMcp(agents, url, 'project', cwd);
155
+ const after = [...new Set(agents.map((agent) => agent.after).filter(Boolean))].map((line) => line.replaceAll('<name>', mcpServerName(url)));
156
+ const done = ['Each agent opens a browser to sign in to Areca on first use.', ...after].join('\n');
157
+ if (interactive)
158
+ outro(done);
159
+ else
160
+ console.log(done);
161
+ return 0;
162
+ }
163
+ async function remove(options, cwd) {
164
+ const interactive = !options.yes;
165
+ if (interactive)
166
+ intro('Remove Areca agent skills');
167
+ const agents = await chooseAgents(options, cwd, 'Remove the Areca skills from which agents?');
168
+ const names = (options.skills.length ? options.skills : loadSkills().map((skill) => skill.name));
169
+ const scope = options.global ? 'global' : 'project';
170
+ const removed = removeSkills(names, resolveTargets(agents, scope, cwd));
171
+ const message = removed.length ? `Removed ${removed.join(', ')}` : 'Nothing to remove.';
172
+ if (interactive)
173
+ outro(message);
174
+ else
175
+ console.log(message);
176
+ return 0;
177
+ }
178
+ function exportCommand(options, cwd) {
179
+ const files = exportZips(selectSkills(options.skills), resolve(cwd, options.out));
180
+ console.log(`Wrote ${files.map((file) => relative(cwd, file)).join(', ')}`);
181
+ for (const id of ['claude-app', 'chatgpt'])
182
+ console.log(findAgent(id).after);
183
+ return 0;
184
+ }
185
+ function list() {
186
+ const skills = loadSkills();
187
+ console.log('Skills:');
188
+ for (const skill of skills)
189
+ console.log(` ${skill.name.padEnd(12)} ${skill.description.slice(0, 90)}…`);
190
+ for (const group of Object.keys(GROUP_LABELS)) {
191
+ console.log(`\n${GROUP_LABELS[group]}:`);
192
+ for (const agent of AGENTS.filter((item) => item.group === group)) {
193
+ const where = agent.projectDir ? `${agent.projectDir} ~/${agent.globalDir}` : 'zip upload';
194
+ console.log(` ${agent.id.padEnd(15)} ${agent.label.padEnd(40)} ${where}`);
195
+ }
196
+ }
197
+ return 0;
198
+ }
199
+ async function chooseAgents(options, cwd, message) {
200
+ if (options.agents.length)
201
+ return options.agents.map((id) => findAgent(id));
202
+ if (options.yes)
203
+ throw new Error('Pass --agent <id> (repeatable). Run "list" to see ids.');
204
+ const detected = AGENTS.filter((agent) => agent.detect.some((path) => existsSync(join(cwd, path))));
205
+ const groups = {};
206
+ for (const agent of AGENTS) {
207
+ const group = GROUP_LABELS[agent.group];
208
+ (groups[group] ??= []).push({
209
+ value: agent.id,
210
+ label: agent.label,
211
+ hint: agent.projectDir ?? 'zip',
212
+ });
213
+ }
214
+ const ids = unwrap(await groupMultiselect({
215
+ message: `${message} (space to toggle, enter to confirm)`,
216
+ options: groups,
217
+ initialValues: detected.map((agent) => agent.id),
218
+ required: true,
219
+ }));
220
+ return ids.map((id) => findAgent(id));
221
+ }
222
+ async function chooseSkills(options) {
223
+ if (options.skills.length || options.yes)
224
+ return selectSkills(options.skills);
225
+ const all = loadSkills();
226
+ const names = unwrap(await multiselect({
227
+ message: 'Which skills?',
228
+ options: all.map((skill) => ({ value: skill.name, label: skill.name, hint: hintFor(skill.name) })),
229
+ initialValues: all.map((skill) => skill.name),
230
+ required: true,
231
+ }));
232
+ return all.filter((skill) => names.includes(skill.name));
233
+ }
234
+ function selectSkills(names) {
235
+ const all = loadSkills();
236
+ if (!names.length)
237
+ return all;
238
+ const missing = names.filter((name) => !all.some((skill) => skill.name === name));
239
+ if (missing.length)
240
+ throw new Error(`Unknown skill: ${missing.join(', ')}. Available: ${all.map((s) => s.name).join(', ')}`);
241
+ return all.filter((skill) => names.includes(skill.name));
242
+ }
243
+ function hintFor(name) {
244
+ if (name === 'areca-sdk')
245
+ return 'website code: setup, config, CLI, content, preview, deploy';
246
+ if (name === 'areca-mcp')
247
+ return 'AI content editing: connect, tools, workflow';
248
+ return '';
249
+ }
250
+ async function chooseMcpUrl(options, cwd, interactive, required = false) {
251
+ if (options.mcpUrl)
252
+ return options.mcpUrl;
253
+ if (!interactive)
254
+ return undefined;
255
+ if (!required) {
256
+ const wanted = unwrap(await confirm({
257
+ message: 'Also add the Areca MCP server to these agents? (URL only, sign-in happens in the browser)',
258
+ initialValue: false,
259
+ }));
260
+ if (!wanted)
261
+ return undefined;
262
+ }
263
+ const url = unwrap(await text({
264
+ message: 'Connector URL (CMS > your site > Connect AI)',
265
+ placeholder: 'https://cms.example.com/api/mcp/acme-site',
266
+ initialValue: guessConnectorUrl(cwd),
267
+ validate: (value) => validateConnectorUrl(value ?? ''),
268
+ }));
269
+ options.mcpUrl = url;
270
+ return url;
271
+ }
272
+ function registerMcp(agents, url, scope, cwd) {
273
+ const name = mcpServerName(url);
274
+ // Global installs never write outside the repo: show what to add instead.
275
+ const results = configureMcp(agents, url, name, cwd, scope === 'project');
276
+ for (const result of results) {
277
+ if (result.status === 'manual') {
278
+ note(result.snippet, `Add to ${result.where}${result.reason ? ` (${result.reason})` : ''}`);
279
+ }
280
+ else {
281
+ log.success(`${result.file} ${result.status === 'written' ? 'updated' : 'already set'} (${name})`);
282
+ }
283
+ }
284
+ return results;
285
+ }
286
+ /** `${ARECA_CMS_URL}/api/mcp/${siteKey}` from the site project, if both are present. */
287
+ export function guessConnectorUrl(cwd) {
288
+ const env = ['.env.local', '.env']
289
+ .map((file) => join(cwd, file))
290
+ .filter((file) => existsSync(file))
291
+ .map((file) => readFileSync(file, 'utf8'))
292
+ .join('\n');
293
+ const cmsUrl = process.env.ARECA_CMS_URL ?? /^\s*ARECA_CMS_URL\s*=\s*["']?([^"'\s#]+)/m.exec(env)?.[1];
294
+ const config = ['areca.config.ts', 'areca.config.mts', 'areca.config.js', 'areca.config.mjs']
295
+ .map((file) => join(cwd, file))
296
+ .find((file) => existsSync(file));
297
+ const siteKey = config ? /siteKey\s*:\s*["'`]([a-z0-9-]+)["'`]/.exec(readFileSync(config, 'utf8'))?.[1] : undefined;
298
+ if (!cmsUrl || !siteKey)
299
+ return undefined;
300
+ return `${cmsUrl.replace(/\/+$/, '')}/api/mcp/${siteKey}`;
301
+ }
302
+ function splitList(values) {
303
+ return (values ?? []).flatMap((value) => value.split(',')).map((value) => value.trim()).filter(Boolean);
304
+ }
305
+ function unwrap(value) {
306
+ if (isCancel(value))
307
+ throw new Cancelled();
308
+ return value;
309
+ }
310
+ function isMainModule() {
311
+ const entry = process.argv[1];
312
+ if (!entry)
313
+ return false;
314
+ try {
315
+ return realpathSync(fileURLToPath(import.meta.url)).toLowerCase() === realpathSync(entry).toLowerCase();
316
+ }
317
+ catch {
318
+ return resolve(fileURLToPath(import.meta.url)).toLowerCase() === resolve(entry).toLowerCase();
319
+ }
320
+ }
321
+ if (isMainModule()) {
322
+ void run(process.argv.slice(2)).then((code) => {
323
+ if (code !== 0)
324
+ process.exit(code);
325
+ });
326
+ }
@@ -0,0 +1,18 @@
1
+ import type { Agent } from './agents.js';
2
+ import { type Skill } from './skills.js';
3
+ export type Scope = 'project' | 'global';
4
+ export type InstallTarget = {
5
+ /** Absolute skills directory, e.g. `<repo>/.agents/skills`. */
6
+ dir: string;
7
+ /** Display path, e.g. `.agents/skills` or `~/.claude/skills`. */
8
+ label: string;
9
+ agents: string[];
10
+ };
11
+ /** One target per unique skills directory, so shared folders get one copy. */
12
+ export declare function resolveTargets(agents: Agent[], scope: Scope, cwd: string, home?: string): InstallTarget[];
13
+ /** Replace each skill folder in each target with a fresh copy. */
14
+ export declare function installSkills(skills: Skill[], targets: InstallTarget[]): string[];
15
+ /** Remove only the named skill folders; other skills in the folder stay. */
16
+ export declare function removeSkills(names: string[], targets: InstallTarget[]): string[];
17
+ /** One `<name>.zip` per skill with the folder at the zip root (Claude app / ChatGPT upload format). */
18
+ export declare function exportZips(skills: Skill[], outDir: string): string[];
@@ -0,0 +1,58 @@
1
+ import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { skillFiles } from './skills.js';
5
+ import { createZip } from './zip.js';
6
+ /** One target per unique skills directory, so shared folders get one copy. */
7
+ export function resolveTargets(agents, scope, cwd, home = homedir()) {
8
+ const byDir = new Map();
9
+ for (const agent of agents) {
10
+ const rel = scope === 'project' ? agent.projectDir : agent.globalDir;
11
+ if (!rel)
12
+ continue;
13
+ const dir = scope === 'project' ? join(cwd, rel) : join(home, rel);
14
+ const label = scope === 'project' ? rel : `~/${rel}`;
15
+ const target = byDir.get(dir) ?? { dir, label, agents: [] };
16
+ target.agents.push(agent.label);
17
+ byDir.set(dir, target);
18
+ }
19
+ return [...byDir.values()];
20
+ }
21
+ /** Replace each skill folder in each target with a fresh copy. */
22
+ export function installSkills(skills, targets) {
23
+ const written = [];
24
+ for (const target of targets) {
25
+ for (const skill of skills) {
26
+ const dest = join(target.dir, skill.name);
27
+ rmSync(dest, { recursive: true, force: true });
28
+ mkdirSync(target.dir, { recursive: true });
29
+ cpSync(skill.dir, dest, { recursive: true });
30
+ written.push(`${target.label}/${skill.name}`);
31
+ }
32
+ }
33
+ return written;
34
+ }
35
+ /** Remove only the named skill folders; other skills in the folder stay. */
36
+ export function removeSkills(names, targets) {
37
+ const removed = [];
38
+ for (const target of targets) {
39
+ for (const name of names) {
40
+ const dest = join(target.dir, name);
41
+ if (!existsSync(dest))
42
+ continue;
43
+ rmSync(dest, { recursive: true, force: true });
44
+ removed.push(`${target.label}/${name}`);
45
+ }
46
+ }
47
+ return removed;
48
+ }
49
+ /** One `<name>.zip` per skill with the folder at the zip root (Claude app / ChatGPT upload format). */
50
+ export function exportZips(skills, outDir) {
51
+ mkdirSync(outDir, { recursive: true });
52
+ return skills.map((skill) => {
53
+ const entries = skillFiles(skill.dir).map(([path, data]) => ({ path: `${skill.name}/${path}`, data }));
54
+ const file = join(outDir, `${skill.name}.zip`);
55
+ writeFileSync(file, createZip(entries));
56
+ return file;
57
+ });
58
+ }
@@ -0,0 +1,20 @@
1
+ import type { Agent, McpTarget } from './agents.js';
2
+ export type McpResult = {
3
+ status: 'written' | 'unchanged';
4
+ file: string;
5
+ }
6
+ /** Could not edit safely (or no project config exists): show this to the user. */
7
+ | {
8
+ status: 'manual';
9
+ where: string;
10
+ snippet: string;
11
+ reason?: string;
12
+ };
13
+ /**
14
+ * Register the connector URL (OAuth only, no secrets) for each agent. Files
15
+ * shared by several agents are written once. Existing servers and keys are kept.
16
+ */
17
+ export declare function configureMcp(agents: Agent[], url: string, name: string, cwd: string, write?: boolean): McpResult[];
18
+ /** The snippet for a target without touching disk (used for global installs). */
19
+ export declare function describeTarget(target: McpTarget, url: string, name: string): McpResult;
20
+ export declare function applyTarget(target: McpTarget, url: string, name: string, cwd: string): McpResult;
@@ -0,0 +1,97 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ /**
4
+ * Register the connector URL (OAuth only, no secrets) for each agent. Files
5
+ * shared by several agents are written once. Existing servers and keys are kept.
6
+ */
7
+ export function configureMcp(agents, url, name, cwd, write = true) {
8
+ const results = [];
9
+ const seen = new Set();
10
+ for (const agent of agents) {
11
+ for (const target of agent.mcp) {
12
+ const key = target.kind === 'manual' ? `manual:${target.where}` : targetPath(target, name);
13
+ if (seen.has(key))
14
+ continue;
15
+ seen.add(key);
16
+ results.push(write ? applyTarget(target, url, name, cwd) : describeTarget(target, url, name));
17
+ }
18
+ }
19
+ return results;
20
+ }
21
+ function targetPath(target, name) {
22
+ return target.kind === 'yaml' ? `${target.dir}/${name}.yaml` : target.file;
23
+ }
24
+ /** The snippet for a target without touching disk (used for global installs). */
25
+ export function describeTarget(target, url, name) {
26
+ if (target.kind === 'manual')
27
+ return { status: 'manual', where: target.where, snippet: target.snippet(name, url) };
28
+ if (target.kind === 'json') {
29
+ return { status: 'manual', where: target.file, snippet: JSON.stringify({ [target.key]: { [name]: target.entry(url) } }, null, 2) };
30
+ }
31
+ if (target.kind === 'toml') {
32
+ return { status: 'manual', where: target.file, snippet: `[mcp_servers.${name}]\nurl = ${JSON.stringify(url)}` };
33
+ }
34
+ return { status: 'manual', where: `${target.dir}/${name}.yaml`, snippet: yamlServer(name, url) };
35
+ }
36
+ export function applyTarget(target, url, name, cwd) {
37
+ switch (target.kind) {
38
+ case 'manual':
39
+ return { status: 'manual', where: target.where, snippet: target.snippet(name, url) };
40
+ case 'json':
41
+ return mergeJson(target.file, target.key, name, target.entry(url), cwd);
42
+ case 'toml':
43
+ return appendToml(target.file, name, url, cwd);
44
+ case 'yaml': {
45
+ const file = `${target.dir}/${name}.yaml`;
46
+ return writeIfChanged(join(cwd, file), file, yamlServer(name, url));
47
+ }
48
+ }
49
+ }
50
+ function mergeJson(file, key, name, entry, cwd) {
51
+ const path = join(cwd, file);
52
+ const snippet = JSON.stringify({ [key]: { [name]: entry } }, null, 2);
53
+ let config = {};
54
+ if (existsSync(path)) {
55
+ const source = readFileSync(path, 'utf8');
56
+ try {
57
+ config = source.trim() ? JSON.parse(source) : {};
58
+ }
59
+ catch {
60
+ // JSONC with comments: rewriting it would drop them, so let the user paste.
61
+ return { status: 'manual', where: file, snippet, reason: 'has comments or invalid JSON' };
62
+ }
63
+ if (!isObject(config))
64
+ return { status: 'manual', where: file, snippet, reason: 'is not a JSON object' };
65
+ }
66
+ const servers = config[key] ?? {};
67
+ if (!isObject(servers))
68
+ return { status: 'manual', where: file, snippet, reason: `"${key}" is not an object` };
69
+ const next = { ...config, [key]: { ...servers, [name]: entry } };
70
+ return writeIfChanged(path, file, `${JSON.stringify(next, null, 2)}\n`);
71
+ }
72
+ function appendToml(file, name, url, cwd) {
73
+ const path = join(cwd, file);
74
+ const section = `[mcp_servers.${name}]\nurl = ${JSON.stringify(url)}\n`;
75
+ const source = existsSync(path) ? readFileSync(path, 'utf8') : '';
76
+ const header = new RegExp(`^\\[mcp_servers\\.(?:${name}|"${name}")\\]`, 'm');
77
+ if (header.test(source)) {
78
+ if (source.includes(`url = ${JSON.stringify(url)}`))
79
+ return { status: 'unchanged', file };
80
+ return { status: 'manual', where: file, snippet: section, reason: `already has [mcp_servers.${name}]` };
81
+ }
82
+ const prefix = source && !source.endsWith('\n') ? `${source}\n` : source;
83
+ return writeIfChanged(path, file, `${prefix}${prefix ? '\n' : ''}${section}`);
84
+ }
85
+ function writeIfChanged(path, file, body) {
86
+ if (existsSync(path) && readFileSync(path, 'utf8') === body)
87
+ return { status: 'unchanged', file };
88
+ mkdirSync(dirname(path), { recursive: true });
89
+ writeFileSync(path, body);
90
+ return { status: 'written', file };
91
+ }
92
+ function yamlServer(name, url) {
93
+ return `name: ${name}\nversion: 0.0.1\nschema: v1\nmcpServers:\n - name: ${name}\n type: streamable-http\n url: ${url}\n`;
94
+ }
95
+ function isObject(value) {
96
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
97
+ }
@@ -0,0 +1,16 @@
1
+ export type Skill = {
2
+ name: string;
3
+ description: string;
4
+ /** Absolute path of the skill folder. */
5
+ dir: string;
6
+ };
7
+ /**
8
+ * Skills ship in the package's `skills/` folder (copied from the repo root at
9
+ * build). From source inside the monorepo, fall back to the repo root copy.
10
+ */
11
+ export declare function skillsRoot(): string;
12
+ export declare function loadSkills(root?: string): Skill[];
13
+ /** Top-level `key: value` pairs of a SKILL.md frontmatter block. */
14
+ export declare function parseFrontmatter(source: string): Record<string, string>;
15
+ /** Every file in a skill folder as [path relative to the folder, contents]. */
16
+ export declare function skillFiles(dir: string): [string, Buffer][];
package/dist/skills.js ADDED
@@ -0,0 +1,55 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /**
5
+ * Skills ship in the package's `skills/` folder (copied from the repo root at
6
+ * build). From source inside the monorepo, fall back to the repo root copy.
7
+ */
8
+ export function skillsRoot() {
9
+ const candidates = [
10
+ fileURLToPath(new URL('../skills', import.meta.url)),
11
+ fileURLToPath(new URL('../../../skills', import.meta.url)),
12
+ ];
13
+ const found = candidates.find((dir) => existsSync(join(dir, 'areca-sdk', 'SKILL.md')));
14
+ if (!found)
15
+ throw new Error(`Bundled skills not found (looked in ${candidates.join(', ')})`);
16
+ return found;
17
+ }
18
+ export function loadSkills(root = skillsRoot()) {
19
+ return readdirSync(root, { withFileTypes: true })
20
+ .filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, 'SKILL.md')))
21
+ .map((entry) => {
22
+ const dir = join(root, entry.name);
23
+ const meta = parseFrontmatter(readFileSync(join(dir, 'SKILL.md'), 'utf8'));
24
+ return { name: meta.name ?? entry.name, description: meta.description ?? '', dir };
25
+ })
26
+ .sort((a, b) => a.name.localeCompare(b.name));
27
+ }
28
+ /** Top-level `key: value` pairs of a SKILL.md frontmatter block. */
29
+ export function parseFrontmatter(source) {
30
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(source);
31
+ if (!match)
32
+ return {};
33
+ const out = {};
34
+ for (const line of match[1].split(/\r?\n/)) {
35
+ const pair = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
36
+ if (pair && pair[2])
37
+ out[pair[1]] = pair[2].replace(/^["']|["']$/g, '');
38
+ }
39
+ return out;
40
+ }
41
+ /** Every file in a skill folder as [path relative to the folder, contents]. */
42
+ export function skillFiles(dir) {
43
+ const out = [];
44
+ const walk = (current) => {
45
+ for (const name of readdirSync(current).sort()) {
46
+ const path = join(current, name);
47
+ if (statSync(path).isDirectory())
48
+ walk(path);
49
+ else
50
+ out.push([relative(dir, path).split('\\').join('/'), readFileSync(path)]);
51
+ }
52
+ };
53
+ walk(dir);
54
+ return out;
55
+ }
package/dist/zip.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type ZipEntry = {
2
+ path: string;
3
+ data: Buffer;
4
+ };
5
+ /** Minimal deflate ZIP writer, enough for skill uploads. No external deps. */
6
+ export declare function createZip(entries: ZipEntry[]): Buffer;
package/dist/zip.js ADDED
@@ -0,0 +1,51 @@
1
+ import { crc32, deflateRawSync } from 'node:zlib';
2
+ /** Minimal deflate ZIP writer, enough for skill uploads. No external deps. */
3
+ export function createZip(entries) {
4
+ const locals = [];
5
+ const centrals = [];
6
+ let offset = 0;
7
+ // 1980-01-01 00:00, so the same skills always give the same bytes.
8
+ const dosTime = 0;
9
+ const dosDate = (0 << 9) | (1 << 5) | 1;
10
+ for (const { path, data } of entries) {
11
+ const name = Buffer.from(path, 'utf8');
12
+ const compressed = deflateRawSync(data);
13
+ const crc = crc32(data);
14
+ const local = Buffer.alloc(30);
15
+ local.writeUInt32LE(0x04034b50, 0);
16
+ local.writeUInt16LE(20, 4); // version needed
17
+ local.writeUInt16LE(0x0800, 6); // UTF-8 names
18
+ local.writeUInt16LE(8, 8); // deflate
19
+ local.writeUInt16LE(dosTime, 10);
20
+ local.writeUInt16LE(dosDate, 12);
21
+ local.writeUInt32LE(crc, 14);
22
+ local.writeUInt32LE(compressed.length, 18);
23
+ local.writeUInt32LE(data.length, 22);
24
+ local.writeUInt16LE(name.length, 26);
25
+ local.writeUInt16LE(0, 28);
26
+ const central = Buffer.alloc(46);
27
+ central.writeUInt32LE(0x02014b50, 0);
28
+ central.writeUInt16LE(20, 4); // version made by
29
+ central.writeUInt16LE(20, 6);
30
+ central.writeUInt16LE(0x0800, 8);
31
+ central.writeUInt16LE(8, 10);
32
+ central.writeUInt16LE(dosTime, 12);
33
+ central.writeUInt16LE(dosDate, 14);
34
+ central.writeUInt32LE(crc, 16);
35
+ central.writeUInt32LE(compressed.length, 20);
36
+ central.writeUInt32LE(data.length, 24);
37
+ central.writeUInt16LE(name.length, 28);
38
+ central.writeUInt32LE(offset, 42);
39
+ locals.push(local, name, compressed);
40
+ centrals.push(central, name);
41
+ offset += local.length + name.length + compressed.length;
42
+ }
43
+ const centralSize = centrals.reduce((sum, part) => sum + part.length, 0);
44
+ const end = Buffer.alloc(22);
45
+ end.writeUInt32LE(0x06054b50, 0);
46
+ end.writeUInt16LE(entries.length, 8);
47
+ end.writeUInt16LE(entries.length, 10);
48
+ end.writeUInt32LE(centralSize, 12);
49
+ end.writeUInt32LE(offset, 16);
50
+ return Buffer.concat([...locals, ...centrals, end]);
51
+ }