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 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(0);
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
- const nextStep = step + 1;
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
+ };
@@ -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);
@@ -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
@@ -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 or Report first (this is WRONG)
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-4: system capabilities (origin: "system")
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. **Execute**
87
+ 4. **Learn**
88
+ 5. **Execute**
88
89
 
89
- ### Position 5-7: meta workflow capabilities (origin: "meta")
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
- 5. **Schedule**
94
- 6. **Validate**
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, Report
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, Report
163
- with origin "meta").
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"), meta
178
- capabilities (Schedule, Validate, Report with origin "meta"), plus the
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
 
@@ -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. **Information requests** (questions) - Use question keywords:
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
- 3. **Action requests** (commands) - Must match skills in "Available
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
- 4. **Vague/ambiguous requests** without clear verb:
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, Report) with origin "meta", and user-provided skills from the Available Skills section with origin "user".',
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, Report), and "user" for user-provided skills.',
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',
@@ -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 = {}));
@@ -10,6 +10,7 @@ export const TaskTypeSchema = z.enum([
10
10
  TaskType.Execute,
11
11
  TaskType.Answer,
12
12
  TaskType.Introspect,
13
+ TaskType.Learn,
13
14
  TaskType.Report,
14
15
  TaskType.Define,
15
16
  TaskType.Ignore,
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Your personal command-line concierge. Ask politely, and it gets things done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",