prompt-language-shell 0.1.4 → 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
 
package/dist/index.js CHANGED
@@ -6,7 +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 { PLS } from './ui/Please.js';
9
+ import { Main } from './ui/Main.js';
10
10
  const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
12
  // Get package info
@@ -17,7 +17,7 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
17
17
  // In production, we're in node_modules and src/ doesn't exist alongside
18
18
  const srcPath = join(__dirname, '../src');
19
19
  const isDev = existsSync(srcPath);
20
- const appInfo = {
20
+ const app = {
21
21
  name: packageJson.name,
22
22
  version: packageJson.version,
23
23
  description: packageJson.description,
@@ -25,13 +25,14 @@ const appInfo = {
25
25
  };
26
26
  // Get command from command-line arguments
27
27
  const args = process.argv.slice(2);
28
- const rawCommand = args.join(' ').trim();
28
+ const command = args.join(' ').trim() || null;
29
29
  async function runApp() {
30
30
  // First-time setup: config doesn't exist
31
31
  if (!configExists()) {
32
- const { waitUntilExit } = render(_jsx(PLS, { app: appInfo, command: rawCommand || null, showConfigSetup: true, onConfigComplete: ({ apiKey, model }) => {
33
- saveConfig(apiKey, model);
34
- return rawCommand ? createAnthropicService(apiKey, model) : undefined;
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;
35
36
  } }));
36
37
  await waitUntilExit();
37
38
  return;
@@ -39,15 +40,9 @@ async function runApp() {
39
40
  // Try to load and validate config
40
41
  try {
41
42
  const config = loadConfig();
42
- if (!rawCommand) {
43
- // "pls" when config present: show welcome box
44
- render(_jsx(PLS, { app: appInfo, command: null }));
45
- }
46
- else {
47
- // "pls do stuff": fetch and show the plan
48
- const claudeService = createAnthropicService(config.anthropic.apiKey, config.anthropic.model);
49
- render(_jsx(PLS, { app: appInfo, command: rawCommand, claudeService: claudeService }));
50
- }
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 }));
51
46
  }
52
47
  catch (error) {
53
48
  if (error instanceof ConfigError) {
@@ -9,23 +9,23 @@ const PLAN_PROMPT = readFileSync(join(__dirname, '../config/PLAN.md'), 'utf-8');
9
9
  export class AnthropicService {
10
10
  client;
11
11
  model;
12
- constructor(apiKey, model = 'claude-haiku-4-5-20251001') {
13
- this.client = new Anthropic({ apiKey });
12
+ constructor(key, model = 'claude-haiku-4-5-20251001') {
13
+ this.client = new Anthropic({ apiKey: key });
14
14
  this.model = model;
15
15
  }
16
- async processCommand(rawCommand) {
16
+ async processCommand(command) {
17
17
  // Load skills and augment the planning prompt
18
18
  const skills = loadSkills();
19
19
  const skillsSection = formatSkillsForPrompt(skills);
20
20
  const systemPrompt = PLAN_PROMPT + skillsSection;
21
21
  const response = await this.client.messages.create({
22
22
  model: this.model,
23
- max_tokens: 200,
23
+ max_tokens: 512,
24
24
  system: systemPrompt,
25
25
  messages: [
26
26
  {
27
27
  role: 'user',
28
- content: rawCommand,
28
+ content: command,
29
29
  },
30
30
  ],
31
31
  });
@@ -69,6 +69,6 @@ export class AnthropicService {
69
69
  };
70
70
  }
71
71
  }
72
- export function createAnthropicService(apiKey, model) {
73
- return new AnthropicService(apiKey, model);
72
+ export function createAnthropicService(config) {
73
+ return new AnthropicService(config.key, config.model);
74
74
  }
@@ -29,20 +29,19 @@ function validateConfig(parsed) {
29
29
  throw new ConfigError(`\nMissing or invalid 'anthropic' section in ${CONFIG_FILE}\n` +
30
30
  'Please add:\n' +
31
31
  'anthropic:\n' +
32
- ' api-key: sk-ant-...');
32
+ ' key: sk-ant-...');
33
33
  }
34
34
  const anthropic = config.anthropic;
35
- // Support both 'api-key' (kebab-case) and 'apiKey' (camelCase)
36
- const apiKey = anthropic['api-key'] || anthropic.apiKey;
37
- if (!apiKey || typeof apiKey !== 'string') {
38
- 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` +
39
38
  'Please add your Anthropic API key:\n' +
40
39
  'anthropic:\n' +
41
- ' api-key: sk-ant-...');
40
+ ' key: sk-ant-...');
42
41
  }
43
42
  const validatedConfig = {
44
43
  anthropic: {
45
- apiKey: apiKey,
44
+ key,
46
45
  },
47
46
  };
48
47
  // Optional model
@@ -57,7 +56,7 @@ export function loadConfig() {
57
56
  'Please create it with your Anthropic API key.\n' +
58
57
  'Example:\n\n' +
59
58
  'anthropic:\n' +
60
- ' api-key: sk-ant-...\n' +
59
+ ' key: sk-ant-...\n' +
61
60
  ' model: claude-haiku-4-5-20251001\n');
62
61
  }
63
62
  const content = readFileSync(CONFIG_FILE, 'utf-8');
@@ -72,10 +71,10 @@ export function configExists() {
72
71
  }
73
72
  export function mergeConfig(existingContent, sectionName, newValues) {
74
73
  const parsed = existingContent.trim()
75
- ? YAML.parse(existingContent) || {}
74
+ ? YAML.parse(existingContent)
76
75
  : {};
77
76
  // Update or add section
78
- const section = parsed[sectionName] || {};
77
+ const section = parsed[sectionName] ?? {};
79
78
  for (const [key, value] of Object.entries(newValues)) {
80
79
  section[key] = value;
81
80
  }
@@ -89,13 +88,10 @@ export function mergeConfig(existingContent, sectionName, newValues) {
89
88
  // Convert back to YAML
90
89
  return YAML.stringify(sortedConfig);
91
90
  }
92
- export function saveConfig(apiKey, model) {
91
+ export function saveConfig(section, config) {
93
92
  const existingContent = existsSync(CONFIG_FILE)
94
93
  ? readFileSync(CONFIG_FILE, 'utf-8')
95
94
  : '';
96
- const newContent = mergeConfig(existingContent, 'anthropic', {
97
- 'api-key': apiKey,
98
- model: model,
99
- });
95
+ const newContent = mergeConfig(existingContent, section, config);
100
96
  writeFileSync(CONFIG_FILE, newContent, 'utf-8');
101
97
  }
@@ -27,7 +27,7 @@ export function loadSkills() {
27
27
  return readFileSync(filePath, 'utf-8');
28
28
  });
29
29
  }
30
- catch (error) {
30
+ catch {
31
31
  // Return empty array if there's any error reading the directory
32
32
  return [];
33
33
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,18 +2,29 @@ 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 [systemPrompt, setSystemPrompt] = useState();
9
- const [error, setError] = useState(null);
10
- 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);
11
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
+ }
12
23
  let mounted = true;
13
- async function process() {
24
+ async function process(svc) {
14
25
  const startTime = Date.now();
15
26
  try {
16
- const result = await claudeService.processCommand(rawCommand);
27
+ const result = await svc.processCommand(command);
17
28
  const elapsed = Date.now() - startTime;
18
29
  const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
19
30
  await new Promise((resolve) => setTimeout(resolve, remainingTime));
@@ -33,10 +44,10 @@ export function Command({ rawCommand, claudeService }) {
33
44
  }
34
45
  }
35
46
  }
36
- process();
47
+ process(service);
37
48
  return () => {
38
49
  mounted = false;
39
50
  };
40
- }, [rawCommand, claudeService]);
41
- 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))) }))] }));
42
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
+ };
@@ -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.4",
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
  },