Add a command-line cline powered by deno

This commit is contained in:
Matt Rubens
2024-11-20 23:21:38 -05:00
parent e55696e247
commit 1c471bd3cb
12 changed files with 1103 additions and 0 deletions

110
cli/README.md Normal file
View File

@@ -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
cli/api/mod.ts Normal file
View File

@@ -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 });
}

View File

@@ -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))
);
}
}

164
cli/core/StandaloneAgent.ts Normal file
View File

@@ -0,0 +1,164 @@
import { blue, red, yellow } from "../deps.ts";
import { ApiHandler } from "../api/mod.ts";
import { executeCommand, readFile, writeFile, searchFiles, listFiles, listCodeDefinitions } from "../tools/mod.ts";
import type { Message, TextBlock, ToolResult } from "../types.d.ts";
interface AgentConfig {
api: ApiHandler;
systemPrompt: string;
workingDir: string;
}
export class StandaloneAgent {
private api: ApiHandler;
private systemPrompt: string;
private workingDir: string;
private conversationHistory: Message[] = [];
constructor(config: AgentConfig) {
this.api = config.api;
this.systemPrompt = config.systemPrompt;
this.workingDir = config.workingDir;
}
async runTask(task: string): Promise<void> {
this.conversationHistory.push({
role: "user",
content: [{ type: "text", text: `<task>\n${task}\n</task>` }]
});
let isTaskComplete = false;
const encoder = new TextEncoder();
while (!isTaskComplete) {
const stream = this.api.createMessage(this.systemPrompt, this.conversationHistory);
let assistantMessage = "";
console.log(blue("Thinking..."));
for await (const chunk of stream) {
if (chunk.type === "text") {
assistantMessage += chunk.text;
await Deno.stdout.write(encoder.encode(chunk.text));
}
}
this.conversationHistory.push({
role: "assistant",
content: [{ type: "text", text: assistantMessage }]
});
const toolResults = await this.executeTools(assistantMessage);
if (toolResults.length > 0) {
this.conversationHistory.push({
role: "user",
content: toolResults.map(result => ({
type: "text",
text: `[${result.tool}] Result:${result.output}`
})) as TextBlock[]
});
} else {
if (assistantMessage.includes("<attempt_completion>")) {
isTaskComplete = true;
} else {
this.conversationHistory.push({
role: "user",
content: [{
type: "text",
text: "You must either use available tools to accomplish the task or call attempt_completion when the task is complete."
}]
});
}
}
}
}
private async executeTools(message: string): Promise<ToolResult[]> {
const results: ToolResult[] = [];
const toolRegex = /<(\w+)>\s*([\s\S]*?)\s*<\/\1>/g;
let match;
while ((match = toolRegex.exec(message)) !== null) {
const [_, toolName, paramsXml] = match;
const params: Record<string, string> = {};
const paramRegex = /<(\w+)>\s*([\s\S]*?)\s*<\/\1>/g;
let paramMatch;
while ((paramMatch = paramRegex.exec(paramsXml)) !== null) {
const [__, paramName, paramValue] = paramMatch;
params[paramName] = paramValue.trim();
}
let output: string;
try {
console.log(yellow(`\nExecuting: ${this.getToolDescription(toolName, params)}`));
switch (toolName) {
case "execute_command":
output = await executeCommand(params.command);
break;
case "read_file":
output = await readFile(this.workingDir, params.path);
break;
case "write_to_file":
output = await writeFile(this.workingDir, params.path, params.content);
break;
case "search_files":
output = await searchFiles(this.workingDir, params.path, params.regex, params.file_pattern);
break;
case "list_files":
output = await listFiles(this.workingDir, params.path, params.recursive === "true");
break;
case "list_code_definition_names":
output = await listCodeDefinitions(this.workingDir, params.path);
break;
case "attempt_completion":
return results;
default:
console.warn(red(`Unknown tool: ${toolName}`));
continue;
}
results.push({
tool: toolName,
params,
output: output || "(No output)"
});
break;
} catch (error) {
const errorMessage = `Error executing ${toolName}: ${error instanceof Error ? error.message : String(error)}`;
console.error(red(errorMessage));
results.push({
tool: toolName,
params,
output: errorMessage
});
break;
}
}
return results;
}
private getToolDescription(toolName: string, params: Record<string, string>): string {
switch (toolName) {
case "execute_command":
return `Running command: ${params.command}`;
case "read_file":
return `Reading file: ${params.path}`;
case "write_to_file":
return `Writing to file: ${params.path}`;
case "search_files":
return `Searching for "${params.regex}" in ${params.path}`;
case "list_files":
return `Listing files in ${params.path}`;
case "list_code_definition_names":
return `Analyzing code in ${params.path}`;
case "attempt_completion":
return "Completing task";
default:
return toolName;
}
}
}

120
cli/core/prompts.ts Normal file
View File

@@ -0,0 +1,120 @@
import { join } from "https://deno.land/std@0.220.1/path/mod.ts";
export const SYSTEM_PROMPT = async (cwd: string): Promise<string> => {
let rulesContent = "";
// Load and combine rules from configuration files
const ruleFiles = ['.clinerules', '.cursorrules'];
for (const file of ruleFiles) {
const rulePath = join(cwd, file);
try {
const stat = await Deno.stat(rulePath);
if (stat.isFile) {
const content = await Deno.readTextFile(rulePath);
if (content.trim()) {
rulesContent += `\n# Rules from ${file}:\n${content.trim()}\n\n`;
}
}
} catch (err) {
// Only ignore ENOENT (file not found) errors
if (!(err instanceof Deno.errors.NotFound)) {
throw err;
}
}
}
return `You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
====
TOOL USE
You have access to tools that are executed upon approval. Use one tool per message and wait for the result before proceeding. Each tool must be used with proper XML-style formatting:
<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
</tool_name>
# Available Tools
## execute_command
Description: Execute a CLI command on the system. Commands run in the current working directory: ${cwd}
Parameters:
- command: (required) The command to execute. Must be valid for the current OS.
Usage:
<execute_command>
<command>command to run</command>
</execute_command>
## read_file
Description: Read contents of a file. Supports text files and automatically extracts content from PDFs/DOCXs.
Parameters:
- path: (required) Path to file (relative to ${cwd})
Usage:
<read_file>
<path>path to file</path>
</read_file>
## write_to_file
Description: Write content to a file. Creates directories as needed. Will overwrite existing files.
Parameters:
- path: (required) Path to write to (relative to ${cwd})
- content: (required) Complete file content. Must include ALL parts, even unchanged sections.
Usage:
<write_to_file>
<path>path to file</path>
<content>complete file content</content>
</write_to_file>
## search_files
Description: Search files using regex patterns. Shows matches with surrounding context.
Parameters:
- path: (required) Directory to search (relative to ${cwd})
- regex: (required) Rust regex pattern to search for
- file_pattern: (optional) Glob pattern to filter files (e.g. "*.ts")
Usage:
<search_files>
<path>directory to search</path>
<regex>pattern to search</regex>
<file_pattern>optional file pattern</file_pattern>
</search_files>
## list_code_definition_names
Description: List code definitions (classes, functions, etc.) in source files.
Parameters:
- path: (required) Directory to analyze (relative to ${cwd})
Usage:
<list_code_definition_names>
<path>directory to analyze</path>
</list_code_definition_names>
## attempt_completion
Description: Signal task completion and present results.
Parameters:
- result: (required) Description of completed work
- command: (optional) Command to demonstrate result
Usage:
<attempt_completion>
<result>description of completed work</result>
<command>optional demo command</command>
</attempt_completion>
# Guidelines
1. Use one tool at a time and wait for results
2. Provide complete file content when using write_to_file
3. Be direct and technical in responses
4. Present final results using attempt_completion
5. Do not make assumptions about command success
6. Do not make up commands that don't exist
# Rules
- Current working directory is: ${cwd}
- Cannot cd to different directories
- Must wait for confirmation after each tool use
- Must provide complete file content when writing files
- Be direct and technical, not conversational
- Do not end messages with questions${rulesContent}`;
};

40
cli/deno.d.ts vendored Normal file
View File

@@ -0,0 +1,40 @@
declare namespace Deno {
export const args: string[];
export function exit(code?: number): never;
export const env: {
get(key: string): string | undefined;
};
export function cwd(): string;
export function readTextFile(path: string): Promise<string>;
export function writeTextFile(path: string, data: string): Promise<void>;
export function mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
export function readDir(path: string): AsyncIterable<{
name: string;
isFile: boolean;
isDirectory: boolean;
}>;
export function stat(path: string): Promise<{
isFile: boolean;
isDirectory: boolean;
}>;
export class Command {
constructor(cmd: string, options?: {
args?: string[];
stdout?: "piped";
stderr?: "piped";
});
output(): Promise<{
stdout: Uint8Array;
stderr: Uint8Array;
}>;
}
export const permissions: {
query(desc: { name: string; path?: string }): Promise<{ state: "granted" | "denied" }>;
};
export const errors: {
PermissionDenied: typeof Error;
};
export const stdout: {
write(data: Uint8Array): Promise<number>;
};
}

13
cli/deno.jsonc Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"allowJs": true,
"strict": true,
"lib": ["deno.ns", "dom"]
},
"tasks": {
"start": "deno run --allow-read=. mod.ts",
"dev": "deno run --allow-read=. mod.ts",
"install": "deno install --allow-read --allow-write --allow-net --allow-env --allow-run --global --name cline mod.ts",
"check": "deno check mod.ts"
}
}

87
cli/deno.lock generated Normal file
View File

@@ -0,0 +1,87 @@
{
"version": "4",
"remote": {
"https://deno.land/std@0.220.1/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
"https://deno.land/std@0.220.1/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
"https://deno.land/std@0.220.1/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
"https://deno.land/std@0.220.1/flags/mod.ts": "9f13f3a49c54618277ac49195af934f1c7d235731bcf80fd33b8b234e6839ce9",
"https://deno.land/std@0.220.1/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a",
"https://deno.land/std@0.220.1/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8",
"https://deno.land/std@0.220.1/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2",
"https://deno.land/std@0.220.1/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c",
"https://deno.land/std@0.220.1/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c",
"https://deno.land/std@0.220.1/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8",
"https://deno.land/std@0.220.1/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b",
"https://deno.land/std@0.220.1/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf",
"https://deno.land/std@0.220.1/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d",
"https://deno.land/std@0.220.1/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8",
"https://deno.land/std@0.220.1/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3",
"https://deno.land/std@0.220.1/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607",
"https://deno.land/std@0.220.1/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a",
"https://deno.land/std@0.220.1/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883",
"https://deno.land/std@0.220.1/path/_interface.ts": "a1419fcf45c0ceb8acdccc94394e3e94f99e18cfd32d509aab514c8841799600",
"https://deno.land/std@0.220.1/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15",
"https://deno.land/std@0.220.1/path/basename.ts": "5d341aadb7ada266e2280561692c165771d071c98746fcb66da928870cd47668",
"https://deno.land/std@0.220.1/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643",
"https://deno.land/std@0.220.1/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36",
"https://deno.land/std@0.220.1/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c",
"https://deno.land/std@0.220.1/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441",
"https://deno.land/std@0.220.1/path/format.ts": "42a2f3201343df77061207e6aaf78c95bafce7f711dcb7fe1e5840311c505778",
"https://deno.land/std@0.220.1/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069",
"https://deno.land/std@0.220.1/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972",
"https://deno.land/std@0.220.1/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7",
"https://deno.land/std@0.220.1/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141",
"https://deno.land/std@0.220.1/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a",
"https://deno.land/std@0.220.1/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0",
"https://deno.land/std@0.220.1/path/mod.ts": "2821a1bb3a4148a0ffe79c92aa41aa9319fef73c6d6f5178f52b2c720d3eb02d",
"https://deno.land/std@0.220.1/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352",
"https://deno.land/std@0.220.1/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f",
"https://deno.land/std@0.220.1/path/parse.ts": "65e8e285f1a63b714e19ef24b68f56e76934c3df0b6e65fd440d3991f4f8aefb",
"https://deno.land/std@0.220.1/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d",
"https://deno.land/std@0.220.1/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0",
"https://deno.land/std@0.220.1/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4",
"https://deno.land/std@0.220.1/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1",
"https://deno.land/std@0.220.1/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00",
"https://deno.land/std@0.220.1/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2",
"https://deno.land/std@0.220.1/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1",
"https://deno.land/std@0.220.1/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40",
"https://deno.land/std@0.220.1/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f",
"https://deno.land/std@0.220.1/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede",
"https://deno.land/std@0.220.1/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9",
"https://deno.land/std@0.220.1/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63",
"https://deno.land/std@0.220.1/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25",
"https://deno.land/std@0.220.1/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604",
"https://deno.land/std@0.220.1/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91",
"https://deno.land/std@0.220.1/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6",
"https://deno.land/std@0.220.1/path/posix/parse.ts": "0b1fc4cb890dbb699ec1d2c232d274843b4a7142e1ad976b69fe51c954eb6080",
"https://deno.land/std@0.220.1/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c",
"https://deno.land/std@0.220.1/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf",
"https://deno.land/std@0.220.1/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf",
"https://deno.land/std@0.220.1/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0",
"https://deno.land/std@0.220.1/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add",
"https://deno.land/std@0.220.1/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d",
"https://deno.land/std@0.220.1/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b",
"https://deno.land/std@0.220.1/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40",
"https://deno.land/std@0.220.1/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808",
"https://deno.land/std@0.220.1/path/windows/basename.ts": "e2dbf31d1d6385bfab1ce38c333aa290b6d7ae9e0ecb8234a654e583cf22f8fe",
"https://deno.land/std@0.220.1/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4",
"https://deno.land/std@0.220.1/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5",
"https://deno.land/std@0.220.1/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9",
"https://deno.land/std@0.220.1/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef",
"https://deno.land/std@0.220.1/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6",
"https://deno.land/std@0.220.1/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01",
"https://deno.land/std@0.220.1/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8",
"https://deno.land/std@0.220.1/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a",
"https://deno.land/std@0.220.1/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9",
"https://deno.land/std@0.220.1/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf",
"https://deno.land/std@0.220.1/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25",
"https://deno.land/std@0.220.1/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604",
"https://deno.land/std@0.220.1/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780",
"https://deno.land/std@0.220.1/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6",
"https://deno.land/std@0.220.1/path/windows/parse.ts": "dbdfe2bc6db482d755b5f63f7207cd019240fcac02ad2efa582adf67ff10553a",
"https://deno.land/std@0.220.1/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7",
"https://deno.land/std@0.220.1/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972",
"https://deno.land/std@0.220.1/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e",
"https://deno.land/std@0.220.1/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c"
}
}

21
cli/deps.ts Normal file
View File

@@ -0,0 +1,21 @@
// Re-export standard library dependencies
export { parse } from "https://deno.land/std@0.220.1/flags/mod.ts";
export {
blue,
red,
gray,
yellow,
bold,
} from "https://deno.land/std@0.220.1/fmt/colors.ts";
export {
join,
dirname,
} from "https://deno.land/std@0.220.1/path/mod.ts";
// Export types
export type {
ApiHandler,
AgentConfig,
OperationMode,
ToolResponse,
} from "./types.d.ts";

123
cli/mod.ts Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env -S deno run --allow-read=. --allow-write=. --allow-run --allow-net --allow-env
import { parse } from "./deps.ts";
import { blue, red, gray, yellow, bold } from "./deps.ts";
import { buildApiHandler } from "./api/mod.ts";
import { StandaloneAgent } from "./core/StandaloneAgent.ts";
import { SYSTEM_PROMPT } from "./core/prompts.ts";
import type { ApiHandler, AgentConfig } from "./types.d.ts";
// Parse command line arguments
const args = parse(Deno.args, {
string: ["model", "key"],
boolean: ["help"],
alias: {
m: "model",
k: "key",
h: "help"
},
default: {
model: "anthropic/claude-3.5-sonnet"
},
});
if (args.help || Deno.args.length === 0) {
console.log(blue("\nCline - AI Coding Assistant\n"));
console.log("Usage:");
console.log(" cline <task> [options]\n");
console.log("Required Permissions:");
console.log(" --allow-read=. Read files in working directory");
console.log(" --allow-write=. Write files in working directory");
console.log(" --allow-run Execute commands (with interactive prompts)\n");
console.log(" --allow-net Make API calls");
console.log(" --allow-env Access environment variables\n");
console.log("Pre-approved Commands:");
console.log(" npm - Package management (install, run, test, build)");
console.log(" git - Version control (status, add, commit, push, pull, clone)");
console.log(" deno - Deno runtime (run, test, fmt, lint, check)");
console.log(" ls - List directory contents");
console.log(" cat - Show file contents");
console.log(" echo - Print text");
console.log(" find - Search for files");
console.log("\nOther commands will prompt for confirmation before execution.\n");
console.log("Options:");
console.log(" -m, --model <model> LLM model to use (default: \"anthropic/claude-3.5-sonnet\")");
console.log(" -k, --key <key> OpenRouter API key (or set OPENROUTER_API_KEY env var)");
console.log(" -h, --help Display help for command\n");
console.log("Examples:");
console.log(gray(" # Run pre-approved command"));
console.log(" cline \"Run npm install\"\n");
console.log(gray(" # Run command that requires confirmation"));
console.log(" cline \"Run yarn install\"\n");
Deno.exit(0);
}
// Verify required permissions
const requiredPermissions = [
{ name: "read", path: "." },
{ name: "write", path: "." },
{ name: "run" },
{ name: "net" },
{ name: "env" }
] as const;
for (const permission of requiredPermissions) {
const status = await Deno.permissions.query(permission);
if (status.state !== "granted") {
console.error(red(`Error: Missing required permission`));
console.error(yellow(`Hint: Run with the following permissions:`));
console.error(yellow(` deno run ${requiredPermissions.map(p =>
"path" in p ? `--allow-${p.name}=${p.path}` : `--allow-${p.name}`
).join(" ")} cli/mod.ts ...\n`));
Deno.exit(1);
}
}
const task = args._[0] as string;
const apiKey = args.key || Deno.env.get("OPENROUTER_API_KEY");
if (!apiKey) {
console.error(red("Error: OpenRouter API key is required. Set it with --key or OPENROUTER_API_KEY env var"));
console.error(yellow("Get your API key from: https://openrouter.ai/keys"));
Deno.exit(1);
}
try {
const workingDir = Deno.cwd();
// Initialize API handler
const apiHandler = buildApiHandler({
model: args.model,
apiKey
});
// Create agent instance
const agent = new StandaloneAgent({
api: apiHandler,
systemPrompt: await SYSTEM_PROMPT(workingDir),
workingDir
});
// Run the task
console.log(blue(`\nStarting task: ${bold(task)}`));
console.log(gray(`Working directory: ${workingDir}`));
console.log(gray(`Model: ${args.model}`));
console.log(gray("---\n"));
await agent.runTask(task);
} catch (error) {
if (error instanceof Error) {
console.error(red(`\nError: ${error.message}`));
} else {
console.error(red("\nAn unknown error occurred"));
}
Deno.exit(1);
}

225
cli/tools/mod.ts Normal file
View File

@@ -0,0 +1,225 @@
/// <reference lib="deno.ns" />
import { join, dirname } from "https://deno.land/std@0.220.1/path/mod.ts";
import { red, yellow, green } from "https://deno.land/std@0.220.1/fmt/colors.ts";
import type { ToolResponse } from "../types.d.ts";
interface CommandConfig {
desc: string;
args: readonly string[];
}
// Define allowed commands and their descriptions
const ALLOWED_COMMANDS: Record<string, CommandConfig> = {
'npm': {
desc: "Node package manager",
args: ["install", "run", "test", "build"]
},
'git': {
desc: "Version control",
args: ["status", "add", "commit", "push", "pull", "clone", "checkout", "branch"]
},
'deno': {
desc: "Deno runtime",
args: ["run", "test", "fmt", "lint", "check", "compile", "bundle"]
},
'ls': {
desc: "List directory contents",
args: ["-l", "-a", "-la", "-lh"]
},
'cat': {
desc: "Show file contents",
args: []
},
'echo': {
desc: "Print text",
args: []
}
};
// Track commands that have been allowed for this session
const alwaysAllowedCommands = new Set<string>();
function isCommandAllowed(command: string): boolean {
// Split command into parts
const parts = command.trim().split(/\s+/);
if (parts.length === 0) return false;
// Get base command
const baseCmd = parts[0];
if (!(baseCmd in ALLOWED_COMMANDS)) return false;
// If command has arguments, check if they're allowed
if (parts.length > 1 && ALLOWED_COMMANDS[baseCmd].args.length > 0) {
const arg = parts[1];
return ALLOWED_COMMANDS[baseCmd].args.includes(arg);
}
return true;
}
async function promptForCommand(command: string): Promise<boolean> {
// Check if command has been previously allowed
if (alwaysAllowedCommands.has(command)) {
console.log(yellow("\nWarning: Running previously allowed command:"), red(command));
return true;
}
console.log(yellow("\nWarning: Command not in allowlist"));
console.log("Command:", red(command));
console.log("\nAllowed commands:");
Object.entries(ALLOWED_COMMANDS).forEach(([cmd, { desc, args }]) => {
console.log(` ${green(cmd)}: ${desc}`);
if (args.length) {
console.log(` Arguments: ${args.join(", ")}`);
}
});
const answer = prompt("\nDo you want to run this command? (y/n/always) ");
if (answer?.toLowerCase() === 'always') {
alwaysAllowedCommands.add(command);
return true;
}
return answer?.toLowerCase() === 'y';
}
export async function executeCommand(command: string): Promise<ToolResponse> {
try {
// Check if command is allowed
if (!isCommandAllowed(command)) {
// Prompt user for confirmation
const shouldRun = await promptForCommand(command);
if (!shouldRun) {
return "Command execution cancelled by user";
}
console.log(yellow("\nProceeding with command execution..."));
}
const process = new Deno.Command("sh", {
args: ["-c", command],
stdout: "piped",
stderr: "piped",
});
const { stdout, stderr } = await process.output();
const decoder = new TextDecoder();
return decoder.decode(stdout) + (stderr.length ? `\nStderr:\n${decoder.decode(stderr)}` : "");
} catch (error) {
return `Error executing command: ${error instanceof Error ? error.message : String(error)}`;
}
}
export async function readFile(workingDir: string, relativePath: string): Promise<ToolResponse> {
try {
const fullPath = join(workingDir, relativePath);
const content = await Deno.readTextFile(fullPath);
return content;
} catch (error) {
return `Error reading file: ${error instanceof Error ? error.message : String(error)}`;
}
}
export async function writeFile(workingDir: string, relativePath: string, content: string): Promise<ToolResponse> {
try {
const fullPath = join(workingDir, relativePath);
await Deno.mkdir(dirname(fullPath), { recursive: true });
await Deno.writeTextFile(fullPath, content);
return `Successfully wrote to ${relativePath}`;
} catch (error) {
return `Error writing file: ${error instanceof Error ? error.message : String(error)}`;
}
}
export async function searchFiles(
workingDir: string,
searchPath: string,
regex: string,
filePattern?: string
): Promise<ToolResponse> {
try {
const fullPath = join(workingDir, searchPath);
const results: string[] = [];
const regexObj = new RegExp(regex, "g");
const patternObj = filePattern ? new RegExp(filePattern) : null;
for await (const entry of Deno.readDir(fullPath)) {
if (entry.isFile && (!patternObj || patternObj.test(entry.name))) {
const filePath = join(fullPath, entry.name);
const content = await Deno.readTextFile(filePath);
const matches = content.match(regexObj);
if (matches) {
results.push(`File: ${entry.name}\nMatches:\n${matches.join("\n")}\n`);
}
}
}
return results.join("\n") || "No matches found";
} catch (error) {
return `Error searching files: ${error instanceof Error ? error.message : String(error)}`;
}
}
export async function listFiles(workingDir: string, relativePath: string, recursive: boolean): Promise<ToolResponse> {
try {
const fullPath = join(workingDir, relativePath);
const files: string[] = [];
async function* walkDir(dir: string): AsyncGenerator<string> {
for await (const entry of Deno.readDir(dir)) {
const entryPath = join(dir, entry.name);
if (entry.isFile) {
yield entryPath.replace(fullPath + "/", "");
} else if (recursive && entry.isDirectory) {
yield* walkDir(entryPath);
}
}
}
for await (const file of walkDir(fullPath)) {
files.push(file);
}
return files.join("\n") || "No files found";
} catch (error) {
return `Error listing files: ${error instanceof Error ? error.message : String(error)}`;
}
}
export async function listCodeDefinitions(workingDir: string, relativePath: string): Promise<ToolResponse> {
try {
const fullPath = join(workingDir, relativePath);
const content = await Deno.readTextFile(fullPath);
// Basic regex patterns for common code definitions
const patterns = {
function: /(?:function|const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:=\s*(?:function|\([^)]*\)\s*=>)|[({])/g,
class: /class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
method: /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/g,
};
const definitions: Record<string, string[]> = {
functions: [],
classes: [],
methods: [],
};
let match;
while ((match = patterns.function.exec(content)) !== null) {
definitions.functions.push(match[1]);
}
while ((match = patterns.class.exec(content)) !== null) {
definitions.classes.push(match[1]);
}
while ((match = patterns.method.exec(content)) !== null) {
definitions.methods.push(match[1]);
}
return Object.entries(definitions)
.map(([type, names]) => `${type}:\n${names.join("\n")}`)
.join("\n\n");
} catch (error) {
return `Error listing code definitions: ${error instanceof Error ? error.message : String(error)}`;
}
}

43
cli/types.d.ts vendored Normal file
View File

@@ -0,0 +1,43 @@
export interface ApiHandler {
sendMessage(message: string): Promise<string>;
createMessage(systemPrompt: string, history: Message[]): AsyncIterable<MessageChunk>;
}
export interface AgentConfig {
api: ApiHandler;
systemPrompt: string;
workingDir: string;
debug?: boolean;
}
export type ToolResponse = string;
export interface Message {
role: "user" | "assistant";
content: TextBlock[];
}
export interface TextBlock {
type: "text";
text: string;
}
export interface ToolResult {
tool: string;
params: Record<string, string>;
output: string;
}
export interface MessageChunk {
type: "text";
text: string;
}
export interface UsageBlock {
type: "usage";
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}