prompt-language-shell 0.3.6 → 0.3.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.
package/README.md CHANGED
@@ -45,6 +45,12 @@ Your configuration is stored in `~/.plsrc` as a YAML file. Supported settings:
45
45
  - `anthropic.key` - Your API key
46
46
  - `anthropic.model` - The model to use
47
47
 
48
+ ## Skills
49
+
50
+ You can extend `pls` with custom workflows by creating markdown files in `~/.pls/skills/`. Skills define domain-specific operations, parameters, and steps that guide both planning and execution of tasks.
51
+
52
+ Your skills are referenced when planning requests, enabling `pls` to understand specific workflows, create and execute structured plans tailored to your environment.
53
+
48
54
  ## Development
49
55
 
50
56
  See [CLAUDE.md](./CLAUDE.md) for development guidelines and architecture.
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ const app = {
20
20
  version: packageJson.version,
21
21
  description: packageJson.description,
22
22
  isDev,
23
+ isDebug: false,
23
24
  };
24
25
  // Get command from command-line arguments
25
26
  const args = process.argv.slice(2);
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { ComponentName } from '../types/types.js';
3
3
  import { AnthropicModel, isValidAnthropicApiKey, isValidAnthropicModel, } from './config.js';
4
+ import { getConfirmationMessage } from './messages.js';
4
5
  import { StepType } from '../ui/Config.js';
5
6
  export function markAsDone(component) {
6
7
  return { ...component, state: { ...component.state, done: true } };
@@ -123,6 +124,18 @@ export function createRefinement(text, onAborted) {
123
124
  },
124
125
  };
125
126
  }
127
+ export function createConfirmDefinition(onConfirmed, onCancelled) {
128
+ return {
129
+ id: randomUUID(),
130
+ name: ComponentName.Confirm,
131
+ state: { done: false },
132
+ props: {
133
+ message: getConfirmationMessage(),
134
+ onConfirmed,
135
+ onCancelled,
136
+ },
137
+ };
138
+ }
126
139
  export function isStateless(component) {
127
140
  return !('state' in component);
128
141
  }
@@ -50,6 +50,14 @@ function validateConfig(parsed) {
50
50
  if (model && typeof model === 'string' && isValidAnthropicModel(model)) {
51
51
  validatedConfig.anthropic.model = model;
52
52
  }
53
+ // Optional settings section
54
+ if (config.settings && typeof config.settings === 'object') {
55
+ const settings = config.settings;
56
+ validatedConfig.settings = {};
57
+ if ('debug' in settings && typeof settings.debug === 'boolean') {
58
+ validatedConfig.settings.debug = settings.debug;
59
+ }
60
+ }
53
61
  return validatedConfig;
54
62
  }
55
63
  export function loadConfig() {
@@ -116,6 +124,18 @@ export function saveConfig(section, config) {
116
124
  export function saveAnthropicConfig(config) {
117
125
  saveConfig('anthropic', config);
118
126
  }
127
+ export function saveDebugSetting(debug) {
128
+ saveConfig('settings', { debug });
129
+ }
130
+ export function loadDebugSetting() {
131
+ try {
132
+ const config = loadConfig();
133
+ return config.settings?.debug ?? false;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
119
139
  /**
120
140
  * Returns a message requesting initial setup.
121
141
  * Provides natural language variations that sound like a professional concierge
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Returns a natural language confirmation message for plan execution.
3
+ * Randomly selects from variations to sound less robotic.
4
+ */
5
+ export function getConfirmationMessage() {
6
+ const messages = [
7
+ 'Should I execute this plan?',
8
+ 'Do you want me to proceed with these tasks?',
9
+ 'Ready to execute?',
10
+ 'Shall I execute this plan?',
11
+ 'Would you like me to run these tasks?',
12
+ 'Execute this plan?',
13
+ ];
14
+ return messages[Math.floor(Math.random() * messages.length)];
15
+ }
@@ -7,6 +7,7 @@ export var ComponentName;
7
7
  ComponentName["Plan"] = "plan";
8
8
  ComponentName["Refinement"] = "refinement";
9
9
  ComponentName["Feedback"] = "feedback";
10
+ ComponentName["Confirm"] = "confirm";
10
11
  })(ComponentName || (ComponentName = {}));
11
12
  export var TaskType;
12
13
  (function (TaskType) {
package/dist/ui/Column.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
3
  import { Component } from './Component.js';
4
- export const Column = ({ items }) => {
5
- return (_jsx(Box, { marginTop: 1, marginBottom: 1, marginLeft: 1, flexDirection: "column", gap: 1, children: items.map((item) => (_jsx(Box, { children: _jsx(Component, { def: item }) }, item.id))) }));
4
+ export const Column = ({ items, debug }) => {
5
+ return (_jsx(Box, { marginTop: 1, marginBottom: 1, marginLeft: 1, flexDirection: "column", gap: 1, children: items.map((item) => (_jsx(Box, { children: _jsx(Component, { def: item, debug: debug }) }, item.id))) }));
6
6
  };
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { ComponentName } from '../types/types.js';
3
3
  import { Command } from './Command.js';
4
+ import { Confirm } from './Confirm.js';
4
5
  import { Config } from './Config.js';
5
6
  import { Feedback } from './Feedback.js';
6
7
  import { Message } from './Message.js';
7
8
  import { Plan } from './Plan.js';
8
9
  import { Refinement } from './Refinement.js';
9
10
  import { Welcome } from './Welcome.js';
10
- export function Component({ def }) {
11
+ export function Component({ def, debug }) {
11
12
  switch (def.name) {
12
13
  case ComponentName.Welcome:
13
14
  return _jsx(Welcome, { ...def.props });
@@ -22,7 +23,7 @@ export function Component({ def }) {
22
23
  return _jsx(Command, { ...props, state: state });
23
24
  }
24
25
  case ComponentName.Plan:
25
- return _jsx(Plan, { ...def.props });
26
+ return _jsx(Plan, { ...def.props, debug: debug });
26
27
  case ComponentName.Feedback:
27
28
  return _jsx(Feedback, { ...def.props });
28
29
  case ComponentName.Message:
@@ -32,5 +33,10 @@ export function Component({ def }) {
32
33
  const state = def.state;
33
34
  return _jsx(Refinement, { ...props, state: state });
34
35
  }
36
+ case ComponentName.Confirm: {
37
+ const props = def.props;
38
+ const state = def.state;
39
+ return _jsx(Confirm, { ...props, state: state });
40
+ }
35
41
  }
36
42
  }
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export function Confirm({ message, state, onConfirmed, onCancelled, }) {
5
+ const done = state?.done ?? false;
6
+ const [selectedIndex, setSelectedIndex] = React.useState(0); // 0 = Yes, 1 = No
7
+ useInput((input, key) => {
8
+ if (done)
9
+ return;
10
+ if (key.escape) {
11
+ // Escape: highlight "No" and cancel
12
+ setSelectedIndex(1);
13
+ onCancelled?.();
14
+ }
15
+ else if (key.tab) {
16
+ // Toggle between Yes (0) and No (1)
17
+ setSelectedIndex((prev) => (prev === 0 ? 1 : 0));
18
+ }
19
+ else if (key.return) {
20
+ // Confirm selection
21
+ if (selectedIndex === 0) {
22
+ onConfirmed?.();
23
+ }
24
+ else {
25
+ onCancelled?.();
26
+ }
27
+ }
28
+ }, { isActive: !done });
29
+ const options = [
30
+ { label: 'Yes', value: 'yes', color: '#4a9a7a' }, // green (execute)
31
+ { label: 'No', value: 'no', color: '#a85c3f' }, // dark orange (discard)
32
+ ];
33
+ if (done) {
34
+ // When done, show both the message and user's choice in timeline
35
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: message }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["> ", options[selectedIndex].label] }) })] }));
36
+ }
37
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: message }) }), _jsxs(Box, { children: [_jsx(Text, { color: "#5c8cbc", children: ">" }), _jsx(Text, { children: " " }), _jsx(Box, { children: options.map((option, index) => {
38
+ const isSelected = index === selectedIndex;
39
+ return (_jsx(Box, { marginRight: 2, children: _jsx(Text, { color: isSelected ? option.color : undefined, dimColor: !isSelected, bold: isSelected, children: option.label }) }, option.value));
40
+ }) })] })] }));
41
+ }
package/dist/ui/Label.js CHANGED
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { Separator } from './Separator.js';
4
- export function Label({ description, descriptionColor, type, typeColor, }) {
5
- return (_jsxs(Box, { children: [_jsx(Text, { color: descriptionColor, children: description }), _jsx(Separator, {}), _jsx(Text, { color: typeColor, children: type })] }));
4
+ export function Label({ description, descriptionColor, type, typeColor, showType = false, }) {
5
+ return (_jsxs(Box, { children: [_jsx(Text, { color: descriptionColor, children: description }), showType && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(Text, { color: typeColor, children: type })] }))] }));
6
6
  }
package/dist/ui/List.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { Label } from './Label.js';
4
- export const List = ({ items, level = 0, highlightedIndex = null, highlightedParentIndex = null, }) => {
4
+ export const List = ({ items, level = 0, highlightedIndex = null, highlightedParentIndex = null, showType = false, }) => {
5
5
  const marginLeft = level > 0 ? 4 : 0;
6
6
  return (_jsx(Box, { flexDirection: "column", marginLeft: marginLeft, children: items.map((item, index) => {
7
7
  // At level 0, track which parent is active for child highlighting
@@ -16,6 +16,11 @@ export const List = ({ items, level = 0, highlightedIndex = null, highlightedPar
16
16
  const typeColor = isHighlighted && item.type.highlightedColor
17
17
  ? item.type.highlightedColor
18
18
  : item.type.color;
19
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "whiteBright", children: marker }), _jsx(Label, { description: item.description.text, descriptionColor: descriptionColor, type: item.type.text, typeColor: typeColor })] }), item.children && item.children.length > 0 && (_jsx(List, { items: item.children, level: level + 1, highlightedIndex: shouldHighlightChildren ? highlightedIndex : null }))] }, index));
19
+ // Use highlighted type color for arrow markers when highlighted
20
+ const markerColor = item.markerColor ||
21
+ (isHighlighted && item.type.highlightedColor
22
+ ? item.type.highlightedColor
23
+ : 'whiteBright');
24
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: markerColor, children: marker }), _jsx(Label, { description: item.description.text, descriptionColor: descriptionColor, type: item.type.text, typeColor: typeColor, showType: showType })] }), item.children && item.children.length > 0 && (_jsx(List, { items: item.children, level: level + 1, highlightedIndex: shouldHighlightChildren ? highlightedIndex : null, showType: showType }))] }, index));
20
25
  }) }));
21
26
  };
package/dist/ui/Main.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
+ import { useInput } from 'ink';
3
4
  import { ComponentName, FeedbackType, TaskType, } from '../types/types.js';
4
5
  import { createAnthropicService, } from '../services/anthropic.js';
5
- import { getConfigurationRequiredMessage, hasValidAnthropicKey, loadConfig, saveAnthropicConfig, } from '../services/config.js';
6
- import { createCommandDefinition, createConfigDefinition, createFeedback, createMessage, createRefinement, createPlanDefinition, createWelcomeDefinition, getRefiningMessage, isStateless, markAsDone, } from '../services/components.js';
6
+ import { getConfigurationRequiredMessage, hasValidAnthropicKey, loadConfig, loadDebugSetting, saveAnthropicConfig, saveDebugSetting, } from '../services/config.js';
7
+ import { createCommandDefinition, createConfirmDefinition, createConfigDefinition, createFeedback, createMessage, createRefinement, createPlanDefinition, createWelcomeDefinition, getRefiningMessage, isStateless, markAsDone, } from '../services/components.js';
7
8
  import { exitApp } from '../services/process.js';
8
9
  import { Column } from './Column.js';
9
10
  export const Main = ({ app, command }) => {
@@ -17,6 +18,18 @@ export const Main = ({ app, command }) => {
17
18
  });
18
19
  const [timeline, setTimeline] = React.useState([]);
19
20
  const [queue, setQueue] = React.useState([]);
21
+ const [isDebug, setIsDebug] = React.useState(() => loadDebugSetting());
22
+ // Top-level Shift+Tab handler for debug mode toggle
23
+ // Child components must ignore Shift+Tab to prevent conflicts
24
+ useInput((input, key) => {
25
+ if (key.shift && key.tab) {
26
+ setIsDebug((prev) => {
27
+ const newValue = !prev;
28
+ saveDebugSetting(newValue);
29
+ return newValue;
30
+ });
31
+ }
32
+ }, { isActive: true });
20
33
  const addToTimeline = React.useCallback((...items) => {
21
34
  setTimeline((timeline) => [...timeline, ...items]);
22
35
  }, []);
@@ -69,6 +82,30 @@ export const Main = ({ app, command }) => {
69
82
  const handleRefinementAborted = React.useCallback(() => {
70
83
  handleAborted('Plan refinement');
71
84
  }, [handleAborted]);
85
+ const handleExecutionConfirmed = React.useCallback(() => {
86
+ setQueue((currentQueue) => {
87
+ if (currentQueue.length === 0)
88
+ return currentQueue;
89
+ const [first] = currentQueue;
90
+ if (first.name === ComponentName.Confirm) {
91
+ addToTimeline(markAsDone(first));
92
+ }
93
+ exitApp(0);
94
+ return [];
95
+ });
96
+ }, [addToTimeline]);
97
+ const handleExecutionCancelled = React.useCallback(() => {
98
+ setQueue((currentQueue) => {
99
+ if (currentQueue.length === 0)
100
+ return currentQueue;
101
+ const [first] = currentQueue;
102
+ if (first.name === ComponentName.Confirm) {
103
+ addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, 'Execution cancelled'));
104
+ }
105
+ exitApp(0);
106
+ return [];
107
+ });
108
+ }, [addToTimeline]);
72
109
  const handlePlanSelectionConfirmed = React.useCallback(async (selectedTasks) => {
73
110
  // Mark current plan as done and add refinement to queue
74
111
  let refinementDef = null;
@@ -101,10 +138,11 @@ export const Main = ({ app, command }) => {
101
138
  }
102
139
  return [];
103
140
  });
104
- // Show final execution plan
141
+ // Show final execution plan with confirmation
105
142
  const planDefinition = createPlanDefinition(result.message, result.tasks, handlePlanAborted, undefined);
143
+ const confirmDefinition = createConfirmDefinition(handleExecutionConfirmed, handleExecutionCancelled);
106
144
  addToTimeline(planDefinition);
107
- exitApp(0);
145
+ setQueue([confirmDefinition]);
108
146
  }
109
147
  catch (error) {
110
148
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
@@ -135,16 +173,21 @@ export const Main = ({ app, command }) => {
135
173
  return [planDefinition];
136
174
  }
137
175
  else {
138
- // No define task - add plan to timeline and exit
176
+ // No define task - show plan and confirmation
177
+ const confirmDefinition = createConfirmDefinition(handleExecutionConfirmed, handleExecutionCancelled);
139
178
  addToTimeline(markAsDone(first), planDefinition);
140
- exitApp(0);
141
- return [];
179
+ return [confirmDefinition];
142
180
  }
143
181
  }
144
182
  exitApp(0);
145
183
  return [];
146
184
  });
147
- }, [addToTimeline, handlePlanSelectionConfirmed]);
185
+ }, [
186
+ addToTimeline,
187
+ handlePlanSelectionConfirmed,
188
+ handleExecutionConfirmed,
189
+ handleExecutionCancelled,
190
+ ]);
148
191
  const handleConfigFinished = React.useCallback((config) => {
149
192
  const anthropicConfig = config;
150
193
  saveAnthropicConfig(anthropicConfig);
@@ -203,7 +246,13 @@ export const Main = ({ app, command }) => {
203
246
  React.useEffect(() => {
204
247
  processNextInQueue();
205
248
  }, [queue, processNextInQueue]);
249
+ // Exit when queue is empty and timeline has content (all stateless components done)
250
+ React.useEffect(() => {
251
+ if (queue.length === 0 && timeline.length > 0) {
252
+ exitApp(0);
253
+ }
254
+ }, [queue, timeline]);
206
255
  const current = queue.length > 0 ? queue[0] : null;
207
256
  const items = [...timeline, ...(current ? [current] : [])];
208
- return _jsx(Column, { items: items });
257
+ return _jsx(Column, { items: items, debug: isDebug });
209
258
  };
package/dist/ui/Plan.js CHANGED
@@ -54,6 +54,7 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
54
54
  // Mark define tasks with right arrow when no selection has been made
55
55
  if (isDefineTaskWithoutSelection) {
56
56
  item.marker = ' → ';
57
+ item.markerColor = ColorPalette[TaskType.Plan].type;
57
58
  }
58
59
  // Add children for Define tasks with options
59
60
  if (task.type === TaskType.Define && Array.isArray(task.params?.options)) {
@@ -82,7 +83,7 @@ function taskToListItem(task, highlightedChildIndex = null, isDefineTaskWithoutS
82
83
  }
83
84
  return item;
84
85
  }
85
- export function Plan({ message, tasks, state, onSelectionConfirmed, onAborted, }) {
86
+ export function Plan({ message, tasks, state, debug = false, onSelectionConfirmed, onAborted, }) {
86
87
  const [highlightedIndex, setHighlightedIndex] = useState(state?.highlightedIndex ?? null);
87
88
  const [currentDefineGroupIndex, setCurrentDefineGroupIndex] = useState(state?.currentDefineGroupIndex ?? 0);
88
89
  const [completedSelections, setCompletedSelections] = useState(state?.completedSelections ?? []);
@@ -208,5 +209,5 @@ export function Plan({ message, tasks, state, onSelectionConfirmed, onAborted, }
208
209
  !isDone;
209
210
  return taskToListItem(task, childIndex, isDefineWithoutSelection);
210
211
  });
211
- return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, children: _jsx(Label, { description: message, descriptionColor: ColorPalette[TaskType.Plan].description, type: TaskType.Plan, typeColor: ColorPalette[TaskType.Plan].type }) })), _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex })] }));
212
+ return (_jsxs(Box, { flexDirection: "column", children: [message && (_jsx(Box, { marginBottom: 1, children: _jsx(Label, { description: message, descriptionColor: ColorPalette[TaskType.Plan].description, type: TaskType.Plan, typeColor: ColorPalette[TaskType.Plan].type, showType: debug }) })), _jsx(List, { items: listItems, highlightedIndex: currentDefineTaskIndex >= 0 ? highlightedIndex : null, highlightedParentIndex: currentDefineTaskIndex, showType: debug })] }));
212
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.3.6",
3
+ "version": "0.3.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",