Răsfoiți Sursa

Add a command-line cline powered by deno

Matt Rubens 1 an în urmă
părinte
comite
1c471bd3cb
12 a modificat fișierele cu 1103 adăugiri și 0 ștergeri
  1. 110 0
      cli/README.md
  2. 10 0
      cli/api/mod.ts
  3. 147 0
      cli/api/providers/openrouter.ts
  4. 164 0
      cli/core/StandaloneAgent.ts
  5. 120 0
      cli/core/prompts.ts
  6. 40 0
      cli/deno.d.ts
  7. 13 0
      cli/deno.jsonc
  8. 87 0
      cli/deno.lock
  9. 21 0
      cli/deps.ts
  10. 123 0
      cli/mod.ts
  11. 225 0
      cli/tools/mod.ts
  12. 43 0
      cli/types.d.ts

+ 110 - 0
cli/README.md

@@ -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 - 0
cli/api/mod.ts

@@ -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 - 0
cli/api/providers/openrouter.ts

@@ -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 - 0
cli/core/StandaloneAgent.ts

@@ -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 - 0
cli/core/prompts.ts

@@ -0,0 +1,120 @@
+import { join } from "https://deno.land/[email protected]/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 - 0
cli/deno.d.ts

@@ -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 - 0
cli/deno.jsonc

@@ -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 - 0
cli/deno.lock

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

+ 21 - 0
cli/deps.ts

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

+ 123 - 0
cli/mod.ts

@@ -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 - 0
cli/tools/mod.ts

@@ -0,0 +1,225 @@
+/// <reference lib="deno.ns" />
+import { join, dirname } from "https://deno.land/[email protected]/path/mod.ts";
+import { red, yellow, green } from "https://deno.land/[email protected]/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 - 0
cli/types.d.ts

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