prompt-language-shell 0.9.6 → 1.0.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/README.md CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  Your personal command-line concierge. Ask politely, and it gets things done.
4
4
 
5
- > **Note:** This project is in early preview. Features and APIs will change.
6
- > See [roadmap](#roadmap).
7
-
8
5
  ## Installation
9
6
 
10
7
  ```bash
@@ -31,30 +28,29 @@ Here's what I can help with:
31
28
  - Configure - manage and configure system settings
32
29
  - Answer - respond to questions and provide information
33
30
  - Execute - run shell commands and process operations
34
- ```
31
+ ```
35
32
 
36
33
  Skills are custom workflows you can define to teach `pls` about your specific
37
34
  projects and commands. Once defined, you can use them naturally:
38
35
 
39
36
  ```
40
- $ pls build project
37
+ $ pls convert video.mp4
41
38
 
42
39
  Here's my plan.
43
40
 
44
- - Navigate to project directory
45
- - Compile source code
41
+ - Compress video.mp4 with H.264 codec
46
42
  ```
47
43
 
48
44
  You can provide multiple requests at once:
49
45
 
50
46
  ```
51
- $ pls install deps, run tests and build
47
+ $ pls backup photos, compress and upload
52
48
 
53
49
  Here's what I'll do.
54
50
 
55
- - Install dependencies
56
- - Run tests
57
- - Build the project
51
+ - Copy photos to backup folder
52
+ - Create zip archive
53
+ - Upload to cloud storage
58
54
  ```
59
55
 
60
56
  When `pls` needs clarification, it will present options to choose from:
@@ -87,16 +83,47 @@ commands your environment requires.
87
83
 
88
84
  ## Configuration
89
85
 
90
- Your configuration is stored in `~/.plsrc` as a YAML file. Supported settings:
86
+ Your configuration is stored in `~/.plsrc` as a YAML file:
87
+
88
+ ```yaml
89
+ # Mandatory
90
+ anthropic:
91
+ key: sk-ant-...
92
+ model: claude-...
93
+
94
+ # Optional
95
+ settings:
96
+ memory: 1024 # Child process memory limit (MB)
97
+ debug: none # none | info | verbose
91
98
 
92
- - `anthropic.key` - Your API key
93
- - `anthropic.model` - The model to use
99
+ # Custom
100
+ project:
101
+ path: ~/projects/app
102
+ ```
103
+
104
+ Skills can define their own configuration properties via a `Config` section. When
105
+ a skill requires config values that don't exist, `pls` prompts you to provide
106
+ them before execution. See [Skills](#skills) for details.
107
+
108
+ ## Reference
109
+
110
+ ### Debug Mode
111
+ Press `Shift+Tab` during execution to cycle through debug levels
112
+ (none → info → verbose).
113
+ Logs are saved to `~/.pls/logs/` when debug is `info` or `verbose`.
114
+
115
+ ### Data Locations
116
+ ```
117
+ ~/.plsrc # Configuration
118
+ ~/.pls/skills/ # Custom skills
119
+ ~/.pls/logs/ # Debug logs
120
+ ```
94
121
 
95
122
  ## Skills
96
123
 
97
124
  Skills let you teach `pls` about your project-specific workflows. Create
98
- markdown files in `~/.pls/skills/` to define custom operations that `pls` can
99
- understand and execute.
125
+ markdown files in `~/.pls/skills/` to define custom operations that
126
+ `pls` can understand and execute.
100
127
 
101
128
  For complete documentation, see [docs/SKILLS.md](./docs/SKILLS.md).
102
129
 
@@ -107,60 +134,121 @@ Each skill file uses a simple markdown format:
107
134
  - **Name**: What you call this workflow (e.g., "Build Project")
108
135
  - **Description**: What it does and any variants or options
109
136
  - **Steps**: What needs to happen, in order
110
- - **Execution** (optional): The actual shell commands to run
137
+ - **Execution**: The actual shell commands to run
111
138
 
112
139
  ### Example
113
140
 
114
- Here's a skill that builds different project variants:
141
+ Here's a skill for building a product from source:
115
142
 
116
143
  ```markdown
117
144
  ### Name
118
- Build Project
145
+ Build Product
119
146
 
120
147
  ### Description
121
- Build a project in different configurations:
122
- - dev (debug build with source maps)
123
- - prod (optimized build)
124
- - test (with test coverage)
148
+ Build a product from source. Handles the full compilation pipeline.
149
+
150
+ The company maintains two product lines:
151
+ - Stable: the flagship product
152
+ - Beta: the experimental product
153
+
154
+ If the user says "just compile" or "recompile", dependency installation and
155
+ tests MUST be skipped. Tests MUST also be skipped if the user says "without
156
+ tests". Deployment MUST only run if the user explicitly asks. Compile and
157
+ package steps are MANDATORY.
125
158
 
126
159
  ### Steps
127
160
  - Navigate to the project directory
128
- - Install dependencies if needed
129
- - Run the {ENV} build script
130
- - Generate build artifacts
161
+ - Install build dependencies
162
+ - Run the test suite
163
+ - Compile source code
164
+ - Package build artifacts
165
+ - Deploy to server
131
166
 
132
167
  ### Execution
133
- - cd ~/projects/next
134
- - npm install
135
- - npm run build:{ENV}
136
- - cp -r dist/ builds/{ENV}/
168
+ - [ Navigate To Project ]
169
+ - ./configure && make deps
170
+ - make test
171
+ - make build
172
+ - make package
173
+ - ./scripts/deploy.sh
137
174
  ```
138
175
 
139
- With this skill defined, you can use natural language like:
176
+ The `[ Navigate To Project ]` reference invokes another skill by name. When `pls`
177
+ plans this workflow, it expands the reference inline, inserting that skill's
178
+ execution steps at this position. This lets you compose complex workflows from
179
+ simpler, reusable skills. Here's what that skill might look like:
180
+
181
+ ```markdown
182
+ ### Name
183
+ Navigate To Project
184
+
185
+ ### Description
186
+ The company maintains two product lines:
187
+ - Stable: the flagship product
188
+ - Beta: the experimental product
189
+
190
+ ### Aliases
191
+ - go to project
192
+ - navigate to repo
193
+
194
+ ### Config
195
+ project:
196
+ stable:
197
+ path: string
198
+ beta:
199
+ path: string
200
+
201
+ ### Steps
202
+ - Navigate to project directory
203
+
204
+ ### Execution
205
+ - cd {project.PRODUCT.path}
206
+ ```
207
+
208
+ The `{project.PRODUCT.path}` placeholder uses config values from `~/.plsrc`. The
209
+ PRODUCT is matched from user intent (e.g., "build stable" resolves to
210
+ `project.stable.path`).
211
+
212
+ The Description tells `pls` when to skip optional steps. This lets you say:
213
+
140
214
  ```
141
- $ pls build project for production
142
- $ pls build dev environment
143
- $ pls build with testing enabled
215
+ $ pls build stable
216
+
217
+ - Navigate to the Stable directory
218
+ - Install build dependencies
219
+ - Run the test suite
220
+ - Compile source code
221
+ - Package build artifacts
144
222
  ```
145
- The `{ENV}` placeholder gets replaced with the variant you specify.
146
- Instead of remembering the exact commands and paths for each environment, just
147
- tell `pls` what you want in plain English. The Execution section ensures the right commands run every time.
148
223
 
149
- ### Keep It Short
224
+ Here "stable" matches the PRODUCT, so `pls` looks up `project.stable.path` in your
225
+ config. All steps run except deploy (not requested). When iterating quickly:
150
226
 
151
- Skills also work with concise commands. Once you've taught `pls` about your
152
- workflow, you can use minimal phrasing:
227
+ ```
228
+ $ pls just recompile experimental
153
229
 
230
+ - Navigate to the Beta directory
231
+ - Compile source code
232
+ - Package build artifacts
154
233
  ```
155
- $ pls build prod
156
- $ pls build dev
157
- $ pls build test
234
+
235
+ Now "experimental" resolves to `project.beta.path`. And when you're ready to ship:
236
+
158
237
  ```
238
+ $ pls build and deploy main
159
239
 
160
- ## Roadmap
240
+ - Navigate to the Stable directory
241
+ - Install build dependencies
242
+ - Run the test suite
243
+ - Compile source code
244
+ - Package build artifacts
245
+ - Deploy to server
246
+ ```
161
247
 
162
- - **0.9** - Learn skill, codebase refinement, complex dependency handling
163
- - **1.0** - Production release
248
+ The same skill handles all cases based on your intent, something an alias or
249
+ script can't do. Skills are fully dynamic: you can add new variants, change step
250
+ conditions, or introduce new options anytime by editing the markdown file - no
251
+ code changes required.
164
252
 
165
253
  ## Development
166
254
 
@@ -51,8 +51,8 @@ export const SimpleComponent = memo(function SimpleComponent({ def, }) {
51
51
  export const ControllerComponent = memo(function ControllerComponent({ def, debug, requestHandlers, lifecycleHandlers, workflowHandlers, }) {
52
52
  switch (def.name) {
53
53
  case ComponentName.Config: {
54
- const { props: { steps, onFinished, onAborted }, status, } = def;
55
- return (_jsx(Config, { steps: steps, onFinished: onFinished, onAborted: onAborted, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, status: status, debug: debug }));
54
+ const { props: { steps, query, service, onFinished, onAborted }, status, } = def;
55
+ return (_jsx(Config, { steps: steps, query: query, service: service, onFinished: onFinished, onAborted: onAborted, requestHandlers: requestHandlers, lifecycleHandlers: lifecycleHandlers, workflowHandlers: workflowHandlers, status: status, debug: debug }));
56
56
  }
57
57
  case ComponentName.Command: {
58
58
  const { props: { command, service, onAborted }, status, } = def;
@@ -100,7 +100,9 @@ export const ViewComponent = memo(function ViewComponent({ def, }) {
100
100
  return (_jsx(ConfirmView, { status: status, message: message, selectedIndex: state.selectedIndex }));
101
101
  }
102
102
  case ComponentName.Config: {
103
- const { props: { steps }, state, status, } = def;
103
+ const { props: { steps: propSteps = [] }, state, status, } = def;
104
+ // Use resolved steps from state if available (for query-based configs)
105
+ const steps = state.steps ?? propSteps;
104
106
  return _jsx(ConfigView, { steps: steps, state: state, status: status });
105
107
  }
106
108
  case ComponentName.Schedule: {
@@ -34,28 +34,12 @@ export function Command({ command, status, service, requestHandlers, lifecycleHa
34
34
  async function process(svc) {
35
35
  const startTime = Date.now();
36
36
  try {
37
- let result = await svc.processWithTool(command, 'schedule');
38
- // Save schedule debug output before potentially delegating
39
- const scheduleDebug = result.debug || [];
40
- // If all tasks are configure type, delegate to CONFIGURE tool
41
- const allConfig = result.tasks.length > 0 &&
42
- result.tasks.every((task) => task.type === TaskType.Config);
43
- if (allConfig) {
44
- // Extract query from first config task params, default to 'app'
45
- const query = result.tasks[0].params?.query || 'app';
46
- // Call CONFIGURE tool to get specific config keys
47
- result = await svc.processWithTool(query, 'configure');
48
- }
37
+ const result = await svc.processWithTool(command, 'schedule');
49
38
  await ensureMinimumTime(startTime, MIN_PROCESSING_TIME);
50
39
  if (mounted) {
51
40
  // Add debug components to timeline if present
52
- // If we delegated to configure, include both schedule and configure debug
53
- // If not, only include schedule debug (result.debug is same as scheduleDebug)
54
- const debugComponents = allConfig
55
- ? [...scheduleDebug, ...(result.debug || [])]
56
- : scheduleDebug;
57
- if (debugComponents.length > 0) {
58
- workflowHandlers.addToTimeline(...debugComponents);
41
+ if (result.debug?.length) {
42
+ workflowHandlers.addToTimeline(...result.debug);
59
43
  }
60
44
  // Update local state
61
45
  setMessage(result.message);
@@ -1,24 +1,57 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
- import { ComponentStatus } from '../../types/components.js';
4
- import { FeedbackType } from '../../types/types.js';
3
+ import { Box, Text } from 'ink';
4
+ import { ComponentStatus, } from '../../types/components.js';
5
+ import { FeedbackType, TaskType } from '../../types/types.js';
5
6
  import { createFeedback } from '../../services/components.js';
7
+ import { createConfigStepsFromSchema } from '../../configuration/steps.js';
8
+ import { saveConfigLabels } from '../../configuration/labels.js';
6
9
  import { DebugLevel } from '../../configuration/types.js';
7
10
  import { useInput } from '../../services/keyboard.js';
8
11
  import { ConfigView, StepType } from '../views/Config.js';
12
+ import { Spinner } from '../views/Spinner.js';
9
13
  export { ConfigView, StepType, } from '../views/Config.js';
14
+ /**
15
+ * Resolve query to config steps via CONFIGURE tool
16
+ */
17
+ async function resolveQueryToSteps(query, service) {
18
+ const result = await service.processWithTool(query, 'configure');
19
+ const configTasks = result.tasks.filter((task) => task.type === TaskType.Config && task.params?.key);
20
+ if (configTasks.length === 0) {
21
+ throw new Error('No configuration settings matched your query.');
22
+ }
23
+ const keys = configTasks.map((task) => task.params?.key);
24
+ const labels = {};
25
+ for (const task of configTasks) {
26
+ const key = task.params?.key;
27
+ if (key && task.action) {
28
+ labels[key] = task.action;
29
+ }
30
+ }
31
+ if (Object.keys(labels).length > 0) {
32
+ saveConfigLabels(labels);
33
+ }
34
+ const steps = createConfigStepsFromSchema(keys);
35
+ return {
36
+ steps: steps.map((step, i) => ({
37
+ ...step,
38
+ description: labels[keys[i]] || step.description,
39
+ })),
40
+ debug: result.debug || [],
41
+ };
42
+ }
10
43
  /**
11
44
  * Config controller: Multi-step wizard logic
12
45
  */
13
46
  export function Config(props) {
14
- const { steps, status, debug = DebugLevel.None, requestHandlers, lifecycleHandlers, onFinished, onAborted, } = props;
47
+ const { steps: initialSteps, query, service, status, debug = DebugLevel.None, requestHandlers, lifecycleHandlers, workflowHandlers, onFinished, onAborted, } = props;
15
48
  const isActive = status === ComponentStatus.Active;
49
+ const [steps, setSteps] = useState(initialSteps || []);
50
+ const [resolving, setResolving] = useState(!initialSteps?.length && !!query);
16
51
  const [step, setStep] = useState(0);
17
52
  const [values, setValues] = useState(() => {
18
- // Initialize from step defaults
19
53
  const initial = {};
20
- steps.forEach((stepConfig) => {
21
- // Use full path if available, otherwise use key
54
+ (initialSteps || []).forEach((stepConfig) => {
22
55
  const configKey = stepConfig.path || stepConfig.key;
23
56
  switch (stepConfig.type) {
24
57
  case StepType.Text:
@@ -30,45 +63,75 @@ export function Config(props) {
30
63
  initial[configKey] =
31
64
  stepConfig.options[stepConfig.defaultIndex].value;
32
65
  break;
33
- default: {
34
- const _exhaustiveCheck = stepConfig;
35
- throw new Error('Unsupported step type');
36
- }
37
66
  }
38
67
  });
39
68
  return initial;
40
69
  });
41
- const [inputValue, setInputValue] = useState(() => {
42
- // Initialize with the current step's value if available
43
- if (step < steps.length) {
44
- const stepConfig = steps[step];
45
- const configKey = stepConfig.path || stepConfig.key;
46
- return values[configKey] || '';
47
- }
48
- return '';
49
- });
70
+ const [inputValue, setInputValue] = useState('');
50
71
  const [selectedIndex, setSelectedIndex] = useState(0);
51
- const normalizeValue = (value) => {
52
- if (value === null || value === undefined) {
53
- return '';
54
- }
55
- return value.replace(/\n/g, '').trim();
56
- };
72
+ // Resolve query to steps
73
+ useEffect(() => {
74
+ if (!isActive || !query || !service || initialSteps?.length)
75
+ return;
76
+ resolveQueryToSteps(query, service)
77
+ .then((result) => {
78
+ // Add debug components to timeline if present
79
+ if (result.debug.length) {
80
+ workflowHandlers.addToTimeline(...result.debug);
81
+ }
82
+ setSteps(result.steps);
83
+ setResolving(false);
84
+ // Initialize values for resolved steps
85
+ const initial = {};
86
+ result.steps.forEach((stepConfig) => {
87
+ const configKey = stepConfig.path || stepConfig.key;
88
+ switch (stepConfig.type) {
89
+ case StepType.Text:
90
+ if (stepConfig.value !== null) {
91
+ initial[configKey] = stepConfig.value;
92
+ }
93
+ break;
94
+ case StepType.Selection:
95
+ initial[configKey] =
96
+ stepConfig.options[stepConfig.defaultIndex].value;
97
+ break;
98
+ }
99
+ });
100
+ setValues(initial);
101
+ })
102
+ .catch((err) => {
103
+ setResolving(false);
104
+ lifecycleHandlers.completeActive(createFeedback({
105
+ type: FeedbackType.Failed,
106
+ message: err instanceof Error ? err.message : 'Failed to resolve',
107
+ }));
108
+ });
109
+ }, [
110
+ isActive,
111
+ query,
112
+ service,
113
+ initialSteps,
114
+ lifecycleHandlers,
115
+ workflowHandlers,
116
+ ]);
57
117
  // Update inputValue when step changes
58
118
  useEffect(() => {
59
119
  if (isActive && step < steps.length) {
60
120
  const stepConfig = steps[step];
61
121
  const configKey = stepConfig.path || stepConfig.key;
62
- const value = values[configKey] || '';
63
- setInputValue(value);
122
+ setInputValue(values[configKey] || '');
64
123
  }
65
- }, [step, isActive, steps]);
124
+ }, [step, isActive, steps, values]);
125
+ const normalizeValue = (value) => {
126
+ if (value === null || value === undefined)
127
+ return '';
128
+ return value.replace(/\n/g, '').trim();
129
+ };
66
130
  useInput((_, key) => {
67
131
  if (!isActive || step >= steps.length)
68
132
  return;
69
133
  const currentStepConfig = steps[step];
70
134
  if (key.escape) {
71
- // Save current value before aborting
72
135
  const configKey = currentStepConfig.path || currentStepConfig.key;
73
136
  let currentValue = '';
74
137
  switch (currentStepConfig.type) {
@@ -78,28 +141,20 @@ export function Config(props) {
78
141
  case StepType.Selection:
79
142
  currentValue = values[configKey] || '';
80
143
  break;
81
- default: {
82
- const _exhaustiveCheck = currentStepConfig;
83
- throw new Error('Unsupported step type');
84
- }
85
144
  }
86
145
  const finalValues = currentValue
87
146
  ? { ...values, [configKey]: currentValue }
88
147
  : values;
89
- // Expose final state
90
- const finalState = {
148
+ requestHandlers.onCompleted({
91
149
  values: finalValues,
92
150
  completedStep: step,
93
151
  selectedIndex,
94
- };
95
- requestHandlers.onCompleted(finalState);
96
- // Abort configuration
152
+ steps,
153
+ });
97
154
  if (onAborted) {
98
- // Let Workflow handler complete and add feedback
99
155
  onAborted('configuration');
100
156
  }
101
157
  else {
102
- // Fallback: complete with abort feedback directly
103
158
  lifecycleHandlers.completeActive(createFeedback({
104
159
  type: FeedbackType.Aborted,
105
160
  message: 'Configuration cancelled.',
@@ -107,7 +162,6 @@ export function Config(props) {
107
162
  }
108
163
  return;
109
164
  }
110
- // Handle selection step navigation
111
165
  if (currentStepConfig.type === StepType.Selection) {
112
166
  if (key.tab) {
113
167
  setSelectedIndex((prev) => (prev + 1) % currentStepConfig.options.length);
@@ -122,13 +176,10 @@ export function Config(props) {
122
176
  let finalValue = '';
123
177
  switch (currentStepConfig.type) {
124
178
  case StepType.Selection:
125
- // For selection, value is already validated by options
126
179
  finalValue = value;
127
180
  break;
128
181
  case StepType.Text: {
129
- // For text input
130
182
  const normalizedInput = normalizeValue(value);
131
- // Try user input first, then fall back to default
132
183
  if (normalizedInput && currentStepConfig.validate(normalizedInput)) {
133
184
  finalValue = normalizedInput;
134
185
  }
@@ -138,63 +189,46 @@ export function Config(props) {
138
189
  }
139
190
  break;
140
191
  }
141
- default: {
142
- const _exhaustiveCheck = currentStepConfig;
143
- throw new Error('Unsupported step type');
144
- }
145
192
  }
146
- // Don't allow empty or invalid value
147
- if (!finalValue) {
193
+ if (!finalValue)
148
194
  return;
149
- }
150
- // Use full path if available, otherwise use key
151
195
  const configKey = currentStepConfig.path || currentStepConfig.key;
152
196
  const newValues = { ...values, [configKey]: finalValue };
153
197
  setValues(newValues);
154
198
  setInputValue('');
155
199
  if (step === steps.length - 1) {
156
- // Last step completed
157
- // Expose final state
158
- const finalState = {
200
+ requestHandlers.onCompleted({
159
201
  values: newValues,
160
202
  completedStep: steps.length,
161
203
  selectedIndex,
162
- };
163
- requestHandlers.onCompleted(finalState);
164
- // Call onFinished callback and handle result
204
+ steps,
205
+ });
165
206
  try {
166
- if (onFinished) {
167
- onFinished(newValues);
168
- }
169
- // Success - complete with success feedback
207
+ onFinished?.(newValues);
170
208
  lifecycleHandlers.completeActive(createFeedback({
171
209
  type: FeedbackType.Succeeded,
172
210
  message: 'Configuration saved successfully.',
173
211
  }));
174
212
  }
175
213
  catch (error) {
176
- // Failure - complete with error feedback
177
- const errorMessage = error instanceof Error ? error.message : 'Configuration failed';
178
- lifecycleHandlers.completeActive(createFeedback({ type: FeedbackType.Failed, message: errorMessage }));
214
+ lifecycleHandlers.completeActive(createFeedback({
215
+ type: FeedbackType.Failed,
216
+ message: error instanceof Error ? error.message : 'Configuration failed',
217
+ }));
179
218
  }
180
219
  setStep(steps.length);
181
220
  }
182
221
  else {
183
222
  const nextStep = step + 1;
184
223
  setStep(nextStep);
185
- // Reset selectedIndex for next step
186
224
  if (nextStep < steps.length &&
187
225
  steps[nextStep].type === StepType.Selection) {
188
226
  setSelectedIndex(steps[nextStep].defaultIndex);
189
227
  }
190
228
  }
191
229
  };
192
- // Build current state for View
193
- // Controller always renders View, passing current state and callbacks
194
- const state = {
195
- values,
196
- completedStep: step,
197
- selectedIndex,
198
- };
199
- return (_jsx(ConfigView, { steps: steps, state: state, status: status, debug: debug, onInputChange: setInputValue, onInputSubmit: handleSubmit }));
230
+ if (resolving) {
231
+ return (_jsxs(Box, { marginLeft: 1, children: [_jsx(Text, { children: "Resolving configuration... " }), _jsx(Spinner, {})] }));
232
+ }
233
+ return (_jsx(ConfigView, { steps: steps, state: { values, completedStep: step, selectedIndex }, status: status, debug: debug, onInputChange: setInputValue, onInputSubmit: handleSubmit }));
200
234
  }