prompt-language-shell 0.8.2 → 0.8.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.
Files changed (45) hide show
  1. package/dist/configuration/io.js +85 -0
  2. package/dist/configuration/messages.js +30 -0
  3. package/dist/configuration/schema.js +167 -0
  4. package/dist/configuration/transformation.js +55 -0
  5. package/dist/configuration/types.js +30 -0
  6. package/dist/configuration/validation.js +52 -0
  7. package/dist/execution/handlers.js +135 -0
  8. package/dist/execution/processing.js +35 -0
  9. package/dist/execution/reducer.js +148 -0
  10. package/dist/execution/types.js +12 -0
  11. package/dist/execution/validation.js +12 -0
  12. package/dist/index.js +1 -1
  13. package/dist/services/anthropic.js +43 -56
  14. package/dist/services/colors.js +2 -1
  15. package/dist/services/components.js +40 -24
  16. package/dist/services/config-labels.js +15 -15
  17. package/dist/services/filesystem.js +114 -0
  18. package/dist/services/loader.js +8 -5
  19. package/dist/services/logger.js +26 -1
  20. package/dist/services/messages.js +32 -1
  21. package/dist/services/parser.js +3 -1
  22. package/dist/services/refinement.js +10 -10
  23. package/dist/services/router.js +43 -27
  24. package/dist/services/skills.js +12 -11
  25. package/dist/services/validator.js +4 -3
  26. package/dist/types/guards.js +4 -6
  27. package/dist/types/handlers.js +1 -0
  28. package/dist/types/schemas.js +103 -0
  29. package/dist/types/types.js +1 -0
  30. package/dist/ui/Answer.js +38 -16
  31. package/dist/ui/Command.js +48 -22
  32. package/dist/ui/Component.js +147 -33
  33. package/dist/ui/Config.js +69 -78
  34. package/dist/ui/Confirm.js +34 -21
  35. package/dist/ui/Execute.js +151 -178
  36. package/dist/ui/Feedback.js +1 -0
  37. package/dist/ui/Introspect.js +54 -25
  38. package/dist/ui/Label.js +1 -1
  39. package/dist/ui/Main.js +10 -6
  40. package/dist/ui/Refinement.js +8 -1
  41. package/dist/ui/Schedule.js +76 -53
  42. package/dist/ui/Validate.js +77 -77
  43. package/dist/ui/Workflow.js +60 -61
  44. package/package.json +3 -2
  45. package/dist/services/configuration.js +0 -409
@@ -0,0 +1,114 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'fs';
2
+ import { dirname } from 'path';
3
+ /**
4
+ * Real filesystem implementation using Node's fs module
5
+ */
6
+ export class RealFileSystem {
7
+ exists(path) {
8
+ return existsSync(path);
9
+ }
10
+ readFile(path, encoding) {
11
+ return readFileSync(path, encoding);
12
+ }
13
+ writeFile(path, data) {
14
+ writeFileSync(path, data, 'utf-8');
15
+ }
16
+ readDirectory(path) {
17
+ return readdirSync(path);
18
+ }
19
+ createDirectory(path, options) {
20
+ mkdirSync(path, options);
21
+ }
22
+ }
23
+ /**
24
+ * In-memory filesystem implementation for testing
25
+ * Simulates filesystem behavior without touching disk
26
+ */
27
+ export class MemoryFileSystem {
28
+ files = new Map();
29
+ directories = new Set();
30
+ exists(path) {
31
+ return this.files.has(path) || this.directories.has(path);
32
+ }
33
+ readFile(path, _encoding) {
34
+ const content = this.files.get(path);
35
+ if (content === undefined) {
36
+ throw new Error(`ENOENT: no such file or directory, open '${path}'`);
37
+ }
38
+ return content;
39
+ }
40
+ writeFile(path, data) {
41
+ // Auto-create parent directories
42
+ const dir = dirname(path);
43
+ if (dir !== '.' && dir !== path) {
44
+ this.createDirectory(dir, { recursive: true });
45
+ }
46
+ this.files.set(path, data);
47
+ }
48
+ readDirectory(path) {
49
+ if (!this.directories.has(path)) {
50
+ throw new Error(`ENOENT: no such file or directory, scandir '${path}'`);
51
+ }
52
+ const results = [];
53
+ const prefix = path.endsWith('/') ? path : `${path}/`;
54
+ // Find all direct children (files and directories)
55
+ for (const filePath of this.files.keys()) {
56
+ if (filePath.startsWith(prefix)) {
57
+ const relative = filePath.slice(prefix.length);
58
+ const firstSlash = relative.indexOf('/');
59
+ if (firstSlash === -1) {
60
+ // Direct file child
61
+ results.push(relative);
62
+ }
63
+ }
64
+ }
65
+ for (const dirPath of this.directories) {
66
+ if (dirPath.startsWith(prefix) && dirPath !== path) {
67
+ const relative = dirPath.slice(prefix.length);
68
+ const firstSlash = relative.indexOf('/');
69
+ if (firstSlash === -1) {
70
+ // Direct directory child
71
+ results.push(relative);
72
+ }
73
+ }
74
+ }
75
+ return results;
76
+ }
77
+ createDirectory(path, options) {
78
+ if (options?.recursive) {
79
+ // Create all parent directories
80
+ const parts = path.split('/').filter((p) => p);
81
+ let current = path.startsWith('/') ? '/' : '';
82
+ for (const part of parts) {
83
+ current = current === '/' ? `/${part}` : `${current}/${part}`;
84
+ this.directories.add(current);
85
+ }
86
+ }
87
+ else {
88
+ // Non-recursive: parent must exist
89
+ const parent = dirname(path);
90
+ if (parent !== '.' && parent !== path && !this.directories.has(parent)) {
91
+ throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`);
92
+ }
93
+ this.directories.add(path);
94
+ }
95
+ }
96
+ /**
97
+ * Clear all files and directories (useful for test cleanup)
98
+ */
99
+ clear() {
100
+ this.files.clear();
101
+ this.directories.clear();
102
+ }
103
+ /**
104
+ * Get all files for debugging
105
+ */
106
+ getFiles() {
107
+ return new Map(this.files);
108
+ }
109
+ }
110
+ /**
111
+ * Default filesystem instance (uses real fs)
112
+ * Services can accept optional FileSystem parameter for testing
113
+ */
114
+ export const defaultFileSystem = new RealFileSystem();
@@ -1,24 +1,27 @@
1
- import { existsSync, readFileSync } from 'fs';
2
1
  import { homedir } from 'os';
3
2
  import { join } from 'path';
4
3
  import YAML from 'yaml';
4
+ import { defaultFileSystem } from './filesystem.js';
5
+ import { displayWarning } from './logger.js';
5
6
  /**
6
7
  * Load user config from ~/.plsrc
7
8
  */
8
- export function loadUserConfig() {
9
+ export function loadUserConfig(fs = defaultFileSystem) {
9
10
  const configPath = join(homedir(), '.plsrc');
10
- if (!existsSync(configPath)) {
11
+ if (!fs.exists(configPath)) {
11
12
  return {};
12
13
  }
13
14
  try {
14
- const content = readFileSync(configPath, 'utf-8');
15
+ const content = fs.readFile(configPath, 'utf-8');
15
16
  const parsed = YAML.parse(content);
16
17
  if (parsed && typeof parsed === 'object') {
17
18
  return parsed;
18
19
  }
20
+ displayWarning('User config file exists but is not a valid object');
19
21
  return {};
20
22
  }
21
- catch {
23
+ catch (error) {
24
+ displayWarning('Failed to load user config', error);
22
25
  return {};
23
26
  }
24
27
  }
@@ -1,11 +1,16 @@
1
+ import { DebugLevel } from '../configuration/types.js';
1
2
  import { createDebugDefinition } from './components.js';
2
- import { DebugLevel, loadDebugSetting } from './configuration.js';
3
+ import { loadDebugSetting } from '../configuration/io.js';
3
4
  import { Palette } from './colors.js';
4
5
  /**
5
6
  * Debug logger for the application
6
7
  * Logs information based on the current debug level setting
7
8
  */
8
9
  let currentDebugLevel = DebugLevel.None;
10
+ /**
11
+ * Accumulated warnings to be displayed in the timeline
12
+ */
13
+ const warnings = [];
9
14
  /**
10
15
  * Initialize the logger with the current debug level from config
11
16
  */
@@ -24,6 +29,26 @@ export function setDebugLevel(debug) {
24
29
  export function getDebugLevel() {
25
30
  return currentDebugLevel;
26
31
  }
32
+ /**
33
+ * Store a warning message to be displayed in the timeline
34
+ * Only stores warnings at Info or Verbose debug levels
35
+ */
36
+ export function displayWarning(message, error) {
37
+ if (currentDebugLevel === DebugLevel.None) {
38
+ return;
39
+ }
40
+ const errorDetails = error instanceof Error ? `: ${error.message}` : '';
41
+ warnings.push(`${message}${errorDetails}`);
42
+ }
43
+ /**
44
+ * Get all accumulated warnings and clear the list
45
+ * Returns array of warning messages
46
+ */
47
+ export function getWarnings() {
48
+ const result = [...warnings];
49
+ warnings.length = 0;
50
+ return result;
51
+ }
27
52
  /**
28
53
  * Create debug component for system prompts sent to the LLM
29
54
  * Only creates at Verbose level
@@ -1,4 +1,5 @@
1
- import { DebugLevel, loadDebugSetting } from './configuration.js';
1
+ import { DebugLevel } from '../configuration/types.js';
2
+ import { loadDebugSetting } from '../configuration/io.js';
2
3
  export { formatDuration } from './utils.js';
3
4
  /**
4
5
  * Returns a natural language confirmation message for plan execution.
@@ -80,6 +81,36 @@ export function getMixedTaskTypesError(types) {
80
81
  const typeList = types.join(', ');
81
82
  return `Mixed task types are not supported. Found: ${typeList}. All tasks in a plan must have the same type.`;
82
83
  }
84
+ /**
85
+ * Returns a message for unresolved placeholders/missing configuration.
86
+ * Each message has two sentences: what's missing + what will be done.
87
+ * Both sentences are randomly selected independently for variety.
88
+ * Supports singular and plural forms.
89
+ */
90
+ export function getUnresolvedPlaceholdersMessage(count) {
91
+ const plural = count === 1 ? '' : 's';
92
+ const it = count === 1 ? 'it' : 'them';
93
+ const valueWord = count === 1 ? 'value' : 'values';
94
+ // First sentence: what's missing
95
+ const firstSentences = [
96
+ `Missing configuration ${valueWord} detected.`,
97
+ `Configuration ${valueWord} needed.`,
98
+ `Found unresolved placeholder${plural}.`,
99
+ `Additional configuration ${valueWord} required.`,
100
+ `Setup requires configuration ${valueWord}.`,
101
+ ];
102
+ // Second sentence: what will be done
103
+ const secondSentences = [
104
+ `Let me gather ${it} now.`,
105
+ `I'll set ${it} up for you.`,
106
+ `Let me configure ${it} first.`,
107
+ `I'll help you provide ${it}.`,
108
+ `Let me collect ${it} from you.`,
109
+ ];
110
+ const first = firstSentences[Math.floor(Math.random() * firstSentences.length)];
111
+ const second = secondSentences[Math.floor(Math.random() * secondSentences.length)];
112
+ return `${first} ${second}`;
113
+ }
83
114
  /**
84
115
  * Feedback messages for various operations
85
116
  */
@@ -1,4 +1,5 @@
1
1
  import YAML from 'yaml';
2
+ import { displayWarning } from './logger.js';
2
3
  /**
3
4
  * Validate a skill without parsing it fully
4
5
  * Returns validation error if skill is invalid, null if valid
@@ -188,7 +189,8 @@ function parseConfigSchema(content) {
188
189
  }
189
190
  return parsed;
190
191
  }
191
- catch {
192
+ catch (error) {
193
+ displayWarning('Failed to parse config schema in skill', error);
192
194
  return undefined;
193
195
  }
194
196
  }
@@ -5,12 +5,12 @@ import { routeTasksWithConfirm } from './router.js';
5
5
  * Handle refinement flow for DEFINE tasks
6
6
  * Called when user selects options from a plan with DEFINE tasks
7
7
  */
8
- export async function handleRefinement(selectedTasks, service, originalCommand, handlers) {
8
+ export async function handleRefinement(selectedTasks, service, originalCommand, lifecycleHandlers, workflowHandlers, requestHandlers) {
9
9
  // Create and add refinement component to queue
10
10
  const refinementDef = createRefinement(getRefiningMessage(), (operation) => {
11
- handlers.onAborted(operation);
11
+ requestHandlers.onAborted(operation);
12
12
  });
13
- handlers.addToQueue(refinementDef);
13
+ workflowHandlers.addToQueue(refinementDef);
14
14
  try {
15
15
  // Build refined command from selected tasks
16
16
  const refinedCommand = selectedTasks
@@ -22,19 +22,19 @@ export async function handleRefinement(selectedTasks, service, originalCommand,
22
22
  .join(', ');
23
23
  // Call LLM to refine plan with selected tasks
24
24
  const refinedResult = await service.processWithTool(refinedCommand, 'schedule');
25
- // Complete the Refinement component
26
- handlers.completeActive();
25
+ // Complete the Refinement component with success state
26
+ lifecycleHandlers.completeActive();
27
27
  // Add debug components to timeline if present
28
- if (refinedResult.debug && refinedResult.debug.length > 0) {
29
- handlers.addToTimeline(...refinedResult.debug);
28
+ if (refinedResult.debug?.length) {
29
+ workflowHandlers.addToTimeline(...refinedResult.debug);
30
30
  }
31
31
  // Route refined tasks to appropriate components
32
- routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false // No DEFINE tasks in refined result
32
+ routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, lifecycleHandlers, workflowHandlers, requestHandlers, false // No DEFINE tasks in refined result
33
33
  );
34
34
  }
35
35
  catch (err) {
36
- handlers.completeActive();
36
+ lifecycleHandlers.completeActive();
37
37
  const errorMessage = formatErrorMessage(err);
38
- handlers.onError(errorMessage);
38
+ requestHandlers.onError(errorMessage);
39
39
  }
40
40
  }
@@ -1,7 +1,10 @@
1
1
  import { asScheduledTasks } from '../types/guards.js';
2
2
  import { FeedbackType, TaskType } from '../types/types.js';
3
+ import { saveConfig } from '../configuration/io.js';
4
+ import { getConfigSchema } from '../configuration/schema.js';
5
+ import { unflattenConfig } from '../configuration/transformation.js';
6
+ import { saveConfigLabels } from './config-labels.js';
3
7
  import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDefinition, createExecuteDefinition, createFeedback, createIntrospectDefinition, createMessage, createScheduleDefinition, createValidateDefinition, } from './components.js';
4
- import { saveConfig, unflattenConfig } from './configuration.js';
5
8
  import { getCancellationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
6
9
  import { validateExecuteTasks } from './validator.js';
7
10
  /**
@@ -20,7 +23,7 @@ export function getOperationName(tasks) {
20
23
  * Route tasks to appropriate components with Confirm flow
21
24
  * Handles the complete flow: Plan → Confirm → Execute/Answer/Introspect
22
25
  */
23
- export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false) {
26
+ export function routeTasksWithConfirm(tasks, message, service, userRequest, lifecycleHandlers, workflowHandlers, requestHandlers, hasDefineTask = false) {
24
27
  if (tasks.length === 0)
25
28
  return;
26
29
  // Filter out ignore and discard tasks early
@@ -28,7 +31,7 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, hand
28
31
  // Check if no valid tasks remain after filtering
29
32
  if (validTasks.length === 0) {
30
33
  const message = createMessage(getUnknownRequestMessage());
31
- handlers.addToQueue(message);
34
+ workflowHandlers.addToQueue(message);
32
35
  return;
33
36
  }
34
37
  const operation = getOperationName(validTasks);
@@ -36,7 +39,7 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, hand
36
39
  // Has DEFINE tasks - add Schedule to queue for user selection
37
40
  // Refinement flow will call this function again with refined tasks
38
41
  const scheduleDefinition = createScheduleDefinition(message, validTasks);
39
- handlers.addToQueue(scheduleDefinition);
42
+ workflowHandlers.addToQueue(scheduleDefinition);
40
43
  }
41
44
  else {
42
45
  // No DEFINE tasks - Schedule auto-completes and adds Confirm to queue
@@ -47,17 +50,17 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, hand
47
50
  // Schedule completed - add Confirm to queue
48
51
  const confirmDefinition = createConfirmDefinition(() => {
49
52
  // User confirmed - complete both Confirm and Schedule, then route to appropriate component
50
- handlers.completeActiveAndPending();
51
- executeTasksAfterConfirm(validTasks, service, userRequest, handlers);
53
+ lifecycleHandlers.completeActiveAndPending();
54
+ executeTasksAfterConfirm(validTasks, service, userRequest, workflowHandlers, requestHandlers);
52
55
  }, () => {
53
56
  // User cancelled - complete both Confirm and Schedule, then show cancellation
54
- handlers.completeActiveAndPending();
57
+ lifecycleHandlers.completeActiveAndPending();
55
58
  const message = getCancellationMessage(operation);
56
- handlers.addToQueue(createFeedback(FeedbackType.Aborted, message));
59
+ workflowHandlers.addToQueue(createFeedback(FeedbackType.Aborted, message));
57
60
  });
58
- handlers.addToQueue(confirmDefinition);
61
+ workflowHandlers.addToQueue(confirmDefinition);
59
62
  });
60
- handlers.addToQueue(scheduleDefinition);
63
+ workflowHandlers.addToQueue(scheduleDefinition);
61
64
  }
62
65
  }
63
66
  /**
@@ -88,13 +91,13 @@ function validateTaskTypes(tasks) {
88
91
  * Validates task types and routes each type appropriately
89
92
  * Supports mixed types at top level with Groups
90
93
  */
91
- function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
94
+ function executeTasksAfterConfirm(tasks, service, userRequest, workflowHandlers, requestHandlers) {
92
95
  // Validate task types (Groups must have uniform subtasks)
93
96
  try {
94
97
  validateTaskTypes(tasks);
95
98
  }
96
99
  catch (error) {
97
- handlers.onError(error instanceof Error ? error.message : String(error));
100
+ requestHandlers.onError(error instanceof Error ? error.message : String(error));
98
101
  return;
99
102
  }
100
103
  const scheduledTasks = asScheduledTasks(tasks);
@@ -117,7 +120,7 @@ function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
117
120
  const taskType = type;
118
121
  if (typeTasks.length === 0)
119
122
  continue;
120
- routeTasksByType(taskType, typeTasks, service, userRequest, handlers);
123
+ routeTasksByType(taskType, typeTasks, service, userRequest, workflowHandlers, requestHandlers);
121
124
  }
122
125
  consecutiveStandaloneTasks = [];
123
126
  };
@@ -130,7 +133,7 @@ function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
130
133
  if (task.subtasks.length > 0) {
131
134
  const subtasks = task.subtasks;
132
135
  const taskType = subtasks[0].type;
133
- routeTasksByType(taskType, subtasks, service, userRequest, handlers);
136
+ routeTasksByType(taskType, subtasks, service, userRequest, workflowHandlers, requestHandlers);
134
137
  }
135
138
  }
136
139
  else {
@@ -145,22 +148,35 @@ function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
145
148
  * Route tasks by type to appropriate components
146
149
  * Extracted to allow reuse for both Groups and standalone tasks
147
150
  */
148
- function routeTasksByType(taskType, typeTasks, service, userRequest, handlers) {
151
+ function routeTasksByType(taskType, typeTasks, service, userRequest, workflowHandlers, requestHandlers) {
149
152
  if (taskType === TaskType.Answer) {
150
153
  // Create separate Answer component for each question
151
154
  for (const task of typeTasks) {
152
- handlers.addToQueue(createAnswerDefinition(task.action, service));
155
+ workflowHandlers.addToQueue(createAnswerDefinition(task.action, service));
153
156
  }
154
157
  }
155
158
  else if (taskType === TaskType.Introspect) {
156
- handlers.addToQueue(createIntrospectDefinition(typeTasks, service));
159
+ workflowHandlers.addToQueue(createIntrospectDefinition(typeTasks, service));
157
160
  }
158
161
  else if (taskType === TaskType.Config) {
159
- // Route to Config flow - extract keys from task params
162
+ // Route to Config flow - extract keys and descriptions from task params
160
163
  const configKeys = typeTasks
161
164
  .map((task) => task.params?.key)
162
165
  .filter((key) => key !== undefined);
163
- handlers.addToQueue(createConfigDefinitionWithKeys(configKeys, (config) => {
166
+ // Extract and cache labels from task descriptions
167
+ // Only cache labels for dynamically discovered keys (not in schema)
168
+ const schema = getConfigSchema();
169
+ const labels = {};
170
+ for (const task of typeTasks) {
171
+ const key = task.params?.key;
172
+ if (key && task.action && !(key in schema)) {
173
+ labels[key] = task.action;
174
+ }
175
+ }
176
+ if (Object.keys(labels).length > 0) {
177
+ saveConfigLabels(labels);
178
+ }
179
+ workflowHandlers.addToQueue(createConfigDefinitionWithKeys(configKeys, (config) => {
164
180
  // Save config - Config component will handle completion and feedback
165
181
  try {
166
182
  // Convert flat dotted keys to nested structure grouped by section
@@ -177,7 +193,7 @@ function routeTasksByType(taskType, typeTasks, service, userRequest, handlers) {
177
193
  throw new Error(errorMessage);
178
194
  }
179
195
  }, (operation) => {
180
- handlers.onAborted(operation);
196
+ requestHandlers.onAborted(operation);
181
197
  }));
182
198
  }
183
199
  else if (taskType === TaskType.Execute) {
@@ -192,26 +208,26 @@ function routeTasksByType(taskType, typeTasks, service, userRequest, handlers) {
192
208
  .join('\n');
193
209
  return `Invalid skill definition "${error.skill}":\n\n${issuesList}`;
194
210
  });
195
- handlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessages.join('\n\n')));
211
+ workflowHandlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessages.join('\n\n')));
196
212
  }
197
213
  else if (validation.missingConfig.length > 0) {
198
- handlers.addToQueue(createValidateDefinition(validation.missingConfig, userRequest, service, (error) => {
199
- handlers.onError(error);
214
+ workflowHandlers.addToQueue(createValidateDefinition(validation.missingConfig, userRequest, service, (error) => {
215
+ requestHandlers.onError(error);
200
216
  }, () => {
201
- handlers.addToQueue(createExecuteDefinition(typeTasks, service));
217
+ workflowHandlers.addToQueue(createExecuteDefinition(typeTasks, service));
202
218
  }, (operation) => {
203
- handlers.onAborted(operation);
219
+ requestHandlers.onAborted(operation);
204
220
  }));
205
221
  }
206
222
  else {
207
- handlers.addToQueue(createExecuteDefinition(typeTasks, service));
223
+ workflowHandlers.addToQueue(createExecuteDefinition(typeTasks, service));
208
224
  }
209
225
  }
210
226
  catch (error) {
211
227
  // Handle skill reference errors (e.g., unknown skills)
212
228
  const errorMessage = error instanceof Error ? error.message : String(error);
213
229
  const message = createMessage(errorMessage);
214
- handlers.addToQueue(message);
230
+ workflowHandlers.addToQueue(message);
215
231
  }
216
232
  }
217
233
  }
@@ -1,6 +1,7 @@
1
- import { existsSync, readdirSync, readFileSync } from 'fs';
2
1
  import { homedir } from 'os';
3
2
  import { join } from 'path';
3
+ import { defaultFileSystem } from './filesystem.js';
4
+ import { displayWarning } from './logger.js';
4
5
  import { getUnknownSkillMessage } from './messages.js';
5
6
  import { parseSkillMarkdown, displayNameToKey } from './parser.js';
6
7
  /**
@@ -48,14 +49,14 @@ export function getSkillsDirectory() {
48
49
  * Returns an array of objects with filename (key) and content
49
50
  * Filters out invalid filenames and conflicts with system skills
50
51
  */
51
- export function loadSkills() {
52
+ export function loadSkills(fs = defaultFileSystem) {
52
53
  const skillsDir = getSkillsDirectory();
53
54
  // Return empty array if directory doesn't exist
54
- if (!existsSync(skillsDir)) {
55
+ if (!fs.exists(skillsDir)) {
55
56
  return [];
56
57
  }
57
58
  try {
58
- const files = readdirSync(skillsDir);
59
+ const files = fs.readDirectory(skillsDir);
59
60
  // Filter and map valid skill files
60
61
  return files
61
62
  .filter((file) => {
@@ -75,12 +76,12 @@ export function loadSkills() {
75
76
  // Extract key (filename without extension, handles both .md and .MD)
76
77
  const key = file.slice(0, -3);
77
78
  const filePath = join(skillsDir, file);
78
- const content = readFileSync(filePath, 'utf-8');
79
+ const content = fs.readFile(filePath, 'utf-8');
79
80
  return { key, content };
80
81
  });
81
82
  }
82
- catch {
83
- // Return empty array if there's any error reading the directory
83
+ catch (error) {
84
+ displayWarning('Failed to load skills directory', error);
84
85
  return [];
85
86
  }
86
87
  }
@@ -88,16 +89,16 @@ export function loadSkills() {
88
89
  * Load and parse all skill definitions
89
90
  * Returns structured skill definitions (including invalid skills)
90
91
  */
91
- export function loadSkillDefinitions() {
92
- const skills = loadSkills();
92
+ export function loadSkillDefinitions(fs = defaultFileSystem) {
93
+ const skills = loadSkills(fs);
93
94
  return skills.map(({ key, content }) => parseSkillMarkdown(key, content));
94
95
  }
95
96
  /**
96
97
  * Load skills and mark incomplete ones in their markdown
97
98
  * Returns array of skill markdown with status markers
98
99
  */
99
- export function loadSkillsWithValidation() {
100
- const skills = loadSkills();
100
+ export function loadSkillsWithValidation(fs = defaultFileSystem) {
101
+ const skills = loadSkills(fs);
101
102
  return skills.map(({ key, content }) => {
102
103
  const parsed = parseSkillMarkdown(key, content);
103
104
  // If skill is incomplete (either validation failed or needs more documentation), append (INCOMPLETE) to the name
@@ -1,17 +1,18 @@
1
+ import { defaultFileSystem } from './filesystem.js';
1
2
  import { loadUserConfig, hasConfigPath } from './loader.js';
2
3
  import { loadSkillDefinitions, createSkillLookup } from './skills.js';
3
4
  /**
4
5
  * Validate config requirements for execute tasks
5
6
  * Returns validation result with missing config and validation errors
6
7
  */
7
- export function validateExecuteTasks(tasks) {
8
- const userConfig = loadUserConfig();
8
+ export function validateExecuteTasks(tasks, fs = defaultFileSystem) {
9
+ const userConfig = loadUserConfig(fs);
9
10
  const missing = [];
10
11
  const seenPaths = new Set();
11
12
  const validationErrors = [];
12
13
  const seenSkills = new Set();
13
14
  // Load all skills (including invalid ones for validation)
14
- const parsedSkills = loadSkillDefinitions();
15
+ const parsedSkills = loadSkillDefinitions(fs);
15
16
  const skillLookup = createSkillLookup(parsedSkills);
16
17
  // Check for invalid skills being used in tasks
17
18
  for (const task of tasks) {
@@ -1,4 +1,5 @@
1
1
  import { TaskType } from './types.js';
2
+ import { TaskSchema } from './schemas.js';
2
3
  /**
3
4
  * Type guard to check if a task is a ScheduledTask
4
5
  * ScheduledTask has optional subtasks property or is a Group type
@@ -14,12 +15,9 @@ export function asScheduledTasks(tasks) {
14
15
  return tasks;
15
16
  }
16
17
  /**
17
- * Type guard to check if a value is a valid Task
18
+ * Type guard to check if a value is a valid Task.
19
+ * Uses Zod schema for comprehensive runtime validation.
18
20
  */
19
21
  export function isTask(value) {
20
- return (typeof value === 'object' &&
21
- value !== null &&
22
- 'action' in value &&
23
- typeof value.action === 'string' &&
24
- 'type' in value);
22
+ return TaskSchema.safeParse(value).success;
25
23
  }
@@ -0,0 +1 @@
1
+ export {};