prompt-language-shell 0.5.2 → 0.6.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.
@@ -42,6 +42,8 @@ Provide a direct, helpful answer following these strict formatting rules:
42
42
  - Break long sentences naturally at phrase boundaries
43
43
  - If the answer requires more than 4 lines, prioritize the most essential
44
44
  information
45
+ - **Do NOT use citation tags** like `<cite>` or any HTML/XML markup
46
+ - Provide direct answers in plain text only
45
47
 
46
48
  ## Examples
47
49
 
@@ -112,8 +114,10 @@ They enable cleaner, more reusable component logic.
112
114
  ❌ Including unnecessary details
113
115
  ❌ Using overly technical jargon without explanation
114
116
  ❌ Repeating the question in the answer
117
+ ❌ Using citation tags like `<cite>` or any HTML/XML markup
115
118
 
116
119
  ✅ Direct, concise answers
117
120
  ✅ Proper line breaks at natural phrase boundaries
118
121
  ✅ Essential information only
119
122
  ✅ Clear, accessible language
123
+ ✅ Plain text only - no markup tags
@@ -280,6 +280,29 @@ Examples that should be aborted as offensive:
280
280
  - Requests to create malware or exploit vulnerabilities
281
281
  - Requests with offensive, discriminatory, or abusive language
282
282
 
283
+ **CRITICAL: Distinguishing Questions from Actions**
284
+
285
+ User requests fall into two categories:
286
+
287
+ 1. **Information requests (questions)** - Must use question keywords:
288
+ - "explain", "answer", "describe", "tell me", "say", "what is", "what are",
289
+ "how does", "how do", "find", "search", "lookup"
290
+ - Example: "pls explain TypeScript" → answer type
291
+ - Example: "pls what is the weather" → answer type
292
+
293
+ 2. **Action requests (commands)** - Must match available skills:
294
+ - Verbs like "test", "deploy", "process", "backup", "sync"
295
+ - If verb matches a skill → use that skill
296
+ - If verb does NOT match any skill → use "ignore" type
297
+ - Example: "pls test" with no test skill → ignore type
298
+ - Example: "pls reverberate" with no reverberate skill → ignore type
299
+ - Example: "pls shut down" with no shutdown skill → ignore type
300
+
301
+ **Critical rule:** Requests using action verbs that don't match question
302
+ keywords AND don't match any available skills should ALWAYS be classified
303
+ as "ignore" type. Do NOT try to infer or create generic execute tasks for
304
+ unrecognized verbs.
305
+
283
306
  **For requests with clear intent:**
284
307
 
285
308
  1. **Introspection requests** - Use "introspect" type when request asks about
@@ -29,12 +29,12 @@ For each CONFIG task, create a natural language description that:
29
29
 
30
30
  ## Description Format
31
31
 
32
- **Format:** "Brief description" (NO {config.path} at the end!)
32
+ **Format:** "Brief description" (DO NOT include {config.path}!)
33
33
 
34
34
  The description should:
35
35
  - Start with what the config value represents (e.g., "Path to...", "URL for...", "Name of...")
36
36
  - Be SHORT and direct - no extra details or variant explanations
37
- - NOT include the config path in curly brackets - that's added automatically
37
+ - NEVER include the config path in curly brackets like {config.path}
38
38
 
39
39
  ## Examples
40
40
 
@@ -50,7 +50,7 @@ The description should:
50
50
  message: ""
51
51
  tasks: [
52
52
  {
53
- action: "Path to Alpha repository {project.alpha.repo}",
53
+ action: "Path to Alpha repository",
54
54
  type: "config",
55
55
  params: { key: "project.alpha.repo" }
56
56
  }
@@ -69,7 +69,7 @@ tasks: [
69
69
  message: ""
70
70
  tasks: [
71
71
  {
72
- action: "Staging environment URL {env.staging.url}",
72
+ action: "Staging environment URL",
73
73
  type: "config",
74
74
  params: { key: "env.staging.url" }
75
75
  }
@@ -88,7 +88,7 @@ tasks: [
88
88
  message: ""
89
89
  tasks: [
90
90
  {
91
- action: "Path to Beta workspace {workspace.beta.path}",
91
+ action: "Path to Beta workspace",
92
92
  type: "config",
93
93
  params: { key: "workspace.beta.path" }
94
94
  }
@@ -98,10 +98,10 @@ tasks: [
98
98
  ## Guidelines
99
99
 
100
100
  1. **Use skill context**: Read the skill's Description section to understand what the variant represents
101
- 2. **Be specific**: Don't just say "Repository path" - say "Alpha project repository path"
102
- 3. **Add helpful details**: Include information from the description (e.g., "legacy implementation")
103
- 4. **Keep it concise**: One sentence that clearly explains what's needed
104
- 5. **Always include the path**: End with `{config.path}` for technical reference
101
+ 2. **Be specific**: Don't just say "Repository path" - say "Path to Alpha repository"
102
+ 3. **Add helpful details**: Include information from the description when relevant
103
+ 4. **Keep it concise**: One brief phrase that clearly explains what's needed
104
+ 5. **Never include the path**: Do not append `{config.path}` - it's shown separately in debug mode
105
105
 
106
106
  ## Common Config Types
107
107
 
@@ -122,7 +122,7 @@ Return a message field (can be empty string) and an array of CONFIG tasks:
122
122
  message: ""
123
123
  tasks: [
124
124
  {
125
- action: "Natural description {config.path}",
125
+ action: "Natural description without config path",
126
126
  type: "config",
127
127
  params: { key: "config.path" }
128
128
  },
@@ -136,4 +136,5 @@ tasks: [
136
136
  - All tasks must include params.key with the config path
137
137
  - Descriptions should be helpful and contextual, not just technical
138
138
  - Use information from Available Skills section to provide context
139
- - Keep descriptions to one concise sentence
139
+ - Keep descriptions to one brief phrase (3-6 words)
140
+ - NEVER include the config path in the action/description - it's shown separately
@@ -1,11 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
2
3
  import { ComponentName } from '../types/types.js';
3
- import { getConfigSchema, loadConfig, } from './configuration.js';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
4
6
  import { getConfirmationMessage } from './messages.js';
5
7
  import { StepType } from '../ui/Config.js';
6
- export function markAsDone(component) {
7
- return { ...component, state: { ...component.state, done: true } };
8
- }
9
8
  export function createWelcomeDefinition(app) {
10
9
  return {
11
10
  id: randomUUID(),
@@ -58,27 +57,43 @@ function getValidator(definition) {
58
57
  export function createConfigStepsFromSchema(keys) {
59
58
  const schema = getConfigSchema();
60
59
  let currentConfig = null;
60
+ let rawConfig = null;
61
+ // Load validated config (may fail if config has validation errors)
61
62
  try {
62
63
  currentConfig = loadConfig();
63
64
  }
64
65
  catch {
65
- // Config doesn't exist yet, use defaults
66
+ // Config doesn't exist or has validation errors, use defaults
67
+ }
68
+ // Load raw config separately (for discovered keys not in schema)
69
+ try {
70
+ const configFile = getConfigPath();
71
+ if (existsSync(configFile)) {
72
+ const content = readFileSync(configFile, 'utf-8');
73
+ rawConfig = parseYaml(content);
74
+ }
75
+ }
76
+ catch {
77
+ // Config file doesn't exist or can't be parsed
66
78
  }
67
79
  return keys.map((key) => {
68
80
  // Check if key is in schema (built-in config)
69
81
  if (!(key in schema)) {
70
- // Key is not in schema - it's from a skill
71
- // Create a simple text step with placeholder description
82
+ // Key is not in schema - it's from a skill or discovered config
83
+ // Create a simple text step with the full path as description
72
84
  const keyParts = key.split('.');
73
85
  const shortKey = keyParts[keyParts.length - 1];
74
- const description = keyParts
75
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
76
- .join(' ');
86
+ // Load current value if it exists (use rawConfig since discovered keys aren't in validated config)
87
+ const currentValue = getConfigValue(rawConfig, key);
88
+ const value = currentValue !== undefined && typeof currentValue === 'string'
89
+ ? currentValue
90
+ : null;
77
91
  return {
78
- description: `${description} {${key}}`,
92
+ description: key,
79
93
  key: shortKey,
94
+ path: key,
80
95
  type: StepType.Text,
81
- value: null,
96
+ value,
82
97
  validate: () => true, // Accept any string for now
83
98
  };
84
99
  }
@@ -98,6 +113,7 @@ export function createConfigStepsFromSchema(keys) {
98
113
  return {
99
114
  description: definition.description,
100
115
  key: shortKey,
116
+ path: key,
101
117
  type: StepType.Text,
102
118
  value,
103
119
  validate: getValidator(definition),
@@ -112,6 +128,7 @@ export function createConfigStepsFromSchema(keys) {
112
128
  return {
113
129
  description: definition.description,
114
130
  key: shortKey,
131
+ path: key,
115
132
  type: StepType.Text,
116
133
  value,
117
134
  validate: getValidator(definition),
@@ -127,6 +144,7 @@ export function createConfigStepsFromSchema(keys) {
127
144
  return {
128
145
  description: definition.description,
129
146
  key: shortKey,
147
+ path: key,
130
148
  type: StepType.Selection,
131
149
  options: definition.values.map((value) => ({
132
150
  label: value,
@@ -143,6 +161,7 @@ export function createConfigStepsFromSchema(keys) {
143
161
  return {
144
162
  description: definition.description,
145
163
  key: shortKey,
164
+ path: key,
146
165
  type: StepType.Selection,
147
166
  options: [
148
167
  { label: 'Yes', value: 'true' },
@@ -159,7 +178,7 @@ export function createConfigDefinition(onFinished, onAborted) {
159
178
  return {
160
179
  id: randomUUID(),
161
180
  name: ComponentName.Config,
162
- state: { done: false },
181
+ state: {},
163
182
  props: {
164
183
  steps: createConfigSteps(),
165
184
  onFinished,
@@ -174,7 +193,7 @@ export function createConfigDefinitionWithKeys(keys, onFinished, onAborted) {
174
193
  return {
175
194
  id: randomUUID(),
176
195
  name: ComponentName.Config,
177
- state: { done: false },
196
+ state: {},
178
197
  props: {
179
198
  steps: createConfigStepsFromSchema(keys),
180
199
  onFinished,
@@ -182,29 +201,22 @@ export function createConfigDefinitionWithKeys(keys, onFinished, onAborted) {
182
201
  },
183
202
  };
184
203
  }
185
- export function createCommandDefinition(command, service, onError, onComplete, onAborted) {
204
+ export function createCommandDefinition(command, service) {
186
205
  return {
187
206
  id: randomUUID(),
188
207
  name: ComponentName.Command,
189
- state: {
190
- done: false,
191
- isLoading: true,
192
- },
208
+ state: {},
193
209
  props: {
194
210
  command,
195
211
  service,
196
- onError,
197
- onComplete,
198
- onAborted,
199
212
  },
200
213
  };
201
214
  }
202
- export function createPlanDefinition(message, tasks, onAborted, onSelectionConfirmed) {
215
+ export function createPlanDefinition(message, tasks, onSelectionConfirmed) {
203
216
  return {
204
217
  id: randomUUID(),
205
218
  name: ComponentName.Plan,
206
219
  state: {
207
- done: false,
208
220
  highlightedIndex: null,
209
221
  currentDefineGroupIndex: 0,
210
222
  completedSelections: [],
@@ -213,7 +225,6 @@ export function createPlanDefinition(message, tasks, onAborted, onSelectionConfi
213
225
  message,
214
226
  tasks,
215
227
  onSelectionConfirmed,
216
- onAborted,
217
228
  },
218
229
  };
219
230
  }
@@ -240,7 +251,7 @@ export function createRefinement(text, onAborted) {
240
251
  return {
241
252
  id: randomUUID(),
242
253
  name: ComponentName.Refinement,
243
- state: { done: false },
254
+ state: {},
244
255
  props: {
245
256
  text,
246
257
  onAborted,
@@ -251,7 +262,7 @@ export function createConfirmDefinition(onConfirmed, onCancelled) {
251
262
  return {
252
263
  id: randomUUID(),
253
264
  name: ComponentName.Confirm,
254
- state: { done: false },
265
+ state: {},
255
266
  props: {
256
267
  message: getConfirmationMessage(),
257
268
  onConfirmed,
@@ -259,20 +270,14 @@ export function createConfirmDefinition(onConfirmed, onCancelled) {
259
270
  },
260
271
  };
261
272
  }
262
- export function createIntrospectDefinition(tasks, service, onError, onComplete, onAborted) {
273
+ export function createIntrospectDefinition(tasks, service) {
263
274
  return {
264
275
  id: randomUUID(),
265
276
  name: ComponentName.Introspect,
266
- state: {
267
- done: false,
268
- isLoading: true,
269
- },
277
+ state: {},
270
278
  props: {
271
279
  tasks,
272
280
  service,
273
- onError,
274
- onComplete,
275
- onAborted,
276
281
  },
277
282
  };
278
283
  }
@@ -286,49 +291,37 @@ export function createReportDefinition(message, capabilities) {
286
291
  },
287
292
  };
288
293
  }
289
- export function createAnswerDefinition(question, service, onError, onComplete, onAborted) {
294
+ export function createAnswerDefinition(question, service) {
290
295
  return {
291
296
  id: randomUUID(),
292
297
  name: ComponentName.Answer,
293
- state: {
294
- done: false,
295
- isLoading: true,
296
- },
298
+ state: {},
297
299
  props: {
298
300
  question,
299
301
  service,
300
- onError,
301
- onComplete,
302
- onAborted,
303
- },
304
- };
305
- }
306
- export function createAnswerDisplayDefinition(answer) {
307
- return {
308
- id: randomUUID(),
309
- name: ComponentName.AnswerDisplay,
310
- props: {
311
- answer,
312
302
  },
313
303
  };
314
304
  }
315
305
  export function isStateless(component) {
316
306
  return !('state' in component);
317
307
  }
318
- export function createExecuteDefinition(tasks, service, onError, onComplete, onAborted) {
308
+ /**
309
+ * Mark a component as done. Returns the component to be added to timeline.
310
+ * Components use handlers.updateState to save their state before completion,
311
+ * so this function simply returns the component as-is.
312
+ */
313
+ export function markAsDone(component) {
314
+ // State already updated via handlers.updateState
315
+ return component;
316
+ }
317
+ export function createExecuteDefinition(tasks, service) {
319
318
  return {
320
319
  id: randomUUID(),
321
320
  name: ComponentName.Execute,
322
- state: {
323
- done: false,
324
- isLoading: true,
325
- },
321
+ state: {},
326
322
  props: {
327
323
  tasks,
328
324
  service,
329
- onError,
330
- onComplete,
331
- onAborted,
332
325
  },
333
326
  };
334
327
  }
@@ -336,10 +329,7 @@ export function createValidateDefinition(missingConfig, userRequest, service, on
336
329
  return {
337
330
  id: randomUUID(),
338
331
  name: ComponentName.Validate,
339
- state: {
340
- done: false,
341
- isLoading: true,
342
- },
332
+ state: {},
343
333
  props: {
344
334
  missingConfig,
345
335
  userRequest,
@@ -123,6 +123,7 @@ export function saveConfig(section, config) {
123
123
  }
124
124
  export function saveAnthropicConfig(config) {
125
125
  saveConfig('anthropic', config);
126
+ return loadConfig();
126
127
  }
127
128
  export function saveDebugSetting(debug) {
128
129
  saveConfig('settings', { debug });
@@ -200,6 +201,67 @@ export function getConfigSchema() {
200
201
  // Future: ...loadSkillSchemas()
201
202
  };
202
203
  }
204
+ /**
205
+ * Get missing required configuration keys
206
+ * Returns array of keys that are required but not present or invalid in config
207
+ */
208
+ export function getMissingConfigKeys() {
209
+ const schema = getConfigSchema();
210
+ const missing = [];
211
+ let currentConfig = null;
212
+ try {
213
+ currentConfig = loadConfig();
214
+ }
215
+ catch {
216
+ // Config doesn't exist
217
+ }
218
+ for (const [key, definition] of Object.entries(schema)) {
219
+ if (!definition.required) {
220
+ continue;
221
+ }
222
+ // Get current value for this key
223
+ const parts = key.split('.');
224
+ let value = currentConfig;
225
+ for (const part of parts) {
226
+ if (value && typeof value === 'object' && part in value) {
227
+ value = value[part];
228
+ }
229
+ else {
230
+ value = undefined;
231
+ break;
232
+ }
233
+ }
234
+ // Check if value is missing or invalid
235
+ if (value === undefined || value === null) {
236
+ missing.push(key);
237
+ continue;
238
+ }
239
+ // Validate based on type
240
+ let isValid = false;
241
+ switch (definition.type) {
242
+ case 'regexp':
243
+ isValid = typeof value === 'string' && definition.pattern.test(value);
244
+ break;
245
+ case 'string':
246
+ isValid = typeof value === 'string';
247
+ break;
248
+ case 'enum':
249
+ isValid =
250
+ typeof value === 'string' && definition.values.includes(value);
251
+ break;
252
+ case 'number':
253
+ isValid = typeof value === 'number';
254
+ break;
255
+ case 'boolean':
256
+ isValid = typeof value === 'boolean';
257
+ break;
258
+ }
259
+ if (!isValid) {
260
+ missing.push(key);
261
+ }
262
+ }
263
+ return missing;
264
+ }
203
265
  /**
204
266
  * Get available config structure for CONFIG tool
205
267
  * Returns keys with descriptions only (no values for privacy)
@@ -246,3 +308,25 @@ export function getAvailableConfigStructure() {
246
308
  }
247
309
  return structure;
248
310
  }
311
+ /**
312
+ * Unflatten dotted keys into nested structure
313
+ * Example: { "product.alpha.path": "value" } -> { product: { alpha: { path: "value" } } }
314
+ */
315
+ export function unflattenConfig(dotted) {
316
+ const result = {};
317
+ for (const [dottedKey, value] of Object.entries(dotted)) {
318
+ const parts = dottedKey.split('.');
319
+ const section = parts[0];
320
+ // Initialize section if needed
321
+ result[section] = result[section] ?? {};
322
+ // Build nested structure for this section
323
+ let current = result[section];
324
+ for (let i = 1; i < parts.length - 1; i++) {
325
+ current[parts[i]] = current[parts[i]] ?? {};
326
+ current = current[parts[i]];
327
+ }
328
+ // Set final value
329
+ current[parts[parts.length - 1]] = value;
330
+ }
331
+ return result;
332
+ }
@@ -43,6 +43,28 @@ export function getCancellationMessage(operation) {
43
43
  ];
44
44
  return templates[Math.floor(Math.random() * templates.length)];
45
45
  }
46
+ /**
47
+ * Returns an error message when the request cannot be understood.
48
+ * Randomly selects from variations to sound natural.
49
+ */
50
+ export function getUnknownRequestMessage() {
51
+ const messages = [
52
+ 'I do not understand the request.',
53
+ 'I cannot understand what you want me to do.',
54
+ "I'm not sure what you're asking for.",
55
+ 'I cannot determine what action to take.',
56
+ 'This request is unclear to me.',
57
+ 'I do not recognize this command.',
58
+ ];
59
+ return messages[Math.floor(Math.random() * messages.length)];
60
+ }
61
+ /**
62
+ * Returns an error message for mixed task types.
63
+ */
64
+ export function getMixedTaskTypesError(types) {
65
+ const typeList = types.join(', ');
66
+ return `Mixed task types are not supported. Found: ${typeList}. All tasks in a plan must have the same type.`;
67
+ }
46
68
  /**
47
69
  * Feedback messages for various operations
48
70
  */
@@ -1,5 +1,5 @@
1
1
  import { FeedbackType } from '../types/types.js';
2
- import { createFeedback, markAsDone } from './components.js';
2
+ import { createFeedback } from './components.js';
3
3
  import { FeedbackMessages } from './messages.js';
4
4
  import { exitApp } from './process.js';
5
5
  /**
@@ -37,7 +37,7 @@ export function withQueueHandler(componentName, callback, shouldExit = false, ex
37
37
  */
38
38
  export function createErrorHandler(componentName, addToTimeline) {
39
39
  return (error) => withQueueHandler(componentName, (first) => {
40
- addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, error));
40
+ addToTimeline(first, createFeedback(FeedbackType.Failed, FeedbackMessages.UnexpectedError, error));
41
41
  return undefined;
42
42
  }, true, 1);
43
43
  }
@@ -0,0 +1,36 @@
1
+ import { createRefinement } from './components.js';
2
+ import { formatErrorMessage, getRefiningMessage } from './messages.js';
3
+ import { routeTasksWithConfirm } from './task-router.js';
4
+ /**
5
+ * Handle refinement flow for DEFINE tasks
6
+ * Called when user selects options from a plan with DEFINE tasks
7
+ */
8
+ export async function handleRefinement(selectedTasks, service, originalCommand, handlers) {
9
+ // Create and add refinement component to queue
10
+ const refinementDef = createRefinement(getRefiningMessage(), (operation) => {
11
+ handlers.onAborted(operation);
12
+ });
13
+ handlers.addToQueue(refinementDef);
14
+ try {
15
+ // Build refined command from selected tasks
16
+ const refinedCommand = selectedTasks
17
+ .map((task) => {
18
+ const action = task.action.toLowerCase().replace(/,/g, ' -');
19
+ const type = task.type;
20
+ return `${action} (type: ${type})`;
21
+ })
22
+ .join(', ');
23
+ // Call LLM to refine plan with selected tasks
24
+ const refinedResult = await service.processWithTool(refinedCommand, 'plan');
25
+ // Complete the Refinement component
26
+ handlers.completeActive();
27
+ // Route refined tasks to appropriate components
28
+ routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false // No DEFINE tasks in refined result
29
+ );
30
+ }
31
+ catch (err) {
32
+ handlers.completeActive();
33
+ const errorMessage = formatErrorMessage(err);
34
+ handlers.onError(errorMessage);
35
+ }
36
+ }