prompt-language-shell 0.4.9 → 0.5.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 +114 -10
- package/dist/config/INTROSPECT.md +9 -5
- package/dist/config/PLAN.md +39 -1
- package/dist/config/VALIDATE.md +139 -0
- package/dist/handlers/config.js +23 -6
- package/dist/handlers/execute.js +10 -2
- package/dist/handlers/execution.js +68 -1
- package/dist/services/anthropic.js +3 -2
- package/dist/services/colors.js +2 -2
- package/dist/services/components.js +33 -1
- package/dist/services/config-loader.js +67 -0
- package/dist/services/execution-validator.js +110 -0
- package/dist/services/placeholder-resolver.js +120 -0
- package/dist/services/shell.js +1 -0
- package/dist/services/skill-expander.js +91 -0
- package/dist/services/skill-parser.js +169 -0
- package/dist/services/skills.js +26 -0
- package/dist/services/timing.js +38 -0
- package/dist/services/tool-registry.js +5 -0
- package/dist/tools/validate.tool.js +43 -0
- package/dist/types/skills.js +4 -0
- package/dist/types/types.js +1 -0
- package/dist/ui/Answer.js +3 -9
- package/dist/ui/Command.js +3 -6
- package/dist/ui/Component.js +7 -1
- package/dist/ui/Config.js +2 -2
- package/dist/ui/Execute.js +59 -14
- package/dist/ui/Introspect.js +6 -7
- package/dist/ui/Validate.js +120 -0
- package/dist/ui/Welcome.js +12 -5
- package/package.json +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
/**
|
|
6
|
+
* Load user config from ~/.plsrc
|
|
7
|
+
*/
|
|
8
|
+
export function loadUserConfig() {
|
|
9
|
+
const configPath = join(homedir(), '.plsrc');
|
|
10
|
+
if (!existsSync(configPath)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
15
|
+
const parsed = YAML.parse(content);
|
|
16
|
+
if (parsed && typeof parsed === 'object') {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if config has a specific path
|
|
27
|
+
*/
|
|
28
|
+
export function hasConfigPath(config, path) {
|
|
29
|
+
const parts = path.split('.');
|
|
30
|
+
let current = config;
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
if (current === null || current === undefined) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (typeof current !== 'object') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
current = current[part];
|
|
39
|
+
}
|
|
40
|
+
return (current !== null &&
|
|
41
|
+
current !== undefined &&
|
|
42
|
+
(typeof current === 'string' ||
|
|
43
|
+
typeof current === 'boolean' ||
|
|
44
|
+
typeof current === 'number'));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get config value at path
|
|
48
|
+
*/
|
|
49
|
+
export function getConfigValue(config, path) {
|
|
50
|
+
const parts = path.split('.');
|
|
51
|
+
let current = config;
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
if (current === null || current === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (typeof current !== 'object') {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
current = current[part];
|
|
60
|
+
}
|
|
61
|
+
if (typeof current === 'string' ||
|
|
62
|
+
typeof current === 'boolean' ||
|
|
63
|
+
typeof current === 'number') {
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { extractPlaceholders, pathToString, resolveVariant, } from './placeholder-resolver.js';
|
|
2
|
+
import { loadUserConfig, hasConfigPath } from './config-loader.js';
|
|
3
|
+
import { loadSkills } from './skills.js';
|
|
4
|
+
import { expandSkillReferences } from './skill-expander.js';
|
|
5
|
+
import { getConfigType, parseSkillMarkdown } from './skill-parser.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validate config requirements for execute tasks
|
|
8
|
+
* Returns missing config requirements
|
|
9
|
+
*/
|
|
10
|
+
export function validateExecuteTasks(tasks) {
|
|
11
|
+
const userConfig = loadUserConfig();
|
|
12
|
+
const missing = [];
|
|
13
|
+
const seenPaths = new Set();
|
|
14
|
+
// Load and parse all skills for validation
|
|
15
|
+
const skillContents = loadSkills();
|
|
16
|
+
const parsedSkills = skillContents
|
|
17
|
+
.map((content) => parseSkillMarkdown(content))
|
|
18
|
+
.filter((s) => s !== null);
|
|
19
|
+
const skillLookup = (name) => parsedSkills.find((s) => s.name === name) || null;
|
|
20
|
+
for (const task of tasks) {
|
|
21
|
+
// Check if task originates from a skill
|
|
22
|
+
const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
|
|
23
|
+
if (skillName) {
|
|
24
|
+
// Task comes from a skill - check skill's Execution section
|
|
25
|
+
const skill = skillLookup(skillName);
|
|
26
|
+
if (!skill || !skill.execution) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// Get variant from task params (if any)
|
|
30
|
+
// Try params.variant first, then look for other param keys that might be the variant
|
|
31
|
+
let variant = null;
|
|
32
|
+
if (typeof task.params?.variant === 'string') {
|
|
33
|
+
variant = task.params.variant.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
else if (task.params && typeof task.params === 'object') {
|
|
36
|
+
// Look for other params that could be the variant (e.g., product, target, option, etc.)
|
|
37
|
+
// Exclude known non-variant params
|
|
38
|
+
const excludeKeys = new Set(['skill', 'type']);
|
|
39
|
+
for (const [key, value] of Object.entries(task.params)) {
|
|
40
|
+
if (!excludeKeys.has(key) && typeof value === 'string') {
|
|
41
|
+
variant = value.toLowerCase();
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Expand skill references to get actual commands
|
|
47
|
+
const expanded = expandSkillReferences(skill.execution, skillLookup);
|
|
48
|
+
// Extract placeholders from actual commands
|
|
49
|
+
for (const line of expanded) {
|
|
50
|
+
const placeholders = extractPlaceholders(line);
|
|
51
|
+
for (const placeholder of placeholders) {
|
|
52
|
+
let resolvedPath;
|
|
53
|
+
if (placeholder.hasVariant) {
|
|
54
|
+
// Variant placeholder - resolve with variant from params
|
|
55
|
+
if (!variant) {
|
|
56
|
+
// No variant provided - skip this placeholder
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const resolvedPathArray = resolveVariant(placeholder.path, variant);
|
|
60
|
+
resolvedPath = pathToString(resolvedPathArray);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Strict placeholder - use as-is
|
|
64
|
+
resolvedPath = pathToString(placeholder.path);
|
|
65
|
+
}
|
|
66
|
+
// Skip if already processed
|
|
67
|
+
if (seenPaths.has(resolvedPath)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
seenPaths.add(resolvedPath);
|
|
71
|
+
// Check if config exists
|
|
72
|
+
if (!hasConfigPath(userConfig, resolvedPath)) {
|
|
73
|
+
// Get type from skill config
|
|
74
|
+
const type = skill.config
|
|
75
|
+
? getConfigType(skill.config, resolvedPath)
|
|
76
|
+
: undefined;
|
|
77
|
+
missing.push({
|
|
78
|
+
path: resolvedPath,
|
|
79
|
+
type: type || 'string',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Task doesn't come from a skill - check task action for placeholders
|
|
87
|
+
const placeholders = extractPlaceholders(task.action);
|
|
88
|
+
for (const placeholder of placeholders) {
|
|
89
|
+
// Skip variant placeholders - they should have been resolved during planning
|
|
90
|
+
if (placeholder.hasVariant) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const path = placeholder.path.join('.');
|
|
94
|
+
// Skip if already processed
|
|
95
|
+
if (seenPaths.has(path)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
seenPaths.add(path);
|
|
99
|
+
// Check if config exists
|
|
100
|
+
if (!hasConfigPath(userConfig, path)) {
|
|
101
|
+
missing.push({
|
|
102
|
+
path,
|
|
103
|
+
type: 'string', // Default to string for now
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return missing;
|
|
110
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a string is all uppercase (variant placeholder indicator)
|
|
3
|
+
*/
|
|
4
|
+
function isUpperCase(str) {
|
|
5
|
+
return str === str.toUpperCase() && str !== str.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse placeholder from string
|
|
9
|
+
* Returns placeholder info or null if no valid placeholder found
|
|
10
|
+
*/
|
|
11
|
+
export function parsePlaceholder(text) {
|
|
12
|
+
// Match {path.to.property} format
|
|
13
|
+
const match = text.match(/\{([^}]+)\}/);
|
|
14
|
+
if (!match) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const original = match[0];
|
|
18
|
+
const pathString = match[1];
|
|
19
|
+
const path = pathString.split('.');
|
|
20
|
+
// Check if any path component is uppercase (variant placeholder)
|
|
21
|
+
const variantIndex = path.findIndex((part) => isUpperCase(part));
|
|
22
|
+
const hasVariant = variantIndex !== -1;
|
|
23
|
+
return {
|
|
24
|
+
original,
|
|
25
|
+
path,
|
|
26
|
+
hasVariant,
|
|
27
|
+
variantIndex: hasVariant ? variantIndex : undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract all placeholders from text
|
|
32
|
+
*/
|
|
33
|
+
export function extractPlaceholders(text) {
|
|
34
|
+
const placeholders = [];
|
|
35
|
+
const regex = /\{([^}]+)\}/g;
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = regex.exec(text)) !== null) {
|
|
38
|
+
const pathString = match[1];
|
|
39
|
+
const path = pathString.split('.');
|
|
40
|
+
const variantIndex = path.findIndex((part) => isUpperCase(part));
|
|
41
|
+
const hasVariant = variantIndex !== -1;
|
|
42
|
+
placeholders.push({
|
|
43
|
+
original: match[0],
|
|
44
|
+
path,
|
|
45
|
+
hasVariant,
|
|
46
|
+
variantIndex: hasVariant ? variantIndex : undefined,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return placeholders;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Replace uppercase component in path with actual variant name
|
|
53
|
+
* Returns new path with variant replaced
|
|
54
|
+
*/
|
|
55
|
+
export function resolveVariant(path, variantName) {
|
|
56
|
+
return path.map((part) => (isUpperCase(part) ? variantName : part));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Convert path array to dot notation string
|
|
60
|
+
*/
|
|
61
|
+
export function pathToString(path) {
|
|
62
|
+
return path.join('.');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve placeholder value from config
|
|
66
|
+
* Returns the value at the specified path or undefined if not found
|
|
67
|
+
*/
|
|
68
|
+
export function resolveFromConfig(config, path) {
|
|
69
|
+
let current = config;
|
|
70
|
+
for (const part of path) {
|
|
71
|
+
if (current === null || current === undefined) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (typeof current !== 'object') {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
current = current[part];
|
|
78
|
+
}
|
|
79
|
+
if (typeof current === 'string' ||
|
|
80
|
+
typeof current === 'boolean' ||
|
|
81
|
+
typeof current === 'number') {
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Replace all placeholders in text with values from config
|
|
88
|
+
* Note: Variant placeholders (with uppercase components) must be resolved first
|
|
89
|
+
*/
|
|
90
|
+
export function replacePlaceholders(text, config) {
|
|
91
|
+
return text.replace(/\{([^}]+)\}/g, (_match, pathString) => {
|
|
92
|
+
const path = pathString.split('.');
|
|
93
|
+
const value = resolveFromConfig(config, path);
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
// Keep placeholder if not found in config
|
|
96
|
+
return `{${pathString}}`;
|
|
97
|
+
}
|
|
98
|
+
return String(value);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if text contains any placeholders
|
|
103
|
+
*/
|
|
104
|
+
export function hasPlaceholders(text) {
|
|
105
|
+
return /\{[^}]+\}/.test(text);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get all unique config paths required by placeholders in text
|
|
109
|
+
* Note: Variant placeholders (with uppercase components) must be resolved first
|
|
110
|
+
*/
|
|
111
|
+
export function getRequiredConfigPaths(text) {
|
|
112
|
+
const placeholders = extractPlaceholders(text);
|
|
113
|
+
const paths = new Set();
|
|
114
|
+
for (const placeholder of placeholders) {
|
|
115
|
+
if (!placeholder.hasVariant) {
|
|
116
|
+
paths.add(pathToString(placeholder.path));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return Array.from(paths);
|
|
120
|
+
}
|
package/dist/services/shell.js
CHANGED
|
@@ -4,6 +4,7 @@ export var ExecutionStatus;
|
|
|
4
4
|
ExecutionStatus["Running"] = "running";
|
|
5
5
|
ExecutionStatus["Success"] = "success";
|
|
6
6
|
ExecutionStatus["Failed"] = "failed";
|
|
7
|
+
ExecutionStatus["Aborted"] = "aborted";
|
|
7
8
|
})(ExecutionStatus || (ExecutionStatus = {}));
|
|
8
9
|
export var ExecutionResult;
|
|
9
10
|
(function (ExecutionResult) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse skill reference from execution line
|
|
3
|
+
* Returns skill name if line is a reference, otherwise null
|
|
4
|
+
*/
|
|
5
|
+
export function parseSkillReference(line) {
|
|
6
|
+
const match = line.match(/^\[(.+)\]$/);
|
|
7
|
+
return match ? match[1].trim() : null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Check if execution line is a skill reference
|
|
11
|
+
*/
|
|
12
|
+
export function isSkillReference(line) {
|
|
13
|
+
return /^\[.+\]$/.test(line.trim());
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Expand skill references in execution commands
|
|
17
|
+
* Returns expanded execution lines with references replaced
|
|
18
|
+
* Throws error if circular reference detected
|
|
19
|
+
*/
|
|
20
|
+
export function expandSkillReferences(execution, skillLookup, visited = new Set()) {
|
|
21
|
+
const expanded = [];
|
|
22
|
+
for (const line of execution) {
|
|
23
|
+
const skillName = parseSkillReference(line);
|
|
24
|
+
if (!skillName) {
|
|
25
|
+
// Not a reference, keep as-is
|
|
26
|
+
expanded.push(line);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
// Check for circular reference
|
|
30
|
+
if (visited.has(skillName)) {
|
|
31
|
+
throw new Error(`Circular skill reference detected: ${Array.from(visited).join(' → ')} → ${skillName}`);
|
|
32
|
+
}
|
|
33
|
+
// Look up referenced skill
|
|
34
|
+
const skill = skillLookup(skillName);
|
|
35
|
+
if (!skill) {
|
|
36
|
+
// Referenced skill not found, keep as-is
|
|
37
|
+
expanded.push(line);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!skill.execution || skill.execution.length === 0) {
|
|
41
|
+
// Referenced skill has no execution, skip
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Recursively expand referenced skill's execution
|
|
45
|
+
const newVisited = new Set(visited);
|
|
46
|
+
newVisited.add(skillName);
|
|
47
|
+
const referencedExecution = expandSkillReferences(skill.execution, skillLookup, newVisited);
|
|
48
|
+
expanded.push(...referencedExecution);
|
|
49
|
+
}
|
|
50
|
+
return expanded;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all skill names referenced in execution (including nested)
|
|
54
|
+
* Returns unique set of skill names
|
|
55
|
+
*/
|
|
56
|
+
export function getReferencedSkills(execution, skillLookup, visited = new Set()) {
|
|
57
|
+
const referenced = new Set();
|
|
58
|
+
for (const line of execution) {
|
|
59
|
+
const skillName = parseSkillReference(line);
|
|
60
|
+
if (!skillName || visited.has(skillName)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
referenced.add(skillName);
|
|
64
|
+
const skill = skillLookup(skillName);
|
|
65
|
+
if (skill && skill.execution) {
|
|
66
|
+
const newVisited = new Set(visited);
|
|
67
|
+
newVisited.add(skillName);
|
|
68
|
+
const nested = getReferencedSkills(skill.execution, skillLookup, newVisited);
|
|
69
|
+
for (const name of nested) {
|
|
70
|
+
referenced.add(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return referenced;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validate skill references don't form cycles
|
|
78
|
+
* Returns true if valid, false if circular reference detected
|
|
79
|
+
*/
|
|
80
|
+
export function validateNoCycles(execution, skillLookup, visited = new Set()) {
|
|
81
|
+
try {
|
|
82
|
+
expandSkillReferences(execution, skillLookup, visited);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof Error && error.message.includes('Circular')) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a skill markdown file into structured definition
|
|
4
|
+
*/
|
|
5
|
+
export function parseSkillMarkdown(content) {
|
|
6
|
+
const sections = extractSections(content);
|
|
7
|
+
// Name is required
|
|
8
|
+
if (!sections.name) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
// Description is required
|
|
12
|
+
if (!sections.description) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
// Steps are required
|
|
16
|
+
if (!sections.steps || sections.steps.length === 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
// Validate execution and steps have same count (if execution exists)
|
|
20
|
+
if (sections.execution &&
|
|
21
|
+
sections.execution.length !== sections.steps.length) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const skill = {
|
|
25
|
+
name: sections.name,
|
|
26
|
+
description: sections.description,
|
|
27
|
+
steps: sections.steps,
|
|
28
|
+
};
|
|
29
|
+
if (sections.aliases && sections.aliases.length > 0) {
|
|
30
|
+
skill.aliases = sections.aliases;
|
|
31
|
+
}
|
|
32
|
+
if (sections.config) {
|
|
33
|
+
skill.config = sections.config;
|
|
34
|
+
}
|
|
35
|
+
if (sections.execution && sections.execution.length > 0) {
|
|
36
|
+
skill.execution = sections.execution;
|
|
37
|
+
}
|
|
38
|
+
return skill;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract sections from markdown content
|
|
42
|
+
*/
|
|
43
|
+
function extractSections(content) {
|
|
44
|
+
const lines = content.split('\n');
|
|
45
|
+
const sections = {};
|
|
46
|
+
let currentSection = null;
|
|
47
|
+
let sectionLines = [];
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
// Check for section headers (### SectionName)
|
|
50
|
+
const headerMatch = line.match(/^###\s+(.+)$/);
|
|
51
|
+
if (headerMatch) {
|
|
52
|
+
// Process previous section
|
|
53
|
+
if (currentSection) {
|
|
54
|
+
processSectionContent(currentSection, sectionLines, sections);
|
|
55
|
+
}
|
|
56
|
+
// Start new section
|
|
57
|
+
currentSection = headerMatch[1].trim().toLowerCase();
|
|
58
|
+
sectionLines = [];
|
|
59
|
+
}
|
|
60
|
+
else if (currentSection) {
|
|
61
|
+
// Accumulate lines for current section
|
|
62
|
+
sectionLines.push(line);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Process final section
|
|
66
|
+
if (currentSection) {
|
|
67
|
+
processSectionContent(currentSection, sectionLines, sections);
|
|
68
|
+
}
|
|
69
|
+
return sections;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Process accumulated section content
|
|
73
|
+
*/
|
|
74
|
+
function processSectionContent(sectionName, lines, sections) {
|
|
75
|
+
const content = lines.join('\n').trim();
|
|
76
|
+
if (!content) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
switch (sectionName) {
|
|
80
|
+
case 'name':
|
|
81
|
+
sections.name = content;
|
|
82
|
+
break;
|
|
83
|
+
case 'description':
|
|
84
|
+
sections.description = content;
|
|
85
|
+
break;
|
|
86
|
+
case 'aliases':
|
|
87
|
+
sections.aliases = extractBulletList(content);
|
|
88
|
+
break;
|
|
89
|
+
case 'config':
|
|
90
|
+
sections.config = parseConfigSchema(content);
|
|
91
|
+
break;
|
|
92
|
+
case 'steps':
|
|
93
|
+
sections.steps = extractBulletList(content);
|
|
94
|
+
break;
|
|
95
|
+
case 'execution':
|
|
96
|
+
sections.execution = extractBulletList(content);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extract bullet list items from content
|
|
102
|
+
*/
|
|
103
|
+
function extractBulletList(content) {
|
|
104
|
+
const lines = content.split('\n');
|
|
105
|
+
const items = [];
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
// Match bullet points: "- text" or "* text"
|
|
109
|
+
const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
|
|
110
|
+
if (bulletMatch) {
|
|
111
|
+
items.push(bulletMatch[1].trim());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return items;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Parse YAML config schema
|
|
118
|
+
*/
|
|
119
|
+
function parseConfigSchema(content) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = YAML.parse(content);
|
|
122
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Generate all config paths from schema
|
|
133
|
+
*/
|
|
134
|
+
export function generateConfigPaths(schema, prefix = '') {
|
|
135
|
+
const paths = [];
|
|
136
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
137
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
138
|
+
if (typeof value === 'string') {
|
|
139
|
+
// Leaf node with type annotation
|
|
140
|
+
paths.push(fullKey);
|
|
141
|
+
}
|
|
142
|
+
else if (typeof value === 'object') {
|
|
143
|
+
// Nested object - recurse
|
|
144
|
+
paths.push(...generateConfigPaths(value, fullKey));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return paths;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get config type for a specific path
|
|
151
|
+
*/
|
|
152
|
+
export function getConfigType(schema, path) {
|
|
153
|
+
const parts = path.split('.');
|
|
154
|
+
let current = schema;
|
|
155
|
+
for (const part of parts) {
|
|
156
|
+
if (typeof current === 'string') {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
if (typeof current !== 'object') {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
current = current[part];
|
|
163
|
+
}
|
|
164
|
+
if (typeof current === 'string' &&
|
|
165
|
+
(current === 'string' || current === 'boolean' || current === 'number')) {
|
|
166
|
+
return current;
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
package/dist/services/skills.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { parseSkillMarkdown } from './skill-parser.js';
|
|
4
5
|
/**
|
|
5
6
|
* Get the path to the skills directory
|
|
6
7
|
*/
|
|
@@ -32,6 +33,31 @@ export function loadSkills() {
|
|
|
32
33
|
return [];
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Load and parse all skill definitions
|
|
38
|
+
* Returns structured skill definitions
|
|
39
|
+
*/
|
|
40
|
+
export function loadSkillDefinitions() {
|
|
41
|
+
const skillContents = loadSkills();
|
|
42
|
+
const definitions = [];
|
|
43
|
+
for (const content of skillContents) {
|
|
44
|
+
const parsed = parseSkillMarkdown(content);
|
|
45
|
+
if (parsed) {
|
|
46
|
+
definitions.push(parsed);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return definitions;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create skill lookup function from definitions
|
|
53
|
+
*/
|
|
54
|
+
export function createSkillLookup(definitions) {
|
|
55
|
+
const map = new Map();
|
|
56
|
+
for (const definition of definitions) {
|
|
57
|
+
map.set(definition.name, definition);
|
|
58
|
+
}
|
|
59
|
+
return (name) => map.get(name) || null;
|
|
60
|
+
}
|
|
35
61
|
/**
|
|
36
62
|
* Format skills for inclusion in the planning prompt
|
|
37
63
|
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing utilities for UI components
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Waits for at least the minimum processing time.
|
|
6
|
+
* Ensures async operations don't complete too quickly for good UX.
|
|
7
|
+
*
|
|
8
|
+
* @param startTime - The timestamp when the operation started
|
|
9
|
+
* @param minimumTime - The minimum total time the operation should take
|
|
10
|
+
*/
|
|
11
|
+
export async function ensureMinimumTime(startTime, minimumTime) {
|
|
12
|
+
const elapsed = Date.now() - startTime;
|
|
13
|
+
const remainingTime = Math.max(0, minimumTime - elapsed);
|
|
14
|
+
if (remainingTime > 0) {
|
|
15
|
+
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wraps an async operation with minimum processing time UX polish.
|
|
20
|
+
* Ensures successful operations take at least `minimumTime` milliseconds.
|
|
21
|
+
* Errors are thrown immediately without delay for better UX.
|
|
22
|
+
*
|
|
23
|
+
* @param operation - The async operation to perform
|
|
24
|
+
* @param minimumTime - Minimum time in milliseconds for UX polish on success
|
|
25
|
+
* @returns The result of the operation
|
|
26
|
+
*/
|
|
27
|
+
export async function withMinimumTime(operation, minimumTime) {
|
|
28
|
+
const startTime = Date.now();
|
|
29
|
+
try {
|
|
30
|
+
const result = await operation();
|
|
31
|
+
await ensureMinimumTime(startTime, minimumTime);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
// Don't wait on error - fail fast for better UX
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -38,6 +38,7 @@ import { configTool } from '../tools/config.tool.js';
|
|
|
38
38
|
import { executeTool } from '../tools/execute.tool.js';
|
|
39
39
|
import { introspectTool } from '../tools/introspect.tool.js';
|
|
40
40
|
import { planTool } from '../tools/plan.tool.js';
|
|
41
|
+
import { validateTool } from '../tools/validate.tool.js';
|
|
41
42
|
toolRegistry.register('plan', {
|
|
42
43
|
schema: planTool,
|
|
43
44
|
instructionsPath: 'config/PLAN.md',
|
|
@@ -58,3 +59,7 @@ toolRegistry.register('execute', {
|
|
|
58
59
|
schema: executeTool,
|
|
59
60
|
instructionsPath: 'config/EXECUTE.md',
|
|
60
61
|
});
|
|
62
|
+
toolRegistry.register('validate', {
|
|
63
|
+
schema: validateTool,
|
|
64
|
+
instructionsPath: 'config/VALIDATE.md',
|
|
65
|
+
});
|