prompt-language-shell 1.0.0 → 1.0.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/README.md +40 -0
- package/dist/components/Component.js +10 -0
- package/dist/components/controllers/Config.js +11 -8
- package/dist/components/controllers/Learn.js +416 -0
- package/dist/components/views/Learn.js +147 -0
- package/dist/services/colors.js +5 -0
- package/dist/services/components.js +19 -1
- package/dist/services/router.js +12 -1
- package/dist/services/skills.js +102 -0
- package/dist/skills/introspect.md +16 -15
- package/dist/skills/schedule.md +35 -3
- package/dist/tools/introspect.tool.js +2 -2
- package/dist/tools/schedule.tool.js +1 -1
- package/dist/types/components.js +14 -0
- package/dist/types/schemas.js +1 -0
- package/dist/types/types.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,46 @@ Skills let you teach `pls` about your project-specific workflows. Create
|
|
|
125
125
|
markdown files in `~/.pls/skills/` to define custom operations that
|
|
126
126
|
`pls` can understand and execute.
|
|
127
127
|
|
|
128
|
+
### Creating Skills
|
|
129
|
+
|
|
130
|
+
The easiest way to create a new skill is with the guided walkthrough:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
$ pls learn
|
|
134
|
+
|
|
135
|
+
Creating a new skill...
|
|
136
|
+
|
|
137
|
+
Skill name (e.g., "Deploy Project"):
|
|
138
|
+
> Build Frontend
|
|
139
|
+
|
|
140
|
+
Description (min 20 characters):
|
|
141
|
+
> Build the frontend application using npm
|
|
142
|
+
|
|
143
|
+
Step 1 - What does this step do?
|
|
144
|
+
> Install dependencies
|
|
145
|
+
|
|
146
|
+
How should this step be executed?
|
|
147
|
+
> shell command
|
|
148
|
+
|
|
149
|
+
Enter the shell command:
|
|
150
|
+
> npm install
|
|
151
|
+
|
|
152
|
+
Add another step?
|
|
153
|
+
> yes
|
|
154
|
+
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The walkthrough guides you through defining:
|
|
159
|
+
- **Name**: A unique name for the skill
|
|
160
|
+
- **Description**: What the skill does (min 20 characters)
|
|
161
|
+
- **Aliases**: Example commands that invoke the skill (optional)
|
|
162
|
+
- **Config**: Configuration properties needed (optional)
|
|
163
|
+
- **Steps**: Each step with either a shell command or reference to another skill
|
|
164
|
+
|
|
165
|
+
Skills are saved to `~/.pls/skills/` as markdown files. File names use
|
|
166
|
+
kebab-case (e.g., "Build Frontend" becomes `build-frontend.md`).
|
|
167
|
+
|
|
128
168
|
For complete documentation, see [docs/SKILLS.md](./docs/SKILLS.md).
|
|
129
169
|
|
|
130
170
|
### Structure
|
|
@@ -10,6 +10,7 @@ import { Debug } from './views/Debug.js';
|
|
|
10
10
|
import { Execute, ExecuteView, mapStateToViewProps, } from './controllers/Execute.js';
|
|
11
11
|
import { Feedback } from './views/Feedback.js';
|
|
12
12
|
import { Introspect, IntrospectView } from './controllers/Introspect.js';
|
|
13
|
+
import { Learn, LearnView } from './controllers/Learn.js';
|
|
13
14
|
import { Message } from './views/Message.js';
|
|
14
15
|
import { Refinement, RefinementView } from './controllers/Refinement.js';
|
|
15
16
|
import { Report } from './views/Report.js';
|
|
@@ -86,6 +87,10 @@ export const ControllerComponent = memo(function ControllerComponent({ def, debu
|
|
|
86
87
|
const { props: { tasks, service, upcoming, label }, status, } = def;
|
|
87
88
|
return (_jsx(Execute, { tasks: tasks, service: service, upcoming: upcoming, label: label, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, workflowHandlers: workflowHandlers, status: status }));
|
|
88
89
|
}
|
|
90
|
+
case ComponentName.Learn: {
|
|
91
|
+
const { props: { suggestedName, onFinished, onAborted }, status, } = def;
|
|
92
|
+
return (_jsx(Learn, { suggestedName: suggestedName, onFinished: onFinished, onAborted: onAborted, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, workflowHandlers: workflowHandlers, status: status }));
|
|
93
|
+
}
|
|
89
94
|
default:
|
|
90
95
|
throw new Error(`Unknown managed component: ${def.name}`);
|
|
91
96
|
}
|
|
@@ -137,6 +142,10 @@ export const ViewComponent = memo(function ViewComponent({ def, }) {
|
|
|
137
142
|
const { props: { text }, status, } = def;
|
|
138
143
|
return _jsx(RefinementView, { text: text, status: status });
|
|
139
144
|
}
|
|
145
|
+
case ComponentName.Learn: {
|
|
146
|
+
const { state, status } = def;
|
|
147
|
+
return (_jsx(LearnView, { state: state, status: status, onInputChange: () => { }, onInputSubmit: () => { } }));
|
|
148
|
+
}
|
|
140
149
|
default:
|
|
141
150
|
throw new Error(`Unknown managed component: ${def.name}`);
|
|
142
151
|
}
|
|
@@ -163,6 +172,7 @@ export const TimelineComponent = ({ def, }) => {
|
|
|
163
172
|
case ComponentName.Execute:
|
|
164
173
|
case ComponentName.Answer:
|
|
165
174
|
case ComponentName.Introspect:
|
|
175
|
+
case ComponentName.Learn:
|
|
166
176
|
return _jsx(ViewComponent, { def: def });
|
|
167
177
|
default:
|
|
168
178
|
throw new Error('Unknown component type');
|
|
@@ -68,7 +68,12 @@ export function Config(props) {
|
|
|
68
68
|
return initial;
|
|
69
69
|
});
|
|
70
70
|
const [inputValue, setInputValue] = useState('');
|
|
71
|
-
const [selectedIndex, setSelectedIndex] = useState(
|
|
71
|
+
const [selectedIndex, setSelectedIndex] = useState(() => {
|
|
72
|
+
if (!initialSteps?.length)
|
|
73
|
+
return 0;
|
|
74
|
+
const first = initialSteps[0];
|
|
75
|
+
return first.type === StepType.Selection ? first.defaultIndex : 0;
|
|
76
|
+
});
|
|
72
77
|
// Resolve query to steps
|
|
73
78
|
useEffect(() => {
|
|
74
79
|
if (!isActive || !query || !service || initialSteps?.length)
|
|
@@ -114,12 +119,15 @@ export function Config(props) {
|
|
|
114
119
|
lifecycleHandlers,
|
|
115
120
|
workflowHandlers,
|
|
116
121
|
]);
|
|
117
|
-
// Update inputValue when step changes
|
|
122
|
+
// Update inputValue and selectedIndex when step changes
|
|
118
123
|
useEffect(() => {
|
|
119
124
|
if (isActive && step < steps.length) {
|
|
120
125
|
const stepConfig = steps[step];
|
|
121
126
|
const configKey = stepConfig.path || stepConfig.key;
|
|
122
127
|
setInputValue(values[configKey] || '');
|
|
128
|
+
if (stepConfig.type === StepType.Selection) {
|
|
129
|
+
setSelectedIndex(stepConfig.defaultIndex);
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
}, [step, isActive, steps, values]);
|
|
125
133
|
const normalizeValue = (value) => {
|
|
@@ -219,12 +227,7 @@ export function Config(props) {
|
|
|
219
227
|
setStep(steps.length);
|
|
220
228
|
}
|
|
221
229
|
else {
|
|
222
|
-
|
|
223
|
-
setStep(nextStep);
|
|
224
|
-
if (nextStep < steps.length &&
|
|
225
|
-
steps[nextStep].type === StepType.Selection) {
|
|
226
|
-
setSelectedIndex(steps[nextStep].defaultIndex);
|
|
227
|
-
}
|
|
230
|
+
setStep(step + 1);
|
|
228
231
|
}
|
|
229
232
|
};
|
|
230
233
|
if (resolving) {
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { ComponentStatus, LearnPhase, } from '../../types/components.js';
|
|
4
|
+
import { FeedbackType } from '../../types/types.js';
|
|
5
|
+
import { createFeedback } from '../../services/components.js';
|
|
6
|
+
import { useInput } from '../../services/keyboard.js';
|
|
7
|
+
import { generateSkillMarkdown, getAvailableSkillNames, isSkillNameAvailable, saveSkill, } from '../../services/skills.js';
|
|
8
|
+
import { displayNameToKey } from '../../services/parser.js';
|
|
9
|
+
import { LearnView } from '../views/Learn.js';
|
|
10
|
+
export { LearnView } from '../views/Learn.js';
|
|
11
|
+
/**
|
|
12
|
+
* Learn controller: Guided walkthrough for skill creation
|
|
13
|
+
*/
|
|
14
|
+
export function Learn(props) {
|
|
15
|
+
const { status, requestHandlers, lifecycleHandlers, onFinished, onAborted, suggestedName, } = props;
|
|
16
|
+
const isActive = status === ComponentStatus.Active;
|
|
17
|
+
const [state, setState] = useState(() => ({
|
|
18
|
+
name: null,
|
|
19
|
+
description: null,
|
|
20
|
+
aliases: [],
|
|
21
|
+
configEntries: [],
|
|
22
|
+
stepPairs: [],
|
|
23
|
+
currentPhase: LearnPhase.Name,
|
|
24
|
+
inputValue: suggestedName || '',
|
|
25
|
+
selectedIndex: 0,
|
|
26
|
+
error: null,
|
|
27
|
+
availableSkills: [],
|
|
28
|
+
pendingStepDescription: null,
|
|
29
|
+
pendingExecutionType: null,
|
|
30
|
+
}));
|
|
31
|
+
// Load available skills on mount
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (isActive) {
|
|
34
|
+
const skills = getAvailableSkillNames();
|
|
35
|
+
setState((prev) => ({ ...prev, availableSkills: skills }));
|
|
36
|
+
}
|
|
37
|
+
}, [isActive]);
|
|
38
|
+
const handleInputChange = (value) => {
|
|
39
|
+
setState((prev) => ({ ...prev, inputValue: value, error: null }));
|
|
40
|
+
};
|
|
41
|
+
const handleNameSubmit = (value) => {
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
setState((prev) => ({ ...prev, error: 'Skill name is required' }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const availability = isSkillNameAvailable(trimmed);
|
|
48
|
+
if (!availability.available) {
|
|
49
|
+
setState((prev) => ({
|
|
50
|
+
...prev,
|
|
51
|
+
error: availability.reason || 'Invalid name',
|
|
52
|
+
}));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setState((prev) => ({
|
|
56
|
+
...prev,
|
|
57
|
+
name: trimmed,
|
|
58
|
+
inputValue: '',
|
|
59
|
+
error: null,
|
|
60
|
+
currentPhase: LearnPhase.Description,
|
|
61
|
+
}));
|
|
62
|
+
};
|
|
63
|
+
const handleDescriptionSubmit = (value) => {
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
if (trimmed.length < 20) {
|
|
66
|
+
setState((prev) => ({
|
|
67
|
+
...prev,
|
|
68
|
+
error: 'Description must be at least 20 characters',
|
|
69
|
+
}));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setState((prev) => ({
|
|
73
|
+
...prev,
|
|
74
|
+
description: trimmed,
|
|
75
|
+
inputValue: '',
|
|
76
|
+
error: null,
|
|
77
|
+
currentPhase: LearnPhase.Aliases,
|
|
78
|
+
}));
|
|
79
|
+
};
|
|
80
|
+
const handleAliasSubmit = (value) => {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (!trimmed) {
|
|
83
|
+
// Skip to config phase
|
|
84
|
+
setState((prev) => ({
|
|
85
|
+
...prev,
|
|
86
|
+
inputValue: '',
|
|
87
|
+
currentPhase: LearnPhase.Config,
|
|
88
|
+
}));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Add alias and ask for more
|
|
92
|
+
setState((prev) => ({
|
|
93
|
+
...prev,
|
|
94
|
+
aliases: [...prev.aliases, trimmed],
|
|
95
|
+
inputValue: '',
|
|
96
|
+
selectedIndex: 0,
|
|
97
|
+
currentPhase: LearnPhase.AliasMore,
|
|
98
|
+
}));
|
|
99
|
+
};
|
|
100
|
+
const handleAliasMoreSelection = (addMore) => {
|
|
101
|
+
if (addMore) {
|
|
102
|
+
setState((prev) => ({
|
|
103
|
+
...prev,
|
|
104
|
+
inputValue: '',
|
|
105
|
+
currentPhase: LearnPhase.Aliases,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
setState((prev) => ({
|
|
110
|
+
...prev,
|
|
111
|
+
inputValue: '',
|
|
112
|
+
currentPhase: LearnPhase.Config,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const validateConfigEntry = (entry) => {
|
|
117
|
+
const configPattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*:\s*(string|number|boolean)$/;
|
|
118
|
+
return configPattern.test(entry.trim());
|
|
119
|
+
};
|
|
120
|
+
const handleConfigSubmit = (value) => {
|
|
121
|
+
const trimmed = value.trim();
|
|
122
|
+
// Empty is allowed (skip config)
|
|
123
|
+
if (!trimmed) {
|
|
124
|
+
setState((prev) => ({
|
|
125
|
+
...prev,
|
|
126
|
+
inputValue: '',
|
|
127
|
+
currentPhase: LearnPhase.StepDescription,
|
|
128
|
+
}));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Validate entry
|
|
132
|
+
if (!validateConfigEntry(trimmed)) {
|
|
133
|
+
setState((prev) => ({
|
|
134
|
+
...prev,
|
|
135
|
+
error: 'Format: property.path: string | number | boolean',
|
|
136
|
+
}));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Add config entry and ask for more
|
|
140
|
+
setState((prev) => ({
|
|
141
|
+
...prev,
|
|
142
|
+
configEntries: [...prev.configEntries, trimmed],
|
|
143
|
+
inputValue: '',
|
|
144
|
+
selectedIndex: 0,
|
|
145
|
+
error: null,
|
|
146
|
+
currentPhase: LearnPhase.ConfigMore,
|
|
147
|
+
}));
|
|
148
|
+
};
|
|
149
|
+
const handleConfigMoreSelection = (addMore) => {
|
|
150
|
+
if (addMore) {
|
|
151
|
+
setState((prev) => ({
|
|
152
|
+
...prev,
|
|
153
|
+
inputValue: '',
|
|
154
|
+
currentPhase: LearnPhase.Config,
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
setState((prev) => ({
|
|
159
|
+
...prev,
|
|
160
|
+
inputValue: '',
|
|
161
|
+
currentPhase: LearnPhase.StepDescription,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const handleStepDescriptionSubmit = (value) => {
|
|
166
|
+
const trimmed = value.trim();
|
|
167
|
+
// For first step, use skill name as default if empty
|
|
168
|
+
const description = trimmed || (state.stepPairs.length === 0 ? state.name : null);
|
|
169
|
+
if (!description) {
|
|
170
|
+
setState((prev) => ({
|
|
171
|
+
...prev,
|
|
172
|
+
error: 'Step description is required',
|
|
173
|
+
}));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
setState((prev) => ({
|
|
177
|
+
...prev,
|
|
178
|
+
pendingStepDescription: description,
|
|
179
|
+
inputValue: description, // Keep for display in next phase
|
|
180
|
+
selectedIndex: 0,
|
|
181
|
+
error: null,
|
|
182
|
+
currentPhase: LearnPhase.StepExecutionType,
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
const handleExecutionTypeSelection = (type) => {
|
|
186
|
+
setState((prev) => ({
|
|
187
|
+
...prev,
|
|
188
|
+
pendingExecutionType: type,
|
|
189
|
+
inputValue: '',
|
|
190
|
+
selectedIndex: 0,
|
|
191
|
+
currentPhase: LearnPhase.StepExecutionValue,
|
|
192
|
+
}));
|
|
193
|
+
};
|
|
194
|
+
const handleExecutionValueSubmit = (value) => {
|
|
195
|
+
if (!state.pendingStepDescription || !state.pendingExecutionType)
|
|
196
|
+
return;
|
|
197
|
+
const trimmed = value.trim();
|
|
198
|
+
if (!trimmed) {
|
|
199
|
+
setState((prev) => ({
|
|
200
|
+
...prev,
|
|
201
|
+
error: state.pendingExecutionType === 'command'
|
|
202
|
+
? 'Command is required'
|
|
203
|
+
: 'Skill selection is required',
|
|
204
|
+
}));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const newPair = {
|
|
208
|
+
description: state.pendingStepDescription,
|
|
209
|
+
executionType: state.pendingExecutionType,
|
|
210
|
+
execution: trimmed,
|
|
211
|
+
};
|
|
212
|
+
setState((prev) => ({
|
|
213
|
+
...prev,
|
|
214
|
+
stepPairs: [...prev.stepPairs, newPair],
|
|
215
|
+
pendingStepDescription: null,
|
|
216
|
+
pendingExecutionType: null,
|
|
217
|
+
inputValue: '',
|
|
218
|
+
selectedIndex: 0,
|
|
219
|
+
error: null,
|
|
220
|
+
currentPhase: LearnPhase.StepMore,
|
|
221
|
+
}));
|
|
222
|
+
};
|
|
223
|
+
const handleSkillReferenceSelection = (skillName) => {
|
|
224
|
+
if (!state.pendingStepDescription)
|
|
225
|
+
return;
|
|
226
|
+
const newPair = {
|
|
227
|
+
description: state.pendingStepDescription,
|
|
228
|
+
executionType: 'reference',
|
|
229
|
+
execution: skillName,
|
|
230
|
+
};
|
|
231
|
+
setState((prev) => ({
|
|
232
|
+
...prev,
|
|
233
|
+
stepPairs: [...prev.stepPairs, newPair],
|
|
234
|
+
pendingStepDescription: null,
|
|
235
|
+
pendingExecutionType: null,
|
|
236
|
+
inputValue: '',
|
|
237
|
+
selectedIndex: 0,
|
|
238
|
+
error: null,
|
|
239
|
+
currentPhase: LearnPhase.StepMore,
|
|
240
|
+
}));
|
|
241
|
+
};
|
|
242
|
+
const handleStepMoreSelection = (addMore) => {
|
|
243
|
+
if (addMore) {
|
|
244
|
+
setState((prev) => ({
|
|
245
|
+
...prev,
|
|
246
|
+
inputValue: '',
|
|
247
|
+
currentPhase: LearnPhase.StepDescription,
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
setState((prev) => ({
|
|
252
|
+
...prev,
|
|
253
|
+
inputValue: '',
|
|
254
|
+
selectedIndex: 0,
|
|
255
|
+
currentPhase: LearnPhase.Review,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const handleReviewSelection = (save) => {
|
|
260
|
+
if (save && state.name && state.description) {
|
|
261
|
+
try {
|
|
262
|
+
const markdown = generateSkillMarkdown(state.name, state.description, state.aliases, state.configEntries, state.stepPairs);
|
|
263
|
+
const key = displayNameToKey(state.name);
|
|
264
|
+
saveSkill(key, markdown);
|
|
265
|
+
requestHandlers.onCompleted(state);
|
|
266
|
+
onFinished?.(key);
|
|
267
|
+
lifecycleHandlers.completeActive(createFeedback({
|
|
268
|
+
type: FeedbackType.Info,
|
|
269
|
+
message: `Skill "${state.name}" saved to ~/.pls/skills/${key}.md`,
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
lifecycleHandlers.completeActive(createFeedback({
|
|
274
|
+
type: FeedbackType.Failed,
|
|
275
|
+
message: error instanceof Error ? error.message : 'Failed to save skill',
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
handleAbort();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const handleAbort = () => {
|
|
284
|
+
requestHandlers.onCompleted(state);
|
|
285
|
+
if (onAborted) {
|
|
286
|
+
onAborted('skill creation');
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
lifecycleHandlers.completeActive(createFeedback({
|
|
290
|
+
type: FeedbackType.Aborted,
|
|
291
|
+
message: 'Skill creation cancelled.',
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const handleInputSubmit = (value) => {
|
|
296
|
+
switch (state.currentPhase) {
|
|
297
|
+
case LearnPhase.Name:
|
|
298
|
+
handleNameSubmit(value);
|
|
299
|
+
break;
|
|
300
|
+
case LearnPhase.Description:
|
|
301
|
+
handleDescriptionSubmit(value);
|
|
302
|
+
break;
|
|
303
|
+
case LearnPhase.Aliases:
|
|
304
|
+
handleAliasSubmit(value);
|
|
305
|
+
break;
|
|
306
|
+
case LearnPhase.Config:
|
|
307
|
+
handleConfigSubmit(value);
|
|
308
|
+
break;
|
|
309
|
+
case LearnPhase.StepDescription:
|
|
310
|
+
handleStepDescriptionSubmit(value);
|
|
311
|
+
break;
|
|
312
|
+
case LearnPhase.StepExecutionValue:
|
|
313
|
+
if (state.pendingExecutionType === 'command') {
|
|
314
|
+
handleExecutionValueSubmit(value);
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
// Keyboard input handling
|
|
320
|
+
useInput((_, key) => {
|
|
321
|
+
if (!isActive)
|
|
322
|
+
return;
|
|
323
|
+
if (key.escape) {
|
|
324
|
+
handleAbort();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// Handle selection phases
|
|
328
|
+
switch (state.currentPhase) {
|
|
329
|
+
case LearnPhase.AliasMore:
|
|
330
|
+
if (key.tab) {
|
|
331
|
+
setState((prev) => ({
|
|
332
|
+
...prev,
|
|
333
|
+
selectedIndex: (prev.selectedIndex + 1) % 2,
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
else if (key.return) {
|
|
337
|
+
handleAliasMoreSelection(state.selectedIndex === 0);
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
case LearnPhase.ConfigMore:
|
|
341
|
+
if (key.tab) {
|
|
342
|
+
setState((prev) => ({
|
|
343
|
+
...prev,
|
|
344
|
+
selectedIndex: (prev.selectedIndex + 1) % 2,
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
else if (key.return) {
|
|
348
|
+
handleConfigMoreSelection(state.selectedIndex === 0);
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
case LearnPhase.StepMore:
|
|
352
|
+
if (key.tab) {
|
|
353
|
+
setState((prev) => ({
|
|
354
|
+
...prev,
|
|
355
|
+
selectedIndex: (prev.selectedIndex + 1) % 2,
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
else if (key.return) {
|
|
359
|
+
handleStepMoreSelection(state.selectedIndex === 0);
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
case LearnPhase.Review:
|
|
363
|
+
if (key.tab) {
|
|
364
|
+
setState((prev) => ({
|
|
365
|
+
...prev,
|
|
366
|
+
selectedIndex: (prev.selectedIndex + 1) % 2,
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
else if (key.return) {
|
|
370
|
+
handleReviewSelection(state.selectedIndex === 0);
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
case LearnPhase.StepExecutionType:
|
|
374
|
+
if (key.tab) {
|
|
375
|
+
setState((prev) => ({
|
|
376
|
+
...prev,
|
|
377
|
+
selectedIndex: (prev.selectedIndex + 1) % 2,
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
else if (key.return) {
|
|
381
|
+
const type = state.selectedIndex === 0 ? 'command' : 'reference';
|
|
382
|
+
handleExecutionTypeSelection(type);
|
|
383
|
+
}
|
|
384
|
+
break;
|
|
385
|
+
case LearnPhase.StepExecutionValue:
|
|
386
|
+
if (state.pendingExecutionType === 'reference') {
|
|
387
|
+
const skillCount = state.availableSkills.length;
|
|
388
|
+
if (skillCount === 0) {
|
|
389
|
+
// No skills available, go back to type selection
|
|
390
|
+
if (key.return) {
|
|
391
|
+
setState((prev) => ({
|
|
392
|
+
...prev,
|
|
393
|
+
error: 'No skills available to reference',
|
|
394
|
+
selectedIndex: 0,
|
|
395
|
+
currentPhase: LearnPhase.StepExecutionType,
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
if (key.tab) {
|
|
401
|
+
setState((prev) => ({
|
|
402
|
+
...prev,
|
|
403
|
+
selectedIndex: (prev.selectedIndex + 1) % skillCount,
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
else if (key.return) {
|
|
407
|
+
const selectedSkill = state.availableSkills[state.selectedIndex];
|
|
408
|
+
handleSkillReferenceSelection(selectedSkill);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}, { isActive });
|
|
415
|
+
return (_jsx(LearnView, { state: state, status: status, onInputChange: handleInputChange, onInputSubmit: handleInputSubmit }));
|
|
416
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useFocus } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { ComponentStatus, LearnPhase, } from '../../types/components.js';
|
|
6
|
+
import { Colors, Palette } from '../../services/colors.js';
|
|
7
|
+
import { useInput } from '../../services/keyboard.js';
|
|
8
|
+
import { configEntriesToYaml } from '../../services/skills.js';
|
|
9
|
+
const YES_NO_OPTIONS = [
|
|
10
|
+
{ label: 'yes', value: true },
|
|
11
|
+
{ label: 'no', value: false },
|
|
12
|
+
];
|
|
13
|
+
function ValidationMessage({ message }) {
|
|
14
|
+
return (_jsx(Box, { marginTop: 1, minWidth: 40, children: _jsxs(Text, { color: Colors.Status.Warning, children: [message, "."] }) }));
|
|
15
|
+
}
|
|
16
|
+
// Phase ordering for visibility logic
|
|
17
|
+
const PHASE_ORDER = [
|
|
18
|
+
LearnPhase.Name,
|
|
19
|
+
LearnPhase.Description,
|
|
20
|
+
LearnPhase.Aliases,
|
|
21
|
+
LearnPhase.AliasMore,
|
|
22
|
+
LearnPhase.Config,
|
|
23
|
+
LearnPhase.ConfigMore,
|
|
24
|
+
LearnPhase.StepDescription,
|
|
25
|
+
LearnPhase.StepExecutionType,
|
|
26
|
+
LearnPhase.StepExecutionValue,
|
|
27
|
+
LearnPhase.StepMore,
|
|
28
|
+
LearnPhase.Review,
|
|
29
|
+
];
|
|
30
|
+
function createPhaseHelpers(current) {
|
|
31
|
+
const currentIndex = PHASE_ORDER.indexOf(current);
|
|
32
|
+
return {
|
|
33
|
+
isPast: (target) => currentIndex > PHASE_ORDER.indexOf(target),
|
|
34
|
+
isInRange: (start, end) => currentIndex >= PHASE_ORDER.indexOf(start) &&
|
|
35
|
+
currentIndex < PHASE_ORDER.indexOf(end),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const EXECUTION_TYPE_OPTIONS = [
|
|
39
|
+
{ label: 'shell command', value: 'command' },
|
|
40
|
+
{ label: 'reference existing skill', value: 'reference' },
|
|
41
|
+
];
|
|
42
|
+
function TextInputStep({ value, placeholder, onChange, onSubmit, }) {
|
|
43
|
+
const [inputValue, setInputValue] = useState(value);
|
|
44
|
+
const { isFocused } = useFocus({ autoFocus: true });
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setInputValue(value);
|
|
47
|
+
}, [value]);
|
|
48
|
+
const handleChange = (newValue) => {
|
|
49
|
+
setInputValue(newValue);
|
|
50
|
+
onChange(newValue);
|
|
51
|
+
};
|
|
52
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: Colors.Action.Select, children: ">" }), _jsx(Text, { children: " " }), isFocused ? (_jsx(TextInput, { value: inputValue, onChange: handleChange, onSubmit: onSubmit, placeholder: placeholder })) : (_jsx(Text, { dimColor: true, children: inputValue || placeholder }))] }));
|
|
53
|
+
}
|
|
54
|
+
function SelectionStep({ options, selectedIndex, isActive, }) {
|
|
55
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: Colors.Action.Select, children: ">" }), _jsx(Text, { children: " " }), options.map((option, index) => {
|
|
56
|
+
const isSelected = index === selectedIndex;
|
|
57
|
+
return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { color: isSelected && isActive ? Palette.BrightGreen : undefined, dimColor: !isSelected, bold: isSelected, children: option.label }) }, option.label));
|
|
58
|
+
})] }));
|
|
59
|
+
}
|
|
60
|
+
function SkillSelectionStep({ skills, selectedIndex, }) {
|
|
61
|
+
if (skills.length === 0) {
|
|
62
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: Colors.Action.Select, children: ">" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "no skills available" })] }));
|
|
63
|
+
}
|
|
64
|
+
return (_jsx(Box, { flexDirection: "column", children: skills.map((skill, index) => {
|
|
65
|
+
const isSelected = index === selectedIndex;
|
|
66
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: Colors.Action.Select, children: isSelected ? '>' : ' ' }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? undefined : Palette.LightGray, bold: isSelected, children: skill })] }, skill));
|
|
67
|
+
}) }));
|
|
68
|
+
}
|
|
69
|
+
function CompletedValue({ value }) {
|
|
70
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: Colors.Action.Select, dimColor: true, children: ">" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: value })] }));
|
|
71
|
+
}
|
|
72
|
+
function CompletedStep({ index, description, executionType, execution, }) {
|
|
73
|
+
const executionValue = executionType === 'reference' ? `[ ${execution} ]` : execution;
|
|
74
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Step ", index, ": ", description] }), _jsx(CompletedValue, { value: executionValue })] }));
|
|
75
|
+
}
|
|
76
|
+
function WizardHeader({ name, description, aliases = [], configEntries = [], stepPairs = [], showDescription = false, showAliases = false, showConfig = false, showSteps = false, }) {
|
|
77
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: Colors.Action.Execute, children: "Creating a new skill..." }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Name:" }), _jsx(CompletedValue, { value: name || '' })] }), showDescription && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Description:" }), _jsx(CompletedValue, { value: description || '' })] })), showAliases && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Aliases:" }), aliases.length > 0 ? (aliases.map((alias, i) => _jsx(CompletedValue, { value: alias }, i))) : (_jsx(Text, { dimColor: true, children: " (none)" }))] })), showConfig && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Config:" }), configEntries.length > 0 ? (configEntries.map((entry, i) => (_jsx(CompletedValue, { value: entry }, i)))) : (_jsx(Text, { dimColor: true, children: " (none)" }))] })), showSteps &&
|
|
78
|
+
stepPairs.map((pair, i) => (_jsx(CompletedStep, { index: i + 1, description: pair.description, executionType: pair.executionType, execution: pair.execution }, i)))] }));
|
|
79
|
+
}
|
|
80
|
+
function Preview({ name, description, aliases, configEntries, stepPairs, }) {
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: Palette.Gray, paddingY: 1, paddingX: 2, gap: 1, minWidth: 60, children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: Palette.Cyan, children: "### Name" }), _jsx(Text, { children: name })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: Palette.Cyan, children: "### Description" }), _jsx(Text, { children: description })] }), aliases.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: Palette.Cyan, children: "### Aliases" }), aliases.map((alias, i) => (_jsxs(Text, { children: ["- ", alias] }, i)))] })), configEntries.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: Palette.Cyan, children: "### Config" }), _jsx(Text, { children: configEntriesToYaml(configEntries).trimEnd() })] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: Palette.Cyan, children: "### Steps" }), stepPairs.map((pair, i) => (_jsxs(Text, { children: ["- ", pair.description] }, i)))] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: Palette.Cyan, children: "### Execution" }), stepPairs.map((pair, i) => (_jsxs(Text, { children: ["-", ' ', pair.executionType === 'reference'
|
|
82
|
+
? `[ ${pair.execution} ]`
|
|
83
|
+
: pair.execution] }, i)))] })] }));
|
|
84
|
+
}
|
|
85
|
+
export const LearnView = ({ state, status, onInputChange, onInputSubmit, }) => {
|
|
86
|
+
const isActive = status === ComponentStatus.Active;
|
|
87
|
+
const { name, description, aliases, configEntries, stepPairs, currentPhase, inputValue, selectedIndex, error, availableSkills, pendingStepDescription, } = state;
|
|
88
|
+
// Prevent keyboard input when not active
|
|
89
|
+
useInput(() => { }, { isActive: false });
|
|
90
|
+
const headerProps = {
|
|
91
|
+
name,
|
|
92
|
+
description,
|
|
93
|
+
aliases,
|
|
94
|
+
configEntries,
|
|
95
|
+
stepPairs,
|
|
96
|
+
};
|
|
97
|
+
// Show section if we've moved past where it was entered
|
|
98
|
+
const { isPast, isInRange } = createPhaseHelpers(currentPhase);
|
|
99
|
+
const showDescription = isPast(LearnPhase.Description);
|
|
100
|
+
const showAliases = isPast(LearnPhase.AliasMore);
|
|
101
|
+
const showConfig = isPast(LearnPhase.ConfigMore);
|
|
102
|
+
const showSteps = isInRange(LearnPhase.StepDescription, LearnPhase.Review);
|
|
103
|
+
const isReference = state.pendingExecutionType === 'reference';
|
|
104
|
+
const renderPhaseContent = () => {
|
|
105
|
+
switch (currentPhase) {
|
|
106
|
+
case LearnPhase.Name:
|
|
107
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Name:" }), _jsx(TextInputStep, { value: inputValue, onChange: onInputChange, onSubmit: onInputSubmit }, "name"), error && _jsx(ValidationMessage, { message: error })] }));
|
|
108
|
+
case LearnPhase.Description:
|
|
109
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Description (min 20 characters):" }), _jsx(TextInputStep, { value: inputValue, onChange: onInputChange, onSubmit: onInputSubmit }, "description"), error && _jsx(ValidationMessage, { message: error })] }));
|
|
110
|
+
case LearnPhase.Aliases:
|
|
111
|
+
return (_jsxs(_Fragment, { children: [aliases.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Aliases:" }), aliases.map((alias, i) => (_jsx(CompletedValue, { value: alias }, i)))] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: aliases.length === 0
|
|
112
|
+
? 'Alias (Enter to skip):'
|
|
113
|
+
: 'Another alias (Enter to skip):' }), _jsx(TextInputStep, { value: inputValue, placeholder: "e.g. deploy to prod", onChange: onInputChange, onSubmit: onInputSubmit }, `alias-${aliases.length}`)] })] }));
|
|
114
|
+
case LearnPhase.AliasMore:
|
|
115
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Aliases:" }), aliases.map((alias, i) => (_jsx(CompletedValue, { value: alias }, i)))] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Add another alias?" }), _jsx(SelectionStep, { options: YES_NO_OPTIONS, selectedIndex: selectedIndex, isActive: isActive }, "alias-more")] })] }));
|
|
116
|
+
case LearnPhase.Config:
|
|
117
|
+
return (_jsxs(_Fragment, { children: [configEntries.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Config:" }), configEntries.map((entry, i) => (_jsx(CompletedValue, { value: entry }, i)))] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: configEntries.length === 0
|
|
118
|
+
? 'Config (Enter to skip):'
|
|
119
|
+
: 'Another config (Enter to skip):' }), _jsx(TextInputStep, { value: inputValue, placeholder: "e.g. server.production.url: string", onChange: onInputChange, onSubmit: onInputSubmit }, `config-${configEntries.length}`), error && _jsx(ValidationMessage, { message: error })] })] }));
|
|
120
|
+
case LearnPhase.ConfigMore:
|
|
121
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Config:" }), configEntries.map((entry, i) => (_jsx(CompletedValue, { value: entry }, i)))] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Add another configuration entry?" }), _jsx(SelectionStep, { options: YES_NO_OPTIONS, selectedIndex: selectedIndex, isActive: isActive }, "config-more")] })] }));
|
|
122
|
+
case LearnPhase.StepDescription:
|
|
123
|
+
return (_jsxs(Box, { flexDirection: "column", children: [stepPairs.length === 0 && (_jsx(Text, { children: "Step 1 - What does this step do?" })), _jsx(TextInputStep, { value: inputValue, placeholder: stepPairs.length === 0
|
|
124
|
+
? name || undefined
|
|
125
|
+
: `Describe step ${stepPairs.length + 1}`, onChange: onInputChange, onSubmit: onInputSubmit }, `step-desc-${stepPairs.length}`), error && _jsx(ValidationMessage, { message: error })] }));
|
|
126
|
+
case LearnPhase.StepExecutionType:
|
|
127
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { dimColor: true, children: ["Step ", stepPairs.length + 1, ": ", inputValue] }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { children: "How should this step be executed?" }), _jsx(SelectionStep, { options: EXECUTION_TYPE_OPTIONS, selectedIndex: selectedIndex, isActive: isActive }, `step-type-${stepPairs.length}`)] })] }));
|
|
128
|
+
case LearnPhase.StepExecutionValue:
|
|
129
|
+
return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Text, { dimColor: true, children: ["Step ", stepPairs.length + 1, ": ", pendingStepDescription] }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [isReference ? (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "Select skill to reference:" }) }), _jsx(SkillSelectionStep, { skills: availableSkills, selectedIndex: selectedIndex, isActive: isActive }, `step-ref-${stepPairs.length}`)] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { children: "Enter the shell command:" }), _jsx(TextInputStep, { value: inputValue, placeholder: "e.g. npm install", onChange: onInputChange, onSubmit: onInputSubmit }, `step-exec-${stepPairs.length}`)] })), error && _jsx(ValidationMessage, { message: error })] })] }));
|
|
130
|
+
case LearnPhase.StepMore:
|
|
131
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Add another step?" }), _jsx(SelectionStep, { options: YES_NO_OPTIONS, selectedIndex: selectedIndex, isActive: isActive }, `step-more-${stepPairs.length}`)] }));
|
|
132
|
+
case LearnPhase.Review:
|
|
133
|
+
return null; // Review has its own layout
|
|
134
|
+
default:
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
// Name phase has its own layout (no header yet)
|
|
139
|
+
if (currentPhase === LearnPhase.Name) {
|
|
140
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, gap: 1, children: [_jsx(Text, { color: Colors.Action.Execute, children: "Creating a new skill..." }), renderPhaseContent()] }));
|
|
141
|
+
}
|
|
142
|
+
// Review phase has a different layout
|
|
143
|
+
if (currentPhase === LearnPhase.Review) {
|
|
144
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, gap: 1, children: [_jsx(Text, { children: "Review your new skill:" }), _jsx(Preview, { name: name || '', description: description || '', aliases: aliases, configEntries: configEntries, stepPairs: stepPairs }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Save this skill?" }), _jsx(SelectionStep, { options: YES_NO_OPTIONS, selectedIndex: selectedIndex, isActive: isActive }, "review-save")] })] }));
|
|
145
|
+
}
|
|
146
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, gap: 1, children: [_jsx(WizardHeader, { ...headerProps, showDescription: showDescription, showAliases: showAliases, showConfig: showConfig, showSteps: showSteps }), renderPhaseContent()] }));
|
|
147
|
+
};
|
package/dist/services/colors.js
CHANGED
|
@@ -127,6 +127,10 @@ const taskColors = {
|
|
|
127
127
|
description: Colors.Label.Default,
|
|
128
128
|
type: Colors.Type.Group,
|
|
129
129
|
},
|
|
130
|
+
[TaskType.Learn]: {
|
|
131
|
+
description: Colors.Label.Default,
|
|
132
|
+
type: Palette.LightGreen,
|
|
133
|
+
},
|
|
130
134
|
};
|
|
131
135
|
/**
|
|
132
136
|
* Feedback-specific color mappings (internal)
|
|
@@ -221,6 +225,7 @@ const verboseTaskTypeLabels = {
|
|
|
221
225
|
[TaskType.Execute]: 'execute command',
|
|
222
226
|
[TaskType.Answer]: 'answer question',
|
|
223
227
|
[TaskType.Introspect]: 'introspect capabilities',
|
|
228
|
+
[TaskType.Learn]: 'learn skill',
|
|
224
229
|
[TaskType.Report]: 'report results',
|
|
225
230
|
[TaskType.Define]: 'define options',
|
|
226
231
|
[TaskType.Ignore]: 'ignore request',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { ComponentStatus, } from '../types/components.js';
|
|
2
|
+
import { ComponentStatus, LearnPhase, } from '../types/components.js';
|
|
3
3
|
import { ComponentName } from '../types/types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Shared component creation utility
|
|
@@ -64,6 +64,20 @@ const InitialValidateState = {
|
|
|
64
64
|
configRequirements: [],
|
|
65
65
|
validated: false,
|
|
66
66
|
};
|
|
67
|
+
const InitialLearnState = {
|
|
68
|
+
name: null,
|
|
69
|
+
description: null,
|
|
70
|
+
aliases: [],
|
|
71
|
+
configEntries: [],
|
|
72
|
+
stepPairs: [],
|
|
73
|
+
currentPhase: LearnPhase.Name,
|
|
74
|
+
inputValue: '',
|
|
75
|
+
selectedIndex: 0,
|
|
76
|
+
error: null,
|
|
77
|
+
availableSkills: [],
|
|
78
|
+
pendingStepDescription: null,
|
|
79
|
+
pendingExecutionType: null,
|
|
80
|
+
};
|
|
67
81
|
/**
|
|
68
82
|
* Create a welcome component that displays application information
|
|
69
83
|
*/
|
|
@@ -120,3 +134,7 @@ export const createExecute = (props, status) => createManagedComponent(Component
|
|
|
120
134
|
* Create a validate component that checks and collects missing configuration
|
|
121
135
|
*/
|
|
122
136
|
export const createValidate = (props, status) => createManagedComponent(ComponentName.Validate, props, InitialValidateState, status);
|
|
137
|
+
/**
|
|
138
|
+
* Create a learn component that guides users through skill creation
|
|
139
|
+
*/
|
|
140
|
+
export const createLearn = (props, status) => createManagedComponent(ComponentName.Learn, props, InitialLearnState, status);
|
package/dist/services/router.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getConfigSchema } from '../configuration/schema.js';
|
|
|
5
5
|
import { createConfigStepsFromSchema } from '../configuration/steps.js';
|
|
6
6
|
import { unflattenConfig } from '../configuration/transformation.js';
|
|
7
7
|
import { saveConfigLabels } from '../configuration/labels.js';
|
|
8
|
-
import { createAnswer, createConfig, createConfirm, createExecute, createFeedback, createIntrospect, createSchedule, createValidate, } from './components.js';
|
|
8
|
+
import { createAnswer, createConfig, createConfirm, createExecute, createFeedback, createIntrospect, createLearn, createSchedule, createValidate, } from './components.js';
|
|
9
9
|
import { getCancellationMessage, getConfirmationMessage, getUnknownRequestMessage, } from './messages.js';
|
|
10
10
|
import { validateExecuteTasks } from './validator.js';
|
|
11
11
|
/**
|
|
@@ -297,6 +297,8 @@ export function getRoutingCategory(task) {
|
|
|
297
297
|
return 'execute';
|
|
298
298
|
case TaskType.Answer:
|
|
299
299
|
return 'answer';
|
|
300
|
+
case TaskType.Learn:
|
|
301
|
+
return 'learn';
|
|
300
302
|
default:
|
|
301
303
|
return task.type;
|
|
302
304
|
}
|
|
@@ -512,6 +514,14 @@ function routeConfigTasks(tasks, context, _upcoming) {
|
|
|
512
514
|
function routeExecuteTasks(tasks, context, upcoming, label) {
|
|
513
515
|
context.workflowHandlers.addToQueue(createExecute({ tasks, service: context.service, upcoming, label }));
|
|
514
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Route Learn tasks - creates Learn component for skill creation walkthrough
|
|
519
|
+
*/
|
|
520
|
+
function routeLearnTasks(tasks, context, _upcoming) {
|
|
521
|
+
// Extract suggested name from first task's params if provided
|
|
522
|
+
const suggestedName = tasks[0]?.params?.suggestedName;
|
|
523
|
+
context.workflowHandlers.addToQueue(createLearn({ suggestedName }));
|
|
524
|
+
}
|
|
515
525
|
/**
|
|
516
526
|
* Registry mapping task types to their route handlers
|
|
517
527
|
*/
|
|
@@ -520,6 +530,7 @@ const taskRouteHandlers = {
|
|
|
520
530
|
[TaskType.Introspect]: routeIntrospectTasks,
|
|
521
531
|
[TaskType.Config]: routeConfigTasks,
|
|
522
532
|
[TaskType.Execute]: routeExecuteTasks,
|
|
533
|
+
[TaskType.Learn]: routeLearnTasks,
|
|
523
534
|
};
|
|
524
535
|
/**
|
|
525
536
|
* Route tasks by type to appropriate components
|
package/dist/services/skills.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import YAML from 'yaml';
|
|
3
4
|
import { AppError, ErrorCode } from '../types/errors.js';
|
|
4
5
|
import { defaultFileSystem } from './filesystem.js';
|
|
5
6
|
import { displayWarning } from './logger.js';
|
|
@@ -274,3 +275,104 @@ export function validateNoCycles(execution, skillLookup, visited = new Set()) {
|
|
|
274
275
|
throw error;
|
|
275
276
|
}
|
|
276
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if a skill name is available (not conflicting with existing files
|
|
280
|
+
* or built-in skills)
|
|
281
|
+
*/
|
|
282
|
+
export function isSkillNameAvailable(name, fs = defaultFileSystem) {
|
|
283
|
+
const key = displayNameToKey(name);
|
|
284
|
+
if (!key) {
|
|
285
|
+
return { available: false, reason: 'Skill name is required' };
|
|
286
|
+
}
|
|
287
|
+
if (conflictsWithBuiltIn(key)) {
|
|
288
|
+
return { available: false, reason: 'Name conflicts with a built-in skill' };
|
|
289
|
+
}
|
|
290
|
+
const skillsDir = getSkillsDirectory();
|
|
291
|
+
const filePath = join(skillsDir, `${key}.md`);
|
|
292
|
+
if (fs.exists(filePath)) {
|
|
293
|
+
return {
|
|
294
|
+
available: false,
|
|
295
|
+
reason: 'A skill with this name already exists',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return { available: true };
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Convert dot-notation config entries to nested YAML format.
|
|
302
|
+
* Input: ["pdf.base.dir: string", "pdf.base.format: string"]
|
|
303
|
+
* Output: "pdf:\n base:\n dir: string\n format: string\n"
|
|
304
|
+
*/
|
|
305
|
+
export function configEntriesToYaml(entries) {
|
|
306
|
+
const root = {};
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
const colonIndex = entry.lastIndexOf(':');
|
|
309
|
+
if (colonIndex === -1)
|
|
310
|
+
continue;
|
|
311
|
+
const path = entry.slice(0, colonIndex).trim();
|
|
312
|
+
const type = entry.slice(colonIndex + 1).trim();
|
|
313
|
+
const parts = path.split('.');
|
|
314
|
+
let current = root;
|
|
315
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
316
|
+
const part = parts[i];
|
|
317
|
+
if (!(part in current) || typeof current[part] !== 'object') {
|
|
318
|
+
current[part] = {};
|
|
319
|
+
}
|
|
320
|
+
current = current[part];
|
|
321
|
+
}
|
|
322
|
+
current[parts[parts.length - 1]] = type;
|
|
323
|
+
}
|
|
324
|
+
return YAML.stringify(root, { indent: 2 });
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Generate skill markdown from collected data
|
|
328
|
+
*/
|
|
329
|
+
export function generateSkillMarkdown(name, description, aliases, configEntries, stepPairs) {
|
|
330
|
+
let markdown = `### Name\n${name}\n\n`;
|
|
331
|
+
markdown += `### Description\n${description}\n\n`;
|
|
332
|
+
if (aliases.length > 0) {
|
|
333
|
+
markdown += `### Aliases\n`;
|
|
334
|
+
for (const alias of aliases) {
|
|
335
|
+
markdown += `- ${alias}\n`;
|
|
336
|
+
}
|
|
337
|
+
markdown += '\n';
|
|
338
|
+
}
|
|
339
|
+
if (configEntries.length > 0) {
|
|
340
|
+
markdown += `### Config\n`;
|
|
341
|
+
markdown += configEntriesToYaml(configEntries);
|
|
342
|
+
markdown += '\n';
|
|
343
|
+
}
|
|
344
|
+
markdown += `### Steps\n`;
|
|
345
|
+
for (const pair of stepPairs) {
|
|
346
|
+
markdown += `- ${pair.description}\n`;
|
|
347
|
+
}
|
|
348
|
+
markdown += '\n';
|
|
349
|
+
markdown += `### Execution\n`;
|
|
350
|
+
for (const pair of stepPairs) {
|
|
351
|
+
if (pair.executionType === 'reference') {
|
|
352
|
+
markdown += `- [ ${pair.execution} ]\n`;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
markdown += `- ${pair.execution}\n`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return markdown;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Save skill to file (creates directory if needed)
|
|
362
|
+
*/
|
|
363
|
+
export function saveSkill(key, content, fs = defaultFileSystem) {
|
|
364
|
+
const skillsDir = getSkillsDirectory();
|
|
365
|
+
// Create directory if it doesn't exist
|
|
366
|
+
if (!fs.exists(skillsDir)) {
|
|
367
|
+
fs.createDirectory(skillsDir, { recursive: true });
|
|
368
|
+
}
|
|
369
|
+
const filePath = join(skillsDir, `${key}.md`);
|
|
370
|
+
fs.writeFile(filePath, content);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get list of available skill names for reference selection
|
|
374
|
+
*/
|
|
375
|
+
export function getAvailableSkillNames(fs = defaultFileSystem) {
|
|
376
|
+
const definitions = loadSkillDefinitions(fs);
|
|
377
|
+
return definitions.filter((def) => def.isValid).map((def) => def.name);
|
|
378
|
+
}
|
|
@@ -71,28 +71,28 @@ NON-NEGOTIABLE and applies to EVERY response.
|
|
|
71
71
|
|
|
72
72
|
**DO NOT:**
|
|
73
73
|
- Reorder capabilities based on alphabetical sorting
|
|
74
|
-
- Put Schedule
|
|
74
|
+
- Put Schedule first (this is WRONG)
|
|
75
75
|
- Rearrange based on perceived importance
|
|
76
76
|
- Deviate from this order for any reason
|
|
77
77
|
|
|
78
78
|
**CORRECT ORDER - FOLLOW EXACTLY:**
|
|
79
79
|
|
|
80
|
-
### Position 1-
|
|
80
|
+
### Position 1-5: system capabilities (origin: "system")
|
|
81
81
|
|
|
82
82
|
These MUST appear FIRST, in this EXACT sequence:
|
|
83
83
|
|
|
84
84
|
1. **Introspect**
|
|
85
85
|
2. **Configure**
|
|
86
86
|
3. **Answer**
|
|
87
|
-
4. **
|
|
87
|
+
4. **Learn**
|
|
88
|
+
5. **Execute**
|
|
88
89
|
|
|
89
|
-
### Position
|
|
90
|
+
### Position 6-7: meta workflow capabilities (origin: "meta")
|
|
90
91
|
|
|
91
92
|
These MUST appear AFTER Execute and BEFORE user-provided skills:
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
7. **Report**
|
|
94
|
+
6. **Schedule**
|
|
95
|
+
7. **Validate**
|
|
96
96
|
|
|
97
97
|
### Position 8+: user-provided skills (origin: "user")
|
|
98
98
|
|
|
@@ -122,6 +122,7 @@ Create capability objects for each capability. Each object should have:
|
|
|
122
122
|
- Start with lowercase letter, no ending punctuation
|
|
123
123
|
- Focus on clarity and brevity
|
|
124
124
|
- Describe the core purpose in one short phrase
|
|
125
|
+
- For Learn: "learn new skills or capabilities"
|
|
125
126
|
- Examples:
|
|
126
127
|
- "break down requests into actionable steps"
|
|
127
128
|
- "run shell commands and process operations"
|
|
@@ -129,8 +130,8 @@ Create capability objects for each capability. Each object should have:
|
|
|
129
130
|
|
|
130
131
|
- **origin**: The origin type of the capability
|
|
131
132
|
- Use "system" for system capabilities: Introspect, Configure, Answer,
|
|
132
|
-
Execute
|
|
133
|
-
- Use "meta" for meta workflow capabilities: Schedule, Validate
|
|
133
|
+
Learn, Execute
|
|
134
|
+
- Use "meta" for meta workflow capabilities: Schedule, Validate
|
|
134
135
|
- Use "user" for all user-provided skills
|
|
135
136
|
|
|
136
137
|
- **isIncomplete**: Optional boolean flag
|
|
@@ -158,9 +159,9 @@ Examples:
|
|
|
158
159
|
|
|
159
160
|
When user asks "list your skills", create an introductory message like
|
|
160
161
|
"here are my capabilities:" followed by capability objects for system
|
|
161
|
-
capabilities (Introspect, Configure, Answer, Execute with origin
|
|
162
|
-
"system"), then meta workflow capabilities (Schedule, Validate
|
|
163
|
-
|
|
162
|
+
capabilities (Introspect, Configure, Answer, Learn, Execute with origin
|
|
163
|
+
"system"), then meta workflow capabilities (Schedule, Validate with
|
|
164
|
+
origin "meta").
|
|
164
165
|
|
|
165
166
|
### Example 2: Filtered Skills
|
|
166
167
|
|
|
@@ -174,9 +175,9 @@ app skill with origin "user".
|
|
|
174
175
|
When user asks "what can you do" and user-provided skills like "process
|
|
175
176
|
data" and "backup files" exist, create an introductory message like "i can
|
|
176
177
|
help with these operations:" followed by all system capabilities
|
|
177
|
-
(Introspect, Configure, Answer, Execute with origin "system"),
|
|
178
|
-
capabilities (Schedule, Validate
|
|
179
|
-
user-provided skills with origin "user".
|
|
178
|
+
(Introspect, Configure, Answer, Learn, Execute with origin "system"),
|
|
179
|
+
meta capabilities (Schedule, Validate with origin "meta"), plus
|
|
180
|
+
the user-provided skills with origin "user".
|
|
180
181
|
|
|
181
182
|
## Final Validation
|
|
182
183
|
|
package/dist/skills/schedule.md
CHANGED
|
@@ -76,6 +76,7 @@ Every task MUST have a type field. Use the appropriate type:
|
|
|
76
76
|
- `execute` - Shell commands, running programs (ONLY if skill exists)
|
|
77
77
|
- `answer` - Answering questions, explaining concepts
|
|
78
78
|
- `introspect` - Listing capabilities when user asks what you can do
|
|
79
|
+
- `learn` - Creating a new skill (guided walkthrough for skill creation)
|
|
79
80
|
- `report` - Generating summaries, displaying results
|
|
80
81
|
- `define` - Presenting options when a matching skill needs variant
|
|
81
82
|
selection
|
|
@@ -136,12 +137,43 @@ Before creating tasks, evaluate the request type:
|
|
|
136
137
|
- NEVER break down capabilities into separate introspect tasks
|
|
137
138
|
- The single introspect task will list ALL capabilities
|
|
138
139
|
|
|
139
|
-
2. **
|
|
140
|
+
2. **Skill creation requests** - User wants to create/teach a new skill:
|
|
141
|
+
- "learn", "teach", "create skill", "new skill", "add skill",
|
|
142
|
+
"define skill", "make skill"
|
|
143
|
+
- Example: "learn" → learn type
|
|
144
|
+
- Example: "teach me to build" → learn type
|
|
145
|
+
- Example: "create a new skill" → learn type
|
|
146
|
+
|
|
147
|
+
**CRITICAL - Learn is ALWAYS a single task:**
|
|
148
|
+
- Learn requests MUST result in exactly ONE learn leaf task
|
|
149
|
+
- NEVER create multiple learn tasks for a single request
|
|
150
|
+
- NEVER nest learn tasks within groups
|
|
151
|
+
- The learn task launches a guided walkthrough for skill creation
|
|
152
|
+
|
|
153
|
+
**Learn task action format**: The action MUST explicitly mention
|
|
154
|
+
learning a new skill/capability. Use the user's original phrasing:
|
|
155
|
+
- "learn to build docker images" → action: "Learn to build docker images"
|
|
156
|
+
- "teach me to deploy" → action: "Learn to deploy"
|
|
157
|
+
- "create a new skill" → action: "Learn a new skill"
|
|
158
|
+
- "learn" → action: "Learn a new skill"
|
|
159
|
+
|
|
160
|
+
**Skill name extraction**: If the user's request includes a skill
|
|
161
|
+
topic (after "learn", "teach", "create skill", etc.), extract it and
|
|
162
|
+
include as `params.suggestedName`. Convert to imperative mood (command
|
|
163
|
+
form) with title case. Convert gerunds (-ing) to base verb form:
|
|
164
|
+
- "learn refining prompts" → params: { suggestedName: "Refine Prompts" }
|
|
165
|
+
- "learn building apps" → params: { suggestedName: "Build Apps" }
|
|
166
|
+
- "teach me to deploy" → params: { suggestedName: "Deploy" }
|
|
167
|
+
- "learn how to do stuff" → params: { suggestedName: "Do Stuff" }
|
|
168
|
+
- "create a deploy script skill" → params: { suggestedName: "Deploy Script" }
|
|
169
|
+
- "learn" (no topic) → no params needed
|
|
170
|
+
|
|
171
|
+
3. **Information requests** (questions) - Use question keywords:
|
|
140
172
|
- "explain", "describe", "tell me", "what is", "how does", "find",
|
|
141
173
|
"search"
|
|
142
174
|
- Example: "explain docker" → answer type
|
|
143
175
|
|
|
144
|
-
|
|
176
|
+
4. **Action requests** (commands) - Must match skills in "Available
|
|
145
177
|
Skills" section:
|
|
146
178
|
- Check if action verb matches ANY skill in "Available Skills"
|
|
147
179
|
section
|
|
@@ -158,7 +190,7 @@ Before creating tasks, evaluate the request type:
|
|
|
158
190
|
- Example: "build" with no matching skill in "Available Skills" →
|
|
159
191
|
action "Ignore unknown 'build' request"
|
|
160
192
|
|
|
161
|
-
|
|
193
|
+
5. **Vague/ambiguous requests** without clear verb:
|
|
162
194
|
- Phrases like "do something", "handle it" → ignore type
|
|
163
195
|
- Action format: "Ignore unknown 'X' request" where X is the phrase
|
|
164
196
|
|
|
@@ -10,7 +10,7 @@ export const introspectTool = {
|
|
|
10
10
|
},
|
|
11
11
|
capabilities: {
|
|
12
12
|
type: 'array',
|
|
13
|
-
description: 'Array of capabilities and skills. Include system capabilities (Introspect, Configure, Answer, Execute) with origin "system", meta workflow capabilities (Schedule, Validate
|
|
13
|
+
description: 'Array of capabilities and skills. Include system capabilities (Introspect, Configure, Answer, Learn, Execute) with origin "system", meta workflow capabilities (Schedule, Validate) with origin "meta", and user-provided skills from the Available Skills section with origin "user".',
|
|
14
14
|
items: {
|
|
15
15
|
type: 'object',
|
|
16
16
|
properties: {
|
|
@@ -25,7 +25,7 @@ export const introspectTool = {
|
|
|
25
25
|
origin: {
|
|
26
26
|
type: 'string',
|
|
27
27
|
enum: ['system', 'user', 'meta'],
|
|
28
|
-
description: 'Origin of the capability. Use "system" for system capabilities (Introspect, Configure, Answer, Execute), "meta" for meta workflow capabilities (Schedule, Validate
|
|
28
|
+
description: 'Origin of the capability. Use "system" for system capabilities (Introspect, Configure, Answer, Learn, Execute), "meta" for meta workflow capabilities (Schedule, Validate), and "user" for user-provided skills.',
|
|
29
29
|
},
|
|
30
30
|
isIncomplete: {
|
|
31
31
|
type: 'boolean',
|
|
@@ -27,7 +27,7 @@ export const scheduleTool = {
|
|
|
27
27
|
},
|
|
28
28
|
type: {
|
|
29
29
|
type: 'string',
|
|
30
|
-
description: 'Type: "group" for parent tasks with subtasks. For leaf tasks: "configure", "execute", "answer", "introspect", "report", "define", "ignore"',
|
|
30
|
+
description: 'Type: "group" for parent tasks with subtasks. For leaf tasks: "configure", "execute", "answer", "introspect", "learn", "report", "define", "ignore"',
|
|
31
31
|
},
|
|
32
32
|
params: {
|
|
33
33
|
type: 'object',
|
package/dist/types/components.js
CHANGED
|
@@ -6,3 +6,17 @@ export var ComponentStatus;
|
|
|
6
6
|
ComponentStatus["Pending"] = "pending";
|
|
7
7
|
ComponentStatus["Done"] = "done";
|
|
8
8
|
})(ComponentStatus || (ComponentStatus = {}));
|
|
9
|
+
export var LearnPhase;
|
|
10
|
+
(function (LearnPhase) {
|
|
11
|
+
LearnPhase["Name"] = "name";
|
|
12
|
+
LearnPhase["Description"] = "description";
|
|
13
|
+
LearnPhase["Aliases"] = "aliases";
|
|
14
|
+
LearnPhase["AliasMore"] = "alias_more";
|
|
15
|
+
LearnPhase["Config"] = "config";
|
|
16
|
+
LearnPhase["ConfigMore"] = "config_more";
|
|
17
|
+
LearnPhase["StepDescription"] = "step_description";
|
|
18
|
+
LearnPhase["StepExecutionType"] = "step_execution_type";
|
|
19
|
+
LearnPhase["StepExecutionValue"] = "step_execution_value";
|
|
20
|
+
LearnPhase["StepMore"] = "step_more";
|
|
21
|
+
LearnPhase["Review"] = "review";
|
|
22
|
+
})(LearnPhase || (LearnPhase = {}));
|
package/dist/types/schemas.js
CHANGED
package/dist/types/types.js
CHANGED
|
@@ -14,6 +14,7 @@ export var ComponentName;
|
|
|
14
14
|
ComponentName["Answer"] = "answer";
|
|
15
15
|
ComponentName["Execute"] = "execute";
|
|
16
16
|
ComponentName["Validate"] = "validate";
|
|
17
|
+
ComponentName["Learn"] = "learn";
|
|
17
18
|
})(ComponentName || (ComponentName = {}));
|
|
18
19
|
export var TaskType;
|
|
19
20
|
(function (TaskType) {
|
|
@@ -28,6 +29,7 @@ export var TaskType;
|
|
|
28
29
|
TaskType["Select"] = "select";
|
|
29
30
|
TaskType["Discard"] = "discard";
|
|
30
31
|
TaskType["Group"] = "group";
|
|
32
|
+
TaskType["Learn"] = "learn";
|
|
31
33
|
})(TaskType || (TaskType = {}));
|
|
32
34
|
export var Origin;
|
|
33
35
|
(function (Origin) {
|