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.
@@ -6,6 +6,9 @@ import { useInput } from '../services/keyboard.js';
6
6
  import { formatErrorMessage } from '../services/messages.js';
7
7
  import { formatDuration } from '../services/utils.js';
8
8
  import { ExecutionStatus, executeCommands, } from '../services/shell.js';
9
+ import { replacePlaceholders } from '../services/placeholder-resolver.js';
10
+ import { loadUserConfig } from '../services/config-loader.js';
11
+ import { ensureMinimumTime } from '../services/timing.js';
9
12
  import { Spinner } from './Spinner.js';
10
13
  const MINIMUM_PROCESSING_TIME = 400;
11
14
  const STATUS_ICONS = {
@@ -13,7 +16,22 @@ const STATUS_ICONS = {
13
16
  [ExecutionStatus.Running]: '• ',
14
17
  [ExecutionStatus.Success]: '✓ ',
15
18
  [ExecutionStatus.Failed]: '✗ ',
19
+ [ExecutionStatus.Aborted]: '⊘ ',
16
20
  };
21
+ function calculateTotalElapsed(commandStatuses) {
22
+ return commandStatuses.reduce((sum, cmd) => {
23
+ if (cmd.elapsed !== undefined) {
24
+ return sum + cmd.elapsed;
25
+ }
26
+ if (cmd.startTime) {
27
+ const elapsed = cmd.endTime
28
+ ? cmd.endTime - cmd.startTime
29
+ : Date.now() - cmd.startTime;
30
+ return sum + elapsed;
31
+ }
32
+ return sum;
33
+ }, 0);
34
+ }
17
35
  function getStatusColors(status) {
18
36
  switch (status) {
19
37
  case ExecutionStatus.Pending:
@@ -44,6 +62,13 @@ function getStatusColors(status) {
44
62
  command: Colors.Status.Error,
45
63
  symbol: Palette.Gray,
46
64
  };
65
+ case ExecutionStatus.Aborted:
66
+ return {
67
+ icon: Palette.DarkOrange,
68
+ description: getTextColor(true),
69
+ command: Palette.DarkOrange,
70
+ symbol: Palette.Gray,
71
+ };
47
72
  }
48
73
  }
49
74
  function CommandStatusDisplay({ item, elapsed }) {
@@ -75,7 +100,24 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
75
100
  if (key.escape && (isLoading || isExecuting) && !done) {
76
101
  setIsLoading(false);
77
102
  setIsExecuting(false);
78
- onAborted();
103
+ setRunningIndex(null);
104
+ // Mark any running command as aborted when cancelled
105
+ const now = Date.now();
106
+ setCommandStatuses((prev) => prev.map((item) => {
107
+ if (item.status === ExecutionStatus.Running) {
108
+ const elapsed = item.startTime
109
+ ? Math.floor((now - item.startTime) / 1000) * 1000
110
+ : undefined;
111
+ return {
112
+ ...item,
113
+ status: ExecutionStatus.Aborted,
114
+ endTime: now,
115
+ elapsed,
116
+ };
117
+ }
118
+ return item;
119
+ }));
120
+ onAborted(calculateTotalElapsed(commandStatuses));
79
121
  }
80
122
  }, { isActive: (isLoading || isExecuting) && !done });
81
123
  // Update elapsed time for running command
@@ -97,9 +139,7 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
97
139
  useEffect(() => {
98
140
  if (isExecuting || commandStatuses.length === 0 || !outputs.length)
99
141
  return;
100
- // Sum up elapsed times from all commands
101
- const totalElapsed = commandStatuses.reduce((sum, cmd) => sum + (cmd.elapsed ?? 0), 0);
102
- onComplete?.(outputs, totalElapsed);
142
+ onComplete?.(outputs, calculateTotalElapsed(commandStatuses));
103
143
  }, [isExecuting, commandStatuses, outputs, onComplete]);
104
144
  useEffect(() => {
105
145
  if (done) {
@@ -114,20 +154,22 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
114
154
  async function process(svc) {
115
155
  const startTime = Date.now();
116
156
  try {
117
- // Format tasks for the execute tool
157
+ // Load user config for placeholder resolution
158
+ const userConfig = loadUserConfig();
159
+ // Format tasks for the execute tool and resolve placeholders
118
160
  const taskDescriptions = tasks
119
161
  .map((task) => {
162
+ // Resolve placeholders in task action
163
+ const resolvedAction = replacePlaceholders(task.action, userConfig);
120
164
  const params = task.params
121
165
  ? ` (params: ${JSON.stringify(task.params)})`
122
166
  : '';
123
- return `- ${task.action}${params}`;
167
+ return `- ${resolvedAction}${params}`;
124
168
  })
125
169
  .join('\n');
126
170
  // Call execute tool to get commands
127
171
  const result = await svc.processWithTool(taskDescriptions, 'execute');
128
- const elapsed = Date.now() - startTime;
129
- const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
130
- await new Promise((resolve) => setTimeout(resolve, remainingTime));
172
+ await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
131
173
  if (!mounted)
132
174
  return;
133
175
  if (!result.commands || result.commands.length === 0) {
@@ -136,9 +178,14 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
136
178
  onComplete?.([], 0);
137
179
  return;
138
180
  }
181
+ // Resolve placeholders in command strings before execution
182
+ const resolvedCommands = result.commands.map((cmd) => ({
183
+ ...cmd,
184
+ command: replacePlaceholders(cmd.command, userConfig),
185
+ }));
139
186
  // Set message and initialize command statuses
140
187
  setMessage(result.message);
141
- setCommandStatuses(result.commands.map((cmd, index) => ({
188
+ setCommandStatuses(resolvedCommands.map((cmd, index) => ({
142
189
  command: cmd,
143
190
  status: ExecutionStatus.Pending,
144
191
  label: tasks[index]?.action,
@@ -146,7 +193,7 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
146
193
  setIsLoading(false);
147
194
  setIsExecuting(true);
148
195
  // Execute commands sequentially
149
- const outputs = await executeCommands(result.commands, (progress) => {
196
+ const outputs = await executeCommands(resolvedCommands, (progress) => {
150
197
  if (!mounted)
151
198
  return;
152
199
  const now = Date.now();
@@ -186,9 +233,7 @@ export function Execute({ tasks, state, service, onError, onComplete, onAborted,
186
233
  }
187
234
  }
188
235
  catch (err) {
189
- const elapsed = Date.now() - startTime;
190
- const remainingTime = Math.max(0, MINIMUM_PROCESSING_TIME - elapsed);
191
- await new Promise((resolve) => setTimeout(resolve, remainingTime));
236
+ await ensureMinimumTime(startTime, MINIMUM_PROCESSING_TIME);
192
237
  if (mounted) {
193
238
  const errorMessage = formatErrorMessage(err);
194
239
  setIsLoading(false);
@@ -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);
@@ -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.9",
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",