prompt-language-shell 0.1.0 → 0.1.4

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.
@@ -15,13 +15,110 @@ preserving the original intent. Apply minimal necessary changes to achieve
15
15
  optimal clarity. The refined output will be used to plan and execute real
16
16
  operations, so precision and unambiguous language are essential.
17
17
 
18
+ ## Skills Integration
19
+
20
+ If skills are provided in the "Available Skills" section below, you MUST
21
+ use them when the user's query matches a skill's domain.
22
+
23
+ When a query matches a skill:
24
+ 1. Recognize the semantic match between the user's request and the skill
25
+ description
26
+ 2. Extract the individual steps from the skill's "Steps" section
27
+ 3. Refine each step into clear, professional task descriptions that start
28
+ with a capital letter like a sentence
29
+ 4. Return each step as a separate task in a JSON array
30
+ 5. If the user's query includes additional requirements beyond the skill,
31
+ append those as additional tasks
32
+ 6. NEVER replace the skill's detailed steps with a generic restatement of
33
+ the user's request
34
+
35
+ Example 1:
36
+ - Skill has steps: "- Navigate to the project directory. - Run the build
37
+ script - Execute the test suite"
38
+ - User asks: "test the application"
39
+ - Correct output: ["Navigate to the project directory", "Run the build
40
+ script", "Execute the test suite"]
41
+ - WRONG output: ["test the application"]
42
+
43
+ Example 2:
44
+ - Skill has steps: "- Navigate to the project directory. - Run the build
45
+ script - Execute the test suite"
46
+ - User asks: "test the application and generate a report"
47
+ - Correct output: ["Navigate to the project directory", "Run the build
48
+ script", "Execute the test suite", "Generate a report"]
49
+
50
+ ## Evaluation of Requests
51
+
52
+ Before processing any request, evaluate its nature and respond appropriately:
53
+
54
+ **For harmful or offensive requests:**
55
+ If the request is clearly harmful, malicious, unethical, or offensive, return
56
+ the exact phrase "abort offensive request".
57
+
58
+ Examples that should be aborted as offensive:
59
+ - Requests to harm systems, delete critical data without authorization, or
60
+ perform malicious attacks
61
+ - Requests involving unethical surveillance or privacy violations
62
+ - Requests to create malware or exploit vulnerabilities
63
+ - Requests with offensive, discriminatory, or abusive language
64
+
65
+ **For vague or unclear requests:**
66
+ If the request is too vague or unclear to understand what action should be
67
+ taken, return the exact phrase "abort unclear request".
68
+
69
+ Before marking a request as unclear, try to infer meaning from:
70
+ - **Available skills**: If a skill is provided that narrows down a domain,
71
+ use that context to interpret the request. Skills define the scope of what
72
+ generic terms mean in a specific context. When a user says "all X" or
73
+ "the Y", check if an available skill defines what X or Y means. For example,
74
+ if a skill defines specific deployment environments for a project, then
75
+ "deploy to all environments" should be interpreted within that skill's
76
+ context, not as a generic unclear request.
77
+ - Common abbreviations and acronyms in technical contexts
78
+ - Well-known product names, tools, or technologies
79
+ - Context clues within the request itself
80
+ - Standard industry terminology
81
+
82
+ For example using skills context:
83
+ - "build all applications" + build skill defining mobile, desktop, and web
84
+ applications → interpret as those three specific applications
85
+ - "deploy to all environments" + deployment skill defining staging, production,
86
+ and canary → interpret as those three specific environments
87
+ - "run all test suites" + testing skill listing unit and integration tests →
88
+ interpret as those two specific test types
89
+ - "build the package" + monorepo skill defining a single backend package →
90
+ interpret as that one specific package
91
+ - "check all services" + microservices skill listing auth, api, and database
92
+ services → interpret as those three specific services
93
+ - "run both compilers" + build skill defining TypeScript and Sass compilers →
94
+ interpret as those two specific compilers
95
+ - "start the server" + infrastructure skill defining a single Node.js server →
96
+ interpret as that one specific server
97
+
98
+ For example using common context:
99
+ - "run TS compiler" → "TS" stands for TypeScript
100
+ - "open VSC" → "VSC" likely means Visual Studio Code
101
+ - "run unit tests" → standard development terminology for testing
102
+
103
+ Only mark as unclear if the request is truly unintelligible or lacks any
104
+ discernible intent, even after considering available skills and context.
105
+
106
+ Examples that are too vague:
107
+ - "do stuff"
108
+ - "handle it"
109
+
110
+ **For legitimate requests:**
111
+ If the request is clear enough to understand the intent, even if informal or
112
+ playful, process it normally. Refine casual language into professional task
113
+ descriptions.
114
+
18
115
  ## Refinement Guidelines
19
116
 
20
117
  Focus on these elements when refining commands:
21
118
 
22
119
  - Correct grammar and sentence structure
23
- - Replace words with more precise or contextually appropriate alternatives, even
24
- when the original word is grammatically correct
120
+ - Replace words with more precise or contextually appropriate alternatives,
121
+ even when the original word is grammatically correct
25
122
  - Use professional, clear terminology suitable for technical documentation
26
123
  - Maintain natural, fluent English phrasing
27
124
  - Preserve the original intent and meaning
@@ -47,7 +144,7 @@ When breaking down complex questions:
47
144
  - Separate conditional checks into distinct tasks
48
145
  - Keep each task simple and focused on one operation
49
146
 
50
- Before returning a JSON array, validate that:
147
+ Before returning a JSON array, perform strict validation:
51
148
 
52
149
  1. Each task is semantically unique (no duplicates with different words)
53
150
  2. Each task provides distinct value
@@ -55,6 +152,12 @@ Before returning a JSON array, validate that:
55
152
  4. When uncertain whether to split, default to a single task
56
153
  5. Executing the tasks will not result in duplicate work
57
154
 
155
+ Critical validation check: After creating the array, examine each pair of
156
+ tasks and ask "Would these perform the same operation?" If yes, they are
157
+ duplicates and must be merged or removed. Pay special attention to synonym
158
+ verbs (delete, remove, erase) and equivalent noun phrases (unused apps,
159
+ applications not used).
160
+
58
161
  ## Avoiding Duplicates
59
162
 
60
163
  Each task in an array must be semantically unique and provide distinct value.
@@ -62,28 +165,34 @@ Before returning multiple tasks, verify there are no duplicates.
62
165
 
63
166
  Rules for preventing duplicates:
64
167
 
65
- 1. Modifiers are not separate tasks. Adverbs and adjectives that modify how to
66
- perform a task are part of the task description, not separate tasks.
67
- - "explain X in simple terms" = ONE task (not "explain X" + "use simple terms")
68
- - "describe X in detail" = ONE task (not "describe X" + "make it detailed")
168
+ 1. Modifiers are not separate tasks. Adverbs and adjectives that modify how
169
+ to perform a task are part of the task description, not separate tasks.
170
+ - "explain X in simple terms" = ONE task (not "explain X" + "use simple
171
+ terms")
172
+ - "describe X in detail" = ONE task (not "describe X" + "make it
173
+ detailed")
69
174
  - "list X completely" = ONE task (not "list X" + "be complete")
70
175
 
71
- 2. Synonymous verbs are duplicates. Different verbs meaning the same thing with
72
- the same object are duplicates. Keep only one or merge them.
176
+ 2. Synonymous verbs are duplicates. Different verbs meaning the same thing
177
+ with the same object are duplicates. Keep only one or merge them.
73
178
  - "explain X" + "describe X" = DUPLICATE (choose one)
74
179
  - "show X" + "display X" = DUPLICATE (choose one)
75
180
  - "check X" + "verify X" = DUPLICATE (choose one)
76
181
  - "list X" + "enumerate X" = DUPLICATE (choose one)
77
-
78
- 3. Tautological patterns stay single. When a request uses a phrase that already
79
- describes how to do something, do not split it.
80
- - "explain Lehman's terms in Lehman's terms" = ONE task (the phrase already
81
- means "in simple language")
182
+ - "delete X" + "remove X" = DUPLICATE (choose one)
183
+ - "erase X" + "remove X" = DUPLICATE (choose one)
184
+ - "create X" + "make X" = DUPLICATE (choose one)
185
+ - "find X" + "locate X" = DUPLICATE (choose one)
186
+
187
+ 3. Tautological patterns stay single. When a request uses a phrase that
188
+ already describes how to do something, do not split it.
189
+ - "explain Lehman's terms in Lehman's terms" = ONE task (the phrase
190
+ already means "in simple language")
82
191
  - "describe it simply in simple words" = ONE task (redundant modifiers)
83
192
  - "show clearly and display obviously" = ONE task (redundant verbs)
84
193
 
85
- 4. Redundant operations are duplicates. If two alleged tasks would perform the
86
- same operation, they are duplicates.
194
+ 4. Redundant operations are duplicates. If two alleged tasks would perform
195
+ the same operation, they are duplicates.
87
196
  - "install and set up dependencies" = ONE task (setup is part of install)
88
197
  - "check and verify disk space" = ONE task (verify means check)
89
198
  - "list and show all files" = ONE task (list and show are the same)
@@ -96,15 +205,15 @@ Keep as a single task when:
96
205
  - Tautological phrasing: "do X in terms of X" (one action)
97
206
  - Redundant verb pairs: "check and verify X" (same operation)
98
207
  - Compound modifiers: "quickly and efficiently process X" (one action)
99
- - Implicit single operation: "install dependencies" even if it involves multiple
100
- steps internally
208
+ - Implicit single operation: "install dependencies" even if it involves
209
+ multiple steps internally
101
210
 
102
211
  Split into multiple tasks when:
103
212
 
104
213
  - Distinct sequential operations: "install deps, run tests" (two separate
105
214
  commands)
106
- - Action with conditional: "check disk space and warn if below 10%" (check, then
107
- conditional action)
215
+ - Action with conditional: "check disk space and warn if below 10%" (check,
216
+ then conditional action)
108
217
  - Different subjects: "explain X and demonstrate Y" (two different things)
109
218
  - Truly separate steps: "create file and add content to it" (two distinct
110
219
  operations)
@@ -114,7 +223,27 @@ Split into multiple tasks when:
114
223
  - Single task: Return ONLY the corrected command text
115
224
  - Multiple tasks: Return ONLY a JSON array of strings
116
225
 
117
- Do not include explanations, commentary, or any other text.
226
+ Do not include explanations, commentary, markdown formatting, code blocks, or
227
+ any other text. For JSON arrays, return the raw JSON without ```json``` or
228
+ any other wrapping.
229
+
230
+ ## Final Validation Before Response
231
+
232
+ Before returning any JSON array, perform this final check:
233
+
234
+ 1. Compare each task against every other task in the array
235
+ 2. Ask for each pair: "Do these describe the same operation using different
236
+ words?"
237
+ 3. Check specifically for:
238
+ - Synonym verbs (delete/remove, show/display, create/make, find/locate)
239
+ - Equivalent noun phrases (apps/applications, unused/not used,
240
+ files/documents)
241
+ - Same operation with different modifiers
242
+ 4. If any pair is semantically identical, merge them or keep only one
243
+ 5. If in doubt about whether tasks are duplicates, they probably are - merge
244
+ them
245
+
246
+ Only return the array after confirming no semantic duplicates exist.
118
247
 
119
248
  ## Examples
120
249
 
@@ -123,25 +252,53 @@ Do not include explanations, commentary, or any other text.
123
252
  These examples show common mistakes that create semantic duplicates:
124
253
 
125
254
  - "explain Lehman's terms in Lehman's terms" →
126
- - wrong: ["explain what Lehman's terms are in simple language", "describe Lehman's terms using easy-to-understand words"]
255
+ - wrong:
256
+ [
257
+ "explain what Lehman's terms are in simple language",
258
+ "describe Lehman's terms using easy-to-understand words",
259
+ ]
127
260
  - correct: explain Lehman's terms in simple language
128
261
 
129
262
  - "show and display files" →
130
- - wrong: ["show the files", "display the files"]
263
+ - wrong:
264
+ [
265
+ "show the files",
266
+ "display the files",
267
+ ]
131
268
  - correct: "show the files"
132
269
 
133
270
  - "check and verify disk space" →
134
- - wrong: ["check the disk space", "verify the disk space"]
271
+ - wrong:
272
+ [
273
+ "check the disk space",
274
+ "verify the disk space",
275
+ ]
135
276
  - correct: "check the disk space"
136
277
 
137
278
  - "list directory contents completely" →
138
- - wrong: ["list the directory contents", "show all items"]
279
+ - wrong:
280
+ [
281
+ "list the directory contents",
282
+ "show all items",
283
+ ]
139
284
  - correct: "list all directory contents"
140
285
 
141
286
  - "install and set up dependencies" →
142
- - wrong: ["install dependencies", "set up dependencies"]
287
+ - wrong:
288
+ [
289
+ "install dependencies",
290
+ "set up dependencies",
291
+ ]
143
292
  - correct: "install dependencies"
144
293
 
294
+ - "delete apps and remove all apps unused in a year" →
295
+ - wrong:
296
+ [
297
+ "delete unused applications",
298
+ "remove apps not used in the past year",
299
+ ]
300
+ - correct: "delete all applications unused in the past year"
301
+
145
302
  ### Correct Examples: Single Task
146
303
 
147
304
  Simple requests should remain as single tasks:
@@ -158,15 +315,43 @@ Simple requests should remain as single tasks:
158
315
 
159
316
  Only split when tasks are truly distinct operations:
160
317
 
161
- - "install deps, run tests" → ["install dependencies", "run tests"]
162
- - "create file; add content" → ["create a file", "add content"]
163
- - "build project and deploy" → ["build the project", "deploy"]
318
+ - "install deps, run tests" →
319
+ [
320
+ "install dependencies",
321
+ "run tests",
322
+ ]
323
+ - "create file; add content" →
324
+ [
325
+ "create a file",
326
+ "add content",
327
+ ]
328
+ - "build project and deploy" →
329
+ [
330
+ "build the project",
331
+ "deploy",
332
+ ]
164
333
 
165
334
  ### Correct Examples: Complex Questions
166
335
 
167
336
  Split only when multiple distinct queries or operations are needed:
168
337
 
169
- - "tell me weather in Wro, is it over 70 deg" → ["show the weather in Wrocław", "check if the temperature is above 70 degrees"]
170
- - "pls what is 7th prime and how many are to 1000" → ["find the 7th prime number", "count how many prime numbers are below 1000"]
171
- - "check disk space and warn if below 10%" → ["check the disk space", "show a warning if it is below 10%"]
172
- - "find config file and show its contents" → ["find the config file", "show its contents"]
338
+ - "tell me weather in Wro, is it over 70 deg" →
339
+ [
340
+ "show the weather in Wrocław",
341
+ "check if the temperature is above 70 degrees",
342
+ ]
343
+ - "pls what is 7th prime and how many are to 1000" →
344
+ [
345
+ "find the 7th prime number",
346
+ "count how many prime numbers are below 1000",
347
+ ]
348
+ - "check disk space and warn if below 10%" →
349
+ [
350
+ "check the disk space",
351
+ "show a warning if it is below 10%",
352
+ ]
353
+ - "find config file and show its contents" →
354
+ [
355
+ "find the config file",
356
+ "show its contents",
357
+ ]
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ import { readFileSync, existsSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { dirname, join } from 'path';
6
6
  import { render, Text } from 'ink';
7
- import { loadConfig, ConfigError } from './services/config.js';
7
+ import { loadConfig, ConfigError, configExists, saveConfig, } from './services/config.js';
8
8
  import { createAnthropicService } from './services/anthropic.js';
9
- import { Please } from './ui/Please.js';
9
+ import { PLS } from './ui/Please.js';
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
12
  // Get package info
@@ -26,16 +26,28 @@ const appInfo = {
26
26
  // Get command from command-line arguments
27
27
  const args = process.argv.slice(2);
28
28
  const rawCommand = args.join(' ').trim();
29
- // If no command provided, show welcome screen
30
- if (!rawCommand) {
31
- render(_jsx(Please, { app: appInfo }));
32
- }
33
- else {
34
- // Load config and create Claude service
29
+ async function runApp() {
30
+ // First-time setup: config doesn't exist
31
+ if (!configExists()) {
32
+ const { waitUntilExit } = render(_jsx(PLS, { app: appInfo, command: rawCommand || null, showConfigSetup: true, onConfigComplete: ({ apiKey, model }) => {
33
+ saveConfig(apiKey, model);
34
+ return rawCommand ? createAnthropicService(apiKey, model) : undefined;
35
+ } }));
36
+ await waitUntilExit();
37
+ return;
38
+ }
39
+ // Try to load and validate config
35
40
  try {
36
41
  const config = loadConfig();
37
- const claudeService = createAnthropicService(config.claudeApiKey);
38
- render(_jsx(Please, { app: appInfo, command: rawCommand, claudeService: claudeService }));
42
+ if (!rawCommand) {
43
+ // "pls" when config present: show welcome box
44
+ render(_jsx(PLS, { app: appInfo, command: null }));
45
+ }
46
+ else {
47
+ // "pls do stuff": fetch and show the plan
48
+ const claudeService = createAnthropicService(config.anthropic.apiKey, config.anthropic.model);
49
+ render(_jsx(PLS, { app: appInfo, command: rawCommand, claudeService: claudeService }));
50
+ }
39
51
  }
40
52
  catch (error) {
41
53
  if (error instanceof ConfigError) {
@@ -45,3 +57,4 @@ else {
45
57
  throw error;
46
58
  }
47
59
  }
60
+ runApp();
@@ -2,21 +2,26 @@ import { readFileSync } from 'fs';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, join } from 'path';
4
4
  import Anthropic from '@anthropic-ai/sdk';
5
+ import { loadSkills, formatSkillsForPrompt } from './skills.js';
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
7
8
  const PLAN_PROMPT = readFileSync(join(__dirname, '../config/PLAN.md'), 'utf-8');
8
9
  export class AnthropicService {
9
10
  client;
10
11
  model;
11
- constructor(apiKey, model = 'claude-3-5-haiku-20241022') {
12
+ constructor(apiKey, model = 'claude-haiku-4-5-20251001') {
12
13
  this.client = new Anthropic({ apiKey });
13
14
  this.model = model;
14
15
  }
15
16
  async processCommand(rawCommand) {
17
+ // Load skills and augment the planning prompt
18
+ const skills = loadSkills();
19
+ const skillsSection = formatSkillsForPrompt(skills);
20
+ const systemPrompt = PLAN_PROMPT + skillsSection;
16
21
  const response = await this.client.messages.create({
17
22
  model: this.model,
18
23
  max_tokens: 200,
19
- system: PLAN_PROMPT,
24
+ system: systemPrompt,
20
25
  messages: [
21
26
  {
22
27
  role: 'user',
@@ -29,26 +34,41 @@ export class AnthropicService {
29
34
  throw new Error('Unexpected response type from Claude API');
30
35
  }
31
36
  const text = content.text.trim();
37
+ let tasks;
32
38
  // Try to parse as JSON array
33
39
  if (text.startsWith('[') && text.endsWith(']')) {
34
40
  try {
35
41
  const parsed = JSON.parse(text);
36
- if (Array.isArray(parsed) && parsed.length > 0) {
42
+ if (Array.isArray(parsed)) {
37
43
  // Validate all items are strings
38
44
  const allStrings = parsed.every((item) => typeof item === 'string');
39
45
  if (allStrings) {
40
- return parsed.filter((item) => typeof item === 'string');
46
+ tasks = parsed.filter((item) => typeof item === 'string');
41
47
  }
48
+ else {
49
+ tasks = [text];
50
+ }
51
+ }
52
+ else {
53
+ tasks = [text];
42
54
  }
43
55
  }
44
56
  catch {
45
57
  // If JSON parsing fails, treat as single task
58
+ tasks = [text];
46
59
  }
47
60
  }
48
- // Single task
49
- return [text];
61
+ else {
62
+ // Single task
63
+ tasks = [text];
64
+ }
65
+ const isDebug = process.env.DEBUG === 'true';
66
+ return {
67
+ tasks,
68
+ systemPrompt: isDebug ? systemPrompt : undefined,
69
+ };
50
70
  }
51
71
  }
52
- export function createAnthropicService(apiKey) {
53
- return new AnthropicService(apiKey);
72
+ export function createAnthropicService(apiKey, model) {
73
+ return new AnthropicService(apiKey, model);
54
74
  }
@@ -1,57 +1,101 @@
1
- import { existsSync, mkdirSync, readFileSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
+ import YAML from 'yaml';
4
5
  export class ConfigError extends Error {
5
6
  constructor(message) {
6
7
  super(message);
7
8
  this.name = 'ConfigError';
8
9
  }
9
10
  }
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 });
15
- }
16
- }
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;
28
- }
29
- const key = trimmed.slice(0, equalsIndex).trim();
30
- const value = trimmed.slice(equalsIndex + 1).trim();
31
- if (key) {
32
- result[key] = value;
33
- }
34
- }
35
- return result;
11
+ const CONFIG_FILE = join(homedir(), '.plsrc');
12
+ function parseYamlConfig(content) {
13
+ try {
14
+ return YAML.parse(content);
15
+ }
16
+ catch (error) {
17
+ throw new ConfigError(`\nFailed to parse YAML configuration file at ${CONFIG_FILE}\n` +
18
+ `Error: ${error instanceof Error ? error.message : String(error)}`);
19
+ }
20
+ }
21
+ function validateConfig(parsed) {
22
+ if (!parsed || typeof parsed !== 'object') {
23
+ throw new ConfigError(`\nInvalid configuration format in ${CONFIG_FILE}\n` +
24
+ 'Expected a YAML object with configuration settings.');
25
+ }
26
+ const config = parsed;
27
+ // Validate anthropic section
28
+ if (!config.anthropic || typeof config.anthropic !== 'object') {
29
+ throw new ConfigError(`\nMissing or invalid 'anthropic' section in ${CONFIG_FILE}\n` +
30
+ 'Please add:\n' +
31
+ 'anthropic:\n' +
32
+ ' api-key: sk-ant-...');
33
+ }
34
+ const anthropic = config.anthropic;
35
+ // Support both 'api-key' (kebab-case) and 'apiKey' (camelCase)
36
+ const apiKey = anthropic['api-key'] || anthropic.apiKey;
37
+ if (!apiKey || typeof apiKey !== 'string') {
38
+ throw new ConfigError(`\nMissing or invalid 'anthropic.api-key' in ${CONFIG_FILE}\n` +
39
+ 'Please add your Anthropic API key:\n' +
40
+ 'anthropic:\n' +
41
+ ' api-key: sk-ant-...');
42
+ }
43
+ const validatedConfig = {
44
+ anthropic: {
45
+ apiKey: apiKey,
46
+ },
47
+ };
48
+ // Optional model
49
+ if (anthropic.model && typeof anthropic.model === 'string') {
50
+ validatedConfig.anthropic.model = anthropic.model;
51
+ }
52
+ return validatedConfig;
36
53
  }
37
54
  export function loadConfig() {
38
- ensureConfigDirectory();
39
55
  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');
56
+ throw new ConfigError(`\nConfiguration file not found at ${CONFIG_FILE}\n\n` +
57
+ 'Please create it with your Anthropic API key.\n' +
58
+ 'Example:\n\n' +
59
+ 'anthropic:\n' +
60
+ ' api-key: sk-ant-...\n' +
61
+ ' model: claude-haiku-4-5-20251001\n');
43
62
  }
44
63
  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
- };
64
+ const parsed = parseYamlConfig(content);
65
+ return validateConfig(parsed);
54
66
  }
55
67
  export function getConfigPath() {
56
68
  return CONFIG_FILE;
57
69
  }
70
+ export function configExists() {
71
+ return existsSync(CONFIG_FILE);
72
+ }
73
+ export function mergeConfig(existingContent, sectionName, newValues) {
74
+ const parsed = existingContent.trim()
75
+ ? YAML.parse(existingContent) || {}
76
+ : {};
77
+ // Update or add section
78
+ const section = parsed[sectionName] || {};
79
+ for (const [key, value] of Object.entries(newValues)) {
80
+ section[key] = value;
81
+ }
82
+ parsed[sectionName] = section;
83
+ // Sort sections alphabetically
84
+ const sortedKeys = Object.keys(parsed).sort();
85
+ const sortedConfig = {};
86
+ for (const key of sortedKeys) {
87
+ sortedConfig[key] = parsed[key];
88
+ }
89
+ // Convert back to YAML
90
+ return YAML.stringify(sortedConfig);
91
+ }
92
+ export function saveConfig(apiKey, model) {
93
+ const existingContent = existsSync(CONFIG_FILE)
94
+ ? readFileSync(CONFIG_FILE, 'utf-8')
95
+ : '';
96
+ const newContent = mergeConfig(existingContent, 'anthropic', {
97
+ 'api-key': apiKey,
98
+ model: model,
99
+ });
100
+ writeFileSync(CONFIG_FILE, newContent, 'utf-8');
101
+ }
@@ -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
+ });
@@ -0,0 +1,52 @@
1
+ import { readFileSync, readdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ /**
5
+ * Get the path to the skills directory
6
+ */
7
+ export function getSkillsDirectory() {
8
+ return join(homedir(), '.pls', 'skills');
9
+ }
10
+ /**
11
+ * Load all skill markdown files from the skills directory
12
+ * Returns an array of skill file contents
13
+ */
14
+ export function loadSkills() {
15
+ const skillsDir = getSkillsDirectory();
16
+ // Return empty array if directory doesn't exist
17
+ if (!existsSync(skillsDir)) {
18
+ return [];
19
+ }
20
+ try {
21
+ const files = readdirSync(skillsDir);
22
+ // Filter for markdown files
23
+ const skillFiles = files.filter((file) => file.endsWith('.md') || file.endsWith('.MD'));
24
+ // Read and return contents of each skill file
25
+ return skillFiles.map((file) => {
26
+ const filePath = join(skillsDir, file);
27
+ return readFileSync(filePath, 'utf-8');
28
+ });
29
+ }
30
+ catch (error) {
31
+ // Return empty array if there's any error reading the directory
32
+ return [];
33
+ }
34
+ }
35
+ /**
36
+ * Format skills for inclusion in the planning prompt
37
+ */
38
+ export function formatSkillsForPrompt(skills) {
39
+ if (skills.length === 0) {
40
+ return '';
41
+ }
42
+ const header = `
43
+
44
+ ## Available Skills
45
+
46
+ The following skills define domain-specific workflows. When the user's
47
+ query matches a skill, incorporate the skill's steps into your plan.
48
+
49
+ `;
50
+ const skillsContent = skills.join('\n\n');
51
+ return header + skillsContent;
52
+ }
@@ -5,6 +5,7 @@ import { Spinner } from './Spinner.js';
5
5
  const MIN_PROCESSING_TIME = 2000; // purelly for visual effect
6
6
  export function Command({ rawCommand, claudeService }) {
7
7
  const [processedTasks, setProcessedTasks] = useState([]);
8
+ const [systemPrompt, setSystemPrompt] = useState();
8
9
  const [error, setError] = useState(null);
9
10
  const [isLoading, setIsLoading] = useState(true);
10
11
  useEffect(() => {
@@ -17,7 +18,8 @@ export function Command({ rawCommand, claudeService }) {
17
18
  const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
18
19
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
19
20
  if (mounted) {
20
- setProcessedTasks(result);
21
+ setProcessedTasks(result.tasks);
22
+ setSystemPrompt(result.systemPrompt);
21
23
  setIsLoading(false);
22
24
  }
23
25
  }
@@ -36,5 +38,5 @@ export function Command({ rawCommand, claudeService }) {
36
38
  mounted = false;
37
39
  };
38
40
  }, [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))) }))] }));
41
+ 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
42
  }
@@ -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,9 +1,25 @@
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';
2
4
  import { Command } from './Command.js';
3
5
  import { Welcome } from './Welcome.js';
4
- export const Please = ({ app: info, command, claudeService }) => {
5
- if (command && claudeService) {
6
- return _jsx(Command, { rawCommand: command, claudeService: claudeService });
6
+ import { ConfigSetup } from './ConfigSetup.js';
7
+ import { History } from './History.js';
8
+ export const PLS = ({ app: info, command, claudeService, showConfigSetup, onConfigComplete, }) => {
9
+ const [history, setHistory] = React.useState([]);
10
+ const [service, setService] = React.useState(claudeService);
11
+ const handleConfigComplete = (config) => {
12
+ if (onConfigComplete) {
13
+ const result = onConfigComplete(config);
14
+ if (result) {
15
+ setService(result);
16
+ }
17
+ }
18
+ };
19
+ // Command execution (with service from props or after config)
20
+ if (command && service) {
21
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: [_jsx(History, { items: history }), _jsx(Command, { rawCommand: command, claudeService: service })] }));
7
22
  }
8
- return _jsx(Welcome, { info: info });
23
+ // Welcome screen with optional config setup
24
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, gap: 1, children: [_jsx(History, { items: history }), !showConfigSetup && _jsx(Welcome, { info: info }), showConfigSetup && _jsx(ConfigSetup, { onComplete: handleConfigComplete })] }));
9
25
  };
@@ -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.1.0",
3
+ "version": "0.1.4",
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",
@@ -42,7 +42,9 @@
42
42
  "dependencies": {
43
43
  "@anthropic-ai/sdk": "^0.68.0",
44
44
  "ink": "^6.4.0",
45
- "react": "^19.2.0"
45
+ "ink-text-input": "^6.0.0",
46
+ "react": "^19.2.0",
47
+ "yaml": "^2.8.1"
46
48
  },
47
49
  "devDependencies": {
48
50
  "@types/node": "^20.10.6",