@vdntio/clai 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/ai/index.d.ts +24 -0
- package/dist/ai/index.js +78 -0
- package/dist/ai/mock.d.ts +17 -0
- package/dist/ai/mock.js +49 -0
- package/dist/ai/parser.d.ts +14 -0
- package/dist/ai/parser.js +109 -0
- package/dist/ai/prompt.d.ts +12 -0
- package/dist/ai/prompt.js +76 -0
- package/dist/ai/providers/index.d.ts +1 -0
- package/dist/ai/providers/index.js +2 -0
- package/dist/ai/providers/openrouter.d.ts +31 -0
- package/dist/ai/providers/openrouter.js +142 -0
- package/dist/ai/types.d.ts +46 -0
- package/dist/ai/types.js +15 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +71 -0
- package/dist/config/index.d.ts +12 -0
- package/dist/config/index.js +363 -0
- package/dist/config/types.d.ts +76 -0
- package/dist/config/types.js +40 -0
- package/dist/context/directory.d.ts +18 -0
- package/dist/context/directory.js +71 -0
- package/dist/context/history.d.ts +16 -0
- package/dist/context/history.js +89 -0
- package/dist/context/index.d.ts +24 -0
- package/dist/context/index.js +61 -0
- package/dist/context/redaction.d.ts +14 -0
- package/dist/context/redaction.js +57 -0
- package/dist/context/stdin.d.ts +13 -0
- package/dist/context/stdin.js +86 -0
- package/dist/context/system.d.ts +11 -0
- package/dist/context/system.js +56 -0
- package/dist/context/types.d.ts +31 -0
- package/dist/context/types.js +10 -0
- package/dist/error/index.d.ts +30 -0
- package/dist/error/index.js +50 -0
- package/dist/logging/file-logger.d.ts +12 -0
- package/dist/logging/file-logger.js +66 -0
- package/dist/logging/index.d.ts +15 -0
- package/dist/logging/index.js +33 -0
- package/dist/logging/logger.d.ts +15 -0
- package/dist/logging/logger.js +60 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +192 -0
- package/dist/output/execute.d.ts +30 -0
- package/dist/output/execute.js +144 -0
- package/dist/output/index.d.ts +4 -0
- package/dist/output/index.js +7 -0
- package/dist/output/types.d.ts +48 -0
- package/dist/output/types.js +34 -0
- package/dist/output/validate.d.ts +23 -0
- package/dist/output/validate.js +42 -0
- package/dist/safety/index.d.ts +34 -0
- package/dist/safety/index.js +59 -0
- package/dist/safety/patterns.d.ts +23 -0
- package/dist/safety/patterns.js +96 -0
- package/dist/safety/types.d.ts +20 -0
- package/dist/safety/types.js +18 -0
- package/dist/signals/index.d.ts +4 -0
- package/dist/signals/index.js +35 -0
- package/dist/ui/App.d.ts +4 -0
- package/dist/ui/App.js +57 -0
- package/dist/ui/components/ActionPrompt.d.ts +8 -0
- package/dist/ui/components/ActionPrompt.js +9 -0
- package/dist/ui/components/CommandDisplay.d.ts +9 -0
- package/dist/ui/components/CommandDisplay.js +13 -0
- package/dist/ui/components/DangerousWarning.d.ts +6 -0
- package/dist/ui/components/DangerousWarning.js +6 -0
- package/dist/ui/components/Spinner.d.ts +15 -0
- package/dist/ui/components/Spinner.js +17 -0
- package/dist/ui/hooks/useAnimation.d.ts +27 -0
- package/dist/ui/hooks/useAnimation.js +85 -0
- package/dist/ui/hooks/useTerminalSize.d.ts +12 -0
- package/dist/ui/hooks/useTerminalSize.js +56 -0
- package/dist/ui/hooks/useTimeout.d.ts +9 -0
- package/dist/ui/hooks/useTimeout.js +31 -0
- package/dist/ui/index.d.ts +25 -0
- package/dist/ui/index.js +80 -0
- package/dist/ui/output.d.ts +20 -0
- package/dist/ui/output.js +56 -0
- package/dist/ui/spinner.d.ts +13 -0
- package/dist/ui/spinner.js +58 -0
- package/dist/ui/types.d.ts +105 -0
- package/dist/ui/types.js +60 -0
- package/dist/ui/utils/formatCommand.d.ts +50 -0
- package/dist/ui/utils/formatCommand.js +113 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vdntio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# clAI
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Alpha Release**: This is an early alpha version. Expect bugs and breaking changes.
|
|
4
|
+
|
|
5
|
+
AI-powered CLI that converts natural language instructions into executable shell commands using OpenRouter.
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
clai "find all TypeScript files modified in the last 7 days"
|
|
11
|
+
# Outputs: find . -name "*.ts" -mtime -7
|
|
12
|
+
|
|
13
|
+
clai "list top 10 largest files in current directory"
|
|
14
|
+
# Outputs: du -h * | sort -rh | head -10
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- 🤖 Natural language to shell commands using AI
|
|
20
|
+
- 🎨 Beautiful terminal UI with Ink
|
|
21
|
+
- 🔒 Safety checks for dangerous commands (rm -rf, etc.)
|
|
22
|
+
- 📝 Context-aware (current directory, shell type, git status)
|
|
23
|
+
- ⚡ Fast startup with Bun runtime
|
|
24
|
+
- 🌍 Cross-platform (Linux, macOS, Windows)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
### npm (Recommended)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g clai@alpha
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Standalone Binary
|
|
35
|
+
|
|
36
|
+
Download from [Releases](https://github.com/vdntio/clAI/releases)
|
|
37
|
+
|
|
38
|
+
See [Installation Guide](docs/installation.md) for detailed instructions.
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
1. Get an OpenRouter API key from [openrouter.ai](https://openrouter.ai)
|
|
43
|
+
|
|
44
|
+
2. Set your API key:
|
|
45
|
+
```bash
|
|
46
|
+
export OPENROUTER_API_KEY="your-key-here"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. Run a command:
|
|
50
|
+
```bash
|
|
51
|
+
clai "your natural language instruction"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
clAI uses TOML config files. Priority order:
|
|
57
|
+
1. `./.clai.toml` (project-level)
|
|
58
|
+
2. `~/.config/clai/config.toml` (user-level)
|
|
59
|
+
3. `/etc/clai/config.toml` (system-level)
|
|
60
|
+
|
|
61
|
+
Example config:
|
|
62
|
+
```toml
|
|
63
|
+
openrouter_api_key = "sk-..."
|
|
64
|
+
|
|
65
|
+
[providers.openrouter]
|
|
66
|
+
model = "qwen/qwen3-coder" # Default model (can override with anthropic/claude-3.5-sonnet, etc.)
|
|
67
|
+
|
|
68
|
+
[safety]
|
|
69
|
+
confirm_dangerous = true
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Install dependencies
|
|
76
|
+
bun install
|
|
77
|
+
|
|
78
|
+
# Run in development mode
|
|
79
|
+
bun run dev
|
|
80
|
+
|
|
81
|
+
# Run tests
|
|
82
|
+
bun test
|
|
83
|
+
|
|
84
|
+
# Build
|
|
85
|
+
bun run build
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## OpenRouter Costs
|
|
89
|
+
|
|
90
|
+
clAI uses OpenRouter's API which charges per token. Default model is `qwen/qwen3-coder` (free tier available). You can override to use other models like `anthropic/claude-3.5-sonnet` (~$3 per million input tokens). Get credits at [openrouter.ai](https://openrouter.ai).
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT - See [LICENSE](LICENSE) for details.
|
|
95
|
+
|
|
96
|
+
## Issues & Feedback
|
|
97
|
+
|
|
98
|
+
Report bugs or request features at [GitHub Issues](https://github.com/vdntio/clAI/issues).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ContextData } from '../context/types.js';
|
|
2
|
+
import type { Config } from '../config/types.js';
|
|
3
|
+
import { ChatMessage } from './types.js';
|
|
4
|
+
export { AIError, } from './types.js';
|
|
5
|
+
export type { ChatMessage, ChatRequest, ChatResponse, AIProvider, } from './types.js';
|
|
6
|
+
export { buildPrompt } from './prompt.js';
|
|
7
|
+
export { parseResponse } from './parser.js';
|
|
8
|
+
export { OpenRouterProvider } from './providers/index.js';
|
|
9
|
+
export { MockProvider } from './mock.js';
|
|
10
|
+
/**
|
|
11
|
+
* Generate shell commands from natural language instruction
|
|
12
|
+
*
|
|
13
|
+
* @param context - Gathered context (system, directory, history, stdin)
|
|
14
|
+
* @param instruction - User's natural language instruction
|
|
15
|
+
* @param config - Runtime configuration
|
|
16
|
+
* @returns Array of command strings (1 for single mode, N for multi mode)
|
|
17
|
+
* @throws AIError on API failure or parse error (exit code 4)
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateCommands(context: ContextData, instruction: string, config: Config): Promise<string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Format messages for debug output
|
|
22
|
+
* Returns formatted string showing all messages
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatPromptForDebug(messages: ChatMessage[]): string;
|
package/dist/ai/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// AI module main entry point
|
|
2
|
+
// Provides generateCommands() for converting natural language to shell commands
|
|
3
|
+
import { getProviderApiKey, getProviderModel } from '../config/index.js';
|
|
4
|
+
import { AIError, } from './types.js';
|
|
5
|
+
import { buildPrompt } from './prompt.js';
|
|
6
|
+
import { parseResponse } from './parser.js';
|
|
7
|
+
import { OpenRouterProvider } from './providers/index.js';
|
|
8
|
+
import { MockProvider } from './mock.js';
|
|
9
|
+
// Re-export types for consumers
|
|
10
|
+
export { AIError, } from './types.js';
|
|
11
|
+
export { buildPrompt } from './prompt.js';
|
|
12
|
+
export { parseResponse } from './parser.js';
|
|
13
|
+
export { OpenRouterProvider } from './providers/index.js';
|
|
14
|
+
export { MockProvider } from './mock.js';
|
|
15
|
+
/**
|
|
16
|
+
* Generate shell commands from natural language instruction
|
|
17
|
+
*
|
|
18
|
+
* @param context - Gathered context (system, directory, history, stdin)
|
|
19
|
+
* @param instruction - User's natural language instruction
|
|
20
|
+
* @param config - Runtime configuration
|
|
21
|
+
* @returns Array of command strings (1 for single mode, N for multi mode)
|
|
22
|
+
* @throws AIError on API failure or parse error (exit code 4)
|
|
23
|
+
*/
|
|
24
|
+
export async function generateCommands(context, instruction, config) {
|
|
25
|
+
const providerName = config.providerName || config.provider.default;
|
|
26
|
+
const numOptions = config.ui.numOptions;
|
|
27
|
+
// Get appropriate provider
|
|
28
|
+
const provider = getProvider(providerName, config);
|
|
29
|
+
// Build prompt messages
|
|
30
|
+
const messages = buildPrompt(context, instruction, numOptions);
|
|
31
|
+
// Get model (from CLI, config, or default)
|
|
32
|
+
const model = getProviderModel(providerName, config);
|
|
33
|
+
// Make API request
|
|
34
|
+
const request = {
|
|
35
|
+
model,
|
|
36
|
+
messages,
|
|
37
|
+
temperature: 0.1, // Low temperature for more deterministic commands
|
|
38
|
+
};
|
|
39
|
+
const response = await provider.complete(request);
|
|
40
|
+
// Parse response into command(s)
|
|
41
|
+
return parseResponse(response.content, numOptions > 1);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get provider instance based on configuration
|
|
45
|
+
* Returns mock provider if MOCK_AI=1 is set
|
|
46
|
+
*/
|
|
47
|
+
function getProvider(name, config) {
|
|
48
|
+
// Check for mock mode
|
|
49
|
+
if (process.env.MOCK_AI === '1') {
|
|
50
|
+
return new MockProvider();
|
|
51
|
+
}
|
|
52
|
+
// Currently only OpenRouter is supported
|
|
53
|
+
// Future: add more providers here (Anthropic, Ollama, etc.)
|
|
54
|
+
if (name === 'openrouter') {
|
|
55
|
+
const apiKey = getProviderApiKey('openrouter', config);
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
throw new AIError('OpenRouter API key not configured. ' +
|
|
58
|
+
'Set OPENROUTER_API_KEY environment variable or configure api_key in .clai.toml');
|
|
59
|
+
}
|
|
60
|
+
return new OpenRouterProvider(apiKey);
|
|
61
|
+
}
|
|
62
|
+
throw new AIError(`Unknown provider: ${name}. ` + 'Currently only "openrouter" is supported.');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Format messages for debug output
|
|
66
|
+
* Returns formatted string showing all messages
|
|
67
|
+
*/
|
|
68
|
+
export function formatPromptForDebug(messages) {
|
|
69
|
+
return messages
|
|
70
|
+
.map((msg) => {
|
|
71
|
+
const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
|
|
72
|
+
const content = msg.content.length > 500
|
|
73
|
+
? msg.content.substring(0, 500) + '...'
|
|
74
|
+
: msg.content;
|
|
75
|
+
return `[${role}]\n${content}`;
|
|
76
|
+
})
|
|
77
|
+
.join('\n\n');
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AIProvider, ChatRequest, ChatResponse } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mock provider for testing
|
|
4
|
+
* Returns predictable responses without API calls
|
|
5
|
+
*/
|
|
6
|
+
export declare class MockProvider implements AIProvider {
|
|
7
|
+
name: string;
|
|
8
|
+
/**
|
|
9
|
+
* Always available (no API key needed)
|
|
10
|
+
*/
|
|
11
|
+
isAvailable(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Return mock response based on request
|
|
14
|
+
* Detects multi-command mode from system message
|
|
15
|
+
*/
|
|
16
|
+
complete(request: ChatRequest): Promise<ChatResponse>;
|
|
17
|
+
}
|
package/dist/ai/mock.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Mock AI Provider for testing
|
|
2
|
+
// Returns simple echo commands without making real API calls
|
|
3
|
+
// Activated by setting MOCK_AI=1 environment variable
|
|
4
|
+
/**
|
|
5
|
+
* Mock provider for testing
|
|
6
|
+
* Returns predictable responses without API calls
|
|
7
|
+
*/
|
|
8
|
+
export class MockProvider {
|
|
9
|
+
name = 'mock';
|
|
10
|
+
/**
|
|
11
|
+
* Always available (no API key needed)
|
|
12
|
+
*/
|
|
13
|
+
isAvailable() {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Return mock response based on request
|
|
18
|
+
* Detects multi-command mode from system message
|
|
19
|
+
*/
|
|
20
|
+
async complete(request) {
|
|
21
|
+
const systemMsg = request.messages[0]?.content || '';
|
|
22
|
+
const isMultiCommand = systemMsg.includes('JSON');
|
|
23
|
+
if (isMultiCommand) {
|
|
24
|
+
// Extract number of commands from system message
|
|
25
|
+
const match = systemMsg.match(/exactly (\d+) different/);
|
|
26
|
+
const numCommands = match?.[1] ? parseInt(match[1], 10) : 3;
|
|
27
|
+
const commands = Array.from({ length: numCommands }, (_, i) => `echo "mock command ${i + 1}"`);
|
|
28
|
+
return {
|
|
29
|
+
content: JSON.stringify({ commands }),
|
|
30
|
+
model: 'mock',
|
|
31
|
+
usage: {
|
|
32
|
+
promptTokens: 100,
|
|
33
|
+
completionTokens: 50,
|
|
34
|
+
totalTokens: 150,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Single command
|
|
39
|
+
return {
|
|
40
|
+
content: 'echo "mock command"',
|
|
41
|
+
model: 'mock',
|
|
42
|
+
usage: {
|
|
43
|
+
promptTokens: 50,
|
|
44
|
+
completionTokens: 10,
|
|
45
|
+
totalTokens: 60,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse AI response content into command(s)
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Markdown code fence stripping (```bash, ```sh, ```shell, ```json, ```)
|
|
6
|
+
* - JSON parsing for multi-command mode: {"commands": [...]} or [...]
|
|
7
|
+
* - Fallback to single trimmed command
|
|
8
|
+
*
|
|
9
|
+
* @param content - Raw response content from AI
|
|
10
|
+
* @param expectMultiple - Whether to expect multiple commands (numOptions > 1)
|
|
11
|
+
* @returns Array of command strings
|
|
12
|
+
* @throws AIError if response is empty or cannot be parsed
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseResponse(content: string, expectMultiple: boolean): string[];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Response parser for AI output
|
|
2
|
+
import { AIError } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse AI response content into command(s)
|
|
5
|
+
*
|
|
6
|
+
* Handles:
|
|
7
|
+
* - Markdown code fence stripping (```bash, ```sh, ```shell, ```json, ```)
|
|
8
|
+
* - JSON parsing for multi-command mode: {"commands": [...]} or [...]
|
|
9
|
+
* - Fallback to single trimmed command
|
|
10
|
+
*
|
|
11
|
+
* @param content - Raw response content from AI
|
|
12
|
+
* @param expectMultiple - Whether to expect multiple commands (numOptions > 1)
|
|
13
|
+
* @returns Array of command strings
|
|
14
|
+
* @throws AIError if response is empty or cannot be parsed
|
|
15
|
+
*/
|
|
16
|
+
export function parseResponse(content, expectMultiple) {
|
|
17
|
+
// 1. Trim whitespace
|
|
18
|
+
let cleaned = content.trim();
|
|
19
|
+
if (!cleaned) {
|
|
20
|
+
throw new AIError('AI returned empty response');
|
|
21
|
+
}
|
|
22
|
+
// 2. Strip markdown code fences
|
|
23
|
+
cleaned = stripCodeFences(cleaned);
|
|
24
|
+
// Check if empty after stripping fences
|
|
25
|
+
if (!cleaned) {
|
|
26
|
+
throw new AIError('AI returned empty response after parsing');
|
|
27
|
+
}
|
|
28
|
+
// 3. Try JSON parse for multi-command mode
|
|
29
|
+
if (expectMultiple) {
|
|
30
|
+
const commands = tryParseMultipleCommands(cleaned);
|
|
31
|
+
if (commands.length > 0) {
|
|
32
|
+
return commands;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 4. Fallback: treat as single command
|
|
36
|
+
cleaned = cleaned.trim();
|
|
37
|
+
if (!cleaned) {
|
|
38
|
+
throw new AIError('AI returned empty response after parsing');
|
|
39
|
+
}
|
|
40
|
+
return [cleaned];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Strip markdown code fences from content
|
|
44
|
+
* Handles multiple fence formats
|
|
45
|
+
*/
|
|
46
|
+
function stripCodeFences(content) {
|
|
47
|
+
// Pattern 1: ```language\ncontent\n```
|
|
48
|
+
// Pattern 2: ```\ncontent\n```
|
|
49
|
+
// Must match the entire string from start to end
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
// Check if first line is an opening fence
|
|
52
|
+
if (lines[0]?.startsWith('```')) {
|
|
53
|
+
// Check if last line is a closing fence
|
|
54
|
+
if (lines[lines.length - 1] === '```') {
|
|
55
|
+
// Remove first and last lines
|
|
56
|
+
return lines.slice(1, -1).join('\n').trim();
|
|
57
|
+
}
|
|
58
|
+
// Single line case: ```content```
|
|
59
|
+
const singleLineMatch = content.match(/^```[\w]*\s*(.+?)\s*```$/);
|
|
60
|
+
if (singleLineMatch?.[1]) {
|
|
61
|
+
return singleLineMatch[1].trim();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return content;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Try to parse multiple commands from JSON
|
|
68
|
+
* Returns empty array if parsing fails
|
|
69
|
+
*/
|
|
70
|
+
function tryParseMultipleCommands(content) {
|
|
71
|
+
// Try to extract JSON object if content has extra text
|
|
72
|
+
let jsonContent = content;
|
|
73
|
+
// Look for JSON object pattern if content has extra text
|
|
74
|
+
const jsonMatch = content.match(/\{[\s\S]*"commands"[\s\S]*\}/);
|
|
75
|
+
if (jsonMatch) {
|
|
76
|
+
jsonContent = jsonMatch[0];
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(jsonContent);
|
|
80
|
+
// Handle {"commands": [...]}
|
|
81
|
+
if (parsed.commands && Array.isArray(parsed.commands)) {
|
|
82
|
+
const cmds = parsed.commands.filter((c) => typeof c === 'string' && c.trim());
|
|
83
|
+
return cmds.map((c) => c.trim());
|
|
84
|
+
}
|
|
85
|
+
// Handle raw array [...]
|
|
86
|
+
if (Array.isArray(parsed)) {
|
|
87
|
+
const cmds = parsed.filter((c) => typeof c === 'string' && c.trim());
|
|
88
|
+
return cmds.map((c) => c.trim());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// JSON parse failed, try to extract from text
|
|
93
|
+
// Look for array pattern: ["cmd1", "cmd2"]
|
|
94
|
+
const arrayMatch = content.match(/\[[\s\S]*\]/);
|
|
95
|
+
if (arrayMatch) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(arrayMatch[0]);
|
|
98
|
+
if (Array.isArray(parsed)) {
|
|
99
|
+
const cmds = parsed.filter((c) => typeof c === 'string' && c.trim());
|
|
100
|
+
return cmds.map((c) => c.trim());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// JSON parse failed, continue to other strategies
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ContextData } from '../context/types.js';
|
|
2
|
+
import { ChatMessage } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build chat messages for AI request
|
|
5
|
+
* Constructs system and user messages based on context and instruction
|
|
6
|
+
*
|
|
7
|
+
* @param context - Gathered system/directory/history/stdin context
|
|
8
|
+
* @param instruction - User's natural language instruction
|
|
9
|
+
* @param numOptions - Number of command options to generate (1 for single, >1 for multi)
|
|
10
|
+
* @returns Array of chat messages for the AI
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildPrompt(context: ContextData, instruction: string, numOptions: number): ChatMessage[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Prompt builder for AI requests
|
|
2
|
+
/**
|
|
3
|
+
* Build chat messages for AI request
|
|
4
|
+
* Constructs system and user messages based on context and instruction
|
|
5
|
+
*
|
|
6
|
+
* @param context - Gathered system/directory/history/stdin context
|
|
7
|
+
* @param instruction - User's natural language instruction
|
|
8
|
+
* @param numOptions - Number of command options to generate (1 for single, >1 for multi)
|
|
9
|
+
* @returns Array of chat messages for the AI
|
|
10
|
+
*/
|
|
11
|
+
export function buildPrompt(context, instruction, numOptions) {
|
|
12
|
+
const isMultiCommand = numOptions > 1;
|
|
13
|
+
// System message differs for single vs multi-command
|
|
14
|
+
const systemMessage = isMultiCommand
|
|
15
|
+
? buildMultiCommandSystemPrompt(numOptions)
|
|
16
|
+
: buildSingleCommandSystemPrompt();
|
|
17
|
+
// User message includes context and instruction
|
|
18
|
+
const userMessage = buildUserPrompt(context, instruction, numOptions);
|
|
19
|
+
return [
|
|
20
|
+
{ role: 'system', content: systemMessage },
|
|
21
|
+
{ role: 'user', content: userMessage },
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build system prompt for single command generation
|
|
26
|
+
*/
|
|
27
|
+
function buildSingleCommandSystemPrompt() {
|
|
28
|
+
return `You are a helpful assistant that converts natural language instructions into executable shell commands. Respond with ONLY the command, no explanations or markdown.`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build system prompt for multi-command generation
|
|
32
|
+
*/
|
|
33
|
+
function buildMultiCommandSystemPrompt(numOptions) {
|
|
34
|
+
return `You are a helpful assistant that converts natural language instructions into executable shell commands. Generate exactly ${numOptions} different command options. Respond ONLY with a JSON object in this format: {"commands": ["cmd1", "cmd2", ...]}. No markdown, no explanations.`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build user prompt with context data
|
|
38
|
+
*/
|
|
39
|
+
function buildUserPrompt(context, instruction, numOptions) {
|
|
40
|
+
const parts = [];
|
|
41
|
+
// System context
|
|
42
|
+
parts.push(`System Context:
|
|
43
|
+
OS: ${context.system.osName} ${context.system.osVersion}
|
|
44
|
+
Architecture: ${context.system.architecture}
|
|
45
|
+
Shell: ${context.system.shell}
|
|
46
|
+
User: ${context.system.user}
|
|
47
|
+
Memory: ${context.system.totalMemoryMb} MB`);
|
|
48
|
+
// Directory context
|
|
49
|
+
const filesList = context.files.length > 0
|
|
50
|
+
? context.files.slice(0, 20).join(', ')
|
|
51
|
+
: '(empty directory)';
|
|
52
|
+
parts.push(`\nDirectory Context:
|
|
53
|
+
Current directory: ${context.cwd}
|
|
54
|
+
Files: ${filesList}`);
|
|
55
|
+
// History context (if available)
|
|
56
|
+
if (context.history.length > 0) {
|
|
57
|
+
const historyList = context.history
|
|
58
|
+
.map((h, i) => `${i + 1}. ${h}`)
|
|
59
|
+
.join('\n');
|
|
60
|
+
parts.push(`\nRecent Shell History:\n${historyList}`);
|
|
61
|
+
}
|
|
62
|
+
// Stdin context (if available)
|
|
63
|
+
if (context.stdin) {
|
|
64
|
+
parts.push(`\nStdin input:\n${context.stdin}`);
|
|
65
|
+
}
|
|
66
|
+
// User instruction
|
|
67
|
+
parts.push(`\nUser Instruction: ${instruction}`);
|
|
68
|
+
// Response instruction differs for single vs multi
|
|
69
|
+
if (numOptions > 1) {
|
|
70
|
+
parts.push(`\nRespond with exactly ${numOptions} different command options as JSON: {"commands": ["cmd1", "cmd2", ...]}. Order from simplest to most advanced. No markdown or explanations.`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
parts.push(`\nRespond ONLY with the executable command. Do not include markdown code fences, explanations, or any other text. Just the command itself.`);
|
|
74
|
+
}
|
|
75
|
+
return parts.join('');
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OpenRouterProvider } from './openrouter.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AIProvider, ChatRequest, ChatResponse } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* OpenRouter provider implementation
|
|
4
|
+
* Handles API calls with authentication, timeout, and retry logic
|
|
5
|
+
*/
|
|
6
|
+
export declare class OpenRouterProvider implements AIProvider {
|
|
7
|
+
name: string;
|
|
8
|
+
private apiKey;
|
|
9
|
+
constructor(apiKey: string);
|
|
10
|
+
/**
|
|
11
|
+
* Check if provider is available (has API key)
|
|
12
|
+
*/
|
|
13
|
+
isAvailable(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Send completion request to OpenRouter
|
|
16
|
+
* Retries on 429 rate limit with exponential backoff
|
|
17
|
+
*/
|
|
18
|
+
complete(request: ChatRequest): Promise<ChatResponse>;
|
|
19
|
+
/**
|
|
20
|
+
* Make the actual HTTP request
|
|
21
|
+
*/
|
|
22
|
+
private makeRequest;
|
|
23
|
+
/**
|
|
24
|
+
* Map HTTP status to appropriate AIError
|
|
25
|
+
*/
|
|
26
|
+
private mapError;
|
|
27
|
+
/**
|
|
28
|
+
* Parse successful response JSON
|
|
29
|
+
*/
|
|
30
|
+
private parseResponse;
|
|
31
|
+
}
|