@crouton-kit/crouter 0.3.11 → 0.3.13
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 +14 -6
- package/dist/commands/{mode.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/daemon.d.ts +2 -0
- 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 +10 -454
- package/dist/commands/node.d.ts +2 -0
- package/dist/commands/node.js +407 -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 +3 -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 +6 -691
- 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 +4 -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/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 +5 -0
- package/dist/core/command.js +35 -10
- 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/help.js +5 -3
- 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 +109 -1
- 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 +55 -0
- package/dist/core/runtime/presence.js +198 -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 +87 -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 +31 -0
- package/dist/core/runtime/spawn.js +123 -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 +107 -0
- package/dist/core/runtime/tmux.js +244 -0
- package/dist/core/spawn.d.ts +17 -197
- package/dist/core/spawn.js +16 -539
- 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 +396 -0
- package/package.json +6 -5
- package/dist/commands/agent.d.ts +0 -6
- package/dist/commands/agent.js +0 -585
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -192
- package/dist/commands/job.d.ts +0 -11
- package/dist/commands/job.js +0 -384
- package/dist/commands/mode.js +0 -231
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -322
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -299
- 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 -98
- package/dist/core/__tests__/spawn.test.js +0 -138
- package/dist/core/__tests__/subagents.test.d.ts +0 -1
- package/dist/core/__tests__/subagents.test.js +0 -75
- package/dist/core/jobs.d.ts +0 -107
- package/dist/core/jobs.js +0 -565
- package/dist/core/subagents.d.ts +0 -18
- package/dist/core/subagents.js +0 -163
- package/dist/prompts/agent.d.ts +0 -27
- package/dist/prompts/agent.js +0 -184
- 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/{core/__tests__/spawn.test.d.ts → daemon/crtrd-cli.d.ts} +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// `crtr push` + `crtr feed` — the one verb up the graph, and its read side.
|
|
2
|
+
//
|
|
3
|
+
// crtr push update [body] — routine progress (also auto-emitted every stop)
|
|
4
|
+
// crtr push urgent [body] — force-wake subscribers
|
|
5
|
+
// crtr push final [body] — finish: write result, mark node done, close window
|
|
6
|
+
// crtr feed read — drain the caller's (or a named) inbox into a digest
|
|
7
|
+
//
|
|
8
|
+
// "push" is THE verb a node uses to talk to its managers; the tier is the
|
|
9
|
+
// subcommand. The caller's node is resolved from CRTR_NODE_ID (injected by the
|
|
10
|
+
// runtime into every node process). Absent ⇒ InputError — push is only
|
|
11
|
+
// meaningful inside a live node.
|
|
12
|
+
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
13
|
+
import { InputError, readStdinRaw } from '../core/io.js';
|
|
14
|
+
import { push } from '../core/feed/feed.js';
|
|
15
|
+
import { readInboxSince, readCursor, writeCursor, coalesce, } from '../core/feed/inbox.js';
|
|
16
|
+
function requireCallerNode() {
|
|
17
|
+
const id = process.env['CRTR_NODE_ID'];
|
|
18
|
+
if (id === undefined || id.trim() === '') {
|
|
19
|
+
throw new InputError({
|
|
20
|
+
error: 'no_node_context',
|
|
21
|
+
message: 'CRTR_NODE_ID is not set — push runs inside a canvas node.',
|
|
22
|
+
next: 'Run push from a node process spawned by the runtime.',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return id.trim();
|
|
26
|
+
}
|
|
27
|
+
const TIER_BLURB = {
|
|
28
|
+
update: 'routine progress — fans a pointer to subscribers, no forced wake',
|
|
29
|
+
urgent: 'force-wake subscribers (inbox tier urgent)',
|
|
30
|
+
final: 'finish: write the canonical result, mark the node done, close its window',
|
|
31
|
+
};
|
|
32
|
+
function makeTierLeaf(tier) {
|
|
33
|
+
return defineLeaf({
|
|
34
|
+
name: tier,
|
|
35
|
+
help: {
|
|
36
|
+
name: `push ${tier}`,
|
|
37
|
+
summary: TIER_BLURB[tier],
|
|
38
|
+
params: [
|
|
39
|
+
{ kind: 'stdin', name: 'body', required: true, constraint: 'Report body (markdown). Positional or stdin.' },
|
|
40
|
+
],
|
|
41
|
+
output: [
|
|
42
|
+
{ name: 'report_path', type: 'string', required: true, constraint: 'Path of the written report.' },
|
|
43
|
+
{ name: 'delivered_to', type: 'string[]', required: true, constraint: 'Subscriber node ids that received a pointer.' },
|
|
44
|
+
{ name: 'status', type: 'string', required: true, constraint: '"done" for final, else "active".' },
|
|
45
|
+
],
|
|
46
|
+
outputKind: 'object',
|
|
47
|
+
effects: [
|
|
48
|
+
'Writes nodes/<nodeId>/reports/<ts>-<tier>.md.',
|
|
49
|
+
'Appends one inbox pointer per subscriber.',
|
|
50
|
+
...(tier === 'final' ? ['Marks the node done (status + intent); its window closes on next stop.'] : []),
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
run: async (input) => {
|
|
54
|
+
const nodeId = requireCallerNode();
|
|
55
|
+
let body = typeof input['body'] === 'string' ? input['body'].trim() : '';
|
|
56
|
+
if (body === '')
|
|
57
|
+
body = (await readStdinRaw()).trim();
|
|
58
|
+
if (body === '') {
|
|
59
|
+
throw new InputError({ error: 'missing_body', message: 'no report body', field: 'body', next: 'Pass the body as an argument or on stdin.' });
|
|
60
|
+
}
|
|
61
|
+
const result = await push(nodeId, { kind: tier, body });
|
|
62
|
+
return { report_path: result.reportPath, delivered_to: result.deliveredTo, status: tier === 'final' ? 'done' : 'active' };
|
|
63
|
+
},
|
|
64
|
+
render: (r) => {
|
|
65
|
+
const n = Array.isArray(r['delivered_to']) ? r['delivered_to'].length : 0;
|
|
66
|
+
const line = tier === 'final'
|
|
67
|
+
? 'Result recorded — node finished; its window closes on next stop. Nothing more to do here.'
|
|
68
|
+
: tier === 'urgent'
|
|
69
|
+
? `Urgent report fanned to ${n} subscriber(s) — they are force-woken.`
|
|
70
|
+
: `Progress report fanned to ${n} subscriber(s).`;
|
|
71
|
+
return `<pushed tier="${tier}" status="${r['status']}" delivered="${n}">\n${line}\nreport: ${r['report_path']}\n</pushed>`;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// feed read — drain the inbox
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
const feedReadLeaf = defineLeaf({
|
|
79
|
+
name: 'read',
|
|
80
|
+
help: {
|
|
81
|
+
name: 'feed read',
|
|
82
|
+
summary: 'drain unread inbox pointers for the caller (or a named node) into a compact digest',
|
|
83
|
+
params: [
|
|
84
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node whose inbox to read. Defaults to CRTR_NODE_ID. Use to inspect a worker\'s inbox as an orchestrator.' },
|
|
85
|
+
{ kind: 'flag', name: 'all', type: 'bool', required: false, default: false, constraint: 'Ignore the cursor and return everything from the start.' },
|
|
86
|
+
],
|
|
87
|
+
output: [
|
|
88
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'Node whose inbox was read.' },
|
|
89
|
+
{ name: 'digest', type: 'string', required: true, constraint: 'Coalesced digest — paste directly into a prompt.' },
|
|
90
|
+
{ name: 'entries', type: 'object[]', required: true, constraint: 'Raw InboxEntry objects.' },
|
|
91
|
+
{ name: 'cursor', type: 'string', required: true, constraint: 'New cursor ISO written after draining.' },
|
|
92
|
+
],
|
|
93
|
+
outputKind: 'object',
|
|
94
|
+
effects: ['Advances nodes/<nodeId>/inbox.jsonl.cursor.', 'Read-only on inbox.jsonl itself.'],
|
|
95
|
+
},
|
|
96
|
+
run: async (input) => {
|
|
97
|
+
const nodeId = typeof input['node'] === 'string' && input['node'].trim() !== ''
|
|
98
|
+
? input['node'].trim()
|
|
99
|
+
: requireCallerNode();
|
|
100
|
+
const cursor = input['all'] === true ? undefined : readCursor(nodeId);
|
|
101
|
+
const entries = readInboxSince(nodeId, cursor);
|
|
102
|
+
const newCursor = entries.length > 0 ? entries[entries.length - 1].ts : cursor ?? new Date().toISOString();
|
|
103
|
+
writeCursor(nodeId, newCursor);
|
|
104
|
+
return {
|
|
105
|
+
node_id: nodeId,
|
|
106
|
+
digest: coalesce(entries),
|
|
107
|
+
entries: entries,
|
|
108
|
+
cursor: newCursor,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
render: (r) => {
|
|
112
|
+
const n = Array.isArray(r['entries']) ? r['entries'].length : 0;
|
|
113
|
+
const digest = typeof r['digest'] === 'string' && r['digest'].trim() !== ''
|
|
114
|
+
? r['digest']
|
|
115
|
+
: 'Inbox empty — nothing new from your subscriptions.';
|
|
116
|
+
return `<feed node="${r['node_id']}" unread="${n}">\n${digest}\n</feed>`;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Registration
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
export function registerPush() {
|
|
123
|
+
return defineBranch({
|
|
124
|
+
name: 'push',
|
|
125
|
+
rootEntry: {
|
|
126
|
+
concept: 'the one verb up the graph — a node reports to whoever subscribes to it',
|
|
127
|
+
desc: 'push a report (update / urgent / final) to your subscribers',
|
|
128
|
+
useWhen: 'sharing progress, raising an alert, or finishing your work',
|
|
129
|
+
},
|
|
130
|
+
help: {
|
|
131
|
+
name: 'push',
|
|
132
|
+
summary: 'push a report to your subscribers',
|
|
133
|
+
model: 'A push writes a markdown report to the node\'s reports/ history and fans a lightweight pointer to every subscriber\'s inbox (not the content — they dereference lazily). The stophook auto-pushes an `update` every stop, so the feed is continuous; you push explicitly for intentional signals. `push final` is how ANY node finishes: write the canonical result, mark done, close the window.',
|
|
134
|
+
children: [
|
|
135
|
+
{ name: 'update', desc: TIER_BLURB.update, useWhen: 'sharing routine progress explicitly' },
|
|
136
|
+
{ name: 'urgent', desc: TIER_BLURB.urgent, useWhen: 'something your managers must see now' },
|
|
137
|
+
{ name: 'final', desc: TIER_BLURB.final, useWhen: 'the work is done — this finishes the node' },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
children: [makeTierLeaf('update'), makeTierLeaf('urgent'), makeTierLeaf('final')],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
export function registerFeed() {
|
|
144
|
+
return defineBranch({
|
|
145
|
+
name: 'feed',
|
|
146
|
+
rootEntry: {
|
|
147
|
+
concept: 'the read side of the spine — pointers your subscriptions have pushed',
|
|
148
|
+
desc: 'drain your inbox feed into a digest',
|
|
149
|
+
useWhen: 'catching up on what the nodes you subscribe to have reported',
|
|
150
|
+
},
|
|
151
|
+
help: {
|
|
152
|
+
name: 'feed',
|
|
153
|
+
summary: 'read the per-node inbox feed',
|
|
154
|
+
model: 'Each node has an inbox.jsonl that accumulates ~30-token pointers from publishers it subscribes to. `feed read` coalesces unread pointers into one digest; dereference the reports that matter by reading their ref paths.',
|
|
155
|
+
children: [{ name: 'read', desc: 'drain unread pointers into a digest', useWhen: 'checking what your subscriptions pushed' }],
|
|
156
|
+
},
|
|
157
|
+
children: [feedReadLeaf],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// `crtr canvas revive` — explicit node revival.
|
|
2
|
+
//
|
|
3
|
+
// Bypasses the daemon: directly opens a fresh tmux window for a node that is
|
|
4
|
+
// done, idle, or dead. Default behavior resumes the saved pi conversation
|
|
5
|
+
// (--resume); pass --fresh to start a clean pi session against the context dir.
|
|
6
|
+
import { defineLeaf } from '../core/command.js';
|
|
7
|
+
import { InputError } from '../core/io.js';
|
|
8
|
+
import { reviveNode } from '../core/runtime/revive.js';
|
|
9
|
+
import { getNode } from '../core/canvas/index.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// revive node
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export const reviveLeaf = defineLeaf({
|
|
14
|
+
name: 'revive',
|
|
15
|
+
help: {
|
|
16
|
+
name: 'canvas revive',
|
|
17
|
+
summary: 'open a fresh tmux window for a node, optionally resuming its saved pi conversation',
|
|
18
|
+
params: [
|
|
19
|
+
{
|
|
20
|
+
kind: 'positional',
|
|
21
|
+
name: 'node',
|
|
22
|
+
required: true,
|
|
23
|
+
constraint: 'Node id to revive.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
kind: 'flag',
|
|
27
|
+
name: 'fresh',
|
|
28
|
+
type: 'bool',
|
|
29
|
+
required: false,
|
|
30
|
+
default: false,
|
|
31
|
+
constraint: 'When set, start a clean pi session (no --resume). Default: resume the saved conversation.',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
output: [
|
|
35
|
+
{ name: 'window', type: 'string', required: false, constraint: 'New tmux window id.' },
|
|
36
|
+
{ name: 'session', type: 'string', required: true, constraint: 'Tmux session the node was placed in.' },
|
|
37
|
+
{ name: 'resumed', type: 'boolean', required: true, constraint: 'True when pi was told to --resume the saved conversation.' },
|
|
38
|
+
],
|
|
39
|
+
outputKind: 'object',
|
|
40
|
+
effects: [
|
|
41
|
+
'Opens a background (non-focus-stealing) tmux window running pi.',
|
|
42
|
+
'Updates the node\'s canvas record: status=active, intent=null, window=<new>.',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
run: async (input) => {
|
|
46
|
+
const nodeId = input['node'];
|
|
47
|
+
const fresh = input['fresh'] ?? false;
|
|
48
|
+
// Validate the node exists before attempting revival.
|
|
49
|
+
const meta = getNode(nodeId);
|
|
50
|
+
if (meta === null) {
|
|
51
|
+
throw new InputError({
|
|
52
|
+
error: 'not_found',
|
|
53
|
+
message: `no node: ${nodeId}`,
|
|
54
|
+
next: 'List nodes with `crtr node inspect list`.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const result = reviveNode(nodeId, { resume: !fresh });
|
|
58
|
+
return {
|
|
59
|
+
window: result.window ?? undefined,
|
|
60
|
+
session: result.session,
|
|
61
|
+
resumed: result.resumed,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { defineLeaf, defineBranch } from '../../core/command.js';
|
|
4
|
+
import { usage, general, notFound } from '../../core/errors.js';
|
|
5
|
+
import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, SKILL_TYPES, isSkillType, } from '../../types.js';
|
|
6
|
+
import { parseSkillQualifier, findPluginByName, } from '../../core/resolver.js';
|
|
7
|
+
import { resolveScopeArg, requireScopeRoot, scopeSkillsDir, } from '../../core/scope.js';
|
|
8
|
+
import { serializeFrontmatter } from '../../core/frontmatter.js';
|
|
9
|
+
import { ensureScopeInitialized } from '../../core/config.js';
|
|
10
|
+
import { ensureDir, pathExists } from '../../core/fs-utils.js';
|
|
11
|
+
import { skillCreatePrompt, skillTemplatePrompt } from '../../prompts/skill.js';
|
|
12
|
+
import { VALID_TYPES, resolveWriteScope } from './shared.js';
|
|
13
|
+
export const authorGuide = defineLeaf({
|
|
14
|
+
name: 'guide',
|
|
15
|
+
help: {
|
|
16
|
+
name: 'skill author guide',
|
|
17
|
+
summary: 'load the skill authoring workflow — two stages: omit type to pick one, pass type for its full skeleton',
|
|
18
|
+
params: [
|
|
19
|
+
{ 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.' },
|
|
20
|
+
{ kind: 'flag', name: 'topic', type: 'string', required: false, constraint: 'Optional topic context injected into the guide.' },
|
|
21
|
+
],
|
|
22
|
+
output: [
|
|
23
|
+
{ 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.' },
|
|
24
|
+
{ name: 'type', type: 'string | null', required: true, constraint: 'Echo of the requested type, or null on the picker stage.' },
|
|
25
|
+
],
|
|
26
|
+
outputKind: 'object',
|
|
27
|
+
effects: ['None. Read-only.'],
|
|
28
|
+
},
|
|
29
|
+
run: async (input) => {
|
|
30
|
+
const type = input['type'];
|
|
31
|
+
const topic = input['topic'];
|
|
32
|
+
const topicArg = topic !== undefined ? topic : '';
|
|
33
|
+
// Progressive disclosure: no type → template picker (stage 1);
|
|
34
|
+
// type given → that type's full workflow + skeleton (stage 2).
|
|
35
|
+
if (type === undefined) {
|
|
36
|
+
return { guide: skillCreatePrompt(topicArg), type: null };
|
|
37
|
+
}
|
|
38
|
+
return { guide: skillTemplatePrompt(type, topicArg), type };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
export const authorScaffold = defineLeaf({
|
|
42
|
+
name: 'scaffold',
|
|
43
|
+
help: {
|
|
44
|
+
name: 'skill author scaffold',
|
|
45
|
+
summary: 'create an empty SKILL.md stub at the given qualifier',
|
|
46
|
+
params: [
|
|
47
|
+
{ kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier in <plugin>/<skill> form.' },
|
|
48
|
+
{ kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform.' },
|
|
49
|
+
{ kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Short description written to frontmatter.' },
|
|
50
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
51
|
+
],
|
|
52
|
+
output: [
|
|
53
|
+
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded SKILL.md.' },
|
|
54
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call to load the authoring guide.' },
|
|
55
|
+
],
|
|
56
|
+
outputKind: 'object',
|
|
57
|
+
effects: [
|
|
58
|
+
'Creates the skill directory and SKILL.md stub at the resolved location.',
|
|
59
|
+
'Writes frontmatter with name, description (if provided), and type (if provided).',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
run: async (input) => {
|
|
63
|
+
const qualifier = input['qualifier'];
|
|
64
|
+
const typeStr = input['type'];
|
|
65
|
+
const description = input['description'];
|
|
66
|
+
const scopeStr = input['scope'];
|
|
67
|
+
const parsed = parseSkillQualifier(qualifier);
|
|
68
|
+
if (parsed.segments.length === 0) {
|
|
69
|
+
throw usage('skill name required in qualifier');
|
|
70
|
+
}
|
|
71
|
+
// For scaffold, the qualifier is always <plugin>/<skill>. If it's a single segment,
|
|
72
|
+
// treat it as a scope-direct skill name. Otherwise first segment is the plugin.
|
|
73
|
+
const pluginName = parsed.segments.length > 1 ? parsed.segments[0] : undefined;
|
|
74
|
+
const skillName = parsed.segments.length > 1
|
|
75
|
+
? parsed.segments.slice(1).join('/')
|
|
76
|
+
: parsed.segments[0];
|
|
77
|
+
if (typeStr !== undefined && !isSkillType(typeStr)) {
|
|
78
|
+
throw usage(`unknown skill type: ${typeStr} / valid: ${SKILL_TYPES.join(' | ')}`);
|
|
79
|
+
}
|
|
80
|
+
const skillType = typeStr !== undefined && isSkillType(typeStr) ? typeStr : undefined;
|
|
81
|
+
let skillFile;
|
|
82
|
+
// Scope-direct: no plugin qualifier, or explicit `_/` sentinel (internal only)
|
|
83
|
+
if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
|
|
84
|
+
const scope = resolveWriteScope(scopeStr);
|
|
85
|
+
const scopeRootPath = requireScopeRoot(scope);
|
|
86
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
87
|
+
const skillsRoot = scopeSkillsDir(scope);
|
|
88
|
+
if (!skillsRoot) {
|
|
89
|
+
throw general(`no skills dir for scope ${scope}`);
|
|
90
|
+
}
|
|
91
|
+
const skillDir = join(skillsRoot, ...skillName.split('/'));
|
|
92
|
+
skillFile = join(skillDir, SKILL_ENTRY_FILE);
|
|
93
|
+
if (pathExists(skillFile)) {
|
|
94
|
+
throw general(`skill already exists: ${skillFile}`);
|
|
95
|
+
}
|
|
96
|
+
ensureDir(skillDir);
|
|
97
|
+
const fm = serializeFrontmatter({
|
|
98
|
+
name: skillName,
|
|
99
|
+
description,
|
|
100
|
+
type: skillType,
|
|
101
|
+
});
|
|
102
|
+
writeFileSync(skillFile, fm, 'utf8');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Plugin-scoped scaffold
|
|
106
|
+
const scopeForLookup = scopeStr !== undefined
|
|
107
|
+
? (() => {
|
|
108
|
+
const r = resolveScopeArg(scopeStr);
|
|
109
|
+
return r !== 'all' ? r : undefined;
|
|
110
|
+
})()
|
|
111
|
+
: undefined;
|
|
112
|
+
const plugin = scopeForLookup !== undefined
|
|
113
|
+
? findPluginByName(pluginName, scopeForLookup)
|
|
114
|
+
: findPluginByName(pluginName);
|
|
115
|
+
if (!plugin) {
|
|
116
|
+
throw notFound(`plugin not found: ${pluginName}`);
|
|
117
|
+
}
|
|
118
|
+
const skillDir = join(plugin.root, SKILLS_DIR, ...skillName.split('/'));
|
|
119
|
+
skillFile = join(skillDir, SKILL_ENTRY_FILE);
|
|
120
|
+
if (pathExists(skillFile)) {
|
|
121
|
+
throw general(`skill already exists: ${skillFile}`);
|
|
122
|
+
}
|
|
123
|
+
ensureDir(skillDir);
|
|
124
|
+
const fm = serializeFrontmatter({
|
|
125
|
+
name: skillName,
|
|
126
|
+
description,
|
|
127
|
+
type: skillType,
|
|
128
|
+
});
|
|
129
|
+
writeFileSync(skillFile, fm, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
const typeHint = skillType !== undefined ? `--type ${skillType} ` : '';
|
|
132
|
+
const follow_up = `crtr skill author guide ${typeHint}--topic "${skillName}"`;
|
|
133
|
+
return { path: skillFile, follow_up };
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
export const authorBranch = defineBranch({
|
|
137
|
+
name: 'author',
|
|
138
|
+
help: {
|
|
139
|
+
name: 'skill author',
|
|
140
|
+
summary: 'create and scaffold new skills',
|
|
141
|
+
children: [
|
|
142
|
+
{ name: 'guide', desc: 'load authoring workflow + skeleton for a type', useWhen: 'writing a new skill and need the template and instructions' },
|
|
143
|
+
{ name: 'scaffold', desc: 'create an empty SKILL.md stub', useWhen: 'initializing the file before writing content' },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
children: [authorGuide, authorScaffold],
|
|
147
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const findList: import("../../core/command.js").LeafDef;
|
|
2
|
+
export declare const findSearch: import("../../core/command.js").LeafDef;
|
|
3
|
+
export declare const findGrep: import("../../core/command.js").LeafDef;
|
|
4
|
+
export declare const findBranch: import("../../core/command.js").BranchDef;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { defineLeaf, defineBranch } from '../../core/command.js';
|
|
3
|
+
import { usage } from '../../core/errors.js';
|
|
4
|
+
import { SCOPE_SKILL_PLUGIN, SKILLS_DIR } from '../../types.js';
|
|
5
|
+
import { listScopes, scopeSkillsDir } from '../../core/scope.js';
|
|
6
|
+
import { listAllSkills, listInstalledPlugins } from '../../core/resolver.js';
|
|
7
|
+
import { paginate } from '../../core/pagination.js';
|
|
8
|
+
import { walkFiles, readText } from '../../core/fs-utils.js';
|
|
9
|
+
export const findList = defineLeaf({
|
|
10
|
+
name: 'list',
|
|
11
|
+
help: {
|
|
12
|
+
name: 'skill find list',
|
|
13
|
+
summary: 'paginated list of installed skills',
|
|
14
|
+
params: [
|
|
15
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
|
|
16
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Filter to a single plugin name.' },
|
|
17
|
+
{ kind: 'flag', name: 'include-disabled', type: 'bool', required: false, constraint: 'When present, includes disabled skills.' },
|
|
18
|
+
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 50, constraint: 'Max 200.' },
|
|
19
|
+
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
20
|
+
{ 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.' },
|
|
21
|
+
],
|
|
22
|
+
output: [
|
|
23
|
+
{ 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.' },
|
|
24
|
+
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'null means no more items.' },
|
|
25
|
+
{ name: 'total', type: 'integer | null', required: true, constraint: 'Exact when cheap; null otherwise.' },
|
|
26
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands for drilling into an item or refining the list.' },
|
|
27
|
+
],
|
|
28
|
+
outputKind: 'object',
|
|
29
|
+
effects: ['None. Read-only.'],
|
|
30
|
+
},
|
|
31
|
+
run: async (input) => {
|
|
32
|
+
const scopeStr = input['scope'];
|
|
33
|
+
const pluginFilter = input['plugin'];
|
|
34
|
+
const includeDisabled = input['includeDisabled'];
|
|
35
|
+
const limitRaw = input['limit'];
|
|
36
|
+
const limit = Math.min(Math.max(1, limitRaw), 200);
|
|
37
|
+
const cursor = input['cursor'];
|
|
38
|
+
const full = input['full'];
|
|
39
|
+
const scopes = listScopes(scopeStr);
|
|
40
|
+
const skills = scopes
|
|
41
|
+
.flatMap((s) => listAllSkills(s))
|
|
42
|
+
.filter((sk) => {
|
|
43
|
+
if (pluginFilter !== undefined && sk.plugin !== pluginFilter)
|
|
44
|
+
return false;
|
|
45
|
+
if (!includeDisabled && !sk.enabled)
|
|
46
|
+
return false;
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
// Sort by scope then plugin then name ascending
|
|
50
|
+
const scopeOrder = { project: 0, user: 1, builtin: 2 };
|
|
51
|
+
skills.sort((a, b) => {
|
|
52
|
+
const so = (scopeOrder[a.scope] !== undefined ? scopeOrder[a.scope] : 3) -
|
|
53
|
+
(scopeOrder[b.scope] !== undefined ? scopeOrder[b.scope] : 3);
|
|
54
|
+
if (so !== 0)
|
|
55
|
+
return so;
|
|
56
|
+
const po = a.plugin.localeCompare(b.plugin);
|
|
57
|
+
if (po !== 0)
|
|
58
|
+
return po;
|
|
59
|
+
return a.name.localeCompare(b.name);
|
|
60
|
+
});
|
|
61
|
+
const keyOf = (sk) => `${sk.scope}/${sk.plugin}/${sk.name}`;
|
|
62
|
+
const params = {};
|
|
63
|
+
if (limit !== undefined)
|
|
64
|
+
params.limit = limit;
|
|
65
|
+
if (cursor !== undefined)
|
|
66
|
+
params.cursor = cursor;
|
|
67
|
+
const result = paginate(skills, params, {
|
|
68
|
+
defaultLimit: 50,
|
|
69
|
+
maxLimit: 200,
|
|
70
|
+
keyOf,
|
|
71
|
+
total: 'count',
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
items: result.items.map((sk) => {
|
|
75
|
+
const base = {
|
|
76
|
+
name: sk.name,
|
|
77
|
+
plugin: sk.plugin,
|
|
78
|
+
scope: sk.scope,
|
|
79
|
+
enabled: sk.enabled,
|
|
80
|
+
disabled_in: sk.disabledIn !== undefined ? sk.disabledIn : null,
|
|
81
|
+
};
|
|
82
|
+
if (full) {
|
|
83
|
+
base['description'] = sk.frontmatter.description !== undefined ? sk.frontmatter.description : null;
|
|
84
|
+
}
|
|
85
|
+
return base;
|
|
86
|
+
}),
|
|
87
|
+
next_cursor: result.next_cursor,
|
|
88
|
+
total: result.total,
|
|
89
|
+
follow_up: 'Use `crtr skill read <name>` for the full SKILL.md body. Run `crtr skill find list -h` for filters and verbosity.',
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
export const findSearch = defineLeaf({
|
|
94
|
+
name: 'search',
|
|
95
|
+
help: {
|
|
96
|
+
name: 'skill find search',
|
|
97
|
+
summary: 'search skills by name, description, and keywords',
|
|
98
|
+
params: [
|
|
99
|
+
{ kind: 'positional', name: 'query', required: true, constraint: 'Whitespace-separated terms matched case-insensitively against name, description, and keywords; skills matching more terms rank higher.' },
|
|
100
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
|
|
101
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Filter to a single plugin name.' },
|
|
102
|
+
{ kind: 'flag', name: 'include-disabled', type: 'bool', required: false, constraint: 'When present, includes disabled skills.' },
|
|
103
|
+
{ kind: 'flag', name: 'search-body', type: 'bool', required: false, constraint: 'When present, also searches inside SKILL.md body text.' },
|
|
104
|
+
],
|
|
105
|
+
output: [
|
|
106
|
+
{ name: 'query', type: 'string', required: true, constraint: 'Echo of the input query.' },
|
|
107
|
+
{ 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.' },
|
|
108
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands for drilling into a hit or refining the search.' },
|
|
109
|
+
],
|
|
110
|
+
outputKind: 'object',
|
|
111
|
+
effects: ['None. Read-only.'],
|
|
112
|
+
},
|
|
113
|
+
run: async (input) => {
|
|
114
|
+
const query = input['query'];
|
|
115
|
+
const scopeStr = input['scope'];
|
|
116
|
+
const pluginFilter = input['plugin'];
|
|
117
|
+
const includeDisabled = input['includeDisabled'];
|
|
118
|
+
const searchBody = input['searchBody'];
|
|
119
|
+
const terms = query
|
|
120
|
+
.toLowerCase()
|
|
121
|
+
.split(/\s+/)
|
|
122
|
+
.filter((t) => t.length > 0);
|
|
123
|
+
if (terms.length === 0)
|
|
124
|
+
throw usage('query must contain at least one non-whitespace term');
|
|
125
|
+
const scopes = listScopes(scopeStr);
|
|
126
|
+
const candidates = scopes
|
|
127
|
+
.flatMap((s) => listAllSkills(s))
|
|
128
|
+
.filter((sk) => {
|
|
129
|
+
if (pluginFilter !== undefined && sk.plugin !== pluginFilter)
|
|
130
|
+
return false;
|
|
131
|
+
if (!includeDisabled && !sk.enabled)
|
|
132
|
+
return false;
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
const hits = [];
|
|
136
|
+
for (const sk of candidates) {
|
|
137
|
+
const matchedSet = new Set();
|
|
138
|
+
let score = 0;
|
|
139
|
+
const nameLc = sk.name.toLowerCase();
|
|
140
|
+
const descLc = sk.frontmatter.description !== undefined ? sk.frontmatter.description.toLowerCase() : null;
|
|
141
|
+
const kwsLc = sk.frontmatter.keywords !== undefined ? sk.frontmatter.keywords.map((k) => k.toLowerCase()) : null;
|
|
142
|
+
const bodyLc = searchBody ? readText(sk.path).toLowerCase() : null;
|
|
143
|
+
for (const term of terms) {
|
|
144
|
+
if (nameLc.includes(term)) {
|
|
145
|
+
score += 10;
|
|
146
|
+
matchedSet.add('name');
|
|
147
|
+
}
|
|
148
|
+
if (descLc !== null && descLc.includes(term)) {
|
|
149
|
+
score += 4;
|
|
150
|
+
matchedSet.add('description');
|
|
151
|
+
}
|
|
152
|
+
if (kwsLc !== null && kwsLc.some((k) => k.includes(term))) {
|
|
153
|
+
score += 6;
|
|
154
|
+
matchedSet.add('keywords');
|
|
155
|
+
}
|
|
156
|
+
if (bodyLc !== null && bodyLc.includes(term)) {
|
|
157
|
+
score += 1;
|
|
158
|
+
matchedSet.add('body');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (score > 0)
|
|
162
|
+
hits.push({ skill: sk, score, matched: Array.from(matchedSet) });
|
|
163
|
+
}
|
|
164
|
+
hits.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
165
|
+
return {
|
|
166
|
+
query,
|
|
167
|
+
hits: hits.map((h) => ({
|
|
168
|
+
name: h.skill.name,
|
|
169
|
+
plugin: h.skill.plugin,
|
|
170
|
+
scope: h.skill.scope,
|
|
171
|
+
score: h.score,
|
|
172
|
+
description: h.skill.frontmatter.description !== undefined ? h.skill.frontmatter.description : null,
|
|
173
|
+
})),
|
|
174
|
+
follow_up: 'Use `crtr skill read <name>` for the full SKILL.md body. Run `crtr skill find search -h` for filters.',
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
export const findGrep = defineLeaf({
|
|
179
|
+
name: 'grep',
|
|
180
|
+
help: {
|
|
181
|
+
name: 'skill find grep',
|
|
182
|
+
summary: 'search skill file contents for a regex pattern',
|
|
183
|
+
params: [
|
|
184
|
+
{ kind: 'positional', name: 'pattern', required: true, constraint: 'ECMAScript regex. Applied to each line of every SKILL.md file.' },
|
|
185
|
+
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project', 'all'], required: false, constraint: 'Default: all.' },
|
|
186
|
+
{ kind: 'flag', name: 'plugin', type: 'string', required: false, constraint: 'Filter to a single plugin name.' },
|
|
187
|
+
],
|
|
188
|
+
output: [
|
|
189
|
+
{ name: 'matches', type: 'object[]', required: true, constraint: 'Each: {path, line, text}. path is absolute. Sorted by path then line ascending.' },
|
|
190
|
+
],
|
|
191
|
+
outputKind: 'object',
|
|
192
|
+
effects: ['None. Read-only.'],
|
|
193
|
+
},
|
|
194
|
+
run: async (input) => {
|
|
195
|
+
const pattern = input['pattern'];
|
|
196
|
+
const scopeStr = input['scope'];
|
|
197
|
+
const pluginFilter = input['plugin'];
|
|
198
|
+
let regex;
|
|
199
|
+
try {
|
|
200
|
+
regex = new RegExp(pattern);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
throw usage(`invalid regex pattern: ${pattern}`);
|
|
204
|
+
}
|
|
205
|
+
const scopes = listScopes(scopeStr);
|
|
206
|
+
const skillsDirs = [];
|
|
207
|
+
for (const s of scopes) {
|
|
208
|
+
if (pluginFilter === undefined || pluginFilter === SCOPE_SKILL_PLUGIN) {
|
|
209
|
+
const root = scopeSkillsDir(s);
|
|
210
|
+
if (root)
|
|
211
|
+
skillsDirs.push(root);
|
|
212
|
+
}
|
|
213
|
+
for (const plugin of listInstalledPlugins(s)) {
|
|
214
|
+
if (!plugin.enabled)
|
|
215
|
+
continue;
|
|
216
|
+
if (pluginFilter !== undefined && plugin.name !== pluginFilter)
|
|
217
|
+
continue;
|
|
218
|
+
skillsDirs.push(join(plugin.root, SKILLS_DIR));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const matchLines = [];
|
|
222
|
+
for (const skillsDir of skillsDirs) {
|
|
223
|
+
const files = walkFiles(skillsDir);
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
const content = readText(file);
|
|
226
|
+
const lines = content.split('\n');
|
|
227
|
+
lines.forEach((lineText, idx) => {
|
|
228
|
+
if (regex.test(lineText)) {
|
|
229
|
+
matchLines.push({ path: file, line: idx + 1, text: lineText });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Sort by path then line ascending
|
|
235
|
+
matchLines.sort((a, b) => {
|
|
236
|
+
const pc = a.path.localeCompare(b.path);
|
|
237
|
+
return pc !== 0 ? pc : a.line - b.line;
|
|
238
|
+
});
|
|
239
|
+
return { matches: matchLines };
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
export const findBranch = defineBranch({
|
|
243
|
+
name: 'find',
|
|
244
|
+
help: {
|
|
245
|
+
name: 'skill find',
|
|
246
|
+
summary: 'discover skills by listing, keyword search, or body grep',
|
|
247
|
+
children: [
|
|
248
|
+
{ name: 'list', desc: 'paginated list of installed skills', useWhen: 'enumerating all available skills' },
|
|
249
|
+
{ name: 'search', desc: 'keyword search across name/description/keywords', useWhen: 'looking for skills matching a topic' },
|
|
250
|
+
{ name: 'grep', desc: 'regex search across SKILL.md bodies', useWhen: 'finding skills containing specific text or patterns' },
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
children: [findList, findSearch, findGrep],
|
|
254
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const readLeaf: import("../../core/command.js").LeafDef;
|