prompt-language-shell 0.4.8 → 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.
Files changed (41) hide show
  1. package/dist/config/EXECUTE.md +279 -0
  2. package/dist/config/INTROSPECT.md +9 -6
  3. package/dist/config/PLAN.md +57 -6
  4. package/dist/config/VALIDATE.md +139 -0
  5. package/dist/handlers/answer.js +13 -20
  6. package/dist/handlers/command.js +26 -30
  7. package/dist/handlers/config.js +32 -24
  8. package/dist/handlers/execute.js +46 -0
  9. package/dist/handlers/execution.js +133 -81
  10. package/dist/handlers/introspect.js +13 -20
  11. package/dist/handlers/plan.js +31 -34
  12. package/dist/services/anthropic.js +28 -2
  13. package/dist/services/colors.js +3 -3
  14. package/dist/services/components.js +50 -1
  15. package/dist/services/config-loader.js +67 -0
  16. package/dist/services/execution-validator.js +110 -0
  17. package/dist/services/messages.js +1 -0
  18. package/dist/services/placeholder-resolver.js +120 -0
  19. package/dist/services/shell.js +118 -0
  20. package/dist/services/skill-expander.js +91 -0
  21. package/dist/services/skill-parser.js +169 -0
  22. package/dist/services/skills.js +26 -0
  23. package/dist/services/timing.js +38 -0
  24. package/dist/services/tool-registry.js +10 -0
  25. package/dist/services/utils.js +21 -0
  26. package/dist/tools/execute.tool.js +44 -0
  27. package/dist/tools/validate.tool.js +43 -0
  28. package/dist/types/handlers.js +1 -0
  29. package/dist/types/skills.js +4 -0
  30. package/dist/types/types.js +2 -0
  31. package/dist/ui/Answer.js +3 -9
  32. package/dist/ui/Command.js +3 -6
  33. package/dist/ui/Component.js +13 -1
  34. package/dist/ui/Config.js +2 -2
  35. package/dist/ui/Confirm.js +2 -2
  36. package/dist/ui/Execute.js +262 -0
  37. package/dist/ui/Introspect.js +5 -7
  38. package/dist/ui/Main.js +30 -69
  39. package/dist/ui/Spinner.js +10 -5
  40. package/dist/ui/Validate.js +120 -0
  41. package/package.json +7 -7
@@ -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 { ensureMinimumTime } from '../services/timing.js';
7
8
  import { Spinner } from './Spinner.js';
8
9
  const MIN_PROCESSING_TIME = 1000;
9
10
  const BUILT_IN_CAPABILITIES = new Set([
@@ -12,9 +13,10 @@ const BUILT_IN_CAPABILITIES = new Set([
12
13
  'INTROSPECT',
13
14
  'ANSWER',
14
15
  'EXECUTE',
16
+ 'VALIDATE',
15
17
  'REPORT',
16
18
  ]);
17
- const INDIRECT_CAPABILITIES = new Set(['PLAN', 'REPORT']);
19
+ const INDIRECT_CAPABILITIES = new Set(['PLAN', 'VALIDATE', 'REPORT']);
18
20
  function parseCapabilityFromTask(task) {
19
21
  // Parse "NAME: Description" format from task.action
20
22
  const colonIndex = task.action.indexOf(':');
@@ -69,9 +71,7 @@ export function Introspect({ tasks, state, service, children, debug = false, onE
69
71
  const introspectAction = tasks[0]?.action || 'list capabilities';
70
72
  // Call introspect tool
71
73
  const result = await svc.processWithTool(introspectAction, 'introspect');
72
- const elapsed = Date.now() - startTime;
73
- const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
74
- await new Promise((resolve) => setTimeout(resolve, remainingTime));
74
+ await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
75
75
  if (mounted) {
76
76
  // Parse capabilities from returned tasks
77
77
  let capabilities = result.tasks.map(parseCapabilityFromTask);
@@ -85,9 +85,7 @@ export function Introspect({ tasks, state, service, children, debug = false, onE
85
85
  }
86
86
  }
87
87
  catch (err) {
88
- const elapsed = Date.now() - startTime;
89
- const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
90
- await new Promise((resolve) => setTimeout(resolve, remainingTime));
88
+ await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
91
89
  if (mounted) {
92
90
  const errorMessage = formatErrorMessage(err);
93
91
  setIsLoading(false);
package/dist/ui/Main.js CHANGED
@@ -2,20 +2,20 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { FeedbackType } from '../types/types.js';
4
4
  import { createAnthropicService, } from '../services/anthropic.js';
5
+ import { createCommandDefinition, createConfigDefinition, createFeedback, createMessage, createWelcomeDefinition, isStateless, markAsDone, } from '../services/components.js';
5
6
  import { getConfigurationRequiredMessage, hasValidAnthropicKey, loadConfig, loadDebugSetting, saveDebugSetting, } from '../services/configuration.js';
6
7
  import { registerGlobalShortcut } from '../services/keyboard.js';
7
8
  import { getCancellationMessage } from '../services/messages.js';
8
- import { createCommandDefinition, createConfigDefinition, createFeedback, createMessage, createWelcomeDefinition, isStateless, markAsDone, } from '../services/components.js';
9
9
  import { exitApp } from '../services/process.js';
10
- import { createAnswerAbortedHandler, createAnswerCompleteHandler, createAnswerErrorHandler, } from '../handlers/answer.js';
11
- import { createCommandAbortedHandler, createCommandCompleteHandler, createCommandErrorHandler, } from '../handlers/command.js';
12
- import { createConfigAbortedHandler, createConfigFinishedHandler, } from '../handlers/config.js';
13
- import { createExecutionCancelledHandler, createExecutionConfirmedHandler, } from '../handlers/execution.js';
14
- import { createIntrospectAbortedHandler, createIntrospectCompleteHandler, createIntrospectErrorHandler, } from '../handlers/introspect.js';
15
- import { createPlanAbortedHandler, createPlanAbortHandlerFactory, createPlanSelectionConfirmedHandler, } from '../handlers/plan.js';
10
+ import { createAnswerHandlers } from '../handlers/answer.js';
11
+ import { createCommandHandlers } from '../handlers/command.js';
12
+ import { createConfigHandlers } from '../handlers/config.js';
13
+ import { createExecuteHandlers } from '../handlers/execute.js';
14
+ import { createExecutionHandlers } from '../handlers/execution.js';
15
+ import { createIntrospectHandlers } from '../handlers/introspect.js';
16
+ import { createPlanHandlers } from '../handlers/plan.js';
16
17
  import { Column } from './Column.js';
17
18
  export const Main = ({ app, command }) => {
18
- // Initialize service from existing config if available
19
19
  const [service, setService] = React.useState(() => {
20
20
  if (hasValidAnthropicKey()) {
21
21
  const config = loadConfig();
@@ -26,14 +26,8 @@ export const Main = ({ app, command }) => {
26
26
  const [timeline, setTimeline] = React.useState([]);
27
27
  const [queue, setQueue] = React.useState([]);
28
28
  const [isDebug, setIsDebug] = React.useState(() => loadDebugSetting());
29
- // Use ref to track latest timeline for callbacks
30
- const timelineRef = React.useRef(timeline);
31
- React.useEffect(() => {
32
- timelineRef.current = timeline;
33
- }, [timeline]);
34
29
  // Register global keyboard shortcuts
35
30
  React.useEffect(() => {
36
- // Shift+Tab: Toggle debug mode
37
31
  registerGlobalShortcut('shift+tab', () => {
38
32
  setIsDebug((prev) => {
39
33
  const newValue = !prev;
@@ -50,7 +44,6 @@ export const Main = ({ app, command }) => {
50
44
  if (currentQueue.length === 0)
51
45
  return currentQueue;
52
46
  const [first, ...rest] = currentQueue;
53
- // Stateless components auto-complete immediately
54
47
  if (isStateless(first)) {
55
48
  addToTimeline(first);
56
49
  return rest;
@@ -58,7 +51,7 @@ export const Main = ({ app, command }) => {
58
51
  return currentQueue;
59
52
  });
60
53
  }, [addToTimeline]);
61
- const handleCommandError = React.useCallback((error) => setQueue(createCommandErrorHandler(addToTimeline)(error)), [addToTimeline]);
54
+ // Core abort handler
62
55
  const handleAborted = React.useCallback((operationName) => {
63
56
  setQueue((currentQueue) => {
64
57
  if (currentQueue.length === 0)
@@ -71,86 +64,54 @@ export const Main = ({ app, command }) => {
71
64
  return [];
72
65
  });
73
66
  }, [addToTimeline]);
74
- const handleConfigAborted = React.useCallback(createConfigAbortedHandler(handleAborted), [handleAborted]);
75
- const handlePlanAborted = React.useCallback(createPlanAbortedHandler(handleAborted), [handleAborted]);
76
- const createPlanAbortHandler = React.useCallback(createPlanAbortHandlerFactory(handleAborted, handlePlanAborted), [handleAborted, handlePlanAborted]);
77
- const handleCommandAborted = React.useCallback(createCommandAbortedHandler(handleAborted), [handleAborted]);
78
- const handleRefinementAborted = React.useCallback(() => {
79
- handleAborted('Plan refinement');
80
- }, [handleAborted]);
81
- const handleIntrospectAborted = React.useCallback(createIntrospectAbortedHandler(handleAborted), [handleAborted]);
82
- const handleIntrospectError = React.useCallback((error) => setQueue(createIntrospectErrorHandler(addToTimeline)(error)), [addToTimeline]);
83
- const handleIntrospectComplete = React.useCallback((message, capabilities) => setQueue(createIntrospectCompleteHandler(addToTimeline)(message, capabilities)), [addToTimeline]);
84
- const handleAnswerAborted = React.useCallback(createAnswerAbortedHandler(handleAborted), [handleAborted]);
85
- const handleAnswerError = React.useCallback((error) => setQueue(createAnswerErrorHandler(addToTimeline)(error)), [addToTimeline]);
86
- const handleAnswerComplete = React.useCallback((answer) => setQueue(createAnswerCompleteHandler(addToTimeline)(answer)), [addToTimeline]);
87
- const handleExecutionConfirmed = React.useCallback(() => setQueue(createExecutionConfirmedHandler(timelineRef, addToTimeline, service, handleIntrospectError, handleIntrospectComplete, handleIntrospectAborted, handleAnswerError, handleAnswerComplete, handleAnswerAborted, setQueue)()), [
88
- addToTimeline,
89
- service,
90
- handleIntrospectError,
91
- handleIntrospectComplete,
92
- handleIntrospectAborted,
93
- handleAnswerError,
94
- handleAnswerComplete,
95
- handleAnswerAborted,
96
- ]);
97
- const handleExecutionCancelled = React.useCallback(() => setQueue(createExecutionCancelledHandler(timelineRef, addToTimeline)()), [addToTimeline]);
98
- const handlePlanSelectionConfirmed = React.useCallback(createPlanSelectionConfirmedHandler(addToTimeline, service, handleRefinementAborted, createPlanAbortHandler, handleExecutionConfirmed, handleExecutionCancelled, setQueue), [
67
+ // Create operations object
68
+ const ops = React.useMemo(() => ({
99
69
  addToTimeline,
70
+ setQueue,
100
71
  service,
101
- handleRefinementAborted,
102
- createPlanAbortHandler,
103
- handleExecutionConfirmed,
104
- handleExecutionCancelled,
105
- ]);
106
- const handleCommandComplete = React.useCallback((message, tasks) => setQueue(createCommandCompleteHandler(addToTimeline, createPlanAbortHandler, handlePlanSelectionConfirmed, handleExecutionConfirmed, handleExecutionCancelled)(message, tasks)), [
107
- addToTimeline,
108
- createPlanAbortHandler,
109
- handlePlanSelectionConfirmed,
110
- handleExecutionConfirmed,
111
- handleExecutionCancelled,
112
- ]);
113
- const handleConfigFinished = React.useCallback((config) => setQueue(createConfigFinishedHandler(addToTimeline, command, handleCommandError, handleCommandComplete, handleCommandAborted, setService)(config)), [
114
- addToTimeline,
115
- command,
116
- handleCommandError,
117
- handleCommandComplete,
118
- handleCommandAborted,
119
- ]);
72
+ }), [addToTimeline, service]);
73
+ // Create handlers in dependency order
74
+ const introspectHandlers = React.useMemo(() => createIntrospectHandlers(ops, handleAborted), [ops, handleAborted]);
75
+ const answerHandlers = React.useMemo(() => createAnswerHandlers(ops, handleAborted), [ops, handleAborted]);
76
+ const executeHandlers = React.useMemo(() => createExecuteHandlers(ops, handleAborted), [ops, handleAborted]);
77
+ const executionHandlers = React.useMemo(() => createExecutionHandlers(ops, {
78
+ introspect: introspectHandlers,
79
+ answer: answerHandlers,
80
+ execute: executeHandlers,
81
+ }), [ops, introspectHandlers, answerHandlers, executeHandlers]);
82
+ const planHandlers = React.useMemo(() => createPlanHandlers(ops, handleAborted, executionHandlers), [ops, handleAborted, executionHandlers]);
83
+ const commandHandlers = React.useMemo(() => createCommandHandlers(ops, handleAborted, planHandlers, executionHandlers), [ops, handleAborted, planHandlers, executionHandlers]);
84
+ const configHandlers = React.useMemo(() => createConfigHandlers(ops, handleAborted, command, commandHandlers, setService), [ops, handleAborted, command, commandHandlers]);
120
85
  // Initialize queue on mount
121
86
  React.useEffect(() => {
122
87
  const hasConfig = !!service;
123
88
  if (command && hasConfig) {
124
- // With command + valid config: [Command]
125
89
  setQueue([
126
- createCommandDefinition(command, service, handleCommandError, handleCommandComplete, handleCommandAborted),
90
+ createCommandDefinition(command, service, commandHandlers.onError, commandHandlers.onComplete, commandHandlers.onAborted),
127
91
  ]);
128
92
  }
129
93
  else if (command && !hasConfig) {
130
- // With command + no config: [Message, Config] (Command added after config)
131
94
  setQueue([
132
95
  createMessage(getConfigurationRequiredMessage()),
133
- createConfigDefinition(handleConfigFinished, handleConfigAborted),
96
+ createConfigDefinition(configHandlers.onFinished, configHandlers.onAborted),
134
97
  ]);
135
98
  }
136
99
  else if (!command && hasConfig) {
137
- // No command + valid config: [Welcome]
138
100
  setQueue([createWelcomeDefinition(app)]);
139
101
  }
140
102
  else {
141
- // No command + no config: [Welcome, Message, Config]
142
103
  setQueue([
143
104
  createWelcomeDefinition(app),
144
105
  createMessage(getConfigurationRequiredMessage(true)),
145
- createConfigDefinition(handleConfigFinished, handleConfigAborted),
106
+ createConfigDefinition(configHandlers.onFinished, configHandlers.onAborted),
146
107
  ]);
147
108
  }
148
- }, []); // Only run on mount
109
+ }, []);
149
110
  // Process queue whenever it changes
150
111
  React.useEffect(() => {
151
112
  processNextInQueue();
152
113
  }, [queue, processNextInQueue]);
153
- // Exit when queue is empty and timeline has content (all stateless components done)
114
+ // Exit when queue is empty and timeline has content
154
115
  React.useEffect(() => {
155
116
  if (queue.length === 0 && timeline.length > 0) {
156
117
  exitApp(0);
@@ -3,15 +3,20 @@ import { useEffect, useState } from 'react';
3
3
  import { Text } from 'ink';
4
4
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
5
  const INTERVAL = 80;
6
+ const CYCLE = FRAMES.length * INTERVAL;
7
+ function getFrame() {
8
+ return Math.floor((Date.now() % CYCLE) / INTERVAL);
9
+ }
6
10
  export function Spinner() {
7
- const [frame, setFrame] = useState(0);
11
+ const [frame, setFrame] = useState(getFrame);
8
12
  useEffect(() => {
9
13
  const timer = setInterval(() => {
10
- setFrame((prev) => (prev + 1) % FRAMES.length);
14
+ setFrame((prev) => {
15
+ const next = getFrame();
16
+ return next !== prev ? next : prev;
17
+ });
11
18
  }, INTERVAL);
12
- return () => {
13
- clearInterval(timer);
14
- };
19
+ return () => clearInterval(timer);
15
20
  }, []);
16
21
  return _jsx(Text, { color: "blueBright", children: FRAMES[frame] });
17
22
  }
@@ -0,0 +1,120 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { TaskType } from '../types/types.js';
5
+ import { Colors, getTextColor } from '../services/colors.js';
6
+ import { useInput } from '../services/keyboard.js';
7
+ import { formatErrorMessage } from '../services/messages.js';
8
+ import { ensureMinimumTime } from '../services/timing.js';
9
+ import { Spinner } from './Spinner.js';
10
+ const MIN_PROCESSING_TIME = 1000;
11
+ export function Validate({ missingConfig, userRequest, state, service, children, onError, onComplete, onAborted, }) {
12
+ const done = state?.done ?? false;
13
+ const isCurrent = done === false;
14
+ const [error, setError] = useState(null);
15
+ const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
16
+ const [completionMessage, setCompletionMessage] = useState(null);
17
+ useInput((input, key) => {
18
+ if (key.escape && isLoading && !done) {
19
+ setIsLoading(false);
20
+ onAborted();
21
+ }
22
+ }, { isActive: isLoading && !done });
23
+ useEffect(() => {
24
+ // Skip processing if done
25
+ if (done) {
26
+ return;
27
+ }
28
+ // Skip processing if no service available
29
+ if (!service) {
30
+ setError('No service available');
31
+ setIsLoading(false);
32
+ return;
33
+ }
34
+ let mounted = true;
35
+ async function process(svc) {
36
+ const startTime = Date.now();
37
+ try {
38
+ // Build prompt for VALIDATE tool
39
+ const prompt = buildValidatePrompt(missingConfig, userRequest);
40
+ // Call validate tool
41
+ const result = await svc.processWithTool(prompt, 'validate');
42
+ await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
43
+ if (mounted) {
44
+ // Extract CONFIG tasks with descriptions from result
45
+ const configTasks = result.tasks.filter((task) => task.type === TaskType.Config);
46
+ // Build ConfigRequirements with descriptions
47
+ const withDescriptions = configTasks.map((task) => {
48
+ const key = typeof task.params?.key === 'string'
49
+ ? task.params.key
50
+ : 'unknown';
51
+ const original = missingConfig.find((req) => req.path === key);
52
+ return {
53
+ path: key,
54
+ type: original?.type || 'string',
55
+ description: task.action,
56
+ };
57
+ });
58
+ // Build completion message showing which config properties are needed
59
+ const count = withDescriptions.length;
60
+ const propertyWord = count === 1 ? 'property' : 'properties';
61
+ // Shuffle between different message variations
62
+ const messages = [
63
+ `Additional configuration ${propertyWord} required.`,
64
+ `Configuration ${propertyWord} needed.`,
65
+ `Missing configuration ${propertyWord} detected.`,
66
+ `Setup requires configuration ${propertyWord}.`,
67
+ ];
68
+ const message = messages[Math.floor(Math.random() * messages.length)];
69
+ setCompletionMessage(message);
70
+ setIsLoading(false);
71
+ onComplete?.(withDescriptions);
72
+ }
73
+ }
74
+ catch (err) {
75
+ await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
76
+ if (mounted) {
77
+ const errorMessage = formatErrorMessage(err);
78
+ setIsLoading(false);
79
+ if (onError) {
80
+ onError(errorMessage);
81
+ }
82
+ else {
83
+ setError(errorMessage);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ process(service);
89
+ return () => {
90
+ mounted = false;
91
+ };
92
+ }, [
93
+ missingConfig,
94
+ userRequest,
95
+ done,
96
+ service,
97
+ onComplete,
98
+ onError,
99
+ onAborted,
100
+ ]);
101
+ // Don't render when done and nothing to show
102
+ if (done && !completionMessage && !error && !children) {
103
+ return null;
104
+ }
105
+ return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isLoading && (_jsxs(Box, { children: [_jsxs(Text, { color: getTextColor(isCurrent), children: ["Validating configuration requirements.", ' '] }), _jsx(Spinner, {})] })), completionMessage && !isLoading && (_jsx(Box, { children: _jsx(Text, { color: getTextColor(isCurrent), children: completionMessage }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) })), children] }));
106
+ }
107
+ /**
108
+ * Build prompt for VALIDATE tool
109
+ */
110
+ function buildValidatePrompt(missingConfig, userRequest) {
111
+ const configList = missingConfig
112
+ .map((req) => `- Config path: ${req.path}\n Type: ${req.type}`)
113
+ .join('\n');
114
+ return `User requested: "${userRequest}"
115
+
116
+ Missing configuration values:
117
+ ${configList}
118
+
119
+ Generate natural language descriptions for these configuration values based on the skill context.`;
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
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",
@@ -46,22 +46,22 @@
46
46
  },
47
47
  "homepage": "https://github.com/aswitalski/pls#readme",
48
48
  "dependencies": {
49
- "@anthropic-ai/sdk": "^0.69.0",
50
- "ink": "^6.5.0",
49
+ "@anthropic-ai/sdk": "^0.70.1",
50
+ "ink": "^6.5.1",
51
51
  "ink-text-input": "^6.0.0",
52
52
  "react": "^19.2.0",
53
53
  "yaml": "^2.8.1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "^24.10.1",
57
- "@types/react": "^19.2.5",
58
- "@vitest/coverage-v8": "^4.0.9",
57
+ "@types/react": "^19.2.6",
58
+ "@vitest/coverage-v8": "^4.0.12",
59
59
  "eslint": "^9.39.1",
60
60
  "husky": "^9.1.7",
61
61
  "ink-testing-library": "^4.0.0",
62
62
  "prettier": "^3.6.2",
63
63
  "typescript": "^5.9.3",
64
- "typescript-eslint": "^8.46.4",
65
- "vitest": "^4.0.9"
64
+ "typescript-eslint": "^8.47.0",
65
+ "vitest": "^4.0.12"
66
66
  }
67
67
  }