prompt-language-shell 0.5.0 → 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.
Files changed (40) hide show
  1. package/README.md +114 -10
  2. package/dist/config/ANSWER.md +4 -0
  3. package/dist/config/INTROSPECT.md +4 -3
  4. package/dist/config/PLAN.md +23 -0
  5. package/dist/config/VALIDATE.md +12 -11
  6. package/dist/services/components.js +54 -64
  7. package/dist/services/configuration.js +84 -0
  8. package/dist/services/messages.js +22 -0
  9. package/dist/services/queue.js +2 -2
  10. package/dist/services/refinement.js +36 -0
  11. package/dist/services/task-router.js +135 -0
  12. package/dist/types/types.js +0 -1
  13. package/dist/ui/Answer.js +18 -27
  14. package/dist/ui/Command.js +45 -27
  15. package/dist/ui/Component.js +23 -50
  16. package/dist/ui/Config.js +49 -24
  17. package/dist/ui/Confirm.js +17 -11
  18. package/dist/ui/Execute.js +66 -45
  19. package/dist/ui/Feedback.js +1 -1
  20. package/dist/ui/Introspect.js +27 -23
  21. package/dist/ui/Main.js +71 -100
  22. package/dist/ui/Message.js +1 -1
  23. package/dist/ui/Plan.js +54 -32
  24. package/dist/ui/Refinement.js +6 -7
  25. package/dist/ui/Report.js +1 -1
  26. package/dist/ui/UserQuery.js +6 -0
  27. package/dist/ui/Validate.js +49 -19
  28. package/dist/ui/Welcome.js +12 -5
  29. package/dist/ui/Workflow.js +119 -0
  30. package/package.json +1 -1
  31. package/dist/handlers/answer.js +0 -21
  32. package/dist/handlers/command.js +0 -34
  33. package/dist/handlers/config.js +0 -88
  34. package/dist/handlers/execute.js +0 -46
  35. package/dist/handlers/execution.js +0 -140
  36. package/dist/handlers/introspect.js +0 -21
  37. package/dist/handlers/plan.js +0 -79
  38. package/dist/types/handlers.js +0 -1
  39. package/dist/ui/AnswerDisplay.js +0 -8
  40. package/dist/ui/Column.js +0 -7
package/README.md CHANGED
@@ -2,6 +2,9 @@
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 may change as
6
+ > development continues.
7
+
5
8
  ## Installation
6
9
 
7
10
  ```bash
@@ -14,30 +17,74 @@ On first run, `pls` walks you through a quick setup. Your settings will be saved
14
17
 
15
18
  ## Usage
16
19
 
17
- Type `pls` followed by your request in natural language:
20
+ Type `pls` followed by your request in natural language.
21
+
22
+ To see what `pls` can
23
+ do, start by listing available capabilities:
18
24
 
19
- ```bash
20
- pls change dir to ~
21
25
  ```
26
+ $ pls list skills
27
+
28
+ Here's what I can help with:
29
+
30
+ - Introspect - list available capabilities and skills
31
+ - Config - manage and configure system settings
32
+ - Answer - respond to questions and provide information
33
+ - Execute - run shell commands and process operations
34
+ ```
22
35
 
23
- Your command will be interpreted and organized into a list of tasks:
36
+ Skills are custom workflows you can define to teach `pls` about your specific
37
+ projects and commands. Once defined, you can use them naturally:
24
38
 
25
39
  ```
26
- > pls change dir to ~
27
- - Change directory to the home folder
40
+ $ pls build project
41
+
42
+ Here's my plan.
43
+
44
+ - Navigate to project directory
45
+ - Compile source code
28
46
  ```
29
47
 
30
48
  You can provide multiple requests at once:
31
49
 
32
50
  ```
33
- > pls install deps, run tests and deploy
51
+ $ pls install deps, run tests and build
52
+
53
+ Here's what I'll do.
54
+
34
55
  - Install dependencies
35
56
  - Run tests
36
- - Deploy to server
57
+ - Build the project
58
+ ```
59
+
60
+ When `pls` needs clarification, it will present options to choose from:
61
+
62
+ ```
63
+ $ pls deploy
64
+
65
+ Let me clarify.
66
+
67
+ → Choose which environment to deploy to:
68
+ - Deploy to staging
69
+ - Deploy to production
37
70
  ```
38
71
 
39
72
  Run `pls` without arguments to see the welcome screen.
40
73
 
74
+ ## How It Works
75
+
76
+ When you make a request, `pls` interprets your intent and creates a structured
77
+ plan breaking down the work into individual tasks. You'll see this plan
78
+ displayed in your terminal before anything executes.
79
+
80
+ After reviewing the plan, you can confirm to proceed or cancel if something
81
+ doesn't look right. Once confirmed, `pls` executes each task sequentially and
82
+ shows real-time progress and results.
83
+
84
+ If you've defined custom skills, `pls` uses them to understand your
85
+ project-specific workflows and translate high-level requests into the exact
86
+ commands your environment requires.
87
+
41
88
  ## Configuration
42
89
 
43
90
  Your configuration is stored in `~/.plsrc` as a YAML file. Supported settings:
@@ -47,9 +94,66 @@ Your configuration is stored in `~/.plsrc` as a YAML file. Supported settings:
47
94
 
48
95
  ## Skills
49
96
 
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.
97
+ 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.
100
+
101
+ ### Structure
102
+
103
+ Each skill file uses a simple markdown format:
104
+
105
+ - **Name**: What you call this workflow (e.g., "Build Project")
106
+ - **Description**: What it does and any variants or options
107
+ - **Steps**: What needs to happen, in order
108
+ - **Execution** (optional): The actual shell commands to run
109
+
110
+ ### Example
111
+
112
+ Here's a skill that builds different project variants:
113
+
114
+ ```markdown
115
+ ### Name
116
+ Build Project
117
+
118
+ ### Description
119
+ Build a project in different configurations:
120
+ - dev (debug build with source maps)
121
+ - prod (optimized build)
122
+ - test (with test coverage)
123
+
124
+ ### Steps
125
+ - Navigate to the project directory
126
+ - Install dependencies if needed
127
+ - Run the {ENV} build script
128
+ - Generate build artifacts
51
129
 
52
- Your skills are referenced when planning requests, enabling `pls` to understand specific workflows, create and execute structured plans tailored to your environment.
130
+ ### Execution
131
+ - cd ~/projects/next
132
+ - npm install
133
+ - npm run build:{ENV}
134
+ - cp -r dist/ builds/{ENV}/
135
+ ```
136
+
137
+ With this skill defined, you can use natural language like:
138
+ ```
139
+ $ pls build project for production
140
+ $ pls build dev environment
141
+ $ pls build with testing enabled
142
+ ```
143
+ The `{ENV}` placeholder gets replaced with the variant you specify.
144
+ Instead of remembering the exact commands and paths for each environment, just
145
+ tell `pls` what you want in plain English. The Execution section ensures the right commands run every time.
146
+
147
+ ### Keep It Short
148
+
149
+ Skills also work with concise commands. Once you've taught `pls` about your
150
+ workflow, you can use minimal phrasing:
151
+
152
+ ```
153
+ $ pls build prod
154
+ $ pls build dev
155
+ $ pls build test
156
+ ```
53
157
 
54
158
  ## Development
55
159
 
@@ -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
@@ -133,9 +133,10 @@ Examples:
133
133
 
134
134
  When user asks "list your skills", create an introductory message like "here
135
135
  are my capabilities:" followed by tasks for built-in capabilities (Introspect,
136
- Config, Answer, Execute), then indirect workflow capabilities (Validate, Plan,
137
- Report). Each task uses type "introspect" with an action describing the
138
- capability.
136
+ Config, Answer, Execute), then indirect workflow capabilities (Plan, Validate,
137
+ Report).
138
+
139
+ Each task uses type "introspect" with an action describing the capability.
139
140
 
140
141
  ### Example 2: Filtered Skills
141
142
 
@@ -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
+ }