shellsia 1.0.1
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 +156 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +96 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +40 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +9 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +101 -0
- package/dist/llm.js.map +1 -0
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +49 -0
- package/dist/prompt.js.map +1 -0
- package/dist/renderer.d.ts +8 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +73 -0
- package/dist/renderer.js.map +1 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +90 -0
- package/dist/runner.js.map +1 -0
- package/package.json +52 -0
- package/src/config.ts +109 -0
- package/src/context.ts +51 -0
- package/src/index.ts +135 -0
- package/src/llm.ts +133 -0
- package/src/prompt.ts +48 -0
- package/src/renderer.ts +77 -0
- package/src/runner.ts +97 -0
package/src/llm.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import Groq from 'groq-sdk';
|
|
2
|
+
import OpenAI from 'openai';
|
|
3
|
+
import { getConfig } from './config';
|
|
4
|
+
import { SystemContext } from './context';
|
|
5
|
+
import { buildSystemPrompt, buildUserPrompt } from './prompt';
|
|
6
|
+
|
|
7
|
+
export interface LLMResponse {
|
|
8
|
+
command: string;
|
|
9
|
+
explanation: string;
|
|
10
|
+
dangerous: boolean;
|
|
11
|
+
alternatives: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseResponse(raw: string): LLMResponse {
|
|
15
|
+
// Try to extract JSON from the response, handling potential markdown code fences
|
|
16
|
+
let jsonStr = raw.trim();
|
|
17
|
+
|
|
18
|
+
// Remove markdown code fences if present
|
|
19
|
+
const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
20
|
+
if (jsonMatch) {
|
|
21
|
+
jsonStr = jsonMatch[1].trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Try to find a JSON object in the response
|
|
25
|
+
const objectMatch = jsonStr.match(/\{[\s\S]*\}/);
|
|
26
|
+
if (objectMatch) {
|
|
27
|
+
jsonStr = objectMatch[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(jsonStr);
|
|
32
|
+
return {
|
|
33
|
+
command: parsed.command || '',
|
|
34
|
+
explanation: parsed.explanation || 'No explanation provided',
|
|
35
|
+
dangerous: Boolean(parsed.dangerous),
|
|
36
|
+
alternatives: Array.isArray(parsed.alternatives) ? parsed.alternatives : [],
|
|
37
|
+
};
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Failed to parse LLM response as JSON.\nRaw response:\n${raw}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function callGroq(
|
|
46
|
+
query: string,
|
|
47
|
+
context: SystemContext,
|
|
48
|
+
apiKey: string
|
|
49
|
+
): Promise<LLMResponse> {
|
|
50
|
+
const groq = new Groq({ apiKey });
|
|
51
|
+
|
|
52
|
+
const completion = await groq.chat.completions.create({
|
|
53
|
+
model: 'llama-3.3-70b-versatile',
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: 'system', content: buildSystemPrompt(context) },
|
|
56
|
+
{ role: 'user', content: buildUserPrompt(query) },
|
|
57
|
+
],
|
|
58
|
+
temperature: 0.1,
|
|
59
|
+
max_tokens: 512,
|
|
60
|
+
response_format: { type: 'json_object' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const content = completion.choices[0]?.message?.content;
|
|
64
|
+
if (!content) {
|
|
65
|
+
throw new Error('No response received from Groq');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return parseResponse(content);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function callOpenAI(
|
|
72
|
+
query: string,
|
|
73
|
+
context: SystemContext,
|
|
74
|
+
apiKey: string
|
|
75
|
+
): Promise<LLMResponse> {
|
|
76
|
+
const openai = new OpenAI({ apiKey });
|
|
77
|
+
|
|
78
|
+
const completion = await openai.chat.completions.create({
|
|
79
|
+
model: 'gpt-4o-mini',
|
|
80
|
+
messages: [
|
|
81
|
+
{ role: 'system', content: buildSystemPrompt(context) },
|
|
82
|
+
{ role: 'user', content: buildUserPrompt(query) },
|
|
83
|
+
],
|
|
84
|
+
temperature: 0.1,
|
|
85
|
+
max_tokens: 512,
|
|
86
|
+
response_format: { type: 'json_object' },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const content = completion.choices[0]?.message?.content;
|
|
90
|
+
if (!content) {
|
|
91
|
+
throw new Error('No response received from OpenAI');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return parseResponse(content);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function queryLLM(
|
|
98
|
+
query: string,
|
|
99
|
+
context: SystemContext
|
|
100
|
+
): Promise<LLMResponse> {
|
|
101
|
+
const config = getConfig();
|
|
102
|
+
const provider = config.defaultProvider;
|
|
103
|
+
|
|
104
|
+
if (provider === 'groq' && config.groqApiKey) {
|
|
105
|
+
try {
|
|
106
|
+
return await callGroq(query, context, config.groqApiKey);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// If Groq fails and we have an OpenAI key, fall back
|
|
109
|
+
if (config.openaiApiKey) {
|
|
110
|
+
console.warn('Groq failed, falling back to OpenAI...');
|
|
111
|
+
return await callOpenAI(query, context, config.openaiApiKey);
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (provider === 'openai' && config.openaiApiKey) {
|
|
118
|
+
return await callOpenAI(query, context, config.openaiApiKey);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Auto-detect available provider
|
|
122
|
+
if (config.groqApiKey) {
|
|
123
|
+
return await callGroq(query, context, config.groqApiKey);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (config.openaiApiKey) {
|
|
127
|
+
return await callOpenAI(query, context, config.openaiApiKey);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new Error(
|
|
131
|
+
'No API key configured. Run "s --setup" to configure your API keys.'
|
|
132
|
+
);
|
|
133
|
+
}
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SystemContext } from './context';
|
|
2
|
+
|
|
3
|
+
export function buildSystemPrompt(context: SystemContext): string {
|
|
4
|
+
const osName = {
|
|
5
|
+
darwin: 'macOS',
|
|
6
|
+
linux: 'Linux',
|
|
7
|
+
win32: 'Windows',
|
|
8
|
+
}[context.os] || context.os;
|
|
9
|
+
|
|
10
|
+
return `You are Shellsia, an expert shell command assistant. Your job is to translate natural language requests into precise, working shell commands.
|
|
11
|
+
|
|
12
|
+
ENVIRONMENT:
|
|
13
|
+
- Operating System: ${osName} (${context.os})
|
|
14
|
+
- Shell: ${context.shell}
|
|
15
|
+
- Current Directory: ${context.cwd}
|
|
16
|
+
- Home Directory: ${context.homeDir}
|
|
17
|
+
- User: ${context.username}
|
|
18
|
+
- Architecture: ${context.arch}
|
|
19
|
+
|
|
20
|
+
RULES:
|
|
21
|
+
1. Return ONLY a valid JSON object — no markdown, no code fences, no extra text.
|
|
22
|
+
2. Never use sudo unless the user explicitly asks for it.
|
|
23
|
+
3. Prefer readable, well-structured commands over clever one-liners.
|
|
24
|
+
4. Use commands compatible with the detected OS and shell.
|
|
25
|
+
5. If a command could be destructive (deletes files, overwrites data, modifies system config), mark "dangerous" as true.
|
|
26
|
+
6. Provide 1-2 alternative commands when applicable (simpler or safer variants).
|
|
27
|
+
7. The explanation should be one clear sentence describing what the command does.
|
|
28
|
+
|
|
29
|
+
RESPONSE FORMAT (strict JSON):
|
|
30
|
+
{
|
|
31
|
+
"command": "the exact shell command to run",
|
|
32
|
+
"explanation": "one sentence explaining what this command does",
|
|
33
|
+
"dangerous": false,
|
|
34
|
+
"alternatives": ["alternative command 1", "alternative command 2"]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
If you cannot produce a valid command, return:
|
|
38
|
+
{
|
|
39
|
+
"command": "",
|
|
40
|
+
"explanation": "I'm unable to generate a command for this request because...",
|
|
41
|
+
"dangerous": false,
|
|
42
|
+
"alternatives": []
|
|
43
|
+
}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildUserPrompt(query: string): string {
|
|
47
|
+
return `Convert this to a shell command: "${query}"`;
|
|
48
|
+
}
|
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { LLMResponse } from './llm';
|
|
3
|
+
|
|
4
|
+
function pad(str: string, width: number): string {
|
|
5
|
+
const visible = str.replace(/\u001b\[[0-9;]*m/g, '');
|
|
6
|
+
const padding = Math.max(0, width - visible.length);
|
|
7
|
+
return str + ' '.repeat(padding);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function renderResponse(response: LLMResponse): void {
|
|
11
|
+
const { command, explanation, dangerous, alternatives } = response;
|
|
12
|
+
|
|
13
|
+
if (!command) {
|
|
14
|
+
console.log(chalk.yellow('\n⚠ Could not generate a command.'));
|
|
15
|
+
console.log(chalk.gray(` ${explanation}\n`));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const boxWidth = Math.max(command.length + 4, 44);
|
|
20
|
+
const innerWidth = boxWidth - 4;
|
|
21
|
+
|
|
22
|
+
// Command box
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(chalk.cyan(` ╭─ Command ${'─'.repeat(Math.max(0, boxWidth - 13))}╮`));
|
|
25
|
+
console.log(chalk.cyan(' │ ') + chalk.bold.white(pad(command, innerWidth)) + chalk.cyan('│'));
|
|
26
|
+
console.log(chalk.cyan(` ╰${'─'.repeat(boxWidth - 2)}╯`));
|
|
27
|
+
|
|
28
|
+
// Explanation
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log(chalk.gray(' Explains ') + chalk.white(explanation));
|
|
31
|
+
|
|
32
|
+
// Dangerous warning
|
|
33
|
+
if (dangerous) {
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(chalk.red.bold(' [!] Warning: this command modifies files'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Sudo warning
|
|
39
|
+
if (command.toLowerCase().includes('sudo')) {
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(chalk.yellow.bold(' [⚡] This command uses sudo — elevated privileges required'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Alternatives
|
|
45
|
+
if (alternatives.length > 0) {
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(chalk.gray(' Alternatives:'));
|
|
48
|
+
for (const alt of alternatives) {
|
|
49
|
+
console.log(chalk.gray(' • ') + chalk.white(alt));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function renderRunning(command: string): void {
|
|
57
|
+
console.log(chalk.cyan.bold('\n ▶ Running: ') + chalk.white(command));
|
|
58
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function renderSuccess(): void {
|
|
62
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)));
|
|
63
|
+
console.log(chalk.green.bold(' ✓ Command finished successfully\n'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function renderError(error: string): void {
|
|
67
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)));
|
|
68
|
+
console.log(chalk.red.bold(' ✗ Command failed: ') + chalk.red(error) + '\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function renderCopied(): void {
|
|
72
|
+
console.log(chalk.green.bold(' 📋 Copied to clipboard!\n'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renderCancelled(): void {
|
|
76
|
+
console.log(chalk.gray(' Cancelled.\n'));
|
|
77
|
+
}
|
package/src/runner.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import execa from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { getConfig } from './config';
|
|
5
|
+
import { renderRunning, renderSuccess, renderError } from './renderer';
|
|
6
|
+
|
|
7
|
+
async function confirmDangerousCommand(command: string): Promise<boolean> {
|
|
8
|
+
console.log(chalk.red.bold('\n ⚠️ DANGER: This command may modify or delete files!'));
|
|
9
|
+
console.log(chalk.red(` Command: ${command}\n`));
|
|
10
|
+
|
|
11
|
+
const { confirm } = await inquirer.prompt([
|
|
12
|
+
{
|
|
13
|
+
type: 'confirm',
|
|
14
|
+
name: 'confirm',
|
|
15
|
+
message: chalk.red.bold('Are you absolutely sure you want to run this?'),
|
|
16
|
+
default: false,
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
return confirm;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function confirmRmRf(command: string): Promise<boolean> {
|
|
24
|
+
console.log(chalk.bgRed.white.bold('\n 🚨 CRITICAL: This command uses rm -rf!'));
|
|
25
|
+
console.log(chalk.red(` Command: ${command}\n`));
|
|
26
|
+
|
|
27
|
+
const { firstConfirm } = await inquirer.prompt([
|
|
28
|
+
{
|
|
29
|
+
type: 'confirm',
|
|
30
|
+
name: 'firstConfirm',
|
|
31
|
+
message: chalk.red.bold('This will PERMANENTLY delete files. Continue?'),
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
if (!firstConfirm) return false;
|
|
37
|
+
|
|
38
|
+
const { secondConfirm } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'input',
|
|
41
|
+
name: 'secondConfirm',
|
|
42
|
+
message: chalk.red.bold('Type "yes" to confirm deletion:'),
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
return secondConfirm.toLowerCase() === 'yes';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runCommand(
|
|
50
|
+
command: string,
|
|
51
|
+
dangerous: boolean
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const config = getConfig();
|
|
54
|
+
|
|
55
|
+
// Safety checks for dangerous commands
|
|
56
|
+
if (dangerous && config.confirmBeforeRun) {
|
|
57
|
+
// Extra check for rm -rf
|
|
58
|
+
if (command.includes('rm -rf') || command.includes('rm --recursive --force')) {
|
|
59
|
+
const confirmed = await confirmRmRf(command);
|
|
60
|
+
if (!confirmed) {
|
|
61
|
+
console.log(chalk.gray(' Aborted.\n'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
const confirmed = await confirmDangerousCommand(command);
|
|
66
|
+
if (!confirmed) {
|
|
67
|
+
console.log(chalk.gray(' Aborted.\n'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Sudo warning
|
|
74
|
+
if (command.includes('sudo')) {
|
|
75
|
+
console.log(chalk.yellow('\n ⚡ Running with elevated privileges (sudo)...'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
renderRunning(command);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const subprocess = execa(command, {
|
|
82
|
+
shell: true,
|
|
83
|
+
stdio: 'inherit',
|
|
84
|
+
cwd: process.cwd(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await subprocess;
|
|
88
|
+
renderSuccess();
|
|
89
|
+
} catch (err: unknown) {
|
|
90
|
+
const error = err as { message?: string; exitCode?: number };
|
|
91
|
+
const message = error.exitCode
|
|
92
|
+
? `Exit code ${error.exitCode}`
|
|
93
|
+
: error.message || 'Unknown error';
|
|
94
|
+
renderError(message);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
}
|
|
97
|
+
}
|