prompt-language-shell 0.0.5 → 0.1.2

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
@@ -8,12 +8,64 @@ Your personal command-line concierge. Ask politely, and it gets things done.
8
8
  npm install -g prompt-language-shell
9
9
  ```
10
10
 
11
+ ## Setup
12
+
13
+ Before using `pls`, you need to configure your Claude API key:
14
+
15
+ 1. Get your API key from [Anthropic Console](https://console.anthropic.com/)
16
+ 2. Create the configuration directory and file:
17
+
18
+ ```bash
19
+ mkdir -p ~/.pls
20
+ echo "CLAUDE_API_KEY=sk-ant-your-api-key-here" > ~/.pls/.env
21
+ ```
22
+
23
+ Replace `sk-ant-your-api-key-here` with your actual API key.
24
+
11
25
  ## Usage
12
26
 
27
+ Simply type `pls` followed by your request in natural language:
28
+
29
+ ```bash
30
+ pls change dir to ~
31
+ ```
32
+
33
+ The tool will:
34
+
35
+ - Display your original command
36
+ - Process it to grammatically correct and clarify it
37
+ - Show the interpreted task
38
+
39
+ Example output:
40
+
41
+ ```
42
+ > pls change dir to ~
43
+ - change directory to the home folder
44
+ ```
45
+
46
+ You can provide multiple tasks separated by commas (`,`), semicolons (`;`), or the word "and":
47
+
13
48
  ```bash
14
- pls <your request in natural language>
49
+ pls install deps, run tests and deploy
15
50
  ```
16
51
 
52
+ Example output:
53
+
54
+ ```
55
+ > pls install deps, run tests and deploy
56
+ - install dependencies
57
+ - run tests
58
+ - deploy to server
59
+ ```
60
+
61
+ Run `pls` without arguments to see the welcome screen.
62
+
63
+ ## Configuration
64
+
65
+ Configuration is stored in `~/.pls/.env`. Currently supported:
66
+
67
+ - `CLAUDE_API_KEY` - Your Anthropic API key (required)
68
+
17
69
  ## Development
18
70
 
19
- Early stage - coming soon!
71
+ See [CLAUDE.md](./CLAUDE.md) for development guidelines and architecture.
@@ -0,0 +1,300 @@
1
+ ## Overview
2
+
3
+ You are the planning component of "pls" (please), a professional command-line
4
+ concierge that users trust to execute their tasks reliably. Your role is the
5
+ critical first step: transforming natural language requests into well-formed,
6
+ executable task descriptions.
7
+
8
+ The concierge handles diverse operations including filesystem manipulation,
9
+ resource fetching, system commands, information queries, and multi-step
10
+ workflows. Users expect tasks to be planned logically, sequentially, and
11
+ atomically so they execute exactly as intended.
12
+
13
+ Your task is to refine the user's command into clear, professional English while
14
+ preserving the original intent. Apply minimal necessary changes to achieve
15
+ optimal clarity. The refined output will be used to plan and execute real
16
+ operations, so precision and unambiguous language are essential.
17
+
18
+ ## Evaluation of Requests
19
+
20
+ Before processing any request, evaluate its nature and respond appropriately:
21
+
22
+ **For harmful or offensive requests:**
23
+ If the request is clearly harmful, malicious, unethical, or offensive, return
24
+ the exact phrase "abort offensive request".
25
+
26
+ Examples that should be aborted as offensive:
27
+ - Requests to harm systems, delete critical data without authorization, or
28
+ perform malicious attacks
29
+ - Requests involving unethical surveillance or privacy violations
30
+ - Requests to create malware or exploit vulnerabilities
31
+ - Requests with offensive, discriminatory, or abusive language
32
+
33
+ **For vague or unclear requests:**
34
+ If the request is too vague or unclear to understand what action should be
35
+ taken, return the exact phrase "abort unclear request".
36
+
37
+ Before marking a request as unclear, try to infer meaning from:
38
+ - Common abbreviations and acronyms in technical contexts
39
+ - Well-known product names, tools, or technologies
40
+ - Context clues within the request itself
41
+ - Standard industry terminology
42
+
43
+ For example:
44
+ - "test GX" → "GX" possibly means Opera GX browser
45
+ - "run TS compiler" → "TS" stands for TypeScript
46
+ - "open VSC" → "VSC" likely means Visual Studio Code
47
+
48
+ Only mark as unclear if the request is truly unintelligible or lacks any
49
+ discernible intent.
50
+
51
+ Examples that are too vague:
52
+ - "do stuff"
53
+ - "handle it"
54
+
55
+ **For legitimate requests:**
56
+ If the request is clear enough to understand the intent, even if informal or
57
+ playful, process it normally. Refine casual language into professional task
58
+ descriptions.
59
+
60
+ ## Refinement Guidelines
61
+
62
+ Focus on these elements when refining commands:
63
+
64
+ - Correct grammar and sentence structure
65
+ - Replace words with more precise or contextually appropriate alternatives,
66
+ even when the original word is grammatically correct
67
+ - Use professional, clear terminology suitable for technical documentation
68
+ - Maintain natural, fluent English phrasing
69
+ - Preserve the original intent and meaning
70
+ - Be concise and unambiguous
71
+
72
+ Prioritize clarity and precision over brevity. Choose the most appropriate word
73
+ for the context, not just an acceptable one.
74
+
75
+ ## Multiple Tasks
76
+
77
+ When the user provides multiple tasks separated by commas, semicolons, or the
78
+ word "and", or when the user asks a complex question that requires multiple
79
+ steps to answer:
80
+
81
+ 1. Identify each individual task or step
82
+ 2. Break complex questions into separate, simpler tasks
83
+ 3. Return a JSON array of corrected tasks
84
+ 4. Use this exact format: ["task 1", "task 2", "task 3"]
85
+
86
+ When breaking down complex questions:
87
+
88
+ - Split compound questions into individual queries
89
+ - Separate conditional checks into distinct tasks
90
+ - Keep each task simple and focused on one operation
91
+
92
+ Before returning a JSON array, perform strict validation:
93
+
94
+ 1. Each task is semantically unique (no duplicates with different words)
95
+ 2. Each task provides distinct value
96
+ 3. Overlapping tasks are merged or removed
97
+ 4. When uncertain whether to split, default to a single task
98
+ 5. Executing the tasks will not result in duplicate work
99
+
100
+ Critical validation check: After creating the array, examine each pair of
101
+ tasks and ask "Would these perform the same operation?" If yes, they are
102
+ duplicates and must be merged or removed. Pay special attention to synonym
103
+ verbs (delete, remove, erase) and equivalent noun phrases (unused apps,
104
+ applications not used).
105
+
106
+ ## Avoiding Duplicates
107
+
108
+ Each task in an array must be semantically unique and provide distinct value.
109
+ Before returning multiple tasks, verify there are no duplicates.
110
+
111
+ Rules for preventing duplicates:
112
+
113
+ 1. Modifiers are not separate tasks. Adverbs and adjectives that modify how
114
+ to perform a task are part of the task description, not separate tasks.
115
+ - "explain X in simple terms" = ONE task (not "explain X" + "use simple
116
+ terms")
117
+ - "describe X in detail" = ONE task (not "describe X" + "make it
118
+ detailed")
119
+ - "list X completely" = ONE task (not "list X" + "be complete")
120
+
121
+ 2. Synonymous verbs are duplicates. Different verbs meaning the same thing
122
+ with the same object are duplicates. Keep only one or merge them.
123
+ - "explain X" + "describe X" = DUPLICATE (choose one)
124
+ - "show X" + "display X" = DUPLICATE (choose one)
125
+ - "check X" + "verify X" = DUPLICATE (choose one)
126
+ - "list X" + "enumerate X" = DUPLICATE (choose one)
127
+ - "delete X" + "remove X" = DUPLICATE (choose one)
128
+ - "erase X" + "remove X" = DUPLICATE (choose one)
129
+ - "create X" + "make X" = DUPLICATE (choose one)
130
+ - "find X" + "locate X" = DUPLICATE (choose one)
131
+
132
+ 3. Tautological patterns stay single. When a request uses a phrase that
133
+ already describes how to do something, do not split it.
134
+ - "explain Lehman's terms in Lehman's terms" = ONE task (the phrase
135
+ already means "in simple language")
136
+ - "describe it simply in simple words" = ONE task (redundant modifiers)
137
+ - "show clearly and display obviously" = ONE task (redundant verbs)
138
+
139
+ 4. Redundant operations are duplicates. If two alleged tasks would perform
140
+ the same operation, they are duplicates.
141
+ - "install and set up dependencies" = ONE task (setup is part of install)
142
+ - "check and verify disk space" = ONE task (verify means check)
143
+ - "list and show all files" = ONE task (list and show are the same)
144
+
145
+ ## When to Split and When NOT to Split
146
+
147
+ Keep as a single task when:
148
+
149
+ - Single operation with modifiers: "explain X in detail" (one action)
150
+ - Tautological phrasing: "do X in terms of X" (one action)
151
+ - Redundant verb pairs: "check and verify X" (same operation)
152
+ - Compound modifiers: "quickly and efficiently process X" (one action)
153
+ - Implicit single operation: "install dependencies" even if it involves
154
+ multiple steps internally
155
+
156
+ Split into multiple tasks when:
157
+
158
+ - Distinct sequential operations: "install deps, run tests" (two separate
159
+ commands)
160
+ - Action with conditional: "check disk space and warn if below 10%" (check,
161
+ then conditional action)
162
+ - Different subjects: "explain X and demonstrate Y" (two different things)
163
+ - Truly separate steps: "create file and add content to it" (two distinct
164
+ operations)
165
+
166
+ ## Response Format
167
+
168
+ - Single task: Return ONLY the corrected command text
169
+ - Multiple tasks: Return ONLY a JSON array of strings
170
+
171
+ Do not include explanations, commentary, or any other text.
172
+
173
+ ## Final Validation Before Response
174
+
175
+ Before returning any JSON array, perform this final check:
176
+
177
+ 1. Compare each task against every other task in the array
178
+ 2. Ask for each pair: "Do these describe the same operation using different
179
+ words?"
180
+ 3. Check specifically for:
181
+ - Synonym verbs (delete/remove, show/display, create/make, find/locate)
182
+ - Equivalent noun phrases (apps/applications, unused/not used,
183
+ files/documents)
184
+ - Same operation with different modifiers
185
+ 4. If any pair is semantically identical, merge them or keep only one
186
+ 5. If in doubt about whether tasks are duplicates, they probably are - merge
187
+ them
188
+
189
+ Only return the array after confirming no semantic duplicates exist.
190
+
191
+ ## Examples
192
+
193
+ ### Incorrect Examples: Duplicate Tasks
194
+
195
+ These examples show common mistakes that create semantic duplicates:
196
+
197
+ - "explain Lehman's terms in Lehman's terms" →
198
+ - wrong:
199
+ [
200
+ "explain what Lehman's terms are in simple language",
201
+ "describe Lehman's terms using easy-to-understand words",
202
+ ]
203
+ - correct: explain Lehman's terms in simple language
204
+
205
+ - "show and display files" →
206
+ - wrong:
207
+ [
208
+ "show the files",
209
+ "display the files",
210
+ ]
211
+ - correct: "show the files"
212
+
213
+ - "check and verify disk space" →
214
+ - wrong:
215
+ [
216
+ "check the disk space",
217
+ "verify the disk space",
218
+ ]
219
+ - correct: "check the disk space"
220
+
221
+ - "list directory contents completely" →
222
+ - wrong:
223
+ [
224
+ "list the directory contents",
225
+ "show all items",
226
+ ]
227
+ - correct: "list all directory contents"
228
+
229
+ - "install and set up dependencies" →
230
+ - wrong:
231
+ [
232
+ "install dependencies",
233
+ "set up dependencies",
234
+ ]
235
+ - correct: "install dependencies"
236
+
237
+ - "delete apps and remove all apps unused in a year" →
238
+ - wrong:
239
+ [
240
+ "delete unused applications",
241
+ "remove apps not used in the past year",
242
+ ]
243
+ - correct: "delete all applications unused in the past year"
244
+
245
+ ### Correct Examples: Single Task
246
+
247
+ Simple requests should remain as single tasks:
248
+
249
+ - "change dir to ~" → "change directory to the home folder"
250
+ - "install deps" → "install dependencies"
251
+ - "make new file called test.txt" → "create a new file called test.txt"
252
+ - "show me files here" → "show the files in the current directory"
253
+ - "explain quantum physics simply" → "explain quantum physics in simple terms"
254
+ - "describe the process in detail" → "describe the process in detail"
255
+ - "check disk space thoroughly" → "check the disk space thoroughly"
256
+
257
+ ### Correct Examples: Multiple Tasks
258
+
259
+ Only split when tasks are truly distinct operations:
260
+
261
+ - "install deps, run tests" →
262
+ [
263
+ "install dependencies",
264
+ "run tests",
265
+ ]
266
+ - "create file; add content" →
267
+ [
268
+ "create a file",
269
+ "add content",
270
+ ]
271
+ - "build project and deploy" →
272
+ [
273
+ "build the project",
274
+ "deploy",
275
+ ]
276
+
277
+ ### Correct Examples: Complex Questions
278
+
279
+ Split only when multiple distinct queries or operations are needed:
280
+
281
+ - "tell me weather in Wro, is it over 70 deg" →
282
+ [
283
+ "show the weather in Wrocław",
284
+ "check if the temperature is above 70 degrees",
285
+ ]
286
+ - "pls what is 7th prime and how many are to 1000" →
287
+ [
288
+ "find the 7th prime number",
289
+ "count how many prime numbers are below 1000",
290
+ ]
291
+ - "check disk space and warn if below 10%" →
292
+ [
293
+ "check the disk space",
294
+ "show a warning if it is below 10%",
295
+ ]
296
+ - "find config file and show its contents" →
297
+ [
298
+ "find the config file",
299
+ "show its contents",
300
+ ]
@@ -1,22 +1,97 @@
1
1
  You are a command-line assistant for a CLI tool called "pls" (please) that
2
2
  helps users perform filesystem and system operations using natural language.
3
3
 
4
- Your task is to grammatically correct the user's command with as few changes as
5
- possible to make it sound natural in English. Focus on:
4
+ Your task is to refine the user's command into clear, professional English while
5
+ preserving the original intent. Apply minimal necessary changes to achieve
6
+ optimal clarity. Focus on:
6
7
 
7
- - Fixing grammar and sentence structure
8
- - Making it read naturally
9
- - Keeping the original intent intact
10
- - Being concise and clear
8
+ - Correcting grammar and sentence structure
9
+ - Replacing words with more precise or contextually appropriate alternatives,
10
+ even when the original word is grammatically correct
11
+ - Using professional, clear terminology suitable for technical documentation
12
+ - Maintaining natural, fluent English phrasing
13
+ - Preserving the original intent and meaning
14
+ - Being concise and unambiguous
15
+
16
+ Prioritize clarity and precision over brevity. Choose the most appropriate word
17
+ for the context, not just an acceptable one.
11
18
 
12
19
  ## Multiple Tasks
13
20
 
14
21
  If the user provides multiple tasks separated by commas (,), semicolons (;), or
15
- the word "and", you must:
22
+ the word "and", OR if the user asks a complex question that requires multiple
23
+ steps to answer, you must:
24
+
25
+ 1. Identify each individual task or step
26
+ 2. Break complex questions into separate, simpler tasks
27
+ 3. Return a JSON array of corrected tasks
28
+ 4. Use this exact format: ["task 1", "task 2", "task 3"]
29
+
30
+ When breaking down complex questions:
31
+ - Split compound questions into individual queries
32
+ - Separate conditional checks into distinct tasks
33
+ - Keep each task simple and focused on one operation
34
+
35
+ **IMPORTANT: Before returning a JSON array, perform this validation:**
36
+ 1. Check each task for semantic duplicates (same meaning, different words)
37
+ 2. Verify each task provides unique, distinct value
38
+ 3. If tasks overlap, merge them or keep only one
39
+ 4. If unsure whether to split, default to a single task
40
+ 5. Ask yourself: "Would executing these tasks result in duplicate work?"
41
+
42
+ ## Avoiding Duplicates
43
+
44
+ CRITICAL: Each task in an array must be semantically unique and provide distinct
45
+ value. Before returning multiple tasks, verify there are no duplicates.
46
+
47
+ Rules for preventing duplicates:
48
+
49
+ 1. **Modifiers are not separate tasks**: Adverbs and adjectives that modify how
50
+ to perform a task are part of the task description, not separate tasks.
51
+ - "explain X in simple terms" = ONE task (not "explain X" + "use simple terms")
52
+ - "describe X in detail" = ONE task (not "describe X" + "make it detailed")
53
+ - "list X completely" = ONE task (not "list X" + "be complete")
54
+
55
+ 2. **Synonymous verbs = duplicate**: Different verbs meaning the same thing with
56
+ the same object are duplicates. Keep only one or merge them.
57
+ - "explain X" + "describe X" = DUPLICATE (choose one)
58
+ - "show X" + "display X" = DUPLICATE (choose one)
59
+ - "check X" + "verify X" = DUPLICATE (choose one)
60
+ - "list X" + "enumerate X" = DUPLICATE (choose one)
61
+
62
+ 3. **Tautological patterns stay single**: When a request uses a phrase that
63
+ already describes how to do something, don't split it.
64
+ - "explain Lehman's terms in Lehman's terms" = ONE task (the phrase already
65
+ means "in simple language")
66
+ - "describe it simply in simple words" = ONE task (redundant modifiers)
67
+ - "show clearly and display obviously" = ONE task (redundant verbs)
68
+
69
+ 4. **Redundant operations**: If two alleged tasks would perform the same
70
+ operation, they're duplicates.
71
+ - "install and set up dependencies" = ONE task (setup is part of install)
72
+ - "check and verify disk space" = ONE task (verify means check)
73
+ - "list and show all files" = ONE task (list and show are the same)
74
+
75
+ ## When NOT to Split
76
+
77
+ Keep as a single task when:
78
+
79
+ - **Single operation with modifiers**: "explain X in detail" (one action)
80
+ - **Tautological phrasing**: "do X in terms of X" (one action)
81
+ - **Redundant verb pairs**: "check and verify X" (same operation)
82
+ - **Compound modifiers**: "quickly and efficiently process X" (one action)
83
+ - **Implicit single operation**: "install dependencies" even if it involves
84
+ multiple steps internally
16
85
 
17
- 1. Identify each individual task
18
- 2. Return a JSON array of corrected tasks
19
- 3. Use this exact format: ["task 1", "task 2", "task 3"]
86
+ DO split when:
87
+
88
+ - **Distinct sequential operations**: "install deps, run tests" (two separate
89
+ commands)
90
+ - **Action + conditional**: "check disk space and warn if below 10%" (check,
91
+ then conditional action)
92
+ - **Different subjects**: "explain X and demonstrate Y" (two different things)
93
+ - **Truly separate steps**: "create file and add content to it" (two distinct
94
+ operations)
20
95
 
21
96
  ## Response Format
22
97
 
@@ -27,15 +102,55 @@ Do not include explanations, commentary, or any other text.
27
102
 
28
103
  ## Examples
29
104
 
30
- Single task:
105
+ ### ❌ INCORRECT: Duplicate Tasks (What NOT to do)
106
+
107
+ These examples show common mistakes that create semantic duplicates:
108
+
109
+ - "explain Lehman's terms in Lehman's terms" →
110
+ ❌ WRONG: ["explain what Lehman's terms are in simple language", "describe Lehman's terms using easy-to-understand words"]
111
+ ✅ CORRECT: explain Lehman's terms in simple language
112
+
113
+ - "show and display files" →
114
+ ❌ WRONG: ["show the files", "display the files"]
115
+ ✅ CORRECT: show the files
116
+
117
+ - "check and verify disk space" →
118
+ ❌ WRONG: ["check the disk space", "verify the disk space"]
119
+ ✅ CORRECT: check the disk space
120
+
121
+ - "list directory contents completely" →
122
+ ❌ WRONG: ["list the directory contents", "show all items"]
123
+ ✅ CORRECT: list all directory contents
124
+
125
+ - "install and set up dependencies" →
126
+ ❌ WRONG: ["install dependencies", "set up dependencies"]
127
+ ✅ CORRECT: install dependencies
128
+
129
+ ### ✅ CORRECT: Single Task
130
+
131
+ Simple requests should remain as single tasks:
31
132
 
32
133
  - "change dir to ~" → change directory to the home folder
33
134
  - "install deps" → install dependencies
34
135
  - "make new file called test.txt" → create a new file called test.txt
35
136
  - "show me files here" → show the files in the current directory
137
+ - "explain quantum physics simply" → explain quantum physics in simple terms
138
+ - "describe the process in detail" → describe the process in detail
139
+ - "check disk space thoroughly" → check the disk space thoroughly
140
+
141
+ ### ✅ CORRECT: Multiple Tasks
36
142
 
37
- Multiple tasks:
143
+ Only split when tasks are truly distinct operations:
38
144
 
39
145
  - "install deps, run tests" → ["install dependencies", "run tests"]
40
146
  - "create file; add content" → ["create a file", "add content"]
41
147
  - "build project and deploy" → ["build the project", "deploy"]
148
+
149
+ ### ✅ CORRECT: Complex Questions (Split into Sequences)
150
+
151
+ Split only when multiple distinct queries or operations are needed:
152
+
153
+ - "tell me weather in Wro, is it over 70 deg" → ["show the weather in Wrocław", "check if the temperature is above 70 degrees"]
154
+ - "pls what is 7th prime and how many are to 1000" → ["find the 7th prime number", "count how many prime numbers are below 1000"]
155
+ - "check disk space and warn if below 10%" → ["check the disk space", "show a warning if it is below 10%"]
156
+ - "find config file and show its contents" → ["find the config file", "show its contents"]
package/dist/index.js CHANGED
@@ -3,8 +3,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { readFileSync, existsSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { dirname, join } from 'path';
6
- import { render } from 'ink';
6
+ import { render, Text } from 'ink';
7
+ import { loadConfig, ConfigError, configExists, saveConfig, } from './services/config.js';
8
+ import { createAnthropicService } from './services/anthropic.js';
7
9
  import { Please } from './ui/Please.js';
10
+ import { ConfigThenCommand } from './ui/ConfigThenCommand.js';
8
11
  const __filename = fileURLToPath(import.meta.url);
9
12
  const __dirname = dirname(__filename);
10
13
  // Get package info
@@ -21,4 +24,45 @@ const appInfo = {
21
24
  description: packageJson.description,
22
25
  isDev,
23
26
  };
24
- render(_jsx(Please, { app: appInfo }));
27
+ // Get command from command-line arguments
28
+ const args = process.argv.slice(2);
29
+ const rawCommand = args.join(' ').trim();
30
+ async function runApp() {
31
+ // Check if config exists, if not run setup
32
+ if (!configExists()) {
33
+ if (!rawCommand) {
34
+ // "pls" for the first time: show welcome box and ask about config below
35
+ const { waitUntilExit } = render(_jsx(Please, { app: appInfo, showConfigSetup: true, onConfigComplete: ({ apiKey, model }) => {
36
+ saveConfig(apiKey, model);
37
+ } }));
38
+ await waitUntilExit();
39
+ return;
40
+ }
41
+ else {
42
+ // "pls do stuff" for the first time: ask about config, then continue
43
+ render(_jsx(ConfigThenCommand, { command: rawCommand, onConfigSave: saveConfig }));
44
+ return;
45
+ }
46
+ }
47
+ // Try to load and validate config
48
+ try {
49
+ const config = loadConfig();
50
+ if (!rawCommand) {
51
+ // "pls" when config present: show welcome box
52
+ render(_jsx(Please, { app: appInfo }));
53
+ }
54
+ else {
55
+ // "pls do stuff": fetch and show the plan
56
+ const claudeService = createAnthropicService(config.anthropic.apiKey, config.anthropic.model);
57
+ render(_jsx(Please, { app: appInfo, command: rawCommand, claudeService: claudeService }));
58
+ }
59
+ }
60
+ catch (error) {
61
+ if (error instanceof ConfigError) {
62
+ render(_jsx(Text, { color: "red", children: error.message }));
63
+ process.exit(1);
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+ runApp();
@@ -4,11 +4,11 @@ import { dirname, join } from 'path';
4
4
  import Anthropic from '@anthropic-ai/sdk';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
- const SYSTEM_PROMPT = readFileSync(join(__dirname, '../config/SYSTEM.md'), 'utf-8');
7
+ const PLAN_PROMPT = readFileSync(join(__dirname, '../config/PLAN.md'), 'utf-8');
8
8
  export class AnthropicService {
9
9
  client;
10
10
  model;
11
- constructor(apiKey, model = 'claude-3-5-haiku-20241022') {
11
+ constructor(apiKey, model = 'claude-haiku-4-5-20251001') {
12
12
  this.client = new Anthropic({ apiKey });
13
13
  this.model = model;
14
14
  }
@@ -16,7 +16,7 @@ export class AnthropicService {
16
16
  const response = await this.client.messages.create({
17
17
  model: this.model,
18
18
  max_tokens: 200,
19
- system: SYSTEM_PROMPT,
19
+ system: PLAN_PROMPT,
20
20
  messages: [
21
21
  {
22
22
  role: 'user',
@@ -49,6 +49,6 @@ export class AnthropicService {
49
49
  return [text];
50
50
  }
51
51
  }
52
- export function createClaudeService(apiKey) {
53
- return new AnthropicService(apiKey);
52
+ export function createAnthropicService(apiKey, model) {
53
+ return new AnthropicService(apiKey, model);
54
54
  }
@@ -1,57 +1,115 @@
1
- import { existsSync, mkdirSync, readFileSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
- import { join } from 'path';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import YAML from 'yaml';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
4
8
  export class ConfigError extends Error {
5
9
  constructor(message) {
6
10
  super(message);
7
11
  this.name = 'ConfigError';
8
12
  }
9
13
  }
10
- const CONFIG_DIR = join(homedir(), '.pls');
11
- const CONFIG_FILE = join(CONFIG_DIR, '.env');
12
- export function ensureConfigDirectory() {
13
- if (!existsSync(CONFIG_DIR)) {
14
- mkdirSync(CONFIG_DIR, { recursive: true });
14
+ const CONFIG_FILE = join(homedir(), '.plsrc');
15
+ function parseYamlConfig(content) {
16
+ try {
17
+ return YAML.parse(content);
18
+ }
19
+ catch (error) {
20
+ throw new ConfigError(`\nFailed to parse YAML configuration file at ${CONFIG_FILE}\n` +
21
+ `Error: ${error instanceof Error ? error.message : String(error)}`);
15
22
  }
16
23
  }
17
- function parseEnvFile(content) {
18
- const result = {};
19
- for (const line of content.split('\n')) {
20
- const trimmed = line.trim();
21
- // Skip empty lines and comments
22
- if (!trimmed || trimmed.startsWith('#')) {
23
- continue;
24
- }
25
- const equalsIndex = trimmed.indexOf('=');
26
- if (equalsIndex === -1) {
27
- continue;
24
+ function validateConfig(parsed) {
25
+ if (!parsed || typeof parsed !== 'object') {
26
+ throw new ConfigError(`\nInvalid configuration format in ${CONFIG_FILE}\n` +
27
+ 'Expected a YAML object with configuration settings.');
28
+ }
29
+ const config = parsed;
30
+ // Validate anthropic section
31
+ if (!config.anthropic || typeof config.anthropic !== 'object') {
32
+ throw new ConfigError(`\nMissing or invalid 'anthropic' section in ${CONFIG_FILE}\n` +
33
+ 'Please add:\n' +
34
+ 'anthropic:\n' +
35
+ ' api-key: sk-ant-...');
36
+ }
37
+ const anthropic = config.anthropic;
38
+ // Support both 'api-key' (kebab-case) and 'apiKey' (camelCase)
39
+ const apiKey = anthropic['api-key'] || anthropic.apiKey;
40
+ if (!apiKey || typeof apiKey !== 'string') {
41
+ throw new ConfigError(`\nMissing or invalid 'anthropic.api-key' in ${CONFIG_FILE}\n` +
42
+ 'Please add your Anthropic API key:\n' +
43
+ 'anthropic:\n' +
44
+ ' api-key: sk-ant-...');
45
+ }
46
+ const validatedConfig = {
47
+ anthropic: {
48
+ apiKey: apiKey,
49
+ },
50
+ };
51
+ // Optional model
52
+ if (anthropic.model && typeof anthropic.model === 'string') {
53
+ validatedConfig.anthropic.model = anthropic.model;
54
+ }
55
+ // Optional UI config
56
+ if (config.ui && typeof config.ui === 'object') {
57
+ const ui = config.ui;
58
+ validatedConfig.ui = {};
59
+ if (ui.theme && typeof ui.theme === 'string') {
60
+ validatedConfig.ui.theme = ui.theme;
28
61
  }
29
- const key = trimmed.slice(0, equalsIndex).trim();
30
- const value = trimmed.slice(equalsIndex + 1).trim();
31
- if (key) {
32
- result[key] = value;
62
+ if (typeof ui.verbose === 'boolean') {
63
+ validatedConfig.ui.verbose = ui.verbose;
33
64
  }
34
65
  }
35
- return result;
66
+ return validatedConfig;
36
67
  }
37
68
  export function loadConfig() {
38
- ensureConfigDirectory();
39
69
  if (!existsSync(CONFIG_FILE)) {
40
- throw new ConfigError(`Configuration file not found at ${CONFIG_FILE}\n` +
41
- 'Please create it with your CLAUDE_API_KEY.\n' +
42
- 'Example: echo "CLAUDE_API_KEY=sk-ant-..." > ~/.pls/.env');
70
+ throw new ConfigError(`\nConfiguration file not found at ${CONFIG_FILE}\n\n` +
71
+ 'Please create it with your Anthropic API key.\n' +
72
+ 'Example:\n\n' +
73
+ 'anthropic:\n' +
74
+ ' api-key: sk-ant-...\n' +
75
+ ' model: claude-haiku-4-5-20251001\n');
43
76
  }
44
77
  const content = readFileSync(CONFIG_FILE, 'utf-8');
45
- const parsed = parseEnvFile(content);
46
- const claudeApiKey = parsed.CLAUDE_API_KEY;
47
- if (!claudeApiKey) {
48
- throw new ConfigError('CLAUDE_API_KEY not found in configuration file.\n' +
49
- `Please add it to ${CONFIG_FILE}`);
50
- }
51
- return {
52
- claudeApiKey,
53
- };
78
+ const parsed = parseYamlConfig(content);
79
+ return validateConfig(parsed);
54
80
  }
55
81
  export function getConfigPath() {
56
82
  return CONFIG_FILE;
57
83
  }
84
+ export function configExists() {
85
+ return existsSync(CONFIG_FILE);
86
+ }
87
+ export function mergeConfig(existingContent, sectionName, newValues) {
88
+ const parsed = existingContent.trim()
89
+ ? YAML.parse(existingContent) || {}
90
+ : {};
91
+ // Update or add section
92
+ const section = parsed[sectionName] || {};
93
+ for (const [key, value] of Object.entries(newValues)) {
94
+ section[key] = value;
95
+ }
96
+ parsed[sectionName] = section;
97
+ // Sort sections alphabetically
98
+ const sortedKeys = Object.keys(parsed).sort();
99
+ const sortedConfig = {};
100
+ for (const key of sortedKeys) {
101
+ sortedConfig[key] = parsed[key];
102
+ }
103
+ // Convert back to YAML
104
+ return YAML.stringify(sortedConfig);
105
+ }
106
+ export function saveConfig(apiKey, model) {
107
+ const existingContent = existsSync(CONFIG_FILE)
108
+ ? readFileSync(CONFIG_FILE, 'utf-8')
109
+ : '';
110
+ const newContent = mergeConfig(existingContent, 'anthropic', {
111
+ 'api-key': apiKey,
112
+ model: model,
113
+ });
114
+ writeFileSync(CONFIG_FILE, newContent, 'utf-8');
115
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mergeConfig } from './config.js';
3
+ describe('mergeConfig', () => {
4
+ it('creates new config when file is empty', () => {
5
+ const result = mergeConfig('', 'anthropic', {
6
+ 'api-key': 'sk-ant-test',
7
+ model: 'claude-haiku-4-5-20251001',
8
+ });
9
+ expect(result).toContain('anthropic:');
10
+ expect(result).toContain(' api-key: sk-ant-test');
11
+ expect(result).toContain(' model: claude-haiku-4-5-20251001');
12
+ });
13
+ it('adds new section to existing config', () => {
14
+ const existing = `ui:
15
+ theme: dark
16
+ verbose: true`;
17
+ const result = mergeConfig(existing, 'anthropic', {
18
+ 'api-key': 'sk-ant-test',
19
+ model: 'claude-haiku-4-5-20251001',
20
+ });
21
+ expect(result).toContain('ui:');
22
+ expect(result).toContain(' theme: dark');
23
+ expect(result).toContain('anthropic:');
24
+ expect(result).toContain(' api-key: sk-ant-test');
25
+ });
26
+ it('sorts sections alphabetically', () => {
27
+ const existing = `ui:
28
+ theme: dark`;
29
+ const result = mergeConfig(existing, 'anthropic', {
30
+ 'api-key': 'sk-ant-test',
31
+ });
32
+ const anthropicIndex = result.indexOf('anthropic:');
33
+ const uiIndex = result.indexOf('ui:');
34
+ expect(anthropicIndex).toBeLessThan(uiIndex);
35
+ });
36
+ it('updates existing section without removing other keys', () => {
37
+ const existing = `anthropic:
38
+ api-key: sk-ant-old
39
+ custom-setting: value`;
40
+ const result = mergeConfig(existing, 'anthropic', {
41
+ 'api-key': 'sk-ant-new',
42
+ model: 'claude-haiku-4-5-20251001',
43
+ });
44
+ expect(result).toContain('api-key: sk-ant-new');
45
+ expect(result).toContain('model: claude-haiku-4-5-20251001');
46
+ expect(result).toContain('custom-setting: value');
47
+ expect(result).not.toContain('sk-ant-old');
48
+ });
49
+ it('adds empty line before new section', () => {
50
+ const existing = `ui:
51
+ theme: dark`;
52
+ const result = mergeConfig(existing, 'anthropic', {
53
+ 'api-key': 'sk-ant-test',
54
+ });
55
+ expect(result).toMatch(/anthropic:\n {2}api-key:/);
56
+ expect(result).toMatch(/ui:\n {2}theme:/);
57
+ });
58
+ it('handles multiple sections with sorting', () => {
59
+ const existing = `ui:
60
+ theme: dark
61
+
62
+ config:
63
+ llm: anthropic
64
+ name: Sensei`;
65
+ const result = mergeConfig(existing, 'anthropic', {
66
+ 'api-key': 'sk-ant-test',
67
+ });
68
+ const sections = result.match(/^[a-z]+:/gm) || [];
69
+ expect(sections).toEqual(['anthropic:', 'config:', 'ui:']);
70
+ });
71
+ it('updates key in existing section', () => {
72
+ const existing = `anthropic:
73
+ api-key: sk-ant-old
74
+ model: old-model`;
75
+ const result = mergeConfig(existing, 'anthropic', {
76
+ model: 'new-model',
77
+ });
78
+ expect(result).toContain('api-key: sk-ant-old');
79
+ expect(result).toContain('model: new-model');
80
+ expect(result).not.toContain('old-model');
81
+ });
82
+ });
@@ -2,7 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { Spinner } from './Spinner.js';
5
- const MIN_PROCESSING_TIME = 3000;
5
+ const MIN_PROCESSING_TIME = 2000; // purelly for visual effect
6
6
  export function Command({ rawCommand, claudeService }) {
7
7
  const [processedTasks, setProcessedTasks] = useState([]);
8
8
  const [error, setError] = useState(null);
@@ -36,5 +36,5 @@ export function Command({ rawCommand, claudeService }) {
36
36
  mounted = false;
37
37
  };
38
38
  }, [rawCommand, claudeService]);
39
- return (_jsxs(Box, { alignSelf: "flex-start", marginTop: 1, marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["> pls ", rawCommand] }), isLoading && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Spinner, {})] }))] }), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), processedTasks.length > 0 && (_jsx(Box, { flexDirection: "column", children: processedTasks.map((task, index) => (_jsxs(Box, { children: [_jsx(Text, { color: "whiteBright", children: ' - ' }), _jsx(Text, { color: "white", children: task })] }, index))) }))] }));
39
+ return (_jsxs(Box, { alignSelf: "flex-start", marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["> pls ", rawCommand] }), isLoading && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Spinner, {})] }))] }), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), processedTasks.length > 0 && (_jsx(Box, { flexDirection: "column", children: processedTasks.map((task, index) => (_jsxs(Box, { children: [_jsx(Text, { color: "whiteBright", children: ' - ' }), _jsx(Text, { color: "white", children: task })] }, index))) }))] }));
40
40
  }
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ export function ConfigSetup({ onComplete }) {
6
+ const [step, setStep] = React.useState('api-key');
7
+ const [apiKey, setApiKey] = React.useState('');
8
+ const [model, setModel] = React.useState('claude-haiku-4-5-20251001');
9
+ const handleApiKeySubmit = (value) => {
10
+ setApiKey(value);
11
+ setStep('model');
12
+ };
13
+ const handleModelSubmit = (value) => {
14
+ const finalModel = value.trim() || 'claude-haiku-4-5-20251001';
15
+ setModel(finalModel);
16
+ setStep('done');
17
+ onComplete({ apiKey, model: finalModel });
18
+ };
19
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsx(Text, { children: "Configuration required." }), _jsx(Box, { children: _jsxs(Text, { color: "whiteBright", dimColor: true, children: ["==>", " Get your API key from: https://platform.claude.com/"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Anthropic API key:" }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "> " }), step === 'api-key' ? (_jsx(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: handleApiKeySubmit, mask: "*" })) : (_jsx(Text, { dimColor: true, children: '*'.repeat(12) }))] }), (step === 'model' || step === 'done') && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Box, { children: _jsxs(Text, { children: ["Model ", _jsx(Text, { dimColor: true, children: "(default: claude-haiku-4-5-20251001)" }), ":"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "> " }), step === 'model' ? (_jsx(TextInput, { value: model, onChange: setModel, onSubmit: handleModelSubmit })) : (_jsx(Text, { dimColor: true, children: model }))] })] })), step === 'done' && (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: "green", children: "\u2713 Configuration saved" }) }))] }));
20
+ }
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box } from 'ink';
4
+ import { createAnthropicService } from '../services/anthropic.js';
5
+ import { ConfigSetup } from './ConfigSetup.js';
6
+ import { Command } from './Command.js';
7
+ export function ConfigThenCommand({ command, onConfigSave, }) {
8
+ const [configComplete, setConfigComplete] = React.useState(false);
9
+ const [savedConfig, setSavedConfig] = React.useState(null);
10
+ const handleConfigComplete = (config) => {
11
+ onConfigSave(config.apiKey, config.model);
12
+ setSavedConfig(config);
13
+ setConfigComplete(true);
14
+ };
15
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: [_jsx(ConfigSetup, { onComplete: handleConfigComplete }), configComplete && savedConfig && (_jsx(Command, { rawCommand: command, claudeService: createAnthropicService(savedConfig.apiKey, savedConfig.model) }))] }));
16
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from 'ink';
3
+ export function History({ items }) {
4
+ if (items.length === 0) {
5
+ return null;
6
+ }
7
+ return (_jsx(Box, { flexDirection: "column", gap: 1, children: items.map((item, index) => (_jsx(Box, { children: item }, index))) }));
8
+ }
package/dist/ui/Please.js CHANGED
@@ -1,5 +1,16 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box } from 'ink';
4
+ import { Command } from './Command.js';
2
5
  import { Welcome } from './Welcome.js';
3
- export const Please = ({ app: info }) => {
4
- return _jsx(Welcome, { info: info });
6
+ import { ConfigSetup } from './ConfigSetup.js';
7
+ import { History } from './History.js';
8
+ export const Please = ({ app: info, command, claudeService, showConfigSetup, onConfigComplete, }) => {
9
+ const [history, setHistory] = React.useState([]);
10
+ // Simple command execution
11
+ if (command && claudeService) {
12
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: [_jsx(History, { items: history }), _jsx(Command, { rawCommand: command, claudeService: claudeService })] }));
13
+ }
14
+ // Welcome screen with optional config setup
15
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, gap: 1, children: [_jsx(History, { items: history }), _jsx(Welcome, { info: info }), showConfigSetup && onConfigComplete && (_jsx(ConfigSetup, { onComplete: onConfigComplete }))] }));
5
16
  };
@@ -9,5 +9,5 @@ export function Welcome({ info: app }) {
9
9
  const words = app.name
10
10
  .split('-')
11
11
  .map((word) => word.charAt(0).toUpperCase() + word.slice(1));
12
- return (_jsx(Box, { alignSelf: "flex-start", marginTop: 1, children: _jsxs(Box, { borderStyle: "round", borderColor: "green", paddingX: 3, paddingY: 1, marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, gap: 1, children: [words.map((word, index) => (_jsx(Text, { color: "greenBright", bold: true, children: word }, index))), _jsxs(Text, { color: "whiteBright", dimColor: true, children: ["v", app.version] }), app.isDev && _jsx(Text, { color: "yellowBright", children: "dev" })] }), descriptionLines.map((line, index) => (_jsx(Box, { children: _jsxs(Text, { color: "white", children: [line, "."] }) }, index))), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "brightWhite", bold: true, children: "Usage:" }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "whiteBright", dimColor: true, children: ">" }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "greenBright", bold: true, children: "pls" }), _jsx(Text, { color: "yellow", bold: true, children: "[describe your request]" })] })] })] })] }) }));
12
+ return (_jsx(Box, { alignSelf: "flex-start", children: _jsxs(Box, { borderStyle: "round", borderColor: "green", paddingX: 3, paddingY: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, gap: 1, children: [words.map((word, index) => (_jsx(Text, { color: "greenBright", bold: true, children: word }, index))), _jsxs(Text, { color: "whiteBright", dimColor: true, children: ["v", app.version] }), app.isDev && _jsx(Text, { color: "yellowBright", children: "dev" })] }), descriptionLines.map((line, index) => (_jsx(Box, { children: _jsxs(Text, { color: "white", children: [line, "."] }) }, index))), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "brightWhite", bold: true, children: "Usage:" }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "whiteBright", dimColor: true, children: ">" }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "greenBright", bold: true, children: "pls" }), _jsx(Text, { color: "yellow", bold: true, children: "[describe your request]" })] })] })] })] }) }));
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.0.5",
3
+ "version": "0.1.2",
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",
@@ -11,8 +11,8 @@
11
11
  "dist"
12
12
  ],
13
13
  "scripts": {
14
- "build": "tsc && chmod +x dist/index.js",
15
- "dev": "tsc && chmod +x dist/index.js && tsc --watch",
14
+ "build": "tsc && chmod +x dist/index.js && mkdir -p dist/config && cp src/config/*.md dist/config/",
15
+ "dev": "npm run build && tsc --watch",
16
16
  "prepare": "npm run build",
17
17
  "test": "vitest run",
18
18
  "test:watch": "vitest",
@@ -40,8 +40,11 @@
40
40
  },
41
41
  "homepage": "https://github.com/aswitalski/pls#readme",
42
42
  "dependencies": {
43
+ "@anthropic-ai/sdk": "^0.68.0",
43
44
  "ink": "^6.4.0",
44
- "react": "^19.2.0"
45
+ "ink-text-input": "^6.0.0",
46
+ "react": "^19.2.0",
47
+ "yaml": "^2.8.1"
45
48
  },
46
49
  "devDependencies": {
47
50
  "@types/node": "^20.10.6",