diff --git a/tools/_lib/__init__.py b/tools/_lib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tools/_lib/lsp/__init__.py b/tools/_lib/lsp/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tools/_lib/lsp/adapters/__init__.py b/tools/_lib/lsp/adapters/__init__.py deleted file mode 100644 index bf6d72b..0000000 --- a/tools/_lib/lsp/adapters/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Adapter registry: detect language from file path, return appropriate adapter.""" - -import os -from .gopls import Gopls -from .clangd import Clangd -from .intelephense import Intelephense - -_ADAPTERS = [Gopls, Clangd, Intelephense] - -_EXT_MAP: dict[str, type] = {} -for _a in _ADAPTERS: - for _ext in _a.extensions: - _EXT_MAP[_ext] = _a - - -def detect(filepath: str): - """Return an adapter class for the given file, or None.""" - ext = os.path.splitext(filepath)[1] - cls = _EXT_MAP.get(ext) - if cls: - return cls() - return None diff --git a/tools/_lib/lsp/adapters/clangd.py b/tools/_lib/lsp/adapters/clangd.py deleted file mode 100644 index 9c7df7d..0000000 --- a/tools/_lib/lsp/adapters/clangd.py +++ /dev/null @@ -1,19 +0,0 @@ -"""clangd adapter.""" - -from ..workspace import find_root - - -class Clangd: - name = "clangd" - extensions = [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx"] - root_markers = ["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"] - cmd = ["clangd", "--background-index"] - - def initialization_options(self): - return {} - - def workspace_root(self, filepath: str, fallback: str | None = None) -> str: - return find_root(filepath, self.root_markers, fallback) - - def prepare_open(self, filepath: str) -> dict | None: - return None diff --git a/tools/_lib/lsp/adapters/gopls.py b/tools/_lib/lsp/adapters/gopls.py deleted file mode 100644 index c7ed160..0000000 --- a/tools/_lib/lsp/adapters/gopls.py +++ /dev/null @@ -1,24 +0,0 @@ -"""gopls adapter.""" - -from ..workspace import find_root - - -class Gopls: - name = "gopls" - extensions = [".go"] - root_markers = ["go.work", "go.mod"] - cmd = ["gopls", "serve"] - - def initialization_options(self): - return { - "usePlaceholders": True, - "staticcheck": True, - "gofumpt": True, - } - - def workspace_root(self, filepath: str, fallback: str | None = None) -> str: - return find_root(filepath, self.root_markers, fallback) - - def prepare_open(self, filepath: str) -> dict | None: - """Return extra didOpen params if needed. gopls needs didOpen before queries.""" - return None # handled generically by the bridge diff --git a/tools/_lib/lsp/adapters/intelephense.py b/tools/_lib/lsp/adapters/intelephense.py deleted file mode 100644 index 34b45e2..0000000 --- a/tools/_lib/lsp/adapters/intelephense.py +++ /dev/null @@ -1,19 +0,0 @@ -"""intelephense adapter (PHP).""" - -from ..workspace import find_root - - -class Intelephense: - name = "intelephense" - extensions = [".php"] - root_markers = ["composer.json", "composer.lock", ".git"] - cmd = ["intelephense", "--stdio"] - - def initialization_options(self): - return {} - - def workspace_root(self, filepath: str, fallback: str | None = None) -> str: - return find_root(filepath, self.root_markers, fallback) - - def prepare_open(self, filepath: str) -> dict | None: - return None diff --git a/tools/_lib/lsp/bridge.py b/tools/_lib/lsp/bridge.py deleted file mode 100644 index 7d2a3ed..0000000 --- a/tools/_lib/lsp/bridge.py +++ /dev/null @@ -1,418 +0,0 @@ -"""LSP bridge daemon: manages LSP server processes, accepts requests over a Unix socket.""" - -import asyncio -import json -import os -import signal -import sys -import time -from pathlib import Path -from urllib.parse import quote as url_quote - -# Ensure _lib is importable when running as a standalone daemon -_tools_dir = str(Path(__file__).resolve().parent.parent.parent) -if _tools_dir not in sys.path: - sys.path.insert(0, _tools_dir) - -SOCK_PATH = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "ollie-lsp.sock") -PID_PATH = SOCK_PATH + ".pid" -IDLE_TIMEOUT = 300 # 5 minutes - - -def file_uri(path: str) -> str: - return "file://" + os.path.abspath(path) - - -def uri_to_path(uri: str) -> str: - if uri.startswith("file://"): - return uri[7:] - return uri - - -class LspServer: - """A running LSP server process.""" - - def __init__(self, adapter, root: str): - self.adapter = adapter - self.root = root - self.proc: asyncio.subprocess.Process | None = None - self.req_id = 0 - self.pending: dict[int, asyncio.Future] = {} - self.initialized = False - self.open_files: set[str] = set() - self._read_task: asyncio.Task | None = None - self._buf = b"" - - async def start(self): - self.proc = await asyncio.create_subprocess_exec( - *self.adapter.cmd, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL, - ) - self._read_task = asyncio.create_task(self._read_loop()) - await self._initialize() - - async def stop(self): - if self.proc and self.proc.returncode is None: - await self._request("shutdown", {}) - self._notify("exit", None) - try: - await asyncio.wait_for(self.proc.wait(), timeout=5) - except asyncio.TimeoutError: - self.proc.kill() - if self._read_task: - self._read_task.cancel() - - async def _initialize(self): - result = await self._request("initialize", { - "processId": os.getpid(), - "rootUri": file_uri(self.root), - "rootPath": self.root, - "capabilities": { - "textDocument": { - "definition": {"linkSupport": True}, - "references": {}, - "hover": {"contentFormat": ["plaintext", "markdown"]}, - "rename": {"prepareSupport": True}, - "documentSymbol": {"hierarchicalDocumentSymbolSupport": True}, - "completion": {"completionItem": {"snippetSupport": False}}, - "publishDiagnostics": {}, - }, - "workspace": { - "workspaceFolders": True, - "symbol": {}, - }, - }, - "initializationOptions": self.adapter.initialization_options(), - "workspaceFolders": [{"uri": file_uri(self.root), "name": os.path.basename(self.root)}], - }) - self._notify("initialized", {}) - self.initialized = True - return result - - async def ensure_open(self, filepath: str): - if filepath in self.open_files: - return - try: - with open(filepath, "r") as f: - text = f.read() - except (OSError, UnicodeDecodeError): - text = "" - ext = os.path.splitext(filepath)[1] - lang_map = { - ".go": "go", ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", - ".cxx": "cpp", ".hpp": "cpp", ".hxx": "cpp", ".php": "php", - } - self._notify("textDocument/didOpen", { - "textDocument": { - "uri": file_uri(filepath), - "languageId": lang_map.get(ext, "plaintext"), - "version": 1, - "text": text, - } - }) - self.open_files.add(filepath) - # Give the server a moment to index the file - await asyncio.sleep(0.1) - - async def definition(self, filepath: str, line: int, col: int): - await self.ensure_open(filepath) - return await self._request("textDocument/definition", { - "textDocument": {"uri": file_uri(filepath)}, - "position": {"line": line, "character": col}, - }) - - async def references(self, filepath: str, line: int, col: int): - await self.ensure_open(filepath) - return await self._request("textDocument/references", { - "textDocument": {"uri": file_uri(filepath)}, - "position": {"line": line, "character": col}, - "context": {"includeDeclaration": True}, - }) - - async def hover(self, filepath: str, line: int, col: int): - await self.ensure_open(filepath) - return await self._request("textDocument/hover", { - "textDocument": {"uri": file_uri(filepath)}, - "position": {"line": line, "character": col}, - }) - - async def completion(self, filepath: str, line: int, col: int): - await self.ensure_open(filepath) - return await self._request("textDocument/completion", { - "textDocument": {"uri": file_uri(filepath)}, - "position": {"line": line, "character": col}, - }) - - async def rename(self, filepath: str, line: int, col: int, new_name: str): - await self.ensure_open(filepath) - return await self._request("textDocument/rename", { - "textDocument": {"uri": file_uri(filepath)}, - "position": {"line": line, "character": col}, - "newName": new_name, - }) - - async def document_symbols(self, filepath: str): - await self.ensure_open(filepath) - return await self._request("textDocument/documentSymbol", { - "textDocument": {"uri": file_uri(filepath)}, - }) - - async def workspace_symbols(self, query: str): - return await self._request("workspace/symbol", {"query": query}) - - async def diagnostics(self, filepath: str): - await self.ensure_open(filepath) - # Most servers push diagnostics via notification; we wait briefly then return cached - await asyncio.sleep(1.0) - return self._cached_diagnostics.get(file_uri(filepath), []) - - _cached_diagnostics: dict[str, list] = {} - - def _notify(self, method: str, params): - msg = {"jsonrpc": "2.0", "method": method} - if params is not None: - msg["params"] = params - self._send(msg) - - async def _request(self, method: str, params: dict, timeout: float = 30.0) -> dict | list | None: - self.req_id += 1 - rid = self.req_id - msg = {"jsonrpc": "2.0", "id": rid, "method": method, "params": params} - fut = asyncio.get_event_loop().create_future() - self.pending[rid] = fut - self._send(msg) - try: - return await asyncio.wait_for(fut, timeout=timeout) - except asyncio.TimeoutError: - self.pending.pop(rid, None) - raise - - def _send(self, msg: dict): - body = json.dumps(msg).encode() - header = f"Content-Length: {len(body)}\r\n\r\n".encode() - if self.proc and self.proc.stdin: - self.proc.stdin.write(header + body) - - async def _read_loop(self): - try: - while self.proc and self.proc.returncode is None: - # Read header - header_line = await self.proc.stdout.readline() - if not header_line: - break - content_length = 0 - while header_line and header_line.strip(): - if header_line.startswith(b"Content-Length:"): - content_length = int(header_line.split(b":")[1].strip()) - header_line = await self.proc.stdout.readline() - if content_length == 0: - continue - body = await self.proc.stdout.readexactly(content_length) - msg = json.loads(body) - self._handle_message(msg) - except (asyncio.CancelledError, asyncio.IncompleteReadError): - pass - - def _handle_message(self, msg: dict): - if "id" in msg and "method" not in msg: - # Response - rid = msg["id"] - fut = self.pending.pop(rid, None) - if fut and not fut.done(): - if "error" in msg: - fut.set_exception(RuntimeError(msg["error"].get("message", str(msg["error"])))) - else: - fut.set_result(msg.get("result")) - elif "method" in msg and "id" not in msg: - # Notification - if msg["method"] == "textDocument/publishDiagnostics": - params = msg.get("params", {}) - self._cached_diagnostics[params.get("uri", "")] = params.get("diagnostics", []) - - -class Bridge: - """The bridge daemon: manages LSP servers, handles tool requests.""" - - def __init__(self): - self.servers: dict[str, LspServer] = {} # key: "adapter_name:root" - self.last_activity = time.time() - - async def handle_request(self, data: dict) -> dict: - self.last_activity = time.time() - method = data.get("method") - filepath = data.get("file") - if not filepath: - return {"error": "missing 'file' parameter"} - - filepath = os.path.abspath(filepath) - - from _lib.lsp.adapters import detect - adapter = detect(filepath) - if not adapter: - return {"error": f"no adapter for file: {filepath}"} - - fallback = data.get("cwd") - root = adapter.workspace_root(filepath, fallback) - server = await self._get_server(adapter, root) - - try: - if method == "definition": - result = await server.definition(filepath, data["line"], data["col"]) - elif method == "references": - result = await server.references(filepath, data["line"], data["col"]) - elif method == "hover": - result = await server.hover(filepath, data["line"], data["col"]) - elif method == "completion": - result = await server.completion(filepath, data["line"], data["col"]) - elif method == "rename": - result = await server.rename(filepath, data["line"], data["col"], data["new_name"]) - if result: - applied = apply_workspace_edit(result) - return {"result": applied} - return {"result": None} - elif method == "symbols": - if data.get("workspace"): - result = await server.workspace_symbols(data.get("query", "")) - else: - result = await server.document_symbols(filepath) - elif method == "diagnostics": - result = await server.diagnostics(filepath) - else: - return {"error": f"unknown method: {method}"} - return {"result": result} - except Exception as e: - return {"error": str(e)} - - async def _get_server(self, adapter, root: str) -> LspServer: - key = f"{adapter.name}:{root}" - if key not in self.servers: - server = LspServer(adapter, root) - await server.start() - self.servers[key] = server - return self.servers[key] - - async def shutdown(self): - for server in self.servers.values(): - await server.stop() - self.servers.clear() - - -def apply_workspace_edit(edit: dict) -> dict: - """Apply a WorkspaceEdit to disk. Returns summary of changes.""" - changes = {} - # Handle documentChanges (versioned) or changes (simple) - if "documentChanges" in edit: - for doc_change in edit["documentChanges"]: - if doc_change.get("kind") in ("create", "rename", "delete"): - continue # skip resource operations for now - uri = doc_change["textDocument"]["uri"] - edits = doc_change.get("edits", []) - path = uri_to_path(uri) - changes.setdefault(path, []).extend(edits) - elif "changes" in edit: - for uri, edits in edit["changes"].items(): - path = uri_to_path(uri) - changes[path] = edits - - summary = {} - for path, edits in changes.items(): - try: - with open(path, "r") as f: - lines = f.readlines() - except OSError: - continue - - # Apply edits in reverse order (bottom-up) to preserve positions - sorted_edits = sorted(edits, key=lambda e: ( - e["range"]["start"]["line"], - e["range"]["start"]["character"] - ), reverse=True) - - for e in sorted_edits: - start = e["range"]["start"] - end = e["range"]["end"] - new_text = e["newText"] - - # Convert line/char positions to string manipulation - start_line, start_char = start["line"], start["character"] - end_line, end_char = end["line"], end["character"] - - # Build the new content - before = "".join(lines[:start_line]) + lines[start_line][:start_char] if start_line < len(lines) else "".join(lines) - after = lines[end_line][end_char:] + "".join(lines[end_line + 1:]) if end_line < len(lines) else "" - content = before + new_text + after - lines = content.splitlines(True) - - with open(path, "w") as f: - f.writelines(lines) - summary[path] = len(sorted_edits) - - return {"files_changed": summary, "total_edits": sum(summary.values())} - - -async def run_bridge(): - bridge = Bridge() - - async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): - try: - data = await asyncio.wait_for(reader.read(1024 * 1024), timeout=5.0) - if not data: - return - request = json.loads(data) - if request.get("method") == "__shutdown": - response = {"result": "ok"} - writer.write(json.dumps(response).encode()) - await writer.drain() - writer.close() - raise SystemExit(0) - response = await bridge.handle_request(request) - writer.write(json.dumps(response).encode()) - await writer.drain() - except (json.JSONDecodeError, asyncio.TimeoutError) as e: - writer.write(json.dumps({"error": str(e)}).encode()) - await writer.drain() - finally: - writer.close() - - # Clean up stale socket - if os.path.exists(SOCK_PATH): - os.unlink(SOCK_PATH) - - server = await asyncio.start_unix_server(handle_client, path=SOCK_PATH) - os.chmod(SOCK_PATH, 0o600) - - # Write PID - with open(PID_PATH, "w") as f: - f.write(str(os.getpid())) - - # Idle timeout loop - async def idle_watchdog(): - while True: - await asyncio.sleep(30) - if time.time() - bridge.last_activity > IDLE_TIMEOUT: - await bridge.shutdown() - server.close() - return - - watchdog = asyncio.create_task(idle_watchdog()) - try: - await server.serve_forever() - except (asyncio.CancelledError, SystemExit): - pass - finally: - watchdog.cancel() - await bridge.shutdown() - if os.path.exists(SOCK_PATH): - os.unlink(SOCK_PATH) - if os.path.exists(PID_PATH): - os.unlink(PID_PATH) - - -def main(): - asyncio.run(run_bridge()) - - -if __name__ == "__main__": - main() diff --git a/tools/_lib/lsp/client.py b/tools/_lib/lsp/client.py deleted file mode 100644 index cedb56a..0000000 --- a/tools/_lib/lsp/client.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Client library for LSP tool scripts. Connects to the bridge daemon, starting it if needed.""" - -import json -import os -import socket -import subprocess -import sys -import time - -SOCK_PATH = os.path.join(os.environ.get("XDG_RUNTIME_DIR", "/tmp"), "ollie-lsp.sock") -PID_PATH = SOCK_PATH + ".pid" - - -def _bridge_running() -> bool: - if not os.path.exists(PID_PATH): - return False - try: - pid = int(open(PID_PATH).read().strip()) - os.kill(pid, 0) - return True - except (OSError, ValueError): - return False - - -def _start_bridge(): - """Start the bridge daemon as a background process.""" - lib_dir = os.path.dirname(os.path.abspath(__file__)) - bridge_script = os.path.join(lib_dir, "bridge.py") - subprocess.Popen( - [sys.executable, bridge_script], - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - start_new_session=True, - ) - # Wait for socket to appear - for _ in range(50): - if os.path.exists(SOCK_PATH): - return - time.sleep(0.1) - print("error: bridge daemon failed to start", file=sys.stderr) - sys.exit(1) - - -def request(method: str, **kwargs) -> dict: - """Send a request to the bridge and return the response.""" - if not _bridge_running(): - _start_bridge() - - payload = {"method": method, **kwargs} - data = json.dumps(payload).encode() - - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - try: - sock.connect(SOCK_PATH) - sock.sendall(data) - sock.shutdown(socket.SHUT_WR) - chunks = [] - while True: - chunk = sock.recv(4096) - if not chunk: - break - chunks.append(chunk) - response = json.loads(b"".join(chunks)) - except ConnectionRefusedError: - # Stale socket, restart - if os.path.exists(SOCK_PATH): - os.unlink(SOCK_PATH) - _start_bridge() - return request(method, **kwargs) - finally: - sock.close() - - if "error" in response: - print(f"error: {response['error']}", file=sys.stderr) - sys.exit(1) - return response.get("result") diff --git a/tools/_lib/lsp/workspace.py b/tools/_lib/lsp/workspace.py deleted file mode 100644 index 8a64f13..0000000 --- a/tools/_lib/lsp/workspace.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Workspace root detection: walk up from a file to find language-specific markers.""" - -import os -from pathlib import Path - - -def find_root(filepath: str, markers: list[str], fallback: str | None = None) -> str: - """Walk up from filepath looking for any marker file/dir. Return the directory containing it.""" - p = Path(filepath).resolve() - if p.is_file(): - p = p.parent - while True: - for marker in markers: - if (p / marker).exists(): - return str(p) - parent = p.parent - if parent == p: - break - p = parent - return fallback or os.getcwd() diff --git a/tools/browser_screencap b/tools/browser_screencap deleted file mode 100755 index d7a9a78..0000000 --- a/tools/browser_screencap +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -# capabilities: web -# description: Capture browser window showing a specific URL: capture_browser.sh [output_path] [url_pattern] -set -e - -# Verify running under landrun (test filesystem restriction) - -OUTPUT="${1:-${TMPDIR:-/tmp}/browser_capture.png}" -URL_PATTERN="${2:-localhost}" - -case "$(uname -s)" in - Darwin) - # macOS: use osascript + screencapture - osascript -e 'tell application "Google Chrome" to activate' 2>/dev/null - sleep 0.3 - osascript -e "tell application \"Google Chrome\" to set index of (first window whose name contains \"$URL_PATTERN\") to 1" 2>/dev/null - sleep 0.3 - screencapture -x "$OUTPUT" - ;; - Linux) - if [ -z "$DISPLAY" ]; then - echo "ERROR: DISPLAY not set" >&2 - exit 1 - fi - - # Linux: use brotab + wmctrl + import - TAB_INFO=$(brotab list | grep -i "$URL_PATTERN" | head -1) - if [ -z "$TAB_INFO" ]; then - echo "ERROR: No tab matching '$URL_PATTERN'" >&2 - exit 1 - fi - - TAB_ID=$(echo "$TAB_INFO" | cut -f1) - TAB_TITLE=$(echo "$TAB_INFO" | cut -f2) - echo "Found tab: $TAB_ID ($TAB_TITLE)" >&2 - - brotab activate "$TAB_ID" - sleep 0.5 - - WIN_ID=$(wmctrl -l | grep -i "$TAB_TITLE" | head -1 | awk '{print $1}') - if [ -z "$WIN_ID" ]; then - WIN_ID=$(wmctrl -l | grep -iE "firefox|chrome|chromium" | head -1 | awk '{print $1}') - fi - - if [ -z "$WIN_ID" ]; then - echo "ERROR: Could not find browser window" >&2 - exit 1 - fi - - echo "Capturing window: $WIN_ID" >&2 - wmctrl -i -a "$WIN_ID" - sleep 0.3 - import -window "$WIN_ID" "$OUTPUT" - ;; - *) - echo "ERROR: Unsupported OS: $(uname -s)" >&2 - exit 1 - ;; -esac - -if [ -f "$OUTPUT" ]; then - echo "$OUTPUT" -else - echo "ERROR: Failed to capture" >&2 - exit 1 -fi diff --git a/tools/denote_view b/tools/denote_view deleted file mode 100755 index 7516496..0000000 --- a/tools/denote_view +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# ollie:parallel read -# capabilities: agent-kb -# description: View a denote document by identifier -# Usage: view_denote.sh --id -set -euo pipefail - -DENOTE="${DENOTE_9MOUNT:-$HOME/mnt/denote}" - -ID="" - -while [[ $# -gt 0 ]]; do - case "$1" in - --id) ID="$2"; shift 2 ;; - *) echo "unknown argument: $1" >&2; exit 1 ;; - esac -done - -if [ -z "$ID" ]; then - echo "usage: view_denote.sh --id " >&2 - exit 1 -fi - -ID="${ID#denote:}" - -if [ -d "$DENOTE/n/$ID" ]; then - cat "$DENOTE/n/$ID/body" -else - echo "Error: no denote document found for identifier $ID" >&2 - exit 1 -fi diff --git a/tools/file_edit b/tools/file_edit deleted file mode 100644 index f0e70fe..0000000 --- a/tools/file_edit +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 -# description: Replace text in a file; old_string not found is the guard -# Args: [replace_all=true|1|yes] - -import sys -import os -import difflib - -def format_diff(path, old, new): - old_lines = old.splitlines(keepends=True) - new_lines = new.splitlines(keepends=True) - diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path)) - if not diff: - return None - return f"```diff\n{diff}```" - -def find_exact(content, find): - return find if find in content else None - -def find_ws_normalized(content, find): - nf = ' '.join(find.split()) - find_lines = find.split('\n') - n = len(find_lines) - lines = content.split('\n') - for i in range(len(lines) - n + 1): - block = '\n'.join(lines[i:i+n]) - if ' '.join(block.split()) == nf: - return block - return None - -def find_indent_flexible(content, find): - def deindent(text): - ls = text.split('\n') - min_indent = float('inf') - for l in ls: - stripped = l.lstrip(' \t') - if stripped: - min_indent = min(min_indent, len(l) - len(stripped)) - if min_indent == float('inf'): - return text - return '\n'.join(l[min_indent:] if len(l) > min_indent else l.lstrip(' \t') for l in ls) - nf = deindent(find) - find_lines = find.split('\n') - n = len(find_lines) - lines = content.split('\n') - for i in range(len(lines) - n + 1): - block = '\n'.join(lines[i:i+n]) - if deindent(block) == nf: - return block - return None - -def find_trimmed_boundary(content, find): - trimmed = find.strip() - if trimmed == find: - return None - if trimmed in content: - return trimmed - find_lines = find.split('\n') - n = len(find_lines) - lines = content.split('\n') - for i in range(len(lines) - n + 1): - block = '\n'.join(lines[i:i+n]) - if block.strip() == trimmed: - return block - return None - -def robust_replace(content, old_string, new_string, replace_all): - match = (find_exact(content, old_string) or - find_ws_normalized(content, old_string) or - find_indent_flexible(content, old_string) or - find_trimmed_boundary(content, old_string)) - if match is None: - return None, "old_string not found in file", False - if replace_all: - return content.replace(match, new_string), None, False - n = content.count(match) - if n > 1: - return None, f"old_string is not unique ({n} occurrences); use replace_all=true or provide more context", True - return content.replace(match, new_string, 1), None, False - -if len(sys.argv) < 4: - print("usage: file_edit [replace_all=true|1|yes]") - sys.exit(1) - -file_path = sys.argv[1] -old_string = sys.argv[2] -new_string = sys.argv[3] -_ra = sys.argv[4].lower() if len(sys.argv) > 4 else '' -replace_all = _ra.split('=')[-1] in ('true', '1', 'yes') - -if not os.path.isabs(file_path): - print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") - print(f"error: path must be absolute, got: {file_path}") - sys.exit(1) -if old_string == new_string: - print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false") - print("error: old_string and new_string must be different") - sys.exit(1) -if not os.path.exists(file_path): - print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") - print(f"error: file not found: {file_path}") - sys.exit(1) - -try: - with open(file_path, 'r', encoding='utf-8') as f: - original = f.read() -except UnicodeDecodeError: - print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") - print(f"error: file is not valid UTF-8: {file_path}") - sys.exit(1) - -new_content, err, retryable = robust_replace(original, old_string, new_string, replace_all) -if err: - print(f"STATUS=error STATE_CHANGED=false RETRYABLE={'true' if retryable else 'false'}") - print(f"error: {err}") - sys.exit(1) - -try: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(new_content) -except OSError as e: - print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") - print(f"error: {e}") - sys.exit(1) - -diff = format_diff(file_path, original, new_content) -if diff is None: - print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false") - print("(no changes)") -else: - print("STATUS=done STATE_CHANGED=true") - print(diff) diff --git a/tools/file_glob b/tools/file_glob deleted file mode 100644 index 2706408..0000000 --- a/tools/file_glob +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -# ollie:parallel read -# description: Find files by name/path pattern, sorted by modification time (newest first) -# Args: [path] - -pattern="$1" -search_path="${2:-$PWD}" - -[ -z "$pattern" ] && echo "usage: file_glob [path]" && exit 1 - -if [ ! -d "$search_path" ]; then - echo "STATUS=error" - echo "error: path is not a directory: $search_path" - exit 1 -fi - -# collect matches and sort by mtime descending -rg_out=$(rg --no-config --files -g "$pattern" "$search_path" 2>&1) -rg_exit=$? -if [ $rg_exit -gt 1 ]; then - echo "STATUS=error" - echo "error: $rg_out" - exit 1 -fi -if [ -z "$rg_out" ]; then - echo "STATUS=no_matches MATCH_COUNT=0" - echo "no matches" - exit 0 -fi - -sorted=$(echo "$rg_out" \ - | xargs -d '\n' stat --printf '%Y\t%n\n' 2>&1 \ - | sort -rn \ - | cut -f2-) - -count=$(echo "$sorted" | wc -l | tr -d ' ') -echo "STATUS=ok MATCH_COUNT=$count" -echo "$sorted" diff --git a/tools/file_grep b/tools/file_grep deleted file mode 100644 index 1401bdb..0000000 --- a/tools/file_grep +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# description: Search file contents using ripgrep. Default output: file:lineno| content (one match per line). -# Args: [--path=] [--mode=files_with_matches|content|count] [--glob=] [--type=] [-i] [-A ] [-B ] [-C ] [--multiline] [--head=] [--offset=] - -import sys -import os -import subprocess -import argparse - -parser = argparse.ArgumentParser(prog='file_grep', add_help=False) -parser.add_argument('pattern') -parser.add_argument('--path', default='') -parser.add_argument('--mode', default='content', - choices=['files_with_matches', 'content', 'count']) -parser.add_argument('--glob', default='') -parser.add_argument('--type', default='') -parser.add_argument('-i', action='store_true') -parser.add_argument('-n', action='store_true', default=True) -parser.add_argument('-A', type=int, default=None) -parser.add_argument('-B', type=int, default=None) -parser.add_argument('-C', type=int, default=None) -parser.add_argument('--multiline', action='store_true') -parser.add_argument('--head', type=int, default=0) -parser.add_argument('--offset', type=int, default=0) - -args = parser.parse_args() - -search_path = args.path or os.getcwd() - -cmd = ['rg', '--no-config', '--color=never'] - -if args.mode == 'files_with_matches': - cmd.append('-l') -elif args.mode == 'count': - cmd.append('--count') -elif args.mode == 'content': - if args.n: - cmd.append('-n') - -if args.i: - cmd.append('-i') -if args.multiline: - cmd += ['-U', '--multiline-dotall'] -if args.glob: - cmd += ['-g', args.glob] -if args.type: - cmd += ['-t', args.type] -if args.C is not None: - cmd += ['-C', str(args.C)] -if args.A is not None: - cmd += ['-A', str(args.A)] -if args.B is not None: - cmd += ['-B', str(args.B)] - -cmd += ['-e', args.pattern, search_path] - -result = subprocess.run(cmd, capture_output=True, text=True) -if result.returncode == 1: - print("STATUS=no_matches MATCH_COUNT=0") - print("no matches") - sys.exit(0) -if result.returncode > 1: - print(f"STATUS=error") - print(f"error: {result.stderr.strip() or f'rg exited with {result.returncode}'}") - sys.exit(1) - -lines = result.stdout.splitlines() -total = len(lines) - -if args.offset: - lines = lines[args.offset:] - -max_matches = args.head if args.head else 50 -capped = len(lines) > max_matches -if capped: - lines = lines[:max_matches] - -if args.mode == 'content': - formatted = [] - for line in lines: - if line == '--': - formatted.append(line) - continue - for sep in (':', '-'): - parts = line.split(sep, 2) - if len(parts) == 3 and parts[1].isdigit(): - formatted.append(f"{parts[0]}:{parts[1]:>5}| {parts[2]}") - break - else: - formatted.append(line) - lines = formatted - -print(f"STATUS=ok MATCH_COUNT={total}") -print('\n'.join(lines)) -if capped: - print(f"\n(showing {max_matches} of {total} matches; use --head=N or --offset=N for more)") diff --git a/tools/file_read b/tools/file_read deleted file mode 100644 index bcada32..0000000 --- a/tools/file_read +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# ollie:tier cold -# description: Read a file with accurate line numbers. Prefer segments (start/end) over full-file reads. -# Args: [start] [end] [--head=] - -import sys -import os -from itertools import islice - -MAX_LINES = 500 # default window for whole-file reads (no start/end) -MAX_SEGMENT = 2000 # max window when start/end are given -MAX_LINE_LEN = 2000 - -# Parse --head=N from args -head = None -args = [] -for a in sys.argv[1:]: - if a.startswith('--head='): - head = int(a.split('=', 1)[1]) - else: - args.append(a) - -if not args: - print("usage: file_read [start] [end] [--head=N]") - sys.exit(1) - -file_path = args[0] -start = int(args[1]) if len(args) > 1 else 1 -end = int(args[2]) if len(args) > 2 else None - -if not os.path.isabs(file_path): - print(f"STATUS=error") - print(f"error: path must be absolute, got: {file_path}") - sys.exit(1) - -if not os.path.exists(file_path): - print("STATUS=not_found") - print("(new file)") - sys.exit(0) - -if not os.path.isfile(file_path): - print(f"STATUS=error") - print(f"error: not a regular file: {file_path}") - sys.exit(1) - -with open(file_path, 'rb') as f: - raw_head = f.read(8192) - if b'\x00' in raw_head: - # Binary file -- try to detect image type - IMAGE_SIGS = [ - b'\x89PNG\r\n\x1a\n', b'\xff\xd8\xff', - b'GIF87a', b'GIF89a', b'RIFF', - ] - is_image = any(raw_head.startswith(s) for s in IMAGE_SIGS) - if is_image: - print("STATUS=ok BINARY=true IMAGE=true") - print("(image file — use image_read for visual content)") - else: - print("STATUS=ok BINARY=true") - print("(binary file)") - sys.exit(0) - -# Count total lines -try: - with open(file_path, 'r', encoding='utf-8') as f: - total_lines = sum(1 for _ in f) -except UnicodeDecodeError: - with open(file_path, 'r', encoding='utf-8', errors='replace') as f: - total_lines = sum(1 for _ in f) - -start = max(1, start) -if head: - end = start + head - 1 -elif end is None: - end = start + MAX_LINES - 1 -else: - end = min(end, start + MAX_SEGMENT - 1) - -try: - with open(file_path, 'r', encoding='utf-8') as f: - for _ in islice(f, start - 1): - pass - segment_lines = list(islice(f, end - start + 1)) -except UnicodeDecodeError: - with open(file_path, 'r', encoding='utf-8', errors='replace') as f: - for _ in islice(f, start - 1): - pass - segment_lines = list(islice(f, end - start + 1)) - -if not segment_lines: - if start == 1: - print(f"STATUS=ok LINES=0 TOTAL_LINES={total_lines} TRUNCATED=false EMPTY=true") - print("(empty file)") - else: - print(f"STATUS=ok LINES=0 TOTAL_LINES={total_lines} TRUNCATED=false EMPTY=true") - print("(no lines in range)") - sys.exit(0) - -out = [] -for i, line in enumerate(segment_lines): - lineno = start + i - text = line.rstrip('\n') - if len(text) > MAX_LINE_LEN: - text = text[:MAX_LINE_LEN] + '...' - out.append(f"{lineno:>6}\t{text}") - -last_line_shown = start + len(out) - 1 -truncated = last_line_shown < total_lines -print(f"STATUS=ok LINES={len(out)} TOTAL_LINES={total_lines} TRUNCATED={str(truncated).lower()}") -print('\n'.join(out)) diff --git a/tools/file_write b/tools/file_write deleted file mode 100644 index 76c54e2..0000000 --- a/tools/file_write +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -# description: Write or overwrite a file -# Args: - -import sys -import os -import difflib - -def format_diff(path, old, new): - old_lines = old.splitlines(keepends=True) - new_lines = new.splitlines(keepends=True) - diff = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path)) - if not diff: - return None - return f"```diff\n{diff}```" - -if len(sys.argv) < 3: - print("usage: file_write ") - sys.exit(1) - -file_path = sys.argv[1] -content = sys.argv[2] - -if not os.path.isabs(file_path): - print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") - print(f"error: path must be absolute, got: {file_path}") - sys.exit(1) - -parent = os.path.dirname(file_path) -if parent and not os.path.isdir(parent): - print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") - print(f"error: parent directory does not exist: {parent}") - sys.exit(1) - -old_content = "" -if os.path.exists(file_path): - try: - with open(file_path, 'r', encoding='utf-8') as f: - old_content = f.read() - except UnicodeDecodeError: - with open(file_path, 'r', encoding='utf-8', errors='replace') as f: - old_content = f.read() - -try: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(content) -except OSError as e: - print(f"STATUS=error STATE_CHANGED=false RETRYABLE=false") - print(f"error: {e}") - sys.exit(1) - -diff = format_diff(file_path, old_content, content) -if diff is None: - print("STATUS=no_change STATE_CHANGED=false RETRYABLE=false") - print("(no changes)") -else: - print("STATUS=done STATE_CHANGED=true") - print(diff) diff --git a/tools/image_read b/tools/image_read deleted file mode 100644 index ff3af6b..0000000 --- a/tools/image_read +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# ollie:tier cold -# description: Read an image file, recompressing/resizing to fit context budget. Returns an image content block. -# args: [--region=x,y,w,h] [--scale=N] [--quality=N] - -import sys -import os -import base64 -import json -import mimetypes - -file_path = None -for a in sys.argv[1:]: - if not a.startswith('--'): - file_path = a - -if not file_path: - print("usage: image_read ") - sys.exit(1) - -if not os.path.isabs(file_path): - print("STATUS=error") - print("error: path must be absolute") - sys.exit(1) - -if not os.path.exists(file_path): - print("STATUS=not_found") - sys.exit(0) - -mime, _ = mimetypes.guess_type(file_path) -if not mime: - mime = 'application/octet-stream' - -with open(file_path, 'rb') as f: - data = f.read() - -MAX_SIZE = 128 * 1024 - -if mime == 'image/png' and len(data) > MAX_SIZE: - from PIL import Image - import io - img = Image.open(io.BytesIO(data)) - # progressively scale down until under budget - for scale in [1.0, 0.75, 0.5, 0.35, 0.25]: - buf = io.BytesIO() - if scale < 1.0: - new_size = (int(img.width * scale), int(img.height * scale)) - resized = img.resize(new_size, Image.LANCZOS) - else: - resized = img - resized.save(buf, format='PNG', optimize=True) - data = buf.getvalue() - if len(data) <= MAX_SIZE: - break - -b64 = base64.b64encode(data).decode('ascii') - -print(json.dumps({"isError": False, "content": [ - {"type": "image", "media_type": mime, "data": b64}, - {"type": "text", "text": f"[image: {os.path.basename(file_path)} {len(data)} bytes {mime}]"} -]})) diff --git a/tools/lsp_completion b/tools/lsp_completion deleted file mode 100755 index b164461..0000000 --- a/tools/lsp_completion +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# description: Get completions at cursor position from the language server. -# Args: - -import os -import sys -sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't')) - -from _lib.lsp.client import request - -if len(sys.argv) < 4: - print("usage: lsp_completion ") - sys.exit(1) - -filepath = os.path.abspath(sys.argv[1]) -line = int(sys.argv[2]) - 1 -col = int(sys.argv[3]) - 1 - -result = request("completion", file=filepath, line=line, col=col) - -if not result: - sys.exit(0) - -items = result.get("items", result) if isinstance(result, dict) else result -if not isinstance(items, list): - sys.exit(0) - -for item in items[:20]: - label = item.get("label", "") - detail = item.get("detail", "") - if detail: - print(f"{label}\t{detail}") - else: - print(label) diff --git a/tools/lsp_definition b/tools/lsp_definition deleted file mode 100755 index 4879b11..0000000 --- a/tools/lsp_definition +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# description: Go to definition of symbol at position. Returns file:line:col. -# Args: - -import os -import sys -sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't')) - -from _lib.lsp.client import request - -if len(sys.argv) < 4: - print("usage: lsp_definition ") - sys.exit(1) - -filepath = os.path.abspath(sys.argv[1]) -line = int(sys.argv[2]) - 1 # convert 1-indexed to 0-indexed -col = int(sys.argv[3]) - 1 - -result = request("definition", file=filepath, line=line, col=col) - -if not result: - print("(no definition found)") - sys.exit(0) - -# Normalize: result can be a Location or list of Locations -locations = result if isinstance(result, list) else [result] -for loc in locations: - uri = loc.get("uri", loc.get("targetUri", "")) - path = uri[7:] if uri.startswith("file://") else uri - pos = loc.get("range", loc.get("targetRange", {})).get("start", {}) - ln = pos.get("line", 0) + 1 - ch = pos.get("character", 0) + 1 - print(f"{path}:{ln}:{ch}") diff --git a/tools/lsp_diagnostics b/tools/lsp_diagnostics deleted file mode 100755 index c593c5d..0000000 --- a/tools/lsp_diagnostics +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# description: Get diagnostics (errors, warnings) for a file. -# Args: - -import os -import sys -sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't')) - -from _lib.lsp.client import request - -if len(sys.argv) < 2: - print("usage: lsp_diagnostics ") - sys.exit(1) - -filepath = os.path.abspath(sys.argv[1]) - -result = request("diagnostics", file=filepath) - -if not result: - print("(no diagnostics)") - sys.exit(0) - -SEVERITY = {1: "error", 2: "warning", 3: "info", 4: "hint"} - -for diag in result: - sev = SEVERITY.get(diag.get("severity", 3), "info") - ln = diag.get("range", {}).get("start", {}).get("line", 0) + 1 - col = diag.get("range", {}).get("start", {}).get("character", 0) + 1 - msg = diag.get("message", "") - src = diag.get("source", "") - prefix = f"{src}: " if src else "" - print(f"{ln}:{col} {sev}: {prefix}{msg}") - -print(f"\n({len(result)} diagnostics)") diff --git a/tools/lsp_hover b/tools/lsp_hover deleted file mode 100755 index 1c21c8b..0000000 --- a/tools/lsp_hover +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# description: Get type/documentation info for symbol at position. -# Args: - -import os -import sys -sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't')) - -from _lib.lsp.client import request - -if len(sys.argv) < 4: - print("usage: lsp_hover ") - sys.exit(1) - -filepath = os.path.abspath(sys.argv[1]) -line = int(sys.argv[2]) - 1 -col = int(sys.argv[3]) - 1 - -result = request("hover", file=filepath, line=line, col=col) - -if not result: - print("(no hover info)") - sys.exit(0) - -contents = result.get("contents", "") -if isinstance(contents, dict): - # MarkedString or MarkupContent - print(contents.get("value", str(contents))) -elif isinstance(contents, list): - for item in contents: - if isinstance(item, dict): - print(item.get("value", str(item))) - else: - print(item) -else: - print(contents) diff --git a/tools/lsp_references b/tools/lsp_references deleted file mode 100755 index 970a394..0000000 --- a/tools/lsp_references +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# description: Find all references to symbol at position. Returns file:line:col per reference. -# Args: - -import os -import sys -sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't')) - -from _lib.lsp.client import request - -if len(sys.argv) < 4: - print("usage: lsp_references ") - sys.exit(1) - -filepath = os.path.abspath(sys.argv[1]) -line = int(sys.argv[2]) - 1 -col = int(sys.argv[3]) - 1 - -result = request("references", file=filepath, line=line, col=col) - -if not result: - print("(no references found)") - sys.exit(0) - -total = len(result) -cap = 50 -refs = result[:cap] if total > cap else result - -for loc in refs: - uri = loc.get("uri", "") - path = uri[7:] if uri.startswith("file://") else uri - pos = loc.get("range", {}).get("start", {}) - ln = pos.get("line", 0) + 1 - ch = pos.get("character", 0) + 1 - print(f"{path}:{ln}:{ch}") - -if total > cap: - print(f"\n(showing {cap} of {total} references)") -else: - print(f"\n({total} references)") diff --git a/tools/lsp_rename b/tools/lsp_rename deleted file mode 100755 index 20f5dda..0000000 --- a/tools/lsp_rename +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# description: Rename symbol at position across the workspace. Applies edits directly to files. -# Args: - -import os -import sys -sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't')) - -from _lib.lsp.client import request - -if len(sys.argv) < 5: - print("usage: lsp_rename ") - sys.exit(1) - -filepath = os.path.abspath(sys.argv[1]) -line = int(sys.argv[2]) - 1 -col = int(sys.argv[3]) - 1 -new_name = sys.argv[4] - -result = request("rename", file=filepath, line=line, col=col, new_name=new_name) - -if not result: - print("(rename returned no edits)") - sys.exit(0) - -files_changed = result.get("files_changed", {}) -total = result.get("total_edits", 0) - -for path, count in files_changed.items(): - print(f" {path} ({count} edits)") - -print(f"\nRenamed to '{new_name}': {total} edits across {len(files_changed)} files.") diff --git a/tools/lsp_symbols b/tools/lsp_symbols deleted file mode 100755 index 8aa00b1..0000000 --- a/tools/lsp_symbols +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# ollie:parallel read -# description: List symbols in a file or search workspace symbols. -# Args: [--workspace] - -import os -import sys -sys.path.insert(0, os.path.join(os.environ['OLLIE'], 's', os.environ['OLLIE_SESSION_ID'], 't')) - -from _lib.lsp.client import request - -if len(sys.argv) < 2: - print("usage: lsp_symbols [--workspace]") - sys.exit(1) - -workspace = "--workspace" in sys.argv -arg = sys.argv[1] - -SYMBOL_KINDS = { - 1: "File", 2: "Module", 3: "Namespace", 4: "Package", 5: "Class", - 6: "Method", 7: "Property", 8: "Field", 9: "Constructor", 10: "Enum", - 11: "Interface", 12: "Function", 13: "Variable", 14: "Constant", - 15: "String", 16: "Number", 17: "Boolean", 18: "Array", 19: "Object", - 20: "Key", 21: "Null", 22: "EnumMember", 23: "Struct", 24: "Event", - 25: "Operator", 26: "TypeParameter", -} - -if workspace: - result = request("symbols", file=arg, workspace=True, query=arg) -else: - filepath = os.path.abspath(arg) - result = request("symbols", file=filepath, workspace=False) - -if not result: - print("(no symbols found)") - sys.exit(0) - - -def print_symbols(symbols, indent=0): - for sym in symbols: - kind = SYMBOL_KINDS.get(sym.get("kind", 0), "?") - name = sym.get("name", "") - # DocumentSymbol has range; SymbolInformation has location - if "range" in sym: - ln = sym["range"]["start"]["line"] + 1 - print(f"{' ' * indent}{kind:12} {name} (line {ln})") - if "children" in sym: - print_symbols(sym["children"], indent + 1) - elif "location" in sym: - uri = sym["location"].get("uri", "") - path = uri[7:] if uri.startswith("file://") else uri - ln = sym["location"]["range"]["start"]["line"] + 1 - print(f"{kind:12} {name} {path}:{ln}") - - -print_symbols(result) diff --git a/tools/memory_recall b/tools/memory_recall deleted file mode 100755 index 9d691bb..0000000 --- a/tools/memory_recall +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -# ollie:parallel read -# ollie:tier cold -# description: Search memories by keyword, tag, or title -# Args: - -[ -z "$*" ] && echo "usage: memory_recall " && exit 1 - -mount="${OLLIE:-$HOME/mnt/ollie}" -mem_dir="$mount/m" - -[ ! -d "$mem_dir" ] && echo "error: memory directory does not exist: $mem_dir" && exit 1 - -query="$*" -max_files=10 -max_lines=50 -stale_days=14 -now=$(date +%s) - -# Search filenames and bodies; discard I/O errors on stale inodes -matches=$(grep -ril --include='*.md' "$query" "$mem_dir" 2>/dev/null) -[ -z "$matches" ] && echo "no matches for: $query" && exit 0 - -total=$(echo "$matches" | wc -l) -count=0 - -while IFS= read -r file; do - count=$((count + 1)) - [ $count -gt $max_files ] && echo "(showing $max_files of $total matched files)" && break - - fname=$(basename "$file") - # Extract date from filename instead of stat - ts=${fname%%__*} - if [[ $ts =~ ^([0-9]{4})([0-9]{2})([0-9]{2})T ]]; then - fdate=$(date -d "${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}" +%s 2>/dev/null) - [ -n "$fdate" ] && age_days=$(( (now - fdate) / 86400 )) || age_days=0 - else - age_days=0 - fi - - [ $age_days -ge $stale_days ] && status="STALE (${age_days}d)" || status="fresh" - - echo "=== $fname [$status] ===" - awk -v max="$max_lines" 'NR<=max{print} NR>max{over++} END{if(over) printf "... (%d lines total, showing first %d)\n", NR, max}' "$file" 2>/dev/null || echo "error: could not read $file" - echo -done <<< "$matches" diff --git a/tools/memory_remember b/tools/memory_remember deleted file mode 100755 index fb1f585..0000000 --- a/tools/memory_remember +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# description: Save a memory as a denote-formatted flat file -# Args: <tags_csv> <body> - -import sys -import os -import re -from datetime import datetime - -if len(sys.argv) < 4: - print("usage: memory_remember <title> <tags_csv> <body>") - sys.exit(1) - -title = sys.argv[1] -tags_csv = sys.argv[2] -body = sys.argv[3] - -mount = os.environ["OLLIE"] -mem_dir = os.path.join(mount, "m") - -if not os.path.isdir(mem_dir): - print(f"error: memory directory does not exist: {mem_dir}") - sys.exit(1) - -now = datetime.now() -ident = now.strftime("%Y%m%dT%H%M%S") -date = now.strftime("%Y-%m-%d %a %H:%M") - -tags = [t.strip() for t in tags_csv.split(",") if t.strip()] - -def slugify(s): - s = s.lower() - s = re.sub(r"[^a-z0-9]+", "-", s) - return s.strip("-") - -slug = slugify(title) -tag_slug = "-".join(slugify(t) for t in tags) -filename = f"{ident}__{slug}--{tag_slug}.md" if tag_slug else f"{ident}__{slug}.md" -path = os.path.join(mem_dir, filename) - -tags_yaml = "[" + ", ".join(f'"{t}"' for t in tags) + "]" - -content = f"""--- -title: {title} -date: {date} -tags: {tags_yaml} -identifier: {ident} -signature: ---- - -{body} -""" - -try: - with open(path, "w") as f: - f.write(content) -except OSError as e: - print(f"error: failed to write {path}: {e}") - sys.exit(1) - -print(f"saved: {filename}") diff --git a/tools/reasoning_think b/tools/reasoning_think deleted file mode 100755 index 4dd7e59..0000000 --- a/tools/reasoning_think +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# ollie:parallel read -# description: Internal reasoning scratchpad; think through complex problems before acting -# Args: <thought> - -[ -z "$*" ] && echo "usage: reasoning_think <thought>" && exit 1 -exit 0 diff --git a/tools/subagent_generate b/tools/subagent_generate deleted file mode 100755 index ce697e1..0000000 --- a/tools/subagent_generate +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -# description: Generate a JIT subagent config. No args prints the JSON template. With JSON as first arg or on stdin, creates the config and prints the agent name. -# Args: [json] — JSON config as first arg, or piped via stdin -# Output: template (no input) or agent name - -import json -import os -import secrets -import sys - -OLLIE = os.environ.get("OLLIE", os.path.expanduser("~/mnt/ollie")) -AGENTS_DIR = f"{OLLIE}/a" -BASE_CONFIG = f"{AGENTS_DIR}/default.json" - -TEMPLATE = { - "name": "", - "role": "", - "roster": "", - "shared_task": "", - "protocol": "", - "constraints": [], - "prompt": "", -} - - -def die(msg): - print(f"subagent_generate: {msg}", file=sys.stderr) - sys.exit(1) - - -def main(): - # No input: print template. - if len(sys.argv) < 2 and sys.stdin.isatty(): - json.dump(TEMPLATE, sys.stdout, indent=2) - print() - return - - # Read config from arg or stdin. - if len(sys.argv) > 1: - text = sys.argv[1] - else: - text = sys.stdin.read() - if not text.strip(): - die("empty input") - - try: - raw = json.loads(text) - except json.JSONDecodeError as e: - die(f"invalid JSON: {e}") - - name = raw.get("name", "") or f"jit-{secrets.token_hex(8)}" - role = raw.get("role", "") - roster = raw.get("roster", "") - shared_task = raw.get("shared_task", "") - protocol = raw.get("protocol", "") - constraints = raw.get("constraints", []) - if not isinstance(constraints, list): - die("constraints must be an array of strings") - legacy = raw.get("prompt", "") - - if not role and not legacy: - die("role or prompt required") - - # Build identity text. - if role: - parts = ["## Role", role] - if roster: - parts += ["## Roster", roster] - if shared_task: - parts += ["## Shared Task", shared_task] - if protocol: - parts += ["## Coordination Protocol", protocol] - if constraints: - parts += ["## Constraints", "\n".join(f"- {c}" for c in constraints)] - identity = "\n\n".join(parts) - else: - identity = legacy - - # Load base config. - try: - with open(BASE_CONFIG) as f: - cfg = json.load(f) - except (OSError, json.JSONDecodeError) as e: - die(f"failed to read base config: {e}") - - cfg["prompt"] = ["$OLLIE/x/prime SYSTEM_PROMPT", f"echo {json.dumps(identity)}"] - - # Write JIT config. - out_path = f"{AGENTS_DIR}/{name}.json" - try: - with open(out_path, "w") as f: - json.dump(cfg, f, indent=2) - except OSError as e: - die(f"failed to write config: {e}") - - print(name) - - -if __name__ == "__main__": - main() diff --git a/tools/subagent_spawn b/tools/subagent_spawn deleted file mode 100755 index e1ea03d..0000000 --- a/tools/subagent_spawn +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash -# description: Spawn a subagent session, submit a prompt, and return immediately. The subagent runs independently; use prompt files for coordination. -# Args: [-name NAME] [-agent A] [-backend B] [-model M] [-cwd DIR] <prompt...> -# Output: structured KV (session, path, backend, model) -set -euo pipefail - -OLLIE="${OLLIE:-$HOME/mnt/ollie}" - -die() { printf 'subagent_spawn: %s\n' "$*" >&2; exit 1; } - -name=; agent=; backend=; model=; cwd= - -while [ $# -gt 0 ]; do - case $1 in - -name) name=$2; shift ;; - -agent) agent=$2; shift ;; - -backend) backend=$2; shift ;; - -model) model=$2; shift ;; - -cwd) cwd=$2; shift ;; - --) shift; break ;; - -*) die "unknown flag: $1" ;; - *) break ;; - esac - shift -done - -[ $# -eq 0 ] && die "prompt required" - -# Read agent name from stdin if not provided via flag and stdin is piped. -if [ -z "$agent" ] && ! [ -t 0 ]; then - agent=$(head -1) -fi - -name=${name:-$(/bin/date +%s%N)-$$} -# Prefix with parent session ID for global uniqueness. -[ -n "$OLLIE_SESSION_ID" ] && name="${OLLIE_SESSION_ID}__${name}" -cwd=${cwd:-$(pwd)} - -# Prepend parent identity and plan so the child always knows who spawned it. -parent_plan="" -if [ -n "$OLLIE_SESSION_ID" ]; then - plan_file="$OLLIE/s/$OLLIE_SESSION_ID/plan" - [ -s "$plan_file" ] && parent_plan=$(cat "$plan_file") -fi - -prompt="[parent_session_id=${OLLIE_SESSION_ID}] -[session_id=${name}]${parent_plan:+ -[parent_plan_start] -${parent_plan} -[parent_plan_end]} -$*" - -kv="name=${name} -cwd=${cwd}" -[ -n "$agent" ] && kv="${kv} -agent=${agent}" -[ -n "$backend" ] && kv="${kv} -backend=${backend}" -[ -n "$model" ] && kv="${kv} -model=${model}" - -/bin/echo "$kv" > "$OLLIE/s/new" || die "failed to create session" - -B="$OLLIE/s/$name" -wait=0.1 -for _ in 1 2 3 4 5 6 7 8; do - [ -d "$B" ] && break - sleep "$wait" - wait=$(awk "BEGIN{printf \"%.2f\", $wait * 2}") -done -[ -d "$B" ] || die "timeout waiting for session $name" - -/bin/echo "$prompt" > "$B/prompt" || die "failed to submit prompt" - -# Clean up JIT agent config now that the server has loaded it. -# Only remove configs with the jit- prefix (generated by subagent_generate). -[[ "${agent:-}" == jit-* ]] && rm -f "${OLLIE_CFG_PATH:-$HOME/.config/ollie}/agents/${agent}.json" 2>/dev/null || true - -# Report resolved values from the live session. -resolved_backend=$(grep -m1 '^backend=' "$B/cfg" 2>/dev/null | cut -d= -f2- || echo "${backend:-default}") -resolved_model=$(grep -m1 '^model=' "$B/cfg" 2>/dev/null | cut -d= -f2- || echo "${model:-default}") -printf 'session=%s\npath=%s\nbackend=%s\nmodel=%s\n' "$name" "$B" "$resolved_backend" "$resolved_model"