prompt-language-shell 0.6.6 → 0.6.8

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.
@@ -1,7 +1,7 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { getAvailableConfigStructure, getConfiguredKeys, } from './configuration.js';
3
3
  import { formatSkillsForPrompt, loadSkillsWithValidation } from './skills.js';
4
- import { toolRegistry } from './tool-registry.js';
4
+ import { toolRegistry } from './registry.js';
5
5
  /**
6
6
  * Wraps text to ensure no line exceeds the specified width.
7
7
  * Breaks at word boundaries to maintain readability.
@@ -58,6 +58,21 @@ export function getUnknownRequestMessage() {
58
58
  ];
59
59
  return messages[Math.floor(Math.random() * messages.length)];
60
60
  }
61
+ /**
62
+ * Returns an error message for unknown skill references.
63
+ * Randomly selects from variations to sound natural.
64
+ * Skill name should be a verb with details (e.g., "Deploy Application")
65
+ */
66
+ export function getUnknownSkillMessage(skillName) {
67
+ const templates = [
68
+ `I don't know how to "${skillName}".`,
69
+ `I'm not familiar with the "${skillName}" command.`,
70
+ `I haven't learned how to "${skillName}" yet.`,
71
+ `I can't "${skillName}".`,
72
+ `I'm unable to "${skillName}".`,
73
+ ];
74
+ return templates[Math.floor(Math.random() * templates.length)];
75
+ }
61
76
  /**
62
77
  * Returns an error message for mixed task types.
63
78
  */
@@ -2,18 +2,13 @@ import YAML from 'yaml';
2
2
  /**
3
3
  * Validate a skill without parsing it fully
4
4
  * Returns validation error if skill is invalid, null if valid
5
+ * Note: Name section is optional - key from filename is used as fallback
5
6
  */
6
- export function validateSkill(content) {
7
+ export function validateSkillStructure(content, key) {
7
8
  const sections = extractSections(content);
8
- // Name is required for error reporting
9
- const skillName = sections.name || 'Unknown skill';
10
- // Check required sections
11
- if (!sections.name) {
12
- return {
13
- skillName,
14
- error: 'The skill file is missing a Name section',
15
- };
16
- }
9
+ // Use key for error reporting if name not present
10
+ const skillName = sections.name || key;
11
+ // Check required sections (Name is now optional)
17
12
  if (!sections.description) {
18
13
  return {
19
14
  skillName,
@@ -41,17 +36,40 @@ export function validateSkill(content) {
41
36
  }
42
37
  return null;
43
38
  }
39
+ /**
40
+ * Convert kebab-case key to Title Case display name
41
+ * Examples: "deploy-app" -> "Deploy App", "build-project-2" -> "Build Project 2"
42
+ */
43
+ function keyToDisplayName(key) {
44
+ return key
45
+ .split('-')
46
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
47
+ .join(' ');
48
+ }
49
+ /**
50
+ * Convert display name to kebab-case key
51
+ * Examples: "Deploy App" -> "deploy-app", "Navigate To Product" -> "navigate-to-product"
52
+ */
53
+ export function displayNameToKey(name) {
54
+ return name
55
+ .toLowerCase()
56
+ .replace(/\s+/g, '-')
57
+ .replace(/[^a-z0-9-]/g, '');
58
+ }
44
59
  /**
45
60
  * Parse a skill markdown file into structured definition
46
61
  */
47
- export function parseSkillMarkdown(content) {
62
+ export function parseSkillMarkdown(key, content) {
48
63
  const sections = extractSections(content);
49
- // Validate the skill
50
- const validationError = validateSkill(content);
64
+ // Determine display name: prefer Name section, otherwise derive from key
65
+ const displayName = sections.name || keyToDisplayName(key);
66
+ // Validate the skill (Name is no longer required since we have key)
67
+ const validationError = validateSkillStructure(content, key);
51
68
  // For invalid skills, return minimal definition with error
52
69
  if (validationError) {
53
70
  return {
54
- name: sections.name || 'Unknown skill',
71
+ key,
72
+ name: displayName,
55
73
  description: sections.description || '',
56
74
  steps: sections.steps || [],
57
75
  execution: sections.execution || [],
@@ -63,7 +81,8 @@ export function parseSkillMarkdown(content) {
63
81
  // Valid skill - all required fields are present (validation passed)
64
82
  const description = sections.description;
65
83
  const skill = {
66
- name: sections.name,
84
+ key,
85
+ name: displayName,
67
86
  description,
68
87
  steps: sections.steps,
69
88
  execution: sections.execution,
@@ -1,6 +1,6 @@
1
1
  import { createRefinement } from './components.js';
2
2
  import { formatErrorMessage, getRefiningMessage } from './messages.js';
3
- import { routeTasksWithConfirm } from './task-router.js';
3
+ import { routeTasksWithConfirm } from './router.js';
4
4
  /**
5
5
  * Handle refinement flow for DEFINE tasks
6
6
  * Called when user selects options from a plan with DEFINE tasks
@@ -39,27 +39,17 @@ 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
41
  import { validateTool } from '../tools/validate.tool.js';
42
- toolRegistry.register('plan', {
43
- schema: planTool,
44
- instructionsPath: 'config/PLAN.md',
45
- });
46
- toolRegistry.register('introspect', {
47
- schema: introspectTool,
48
- instructionsPath: 'config/INTROSPECT.md',
49
- });
50
- toolRegistry.register('answer', {
51
- schema: answerTool,
52
- instructionsPath: 'config/ANSWER.md',
53
- });
54
- toolRegistry.register('config', {
55
- schema: configTool,
56
- instructionsPath: 'config/CONFIG.md',
57
- });
58
- toolRegistry.register('execute', {
59
- schema: executeTool,
60
- instructionsPath: 'config/EXECUTE.md',
61
- });
62
- toolRegistry.register('validate', {
63
- schema: validateTool,
64
- instructionsPath: 'config/VALIDATE.md',
65
- });
42
+ const tools = {
43
+ answer: answerTool,
44
+ config: configTool,
45
+ execute: executeTool,
46
+ introspect: introspectTool,
47
+ plan: planTool,
48
+ validate: validateTool,
49
+ };
50
+ for (const [name, schema] of Object.entries(tools)) {
51
+ toolRegistry.register(name, {
52
+ schema,
53
+ instructionsPath: `skills/${name}.md`,
54
+ });
55
+ }
@@ -2,7 +2,7 @@ import { TaskType } from '../types/types.js';
2
2
  import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDefinition, createExecuteDefinition, createFeedback, createIntrospectDefinition, createMessage, createPlanDefinition, createValidateDefinition, } from './components.js';
3
3
  import { saveConfig, unflattenConfig } from './configuration.js';
4
4
  import { FeedbackType } from '../types/types.js';
5
- import { validateExecuteTasks } from './execution-validator.js';
5
+ import { validateExecuteTasks } from './validator.js';
6
6
  import { getCancellationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
7
7
  /**
8
8
  * Determine the operation name based on task types
@@ -125,28 +125,36 @@ function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
125
125
  }
126
126
  else {
127
127
  // Execute tasks with validation
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) => {
141
- handlers.onError(error);
142
- }, () => {
128
+ try {
129
+ const validation = validateExecuteTasks(tasks);
130
+ if (validation.validationErrors.length > 0) {
131
+ // Show error feedback for invalid skills
132
+ const errorMessages = validation.validationErrors.map((error) => {
133
+ const issuesList = error.issues
134
+ .map((issue) => ` - ${issue}`)
135
+ .join('\n');
136
+ return `Invalid skill definition "${error.skill}":\n\n${issuesList}`;
137
+ });
138
+ handlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessages.join('\n\n')));
139
+ }
140
+ else if (validation.missingConfig.length > 0) {
141
+ handlers.addToQueue(createValidateDefinition(validation.missingConfig, userRequest, service, (error) => {
142
+ handlers.onError(error);
143
+ }, () => {
144
+ handlers.addToQueue(createExecuteDefinition(tasks, service));
145
+ }, (operation) => {
146
+ handlers.onAborted(operation);
147
+ }));
148
+ }
149
+ else {
143
150
  handlers.addToQueue(createExecuteDefinition(tasks, service));
144
- }, (operation) => {
145
- handlers.onAborted(operation);
146
- }));
151
+ }
147
152
  }
148
- else {
149
- handlers.addToQueue(createExecuteDefinition(tasks, service));
153
+ catch (error) {
154
+ // Handle skill reference errors (e.g., unknown skills)
155
+ const errorMessage = error instanceof Error ? error.message : String(error);
156
+ const message = createMessage(errorMessage);
157
+ handlers.addToQueue(message);
150
158
  }
151
159
  }
152
160
  }
@@ -1,7 +1,42 @@
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
+ import { getUnknownSkillMessage } from './messages.js';
5
+ import { parseSkillMarkdown, displayNameToKey } from './parser.js';
6
+ /**
7
+ * Built-in skill names that user skills cannot override
8
+ */
9
+ const BUILT_IN_SKILLS = new Set([
10
+ 'plan',
11
+ 'execute',
12
+ 'answer',
13
+ 'config',
14
+ 'validate',
15
+ 'introspect',
16
+ ]);
17
+ /**
18
+ * Validate filename follows kebab-case pattern
19
+ * Valid: deploy-app.md, build-project-2.md, copy-files.md
20
+ * Invalid: Deploy_App.md, buildProject.md, DEPLOY.md, file name.md
21
+ */
22
+ export function isValidSkillFilename(filename) {
23
+ // Must end with .md or .MD extension
24
+ if (!filename.endsWith('.md') && !filename.endsWith('.MD')) {
25
+ return false;
26
+ }
27
+ // Extract name without extension
28
+ const name = filename.slice(0, -3);
29
+ // Must match kebab-case pattern: lowercase letters, numbers, and hyphens only
30
+ // Must start with a letter, and not start or end with a hyphen
31
+ const kebabCasePattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
32
+ return kebabCasePattern.test(name);
33
+ }
34
+ /**
35
+ * Check if skill key conflicts with built-in skills
36
+ */
37
+ export function conflictsWithBuiltIn(key) {
38
+ return BUILT_IN_SKILLS.has(key);
39
+ }
5
40
  /**
6
41
  * Get the path to the skills directory
7
42
  */
@@ -10,7 +45,8 @@ export function getSkillsDirectory() {
10
45
  }
11
46
  /**
12
47
  * Load all skill markdown files from the skills directory
13
- * Returns an array of skill file contents
48
+ * Returns an array of objects with filename (key) and content
49
+ * Filters out invalid filenames and conflicts with built-in skills
14
50
  */
15
51
  export function loadSkills() {
16
52
  const skillsDir = getSkillsDirectory();
@@ -20,12 +56,27 @@ export function loadSkills() {
20
56
  }
21
57
  try {
22
58
  const files = readdirSync(skillsDir);
23
- // Filter for markdown files
24
- const skillFiles = files.filter((file) => file.endsWith('.md') || file.endsWith('.MD'));
25
- // Read and return contents of each skill file
26
- return skillFiles.map((file) => {
59
+ // Filter and map valid skill files
60
+ return files
61
+ .filter((file) => {
62
+ // Must follow kebab-case naming convention
63
+ if (!isValidSkillFilename(file)) {
64
+ return false;
65
+ }
66
+ // Extract key (filename without extension, handles both .md and .MD)
67
+ const key = file.slice(0, -3);
68
+ // Must not conflict with built-in skills
69
+ if (conflictsWithBuiltIn(key)) {
70
+ return false;
71
+ }
72
+ return true;
73
+ })
74
+ .map((file) => {
75
+ // Extract key (filename without extension, handles both .md and .MD)
76
+ const key = file.slice(0, -3);
27
77
  const filePath = join(skillsDir, file);
28
- return readFileSync(filePath, 'utf-8');
78
+ const content = readFileSync(filePath, 'utf-8');
79
+ return { key, content };
29
80
  });
30
81
  }
31
82
  catch {
@@ -38,17 +89,17 @@ export function loadSkills() {
38
89
  * Returns structured skill definitions (including invalid skills)
39
90
  */
40
91
  export function loadSkillDefinitions() {
41
- const skillContents = loadSkills();
42
- return skillContents.map((content) => parseSkillMarkdown(content));
92
+ const skills = loadSkills();
93
+ return skills.map(({ key, content }) => parseSkillMarkdown(key, content));
43
94
  }
44
95
  /**
45
96
  * Load skills and mark incomplete ones in their markdown
46
97
  * Returns array of skill markdown with status markers
47
98
  */
48
99
  export function loadSkillsWithValidation() {
49
- const skillContents = loadSkills();
50
- return skillContents.map((content) => {
51
- const parsed = parseSkillMarkdown(content);
100
+ const skills = loadSkills();
101
+ return skills.map(({ key, content }) => {
102
+ const parsed = parseSkillMarkdown(key, content);
52
103
  // If skill is incomplete (either validation failed or needs more documentation), append (INCOMPLETE) to the name
53
104
  if (parsed.isIncomplete) {
54
105
  return content.replace(/^(#{1,6}\s+Name\s*\n+)(.+?)(\n|$)/im, `$1$2 (INCOMPLETE)$3`);
@@ -58,13 +109,19 @@ export function loadSkillsWithValidation() {
58
109
  }
59
110
  /**
60
111
  * Create skill lookup function from definitions
112
+ * Lookup by key (display name is converted to kebab-case for matching)
113
+ * Example: "Deploy App" -> "deploy-app" -> matches skill with key "deploy-app"
61
114
  */
62
115
  export function createSkillLookup(definitions) {
63
- const map = new Map();
116
+ const keyMap = new Map();
64
117
  for (const definition of definitions) {
65
- map.set(definition.name, definition);
118
+ keyMap.set(definition.key, definition);
66
119
  }
67
- return (name) => map.get(name) || null;
120
+ return (name) => {
121
+ // Convert display name to kebab-case key for lookup
122
+ const key = displayNameToKey(name);
123
+ return keyMap.get(key) || null;
124
+ };
68
125
  }
69
126
  /**
70
127
  * Format skills for inclusion in the planning prompt
@@ -93,3 +150,95 @@ brackets for additional information. Use commas instead. For example:
93
150
  const skillsContent = skills.join('\n\n');
94
151
  return header + skillsContent;
95
152
  }
153
+ /**
154
+ * Parse skill reference from execution line
155
+ * Format: [ Display Name ] with mandatory spaces
156
+ * Returns display name if line matches format, otherwise null
157
+ * Example: "[ My Skill ]" -> "My Skill"
158
+ */
159
+ export function parseSkillReference(line) {
160
+ // Must match format: [ content ] with at least one space before and after
161
+ const match = line.trim().match(/^\[\s+(.+?)\s+\]$/);
162
+ return match ? match[1] : null;
163
+ }
164
+ /**
165
+ * Check if execution line is a skill reference
166
+ * Must have format: [ content ] with spaces
167
+ */
168
+ export function isSkillReference(line) {
169
+ return /^\[\s+.+?\s+\]$/.test(line.trim());
170
+ }
171
+ /**
172
+ * Expand skill references in execution commands
173
+ * Returns expanded execution lines with references replaced
174
+ * Throws error if circular reference detected or skill not found
175
+ * Reference format: [ Skill Name ]
176
+ */
177
+ export function expandSkillReferences(execution, skillLookup, visited = new Set()) {
178
+ const expanded = [];
179
+ for (const line of execution) {
180
+ // First: Detect if line matches [ XXX ] format
181
+ const skillName = parseSkillReference(line);
182
+ if (!skillName) {
183
+ // Not a reference, keep command as-is
184
+ expanded.push(line);
185
+ continue;
186
+ }
187
+ // Check for circular reference
188
+ if (visited.has(skillName)) {
189
+ throw new Error(`Circular skill reference detected: ${Array.from(visited).join(' → ')} → ${skillName}`);
190
+ }
191
+ // Second: Match against skill name
192
+ const skill = skillLookup(skillName);
193
+ if (!skill) {
194
+ // Referenced skill not found - throw error to break execution
195
+ throw new Error(getUnknownSkillMessage(skillName));
196
+ }
197
+ // Recursively expand referenced skill's execution
198
+ const newVisited = new Set(visited);
199
+ newVisited.add(skillName);
200
+ const referencedExecution = expandSkillReferences(skill.execution, skillLookup, newVisited);
201
+ expanded.push(...referencedExecution);
202
+ }
203
+ return expanded;
204
+ }
205
+ /**
206
+ * Get all skill names referenced in execution (including nested)
207
+ * Returns unique set of skill names
208
+ */
209
+ export function getReferencedSkills(execution, skillLookup, visited = new Set()) {
210
+ const referenced = new Set();
211
+ for (const line of execution) {
212
+ const skillName = parseSkillReference(line);
213
+ if (!skillName || visited.has(skillName)) {
214
+ continue;
215
+ }
216
+ referenced.add(skillName);
217
+ const skill = skillLookup(skillName);
218
+ if (skill) {
219
+ const newVisited = new Set(visited);
220
+ newVisited.add(skillName);
221
+ const nested = getReferencedSkills(skill.execution, skillLookup, newVisited);
222
+ for (const name of nested) {
223
+ referenced.add(name);
224
+ }
225
+ }
226
+ }
227
+ return referenced;
228
+ }
229
+ /**
230
+ * Validate skill references don't form cycles
231
+ * Returns true if valid, false if circular reference detected
232
+ */
233
+ export function validateNoCycles(execution, skillLookup, visited = new Set()) {
234
+ try {
235
+ expandSkillReferences(execution, skillLookup, visited);
236
+ return true;
237
+ }
238
+ catch (error) {
239
+ if (error instanceof Error && error.message.includes('Circular')) {
240
+ return false;
241
+ }
242
+ throw error;
243
+ }
244
+ }
@@ -1,8 +1,7 @@
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';
1
+ import { extractPlaceholders, pathToString, resolveVariant, } from './resolver.js';
2
+ import { loadUserConfig, hasConfigPath } from './loader.js';
3
+ import { loadSkillDefinitions, createSkillLookup, expandSkillReferences, } from './skills.js';
4
+ import { getConfigType } from './parser.js';
6
5
  /**
7
6
  * Validate config requirements for execute tasks
8
7
  * Returns validation result with missing config and validation errors
@@ -14,9 +13,8 @@ export function validateExecuteTasks(tasks) {
14
13
  const validationErrors = [];
15
14
  const seenSkills = new Set();
16
15
  // Load all skills (including invalid ones for validation)
17
- const skillContents = loadSkills();
18
- const parsedSkills = skillContents.map((content) => parseSkillMarkdown(content));
19
- const skillLookup = (name) => parsedSkills.find((s) => s.name === name) || null;
16
+ const parsedSkills = loadSkillDefinitions();
17
+ const skillLookup = createSkillLookup(parsedSkills);
20
18
  // Check for invalid skills being used in tasks
21
19
  for (const task of tasks) {
22
20
  const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
@@ -8,7 +8,7 @@ import { createPlanDefinition } from '../services/components.js';
8
8
  import { formatErrorMessage } from '../services/messages.js';
9
9
  import { useInput } from '../services/keyboard.js';
10
10
  import { handleRefinement } from '../services/refinement.js';
11
- import { routeTasksWithConfirm } from '../services/task-router.js';
11
+ import { routeTasksWithConfirm } from '../services/router.js';
12
12
  import { ensureMinimumTime } from '../services/timing.js';
13
13
  import { Spinner } from './Spinner.js';
14
14
  import { UserQuery } from './UserQuery.js';
@@ -7,8 +7,8 @@ import { useInput } from '../services/keyboard.js';
7
7
  import { formatErrorMessage } from '../services/messages.js';
8
8
  import { formatDuration } from '../services/utils.js';
9
9
  import { ExecutionStatus, executeCommands, } from '../services/shell.js';
10
- import { replacePlaceholders } from '../services/placeholder-resolver.js';
11
- import { loadUserConfig } from '../services/config-loader.js';
10
+ import { replacePlaceholders } from '../services/resolver.js';
11
+ import { loadUserConfig } from '../services/loader.js';
12
12
  import { ensureMinimumTime } from '../services/timing.js';
13
13
  import { Spinner } from './Spinner.js';
14
14
  const MINIMUM_PROCESSING_TIME = 400;
@@ -128,9 +128,15 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
128
128
  for (const [section, sectionConfig] of Object.entries(configBySection)) {
129
129
  saveConfig(section, sectionConfig);
130
130
  }
131
+ // Mark validation component as complete before invoking callback
132
+ // This allows the workflow to proceed to execution
133
+ handlers?.completeActive();
134
+ // Invoke callback which will queue the Execute component
131
135
  onComplete?.(configRequirements);
132
136
  };
133
137
  const handleConfigAborted = (operation) => {
138
+ // Mark validation component as complete when aborted
139
+ handlers?.completeActive();
134
140
  onAborted(operation);
135
141
  };
136
142
  return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isActive && !completionMessage && !error && (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { color: getTextColor(isActive), children: ["Validating configuration requirements.", ' '] }), _jsx(Spinner, {})] })), completionMessage && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: getTextColor(isActive), children: completionMessage }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) })), configSteps && !error && (_jsx(Box, { marginTop: 1, children: _jsx(Config, { steps: configSteps, status: status, debug: debug, onFinished: handleConfigFinished, onAborted: handleConfigAborted, handlers: handlers }) })), children] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
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",
@@ -13,7 +13,7 @@
13
13
  "scripts": {
14
14
  "clean": "rm -rf dist",
15
15
  "typecheck": "tsc --project tsconfig.eslint.json",
16
- "build": "npm run typecheck && npm run clean && tsc && chmod +x dist/index.js && mkdir -p dist/config && cp src/config/*.md dist/config/",
16
+ "build": "npm run typecheck && npm run clean && tsc && chmod +x dist/index.js && mkdir -p dist/skills && cp src/skills/*.md dist/skills/",
17
17
  "dev": "npm run build && tsc --watch",
18
18
  "prepare": "husky",
19
19
  "prepublishOnly": "npm run check",
@@ -1,87 +0,0 @@
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
- // Recursively expand referenced skill's execution
41
- const newVisited = new Set(visited);
42
- newVisited.add(skillName);
43
- const referencedExecution = expandSkillReferences(skill.execution, skillLookup, newVisited);
44
- expanded.push(...referencedExecution);
45
- }
46
- return expanded;
47
- }
48
- /**
49
- * Get all skill names referenced in execution (including nested)
50
- * Returns unique set of skill names
51
- */
52
- export function getReferencedSkills(execution, skillLookup, visited = new Set()) {
53
- const referenced = new Set();
54
- for (const line of execution) {
55
- const skillName = parseSkillReference(line);
56
- if (!skillName || visited.has(skillName)) {
57
- continue;
58
- }
59
- referenced.add(skillName);
60
- const skill = skillLookup(skillName);
61
- if (skill) {
62
- const newVisited = new Set(visited);
63
- newVisited.add(skillName);
64
- const nested = getReferencedSkills(skill.execution, skillLookup, newVisited);
65
- for (const name of nested) {
66
- referenced.add(name);
67
- }
68
- }
69
- }
70
- return referenced;
71
- }
72
- /**
73
- * Validate skill references don't form cycles
74
- * Returns true if valid, false if circular reference detected
75
- */
76
- export function validateNoCycles(execution, skillLookup, visited = new Set()) {
77
- try {
78
- expandSkillReferences(execution, skillLookup, visited);
79
- return true;
80
- }
81
- catch (error) {
82
- if (error instanceof Error && error.message.includes('Circular')) {
83
- return false;
84
- }
85
- throw error;
86
- }
87
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes