prompt-language-shell 0.4.9 → 0.5.0
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/dist/config/INTROSPECT.md +9 -6
- 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 +5 -7
- package/dist/ui/Validate.js +120 -0
- package/package.json +1 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const validateTool = {
|
|
2
|
+
name: 'validate',
|
|
3
|
+
description: 'Validate skill requirements and generate natural language descriptions for missing configuration values. Given skill context and missing config paths, create CONFIG tasks with helpful, contextual descriptions.',
|
|
4
|
+
input_schema: {
|
|
5
|
+
type: 'object',
|
|
6
|
+
properties: {
|
|
7
|
+
message: {
|
|
8
|
+
type: 'string',
|
|
9
|
+
description: 'Empty string or brief message (not shown to user, can be left empty)',
|
|
10
|
+
},
|
|
11
|
+
tasks: {
|
|
12
|
+
type: 'array',
|
|
13
|
+
description: 'Array of CONFIG tasks with natural language descriptions for missing config values',
|
|
14
|
+
items: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
action: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Natural language description explaining what the config value is for, followed by the config path in curly brackets {config.path}. Example: "Path to Alpha project repository (legacy implementation) {project.alpha.repo}"',
|
|
20
|
+
},
|
|
21
|
+
type: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Must be "config" for all tasks returned by this tool',
|
|
24
|
+
},
|
|
25
|
+
params: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
description: 'Must include key field with the config path',
|
|
28
|
+
properties: {
|
|
29
|
+
key: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The config path (e.g., "opera.gx.repo")',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['key'],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['action', 'type', 'params'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ['message', 'tasks'],
|
|
42
|
+
},
|
|
43
|
+
};
|
package/dist/types/types.js
CHANGED
|
@@ -13,6 +13,7 @@ export var ComponentName;
|
|
|
13
13
|
ComponentName["Answer"] = "answer";
|
|
14
14
|
ComponentName["AnswerDisplay"] = "answerDisplay";
|
|
15
15
|
ComponentName["Execute"] = "execute";
|
|
16
|
+
ComponentName["Validate"] = "validate";
|
|
16
17
|
})(ComponentName || (ComponentName = {}));
|
|
17
18
|
export var TaskType;
|
|
18
19
|
(function (TaskType) {
|
package/dist/ui/Answer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Box, Text } from 'ink';
|
|
|
4
4
|
import { Colors, getTextColor } from '../services/colors.js';
|
|
5
5
|
import { useInput } from '../services/keyboard.js';
|
|
6
6
|
import { formatErrorMessage } from '../services/messages.js';
|
|
7
|
+
import { withMinimumTime } from '../services/timing.js';
|
|
7
8
|
import { Spinner } from './Spinner.js';
|
|
8
9
|
const MINIMUM_PROCESSING_TIME = 400;
|
|
9
10
|
export function Answer({ question, state, service, onError, onComplete, onAborted, }) {
|
|
@@ -30,13 +31,9 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
|
|
|
30
31
|
}
|
|
31
32
|
let mounted = true;
|
|
32
33
|
async function process(svc) {
|
|
33
|
-
const startTime = Date.now();
|
|
34
34
|
try {
|
|
35
|
-
// Call answer tool
|
|
36
|
-
const result = await svc.processWithTool(question, 'answer');
|
|
37
|
-
const elapsed = Date.now() - startTime;
|
|
38
|
-
const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
|
|
39
|
-
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
35
|
+
// Call answer tool with minimum processing time for UX polish
|
|
36
|
+
const result = await withMinimumTime(() => svc.processWithTool(question, 'answer'), MINIMUM_PROCESSING_TIME);
|
|
40
37
|
if (mounted) {
|
|
41
38
|
// Extract answer from result
|
|
42
39
|
const answer = result.answer || '';
|
|
@@ -45,9 +42,6 @@ export function Answer({ question, state, service, onError, onComplete, onAborte
|
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
catch (err) {
|
|
48
|
-
const elapsed = Date.now() - startTime;
|
|
49
|
-
const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
|
|
50
|
-
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
51
45
|
if (mounted) {
|
|
52
46
|
const errorMessage = formatErrorMessage(err);
|
|
53
47
|
setIsLoading(false);
|
package/dist/ui/Command.js
CHANGED
|
@@ -5,6 +5,7 @@ import { TaskType } from '../types/types.js';
|
|
|
5
5
|
import { Colors } from '../services/colors.js';
|
|
6
6
|
import { useInput } from '../services/keyboard.js';
|
|
7
7
|
import { formatErrorMessage } from '../services/messages.js';
|
|
8
|
+
import { ensureMinimumTime } from '../services/timing.js';
|
|
8
9
|
import { Spinner } from './Spinner.js';
|
|
9
10
|
const MIN_PROCESSING_TIME = 1000; // purely for visual effect
|
|
10
11
|
export function Command({ command, state, service, children, onError, onComplete, onAborted, }) {
|
|
@@ -42,18 +43,14 @@ export function Command({ command, state, service, children, onError, onComplete
|
|
|
42
43
|
// Call CONFIG tool to get specific config keys
|
|
43
44
|
result = await svc.processWithTool(query, 'config');
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
47
|
-
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
46
|
+
await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
|
|
48
47
|
if (mounted) {
|
|
49
48
|
setIsLoading(false);
|
|
50
49
|
onComplete?.(result.message, result.tasks);
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
catch (err) {
|
|
54
|
-
|
|
55
|
-
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
56
|
-
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
53
|
+
await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
|
|
57
54
|
if (mounted) {
|
|
58
55
|
const errorMessage = formatErrorMessage(err);
|
|
59
56
|
setIsLoading(false);
|
package/dist/ui/Component.js
CHANGED
|
@@ -13,6 +13,7 @@ import { Message } from './Message.js';
|
|
|
13
13
|
import { Plan } from './Plan.js';
|
|
14
14
|
import { Refinement } from './Refinement.js';
|
|
15
15
|
import { Report } from './Report.js';
|
|
16
|
+
import { Validate } from './Validate.js';
|
|
16
17
|
import { Welcome } from './Welcome.js';
|
|
17
18
|
export const Component = React.memo(function Component({ def, debug, }) {
|
|
18
19
|
switch (def.name) {
|
|
@@ -21,7 +22,7 @@ export const Component = React.memo(function Component({ def, debug, }) {
|
|
|
21
22
|
case ComponentName.Config: {
|
|
22
23
|
const props = def.props;
|
|
23
24
|
const state = def.state;
|
|
24
|
-
return _jsx(Config, { ...props, state: state });
|
|
25
|
+
return _jsx(Config, { ...props, state: state, debug: debug });
|
|
25
26
|
}
|
|
26
27
|
case ComponentName.Command: {
|
|
27
28
|
const props = def.props;
|
|
@@ -66,5 +67,10 @@ export const Component = React.memo(function Component({ def, debug, }) {
|
|
|
66
67
|
const state = def.state;
|
|
67
68
|
return _jsx(Execute, { ...props, state: state });
|
|
68
69
|
}
|
|
70
|
+
case ComponentName.Validate: {
|
|
71
|
+
const props = def.props;
|
|
72
|
+
const state = def.state;
|
|
73
|
+
return _jsx(Validate, { ...props, state: state });
|
|
74
|
+
}
|
|
69
75
|
}
|
|
70
76
|
});
|
package/dist/ui/Config.js
CHANGED
|
@@ -57,7 +57,7 @@ function SelectionStep({ options, selectedIndex, isCurrentStep, }) {
|
|
|
57
57
|
return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { dimColor: !isSelected || !isCurrentStep, bold: isSelected, children: option.label }) }, option.value));
|
|
58
58
|
}) }));
|
|
59
59
|
}
|
|
60
|
-
export function Config({ steps, state, onFinished, onAborted }) {
|
|
60
|
+
export function Config({ steps, state, debug, onFinished, onAborted }) {
|
|
61
61
|
const done = state?.done ?? false;
|
|
62
62
|
const [step, setStep] = React.useState(done ? steps.length : 0);
|
|
63
63
|
const [values, setValues] = React.useState(() => {
|
|
@@ -220,6 +220,6 @@ export function Config({ steps, state, onFinished, onAborted }) {
|
|
|
220
220
|
if (!shouldShow) {
|
|
221
221
|
return null;
|
|
222
222
|
}
|
|
223
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [
|
|
223
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: index === 0 ? 0 : 1, children: [_jsxs(Box, { children: [_jsx(Text, { children: stepConfig.description }), _jsx(Text, { children: ": " }), debug && stepConfig.path && (_jsxs(Text, { color: Colors.Type.Define, children: ['{', stepConfig.path, '}'] }))] }), _jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: Colors.Action.Select, dimColor: !isCurrentStep, children: ">" }), _jsx(Text, { children: " " }), renderStepInput(stepConfig, isCurrentStep)] })] }, stepConfig.key));
|
|
224
224
|
}) }));
|
|
225
225
|
}
|