prompt-language-shell 0.1.0 → 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.
@@ -15,13 +15,55 @@ 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
+ ## 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
+
18
60
  ## Refinement Guidelines
19
61
 
20
62
  Focus on these elements when refining commands:
21
63
 
22
64
  - Correct grammar and sentence structure
23
- - Replace words with more precise or contextually appropriate alternatives, even
24
- when the original word is grammatically correct
65
+ - Replace words with more precise or contextually appropriate alternatives,
66
+ even when the original word is grammatically correct
25
67
  - Use professional, clear terminology suitable for technical documentation
26
68
  - Maintain natural, fluent English phrasing
27
69
  - Preserve the original intent and meaning
@@ -47,7 +89,7 @@ When breaking down complex questions:
47
89
  - Separate conditional checks into distinct tasks
48
90
  - Keep each task simple and focused on one operation
49
91
 
50
- Before returning a JSON array, validate that:
92
+ Before returning a JSON array, perform strict validation:
51
93
 
52
94
  1. Each task is semantically unique (no duplicates with different words)
53
95
  2. Each task provides distinct value
@@ -55,6 +97,12 @@ Before returning a JSON array, validate that:
55
97
  4. When uncertain whether to split, default to a single task
56
98
  5. Executing the tasks will not result in duplicate work
57
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
+
58
106
  ## Avoiding Duplicates
59
107
 
60
108
  Each task in an array must be semantically unique and provide distinct value.
@@ -62,28 +110,34 @@ Before returning multiple tasks, verify there are no duplicates.
62
110
 
63
111
  Rules for preventing duplicates:
64
112
 
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")
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")
69
119
  - "list X completely" = ONE task (not "list X" + "be complete")
70
120
 
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.
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.
73
123
  - "explain X" + "describe X" = DUPLICATE (choose one)
74
124
  - "show X" + "display X" = DUPLICATE (choose one)
75
125
  - "check X" + "verify X" = DUPLICATE (choose one)
76
126
  - "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")
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")
82
136
  - "describe it simply in simple words" = ONE task (redundant modifiers)
83
137
  - "show clearly and display obviously" = ONE task (redundant verbs)
84
138
 
85
- 4. Redundant operations are duplicates. If two alleged tasks would perform the
86
- same operation, they are duplicates.
139
+ 4. Redundant operations are duplicates. If two alleged tasks would perform
140
+ the same operation, they are duplicates.
87
141
  - "install and set up dependencies" = ONE task (setup is part of install)
88
142
  - "check and verify disk space" = ONE task (verify means check)
89
143
  - "list and show all files" = ONE task (list and show are the same)
@@ -96,15 +150,15 @@ Keep as a single task when:
96
150
  - Tautological phrasing: "do X in terms of X" (one action)
97
151
  - Redundant verb pairs: "check and verify X" (same operation)
98
152
  - Compound modifiers: "quickly and efficiently process X" (one action)
99
- - Implicit single operation: "install dependencies" even if it involves multiple
100
- steps internally
153
+ - Implicit single operation: "install dependencies" even if it involves
154
+ multiple steps internally
101
155
 
102
156
  Split into multiple tasks when:
103
157
 
104
158
  - Distinct sequential operations: "install deps, run tests" (two separate
105
159
  commands)
106
- - Action with conditional: "check disk space and warn if below 10%" (check, then
107
- conditional action)
160
+ - Action with conditional: "check disk space and warn if below 10%" (check,
161
+ then conditional action)
108
162
  - Different subjects: "explain X and demonstrate Y" (two different things)
109
163
  - Truly separate steps: "create file and add content to it" (two distinct
110
164
  operations)
@@ -116,6 +170,24 @@ Split into multiple tasks when:
116
170
 
117
171
  Do not include explanations, commentary, or any other text.
118
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
+
119
191
  ## Examples
120
192
 
121
193
  ### Incorrect Examples: Duplicate Tasks
@@ -123,25 +195,53 @@ Do not include explanations, commentary, or any other text.
123
195
  These examples show common mistakes that create semantic duplicates:
124
196
 
125
197
  - "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"]
198
+ - wrong:
199
+ [
200
+ "explain what Lehman's terms are in simple language",
201
+ "describe Lehman's terms using easy-to-understand words",
202
+ ]
127
203
  - correct: explain Lehman's terms in simple language
128
204
 
129
205
  - "show and display files" →
130
- - wrong: ["show the files", "display the files"]
206
+ - wrong:
207
+ [
208
+ "show the files",
209
+ "display the files",
210
+ ]
131
211
  - correct: "show the files"
132
212
 
133
213
  - "check and verify disk space" →
134
- - wrong: ["check the disk space", "verify the disk space"]
214
+ - wrong:
215
+ [
216
+ "check the disk space",
217
+ "verify the disk space",
218
+ ]
135
219
  - correct: "check the disk space"
136
220
 
137
221
  - "list directory contents completely" →
138
- - wrong: ["list the directory contents", "show all items"]
222
+ - wrong:
223
+ [
224
+ "list the directory contents",
225
+ "show all items",
226
+ ]
139
227
  - correct: "list all directory contents"
140
228
 
141
229
  - "install and set up dependencies" →
142
- - wrong: ["install dependencies", "set up dependencies"]
230
+ - wrong:
231
+ [
232
+ "install dependencies",
233
+ "set up dependencies",
234
+ ]
143
235
  - correct: "install dependencies"
144
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
+
145
245
  ### Correct Examples: Single Task
146
246
 
147
247
  Simple requests should remain as single tasks:
@@ -158,15 +258,43 @@ Simple requests should remain as single tasks:
158
258
 
159
259
  Only split when tasks are truly distinct operations:
160
260
 
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"]
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
+ ]
164
276
 
165
277
  ### Correct Examples: Complex Questions
166
278
 
167
279
  Split only when multiple distinct queries or operations are needed:
168
280
 
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"]
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
+ ]
package/dist/index.js CHANGED
@@ -4,9 +4,10 @@ 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
9
  import { Please } from './ui/Please.js';
10
+ import { ConfigThenCommand } from './ui/ConfigThenCommand.js';
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = dirname(__filename);
12
13
  // Get package info
@@ -26,16 +27,35 @@ const appInfo = {
26
27
  // Get command from command-line arguments
27
28
  const args = process.argv.slice(2);
28
29
  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
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
35
48
  try {
36
49
  const config = loadConfig();
37
- const claudeService = createAnthropicService(config.claudeApiKey);
38
- render(_jsx(Please, { app: appInfo, command: rawCommand, claudeService: claudeService }));
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
+ }
39
59
  }
40
60
  catch (error) {
41
61
  if (error instanceof ConfigError) {
@@ -45,3 +65,4 @@ else {
45
65
  throw error;
46
66
  }
47
67
  }
68
+ runApp();
@@ -8,7 +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
  }
@@ -49,6 +49,6 @@ export class AnthropicService {
49
49
  return [text];
50
50
  }
51
51
  }
52
- export function createAnthropicService(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
+ });
@@ -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,9 +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';
2
4
  import { Command } from './Command.js';
3
5
  import { Welcome } from './Welcome.js';
4
- export const Please = ({ app: info, command, claudeService }) => {
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
5
11
  if (command && claudeService) {
6
- return _jsx(Command, { rawCommand: command, claudeService: claudeService });
12
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: [_jsx(History, { items: history }), _jsx(Command, { rawCommand: command, claudeService: claudeService })] }));
7
13
  }
8
- return _jsx(Welcome, { info: info });
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 }))] }));
9
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.1.0",
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",
@@ -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",