prompt-language-shell 0.6.2 → 0.6.6
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 +14 -5
- package/dist/config/INTROSPECT.md +2 -0
- package/dist/services/anthropic.js +2 -2
- package/dist/services/components.js +25 -15
- package/dist/services/configuration.js +21 -13
- package/dist/services/execution-validator.js +32 -7
- package/dist/services/refinement.js +1 -2
- package/dist/services/skill-expander.js +1 -5
- package/dist/services/skill-parser.js +65 -20
- package/dist/services/skills.js +19 -7
- package/dist/services/task-router.js +34 -23
- package/dist/types/components.js +8 -1
- package/dist/ui/Answer.js +3 -1
- package/dist/ui/Command.js +6 -4
- package/dist/ui/Component.js +14 -17
- package/dist/ui/Config.js +3 -2
- package/dist/ui/Confirm.js +6 -7
- package/dist/ui/Execute.js +3 -1
- package/dist/ui/Introspect.js +14 -4
- package/dist/ui/Plan.js +8 -5
- package/dist/ui/Refinement.js +3 -1
- package/dist/ui/Report.js +3 -3
- package/dist/ui/Validate.js +4 -3
- package/dist/ui/Workflow.js +134 -74
- package/package.json +1 -1
- package/dist/services/queue.js +0 -52
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Your personal command-line concierge. Ask politely, and it gets things done.
|
|
4
4
|
|
|
5
|
-
> **Note:** This project is in early preview. Features and APIs
|
|
6
|
-
>
|
|
5
|
+
> **Note:** This project is in early preview. Features and APIs will change.
|
|
6
|
+
> See [roadmap](#roadmap).
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
@@ -13,14 +13,14 @@ npm install -g prompt-language-shell
|
|
|
13
13
|
|
|
14
14
|
## Setup
|
|
15
15
|
|
|
16
|
-
On first run, `pls` walks you through a quick setup.
|
|
16
|
+
On first run, `pls` walks you through a quick setup.
|
|
17
|
+
Your settings will be saved to `~/.plsrc`.
|
|
17
18
|
|
|
18
19
|
## Usage
|
|
19
20
|
|
|
20
21
|
Type `pls` followed by your request in natural language.
|
|
21
22
|
|
|
22
|
-
To see what `pls` can
|
|
23
|
-
do, start by listing available capabilities:
|
|
23
|
+
To see what `pls` can do, start by listing available capabilities:
|
|
24
24
|
|
|
25
25
|
```
|
|
26
26
|
$ pls list skills
|
|
@@ -98,6 +98,8 @@ Skills let you teach `pls` about your project-specific workflows. Create
|
|
|
98
98
|
markdown files in `~/.pls/skills/` to define custom operations that `pls` can
|
|
99
99
|
understand and execute.
|
|
100
100
|
|
|
101
|
+
For complete documentation, see [docs/SKILLS.md](./docs/SKILLS.md).
|
|
102
|
+
|
|
101
103
|
### Structure
|
|
102
104
|
|
|
103
105
|
Each skill file uses a simple markdown format:
|
|
@@ -155,6 +157,13 @@ $ pls build dev
|
|
|
155
157
|
$ pls build test
|
|
156
158
|
```
|
|
157
159
|
|
|
160
|
+
## Roadmap
|
|
161
|
+
|
|
162
|
+
- **0.7** - Comprehend skill, simplified prompts, better debugging
|
|
163
|
+
- **0.8** - Sequential and interlaced skill execution
|
|
164
|
+
- **0.9** - Learn skill, codebase refinement, complex dependency handling
|
|
165
|
+
- **1.0** - Production release
|
|
166
|
+
|
|
158
167
|
## Development
|
|
159
168
|
|
|
160
169
|
See [CLAUDE.md](./CLAUDE.md) for development guidelines and architecture.
|
|
@@ -88,6 +88,8 @@ These MUST appear AFTER Execute and BEFORE user skills:
|
|
|
88
88
|
If skills are provided in the "Available Skills" section below, include them
|
|
89
89
|
in the response. For each skill:
|
|
90
90
|
- Extract the skill name from the first heading (# Skill Name)
|
|
91
|
+
- If the skill name contains "(INCOMPLETE)", preserve it exactly in the task
|
|
92
|
+
action
|
|
91
93
|
- Extract a brief description from the Description or Overview section
|
|
92
94
|
- Keep descriptions concise (1-2 lines maximum)
|
|
93
95
|
- If the user specified a filter (e.g., "skills for deployment"), only include
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import { getAvailableConfigStructure, getConfiguredKeys, } from './configuration.js';
|
|
3
|
-
import { formatSkillsForPrompt,
|
|
3
|
+
import { formatSkillsForPrompt, loadSkillsWithValidation } from './skills.js';
|
|
4
4
|
import { toolRegistry } from './tool-registry.js';
|
|
5
5
|
/**
|
|
6
6
|
* Wraps text to ensure no line exceeds the specified width.
|
|
@@ -62,7 +62,7 @@ export class AnthropicService {
|
|
|
62
62
|
toolName === 'introspect' ||
|
|
63
63
|
toolName === 'execute' ||
|
|
64
64
|
toolName === 'validate') {
|
|
65
|
-
const skills =
|
|
65
|
+
const skills = loadSkillsWithValidation();
|
|
66
66
|
const skillsSection = formatSkillsForPrompt(skills);
|
|
67
67
|
systemPrompt += skillsSection;
|
|
68
68
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { ComponentName } from '../types/types.js';
|
|
4
|
+
import { ComponentStatus, } from '../types/components.js';
|
|
4
5
|
import { parse as parseYaml } from 'yaml';
|
|
5
|
-
import { getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
|
|
6
|
+
import { ConfigDefinitionType, getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
|
|
6
7
|
import { getConfirmationMessage } from './messages.js';
|
|
7
8
|
import { StepType } from '../ui/Config.js';
|
|
8
9
|
export function createWelcomeDefinition(app) {
|
|
@@ -39,15 +40,15 @@ function getConfigValue(config, key) {
|
|
|
39
40
|
*/
|
|
40
41
|
function getValidator(definition) {
|
|
41
42
|
switch (definition.type) {
|
|
42
|
-
case
|
|
43
|
+
case ConfigDefinitionType.RegExp:
|
|
43
44
|
return (value) => definition.pattern.test(value);
|
|
44
|
-
case
|
|
45
|
+
case ConfigDefinitionType.String:
|
|
45
46
|
return () => true; // Strings are always valid
|
|
46
|
-
case
|
|
47
|
+
case ConfigDefinitionType.Enum:
|
|
47
48
|
return (value) => definition.values.includes(value);
|
|
48
|
-
case
|
|
49
|
+
case ConfigDefinitionType.Number:
|
|
49
50
|
return (value) => !isNaN(Number(value));
|
|
50
|
-
case
|
|
51
|
+
case ConfigDefinitionType.Boolean:
|
|
51
52
|
return (value) => value === 'true' || value === 'false';
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -103,11 +104,11 @@ export function createConfigStepsFromSchema(keys) {
|
|
|
103
104
|
const shortKey = keyParts[keyParts.length - 1];
|
|
104
105
|
// Map definition to ConfigStep based on type
|
|
105
106
|
switch (definition.type) {
|
|
106
|
-
case
|
|
107
|
-
case
|
|
107
|
+
case ConfigDefinitionType.RegExp:
|
|
108
|
+
case ConfigDefinitionType.String: {
|
|
108
109
|
const value = currentValue !== undefined && typeof currentValue === 'string'
|
|
109
110
|
? currentValue
|
|
110
|
-
: definition.type ===
|
|
111
|
+
: definition.type === ConfigDefinitionType.String
|
|
111
112
|
? (definition.default ?? '')
|
|
112
113
|
: null;
|
|
113
114
|
return {
|
|
@@ -119,7 +120,7 @@ export function createConfigStepsFromSchema(keys) {
|
|
|
119
120
|
validate: getValidator(definition),
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
|
-
case
|
|
123
|
+
case ConfigDefinitionType.Number: {
|
|
123
124
|
const value = currentValue !== undefined && typeof currentValue === 'number'
|
|
124
125
|
? String(currentValue)
|
|
125
126
|
: definition.default !== undefined
|
|
@@ -134,7 +135,7 @@ export function createConfigStepsFromSchema(keys) {
|
|
|
134
135
|
validate: getValidator(definition),
|
|
135
136
|
};
|
|
136
137
|
}
|
|
137
|
-
case
|
|
138
|
+
case ConfigDefinitionType.Enum: {
|
|
138
139
|
const currentStr = currentValue !== undefined && typeof currentValue === 'string'
|
|
139
140
|
? currentValue
|
|
140
141
|
: definition.default;
|
|
@@ -154,7 +155,7 @@ export function createConfigStepsFromSchema(keys) {
|
|
|
154
155
|
validate: getValidator(definition),
|
|
155
156
|
};
|
|
156
157
|
}
|
|
157
|
-
case
|
|
158
|
+
case ConfigDefinitionType.Boolean: {
|
|
158
159
|
const currentBool = currentValue !== undefined && typeof currentValue === 'boolean'
|
|
159
160
|
? currentValue
|
|
160
161
|
: undefined;
|
|
@@ -178,6 +179,7 @@ export function createConfigDefinition(onFinished, onAborted) {
|
|
|
178
179
|
return {
|
|
179
180
|
id: randomUUID(),
|
|
180
181
|
name: ComponentName.Config,
|
|
182
|
+
status: ComponentStatus.Awaiting,
|
|
181
183
|
state: {},
|
|
182
184
|
props: {
|
|
183
185
|
steps: createConfigSteps(),
|
|
@@ -193,6 +195,7 @@ export function createConfigDefinitionWithKeys(keys, onFinished, onAborted) {
|
|
|
193
195
|
return {
|
|
194
196
|
id: randomUUID(),
|
|
195
197
|
name: ComponentName.Config,
|
|
198
|
+
status: ComponentStatus.Awaiting,
|
|
196
199
|
state: {},
|
|
197
200
|
props: {
|
|
198
201
|
steps: createConfigStepsFromSchema(keys),
|
|
@@ -205,6 +208,7 @@ export function createCommandDefinition(command, service) {
|
|
|
205
208
|
return {
|
|
206
209
|
id: randomUUID(),
|
|
207
210
|
name: ComponentName.Command,
|
|
211
|
+
status: ComponentStatus.Awaiting,
|
|
208
212
|
state: {},
|
|
209
213
|
props: {
|
|
210
214
|
command,
|
|
@@ -216,6 +220,7 @@ export function createPlanDefinition(message, tasks, onSelectionConfirmed) {
|
|
|
216
220
|
return {
|
|
217
221
|
id: randomUUID(),
|
|
218
222
|
name: ComponentName.Plan,
|
|
223
|
+
status: ComponentStatus.Awaiting,
|
|
219
224
|
state: {
|
|
220
225
|
highlightedIndex: null,
|
|
221
226
|
currentDefineGroupIndex: 0,
|
|
@@ -251,6 +256,7 @@ export function createRefinement(text, onAborted) {
|
|
|
251
256
|
return {
|
|
252
257
|
id: randomUUID(),
|
|
253
258
|
name: ComponentName.Refinement,
|
|
259
|
+
status: ComponentStatus.Awaiting,
|
|
254
260
|
state: {},
|
|
255
261
|
props: {
|
|
256
262
|
text,
|
|
@@ -262,6 +268,7 @@ export function createConfirmDefinition(onConfirmed, onCancelled) {
|
|
|
262
268
|
return {
|
|
263
269
|
id: randomUUID(),
|
|
264
270
|
name: ComponentName.Confirm,
|
|
271
|
+
status: ComponentStatus.Awaiting,
|
|
265
272
|
state: {},
|
|
266
273
|
props: {
|
|
267
274
|
message: getConfirmationMessage(),
|
|
@@ -274,6 +281,7 @@ export function createIntrospectDefinition(tasks, service) {
|
|
|
274
281
|
return {
|
|
275
282
|
id: randomUUID(),
|
|
276
283
|
name: ComponentName.Introspect,
|
|
284
|
+
status: ComponentStatus.Awaiting,
|
|
277
285
|
state: {},
|
|
278
286
|
props: {
|
|
279
287
|
tasks,
|
|
@@ -295,6 +303,7 @@ export function createAnswerDefinition(question, service) {
|
|
|
295
303
|
return {
|
|
296
304
|
id: randomUUID(),
|
|
297
305
|
name: ComponentName.Answer,
|
|
306
|
+
status: ComponentStatus.Awaiting,
|
|
298
307
|
state: {},
|
|
299
308
|
props: {
|
|
300
309
|
question,
|
|
@@ -308,16 +317,16 @@ export function isStateless(component) {
|
|
|
308
317
|
/**
|
|
309
318
|
* Mark a component as done. Returns the component to be added to timeline.
|
|
310
319
|
* Components use handlers.updateState to save their state before completion,
|
|
311
|
-
* so this function
|
|
320
|
+
* so this function sets the status to Done and returns the updated component.
|
|
312
321
|
*/
|
|
313
322
|
export function markAsDone(component) {
|
|
314
|
-
|
|
315
|
-
return component;
|
|
323
|
+
return { ...component, status: ComponentStatus.Done };
|
|
316
324
|
}
|
|
317
325
|
export function createExecuteDefinition(tasks, service) {
|
|
318
326
|
return {
|
|
319
327
|
id: randomUUID(),
|
|
320
328
|
name: ComponentName.Execute,
|
|
329
|
+
status: ComponentStatus.Awaiting,
|
|
321
330
|
state: {},
|
|
322
331
|
props: {
|
|
323
332
|
tasks,
|
|
@@ -329,6 +338,7 @@ export function createValidateDefinition(missingConfig, userRequest, service, on
|
|
|
329
338
|
return {
|
|
330
339
|
id: randomUUID(),
|
|
331
340
|
name: ComponentName.Validate,
|
|
341
|
+
status: ComponentStatus.Awaiting,
|
|
332
342
|
state: {},
|
|
333
343
|
props: {
|
|
334
344
|
missingConfig,
|
|
@@ -9,6 +9,14 @@ export var AnthropicModel;
|
|
|
9
9
|
AnthropicModel["Opus"] = "claude-opus-4-1";
|
|
10
10
|
})(AnthropicModel || (AnthropicModel = {}));
|
|
11
11
|
export const SUPPORTED_MODELS = Object.values(AnthropicModel);
|
|
12
|
+
export var ConfigDefinitionType;
|
|
13
|
+
(function (ConfigDefinitionType) {
|
|
14
|
+
ConfigDefinitionType["RegExp"] = "regexp";
|
|
15
|
+
ConfigDefinitionType["String"] = "string";
|
|
16
|
+
ConfigDefinitionType["Enum"] = "enum";
|
|
17
|
+
ConfigDefinitionType["Number"] = "number";
|
|
18
|
+
ConfigDefinitionType["Boolean"] = "boolean";
|
|
19
|
+
})(ConfigDefinitionType || (ConfigDefinitionType = {}));
|
|
12
20
|
export class ConfigError extends Error {
|
|
13
21
|
origin;
|
|
14
22
|
constructor(message, origin) {
|
|
@@ -172,20 +180,20 @@ export function getConfigurationRequiredMessage(forFutureUse = false) {
|
|
|
172
180
|
*/
|
|
173
181
|
const coreConfigSchema = {
|
|
174
182
|
'anthropic.key': {
|
|
175
|
-
type:
|
|
183
|
+
type: ConfigDefinitionType.RegExp,
|
|
176
184
|
required: true,
|
|
177
185
|
pattern: /^sk-ant-api03-[A-Za-z0-9_-]{95}$/,
|
|
178
186
|
description: 'Anthropic API key',
|
|
179
187
|
},
|
|
180
188
|
'anthropic.model': {
|
|
181
|
-
type:
|
|
189
|
+
type: ConfigDefinitionType.Enum,
|
|
182
190
|
required: true,
|
|
183
191
|
values: SUPPORTED_MODELS,
|
|
184
192
|
default: AnthropicModel.Haiku,
|
|
185
193
|
description: 'Anthropic model',
|
|
186
194
|
},
|
|
187
195
|
'settings.debug': {
|
|
188
|
-
type:
|
|
196
|
+
type: ConfigDefinitionType.Boolean,
|
|
189
197
|
required: false,
|
|
190
198
|
description: 'Debug mode',
|
|
191
199
|
},
|
|
@@ -239,20 +247,20 @@ export function getMissingConfigKeys() {
|
|
|
239
247
|
// Validate based on type
|
|
240
248
|
let isValid = false;
|
|
241
249
|
switch (definition.type) {
|
|
242
|
-
case
|
|
250
|
+
case ConfigDefinitionType.RegExp:
|
|
243
251
|
isValid = typeof value === 'string' && definition.pattern.test(value);
|
|
244
252
|
break;
|
|
245
|
-
case
|
|
253
|
+
case ConfigDefinitionType.String:
|
|
246
254
|
isValid = typeof value === 'string';
|
|
247
255
|
break;
|
|
248
|
-
case
|
|
256
|
+
case ConfigDefinitionType.Enum:
|
|
249
257
|
isValid =
|
|
250
258
|
typeof value === 'string' && definition.values.includes(value);
|
|
251
259
|
break;
|
|
252
|
-
case
|
|
260
|
+
case ConfigDefinitionType.Number:
|
|
253
261
|
isValid = typeof value === 'number';
|
|
254
262
|
break;
|
|
255
|
-
case
|
|
263
|
+
case ConfigDefinitionType.Boolean:
|
|
256
264
|
isValid = typeof value === 'boolean';
|
|
257
265
|
break;
|
|
258
266
|
}
|
|
@@ -357,13 +365,13 @@ function parseConfigValue(key, stringValue, schema) {
|
|
|
357
365
|
if (key in schema) {
|
|
358
366
|
const definition = schema[key];
|
|
359
367
|
switch (definition.type) {
|
|
360
|
-
case
|
|
368
|
+
case ConfigDefinitionType.Boolean:
|
|
361
369
|
return stringValue === 'true';
|
|
362
|
-
case
|
|
370
|
+
case ConfigDefinitionType.Number:
|
|
363
371
|
return Number(stringValue);
|
|
364
|
-
case
|
|
365
|
-
case
|
|
366
|
-
case
|
|
372
|
+
case ConfigDefinitionType.String:
|
|
373
|
+
case ConfigDefinitionType.RegExp:
|
|
374
|
+
case ConfigDefinitionType.Enum:
|
|
367
375
|
return stringValue;
|
|
368
376
|
}
|
|
369
377
|
}
|
|
@@ -5,25 +5,47 @@ import { expandSkillReferences } from './skill-expander.js';
|
|
|
5
5
|
import { getConfigType, parseSkillMarkdown } from './skill-parser.js';
|
|
6
6
|
/**
|
|
7
7
|
* Validate config requirements for execute tasks
|
|
8
|
-
* Returns missing config
|
|
8
|
+
* Returns validation result with missing config and validation errors
|
|
9
9
|
*/
|
|
10
10
|
export function validateExecuteTasks(tasks) {
|
|
11
11
|
const userConfig = loadUserConfig();
|
|
12
12
|
const missing = [];
|
|
13
13
|
const seenPaths = new Set();
|
|
14
|
-
|
|
14
|
+
const validationErrors = [];
|
|
15
|
+
const seenSkills = new Set();
|
|
16
|
+
// Load all skills (including invalid ones for validation)
|
|
15
17
|
const skillContents = loadSkills();
|
|
16
|
-
const parsedSkills = skillContents
|
|
17
|
-
.map((content) => parseSkillMarkdown(content))
|
|
18
|
-
.filter((s) => s !== null);
|
|
18
|
+
const parsedSkills = skillContents.map((content) => parseSkillMarkdown(content));
|
|
19
19
|
const skillLookup = (name) => parsedSkills.find((s) => s.name === name) || null;
|
|
20
|
+
// Check for invalid skills being used in tasks
|
|
21
|
+
for (const task of tasks) {
|
|
22
|
+
const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
|
|
23
|
+
if (skillName && !seenSkills.has(skillName)) {
|
|
24
|
+
seenSkills.add(skillName);
|
|
25
|
+
// Check if this skill is invalid
|
|
26
|
+
const skill = skillLookup(skillName);
|
|
27
|
+
if (skill && !skill.isValid) {
|
|
28
|
+
validationErrors.push({
|
|
29
|
+
skill: skill.name,
|
|
30
|
+
issues: [skill.validationError || 'Unknown validation error'],
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// If there are validation errors, return early
|
|
36
|
+
if (validationErrors.length > 0) {
|
|
37
|
+
return {
|
|
38
|
+
missingConfig: [],
|
|
39
|
+
validationErrors,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
20
42
|
for (const task of tasks) {
|
|
21
43
|
// Check if task originates from a skill
|
|
22
44
|
const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
|
|
23
45
|
if (skillName) {
|
|
24
46
|
// Task comes from a skill - check skill's Execution section
|
|
25
47
|
const skill = skillLookup(skillName);
|
|
26
|
-
if (!skill
|
|
48
|
+
if (!skill) {
|
|
27
49
|
continue;
|
|
28
50
|
}
|
|
29
51
|
// Get variant from task params (if any)
|
|
@@ -106,5 +128,8 @@ export function validateExecuteTasks(tasks) {
|
|
|
106
128
|
}
|
|
107
129
|
}
|
|
108
130
|
}
|
|
109
|
-
return
|
|
131
|
+
return {
|
|
132
|
+
missingConfig: missing,
|
|
133
|
+
validationErrors: [],
|
|
134
|
+
};
|
|
110
135
|
}
|
|
@@ -25,8 +25,7 @@ export async function handleRefinement(selectedTasks, service, originalCommand,
|
|
|
25
25
|
// Complete the Refinement component
|
|
26
26
|
handlers.completeActive();
|
|
27
27
|
// Route refined tasks to appropriate components
|
|
28
|
-
routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false
|
|
29
|
-
undefined // No commandComponent - use normal flow
|
|
28
|
+
routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false // No DEFINE tasks in refined result
|
|
30
29
|
);
|
|
31
30
|
}
|
|
32
31
|
catch (err) {
|
|
@@ -37,10 +37,6 @@ export function expandSkillReferences(execution, skillLookup, visited = new Set(
|
|
|
37
37
|
expanded.push(line);
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
40
|
-
if (!skill.execution || skill.execution.length === 0) {
|
|
41
|
-
// Referenced skill has no execution, skip
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
40
|
// Recursively expand referenced skill's execution
|
|
45
41
|
const newVisited = new Set(visited);
|
|
46
42
|
newVisited.add(skillName);
|
|
@@ -62,7 +58,7 @@ export function getReferencedSkills(execution, skillLookup, visited = new Set())
|
|
|
62
58
|
}
|
|
63
59
|
referenced.add(skillName);
|
|
64
60
|
const skill = skillLookup(skillName);
|
|
65
|
-
if (skill
|
|
61
|
+
if (skill) {
|
|
66
62
|
const newVisited = new Set(visited);
|
|
67
63
|
newVisited.add(skillName);
|
|
68
64
|
const nested = getReferencedSkills(skill.execution, skillLookup, newVisited);
|
|
@@ -1,40 +1,85 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Validate a skill without parsing it fully
|
|
4
|
+
* Returns validation error if skill is invalid, null if valid
|
|
4
5
|
*/
|
|
5
|
-
export function
|
|
6
|
+
export function validateSkill(content) {
|
|
6
7
|
const sections = extractSections(content);
|
|
7
|
-
// Name is required
|
|
8
|
+
// Name is required for error reporting
|
|
9
|
+
const skillName = sections.name || 'Unknown skill';
|
|
10
|
+
// Check required sections
|
|
8
11
|
if (!sections.name) {
|
|
9
|
-
return
|
|
12
|
+
return {
|
|
13
|
+
skillName,
|
|
14
|
+
error: 'The skill file is missing a Name section',
|
|
15
|
+
};
|
|
10
16
|
}
|
|
11
|
-
// Description is required
|
|
12
17
|
if (!sections.description) {
|
|
13
|
-
return
|
|
18
|
+
return {
|
|
19
|
+
skillName,
|
|
20
|
+
error: 'The skill file is missing a Description section',
|
|
21
|
+
};
|
|
14
22
|
}
|
|
15
|
-
// Steps are required
|
|
16
23
|
if (!sections.steps || sections.steps.length === 0) {
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
return {
|
|
25
|
+
skillName,
|
|
26
|
+
error: 'The skill file is missing a Steps section',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!sections.execution || sections.execution.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
skillName,
|
|
32
|
+
error: 'The skill file is missing an Execution section',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Execution and steps must have same count
|
|
36
|
+
if (sections.execution.length !== sections.steps.length) {
|
|
37
|
+
return {
|
|
38
|
+
skillName,
|
|
39
|
+
error: `The skill has ${String(sections.steps.length)} steps but ${String(sections.execution.length)} execution lines`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse a skill markdown file into structured definition
|
|
46
|
+
*/
|
|
47
|
+
export function parseSkillMarkdown(content) {
|
|
48
|
+
const sections = extractSections(content);
|
|
49
|
+
// Validate the skill
|
|
50
|
+
const validationError = validateSkill(content);
|
|
51
|
+
// For invalid skills, return minimal definition with error
|
|
52
|
+
if (validationError) {
|
|
53
|
+
return {
|
|
54
|
+
name: sections.name || 'Unknown skill',
|
|
55
|
+
description: sections.description || '',
|
|
56
|
+
steps: sections.steps || [],
|
|
57
|
+
execution: sections.execution || [],
|
|
58
|
+
isValid: false,
|
|
59
|
+
validationError: validationError.error,
|
|
60
|
+
isIncomplete: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Valid skill - all required fields are present (validation passed)
|
|
64
|
+
const description = sections.description;
|
|
24
65
|
const skill = {
|
|
25
66
|
name: sections.name,
|
|
26
|
-
description
|
|
67
|
+
description,
|
|
27
68
|
steps: sections.steps,
|
|
69
|
+
execution: sections.execution,
|
|
70
|
+
isValid: true,
|
|
28
71
|
};
|
|
72
|
+
// Check if skill is incomplete (valid but needs more documentation)
|
|
73
|
+
const MIN_DESCRIPTION_LENGTH = 20;
|
|
74
|
+
if (description.trim().length < MIN_DESCRIPTION_LENGTH) {
|
|
75
|
+
skill.isIncomplete = true;
|
|
76
|
+
}
|
|
29
77
|
if (sections.aliases && sections.aliases.length > 0) {
|
|
30
78
|
skill.aliases = sections.aliases;
|
|
31
79
|
}
|
|
32
80
|
if (sections.config) {
|
|
33
81
|
skill.config = sections.config;
|
|
34
82
|
}
|
|
35
|
-
if (sections.execution && sections.execution.length > 0) {
|
|
36
|
-
skill.execution = sections.execution;
|
|
37
|
-
}
|
|
38
83
|
return skill;
|
|
39
84
|
}
|
|
40
85
|
/**
|
|
@@ -46,8 +91,8 @@ function extractSections(content) {
|
|
|
46
91
|
let currentSection = null;
|
|
47
92
|
let sectionLines = [];
|
|
48
93
|
for (const line of lines) {
|
|
49
|
-
// Check for section headers (
|
|
50
|
-
const headerMatch = line.match(
|
|
94
|
+
// Check for section headers (any valid markdown header: #, ##, ###, etc.)
|
|
95
|
+
const headerMatch = line.match(/^#{1,6}\s+(.+)$/);
|
|
51
96
|
if (headerMatch) {
|
|
52
97
|
// Process previous section
|
|
53
98
|
if (currentSection) {
|
package/dist/services/skills.js
CHANGED
|
@@ -35,18 +35,26 @@ export function loadSkills() {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Load and parse all skill definitions
|
|
38
|
-
* Returns structured skill definitions
|
|
38
|
+
* Returns structured skill definitions (including invalid skills)
|
|
39
39
|
*/
|
|
40
40
|
export function loadSkillDefinitions() {
|
|
41
41
|
const skillContents = loadSkills();
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
return skillContents.map((content) => parseSkillMarkdown(content));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load skills and mark incomplete ones in their markdown
|
|
46
|
+
* Returns array of skill markdown with status markers
|
|
47
|
+
*/
|
|
48
|
+
export function loadSkillsWithValidation() {
|
|
49
|
+
const skillContents = loadSkills();
|
|
50
|
+
return skillContents.map((content) => {
|
|
44
51
|
const parsed = parseSkillMarkdown(content);
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
// If skill is incomplete (either validation failed or needs more documentation), append (INCOMPLETE) to the name
|
|
53
|
+
if (parsed.isIncomplete) {
|
|
54
|
+
return content.replace(/^(#{1,6}\s+Name\s*\n+)(.+?)(\n|$)/im, `$1$2 (INCOMPLETE)$3`);
|
|
47
55
|
}
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
return content;
|
|
57
|
+
});
|
|
50
58
|
}
|
|
51
59
|
/**
|
|
52
60
|
* Create skill lookup function from definitions
|
|
@@ -72,6 +80,10 @@ export function formatSkillsForPrompt(skills) {
|
|
|
72
80
|
The following skills define domain-specific workflows. When the user's
|
|
73
81
|
query matches a skill, incorporate the skill's steps into your plan.
|
|
74
82
|
|
|
83
|
+
Skills marked with (INCOMPLETE) have validation errors or need more
|
|
84
|
+
documentation, and cannot be executed. These should be listed in
|
|
85
|
+
introspection with their markers.
|
|
86
|
+
|
|
75
87
|
**IMPORTANT**: When creating options from skill descriptions, do NOT use
|
|
76
88
|
brackets for additional information. Use commas instead. For example:
|
|
77
89
|
- CORRECT: "Build project Alpha, the legacy version"
|
|
@@ -3,7 +3,7 @@ import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDe
|
|
|
3
3
|
import { saveConfig, unflattenConfig } from './configuration.js';
|
|
4
4
|
import { FeedbackType } from '../types/types.js';
|
|
5
5
|
import { validateExecuteTasks } from './execution-validator.js';
|
|
6
|
-
import { getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
|
|
6
|
+
import { getCancellationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
|
|
7
7
|
/**
|
|
8
8
|
* Determine the operation name based on task types
|
|
9
9
|
*/
|
|
@@ -20,7 +20,7 @@ export function getOperationName(tasks) {
|
|
|
20
20
|
* Route tasks to appropriate components with Confirm flow
|
|
21
21
|
* Handles the complete flow: Plan → Confirm → Execute/Answer/Introspect
|
|
22
22
|
*/
|
|
23
|
-
export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false
|
|
23
|
+
export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false) {
|
|
24
24
|
if (tasks.length === 0)
|
|
25
25
|
return;
|
|
26
26
|
// Filter out ignore and discard tasks early
|
|
@@ -32,31 +32,32 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, hand
|
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
const operation = getOperationName(validTasks);
|
|
35
|
-
// Create plan definition with valid tasks only
|
|
36
|
-
const planDefinition = createPlanDefinition(message, validTasks);
|
|
37
35
|
if (hasDefineTask) {
|
|
38
36
|
// Has DEFINE tasks - add Plan to queue for user selection
|
|
39
37
|
// Refinement flow will call this function again with refined tasks
|
|
38
|
+
const planDefinition = createPlanDefinition(message, validTasks);
|
|
40
39
|
handlers.addToQueue(planDefinition);
|
|
41
40
|
}
|
|
42
41
|
else {
|
|
43
|
-
// No DEFINE tasks -
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
// No DEFINE tasks - Plan auto-completes and adds Confirm to queue
|
|
43
|
+
// When Plan activates, Command moves to timeline
|
|
44
|
+
// When Plan completes, it moves to pending
|
|
45
|
+
// When Confirm activates, Plan stays pending (visible for context)
|
|
46
|
+
const planDefinition = createPlanDefinition(message, validTasks, () => {
|
|
47
|
+
// Plan completed - add Confirm to queue
|
|
48
|
+
const confirmDefinition = createConfirmDefinition(() => {
|
|
49
|
+
// User confirmed - complete both Confirm and Plan, then route to appropriate component
|
|
50
|
+
handlers.completeActiveAndPending();
|
|
51
|
+
executeTasksAfterConfirm(validTasks, service, userRequest, handlers);
|
|
52
|
+
}, () => {
|
|
53
|
+
// User cancelled - complete both Confirm and Plan, then show cancellation
|
|
54
|
+
handlers.completeActiveAndPending();
|
|
55
|
+
const message = getCancellationMessage(operation);
|
|
56
|
+
handlers.addToQueue(createFeedback(FeedbackType.Aborted, message));
|
|
57
|
+
});
|
|
58
|
+
handlers.addToQueue(confirmDefinition);
|
|
51
59
|
});
|
|
52
|
-
|
|
53
|
-
if (commandComponent) {
|
|
54
|
-
handlers.completeActive(planDefinition);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
handlers.addToTimeline(planDefinition);
|
|
58
|
-
}
|
|
59
|
-
handlers.addToQueue(confirmDefinition);
|
|
60
|
+
handlers.addToQueue(planDefinition);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
@@ -124,9 +125,19 @@ function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
|
|
|
124
125
|
}
|
|
125
126
|
else {
|
|
126
127
|
// Execute tasks with validation
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
|
|
128
|
+
const validation = validateExecuteTasks(tasks);
|
|
129
|
+
if (validation.validationErrors.length > 0) {
|
|
130
|
+
// Show error feedback for invalid skills
|
|
131
|
+
const errorMessages = validation.validationErrors.map((error) => {
|
|
132
|
+
const issuesList = error.issues
|
|
133
|
+
.map((issue) => ` - ${issue}`)
|
|
134
|
+
.join('\n');
|
|
135
|
+
return `Invalid skill definition "${error.skill}":\n\n${issuesList}`;
|
|
136
|
+
});
|
|
137
|
+
handlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessages.join('\n\n')));
|
|
138
|
+
}
|
|
139
|
+
else if (validation.missingConfig.length > 0) {
|
|
140
|
+
handlers.addToQueue(createValidateDefinition(validation.missingConfig, userRequest, service, (error) => {
|
|
130
141
|
handlers.onError(error);
|
|
131
142
|
}, () => {
|
|
132
143
|
handlers.addToQueue(createExecuteDefinition(tasks, service));
|