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