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.
@@ -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
+ }
@@ -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: [_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { marginBottom: 1, gap: 1, children: [_jsx(Text, { color: "whiteBright", dimColor: true, children: ">" }), _jsx(Text, { children: "please" }), _jsx(Text, { color: "whiteBright", dimColor: true, children: "(" }), _jsx(Text, { color: "greenBright", bold: true, children: "pls" }), _jsx(Text, { color: "whiteBright", dimColor: true, children: ")" }), _jsx(Text, { children: "do stuff" })] }) }), _jsxs(Box, { marginBottom: 1, gap: 1, children: [words.map((word, index) => (_jsx(Text, { color: "greenBright", 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: "whiteBright", dimColor: true, children: [line, "."] }) }, index)))] }) }));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
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",