Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Cline CLI

A command-line interface for Cline, powered by Deno.

## Installation

1. Make sure you have [Deno](https://deno.land/) installed
2. Install the CLI globally:
```bash
cd cli
deno task install
```

If you get a PATH warning during installation, add Deno's bin directory to your PATH:
```bash
echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc
source ~/.bashrc # or ~/.zshrc
```

## Usage

```bash
cline <task> [options]
```

### Security Model

The CLI implements several security measures:

1. File Operations:
- Read/write access limited to working directory (--allow-read=., --allow-write=.)
- Prevents access to files outside the project

2. Command Execution:
- Strict allowlist of safe commands:
* npm (install, run, test, build)
* git (status, add, commit, push, pull, clone, checkout, branch)
* deno (run, test, fmt, lint, check, compile, bundle)
* ls (-l, -a, -la, -lh)
* cat, echo
- Interactive prompts for non-allowlisted commands:
* y - Run once
* n - Cancel execution
* always - Remember for session
- Clear warnings and command details shown
- Session-based memory for approved commands

3. Required Permissions:
- --allow-read=. - Read files in working directory
- --allow-write=. - Write files in working directory
- --allow-run - Execute allowlisted commands
- --allow-net - Make API calls
- --allow-env - Access environment variables

### Options

- `-m, --model <model>` - LLM model to use (default: "anthropic/claude-3.5-sonnet")
- `-k, --key <key>` - OpenRouter API key (required, or set OPENROUTER_API_KEY env var)
- `-h, --help` - Display help for command

### Examples

Analyze code:
```bash
export OPENROUTER_API_KEY=sk-or-v1-...
cline "Analyze this codebase"
```

Create files:
```bash
cline "Create a React component"
```

Run allowed command:
```bash
cline "Run npm install"
```

Run non-allowlisted command (will prompt for decision):
```bash
cline "Run yarn install"
# Responds with:
# Warning: Command not in allowlist
# Command: yarn install
# Do you want to run this command? (y/n/always)
```

## Development

The CLI is built with Deno. Available tasks:

```bash
# Run in development mode
deno task dev "your task here"

# Install globally
deno task install

# Type check the code
deno task check
```

### Security Features

- File operations restricted to working directory
- Command execution controlled by allowlist
- Interactive prompts for unknown commands
- Session-based command approval
- Clear warnings and command details
- Permission validation at runtime
10 changes: 10 additions & 0 deletions cli/api/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ApiConfiguration, ApiHandler } from "../types.d.ts";
import { OpenRouterHandler } from "./providers/openrouter.ts";

// Re-export the ApiHandler interface
export type { ApiHandler };

export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
const { apiKey, model } = configuration;
return new OpenRouterHandler({ apiKey, model });
}
147 changes: 147 additions & 0 deletions cli/api/providers/openrouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { ApiStream, ModelInfo, Message, TextBlock } from "../../types.d.ts";

interface OpenRouterOptions {
model: string;
apiKey: string;
}

export class OpenRouterHandler {
private apiKey: string;
private model: string;

constructor(options: OpenRouterOptions) {
this.apiKey = options.apiKey;
this.model = options.model;
}

async *createMessage(systemPrompt: string, messages: Message[]): ApiStream {
try {
// Convert our messages to OpenRouter format
const openRouterMessages = [
{ role: "system", content: systemPrompt },
...messages.map(msg => ({
role: msg.role,
content: Array.isArray(msg.content)
? msg.content.map(c => c.text).join("\n")
: msg.content
}))
];

const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/mattvr/roo-cline",
"X-Title": "Cline CLI"
},
body: JSON.stringify({
model: this.model,
messages: openRouterMessages,
stream: true,
temperature: 0.7,
max_tokens: 4096
})
});

if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(`OpenRouter API error: ${response.statusText}${errorData ? ` - ${JSON.stringify(errorData)}` : ""}`);
}

if (!response.body) {
throw new Error("No response body received");
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let content = "";

while (true) {
const { done, value } = await reader.read();
if (done) break;

// Add new chunk to buffer and split into lines
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");

// Process all complete lines
buffer = lines.pop() || ""; // Keep the last incomplete line in buffer

for (const line of lines) {
if (line.trim() === "") continue;
if (line === "data: [DONE]") continue;

if (line.startsWith("data: ")) {
try {
const data = JSON.parse(line.slice(6));
if (data.choices?.[0]?.delta?.content) {
const text = data.choices[0].delta.content;
content += text;
yield { type: "text", text };
}
} catch (e) {
// Ignore parse errors for incomplete chunks
continue;
}
}
}
}

// Process any remaining content in buffer
if (buffer.trim() && buffer.startsWith("data: ")) {
try {
const data = JSON.parse(buffer.slice(6));
if (data.choices?.[0]?.delta?.content) {
const text = data.choices[0].delta.content;
content += text;
yield { type: "text", text };
}
} catch (e) {
// Ignore parse errors for final incomplete chunk
}
}

// Estimate token usage (4 chars per token is a rough estimate)
const inputText = systemPrompt + messages.reduce((acc, msg) =>
acc + (typeof msg.content === "string" ?
msg.content :
msg.content.reduce((a, b) => a + b.text, "")), "");

const inputTokens = Math.ceil(inputText.length / 4);
const outputTokens = Math.ceil(content.length / 4);

yield {
type: "usage",
inputTokens,
outputTokens,
totalCost: this.calculateCost(inputTokens, outputTokens)
};

} catch (error) {
console.error("Error in OpenRouter API call:", error);
throw error;
}
}

getModel(): { id: string; info: ModelInfo } {
return {
id: this.model,
info: {
contextWindow: 128000, // This varies by model
supportsComputerUse: true,
inputPricePerToken: 0.000002, // Approximate, varies by model
outputPricePerToken: 0.000002
}
};
}

private calculateCost(inputTokens: number, outputTokens: number): number {
const { inputPricePerToken, outputPricePerToken } = this.getModel().info;
return (
(inputTokens * (inputPricePerToken || 0)) +
(outputTokens * (outputPricePerToken || 0))
);
}
}
Loading