diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 5008ddf..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..086070d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+__pycache__/
+*.py[cod]
+*$py.class
+.pytest_cache/
+.ollama-cli-history
+.ollama-cli-kb.json
+.ollama-cli/
+*.egg-info/
+dist/
+build/
+.DS_Store
diff --git a/build/lib/ollama_cli/__init__.py b/build/lib/ollama_cli/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/build/lib/ollama_cli/agents/__init__.py b/build/lib/ollama_cli/agents/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/build/lib/ollama_cli/core/__init__.py b/build/lib/ollama_cli/core/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/build/lib/ollama_cli/core/config.py b/build/lib/ollama_cli/core/config.py
deleted file mode 100644
index 0c8987a..0000000
--- a/build/lib/ollama_cli/core/config.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import os
-import json
-
-CONFIG_FILE = os.path.expanduser("~/.ollama-cli-config.json")
-
-DEFAULT_CONFIG = {
- "excluded_models": [],
- "git": {
- "remote_url": "",
- "username": "",
- "password": ""
- },
- "gitea": {
- "url": "",
- "token": ""
- },
- "auto_save_sessions": True,
- "mcp_servers": {},
- "ollama_url": "http://localhost:11434",
- "comfy_url": "http://localhost:9000",
- "piper": {
- "path": "~/.ollama-cli/piper/piper",
- "model": "~/.ollama-cli/piper/voice.onnx"
- },
- "ntfy": {
- "enabled": False,
- "topic": "",
- "server": "https://ntfy.sh"
- },
- "default_model": "llama3.2"
-}
-
-def load_config():
- """Load config file"""
- if os.path.exists(CONFIG_FILE):
- try:
- with open(CONFIG_FILE, 'r') as f:
- config = json.load(f)
- # Merge with defaults to ensure all keys exist
- for k, v in DEFAULT_CONFIG.items():
- if k not in config:
- config[k] = v
- return config
- except Exception:
- pass
- return DEFAULT_CONFIG.copy()
-
-def save_config(config):
- """Save config file"""
- with open(CONFIG_FILE, 'w') as f:
- json.dump(config, f, indent=2)
-
-def update_config(key, value):
- """Update a specific config key"""
- config = load_config()
- config[key] = value
- save_config(config)
diff --git a/build/lib/ollama_cli/core/ollama.py b/build/lib/ollama_cli/core/ollama.py
deleted file mode 100644
index d415281..0000000
--- a/build/lib/ollama_cli/core/ollama.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import json
-import requests
-import subprocess
-import sys
-from typing import List, Dict, Optional, Generator
-
-class OllamaClient:
- def __init__(self, base_url: str = "http://localhost:11434"):
- # Strip trailing slashes and common API paths if user provided them in config
- self.base_url = base_url.rstrip("/")
- if self.base_url.endswith("/api/chat"):
- self.base_url = self.base_url[:-9]
- elif self.base_url.endswith("/api"):
- self.base_url = self.base_url[:-4]
-
- self.chat_url = f"{self.base_url}/api/chat"
- self.list_url = f"{self.base_url}/api/tags"
-
- def get_available_models(self) -> 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}>(.*?)(?:{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