From 5c234a811f575d0bc24afa92565c03294e41c9cd Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Mon, 23 Feb 2026 01:12:42 +0000 Subject: [PATCH] Add .gitignore and clean up repository metadata --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 11 + build/lib/ollama_cli/__init__.py | 0 build/lib/ollama_cli/agents/__init__.py | 0 build/lib/ollama_cli/core/__init__.py | 0 build/lib/ollama_cli/core/config.py | 57 --- build/lib/ollama_cli/core/ollama.py | 87 ---- build/lib/ollama_cli/core/sessions.py | 53 --- build/lib/ollama_cli/integrations/__init__.py | 0 build/lib/ollama_cli/integrations/lsp.py | 75 --- build/lib/ollama_cli/integrations/mcp.py | 103 ----- build/lib/ollama_cli/integrations/notify.py | 64 --- build/lib/ollama_cli/main.py | 433 ------------------ build/lib/ollama_cli/tools/__init__.py | 0 build/lib/ollama_cli/tools/base.py | 55 --- build/lib/ollama_cli/tools/code.py | 96 ---- build/lib/ollama_cli/tools/execution.py | 36 -- build/lib/ollama_cli/tools/filesystem.py | 83 ---- build/lib/ollama_cli/tools/git.py | 52 --- build/lib/ollama_cli/tools/knowledge.py | 41 -- build/lib/ollama_cli/tools/media.py | 183 -------- build/lib/ollama_cli/tools/memory.py | 59 --- build/lib/ollama_cli/tools/system.py | 46 -- build/lib/ollama_cli/tools/web.py | 67 --- build/lib/ollama_cli/ui/__init__.py | 0 build/lib/ollama_cli/ui/formatter.py | 40 -- build/lib/ollama_cli/ui/repl.py | 25 - src/ollama_cli.egg-info/PKG-INFO | 9 - src/ollama_cli.egg-info/SOURCES.txt | 33 -- src/ollama_cli.egg-info/dependency_links.txt | 1 - src/ollama_cli.egg-info/entry_points.txt | 2 - src/ollama_cli.egg-info/requires.txt | 3 - src/ollama_cli.egg-info/top_level.txt | 1 - 33 files changed, 11 insertions(+), 1704 deletions(-) delete mode 100644 .DS_Store create mode 100644 .gitignore delete mode 100644 build/lib/ollama_cli/__init__.py delete mode 100644 build/lib/ollama_cli/agents/__init__.py delete mode 100644 build/lib/ollama_cli/core/__init__.py delete mode 100644 build/lib/ollama_cli/core/config.py delete mode 100644 build/lib/ollama_cli/core/ollama.py delete mode 100644 build/lib/ollama_cli/core/sessions.py delete mode 100644 build/lib/ollama_cli/integrations/__init__.py delete mode 100644 build/lib/ollama_cli/integrations/lsp.py delete mode 100644 build/lib/ollama_cli/integrations/mcp.py delete mode 100644 build/lib/ollama_cli/integrations/notify.py delete mode 100644 build/lib/ollama_cli/main.py delete mode 100644 build/lib/ollama_cli/tools/__init__.py delete mode 100644 build/lib/ollama_cli/tools/base.py delete mode 100644 build/lib/ollama_cli/tools/code.py delete mode 100644 build/lib/ollama_cli/tools/execution.py delete mode 100644 build/lib/ollama_cli/tools/filesystem.py delete mode 100644 build/lib/ollama_cli/tools/git.py delete mode 100644 build/lib/ollama_cli/tools/knowledge.py delete mode 100644 build/lib/ollama_cli/tools/media.py delete mode 100644 build/lib/ollama_cli/tools/memory.py delete mode 100644 build/lib/ollama_cli/tools/system.py delete mode 100644 build/lib/ollama_cli/tools/web.py delete mode 100644 build/lib/ollama_cli/ui/__init__.py delete mode 100644 build/lib/ollama_cli/ui/formatter.py delete mode 100644 build/lib/ollama_cli/ui/repl.py delete mode 100644 src/ollama_cli.egg-info/PKG-INFO delete mode 100644 src/ollama_cli.egg-info/SOURCES.txt delete mode 100644 src/ollama_cli.egg-info/dependency_links.txt delete mode 100644 src/ollama_cli.egg-info/entry_points.txt delete mode 100644 src/ollama_cli.egg-info/requires.txt delete mode 100644 src/ollama_cli.egg-info/top_level.txt diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 List[str]: - """Get list of models available in Ollama""" - try: - # Try API first - response = requests.get(self.list_url, timeout=5) - if response.status_code == 200: - data = response.json() - return [m['name'] for m in data.get('models', [])] - - # Fallback to CLI - result = subprocess.run("ollama list", shell=True, capture_output=True, text=True, timeout=5) - models = [] - for line in result.stdout.split('\n')[1:]: # Skip header - if line.strip(): - models.append(line.split()[0]) - return models - except Exception: - return [] - - def chat(self, messages: List[Dict], model: str, stream: bool = True) -> Generator[str, None, str]: - """Call Ollama API with streaming""" - try: - response = requests.post(self.chat_url, json={ - "model": model, - "messages": messages, - "stream": stream - }, stream=stream, timeout=60) - response.raise_for_status() - - full_response = "" - if stream: - for line in response.iter_lines(): - if line: - chunk = json.loads(line) - if "message" in chunk and "content" in chunk["message"]: - content = chunk["message"]["content"] - full_response += content - yield content - return full_response - else: - data = response.json() - return data.get("message", {}).get("content", "") - - except requests.exceptions.ConnectionError: - raise ConnectionError(f"Cannot connect to Ollama at {self.base_url}. Make sure it's running.") - except Exception as e: - raise Exception(f"Error calling Ollama: {str(e)}") - - def select_best_model(self, user_input: str, available_models: List[str], default_model: str) -> str: - """Select best model based on task type""" - if not available_models: - return default_model - - # Model preferences - code_models = ["qwen2.5-coder", "mistral-nemo", "codellama", "deepseek-coder"] - general_models = ["llama3.2", "llama3.1", "mistral"] - - # Simple heuristic - code_keywords = ['code', 'script', 'program', 'function', 'debug', 'python', 'javascript'] - is_code_task = any(k in user_input.lower() for k in code_keywords) - - target_list = code_models if is_code_task else general_models - - for target in target_list: - for available in available_models: - if target in available.lower(): - return available - - return available_models[0] if available_models else default_model diff --git a/build/lib/ollama_cli/core/sessions.py b/build/lib/ollama_cli/core/sessions.py deleted file mode 100644 index bc136c7..0000000 --- a/build/lib/ollama_cli/core/sessions.py +++ /dev/null @@ -1,53 +0,0 @@ -import os -import json -from datetime import datetime -from typing import List, Dict, Optional - -SESSION_DIR = os.path.expanduser("~/.ollama-cli-sessions") - -def save_session(messages: List[Dict], session_name: Optional[str] = None) -> str: - """Save conversation session""" - os.makedirs(SESSION_DIR, exist_ok=True) - if not session_name: - session_name = datetime.now().strftime("%Y%m%d_%H%M%S") - - # Ensure filename is safe - safe_name = "".join([c for c in session_name if c.isalnum() or c in (' ', '.', '_', '-')]).strip() - session_file = os.path.join(SESSION_DIR, f"{safe_name}.json") - - with open(session_file, 'w') as f: - json.dump({ - "messages": messages, - "timestamp": datetime.now().isoformat(), - "name": session_name - }, f, indent=2) - return session_file - -def load_session(session_name: str) -> Optional[List[Dict]]: - """Load conversation session""" - session_file = os.path.join(SESSION_DIR, f"{session_name}.json") - if not os.path.exists(session_file): - # Try without extension if name doesn't have it - if not session_name.endswith('.json'): - session_file = os.path.join(SESSION_DIR, f"{session_name}.json") - - if os.path.exists(session_file): - with open(session_file, 'r') as f: - data = json.load(f) - return data.get("messages", []) - return None - -def list_sessions() -> List[str]: - """List available sessions""" - if not os.path.exists(SESSION_DIR): - return [] - sessions = [f.replace('.json', '') for f in os.listdir(SESSION_DIR) if f.endswith('.json')] - return sorted(sessions, reverse=True) - -def delete_session(session_name: str) -> bool: - """Delete a session""" - session_file = os.path.join(SESSION_DIR, f"{session_name}.json") - if os.path.exists(session_file): - os.remove(session_file) - return True - return False diff --git a/build/lib/ollama_cli/integrations/__init__.py b/build/lib/ollama_cli/integrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/ollama_cli/integrations/lsp.py b/build/lib/ollama_cli/integrations/lsp.py deleted file mode 100644 index 58153b3..0000000 --- a/build/lib/ollama_cli/integrations/lsp.py +++ /dev/null @@ -1,75 +0,0 @@ -import subprocess -import os -import json -from typing import Dict, Any, Optional - -LSP_SERVERS = {} - -def start_lsp_server(language: str, root_path: str = ".") -> str: - """Start a language server""" - lsp_commands = { - "python": ["pyright-langserver", "--stdio"], - "javascript": ["typescript-language-server", "--stdio"], - "typescript": ["typescript-language-server", "--stdio"], - "rust": ["rust-analyzer"], - "go": ["gopls"], - } - - if language not in lsp_commands: - return f"No LSP server configured for {language}" - - try: - process = subprocess.Popen( - lsp_commands[language], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - cwd=os.path.expanduser(root_path) - ) - - # Initialize - init_request = { - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "processId": os.getpid(), - "rootUri": f"file://{os.path.abspath(os.path.expanduser(root_path))}", - "capabilities": {} - } - } - - msg = json.dumps(init_request) - process.stdin.write(f"Content-Length: {len(msg)}\r\n\r\n{msg}") - process.stdin.flush() - - LSP_SERVERS[language] = process - return f"Started LSP server for {language}" - except Exception as e: - return f"Error starting LSP: {str(e)}" - -def lsp_goto_definition(language: str, file_path: str, line: int, character: int) -> str: - """Get definition location""" - if language not in LSP_SERVERS: - return "LSP server not started" - - try: - process = LSP_SERVERS[language] - request = { - "jsonrpc": "2.0", - "id": 2, - "method": "textDocument/definition", - "params": { - "textDocument": {"uri": f"file://{os.path.abspath(file_path)}"}, - "position": {"line": line, "character": character} - } - } - - msg = json.dumps(request) - process.stdin.write(f"Content-Length: {len(msg)}\r\n\r\n{msg}") - process.stdin.flush() - - return "Definition lookup sent (response parsing not implemented)" - except Exception as e: - return f"LSP error: {str(e)}" diff --git a/build/lib/ollama_cli/integrations/mcp.py b/build/lib/ollama_cli/integrations/mcp.py deleted file mode 100644 index f2fa29f..0000000 --- a/build/lib/ollama_cli/integrations/mcp.py +++ /dev/null @@ -1,103 +0,0 @@ -import subprocess -import json -import os -from typing import Dict, Any, Optional, List - -MCP_SERVERS = {} -MCP_TOOLS = {} - -def connect_mcp_server(name: str, command: str, args: Optional[List[str]] = None) -> str: - """Connect to an MCP server via stdio""" - try: - cmd = [command] + (args or []) - process = subprocess.Popen( - cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1 - ) - - # Send initialize request - init_request = { - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "clientInfo": {"name": "ollama-cli", "version": "3.0"} - } - } - - process.stdin.write(json.dumps(init_request) + "\n") - process.stdin.flush() - - # Read response - response = process.stdout.readline() - if response: - data = json.loads(response) - if "result" in data: - MCP_SERVERS[name] = process - - # List tools - tools_request = { - "jsonrpc": "2.0", - "id": 2, - "method": "tools/list" - } - process.stdin.write(json.dumps(tools_request) + "\n") - process.stdin.flush() - - tools_response = process.stdout.readline() - if tools_response: - tools_data = json.loads(tools_response) - if "result" in tools_data and "tools" in tools_data["result"]: - for tool in tools_data["result"]["tools"]: - tool_name = f"mcp_{name}_{tool['name']}" - MCP_TOOLS[tool_name] = { - "server": name, - "name": tool["name"], - "description": tool.get("description", ""), - "schema": tool.get("inputSchema", {}) - } - - return f"Connected to MCP server '{name}' with {len([t for t in MCP_TOOLS if t.startswith(f'mcp_{name}_')])} tools" - - return f"Failed to initialize MCP server '{name}'" - except Exception as e: - return f"Error connecting to MCP server: {str(e)}" - -def call_mcp_tool(server_name: str, tool_name: str, arguments: Dict[str, Any]) -> str: - """Call a tool on an MCP server""" - try: - if server_name not in MCP_SERVERS: - return f"Error: MCP server '{server_name}' not connected" - - process = MCP_SERVERS[server_name] - - request = { - "jsonrpc": "2.0", - "id": 3, - "method": "tools/call", - "params": { - "name": tool_name, - "arguments": arguments - } - } - - process.stdin.write(json.dumps(request) + "\n") - process.stdin.flush() - - response = process.stdout.readline() - if response: - data = json.loads(response) - if "result" in data: - return json.dumps(data["result"], indent=2) - elif "error" in data: - return f"MCP Error: {data['error']}" - - return "No response from MCP server" - except Exception as e: - return f"Error calling MCP tool: {str(e)}" diff --git a/build/lib/ollama_cli/integrations/notify.py b/build/lib/ollama_cli/integrations/notify.py deleted file mode 100644 index c7a2da5..0000000 --- a/build/lib/ollama_cli/integrations/notify.py +++ /dev/null @@ -1,64 +0,0 @@ -import requests -import json -import threading -from typing import Callable, List, Dict - -def send_notification(config: Dict, title: str, message: str, priority: str = "default"): - """Send notification via ntfy.sh""" - ntfy_config = config.get("ntfy", {}) - - if not ntfy_config.get("enabled") or not ntfy_config.get("topic"): - return - - try: - server = ntfy_config.get("server", "https://ntfy.sh") - topic = ntfy_config["topic"] - url = f"{server}/{topic}" - - requests.post(url, - data=message.encode('utf-8'), - headers={ - "Title": title, - "Priority": priority - }, - timeout=5 - ) - except: - pass - -def ntfy_listener(config: Dict, on_message: Callable[[str], str]): - """Listen for ntfy notifications and process them""" - ntfy_config = config.get("ntfy", {}) - - if not ntfy_config.get("enabled") or not ntfy_config.get("topic"): - return - - server = ntfy_config.get("server", "https://ntfy.sh") - topic = ntfy_config["topic"] - url = f"{server}/{topic}/json" - - try: - response = requests.get(url, stream=True, timeout=None) - for line in response.iter_lines(): - if line: - data = json.loads(line) - if data.get("event") == "message": - title = data.get("title", "") - message = data.get("message", "") - - # Skip messages sent by this script - if title in ["Response", "Ollama CLI", "File Created", "Command Complete"]: - continue - - # Process message - response_text = on_message(message) - - # Send response back - send_notification(config, "Response", response_text[:1000]) - except: - pass - -def start_notify_thread(config: Dict, on_message: Callable[[str], str]): - thread = threading.Thread(target=ntfy_listener, args=(config, on_message), daemon=True) - thread.start() - return thread diff --git a/build/lib/ollama_cli/main.py b/build/lib/ollama_cli/main.py deleted file mode 100644 index 91bc83c..0000000 --- a/build/lib/ollama_cli/main.py +++ /dev/null @@ -1,433 +0,0 @@ -""" -Ollama CLI v3.0 -License: MIT -Author: Gemini CLI -""" - -import sys -import os -import json -import re -import logging -from typing import List, Dict, Optional, Tuple - -# Setup logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler(os.path.expanduser("~/.ollama-cli.log")), - ] -) -logger = logging.getLogger("ollama-cli") - -# Add current directory to path -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from ollama_cli.core.config import load_config, save_config, update_config -from ollama_cli.core.ollama import OllamaClient -from ollama_cli.core.sessions import save_session, load_session, list_sessions -from ollama_cli.ui.formatter import console, print_markdown, print_error, print_status, StreamingDisplay -from ollama_cli.ui.repl import REPL -from ollama_cli.tools.base import registry -import ollama_cli.tools.filesystem -import ollama_cli.tools.system -import ollama_cli.tools.web -import ollama_cli.tools.git -import ollama_cli.tools.code -import ollama_cli.tools.knowledge -import ollama_cli.tools.memory -import ollama_cli.tools.execution -import ollama_cli.tools.media -from ollama_cli.integrations.mcp import connect_mcp_server, call_mcp_tool, MCP_TOOLS -from ollama_cli.integrations.lsp import start_lsp_server, lsp_goto_definition -from ollama_cli.integrations.notify import start_notify_thread, send_notification - -VERSION = "3.0.0" - -class OllamaCLI: - def __init__(self): - self.config = load_config() - self.client = OllamaClient(self.config.get("ollama_url")) - self.repl = REPL() - self.messages = [] - self.available_models = [] - self.current_model = self.config.get("default_model", "llama3.2") - self.system_prompt = self._build_system_prompt() - self.max_tool_iterations = 5 - - def _build_system_prompt(self) -> str: - base_prompt = """You are a highly capable AI assistant powered by Ollama. -You have access to a variety of tools to help you complete tasks. - -CRITICAL RULES: -1. When you need to use a tool, you MUST use the EXACT format below. -2. Output ONLY the tool call. DO NOT explain what you are doing. -3. DO NOT repeat a tool call if you have already received a result for it. -4. If you have the answer, just give it. Do not use tools unless necessary. - - -name_of_tool -{"param": "value"} - - -You can call multiple tools in one response by repeating the block above.""" - - tool_desc = registry.generate_system_prompt_snippet() - - # Add MCP tools if any - if MCP_TOOLS: - tool_desc += "\nMCP Tools:\n" - for name, info in MCP_TOOLS.items(): - tool_desc += f"- {name}: {info['description']}\n" - - return f"{base_prompt}\n\n{tool_desc}" - - def reset_messages(self): - self.messages = [{"role": "system", "content": self.system_prompt}] - - def handle_command(self, user_input: str) -> bool: - """Handle slash commands.""" - if not user_input.startswith('/'): - return False - - parts = user_input.split() - cmd = parts[0].lower() - - try: - if cmd == '/help': - help_text = """ -[bold cyan]Ollama CLI v3.0 Help[/bold cyan] - -[bold]General Commands:[/bold] - /help - Show this help message - /quit - Exit the CLI - /clear - Clear conversation history - -[bold]Model & Sessions:[/bold] - /models - List available models - /model - Switch current model - /sessions - List saved sessions - /save [name] - Save current session - /load - Load a saved session - -[bold]Core Agent Tools:[/bold] - [blue]Filesystem:[/blue] read_file, write_file, list_directory, grep_search, get_tree - [blue]Code & Python:[/blue] run_python, replace_text, code_analyze_file, run_linter - [blue]Web:[/blue] web_search, read_url - [blue]Media:[/blue] generate_image, speak_text - [blue]Knowledge:[/blue] kb_add, kb_search, remember_fact, recall_facts, clear_memory - -[bold]Integrations:[/bold] - /mcp connect [args] - Connect MCP server - /lsp start [path] - Start LSP server - /notify setup - Setup ntfy.sh remote control - -[bold]Configuration:[/bold] - /config ollama - Set Ollama server URL (default: http://localhost:11434) - /config comfy - Set ComfyUI server URL (default: http://127.0.0.1:8188) - /config piper_path - Set path to Piper binary - /config piper_model - Set path to Piper voice model -""" - console.print(help_text) - elif cmd == '/config': - if len(parts) == 1: - cfg = load_config() - config_display = f""" -[bold cyan]Current Configuration:[/bold cyan] - [blue]Ollama URL:[/blue] {cfg.get('ollama_url')} - [blue]ComfyUI URL:[/blue] {cfg.get('comfy_url')} - [blue]Image Model:[/blue] {cfg.get('image_model')} - [blue]Piper Path:[/blue] {cfg.get('piper', {}).get('path')} - [blue]Piper Model:[/blue] {cfg.get('piper', {}).get('model')} - [blue]Default Model:[/blue] {cfg.get('default_model')} -""" - console.print(config_display) - elif len(parts) > 2: - subcmd = parts[1].lower() - val = parts[2] - if subcmd == 'ollama': - update_config("ollama_url", val) - print_status(f"Ollama URL updated to: {val}") - self.client = OllamaClient(val) - elif subcmd == 'comfy': - update_config("comfy_url", val) - print_status(f"ComfyUI URL updated to: {val}") - elif subcmd == 'piper_path': - cfg = load_config() - cfg["piper"]["path"] = val - save_config(cfg) - print_status(f"Piper binary path updated to: {val}") - elif subcmd == 'piper_model': - cfg = load_config() - cfg["piper"]["model"] = val - save_config(cfg) - print_status(f"Piper model path updated to: {val}") - else: - print_error("Usage: /config ") - else: - print_error("Usage: /config ") - elif cmd == '/quit': - print_status("Goodbye!") - sys.exit(0) - elif cmd == '/clear': - self.reset_messages() - print_status("History cleared.") - elif cmd == '/models': - self.available_models = self.client.get_available_models() - print_status(f"Available models: {', '.join(self.available_models)}") - elif cmd == '/model': - if len(parts) > 1: - self.current_model = parts[1] - update_config("default_model", self.current_model) - print_status(f"Switched to model: {self.current_model}") - else: - print_status(f"Current model: {self.current_model}") - elif cmd == '/sessions': - sessions = list_sessions() - print_status(f"Recent sessions: {', '.join(sessions[:10])}") - elif cmd == '/save': - name = parts[1] if len(parts) > 1 else None - path = save_session(self.messages, name) - print_status(f"Session saved to {path}") - elif cmd == '/load': - if len(parts) > 1: - messages = load_session(parts[1]) - if messages: - self.messages = messages - print_status(f"Session '{parts[1]}' loaded.") - else: - print_error(f"Session '{parts[1]}' not found.") - else: - print_error("Usage: /load ") - elif cmd == '/mcp': - if len(parts) > 3 and parts[1] == 'connect': - name = parts[2] - command = parts[3] - args = parts[4:] if len(parts) > 4 else None - result = connect_mcp_server(name, command, args) - print_status(result) - # Refresh system prompt with new tools - self.system_prompt = self._build_system_prompt() - self.reset_messages() - else: - print_error("Usage: /mcp connect [args...]") - elif cmd == '/lsp': - if len(parts) > 2 and parts[1] == 'start': - lang = parts[2] - path = parts[3] if len(parts) > 3 else "." - result = start_lsp_server(lang, path) - print_status(result) - else: - print_error("Usage: /lsp start [path]") - elif cmd == '/notify': - if len(parts) > 2 and parts[1] == 'setup': - topic = parts[2] - self.config["ntfy"]["enabled"] = True - self.config["ntfy"]["topic"] = topic - save_config(self.config) - start_notify_thread(self.config, self.process_remote_message) - print_status(f"Notifications enabled for topic: {topic}") - else: - print_error("Usage: /notify setup ") - else: - print_error(f"Unknown command: {cmd}") - except Exception as e: - print_error(f"Command error: {e}") - - return True - - def process_remote_message(self, message: str) -> str: - """Handle messages coming from ntfy.sh""" - temp_messages = self.messages + [{"role": "user", "content": message}] - try: - response = "" - for chunk in self.client.chat(temp_messages, self.current_model, stream=False): - response += chunk - return response - except Exception as e: - return f"Error: {e}" - - def run(self): - console.print(f"[bold cyan]Ollama CLI v{VERSION}[/bold cyan]") - - # Initial model detection - try: - self.available_models = self.client.get_available_models() - if not self.available_models: - print_error("No models found. Make sure Ollama is running.") - else: - if self.current_model not in self.available_models: - self.current_model = self.available_models[0] - print_status(f"Using model: [bold green]{self.current_model}[/bold green]") - except Exception as e: - print_error(f"Could not connect to Ollama: {e}") - - self.reset_messages() - - # Start notification listener if enabled - if self.config.get("ntfy", {}).get("enabled"): - start_notify_thread(self.config, self.process_remote_message) - print_status(f"Notification listener active on topic: {self.config['ntfy']['topic']}") - - while True: - user_input = self.repl.get_input() - - if not user_input: - continue - - if self.handle_command(user_input): - continue - - self.messages.append({"role": "user", "content": user_input}) - self.process_chat_cycle() - - def process_chat_cycle(self): - """The main loop for chat and tool execution.""" - executed_tool_calls = set() - for iteration in range(self.max_tool_iterations): - full_response = "" - try: - print_status(f"Thinking ([dim]{self.current_model}[/dim])...") - with StreamingDisplay() as display: - for chunk in self.client.chat(self.messages, self.current_model): - display.update(chunk) - full_response += chunk - - self.messages.append({"role": "assistant", "content": full_response}) - - # Check for tool calls - tool_calls = self.parse_tool_calls(full_response) - if not tool_calls: - break - - # Execute tool calls - results = [] - for name, params in tool_calls: - # Prevent identical tool call loops across the entire cycle - call_key = f"{name}:{json.dumps(params, sort_keys=True)}" - if call_key in executed_tool_calls: - print_status(f"Skipping duplicate tool call: [dim]{name}[/dim]") - continue - executed_tool_calls.add(call_key) - - result = self.execute_tool(name, params) - # Show result to user - print_status(f"Result: [dim]{result}[/dim]") - results.append(f"Tool Result ({name}):\n{result}") - - if not results: - # If we had tool calls but they were all duplicates, we must stop - break - - # Add all results to conversation - self.messages.append({"role": "user", "content": "\n\n".join(results) + "\n\nTool execution complete. If the task is finished, provide a final response to the user. Do not repeat the tool call."}) - - # If we made it here, loop again to let the model process results - - except Exception as e: - print_error(str(e)) - logger.exception("Chat cycle error") - break - - def parse_tool_calls(self, text: str) -> List[tuple]: - calls = [] - - # 1. Try to find blocks (handles unclosed tags) - tc_blocks = re.findall(r'(.*?)(?:|$)', text, re.DOTALL) - if tc_blocks: - for block in tc_blocks: - if not block.strip(): continue - # Try to find tool_name tag (handles unclosed) - name_match = re.search(r'(.*?)(?:|$)', block, re.DOTALL) - name = name_match.group(1).strip() if name_match else None - - # Try to find parameters tag (handles unclosed) - params_match = re.search(r'(.*?)(?:|$)', block, re.DOTALL) - if name and params_match: - try: - params_str = params_match.group(1).strip() - # Auto-fix unclosed JSON if needed - if params_str.startswith('{') and not params_str.endswith('}'): - params_str += '}' - calls.append((name, json.loads(params_str))) - except: - # Last ditch: search for any JSON-like object in the parameters string - json_match = re.search(r'(\{.*\})', params_str, re.DOTALL) - if json_match: - try: calls.append((name, json.loads(json_match.group(1)))) - except: pass - - # 2. If no blocks, try searching for tool-specific tags - if not calls: - for tool_name in registry.tools.keys(): - # Handle or - attr_pattern = fr'<{tool_name}\s+(.*?)[\s/]*>' - attr_matches = re.findall(attr_pattern, text, re.DOTALL) - for attr_str in attr_matches: - attrs = {} - kv_pairs = re.findall(r'(\w+)\s*=\s*["\'](.*?)["\']', attr_str) - for k, v in kv_pairs: - attrs[k] = v - if attrs: - calls.append((tool_name, attrs)) - - # Handle {json} - block_pattern = fr'<{tool_name}>(.*?)(?:|$)' - block_matches = re.findall(block_pattern, text, re.DOTALL) - for b in block_matches: - json_match = re.search(r'(\{.*\})', b, re.DOTALL) - if json_match: - try: calls.append((tool_name, json.loads(json_match.group(1)))) - except: pass - - # 3. Final fallback: look for tool_name followed by JSON { ... } - if not calls: - for tool_name in registry.tools.keys(): - json_pattern = fr'{tool_name}\s*({{.*?}})' - json_matches = re.findall(json_pattern, text, re.DOTALL) - for json_str in json_matches: - try: - calls.append((tool_name, json.loads(json_str.strip()))) - except: - pass - - return calls - - def execute_tool(self, name: str, params: Dict) -> str: - """Execute a tool (standard or MCP).""" - # Try MCP tools first - if name in MCP_TOOLS: - mcp_tool = MCP_TOOLS[name] - print_status(f"Executing MCP tool [bold blue]{mcp_tool['name']}[/bold blue]...") - return call_mcp_tool(mcp_tool["server"], mcp_tool["name"], params) - - # Try standard tools - tool = registry.get_tool(name) - if tool: - print_status(f"Executing [bold blue]{name}[/bold blue]...") - try: - result = tool.execute(**params) - # Notification for successful file/system ops - if name == "write_file" and "Successfully" in str(result): - send_notification(self.config, "File Created", f"Path: {params.get('path')}") - return result - except Exception as e: - return f"Error executing tool: {e}" - - return f"Unknown tool: {name}" - -def main(): - try: - cli = OllamaCLI() - cli.run() - except KeyboardInterrupt: - print_status("Interrupted by user. Exiting.") - sys.exit(0) - except Exception as e: - print_error(f"Fatal error: {e}") - logger.exception("Fatal error") - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/build/lib/ollama_cli/tools/__init__.py b/build/lib/ollama_cli/tools/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/ollama_cli/tools/base.py b/build/lib/ollama_cli/tools/base.py deleted file mode 100644 index 207594d..0000000 --- a/build/lib/ollama_cli/tools/base.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Dict, Any, Callable, List, Optional -import json -import inspect - -class BaseTool: - def __init__(self, name: str, description: str, parameters: Dict[str, Any]): - self.name = name - self.description = description - self.parameters = parameters - - def execute(self, **kwargs) -> Any: - raise NotImplementedError - -class ToolRegistry: - def __init__(self): - self.tools: Dict[str, BaseTool] = {} - - def register(self, tool: BaseTool): - self.tools[tool.name] = tool - - def get_tool(self, name: str) -> Optional[BaseTool]: - return self.tools.get(name) - - def list_tools(self) -> List[Dict[str, Any]]: - return [ - { - "name": t.name, - "description": t.description, - "parameters": t.parameters - } - for t in self.tools.values() - ] - - def generate_system_prompt_snippet(self) -> str: - snippet = "Available tools:\n" - for t in self.tools.values(): - snippet += f"- {t.name}: {t.description}\n" - snippet += f" Parameters: {json.dumps(t.parameters)}\n" - return snippet - -# Singleton registry -registry = ToolRegistry() - -def tool(name: str, description: str, parameters: Dict[str, Any]): - """Decorator to register a function as a tool""" - def decorator(func: Callable): - class DecoratedTool(BaseTool): - def __init__(self): - super().__init__(name, description, parameters) - def execute(self, **kwargs): - return func(**kwargs) - - registry.register(DecoratedTool()) - return func - return decorator diff --git a/build/lib/ollama_cli/tools/code.py b/build/lib/ollama_cli/tools/code.py deleted file mode 100644 index e8a8798..0000000 --- a/build/lib/ollama_cli/tools/code.py +++ /dev/null @@ -1,96 +0,0 @@ -import os -import subprocess -from .base import tool - -@tool( - name="code_analyze_file", - description="Analyze code structure and suggest improvements", - parameters={ - "file_path": {"type": "string", "description": "Path to the file to analyze"} - } -) -def code_analyze_file(file_path: str) -> str: - try: - with open(os.path.expanduser(file_path), 'r') as f: - content = f.read() - - lines = content.split('\n') - functions = [l for l in lines if 'def ' in l] - classes = [l for l in lines if 'class ' in l] - - issues = [] - if len(lines) > 500: - issues.append(f"Large file ({len(lines)} lines) - consider splitting") - if len(functions) > 30: - issues.append(f"Many functions ({len(functions)}) - consider refactoring") - - return f"""File: {file_path} -Lines: {len(lines)} | Functions: {len(functions)} | Classes: {len(classes)} - -Issues: -{chr(10).join(f'- {i}' for i in issues) if issues else '- None detected'}""" - except Exception as e: - return f"Error: {str(e)}" - -@tool( - name="generate_readme", - description="Generate a README.md for the current project", - parameters={ - "path": {"type": "string", "description": "Project root path", "default": "."} - } -) -def generate_readme(path: str = ".") -> str: - try: - project_name = os.path.basename(os.path.abspath(path)) - readme_content = f"# {project_name}\n\nGenerated by Ollama CLI." - readme_path = os.path.join(path, "README.md") - with open(readme_path, 'w') as f: - f.write(readme_content) - return f"Generated README.md at {readme_path}" - except Exception as e: - return f"Error: {str(e)}" - -@tool( - name="replace_text", - description="Surgically replace text in a file", - parameters={ - "path": {"type": "string", "description": "Path to the file"}, - "old_text": {"type": "string", "description": "The exact text to find"}, - "new_text": {"type": "string", "description": "The text to replace it with"} - } -) -def replace_text(path: str, old_text: str, new_text: str) -> str: - try: - expanded_path = os.path.expanduser(path) - with open(expanded_path, 'r') as f: - content = f.read() - - if old_text not in content: - return f"Error: The text to replace was not found in {path}" - - new_content = content.replace(old_text, new_text, 1) # Replace first occurrence - with open(expanded_path, 'w') as f: - f.write(new_content) - return f"Successfully updated {path}" - except Exception as e: - return f"Error: {str(e)}" - -@tool( - name="run_linter", - description="Run a linter on a file", - parameters={ - "file_path": {"type": "string", "description": "Path to the file"}, - "linter": {"type": "string", "description": "Linter to use (pylint|eslint|auto)", "default": "auto"} - } -) -def run_linter(file_path: str, linter: str = "auto") -> str: - try: - file_path = os.path.expanduser(file_path) - if linter == "auto": - ext = os.path.splitext(file_path)[1] - linter = "pylint" if ext == ".py" else "eslint" - - result = subprocess.run(f"{linter} {file_path}", shell=True, capture_output=True, text=True) - return result.stdout + result.stderr - except Exception as e: - return f"Error: {str(e)}" diff --git a/build/lib/ollama_cli/tools/execution.py b/build/lib/ollama_cli/tools/execution.py deleted file mode 100644 index d7f65e6..0000000 --- a/build/lib/ollama_cli/tools/execution.py +++ /dev/null @@ -1,36 +0,0 @@ -import subprocess -import os -import sys -from .base import tool - -@tool( - name="run_python", - description="Execute a Python script or snippet", - parameters={ - "code": {"type": "string", "description": "Python code to execute"}, - "args": {"type": "string", "description": "Optional arguments", "default": ""} - } -) -def run_python(code: str, args: str = "") -> str: - try: - # Create a temp file - temp_file = ".ollama-cli-exec.py" - with open(temp_file, "w") as f: - f.write(code) - - cmd = f"{sys.executable} {temp_file} {args}" - result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30) - - output = result.stdout + result.stderr - - # Cleanup - if os.path.exists(temp_file): - os.remove(temp_file) - - if result.returncode == 0: - return f"Success:\n{output}" if output else "Success (no output)" - else: - return f"Error (Exit {result.returncode}):\n{output}" - - except Exception as e: - return f"Execution Error: {str(e)}" \ No newline at end of file diff --git a/build/lib/ollama_cli/tools/filesystem.py b/build/lib/ollama_cli/tools/filesystem.py deleted file mode 100644 index ebb02a9..0000000 --- a/build/lib/ollama_cli/tools/filesystem.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -import subprocess -from .base import tool - -@tool( - name="read_file", - description="Read the contents of a file", - parameters={ - "path": {"type": "string", "description": "Path to the file"} - } -) -def read_file(path: str) -> str: - try: - with open(os.path.expanduser(path), 'r') as f: - return f.read() - except Exception as e: - return f"Error reading file: {str(e)}" - -@tool( - name="write_file", - description="Write content to a file", - parameters={ - "path": {"type": "string", "description": "Path to the file"}, - "content": {"type": "string", "description": "Content to write"} - } -) -def write_file(path: str, content: str) -> str: - try: - expanded_path = os.path.expanduser(path) - os.makedirs(os.path.dirname(expanded_path), exist_ok=True) - with open(expanded_path, 'w') as f: - f.write(content) - return f"Successfully written to {path}" - except Exception as e: - return f"Error writing file: {str(e)}" - -@tool( - name="list_directory", - description="List contents of a directory", - parameters={ - "path": {"type": "string", "description": "Path to the directory", "default": "."} - } -) -def list_directory(path: str = ".") -> str: - try: - expanded = os.path.expanduser(path) - files = os.listdir(expanded) - return "\n".join(files) if files else "Directory is empty" - except Exception as e: - return f"Error listing directory: {str(e)}" - -@tool( - name="grep_search", - description="Search for a regular expression pattern in file contents", - parameters={ - "pattern": {"type": "string", "description": "The regex pattern to search for"}, - "path": {"type": "string", "description": "Directory to search in", "default": "."}, - "include": {"type": "string", "description": "Glob pattern for files to include", "default": "*"} - } -) -def grep_search(pattern: str, path: str = ".", include: str = "*") -> str: - try: - cmd = f'grep -rE --include="{include}" "{pattern}" {path}' - result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=20) - return result.stdout if result.stdout else "No matches found." - except Exception as e: - return f"Error: {str(e)}" - -@tool( - name="get_tree", - description="Show the directory structure as a tree", - parameters={ - "path": {"type": "string", "description": "Root path", "default": "."}, - "depth": {"type": "integer", "description": "Max depth", "default": 2} - } -) -def get_tree(path: str = ".", depth: int = 2) -> str: - try: - cmd = f"find {path} -maxdepth {depth} -not -path '*/.*'" - result = subprocess.run(cmd, shell=True, capture_output=True, text=True) - return result.stdout - except Exception as e: - return f"Error: {str(e)}" diff --git a/build/lib/ollama_cli/tools/git.py b/build/lib/ollama_cli/tools/git.py deleted file mode 100644 index 6b2a081..0000000 --- a/build/lib/ollama_cli/tools/git.py +++ /dev/null @@ -1,52 +0,0 @@ -import subprocess -import requests -from typing import Optional -from .base import tool -from ..core.config import load_config - -@tool( - name="git_operation", - description="Perform git operations (status, diff, commit, push, pull, list)", - parameters={ - "operation": {"type": "string", "description": "Operation to perform (status|diff|commit|push|pull|list)"}, - "message": {"type": "string", "description": "Commit message", "default": "Auto commit"} - } -) -def git_operation(operation: str, message: str = "Auto commit") -> str: - config = load_config() - git_config = config.get("git", {}) - gitea_config = config.get("gitea", {}) - - try: - if operation == "status": - result = subprocess.run("git status", shell=True, capture_output=True, text=True) - elif operation == "diff": - result = subprocess.run("git diff", shell=True, capture_output=True, text=True) - elif operation == "commit": - result = subprocess.run(f'git add -A && git commit -m "{message}"', - shell=True, capture_output=True, text=True) - elif operation == "push": - result = subprocess.run("git push", shell=True, capture_output=True, text=True) - elif operation == "pull": - result = subprocess.run("git pull", shell=True, capture_output=True, text=True) - elif operation == "list": - if gitea_config.get("url") and gitea_config.get("token"): - gitea_url = gitea_config["url"].rstrip("/") - token = gitea_config["token"] - response = requests.get( - f"{gitea_url}/api/v1/user/repos", - headers={"Authorization": f"token {token}"}, - timeout=10 - ) - if response.status_code == 200: - repos = response.json() - return "\n".join([f"- {r['full_name']} ({r['clone_url']})" for r in repos]) - return f"Gitea API error: {response.status_code}" - result = subprocess.run("find . -maxdepth 2 -name '.git' -type d | sed 's|/.git||'", - shell=True, capture_output=True, text=True) - else: - return f"Unknown git operation: {operation}" - - return result.stdout + result.stderr - except Exception as e: - return f"Git error: {str(e)}" diff --git a/build/lib/ollama_cli/tools/knowledge.py b/build/lib/ollama_cli/tools/knowledge.py deleted file mode 100644 index 9dc28af..0000000 --- a/build/lib/ollama_cli/tools/knowledge.py +++ /dev/null @@ -1,41 +0,0 @@ -import json -import os -from .base import tool - -KB_FILE = os.path.expanduser("~/.ollama-cli-kb.json") - -def load_kb(): - if os.path.exists(KB_FILE): - with open(KB_FILE, 'r') as f: - return json.load(f) - return {} - -def save_kb(kb): - with open(KB_FILE, 'w') as f: - json.dump(kb, f, indent=2) - -@tool( - name="kb_add", - description="Add content to the knowledge base", - parameters={ - "key": {"type": "string", "description": "Search key"}, - "content": {"type": "string", "description": "Information content"} - } -) -def kb_add(key: str, content: str) -> str: - kb = load_kb() - kb[key] = content - save_kb(kb) - return f"Added '{key}' to knowledge base" - -@tool( - name="kb_search", - description="Search the knowledge base", - parameters={ - "query": {"type": "string", "description": "Search query"} - } -) -def kb_search(query: str) -> str: - kb = load_kb() - results = [f"{k}: {v[:100]}..." for k, v in kb.items() if query.lower() in k.lower() or query.lower() in v.lower()] - return "\n".join(results) if results else "No matches found" diff --git a/build/lib/ollama_cli/tools/media.py b/build/lib/ollama_cli/tools/media.py deleted file mode 100644 index 2611f11..0000000 --- a/build/lib/ollama_cli/tools/media.py +++ /dev/null @@ -1,183 +0,0 @@ -import subprocess -import os -import sys -import requests -import json -import random -from .base import tool -from ..core.config import load_config - -@tool( - name="generate_image", - description="Generate an image using the local Stable Diffusion workflow (ComfyUI)", - parameters={ - "prompt": {"type": "string", "description": "The image description"} - } -) -def generate_image(prompt: str) -> str: - """Invokes the ComfyUI API directly with a predefined workflow""" - config = load_config() - comfy_url = config.get("comfy_url", "http://127.0.0.1:8188").rstrip("/") - image_model = config.get("image_model", "dreamshaper_8_pruned.safetensors") - - # Determine dimensions (SDXL prefers 1024x1024) - width, height = (512, 512) - if "xl" in image_model.lower(): - width, height = (1024, 1024) - - try: - # Check if ComfyUI is running - try: - requests.get(comfy_url, timeout=2) - except: - return f"Error: ComfyUI server ({comfy_url}) appears to be down. Please start it first or configure the URL with '/config comfy '." - - # Define the workflow directly - workflow = { - "1": { - "inputs": {"ckpt_name": image_model}, - "class_type": "CheckpointLoaderSimple" - }, - "2": { - "inputs": { - "text": f"masterpiece, best quality, highly detailed, {prompt}", - "clip": ["1", 1] - }, - "class_type": "CLIPTextEncode" - }, - "3": { - "inputs": { - "text": "blurry, low quality, distorted, watermark, nsfw", - "clip": ["1", 1] - }, - "class_type": "CLIPTextEncode" - }, - "4": { - "inputs": {"width": width, "height": height, "batch_size": 1}, - "class_type": "EmptyLatentImage" - }, - "5": { - "inputs": { - "seed": random.randint(1, 1000000000), "steps": 20, "cfg": 7.0, - "sampler_name": "dpmpp_2m", "scheduler": "karras", - "denoise": 1.0, "model": ["1", 0], - "positive": ["2", 0], "negative": ["3", 0], - "latent_image": ["4", 0] - }, - "class_type": "KSampler" - }, - "6": { - "inputs": {"samples": ["5", 0], "vae": ["1", 2]}, - "class_type": "VAEDecode" - }, - "7": { - "inputs": { - "filename_prefix": "ollama_cli_", - "images": ["6", 0] - }, - "class_type": "SaveImage" - } - } - - payload = {"prompt": workflow} - response = requests.post(f"{comfy_url}/prompt", json=payload, timeout=10) - - if response.status_code == 200: - data = response.json() - prompt_id = data.get('prompt_id', 'unknown') - return f"Image generation queued successfully on {comfy_url}.\nPrompt ID: {prompt_id}\nCheck your ComfyUI output folder for 'ollama_cli_' prefixed files." - else: - return f"Error form ComfyUI: {response.status_code} - {response.text}" - - except Exception as e: - return f"Error generating image: {str(e)}" - -@tool( - name="speak_text", - description="Convert text to speech using Piper TTS (high quality)", - parameters={ - "text": {"type": "string", "description": "Text to speak"}, - "output_file": {"type": "string", "description": "Output filename", "default": "output.wav"} - } -) -def speak_text(text: str, output_file: str = "output.wav") -> str: - """Uses the best available TTS engine (Kokoro > Piper > System)""" - config = load_config() - - # 1. Try Kokoro (Ultra-Realistic) - kokoro_python = os.path.expanduser(config.get("kokoro_path", "~/.ollama-cli/kokoro/venv/bin/python")) - if os.path.exists(kokoro_python): - try: - # Inline script to run in Kokoro venv - kokoro_script = f""" -import sys -import soundfile as sf -import os -try: - from kokoro import KPipeline - import torch -except ImportError: - print("Dependencies missing") - sys.exit(1) - -text = sys.argv[1] -output_file = sys.argv[2] - -# Initialize pipeline -pipeline = KPipeline(lang_code='a') -generator = pipeline(text, voice='af_bella', speed=1) - -all_audio = [] -for gs, ps, audio in generator: - if audio is not None: - all_audio.append(audio) - -if all_audio: - import numpy as np - full_audio = np.concatenate(all_audio) - sf.write(output_file, full_audio, 24000) - print("Success") -else: - print("No audio generated") - sys.exit(1) -""" - # Run in venv - subprocess.run([kokoro_python, "-c", kokoro_script, text, output_file], - check=True, capture_output=True) - - if sys.platform == "darwin": - subprocess.run(["afplay", output_file]) - return f"Audio saved to {output_file} and played (using Ultra-Realistic Kokoro TTS)" - except Exception as e: - # Continue to fallback - pass - - # 2. Try Piper (High-Quality) - piper_config = config.get("piper", {}) - piper_bin = os.path.expanduser(piper_config.get("path", "~/.ollama-cli/piper/piper")) - model_path = os.path.expanduser(piper_config.get("model", "~/.ollama-cli/piper/voice.onnx")) - - if os.path.exists(piper_bin) and os.path.exists(model_path): - try: - # Piper reads from stdin and writes to file - cmd = f'echo "{text}" | {piper_bin} --model {model_path} --output_file {output_file}' - subprocess.run(cmd, shell=True, check=True, capture_output=True) - - # Auto-play on macOS - if sys.platform == "darwin": - subprocess.run(["afplay", output_file]) - - return f"Audio saved to {output_file} and played (using high-quality Piper TTS)" - except Exception as e: - pass - - # 3. Fallback to system 'say' command on macOS - if sys.platform == "darwin": - try: - subprocess.run(["say", "-o", output_file, text], check=True) - subprocess.run(["afplay", output_file]) - return f"Audio saved to {output_file} and played (using system 'say' fallback)" - except Exception as e: - return f"Error using 'say': {e}" - - return "No compatible TTS engine found. Please run the installer." diff --git a/build/lib/ollama_cli/tools/memory.py b/build/lib/ollama_cli/tools/memory.py deleted file mode 100644 index 5e4a2c2..0000000 --- a/build/lib/ollama_cli/tools/memory.py +++ /dev/null @@ -1,59 +0,0 @@ -import json -import os -from .base import tool - -PROJECT_MEMORY_FILE = ".ollama-cli/memory.json" - -def load_project_memory(): - if os.path.exists(PROJECT_MEMORY_FILE): - try: - with open(PROJECT_MEMORY_FILE, 'r') as f: - return json.load(f) - except: - pass - return {} - -def save_project_memory(memory): - os.makedirs(os.path.dirname(PROJECT_MEMORY_FILE), exist_ok=True) - with open(PROJECT_MEMORY_FILE, 'w') as f: - json.dump(memory, f, indent=2) - -@tool( - name="remember_fact", - description="Store a key piece of information about the project", - parameters={ - "fact": {"type": "string", "description": "The fact to remember (e.g., 'API key is in .env', 'Main entry point is src/index.ts')"} - } -) -def remember_fact(fact: str) -> str: - memory = load_project_memory() - facts = memory.get("facts", []) - if fact not in facts: - facts.append(fact) - memory["facts"] = facts - save_project_memory(memory) - return f"Remembered: {fact}" - return "Fact already known." - -@tool( - name="recall_facts", - description="Recall all stored facts about the project", - parameters={} -) -def recall_facts() -> str: - memory = load_project_memory() - facts = memory.get("facts", []) - if not facts: - return "No project facts stored." - return "Project Facts:\n" + "\n".join(f"- {f}" for f in facts) - -@tool( - name="clear_memory", - description="Clear project memory", - parameters={} -) -def clear_memory() -> str: - if os.path.exists(PROJECT_MEMORY_FILE): - os.remove(PROJECT_MEMORY_FILE) - return "Project memory cleared." - return "Memory was already empty." \ No newline at end of file diff --git a/build/lib/ollama_cli/tools/system.py b/build/lib/ollama_cli/tools/system.py deleted file mode 100644 index 329fcc9..0000000 --- a/build/lib/ollama_cli/tools/system.py +++ /dev/null @@ -1,46 +0,0 @@ -import subprocess -import os -from .base import tool - -@tool( - name="execute_bash", - description="Execute a bash command and return its output", - parameters={ - "command": {"type": "string", "description": "The command to execute"} - } -) -def execute_bash(command: str) -> str: - # Basic safety check - dangerous = ['rm -rf /', 'dd if=', 'mkfs', ':(){:|:&};:'] - if any(d in command for d in dangerous): - return f"⚠️ DANGEROUS COMMAND BLOCKED: {command}" - - try: - result = subprocess.run( - command, - shell=True, - capture_output=True, - text=True, - cwd=os.getcwd(), - timeout=60 - ) - output = result.stdout + result.stderr - return f"Exit code: {result.returncode}\n{output}" if output else f"Exit code: {result.returncode} (no output)" - except subprocess.TimeoutExpired: - return "Error: Command timed out after 60 seconds" - except Exception as e: - return f"Error: {str(e)}" - -@tool( - name="get_env_info", - description="Get information about the current system environment", - parameters={} -) -def get_env_info() -> str: - import sys - import platform - return f"""OS: {platform.system()} {platform.release()} -Python: {sys.version} -Shell: {os.environ.get('SHELL', 'unknown')} -User: {os.environ.get('USER', 'unknown')} -CWD: {os.getcwd()}""" diff --git a/build/lib/ollama_cli/tools/web.py b/build/lib/ollama_cli/tools/web.py deleted file mode 100644 index bef085e..0000000 --- a/build/lib/ollama_cli/tools/web.py +++ /dev/null @@ -1,67 +0,0 @@ -import requests -import urllib.parse -from .base import tool - -@tool( - name="web_search", - description="Search the web for information", - parameters={ - "query": {"type": "string", "description": "The search query"} - } -) -def web_search(query: str) -> str: - """Enhanced web search with DuckDuckGo""" - try: - encoded = urllib.parse.quote(query) - # Use DuckDuckGo Instant Answer API - api_url = f"https://api.duckduckgo.com/?q={encoded}&format=json" - response = requests.get(api_url, timeout=10) - data = response.json() - - results = [] - if data.get("AbstractText"): - results.append(f"Summary: {data['AbstractText']}") - - for topic in data.get("RelatedTopics", [])[:5]: - if isinstance(topic, dict) and "Text" in topic: - results.append(f"- {topic['Text']}") - - return "\n".join(results) if results else "No direct results found. Try a different query." - except Exception as e: - return f"Search error: {str(e)}" - -@tool( - name="read_url", - description="Read and extract text content from a URL", - parameters={ - "url": {"type": "string", "description": "The URL to read"} - } -) -def read_url(url: str) -> str: - try: - response = requests.get(url, timeout=10) - response.raise_for_status() - - try: - from bs4 import BeautifulSoup - soup = BeautifulSoup(response.text, 'html.parser') - - # Remove scripts and styles - for script in soup(["script", "style"]): - script.decompose() - - text = soup.get_text() - - # Clean up whitespace - lines = (line.strip() for line in text.splitlines()) - chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) - text = '\n'.join(chunk for chunk in chunks if chunk) - - return text[:10000] + ("\n... (truncated)" if len(text) > 10000 else "") - - except ImportError: - # Fallback if bs4 is missing (though we checked) - return response.text[:10000] - - except Exception as e: - return f"Error reading URL: {str(e)}" diff --git a/build/lib/ollama_cli/ui/__init__.py b/build/lib/ollama_cli/ui/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/ollama_cli/ui/formatter.py b/build/lib/ollama_cli/ui/formatter.py deleted file mode 100644 index b9cd1f8..0000000 --- a/build/lib/ollama_cli/ui/formatter.py +++ /dev/null @@ -1,40 +0,0 @@ -from rich.console import Console -from rich.markdown import Markdown -from rich.panel import Panel -from rich.live import Live -from rich.text import Text -from rich.syntax import Syntax -from typing import Optional - -console = Console() - -def print_markdown(text: str): - console.print(Markdown(text)) - -def print_panel(text: str, title: Optional[str] = None, style: str = "blue"): - console.print(Panel(text, title=title, border_style=style)) - -def print_error(text: str): - console.print(f"[bold red]Error:[/bold red] {text}") - -def print_status(text: str): - console.print(f"[bold blue]>>>[/bold blue] {text}") - -class StreamingDisplay: - def __init__(self, title: Optional[str] = None): - self.text = "" - self.live = Live(Text(self.text), refresh_per_second=10) - self.title = title - - def __enter__(self): - self.live.start() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.live.stop() - - def update(self, chunk: str): - self.text += chunk - # Try to parse markdown if it looks complete-ish, otherwise just show text - # For performance, we might just show raw text during streaming - self.live.update(Markdown(self.text) if "```" in self.text else Text(self.text)) diff --git a/build/lib/ollama_cli/ui/repl.py b/build/lib/ollama_cli/ui/repl.py deleted file mode 100644 index 9534678..0000000 --- a/build/lib/ollama_cli/ui/repl.py +++ /dev/null @@ -1,25 +0,0 @@ -from prompt_toolkit import PromptSession -from prompt_toolkit.history import FileHistory -from prompt_toolkit.styles import Style -from prompt_toolkit.formatted_text import HTML -import os - -class REPL: - def __init__(self): - history_file = os.path.expanduser("~/.ollama-cli-history") - self.session = PromptSession(history=FileHistory(history_file)) - self.style = Style.from_dict({ - 'prompt': 'ansicyan bold', - 'command': 'ansigreen', - }) - - def get_input(self) -> str: - try: - return self.session.prompt( - HTML('> '), - style=self.style - ).strip() - except KeyboardInterrupt: - return "" - except EOFError: - return "/quit" diff --git a/src/ollama_cli.egg-info/PKG-INFO b/src/ollama_cli.egg-info/PKG-INFO deleted file mode 100644 index fe1f24a..0000000 --- a/src/ollama_cli.egg-info/PKG-INFO +++ /dev/null @@ -1,9 +0,0 @@ -Metadata-Version: 2.4 -Name: ollama-cli -Version: 3.0.0 -Summary: A powerful, commercially-viable CLI for local Ollama LLMs -Author-email: Gemini CLI -Requires-Python: >=3.8 -Requires-Dist: requests -Requires-Dist: rich -Requires-Dist: prompt_toolkit diff --git a/src/ollama_cli.egg-info/SOURCES.txt b/src/ollama_cli.egg-info/SOURCES.txt deleted file mode 100644 index a434739..0000000 --- a/src/ollama_cli.egg-info/SOURCES.txt +++ /dev/null @@ -1,33 +0,0 @@ -README.md -pyproject.toml -src/ollama_cli/__init__.py -src/ollama_cli/main.py -src/ollama_cli.egg-info/PKG-INFO -src/ollama_cli.egg-info/SOURCES.txt -src/ollama_cli.egg-info/dependency_links.txt -src/ollama_cli.egg-info/entry_points.txt -src/ollama_cli.egg-info/requires.txt -src/ollama_cli.egg-info/top_level.txt -src/ollama_cli/agents/__init__.py -src/ollama_cli/core/__init__.py -src/ollama_cli/core/config.py -src/ollama_cli/core/ollama.py -src/ollama_cli/core/sessions.py -src/ollama_cli/integrations/__init__.py -src/ollama_cli/integrations/lsp.py -src/ollama_cli/integrations/mcp.py -src/ollama_cli/integrations/notify.py -src/ollama_cli/tools/__init__.py -src/ollama_cli/tools/base.py -src/ollama_cli/tools/code.py -src/ollama_cli/tools/execution.py -src/ollama_cli/tools/filesystem.py -src/ollama_cli/tools/git.py -src/ollama_cli/tools/knowledge.py -src/ollama_cli/tools/media.py -src/ollama_cli/tools/memory.py -src/ollama_cli/tools/system.py -src/ollama_cli/tools/web.py -src/ollama_cli/ui/__init__.py -src/ollama_cli/ui/formatter.py -src/ollama_cli/ui/repl.py \ No newline at end of file diff --git a/src/ollama_cli.egg-info/dependency_links.txt b/src/ollama_cli.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/ollama_cli.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/ollama_cli.egg-info/entry_points.txt b/src/ollama_cli.egg-info/entry_points.txt deleted file mode 100644 index ce90b47..0000000 --- a/src/ollama_cli.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -ollama-cli = ollama_cli.main:main diff --git a/src/ollama_cli.egg-info/requires.txt b/src/ollama_cli.egg-info/requires.txt deleted file mode 100644 index 6ce68bf..0000000 --- a/src/ollama_cli.egg-info/requires.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests -rich -prompt_toolkit diff --git a/src/ollama_cli.egg-info/top_level.txt b/src/ollama_cli.egg-info/top_level.txt deleted file mode 100644 index d8bb51b..0000000 --- a/src/ollama_cli.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -ollama_cli