remove tools/ — now at repo root
This commit is contained in:
parent
f9a2e3249d
commit
ebc82f338b
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ollie:parallel read
|
||||
# capabilities: agent-kb
|
||||
# description: View a denote document by identifier
|
||||
# Usage: view_denote.sh --id <denote:identifier|identifier>
|
||||
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 <denote:identifier|identifier>" >&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
|
||||
132
tools/file_edit
132
tools/file_edit
|
|
@ -1,132 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# description: Replace text in a file; old_string not found is the guard
|
||||
# Args: <file_path> <old_string> <new_string> [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 <file_path> <old_string> <new_string> [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)
|
||||
|
|
@ -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: <pattern> [path]
|
||||
|
||||
pattern="$1"
|
||||
search_path="${2:-$PWD}"
|
||||
|
||||
[ -z "$pattern" ] && echo "usage: file_glob <pattern> [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"
|
||||
|
|
@ -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: <pattern> [--path=<dir>] [--mode=files_with_matches|content|count] [--glob=<pattern>] [--type=<type>] [-i] [-A <n>] [-B <n>] [-C <n>] [--multiline] [--head=<n>] [--offset=<n>]
|
||||
|
||||
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)")
|
||||
111
tools/file_read
111
tools/file_read
|
|
@ -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: <file_path> [start] [end] [--head=<n>]
|
||||
|
||||
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 <file_path> [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))
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# description: Write or overwrite a file
|
||||
# Args: <file_path> <content>
|
||||
|
||||
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 <file_path> <content>")
|
||||
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)
|
||||
|
|
@ -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: <file_path> [--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 <file_path>")
|
||||
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}]"}
|
||||
]}))
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# ollie:parallel read
|
||||
# description: Get completions at cursor position from the language server.
|
||||
# Args: <file> <line> <col>
|
||||
|
||||
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 <file> <line> <col>")
|
||||
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)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# ollie:parallel read
|
||||
# description: Go to definition of symbol at position. Returns file:line:col.
|
||||
# Args: <file> <line> <col>
|
||||
|
||||
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 <file> <line> <col>")
|
||||
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}")
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# ollie:parallel read
|
||||
# description: Get diagnostics (errors, warnings) for a file.
|
||||
# Args: <file>
|
||||
|
||||
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 <file>")
|
||||
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)")
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# ollie:parallel read
|
||||
# description: Get type/documentation info for symbol at position.
|
||||
# Args: <file> <line> <col>
|
||||
|
||||
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 <file> <line> <col>")
|
||||
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)
|
||||
|
|
@ -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: <file> <line> <col>
|
||||
|
||||
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 <file> <line> <col>")
|
||||
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)")
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# description: Rename symbol at position across the workspace. Applies edits directly to files.
|
||||
# Args: <file> <line> <col> <new_name>
|
||||
|
||||
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 <file> <line> <col> <new_name>")
|
||||
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.")
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# ollie:parallel read
|
||||
# description: List symbols in a file or search workspace symbols.
|
||||
# Args: <file_or_query> [--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 <file_or_query> [--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)
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# ollie:parallel read
|
||||
# ollie:tier cold
|
||||
# description: Search memories by keyword, tag, or title
|
||||
# Args: <query>
|
||||
|
||||
[ -z "$*" ] && echo "usage: memory_recall <query>" && 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"
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# description: Save a memory as a denote-formatted flat file
|
||||
# Args: <title> <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}")
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
Reference in New Issue