@crouton-kit/crouter 0.1.8 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-skills/.crouter-plugin/plugin.json +5 -0
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +157 -0
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +156 -0
- package/dist/builtin-skills/skills/crouter-development/skills/SKILL.md +166 -0
- package/dist/commands/doctor.js +54 -2
- package/dist/commands/plugin.js +4 -1
- package/dist/commands/skill.js +78 -7
- package/dist/core/artifact.js +14 -3
- package/dist/core/frontmatter.d.ts +1 -1
- package/dist/core/frontmatter.js +5 -0
- package/dist/core/resolver.d.ts +6 -2
- package/dist/core/resolver.js +208 -20
- package/dist/core/scope.d.ts +1 -0
- package/dist/core/scope.js +13 -2
- package/dist/prompts/agent.js +87 -27
- package/dist/prompts/plan.js +34 -1
- package/dist/prompts/review.js +48 -28
- package/dist/prompts/skill.js +291 -27
- package/dist/prompts/spec.js +24 -9
- package/dist/types.d.ts +6 -1
- package/dist/types.js +4 -0
- package/package.json +2 -2
package/dist/commands/skill.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { writeFileSync } from 'node:fs';
|
|
3
|
-
import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, } from '../types.js';
|
|
3
|
+
import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, SKILL_TYPES, isSkillType, } from '../types.js';
|
|
4
4
|
import { skillConfigKey } from '../types.js';
|
|
5
5
|
import { notFound, usage, general } from '../core/errors.js';
|
|
6
6
|
import { out, hint, info, jsonOut, handleError, } from '../core/output.js';
|
|
7
7
|
import { listScopes, requireScopeRoot, resolveScopeArg, projectScopeRoot, scopeSkillsDir, } from '../core/scope.js';
|
|
8
|
-
import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, } from '../core/resolver.js';
|
|
8
|
+
import { resolveSkill, listAllSkills, listInstalledPlugins, findPluginByName, parseSkillQualifier, listSkillSiblings, listSkillChildren, } from '../core/resolver.js';
|
|
9
9
|
import { updateConfig, ensureScopeInitialized } from '../core/config.js';
|
|
10
10
|
import { parseFrontmatter, serializeFrontmatter } from '../core/frontmatter.js';
|
|
11
11
|
import { ensureDir, pathExists, readText, walkFiles } from '../core/fs-utils.js';
|
|
@@ -23,7 +23,7 @@ const KNOWN_VERBS = new Set([
|
|
|
23
23
|
'disable',
|
|
24
24
|
'search',
|
|
25
25
|
]);
|
|
26
|
-
const AUTHORING_GUIDE_SKILL = '
|
|
26
|
+
const AUTHORING_GUIDE_SKILL = 'crouter-development/skills';
|
|
27
27
|
function buildShowFooter(skillPath) {
|
|
28
28
|
return (`crtr: edit this skill directly at ${skillPath} — ` +
|
|
29
29
|
`for SKILL.md authoring guidance run \`crtr skill ${AUTHORING_GUIDE_SKILL}\``);
|
|
@@ -31,11 +31,58 @@ function buildShowFooter(skillPath) {
|
|
|
31
31
|
function wrapSkill(name, path, content) {
|
|
32
32
|
return `<skill name="${name}" path="${path}">\n${content.endsWith('\n') ? content : content + '\n'}</skill>`;
|
|
33
33
|
}
|
|
34
|
+
function formatNeighborQualifier(s) {
|
|
35
|
+
return s.plugin === SCOPE_SKILL_PLUGIN ? `${s.scope}:${s.name}` : `${s.plugin}/${s.name}`;
|
|
36
|
+
}
|
|
37
|
+
function buildNeighborsSection(skill) {
|
|
38
|
+
const siblings = listSkillSiblings(skill);
|
|
39
|
+
const children = listSkillChildren(skill);
|
|
40
|
+
if (siblings.length === 0 && children.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
const lines = [
|
|
43
|
+
'## Neighbors',
|
|
44
|
+
'*Auto-discovered from filesystem. Use `--no-neighbors` to suppress.*',
|
|
45
|
+
'',
|
|
46
|
+
];
|
|
47
|
+
if (siblings.length > 0) {
|
|
48
|
+
lines.push('**Siblings:**');
|
|
49
|
+
for (const s of siblings) {
|
|
50
|
+
const desc = s.frontmatter.description !== undefined ? s.frontmatter.description : '';
|
|
51
|
+
lines.push(`- \`${formatNeighborQualifier(s)}\`${desc ? ` — ${desc}` : ''}`);
|
|
52
|
+
}
|
|
53
|
+
if (children.length > 0)
|
|
54
|
+
lines.push('');
|
|
55
|
+
}
|
|
56
|
+
if (children.length > 0) {
|
|
57
|
+
lines.push('**Nested:**');
|
|
58
|
+
for (const s of children) {
|
|
59
|
+
const desc = s.frontmatter.description !== undefined ? s.frontmatter.description : '';
|
|
60
|
+
lines.push(`- \`${formatNeighborQualifier(s)}\`${desc ? ` — ${desc}` : ''}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
function appendNeighbors(skill, body, suppress) {
|
|
66
|
+
if (suppress)
|
|
67
|
+
return body;
|
|
68
|
+
const section = buildNeighborsSection(skill);
|
|
69
|
+
if (section === null)
|
|
70
|
+
return body;
|
|
71
|
+
const sep = body.endsWith('\n') ? '\n' : '\n\n';
|
|
72
|
+
return body + sep + `<neighbors>\n${section}\n</neighbors>\n`;
|
|
73
|
+
}
|
|
74
|
+
const SKILL_IDENTIFIER_HELP = 'Skill identifier forms (accepted by show, path, where, enable, disable):\n' +
|
|
75
|
+
' <name> bare name — resolves scope-root first, then plugins\n' +
|
|
76
|
+
' <plugin>:<name> explicit plugin (canonical)\n' +
|
|
77
|
+
' <scope>:<name> scope-root skill in a specific scope (user|project)\n' +
|
|
78
|
+
' <scope>:<plugin>/<name> fully qualified — matches `skill list` / `skill search` output\n' +
|
|
79
|
+
' <plugin>/<name> shorthand for <plugin>:<name> when unambiguous';
|
|
34
80
|
export function registerSkillCommands(program) {
|
|
35
81
|
const skill = program
|
|
36
82
|
.command('skill [nameOrVerb] [rest...]')
|
|
37
83
|
.description('manage and inspect skills')
|
|
38
84
|
.option('--frontmatter', 'include YAML frontmatter in the printed body')
|
|
85
|
+
.addHelpText('after', '\n' + SKILL_IDENTIFIER_HELP)
|
|
39
86
|
.action(async (nameOrVerb, _rest, opts) => {
|
|
40
87
|
if (nameOrVerb === undefined) {
|
|
41
88
|
out(skillPrompt());
|
|
@@ -45,7 +92,8 @@ export function registerSkillCommands(program) {
|
|
|
45
92
|
try {
|
|
46
93
|
const skillObj = resolveSkill(nameOrVerb);
|
|
47
94
|
const content = readText(skillObj.path);
|
|
48
|
-
const
|
|
95
|
+
const rawBody = opts.frontmatter ? content : parseFrontmatter(content).body;
|
|
96
|
+
const body = appendNeighbors(skillObj, rawBody, false);
|
|
49
97
|
out(wrapSkill(skillObj.name, skillObj.path, body));
|
|
50
98
|
hint(buildShowFooter(skillObj.path));
|
|
51
99
|
}
|
|
@@ -62,6 +110,8 @@ export function registerSkillCommands(program) {
|
|
|
62
110
|
.option('--plugin <name>', 'filter by plugin name')
|
|
63
111
|
.option('-a, --all', 'include disabled skills')
|
|
64
112
|
.option('--json', 'emit JSON')
|
|
113
|
+
.addHelpText('after', '\nOutput format: <scope>:<plugin>/<name> — paste this identifier into ' +
|
|
114
|
+
'`crtr skill show` to read the skill.')
|
|
65
115
|
.action(async (opts) => {
|
|
66
116
|
try {
|
|
67
117
|
const scopes = listScopes(opts.scope);
|
|
@@ -108,7 +158,14 @@ export function registerSkillCommands(program) {
|
|
|
108
158
|
.option('--scope <scope>', 'user|project')
|
|
109
159
|
.option('--plugin <name>', 'filter by plugin name')
|
|
110
160
|
.option('--frontmatter', 'include YAML frontmatter in the printed body')
|
|
161
|
+
.option('--no-neighbors', 'suppress the auto-appended ## Neighbors section')
|
|
111
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)
|
|
112
169
|
.action(async (name, opts) => {
|
|
113
170
|
try {
|
|
114
171
|
const scopeArg = resolveScopeArg(opts.scope);
|
|
@@ -118,7 +175,9 @@ export function registerSkillCommands(program) {
|
|
|
118
175
|
}
|
|
119
176
|
const skillObj = resolveSkill(name, resolveOpts);
|
|
120
177
|
const content = readText(skillObj.path);
|
|
121
|
-
const
|
|
178
|
+
const rawBody = opts.frontmatter ? content : parseFrontmatter(content).body;
|
|
179
|
+
const suppressNeighbors = opts.neighbors === false;
|
|
180
|
+
const body = appendNeighbors(skillObj, rawBody, suppressNeighbors);
|
|
122
181
|
if (opts.json) {
|
|
123
182
|
jsonOut({
|
|
124
183
|
name: skillObj.name,
|
|
@@ -220,12 +279,20 @@ export function registerSkillCommands(program) {
|
|
|
220
279
|
.description('scaffold a new skill — <name> (scope-direct) or <plugin>:<name>')
|
|
221
280
|
.option('--scope <scope>', 'user|project (default: project then user)')
|
|
222
281
|
.option('--description <text>', 'skill description for frontmatter')
|
|
282
|
+
.option('--type <type>', `skill type for frontmatter — one of: ${SKILL_TYPES.join(' | ')}`)
|
|
223
283
|
.action(async (qualifier, opts) => {
|
|
224
284
|
try {
|
|
225
285
|
const { plugin: pluginName, name: skillName } = parseSkillQualifier(qualifier);
|
|
226
286
|
if (!skillName) {
|
|
227
287
|
throw usage('skill name required');
|
|
228
288
|
}
|
|
289
|
+
let skillType;
|
|
290
|
+
if (opts.type !== undefined) {
|
|
291
|
+
if (!isSkillType(opts.type)) {
|
|
292
|
+
throw usage(`unknown skill type: ${opts.type} / valid: ${SKILL_TYPES.join(' | ')}`);
|
|
293
|
+
}
|
|
294
|
+
skillType = opts.type;
|
|
295
|
+
}
|
|
229
296
|
const scopeArg = opts.scope !== undefined ? resolveScopeArg(opts.scope) : undefined;
|
|
230
297
|
// Scope-direct: no plugin qualifier, or explicit `_:` sentinel
|
|
231
298
|
if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
|
|
@@ -251,6 +318,7 @@ export function registerSkillCommands(program) {
|
|
|
251
318
|
const fm = serializeFrontmatter({
|
|
252
319
|
name: skillName,
|
|
253
320
|
description: opts.description,
|
|
321
|
+
type: skillType,
|
|
254
322
|
});
|
|
255
323
|
writeFileSync(skillFile, fm, 'utf8');
|
|
256
324
|
out(skillFile);
|
|
@@ -277,6 +345,7 @@ export function registerSkillCommands(program) {
|
|
|
277
345
|
const fm = serializeFrontmatter({
|
|
278
346
|
name: skillName,
|
|
279
347
|
description: opts.description,
|
|
348
|
+
type: skillType,
|
|
280
349
|
});
|
|
281
350
|
writeFileSync(skillFile, fm, 'utf8');
|
|
282
351
|
out(skillFile);
|
|
@@ -290,7 +359,7 @@ export function registerSkillCommands(program) {
|
|
|
290
359
|
// create — pick a template type
|
|
291
360
|
skill
|
|
292
361
|
.command('create [topic...]')
|
|
293
|
-
.description(
|
|
362
|
+
.description(`pick a template type for a new skill (${SKILL_TYPES.join(' | ')})`)
|
|
294
363
|
.action(async (topic) => {
|
|
295
364
|
const arg = topic && topic.length > 0 ? topic.join(' ') : '';
|
|
296
365
|
out(skillCreatePrompt(arg));
|
|
@@ -298,7 +367,7 @@ export function registerSkillCommands(program) {
|
|
|
298
367
|
// template — full workflow + skeleton for one template type
|
|
299
368
|
skill
|
|
300
369
|
.command('template <type> [topic...]')
|
|
301
|
-
.description(
|
|
370
|
+
.description(`full workflow + skeleton for a template type (${SKILL_TYPES.join(' | ')})`)
|
|
302
371
|
.action(async (type, topic) => {
|
|
303
372
|
const arg = topic && topic.length > 0 ? topic.join(' ') : '';
|
|
304
373
|
out(skillTemplatePrompt(type, arg));
|
|
@@ -363,6 +432,8 @@ export function registerSkillCommands(program) {
|
|
|
363
432
|
.option('-a, --all', 'include disabled skills')
|
|
364
433
|
.option('--body', 'also search SKILL.md body')
|
|
365
434
|
.option('--json', 'emit JSON')
|
|
435
|
+
.addHelpText('after', '\nOutput columns (tab-separated): <scope>:<plugin>/<name> <matched-fields> <description>\n' +
|
|
436
|
+
'The identifier is pasteable into `crtr skill show`.')
|
|
366
437
|
.action(async (query, opts) => {
|
|
367
438
|
try {
|
|
368
439
|
const needle = query.toLowerCase();
|
package/dist/core/artifact.js
CHANGED
|
@@ -6,7 +6,8 @@ import { CRTR_DIR_NAME } from '../types.js';
|
|
|
6
6
|
import { ensureDir, pathExists, readText, walkFiles } from './fs-utils.js';
|
|
7
7
|
import { usage, notFound, general } from './errors.js';
|
|
8
8
|
import { out, hint, jsonOut, handleError, info } from './output.js';
|
|
9
|
-
import { spawnSidePaneReview, DEFAULT_PANE_OPTS, } from './spawn.js';
|
|
9
|
+
import { spawnSidePaneReview, countPanesInCurrentWindow, DEFAULT_PANE_OPTS, } from './spawn.js';
|
|
10
|
+
import { readConfig } from './config.js';
|
|
10
11
|
export function mangleCwd(cwd = process.cwd()) {
|
|
11
12
|
return cwd.replace(/\//g, '-');
|
|
12
13
|
}
|
|
@@ -32,7 +33,17 @@ export function inTmux() {
|
|
|
32
33
|
return Boolean(process.env.TMUX);
|
|
33
34
|
}
|
|
34
35
|
export function openInTmuxPane(path) {
|
|
35
|
-
|
|
36
|
+
// Always pass --watch so the pane live-updates when the agent edits the
|
|
37
|
+
// file. The watcher re-detects width on resize and survives parse errors.
|
|
38
|
+
const args = ['--tmux', '--watch'];
|
|
39
|
+
// If the current tmux window is already at the configured pane budget,
|
|
40
|
+
// open in a new window instead of cramping the existing split further.
|
|
41
|
+
const maxPanes = readConfig('user').max_panes_per_window;
|
|
42
|
+
if (countPanesInCurrentWindow() >= maxPanes) {
|
|
43
|
+
args.push('--tmux-new-window');
|
|
44
|
+
}
|
|
45
|
+
args.push(path);
|
|
46
|
+
const result = spawnSync('termrender', args, {
|
|
36
47
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
48
|
});
|
|
38
49
|
if (result.error) {
|
|
@@ -52,7 +63,7 @@ export function openInTmuxPane(path) {
|
|
|
52
63
|
}
|
|
53
64
|
const paneId = result.stdout.toString().trim();
|
|
54
65
|
if (paneId)
|
|
55
|
-
hint(`opened in tmux pane ${paneId}`);
|
|
66
|
+
hint(`opened in tmux pane ${paneId} (live — edits to the file refresh the view)`);
|
|
56
67
|
}
|
|
57
68
|
async function readStdin() {
|
|
58
69
|
const chunks = [];
|
package/dist/core/frontmatter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSkillType } from '../types.js';
|
|
1
2
|
const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
|
|
2
3
|
export function parseFrontmatter(source) {
|
|
3
4
|
const match = source.match(FRONTMATTER_RE);
|
|
@@ -126,6 +127,7 @@ function parseSimpleYaml(yaml) {
|
|
|
126
127
|
name: typeof out.name === 'string' ? out.name : '',
|
|
127
128
|
description: typeof out.description === 'string' ? out.description : undefined,
|
|
128
129
|
keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
|
|
130
|
+
type: isSkillType(out.type) ? out.type : undefined,
|
|
129
131
|
};
|
|
130
132
|
return fm;
|
|
131
133
|
}
|
|
@@ -141,6 +143,9 @@ export function serializeFrontmatter(data) {
|
|
|
141
143
|
if (data.description !== undefined) {
|
|
142
144
|
lines.push(`description: ${quoteIfNeeded(data.description)}`);
|
|
143
145
|
}
|
|
146
|
+
if (data.type !== undefined) {
|
|
147
|
+
lines.push(`type: ${data.type}`);
|
|
148
|
+
}
|
|
144
149
|
if (data.keywords && data.keywords.length) {
|
|
145
150
|
const inline = `[${data.keywords.map(quoteIfNeeded).join(', ')}]`;
|
|
146
151
|
lines.push(`keywords: ${inline}`);
|
package/dist/core/resolver.d.ts
CHANGED
|
@@ -13,15 +13,19 @@ export declare function effectiveSkillEnabled(pluginName: string, skillName: str
|
|
|
13
13
|
export declare function listSkillsInPlugin(plugin: InstalledPlugin, cfgs?: ScopeConfigs): Skill[];
|
|
14
14
|
export declare function listScopeRootSkills(scope: Scope, cfgs?: ScopeConfigs): Skill[];
|
|
15
15
|
export declare function listAllSkills(scopeFilter?: Scope): Skill[];
|
|
16
|
+
export declare function listSkillSiblings(skill: Skill): Skill[];
|
|
17
|
+
export declare function listSkillChildren(skill: Skill): Skill[];
|
|
16
18
|
export interface SkillResolutionOpts {
|
|
17
19
|
scope?: Scope;
|
|
18
20
|
pluginFilter?: string;
|
|
19
21
|
}
|
|
20
22
|
export declare function resolveSkill(rawName: string, opts?: SkillResolutionOpts): Skill;
|
|
21
|
-
export
|
|
23
|
+
export interface ParsedSkillQualifier {
|
|
24
|
+
scope?: Scope;
|
|
22
25
|
plugin?: string;
|
|
23
26
|
name: string;
|
|
24
|
-
}
|
|
27
|
+
}
|
|
28
|
+
export declare function parseSkillQualifier(raw: string): ParsedSkillQualifier;
|
|
25
29
|
export declare function listInstalledMarketplaces(scope: Scope): InstalledMarketplace[];
|
|
26
30
|
export declare function listAllMarketplaces(): InstalledMarketplace[];
|
|
27
31
|
export declare function findMarketplaceByName(name: string, scope?: Scope): InstalledMarketplace | null;
|
package/dist/core/resolver.js
CHANGED
|
@@ -4,9 +4,30 @@ import { readConfig } from './config.js';
|
|
|
4
4
|
import { listDirs, pathExists, readText, walkFiles, } from './fs-utils.js';
|
|
5
5
|
import { readMarketplaceManifest, readPluginManifest } from './manifest.js';
|
|
6
6
|
import { parseFrontmatter } from './frontmatter.js';
|
|
7
|
-
import { ambiguous, notFound } from './errors.js';
|
|
8
|
-
import { marketplacesDir, pluginsDir, projectScopeRoot, scopeSkillsDir, userScopeRoot, } from './scope.js';
|
|
7
|
+
import { ambiguous, notFound, usage } from './errors.js';
|
|
8
|
+
import { builtinSkillsRoot, marketplacesDir, pluginsDir, projectScopeRoot, scopeSkillsDir, userScopeRoot, } from './scope.js';
|
|
9
|
+
function getBuiltinPlugin() {
|
|
10
|
+
const root = builtinSkillsRoot();
|
|
11
|
+
if (!pathExists(root))
|
|
12
|
+
return null;
|
|
13
|
+
const manifest = readPluginManifest(root);
|
|
14
|
+
if (!manifest)
|
|
15
|
+
return null;
|
|
16
|
+
return {
|
|
17
|
+
name: manifest.name,
|
|
18
|
+
scope: 'builtin',
|
|
19
|
+
root,
|
|
20
|
+
manifest,
|
|
21
|
+
enabled: true,
|
|
22
|
+
builtin: true,
|
|
23
|
+
version: manifest.version,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
9
26
|
export function listInstalledPlugins(scope) {
|
|
27
|
+
if (scope === 'builtin') {
|
|
28
|
+
const builtin = getBuiltinPlugin();
|
|
29
|
+
return builtin ? [builtin] : [];
|
|
30
|
+
}
|
|
10
31
|
const dir = pluginsDir(scope);
|
|
11
32
|
if (!dir || !pathExists(dir))
|
|
12
33
|
return [];
|
|
@@ -40,13 +61,14 @@ export function listAllPlugins() {
|
|
|
40
61
|
if (projectScopeRoot())
|
|
41
62
|
scopes.push('project');
|
|
42
63
|
scopes.push('user');
|
|
64
|
+
scopes.push('builtin');
|
|
43
65
|
return scopes.flatMap(listInstalledPlugins);
|
|
44
66
|
}
|
|
45
67
|
export function findPluginByName(name, scope) {
|
|
46
68
|
if (scope) {
|
|
47
69
|
return listInstalledPlugins(scope).find((p) => p.name === name) ?? null;
|
|
48
70
|
}
|
|
49
|
-
for (const s of ['project', 'user'].filter((sc) => sc === 'project' ? projectScopeRoot() !== null : true)) {
|
|
71
|
+
for (const s of ['project', 'user', 'builtin'].filter((sc) => sc === 'project' ? projectScopeRoot() !== null : true)) {
|
|
50
72
|
const match = listInstalledPlugins(s).find((p) => p.name === name);
|
|
51
73
|
if (match)
|
|
52
74
|
return match;
|
|
@@ -100,6 +122,8 @@ export function listSkillsInPlugin(plugin, cfgs) {
|
|
|
100
122
|
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
123
|
}
|
|
102
124
|
export function listScopeRootSkills(scope, cfgs) {
|
|
125
|
+
if (scope === 'builtin')
|
|
126
|
+
return [];
|
|
103
127
|
const skillsRoot = scopeSkillsDir(scope);
|
|
104
128
|
if (!skillsRoot || !pathExists(skillsRoot))
|
|
105
129
|
return [];
|
|
@@ -138,17 +162,83 @@ export function listAllSkills(scopeFilter) {
|
|
|
138
162
|
...plugins.filter((p) => p.enabled).flatMap((p) => listSkillsInPlugin(p, cfgs)),
|
|
139
163
|
];
|
|
140
164
|
}
|
|
165
|
+
function enumerateNeighborPool(skill) {
|
|
166
|
+
if (skill.plugin === SCOPE_SKILL_PLUGIN) {
|
|
167
|
+
return listScopeRootSkills(skill.scope);
|
|
168
|
+
}
|
|
169
|
+
const plugin = listInstalledPlugins(skill.scope).find((p) => p.name === skill.plugin);
|
|
170
|
+
if (!plugin)
|
|
171
|
+
return [];
|
|
172
|
+
return listSkillsInPlugin(plugin);
|
|
173
|
+
}
|
|
174
|
+
export function listSkillSiblings(skill) {
|
|
175
|
+
const pool = enumerateNeighborPool(skill);
|
|
176
|
+
const segs = skill.name.split('/');
|
|
177
|
+
const depth = segs.length;
|
|
178
|
+
const parentPrefix = segs.slice(0, -1).join('/');
|
|
179
|
+
return pool.filter((s) => {
|
|
180
|
+
if (s.name === skill.name)
|
|
181
|
+
return false;
|
|
182
|
+
const sSegs = s.name.split('/');
|
|
183
|
+
if (sSegs.length !== depth)
|
|
184
|
+
return false;
|
|
185
|
+
if (parentPrefix === '')
|
|
186
|
+
return !s.name.includes('/');
|
|
187
|
+
return s.name.startsWith(parentPrefix + '/');
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
export function listSkillChildren(skill) {
|
|
191
|
+
const pool = enumerateNeighborPool(skill);
|
|
192
|
+
const prefix = skill.name + '/';
|
|
193
|
+
return pool.filter((s) => {
|
|
194
|
+
if (!s.name.startsWith(prefix))
|
|
195
|
+
return false;
|
|
196
|
+
const rest = s.name.slice(prefix.length);
|
|
197
|
+
return rest.length > 0 && !rest.includes('/');
|
|
198
|
+
});
|
|
199
|
+
}
|
|
141
200
|
export function resolveSkill(rawName, opts = {}) {
|
|
142
|
-
const
|
|
143
|
-
|
|
201
|
+
const parsed = parseSkillQualifier(rawName);
|
|
202
|
+
if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
|
|
203
|
+
throw usage(`scope conflict: identifier "${rawName}" uses scope "${parsed.scope}" but --scope is "${opts.scope}"`);
|
|
204
|
+
}
|
|
205
|
+
if (parsed.plugin && opts.pluginFilter && parsed.plugin !== opts.pluginFilter) {
|
|
206
|
+
throw usage(`plugin conflict: identifier "${rawName}" uses plugin "${parsed.plugin}" but --plugin is "${opts.pluginFilter}"`);
|
|
207
|
+
}
|
|
208
|
+
const effectiveScope = opts.scope ?? parsed.scope;
|
|
209
|
+
const effectivePluginFilter = opts.pluginFilter ?? parsed.plugin;
|
|
210
|
+
const direct = findSkillMatches(parsed.name, parsed.plugin, effectiveScope, effectivePluginFilter);
|
|
211
|
+
if (direct.length > 0)
|
|
212
|
+
return pickMatch(direct, parsed.name, parsed.plugin);
|
|
213
|
+
// Fallback: bare `plugin/name` (no colon) — try splitting on first `/`.
|
|
214
|
+
// Disambiguates "claude-authoring/rules" (which the search/list display also emits as
|
|
215
|
+
// "user:claude-authoring/rules") from a nested scope-root skill of the same shape.
|
|
216
|
+
if (!parsed.plugin && parsed.name.includes('/')) {
|
|
217
|
+
const slashIdx = parsed.name.indexOf('/');
|
|
218
|
+
const maybePlugin = parsed.name.slice(0, slashIdx);
|
|
219
|
+
const rest = parsed.name.slice(slashIdx + 1);
|
|
220
|
+
if (effectivePluginFilter === undefined || effectivePluginFilter === maybePlugin) {
|
|
221
|
+
const fallback = findSkillMatches(rest, maybePlugin, effectiveScope, maybePlugin);
|
|
222
|
+
if (fallback.length > 0)
|
|
223
|
+
return pickMatch(fallback, rest, maybePlugin);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
throw notFound(formatNotFoundMessage(rawName, parsed), {
|
|
227
|
+
skill: parsed.name,
|
|
228
|
+
plugin: parsed.plugin,
|
|
229
|
+
scope: parsed.scope,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function findSkillMatches(name, pluginQualifier, scope, pluginFilter) {
|
|
233
|
+
const plugins = scope ? listInstalledPlugins(scope) : listAllPlugins();
|
|
144
234
|
const enabledPlugins = plugins.filter((p) => p.enabled);
|
|
145
235
|
const cfgs = loadScopeConfigs();
|
|
146
236
|
const matches = [];
|
|
147
237
|
// Scope-root skills first — they're the user's own captured knowledge.
|
|
148
|
-
if (!
|
|
238
|
+
if (!pluginFilter &&
|
|
149
239
|
(pluginQualifier === undefined || pluginQualifier === SCOPE_SKILL_PLUGIN)) {
|
|
150
|
-
const scopes =
|
|
151
|
-
? [
|
|
240
|
+
const scopes = scope
|
|
241
|
+
? [scope]
|
|
152
242
|
: [projectScopeRoot() ? 'project' : null, 'user'].filter(Boolean);
|
|
153
243
|
for (const s of scopes) {
|
|
154
244
|
const skillsRoot = scopeSkillsDir(s);
|
|
@@ -176,7 +266,7 @@ export function resolveSkill(rawName, opts = {}) {
|
|
|
176
266
|
for (const plugin of ordered) {
|
|
177
267
|
if (pluginQualifier && plugin.name !== pluginQualifier)
|
|
178
268
|
continue;
|
|
179
|
-
if (
|
|
269
|
+
if (pluginFilter && plugin.name !== pluginFilter)
|
|
180
270
|
continue;
|
|
181
271
|
const skillPath = join(plugin.root, SKILLS_DIR, ...name.split('/'), SKILL_ENTRY_FILE);
|
|
182
272
|
if (!pathExists(skillPath))
|
|
@@ -195,20 +285,16 @@ export function resolveSkill(rawName, opts = {}) {
|
|
|
195
285
|
disabledIn,
|
|
196
286
|
});
|
|
197
287
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
: `skill not found: ${name}`, { skill: name, plugin: pluginQualifier });
|
|
202
|
-
}
|
|
288
|
+
return matches;
|
|
289
|
+
}
|
|
290
|
+
function pickMatch(matches, name, pluginQualifier) {
|
|
203
291
|
if (matches.length === 1)
|
|
204
292
|
return matches[0];
|
|
205
293
|
const sameScopeAndPlugin = matches.every((m) => m.plugin === matches[0].plugin && m.scope === matches[0].scope);
|
|
206
294
|
if (sameScopeAndPlugin)
|
|
207
295
|
return matches[0];
|
|
208
|
-
|
|
209
|
-
if (!pluginQualifier) {
|
|
296
|
+
if (!pluginQualifier)
|
|
210
297
|
return matches[0];
|
|
211
|
-
}
|
|
212
298
|
throw ambiguous(`ambiguous skill: ${name}`, {
|
|
213
299
|
skill: name,
|
|
214
300
|
candidates: matches.map((m) => ({
|
|
@@ -218,14 +304,116 @@ export function resolveSkill(rawName, opts = {}) {
|
|
|
218
304
|
})),
|
|
219
305
|
});
|
|
220
306
|
}
|
|
307
|
+
function formatNotFoundMessage(rawName, parsed) {
|
|
308
|
+
const suggestions = suggestSkills(parsed.name, parsed.plugin);
|
|
309
|
+
const lines = [`skill not found: ${rawName}`];
|
|
310
|
+
lines.push(' expected forms: <name>, <plugin>:<name>, <scope>:<plugin>/<name>');
|
|
311
|
+
if (suggestions.length > 0) {
|
|
312
|
+
const formatted = suggestions
|
|
313
|
+
.map((s) => s.plugin === SCOPE_SKILL_PLUGIN ? s.name : `${s.plugin}:${s.name}`)
|
|
314
|
+
.slice(0, 3);
|
|
315
|
+
lines.push(` did you mean: ${formatted.join(', ')}`);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
lines.push(' run `crtr skill list` or `crtr skill search <query>` to discover skills');
|
|
319
|
+
}
|
|
320
|
+
return lines.join('\n');
|
|
321
|
+
}
|
|
322
|
+
function suggestSkills(name, plugin) {
|
|
323
|
+
let all;
|
|
324
|
+
try {
|
|
325
|
+
all = listAllSkills();
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
const target = name.toLowerCase();
|
|
331
|
+
const targetBase = target.split('/').pop() ?? target;
|
|
332
|
+
const targetPluginGuess = target.includes('/') ? target.split('/')[0] : undefined;
|
|
333
|
+
const exactName = all.filter((s) => s.name.toLowerCase() === target);
|
|
334
|
+
if (exactName.length > 0)
|
|
335
|
+
return exactName;
|
|
336
|
+
const exactBase = all.filter((s) => {
|
|
337
|
+
const sBase = s.name.toLowerCase().split('/').pop() ?? s.name.toLowerCase();
|
|
338
|
+
return sBase === targetBase;
|
|
339
|
+
});
|
|
340
|
+
if (exactBase.length > 0)
|
|
341
|
+
return exactBase;
|
|
342
|
+
const scored = all
|
|
343
|
+
.map((s) => {
|
|
344
|
+
const sName = s.name.toLowerCase();
|
|
345
|
+
const sBase = sName.split('/').pop() ?? sName;
|
|
346
|
+
const sPlugin = s.plugin.toLowerCase();
|
|
347
|
+
let score = 0;
|
|
348
|
+
if (plugin !== undefined && sPlugin === plugin.toLowerCase())
|
|
349
|
+
score += 5;
|
|
350
|
+
if (targetPluginGuess !== undefined && sPlugin === targetPluginGuess)
|
|
351
|
+
score += 5;
|
|
352
|
+
if (sName.includes(target) || target.includes(sName))
|
|
353
|
+
score += 4;
|
|
354
|
+
if (sBase.includes(targetBase) || targetBase.includes(sBase))
|
|
355
|
+
score += 3;
|
|
356
|
+
if (editDistance(sBase, targetBase) <= 2)
|
|
357
|
+
score += 4;
|
|
358
|
+
return { skill: s, score };
|
|
359
|
+
})
|
|
360
|
+
.filter((x) => x.score > 0)
|
|
361
|
+
.sort((a, b) => b.score - a.score);
|
|
362
|
+
return scored.slice(0, 3).map((x) => x.skill);
|
|
363
|
+
}
|
|
364
|
+
function editDistance(a, b) {
|
|
365
|
+
if (a === b)
|
|
366
|
+
return 0;
|
|
367
|
+
if (a.length === 0)
|
|
368
|
+
return b.length;
|
|
369
|
+
if (b.length === 0)
|
|
370
|
+
return a.length;
|
|
371
|
+
const prev = new Array(b.length + 1);
|
|
372
|
+
const curr = new Array(b.length + 1);
|
|
373
|
+
for (let j = 0; j <= b.length; j++)
|
|
374
|
+
prev[j] = j;
|
|
375
|
+
for (let i = 1; i <= a.length; i++) {
|
|
376
|
+
curr[0] = i;
|
|
377
|
+
for (let j = 1; j <= b.length; j++) {
|
|
378
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
379
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
380
|
+
}
|
|
381
|
+
for (let j = 0; j <= b.length; j++)
|
|
382
|
+
prev[j] = curr[j];
|
|
383
|
+
}
|
|
384
|
+
return prev[b.length];
|
|
385
|
+
}
|
|
386
|
+
const SCOPE_QUALIFIERS = new Set(['user', 'project']);
|
|
387
|
+
// Accepted identifier forms:
|
|
388
|
+
// <name> — bare name; scope-root first, then plugins
|
|
389
|
+
// <plugin>:<name> — explicit plugin
|
|
390
|
+
// <scope>:<name> — scope-root in a specific scope
|
|
391
|
+
// <scope>:<plugin>/<name> — fully qualified (matches `skill list` / `skill search` display)
|
|
392
|
+
// Bare `<plugin>/<name>` (no colon) is handled as a fallback inside resolveSkill.
|
|
221
393
|
export function parseSkillQualifier(raw) {
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
394
|
+
const colonIdx = raw.indexOf(':');
|
|
395
|
+
if (colonIdx === -1)
|
|
224
396
|
return { name: raw };
|
|
225
|
-
|
|
397
|
+
const before = raw.slice(0, colonIdx);
|
|
398
|
+
const after = raw.slice(colonIdx + 1);
|
|
399
|
+
if (SCOPE_QUALIFIERS.has(before)) {
|
|
400
|
+
const scope = before;
|
|
401
|
+
const slashIdx = after.indexOf('/');
|
|
402
|
+
if (slashIdx !== -1) {
|
|
403
|
+
return {
|
|
404
|
+
scope,
|
|
405
|
+
plugin: after.slice(0, slashIdx),
|
|
406
|
+
name: after.slice(slashIdx + 1),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return { scope, name: after };
|
|
410
|
+
}
|
|
411
|
+
return { plugin: before, name: after };
|
|
226
412
|
}
|
|
227
413
|
function orderPluginsByResolution(plugins) {
|
|
228
414
|
const score = (p) => {
|
|
415
|
+
if (p.scope === 'builtin')
|
|
416
|
+
return 4;
|
|
229
417
|
const fromMarketplace = Boolean(p.sourceMarketplace);
|
|
230
418
|
if (p.scope === 'project' && !fromMarketplace)
|
|
231
419
|
return 0;
|
package/dist/core/scope.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Scope } from '../types.js';
|
|
2
|
+
export declare function builtinSkillsRoot(): string;
|
|
2
3
|
export declare function userScopeRoot(): string;
|
|
3
4
|
export declare function findProjectScopeRoot(startDir?: string): string | null;
|
|
4
5
|
export declare function projectScopeRoot(startDir?: string): string | null;
|
package/dist/core/scope.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { join, resolve, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
5
6
|
import { usage } from './errors.js';
|
|
6
7
|
let cachedProjectRoot;
|
|
8
|
+
export function builtinSkillsRoot() {
|
|
9
|
+
// Resolve relative to this file: src/core/scope.ts → src/builtin-skills/ OR dist/core/scope.js → dist/builtin-skills/
|
|
10
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
11
|
+
const coreDir = dirname(thisFile);
|
|
12
|
+
const pkgDir = dirname(coreDir); // src/ or dist/
|
|
13
|
+
return join(pkgDir, 'builtin-skills');
|
|
14
|
+
}
|
|
7
15
|
export function userScopeRoot() {
|
|
8
16
|
return join(homedir(), CRTR_DIR_NAME);
|
|
9
17
|
}
|
|
@@ -37,6 +45,8 @@ export function projectScopeRoot(startDir) {
|
|
|
37
45
|
return findProjectScopeRoot(startDir);
|
|
38
46
|
}
|
|
39
47
|
export function scopeRoot(scope) {
|
|
48
|
+
if (scope === 'builtin')
|
|
49
|
+
return builtinSkillsRoot();
|
|
40
50
|
return scope === 'user' ? userScopeRoot() : projectScopeRoot();
|
|
41
51
|
}
|
|
42
52
|
export function requireScopeRoot(scope) {
|
|
@@ -71,9 +81,9 @@ export function resolveScopeArg(scopeArg) {
|
|
|
71
81
|
if (scopeArg === undefined)
|
|
72
82
|
return 'all';
|
|
73
83
|
const value = scopeArg.toLowerCase();
|
|
74
|
-
if (value === 'user' || value === 'project' || value === 'all')
|
|
84
|
+
if (value === 'user' || value === 'project' || value === 'builtin' || value === 'all')
|
|
75
85
|
return value;
|
|
76
|
-
throw usage(`invalid --scope: ${scopeArg} (expected user|project|all)`);
|
|
86
|
+
throw usage(`invalid --scope: ${scopeArg} (expected user|project|builtin|all)`);
|
|
77
87
|
}
|
|
78
88
|
export function listScopes(scopeArg) {
|
|
79
89
|
const v = resolveScopeArg(scopeArg);
|
|
@@ -82,6 +92,7 @@ export function listScopes(scopeArg) {
|
|
|
82
92
|
if (projectScopeRoot())
|
|
83
93
|
out.push('project');
|
|
84
94
|
out.push('user');
|
|
95
|
+
out.push('builtin');
|
|
85
96
|
return out;
|
|
86
97
|
}
|
|
87
98
|
return [v];
|