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 +11 -32
- package/dist/config/PLAN.md +61 -4
- package/dist/index.js +14 -27
- package/dist/services/anthropic.js +32 -12
- package/dist/services/config.js +12 -30
- package/dist/services/skills.js +52 -0
- package/dist/types/components.js +1 -0
- package/dist/ui/Command.js +24 -11
- package/dist/ui/Configure.js +23 -0
- package/dist/ui/History.js +2 -1
- package/dist/ui/Main.js +55 -0
- package/dist/ui/Please.js +14 -5
- package/dist/ui/Welcome.js +2 -2
- package/dist/ui/renderComponent.js +14 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -10,61 +10,40 @@ npm install -g prompt-language-shell
|
|
|
10
10
|
|
|
11
11
|
## Setup
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
30
|
+
You can provide multiple requests at once:
|
|
53
31
|
|
|
54
32
|
```
|
|
55
33
|
> pls install deps, run tests and deploy
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
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
|
-
|
|
43
|
+
Your configuration is stored in `~/.plsrc` as a YAML file. Supported settings:
|
|
66
44
|
|
|
67
|
-
- `
|
|
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
|
|
package/dist/config/PLAN.md
CHANGED
|
@@ -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
|
-
- "
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
|
28
|
+
const command = args.join(' ').trim() || null;
|
|
30
29
|
async function runApp() {
|
|
31
|
-
//
|
|
30
|
+
// First-time setup: config doesn't exist
|
|
32
31
|
if (!configExists()) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
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(
|
|
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:
|
|
19
|
-
system:
|
|
23
|
+
max_tokens: 512,
|
|
24
|
+
system: systemPrompt,
|
|
20
25
|
messages: [
|
|
21
26
|
{
|
|
22
27
|
role: 'user',
|
|
23
|
-
content:
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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(
|
|
53
|
-
return new AnthropicService(
|
|
72
|
+
export function createAnthropicService(config) {
|
|
73
|
+
return new AnthropicService(config.key, config.model);
|
|
54
74
|
}
|
package/dist/services/config.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
|
-
import { join
|
|
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
|
-
'
|
|
32
|
+
' key: sk-ant-...');
|
|
36
33
|
}
|
|
37
34
|
const anthropic = config.anthropic;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
'
|
|
40
|
+
' key: sk-ant-...');
|
|
45
41
|
}
|
|
46
42
|
const validatedConfig = {
|
|
47
43
|
anthropic: {
|
|
48
|
-
|
|
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
|
-
'
|
|
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(
|
|
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,
|
|
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 {};
|
package/dist/ui/Command.js
CHANGED
|
@@ -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; //
|
|
6
|
-
export function Command({
|
|
7
|
-
const
|
|
8
|
-
const [
|
|
9
|
-
const [
|
|
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
|
|
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
|
-
}, [
|
|
39
|
-
return (_jsxs(Box, { alignSelf: "flex-start", marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: ["> pls ",
|
|
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
|
+
}
|
package/dist/ui/History.js
CHANGED
|
@@ -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
|
}
|
package/dist/ui/Main.js
ADDED
|
@@ -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
|
|
8
|
+
export const PLS = ({ app: info, command, claudeService, showConfigSetup, onConfigComplete, }) => {
|
|
9
9
|
const [history, setHistory] = React.useState([]);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 &&
|
|
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
|
};
|
package/dist/ui/Welcome.js
CHANGED
|
@@ -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({
|
|
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.
|
|
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
|
},
|