Skip to content

Commit 17f307e

Browse files
authored
Merge pull request #13 from RooVetGit/cli
Add a command-line cline
2 parents a8993bf + 1c471bd commit 17f307e

File tree

12 files changed

+1103
-0
lines changed

12 files changed

+1103
-0
lines changed

cli/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Cline CLI
2+
3+
A command-line interface for Cline, powered by Deno.
4+
5+
## Installation
6+
7+
1. Make sure you have [Deno](https://deno.land/) installed
8+
2. Install the CLI globally:
9+
```bash
10+
cd cli
11+
deno task install
12+
```
13+
14+
If you get a PATH warning during installation, add Deno's bin directory to your PATH:
15+
```bash
16+
echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
17+
source ~/.bashrc # or ~/.zshrc
18+
```
19+
20+
## Usage
21+
22+
```bash
23+
cline <task> [options]
24+
```
25+
26+
### Security Model
27+
28+
The CLI implements several security measures:
29+
30+
1. File Operations:
31+
- Read/write access limited to working directory (--allow-read=., --allow-write=.)
32+
- Prevents access to files outside the project
33+
34+
2. Command Execution:
35+
- Strict allowlist of safe commands:
36+
* npm (install, run, test, build)
37+
* git (status, add, commit, push, pull, clone, checkout, branch)
38+
* deno (run, test, fmt, lint, check, compile, bundle)
39+
* ls (-l, -a, -la, -lh)
40+
* cat, echo
41+
- Interactive prompts for non-allowlisted commands:
42+
* y - Run once
43+
* n - Cancel execution
44+
* always - Remember for session
45+
- Clear warnings and command details shown
46+
- Session-based memory for approved commands
47+
48+
3. Required Permissions:
49+
- --allow-read=. - Read files in working directory
50+
- --allow-write=. - Write files in working directory
51+
- --allow-run - Execute allowlisted commands
52+
- --allow-net - Make API calls
53+
- --allow-env - Access environment variables
54+
55+
### Options
56+
57+
- `-m, --model <model>` - LLM model to use (default: "anthropic/claude-3.5-sonnet")
58+
- `-k, --key <key>` - OpenRouter API key (required, or set OPENROUTER_API_KEY env var)
59+
- `-h, --help` - Display help for command
60+
61+
### Examples
62+
63+
Analyze code:
64+
```bash
65+
export OPENROUTER_API_KEY=sk-or-v1-...
66+
cline "Analyze this codebase"
67+
```
68+
69+
Create files:
70+
```bash
71+
cline "Create a React component"
72+
```
73+
74+
Run allowed command:
75+
```bash
76+
cline "Run npm install"
77+
```
78+
79+
Run non-allowlisted command (will prompt for decision):
80+
```bash
81+
cline "Run yarn install"
82+
# Responds with:
83+
# Warning: Command not in allowlist
84+
# Command: yarn install
85+
# Do you want to run this command? (y/n/always)
86+
```
87+
88+
## Development
89+
90+
The CLI is built with Deno. Available tasks:
91+
92+
```bash
93+
# Run in development mode
94+
deno task dev "your task here"
95+
96+
# Install globally
97+
deno task install
98+
99+
# Type check the code
100+
deno task check
101+
```
102+
103+
### Security Features
104+
105+
- File operations restricted to working directory
106+
- Command execution controlled by allowlist
107+
- Interactive prompts for unknown commands
108+
- Session-based command approval
109+
- Clear warnings and command details
110+
- Permission validation at runtime

cli/api/mod.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { ApiConfiguration, ApiHandler } from "../types.d.ts";
2+
import { OpenRouterHandler } from "./providers/openrouter.ts";
3+
4+
// Re-export the ApiHandler interface
5+
export type { ApiHandler };
6+
7+
export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
8+
const { apiKey, model } = configuration;
9+
return new OpenRouterHandler({ apiKey, model });
10+
}

cli/api/providers/openrouter.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { ApiStream, ModelInfo, Message, TextBlock } from "../../types.d.ts";
2+
3+
interface OpenRouterOptions {
4+
model: string;
5+
apiKey: string;
6+
}
7+
8+
export class OpenRouterHandler {
9+
private apiKey: string;
10+
private model: string;
11+
12+
constructor(options: OpenRouterOptions) {
13+
this.apiKey = options.apiKey;
14+
this.model = options.model;
15+
}
16+
17+
async *createMessage(systemPrompt: string, messages: Message[]): ApiStream {
18+
try {
19+
// Convert our messages to OpenRouter format
20+
const openRouterMessages = [
21+
{ role: "system", content: systemPrompt },
22+
...messages.map(msg => ({
23+
role: msg.role,
24+
content: Array.isArray(msg.content)
25+
? msg.content.map(c => c.text).join("\n")
26+
: msg.content
27+
}))
28+
];
29+
30+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
31+
method: "POST",
32+
headers: {
33+
"Authorization": `Bearer ${this.apiKey}`,
34+
"Content-Type": "application/json",
35+
"HTTP-Referer": "https://github.com/mattvr/roo-cline",
36+
"X-Title": "Cline CLI"
37+
},
38+
body: JSON.stringify({
39+
model: this.model,
40+
messages: openRouterMessages,
41+
stream: true,
42+
temperature: 0.7,
43+
max_tokens: 4096
44+
})
45+
});
46+
47+
if (!response.ok) {
48+
const errorData = await response.json().catch(() => null);
49+
throw new Error(`OpenRouter API error: ${response.statusText}${errorData ? ` - ${JSON.stringify(errorData)}` : ""}`);
50+
}
51+
52+
if (!response.body) {
53+
throw new Error("No response body received");
54+
}
55+
56+
const reader = response.body.getReader();
57+
const decoder = new TextDecoder();
58+
let buffer = "";
59+
let content = "";
60+
61+
while (true) {
62+
const { done, value } = await reader.read();
63+
if (done) break;
64+
65+
// Add new chunk to buffer and split into lines
66+
buffer += decoder.decode(value, { stream: true });
67+
const lines = buffer.split("\n");
68+
69+
// Process all complete lines
70+
buffer = lines.pop() || ""; // Keep the last incomplete line in buffer
71+
72+
for (const line of lines) {
73+
if (line.trim() === "") continue;
74+
if (line === "data: [DONE]") continue;
75+
76+
if (line.startsWith("data: ")) {
77+
try {
78+
const data = JSON.parse(line.slice(6));
79+
if (data.choices?.[0]?.delta?.content) {
80+
const text = data.choices[0].delta.content;
81+
content += text;
82+
yield { type: "text", text };
83+
}
84+
} catch (e) {
85+
// Ignore parse errors for incomplete chunks
86+
continue;
87+
}
88+
}
89+
}
90+
}
91+
92+
// Process any remaining content in buffer
93+
if (buffer.trim() && buffer.startsWith("data: ")) {
94+
try {
95+
const data = JSON.parse(buffer.slice(6));
96+
if (data.choices?.[0]?.delta?.content) {
97+
const text = data.choices[0].delta.content;
98+
content += text;
99+
yield { type: "text", text };
100+
}
101+
} catch (e) {
102+
// Ignore parse errors for final incomplete chunk
103+
}
104+
}
105+
106+
// Estimate token usage (4 chars per token is a rough estimate)
107+
const inputText = systemPrompt + messages.reduce((acc, msg) =>
108+
acc + (typeof msg.content === "string" ?
109+
msg.content :
110+
msg.content.reduce((a, b) => a + b.text, "")), "");
111+
112+
const inputTokens = Math.ceil(inputText.length / 4);
113+
const outputTokens = Math.ceil(content.length / 4);
114+
115+
yield {
116+
type: "usage",
117+
inputTokens,
118+
outputTokens,
119+
totalCost: this.calculateCost(inputTokens, outputTokens)
120+
};
121+
122+
} catch (error) {
123+
console.error("Error in OpenRouter API call:", error);
124+
throw error;
125+
}
126+
}
127+
128+
getModel(): { id: string; info: ModelInfo } {
129+
return {
130+
id: this.model,
131+
info: {
132+
contextWindow: 128000, // This varies by model
133+
supportsComputerUse: true,
134+
inputPricePerToken: 0.000002, // Approximate, varies by model
135+
outputPricePerToken: 0.000002
136+
}
137+
};
138+
}
139+
140+
private calculateCost(inputTokens: number, outputTokens: number): number {
141+
const { inputPricePerToken, outputPricePerToken } = this.getModel().info;
142+
return (
143+
(inputTokens * (inputPricePerToken || 0)) +
144+
(outputTokens * (outputPricePerToken || 0))
145+
);
146+
}
147+
}

0 commit comments

Comments
 (0)