prompt-language-shell 0.1.2 → 0.1.6

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
@@ -10,61 +10,40 @@ npm install -g prompt-language-shell
10
10
 
11
11
  ## Setup
12
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.
13
+ On first run, `pls` walks you through a quick setup. Your settings will be saved to `~/.plsrc`.
24
14
 
25
15
  ## Usage
26
16
 
27
- Simply type `pls` followed by your request in natural language:
17
+ Type `pls` followed by your request in natural language:
28
18
 
29
19
  ```bash
30
20
  pls change dir to ~
31
21
  ```
32
22
 
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:
23
+ Your command will be interpreted and organized into a list of tasks:
40
24
 
41
25
  ```
42
26
  > 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
-
48
- ```bash
49
- pls install deps, run tests and deploy
27
+ - Change directory to the home folder
50
28
  ```
51
29
 
52
- Example output:
30
+ You can provide multiple requests at once:
53
31
 
54
32
  ```
55
33
  > pls install deps, run tests and deploy
56
- - install dependencies
57
- - run tests
58
- - deploy to server
34
+ - Install dependencies
35
+ - Run tests
36
+ - Deploy to server
59
37
  ```
60
38
 
61
39
  Run `pls` without arguments to see the welcome screen.
62
40
 
63
41
  ## Configuration
64
42
 
65
- Configuration is stored in `~/.pls/.env`. Currently supported:
43
+ Your configuration is stored in `~/.plsrc` as a YAML file. Supported settings:
66
44
 
67
- - `CLAUDE_API_KEY` - Your Anthropic API key (required)
45
+ - `anthropic.api-key` - Your Anthropic API key
46
+ - `anthropic.model` - The Claude model to use for task planning
68
47
 
69
48
  ## Development
70
49
 
@@ -15,6 +15,38 @@ 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
+
18
50
  ## Evaluation of Requests
19
51
 
20
52
  Before processing any request, evaluate its nature and respond appropriately:
@@ -35,18 +67,41 @@ If the request is too vague or unclear to understand what action should be
35
67
  taken, return the exact phrase "abort unclear request".
36
68
 
37
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.
38
77
  - Common abbreviations and acronyms in technical contexts
39
78
  - Well-known product names, tools, or technologies
40
79
  - Context clues within the request itself
41
80
  - Standard industry terminology
42
81
 
43
- For example:
44
- - "test GX" "GX" possibly means Opera GX browser
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:
45
99
  - "run TS compiler" → "TS" stands for TypeScript
46
100
  - "open VSC" → "VSC" likely means Visual Studio Code
101
+ - "run unit tests" → standard development terminology for testing
47
102
 
48
103
  Only mark as unclear if the request is truly unintelligible or lacks any
49
- discernible intent.
104
+ discernible intent, even after considering available skills and context.
50
105
 
51
106
  Examples that are too vague:
52
107
  - "do stuff"
@@ -168,7 +223,9 @@ Split into multiple tasks when:
168
223
  - Single task: Return ONLY the corrected command text
169
224
  - Multiple tasks: Return ONLY a JSON array of strings
170
225
 
171
- 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.
172
229
 
173
230
  ## Final Validation Before Response
174
231
 
package/dist/index.js CHANGED
@@ -6,8 +6,7 @@ import { dirname, join } from 'path';
6
6
  import { render, Text } from 'ink';
7
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';
10
- import { ConfigThenCommand } from './ui/ConfigThenCommand.js';
9
+ import { Main } from './ui/Main.js';
11
10
  const __filename = fileURLToPath(import.meta.url);
12
11
  const __dirname = dirname(__filename);
13
12
  // Get package info
@@ -18,7 +17,7 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
18
17
  // In production, we're in node_modules and src/ doesn't exist alongside
19
18
  const srcPath = join(__dirname, '../src');
20
19
  const isDev = existsSync(srcPath);
21
- const appInfo = {
20
+ const app = {
22
21
  name: packageJson.name,
23
22
  version: packageJson.version,
24
23
  description: packageJson.description,
@@ -26,36 +25,24 @@ const appInfo = {
26
25
  };
27
26
  // Get command from command-line arguments
28
27
  const args = process.argv.slice(2);
29
- const rawCommand = args.join(' ').trim();
28
+ const command = args.join(' ').trim() || null;
30
29
  async function runApp() {
31
- // Check if config exists, if not run setup
30
+ // First-time setup: config doesn't exist
32
31
  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
- }
32
+ const { waitUntilExit } = render(_jsx(Main, { app: app, command: command, isReady: false, onConfigured: (config) => {
33
+ saveConfig('anthropic', config);
34
+ // Create service once for the session
35
+ return command ? createAnthropicService(config) : undefined;
36
+ } }));
37
+ await waitUntilExit();
38
+ return;
46
39
  }
47
40
  // Try to load and validate config
48
41
  try {
49
42
  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
- }
43
+ // Create service once at app initialization
44
+ const service = createAnthropicService(config.anthropic);
45
+ render(_jsx(Main, { app: app, command: command, service: service, isReady: true }));
59
46
  }
60
47
  catch (error) {
61
48
  if (error instanceof ConfigError) {
@@ -2,25 +2,30 @@ 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-haiku-4-5-20251001') {
12
- this.client = new Anthropic({ apiKey });
12
+ constructor(key, model = 'claude-haiku-4-5-20251001') {
13
+ this.client = new Anthropic({ apiKey: key });
13
14
  this.model = model;
14
15
  }
15
- async processCommand(rawCommand) {
16
+ async processCommand(command) {
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
- max_tokens: 200,
19
- system: PLAN_PROMPT,
23
+ max_tokens: 512,
24
+ system: systemPrompt,
20
25
  messages: [
21
26
  {
22
27
  role: 'user',
23
- content: rawCommand,
28
+ content: command,
24
29
  },
25
30
  ],
26
31
  });
@@ -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, model) {
53
- return new AnthropicService(apiKey, model);
72
+ export function createAnthropicService(config) {
73
+ return new AnthropicService(config.key, config.model);
54
74
  }
@@ -1,10 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
- import { join, dirname } from 'path';
4
- import { fileURLToPath } from 'url';
3
+ import { join } from 'path';
5
4
  import YAML from 'yaml';
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
8
5
  export class ConfigError extends Error {
9
6
  constructor(message) {
10
7
  super(message);
@@ -32,37 +29,25 @@ function validateConfig(parsed) {
32
29
  throw new ConfigError(`\nMissing or invalid 'anthropic' section in ${CONFIG_FILE}\n` +
33
30
  'Please add:\n' +
34
31
  'anthropic:\n' +
35
- ' api-key: sk-ant-...');
32
+ ' key: sk-ant-...');
36
33
  }
37
34
  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` +
35
+ const key = anthropic['key'];
36
+ if (!key || typeof key !== 'string') {
37
+ throw new ConfigError(`\nMissing or invalid 'anthropic.key' in ${CONFIG_FILE}\n` +
42
38
  'Please add your Anthropic API key:\n' +
43
39
  'anthropic:\n' +
44
- ' api-key: sk-ant-...');
40
+ ' key: sk-ant-...');
45
41
  }
46
42
  const validatedConfig = {
47
43
  anthropic: {
48
- apiKey: apiKey,
44
+ key,
49
45
  },
50
46
  };
51
47
  // Optional model
52
48
  if (anthropic.model && typeof anthropic.model === 'string') {
53
49
  validatedConfig.anthropic.model = anthropic.model;
54
50
  }
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;
61
- }
62
- if (typeof ui.verbose === 'boolean') {
63
- validatedConfig.ui.verbose = ui.verbose;
64
- }
65
- }
66
51
  return validatedConfig;
67
52
  }
68
53
  export function loadConfig() {
@@ -71,7 +56,7 @@ export function loadConfig() {
71
56
  'Please create it with your Anthropic API key.\n' +
72
57
  'Example:\n\n' +
73
58
  'anthropic:\n' +
74
- ' api-key: sk-ant-...\n' +
59
+ ' key: sk-ant-...\n' +
75
60
  ' model: claude-haiku-4-5-20251001\n');
76
61
  }
77
62
  const content = readFileSync(CONFIG_FILE, 'utf-8');
@@ -86,10 +71,10 @@ export function configExists() {
86
71
  }
87
72
  export function mergeConfig(existingContent, sectionName, newValues) {
88
73
  const parsed = existingContent.trim()
89
- ? YAML.parse(existingContent) || {}
74
+ ? YAML.parse(existingContent)
90
75
  : {};
91
76
  // Update or add section
92
- const section = parsed[sectionName] || {};
77
+ const section = parsed[sectionName] ?? {};
93
78
  for (const [key, value] of Object.entries(newValues)) {
94
79
  section[key] = value;
95
80
  }
@@ -103,13 +88,10 @@ export function mergeConfig(existingContent, sectionName, newValues) {
103
88
  // Convert back to YAML
104
89
  return YAML.stringify(sortedConfig);
105
90
  }
106
- export function saveConfig(apiKey, model) {
91
+ export function saveConfig(section, config) {
107
92
  const existingContent = existsSync(CONFIG_FILE)
108
93
  ? readFileSync(CONFIG_FILE, 'utf-8')
109
94
  : '';
110
- const newContent = mergeConfig(existingContent, 'anthropic', {
111
- 'api-key': apiKey,
112
- model: model,
113
- });
95
+ const newContent = mergeConfig(existingContent, section, config);
114
96
  writeFileSync(CONFIG_FILE, newContent, 'utf-8');
115
97
  }
@@ -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 {
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
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,22 +2,35 @@ 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 = 2000; // purelly for visual effect
6
- export function Command({ rawCommand, claudeService }) {
7
- const [processedTasks, setProcessedTasks] = useState([]);
8
- const [error, setError] = useState(null);
9
- const [isLoading, setIsLoading] = useState(true);
5
+ const MIN_PROCESSING_TIME = 2000; // purely for visual effect
6
+ export function Command({ command, state, service, tasks, error: errorProp, systemPrompt: systemPromptProp, }) {
7
+ const done = state?.done ?? false;
8
+ const [processedTasks, setProcessedTasks] = useState(tasks || []);
9
+ const [systemPrompt, setSystemPrompt] = useState(systemPromptProp);
10
+ const [error, setError] = useState(state?.error || errorProp || null);
11
+ const [isLoading, setIsLoading] = useState(state?.isLoading ?? !done);
10
12
  useEffect(() => {
13
+ // Skip processing if done (showing historical/final state)
14
+ if (done) {
15
+ return;
16
+ }
17
+ // Skip processing if no service available
18
+ if (!service) {
19
+ setError('No service available');
20
+ setIsLoading(false);
21
+ return;
22
+ }
11
23
  let mounted = true;
12
- async function process() {
24
+ async function process(svc) {
13
25
  const startTime = Date.now();
14
26
  try {
15
- const result = await claudeService.processCommand(rawCommand);
27
+ const result = await svc.processCommand(command);
16
28
  const elapsed = Date.now() - startTime;
17
29
  const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
18
30
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
19
31
  if (mounted) {
20
- setProcessedTasks(result);
32
+ setProcessedTasks(result.tasks);
33
+ setSystemPrompt(result.systemPrompt);
21
34
  setIsLoading(false);
22
35
  }
23
36
  }
@@ -31,10 +44,10 @@ export function Command({ rawCommand, claudeService }) {
31
44
  }
32
45
  }
33
46
  }
34
- process();
47
+ process(service);
35
48
  return () => {
36
49
  mounted = false;
37
50
  };
38
- }, [rawCommand, claudeService]);
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))) }))] }));
51
+ }, [command, done, service]);
52
+ return (_jsxs(Box, { alignSelf: "flex-start", marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["> pls ", command] }), 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
53
  }
@@ -0,0 +1,23 @@
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 Configure({ state, key: keyProp, model: modelProp, onComplete, }) {
6
+ const done = state?.done ?? false;
7
+ const [step, setStep] = React.useState(state?.step ?? (done ? 'done' : 'key'));
8
+ const [key, setKey] = React.useState(keyProp || '');
9
+ const [model, setModel] = React.useState(modelProp || 'claude-haiku-4-5-20251001');
10
+ const handleKeySubmit = (value) => {
11
+ setKey(value);
12
+ setStep('model');
13
+ };
14
+ const handleModelSubmit = (value) => {
15
+ const finalModel = value.trim() || 'claude-haiku-4-5-20251001';
16
+ setModel(finalModel);
17
+ setStep('done');
18
+ if (onComplete) {
19
+ onComplete({ key, model: finalModel });
20
+ }
21
+ };
22
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [!done && _jsx(Text, { children: "Configuration required." }), !done && (_jsx(Box, { children: _jsxs(Text, { color: "whiteBright", dimColor: true, children: ['==>', " Get your API key from: https://platform.claude.com/"] }) })), _jsx(Box, { marginTop: done ? 0 : 1, children: _jsx(Text, { children: "Anthropic API key:" }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "> " }), step === 'key' && !done ? (_jsx(TextInput, { value: key, onChange: setKey, onSubmit: handleKeySubmit, 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", ' ', !done && (_jsx(Text, { dimColor: true, children: "(default: claude-haiku-4-5-20251001)" })), ":"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "> " }), step === 'model' && !done ? (_jsx(TextInput, { value: model, onChange: setModel, onSubmit: handleModelSubmit })) : (_jsx(Text, { dimColor: true, children: model }))] })] })), step === 'done' && !done && (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: "green", children: "\u2713 Configuration saved" }) }))] }));
23
+ }
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
+ import { renderComponent } from './renderComponent.js';
3
4
  export function History({ items }) {
4
5
  if (items.length === 0) {
5
6
  return null;
6
7
  }
7
- return (_jsx(Box, { flexDirection: "column", gap: 1, children: items.map((item, index) => (_jsx(Box, { children: item }, index))) }));
8
+ return (_jsx(Box, { flexDirection: "column", gap: 1, children: items.map((item, index) => (_jsx(Box, { children: renderComponent(item) }, `${item.name}-${index}`))) }));
8
9
  }
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box } from 'ink';
4
+ import { History } from './History.js';
5
+ import { renderComponent } from './renderComponent.js';
6
+ export const Main = ({ app, command, service, isReady, onConfigured, }) => {
7
+ const [history, setHistory] = React.useState([]);
8
+ const [current, setCurrent] = React.useState(null);
9
+ React.useEffect(() => {
10
+ // Initialize history and current component based on props
11
+ if (!isReady) {
12
+ // Not configured - show welcome in history, configure as current
13
+ setHistory([
14
+ {
15
+ name: 'welcome',
16
+ props: {
17
+ app,
18
+ },
19
+ },
20
+ ]);
21
+ setCurrent({
22
+ name: 'configure',
23
+ state: {
24
+ done: false,
25
+ step: 'key',
26
+ },
27
+ props: {
28
+ onComplete: onConfigured,
29
+ },
30
+ });
31
+ }
32
+ else if (command && service) {
33
+ setCurrent({
34
+ name: 'command',
35
+ state: {
36
+ done: false,
37
+ isLoading: true,
38
+ },
39
+ props: {
40
+ command,
41
+ service,
42
+ },
43
+ });
44
+ }
45
+ else {
46
+ setCurrent({
47
+ name: 'welcome',
48
+ props: {
49
+ app,
50
+ },
51
+ });
52
+ }
53
+ }, [isReady, command, service, app, onConfigured]);
54
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: [_jsx(History, { items: history }), current && renderComponent(current)] }));
55
+ };
package/dist/ui/Please.js CHANGED
@@ -5,12 +5,21 @@ import { Command } from './Command.js';
5
5
  import { Welcome } from './Welcome.js';
6
6
  import { ConfigSetup } from './ConfigSetup.js';
7
7
  import { History } from './History.js';
8
- export const Please = ({ app: info, command, claudeService, showConfigSetup, onConfigComplete, }) => {
8
+ export const PLS = ({ app: info, command, claudeService, showConfigSetup, onConfigComplete, }) => {
9
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 })] }));
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 })] }));
13
22
  }
14
23
  // 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 }))] }));
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 })] }));
16
25
  };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text, Box } from 'ink';
3
- export function Welcome({ info: app }) {
3
+ export function Welcome({ app: app }) {
4
4
  const descriptionLines = app.description
5
5
  .split('. ')
6
6
  .map((line) => line.replace(/\.$/, ''))
@@ -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", 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]" })] })] })] })] }) }));
12
+ return (_jsx(Box, { alignSelf: "flex-start", marginBottom: 1, 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
  }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Welcome } from './Welcome.js';
3
+ import { Configure } from './Configure.js';
4
+ import { Command } from './Command.js';
5
+ export function renderComponent(def) {
6
+ switch (def.name) {
7
+ case 'welcome':
8
+ return _jsx(Welcome, { ...def.props });
9
+ case 'configure':
10
+ return (_jsx(Configure, { ...def.props, state: 'state' in def ? def.state : undefined }));
11
+ case 'command':
12
+ return (_jsx(Command, { ...def.props, state: 'state' in def ? def.state : undefined }));
13
+ }
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.1.2",
3
+ "version": "0.1.6",
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",
@@ -16,8 +16,8 @@
16
16
  "prepare": "npm run build",
17
17
  "test": "vitest run",
18
18
  "test:watch": "vitest",
19
- "format": "prettier --write .",
20
- "format:check": "prettier --check .",
19
+ "format": "prettier --write '**/*.{ts,tsx}'",
20
+ "format:check": "prettier --check '**/*.{ts,tsx}'",
21
21
  "lint": "eslint .",
22
22
  "lint:fix": "eslint --fix ."
23
23
  },