prompt-language-shell 0.0.4 → 0.0.5
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/dist/config/SYSTEM.md +41 -0
- package/dist/services/anthropic.js +54 -0
- package/dist/services/claude.js +54 -0
- package/dist/services/config.js +57 -0
- package/dist/ui/Command.js +40 -0
- package/dist/ui/CommandProcessor.js +31 -0
- package/dist/ui/Spinner.js +17 -0
- package/dist/ui/Welcome.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
You are a command-line assistant for a CLI tool called "pls" (please) that
|
|
2
|
+
helps users perform filesystem and system operations using natural language.
|
|
3
|
+
|
|
4
|
+
Your task is to grammatically correct the user's command with as few changes as
|
|
5
|
+
possible to make it sound natural in English. Focus on:
|
|
6
|
+
|
|
7
|
+
- Fixing grammar and sentence structure
|
|
8
|
+
- Making it read naturally
|
|
9
|
+
- Keeping the original intent intact
|
|
10
|
+
- Being concise and clear
|
|
11
|
+
|
|
12
|
+
## Multiple Tasks
|
|
13
|
+
|
|
14
|
+
If the user provides multiple tasks separated by commas (,), semicolons (;), or
|
|
15
|
+
the word "and", you must:
|
|
16
|
+
|
|
17
|
+
1. Identify each individual task
|
|
18
|
+
2. Return a JSON array of corrected tasks
|
|
19
|
+
3. Use this exact format: ["task 1", "task 2", "task 3"]
|
|
20
|
+
|
|
21
|
+
## Response Format
|
|
22
|
+
|
|
23
|
+
- Single task: Return ONLY the corrected command text
|
|
24
|
+
- Multiple tasks: Return ONLY a JSON array of strings
|
|
25
|
+
|
|
26
|
+
Do not include explanations, commentary, or any other text.
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
Single task:
|
|
31
|
+
|
|
32
|
+
- "change dir to ~" → change directory to the home folder
|
|
33
|
+
- "install deps" → install dependencies
|
|
34
|
+
- "make new file called test.txt" → create a new file called test.txt
|
|
35
|
+
- "show me files here" → show the files in the current directory
|
|
36
|
+
|
|
37
|
+
Multiple tasks:
|
|
38
|
+
|
|
39
|
+
- "install deps, run tests" → ["install dependencies", "run tests"]
|
|
40
|
+
- "create file; add content" → ["create a file", "add content"]
|
|
41
|
+
- "build project and deploy" → ["build the project", "deploy"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
const SYSTEM_PROMPT = readFileSync(join(__dirname, '../config/SYSTEM.md'), 'utf-8');
|
|
8
|
+
export class AnthropicService {
|
|
9
|
+
client;
|
|
10
|
+
model;
|
|
11
|
+
constructor(apiKey, model = 'claude-3-5-haiku-20241022') {
|
|
12
|
+
this.client = new Anthropic({ apiKey });
|
|
13
|
+
this.model = model;
|
|
14
|
+
}
|
|
15
|
+
async processCommand(rawCommand) {
|
|
16
|
+
const response = await this.client.messages.create({
|
|
17
|
+
model: this.model,
|
|
18
|
+
max_tokens: 200,
|
|
19
|
+
system: SYSTEM_PROMPT,
|
|
20
|
+
messages: [
|
|
21
|
+
{
|
|
22
|
+
role: 'user',
|
|
23
|
+
content: rawCommand,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
const content = response.content[0];
|
|
28
|
+
if (content.type !== 'text') {
|
|
29
|
+
throw new Error('Unexpected response type from Claude API');
|
|
30
|
+
}
|
|
31
|
+
const text = content.text.trim();
|
|
32
|
+
// Try to parse as JSON array
|
|
33
|
+
if (text.startsWith('[') && text.endsWith(']')) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(text);
|
|
36
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
37
|
+
// Validate all items are strings
|
|
38
|
+
const allStrings = parsed.every((item) => typeof item === 'string');
|
|
39
|
+
if (allStrings) {
|
|
40
|
+
return parsed.filter((item) => typeof item === 'string');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// If JSON parsing fails, treat as single task
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Single task
|
|
49
|
+
return [text];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function createClaudeService(apiKey) {
|
|
53
|
+
return new AnthropicService(apiKey);
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
const SYSTEM_PROMPT = readFileSync(join(__dirname, '../config/SYSTEM.md'), 'utf-8');
|
|
8
|
+
export class AnthropicClaudeService {
|
|
9
|
+
client;
|
|
10
|
+
model;
|
|
11
|
+
constructor(apiKey, model = 'claude-3-5-haiku-20241022') {
|
|
12
|
+
this.client = new Anthropic({ apiKey });
|
|
13
|
+
this.model = model;
|
|
14
|
+
}
|
|
15
|
+
async processCommand(rawCommand) {
|
|
16
|
+
const response = await this.client.messages.create({
|
|
17
|
+
model: this.model,
|
|
18
|
+
max_tokens: 200,
|
|
19
|
+
system: SYSTEM_PROMPT,
|
|
20
|
+
messages: [
|
|
21
|
+
{
|
|
22
|
+
role: 'user',
|
|
23
|
+
content: rawCommand,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
const content = response.content[0];
|
|
28
|
+
if (content.type !== 'text') {
|
|
29
|
+
throw new Error('Unexpected response type from Claude API');
|
|
30
|
+
}
|
|
31
|
+
const text = content.text.trim();
|
|
32
|
+
// Try to parse as JSON array
|
|
33
|
+
if (text.startsWith('[') && text.endsWith(']')) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(text);
|
|
36
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
37
|
+
// Validate all items are strings
|
|
38
|
+
const allStrings = parsed.every((item) => typeof item === 'string');
|
|
39
|
+
if (allStrings) {
|
|
40
|
+
return parsed.filter((item) => typeof item === 'string');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// If JSON parsing fails, treat as single task
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Single task
|
|
49
|
+
return [text];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function createClaudeService(apiKey) {
|
|
53
|
+
return new AnthropicClaudeService(apiKey);
|
|
54
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
export class ConfigError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ConfigError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const CONFIG_DIR = join(homedir(), '.pls');
|
|
11
|
+
const CONFIG_FILE = join(CONFIG_DIR, '.env');
|
|
12
|
+
export function ensureConfigDirectory() {
|
|
13
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
14
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function parseEnvFile(content) {
|
|
18
|
+
const result = {};
|
|
19
|
+
for (const line of content.split('\n')) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
// Skip empty lines and comments
|
|
22
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const equalsIndex = trimmed.indexOf('=');
|
|
26
|
+
if (equalsIndex === -1) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const key = trimmed.slice(0, equalsIndex).trim();
|
|
30
|
+
const value = trimmed.slice(equalsIndex + 1).trim();
|
|
31
|
+
if (key) {
|
|
32
|
+
result[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
export function loadConfig() {
|
|
38
|
+
ensureConfigDirectory();
|
|
39
|
+
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');
|
|
43
|
+
}
|
|
44
|
+
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
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function getConfigPath() {
|
|
56
|
+
return CONFIG_FILE;
|
|
57
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { Spinner } from './Spinner.js';
|
|
5
|
+
const MIN_PROCESSING_TIME = 3000;
|
|
6
|
+
export function Command({ rawCommand, claudeService }) {
|
|
7
|
+
const [processedTasks, setProcessedTasks] = useState([]);
|
|
8
|
+
const [error, setError] = useState(null);
|
|
9
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let mounted = true;
|
|
12
|
+
async function process() {
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
try {
|
|
15
|
+
const result = await claudeService.processCommand(rawCommand);
|
|
16
|
+
const elapsed = Date.now() - startTime;
|
|
17
|
+
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
19
|
+
if (mounted) {
|
|
20
|
+
setProcessedTasks(result);
|
|
21
|
+
setIsLoading(false);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const elapsed = Date.now() - startTime;
|
|
26
|
+
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, remainingTime));
|
|
28
|
+
if (mounted) {
|
|
29
|
+
setError(err instanceof Error ? err.message : 'Unknown error occurred');
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
process();
|
|
35
|
+
return () => {
|
|
36
|
+
mounted = false;
|
|
37
|
+
};
|
|
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))) }))] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
export function CommandProcessor({ rawCommand, claudeService, }) {
|
|
5
|
+
const [processedTask, setProcessedTask] = useState(null);
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
let mounted = true;
|
|
10
|
+
async function process() {
|
|
11
|
+
try {
|
|
12
|
+
const result = await claudeService.processCommand(rawCommand);
|
|
13
|
+
if (mounted) {
|
|
14
|
+
setProcessedTask(result);
|
|
15
|
+
setIsLoading(false);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (mounted) {
|
|
20
|
+
setError(err instanceof Error ? err.message : 'Unknown error occurred');
|
|
21
|
+
setIsLoading(false);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
process();
|
|
26
|
+
return () => {
|
|
27
|
+
mounted = false;
|
|
28
|
+
};
|
|
29
|
+
}, [rawCommand, claudeService]);
|
|
30
|
+
return (_jsxs(Box, { alignSelf: "flex-start", marginTop: 1, marginBottom: 1, flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["> pls ", rawCommand] }) }), isLoading && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "whiteBright", dimColor: true, children: "Processing..." }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), processedTask && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "whiteBright", dimColor: true, children: [' ⎿ ', "Task: \"", processedTask, "\""] }) }))] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
5
|
+
const INTERVAL = 80;
|
|
6
|
+
export function Spinner() {
|
|
7
|
+
const [frame, setFrame] = useState(0);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const timer = setInterval(() => {
|
|
10
|
+
setFrame((prev) => (prev + 1) % FRAMES.length);
|
|
11
|
+
}, INTERVAL);
|
|
12
|
+
return () => {
|
|
13
|
+
clearInterval(timer);
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
16
|
+
return _jsx(Text, { color: "blueBright", children: FRAMES[frame] });
|
|
17
|
+
}
|
package/dist/ui/Welcome.js
CHANGED
|
@@ -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: [
|
|
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]" })] })] })] })] }) }));
|
|
13
13
|
}
|