scaffold-agent-skill 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aryan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # create-agent-skill
2
+
3
+ Interactively scaffold agent skills and plugins from a single specification. Target multiple developer and agent platforms simultaneously with a clean, validated scaffolding directory.
4
+
5
+ Supported platforms include:
6
+
7
+ - Claude Code Plugin: Generates `.claude-plugin/plugin.json` and `skills/<id>/SKILL.md` alongside individual subagent prompts in `agents/`.
8
+ - OpenAI Custom GPT Action: Generates a serialized `openapi.yaml` and a production-hardened Express server (with helmet, cors, input validation, global error handling, and pinned dependencies).
9
+ - OpenClaw Gateway Skill: Generates `skills/<id>/SKILL.md` (and files for each subagent) alongside a config snippet in `openclaw.json`.
10
+ - Google ADK / Gemini Agent: Generates `agent.py` and a `requirements.txt` file setup with `google-adk` dependencies.
11
+
12
+ ## Key Features
13
+
14
+ - Interactive CLI: Step-by-step prompts to configure your main agent's ID, human-readable name, description, instructions, interfaces, platforms, and subagents.
15
+ - Multi-Platform Code Generation: Generates required files for selected targets simultaneously.
16
+ - Security and Robustness Guards:
17
+ - Path Traversal Guard: Prevents directory escape by checking that output directories remain inside the current working directory.
18
+ - Safe String Escaping: Employs proper escaping for Python strings (triple quote breaks), JavaScript template literals (backticks, expression placeholders), and comments (newlines, comment markers).
19
+ - Validation: Enforces constraints on ID characters (alphanumeric and hyphens only), ID length (maximum 64 characters), descriptions (maximum 200 characters), and instructions (maximum 2000 characters).
20
+ - ID Collision Resolution: Rejects duplicate subagent IDs and prevents subagent IDs from colliding with the main agent ID.
21
+ - Subagent Limit: Restricts subagents to a maximum of 20 to maintain manageable architectures.
22
+ - Clean Casing Utilities: ASCII-safe camelCase, PascalCase, and snake_case converters that strip invalid characters to prevent compiler errors.
23
+ - Overwrite Protection: Prompts for confirmation before writing files into directories that already contain content.
24
+
25
+ ## Quick Start (npx)
26
+
27
+ You can run the tool interactively without installing it:
28
+
29
+ ```bash
30
+ npx create-agent-skill
31
+ ```
32
+
33
+ You will be prompted to:
34
+ 1. Provide a skill ID (kebab-case, e.g. "check-stock").
35
+ 2. Enter a human-readable name and description.
36
+ 3. Provide instructions for what the skill should do.
37
+ 4. Select the target interface types (CLI, service API, or developer SDK).
38
+ 5. Select platforms based on those interfaces (Claude Code, OpenAI, OpenClaw, Google ADK).
39
+ 6. Optionally configure up to 20 subagents (which will be linked as tools).
40
+ 7. Select or confirm the output directory.
41
+
42
+ ## Development and Local Installation
43
+
44
+ Clone the repository and install dependencies:
45
+
46
+ ```bash
47
+ git clone https://github.com/Aryan/create-agent-skill.git
48
+ cd create-agent-skill
49
+ npm install
50
+ ```
51
+
52
+ Run the tool locally:
53
+
54
+ ```bash
55
+ node bin/cli.js
56
+ ```
57
+
58
+ Or link it globally to run it from anywhere on your machine:
59
+
60
+ ```bash
61
+ npm link
62
+ create-agent-skill
63
+ ```
64
+
65
+ ## Generated Outputs and Usage
66
+
67
+ Running the tool with all options will generate the following structure in your output folder:
68
+
69
+ ```
70
+ my-skill/
71
+ ├── .gitignore
72
+ ├── README.md
73
+ ├── .claude-plugin/
74
+ │ └── plugin.json
75
+ ├── skills/
76
+ │ ├── my-skill/
77
+ │ │ └── SKILL.md
78
+ │ └── my-subagent/
79
+ │ └── SKILL.md
80
+ ├── agents/
81
+ │ └── my-subagent.md
82
+ ├── openapi.yaml
83
+ ├── server/
84
+ │ ├── index.js
85
+ │ └── package.json
86
+ ├── agent.py
87
+ ├── requirements.txt
88
+ └── openclaw.json
89
+ ```
90
+
91
+ ### Claude Code Plugin
92
+
93
+ Test the skill locally in Claude Code:
94
+
95
+ ```bash
96
+ cd my-skill
97
+ claude --plugin-dir .
98
+ ```
99
+ Inside Claude Code, invoke your plugin:
100
+ ```
101
+ /my-skill
102
+ ```
103
+
104
+ ### OpenAI Custom GPT Action
105
+
106
+ 1. Install dependencies and start the Express server stub:
107
+ ```bash
108
+ cd my-skill/server
109
+ npm install
110
+ npm start
111
+ ```
112
+ 2. Deploy the server to a hosting provider with HTTPS support (e.g. Railway, Fly.io, Render).
113
+ 3. Update the `servers.url` field in the generated `openapi.yaml` with your deployed URL.
114
+ 4. Paste the contents of `openapi.yaml` into the Actions configuration page of your Custom GPT in ChatGPT.
115
+
116
+ ### OpenClaw Gateway Skill
117
+
118
+ 1. Copy the generated `skills/` folders to your OpenClaw skills directory (usually located at `~/.openclaw/skills/`).
119
+ 2. Merge the configuration options from the generated `openclaw.json` file into your global OpenClaw configuration file (`~/.openclaw/openclaw.json`).
120
+ 3. Run OpenClaw and invoke the skill using your main skill ID.
121
+
122
+ ### Google ADK / Gemini Agent
123
+
124
+ 1. Install the required libraries:
125
+ ```bash
126
+ pip install -r requirements.txt
127
+ ```
128
+ 2. Run your Gemini agent:
129
+ ```bash
130
+ python agent.py "Your query prompt here"
131
+ ```
132
+
133
+ ## Running Tests
134
+
135
+ The codebase comes equipped with a comprehensive stress test suite to verify case converter logic, path containment, and escape safety across all platforms.
136
+
137
+ Run the test suite with:
138
+
139
+ ```bash
140
+ npm test
141
+ ```
142
+
143
+ ## License
144
+
145
+ This project is licensed under the MIT License - see the LICENSE file for details.
package/bin/cli.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli.js';
3
+
4
+ run(process.argv.slice(2)).catch((err) => {
5
+ console.error(err);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "scaffold-agent-skill",
3
+ "version": "0.1.0",
4
+ "description": "Interactively scaffold an AI agent skill or plugin (Claude Code plugin, OpenAI Custom GPT Action, OpenClaw, or Google ADK/Gemini) from a single spec.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "",
8
+ "bin": {
9
+ "scaffold-agent-skill": "bin/cli.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "scripts": {
21
+ "test": "node test/smoke.mjs"
22
+ },
23
+ "keywords": [
24
+ "claude-code",
25
+ "claude-code-plugin",
26
+ "agent",
27
+ "skill",
28
+ "openai",
29
+ "custom-gpt",
30
+ "openclaw",
31
+ "google-adk",
32
+ "gemini",
33
+ "subagent",
34
+ "scaffold",
35
+ "cli",
36
+ "generator"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/YOUR_GITHUB_USERNAME/create-agent-skill.git"
41
+ },
42
+ "dependencies": {
43
+ "js-yaml": "^4.1.0",
44
+ "kleur": "^4.1.5",
45
+ "prompts": "^2.4.2"
46
+ }
47
+ }
package/src/cli.js ADDED
@@ -0,0 +1,441 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import prompts from 'prompts';
4
+ import kleur from 'kleur';
5
+ import { generateClaudeCodePlugin } from './generators/claudeCode.js';
6
+ import { generateOpenAiAction } from './generators/openaiAction.js';
7
+ import { generateOpenclawPlugin } from './generators/openclaw.js';
8
+ import { generateGeminiAdkAgent } from './generators/geminiAdk.js';
9
+
10
+ const KEBAB_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
11
+
12
+ class CancelledError extends Error {
13
+ constructor() {
14
+ super('Cancelled');
15
+ this.name = 'CancelledError';
16
+ }
17
+ }
18
+
19
+ async function isDirectoryNotEmpty(dirPath) {
20
+ try {
21
+ const files = await fs.readdir(dirPath);
22
+ return files.length > 0;
23
+ } catch (err) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ async function writeGitignore(outDir) {
29
+ const content = `# Logs
30
+ logs
31
+ *.log
32
+ npm-debug.log*
33
+
34
+ # Runtime data
35
+ pids
36
+ *.pid
37
+ *.seed
38
+ *.pid.lock
39
+
40
+ # Dependency directories
41
+ node_modules/
42
+ jspm_packages/
43
+
44
+ # Virtual environments
45
+ .venv/
46
+ venv/
47
+ env/
48
+ ENV/
49
+
50
+ # OS files
51
+ .DS_Store
52
+ Thumbs.db
53
+ `;
54
+ await fs.writeFile(path.join(outDir, '.gitignore'), content, 'utf8');
55
+ }
56
+
57
+ export async function run(argv = []) {
58
+ console.log(kleur.bold('\nCreate Agent Skill\n'));
59
+ console.log(
60
+ 'Answer a few questions and this will scaffold a ready-to-edit skill/plugin\nfor Claude Code, OpenAI Custom GPT Actions, OpenClaw, or Google ADK.\n'
61
+ );
62
+
63
+ const cliId = argv[0] && !argv[0].startsWith('-') ? argv[0] : undefined;
64
+
65
+ const questions = [
66
+ {
67
+ type: 'text',
68
+ name: 'id',
69
+ message: 'Skill id (kebab-case, e.g. "check-stock"):',
70
+ initial: cliId,
71
+ validate: (value) => {
72
+ if (!KEBAB_CASE_RE.test(value)) {
73
+ return 'Use lowercase letters, numbers, and hyphens only (e.g. "check-stock").';
74
+ }
75
+ if (value.length > 64) {
76
+ return 'Maximum length is 64 characters.';
77
+ }
78
+ return true;
79
+ },
80
+ },
81
+ {
82
+ type: 'text',
83
+ name: 'nameForHuman',
84
+ message: 'Human-readable name:',
85
+ initial: (prev) => titleCase(prev),
86
+ validate: (value) => value.trim().length > 0 || 'Required.',
87
+ },
88
+ {
89
+ type: 'text',
90
+ name: 'description',
91
+ message: 'One-line description (what does it do?):',
92
+ validate: (value) => {
93
+ if (value.trim().length === 0) return 'Required.';
94
+ if (value.length > 200) return 'Maximum length is 200 characters.';
95
+ return true;
96
+ },
97
+ },
98
+ {
99
+ type: 'text',
100
+ name: 'instructions',
101
+ message: 'Instructions for the model (a sentence or two):',
102
+ validate: (value) => {
103
+ if (value.trim().length === 0) return 'Required.';
104
+ if (value.length > 2000) return 'Maximum length is 2000 characters.';
105
+ return true;
106
+ },
107
+ },
108
+ {
109
+ type: 'text',
110
+ name: 'author',
111
+ message: 'Author name (optional):',
112
+ },
113
+ {
114
+ type: 'multiselect',
115
+ name: 'interfaceTypes',
116
+ message: 'Select target interface types:',
117
+ choices: [
118
+ { title: 'CLI Plugin (run in command line)', value: 'cli', selected: true },
119
+ { title: 'Web App / Service API (run as a web service)', value: 'service', selected: true },
120
+ { title: 'SDK / Developer Framework (run in code)', value: 'sdk', selected: true },
121
+ ],
122
+ min: 1,
123
+ hint: '- Space to select, Enter to confirm',
124
+ },
125
+ {
126
+ type: 'multiselect',
127
+ name: 'platforms',
128
+ message: 'Which platform(s) should this target?',
129
+ choices: (prev, values) => {
130
+ const list = [];
131
+ if (values.interfaceTypes.includes('cli')) {
132
+ list.push({ title: 'Claude Code plugin', value: 'claude-code', selected: true });
133
+ }
134
+ if (values.interfaceTypes.includes('service')) {
135
+ list.push({ title: 'OpenAI Custom GPT Action', value: 'openai-action', selected: true });
136
+ list.push({ title: 'OpenClaw Gateway Skill', value: 'openclaw', selected: true });
137
+ }
138
+ if (values.interfaceTypes.includes('sdk')) {
139
+ list.push({ title: 'Google ADK / Gemini Agent', value: 'gemini-adk', selected: true });
140
+ }
141
+ return list;
142
+ },
143
+ min: 1,
144
+ hint: '- Space to select, Enter to confirm',
145
+ },
146
+ {
147
+ type: 'select',
148
+ name: 'logicType',
149
+ message: 'Select the backend logic template style:',
150
+ choices: [
151
+ { title: 'Simple Mock/Stub', value: 'stub' },
152
+ { title: 'External API Client (fetches from an HTTP endpoint)', value: 'fetch' },
153
+ { title: 'Local Filesystem (reads/writes files in a secure directory)', value: 'fs' },
154
+ { title: 'SQLite Database Utility (runs SQL queries on a local DB)', value: 'database' },
155
+ ],
156
+ initial: 0,
157
+ },
158
+ {
159
+ type: 'confirm',
160
+ name: 'hasSubagents',
161
+ message: 'Do you want to define subagents for delegation?',
162
+ initial: false,
163
+ },
164
+ {
165
+ type: 'text',
166
+ name: 'outDir',
167
+ message: 'Output directory:',
168
+ initial: (prev, values) => values.id,
169
+ validate: (value) => {
170
+ if (!value || value.trim().length === 0) return 'Required.';
171
+ const resolved = path.resolve(process.cwd(), value);
172
+ const relative = path.relative(process.cwd(), resolved);
173
+ if (relative.startsWith('..') || path.isAbsolute(value) || path.win32.isAbsolute(value) || path.posix.isAbsolute(value)) {
174
+ return 'Path traversal detected. Output directory must be within the current working directory.';
175
+ }
176
+ return true;
177
+ },
178
+ },
179
+ ];
180
+
181
+ try {
182
+ const spec = await prompts(questions, {
183
+ onCancel: () => {
184
+ throw new CancelledError();
185
+ },
186
+ });
187
+
188
+ spec.subagents = [];
189
+ if (spec.hasSubagents) {
190
+ console.log(kleur.bold('\n--- Subagents Configuration ---\n'));
191
+ let addMore = true;
192
+ const seenIds = new Set([spec.id]);
193
+ while (addMore) {
194
+ if (spec.subagents.length >= 20) {
195
+ console.log(kleur.yellow('\nMaximum cap of 20 subagents reached.'));
196
+ break;
197
+ }
198
+ const subagentQuestions = [
199
+ {
200
+ type: 'text',
201
+ name: 'id',
202
+ message: 'Subagent id (kebab-case, e.g. "coder-agent"):',
203
+ validate: (value) => {
204
+ if (!KEBAB_CASE_RE.test(value)) {
205
+ return 'Use lowercase letters, numbers, and hyphens only.';
206
+ }
207
+ if (value.length > 64) {
208
+ return 'Maximum length is 64 characters.';
209
+ }
210
+ if (seenIds.has(value)) {
211
+ if (value === spec.id) {
212
+ return 'Subagent ID cannot be the same as the main skill ID.';
213
+ }
214
+ return 'Subagent ID must be unique (this ID is already in use).';
215
+ }
216
+ return true;
217
+ },
218
+ },
219
+ {
220
+ type: 'text',
221
+ name: 'nameForHuman',
222
+ message: 'Human-readable name:',
223
+ initial: (prev) => titleCase(prev),
224
+ validate: (value) => value.trim().length > 0 || 'Required.',
225
+ },
226
+ {
227
+ type: 'text',
228
+ name: 'description',
229
+ message: 'One-line description:',
230
+ validate: (value) => {
231
+ if (value.trim().length === 0) return 'Required.';
232
+ if (value.length > 200) return 'Maximum length is 200 characters.';
233
+ return true;
234
+ },
235
+ },
236
+ {
237
+ type: 'text',
238
+ name: 'instructions',
239
+ message: 'Instructions / system prompt for this subagent:',
240
+ validate: (value) => {
241
+ if (value.trim().length === 0) return 'Required.';
242
+ if (value.length > 2000) return 'Maximum length is 2000 characters.';
243
+ return true;
244
+ },
245
+ },
246
+ {
247
+ type: 'confirm',
248
+ name: 'addMore',
249
+ message: 'Add another subagent?',
250
+ initial: false,
251
+ },
252
+ ];
253
+ const subagentSpec = await prompts(subagentQuestions, {
254
+ onCancel: () => {
255
+ throw new CancelledError();
256
+ },
257
+ });
258
+ addMore = subagentSpec.addMore;
259
+ delete subagentSpec.addMore;
260
+ seenIds.add(subagentSpec.id);
261
+ spec.subagents.push(subagentSpec);
262
+ }
263
+ }
264
+
265
+ const outDir = path.resolve(process.cwd(), spec.outDir);
266
+
267
+ if (await isDirectoryNotEmpty(outDir)) {
268
+ const confirmOverwrite = await prompts({
269
+ type: 'confirm',
270
+ name: 'overwrite',
271
+ message: `The output directory "${spec.outDir}" is not empty. Overwrite existing files?`,
272
+ initial: false,
273
+ }, {
274
+ onCancel: () => {
275
+ throw new CancelledError();
276
+ }
277
+ });
278
+
279
+ if (!confirmOverwrite.overwrite) {
280
+ console.log(kleur.yellow('\nAborted to prevent overwriting existing files.'));
281
+ return;
282
+ }
283
+ }
284
+
285
+ await fs.mkdir(outDir, { recursive: true });
286
+
287
+ const summaryFiles = [];
288
+ const summarySteps = [];
289
+ const successfulPlatforms = [];
290
+ const failedPlatforms = [];
291
+
292
+ if (spec.platforms.includes('claude-code')) {
293
+ try {
294
+ const result = await generateClaudeCodePlugin(spec, outDir);
295
+ summaryFiles.push(...result.files);
296
+ summarySteps.push(...result.nextSteps);
297
+ successfulPlatforms.push('claude-code');
298
+ } catch (err) {
299
+ console.error(kleur.red(`Error generating Claude Code plugin: ${err.message}`));
300
+ failedPlatforms.push({ platform: 'claude-code', error: err.message });
301
+ }
302
+ }
303
+
304
+ if (spec.platforms.includes('openai-action')) {
305
+ try {
306
+ const result = await generateOpenAiAction(spec, outDir);
307
+ summaryFiles.push(...result.files);
308
+ summarySteps.push(...result.nextSteps);
309
+ successfulPlatforms.push('openai-action');
310
+ } catch (err) {
311
+ console.error(kleur.red(`Error generating OpenAI Custom GPT Action: ${err.message}`));
312
+ failedPlatforms.push({ platform: 'openai-action', error: err.message });
313
+ }
314
+ }
315
+
316
+ if (spec.platforms.includes('openclaw')) {
317
+ try {
318
+ const result = await generateOpenclawPlugin(spec, outDir);
319
+ summaryFiles.push(...result.files);
320
+ summarySteps.push(...result.nextSteps);
321
+ successfulPlatforms.push('openclaw');
322
+ } catch (err) {
323
+ console.error(kleur.red(`Error generating OpenClaw Gateway Skill: ${err.message}`));
324
+ failedPlatforms.push({ platform: 'openclaw', error: err.message });
325
+ }
326
+ }
327
+
328
+ if (spec.platforms.includes('gemini-adk')) {
329
+ try {
330
+ const result = await generateGeminiAdkAgent(spec, outDir);
331
+ summaryFiles.push(...result.files);
332
+ summarySteps.push(...result.nextSteps);
333
+ successfulPlatforms.push('gemini-adk');
334
+ } catch (err) {
335
+ console.error(kleur.red(`Error generating Google ADK / Gemini Agent: ${err.message}`));
336
+ failedPlatforms.push({ platform: 'gemini-adk', error: err.message });
337
+ }
338
+ }
339
+
340
+ if (failedPlatforms.length > 0 && successfulPlatforms.length === 0) {
341
+ throw new Error('All platform generators failed.');
342
+ }
343
+
344
+ await writeTopLevelReadme(spec, outDir);
345
+ summaryFiles.push('README.md');
346
+
347
+ await writeGitignore(outDir);
348
+ summaryFiles.push('.gitignore');
349
+
350
+ console.log(kleur.green(`\nDone! Created ${path.relative(process.cwd(), outDir).replace(/\\/g, '/') || '.'}/`));
351
+ console.log('\nFiles:');
352
+ for (const f of summaryFiles) {
353
+ console.log(` ${kleur.cyan(f.replace(/\\/g, '/'))}`);
354
+ }
355
+
356
+ console.log('\nNext steps:');
357
+ for (const step of summarySteps) {
358
+ console.log(` - ${step}`);
359
+ }
360
+ console.log('');
361
+
362
+ if (failedPlatforms.length > 0) {
363
+ console.log(kleur.yellow(`Warning: Some platforms failed to generate:`));
364
+ for (const fp of failedPlatforms) {
365
+ console.log(` - ${kleur.bold(fp.platform)}: ${fp.error}`);
366
+ }
367
+ console.log('');
368
+ }
369
+ } catch (err) {
370
+ if (err instanceof CancelledError || err.name === 'CancelledError') {
371
+ console.log(kleur.yellow('\nCancelled.'));
372
+ return;
373
+ }
374
+ throw err;
375
+ }
376
+ }
377
+
378
+ function titleCase(id = '') {
379
+ return id
380
+ .split(/[-_\s]+/)
381
+ .filter(Boolean)
382
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
383
+ .join(' ');
384
+ }
385
+
386
+ async function writeTopLevelReadme(spec, outDir) {
387
+ const lines = [
388
+ `# ${spec.nameForHuman}`,
389
+ '',
390
+ spec.description,
391
+ '',
392
+ '## Contents',
393
+ '',
394
+ ];
395
+
396
+ if (spec.platforms.includes('claude-code')) {
397
+ lines.push(
398
+ '- `.claude-plugin/plugin.json` + `skills/' + spec.id + '/SKILL.md` — Claude Code plugin.',
399
+ ' Test with `claude --plugin-dir .` from this directory, then try `/' + spec.id + '`.'
400
+ );
401
+ }
402
+
403
+ if (spec.platforms.includes('openai-action')) {
404
+ lines.push(
405
+ '- `openapi.yaml` + `server/` — Express stub and OpenAPI spec for an OpenAI Custom GPT Action.',
406
+ ' Run `cd server && npm install && npm start`, deploy it somewhere with HTTPS, then paste',
407
+ ' `openapi.yaml` into the GPT builder under Configure > Actions.'
408
+ );
409
+ }
410
+
411
+ if (spec.platforms.includes('openclaw')) {
412
+ lines.push(
413
+ '- `skills/` + `openclaw.json` — OpenClaw gateway configuration and skills.',
414
+ ' Copy the folders under `skills/` to your OpenClaw skills path and merge `openclaw.json` into your primary config.'
415
+ );
416
+ }
417
+
418
+ if (spec.platforms.includes('gemini-adk')) {
419
+ lines.push(
420
+ '- `agent.py` + `requirements.txt` — Google ADK / Gemini Agent definition.',
421
+ ' Run `pip install -r requirements.txt` and execute with `python agent.py "query"`.'
422
+ );
423
+ }
424
+
425
+ if (spec.subagents && spec.subagents.length > 0) {
426
+ lines.push(
427
+ '',
428
+ '### Subagents',
429
+ '',
430
+ 'This scaffold includes ' + spec.subagents.length + ' subagent(s) for task delegation:'
431
+ );
432
+ for (const sa of spec.subagents) {
433
+ lines.push(`- **${sa.nameForHuman}** (${sa.id}): ${sa.description}`);
434
+ }
435
+ }
436
+
437
+ lines.push('', '## Generated by', '', '`create-agent-skill` — edit everything above freely, this is just a starting point.', '');
438
+
439
+ await fs.writeFile(path.join(outDir, 'README.md'), lines.join('\n'), 'utf8');
440
+ }
441
+
@@ -0,0 +1,89 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import yaml from 'js-yaml';
4
+
5
+ /**
6
+ * Generates a Claude Code plugin:
7
+ * <out>/.claude-plugin/plugin.json
8
+ * <out>/skills/<id>/SKILL.md
9
+ * <out>/README.md (only if not already created by another generator)
10
+ *
11
+ * @param {import('../schema.js').SkillSpec} spec
12
+ * @param {string} outDir
13
+ */
14
+ export async function generateClaudeCodePlugin(spec, outDir) {
15
+ const pluginDir = path.join(outDir, '.claude-plugin');
16
+ const skillDir = path.join(outDir, 'skills', spec.id);
17
+
18
+ await fs.mkdir(pluginDir, { recursive: true });
19
+ await fs.mkdir(skillDir, { recursive: true });
20
+
21
+ const pluginJson = {
22
+ name: spec.id,
23
+ version: '0.1.0',
24
+ description: spec.description,
25
+ author: spec.author ? { name: spec.author } : undefined,
26
+ };
27
+
28
+ await fs.writeFile(
29
+ path.join(pluginDir, 'plugin.json'),
30
+ JSON.stringify(pluginJson, null, 2) + '\n',
31
+ 'utf8'
32
+ );
33
+
34
+ const logicType = spec.logicType || 'stub';
35
+ let logicTypeInstructions = '';
36
+ if (logicType === 'fetch') {
37
+ logicTypeInstructions = '\n\nThis skill requires fetching data from an external HTTP API. Use curl or similar command line utilities to query resources on the network as needed.';
38
+ } else if (logicType === 'fs') {
39
+ logicTypeInstructions = '\n\nThis skill reads and writes local files to maintain and manage state. Use file access tools to check, modify, or create files within the workspace folder.';
40
+ } else if (logicType === 'database') {
41
+ logicTypeInstructions = '\n\nThis skill interacts with a local SQLite database at "database.db". Use the sqlite3 command-line utility to query or update data in this database.';
42
+ }
43
+
44
+ const mainFrontmatter = yaml.dump({ description: spec.description }).trim();
45
+ const skillMd = `---
46
+ ${mainFrontmatter}
47
+ ---
48
+
49
+ ${spec.instructions.trim()}${logicTypeInstructions}
50
+
51
+ When the user provides additional details after the skill name, they are available as "$ARGUMENTS".
52
+ `;
53
+
54
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd, 'utf8');
55
+
56
+ const files = [
57
+ path.relative(outDir, path.join(pluginDir, 'plugin.json')),
58
+ path.relative(outDir, path.join(skillDir, 'SKILL.md')),
59
+ ];
60
+
61
+ const subagents = spec.subagents || [];
62
+ if (subagents.length > 0) {
63
+ const agentsDir = path.join(outDir, 'agents');
64
+ await fs.mkdir(agentsDir, { recursive: true });
65
+
66
+ for (const sa of subagents) {
67
+ const saFrontmatter = yaml.dump({ description: sa.description }).trim();
68
+ const saMd = `---
69
+ ${saFrontmatter}
70
+ ---
71
+
72
+ ${sa.instructions.trim()}
73
+ `;
74
+ const saPath = path.join(agentsDir, `${sa.id}.md`);
75
+ await fs.writeFile(saPath, saMd, 'utf8');
76
+ files.push(path.relative(outDir, saPath));
77
+ }
78
+ }
79
+
80
+ return {
81
+ files,
82
+ nextSteps: [
83
+ `Test locally with: claude --plugin-dir ${path.relative(process.cwd(), outDir).replace(/\\/g, '/') || '.'}`,
84
+ `Try the skill in Claude Code with: /${spec.id}`,
85
+ 'To share it, add this directory to a plugin marketplace (.claude-plugin/marketplace.json) and have users run /plugin install.',
86
+ ],
87
+ };
88
+ }
89
+
@@ -0,0 +1,153 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import { toSnakeCase, escapePythonString, cleanComment } from '../schema.js';
4
+
5
+ /**
6
+ * Generates Google ADK / Gemini Agent code:
7
+ * <out>/agent.py
8
+ * <out>/requirements.txt
9
+ *
10
+ * @param {import('../schema.js').SkillSpec} spec
11
+ * @param {string} outDir
12
+ */
13
+ export async function generateGeminiAdkAgent(spec, outDir) {
14
+ const generatedFiles = [];
15
+
16
+ const mainAgentVar = toSnakeCase(spec.id);
17
+ const mainAgentName = toSnakeCase(spec.id) + '_agent';
18
+
19
+ // Construct subagents definitions
20
+ const subagents = spec.subagents || [];
21
+ const subagentDefs = [];
22
+ const subagentVars = [];
23
+
24
+ for (const sa of subagents) {
25
+ const saVar = toSnakeCase(sa.id);
26
+ const saName = toSnakeCase(sa.id) + '_agent';
27
+ subagentVars.push(saVar);
28
+ subagentDefs.push(`# Subagent: ${cleanComment(sa.nameForHuman)}
29
+ ${saVar} = LlmAgent(
30
+ model="gemini-2.0-flash-exp",
31
+ name="${saName}",
32
+ description="${escapePythonString(sa.description)}",
33
+ instruction="${escapePythonString(sa.instructions.trim())}",
34
+ )
35
+ `);
36
+ }
37
+
38
+ const logicType = spec.logicType;
39
+ let pythonToolsDefs = '';
40
+ let pythonToolsList = [...subagentVars];
41
+
42
+ if (logicType === 'fetch') {
43
+ pythonToolsDefs = `def fetch_external_api(query: str) -> str:
44
+ """Fetches data from an external API based on a query string."""
45
+ import urllib.request
46
+ import urllib.parse
47
+ import json
48
+ url = f"https://api.example.com/data?query={urllib.parse.quote(query)}"
49
+ try:
50
+ req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
51
+ with urllib.request.urlopen(req) as response:
52
+ return response.read().decode('utf-8')
53
+ except Exception as e:
54
+ return f"Error fetching from external API: {str(e)}"
55
+
56
+ `;
57
+ pythonToolsList.push('fetch_external_api');
58
+ } else if (logicType === 'fs') {
59
+ pythonToolsDefs = `def interact_with_filesystem(filename: str, content: str = None) -> str:
60
+ """Reads or writes text content from/to a local file securely."""
61
+ import os
62
+ safe_path = os.path.basename(filename)
63
+ try:
64
+ if content is not None:
65
+ with open(safe_path, 'w', encoding='utf-8') as f:
66
+ f.write(content)
67
+ return f"Successfully wrote content to {safe_path}"
68
+ else:
69
+ if not os.path.exists(safe_path):
70
+ return f"File {safe_path} does not exist"
71
+ with open(safe_path, 'r', encoding='utf-8') as f:
72
+ return f.read()
73
+ except Exception as e:
74
+ return f"Filesystem error: {str(e)}"
75
+
76
+ `;
77
+ pythonToolsList.push('interact_with_filesystem');
78
+ } else if (logicType === 'database') {
79
+ pythonToolsDefs = `def query_local_database(sql_query: str) -> str:
80
+ """Executes a SQL query on the local SQLite database and returns the rows as JSON."""
81
+ import sqlite3
82
+ import json
83
+ try:
84
+ conn = sqlite3.connect("database.db")
85
+ cursor = conn.cursor()
86
+ cursor.execute("CREATE TABLE IF NOT EXISTS queries (id INTEGER PRIMARY KEY, val TEXT)")
87
+ conn.commit()
88
+ cursor.execute(sql_query)
89
+ if sql_query.strip().upper().startswith("SELECT"):
90
+ rows = cursor.fetchall()
91
+ conn.close()
92
+ return json.dumps(rows)
93
+ else:
94
+ conn.commit()
95
+ conn.close()
96
+ return "Query executed successfully"
97
+ except Exception as e:
98
+ return f"Database error: {str(e)}"
99
+
100
+ `;
101
+ pythonToolsList.push('query_local_database');
102
+ } else if (logicType === 'stub') {
103
+ pythonToolsDefs = `def stub_tool(input_val: str) -> str:
104
+ """A placeholder tool that echoes the input parameter."""
105
+ return f"Stub tool received: {input_val}"
106
+
107
+ `;
108
+ pythonToolsList.push('stub_tool');
109
+ }
110
+
111
+ const subagentsSection = subagentDefs.length > 0 ? subagentDefs.join('\n') + '\n' : '';
112
+ const toolsList = pythonToolsList.length > 0 ? `tools=[${pythonToolsList.join(', ')}]` : 'tools=[]';
113
+
114
+ const agentPy = `from google.adk.agents import LlmAgent
115
+
116
+ ${pythonToolsDefs}
117
+ ${subagentsSection}# Main Agent: ${cleanComment(spec.nameForHuman)}
118
+ ${mainAgentVar} = LlmAgent(
119
+ model="gemini-2.0-flash-exp",
120
+ name="${mainAgentName}",
121
+ description="${escapePythonString(spec.description)}",
122
+ instruction="${escapePythonString(spec.instructions.trim())}",
123
+ ${toolsList},
124
+ )
125
+
126
+ if __name__ == "__main__":
127
+ import sys
128
+ query = sys.argv[1] if len(sys.argv) > 1 else "Hello"
129
+ print(f"Running agent: {${mainAgentVar}.name} with query: '{query}'")
130
+ # Example execution:
131
+ # response = ${mainAgentVar}.run(query)
132
+ # print(response)
133
+ `;
134
+
135
+ const agentPyPath = path.join(outDir, 'agent.py');
136
+ await fs.writeFile(agentPyPath, agentPy, 'utf8');
137
+ generatedFiles.push(path.relative(outDir, agentPyPath));
138
+
139
+ const requirementsTxt = `google-adk>=0.1.0
140
+ `;
141
+ const reqPath = path.join(outDir, 'requirements.txt');
142
+ await fs.writeFile(reqPath, requirementsTxt, 'utf8');
143
+ generatedFiles.push(path.relative(outDir, reqPath));
144
+
145
+ return {
146
+ files: generatedFiles,
147
+ nextSteps: [
148
+ 'Install dependencies with: pip install -r requirements.txt',
149
+ `Run the agent locally using: python agent.py "your-prompt-here"`,
150
+ ],
151
+ };
152
+ }
153
+
@@ -0,0 +1,281 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import yaml from 'js-yaml';
4
+ import { toCamelCase, escapeJsTemplateLiteral, escapeJsSingleQuoteString, cleanComment } from '../schema.js';
5
+
6
+ /**
7
+ * Generates a secure OpenAPI spec + Express server stub suitable for
8
+ * pasting into a Custom GPT's Actions config, or wrapping in an MCP
9
+ * server later.
10
+ *
11
+ * <out>/openapi.yaml
12
+ * <out>/server/index.js
13
+ * <out>/server/package.json
14
+ *
15
+ * @param {import('../schema.js').SkillSpec} spec
16
+ * @param {string} outDir
17
+ */
18
+ export async function generateOpenAiAction(spec, outDir) {
19
+ const serverDir = path.join(outDir, 'server');
20
+ await fs.mkdir(serverDir, { recursive: true });
21
+
22
+ const handlerName = toCamelCase(spec.id);
23
+ const endpointPath = spec.endpointPath || `/${spec.id}`;
24
+
25
+ const openapiObj = {
26
+ openapi: '3.1.0',
27
+ info: {
28
+ title: spec.nameForHuman,
29
+ description: spec.description,
30
+ version: '0.1.0',
31
+ },
32
+ servers: [
33
+ {
34
+ url: 'https://YOUR-DEPLOYED-URL.example.com',
35
+ },
36
+ ],
37
+ paths: {
38
+ [endpointPath]: {
39
+ get: {
40
+ operationId: handlerName,
41
+ summary: spec.description,
42
+ parameters: [
43
+ {
44
+ name: 'input',
45
+ in: 'query',
46
+ required: true,
47
+ schema: {
48
+ type: 'string',
49
+ },
50
+ description: `Free-form input for "${spec.nameForHuman}"`,
51
+ },
52
+ ],
53
+ responses: {
54
+ 200: {
55
+ description: 'Successful response',
56
+ content: {
57
+ 'application/json': {
58
+ schema: {
59
+ type: 'object',
60
+ properties: {
61
+ result: {
62
+ type: 'string',
63
+ },
64
+ },
65
+ },
66
+ },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ };
74
+
75
+ const subagents = spec.subagents || [];
76
+ for (const sa of subagents) {
77
+ const saHandler = toCamelCase(sa.id);
78
+ const saPath = `/agents/${sa.id}`;
79
+ openapiObj.paths[saPath] = {
80
+ get: {
81
+ operationId: saHandler,
82
+ summary: sa.description,
83
+ parameters: [
84
+ {
85
+ name: 'input',
86
+ in: 'query',
87
+ required: true,
88
+ schema: {
89
+ type: 'string',
90
+ },
91
+ description: `Free-form input for subagent "${sa.nameForHuman}"`,
92
+ },
93
+ ],
94
+ responses: {
95
+ 200: {
96
+ description: 'Successful response',
97
+ content: {
98
+ 'application/json': {
99
+ schema: {
100
+ type: 'object',
101
+ properties: {
102
+ result: {
103
+ type: 'string',
104
+ },
105
+ },
106
+ },
107
+ },
108
+ },
109
+ },
110
+ },
111
+ },
112
+ };
113
+ }
114
+
115
+ const openapiYaml = yaml.dump(openapiObj);
116
+ await fs.writeFile(path.join(outDir, 'openapi.yaml'), openapiYaml, 'utf8');
117
+
118
+ const logicType = spec.logicType || 'stub';
119
+
120
+ let subagentRoutes = '';
121
+ for (const sa of subagents) {
122
+ const saNameEscaped = escapeJsTemplateLiteral(sa.nameForHuman);
123
+ const saDescComment = cleanComment(sa.description);
124
+ const saInstComment = cleanComment(sa.instructions.trim().split('\n')[0]);
125
+ subagentRoutes += `
126
+ // Subagent: ${saNameEscaped} - ${saDescComment}
127
+ // Instructions: ${saInstComment}
128
+ app.get('/agents/${sa.id}', async (req, res) => {
129
+ const { input } = req.query;
130
+ if (!input || typeof input !== 'string' || input.trim().length === 0) {
131
+ return res.status(400).json({ error: 'Missing or invalid required query parameter: input' });
132
+ }
133
+
134
+ // TODO: replace with real logic for "${saNameEscaped}"
135
+ res.json({ result: \`[${saNameEscaped}] Received input: \${input}\` });
136
+ });
137
+ `;
138
+ }
139
+
140
+ const mainNameEscaped = escapeJsTemplateLiteral(spec.nameForHuman);
141
+ const mainDescComment = cleanComment(spec.description);
142
+ const mainInstComment = cleanComment(spec.instructions.trim().split('\n')[0]);
143
+
144
+ let importsList = `import express from 'express';
145
+ import cors from 'cors';
146
+ import helmet from 'helmet';`;
147
+
148
+ let mainHandlerBody = '';
149
+
150
+ if (logicType === 'fetch') {
151
+ mainHandlerBody = ` // Fetch data from an external API
152
+ const url = \`https://api.example.com/data?query=\${encodeURIComponent(input)}\`;
153
+ try {
154
+ const response = await fetch(url);
155
+ if (!response.ok) {
156
+ return res.status(response.status).json({ error: \`External API responded with status \${response.status}\` });
157
+ }
158
+ const data = await response.json();
159
+ res.json({ result: data });
160
+ } catch (err) {
161
+ res.status(500).json({ error: \`Failed to fetch external API: \${err.message}\` });
162
+ }`;
163
+ } else if (logicType === 'fs') {
164
+ importsList += `\nimport fs from 'node:fs/promises';\nimport path from 'node:path';`;
165
+ mainHandlerBody = ` // Read and write data securely to local files
166
+ const dataFilePath = path.join(process.cwd(), 'data.json');
167
+ try {
168
+ let currentData = {};
169
+ try {
170
+ const content = await fs.readFile(dataFilePath, 'utf8');
171
+ currentData = JSON.parse(content);
172
+ } catch (readErr) {
173
+ // Ignore if file doesn't exist yet
174
+ }
175
+ currentData[new Date().toISOString()] = input;
176
+ await fs.writeFile(dataFilePath, JSON.stringify(currentData, null, 2), 'utf8');
177
+ res.json({ result: \`Successfully wrote input to local file data.json. Total entries: \${Object.keys(currentData).length}\` });
178
+ } catch (err) {
179
+ res.status(500).json({ error: \`Failed to perform filesystem operation: \${err.message}\` });
180
+ }`;
181
+ } else if (logicType === 'database') {
182
+ importsList += `\nimport sqlite3 from 'sqlite3';\nimport path from 'node:path';`;
183
+ mainHandlerBody = ` // Connect to local SQLite database and query
184
+ const dbPath = path.join(process.cwd(), 'database.db');
185
+ const db = new sqlite3.Database(dbPath);
186
+ db.serialize(() => {
187
+ db.run("CREATE TABLE IF NOT EXISTS queries (id INTEGER PRIMARY KEY, val TEXT)");
188
+ db.run("INSERT INTO queries (val) VALUES (?)", [input], function(err) {
189
+ if (err) {
190
+ db.close();
191
+ return res.status(500).json({ error: \`Database insert error: \${err.message}\` });
192
+ }
193
+ db.all("SELECT * FROM queries ORDER BY id DESC LIMIT 5", [], (selectErr, rows) => {
194
+ db.close();
195
+ if (selectErr) {
196
+ return res.status(500).json({ error: \`Database select error: \${selectErr.message}\` });
197
+ }
198
+ res.json({ result: \`Inserted entry. Last 5 entries: \${JSON.stringify(rows)}\` });
199
+ });
200
+ });
201
+ });`;
202
+ } else {
203
+ mainHandlerBody = ` // TODO: replace with real logic for "${mainNameEscaped}"
204
+ res.json({ result: \`Received input: \${input}\` });`;
205
+ }
206
+
207
+ const serverIndexJs = `${importsList}
208
+
209
+ const app = express();
210
+ const port = process.env.PORT || 3000;
211
+
212
+ app.use(helmet());
213
+ app.use(cors()); // Configure allowed origins, methods, and headers for production
214
+ app.use(express.json());
215
+
216
+ // Main Skill: ${mainDescComment}
217
+ // Instructions: ${mainInstComment}
218
+ app.get('${escapeJsSingleQuoteString(endpointPath)}', async (req, res) => {
219
+ const { input } = req.query;
220
+ if (!input || typeof input !== 'string' || input.trim().length === 0) {
221
+ return res.status(400).json({ error: 'Missing or invalid required query parameter: input' });
222
+ }
223
+
224
+ ${mainHandlerBody}
225
+ });
226
+ ${subagentRoutes}
227
+ // Global Error Handler
228
+ app.use((err, req, res, next) => {
229
+ console.error(err.stack);
230
+ res.status(500).json({ error: 'Internal Server Error' });
231
+ });
232
+
233
+ app.listen(port, () => {
234
+ console.log(\`${mainNameEscaped} action server listening on port \${port}\`);
235
+ });
236
+ `;
237
+
238
+ await fs.writeFile(path.join(serverDir, 'index.js'), serverIndexJs, 'utf8');
239
+
240
+ const serverDependencies = {
241
+ express: '4.21.0',
242
+ cors: '2.8.5',
243
+ helmet: '7.1.0',
244
+ };
245
+ if (logicType === 'database') {
246
+ serverDependencies.sqlite3 = '5.1.7';
247
+ }
248
+
249
+ const serverPackageJson = {
250
+ name: `${spec.id}-action-server`,
251
+ version: '0.1.0',
252
+ private: true,
253
+ type: 'module',
254
+ main: 'index.js',
255
+ scripts: {
256
+ start: 'node index.js',
257
+ },
258
+ dependencies: serverDependencies,
259
+ };
260
+
261
+ await fs.writeFile(
262
+ path.join(serverDir, 'package.json'),
263
+ JSON.stringify(serverPackageJson, null, 2) + '\n',
264
+ 'utf8'
265
+ );
266
+
267
+ return {
268
+ files: [
269
+ path.relative(outDir, path.join(outDir, 'openapi.yaml')),
270
+ path.relative(outDir, path.join(serverDir, 'index.js')),
271
+ path.relative(outDir, path.join(serverDir, 'package.json')),
272
+ ],
273
+ nextSteps: [
274
+ 'Deploy server/ somewhere reachable over HTTPS (e.g. a free-tier host), then update the `servers.url` in openapi.yaml.',
275
+ 'In ChatGPT, create a Custom GPT, open Configure > Actions, and paste in the contents of openapi.yaml.',
276
+ 'Set an auth method in the Actions config if your endpoint needs one (none / API key / OAuth).',
277
+ 'Run "npm install" inside the server directory and run "npm audit" to check for vulnerabilities.',
278
+ ],
279
+ };
280
+ }
281
+
@@ -0,0 +1,100 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import yaml from 'js-yaml';
4
+
5
+ /**
6
+ * Generates OpenClaw config and skills:
7
+ * <out>/skills/<id>/SKILL.md
8
+ * <out>/skills/<subagent-id>/SKILL.md (for each subagent)
9
+ * <out>/openclaw.json
10
+ *
11
+ * @param {import('../schema.js').SkillSpec} spec
12
+ * @param {string} outDir
13
+ */
14
+ export async function generateOpenclawPlugin(spec, outDir) {
15
+ const generatedFiles = [];
16
+ const skillsDir = path.join(outDir, 'skills');
17
+ await fs.mkdir(skillsDir, { recursive: true });
18
+
19
+ const logicType = spec.logicType || 'stub';
20
+ let logicTypeInstructions = '';
21
+ if (logicType === 'fetch') {
22
+ logicTypeInstructions = '\n\nThis skill requires fetching data from an external HTTP API. Use curl or similar command line utilities to query resources on the network as needed.';
23
+ } else if (logicType === 'fs') {
24
+ logicTypeInstructions = '\n\nThis skill reads and writes local files to maintain and manage state. Use file access tools to check, modify, or create files within the workspace folder.';
25
+ } else if (logicType === 'database') {
26
+ logicTypeInstructions = '\n\nThis skill interacts with a local SQLite database at "database.db". Use the sqlite3 command-line utility to query or update data in this database.';
27
+ }
28
+
29
+ // 1. Generate main skill file
30
+ const mainSkillDir = path.join(skillsDir, spec.id);
31
+ await fs.mkdir(mainSkillDir, { recursive: true });
32
+ const mainFrontmatter = yaml.dump({ description: spec.description }).trim();
33
+ const mainSkillMd = `---
34
+ ${mainFrontmatter}
35
+ ---
36
+
37
+ ${spec.instructions.trim()}${logicTypeInstructions}
38
+ `;
39
+ const mainSkillPath = path.join(mainSkillDir, 'SKILL.md');
40
+ await fs.writeFile(mainSkillPath, mainSkillMd, 'utf8');
41
+ generatedFiles.push(path.relative(outDir, mainSkillPath));
42
+
43
+ // 2. Generate subagent skill files
44
+ const subagents = spec.subagents || [];
45
+ for (const sa of subagents) {
46
+ const saSkillDir = path.join(skillsDir, sa.id);
47
+ await fs.mkdir(saSkillDir, { recursive: true });
48
+ const saFrontmatter = yaml.dump({ description: sa.description }).trim();
49
+ const saSkillMd = `---
50
+ ${saFrontmatter}
51
+ ---
52
+
53
+ ${sa.instructions.trim()}
54
+ `;
55
+ const saSkillPath = path.join(saSkillDir, 'SKILL.md');
56
+ await fs.writeFile(saSkillPath, saSkillMd, 'utf8');
57
+ generatedFiles.push(path.relative(outDir, saSkillPath));
58
+ }
59
+
60
+ // 3. Generate openclaw.json config snippet
61
+ const openclawJson = {
62
+ agents: {
63
+ [spec.id]: {
64
+ model: {
65
+ primary: 'anthropic/claude-3-5-sonnet',
66
+ fallback: 'openai/gpt-4o',
67
+ },
68
+ workspace: './workspace',
69
+ tools: ['file', 'shell', 'browser'],
70
+ skills: [spec.id, ...subagents.map((sa) => sa.id)],
71
+ },
72
+ },
73
+ };
74
+
75
+ // Add individual subagent profiles to the config
76
+ for (const sa of subagents) {
77
+ openclawJson.agents[sa.id] = {
78
+ model: {
79
+ primary: 'anthropic/claude-3-5-sonnet',
80
+ },
81
+ workspace: './workspace',
82
+ tools: ['file'],
83
+ skills: [sa.id],
84
+ };
85
+ }
86
+
87
+ const configPath = path.join(outDir, 'openclaw.json');
88
+ await fs.writeFile(configPath, JSON.stringify(openclawJson, null, 2) + '\n', 'utf8');
89
+ generatedFiles.push(path.relative(outDir, configPath));
90
+
91
+ return {
92
+ files: generatedFiles,
93
+ nextSteps: [
94
+ 'Copy the generated `skills/` folders to your OpenClaw skills directory (typically `~/.openclaw/skills/`).',
95
+ 'Merge the generated `openclaw.json` configuration into your global `~/.openclaw/openclaw.json` file.',
96
+ `Run OpenClaw and target the agent using the ID "${spec.id}".`,
97
+ ],
98
+ };
99
+ }
100
+
package/src/schema.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The internal "spec" object collected from the user via prompts.
3
+ * This mirrors the modular schema described in the design doc:
4
+ * a small, platform-agnostic description of a single skill/tool
5
+ * that generators can translate into platform-specific artifacts.
6
+ *
7
+ * @typedef {Object} SubagentSpec
8
+ * @property {string} id kebab-case identifier, e.g. "coder-agent"
9
+ * @property {string} nameForHuman Human-readable name, e.g. "Coder Agent"
10
+ * @property {string} description One-line description of what it does
11
+ * @property {string} instructions Instructions / system prompt for this subagent
12
+ *
13
+ * @typedef {Object} SkillSpec
14
+ * @property {string} id kebab-case identifier, e.g. "check-stock"
15
+ * @property {string} nameForHuman Human-readable name, e.g. "Check Stock"
16
+ * @property {string} description One-line description of what it does
17
+ * @property {string} author Author name (optional)
18
+ * @property {string} instructions What the skill/tool should do, in prose
19
+ * @property {string[]} platforms Subset of ["claude-code", "openai-action", "openclaw", "gemini-adk"]
20
+ * @property {string} endpointPath HTTP path used for the OpenAI Action stub, e.g. "/check-stock"
21
+ * @property {SubagentSpec[]} subagents Array of subagents (optional)
22
+ */
23
+
24
+ export function toPascalCase(id) {
25
+ if (!id || typeof id !== 'string' || id.trim().length === 0) {
26
+ throw new Error('Input must be a non-empty string');
27
+ }
28
+ // Strip non-ASCII characters first
29
+ const clean = id.replace(/[^\x00-\x7F]/g, '');
30
+ // Strip characters that are not alphanumeric, spaces, hyphens, or underscores
31
+ const cleanAlphaNum = clean.replace(/[^a-zA-Z0-9\s-_]/g, '').trim();
32
+ if (cleanAlphaNum.length === 0) {
33
+ throw new Error('Input contains no valid characters for casing');
34
+ }
35
+ return cleanAlphaNum
36
+ .split(/[-_\s]+/)
37
+ .filter(Boolean)
38
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
39
+ .join('');
40
+ }
41
+
42
+ export function toCamelCase(id) {
43
+ const pascal = toPascalCase(id);
44
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
45
+ }
46
+
47
+ export function toSnakeCase(id) {
48
+ if (!id || typeof id !== 'string' || id.trim().length === 0) {
49
+ throw new Error('Input must be a non-empty string');
50
+ }
51
+ const clean = id.replace(/[^\x00-\x7F]/g, '');
52
+ const cleanAlphaNum = clean.replace(/[^a-zA-Z0-9\s-_]/g, '').trim();
53
+ if (cleanAlphaNum.length === 0) {
54
+ throw new Error('Input contains no valid characters for casing');
55
+ }
56
+ return cleanAlphaNum
57
+ .split(/[-_\s]+/)
58
+ .filter(Boolean)
59
+ .map((part) => part.toLowerCase())
60
+ .join('_');
61
+ }
62
+
63
+ export function escapePythonString(val) {
64
+ if (typeof val !== 'string') return '';
65
+ return val
66
+ .replace(/\\/g, '\\\\')
67
+ .replace(/"/g, '\\"')
68
+ .replace(/'/g, "\\'")
69
+ .replace(/\n/g, '\\n')
70
+ .replace(/\r/g, '\\r')
71
+ .replace(/\t/g, '\\t');
72
+ }
73
+
74
+ export function escapeJsTemplateLiteral(val) {
75
+ if (typeof val !== 'string') return '';
76
+ return val
77
+ .replace(/\\/g, '\\\\')
78
+ .replace(/`/g, '\\`')
79
+ .replace(/\${/g, '\\${');
80
+ }
81
+
82
+ export function escapeJsSingleQuoteString(val) {
83
+ if (typeof val !== 'string') return '';
84
+ return val
85
+ .replace(/\\/g, '\\\\')
86
+ .replace(/'/g, "\\'");
87
+ }
88
+
89
+ export function cleanComment(val) {
90
+ if (typeof val !== 'string') return '';
91
+ return val.replace(/[\r\n]/g, ' ').replace(/\*\//g, '* /');
92
+ }
93
+
94
+