@crouton-kit/crouter 0.2.5 → 0.3.1
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/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +9 -9
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +19 -19
- package/dist/cli.js +42 -37
- package/dist/commands/__tests__/human.test.d.ts +1 -0
- package/dist/commands/__tests__/human.test.js +214 -0
- package/dist/commands/__tests__/skill.test.d.ts +1 -0
- package/dist/commands/__tests__/skill.test.js +287 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +179 -0
- package/dist/commands/flow.d.ts +2 -0
- package/dist/commands/flow.js +24 -0
- package/dist/commands/human.d.ts +2 -0
- package/dist/commands/human.js +480 -0
- package/dist/commands/job.d.ts +2 -0
- package/dist/commands/job.js +669 -0
- package/dist/commands/pkg.d.ts +2 -0
- package/dist/commands/pkg.js +1021 -0
- package/dist/commands/plan.d.ts +4 -2
- package/dist/commands/plan.js +306 -22
- package/dist/commands/skill.d.ts +2 -2
- package/dist/commands/skill.js +615 -459
- package/dist/commands/spec.d.ts +3 -2
- package/dist/commands/spec.js +283 -10
- package/dist/commands/sys.d.ts +2 -0
- package/dist/commands/sys.js +712 -0
- package/dist/core/__tests__/argv-parser.test.d.ts +1 -0
- package/dist/core/__tests__/argv-parser.test.js +199 -0
- package/dist/core/__tests__/flow-leaves.test.d.ts +1 -0
- package/dist/core/__tests__/flow-leaves.test.js +248 -0
- package/dist/core/__tests__/job.test.d.ts +1 -0
- package/dist/core/__tests__/job.test.js +346 -0
- package/dist/core/__tests__/pkg.test.d.ts +1 -0
- package/dist/core/__tests__/pkg.test.js +218 -0
- package/dist/core/__tests__/sys.test.d.ts +1 -0
- package/dist/core/__tests__/sys.test.js +208 -0
- package/dist/core/artifact.d.ts +29 -18
- package/dist/core/artifact.js +78 -221
- package/dist/core/auto-update.js +11 -3
- package/dist/core/command.d.ts +36 -0
- package/dist/core/command.js +287 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +5 -0
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +4 -0
- package/dist/core/help.d.ts +98 -0
- package/dist/core/help.js +163 -0
- package/dist/core/io.d.ts +29 -0
- package/dist/core/io.js +83 -0
- package/dist/core/jobs.d.ts +87 -0
- package/dist/core/jobs.js +353 -0
- package/dist/core/pagination.d.ts +33 -0
- package/dist/core/pagination.js +89 -0
- package/dist/core/self-update.d.ts +21 -0
- package/dist/{commands/update.js → core/self-update.js} +28 -63
- package/dist/core/spawn.d.ts +47 -65
- package/dist/core/spawn.js +78 -228
- package/dist/prompts/agent.d.ts +10 -5
- package/dist/prompts/agent.js +51 -74
- package/dist/prompts/debug.d.ts +8 -0
- package/dist/prompts/debug.js +37 -0
- package/dist/prompts/review.js +4 -11
- package/dist/prompts/skill.d.ts +0 -1
- package/dist/prompts/skill.js +95 -149
- package/package.json +4 -2
- package/dist/commands/agent.d.ts +0 -2
- package/dist/commands/agent.js +0 -265
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -146
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -268
- package/dist/commands/marketplace.d.ts +0 -2
- package/dist/commands/marketplace.js +0 -365
- package/dist/commands/plugin.d.ts +0 -2
- package/dist/commands/plugin.js +0 -367
- package/dist/commands/update.d.ts +0 -4
- package/dist/prompts/plan.d.ts +0 -1
- package/dist/prompts/plan.js +0 -175
- package/dist/prompts/spec.d.ts +0 -1
- package/dist/prompts/spec.js +0 -153
package/dist/commands/skill.js
CHANGED
|
@@ -1,37 +1,30 @@
|
|
|
1
|
+
// `crtr skill` subtree handlers — P3 implementation.
|
|
2
|
+
// Sub-branches: find {list, search, grep}, read {show, where}, author {guide, scaffold}, state {enable, disable}.
|
|
1
3
|
import { join } from 'node:path';
|
|
2
4
|
import { writeFileSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { out, hint, info, jsonOut, handleError, } from '../core/output.js';
|
|
7
|
-
import { listScopes, requireScopeRoot, resolveScopeArg, projectScopeRoot, scopeSkillsDir, } from '../core/scope.js';
|
|
5
|
+
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
6
|
+
import { usage, general, notFound } from '../core/errors.js';
|
|
7
|
+
import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, SKILL_TYPES, isSkillType, skillConfigKey, } from '../types.js';
|
|
8
8
|
import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, listSkillSiblings, listSkillChildren, } from '../core/resolver.js';
|
|
9
|
-
import {
|
|
9
|
+
import { listScopes, resolveScopeArg, requireScopeRoot, scopeSkillsDir, projectScopeRoot, } from '../core/scope.js';
|
|
10
10
|
import { parseFrontmatter, serializeFrontmatter } from '../core/frontmatter.js';
|
|
11
|
+
import { updateConfig, ensureScopeInitialized } from '../core/config.js';
|
|
12
|
+
import { paginate } from '../core/pagination.js';
|
|
11
13
|
import { ensureDir, pathExists, readText, walkFiles } from '../core/fs-utils.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
'path',
|
|
17
|
-
'grep',
|
|
18
|
-
'new',
|
|
19
|
-
'create',
|
|
20
|
-
'template',
|
|
21
|
-
'where',
|
|
22
|
-
'enable',
|
|
23
|
-
'disable',
|
|
24
|
-
'search',
|
|
25
|
-
]);
|
|
26
|
-
function buildShowFooter(skillPath) {
|
|
27
|
-
return (`crtr: edit this skill directly at ${skillPath} — ` +
|
|
28
|
-
`for SKILL.md format + authoring workflow run \`crtr skill\` (no args)`);
|
|
29
|
-
}
|
|
30
|
-
function wrapSkill(name, path, content) {
|
|
31
|
-
return `<skill name="${name}" path="${path}">\n${content.endsWith('\n') ? content : content + '\n'}</skill>`;
|
|
32
|
-
}
|
|
14
|
+
import { skillCreatePrompt, skillTemplatePrompt } from '../prompts/skill.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Neighbors section (ported from old impl)
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
33
18
|
function formatNeighborQualifier(s) {
|
|
34
|
-
return s.plugin === SCOPE_SKILL_PLUGIN
|
|
19
|
+
return s.plugin === SCOPE_SKILL_PLUGIN
|
|
20
|
+
? `${s.scope}:${s.name}`
|
|
21
|
+
: `${s.plugin}/${s.name}`;
|
|
22
|
+
}
|
|
23
|
+
function formatNeighborKeywords(s) {
|
|
24
|
+
const kw = s.frontmatter.keywords;
|
|
25
|
+
if (!kw || kw.length === 0)
|
|
26
|
+
return '';
|
|
27
|
+
return ` — [${kw.join(', ')}]`;
|
|
35
28
|
}
|
|
36
29
|
function buildNeighborsSection(skill) {
|
|
37
30
|
const siblings = listSkillSiblings(skill);
|
|
@@ -40,14 +33,13 @@ function buildNeighborsSection(skill) {
|
|
|
40
33
|
return null;
|
|
41
34
|
const lines = [
|
|
42
35
|
'## Neighbors',
|
|
43
|
-
'*Auto-discovered from filesystem.
|
|
36
|
+
'*Auto-discovered from filesystem. Run `crtr skill read show <name>` for full description + body.*',
|
|
44
37
|
'',
|
|
45
38
|
];
|
|
46
39
|
if (siblings.length > 0) {
|
|
47
40
|
lines.push('**Siblings:**');
|
|
48
41
|
for (const s of siblings) {
|
|
49
|
-
|
|
50
|
-
lines.push(`- \`${formatNeighborQualifier(s)}\`${desc ? ` — ${desc}` : ''}`);
|
|
42
|
+
lines.push(`- \`${formatNeighborQualifier(s)}\`${formatNeighborKeywords(s)}`);
|
|
51
43
|
}
|
|
52
44
|
if (children.length > 0)
|
|
53
45
|
lines.push('');
|
|
@@ -55,479 +47,643 @@ function buildNeighborsSection(skill) {
|
|
|
55
47
|
if (children.length > 0) {
|
|
56
48
|
lines.push('**Nested:**');
|
|
57
49
|
for (const s of children) {
|
|
58
|
-
|
|
59
|
-
lines.push(`- \`${formatNeighborQualifier(s)}\`${desc ? ` — ${desc}` : ''}`);
|
|
50
|
+
lines.push(`- \`${formatNeighborQualifier(s)}\`${formatNeighborKeywords(s)}`);
|
|
60
51
|
}
|
|
61
52
|
}
|
|
62
53
|
return lines.join('\n');
|
|
63
54
|
}
|
|
64
|
-
function appendNeighbors(skill, body
|
|
65
|
-
if (suppress)
|
|
66
|
-
return body;
|
|
55
|
+
function appendNeighbors(skill, body) {
|
|
67
56
|
const section = buildNeighborsSection(skill);
|
|
68
57
|
if (section === null)
|
|
69
58
|
return body;
|
|
70
59
|
const sep = body.endsWith('\n') ? '\n' : '\n\n';
|
|
71
60
|
return body + sep + `<neighbors>\n${section}\n</neighbors>\n`;
|
|
72
61
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.command('skill [nameOrVerb] [rest...]')
|
|
82
|
-
.description('manage and inspect skills')
|
|
83
|
-
.option('--frontmatter', 'include YAML frontmatter in the printed body')
|
|
84
|
-
.addHelpText('before', '\n' + skillPrompt() + '\n')
|
|
85
|
-
.addHelpText('after', '\n' + SKILL_IDENTIFIER_HELP)
|
|
86
|
-
.action(async (nameOrVerb, _rest, opts) => {
|
|
87
|
-
if (nameOrVerb === undefined) {
|
|
88
|
-
skill.help();
|
|
89
|
-
return;
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Resolve scope for enable/disable/scaffold
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
function resolveWriteScope(scopeStr) {
|
|
66
|
+
if (scopeStr !== undefined) {
|
|
67
|
+
const resolved = resolveScopeArg(scopeStr);
|
|
68
|
+
if (resolved === 'all') {
|
|
69
|
+
throw usage('scope must be user or project, not all');
|
|
90
70
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
return projectScopeRoot() !== null ? 'project' : 'user';
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// find sub-branch
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
const findList = defineLeaf({
|
|
79
|
+
name: 'list',
|
|
80
|
+
help: {
|
|
81
|
+
name: 'skill find list',
|
|
82
|
+
summary: 'paginated list of installed skills',
|
|
83
|
+
params: [
|
|
84
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
|
|
85
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Filter to a single plugin name.' },
|
|
86
|
+
{ kind: 'flag', name: 'include-disabled', type: 'bool', required: false, constraint: 'When present, includes disabled skills.' },
|
|
87
|
+
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 50, constraint: 'Max 200.' },
|
|
88
|
+
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
89
|
+
],
|
|
90
|
+
output: [
|
|
91
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {name, plugin, scope, path, description?, enabled, disabled_in?}. Sorted by scope then plugin then name ascending.' },
|
|
92
|
+
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'null means no more items.' },
|
|
93
|
+
{ name: 'total', type: 'integer | null', required: true, constraint: 'Exact when cheap; null otherwise.' },
|
|
94
|
+
],
|
|
95
|
+
outputKind: 'object',
|
|
96
|
+
effects: ['None. Read-only.'],
|
|
97
|
+
},
|
|
98
|
+
run: async (input) => {
|
|
99
|
+
const scopeStr = input['scope'];
|
|
100
|
+
const pluginFilter = input['plugin'];
|
|
101
|
+
const includeDisabled = input['includeDisabled'];
|
|
102
|
+
const limitRaw = input['limit'];
|
|
103
|
+
const limit = Math.min(Math.max(1, limitRaw), 200);
|
|
104
|
+
const cursor = input['cursor'];
|
|
105
|
+
const scopes = listScopes(scopeStr);
|
|
106
|
+
const skills = scopes
|
|
107
|
+
.flatMap((s) => listAllSkills(s))
|
|
108
|
+
.filter((sk) => {
|
|
109
|
+
if (pluginFilter !== undefined && sk.plugin !== pluginFilter)
|
|
110
|
+
return false;
|
|
111
|
+
if (!includeDisabled && !sk.enabled)
|
|
112
|
+
return false;
|
|
113
|
+
return true;
|
|
114
|
+
});
|
|
115
|
+
// Sort by scope then plugin then name ascending
|
|
116
|
+
const scopeOrder = { project: 0, user: 1, builtin: 2 };
|
|
117
|
+
skills.sort((a, b) => {
|
|
118
|
+
const so = (scopeOrder[a.scope] !== undefined ? scopeOrder[a.scope] : 3) -
|
|
119
|
+
(scopeOrder[b.scope] !== undefined ? scopeOrder[b.scope] : 3);
|
|
120
|
+
if (so !== 0)
|
|
121
|
+
return so;
|
|
122
|
+
const po = a.plugin.localeCompare(b.plugin);
|
|
123
|
+
if (po !== 0)
|
|
124
|
+
return po;
|
|
125
|
+
return a.name.localeCompare(b.name);
|
|
126
|
+
});
|
|
127
|
+
const keyOf = (sk) => `${sk.scope}:${sk.plugin}/${sk.name}`;
|
|
128
|
+
const params = {};
|
|
129
|
+
if (limit !== undefined)
|
|
130
|
+
params.limit = limit;
|
|
131
|
+
if (cursor !== undefined)
|
|
132
|
+
params.cursor = cursor;
|
|
133
|
+
const result = paginate(skills, params, {
|
|
134
|
+
defaultLimit: 50,
|
|
135
|
+
maxLimit: 200,
|
|
136
|
+
keyOf,
|
|
137
|
+
total: 'count',
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
items: result.items.map((sk) => ({
|
|
141
|
+
name: sk.name,
|
|
142
|
+
plugin: sk.plugin,
|
|
143
|
+
scope: sk.scope,
|
|
144
|
+
path: sk.path,
|
|
145
|
+
description: sk.frontmatter.description !== undefined ? sk.frontmatter.description : null,
|
|
146
|
+
enabled: sk.enabled,
|
|
147
|
+
disabled_in: sk.disabledIn !== undefined ? sk.disabledIn : null,
|
|
148
|
+
})),
|
|
149
|
+
next_cursor: result.next_cursor,
|
|
150
|
+
total: result.total,
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const findSearch = defineLeaf({
|
|
155
|
+
name: 'search',
|
|
156
|
+
help: {
|
|
157
|
+
name: 'skill find search',
|
|
158
|
+
summary: 'search skills by name, description, and keywords',
|
|
159
|
+
params: [
|
|
160
|
+
{ kind: 'positional', name: 'query', required: true, constraint: 'Whitespace-separated terms matched case-insensitively against name, description, and keywords; skills matching more terms rank higher.' },
|
|
161
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
|
|
162
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Filter to a single plugin name.' },
|
|
163
|
+
{ kind: 'flag', name: 'include-disabled', type: 'bool', required: false, constraint: 'When present, includes disabled skills.' },
|
|
164
|
+
{ kind: 'flag', name: 'search-body', type: 'bool', required: false, constraint: 'When present, also searches inside SKILL.md body text.' },
|
|
165
|
+
],
|
|
166
|
+
output: [
|
|
167
|
+
{ name: 'query', type: 'string', required: true, constraint: 'Echo of the input query.' },
|
|
168
|
+
{ name: 'hits', type: 'object[]', required: true, constraint: 'Each: {name, plugin, scope, path, description?, keywords?, enabled, score, matched}. Sorted by score descending.' },
|
|
169
|
+
],
|
|
170
|
+
outputKind: 'object',
|
|
171
|
+
effects: ['None. Read-only.'],
|
|
172
|
+
},
|
|
173
|
+
run: async (input) => {
|
|
174
|
+
const query = input['query'];
|
|
175
|
+
const scopeStr = input['scope'];
|
|
176
|
+
const pluginFilter = input['plugin'];
|
|
177
|
+
const includeDisabled = input['includeDisabled'];
|
|
178
|
+
const searchBody = input['searchBody'];
|
|
179
|
+
const terms = query
|
|
180
|
+
.toLowerCase()
|
|
181
|
+
.split(/\s+/)
|
|
182
|
+
.filter((t) => t.length > 0);
|
|
183
|
+
if (terms.length === 0)
|
|
184
|
+
throw usage('query must contain at least one non-whitespace term');
|
|
185
|
+
const scopes = listScopes(scopeStr);
|
|
186
|
+
const candidates = scopes
|
|
187
|
+
.flatMap((s) => listAllSkills(s))
|
|
188
|
+
.filter((sk) => {
|
|
189
|
+
if (pluginFilter !== undefined && sk.plugin !== pluginFilter)
|
|
190
|
+
return false;
|
|
191
|
+
if (!includeDisabled && !sk.enabled)
|
|
192
|
+
return false;
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
const hits = [];
|
|
196
|
+
for (const sk of candidates) {
|
|
197
|
+
const matchedSet = new Set();
|
|
198
|
+
let score = 0;
|
|
199
|
+
const nameLc = sk.name.toLowerCase();
|
|
200
|
+
const descLc = sk.frontmatter.description !== undefined ? sk.frontmatter.description.toLowerCase() : null;
|
|
201
|
+
const kwsLc = sk.frontmatter.keywords !== undefined ? sk.frontmatter.keywords.map((k) => k.toLowerCase()) : null;
|
|
202
|
+
const bodyLc = searchBody ? readText(sk.path).toLowerCase() : null;
|
|
203
|
+
for (const term of terms) {
|
|
204
|
+
if (nameLc.includes(term)) {
|
|
205
|
+
score += 10;
|
|
206
|
+
matchedSet.add('name');
|
|
207
|
+
}
|
|
208
|
+
if (descLc !== null && descLc.includes(term)) {
|
|
209
|
+
score += 4;
|
|
210
|
+
matchedSet.add('description');
|
|
211
|
+
}
|
|
212
|
+
if (kwsLc !== null && kwsLc.some((k) => k.includes(term))) {
|
|
213
|
+
score += 6;
|
|
214
|
+
matchedSet.add('keywords');
|
|
215
|
+
}
|
|
216
|
+
if (bodyLc !== null && bodyLc.includes(term)) {
|
|
217
|
+
score += 1;
|
|
218
|
+
matchedSet.add('body');
|
|
219
|
+
}
|
|
102
220
|
}
|
|
221
|
+
if (score > 0)
|
|
222
|
+
hits.push({ skill: sk, score, matched: Array.from(matchedSet) });
|
|
103
223
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
224
|
+
hits.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
225
|
+
return {
|
|
226
|
+
query,
|
|
227
|
+
hits: hits.map((h) => ({
|
|
228
|
+
name: h.skill.name,
|
|
229
|
+
plugin: h.skill.plugin,
|
|
230
|
+
scope: h.skill.scope,
|
|
231
|
+
path: h.skill.path,
|
|
232
|
+
description: h.skill.frontmatter.description !== undefined ? h.skill.frontmatter.description : null,
|
|
233
|
+
keywords: h.skill.frontmatter.keywords !== undefined ? h.skill.frontmatter.keywords : null,
|
|
234
|
+
enabled: h.skill.enabled,
|
|
235
|
+
score: h.score,
|
|
236
|
+
matched: h.matched,
|
|
237
|
+
})),
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
const findGrep = defineLeaf({
|
|
242
|
+
name: 'grep',
|
|
243
|
+
help: {
|
|
244
|
+
name: 'skill find grep',
|
|
245
|
+
summary: 'search skill file contents for a regex pattern',
|
|
246
|
+
params: [
|
|
247
|
+
{ kind: 'positional', name: 'pattern', required: true, constraint: 'ECMAScript regex. Applied to each line of every SKILL.md file.' },
|
|
248
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
|
|
249
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Filter to a single plugin name.' },
|
|
250
|
+
],
|
|
251
|
+
output: [
|
|
252
|
+
{ name: 'matches', type: 'object[]', required: true, constraint: 'Each: {path, line, text}. path is absolute. Sorted by path then line ascending.' },
|
|
253
|
+
],
|
|
254
|
+
outputKind: 'object',
|
|
255
|
+
effects: ['None. Read-only.'],
|
|
256
|
+
},
|
|
257
|
+
run: async (input) => {
|
|
258
|
+
const pattern = input['pattern'];
|
|
259
|
+
const scopeStr = input['scope'];
|
|
260
|
+
const pluginFilter = input['plugin'];
|
|
261
|
+
let regex;
|
|
116
262
|
try {
|
|
117
|
-
|
|
118
|
-
const skills = scopes
|
|
119
|
-
.flatMap((s) => listAllSkills(s))
|
|
120
|
-
.filter((sk) => {
|
|
121
|
-
if (opts.plugin !== undefined && sk.plugin !== opts.plugin)
|
|
122
|
-
return false;
|
|
123
|
-
if (!opts.all && !sk.enabled)
|
|
124
|
-
return false;
|
|
125
|
-
return true;
|
|
126
|
-
});
|
|
127
|
-
if (opts.json) {
|
|
128
|
-
jsonOut({
|
|
129
|
-
skills: skills.map((sk) => ({
|
|
130
|
-
name: sk.name,
|
|
131
|
-
plugin: sk.plugin,
|
|
132
|
-
scope: sk.scope,
|
|
133
|
-
path: sk.path,
|
|
134
|
-
description: sk.frontmatter.description,
|
|
135
|
-
enabled: sk.enabled,
|
|
136
|
-
disabled_in: sk.disabledIn,
|
|
137
|
-
})),
|
|
138
|
-
});
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
for (const sk of skills) {
|
|
142
|
-
const desc = sk.frontmatter.description !== undefined ? sk.frontmatter.description : '';
|
|
143
|
-
const marker = sk.enabled ? '' : ` [disabled${sk.disabledIn ? `@${sk.disabledIn}` : ''}]`;
|
|
144
|
-
const qualified = sk.plugin === SCOPE_SKILL_PLUGIN
|
|
145
|
-
? `${sk.scope}:${sk.name}`
|
|
146
|
-
: `${sk.scope}:${sk.plugin}/${sk.name}`;
|
|
147
|
-
out(`${qualified}${marker}${desc ? ` — ${desc}` : ''}`);
|
|
148
|
-
}
|
|
263
|
+
regex = new RegExp(pattern);
|
|
149
264
|
}
|
|
150
|
-
catch
|
|
151
|
-
|
|
265
|
+
catch {
|
|
266
|
+
throw usage(`invalid regex pattern: ${pattern}`);
|
|
152
267
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.option('--frontmatter', 'include YAML frontmatter in the printed body')
|
|
161
|
-
.option('--no-neighbors', 'suppress the auto-appended ## Neighbors section')
|
|
162
|
-
.option('--json', 'emit JSON')
|
|
163
|
-
.addHelpText('after', '\nExamples:\n' +
|
|
164
|
-
' crtr skill show rules # bare name\n' +
|
|
165
|
-
' crtr skill show claude-authoring:rules # plugin:name (canonical)\n' +
|
|
166
|
-
' crtr skill show user:claude-authoring/rules # scope:plugin/name (matches search/list output)\n' +
|
|
167
|
-
' crtr skill show claude-authoring/rules # plugin/name shorthand\n\n' +
|
|
168
|
-
SKILL_IDENTIFIER_HELP)
|
|
169
|
-
.action(async (name, opts) => {
|
|
170
|
-
try {
|
|
171
|
-
const scopeArg = resolveScopeArg(opts.scope);
|
|
172
|
-
const resolveOpts = scopeArg !== 'all' ? { scope: scopeArg } : {};
|
|
173
|
-
if (opts.plugin !== undefined) {
|
|
174
|
-
Object.assign(resolveOpts, { pluginFilter: opts.plugin });
|
|
268
|
+
const scopes = listScopes(scopeStr);
|
|
269
|
+
const skillsDirs = [];
|
|
270
|
+
for (const s of scopes) {
|
|
271
|
+
if (pluginFilter === undefined || pluginFilter === SCOPE_SKILL_PLUGIN) {
|
|
272
|
+
const root = scopeSkillsDir(s);
|
|
273
|
+
if (root)
|
|
274
|
+
skillsDirs.push(root);
|
|
175
275
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
276
|
+
for (const plugin of listInstalledPlugins(s)) {
|
|
277
|
+
if (!plugin.enabled)
|
|
278
|
+
continue;
|
|
279
|
+
if (pluginFilter !== undefined && plugin.name !== pluginFilter)
|
|
280
|
+
continue;
|
|
281
|
+
skillsDirs.push(join(plugin.root, SKILLS_DIR));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const matchLines = [];
|
|
285
|
+
for (const skillsDir of skillsDirs) {
|
|
286
|
+
const files = walkFiles(skillsDir);
|
|
287
|
+
for (const file of files) {
|
|
288
|
+
const content = readText(file);
|
|
289
|
+
const lines = content.split('\n');
|
|
290
|
+
lines.forEach((lineText, idx) => {
|
|
291
|
+
if (regex.test(lineText)) {
|
|
292
|
+
matchLines.push({ path: file, line: idx + 1, text: lineText });
|
|
293
|
+
}
|
|
189
294
|
});
|
|
190
|
-
return;
|
|
191
295
|
}
|
|
192
|
-
out(wrapSkill(skillObj.name, skillObj.path, body));
|
|
193
|
-
hint(buildShowFooter(skillObj.path));
|
|
194
296
|
}
|
|
195
|
-
|
|
196
|
-
|
|
297
|
+
// Sort by path then line ascending
|
|
298
|
+
matchLines.sort((a, b) => {
|
|
299
|
+
const pc = a.path.localeCompare(b.path);
|
|
300
|
+
return pc !== 0 ? pc : a.line - b.line;
|
|
301
|
+
});
|
|
302
|
+
return { matches: matchLines };
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
const findBranch = defineBranch({
|
|
306
|
+
name: 'find',
|
|
307
|
+
help: {
|
|
308
|
+
name: 'skill find',
|
|
309
|
+
summary: 'discover skills by listing, keyword search, or body grep',
|
|
310
|
+
children: [
|
|
311
|
+
{ name: 'list', desc: 'paginated list of installed skills', useWhen: 'enumerating all available skills' },
|
|
312
|
+
{ name: 'search', desc: 'keyword search across name/description/keywords', useWhen: 'looking for skills matching a topic' },
|
|
313
|
+
{ name: 'grep', desc: 'regex search across SKILL.md bodies', useWhen: 'finding skills containing specific text or patterns' },
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
children: [findList, findSearch, findGrep],
|
|
317
|
+
});
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// read sub-branch
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
const readShow = defineLeaf({
|
|
322
|
+
name: 'show',
|
|
323
|
+
help: {
|
|
324
|
+
name: 'skill read show',
|
|
325
|
+
summary: 'print SKILL.md body for a named skill',
|
|
326
|
+
params: [
|
|
327
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Forms: <name>, <plugin>:<name>, <scope>:<name>, <scope>:<plugin>/<name>.' },
|
|
328
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution when name is ambiguous.' },
|
|
329
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Narrows resolution to a specific plugin.' },
|
|
330
|
+
{ kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'When present, includes YAML frontmatter in the output.' },
|
|
331
|
+
],
|
|
332
|
+
output: [
|
|
333
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
|
|
334
|
+
{ name: 'plugin', type: 'string', required: true, constraint: 'Plugin the skill belongs to.' },
|
|
335
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the skill was resolved from.' },
|
|
336
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to SKILL.md.' },
|
|
337
|
+
{ name: 'content', type: 'string', required: true, constraint: 'SKILL.md body (with or without frontmatter per the --frontmatter flag).' },
|
|
338
|
+
],
|
|
339
|
+
outputKind: 'object',
|
|
340
|
+
effects: ['None. Read-only.'],
|
|
341
|
+
},
|
|
342
|
+
run: async (input) => {
|
|
343
|
+
const nameRaw = input['name'];
|
|
344
|
+
const scopeStr = input['scope'];
|
|
345
|
+
const pluginFilter = input['plugin'];
|
|
346
|
+
const includeFrontmatter = input['frontmatter'];
|
|
347
|
+
const resolveOpts = {};
|
|
348
|
+
if (scopeStr !== undefined) {
|
|
349
|
+
const resolved = resolveScopeArg(scopeStr);
|
|
350
|
+
if (resolved !== 'all')
|
|
351
|
+
resolveOpts.scope = resolved;
|
|
197
352
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
353
|
+
if (pluginFilter !== undefined)
|
|
354
|
+
resolveOpts.pluginFilter = pluginFilter;
|
|
355
|
+
const skillObj = resolveSkill(nameRaw, resolveOpts);
|
|
356
|
+
const rawContent = readText(skillObj.path);
|
|
357
|
+
const rawBody = includeFrontmatter ? rawContent : parseFrontmatter(rawContent).body;
|
|
358
|
+
const content = appendNeighbors(skillObj, rawBody);
|
|
359
|
+
return {
|
|
360
|
+
name: skillObj.name,
|
|
361
|
+
plugin: skillObj.plugin,
|
|
362
|
+
scope: skillObj.scope,
|
|
363
|
+
path: skillObj.path,
|
|
364
|
+
content,
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
const readWhere = defineLeaf({
|
|
369
|
+
name: 'where',
|
|
370
|
+
help: {
|
|
371
|
+
name: 'skill read where',
|
|
372
|
+
summary: 'show resolution metadata for a named skill without reading its body',
|
|
373
|
+
params: [
|
|
374
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read show.' },
|
|
375
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
|
|
376
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Narrows resolution to a specific plugin.' },
|
|
377
|
+
],
|
|
378
|
+
output: [
|
|
379
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
|
|
380
|
+
{ name: 'plugin', type: 'string', required: true, constraint: 'Plugin the skill belongs to.' },
|
|
381
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the skill was resolved from.' },
|
|
382
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to SKILL.md.' },
|
|
383
|
+
],
|
|
384
|
+
outputKind: 'object',
|
|
385
|
+
effects: ['None. Read-only.'],
|
|
386
|
+
},
|
|
387
|
+
run: async (input) => {
|
|
388
|
+
const nameRaw = input['name'];
|
|
389
|
+
const scopeStr = input['scope'];
|
|
390
|
+
const pluginFilter = input['plugin'];
|
|
391
|
+
const resolveOpts = {};
|
|
392
|
+
if (scopeStr !== undefined) {
|
|
393
|
+
const resolved = resolveScopeArg(scopeStr);
|
|
394
|
+
if (resolved !== 'all')
|
|
395
|
+
resolveOpts.scope = resolved;
|
|
214
396
|
}
|
|
215
|
-
|
|
216
|
-
|
|
397
|
+
if (pluginFilter !== undefined)
|
|
398
|
+
resolveOpts.pluginFilter = pluginFilter;
|
|
399
|
+
const skillObj = resolveSkill(nameRaw, resolveOpts);
|
|
400
|
+
return {
|
|
401
|
+
name: skillObj.name,
|
|
402
|
+
plugin: skillObj.plugin,
|
|
403
|
+
scope: skillObj.scope,
|
|
404
|
+
path: skillObj.path,
|
|
405
|
+
};
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
const readBranch = defineBranch({
|
|
409
|
+
name: 'read',
|
|
410
|
+
help: {
|
|
411
|
+
name: 'skill read',
|
|
412
|
+
summary: 'read skill content or resolve its location',
|
|
413
|
+
children: [
|
|
414
|
+
{ name: 'show', desc: 'print SKILL.md body', useWhen: 'loading skill content to act on it' },
|
|
415
|
+
{ name: 'where', desc: 'show resolution metadata only', useWhen: 'verifying which skill resolves and from where, without loading its body' },
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
children: [readShow, readWhere],
|
|
419
|
+
});
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// author sub-branch
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
const VALID_TYPES = ['playbook', 'primer', 'reference', 'runbook', 'freeform'];
|
|
424
|
+
const authorGuide = defineLeaf({
|
|
425
|
+
name: 'guide',
|
|
426
|
+
help: {
|
|
427
|
+
name: 'skill author guide',
|
|
428
|
+
summary: 'load the skill authoring workflow — two stages: omit type to pick one, pass type for its full skeleton',
|
|
429
|
+
params: [
|
|
430
|
+
{ kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'OMIT to receive the template-picker guide first; pass on the second call for that type\'s full workflow + skeleton.' },
|
|
431
|
+
{ kind: 'flag', name: 'topic', type: 'string', required: false, constraint: 'Optional topic context injected into the guide.' },
|
|
432
|
+
],
|
|
433
|
+
output: [
|
|
434
|
+
{ name: 'guide', type: 'string', required: true, constraint: 'Stage 1 (no type): the template-picker workflow. Stage 2 (type given): that type\'s authoring workflow + skeleton.' },
|
|
435
|
+
{ name: 'type', type: 'string | null', required: true, constraint: 'Echo of the requested type, or null on the picker stage.' },
|
|
436
|
+
],
|
|
437
|
+
outputKind: 'object',
|
|
438
|
+
effects: ['None. Read-only.'],
|
|
439
|
+
},
|
|
440
|
+
run: async (input) => {
|
|
441
|
+
const type = input['type'];
|
|
442
|
+
const topic = input['topic'];
|
|
443
|
+
const topicArg = topic !== undefined ? topic : '';
|
|
444
|
+
// Progressive disclosure: no type → template picker (stage 1);
|
|
445
|
+
// type given → that type's full workflow + skeleton (stage 2).
|
|
446
|
+
if (type === undefined) {
|
|
447
|
+
return { guide: skillCreatePrompt(topicArg), type: null };
|
|
217
448
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
const matchLines = [];
|
|
252
|
-
for (const skillsDir of skillsDirs) {
|
|
253
|
-
const files = walkFiles(skillsDir);
|
|
254
|
-
for (const file of files) {
|
|
255
|
-
const content = readText(file);
|
|
256
|
-
const lines = content.split('\n');
|
|
257
|
-
lines.forEach((lineText, idx) => {
|
|
258
|
-
if (regex.test(lineText)) {
|
|
259
|
-
matchLines.push({ path: file, line: idx + 1, text: lineText });
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (opts.json) {
|
|
265
|
-
jsonOut({ matches: matchLines });
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
for (const m of matchLines) {
|
|
269
|
-
out(`${m.path}:${m.line}: ${m.text}`);
|
|
270
|
-
}
|
|
449
|
+
return { guide: skillTemplatePrompt(type, topicArg), type };
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
const authorScaffold = defineLeaf({
|
|
453
|
+
name: 'scaffold',
|
|
454
|
+
help: {
|
|
455
|
+
name: 'skill author scaffold',
|
|
456
|
+
summary: 'create an empty SKILL.md stub at the given qualifier',
|
|
457
|
+
params: [
|
|
458
|
+
{ kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier in <plugin>:<skill> form.' },
|
|
459
|
+
{ kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform.' },
|
|
460
|
+
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Short description written to frontmatter.' },
|
|
461
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
462
|
+
],
|
|
463
|
+
output: [
|
|
464
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded SKILL.md.' },
|
|
465
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call to load the authoring guide.' },
|
|
466
|
+
],
|
|
467
|
+
outputKind: 'object',
|
|
468
|
+
effects: [
|
|
469
|
+
'Creates the skill directory and SKILL.md stub at the resolved location.',
|
|
470
|
+
'Writes frontmatter with name, description (if provided), and type (if provided).',
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
run: async (input) => {
|
|
474
|
+
const qualifier = input['qualifier'];
|
|
475
|
+
const typeStr = input['type'];
|
|
476
|
+
const description = input['description'];
|
|
477
|
+
const scopeStr = input['scope'];
|
|
478
|
+
const { plugin: pluginName, name: skillName } = parseSkillQualifier(qualifier);
|
|
479
|
+
if (!skillName) {
|
|
480
|
+
throw usage('skill name required in qualifier');
|
|
271
481
|
}
|
|
272
|
-
|
|
273
|
-
|
|
482
|
+
if (typeStr !== undefined && !isSkillType(typeStr)) {
|
|
483
|
+
throw usage(`unknown skill type: ${typeStr} / valid: ${SKILL_TYPES.join(' | ')}`);
|
|
274
484
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const { plugin: pluginName, name: skillName } = parseSkillQualifier(qualifier);
|
|
286
|
-
if (!skillName) {
|
|
287
|
-
throw usage('skill name required');
|
|
485
|
+
const skillType = typeStr !== undefined && isSkillType(typeStr) ? typeStr : undefined;
|
|
486
|
+
let skillFile;
|
|
487
|
+
// Scope-direct: no plugin qualifier, or explicit `_:` sentinel
|
|
488
|
+
if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
|
|
489
|
+
const scope = resolveWriteScope(scopeStr);
|
|
490
|
+
const scopeRootPath = requireScopeRoot(scope);
|
|
491
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
492
|
+
const skillsRoot = scopeSkillsDir(scope);
|
|
493
|
+
if (!skillsRoot) {
|
|
494
|
+
throw general(`no skills dir for scope ${scope}`);
|
|
288
495
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
skillType = opts.type;
|
|
295
|
-
}
|
|
296
|
-
const scopeArg = opts.scope !== undefined ? resolveScopeArg(opts.scope) : undefined;
|
|
297
|
-
// Scope-direct: no plugin qualifier, or explicit `_:` sentinel
|
|
298
|
-
if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
|
|
299
|
-
let scope;
|
|
300
|
-
if (scopeArg !== undefined && scopeArg !== 'all') {
|
|
301
|
-
scope = scopeArg;
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
scope = projectScopeRoot() !== null ? 'project' : 'user';
|
|
305
|
-
}
|
|
306
|
-
const scopeRootPath = requireScopeRoot(scope);
|
|
307
|
-
ensureScopeInitialized(scope, scopeRootPath);
|
|
308
|
-
const skillsRoot = scopeSkillsDir(scope);
|
|
309
|
-
if (!skillsRoot) {
|
|
310
|
-
throw general(`no skills dir for scope ${scope}`);
|
|
311
|
-
}
|
|
312
|
-
const skillDir = join(skillsRoot, ...skillName.split('/'));
|
|
313
|
-
const skillFile = join(skillDir, SKILL_ENTRY_FILE);
|
|
314
|
-
if (pathExists(skillFile)) {
|
|
315
|
-
throw general(`skill already exists: ${skillFile}`);
|
|
316
|
-
}
|
|
317
|
-
ensureDir(skillDir);
|
|
318
|
-
const fm = serializeFrontmatter({
|
|
319
|
-
name: skillName,
|
|
320
|
-
description: opts.description,
|
|
321
|
-
type: skillType,
|
|
322
|
-
});
|
|
323
|
-
writeFileSync(skillFile, fm, 'utf8');
|
|
324
|
-
out(skillFile);
|
|
325
|
-
const templateHint = skillType !== undefined ? skillType : '<type>';
|
|
326
|
-
hint(`crtr: scaffolded ${scope}-scope skill ${skillName} — edit directly; ` +
|
|
327
|
-
`\`crtr skill template ${templateHint}\` for body skeleton`);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
let plugin;
|
|
331
|
-
if (scopeArg !== undefined && scopeArg !== 'all') {
|
|
332
|
-
plugin = findPluginByName(pluginName, scopeArg);
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
plugin = findPluginByName(pluginName);
|
|
496
|
+
const skillDir = join(skillsRoot, ...skillName.split('/'));
|
|
497
|
+
skillFile = join(skillDir, SKILL_ENTRY_FILE);
|
|
498
|
+
if (pathExists(skillFile)) {
|
|
499
|
+
throw general(`skill already exists: ${skillFile}`);
|
|
336
500
|
}
|
|
501
|
+
ensureDir(skillDir);
|
|
502
|
+
const fm = serializeFrontmatter({
|
|
503
|
+
name: skillName,
|
|
504
|
+
description,
|
|
505
|
+
type: skillType,
|
|
506
|
+
});
|
|
507
|
+
writeFileSync(skillFile, fm, 'utf8');
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// Plugin-scoped scaffold
|
|
511
|
+
const scopeForLookup = scopeStr !== undefined
|
|
512
|
+
? (() => {
|
|
513
|
+
const r = resolveScopeArg(scopeStr);
|
|
514
|
+
return r !== 'all' ? r : undefined;
|
|
515
|
+
})()
|
|
516
|
+
: undefined;
|
|
517
|
+
const plugin = scopeForLookup !== undefined
|
|
518
|
+
? findPluginByName(pluginName, scopeForLookup)
|
|
519
|
+
: findPluginByName(pluginName);
|
|
337
520
|
if (!plugin) {
|
|
338
521
|
throw notFound(`plugin not found: ${pluginName}`);
|
|
339
522
|
}
|
|
340
523
|
const skillDir = join(plugin.root, SKILLS_DIR, ...skillName.split('/'));
|
|
341
|
-
|
|
524
|
+
skillFile = join(skillDir, SKILL_ENTRY_FILE);
|
|
342
525
|
if (pathExists(skillFile)) {
|
|
343
526
|
throw general(`skill already exists: ${skillFile}`);
|
|
344
527
|
}
|
|
345
528
|
ensureDir(skillDir);
|
|
346
529
|
const fm = serializeFrontmatter({
|
|
347
530
|
name: skillName,
|
|
348
|
-
description
|
|
531
|
+
description,
|
|
349
532
|
type: skillType,
|
|
350
533
|
});
|
|
351
534
|
writeFileSync(skillFile, fm, 'utf8');
|
|
352
|
-
out(skillFile);
|
|
353
|
-
const templateHint = skillType !== undefined ? skillType : '<type>';
|
|
354
|
-
hint(`crtr: scaffolded ${skillFile} — edit directly; ` +
|
|
355
|
-
`\`crtr skill template ${templateHint}\` for body skeleton`);
|
|
356
|
-
}
|
|
357
|
-
catch (e) {
|
|
358
|
-
handleError(e);
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
// create — pick a template type
|
|
362
|
-
skill
|
|
363
|
-
.command('create [topic...]')
|
|
364
|
-
.description(`pick a template type for a new skill (${SKILL_TYPES.join(' | ')})`)
|
|
365
|
-
.action(async (topic) => {
|
|
366
|
-
const arg = topic && topic.length > 0 ? topic.join(' ') : '';
|
|
367
|
-
out(skillCreatePrompt(arg));
|
|
368
|
-
});
|
|
369
|
-
// template — full workflow + skeleton for one template type
|
|
370
|
-
skill
|
|
371
|
-
.command('template <type> [topic...]')
|
|
372
|
-
.description(`full workflow + skeleton for a template type (${SKILL_TYPES.join(' | ')})`)
|
|
373
|
-
.action(async (type, topic) => {
|
|
374
|
-
const arg = topic && topic.length > 0 ? topic.join(' ') : '';
|
|
375
|
-
out(skillTemplatePrompt(type, arg));
|
|
376
|
-
});
|
|
377
|
-
// where
|
|
378
|
-
skill
|
|
379
|
-
.command('where <name>')
|
|
380
|
-
.description('show resolution info as JSON')
|
|
381
|
-
.option('--scope <scope>', 'user|project')
|
|
382
|
-
.option('--plugin <name>', 'filter by plugin name')
|
|
383
|
-
.action(async (name, opts) => {
|
|
384
|
-
try {
|
|
385
|
-
const scopeArg = resolveScopeArg(opts.scope);
|
|
386
|
-
const resolveOpts = scopeArg !== 'all' ? { scope: scopeArg } : {};
|
|
387
|
-
if (opts.plugin !== undefined) {
|
|
388
|
-
Object.assign(resolveOpts, { pluginFilter: opts.plugin });
|
|
389
|
-
}
|
|
390
|
-
const skillObj = resolveSkill(name, resolveOpts);
|
|
391
|
-
jsonOut({
|
|
392
|
-
name: skillObj.name,
|
|
393
|
-
plugin: skillObj.plugin,
|
|
394
|
-
scope: skillObj.scope,
|
|
395
|
-
path: skillObj.path,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
catch (e) {
|
|
399
|
-
handleError(e);
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
// enable
|
|
403
|
-
skill
|
|
404
|
-
.command('enable <name>')
|
|
405
|
-
.description('enable a skill (clears any disable in the chosen scope)')
|
|
406
|
-
.option('--scope <scope>', 'user|project (default: project if available, else user)')
|
|
407
|
-
.action(async (name, opts) => {
|
|
408
|
-
try {
|
|
409
|
-
await toggleSkill(name, true, opts.scope);
|
|
410
|
-
}
|
|
411
|
-
catch (e) {
|
|
412
|
-
handleError(e);
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
// disable
|
|
416
|
-
skill
|
|
417
|
-
.command('disable <name>')
|
|
418
|
-
.description('disable a skill (hides from list and agent discovery)')
|
|
419
|
-
.option('--scope <scope>', 'user|project (default: project if available, else user)')
|
|
420
|
-
.action(async (name, opts) => {
|
|
421
|
-
try {
|
|
422
|
-
await toggleSkill(name, false, opts.scope);
|
|
423
|
-
}
|
|
424
|
-
catch (e) {
|
|
425
|
-
handleError(e);
|
|
426
535
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const hits = [];
|
|
453
|
-
for (const sk of candidates) {
|
|
454
|
-
const matched = [];
|
|
455
|
-
let score = 0;
|
|
456
|
-
if (sk.name.toLowerCase().includes(needle)) {
|
|
457
|
-
score += 10;
|
|
458
|
-
matched.push('name');
|
|
459
|
-
}
|
|
460
|
-
const desc = sk.frontmatter.description;
|
|
461
|
-
if (desc !== undefined && desc.toLowerCase().includes(needle)) {
|
|
462
|
-
score += 4;
|
|
463
|
-
matched.push('description');
|
|
464
|
-
}
|
|
465
|
-
const kws = sk.frontmatter.keywords;
|
|
466
|
-
if (kws && kws.some((k) => k.toLowerCase().includes(needle))) {
|
|
467
|
-
score += 6;
|
|
468
|
-
matched.push('keywords');
|
|
469
|
-
}
|
|
470
|
-
if (opts.body) {
|
|
471
|
-
const text = readText(sk.path).toLowerCase();
|
|
472
|
-
if (text.includes(needle)) {
|
|
473
|
-
score += 1;
|
|
474
|
-
matched.push('body');
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
if (score > 0)
|
|
478
|
-
hits.push({ skill: sk, score, matched });
|
|
479
|
-
}
|
|
480
|
-
hits.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
481
|
-
if (opts.json) {
|
|
482
|
-
jsonOut({
|
|
483
|
-
query,
|
|
484
|
-
hits: hits.map((h) => ({
|
|
485
|
-
name: h.skill.name,
|
|
486
|
-
plugin: h.skill.plugin,
|
|
487
|
-
scope: h.skill.scope,
|
|
488
|
-
path: h.skill.path,
|
|
489
|
-
description: h.skill.frontmatter.description,
|
|
490
|
-
keywords: h.skill.frontmatter.keywords,
|
|
491
|
-
enabled: h.skill.enabled,
|
|
492
|
-
score: h.score,
|
|
493
|
-
matched: h.matched,
|
|
494
|
-
})),
|
|
495
|
-
});
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
for (const h of hits) {
|
|
499
|
-
const desc = h.skill.frontmatter.description !== undefined
|
|
500
|
-
? h.skill.frontmatter.description
|
|
501
|
-
: '';
|
|
502
|
-
const marker = h.skill.enabled ? '' : ' [disabled]';
|
|
503
|
-
const qualified = h.skill.plugin === SCOPE_SKILL_PLUGIN
|
|
504
|
-
? `${h.skill.scope}:${h.skill.name}`
|
|
505
|
-
: `${h.skill.scope}:${h.skill.plugin}/${h.skill.name}`;
|
|
506
|
-
out(`${qualified}${marker}\t${h.matched.join(',')}\t${desc}`);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
catch (e) {
|
|
510
|
-
handleError(e, { json: opts.json });
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
async function toggleSkill(name, enabled, scopeArgRaw) {
|
|
515
|
-
let scope;
|
|
516
|
-
if (scopeArgRaw !== undefined) {
|
|
517
|
-
const resolved = resolveScopeArg(scopeArgRaw);
|
|
518
|
-
if (resolved === 'all')
|
|
519
|
-
throw usage('--scope must be user or project for enable/disable');
|
|
520
|
-
scope = resolved;
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
scope = projectScopeRoot() !== null ? 'project' : 'user';
|
|
524
|
-
}
|
|
525
|
-
const skillObj = resolveSkill(name);
|
|
536
|
+
const typeHint = skillType !== undefined ? `--type ${skillType} ` : '';
|
|
537
|
+
const follow_up = `crtr skill author guide ${typeHint}--topic "${skillName}"`;
|
|
538
|
+
return { path: skillFile, follow_up };
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
const authorBranch = defineBranch({
|
|
542
|
+
name: 'author',
|
|
543
|
+
help: {
|
|
544
|
+
name: 'skill author',
|
|
545
|
+
summary: 'create and scaffold new skills',
|
|
546
|
+
children: [
|
|
547
|
+
{ name: 'guide', desc: 'load authoring workflow + skeleton for a type', useWhen: 'writing a new skill and need the template and instructions' },
|
|
548
|
+
{ name: 'scaffold', desc: 'create an empty SKILL.md stub', useWhen: 'initializing the file before writing content' },
|
|
549
|
+
],
|
|
550
|
+
},
|
|
551
|
+
children: [authorGuide, authorScaffold],
|
|
552
|
+
});
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
// state sub-branch
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
async function toggleSkill(input, enabled) {
|
|
557
|
+
const nameRaw = input['name'];
|
|
558
|
+
const scopeStr = input['scope'];
|
|
559
|
+
const scope = resolveWriteScope(scopeStr);
|
|
560
|
+
const skillObj = resolveSkill(nameRaw);
|
|
526
561
|
const key = skillConfigKey(skillObj.plugin, skillObj.name);
|
|
527
562
|
const scopeRootPath = requireScopeRoot(scope);
|
|
528
563
|
ensureScopeInitialized(scope, scopeRootPath);
|
|
529
564
|
updateConfig(scope, (cfg) => {
|
|
530
565
|
cfg.skills[key] = { enabled };
|
|
531
566
|
});
|
|
532
|
-
|
|
567
|
+
return { name: skillObj.name, scope, enabled };
|
|
568
|
+
}
|
|
569
|
+
const stateEnable = defineLeaf({
|
|
570
|
+
name: 'enable',
|
|
571
|
+
help: {
|
|
572
|
+
name: 'skill state enable',
|
|
573
|
+
summary: 'enable a skill in the given scope',
|
|
574
|
+
params: [
|
|
575
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read show.' },
|
|
576
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
577
|
+
],
|
|
578
|
+
output: [
|
|
579
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
|
|
580
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope where the enable was applied.' },
|
|
581
|
+
{ name: 'enabled', type: 'boolean', required: true, constraint: 'Always true.' },
|
|
582
|
+
],
|
|
583
|
+
outputKind: 'object',
|
|
584
|
+
effects: ['Writes the skill enable flag to config.json in the target scope.'],
|
|
585
|
+
},
|
|
586
|
+
run: async (input) => toggleSkill(input, true),
|
|
587
|
+
});
|
|
588
|
+
const stateDisable = defineLeaf({
|
|
589
|
+
name: 'disable',
|
|
590
|
+
help: {
|
|
591
|
+
name: 'skill state disable',
|
|
592
|
+
summary: 'disable a skill in the given scope, hiding it from list and agent discovery',
|
|
593
|
+
params: [
|
|
594
|
+
{ kind: 'positional', name: 'name', required: true, constraint: 'Skill identifier. Same forms as skill read show.' },
|
|
595
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
596
|
+
],
|
|
597
|
+
output: [
|
|
598
|
+
{ name: 'name', type: 'string', required: true, constraint: 'Resolved skill name.' },
|
|
599
|
+
{ name: 'scope', type: 'string', required: true, constraint: 'Scope where the disable was applied.' },
|
|
600
|
+
{ name: 'enabled', type: 'boolean', required: true, constraint: 'Always false.' },
|
|
601
|
+
],
|
|
602
|
+
outputKind: 'object',
|
|
603
|
+
effects: ['Writes the skill disable flag to config.json in the target scope.'],
|
|
604
|
+
},
|
|
605
|
+
run: async (input) => toggleSkill(input, false),
|
|
606
|
+
});
|
|
607
|
+
const stateBranch = defineBranch({
|
|
608
|
+
name: 'state',
|
|
609
|
+
help: {
|
|
610
|
+
name: 'skill state',
|
|
611
|
+
summary: 'enable or disable skills',
|
|
612
|
+
children: [
|
|
613
|
+
{ name: 'enable', desc: 'enable a skill', useWhen: 'making a previously disabled skill available again' },
|
|
614
|
+
{ name: 'disable', desc: 'disable a skill', useWhen: 'hiding a skill from list and agent discovery without removing it' },
|
|
615
|
+
],
|
|
616
|
+
},
|
|
617
|
+
children: [stateEnable, stateDisable],
|
|
618
|
+
});
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// Loaded-skills catalog (dynamicState for `skill -h`)
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
// A skill is a forest root within its source (scope+plugin) when no other
|
|
623
|
+
// skill in that same source is its ancestor (`name` prefix + '/'). Nested
|
|
624
|
+
// children stay discoverable via `skill find list` and the Neighbors section.
|
|
625
|
+
function buildSkillCatalog() {
|
|
626
|
+
let skills;
|
|
627
|
+
try {
|
|
628
|
+
skills = listAllSkills().filter((s) => s.enabled);
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
if (skills.length === 0)
|
|
634
|
+
return null;
|
|
635
|
+
const bySource = new Map();
|
|
636
|
+
for (const s of skills) {
|
|
637
|
+
const key = `${s.scope}${s.plugin}`;
|
|
638
|
+
const arr = bySource.get(key);
|
|
639
|
+
if (arr)
|
|
640
|
+
arr.push(s);
|
|
641
|
+
else
|
|
642
|
+
bySource.set(key, [s]);
|
|
643
|
+
}
|
|
644
|
+
const byPrefix = new Map();
|
|
645
|
+
for (const group of bySource.values()) {
|
|
646
|
+
const names = group.map((g) => g.name);
|
|
647
|
+
for (const s of group) {
|
|
648
|
+
const isChild = names.some((n) => n !== s.name && s.name.startsWith(n + '/'));
|
|
649
|
+
if (isChild)
|
|
650
|
+
continue;
|
|
651
|
+
const prefix = s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}:` : `${s.plugin}/`;
|
|
652
|
+
let set = byPrefix.get(prefix);
|
|
653
|
+
if (!set) {
|
|
654
|
+
set = new Set();
|
|
655
|
+
byPrefix.set(prefix, set);
|
|
656
|
+
}
|
|
657
|
+
set.add(s.name);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const prefixes = [...byPrefix.keys()].sort();
|
|
661
|
+
const prefixW = prefixes.reduce((m, p) => (p.length > m ? p.length : m), 0);
|
|
662
|
+
const lines = [`Loaded skills (${skills.length})`];
|
|
663
|
+
for (const prefix of prefixes) {
|
|
664
|
+
const names = [...byPrefix.get(prefix)].sort();
|
|
665
|
+
lines.push(` ${prefix.padEnd(prefixW)} ${names.join(', ')}`);
|
|
666
|
+
}
|
|
667
|
+
return lines.join('\n');
|
|
668
|
+
}
|
|
669
|
+
// ---------------------------------------------------------------------------
|
|
670
|
+
// Root export
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
672
|
+
export function registerSkill() {
|
|
673
|
+
return defineBranch({
|
|
674
|
+
name: 'skill',
|
|
675
|
+
help: {
|
|
676
|
+
name: 'skill',
|
|
677
|
+
summary: 'discover, read, author, and manage skill state',
|
|
678
|
+
model: 'To consume: `find search <topic>` discovers candidates; `read show <name>` loads each relevant SKILL.md body (multiple may apply — load them all). To create: `author guide` picks a template, then re-run with `--type <t>` for the skeleton, then `author scaffold <plugin>:<name>` materializes the file. `state enable|disable` toggles visibility without deleting.',
|
|
679
|
+
dynamicState: buildSkillCatalog,
|
|
680
|
+
children: [
|
|
681
|
+
{ name: 'find', desc: 'list, search, or grep skills', useWhen: 'discovering what skills are available' },
|
|
682
|
+
{ name: 'read', desc: 'read skill content or resolve location', useWhen: 'loading a skill to act on it' },
|
|
683
|
+
{ name: 'author', desc: 'create and scaffold skills', useWhen: 'writing a new skill' },
|
|
684
|
+
{ name: 'state', desc: 'enable or disable skills', useWhen: 'toggling skill visibility' },
|
|
685
|
+
],
|
|
686
|
+
},
|
|
687
|
+
children: [findBranch, readBranch, authorBranch, stateBranch],
|
|
688
|
+
});
|
|
533
689
|
}
|