From 6d69eecf1ba5340febed8019133fce530d005ea1 Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Fri, 3 Apr 2026 04:02:40 -0700 Subject: [PATCH 01/12] clean up mega file --- truffile/cli.py | 2518 -------------------------------------- truffile/cli/__init__.py | 114 ++ truffile/cli/apps.py | 218 ++++ truffile/cli/chat.py | 1118 +++++++++++++++++ truffile/cli/connect.py | 246 ++++ truffile/cli/create.py | 167 +++ truffile/cli/deploy.py | 255 ++++ truffile/cli/models.py | 221 ++++ truffile/cli/ui.py | 190 +++ truffile/cli/validate.py | 24 + 10 files changed, 2553 insertions(+), 2518 deletions(-) delete mode 100644 truffile/cli.py create mode 100644 truffile/cli/__init__.py create mode 100644 truffile/cli/apps.py create mode 100644 truffile/cli/chat.py create mode 100644 truffile/cli/connect.py create mode 100644 truffile/cli/create.py create mode 100644 truffile/cli/deploy.py create mode 100644 truffile/cli/models.py create mode 100644 truffile/cli/ui.py create mode 100644 truffile/cli/validate.py diff --git a/truffile/cli.py b/truffile/cli.py deleted file mode 100644 index 1a484c3..0000000 --- a/truffile/cli.py +++ /dev/null @@ -1,2518 +0,0 @@ -import argparse -import asyncio -import base64 -import json -import mimetypes -import os -import re -from importlib import resources as importlib_resources -import select -import signal -import socket -import sys -import threading -import time -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Callable - -import httpx -from truffile.storage import StorageService -from truffile.client import TruffleClient, resolve_mdns, NewSessionStatus -from truffile.schema import validate_app_dir -from truffile.deploy import build_deploy_plan, deploy_with_builder - -try: - import readline -except Exception: - readline = None # type: ignore[assignment] - -try: - import termios - import tty -except Exception: - termios = None # type: ignore[assignment] - tty = None # type: ignore[assignment] - - -# ANSI colors -class C: - RED = "\033[91m" - GREEN = "\033[92m" - YELLOW = "\033[93m" - BLUE = "\033[94m" - MAGENTA = "\033[95m" - CYAN = "\033[96m" - GRAY = "\033[90m" - DIM = "\033[2m" - BOLD = "\033[1m" - RESET = "\033[0m" - - -# Icons -MUSHROOM = "๐Ÿ„โ€๐ŸŸซ" -CHECK = "โœ“" -CROSS = "โœ—" -ARROW = "โ†’" -DOT = "โ€ข" -WARN = "โš " -HAMMER = "๐Ÿ”จ" -SUPPORTED_SERVER_MIME_TYPES = {"image/jpeg", "image/png", "image/bmp"} -REPL_COMMANDS = [ - "/help", - "/", - "/history", - "/reset", - "/models", - "/config", - "/reasoning", - "/stream", - "/json", - "/tools", - "/max_tokens", - "/temperature", - "/top_p", - "/max_rounds", - "/system", - "/mcp", - "/attach", - "/exit", - "/quit", -] -SCAFFOLD_ICON_RESOURCE_REL = Path("assets") / "Truffle.png" - - -class Spinner: - FRAMES = ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "] - - def __init__(self, message: str): - self.message = message - self.running = False - self.thread = None - self.frame_idx = 0 - - def _spin(self): - while self.running: - frame = self.FRAMES[self.frame_idx % len(self.FRAMES)] - sys.stdout.write(f"\r{C.CYAN}{frame}{C.RESET} {self.message}") - sys.stdout.flush() - self.frame_idx += 1 - time.sleep(0.08) - - def start(self): - self.running = True - self.thread = threading.Thread(target=self._spin, daemon=True) - self.thread.start() - - def stop(self, success: bool = True): - self.running = False - if self.thread: - self.thread.join(timeout=0.2) - icon = f"{C.GREEN}{CHECK}{C.RESET}" if success else f"{C.RED}{CROSS}{C.RESET}" - sys.stdout.write(f"\r{icon} {self.message}\n") - sys.stdout.flush() - - def fail(self, message: str | None = None): - self.running = False - if self.thread: - self.thread.join(timeout=0.2) - msg = message or self.message - sys.stdout.write(f"\r{C.RED}{CROSS}{C.RESET} {msg}\n") - sys.stdout.flush() - - -class MushroomPulse: - FRAMES = ["(๐Ÿ„ )", "(๐Ÿ„. )", "(๐Ÿ„.. )", "(๐Ÿ„...)", "(๐Ÿ„ ..)", "(๐Ÿ„ .)"] - - def __init__(self, message: str = "thinking", interval: float = 0.09): - self.message = message - self.interval = interval - self.running = False - self.thread: threading.Thread | None = None - self.frame_idx = 0 - self.enabled = bool(sys.stdout.isatty()) - - def _spin(self) -> None: - while self.running: - frame = self.FRAMES[self.frame_idx % len(self.FRAMES)] - sys.stdout.write(f"\r{C.MAGENTA}{frame}{C.RESET} {C.DIM}{self.message}{C.RESET}") - sys.stdout.flush() - self.frame_idx += 1 - time.sleep(self.interval) - - def start(self) -> None: - if not self.enabled or self.running: - return - self.running = True - self.thread = threading.Thread(target=self._spin, daemon=True) - self.thread.start() - - def stop(self) -> None: - if not self.running: - return - self.running = False - if self.thread: - self.thread.join(timeout=0.2) - sys.stdout.write("\r\033[K") - sys.stdout.flush() - - -class ScrollingLog: - #felt a little fancy lol - """A scrolling log window that shows the last N lines in place.""" - - def __init__(self, height: int = 6, prefix: str = " "): - self.height = height - self.prefix = prefix - self.lines: list[str] = [] - self.started = False - try: - import shutil - self.width = shutil.get_terminal_size().columns - len(prefix) - 2 - except Exception: - self.width = 76 - - def _truncate(self, line: str) -> str: - if len(line) > self.width: - return line[:self.width - 3] + "..." - return line - - def _render(self): - if self.started: - sys.stdout.write(f"\033[{self.height}A") - - display = self.lines[-self.height:] if len(self.lines) >= self.height else self.lines - - while len(display) < self.height: - display.insert(0, "") - - for line in display: - truncated = self._truncate(line) - sys.stdout.write(f"\033[K{self.prefix}{C.DIM}{truncated}{C.RESET}\n") - - sys.stdout.flush() - self.started = True - - def add(self, line: str): - self.lines.append(line.rstrip()) - self._render() - - def finish(self): - if self.started: - sys.stdout.write(f"\033[{self.height}A") - for _ in range(self.height): - sys.stdout.write("\033[K\n") - sys.stdout.write(f"\033[{self.height}A") - sys.stdout.flush() - - -def error(msg: str): - print(f"{C.RED}{CROSS} Error:{C.RESET} {msg}") - - -def warn(msg: str): - print(f"{C.YELLOW}{WARN} Warning:{C.RESET} {msg}") - - -def success(msg: str): - print(f"{C.GREEN}{CHECK}{C.RESET} {msg}") - - -def info(msg: str): - print(f"{C.CYAN}{DOT}{C.RESET} {msg}") - - -async def cmd_connect(args, storage: StorageService) -> int: - device_name = args.device - - spinner = Spinner(f"Resolving {device_name}.local") - spinner.start() - - hostname = f"{device_name}.local" - try: - ip = await resolve_mdns(hostname) - spinner.stop(success=True) - except RuntimeError: - spinner.fail(f"Could not resolve {device_name}.local") - print() - print(f" {C.DIM}Try running:{C.RESET}") - print(f" {C.CYAN}ping {device_name}.local{C.RESET}") - print() - print(f" {C.DIM}If ping fails, check:{C.RESET}") - print(f" {C.DIM}{DOT} Device is powered on and connected to WiFi{C.RESET}") - print(f" {C.DIM}{DOT} Your computer is on the same network{C.RESET}") - print(f" {C.DIM}{DOT} mDNS is working{C.RESET}") - print() - return 1 - - address = f"{ip}:80" - existing_token = storage.get_token(device_name) - - if existing_token: - spinner = Spinner("Validating existing token") - spinner.start() - client = TruffleClient(address, existing_token) - try: - await client.connect() - if await client.check_auth(): - spinner.stop(success=True) - storage.set_last_used(device_name) - success(f"Already connected to {C.BOLD}{device_name}{C.RESET}") - await client.close() - return 0 - spinner.fail("Token invalid, re-authenticating") - except Exception: - spinner.fail("Token validation failed") - finally: - await client.close() - - print() - print(f" {C.DIM}Make sure you have:{C.RESET}") - print(f" {C.DIM}{DOT} Onboarded with the Truffle app{C.RESET}") - print(f" {C.DIM}{DOT} Your User ID from the recovery codes{C.RESET}") - print() - - try: - user_id = input(f"{C.CYAN}?{C.RESET} Enter your User ID: ").strip() - except (KeyboardInterrupt, EOFError): - print() - raise KeyboardInterrupt() - if not user_id: - error("User ID is required") - return 1 - - spinner = Spinner("Connecting to device") - spinner.start() - - client = TruffleClient(address, token="") - try: - await client.connect() - spinner.stop(success=True) - except Exception as e: - spinner.fail(f"Failed to connect: {e}") - return 1 - - print() - info("Requesting authorization...") - print(f" {C.DIM}Please approve on your Truffle device{C.RESET}") - - spinner = Spinner("Waiting for approval") - spinner.start() - - try: - status, token = await client.register_new_session(user_id) - except Exception as e: - spinner.fail(f"Failed to register: {e}") - await client.close() - return 1 - - await client.close() - - if status.error == NewSessionStatus.NEW_SESSION_SUCCESS and token: - spinner.stop(success=True) - storage.set_token(device_name, token) - storage.set_last_used(device_name) - print() - success(f"Connected to {C.BOLD}{device_name}{C.RESET}") - return 0 - elif status.error == NewSessionStatus.NEW_SESSION_TIMEOUT: - spinner.fail("Approval timed out") - return 1 - elif status.error == NewSessionStatus.NEW_SESSION_REJECTED: - spinner.fail("Request was rejected") - return 1 - else: - spinner.fail(f"Authentication failed: {status.error}") - return 1 - - -def cmd_disconnect(args, storage: StorageService) -> int: - target = args.target - if target == "all": - storage.clear_all() - success("All device credentials cleared") - else: - if storage.remove_device(target): - success(f"Disconnected from {C.BOLD}{target}{C.RESET}") - else: - error(f"No credentials found for {target}") - return 0 - - -def _safe_app_slug(app_name: str) -> str: - slug = re.sub(r"[^a-z0-9]+", "_", app_name.lower()).strip("_") - if not slug: - return "my_app" - if slug[0].isdigit(): - return f"app_{slug}" - return slug - - -def _sample_truffile_yaml(app_name: str, slug: str) -> str: - quoted_name = json.dumps(app_name) - return ( - "metadata:\n" - f" name: {quoted_name}\n" - f" bundle_id: org.truffle.{slug.replace('_', '.')}\n" - " description: |\n" - " Describe what this app does.\n" - " icon_file: ./icon.png\n" - " foreground:\n" - " process:\n" - " cmd:\n" - " - python\n" - f" - {slug}_foreground.py\n" - " working_directory: /\n" - " environment:\n" - ' PYTHONUNBUFFERED: "1"\n' - " background:\n" - " process:\n" - " cmd:\n" - " - python\n" - f" - {slug}_background.py\n" - " working_directory: /\n" - " environment:\n" - ' PYTHONUNBUFFERED: "1"\n' - " default_schedule:\n" - " type: interval\n" - " interval:\n" - " duration: 30m\n" - " schedule:\n" - ' daily_window: "00:00-23:59"\n' - "\n" - "steps:\n" - " - name: Copy application files\n" - " type: files\n" - " files:\n" - f" - source: ./{slug}_foreground.py\n" - f" destination: ./{slug}_foreground.py\n" - f" - source: ./{slug}_background.py\n" - f" destination: ./{slug}_background.py\n" - ) - - -def _sample_foreground_py() -> str: - return ( - '"""Foreground app entrypoint (MCP-facing surface)."""\n' - "\n" - "def main() -> None:\n" - ' print(\"TODO: implement foreground MCP tool server\")\n' - "\n" - "\n" - "if __name__ == \"__main__\":\n" - " main()\n" - ) - - -def _sample_background_py() -> str: - return ( - '"""Background app entrypoint (scheduled context emitter)."""\n' - "\n" - "def main() -> None:\n" - ' print(\"TODO: implement background scheduled job\")\n' - "\n" - "\n" - "if __name__ == \"__main__\":\n" - " main()\n" - ) - - -def _load_stock_icon_bytes() -> tuple[bytes | None, str]: - try: - resource_file = importlib_resources.files("truffile").joinpath(str(SCAFFOLD_ICON_RESOURCE_REL)) - icon_bytes = resource_file.read_bytes() - return icon_bytes, f"truffile/{SCAFFOLD_ICON_RESOURCE_REL.as_posix()}" - except Exception: - pass - - local_package_path = Path(__file__).resolve().parent / SCAFFOLD_ICON_RESOURCE_REL - if local_package_path.exists() and local_package_path.is_file(): - return local_package_path.read_bytes(), str(local_package_path) - - legacy_docs_path = Path(__file__).resolve().parents[1] / "docs" / "Truffle.png" - if legacy_docs_path.exists() and legacy_docs_path.is_file(): - return legacy_docs_path.read_bytes(), str(legacy_docs_path) - - return None, f"truffile/{SCAFFOLD_ICON_RESOURCE_REL.as_posix()}" - - -def cmd_create(args) -> int: - app_name = (args.name or "").strip() - if not app_name: - try: - app_name = input(f"{C.CYAN}?{C.RESET} App name: ").strip() - except (KeyboardInterrupt, EOFError): - print() - return 0 - if not app_name: - error("App name is required") - return 1 - if "/" in app_name or "\\" in app_name: - error("App name cannot contain path separators") - return 1 - - base_dir: Path - if args.path: - base_dir = Path(args.path).expanduser().resolve() - else: - cwd = Path.cwd() - try: - raw = input(f"{C.CYAN}?{C.RESET} Base path [{cwd}]: ").strip() - except (KeyboardInterrupt, EOFError): - print() - return 0 - base_dir = Path(raw).expanduser().resolve() if raw else cwd - - app_dir = base_dir / app_name - if app_dir.exists(): - error(f"Target directory already exists: {app_dir}") - return 1 - - slug = _safe_app_slug(app_name) - fg_file = f"{slug}_foreground.py" - bg_file = f"{slug}_background.py" - stock_icon_bytes, stock_icon_source = _load_stock_icon_bytes() - if stock_icon_bytes is None: - error(f"Stock icon not found: {stock_icon_source}") - return 1 - if len(stock_icon_bytes) == 0: - error(f"Stock icon is empty: {stock_icon_source}") - return 1 - - try: - app_dir.mkdir(parents=True, exist_ok=False) - (app_dir / "truffile.yaml").write_text(_sample_truffile_yaml(app_name, slug), encoding="utf-8") - (app_dir / fg_file).write_text(_sample_foreground_py(), encoding="utf-8") - (app_dir / bg_file).write_text(_sample_background_py(), encoding="utf-8") - (app_dir / "icon.png").write_bytes(stock_icon_bytes) - except Exception as exc: - error(f"Failed to scaffold app: {exc}") - return 1 - - success(f"Created app scaffold: {app_dir}") - print(f" {C.DIM}Files:{C.RESET}") - print(f" {C.DIM}{ARROW} truffile.yaml{C.RESET}") - print(f" {C.DIM}{ARROW} {fg_file}{C.RESET}") - print(f" {C.DIM}{ARROW} {bg_file}{C.RESET}") - print(f" {C.DIM}{ARROW} icon.png{C.RESET}") - print() - print(f" {C.DIM}Next:{C.RESET} truffile validate {app_dir}") - return 0 - - -async def cmd_deploy(args, storage: StorageService) -> int: - app_path = args.path if args.path else "." - app_dir = Path(app_path).resolve() - interactive = args.interactive - dry_run = bool(getattr(args, "dry_run", False)) - if not app_dir.exists() or not app_dir.is_dir(): - error(f"{app_dir} is not a valid directory") - return 1 - - info(f"Validating app in {app_dir.name}") - valid, config, app_type, warnings, errors = validate_app_dir(app_dir) - if not valid or not app_type: - for msg in errors: - error(msg) - return 1 - - for w in warnings: - warn(w) - - metadata = config.get("metadata", {}) if isinstance(config, dict) else {} - icon_file = metadata.get("icon_file") if isinstance(metadata, dict) else None - if not isinstance(icon_file, str) or not icon_file.strip(): - error("Deploy requires metadata.icon_file in truffile.yaml") - return 1 - deploy_icon_path = app_dir / icon_file - if not deploy_icon_path.exists() or not deploy_icon_path.is_file(): - error(f"Deploy requires an icon file; not found: {icon_file}") - return 1 - if deploy_icon_path.stat().st_size == 0: - error(f"Deploy requires a non-empty icon file: {icon_file}") - return 1 - - if dry_run: - try: - plan = build_deploy_plan(config=config, app_dir=app_dir, app_type=app_type) - except Exception as e: - error(f"Failed to build deploy plan: {e}") - return 1 - print() - print(f"{C.BOLD}Dry Run: Deploy Plan{C.RESET}") - print(f" Name: {plan['name']}") - print(f" Bundle ID: {plan['bundle_id']}") - print(f" Mode: {plan['finish_label']}") - print(f" App Dir: {app_dir}") - print(f" Exec CWD: {plan['exec_cwd']}") - if plan["icon_path"] is not None: - print(f" Icon: {plan['icon_path']}") - else: - print(f" Icon: {C.DIM}{C.RESET}") - - fg = plan["fg_payload"] - if fg is not None: - fg_keys = [e.split("=", 1)[0] for e in fg.get("env", []) if "=" in e] - print(f" Foreground Cmd: {fg['cmd']} {' '.join(fg.get('args', []))}".rstrip()) - print(f" Foreground Env Keys: {', '.join(fg_keys) if fg_keys else ''}") - - bg = plan["bg_payload"] - if bg is not None: - bg_keys = [e.split('=', 1)[0] for e in bg.get("env", []) if "=" in e] - print(f" Background Cmd: {bg['cmd']} {' '.join(bg.get('args', []))}".rstrip()) - print(f" Background Env Keys: {', '.join(bg_keys) if bg_keys else ''}") - if plan["default_schedule"] is not None: - print(f" Background Schedule: configured") - else: - print(f" Background Schedule: {C.DIM}{C.RESET}") - - files = plan["files_to_upload"] - print(f" Files To Upload: {len(files)}") - for f in files: - src = f.get("source", "") - dst = f.get("destination", "") - print(f" - {src} {ARROW} {dst}") - - cmds = plan["bash_commands"] - print(f" Bash Steps: {len(cmds)}") - for name, _cmd in cmds: - print(f" - {name}") - print() - success("Dry run complete (no device changes made)") - return 0 - - device = storage.state.last_used_device - if not device: - error("No device connected") - print(f" {C.DIM}Run: truffile connect {C.RESET}") - return 1 - - token = storage.get_token(device) - if not token: - error(f"No token for {device}") - print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") - return 1 - - spinner = Spinner(f"Resolving {device}") - spinner.start() - try: - ip = await resolve_mdns(f"{device}.local") - spinner.stop(success=True) - except RuntimeError: - spinner.fail(f"Could not resolve {device}.local") - print(f" {C.DIM}Try: ping {device}.local{C.RESET}") - return 1 - - address = f"{ip}:80" - client = TruffleClient(address, token=token) - deploy_task = None - - loop = asyncio.get_event_loop() - - def handle_sigint(): - print("\nInterrupted!") - if deploy_task and not deploy_task.done(): - deploy_task.cancel() - - loop.add_signal_handler(signal.SIGINT, handle_sigint) - - try: - deploy_task = asyncio.create_task( - deploy_with_builder( - client=client, - config=config, - app_dir=app_dir, - app_type=app_type, - device=device, - interactive=interactive, - spinner_cls=Spinner, - scrolling_log_cls=ScrollingLog, - info=info, - success=success, - error=error, - color_dim=C.DIM, - color_reset=C.RESET, - color_bold=C.BOLD, - arrow=ARROW, - interactive_shell=_interactive_shell, - ) - ) - return await deploy_task - except asyncio.CancelledError: - print() - spinner = Spinner("Discarding build session") - spinner.start() - if client.app_uuid: - try: - await client.discard() - spinner.stop(success=True) - except Exception: - spinner.fail("Failed to discard") - return 130 - except Exception as e: - error(str(e)) - if client.app_uuid: - spinner = Spinner("Discarding build session") - spinner.start() - try: - await client.discard() - spinner.stop(success=True) - except Exception: - spinner.fail("Failed to discard") - return 1 - finally: - loop.remove_signal_handler(signal.SIGINT) - await client.close() - - -async def cmd_list_apps(storage: StorageService) -> int: - device = storage.state.last_used_device - if not device: - error("No device connected") - print(f" {C.DIM}Run: truffile connect {C.RESET}") - return 1 - - token = storage.get_token(device) - if not token: - error(f"No token for {device}") - print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") - return 1 - - spinner = Spinner(f"Connecting to {device}") - spinner.start() - - try: - ip = await resolve_mdns(f"{device}.local") - except RuntimeError as e: - spinner.fail(str(e)) - return 1 - - address = f"{ip}:80" - client = TruffleClient(address, token=token) - - try: - await client.connect() - apps = await client.get_all_apps() - spinner.stop(success=True) - - if not apps: - print(f" {C.DIM}No apps installed{C.RESET}") - return 0 - - focus_apps = [app for app in apps if app.HasField("foreground")] - ambient_apps = [app for app in apps if app.HasField("background")] - both_apps = [app for app in apps if app.HasField("foreground") and app.HasField("background")] - - print() - if focus_apps: - print(f"{C.BOLD}Focus Apps{C.RESET}") - for app in focus_apps: - print(f" {C.CYAN}{DOT}{C.RESET} {app.metadata.name}") - setattr(app.metadata, "description", getattr(app.metadata, "description", "")) - if hasattr(app.metadata, "description") and app.metadata.description: - desc = app.metadata.description.strip().split('\n')[0][:55] - print(f" {C.DIM}{desc}{C.RESET}") - - if ambient_apps: - if focus_apps: - print() - print(f"{C.BOLD}Ambient Apps{C.RESET}") - for app in ambient_apps: - schedule = "" - policy = app.background.runtime_policy - if policy.HasField("interval"): - secs = policy.interval.duration.seconds - if secs >= 3600: - schedule = f"every {secs // 3600}h" - elif secs >= 60: - schedule = f"every {secs // 60}m" - else: - schedule = f"every {secs}s" - elif policy.HasField("always"): - schedule = "always" - print(f" {C.CYAN}{DOT}{C.RESET} {app.metadata.name} {C.DIM}({schedule}){C.RESET}") - setattr(app.metadata, "description", getattr(app.metadata, "description", "")) - if hasattr(app.metadata, "description") and app.metadata.description: - desc = app.metadata.description.strip().split('\n')[0][:55] - print(f" {C.DIM}{desc}{C.RESET}") - - print() - print( - f"{C.DIM}Total: {len(focus_apps)} focus, {len(ambient_apps)} ambient, " - f"{len(both_apps)} both{C.RESET}" - ) - return 0 - - except Exception as e: - spinner.fail(str(e)) - return 1 - finally: - await client.close() - -async def cmd_delete(args, storage: StorageService) -> int: - device = storage.state.last_used_device - if not device: - error("No device connected") - print(f" {C.DIM}Run: truffile connect {C.RESET}") - return 1 - - token = storage.get_token(device) - if not token: - error(f"No token for {device}") - print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") - return 1 - - spinner = Spinner(f"Connecting to {device}") - spinner.start() - - try: - ip = await resolve_mdns(f"{device}.local") - except RuntimeError as e: - spinner.fail(str(e)) - return 1 - - address = f"{ip}:80" - client = TruffleClient(address, token=token) - - try: - await client.connect() - apps = await client.get_all_apps() - spinner.stop(success=True) - - all_apps = [] - for app in apps: - if app.HasField("foreground") and app.HasField("background"): - kind = "both" - elif app.HasField("foreground"): - kind = "focus" - elif app.HasField("background"): - kind = "ambient" - else: - kind = "unknown" - desc = app.metadata.description.strip().split('\n')[0][:55] if app.metadata.description else "" - all_apps.append((kind, app.uuid, app.metadata.name, desc)) - - if not all_apps: - print(f" {C.DIM}No apps installed{C.RESET}") - return 0 - - print() - print(f"{C.BOLD}Installed Apps:{C.RESET}") - print() - for i, (kind, uuid, name, desc) in enumerate(all_apps, 1): - print(f" {C.CYAN}{i}.{C.RESET} {name} {C.DIM}({kind}){C.RESET}") - if desc: - print(f" {C.DIM}{desc}{C.RESET}") - print() - - try: - choice = input(f"Select apps to delete (e.g. 1,3,5 or 'all'): ").strip() - except (KeyboardInterrupt, EOFError): - print() - return 0 - - if not choice: - return 0 - - if choice.lower() == "all": - to_delete = list(range(len(all_apps))) - else: - try: - to_delete = [int(x.strip()) - 1 for x in choice.split(",")] - for idx in to_delete: - if idx < 0 or idx >= len(all_apps): - error(f"Invalid selection: {idx + 1}") - return 1 - except ValueError: - error("Invalid input") - return 1 - - print() - deleted = 0 - for idx in to_delete: - kind, uuid, name, _ = all_apps[idx] - spinner = Spinner(f"Deleting {name}") - spinner.start() - try: - await client.delete_app(uuid) - spinner.stop(success=True) - deleted += 1 - except Exception as e: - spinner.fail(f"Failed to delete {name}: {e}") - - print() - success(f"Deleted {deleted} app(s)") - return 0 - - except Exception as e: - spinner.fail(str(e)) - return 1 - finally: - await client.close() - - -async def _interactive_shell(ws_url: str) -> int: - print(f"{C.DIM}Opening shell... (exit with Ctrl+D or 'exit'){C.RESET}") - import os, termios, fcntl, struct, tty, contextlib, json - try: - import websockets - from websockets.exceptions import ConnectionClosed, ConnectionClosedOK - except Exception: - print(f"{C.RED}{CROSS} Error:{C.RESET} websockets package is required for terminal mode") - return 67 - - def _winsz(): - try: - h, w, _, _ = struct.unpack("HHHH", fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, b"\0"*8)) - return w, h - except Exception: - return 80, 24 - - class Raw: - def __enter__(self): - self.fd = sys.stdin.fileno() - self.old = termios.tcgetattr(self.fd) - tty.setraw(self.fd); return self - def __exit__(self, *a): - termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old) - - async def run_once(): - async with websockets.connect(ws_url, max_size=None, ping_interval=30) as ws: - cols, rows = _winsz() - await ws.send(json.dumps({"resize":[cols, rows]})) - - loop = asyncio.get_running_loop() - q: asyncio.Queue[bytes] = asyncio.Queue() - stop = asyncio.Event() - - def on_stdin(): - try: - data = os.read(sys.stdin.fileno(), 4096) - if data: q.put_nowait(data) - except BlockingIOError: - pass - loop.add_reader(sys.stdin.fileno(), on_stdin) - - async def pump_in(): - try: - while not stop.is_set(): - data = await q.get() - try: await ws.send(data) - except (ConnectionClosed, ConnectionClosedOK): break - finally: - stop.set() - async def pump_out(): - try: - async for msg in ws: - if isinstance(msg, bytes): - os.write(sys.stdout.fileno(), msg) - else: - os.write(sys.stdout.fileno(), msg.encode()) # type: ignore - except (ConnectionClosed, ConnectionClosedOK): - pass - finally: - stop.set() - - with Raw(): - t_in = asyncio.create_task(pump_in()) - t_out = asyncio.create_task(pump_out()) - try: - await asyncio.wait({t_in, t_out}, return_when=asyncio.FIRST_COMPLETED) - finally: - stop.set(); t_in.cancel(); t_out.cancel() - with contextlib.suppress(Exception): - await asyncio.gather(t_in, t_out, return_exceptions=True) - loop.remove_reader(sys.stdin.fileno()) - - - await run_once() - return 67 - -def run_async(coro): - try: - return asyncio.run(coro) - except KeyboardInterrupt: - print(f"\r{C.RED}{CROSS} Cancelled{C.RESET} ") - return 130 - - -def cmd_list(args, storage: StorageService) -> int: - what = args.what - if what == "apps": - return run_async(cmd_list_apps(storage)) - elif what == "devices": - devices = storage.list_devices() - if not devices: - print(f" {C.DIM}No connected devices{C.RESET}") - else: - print(f"{C.BOLD}Connected Devices{C.RESET}") - for d in devices: - if d == storage.state.last_used_device: - print(f" {C.GREEN}{DOT}{C.RESET} {d} {C.DIM}(active){C.RESET}") - else: - print(f" {C.CYAN}{DOT}{C.RESET} {d}") - return 0 - - -async def cmd_models(storage: StorageService) -> int: - """List models on your Truffle.""" - device = storage.state.last_used_device - if not device: - error("No device connected") - print(f" {C.DIM}Run: truffile connect {C.RESET}") - return 1 - - spinner = Spinner(f"Connecting to {device}") - spinner.start() - - try: - ip = await resolve_mdns(f"{device}.local") - except RuntimeError: - spinner.fail(f"Could not resolve {device}.local") - return 1 - - try: - url = f"http://{ip}/if2/v1/models" - with httpx.Client(timeout=15.0) as client: - resp = client.get(url) - resp.raise_for_status() - payload = resp.json() - spinner.stop(success=True) - except Exception as e: - spinner.fail(f"Failed to get IF2 models: {e}") - return 1 - - models = payload.get("data", []) - if not isinstance(models, list): - spinner.fail("Invalid response: missing 'data' list") - return 1 - - print() - print(f"{MUSHROOM} {C.BOLD}IF2 Models on {device}{C.RESET}") - print() - - if not models: - print(f" {C.DIM}No models found{C.RESET}") - return 0 - - for m in models: - if not isinstance(m, dict): - continue - model_id = m.get("id", "") - name = m.get("name", model_id) - uuid = m.get("uuid", "") - ctx = m.get("context_length", "") - arch = m.get("architecture", {}) - tokenizer = arch.get("tokenizer", "") if isinstance(arch, dict) else "" - max_batch = m.get("max_batch_size", "") - print(f" {C.GREEN}{CHECK}{C.RESET} {name}") - print(f" {C.DIM}id: {model_id}{C.RESET}") - print(f" {C.DIM}uuid: {uuid}{C.RESET}") - print(f" {C.DIM}context: {ctx}, tokenizer: {tokenizer}, max_batch: {max_batch}{C.RESET}") - - return 0 - - -async def _resolve_connected_device(storage: StorageService) -> tuple[str, str] | tuple[None, None]: - device = storage.state.last_used_device - if not device: - error("No device connected") - print(f" {C.DIM}Run: truffile connect {C.RESET}") - return None, None - try: - ip = await resolve_mdns(f"{device}.local") - except RuntimeError: - error(f"Could not resolve {device}.local") - return None, None - return device, ip - - -async def _default_model(ip: str) -> str | None: - try: - with httpx.Client(timeout=10.0) as client: - resp = client.get(f"http://{ip}/if2/v1/models") - resp.raise_for_status() - payload = resp.json() - models = payload.get("data", []) - if not isinstance(models, list) or not models: - return None - # sort models by name/id so default is 35b typically & list is consistent - models.sort(key=lambda m: str(m.get("name") or m.get("id") or m.get("uuid") or "")) - first = models[0] - return str(first.get("id") or first.get("uuid") or "") - except Exception: - return None - - -def _model_display_name(model: dict[str, Any]) -> str: - model_id = str(model.get("id") or "") - name = str(model.get("name") or model_id) - if name == model_id: - return name - return f"{name} ({model_id})" - - -def _model_value(model: dict[str, Any]) -> str: - return str(model.get("uuid") or model.get("id") or "") - - -def _model_matches_current(model: dict[str, Any], current_model: str) -> bool: - if not current_model: - return False - mv = _model_value(model) - mid = str(model.get("id") or "") - return current_model in {mv, mid} - - -def _pick_model_with_numbers(models: list[dict[str, Any]], current_model: str) -> str | None: - if not models: - return None - print(f"{C.BLUE}models:{C.RESET}") - default_idx = 0 - for i, m in enumerate(models, start=1): - active = f" {C.DIM}[active]{C.RESET}" if _model_matches_current(m, current_model) else "" - if active: - default_idx = i - 1 - print(f"{C.BLUE}{i}.{C.RESET} {_model_display_name(m)}{active}") - choice = input(f"{C.CYAN}?{C.RESET} select model [1-{len(models)}] (Enter to keep): ").strip() - if not choice: - return _model_value(models[default_idx]) - try: - idx = int(choice) - 1 - except ValueError: - warn("invalid model selection") - return None - if idx < 0 or idx >= len(models): - warn("invalid model selection") - return None - return _model_value(models[idx]) - - -def _pick_model_interactive(models: list[dict[str, Any]], current_model: str) -> str | None: - if not models: - return None - if not sys.stdin.isatty() or not sys.stdout.isatty() or termios is None or tty is None: - return _pick_model_with_numbers(models, current_model) - - selected = 0 - for i, m in enumerate(models): - if _model_matches_current(m, current_model): - selected = i - break - - lines_rendered = 0 - - def _render() -> None: - nonlocal lines_rendered - lines: list[str] = [] - lines.append(f"{C.BLUE}select model (โ†‘/โ†“, Enter=select, q=cancel){C.RESET}") - for i, m in enumerate(models): - pointer = "โ€บ" if i == selected else " " - active = f" {C.DIM}[active]{C.RESET}" if _model_matches_current(m, current_model) else "" - line = f" {C.CYAN}{pointer}{C.RESET} {_model_display_name(m)}{active}" - lines.append(line) - - if lines_rendered > 0: - sys.stdout.write(f"\033[{lines_rendered}A") - for line in lines: - sys.stdout.write(f"\r\033[K{line}\n") - sys.stdout.flush() - lines_rendered = len(lines) - - fd = sys.stdin.fileno() - old_attrs = termios.tcgetattr(fd) - try: - tty.setraw(fd) - _render() - while True: - ch = sys.stdin.read(1) - if ch in ("\r", "\n"): - sys.stdout.write("\r\033[K") - return _model_value(models[selected]) - if ch in ("q", "Q"): - sys.stdout.write("\r\033[K") - return None - if ch == "\x1b": - seq1 = sys.stdin.read(1) - if seq1 == "[": - seq2 = sys.stdin.read(1) - if seq2 == "A": - selected = (selected - 1) % len(models) - _render() - continue - if seq2 == "B": - selected = (selected + 1) % len(models) - _render() - continue - return None - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs) - if lines_rendered > 0: - sys.stdout.write(f"\033[{lines_rendered}A") - for _ in range(lines_rendered): - sys.stdout.write("\r\033[K\n") - sys.stdout.write(f"\033[{lines_rendered}A") - sys.stdout.flush() - - -def _fetch_models_payload(client: httpx.Client, ip: str) -> list[dict[str, Any]]: - resp = client.get(f"http://{ip}/if2/v1/models", timeout=15.0) - resp.raise_for_status() - payload = resp.json() - raw = payload.get("data", []) - if not isinstance(raw, list): - raise RuntimeError("invalid models payload") - out: list[dict[str, Any]] = [] - for m in raw: - if isinstance(m, dict): - out.append(m) - try: - out.sort(key=lambda m: str(m.get("name") or m.get("id") or m.get("uuid") or "")) - except Exception: pass - - return out - - -DEFAULT_SYSTEM_PROMPT = """You are an assistant running on a Truffle device via the SDK's CLI. -If the user is confused why you are not their regular assistant, explain that you are a special CLI for testing and development. -Overall, just create an engaging and interesting experience for the user, match their vibe and tone. -Generally a chat / question type session unless otherwise specified. Use tools only if requested or clearly appropriate. -""" - -@dataclass -class ChatSettings: - model: str - system_prompt: str | None = DEFAULT_SYSTEM_PROMPT - reasoning: bool = True - stream: bool = True - json_mode: bool = False - max_tokens: int = 2048 - temperature: float | None = None - top_p: float | None = None - default_tools: bool = True - max_tool_rounds: int = 8 - - -class ChatMCPClient: - def __init__(self) -> None: - self._group: Any | None = None - self.endpoint: str | None = None - - @property - def connected(self) -> bool: - return self._group is not None - - async def connect_streamable_http(self, endpoint: str) -> None: - from mcp.client.session_group import ClientSessionGroup, StreamableHttpParameters - - await self.disconnect() - group = ClientSessionGroup() - await group.__aenter__() - try: - await group.connect_to_server(StreamableHttpParameters(url=endpoint)) - except Exception: - with contextlib.suppress(Exception): - await group.__aexit__(None, None, None) - raise - self._group = group - self.endpoint = endpoint - - async def disconnect(self) -> None: - if self._group is None: - self.endpoint = None - return - group = self._group - self._group = None - self.endpoint = None - with contextlib.suppress(Exception): - await group.__aexit__(None, None, None) - - def list_tool_names(self) -> list[str]: - if self._group is None: - return [] - names: list[str] = [] - for _server_name, tool in self._group.list_tools(): - name = getattr(tool, "name", None) - if isinstance(name, str): - names.append(name) - return sorted(set(names)) - - def has_tool(self, name: str) -> bool: - if self._group is None: - return False - try: - tool = self._group.get_tool(name) - return tool is not None - except Exception: - return False - - def build_openai_tools(self) -> list[dict[str, Any]]: - if self._group is None: - return [] - tools: list[dict[str, Any]] = [] - for _server_name, tool in self._group.list_tools(): - name = getattr(tool, "name", None) - if not isinstance(name, str) or not name: - continue - description = str(getattr(tool, "description", "") or "") - schema = getattr(tool, "inputSchema", None) - if not isinstance(schema, dict): - schema = {"type": "object", "properties": {}} - if schema.get("type") != "object": - schema = {"type": "object", "properties": {}} - tools.append( - { - "type": "function", - "function": { - "name": name, - "description": description, - "parameters": schema, - }, - } - ) - return tools - - async def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: - if self._group is None: - return {"error": "mcp not connected"} - try: - result = await self._group.call_tool(name, arguments) - content: list[dict[str, Any]] = [] - for part in result.content: - if hasattr(part, "model_dump"): - content.append(part.model_dump()) # type: ignore[call-arg] - elif isinstance(part, dict): - content.append(part) - else: - content.append({"value": str(part)}) - return { - "is_error": bool(result.isError), - "structured_content": result.structuredContent, - "content": content, - } - except Exception as exc: - return {"error": "mcp call failed", "tool": name, "detail": str(exc)} - - -def _print_chat_config(settings: ChatSettings, mcp_client: ChatMCPClient) -> None: - print(f"{C.BLUE}chat config{C.RESET}") - print(f" {C.DIM}model:{C.RESET} {settings.model}") - print(f" {C.DIM}reasoning:{C.RESET} {settings.reasoning}") - print(f" {C.DIM}stream:{C.RESET} {settings.stream}") - print(f" {C.DIM}json:{C.RESET} {settings.json_mode}") - print(f" {C.DIM}tools:{C.RESET} {settings.default_tools}") - print(f" {C.DIM}max_tokens:{C.RESET} {settings.max_tokens}") - print(f" {C.DIM}temperature:{C.RESET} {settings.temperature}") - print(f" {C.DIM}top_p:{C.RESET} {settings.top_p}") - print(f" {C.DIM}max_rounds:{C.RESET} {settings.max_tool_rounds}") - print(f" {C.DIM}system:{C.RESET} {settings.system_prompt or ''}") - print(f" {C.DIM}mcp:{C.RESET} {mcp_client.endpoint or ''}") - - -def _parse_on_off(value: str) -> bool | None: - v = value.strip().lower() - if v in {"on", "true", "1", "yes"}: - return True - if v in {"off", "false", "0", "no"}: - return False - return None - - -def _resolve_image_path(raw_path: str) -> Path: - path = Path(raw_path).expanduser().resolve() - if not path.is_file(): - raise FileNotFoundError(f"image file not found: {path}") - return path - - -def _guess_mime_type(path: Path) -> str: - mime, _ = mimetypes.guess_type(str(path)) - return mime or "image/jpeg" - - -def _normalize_image_for_server(image_bytes: bytes, mime: str) -> tuple[bytes, str, bool]: - mime_l = mime.lower() - if mime_l in SUPPORTED_SERVER_MIME_TYPES: - return image_bytes, mime_l, False - try: - from PIL import Image - except Exception as exc: - raise RuntimeError( - f"image mime {mime!r} is not supported by server decoder and Pillow is unavailable: {exc}" - ) from exc - - from io import BytesIO - - try: - with Image.open(BytesIO(image_bytes)) as im: - rgb = im.convert("RGB") - out = BytesIO() - rgb.save(out, format="PNG") - return out.getvalue(), "image/png", True - except Exception as exc: - raise RuntimeError(f"failed to transcode unsupported image mime {mime!r}: {exc}") from exc - - -def _resolve_image_bytes_and_mime(image_path_or_url: str) -> tuple[bytes, str, str]: - if image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://"): - with httpx.Client(timeout=60.0) as client: - resp = client.get(image_path_or_url) - resp.raise_for_status() - content_type = (resp.headers.get("Content-Type") or "").split(";")[0].strip().lower() - mime = content_type if content_type.startswith("image/") else "image/jpeg" - size_kib = len(resp.content) / 1024.0 - image_bytes, mime, transcoded = _normalize_image_for_server(resp.content, mime) - desc = f"url={image_path_or_url} size={size_kib:.1f} KiB mime={mime}" - if transcoded: - desc += " (transcoded)" - return image_bytes, mime, desc - - path = _resolve_image_path(image_path_or_url) - size_kib = path.stat().st_size / 1024.0 - mime = _guess_mime_type(path) - image_bytes, mime, transcoded = _normalize_image_for_server(path.read_bytes(), mime) - desc = f"path={path} size={size_kib:.1f} KiB mime={mime}" - if transcoded: - desc += " (transcoded)" - return image_bytes, mime, desc - - -def _to_data_url(image_bytes: bytes, mime: str) -> str: - payload = base64.b64encode(image_bytes).decode("ascii") - return f"data:{mime};base64,{payload}" - - -def _make_user_message(text: str, image_data_url: str | None) -> dict[str, Any]: - if image_data_url is None: - return {"role": "user", "content": text} - return { - "role": "user", - "content": [ - {"type": "text", "text": text}, - {"type": "image_url", "image_url": {"url": image_data_url}}, - ], - } - - -def _build_default_tools() -> list[dict[str, Any]]: - return [ - { - "type": "function", - "function": { - "name": "web_search", - "description": "Search the web for a query and return top results.", - "parameters": { - "type": "object", - "properties": { - "query": {"type": "string", "description": "Search query."}, - "max_results": { - "type": "integer", - "description": "Number of results to return (1-10).", - "default": 5, - }, - }, - "required": ["query"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "web_fetch", - "description": "Fetch and extract readable text from a URL.", - "parameters": { - "type": "object", - "properties": { - "url": {"type": "string", "description": "Absolute http/https URL."}, - "max_chars": { - "type": "integer", - "description": "Max number of characters to return (500-20000).", - "default": 8000, - }, - }, - "required": ["url"], - }, - }, - }, - ] - - -def _tool_web_search(arguments: dict[str, Any]) -> dict[str, Any]: - query = str(arguments.get("query", "")).strip() - if not query: - return {"error": "query is required"} - max_results = arguments.get("max_results", 5) - try: - max_results = int(max_results) - except (TypeError, ValueError): - max_results = 5 - max_results = max(1, min(max_results, 10)) - try: - from ddgs import DDGS - except Exception as exc: - return { - "error": "ddgs is not installed or failed to import", - "detail": str(exc), - "hint": "pip install ddgs", - } - rows: list[dict[str, Any]] = [] - try: - with DDGS() as ddgs: - for r in ddgs.text(query, max_results=max_results): - if len(rows) >= max_results: - break - rows.append( - { - "title": r.get("title"), - "url": r.get("href") or r.get("url"), - "snippet": r.get("body") or r.get("snippet"), - } - ) - except Exception as exc: - return {"error": "web_search failed", "detail": str(exc)} - return {"query": query, "count": len(rows), "results": rows} - - -def _tool_web_fetch(arguments: dict[str, Any]) -> dict[str, Any]: - url = str(arguments.get("url", "")).strip() - if not url: - return {"error": "url is required"} - max_chars = arguments.get("max_chars", 8000) - try: - max_chars = int(max_chars) - except (TypeError, ValueError): - max_chars = 8000 - max_chars = max(500, min(max_chars, 20000)) - try: - import trafilatura - except Exception as exc: - return { - "error": "trafilatura is not installed or failed to import", - "detail": str(exc), - "hint": "pip install trafilatura", - } - try: - downloaded = trafilatura.fetch_url(url) - if not downloaded: - return {"error": "failed to download url", "url": url} - text = trafilatura.extract(downloaded, include_links=False, include_images=False) - if not text: - return {"error": "failed to extract readable text", "url": url} - text = text.strip() - truncated = len(text) > max_chars - return { - "url": url, - "content": text[:max_chars], - "truncated": truncated, - "content_chars": min(len(text), max_chars), - } - except Exception as exc: - return {"error": "web_fetch failed", "url": url, "detail": str(exc)} - - -def _execute_default_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: - if name == "web_search": - return _tool_web_search(arguments) - if name == "web_fetch": - return _tool_web_fetch(arguments) - return {"error": f"unknown tool '{name}'"} - - -def _print_history(messages: list[dict[str, Any]]) -> None: - for idx, msg in enumerate(messages): - role = str(msg.get("role", "unknown")) - if role == "assistant" and msg.get("tool_calls"): - text = f"[tool_calls={len(msg.get('tool_calls') or [])}]" - else: - content = msg.get("content", "") - if isinstance(content, list): - text = json.dumps(content, ensure_ascii=True) - else: - text = str(content) - text = text.replace("\n", " ") - if len(text) > 160: - text = text[:157] + "..." - print(f"{idx:03d} {role:9s} {text}") - - -def _build_chat_payload( - *, - model: str, - messages: list[dict[str, Any]], - settings: ChatSettings, - stream: bool, - tools: list[dict[str, Any]] | None, -) -> dict[str, Any]: - body: dict[str, Any] = { - "model": model, - "messages": messages, - "stream": stream, - "reasoning": {"enabled": bool(settings.reasoning)}, - "max_tokens": int(settings.max_tokens), - } - if settings.temperature is not None: - body["temperature"] = settings.temperature - if settings.top_p is not None: - body["top_p"] = settings.top_p - if stream: - body["stream_options"] = {"include_usage": True} - if tools: - body["tools"] = tools - body["tool_choice"] = "auto" - return body - - -def _print_reasoning_and_response(reasoning_text: str, response_text: str, show_reasoning: bool) -> None: - if show_reasoning and reasoning_text: - print(f"{C.GRAY}thinking:{C.RESET}") - print(f"{C.GRAY}{reasoning_text}{C.RESET}") - if response_text: - print() - if response_text: - print(response_text) - - -def _print_repl_commands(prefix: str | None = None) -> None: - command_pool = [cmd for cmd in REPL_COMMANDS if cmd != "/"] - if prefix is None: - matches = command_pool - else: - matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] - if not matches: - print(f"{C.YELLOW}no command matches: {prefix}{C.RESET}") - return - rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in matches) - print(f"commands: {rendered}") - - -def _install_repl_completer(commands: list[str]) -> Callable[[], None] | None: - if readline is None: - return None - try: - prev_completer = readline.get_completer() - prev_delims = readline.get_completer_delims() - prev_display_hook = getattr(readline, "get_completion_display_matches_hook", lambda: None)() - readline.parse_and_bind("tab: complete") - readline.parse_and_bind("set show-all-if-ambiguous on") - readline.parse_and_bind("set show-all-if-unmodified on") - readline.parse_and_bind("set completion-ignore-case on") - readline.set_completer_delims(" \t\n") - matches: list[str] = [] - - def _complete(text: str, state: int) -> str | None: - nonlocal matches - if state == 0: - buffer = readline.get_line_buffer().lstrip() - if buffer.startswith("/"): - prefix = buffer.split()[0] - command_pool = [cmd for cmd in commands if cmd != "/"] - if prefix == "/": - matches = command_pool - else: - matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] - else: - matches = [] - if state < len(matches): - return matches[state] - return None - - readline.set_completer(_complete) - if hasattr(readline, "set_completion_display_matches_hook"): - def _display_matches(substitution: str, display_matches: list[str], longest_match_length: int) -> None: - del substitution, longest_match_length - if not display_matches: - return - print() - rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in display_matches) - print(f"commands: {rendered}") - try: - readline.redisplay() - except Exception: - pass - readline.set_completion_display_matches_hook(_display_matches) - - def _cleanup() -> None: - try: - readline.set_completer(prev_completer) - readline.set_completer_delims(prev_delims) - if hasattr(readline, "set_completion_display_matches_hook"): - readline.set_completion_display_matches_hook(prev_display_hook) - except Exception: - pass - - return _cleanup - except Exception: - return None - - -class StreamAbortWatcher: - def __init__(self) -> None: - self.enabled = bool(sys.stdin.isatty() and termios is not None and tty is not None) - self._fd: int | None = None - self._old_attrs: Any = None - self._thread: threading.Thread | None = None - self._stop = threading.Event() - self._abort_reason: str | None = None - - def __enter__(self) -> "StreamAbortWatcher": - if not self.enabled: - return self - try: - self._fd = sys.stdin.fileno() - self._old_attrs = termios.tcgetattr(self._fd) - tty.setcbreak(self._fd) - except Exception: - self.enabled = False - return self - self._thread = threading.Thread(target=self._watch, daemon=True) - self._thread.start() - return self - - def _watch(self) -> None: - if self._fd is None: - return - while not self._stop.is_set(): - try: - ready, _, _ = select.select([self._fd], [], [], 0.1) - except Exception: - return - if not ready: - continue - try: - ch = os.read(self._fd, 1) - except Exception: - continue - if not ch: - continue - if ch == b"\x1b": - self._abort_reason = "esc" - self._stop.set() - return - - def aborted(self) -> bool: - return self._abort_reason is not None - - def __exit__(self, exc_type, exc, tb) -> bool: - self._stop.set() - if self._thread: - self._thread.join(timeout=0.2) - if self.enabled and self._fd is not None and self._old_attrs is not None: - try: - termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_attrs) - except Exception: - pass - return False - - -def _run_single_chat_request( - *, - client: httpx.Client, - url: str, - headers: dict[str, str], - payload: dict[str, Any], - settings: ChatSettings, - stream: bool, -) -> tuple[dict[str, Any], dict[str, Any] | None, bool]: - wait_anim = MushroomPulse("thinking") - wait_anim.start() - if stream: - content_parts: list[str] = [] - reasoning_parts: list[str] = [] - usage: dict[str, Any] | None = None - tool_calls_by_index: dict[int, dict[str, Any]] = {} - reasoning_stream_started = False - interrupted = False - first_event_seen = False - - try: - with StreamAbortWatcher() as abort_watcher: - with client.stream("POST", url, headers=headers, json=payload) as resp: - resp.raise_for_status() - for raw in resp.iter_lines(): - if abort_watcher.aborted(): - interrupted = True - break - if not raw: - continue - line = raw.strip() - if not line.startswith("data:"): - continue - data = line[len("data:"):].strip() - if data == "[DONE]": - break - try: - evt = json.loads(data) - except Exception: - continue - if not first_event_seen: - wait_anim.stop() - first_event_seen = True - - if isinstance(evt.get("usage"), dict): - usage = evt.get("usage") - - choices = evt.get("choices") - if not isinstance(choices, list) or not choices: - continue - c0 = choices[0] - if not isinstance(c0, dict): - continue - delta = c0.get("delta", {}) - if not isinstance(delta, dict): - continue - - reasoning_chunk = delta.get("reasoning") - if isinstance(reasoning_chunk, str) and reasoning_chunk: - reasoning_parts.append(reasoning_chunk) - if settings.reasoning: - if not reasoning_stream_started: - print(f"{C.GRAY}thinking:{C.RESET}") - reasoning_stream_started = True - print(f"{C.GRAY}{reasoning_chunk}{C.RESET}", end="", flush=True) - - content_chunk = delta.get("content") - if isinstance(content_chunk, str) and content_chunk: - content_parts.append(content_chunk) - print(content_chunk, end="", flush=True) - - for tc in delta.get("tool_calls") or []: - if not isinstance(tc, dict): - continue - idx = tc.get("index") - if not isinstance(idx, int): - idx = len(tool_calls_by_index) - entry = tool_calls_by_index.setdefault( - idx, - { - "id": tc.get("id", ""), - "type": tc.get("type", "function"), - "function": {"name": "", "arguments": ""}, - }, - ) - if tc.get("id"): - entry["id"] = tc["id"] - if tc.get("type"): - entry["type"] = tc["type"] - fn = tc.get("function") or {} - if isinstance(fn, dict): - if fn.get("name"): - entry["function"]["name"] += str(fn["name"]) - if fn.get("arguments"): - entry["function"]["arguments"] += str(fn["arguments"]) - except KeyboardInterrupt: - interrupted = True - finally: - wait_anim.stop() - - msg: dict[str, Any] = {"role": "assistant", "content": "".join(content_parts).strip()} - reasoning_text = "".join(reasoning_parts).strip() - if reasoning_text: - msg["reasoning_content"] = reasoning_text - if tool_calls_by_index: - msg["tool_calls"] = [tool_calls_by_index[i] for i in sorted(tool_calls_by_index)] - if settings.reasoning: - if reasoning_stream_started: - print() - response_text = str(msg.get("content") or "") - if response_text: - print() - print(response_text) - elif content_parts: - print() - if interrupted: - print(f"{C.YELLOW}response interrupted{C.RESET}") - return msg, usage, interrupted - - try: - resp = client.post(url, headers=headers, json=payload, timeout=120.0) - resp.raise_for_status() - body = resp.json() - finally: - wait_anim.stop() - if settings.json_mode: - print(json.dumps(body, indent=2)) - - choices = body.get("choices", []) - c0 = choices[0] if isinstance(choices, list) and choices else {} - msg = c0.get("message", {}) if isinstance(c0, dict) else {} - if not isinstance(msg, dict): - msg = {} - out: dict[str, Any] = {"role": "assistant", "content": str(msg.get("content", "") or "")} - if isinstance(msg.get("reasoning"), str) and msg.get("reasoning"): - out["reasoning_content"] = msg["reasoning"] - if isinstance(msg.get("tool_calls"), list): - out["tool_calls"] = msg.get("tool_calls") - - _print_reasoning_and_response( - str(out.get("reasoning_content") or ""), - str(out.get("content") or ""), - bool(settings.reasoning), - ) - return out, body.get("usage") if isinstance(body.get("usage"), dict) else None, False - - -async def _run_chat_turn( - *, - client: httpx.Client, - url: str, - headers: dict[str, str], - model: str, - settings: ChatSettings, - mcp_client: ChatMCPClient, - messages: list[dict[str, Any]], - user_message: dict[str, Any], -) -> int: - messages.append(user_message) - - max_rounds = max(1, int(settings.max_tool_rounds)) - for _ in range(max_rounds): - stream = settings.stream and not settings.json_mode - tools: list[dict[str, Any]] = [] - if settings.default_tools: - tools.extend(_build_default_tools()) - if mcp_client.connected: - tools.extend(mcp_client.build_openai_tools()) - - payload = _build_chat_payload( - model=model, - messages=messages, - settings=settings, - stream=stream, - tools=tools or None, - ) - assistant_msg, usage, interrupted = _run_single_chat_request( - client=client, url=url, headers=headers, payload=payload, settings=settings, stream=stream - ) - messages.append(assistant_msg) - if isinstance(usage, dict): - image_tokens = usage.get("image_tokens") or 0 - image_tokens_part = f", image: {image_tokens}" if image_tokens else "" - image_tps_part = f", image tps: {usage.get('image_tokens_per_second') or ''}" if image_tokens else "" - print( - f"{C.DIM}[usage] tokens(prompt: {usage.get('prompt_tokens') or ''}, completion: {usage.get('completion_tokens') or ''}, total: {usage.get('total_tokens') or ''}{image_tokens_part}) usage(decode tps: {usage.get('decode_tokens_per_second') or ''}, prefill tps: {usage.get('prefill_tokens_per_second') or ''}{image_tps_part}) itl: {usage.get('itl_ms') or ''}ms ttft: {usage.get('ttft_ms') or ''}ms{C.RESET}" - ) - if interrupted: - return 130 - - tool_calls = assistant_msg.get("tool_calls") if isinstance(assistant_msg, dict) else None - if not tools or not isinstance(tool_calls, list) or not tool_calls: - return 0 - - for tool_call in tool_calls: - if not isinstance(tool_call, dict): - continue - fn = tool_call.get("function") or {} - if not isinstance(fn, dict): - continue - name = str(fn.get("name") or "") - raw_args = str(fn.get("arguments") or "{}") - try: - parsed_args = json.loads(raw_args) - except json.JSONDecodeError: - parsed_args = {"_raw": raw_args} - if name in {"web_search", "web_fetch"}: - print(f"{C.CYAN}{HAMMER} tool{C.RESET} {name}") - tool_result = _execute_default_tool(name, parsed_args) - elif mcp_client.has_tool(name): - print(f"{C.CYAN}{HAMMER} mcp{C.RESET} {name}") - tool_result = await mcp_client.call_tool(name, parsed_args) - else: - print(f"{C.YELLOW}{WARN} unknown tool{C.RESET} {name}") - tool_result = {"error": f"unknown tool '{name}'"} - messages.append( - { - "role": "tool", - "tool_call_id": tool_call.get("id", ""), - "content": json.dumps(tool_result, ensure_ascii=False), - } - ) - - warn("Reached max tool rounds without a final assistant response") - return 1 - - -async def cmd_chat(args, storage: StorageService) -> int: - prompt_words = getattr(args, "prompt_words", None) - prompt = " ".join(prompt_words).strip() if prompt_words else "" - - device, ip = await _resolve_connected_device(storage) - if not device or not ip: - return 1 - - spinner = Spinner("Resolving default model") - spinner.start() - model = await _default_model(ip) - if not model: - spinner.fail("Failed to resolve default model from IF2") - return 1 - spinner.stop(success=True) - - settings = ChatSettings(model=model) - mcp_client = ChatMCPClient() - messages: list[dict[str, Any]] = [] - pending_image_data_url: str | None = None - pending_image_desc: str | None = None - - url = f"http://{ip}/if2/v1/chat/completions" - headers = {"Content-Type": "application/json"} - - try: - spinner = Spinner(f"Connecting to {device}") - spinner.start() - with httpx.Client(timeout=None) as client: - spinner.stop(success=True) - - # REPL mode (default). - print(f"{C.DIM}model: {settings.model}{C.RESET}") - print( - f"{C.DIM}commands: /help, /history, /reset, /models, /attach, /config, /mcp, /exit{C.RESET}" - ) - - cleanup_repl = _install_repl_completer(REPL_COMMANDS) - try: - if prompt: - print(f"{C.CYAN}> {prompt}{C.RESET}") - rc = await _run_chat_turn( - client=client, - url=url, - headers=headers, - model=settings.model, - settings=settings, - mcp_client=mcp_client, - messages=messages, - user_message=_make_user_message(prompt, pending_image_data_url), - ) - if rc != 0: - if rc == 130: - return 0 - else: - return rc - else: - pending_image_data_url = None - pending_image_desc = None - - while True: - try: - line = input(f"{C.CYAN}> {C.RESET}").strip() - except EOFError: - print() - return 0 - except KeyboardInterrupt: - print() - return 0 - - if not line: - continue - if line in {"/", "/help"}: - _print_repl_commands() - continue - if line in {"/exit", "/quit"}: - return 0 - if line == "/history": - _print_history(messages) - continue - if line == "/reset": - messages = [] - if settings.system_prompt: - messages.append({"role": "system", "content": settings.system_prompt}) - pending_image_data_url = None - pending_image_desc = None - print(f"{C.YELLOW}history reset (and cleared pending attachment){C.RESET}") - continue - if line in {"/models", "/model"}: - try: - models = _fetch_models_payload(client, ip) - selected_model = _pick_model_interactive(models, settings.model) - if selected_model and selected_model != settings.model: - settings.model = selected_model - print(f"{C.GREEN}{CHECK}{C.RESET} model switched: {settings.model}") - except Exception as exc: - error(f"failed to list models: {exc}") - continue - if line == "/config": - _print_chat_config(settings, mcp_client) - continue - if line.startswith("/reasoning"): - arg = line[len("/reasoning"):].strip() - if not arg: - print(f"{C.DIM}reasoning={settings.reasoning}{C.RESET}") - continue - val = _parse_on_off(arg) - if val is None: - warn("usage: /reasoning ") - continue - settings.reasoning = val - print(f"{C.GREEN}{CHECK}{C.RESET} reasoning={settings.reasoning}") - continue - if line.startswith("/stream"): - arg = line[len("/stream"):].strip() - if not arg: - print(f"{C.DIM}stream={settings.stream}{C.RESET}") - continue - val = _parse_on_off(arg) - if val is None: - warn("usage: /stream ") - continue - settings.stream = val - print(f"{C.GREEN}{CHECK}{C.RESET} stream={settings.stream}") - continue - if line.startswith("/json"): - arg = line[len("/json"):].strip() - if not arg: - print(f"{C.DIM}json={settings.json_mode}{C.RESET}") - continue - val = _parse_on_off(arg) - if val is None: - warn("usage: /json ") - continue - settings.json_mode = val - print(f"{C.GREEN}{CHECK}{C.RESET} json={settings.json_mode}") - continue - if line.startswith("/tools"): - arg = line[len("/tools"):].strip() - if not arg: - print(f"{C.DIM}tools={settings.default_tools}{C.RESET}") - continue - val = _parse_on_off(arg) - if val is None: - warn("usage: /tools ") - continue - settings.default_tools = val - print(f"{C.GREEN}{CHECK}{C.RESET} tools={settings.default_tools}") - continue - if line.startswith("/max_tokens"): - arg = line[len("/max_tokens"):].strip() - if not arg: - print(f"{C.DIM}max_tokens={settings.max_tokens}{C.RESET}") - continue - try: - settings.max_tokens = max(1, int(arg)) - print(f"{C.GREEN}{CHECK}{C.RESET} max_tokens={settings.max_tokens}") - except ValueError: - warn("usage: /max_tokens ") - continue - if line.startswith("/temperature"): - arg = line[len("/temperature"):].strip() - if not arg: - print(f"{C.DIM}temperature={settings.temperature}{C.RESET}") - continue - if arg.lower() in {"off", "none"}: - settings.temperature = None - print(f"{C.GREEN}{CHECK}{C.RESET} temperature=None") - continue - try: - settings.temperature = float(arg) - print(f"{C.GREEN}{CHECK}{C.RESET} temperature={settings.temperature}") - except ValueError: - warn("usage: /temperature ") - continue - if line.startswith("/top_p"): - arg = line[len("/top_p"):].strip() - if not arg: - print(f"{C.DIM}top_p={settings.top_p}{C.RESET}") - continue - if arg.lower() in {"off", "none"}: - settings.top_p = None - print(f"{C.GREEN}{CHECK}{C.RESET} top_p=None") - continue - try: - settings.top_p = float(arg) - print(f"{C.GREEN}{CHECK}{C.RESET} top_p={settings.top_p}") - except ValueError: - warn("usage: /top_p ") - continue - if line.startswith("/max_rounds"): - arg = line[len("/max_rounds"):].strip() - if not arg: - print(f"{C.DIM}max_rounds={settings.max_tool_rounds}{C.RESET}") - continue - try: - settings.max_tool_rounds = max(1, int(arg)) - print(f"{C.GREEN}{CHECK}{C.RESET} max_rounds={settings.max_tool_rounds}") - except ValueError: - warn("usage: /max_rounds ") - continue - if line.startswith("/system"): - arg = line[len("/system"):].strip() - if not arg: - print(f"{C.DIM}system={settings.system_prompt or ''}{C.RESET}") - continue - if arg.lower() in {"off", "none", "clear"}: - settings.system_prompt = None - if messages and messages[0].get("role") == "system": - messages.pop(0) - print(f"{C.GREEN}{CHECK}{C.RESET} system prompt cleared") - continue - settings.system_prompt = arg - if messages and messages[0].get("role") == "system": - messages[0]["content"] = arg - else: - messages.insert(0, {"role": "system", "content": arg}) - print(f"{C.GREEN}{CHECK}{C.RESET} system prompt updated") - continue - if line.startswith("/mcp"): - parts = line.split(maxsplit=2) - if len(parts) == 1 or parts[1] == "status": - print( - f"{C.BLUE}/mcp status{C.RESET} " - f"{C.DIM}mcp={mcp_client.endpoint or ''} " - f"tools={len(mcp_client.list_tool_names())}{C.RESET}" - ) - print( - f"{C.DIM}subcommands:{C.RESET} " - f"{C.BLUE}/mcp connect {C.RESET}, " - f"{C.BLUE}/mcp tools{C.RESET}, " - f"{C.BLUE}/mcp disconnect{C.RESET}" - ) - continue - sub = parts[1].lower() - if sub == "connect": - if len(parts) < 3: - warn("usage: /mcp connect ") - continue - endpoint = parts[2].strip() - if not endpoint.startswith(("http://", "https://")): - warn("mcp endpoint must start with http:// or https://") - continue - try: - await mcp_client.connect_streamable_http(endpoint) - print( - f"{C.BLUE}/mcp connect{C.RESET} " - f"{C.GREEN}{CHECK}{C.RESET} {endpoint} " - f"({len(mcp_client.list_tool_names())} tools)" - ) - except Exception as exc: - error(f"mcp connect failed: {exc}") - continue - if sub == "disconnect": - await mcp_client.disconnect() - print(f"{C.BLUE}/mcp disconnect{C.RESET} {C.GREEN}{CHECK}{C.RESET}") - continue - if sub == "tools": - names = mcp_client.list_tool_names() - if not names: - print(f"{C.BLUE}/mcp tools{C.RESET} {C.DIM}no tools available{C.RESET}") - else: - print(f"{C.BLUE}/mcp tools{C.RESET} {', '.join(names)}") - continue - warn("usage: /mcp ") - continue - if line.startswith("/attach"): - parts = line.split(maxsplit=1) - if len(parts) != 2 or not parts[1].strip(): - warn("usage: /attach ") - continue - src = parts[1].strip() - try: - image_bytes, mime, desc = _resolve_image_bytes_and_mime(src) - pending_image_data_url = _to_data_url(image_bytes, mime) - pending_image_desc = desc - print(f"{C.GREEN}{CHECK}{C.RESET} attachment ready: {desc}") - except FileNotFoundError as exc: - error(str(exc)) - except httpx.HTTPError as exc: - error(f"failed to fetch image: {exc}") - except RuntimeError as exc: - error(str(exc)) - continue - if line.startswith("/"): - matches = [cmd for cmd in REPL_COMMANDS if cmd.startswith(line)] - if matches: - _print_repl_commands(line) - else: - warn(f"unknown command: {line}") - _print_repl_commands() - continue - - if pending_image_data_url is not None: - print(f"{C.MAGENTA}[attach]{C.RESET} sending with image: {pending_image_desc}") - rc = await _run_chat_turn( - client=client, - url=url, - headers=headers, - model=settings.model, - settings=settings, - mcp_client=mcp_client, - messages=messages, - user_message=_make_user_message(line, pending_image_data_url), - ) - if rc != 0: - if rc == 130: - return 0 - return rc - pending_image_data_url = None - pending_image_desc = None - finally: - if cleanup_repl: - cleanup_repl() - await mcp_client.disconnect() - return 0 - except Exception as e: - try: - spinner.fail(f"Chat request failed: {e}") # type: ignore[name-defined] - except Exception: - error(f"Chat request failed: {e}") - return 1 - - -async def cmd_scan(args, storage: StorageService) -> int: - try: - from zeroconf import ServiceBrowser, ServiceListener, Zeroconf, IPVersion - except ImportError: - error("zeroconf package required for scanning") - print(f" {C.DIM}pip install zeroconf{C.RESET}") - return 1 - - devices: dict[str, dict] = {} - scan_done = asyncio.Event() - - class TruffleListener(ServiceListener): - def add_service(self, zc: Zeroconf, type_: str, name: str): - if name.lower().startswith("truffle-"): - info = zc.get_service_info(type_, name) - device_name = name.split(".")[0] - if info and device_name not in devices: - addresses = [addr for addr in info.parsed_addresses(IPVersion.V4Only)] - devices[device_name] = { - "name": device_name, - "addresses": addresses, - "port": info.port, - } - - def remove_service(self, zc: Zeroconf, type_: str, name: str): - pass - - def update_service(self, zc: Zeroconf, type_: str, name: str): - pass - - timeout = args.timeout if hasattr(args, 'timeout') else 5 - - spinner = Spinner(f"Scanning for Truffle devices ({timeout}s)") - spinner.start() - - try: - zc = Zeroconf(ip_version=IPVersion.V4Only) - listener = TruffleListener() - - browsers = [ - ServiceBrowser(zc, "_truffle._tcp.local.", listener), - ] - - await asyncio.sleep(timeout) - - for browser in browsers: - browser.cancel() - zc.close() - - except Exception as e: - spinner.fail(f"Scan failed: {e}") - return 1 - - spinner.stop(success=True) - - if not devices: - print() - print(f" {C.DIM}No Truffle devices found on the network{C.RESET}") - print() - print(f" {C.DIM}Make sure your Truffle is:{C.RESET}") - print(f" {C.DIM}โ€ข Powered on{C.RESET}") - print(f" {C.DIM}โ€ข Connected to the same network as this computer{C.RESET}") - print() - return 1 - - print() - print(f"{C.BOLD}Found {len(devices)} Truffle device(s):{C.RESET}") - print() - - device_list = list(devices.values()) - for i, device in enumerate(device_list, 1): - name = device["name"] - addrs = ", ".join(device["addresses"]) if device["addresses"] else "unknown" - - already_connected = storage.get_token(name) is not None - if already_connected: - print(f" {C.GREEN}{i}.{C.RESET} {C.BOLD}{name}{C.RESET} {C.DIM}({addrs}){C.RESET} {C.GREEN}[connected]{C.RESET}") - else: - print(f" {C.CYAN}{i}.{C.RESET} {C.BOLD}{name}{C.RESET} {C.DIM}({addrs}){C.RESET}") - - print() - - try: - choice = input(f"Select device to connect (1-{len(device_list)}) or press Enter to cancel: ").strip() - except (KeyboardInterrupt, EOFError): - print() - return 0 - - if not choice: - return 0 - - try: - idx = int(choice) - 1 - if 0 <= idx < len(device_list): - selected = device_list[idx] - print() - - class FakeArgs: - device = selected["name"] - - return await cmd_connect(FakeArgs(), storage) - else: - error("Invalid selection") - return 1 - except ValueError: - error("Invalid input") - return 1 - - -def cmd_validate(args) -> int: - app_dir = Path(args.path).resolve() - if not app_dir.exists() or not app_dir.is_dir(): - error(f"{app_dir} is not a valid directory") - return 1 - - info(f"Validating app in {app_dir.name}") - valid, _config, app_type, warnings, errors = validate_app_dir(app_dir) - for w in warnings: - warn(w) - if not valid: - for e in errors: - error(e) - return 1 - - success(f"Validation passed ({app_type})") - return 0 - - -def print_help(): - if sys.stdout.isatty(): - intro = MushroomPulse("truffile", interval=0.08) - intro.start() - time.sleep(0.65) - intro.stop() - print(f"{MUSHROOM} {C.BOLD}truffile{C.RESET} - TruffleOS SDK") - print() - print(f"{C.BOLD}Usage:{C.RESET} truffile [options]") - print() - print(f"{C.BOLD}Commands:{C.RESET}") - print(f" {C.BLUE}scan{C.RESET} Scan network for Truffle devices") - print(f" {C.BLUE}connect{C.RESET} Connect to a Truffle device") - print(f" {C.BLUE}disconnect{C.RESET} Disconnect and clear credentials") - print(f" {C.BLUE}create{C.RESET} [name] Create a new app scaffold") - print(f" {C.BLUE}deploy{C.RESET} [path] Deploy an app (reads type from truffile.yaml)") - print(f" {C.BLUE}validate{C.RESET} [path] Validate app config and files") - print(f" {C.BLUE}delete{C.RESET} Delete installed apps from device") - print(f" {C.BLUE}list{C.RESET} List installed apps or devices") - print(f" {C.BLUE}models{C.RESET} List models on your Truffle") - print(f" {C.BLUE}chat{C.RESET} Chat on your Truffle (REPL by default)") - print() - print(f"{C.BOLD}Examples:{C.RESET}") - print(f" {C.DIM}truffile scan{C.RESET} {C.DIM}# find devices on network{C.RESET}") - print(f" {C.DIM}truffile connect truffle-6272{C.RESET}") - print(f" {C.DIM}truffile create my-app{C.RESET}") - print(f" {C.DIM}truffile create{C.RESET} {C.DIM}# prompts for app name + path{C.RESET}") - print(f" {C.DIM}truffile deploy ./my-app{C.RESET}") - print(f" {C.DIM}truffile deploy --dry-run ./my-app{C.RESET}") - print(f" {C.DIM}truffile deploy{C.RESET} {C.DIM}# uses current directory{C.RESET}") - print(f" {C.DIM}truffile validate ./my-app{C.RESET}") - print(f" {C.DIM}truffile list apps{C.RESET}") - print(f" {C.DIM}truffile models{C.RESET} {C.DIM}# show models on your Truffle{C.RESET}") - print(f" {C.DIM}truffile chat{C.RESET} {C.DIM}# open interactive REPL chat{C.RESET}") - print( - f" {C.DIM}# in chat: /help, /attach , /config, /reasoning on|off, /mcp connect {C.RESET}" - ) - print() - - -def main() -> int: - if len(sys.argv) == 1 or sys.argv[1] in ("-h", "--help"): - print_help() - return 0 - - parser = argparse.ArgumentParser( - prog="truffile", - description="truffile - TruffleOS SDK CLI", - add_help=False, - ) - subparsers = parser.add_subparsers(dest="command") - - p_scan = subparsers.add_parser("scan", add_help=False) - p_scan.add_argument("-t", "--timeout", type=int, default=5, help="Scan timeout in seconds") - - p_connect = subparsers.add_parser("connect", add_help=False) - p_connect.add_argument("device", nargs="?") - - p_disconnect = subparsers.add_parser("disconnect", add_help=False) - p_disconnect.add_argument("target", nargs="?") - - p_create = subparsers.add_parser("create", add_help=False) - p_create.add_argument("name", nargs="?") - p_create.add_argument("--path", "-p", help="Base directory (default: prompt, Enter uses cwd)") - - - p_deploy = subparsers.add_parser("deploy", add_help=False) - p_deploy.add_argument("path", nargs="?", default=".") - p_deploy.add_argument("-i", "--interactive", action="store_true", help="Interactive terminal mode") - p_deploy.add_argument("--dry-run", action="store_true", help="Show deploy plan without mutating device") - - p_validate = subparsers.add_parser("validate", add_help=False) - p_validate.add_argument("path", nargs="?", default=".") - - p_delete = subparsers.add_parser("delete", add_help=False) - - p_list = subparsers.add_parser("list", add_help=False) - p_list.add_argument("what", choices=["apps", "devices"], nargs="?") - - p_models = subparsers.add_parser("models", add_help=False) - - p_chat = subparsers.add_parser("chat", add_help=False) - p_chat.add_argument("prompt_words", nargs="*", help="Optional initial prompt to send (for non-REPL mode)") - args = parser.parse_args() - - if args.command is None: - print_help() - return 0 - - if args.command == "connect": - if not args.device: - error("Missing device name") - print(f" {C.DIM}Usage: truffile connect {C.RESET}") - return 1 - elif args.command == "disconnect": - if not args.target: - error("Missing device name") - print(f" {C.DIM}Usage: truffile disconnect {C.RESET}") - return 1 - elif args.command == "list": - if not args.what: - error("Missing argument") - print(f" {C.DIM}Usage: truffile list {C.RESET}") - return 1 - - storage = StorageService() - - if args.command == "scan": - return run_async(cmd_scan(args, storage)) - elif args.command == "connect": - return run_async(cmd_connect(args, storage)) - elif args.command == "disconnect": - return cmd_disconnect(args, storage) - elif args.command == "create": - return cmd_create(args) - elif args.command == "delete": - return run_async(cmd_delete(args, storage)) - elif args.command == "deploy": - return run_async(cmd_deploy(args, storage)) - elif args.command == "list": - return cmd_list(args, storage) - elif args.command == "models": - return run_async(cmd_models(storage)) - elif args.command == "chat": - return run_async(cmd_chat(args, storage)) - elif args.command == "validate": - return cmd_validate(args) - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/truffile/cli/__init__.py b/truffile/cli/__init__.py new file mode 100644 index 0000000..a39e362 --- /dev/null +++ b/truffile/cli/__init__.py @@ -0,0 +1,114 @@ +import argparse +import asyncio +import sys +from pathlib import Path + +from .ui import C, MUSHROOM, print_help + + +def run_async(coro): + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as pool: + return pool.submit(asyncio.run, coro).result() + return loop.run_until_complete(coro) + except RuntimeError: + return asyncio.run(coro) + + +def main() -> int: + parser = argparse.ArgumentParser(prog="truffile", add_help=False) + sub = parser.add_subparsers(dest="command") + + # scan + scan_p = sub.add_parser("scan", help="scan for truffle devices") + scan_p.add_argument("--timeout", type=int, default=5) + + # connect + conn_p = sub.add_parser("connect", help="connect to a truffle") + conn_p.add_argument("device", help="device name (e.g. truffle-1234)") + + # disconnect + disc_p = sub.add_parser("disconnect", help="disconnect from device(s)") + disc_p.add_argument("device", nargs="?", default="all") + + # create + create_p = sub.add_parser("create", help="scaffold a new app") + create_p.add_argument("name", nargs="?") + + # validate + val_p = sub.add_parser("validate", help="validate app directory") + val_p.add_argument("path", nargs="?", default=".") + + # deploy + dep_p = sub.add_parser("deploy", help="deploy app to device") + dep_p.add_argument("path", nargs="?", default=".") + dep_p.add_argument("--shell", action="store_true") + dep_p.add_argument("--no-finalize", action="store_true") + + # list + list_p = sub.add_parser("list", help="list apps or devices") + list_p.add_argument("what", choices=["apps", "devices"]) + + # delete + del_p = sub.add_parser("delete", help="delete app from device") + del_p.add_argument("app", nargs="?") + + # models + sub.add_parser("models", help="list inference models") + + # chat + chat_p = sub.add_parser("chat", help="interactive chat") + chat_p.add_argument("--model", type=str, default=None) + chat_p.add_argument("--system", type=str, default=None) + chat_p.add_argument("--no-stream", action="store_true") + chat_p.add_argument("--no-tools", action="store_true") + chat_p.add_argument("--mcp", type=str, action="append", default=None) + + # help + sub.add_parser("help", help="show help") + + args = parser.parse_args() + + if args.command is None or args.command == "help": + print_help() + return 0 + + from truffile.storage import StorageService + storage = StorageService() + + if args.command == "scan": + from .connect import cmd_scan + return run_async(cmd_scan(args, storage)) + elif args.command == "connect": + from .connect import cmd_connect + return run_async(cmd_connect(args, storage)) + elif args.command == "disconnect": + from .connect import cmd_disconnect + return cmd_disconnect(args, storage) + elif args.command == "create": + from .create import cmd_create + return cmd_create(args) + elif args.command == "validate": + from .validate import cmd_validate + return cmd_validate(args) + elif args.command == "deploy": + from .deploy import cmd_deploy + return run_async(cmd_deploy(args, storage)) + elif args.command == "list": + from .apps import cmd_list + return cmd_list(args, storage) + elif args.command == "delete": + from .apps import cmd_delete + return run_async(cmd_delete(args, storage)) + elif args.command == "models": + from .models import cmd_models + return run_async(cmd_models(storage)) + elif args.command == "chat": + from .chat import cmd_chat + return run_async(cmd_chat(args, storage)) + + print_help() + return 1 diff --git a/truffile/cli/apps.py b/truffile/cli/apps.py new file mode 100644 index 0000000..8feaca1 --- /dev/null +++ b/truffile/cli/apps.py @@ -0,0 +1,218 @@ +import asyncio + +from truffile.storage import StorageService +from truffile.client import TruffleClient, resolve_mdns + +from .ui import C, DOT, CROSS, CHECK, Spinner, error, success + + +async def cmd_list_apps(storage: StorageService) -> int: + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + token = storage.get_token(device) + if not token: + error(f"No token for {device}") + print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") + return 1 + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError as e: + spinner.fail(str(e)) + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + + try: + await client.connect() + apps = await client.get_all_apps() + spinner.stop(success=True) + + if not apps: + print(f" {C.DIM}No apps installed{C.RESET}") + return 0 + + focus_apps = [app for app in apps if app.HasField("foreground")] + ambient_apps = [app for app in apps if app.HasField("background")] + both_apps = [app for app in apps if app.HasField("foreground") and app.HasField("background")] + + print() + if focus_apps: + print(f"{C.BOLD}Focus Apps{C.RESET}") + for app in focus_apps: + print(f" {C.CYAN}{DOT}{C.RESET} {app.metadata.name}") + setattr(app.metadata, "description", getattr(app.metadata, "description", "")) + if hasattr(app.metadata, "description") and app.metadata.description: + desc = app.metadata.description.strip().split('\n')[0][:55] + print(f" {C.DIM}{desc}{C.RESET}") + + if ambient_apps: + if focus_apps: + print() + print(f"{C.BOLD}Ambient Apps{C.RESET}") + for app in ambient_apps: + schedule = "" + policy = app.background.runtime_policy + if policy.HasField("interval"): + secs = policy.interval.duration.seconds + if secs >= 3600: + schedule = f"every {secs // 3600}h" + elif secs >= 60: + schedule = f"every {secs // 60}m" + else: + schedule = f"every {secs}s" + elif policy.HasField("always"): + schedule = "always" + print(f" {C.CYAN}{DOT}{C.RESET} {app.metadata.name} {C.DIM}({schedule}){C.RESET}") + setattr(app.metadata, "description", getattr(app.metadata, "description", "")) + if hasattr(app.metadata, "description") and app.metadata.description: + desc = app.metadata.description.strip().split('\n')[0][:55] + print(f" {C.DIM}{desc}{C.RESET}") + + print() + print( + f"{C.DIM}Total: {len(focus_apps)} focus, {len(ambient_apps)} ambient, " + f"{len(both_apps)} both{C.RESET}" + ) + return 0 + + except Exception as e: + spinner.fail(str(e)) + return 1 + finally: + await client.close() + +async def cmd_delete(args, storage: StorageService) -> int: + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + token = storage.get_token(device) + if not token: + error(f"No token for {device}") + print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") + return 1 + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError as e: + spinner.fail(str(e)) + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + + try: + await client.connect() + apps = await client.get_all_apps() + spinner.stop(success=True) + + all_apps = [] + for app in apps: + if app.HasField("foreground") and app.HasField("background"): + kind = "both" + elif app.HasField("foreground"): + kind = "focus" + elif app.HasField("background"): + kind = "ambient" + else: + kind = "unknown" + desc = app.metadata.description.strip().split('\n')[0][:55] if app.metadata.description else "" + all_apps.append((kind, app.uuid, app.metadata.name, desc)) + + if not all_apps: + print(f" {C.DIM}No apps installed{C.RESET}") + return 0 + + print() + print(f"{C.BOLD}Installed Apps:{C.RESET}") + print() + for i, (kind, uuid, name, desc) in enumerate(all_apps, 1): + print(f" {C.CYAN}{i}.{C.RESET} {name} {C.DIM}({kind}){C.RESET}") + if desc: + print(f" {C.DIM}{desc}{C.RESET}") + print() + + try: + choice = input(f"Select apps to delete (e.g. 1,3,5 or 'all'): ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + + if not choice: + return 0 + + if choice.lower() == "all": + to_delete = list(range(len(all_apps))) + else: + try: + to_delete = [int(x.strip()) - 1 for x in choice.split(",")] + for idx in to_delete: + if idx < 0 or idx >= len(all_apps): + error(f"Invalid selection: {idx + 1}") + return 1 + except ValueError: + error("Invalid input") + return 1 + + print() + deleted = 0 + for idx in to_delete: + kind, uuid, name, _ = all_apps[idx] + spinner = Spinner(f"Deleting {name}") + spinner.start() + try: + await client.delete_app(uuid) + spinner.stop(success=True) + deleted += 1 + except Exception as e: + spinner.fail(f"Failed to delete {name}: {e}") + + print() + success(f"Deleted {deleted} app(s)") + return 0 + + except Exception as e: + spinner.fail(str(e)) + return 1 + finally: + await client.close() + + +def cmd_list(args, storage: StorageService) -> int: + what = args.what + if what == "apps": + return _run_async(cmd_list_apps(storage)) + elif what == "devices": + devices = storage.list_devices() + if not devices: + print(f" {C.DIM}No connected devices{C.RESET}") + else: + print(f"{C.BOLD}Connected Devices{C.RESET}") + for d in devices: + if d == storage.state.last_used_device: + print(f" {C.GREEN}{DOT}{C.RESET} {d} {C.DIM}(active){C.RESET}") + else: + print(f" {C.CYAN}{DOT}{C.RESET} {d}") + return 0 + + +def _run_async(coro): + try: + return asyncio.run(coro) + except KeyboardInterrupt: + print(f"\r{C.RED}{CROSS} Cancelled{C.RESET} ") + return 130 diff --git a/truffile/cli/chat.py b/truffile/cli/chat.py new file mode 100644 index 0000000..c2392e3 --- /dev/null +++ b/truffile/cli/chat.py @@ -0,0 +1,1118 @@ +import argparse +import base64 +import contextlib +import json +import mimetypes +import os +import re +import select +import signal +import sys +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable + +import httpx + +from .ui import C, MUSHROOM, SUPPORTED_SERVER_MIME_TYPES, MushroomPulse, error, success, info, warn +from .connect import _resolve_connected_device +from .models import _fetch_models_payload, _pick_model_interactive, _default_model + +try: + import readline +except Exception: + readline = None + +try: + import termios + import tty +except Exception: + termios = None + tty = None + +DEFAULT_SYSTEM_PROMPT = None + +REPL_COMMANDS = [ + "/help", "/", "/history", "/reset", "/models", "/config", + "/reasoning", "/stream", "/json", "/tools", "/max_tokens", + "/temperature", "/top_p", "/max_rounds", "/system", "/mcp", + "/attach", "/exit", "/quit", +] + +class ChatSettings: + model: str + system_prompt: str | None = DEFAULT_SYSTEM_PROMPT + reasoning: bool = True + stream: bool = True + json_mode: bool = False + max_tokens: int = 2048 + temperature: float | None = None + top_p: float | None = None + default_tools: bool = True + max_tool_rounds: int = 8 + + +class ChatMCPClient: + def __init__(self) -> None: + self._group: Any | None = None + self.endpoint: str | None = None + + @property + def connected(self) -> bool: + return self._group is not None + + async def connect_streamable_http(self, endpoint: str) -> None: + from mcp.client.session_group import ClientSessionGroup, StreamableHttpParameters + + await self.disconnect() + group = ClientSessionGroup() + await group.__aenter__() + try: + await group.connect_to_server(StreamableHttpParameters(url=endpoint)) + except Exception: + with contextlib.suppress(Exception): + await group.__aexit__(None, None, None) + raise + self._group = group + self.endpoint = endpoint + + async def disconnect(self) -> None: + if self._group is None: + self.endpoint = None + return + group = self._group + self._group = None + self.endpoint = None + with contextlib.suppress(Exception): + await group.__aexit__(None, None, None) + + def list_tool_names(self) -> list[str]: + if self._group is None: + return [] + names: list[str] = [] + for _server_name, tool in self._group.list_tools(): + name = getattr(tool, "name", None) + if isinstance(name, str): + names.append(name) + return sorted(set(names)) + + def has_tool(self, name: str) -> bool: + if self._group is None: + return False + try: + tool = self._group.get_tool(name) + return tool is not None + except Exception: + return False + + def build_openai_tools(self) -> list[dict[str, Any]]: + if self._group is None: + return [] + tools: list[dict[str, Any]] = [] + for _server_name, tool in self._group.list_tools(): + name = getattr(tool, "name", None) + if not isinstance(name, str) or not name: + continue + description = str(getattr(tool, "description", "") or "") + schema = getattr(tool, "inputSchema", None) + if not isinstance(schema, dict): + schema = {"type": "object", "properties": {}} + if schema.get("type") != "object": + schema = {"type": "object", "properties": {}} + tools.append( + { + "type": "function", + "function": { + "name": name, + "description": description, + "parameters": schema, + }, + } + ) + return tools + + async def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: + if self._group is None: + return {"error": "mcp not connected"} + try: + result = await self._group.call_tool(name, arguments) + content: list[dict[str, Any]] = [] + for part in result.content: + if hasattr(part, "model_dump"): + content.append(part.model_dump()) # type: ignore[call-arg] + elif isinstance(part, dict): + content.append(part) + else: + content.append({"value": str(part)}) + return { + "is_error": bool(result.isError), + "structured_content": result.structuredContent, + "content": content, + } + except Exception as exc: + return {"error": "mcp call failed", "tool": name, "detail": str(exc)} + + +def _print_chat_config(settings: ChatSettings, mcp_client: ChatMCPClient) -> None: + print(f"{C.BLUE}chat config{C.RESET}") + print(f" {C.DIM}model:{C.RESET} {settings.model}") + print(f" {C.DIM}reasoning:{C.RESET} {settings.reasoning}") + print(f" {C.DIM}stream:{C.RESET} {settings.stream}") + print(f" {C.DIM}json:{C.RESET} {settings.json_mode}") + print(f" {C.DIM}tools:{C.RESET} {settings.default_tools}") + print(f" {C.DIM}max_tokens:{C.RESET} {settings.max_tokens}") + print(f" {C.DIM}temperature:{C.RESET} {settings.temperature}") + print(f" {C.DIM}top_p:{C.RESET} {settings.top_p}") + print(f" {C.DIM}max_rounds:{C.RESET} {settings.max_tool_rounds}") + print(f" {C.DIM}system:{C.RESET} {settings.system_prompt or ''}") + print(f" {C.DIM}mcp:{C.RESET} {mcp_client.endpoint or ''}") + + +def _parse_on_off(value: str) -> bool | None: + v = value.strip().lower() + if v in {"on", "true", "1", "yes"}: + return True + if v in {"off", "false", "0", "no"}: + return False + return None + + +def _resolve_image_path(raw_path: str) -> Path: + path = Path(raw_path).expanduser().resolve() + if not path.is_file(): + raise FileNotFoundError(f"image file not found: {path}") + return path + + +def _guess_mime_type(path: Path) -> str: + mime, _ = mimetypes.guess_type(str(path)) + return mime or "image/jpeg" + + +def _normalize_image_for_server(image_bytes: bytes, mime: str) -> tuple[bytes, str, bool]: + mime_l = mime.lower() + if mime_l in SUPPORTED_SERVER_MIME_TYPES: + return image_bytes, mime_l, False + try: + from PIL import Image + except Exception as exc: + raise RuntimeError( + f"image mime {mime!r} is not supported by server decoder and Pillow is unavailable: {exc}" + ) from exc + + from io import BytesIO + + try: + with Image.open(BytesIO(image_bytes)) as im: + rgb = im.convert("RGB") + out = BytesIO() + rgb.save(out, format="PNG") + return out.getvalue(), "image/png", True + except Exception as exc: + raise RuntimeError(f"failed to transcode unsupported image mime {mime!r}: {exc}") from exc + + +def _resolve_image_bytes_and_mime(image_path_or_url: str) -> tuple[bytes, str, str]: + if image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://"): + with httpx.Client(timeout=60.0) as client: + resp = client.get(image_path_or_url) + resp.raise_for_status() + content_type = (resp.headers.get("Content-Type") or "").split(";")[0].strip().lower() + mime = content_type if content_type.startswith("image/") else "image/jpeg" + size_kib = len(resp.content) / 1024.0 + image_bytes, mime, transcoded = _normalize_image_for_server(resp.content, mime) + desc = f"url={image_path_or_url} size={size_kib:.1f} KiB mime={mime}" + if transcoded: + desc += " (transcoded)" + return image_bytes, mime, desc + + path = _resolve_image_path(image_path_or_url) + size_kib = path.stat().st_size / 1024.0 + mime = _guess_mime_type(path) + image_bytes, mime, transcoded = _normalize_image_for_server(path.read_bytes(), mime) + desc = f"path={path} size={size_kib:.1f} KiB mime={mime}" + if transcoded: + desc += " (transcoded)" + return image_bytes, mime, desc + + +def _to_data_url(image_bytes: bytes, mime: str) -> str: + payload = base64.b64encode(image_bytes).decode("ascii") + return f"data:{mime};base64,{payload}" + + +def _make_user_message(text: str, image_data_url: str | None) -> dict[str, Any]: + if image_data_url is None: + return {"role": "user", "content": text} + return { + "role": "user", + "content": [ + {"type": "text", "text": text}, + {"type": "image_url", "image_url": {"url": image_data_url}}, + ], + } + + +def _build_default_tools() -> list[dict[str, Any]]: + return [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web for a query and return top results.", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query."}, + "max_results": { + "type": "integer", + "description": "Number of results to return (1-10).", + "default": 5, + }, + }, + "required": ["query"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "web_fetch", + "description": "Fetch and extract readable text from a URL.", + "parameters": { + "type": "object", + "properties": { + "url": {"type": "string", "description": "Absolute http/https URL."}, + "max_chars": { + "type": "integer", + "description": "Max number of characters to return (500-20000).", + "default": 8000, + }, + }, + "required": ["url"], + }, + }, + }, + ] + + +def _tool_web_search(arguments: dict[str, Any]) -> dict[str, Any]: + query = str(arguments.get("query", "")).strip() + if not query: + return {"error": "query is required"} + max_results = arguments.get("max_results", 5) + try: + max_results = int(max_results) + except (TypeError, ValueError): + max_results = 5 + max_results = max(1, min(max_results, 10)) + try: + from ddgs import DDGS + except Exception as exc: + return { + "error": "ddgs is not installed or failed to import", + "detail": str(exc), + "hint": "pip install ddgs", + } + rows: list[dict[str, Any]] = [] + try: + with DDGS() as ddgs: + for r in ddgs.text(query, max_results=max_results): + if len(rows) >= max_results: + break + rows.append( + { + "title": r.get("title"), + "url": r.get("href") or r.get("url"), + "snippet": r.get("body") or r.get("snippet"), + } + ) + except Exception as exc: + return {"error": "web_search failed", "detail": str(exc)} + return {"query": query, "count": len(rows), "results": rows} + + +def _tool_web_fetch(arguments: dict[str, Any]) -> dict[str, Any]: + url = str(arguments.get("url", "")).strip() + if not url: + return {"error": "url is required"} + max_chars = arguments.get("max_chars", 8000) + try: + max_chars = int(max_chars) + except (TypeError, ValueError): + max_chars = 8000 + max_chars = max(500, min(max_chars, 20000)) + try: + import trafilatura + except Exception as exc: + return { + "error": "trafilatura is not installed or failed to import", + "detail": str(exc), + "hint": "pip install trafilatura", + } + try: + downloaded = trafilatura.fetch_url(url) + if not downloaded: + return {"error": "failed to download url", "url": url} + text = trafilatura.extract(downloaded, include_links=False, include_images=False) + if not text: + return {"error": "failed to extract readable text", "url": url} + text = text.strip() + truncated = len(text) > max_chars + return { + "url": url, + "content": text[:max_chars], + "truncated": truncated, + "content_chars": min(len(text), max_chars), + } + except Exception as exc: + return {"error": "web_fetch failed", "url": url, "detail": str(exc)} + + +def _execute_default_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + if name == "web_search": + return _tool_web_search(arguments) + if name == "web_fetch": + return _tool_web_fetch(arguments) + return {"error": f"unknown tool '{name}'"} + + +def _print_history(messages: list[dict[str, Any]]) -> None: + for idx, msg in enumerate(messages): + role = str(msg.get("role", "unknown")) + if role == "assistant" and msg.get("tool_calls"): + text = f"[tool_calls={len(msg.get('tool_calls') or [])}]" + else: + content = msg.get("content", "") + if isinstance(content, list): + text = json.dumps(content, ensure_ascii=True) + else: + text = str(content) + text = text.replace("\n", " ") + if len(text) > 160: + text = text[:157] + "..." + print(f"{idx:03d} {role:9s} {text}") + + +def _build_chat_payload( + *, + model: str, + messages: list[dict[str, Any]], + settings: ChatSettings, + stream: bool, + tools: list[dict[str, Any]] | None, +) -> dict[str, Any]: + body: dict[str, Any] = { + "model": model, + "messages": messages, + "stream": stream, + "reasoning": {"enabled": bool(settings.reasoning)}, + "max_tokens": int(settings.max_tokens), + } + if settings.temperature is not None: + body["temperature"] = settings.temperature + if settings.top_p is not None: + body["top_p"] = settings.top_p + if stream: + body["stream_options"] = {"include_usage": True} + if tools: + body["tools"] = tools + body["tool_choice"] = "auto" + return body + + +def _print_reasoning_and_response(reasoning_text: str, response_text: str, show_reasoning: bool) -> None: + if show_reasoning and reasoning_text: + print(f"{C.GRAY}thinking:{C.RESET}") + print(f"{C.GRAY}{reasoning_text}{C.RESET}") + if response_text: + print() + if response_text: + print(response_text) + + +def _print_repl_commands(prefix: str | None = None) -> None: + command_pool = [cmd for cmd in REPL_COMMANDS if cmd != "/"] + if prefix is None: + matches = command_pool + else: + matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] + if not matches: + print(f"{C.YELLOW}no command matches: {prefix}{C.RESET}") + return + rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in matches) + print(f"commands: {rendered}") + + +def _install_repl_completer(commands: list[str]) -> Callable[[], None] | None: + if readline is None: + return None + try: + prev_completer = readline.get_completer() + prev_delims = readline.get_completer_delims() + prev_display_hook = getattr(readline, "get_completion_display_matches_hook", lambda: None)() + readline.parse_and_bind("tab: complete") + readline.parse_and_bind("set show-all-if-ambiguous on") + readline.parse_and_bind("set show-all-if-unmodified on") + readline.parse_and_bind("set completion-ignore-case on") + readline.set_completer_delims(" \t\n") + matches: list[str] = [] + + def _complete(text: str, state: int) -> str | None: + nonlocal matches + if state == 0: + buffer = readline.get_line_buffer().lstrip() + if buffer.startswith("/"): + prefix = buffer.split()[0] + command_pool = [cmd for cmd in commands if cmd != "/"] + if prefix == "/": + matches = command_pool + else: + matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] + else: + matches = [] + if state < len(matches): + return matches[state] + return None + + readline.set_completer(_complete) + if hasattr(readline, "set_completion_display_matches_hook"): + def _display_matches(substitution: str, display_matches: list[str], longest_match_length: int) -> None: + del substitution, longest_match_length + if not display_matches: + return + print() + rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in display_matches) + print(f"commands: {rendered}") + try: + readline.redisplay() + except Exception: + pass + readline.set_completion_display_matches_hook(_display_matches) + + def _cleanup() -> None: + try: + readline.set_completer(prev_completer) + readline.set_completer_delims(prev_delims) + if hasattr(readline, "set_completion_display_matches_hook"): + readline.set_completion_display_matches_hook(prev_display_hook) + except Exception: + pass + + return _cleanup + except Exception: + return None + + +class StreamAbortWatcher: + def __init__(self) -> None: + self.enabled = bool(sys.stdin.isatty() and termios is not None and tty is not None) + self._fd: int | None = None + self._old_attrs: Any = None + self._thread: threading.Thread | None = None + self._stop = threading.Event() + self._abort_reason: str | None = None + + def __enter__(self) -> "StreamAbortWatcher": + if not self.enabled: + return self + try: + self._fd = sys.stdin.fileno() + self._old_attrs = termios.tcgetattr(self._fd) + tty.setcbreak(self._fd) + except Exception: + self.enabled = False + return self + self._thread = threading.Thread(target=self._watch, daemon=True) + self._thread.start() + return self + + def _watch(self) -> None: + if self._fd is None: + return + while not self._stop.is_set(): + try: + ready, _, _ = select.select([self._fd], [], [], 0.1) + except Exception: + return + if not ready: + continue + try: + ch = os.read(self._fd, 1) + except Exception: + continue + if not ch: + continue + if ch == b"\x1b": + self._abort_reason = "esc" + self._stop.set() + return + + def aborted(self) -> bool: + return self._abort_reason is not None + + def __exit__(self, exc_type, exc, tb) -> bool: + self._stop.set() + if self._thread: + self._thread.join(timeout=0.2) + if self.enabled and self._fd is not None and self._old_attrs is not None: + try: + termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_attrs) + except Exception: + pass + return False + + +def _run_single_chat_request( + *, + client: httpx.Client, + url: str, + headers: dict[str, str], + payload: dict[str, Any], + settings: ChatSettings, + stream: bool, +) -> tuple[dict[str, Any], dict[str, Any] | None, bool]: + wait_anim = MushroomPulse("thinking") + wait_anim.start() + if stream: + content_parts: list[str] = [] + reasoning_parts: list[str] = [] + usage: dict[str, Any] | None = None + tool_calls_by_index: dict[int, dict[str, Any]] = {} + reasoning_stream_started = False + interrupted = False + first_event_seen = False + + try: + with StreamAbortWatcher() as abort_watcher: + with client.stream("POST", url, headers=headers, json=payload) as resp: + resp.raise_for_status() + for raw in resp.iter_lines(): + if abort_watcher.aborted(): + interrupted = True + break + if not raw: + continue + line = raw.strip() + if not line.startswith("data:"): + continue + data = line[len("data:"):].strip() + if data == "[DONE]": + break + try: + evt = json.loads(data) + except Exception: + continue + if not first_event_seen: + wait_anim.stop() + first_event_seen = True + + if isinstance(evt.get("usage"), dict): + usage = evt.get("usage") + + choices = evt.get("choices") + if not isinstance(choices, list) or not choices: + continue + c0 = choices[0] + if not isinstance(c0, dict): + continue + delta = c0.get("delta", {}) + if not isinstance(delta, dict): + continue + + reasoning_chunk = delta.get("reasoning") + if isinstance(reasoning_chunk, str) and reasoning_chunk: + reasoning_parts.append(reasoning_chunk) + if settings.reasoning: + if not reasoning_stream_started: + print(f"{C.GRAY}thinking:{C.RESET}") + reasoning_stream_started = True + print(f"{C.GRAY}{reasoning_chunk}{C.RESET}", end="", flush=True) + + content_chunk = delta.get("content") + if isinstance(content_chunk, str) and content_chunk: + content_parts.append(content_chunk) + print(content_chunk, end="", flush=True) + + for tc in delta.get("tool_calls") or []: + if not isinstance(tc, dict): + continue + idx = tc.get("index") + if not isinstance(idx, int): + idx = len(tool_calls_by_index) + entry = tool_calls_by_index.setdefault( + idx, + { + "id": tc.get("id", ""), + "type": tc.get("type", "function"), + "function": {"name": "", "arguments": ""}, + }, + ) + if tc.get("id"): + entry["id"] = tc["id"] + if tc.get("type"): + entry["type"] = tc["type"] + fn = tc.get("function") or {} + if isinstance(fn, dict): + if fn.get("name"): + entry["function"]["name"] += str(fn["name"]) + if fn.get("arguments"): + entry["function"]["arguments"] += str(fn["arguments"]) + except KeyboardInterrupt: + interrupted = True + finally: + wait_anim.stop() + + msg: dict[str, Any] = {"role": "assistant", "content": "".join(content_parts).strip()} + reasoning_text = "".join(reasoning_parts).strip() + if reasoning_text: + msg["reasoning_content"] = reasoning_text + if tool_calls_by_index: + msg["tool_calls"] = [tool_calls_by_index[i] for i in sorted(tool_calls_by_index)] + if settings.reasoning: + if reasoning_stream_started: + print() + response_text = str(msg.get("content") or "") + if response_text: + print() + print(response_text) + elif content_parts: + print() + if interrupted: + print(f"{C.YELLOW}response interrupted{C.RESET}") + return msg, usage, interrupted + + try: + resp = client.post(url, headers=headers, json=payload, timeout=120.0) + resp.raise_for_status() + body = resp.json() + finally: + wait_anim.stop() + if settings.json_mode: + print(json.dumps(body, indent=2)) + + choices = body.get("choices", []) + c0 = choices[0] if isinstance(choices, list) and choices else {} + msg = c0.get("message", {}) if isinstance(c0, dict) else {} + if not isinstance(msg, dict): + msg = {} + out: dict[str, Any] = {"role": "assistant", "content": str(msg.get("content", "") or "")} + if isinstance(msg.get("reasoning"), str) and msg.get("reasoning"): + out["reasoning_content"] = msg["reasoning"] + if isinstance(msg.get("tool_calls"), list): + out["tool_calls"] = msg.get("tool_calls") + + _print_reasoning_and_response( + str(out.get("reasoning_content") or ""), + str(out.get("content") or ""), + bool(settings.reasoning), + ) + return out, body.get("usage") if isinstance(body.get("usage"), dict) else None, False + + +async def _run_chat_turn( + *, + client: httpx.Client, + url: str, + headers: dict[str, str], + model: str, + settings: ChatSettings, + mcp_client: ChatMCPClient, + messages: list[dict[str, Any]], + user_message: dict[str, Any], +) -> int: + messages.append(user_message) + + max_rounds = max(1, int(settings.max_tool_rounds)) + for _ in range(max_rounds): + stream = settings.stream and not settings.json_mode + tools: list[dict[str, Any]] = [] + if settings.default_tools: + tools.extend(_build_default_tools()) + if mcp_client.connected: + tools.extend(mcp_client.build_openai_tools()) + + payload = _build_chat_payload( + model=model, + messages=messages, + settings=settings, + stream=stream, + tools=tools or None, + ) + assistant_msg, usage, interrupted = _run_single_chat_request( + client=client, url=url, headers=headers, payload=payload, settings=settings, stream=stream + ) + messages.append(assistant_msg) + if isinstance(usage, dict): + image_tokens = usage.get("image_tokens") or 0 + image_tokens_part = f", image: {image_tokens}" if image_tokens else "" + image_tps_part = f", image tps: {usage.get('image_tokens_per_second') or ''}" if image_tokens else "" + print( + f"{C.DIM}[usage] tokens(prompt: {usage.get('prompt_tokens') or ''}, completion: {usage.get('completion_tokens') or ''}, total: {usage.get('total_tokens') or ''}{image_tokens_part}) usage(decode tps: {usage.get('decode_tokens_per_second') or ''}, prefill tps: {usage.get('prefill_tokens_per_second') or ''}{image_tps_part}) itl: {usage.get('itl_ms') or ''}ms ttft: {usage.get('ttft_ms') or ''}ms{C.RESET}" + ) + if interrupted: + return 130 + + tool_calls = assistant_msg.get("tool_calls") if isinstance(assistant_msg, dict) else None + if not tools or not isinstance(tool_calls, list) or not tool_calls: + return 0 + + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + fn = tool_call.get("function") or {} + if not isinstance(fn, dict): + continue + name = str(fn.get("name") or "") + raw_args = str(fn.get("arguments") or "{}") + try: + parsed_args = json.loads(raw_args) + except json.JSONDecodeError: + parsed_args = {"_raw": raw_args} + if name in {"web_search", "web_fetch"}: + print(f"{C.CYAN}{HAMMER} tool{C.RESET} {name}") + tool_result = _execute_default_tool(name, parsed_args) + elif mcp_client.has_tool(name): + print(f"{C.CYAN}{HAMMER} mcp{C.RESET} {name}") + tool_result = await mcp_client.call_tool(name, parsed_args) + else: + print(f"{C.YELLOW}{WARN} unknown tool{C.RESET} {name}") + tool_result = {"error": f"unknown tool '{name}'"} + messages.append( + { + "role": "tool", + "tool_call_id": tool_call.get("id", ""), + "content": json.dumps(tool_result, ensure_ascii=False), + } + ) + + warn("Reached max tool rounds without a final assistant response") + return 1 + + +async def cmd_chat(args, storage: StorageService) -> int: + prompt_words = getattr(args, "prompt_words", None) + prompt = " ".join(prompt_words).strip() if prompt_words else "" + + device, ip = await _resolve_connected_device(storage) + if not device or not ip: + return 1 + + spinner = Spinner("Resolving default model") + spinner.start() + model = await _default_model(ip) + if not model: + spinner.fail("Failed to resolve default model from IF2") + return 1 + spinner.stop(success=True) + + settings = ChatSettings(model=model) + mcp_client = ChatMCPClient() + messages: list[dict[str, Any]] = [] + pending_image_data_url: str | None = None + pending_image_desc: str | None = None + + url = f"http://{ip}/if2/v1/chat/completions" + headers = {"Content-Type": "application/json"} + + try: + spinner = Spinner(f"Connecting to {device}") + spinner.start() + with httpx.Client(timeout=None) as client: + spinner.stop(success=True) + + # REPL mode (default). + print(f"{C.DIM}model: {settings.model}{C.RESET}") + print( + f"{C.DIM}commands: /help, /history, /reset, /models, /attach, /config, /mcp, /exit{C.RESET}" + ) + + cleanup_repl = _install_repl_completer(REPL_COMMANDS) + try: + if prompt: + print(f"{C.CYAN}> {prompt}{C.RESET}") + rc = await _run_chat_turn( + client=client, + url=url, + headers=headers, + model=settings.model, + settings=settings, + mcp_client=mcp_client, + messages=messages, + user_message=_make_user_message(prompt, pending_image_data_url), + ) + if rc != 0: + if rc == 130: + return 0 + else: + return rc + else: + pending_image_data_url = None + pending_image_desc = None + + while True: + try: + line = input(f"{C.CYAN}> {C.RESET}").strip() + except EOFError: + print() + return 0 + except KeyboardInterrupt: + print() + return 0 + + if not line: + continue + if line in {"/", "/help"}: + _print_repl_commands() + continue + if line in {"/exit", "/quit"}: + return 0 + if line == "/history": + _print_history(messages) + continue + if line == "/reset": + messages = [] + if settings.system_prompt: + messages.append({"role": "system", "content": settings.system_prompt}) + pending_image_data_url = None + pending_image_desc = None + print(f"{C.YELLOW}history reset (and cleared pending attachment){C.RESET}") + continue + if line in {"/models", "/model"}: + try: + models = _fetch_models_payload(client, ip) + selected_model = _pick_model_interactive(models, settings.model) + if selected_model and selected_model != settings.model: + settings.model = selected_model + print(f"{C.GREEN}{CHECK}{C.RESET} model switched: {settings.model}") + except Exception as exc: + error(f"failed to list models: {exc}") + continue + if line == "/config": + _print_chat_config(settings, mcp_client) + continue + if line.startswith("/reasoning"): + arg = line[len("/reasoning"):].strip() + if not arg: + print(f"{C.DIM}reasoning={settings.reasoning}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /reasoning ") + continue + settings.reasoning = val + print(f"{C.GREEN}{CHECK}{C.RESET} reasoning={settings.reasoning}") + continue + if line.startswith("/stream"): + arg = line[len("/stream"):].strip() + if not arg: + print(f"{C.DIM}stream={settings.stream}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /stream ") + continue + settings.stream = val + print(f"{C.GREEN}{CHECK}{C.RESET} stream={settings.stream}") + continue + if line.startswith("/json"): + arg = line[len("/json"):].strip() + if not arg: + print(f"{C.DIM}json={settings.json_mode}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /json ") + continue + settings.json_mode = val + print(f"{C.GREEN}{CHECK}{C.RESET} json={settings.json_mode}") + continue + if line.startswith("/tools"): + arg = line[len("/tools"):].strip() + if not arg: + print(f"{C.DIM}tools={settings.default_tools}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /tools ") + continue + settings.default_tools = val + print(f"{C.GREEN}{CHECK}{C.RESET} tools={settings.default_tools}") + continue + if line.startswith("/max_tokens"): + arg = line[len("/max_tokens"):].strip() + if not arg: + print(f"{C.DIM}max_tokens={settings.max_tokens}{C.RESET}") + continue + try: + settings.max_tokens = max(1, int(arg)) + print(f"{C.GREEN}{CHECK}{C.RESET} max_tokens={settings.max_tokens}") + except ValueError: + warn("usage: /max_tokens ") + continue + if line.startswith("/temperature"): + arg = line[len("/temperature"):].strip() + if not arg: + print(f"{C.DIM}temperature={settings.temperature}{C.RESET}") + continue + if arg.lower() in {"off", "none"}: + settings.temperature = None + print(f"{C.GREEN}{CHECK}{C.RESET} temperature=None") + continue + try: + settings.temperature = float(arg) + print(f"{C.GREEN}{CHECK}{C.RESET} temperature={settings.temperature}") + except ValueError: + warn("usage: /temperature ") + continue + if line.startswith("/top_p"): + arg = line[len("/top_p"):].strip() + if not arg: + print(f"{C.DIM}top_p={settings.top_p}{C.RESET}") + continue + if arg.lower() in {"off", "none"}: + settings.top_p = None + print(f"{C.GREEN}{CHECK}{C.RESET} top_p=None") + continue + try: + settings.top_p = float(arg) + print(f"{C.GREEN}{CHECK}{C.RESET} top_p={settings.top_p}") + except ValueError: + warn("usage: /top_p ") + continue + if line.startswith("/max_rounds"): + arg = line[len("/max_rounds"):].strip() + if not arg: + print(f"{C.DIM}max_rounds={settings.max_tool_rounds}{C.RESET}") + continue + try: + settings.max_tool_rounds = max(1, int(arg)) + print(f"{C.GREEN}{CHECK}{C.RESET} max_rounds={settings.max_tool_rounds}") + except ValueError: + warn("usage: /max_rounds ") + continue + if line.startswith("/system"): + arg = line[len("/system"):].strip() + if not arg: + print(f"{C.DIM}system={settings.system_prompt or ''}{C.RESET}") + continue + if arg.lower() in {"off", "none", "clear"}: + settings.system_prompt = None + if messages and messages[0].get("role") == "system": + messages.pop(0) + print(f"{C.GREEN}{CHECK}{C.RESET} system prompt cleared") + continue + settings.system_prompt = arg + if messages and messages[0].get("role") == "system": + messages[0]["content"] = arg + else: + messages.insert(0, {"role": "system", "content": arg}) + print(f"{C.GREEN}{CHECK}{C.RESET} system prompt updated") + continue + if line.startswith("/mcp"): + parts = line.split(maxsplit=2) + if len(parts) == 1 or parts[1] == "status": + print( + f"{C.BLUE}/mcp status{C.RESET} " + f"{C.DIM}mcp={mcp_client.endpoint or ''} " + f"tools={len(mcp_client.list_tool_names())}{C.RESET}" + ) + print( + f"{C.DIM}subcommands:{C.RESET} " + f"{C.BLUE}/mcp connect {C.RESET}, " + f"{C.BLUE}/mcp tools{C.RESET}, " + f"{C.BLUE}/mcp disconnect{C.RESET}" + ) + continue + sub = parts[1].lower() + if sub == "connect": + if len(parts) < 3: + warn("usage: /mcp connect ") + continue + endpoint = parts[2].strip() + if not endpoint.startswith(("http://", "https://")): + warn("mcp endpoint must start with http:// or https://") + continue + try: + await mcp_client.connect_streamable_http(endpoint) + print( + f"{C.BLUE}/mcp connect{C.RESET} " + f"{C.GREEN}{CHECK}{C.RESET} {endpoint} " + f"({len(mcp_client.list_tool_names())} tools)" + ) + except Exception as exc: + error(f"mcp connect failed: {exc}") + continue + if sub == "disconnect": + await mcp_client.disconnect() + print(f"{C.BLUE}/mcp disconnect{C.RESET} {C.GREEN}{CHECK}{C.RESET}") + continue + if sub == "tools": + names = mcp_client.list_tool_names() + if not names: + print(f"{C.BLUE}/mcp tools{C.RESET} {C.DIM}no tools available{C.RESET}") + else: + print(f"{C.BLUE}/mcp tools{C.RESET} {', '.join(names)}") + continue + warn("usage: /mcp ") + continue + if line.startswith("/attach"): + parts = line.split(maxsplit=1) + if len(parts) != 2 or not parts[1].strip(): + warn("usage: /attach ") + continue + src = parts[1].strip() + try: + image_bytes, mime, desc = _resolve_image_bytes_and_mime(src) + pending_image_data_url = _to_data_url(image_bytes, mime) + pending_image_desc = desc + print(f"{C.GREEN}{CHECK}{C.RESET} attachment ready: {desc}") + except FileNotFoundError as exc: + error(str(exc)) + except httpx.HTTPError as exc: + error(f"failed to fetch image: {exc}") + except RuntimeError as exc: + error(str(exc)) + continue + if line.startswith("/"): + matches = [cmd for cmd in REPL_COMMANDS if cmd.startswith(line)] + if matches: + _print_repl_commands(line) + else: + warn(f"unknown command: {line}") + _print_repl_commands() + continue + + if pending_image_data_url is not None: + print(f"{C.MAGENTA}[attach]{C.RESET} sending with image: {pending_image_desc}") + rc = await _run_chat_turn( + client=client, + url=url, + headers=headers, + model=settings.model, + settings=settings, + mcp_client=mcp_client, + messages=messages, + user_message=_make_user_message(line, pending_image_data_url), + ) + if rc != 0: + if rc == 130: + return 0 + return rc + pending_image_data_url = None + pending_image_desc = None + finally: + if cleanup_repl: + cleanup_repl() + await mcp_client.disconnect() + return 0 + except Exception as e: + try: + spinner.fail(f"Chat request failed: {e}") # type: ignore[name-defined] + except Exception: + error(f"Chat request failed: {e}") + return 1 + + diff --git a/truffile/cli/connect.py b/truffile/cli/connect.py new file mode 100644 index 0000000..fd3fb43 --- /dev/null +++ b/truffile/cli/connect.py @@ -0,0 +1,246 @@ +import asyncio + +from truffile.storage import StorageService +from truffile.client import TruffleClient, resolve_mdns, NewSessionStatus + +from .ui import C, DOT, Spinner, error, success, info + + +async def cmd_connect(args, storage: StorageService) -> int: + device_name = args.device + + spinner = Spinner(f"Resolving {device_name}.local") + spinner.start() + + hostname = f"{device_name}.local" + try: + ip = await resolve_mdns(hostname) + spinner.stop(success=True) + except RuntimeError: + spinner.fail(f"Could not resolve {device_name}.local") + print() + print(f" {C.DIM}Try running:{C.RESET}") + print(f" {C.CYAN}ping {device_name}.local{C.RESET}") + print() + print(f" {C.DIM}If ping fails, check:{C.RESET}") + print(f" {C.DIM}{DOT} Device is powered on and connected to WiFi{C.RESET}") + print(f" {C.DIM}{DOT} Your computer is on the same network{C.RESET}") + print(f" {C.DIM}{DOT} mDNS is working{C.RESET}") + print() + return 1 + + address = f"{ip}:80" + existing_token = storage.get_token(device_name) + + if existing_token: + spinner = Spinner("Validating existing token") + spinner.start() + client = TruffleClient(address, existing_token) + try: + await client.connect() + if await client.check_auth(): + spinner.stop(success=True) + storage.set_last_used(device_name) + success(f"Already connected to {C.BOLD}{device_name}{C.RESET}") + await client.close() + return 0 + spinner.fail("Token invalid, re-authenticating") + except Exception: + spinner.fail("Token validation failed") + finally: + await client.close() + + print() + print(f" {C.DIM}Make sure you have:{C.RESET}") + print(f" {C.DIM}{DOT} Onboarded with the Truffle app{C.RESET}") + print(f" {C.DIM}{DOT} Your User ID from the recovery codes{C.RESET}") + print() + + try: + user_id = input(f"{C.CYAN}?{C.RESET} Enter your User ID: ").strip() + except (KeyboardInterrupt, EOFError): + print() + raise KeyboardInterrupt() + if not user_id: + error("User ID is required") + return 1 + + spinner = Spinner("Connecting to device") + spinner.start() + + client = TruffleClient(address, token="") + try: + await client.connect() + spinner.stop(success=True) + except Exception as e: + spinner.fail(f"Failed to connect: {e}") + return 1 + + print() + info("Requesting authorization...") + print(f" {C.DIM}Please approve on your Truffle device{C.RESET}") + + spinner = Spinner("Waiting for approval") + spinner.start() + + try: + status, token = await client.register_new_session(user_id) + except Exception as e: + spinner.fail(f"Failed to register: {e}") + await client.close() + return 1 + + await client.close() + + if status.error == NewSessionStatus.NEW_SESSION_SUCCESS and token: + spinner.stop(success=True) + storage.set_token(device_name, token) + storage.set_last_used(device_name) + print() + success(f"Connected to {C.BOLD}{device_name}{C.RESET}") + return 0 + elif status.error == NewSessionStatus.NEW_SESSION_TIMEOUT: + spinner.fail("Approval timed out") + return 1 + elif status.error == NewSessionStatus.NEW_SESSION_REJECTED: + spinner.fail("Request was rejected") + return 1 + else: + spinner.fail(f"Authentication failed: {status.error}") + return 1 + + +def cmd_disconnect(args, storage: StorageService) -> int: + target = args.target + if target == "all": + storage.clear_all() + success("All device credentials cleared") + else: + if storage.remove_device(target): + success(f"Disconnected from {C.BOLD}{target}{C.RESET}") + else: + error(f"No credentials found for {target}") + return 0 + + +async def cmd_scan(args, storage: StorageService) -> int: + try: + from zeroconf import ServiceBrowser, ServiceListener, Zeroconf, IPVersion + except ImportError: + error("zeroconf package required for scanning") + print(f" {C.DIM}pip install zeroconf{C.RESET}") + return 1 + + devices: dict[str, dict] = {} + scan_done = asyncio.Event() + + class TruffleListener(ServiceListener): + def add_service(self, zc: Zeroconf, type_: str, name: str): + if name.lower().startswith("truffle-"): + info = zc.get_service_info(type_, name) + device_name = name.split(".")[0] + if info and device_name not in devices: + addresses = [addr for addr in info.parsed_addresses(IPVersion.V4Only)] + devices[device_name] = { + "name": device_name, + "addresses": addresses, + "port": info.port, + } + + def remove_service(self, zc: Zeroconf, type_: str, name: str): + pass + + def update_service(self, zc: Zeroconf, type_: str, name: str): + pass + + timeout = args.timeout if hasattr(args, 'timeout') else 5 + + spinner = Spinner(f"Scanning for Truffle devices ({timeout}s)") + spinner.start() + + try: + zc = Zeroconf(ip_version=IPVersion.V4Only) + listener = TruffleListener() + + browsers = [ + ServiceBrowser(zc, "_truffle._tcp.local.", listener), + ] + + await asyncio.sleep(timeout) + + for browser in browsers: + browser.cancel() + zc.close() + + except Exception as e: + spinner.fail(f"Scan failed: {e}") + return 1 + + spinner.stop(success=True) + + if not devices: + print() + print(f" {C.DIM}No Truffle devices found on the network{C.RESET}") + print() + print(f" {C.DIM}Make sure your Truffle is:{C.RESET}") + print(f" {C.DIM}โ€ข Powered on{C.RESET}") + print(f" {C.DIM}โ€ข Connected to the same network as this computer{C.RESET}") + print() + return 1 + + print() + print(f"{C.BOLD}Found {len(devices)} Truffle device(s):{C.RESET}") + print() + + device_list = list(devices.values()) + for i, device in enumerate(device_list, 1): + name = device["name"] + addrs = ", ".join(device["addresses"]) if device["addresses"] else "unknown" + + already_connected = storage.get_token(name) is not None + if already_connected: + print(f" {C.GREEN}{i}.{C.RESET} {C.BOLD}{name}{C.RESET} {C.DIM}({addrs}){C.RESET} {C.GREEN}[connected]{C.RESET}") + else: + print(f" {C.CYAN}{i}.{C.RESET} {C.BOLD}{name}{C.RESET} {C.DIM}({addrs}){C.RESET}") + + print() + + try: + choice = input(f"Select device to connect (1-{len(device_list)}) or press Enter to cancel: ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + + if not choice: + return 0 + + try: + idx = int(choice) - 1 + if 0 <= idx < len(device_list): + selected = device_list[idx] + print() + + class FakeArgs: + device = selected["name"] + + return await cmd_connect(FakeArgs(), storage) + else: + error("Invalid selection") + return 1 + except ValueError: + error("Invalid input") + return 1 + + +async def _resolve_connected_device(storage: StorageService) -> tuple[str, str] | tuple[None, None]: + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return None, None + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError: + error(f"Could not resolve {device}.local") + return None, None + return device, ip diff --git a/truffile/cli/create.py b/truffile/cli/create.py new file mode 100644 index 0000000..3bc2675 --- /dev/null +++ b/truffile/cli/create.py @@ -0,0 +1,167 @@ +import json +import re +from importlib import resources as importlib_resources +from pathlib import Path + +from .ui import C, ARROW, SCAFFOLD_ICON_RESOURCE_REL, error, success + + +def _safe_app_slug(app_name: str) -> str: + slug = re.sub(r"[^a-z0-9]+", "_", app_name.lower()).strip("_") + if not slug: + return "my_app" + if slug[0].isdigit(): + return f"app_{slug}" + return slug + + +def _sample_truffile_yaml(app_name: str, slug: str) -> str: + quoted_name = json.dumps(app_name) + return ( + "metadata:\n" + f" name: {quoted_name}\n" + f" bundle_id: org.truffle.{slug.replace('_', '.')}\n" + " description: |\n" + " Describe what this app does.\n" + " icon_file: ./icon.png\n" + " foreground:\n" + " process:\n" + " cmd:\n" + " - python\n" + f" - {slug}_foreground.py\n" + " working_directory: /\n" + " environment:\n" + ' PYTHONUNBUFFERED: "1"\n' + " background:\n" + " process:\n" + " cmd:\n" + " - python\n" + f" - {slug}_background.py\n" + " working_directory: /\n" + " environment:\n" + ' PYTHONUNBUFFERED: "1"\n' + " default_schedule:\n" + " type: interval\n" + " interval:\n" + " duration: 30m\n" + " schedule:\n" + ' daily_window: "00:00-23:59"\n' + "\n" + "steps:\n" + " - name: Copy application files\n" + " type: files\n" + " files:\n" + f" - source: ./{slug}_foreground.py\n" + f" destination: ./{slug}_foreground.py\n" + f" - source: ./{slug}_background.py\n" + f" destination: ./{slug}_background.py\n" + ) + + +def _sample_foreground_py() -> str: + return ( + '"""Foreground app entrypoint (MCP-facing surface)."""\n' + "\n" + "def main() -> None:\n" + ' print(\"TODO: implement foreground MCP tool server\")\n' + "\n" + "\n" + "if __name__ == \"__main__\":\n" + " main()\n" + ) + + +def _sample_background_py() -> str: + return ( + '"""Background app entrypoint (scheduled context emitter)."""\n' + "\n" + "def main() -> None:\n" + ' print(\"TODO: implement background scheduled job\")\n' + "\n" + "\n" + "if __name__ == \"__main__\":\n" + " main()\n" + ) + + +def _load_stock_icon_bytes() -> tuple[bytes | None, str]: + try: + resource_file = importlib_resources.files("truffile").joinpath(str(SCAFFOLD_ICON_RESOURCE_REL)) + icon_bytes = resource_file.read_bytes() + return icon_bytes, f"truffile/{SCAFFOLD_ICON_RESOURCE_REL.as_posix()}" + except Exception: + pass + + local_package_path = Path(__file__).resolve().parent.parent / SCAFFOLD_ICON_RESOURCE_REL + if local_package_path.exists() and local_package_path.is_file(): + return local_package_path.read_bytes(), str(local_package_path) + + legacy_docs_path = Path(__file__).resolve().parents[2] / "docs" / "Truffle.png" + if legacy_docs_path.exists() and legacy_docs_path.is_file(): + return legacy_docs_path.read_bytes(), str(legacy_docs_path) + + return None, f"truffile/{SCAFFOLD_ICON_RESOURCE_REL.as_posix()}" + + +def cmd_create(args) -> int: + app_name = (args.name or "").strip() + if not app_name: + try: + app_name = input(f"{C.CYAN}?{C.RESET} App name: ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + if not app_name: + error("App name is required") + return 1 + if "/" in app_name or "\\" in app_name: + error("App name cannot contain path separators") + return 1 + + base_dir: Path + if args.path: + base_dir = Path(args.path).expanduser().resolve() + else: + cwd = Path.cwd() + try: + raw = input(f"{C.CYAN}?{C.RESET} Base path [{cwd}]: ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + base_dir = Path(raw).expanduser().resolve() if raw else cwd + + app_dir = base_dir / app_name + if app_dir.exists(): + error(f"Target directory already exists: {app_dir}") + return 1 + + slug = _safe_app_slug(app_name) + fg_file = f"{slug}_foreground.py" + bg_file = f"{slug}_background.py" + stock_icon_bytes, stock_icon_source = _load_stock_icon_bytes() + if stock_icon_bytes is None: + error(f"Stock icon not found: {stock_icon_source}") + return 1 + if len(stock_icon_bytes) == 0: + error(f"Stock icon is empty: {stock_icon_source}") + return 1 + + try: + app_dir.mkdir(parents=True, exist_ok=False) + (app_dir / "truffile.yaml").write_text(_sample_truffile_yaml(app_name, slug), encoding="utf-8") + (app_dir / fg_file).write_text(_sample_foreground_py(), encoding="utf-8") + (app_dir / bg_file).write_text(_sample_background_py(), encoding="utf-8") + (app_dir / "icon.png").write_bytes(stock_icon_bytes) + except Exception as exc: + error(f"Failed to scaffold app: {exc}") + return 1 + + success(f"Created app scaffold: {app_dir}") + print(f" {C.DIM}Files:{C.RESET}") + print(f" {C.DIM}{ARROW} truffile.yaml{C.RESET}") + print(f" {C.DIM}{ARROW} {fg_file}{C.RESET}") + print(f" {C.DIM}{ARROW} {bg_file}{C.RESET}") + print(f" {C.DIM}{ARROW} icon.png{C.RESET}") + print() + print(f" {C.DIM}Next:{C.RESET} truffile validate {app_dir}") + return 0 diff --git a/truffile/cli/deploy.py b/truffile/cli/deploy.py new file mode 100644 index 0000000..5224d8a --- /dev/null +++ b/truffile/cli/deploy.py @@ -0,0 +1,255 @@ +import asyncio +import os +import signal +import sys +from pathlib import Path + +from truffile.storage import StorageService +from truffile.client import TruffleClient, resolve_mdns +from truffile.schema import validate_app_dir +from truffile.deploy import build_deploy_plan, deploy_with_builder + +from .ui import C, ARROW, CROSS, DOT, Spinner, ScrollingLog, error, warn, info, success + + +async def cmd_deploy(args, storage: StorageService) -> int: + app_path = args.path if args.path else "." + app_dir = Path(app_path).resolve() + interactive = args.interactive + dry_run = bool(getattr(args, "dry_run", False)) + if not app_dir.exists() or not app_dir.is_dir(): + error(f"{app_dir} is not a valid directory") + return 1 + + info(f"Validating app in {app_dir.name}") + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + if not valid or not app_type: + for msg in errors: + error(msg) + return 1 + + for w in warnings: + warn(w) + + metadata = config.get("metadata", {}) if isinstance(config, dict) else {} + icon_file = metadata.get("icon_file") if isinstance(metadata, dict) else None + if not isinstance(icon_file, str) or not icon_file.strip(): + error("Deploy requires metadata.icon_file in truffile.yaml") + return 1 + deploy_icon_path = app_dir / icon_file + if not deploy_icon_path.exists() or not deploy_icon_path.is_file(): + error(f"Deploy requires an icon file; not found: {icon_file}") + return 1 + if deploy_icon_path.stat().st_size == 0: + error(f"Deploy requires a non-empty icon file: {icon_file}") + return 1 + + if dry_run: + try: + plan = build_deploy_plan(config=config, app_dir=app_dir, app_type=app_type) + except Exception as e: + error(f"Failed to build deploy plan: {e}") + return 1 + print() + print(f"{C.BOLD}Dry Run: Deploy Plan{C.RESET}") + print(f" Name: {plan['name']}") + print(f" Bundle ID: {plan['bundle_id']}") + print(f" Mode: {plan['finish_label']}") + print(f" App Dir: {app_dir}") + print(f" Exec CWD: {plan['exec_cwd']}") + if plan["icon_path"] is not None: + print(f" Icon: {plan['icon_path']}") + else: + print(f" Icon: {C.DIM}{C.RESET}") + + fg = plan["fg_payload"] + if fg is not None: + fg_keys = [e.split("=", 1)[0] for e in fg.get("env", []) if "=" in e] + print(f" Foreground Cmd: {fg['cmd']} {' '.join(fg.get('args', []))}".rstrip()) + print(f" Foreground Env Keys: {', '.join(fg_keys) if fg_keys else ''}") + + bg = plan["bg_payload"] + if bg is not None: + bg_keys = [e.split('=', 1)[0] for e in bg.get("env", []) if "=" in e] + print(f" Background Cmd: {bg['cmd']} {' '.join(bg.get('args', []))}".rstrip()) + print(f" Background Env Keys: {', '.join(bg_keys) if bg_keys else ''}") + if plan["default_schedule"] is not None: + print(f" Background Schedule: configured") + else: + print(f" Background Schedule: {C.DIM}{C.RESET}") + + files = plan["files_to_upload"] + print(f" Files To Upload: {len(files)}") + for f in files: + src = f.get("source", "") + dst = f.get("destination", "") + print(f" - {src} {ARROW} {dst}") + + cmds = plan["bash_commands"] + print(f" Bash Steps: {len(cmds)}") + for name, _cmd in cmds: + print(f" - {name}") + print() + success("Dry run complete (no device changes made)") + return 0 + + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + token = storage.get_token(device) + if not token: + error(f"No token for {device}") + print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") + return 1 + + spinner = Spinner(f"Resolving {device}") + spinner.start() + try: + ip = await resolve_mdns(f"{device}.local") + spinner.stop(success=True) + except RuntimeError: + spinner.fail(f"Could not resolve {device}.local") + print(f" {C.DIM}Try: ping {device}.local{C.RESET}") + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + deploy_task = None + + loop = asyncio.get_event_loop() + + def handle_sigint(): + print("\nInterrupted!") + if deploy_task and not deploy_task.done(): + deploy_task.cancel() + + loop.add_signal_handler(signal.SIGINT, handle_sigint) + + try: + deploy_task = asyncio.create_task( + deploy_with_builder( + client=client, + config=config, + app_dir=app_dir, + app_type=app_type, + device=device, + interactive=interactive, + spinner_cls=Spinner, + scrolling_log_cls=ScrollingLog, + info=info, + success=success, + error=error, + color_dim=C.DIM, + color_reset=C.RESET, + color_bold=C.BOLD, + arrow=ARROW, + interactive_shell=_interactive_shell, + ) + ) + return await deploy_task + except asyncio.CancelledError: + print() + spinner = Spinner("Discarding build session") + spinner.start() + if client.app_uuid: + try: + await client.discard() + spinner.stop(success=True) + except Exception: + spinner.fail("Failed to discard") + return 130 + except Exception as e: + error(str(e)) + if client.app_uuid: + spinner = Spinner("Discarding build session") + spinner.start() + try: + await client.discard() + spinner.stop(success=True) + except Exception: + spinner.fail("Failed to discard") + return 1 + finally: + loop.remove_signal_handler(signal.SIGINT) + await client.close() + + +async def _interactive_shell(ws_url: str) -> int: + print(f"{C.DIM}Opening shell... (exit with Ctrl+D or 'exit'){C.RESET}") + import os, termios, fcntl, struct, tty, contextlib, json + try: + import websockets + from websockets.exceptions import ConnectionClosed, ConnectionClosedOK + except Exception: + print(f"{C.RED}{CROSS} Error:{C.RESET} websockets package is required for terminal mode") + return 67 + + def _winsz(): + try: + h, w, _, _ = struct.unpack("HHHH", fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, b"\0"*8)) + return w, h + except Exception: + return 80, 24 + + class Raw: + def __enter__(self): + self.fd = sys.stdin.fileno() + self.old = termios.tcgetattr(self.fd) + tty.setraw(self.fd); return self + def __exit__(self, *a): + termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old) + + async def run_once(): + async with websockets.connect(ws_url, max_size=None, ping_interval=30) as ws: + cols, rows = _winsz() + await ws.send(json.dumps({"resize":[cols, rows]})) + + loop = asyncio.get_running_loop() + q: asyncio.Queue[bytes] = asyncio.Queue() + stop = asyncio.Event() + + def on_stdin(): + try: + data = os.read(sys.stdin.fileno(), 4096) + if data: q.put_nowait(data) + except BlockingIOError: + pass + loop.add_reader(sys.stdin.fileno(), on_stdin) + + async def pump_in(): + try: + while not stop.is_set(): + data = await q.get() + try: await ws.send(data) + except (ConnectionClosed, ConnectionClosedOK): break + finally: + stop.set() + async def pump_out(): + try: + async for msg in ws: + if isinstance(msg, bytes): + os.write(sys.stdout.fileno(), msg) + else: + os.write(sys.stdout.fileno(), msg.encode()) # type: ignore + except (ConnectionClosed, ConnectionClosedOK): + pass + finally: + stop.set() + + with Raw(): + t_in = asyncio.create_task(pump_in()) + t_out = asyncio.create_task(pump_out()) + try: + await asyncio.wait({t_in, t_out}, return_when=asyncio.FIRST_COMPLETED) + finally: + stop.set(); t_in.cancel(); t_out.cancel() + with contextlib.suppress(Exception): + await asyncio.gather(t_in, t_out, return_exceptions=True) + loop.remove_reader(sys.stdin.fileno()) + + + await run_once() + return 67 diff --git a/truffile/cli/models.py b/truffile/cli/models.py new file mode 100644 index 0000000..8bfdb01 --- /dev/null +++ b/truffile/cli/models.py @@ -0,0 +1,221 @@ +import sys +from typing import Any + +import httpx + +from truffile.storage import StorageService +from truffile.client import resolve_mdns + +from .ui import C, CHECK, MUSHROOM, WARN, Spinner, error, warn + +try: + import termios + import tty +except Exception: + termios = None # type: ignore[assignment] + tty = None # type: ignore[assignment] + + +async def cmd_models(storage: StorageService) -> int: + """List models on your Truffle.""" + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError: + spinner.fail(f"Could not resolve {device}.local") + return 1 + + try: + url = f"http://{ip}/if2/v1/models" + with httpx.Client(timeout=15.0) as client: + resp = client.get(url) + resp.raise_for_status() + payload = resp.json() + spinner.stop(success=True) + except Exception as e: + spinner.fail(f"Failed to get IF2 models: {e}") + return 1 + + models = payload.get("data", []) + if not isinstance(models, list): + spinner.fail("Invalid response: missing 'data' list") + return 1 + + print() + print(f"{MUSHROOM} {C.BOLD}IF2 Models on {device}{C.RESET}") + print() + + if not models: + print(f" {C.DIM}No models found{C.RESET}") + return 0 + + for m in models: + if not isinstance(m, dict): + continue + model_id = m.get("id", "") + name = m.get("name", model_id) + uuid = m.get("uuid", "") + ctx = m.get("context_length", "") + arch = m.get("architecture", {}) + tokenizer = arch.get("tokenizer", "") if isinstance(arch, dict) else "" + max_batch = m.get("max_batch_size", "") + print(f" {C.GREEN}{CHECK}{C.RESET} {name}") + print(f" {C.DIM}id: {model_id}{C.RESET}") + print(f" {C.DIM}uuid: {uuid}{C.RESET}") + print(f" {C.DIM}context: {ctx}, tokenizer: {tokenizer}, max_batch: {max_batch}{C.RESET}") + + return 0 + + +async def _default_model(ip: str) -> str | None: + try: + with httpx.Client(timeout=10.0) as client: + resp = client.get(f"http://{ip}/if2/v1/models") + resp.raise_for_status() + payload = resp.json() + models = payload.get("data", []) + if not isinstance(models, list) or not models: + return None + # sort models by name/id so default is 35b typically & list is consistent + models.sort(key=lambda m: str(m.get("name") or m.get("id") or m.get("uuid") or "")) + first = models[0] + return str(first.get("id") or first.get("uuid") or "") + except Exception: + return None + + +def _model_display_name(model: dict[str, Any]) -> str: + model_id = str(model.get("id") or "") + name = str(model.get("name") or model_id) + if name == model_id: + return name + return f"{name} ({model_id})" + + +def _model_value(model: dict[str, Any]) -> str: + return str(model.get("uuid") or model.get("id") or "") + + +def _model_matches_current(model: dict[str, Any], current_model: str) -> bool: + if not current_model: + return False + mv = _model_value(model) + mid = str(model.get("id") or "") + return current_model in {mv, mid} + + +def _pick_model_with_numbers(models: list[dict[str, Any]], current_model: str) -> str | None: + if not models: + return None + print(f"{C.BLUE}models:{C.RESET}") + default_idx = 0 + for i, m in enumerate(models, start=1): + active = f" {C.DIM}[active]{C.RESET}" if _model_matches_current(m, current_model) else "" + if active: + default_idx = i - 1 + print(f"{C.BLUE}{i}.{C.RESET} {_model_display_name(m)}{active}") + choice = input(f"{C.CYAN}?{C.RESET} select model [1-{len(models)}] (Enter to keep): ").strip() + if not choice: + return _model_value(models[default_idx]) + try: + idx = int(choice) - 1 + except ValueError: + warn("invalid model selection") + return None + if idx < 0 or idx >= len(models): + warn("invalid model selection") + return None + return _model_value(models[idx]) + + +def _pick_model_interactive(models: list[dict[str, Any]], current_model: str) -> str | None: + if not models: + return None + if not sys.stdin.isatty() or not sys.stdout.isatty() or termios is None or tty is None: + return _pick_model_with_numbers(models, current_model) + + selected = 0 + for i, m in enumerate(models): + if _model_matches_current(m, current_model): + selected = i + break + + lines_rendered = 0 + + def _render() -> None: + nonlocal lines_rendered + lines: list[str] = [] + lines.append(f"{C.BLUE}select model (โ†‘/โ†“, Enter=select, q=cancel){C.RESET}") + for i, m in enumerate(models): + pointer = "โ€บ" if i == selected else " " + active = f" {C.DIM}[active]{C.RESET}" if _model_matches_current(m, current_model) else "" + line = f" {C.CYAN}{pointer}{C.RESET} {_model_display_name(m)}{active}" + lines.append(line) + + if lines_rendered > 0: + sys.stdout.write(f"\033[{lines_rendered}A") + for line in lines: + sys.stdout.write(f"\r\033[K{line}\n") + sys.stdout.flush() + lines_rendered = len(lines) + + fd = sys.stdin.fileno() + old_attrs = termios.tcgetattr(fd) + try: + tty.setraw(fd) + _render() + while True: + ch = sys.stdin.read(1) + if ch in ("\r", "\n"): + sys.stdout.write("\r\033[K") + return _model_value(models[selected]) + if ch in ("q", "Q"): + sys.stdout.write("\r\033[K") + return None + if ch == "\x1b": + seq1 = sys.stdin.read(1) + if seq1 == "[": + seq2 = sys.stdin.read(1) + if seq2 == "A": + selected = (selected - 1) % len(models) + _render() + continue + if seq2 == "B": + selected = (selected + 1) % len(models) + _render() + continue + return None + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs) + if lines_rendered > 0: + sys.stdout.write(f"\033[{lines_rendered}A") + for _ in range(lines_rendered): + sys.stdout.write("\r\033[K\n") + sys.stdout.write(f"\033[{lines_rendered}A") + sys.stdout.flush() + + +def _fetch_models_payload(client: httpx.Client, ip: str) -> list[dict[str, Any]]: + resp = client.get(f"http://{ip}/if2/v1/models", timeout=15.0) + resp.raise_for_status() + payload = resp.json() + raw = payload.get("data", []) + if not isinstance(raw, list): + raise RuntimeError("invalid models payload") + out: list[dict[str, Any]] = [] + for m in raw: + if isinstance(m, dict): + out.append(m) + try: + out.sort(key=lambda m: str(m.get("name") or m.get("id") or m.get("uuid") or "")) + except Exception: pass + + return out diff --git a/truffile/cli/ui.py b/truffile/cli/ui.py new file mode 100644 index 0000000..c87d969 --- /dev/null +++ b/truffile/cli/ui.py @@ -0,0 +1,190 @@ +import sys +import threading +import time +from pathlib import Path + + +class C: + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + MAGENTA = "\033[95m" + CYAN = "\033[96m" + GRAY = "\033[90m" + DIM = "\033[2m" + BOLD = "\033[1m" + RESET = "\033[0m" + + +MUSHROOM = "๐Ÿ„โ€๐ŸŸซ" +CHECK = "โœ“" +CROSS = "โœ—" +ARROW = "โ†’" +DOT = "โ€ข" +WARN = "โš " +HAMMER = "๐Ÿ”จ" +SUPPORTED_SERVER_MIME_TYPES = {"image/jpeg", "image/png", "image/bmp"} +SCAFFOLD_ICON_RESOURCE_REL = Path("assets") / "Truffle.png" + + +class Spinner: + FRAMES = ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "] + + def __init__(self, message: str): + self.message = message + self.running = False + self.thread = None + self.frame_idx = 0 + + def _spin(self): + while self.running: + frame = self.FRAMES[self.frame_idx % len(self.FRAMES)] + sys.stdout.write(f"\r{C.CYAN}{frame}{C.RESET} {self.message}") + sys.stdout.flush() + self.frame_idx += 1 + time.sleep(0.08) + + def start(self): + self.running = True + self.thread = threading.Thread(target=self._spin, daemon=True) + self.thread.start() + + def stop(self, success: bool = True): + self.running = False + if self.thread: + self.thread.join(timeout=0.2) + icon = f"{C.GREEN}{CHECK}{C.RESET}" if success else f"{C.RED}{CROSS}{C.RESET}" + sys.stdout.write(f"\r{icon} {self.message}\n") + sys.stdout.flush() + + def fail(self, message: str | None = None): + self.running = False + if self.thread: + self.thread.join(timeout=0.2) + msg = message or self.message + sys.stdout.write(f"\r{C.RED}{CROSS}{C.RESET} {msg}\n") + sys.stdout.flush() + + +class MushroomPulse: + FRAMES = ["(๐Ÿ„ )", "(๐Ÿ„. )", "(๐Ÿ„.. )", "(๐Ÿ„...)", "(๐Ÿ„ ..)", "(๐Ÿ„ .)"] + + def __init__(self, message: str = "thinking", interval: float = 0.09): + self.message = message + self.interval = interval + self.running = False + self.thread: threading.Thread | None = None + self.frame_idx = 0 + self.enabled = bool(sys.stdout.isatty()) + + def _spin(self) -> None: + while self.running: + frame = self.FRAMES[self.frame_idx % len(self.FRAMES)] + sys.stdout.write(f"\r{C.MAGENTA}{frame}{C.RESET} {C.DIM}{self.message}{C.RESET}") + sys.stdout.flush() + self.frame_idx += 1 + time.sleep(self.interval) + + def start(self) -> None: + if not self.enabled or self.running: + return + self.running = True + self.thread = threading.Thread(target=self._spin, daemon=True) + self.thread.start() + + def stop(self) -> None: + if not self.running: + return + self.running = False + if self.thread: + self.thread.join(timeout=0.2) + sys.stdout.write("\r\033[K") + sys.stdout.flush() + + +class ScrollingLog: + def __init__(self, height: int = 6, prefix: str = " "): + self.height = height + self.prefix = prefix + self.lines: list[str] = [] + self.started = False + try: + import shutil + self.width = shutil.get_terminal_size().columns - len(prefix) - 2 + except Exception: + self.width = 76 + + def _truncate(self, line: str) -> str: + if len(line) > self.width: + return line[:self.width - 3] + "..." + return line + + def _render(self): + if self.started: + sys.stdout.write(f"\033[{self.height}A") + display = self.lines[-self.height:] if len(self.lines) >= self.height else self.lines + while len(display) < self.height: + display.insert(0, "") + for line in display: + truncated = self._truncate(line) + sys.stdout.write(f"\033[K{self.prefix}{C.DIM}{truncated}{C.RESET}\n") + sys.stdout.flush() + self.started = True + + def add(self, line: str): + self.lines.append(line.rstrip()) + self._render() + + def finish(self): + if self.started: + sys.stdout.write(f"\033[{self.height}A") + for _ in range(self.height): + sys.stdout.write("\033[K\n") + sys.stdout.write(f"\033[{self.height}A") + sys.stdout.flush() + + +def error(msg: str): + print(f"{C.RED}{CROSS} Error:{C.RESET} {msg}") + + +def warn(msg: str): + print(f"{C.YELLOW}{WARN} Warning:{C.RESET} {msg}") + + +def success(msg: str): + print(f"{C.GREEN}{CHECK}{C.RESET} {msg}") + + +def info(msg: str): + print(f"{C.CYAN}{DOT}{C.RESET} {msg}") + + +def print_help(): + print(f""" +{C.BOLD}{MUSHROOM} truffile{C.RESET} โ€” TruffleOS SDK + +{C.BOLD}Usage:{C.RESET} truffile [options] + +{C.BOLD}Commands:{C.RESET} + {C.CYAN}scan{C.RESET} scan for truffle devices on the network + {C.CYAN}connect{C.RESET} connect to a truffle device + {C.CYAN}disconnect{C.RESET} [device|all] disconnect from device(s) + {C.CYAN}create{C.RESET} [name] scaffold a new app + {C.CYAN}validate{C.RESET} [path] validate app directory + {C.CYAN}deploy{C.RESET} [path] deploy app to connected device + {C.CYAN}list{C.RESET} list installed apps or connected devices + {C.CYAN}delete{C.RESET} [app] delete app from device + {C.CYAN}models{C.RESET} list inference models on device + {C.CYAN}chat{C.RESET} interactive chat with device + {C.CYAN}help{C.RESET} show this help + +{C.BOLD}Examples:{C.RESET} + truffile scan + truffile connect truffle-1234 + truffile create my-app + truffile deploy ./my-app + truffile list apps + truffile chat +""") diff --git a/truffile/cli/validate.py b/truffile/cli/validate.py new file mode 100644 index 0000000..f29a69c --- /dev/null +++ b/truffile/cli/validate.py @@ -0,0 +1,24 @@ +from pathlib import Path + +from truffile.schema import validate_app_dir + +from .ui import C, error, warn, info, success + + +def cmd_validate(args) -> int: + app_dir = Path(args.path).resolve() + if not app_dir.exists() or not app_dir.is_dir(): + error(f"{app_dir} is not a valid directory") + return 1 + + info(f"Validating app in {app_dir.name}") + valid, _config, app_type, warnings, errors = validate_app_dir(app_dir) + for w in warnings: + warn(w) + if not valid: + for e in errors: + error(e) + return 1 + + success(f"Validation passed ({app_type})") + return 0 From 5480dcced8cc0251eec63602598cf9b21a154f34 Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Fri, 3 Apr 2026 12:55:58 -0700 Subject: [PATCH 02/12] protos during publishing keep repo clean --- truffile/_version.py | 28 +- truffile/sdk.py | 75 + truffle/__init__.py | 0 truffle/app/__init__.py | 0 truffle/app/app_build_pb2.py | 36 - truffle/app/app_build_pb2.pyi | 19 - truffle/app/app_build_pb2_grpc.py | 24 - truffle/app/app_install_pb2.py | 48 - truffle/app/app_install_pb2.pyi | 25 - truffle/app/app_install_pb2_grpc.py | 142 -- truffle/app/app_pb2.py | 48 - truffle/app/app_pb2.pyi | 69 - truffle/app/app_pb2_grpc.py | 24 - truffle/app/app_runtime_pb2.py | 41 - truffle/app/app_runtime_pb2.pyi | 21 - truffle/app/app_runtime_pb2_grpc.py | 97 - truffle/app/background_pb2.py | 83 - truffle/app/background_pb2.pyi | 164 -- truffle/app/background_pb2_grpc.py | 194 -- truffle/app/default_app_manifest_pb2.py | 41 - truffle/app/default_app_manifest_pb2.pyi | 37 - truffle/app/default_app_manifest_pb2_grpc.py | 24 - truffle/app/foreground_pb2.py | 41 - truffle/app/foreground_pb2.pyi | 29 - truffle/app/foreground_pb2_grpc.py | 24 - truffle/common/__init__.py | 0 truffle/common/content_pb2.py | 38 - truffle/common/content_pb2.pyi | 21 - truffle/common/content_pb2_grpc.py | 24 - truffle/common/file_pb2.py | 40 - truffle/common/file_pb2.pyi | 30 - truffle/common/file_pb2_grpc.py | 24 - truffle/common/icon_pb2.py | 36 - truffle/common/icon_pb2.pyi | 11 - truffle/common/icon_pb2_grpc.py | 24 - truffle/common/tool_provider_pb2.py | 40 - truffle/common/tool_provider_pb2.pyi | 33 - truffle/common/tool_provider_pb2_grpc.py | 24 - truffle/os/__init__.py | 0 truffle/os/app_queries_pb2.py | 43 - truffle/os/app_queries_pb2.pyi | 28 - truffle/os/app_queries_pb2_grpc.py | 24 - truffle/os/background_feed_pb2.py | 50 - truffle/os/background_feed_pb2.pyi | 72 - truffle/os/background_feed_pb2_grpc.py | 24 - truffle/os/background_feed_queries_pb2.py | 44 - truffle/os/background_feed_queries_pb2.pyi | 39 - .../os/background_feed_queries_pb2_grpc.py | 24 - truffle/os/builder_pb2.py | 48 - truffle/os/builder_pb2.pyi | 52 - truffle/os/builder_pb2_grpc.py | 24 - truffle/os/client_metadata_pb2.py | 36 - truffle/os/client_metadata_pb2.pyi | 15 - truffle/os/client_metadata_pb2_grpc.py | 24 - truffle/os/client_session_pb2.py | 50 - truffle/os/client_session_pb2.pyi | 74 - truffle/os/client_session_pb2_grpc.py | 24 - truffle/os/client_state_pb2.py | 52 - truffle/os/client_state_pb2.pyi | 54 - truffle/os/client_state_pb2_grpc.py | 24 - truffle/os/client_user_pb2.py | 43 - truffle/os/client_user_pb2.pyi | 35 - truffle/os/client_user_pb2_grpc.py | 24 - truffle/os/hardware_control_pb2.py | 40 - truffle/os/hardware_control_pb2.pyi | 24 - truffle/os/hardware_control_pb2_grpc.py | 24 - truffle/os/hardware_info_pb2.py | 38 - truffle/os/hardware_info_pb2.pyi | 30 - truffle/os/hardware_info_pb2_grpc.py | 24 - truffle/os/hardware_network_pb2.py | 38 - truffle/os/hardware_network_pb2.pyi | 25 - truffle/os/hardware_network_pb2_grpc.py | 24 - truffle/os/hardware_settings_pb2.py | 38 - truffle/os/hardware_settings_pb2.pyi | 21 - truffle/os/hardware_settings_pb2_grpc.py | 24 - truffle/os/hardware_stats_pb2.py | 47 - truffle/os/hardware_stats_pb2.pyi | 85 - truffle/os/hardware_stats_pb2_grpc.py | 24 - truffle/os/installer_pb2.py | 90 - truffle/os/installer_pb2.pyi | 218 --- truffle/os/installer_pb2_grpc.py | 24 - truffle/os/notification_pb2.py | 45 - truffle/os/notification_pb2.pyi | 56 - truffle/os/notification_pb2_grpc.py | 24 - truffle/os/proactivity_pb2.py | 51 - truffle/os/proactivity_pb2.pyi | 79 - truffle/os/proactivity_pb2_grpc.py | 24 - truffle/os/system_info_pb2.py | 51 - truffle/os/system_info_pb2.pyi | 52 - truffle/os/system_info_pb2_grpc.py | 24 - truffle/os/system_settings_pb2.py | 40 - truffle/os/system_settings_pb2.pyi | 20 - truffle/os/system_settings_pb2_grpc.py | 24 - truffle/os/task_actions_pb2.py | 77 - truffle/os/task_actions_pb2.pyi | 88 - truffle/os/task_actions_pb2_grpc.py | 24 - truffle/os/task_error_pb2.py | 37 - truffle/os/task_error_pb2.pyi | 17 - truffle/os/task_error_pb2_grpc.py | 24 - truffle/os/task_info_pb2.py | 42 - truffle/os/task_info_pb2.pyi | 53 - truffle/os/task_info_pb2_grpc.py | 24 - truffle/os/task_options_pb2.py | 37 - truffle/os/task_options_pb2.pyi | 10 - truffle/os/task_options_pb2_grpc.py | 24 - truffle/os/task_pb2.py | 57 - truffle/os/task_pb2.pyi | 84 - truffle/os/task_pb2_grpc.py | 24 - truffle/os/task_queries_pb2.py | 62 - truffle/os/task_queries_pb2.pyi | 55 - truffle/os/task_queries_pb2_grpc.py | 24 - truffle/os/task_search_pb2.py | 44 - truffle/os/task_search_pb2.pyi | 48 - truffle/os/task_search_pb2_grpc.py | 24 - truffle/os/task_step_pb2.py | 50 - truffle/os/task_step_pb2.pyi | 67 - truffle/os/task_step_pb2_grpc.py | 24 - truffle/os/task_target_pb2.py | 36 - truffle/os/task_target_pb2.pyi | 13 - truffle/os/task_target_pb2_grpc.py | 24 - truffle/os/task_user_response_pb2.py | 42 - truffle/os/task_user_response_pb2.pyi | 37 - truffle/os/task_user_response_pb2_grpc.py | 24 - truffle/os/truffleos_pb2.py | 205 --- truffle/os/truffleos_pb2.pyi | 107 -- truffle/os/truffleos_pb2_grpc.py | 1591 ----------------- 126 files changed, 84 insertions(+), 6949 deletions(-) create mode 100644 truffile/sdk.py delete mode 100644 truffle/__init__.py delete mode 100644 truffle/app/__init__.py delete mode 100644 truffle/app/app_build_pb2.py delete mode 100644 truffle/app/app_build_pb2.pyi delete mode 100644 truffle/app/app_build_pb2_grpc.py delete mode 100644 truffle/app/app_install_pb2.py delete mode 100644 truffle/app/app_install_pb2.pyi delete mode 100644 truffle/app/app_install_pb2_grpc.py delete mode 100644 truffle/app/app_pb2.py delete mode 100644 truffle/app/app_pb2.pyi delete mode 100644 truffle/app/app_pb2_grpc.py delete mode 100644 truffle/app/app_runtime_pb2.py delete mode 100644 truffle/app/app_runtime_pb2.pyi delete mode 100644 truffle/app/app_runtime_pb2_grpc.py delete mode 100644 truffle/app/background_pb2.py delete mode 100644 truffle/app/background_pb2.pyi delete mode 100644 truffle/app/background_pb2_grpc.py delete mode 100644 truffle/app/default_app_manifest_pb2.py delete mode 100644 truffle/app/default_app_manifest_pb2.pyi delete mode 100644 truffle/app/default_app_manifest_pb2_grpc.py delete mode 100644 truffle/app/foreground_pb2.py delete mode 100644 truffle/app/foreground_pb2.pyi delete mode 100644 truffle/app/foreground_pb2_grpc.py delete mode 100644 truffle/common/__init__.py delete mode 100644 truffle/common/content_pb2.py delete mode 100644 truffle/common/content_pb2.pyi delete mode 100644 truffle/common/content_pb2_grpc.py delete mode 100644 truffle/common/file_pb2.py delete mode 100644 truffle/common/file_pb2.pyi delete mode 100644 truffle/common/file_pb2_grpc.py delete mode 100644 truffle/common/icon_pb2.py delete mode 100644 truffle/common/icon_pb2.pyi delete mode 100644 truffle/common/icon_pb2_grpc.py delete mode 100644 truffle/common/tool_provider_pb2.py delete mode 100644 truffle/common/tool_provider_pb2.pyi delete mode 100644 truffle/common/tool_provider_pb2_grpc.py delete mode 100644 truffle/os/__init__.py delete mode 100644 truffle/os/app_queries_pb2.py delete mode 100644 truffle/os/app_queries_pb2.pyi delete mode 100644 truffle/os/app_queries_pb2_grpc.py delete mode 100644 truffle/os/background_feed_pb2.py delete mode 100644 truffle/os/background_feed_pb2.pyi delete mode 100644 truffle/os/background_feed_pb2_grpc.py delete mode 100644 truffle/os/background_feed_queries_pb2.py delete mode 100644 truffle/os/background_feed_queries_pb2.pyi delete mode 100644 truffle/os/background_feed_queries_pb2_grpc.py delete mode 100644 truffle/os/builder_pb2.py delete mode 100644 truffle/os/builder_pb2.pyi delete mode 100644 truffle/os/builder_pb2_grpc.py delete mode 100644 truffle/os/client_metadata_pb2.py delete mode 100644 truffle/os/client_metadata_pb2.pyi delete mode 100644 truffle/os/client_metadata_pb2_grpc.py delete mode 100644 truffle/os/client_session_pb2.py delete mode 100644 truffle/os/client_session_pb2.pyi delete mode 100644 truffle/os/client_session_pb2_grpc.py delete mode 100644 truffle/os/client_state_pb2.py delete mode 100644 truffle/os/client_state_pb2.pyi delete mode 100644 truffle/os/client_state_pb2_grpc.py delete mode 100644 truffle/os/client_user_pb2.py delete mode 100644 truffle/os/client_user_pb2.pyi delete mode 100644 truffle/os/client_user_pb2_grpc.py delete mode 100644 truffle/os/hardware_control_pb2.py delete mode 100644 truffle/os/hardware_control_pb2.pyi delete mode 100644 truffle/os/hardware_control_pb2_grpc.py delete mode 100644 truffle/os/hardware_info_pb2.py delete mode 100644 truffle/os/hardware_info_pb2.pyi delete mode 100644 truffle/os/hardware_info_pb2_grpc.py delete mode 100644 truffle/os/hardware_network_pb2.py delete mode 100644 truffle/os/hardware_network_pb2.pyi delete mode 100644 truffle/os/hardware_network_pb2_grpc.py delete mode 100644 truffle/os/hardware_settings_pb2.py delete mode 100644 truffle/os/hardware_settings_pb2.pyi delete mode 100644 truffle/os/hardware_settings_pb2_grpc.py delete mode 100644 truffle/os/hardware_stats_pb2.py delete mode 100644 truffle/os/hardware_stats_pb2.pyi delete mode 100644 truffle/os/hardware_stats_pb2_grpc.py delete mode 100644 truffle/os/installer_pb2.py delete mode 100644 truffle/os/installer_pb2.pyi delete mode 100644 truffle/os/installer_pb2_grpc.py delete mode 100644 truffle/os/notification_pb2.py delete mode 100644 truffle/os/notification_pb2.pyi delete mode 100644 truffle/os/notification_pb2_grpc.py delete mode 100644 truffle/os/proactivity_pb2.py delete mode 100644 truffle/os/proactivity_pb2.pyi delete mode 100644 truffle/os/proactivity_pb2_grpc.py delete mode 100644 truffle/os/system_info_pb2.py delete mode 100644 truffle/os/system_info_pb2.pyi delete mode 100644 truffle/os/system_info_pb2_grpc.py delete mode 100644 truffle/os/system_settings_pb2.py delete mode 100644 truffle/os/system_settings_pb2.pyi delete mode 100644 truffle/os/system_settings_pb2_grpc.py delete mode 100644 truffle/os/task_actions_pb2.py delete mode 100644 truffle/os/task_actions_pb2.pyi delete mode 100644 truffle/os/task_actions_pb2_grpc.py delete mode 100644 truffle/os/task_error_pb2.py delete mode 100644 truffle/os/task_error_pb2.pyi delete mode 100644 truffle/os/task_error_pb2_grpc.py delete mode 100644 truffle/os/task_info_pb2.py delete mode 100644 truffle/os/task_info_pb2.pyi delete mode 100644 truffle/os/task_info_pb2_grpc.py delete mode 100644 truffle/os/task_options_pb2.py delete mode 100644 truffle/os/task_options_pb2.pyi delete mode 100644 truffle/os/task_options_pb2_grpc.py delete mode 100644 truffle/os/task_pb2.py delete mode 100644 truffle/os/task_pb2.pyi delete mode 100644 truffle/os/task_pb2_grpc.py delete mode 100644 truffle/os/task_queries_pb2.py delete mode 100644 truffle/os/task_queries_pb2.pyi delete mode 100644 truffle/os/task_queries_pb2_grpc.py delete mode 100644 truffle/os/task_search_pb2.py delete mode 100644 truffle/os/task_search_pb2.pyi delete mode 100644 truffle/os/task_search_pb2_grpc.py delete mode 100644 truffle/os/task_step_pb2.py delete mode 100644 truffle/os/task_step_pb2.pyi delete mode 100644 truffle/os/task_step_pb2_grpc.py delete mode 100644 truffle/os/task_target_pb2.py delete mode 100644 truffle/os/task_target_pb2.pyi delete mode 100644 truffle/os/task_target_pb2_grpc.py delete mode 100644 truffle/os/task_user_response_pb2.py delete mode 100644 truffle/os/task_user_response_pb2.pyi delete mode 100644 truffle/os/task_user_response_pb2_grpc.py delete mode 100644 truffle/os/truffleos_pb2.py delete mode 100644 truffle/os/truffleos_pb2.pyi delete mode 100644 truffle/os/truffleos_pb2_grpc.py diff --git a/truffile/_version.py b/truffile/_version.py index 8565e31..b4a3585 100644 --- a/truffile/_version.py +++ b/truffile/_version.py @@ -1,5 +1,6 @@ -# file generated by setuptools-scm +# file generated by vcs-versioning # don't change, don't track in version control +from __future__ import annotations __all__ = [ "__version__", @@ -10,25 +11,14 @@ "commit_id", ] -TYPE_CHECKING = False -if TYPE_CHECKING: - from typing import Tuple - from typing import Union - - VERSION_TUPLE = Tuple[Union[int, str], ...] - COMMIT_ID = Union[str, None] -else: - VERSION_TUPLE = object - COMMIT_ID = object - version: str __version__: str -__version_tuple__: VERSION_TUPLE -version_tuple: VERSION_TUPLE -commit_id: COMMIT_ID -__commit_id__: COMMIT_ID +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None -__version__ = version = '0.1.9.dev38' -__version_tuple__ = version_tuple = (0, 1, 9, 'dev38') +__version__ = version = '0.1.36.dev1' +__version_tuple__ = version_tuple = (0, 1, 36, 'dev1') -__commit_id__ = commit_id = 'g2aac4c451' +__commit_id__ = commit_id = 'g6d69eecf1' diff --git a/truffile/sdk.py b/truffile/sdk.py new file mode 100644 index 0000000..e756f99 --- /dev/null +++ b/truffile/sdk.py @@ -0,0 +1,75 @@ +# truffile SDK โ€” re-exports from app_runtime under the truffile namespace. +# +# usage: +# from truffile.sdk import ForegroundApp, tool, ok, err +# from truffile.sdk import BackgroundWorkerApp +# from truffile.sdk import AppHarness, FakeHttpTransport + +from truffile.app_runtime import ( + # app authoring + ForegroundApp, + BackgroundApp, + BackgroundWorkerApp, + ToolSpec, + Submission, + ok, + err, + phosphor_icon_url, + + # auth + OAuth, + OAuthAuth, + PublicAuth, + TextConfigAuth, + VncAuth, + load_required_env, + + # protocols + ApiKeyProvider, + AuthProvider, + BrowserSessionProvider, + HttpResponse, + HttpTransport, + OAuthProvider, + TokenStore, + CookieStore, + + # stores + FileTokenStore, + MemoryTokenStore, + FileCookieStore, + MemoryCookieStore, + + # errors + AppAuthError, + AppRuntimeFailure, + AppRuntimeErrorType, + + # testing + AppHarness, + FakeApiKeyProvider, + FakeAuthProvider, + FakeBackgroundRuntime, + FakeHttpResponse, + FakeHttpTransport, + FakeOAuthProvider, + HarnessResult, + RecordedBackgroundError, + RecordedSubmission, + make_background_ctx, + + # mcp testing + McpTestServer, + call_tool, + InProcessGrpcServer, + + # utilities + parse_jsonrpc_payload, + HttpxResponseAdapter, + report_app_error, + truncate_result, + truncate_items, +) + +# nice alias โ€” truffile.sdk.tool is ToolSpec +tool = ToolSpec diff --git a/truffle/__init__.py b/truffle/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/truffle/app/__init__.py b/truffle/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/truffle/app/app_build_pb2.py b/truffle/app/app_build_pb2.py deleted file mode 100644 index f371f65..0000000 --- a/truffle/app/app_build_pb2.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/app/app_build.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/app/app_build.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1btruffle/app/app_build.proto\x12\x0btruffle.app\"D\n\rProcessConfig\x12\x0b\n\x03\x63md\x18\x01 \x01(\t\x12\x0c\n\x04\x61rgs\x18\x02 \x03(\t\x12\x0b\n\x03\x65nv\x18\x03 \x03(\t\x12\x0b\n\x03\x63wd\x18\x04 \x01(\tb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.app.app_build_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_PROCESSCONFIG']._serialized_start=44 - _globals['_PROCESSCONFIG']._serialized_end=112 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/app/app_build_pb2.pyi b/truffle/app/app_build_pb2.pyi deleted file mode 100644 index d62fb0c..0000000 --- a/truffle/app/app_build_pb2.pyi +++ /dev/null @@ -1,19 +0,0 @@ -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable -from typing import ClassVar as _ClassVar, Optional as _Optional - -DESCRIPTOR: _descriptor.FileDescriptor - -class ProcessConfig(_message.Message): - __slots__ = ("cmd", "args", "env", "cwd") - CMD_FIELD_NUMBER: _ClassVar[int] - ARGS_FIELD_NUMBER: _ClassVar[int] - ENV_FIELD_NUMBER: _ClassVar[int] - CWD_FIELD_NUMBER: _ClassVar[int] - cmd: str - args: _containers.RepeatedScalarFieldContainer[str] - env: _containers.RepeatedScalarFieldContainer[str] - cwd: str - def __init__(self, cmd: _Optional[str] = ..., args: _Optional[_Iterable[str]] = ..., env: _Optional[_Iterable[str]] = ..., cwd: _Optional[str] = ...) -> None: ... diff --git a/truffle/app/app_build_pb2_grpc.py b/truffle/app/app_build_pb2_grpc.py deleted file mode 100644 index a7be4f2..0000000 --- a/truffle/app/app_build_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/app/app_build_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/app/app_install_pb2.py b/truffle/app/app_install_pb2.py deleted file mode 100644 index 84253bc..0000000 --- a/truffle/app/app_install_pb2.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/app/app_install.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/app/app_install.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import installer_pb2 as truffle_dot_os_dot_installer__pb2 -try: - truffle_dot_app_dot_app__pb2 = truffle_dot_os_dot_installer__pb2.truffle_dot_app_dot_app__pb2 -except AttributeError: - truffle_dot_app_dot_app__pb2 = truffle_dot_os_dot_installer__pb2.truffle.app.app_pb2 -from truffle.app import app_build_pb2 as truffle_dot_app_dot_app__build__pb2 -from truffle.app import background_pb2 as truffle_dot_app_dot_background__pb2 -from truffle.app import foreground_pb2 as truffle_dot_app_dot_foreground__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dtruffle/app/app_install.proto\x12\x0btruffle.app\x1a\x1atruffle/os/installer.proto\x1a\x1btruffle/app/app_build.proto\x1a\x1ctruffle/app/background.proto\x1a\x1ctruffle/app/foreground.proto\".\n\x1aGetFinalInstallInfoRequest\x12\x10\n\x08\x61pp_uuid\x18\x01 \x01(\t\"\xc3\x01\n\x1bGetFinalInstallInfoResponse\x12?\n\rbg_build_info\x18\x01 \x01(\x0b\x32#.truffle.app.BackgroundAppBuildInfoH\x00\x88\x01\x01\x12?\n\rfg_build_info\x18\x02 \x01(\x0b\x32#.truffle.app.ForegroundAppBuildInfoH\x01\x88\x01\x01\x42\x10\n\x0e_bg_build_infoB\x10\n\x0e_fg_build_info2\xce\x01\n\x11\x41ppInstallService\x12O\n\nInstallApp\x12\x1d.truffle.os.AppInstallRequest\x1a\x1e.truffle.os.AppInstallResponse(\x01\x30\x01\x12h\n\x13GetFinalInstallInfo\x12\'.truffle.app.GetFinalInstallInfoRequest\x1a(.truffle.app.GetFinalInstallInfoResponseb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.app.app_install_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_GETFINALINSTALLINFOREQUEST']._serialized_start=163 - _globals['_GETFINALINSTALLINFOREQUEST']._serialized_end=209 - _globals['_GETFINALINSTALLINFORESPONSE']._serialized_start=212 - _globals['_GETFINALINSTALLINFORESPONSE']._serialized_end=407 - _globals['_APPINSTALLSERVICE']._serialized_start=410 - _globals['_APPINSTALLSERVICE']._serialized_end=616 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/app/app_install_pb2.pyi b/truffle/app/app_install_pb2.pyi deleted file mode 100644 index 989aa1b..0000000 --- a/truffle/app/app_install_pb2.pyi +++ /dev/null @@ -1,25 +0,0 @@ -from truffle.os import installer_pb2 as _installer_pb2 -from truffle.app import app_pb2 as _app_pb2 -from truffle.app import app_build_pb2 as _app_build_pb2 -from truffle.app import background_pb2 as _background_pb2 -from truffle.app import foreground_pb2 as _foreground_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class GetFinalInstallInfoRequest(_message.Message): - __slots__ = ("app_uuid",) - APP_UUID_FIELD_NUMBER: _ClassVar[int] - app_uuid: str - def __init__(self, app_uuid: _Optional[str] = ...) -> None: ... - -class GetFinalInstallInfoResponse(_message.Message): - __slots__ = ("bg_build_info", "fg_build_info") - BG_BUILD_INFO_FIELD_NUMBER: _ClassVar[int] - FG_BUILD_INFO_FIELD_NUMBER: _ClassVar[int] - bg_build_info: _background_pb2.BackgroundAppBuildInfo - fg_build_info: _foreground_pb2.ForegroundAppBuildInfo - def __init__(self, bg_build_info: _Optional[_Union[_background_pb2.BackgroundAppBuildInfo, _Mapping]] = ..., fg_build_info: _Optional[_Union[_foreground_pb2.ForegroundAppBuildInfo, _Mapping]] = ...) -> None: ... diff --git a/truffle/app/app_install_pb2_grpc.py b/truffle/app/app_install_pb2_grpc.py deleted file mode 100644 index e888508..0000000 --- a/truffle/app/app_install_pb2_grpc.py +++ /dev/null @@ -1,142 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - -from truffle.app import app_install_pb2 as truffle_dot_app_dot_app__install__pb2 -from truffle.os import installer_pb2 as truffle_dot_os_dot_installer__pb2 - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/app/app_install_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) - - -class AppInstallServiceStub(object): - """Missing associated documentation comment in .proto file.""" - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.InstallApp = channel.stream_stream( - '/truffle.app.AppInstallService/InstallApp', - request_serializer=truffle_dot_os_dot_installer__pb2.AppInstallRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_installer__pb2.AppInstallResponse.FromString, - _registered_method=True) - self.GetFinalInstallInfo = channel.unary_unary( - '/truffle.app.AppInstallService/GetFinalInstallInfo', - request_serializer=truffle_dot_app_dot_app__install__pb2.GetFinalInstallInfoRequest.SerializeToString, - response_deserializer=truffle_dot_app_dot_app__install__pb2.GetFinalInstallInfoResponse.FromString, - _registered_method=True) - - -class AppInstallServiceServicer(object): - """Missing associated documentation comment in .proto file.""" - - def InstallApp(self, request_iterator, context): - """bidi stream for install modals and flow - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def GetFinalInstallInfo(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_AppInstallServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'InstallApp': grpc.stream_stream_rpc_method_handler( - servicer.InstallApp, - request_deserializer=truffle_dot_os_dot_installer__pb2.AppInstallRequest.FromString, - response_serializer=truffle_dot_os_dot_installer__pb2.AppInstallResponse.SerializeToString, - ), - 'GetFinalInstallInfo': grpc.unary_unary_rpc_method_handler( - servicer.GetFinalInstallInfo, - request_deserializer=truffle_dot_app_dot_app__install__pb2.GetFinalInstallInfoRequest.FromString, - response_serializer=truffle_dot_app_dot_app__install__pb2.GetFinalInstallInfoResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'truffle.app.AppInstallService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('truffle.app.AppInstallService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class AppInstallService(object): - """Missing associated documentation comment in .proto file.""" - - @staticmethod - def InstallApp(request_iterator, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.stream_stream( - request_iterator, - target, - '/truffle.app.AppInstallService/InstallApp', - truffle_dot_os_dot_installer__pb2.AppInstallRequest.SerializeToString, - truffle_dot_os_dot_installer__pb2.AppInstallResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def GetFinalInstallInfo(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.app.AppInstallService/GetFinalInstallInfo', - truffle_dot_app_dot_app__install__pb2.GetFinalInstallInfoRequest.SerializeToString, - truffle_dot_app_dot_app__install__pb2.GetFinalInstallInfoResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) diff --git a/truffle/app/app_pb2.py b/truffle/app/app_pb2.py deleted file mode 100644 index bc62753..0000000 --- a/truffle/app/app_pb2.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/app/app.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/app/app.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.common import icon_pb2 as truffle_dot_common_dot_icon__pb2 -from truffle.app import foreground_pb2 as truffle_dot_app_dot_foreground__pb2 -from truffle.app import background_pb2 as truffle_dot_app_dot_background__pb2 -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15truffle/app/app.proto\x12\x0btruffle.app\x1a\x19truffle/common/icon.proto\x1a\x1ctruffle/app/foreground.proto\x1a\x1ctruffle/app/background.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"g\n\x0b\x41ppMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\"\n\x04icon\x18\x02 \x01(\x0b\x32\x14.truffle.common.Icon\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12\x11\n\tbundle_id\x18\x04 \x01(\t\"$\n\tAppConfig\x12\x17\n\x0f\x63\x61n_reconfigure\x18\x01 \x01(\x08\"\x8b\x03\n\x03\x41pp\x12\x0c\n\x04uuid\x18\x01 \x01(\t\x12*\n\x08metadata\x18\x02 \x01(\x0b\x32\x18.truffle.app.AppMetadata\x12\x33\n\nforeground\x18\x03 \x01(\x0b\x32\x1a.truffle.app.ForegroundAppH\x00\x88\x01\x01\x12\x33\n\nbackground\x18\x04 \x01(\x0b\x32\x1a.truffle.app.BackgroundAppH\x01\x88\x01\x01\x12)\n\x05\x65rror\x18\x05 \x01(\x0b\x32\x15.truffle.app.AppErrorH\x02\x88\x01\x01\x12&\n\x06\x63onfig\x18\x06 \x01(\x0b\x32\x16.truffle.app.AppConfig\x12\x30\n\x0cinstalled_at\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x33\n\x0flast_updated_at\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\r\n\x0b_foregroundB\r\n\x0b_backgroundB\x08\n\x06_error\"\xc1\x01\n\x08\x41ppError\x12\x33\n\nerror_type\x18\x01 \x01(\x0e\x32\x1f.truffle.app.AppError.ErrorType\x12\x15\n\rerror_message\x18\x02 \x01(\t\"i\n\tErrorType\x12\x1a\n\x16\x41PP_ERROR_TYPE_INVALID\x10\x00\x12\x15\n\x11\x41PP_ERROR_RUNTIME\x10\x01\x12\x12\n\x0e\x41PP_ERROR_AUTH\x10\x02\x12\x15\n\x11\x41PP_ERROR_UNKNOWN\x10\x03\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.app.app_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_APPMETADATA']._serialized_start=158 - _globals['_APPMETADATA']._serialized_end=261 - _globals['_APPCONFIG']._serialized_start=263 - _globals['_APPCONFIG']._serialized_end=299 - _globals['_APP']._serialized_start=302 - _globals['_APP']._serialized_end=697 - _globals['_APPERROR']._serialized_start=700 - _globals['_APPERROR']._serialized_end=893 - _globals['_APPERROR_ERRORTYPE']._serialized_start=788 - _globals['_APPERROR_ERRORTYPE']._serialized_end=893 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/app/app_pb2.pyi b/truffle/app/app_pb2.pyi deleted file mode 100644 index a9d03e7..0000000 --- a/truffle/app/app_pb2.pyi +++ /dev/null @@ -1,69 +0,0 @@ -import datetime - -from truffle.common import icon_pb2 as _icon_pb2 -from truffle.app import foreground_pb2 as _foreground_pb2 -from truffle.app import background_pb2 as _background_pb2 -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class AppMetadata(_message.Message): - __slots__ = ("name", "icon", "description", "bundle_id") - NAME_FIELD_NUMBER: _ClassVar[int] - ICON_FIELD_NUMBER: _ClassVar[int] - DESCRIPTION_FIELD_NUMBER: _ClassVar[int] - BUNDLE_ID_FIELD_NUMBER: _ClassVar[int] - name: str - icon: _icon_pb2.Icon - description: str - bundle_id: str - def __init__(self, name: _Optional[str] = ..., icon: _Optional[_Union[_icon_pb2.Icon, _Mapping]] = ..., description: _Optional[str] = ..., bundle_id: _Optional[str] = ...) -> None: ... - -class AppConfig(_message.Message): - __slots__ = ("can_reconfigure",) - CAN_RECONFIGURE_FIELD_NUMBER: _ClassVar[int] - can_reconfigure: bool - def __init__(self, can_reconfigure: bool = ...) -> None: ... - -class App(_message.Message): - __slots__ = ("uuid", "metadata", "foreground", "background", "error", "config", "installed_at", "last_updated_at") - UUID_FIELD_NUMBER: _ClassVar[int] - METADATA_FIELD_NUMBER: _ClassVar[int] - FOREGROUND_FIELD_NUMBER: _ClassVar[int] - BACKGROUND_FIELD_NUMBER: _ClassVar[int] - ERROR_FIELD_NUMBER: _ClassVar[int] - CONFIG_FIELD_NUMBER: _ClassVar[int] - INSTALLED_AT_FIELD_NUMBER: _ClassVar[int] - LAST_UPDATED_AT_FIELD_NUMBER: _ClassVar[int] - uuid: str - metadata: AppMetadata - foreground: _foreground_pb2.ForegroundApp - background: _background_pb2.BackgroundApp - error: AppError - config: AppConfig - installed_at: _timestamp_pb2.Timestamp - last_updated_at: _timestamp_pb2.Timestamp - def __init__(self, uuid: _Optional[str] = ..., metadata: _Optional[_Union[AppMetadata, _Mapping]] = ..., foreground: _Optional[_Union[_foreground_pb2.ForegroundApp, _Mapping]] = ..., background: _Optional[_Union[_background_pb2.BackgroundApp, _Mapping]] = ..., error: _Optional[_Union[AppError, _Mapping]] = ..., config: _Optional[_Union[AppConfig, _Mapping]] = ..., installed_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., last_updated_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... - -class AppError(_message.Message): - __slots__ = ("error_type", "error_message") - class ErrorType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - APP_ERROR_TYPE_INVALID: _ClassVar[AppError.ErrorType] - APP_ERROR_RUNTIME: _ClassVar[AppError.ErrorType] - APP_ERROR_AUTH: _ClassVar[AppError.ErrorType] - APP_ERROR_UNKNOWN: _ClassVar[AppError.ErrorType] - APP_ERROR_TYPE_INVALID: AppError.ErrorType - APP_ERROR_RUNTIME: AppError.ErrorType - APP_ERROR_AUTH: AppError.ErrorType - APP_ERROR_UNKNOWN: AppError.ErrorType - ERROR_TYPE_FIELD_NUMBER: _ClassVar[int] - ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int] - error_type: AppError.ErrorType - error_message: str - def __init__(self, error_type: _Optional[_Union[AppError.ErrorType, str]] = ..., error_message: _Optional[str] = ...) -> None: ... diff --git a/truffle/app/app_pb2_grpc.py b/truffle/app/app_pb2_grpc.py deleted file mode 100644 index fa08a1a..0000000 --- a/truffle/app/app_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/app/app_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/app/app_runtime_pb2.py b/truffle/app/app_runtime_pb2.py deleted file mode 100644 index 127582a..0000000 --- a/truffle/app/app_runtime_pb2.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/app/app_runtime.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/app/app_runtime.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.app import app_pb2 as truffle_dot_app_dot_app__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dtruffle/app/app_runtime.proto\x12\x0btruffle.app\x1a\x15truffle/app/app.proto\"r\n\x1c\x41ppRuntimeReportErrorRequest\x12\x10\n\x08\x61pp_uuid\x18\x01 \x01(\t\x12$\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x15.truffle.app.AppError\x12\x1a\n\x12needs_intervention\x18\x03 \x01(\x08\"\x1f\n\x1d\x41ppRuntimeReportErrorResponse2y\n\x11\x41ppRuntimeService\x12\x64\n\x0bReportError\x12).truffle.app.AppRuntimeReportErrorRequest\x1a*.truffle.app.AppRuntimeReportErrorResponseb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.app.app_runtime_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_APPRUNTIMEREPORTERRORREQUEST']._serialized_start=69 - _globals['_APPRUNTIMEREPORTERRORREQUEST']._serialized_end=183 - _globals['_APPRUNTIMEREPORTERRORRESPONSE']._serialized_start=185 - _globals['_APPRUNTIMEREPORTERRORRESPONSE']._serialized_end=216 - _globals['_APPRUNTIMESERVICE']._serialized_start=218 - _globals['_APPRUNTIMESERVICE']._serialized_end=339 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/app/app_runtime_pb2.pyi b/truffle/app/app_runtime_pb2.pyi deleted file mode 100644 index 764c6bf..0000000 --- a/truffle/app/app_runtime_pb2.pyi +++ /dev/null @@ -1,21 +0,0 @@ -from truffle.app import app_pb2 as _app_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class AppRuntimeReportErrorRequest(_message.Message): - __slots__ = ("app_uuid", "error", "needs_intervention") - APP_UUID_FIELD_NUMBER: _ClassVar[int] - ERROR_FIELD_NUMBER: _ClassVar[int] - NEEDS_INTERVENTION_FIELD_NUMBER: _ClassVar[int] - app_uuid: str - error: _app_pb2.AppError - needs_intervention: bool - def __init__(self, app_uuid: _Optional[str] = ..., error: _Optional[_Union[_app_pb2.AppError, _Mapping]] = ..., needs_intervention: bool = ...) -> None: ... - -class AppRuntimeReportErrorResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... diff --git a/truffle/app/app_runtime_pb2_grpc.py b/truffle/app/app_runtime_pb2_grpc.py deleted file mode 100644 index b95d456..0000000 --- a/truffle/app/app_runtime_pb2_grpc.py +++ /dev/null @@ -1,97 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - -from truffle.app import app_runtime_pb2 as truffle_dot_app_dot_app__runtime__pb2 - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/app/app_runtime_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) - - -class AppRuntimeServiceStub(object): - """Missing associated documentation comment in .proto file.""" - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.ReportError = channel.unary_unary( - '/truffle.app.AppRuntimeService/ReportError', - request_serializer=truffle_dot_app_dot_app__runtime__pb2.AppRuntimeReportErrorRequest.SerializeToString, - response_deserializer=truffle_dot_app_dot_app__runtime__pb2.AppRuntimeReportErrorResponse.FromString, - _registered_method=True) - - -class AppRuntimeServiceServicer(object): - """Missing associated documentation comment in .proto file.""" - - def ReportError(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_AppRuntimeServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'ReportError': grpc.unary_unary_rpc_method_handler( - servicer.ReportError, - request_deserializer=truffle_dot_app_dot_app__runtime__pb2.AppRuntimeReportErrorRequest.FromString, - response_serializer=truffle_dot_app_dot_app__runtime__pb2.AppRuntimeReportErrorResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'truffle.app.AppRuntimeService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('truffle.app.AppRuntimeService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class AppRuntimeService(object): - """Missing associated documentation comment in .proto file.""" - - @staticmethod - def ReportError(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.app.AppRuntimeService/ReportError', - truffle_dot_app_dot_app__runtime__pb2.AppRuntimeReportErrorRequest.SerializeToString, - truffle_dot_app_dot_app__runtime__pb2.AppRuntimeReportErrorResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) diff --git a/truffle/app/background_pb2.py b/truffle/app/background_pb2.py deleted file mode 100644 index f481f34..0000000 --- a/truffle/app/background_pb2.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/app/background.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/app/background.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 -from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 -from truffle.common import icon_pb2 as truffle_dot_common_dot_icon__pb2 -from truffle.app import app_build_pb2 as truffle_dot_app_dot_app__build__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/app/background.proto\x12\x0btruffle.app\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/duration.proto\x1a google/protobuf/descriptor.proto\x1a\x19truffle/common/icon.proto\x1a\x1btruffle/app/app_build.proto\"\x9b\x0b\n\x1a\x42\x61\x63kgroundAppRuntimePolicy\x12\x44\n\x08interval\x18\x01 \x01(\x0b\x32\x30.truffle.app.BackgroundAppRuntimePolicy.IntervalH\x00\x12\x46\n\x05times\x18\x02 \x01(\x0b\x32\x35.truffle.app.BackgroundAppRuntimePolicy.SpecificTimesH\x00\x12@\n\x06\x61lways\x18\x03 \x01(\x0b\x32..truffle.app.BackgroundAppRuntimePolicy.AlwaysH\x00\x12\x37\n\x14\x66\x65\x65\x64_entry_retention\x18\n \x01(\x0b\x32\x19.google.protobuf.Duration\x1a\x39\n\tTimeOfDay\x12\x0c\n\x04hour\x18\x01 \x01(\r\x12\x0e\n\x06minute\x18\x02 \x01(\r\x12\x0e\n\x06second\x18\x03 \x01(\r\x1a\xa5\x01\n\x0b\x44\x61ilyWindow\x12K\n\x10\x64\x61ily_start_time\x18\x01 \x01(\x0b\x32\x31.truffle.app.BackgroundAppRuntimePolicy.TimeOfDay\x12I\n\x0e\x64\x61ily_end_time\x18\x02 \x01(\x0b\x32\x31.truffle.app.BackgroundAppRuntimePolicy.TimeOfDay\x1a\x91\x03\n\x0cWeeklyWindow\x12\x10\n\x08\x64\x61y_mask\x18\x01 \x01(\r\"\xee\x02\n\x05Masks\x12\x19\n\x15WEEKLY_WINDOW_DEFAULT\x10\x00\x12\x1a\n\x16WEEKLY_WINDOW_ALL_DAYS\x10\x00\x12\x1a\n\x16WEEKLY_WINDOW_SATURDAY\x10\x01\x12\x18\n\x14WEEKLY_WINDOW_FRIDAY\x10\x02\x12\x1a\n\x16WEEKLY_WINDOW_THURSDAY\x10\x04\x12\x1b\n\x17WEEKLY_WINDOW_WEDNESDAY\x10\x08\x12\x19\n\x15WEEKLY_WINDOW_TUESDAY\x10\x10\x12\x18\n\x14WEEKLY_WINDOW_MONDAY\x10 \x12\x18\n\x14WEEKLY_WINDOW_SUNDAY\x10@\x12\x1a\n\x16WEEKLY_WINDOW_WEEKENDS\x10\x41\x12\x1a\n\x16WEEKLY_WINDOW_WEEKDAYS\x10>\x12\x19\n\x15WEEKLY_WINDOW_NO_DAYS\x10\x7f\x12\x19\n\x15WEEKLY_WINDOW_INVALID\x10\x7f\x1a\x02\x10\x01\x1a\xbf\x02\n\x08Interval\x12+\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.Duration\x12K\n\x08schedule\x18\x02 \x01(\x0b\x32\x39.truffle.app.BackgroundAppRuntimePolicy.Interval.Schedule\x1a\xb8\x01\n\x08Schedule\x12N\n\x0c\x64\x61ily_window\x18\x01 \x01(\x0b\x32\x33.truffle.app.BackgroundAppRuntimePolicy.DailyWindowH\x00\x88\x01\x01\x12K\n\rweekly_window\x18\x02 \x01(\x0b\x32\x34.truffle.app.BackgroundAppRuntimePolicy.WeeklyWindowB\x0f\n\r_daily_window\x1a\xa2\x01\n\rSpecificTimes\x12\x44\n\trun_times\x18\x01 \x03(\x0b\x32\x31.truffle.app.BackgroundAppRuntimePolicy.TimeOfDay\x12K\n\rweekly_window\x18\x02 \x01(\x0b\x32\x34.truffle.app.BackgroundAppRuntimePolicy.WeeklyWindow\x1a\x08\n\x06\x41lwaysB\x06\n\x04whenJ\x04\x08\x04\x10\n\"P\n\rBackgroundApp\x12?\n\x0eruntime_policy\x18\x01 \x01(\x0b\x32\'.truffle.app.BackgroundAppRuntimePolicy\"\x86\x01\n\x16\x42\x61\x63kgroundAppBuildInfo\x12+\n\x07process\x18\x01 \x01(\x0b\x32\x1a.truffle.app.ProcessConfig\x12?\n\x0eruntime_policy\x18\x02 \x01(\x0b\x32\'.truffle.app.BackgroundAppRuntimePolicy\"\xb8\x01\n\x11\x42\x61\x63kgroundContext\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x0c\n\x04uris\x18\x02 \x03(\t\x12\x39\n\x08priority\x18\x03 \x01(\x0e\x32\'.truffle.app.BackgroundContext.Priority\"I\n\x08Priority\x12\x18\n\x14PRIORITY_UNSPECIFIED\x10\x00\x12\x10\n\x0cPRIORITY_LOW\x10\x01\x12\x11\n\rPRIORITY_HIGH\x10\x03\"T\n!BackgroundAppSubmitContextRequest\x12/\n\x07\x63ontent\x18\x01 \x01(\x0b\x32\x1e.truffle.app.BackgroundContext\"$\n\"BackgroundAppSubmitContextResponse\"\x1b\n\x19\x42\x61\x63kgroundAppOnRunRequest\"\x1c\n\x1a\x42\x61\x63kgroundAppOnRunResponse\"\x1b\n\x19\x42\x61\x63kgroundAppYieldRequest\"Y\n\x1a\x42\x61\x63kgroundAppYieldResponse\x12;\n\x17next_scheduled_run_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\"\n BackgroundAppReportErrorResponse2\xb5\x02\n\x14\x42\x61\x63kgroundAppService\x12i\n\x06Submit\x12..truffle.app.BackgroundAppSubmitContextRequest\x1a/.truffle.app.BackgroundAppSubmitContextResponse\x12X\n\x05OnRun\x12&.truffle.app.BackgroundAppOnRunRequest\x1a\'.truffle.app.BackgroundAppOnRunResponse\x12X\n\x05Yield\x12&.truffle.app.BackgroundAppYieldRequest\x1a\'.truffle.app.BackgroundAppYieldResponseb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.app.background_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_WEEKLYWINDOW_MASKS']._loaded_options = None - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_WEEKLYWINDOW_MASKS']._serialized_options = b'\020\001' - _globals['_BACKGROUNDAPPRUNTIMEPOLICY']._serialized_start=201 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY']._serialized_end=1636 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_TIMEOFDAY']._serialized_start=496 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_TIMEOFDAY']._serialized_end=553 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_DAILYWINDOW']._serialized_start=556 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_DAILYWINDOW']._serialized_end=721 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_WEEKLYWINDOW']._serialized_start=724 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_WEEKLYWINDOW']._serialized_end=1125 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_WEEKLYWINDOW_MASKS']._serialized_start=759 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_WEEKLYWINDOW_MASKS']._serialized_end=1125 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_INTERVAL']._serialized_start=1128 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_INTERVAL']._serialized_end=1447 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_INTERVAL_SCHEDULE']._serialized_start=1263 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_INTERVAL_SCHEDULE']._serialized_end=1447 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_SPECIFICTIMES']._serialized_start=1450 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_SPECIFICTIMES']._serialized_end=1612 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_ALWAYS']._serialized_start=1614 - _globals['_BACKGROUNDAPPRUNTIMEPOLICY_ALWAYS']._serialized_end=1622 - _globals['_BACKGROUNDAPP']._serialized_start=1638 - _globals['_BACKGROUNDAPP']._serialized_end=1718 - _globals['_BACKGROUNDAPPBUILDINFO']._serialized_start=1721 - _globals['_BACKGROUNDAPPBUILDINFO']._serialized_end=1855 - _globals['_BACKGROUNDCONTEXT']._serialized_start=1858 - _globals['_BACKGROUNDCONTEXT']._serialized_end=2042 - _globals['_BACKGROUNDCONTEXT_PRIORITY']._serialized_start=1969 - _globals['_BACKGROUNDCONTEXT_PRIORITY']._serialized_end=2042 - _globals['_BACKGROUNDAPPSUBMITCONTEXTREQUEST']._serialized_start=2044 - _globals['_BACKGROUNDAPPSUBMITCONTEXTREQUEST']._serialized_end=2128 - _globals['_BACKGROUNDAPPSUBMITCONTEXTRESPONSE']._serialized_start=2130 - _globals['_BACKGROUNDAPPSUBMITCONTEXTRESPONSE']._serialized_end=2166 - _globals['_BACKGROUNDAPPONRUNREQUEST']._serialized_start=2168 - _globals['_BACKGROUNDAPPONRUNREQUEST']._serialized_end=2195 - _globals['_BACKGROUNDAPPONRUNRESPONSE']._serialized_start=2197 - _globals['_BACKGROUNDAPPONRUNRESPONSE']._serialized_end=2225 - _globals['_BACKGROUNDAPPYIELDREQUEST']._serialized_start=2227 - _globals['_BACKGROUNDAPPYIELDREQUEST']._serialized_end=2254 - _globals['_BACKGROUNDAPPYIELDRESPONSE']._serialized_start=2256 - _globals['_BACKGROUNDAPPYIELDRESPONSE']._serialized_end=2345 - _globals['_BACKGROUNDAPPREPORTERRORRESPONSE']._serialized_start=2347 - _globals['_BACKGROUNDAPPREPORTERRORRESPONSE']._serialized_end=2381 - _globals['_BACKGROUNDAPPSERVICE']._serialized_start=2384 - _globals['_BACKGROUNDAPPSERVICE']._serialized_end=2693 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/app/background_pb2.pyi b/truffle/app/background_pb2.pyi deleted file mode 100644 index a22e289..0000000 --- a/truffle/app/background_pb2.pyi +++ /dev/null @@ -1,164 +0,0 @@ -import datetime - -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from google.protobuf import duration_pb2 as _duration_pb2 -from google.protobuf import descriptor_pb2 as _descriptor_pb2 -from truffle.common import icon_pb2 as _icon_pb2 -from truffle.app import app_build_pb2 as _app_build_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class BackgroundAppRuntimePolicy(_message.Message): - __slots__ = ("interval", "times", "always", "feed_entry_retention") - class TimeOfDay(_message.Message): - __slots__ = ("hour", "minute", "second") - HOUR_FIELD_NUMBER: _ClassVar[int] - MINUTE_FIELD_NUMBER: _ClassVar[int] - SECOND_FIELD_NUMBER: _ClassVar[int] - hour: int - minute: int - second: int - def __init__(self, hour: _Optional[int] = ..., minute: _Optional[int] = ..., second: _Optional[int] = ...) -> None: ... - class DailyWindow(_message.Message): - __slots__ = ("daily_start_time", "daily_end_time") - DAILY_START_TIME_FIELD_NUMBER: _ClassVar[int] - DAILY_END_TIME_FIELD_NUMBER: _ClassVar[int] - daily_start_time: BackgroundAppRuntimePolicy.TimeOfDay - daily_end_time: BackgroundAppRuntimePolicy.TimeOfDay - def __init__(self, daily_start_time: _Optional[_Union[BackgroundAppRuntimePolicy.TimeOfDay, _Mapping]] = ..., daily_end_time: _Optional[_Union[BackgroundAppRuntimePolicy.TimeOfDay, _Mapping]] = ...) -> None: ... - class WeeklyWindow(_message.Message): - __slots__ = ("day_mask",) - class Masks(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - WEEKLY_WINDOW_DEFAULT: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_ALL_DAYS: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_SATURDAY: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_FRIDAY: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_THURSDAY: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_WEDNESDAY: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_TUESDAY: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_MONDAY: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_SUNDAY: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_WEEKENDS: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_WEEKDAYS: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_NO_DAYS: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_INVALID: _ClassVar[BackgroundAppRuntimePolicy.WeeklyWindow.Masks] - WEEKLY_WINDOW_DEFAULT: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_ALL_DAYS: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_SATURDAY: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_FRIDAY: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_THURSDAY: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_WEDNESDAY: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_TUESDAY: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_MONDAY: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_SUNDAY: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_WEEKENDS: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_WEEKDAYS: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_NO_DAYS: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - WEEKLY_WINDOW_INVALID: BackgroundAppRuntimePolicy.WeeklyWindow.Masks - DAY_MASK_FIELD_NUMBER: _ClassVar[int] - day_mask: int - def __init__(self, day_mask: _Optional[int] = ...) -> None: ... - class Interval(_message.Message): - __slots__ = ("duration", "schedule") - class Schedule(_message.Message): - __slots__ = ("daily_window", "weekly_window") - DAILY_WINDOW_FIELD_NUMBER: _ClassVar[int] - WEEKLY_WINDOW_FIELD_NUMBER: _ClassVar[int] - daily_window: BackgroundAppRuntimePolicy.DailyWindow - weekly_window: BackgroundAppRuntimePolicy.WeeklyWindow - def __init__(self, daily_window: _Optional[_Union[BackgroundAppRuntimePolicy.DailyWindow, _Mapping]] = ..., weekly_window: _Optional[_Union[BackgroundAppRuntimePolicy.WeeklyWindow, _Mapping]] = ...) -> None: ... - DURATION_FIELD_NUMBER: _ClassVar[int] - SCHEDULE_FIELD_NUMBER: _ClassVar[int] - duration: _duration_pb2.Duration - schedule: BackgroundAppRuntimePolicy.Interval.Schedule - def __init__(self, duration: _Optional[_Union[datetime.timedelta, _duration_pb2.Duration, _Mapping]] = ..., schedule: _Optional[_Union[BackgroundAppRuntimePolicy.Interval.Schedule, _Mapping]] = ...) -> None: ... - class SpecificTimes(_message.Message): - __slots__ = ("run_times", "weekly_window") - RUN_TIMES_FIELD_NUMBER: _ClassVar[int] - WEEKLY_WINDOW_FIELD_NUMBER: _ClassVar[int] - run_times: _containers.RepeatedCompositeFieldContainer[BackgroundAppRuntimePolicy.TimeOfDay] - weekly_window: BackgroundAppRuntimePolicy.WeeklyWindow - def __init__(self, run_times: _Optional[_Iterable[_Union[BackgroundAppRuntimePolicy.TimeOfDay, _Mapping]]] = ..., weekly_window: _Optional[_Union[BackgroundAppRuntimePolicy.WeeklyWindow, _Mapping]] = ...) -> None: ... - class Always(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - INTERVAL_FIELD_NUMBER: _ClassVar[int] - TIMES_FIELD_NUMBER: _ClassVar[int] - ALWAYS_FIELD_NUMBER: _ClassVar[int] - FEED_ENTRY_RETENTION_FIELD_NUMBER: _ClassVar[int] - interval: BackgroundAppRuntimePolicy.Interval - times: BackgroundAppRuntimePolicy.SpecificTimes - always: BackgroundAppRuntimePolicy.Always - feed_entry_retention: _duration_pb2.Duration - def __init__(self, interval: _Optional[_Union[BackgroundAppRuntimePolicy.Interval, _Mapping]] = ..., times: _Optional[_Union[BackgroundAppRuntimePolicy.SpecificTimes, _Mapping]] = ..., always: _Optional[_Union[BackgroundAppRuntimePolicy.Always, _Mapping]] = ..., feed_entry_retention: _Optional[_Union[datetime.timedelta, _duration_pb2.Duration, _Mapping]] = ...) -> None: ... - -class BackgroundApp(_message.Message): - __slots__ = ("runtime_policy",) - RUNTIME_POLICY_FIELD_NUMBER: _ClassVar[int] - runtime_policy: BackgroundAppRuntimePolicy - def __init__(self, runtime_policy: _Optional[_Union[BackgroundAppRuntimePolicy, _Mapping]] = ...) -> None: ... - -class BackgroundAppBuildInfo(_message.Message): - __slots__ = ("process", "runtime_policy") - PROCESS_FIELD_NUMBER: _ClassVar[int] - RUNTIME_POLICY_FIELD_NUMBER: _ClassVar[int] - process: _app_build_pb2.ProcessConfig - runtime_policy: BackgroundAppRuntimePolicy - def __init__(self, process: _Optional[_Union[_app_build_pb2.ProcessConfig, _Mapping]] = ..., runtime_policy: _Optional[_Union[BackgroundAppRuntimePolicy, _Mapping]] = ...) -> None: ... - -class BackgroundContext(_message.Message): - __slots__ = ("content", "uris", "priority") - class Priority(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - PRIORITY_UNSPECIFIED: _ClassVar[BackgroundContext.Priority] - PRIORITY_LOW: _ClassVar[BackgroundContext.Priority] - PRIORITY_HIGH: _ClassVar[BackgroundContext.Priority] - PRIORITY_UNSPECIFIED: BackgroundContext.Priority - PRIORITY_LOW: BackgroundContext.Priority - PRIORITY_HIGH: BackgroundContext.Priority - CONTENT_FIELD_NUMBER: _ClassVar[int] - URIS_FIELD_NUMBER: _ClassVar[int] - PRIORITY_FIELD_NUMBER: _ClassVar[int] - content: str - uris: _containers.RepeatedScalarFieldContainer[str] - priority: BackgroundContext.Priority - def __init__(self, content: _Optional[str] = ..., uris: _Optional[_Iterable[str]] = ..., priority: _Optional[_Union[BackgroundContext.Priority, str]] = ...) -> None: ... - -class BackgroundAppSubmitContextRequest(_message.Message): - __slots__ = ("content",) - CONTENT_FIELD_NUMBER: _ClassVar[int] - content: BackgroundContext - def __init__(self, content: _Optional[_Union[BackgroundContext, _Mapping]] = ...) -> None: ... - -class BackgroundAppSubmitContextResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class BackgroundAppOnRunRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class BackgroundAppOnRunResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class BackgroundAppYieldRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class BackgroundAppYieldResponse(_message.Message): - __slots__ = ("next_scheduled_run_time",) - NEXT_SCHEDULED_RUN_TIME_FIELD_NUMBER: _ClassVar[int] - next_scheduled_run_time: _timestamp_pb2.Timestamp - def __init__(self, next_scheduled_run_time: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... - -class BackgroundAppReportErrorResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... diff --git a/truffle/app/background_pb2_grpc.py b/truffle/app/background_pb2_grpc.py deleted file mode 100644 index 5f1f658..0000000 --- a/truffle/app/background_pb2_grpc.py +++ /dev/null @@ -1,194 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - -from truffle.app import background_pb2 as truffle_dot_app_dot_background__pb2 - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/app/background_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) - - -class BackgroundAppServiceStub(object): - """ - The service exposed to background apps when running in the Truffle OS. - gated by per app api key, available in the environment when the app is run. - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Submit = channel.unary_unary( - '/truffle.app.BackgroundAppService/Submit', - request_serializer=truffle_dot_app_dot_background__pb2.BackgroundAppSubmitContextRequest.SerializeToString, - response_deserializer=truffle_dot_app_dot_background__pb2.BackgroundAppSubmitContextResponse.FromString, - _registered_method=True) - self.OnRun = channel.unary_unary( - '/truffle.app.BackgroundAppService/OnRun', - request_serializer=truffle_dot_app_dot_background__pb2.BackgroundAppOnRunRequest.SerializeToString, - response_deserializer=truffle_dot_app_dot_background__pb2.BackgroundAppOnRunResponse.FromString, - _registered_method=True) - self.Yield = channel.unary_unary( - '/truffle.app.BackgroundAppService/Yield', - request_serializer=truffle_dot_app_dot_background__pb2.BackgroundAppYieldRequest.SerializeToString, - response_deserializer=truffle_dot_app_dot_background__pb2.BackgroundAppYieldResponse.FromString, - _registered_method=True) - - -class BackgroundAppServiceServicer(object): - """ - The service exposed to background apps when running in the Truffle OS. - gated by per app api key, available in the environment when the app is run. - """ - - def Submit(self, request, context): - """post context - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def OnRun(self, request, context): - """must be called when the app is run by the system scheduler in the first few seconds of execution or the run will be considered failed - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Yield(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_BackgroundAppServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Submit': grpc.unary_unary_rpc_method_handler( - servicer.Submit, - request_deserializer=truffle_dot_app_dot_background__pb2.BackgroundAppSubmitContextRequest.FromString, - response_serializer=truffle_dot_app_dot_background__pb2.BackgroundAppSubmitContextResponse.SerializeToString, - ), - 'OnRun': grpc.unary_unary_rpc_method_handler( - servicer.OnRun, - request_deserializer=truffle_dot_app_dot_background__pb2.BackgroundAppOnRunRequest.FromString, - response_serializer=truffle_dot_app_dot_background__pb2.BackgroundAppOnRunResponse.SerializeToString, - ), - 'Yield': grpc.unary_unary_rpc_method_handler( - servicer.Yield, - request_deserializer=truffle_dot_app_dot_background__pb2.BackgroundAppYieldRequest.FromString, - response_serializer=truffle_dot_app_dot_background__pb2.BackgroundAppYieldResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'truffle.app.BackgroundAppService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('truffle.app.BackgroundAppService', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class BackgroundAppService(object): - """ - The service exposed to background apps when running in the Truffle OS. - gated by per app api key, available in the environment when the app is run. - """ - - @staticmethod - def Submit(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.app.BackgroundAppService/Submit', - truffle_dot_app_dot_background__pb2.BackgroundAppSubmitContextRequest.SerializeToString, - truffle_dot_app_dot_background__pb2.BackgroundAppSubmitContextResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def OnRun(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.app.BackgroundAppService/OnRun', - truffle_dot_app_dot_background__pb2.BackgroundAppOnRunRequest.SerializeToString, - truffle_dot_app_dot_background__pb2.BackgroundAppOnRunResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Yield(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.app.BackgroundAppService/Yield', - truffle_dot_app_dot_background__pb2.BackgroundAppYieldRequest.SerializeToString, - truffle_dot_app_dot_background__pb2.BackgroundAppYieldResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) diff --git a/truffle/app/default_app_manifest_pb2.py b/truffle/app/default_app_manifest_pb2.py deleted file mode 100644 index a996440..0000000 --- a/truffle/app/default_app_manifest_pb2.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/app/default_app_manifest.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/app/default_app_manifest.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.common import icon_pb2 as truffle_dot_common_dot_icon__pb2 -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -from truffle.app import app_pb2 as truffle_dot_app_dot_app__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&truffle/app/default_app_manifest.proto\x12\x0btruffle.app\x1a\x19truffle/common/icon.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x15truffle/app/app.proto\"\xbd\x02\n\x12\x44\x65\x66\x61ultAppManifest\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x30\n\x0cgenerated_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x38\n\x04\x61pps\x18\x03 \x03(\x0b\x32*.truffle.app.DefaultAppManifest.DefaultApp\x1a\xa9\x01\n\nDefaultApp\x12\r\n\x05index\x18\x01 \x01(\r\x12\x12\n\nbundle_url\x18\x02 \x01(\t\x12*\n\x08metadata\x18\x03 \x01(\x0b\x32\x18.truffle.app.AppMetadata\x12\x12\n\nbundle_md5\x18\x04 \x01(\t\x12\x1b\n\x13provides_foreground\x18\x05 \x01(\x08\x12\x1b\n\x13provides_background\x18\x06 \x01(\x08\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.app.default_app_manifest_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_DEFAULTAPPMANIFEST']._serialized_start=139 - _globals['_DEFAULTAPPMANIFEST']._serialized_end=456 - _globals['_DEFAULTAPPMANIFEST_DEFAULTAPP']._serialized_start=287 - _globals['_DEFAULTAPPMANIFEST_DEFAULTAPP']._serialized_end=456 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/app/default_app_manifest_pb2.pyi b/truffle/app/default_app_manifest_pb2.pyi deleted file mode 100644 index aa96a8d..0000000 --- a/truffle/app/default_app_manifest_pb2.pyi +++ /dev/null @@ -1,37 +0,0 @@ -import datetime - -from truffle.common import icon_pb2 as _icon_pb2 -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from truffle.app import app_pb2 as _app_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class DefaultAppManifest(_message.Message): - __slots__ = ("version", "generated_at", "apps") - class DefaultApp(_message.Message): - __slots__ = ("index", "bundle_url", "metadata", "bundle_md5", "provides_foreground", "provides_background") - INDEX_FIELD_NUMBER: _ClassVar[int] - BUNDLE_URL_FIELD_NUMBER: _ClassVar[int] - METADATA_FIELD_NUMBER: _ClassVar[int] - BUNDLE_MD5_FIELD_NUMBER: _ClassVar[int] - PROVIDES_FOREGROUND_FIELD_NUMBER: _ClassVar[int] - PROVIDES_BACKGROUND_FIELD_NUMBER: _ClassVar[int] - index: int - bundle_url: str - metadata: _app_pb2.AppMetadata - bundle_md5: str - provides_foreground: bool - provides_background: bool - def __init__(self, index: _Optional[int] = ..., bundle_url: _Optional[str] = ..., metadata: _Optional[_Union[_app_pb2.AppMetadata, _Mapping]] = ..., bundle_md5: _Optional[str] = ..., provides_foreground: bool = ..., provides_background: bool = ...) -> None: ... - VERSION_FIELD_NUMBER: _ClassVar[int] - GENERATED_AT_FIELD_NUMBER: _ClassVar[int] - APPS_FIELD_NUMBER: _ClassVar[int] - version: str - generated_at: _timestamp_pb2.Timestamp - apps: _containers.RepeatedCompositeFieldContainer[DefaultAppManifest.DefaultApp] - def __init__(self, version: _Optional[str] = ..., generated_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., apps: _Optional[_Iterable[_Union[DefaultAppManifest.DefaultApp, _Mapping]]] = ...) -> None: ... diff --git a/truffle/app/default_app_manifest_pb2_grpc.py b/truffle/app/default_app_manifest_pb2_grpc.py deleted file mode 100644 index f33f561..0000000 --- a/truffle/app/default_app_manifest_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/app/default_app_manifest_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/app/foreground_pb2.py b/truffle/app/foreground_pb2.py deleted file mode 100644 index d358d53..0000000 --- a/truffle/app/foreground_pb2.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/app/foreground.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/app/foreground.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.app import app_build_pb2 as truffle_dot_app_dot_app__build__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/app/foreground.proto\x12\x0btruffle.app\x1a\x1btruffle/app/app_build.proto\"\xa5\x01\n\rForegroundApp\x12\x41\n\x0f\x61vailable_tools\x18\x01 \x03(\x0b\x32(.truffle.app.ForegroundApp.AvailableTool\x1aQ\n\rAvailableTool\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x18\n\x10tool_description\x18\x02 \x01(\t\x12\x13\n\x0b\x61rgs_schema\x18\x03 \x01(\t\"E\n\x16\x46oregroundAppBuildInfo\x12+\n\x07process\x18\x01 \x01(\x0b\x32\x1a.truffle.app.ProcessConfigb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.app.foreground_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_FOREGROUNDAPP']._serialized_start=75 - _globals['_FOREGROUNDAPP']._serialized_end=240 - _globals['_FOREGROUNDAPP_AVAILABLETOOL']._serialized_start=159 - _globals['_FOREGROUNDAPP_AVAILABLETOOL']._serialized_end=240 - _globals['_FOREGROUNDAPPBUILDINFO']._serialized_start=242 - _globals['_FOREGROUNDAPPBUILDINFO']._serialized_end=311 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/app/foreground_pb2.pyi b/truffle/app/foreground_pb2.pyi deleted file mode 100644 index ffe121b..0000000 --- a/truffle/app/foreground_pb2.pyi +++ /dev/null @@ -1,29 +0,0 @@ -from truffle.app import app_build_pb2 as _app_build_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class ForegroundApp(_message.Message): - __slots__ = ("available_tools",) - class AvailableTool(_message.Message): - __slots__ = ("tool_name", "tool_description", "args_schema") - TOOL_NAME_FIELD_NUMBER: _ClassVar[int] - TOOL_DESCRIPTION_FIELD_NUMBER: _ClassVar[int] - ARGS_SCHEMA_FIELD_NUMBER: _ClassVar[int] - tool_name: str - tool_description: str - args_schema: str - def __init__(self, tool_name: _Optional[str] = ..., tool_description: _Optional[str] = ..., args_schema: _Optional[str] = ...) -> None: ... - AVAILABLE_TOOLS_FIELD_NUMBER: _ClassVar[int] - available_tools: _containers.RepeatedCompositeFieldContainer[ForegroundApp.AvailableTool] - def __init__(self, available_tools: _Optional[_Iterable[_Union[ForegroundApp.AvailableTool, _Mapping]]] = ...) -> None: ... - -class ForegroundAppBuildInfo(_message.Message): - __slots__ = ("process",) - PROCESS_FIELD_NUMBER: _ClassVar[int] - process: _app_build_pb2.ProcessConfig - def __init__(self, process: _Optional[_Union[_app_build_pb2.ProcessConfig, _Mapping]] = ...) -> None: ... diff --git a/truffle/app/foreground_pb2_grpc.py b/truffle/app/foreground_pb2_grpc.py deleted file mode 100644 index 31cdf1f..0000000 --- a/truffle/app/foreground_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/app/foreground_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/common/__init__.py b/truffle/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/truffle/common/content_pb2.py b/truffle/common/content_pb2.py deleted file mode 100644 index 075e214..0000000 --- a/truffle/common/content_pb2.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/common/content.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/common/content.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/common/content.proto\x12\x0etruffle.common\"\x1a\n\x0bMediaSource\x12\x0b\n\x03uri\x18\x01 \x01(\t\":\n\x0cWebComponent\x12\x0b\n\x03uri\x18\x01 \x01(\t\x12\r\n\x05width\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x05\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.common.content_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_MEDIASOURCE']._serialized_start=48 - _globals['_MEDIASOURCE']._serialized_end=74 - _globals['_WEBCOMPONENT']._serialized_start=76 - _globals['_WEBCOMPONENT']._serialized_end=134 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/common/content_pb2.pyi b/truffle/common/content_pb2.pyi deleted file mode 100644 index f31bca8..0000000 --- a/truffle/common/content_pb2.pyi +++ /dev/null @@ -1,21 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional - -DESCRIPTOR: _descriptor.FileDescriptor - -class MediaSource(_message.Message): - __slots__ = ("uri",) - URI_FIELD_NUMBER: _ClassVar[int] - uri: str - def __init__(self, uri: _Optional[str] = ...) -> None: ... - -class WebComponent(_message.Message): - __slots__ = ("uri", "width", "height") - URI_FIELD_NUMBER: _ClassVar[int] - WIDTH_FIELD_NUMBER: _ClassVar[int] - HEIGHT_FIELD_NUMBER: _ClassVar[int] - uri: str - width: int - height: int - def __init__(self, uri: _Optional[str] = ..., width: _Optional[int] = ..., height: _Optional[int] = ...) -> None: ... diff --git a/truffle/common/content_pb2_grpc.py b/truffle/common/content_pb2_grpc.py deleted file mode 100644 index 9f80a64..0000000 --- a/truffle/common/content_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/common/content_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/common/file_pb2.py b/truffle/common/file_pb2.py deleted file mode 100644 index 6d6ee4f..0000000 --- a/truffle/common/file_pb2.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/common/file.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/common/file.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19truffle/common/file.proto\x12\x0etruffle.common\"=\n\x0c\x46ileMetadata\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tmime_type\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x04\"L\n\x0c\x41ttachedFile\x12\x0c\n\x04path\x18\x01 \x01(\t\x12.\n\x08metadata\x18\x02 \x01(\x0b\x32\x1c.truffle.common.FileMetadata\"@\n\x12\x41ttachedFileIntent\x12*\n\x04\x66ile\x18\x01 \x01(\x0b\x32\x1c.truffle.common.AttachedFileb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.common.file_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_FILEMETADATA']._serialized_start=45 - _globals['_FILEMETADATA']._serialized_end=106 - _globals['_ATTACHEDFILE']._serialized_start=108 - _globals['_ATTACHEDFILE']._serialized_end=184 - _globals['_ATTACHEDFILEINTENT']._serialized_start=186 - _globals['_ATTACHEDFILEINTENT']._serialized_end=250 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/common/file_pb2.pyi b/truffle/common/file_pb2.pyi deleted file mode 100644 index 79b2d95..0000000 --- a/truffle/common/file_pb2.pyi +++ /dev/null @@ -1,30 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class FileMetadata(_message.Message): - __slots__ = ("name", "mime_type", "size") - NAME_FIELD_NUMBER: _ClassVar[int] - MIME_TYPE_FIELD_NUMBER: _ClassVar[int] - SIZE_FIELD_NUMBER: _ClassVar[int] - name: str - mime_type: str - size: int - def __init__(self, name: _Optional[str] = ..., mime_type: _Optional[str] = ..., size: _Optional[int] = ...) -> None: ... - -class AttachedFile(_message.Message): - __slots__ = ("path", "metadata") - PATH_FIELD_NUMBER: _ClassVar[int] - METADATA_FIELD_NUMBER: _ClassVar[int] - path: str - metadata: FileMetadata - def __init__(self, path: _Optional[str] = ..., metadata: _Optional[_Union[FileMetadata, _Mapping]] = ...) -> None: ... - -class AttachedFileIntent(_message.Message): - __slots__ = ("file",) - FILE_FIELD_NUMBER: _ClassVar[int] - file: AttachedFile - def __init__(self, file: _Optional[_Union[AttachedFile, _Mapping]] = ...) -> None: ... diff --git a/truffle/common/file_pb2_grpc.py b/truffle/common/file_pb2_grpc.py deleted file mode 100644 index bfc7eb8..0000000 --- a/truffle/common/file_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/common/file_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/common/icon_pb2.py b/truffle/common/icon_pb2.py deleted file mode 100644 index 2318742..0000000 --- a/truffle/common/icon_pb2.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/common/icon.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/common/icon.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19truffle/common/icon.proto\x12\x0etruffle.common\"\x18\n\x04Icon\x12\x10\n\x08png_data\x18\x01 \x01(\x0c\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.common.icon_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_ICON']._serialized_start=45 - _globals['_ICON']._serialized_end=69 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/common/icon_pb2.pyi b/truffle/common/icon_pb2.pyi deleted file mode 100644 index 7ebe6b8..0000000 --- a/truffle/common/icon_pb2.pyi +++ /dev/null @@ -1,11 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional - -DESCRIPTOR: _descriptor.FileDescriptor - -class Icon(_message.Message): - __slots__ = ("png_data",) - PNG_DATA_FIELD_NUMBER: _ClassVar[int] - png_data: bytes - def __init__(self, png_data: _Optional[bytes] = ...) -> None: ... diff --git a/truffle/common/icon_pb2_grpc.py b/truffle/common/icon_pb2_grpc.py deleted file mode 100644 index 2d156fd..0000000 --- a/truffle/common/icon_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/common/icon_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/common/tool_provider_pb2.py b/truffle/common/tool_provider_pb2.py deleted file mode 100644 index a96461b..0000000 --- a/truffle/common/tool_provider_pb2.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/common/tool_provider.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/common/tool_provider.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"truffle/common/tool_provider.proto\x12\x0etruffle.common\"\xce\x01\n\x14\x45xternalToolProvider\x12\x0c\n\x04uuid\x18\x01 \x01(\t\x12L\n\nmcp_server\x18\x02 \x01(\x0b\x32\x36.truffle.common.ExternalToolProvider.ExternalMCPServerH\x00\x1aN\n\x11\x45xternalMCPServer\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\r\x12\x11\n\x04path\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_pathB\n\n\x08provider\"d\n\x1a\x45xternalToolProvidersError\x12\x15\n\rprovider_uuid\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x14\n\x07\x64\x65tails\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\n\n\x08_detailsb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.common.tool_provider_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_EXTERNALTOOLPROVIDER']._serialized_start=55 - _globals['_EXTERNALTOOLPROVIDER']._serialized_end=261 - _globals['_EXTERNALTOOLPROVIDER_EXTERNALMCPSERVER']._serialized_start=171 - _globals['_EXTERNALTOOLPROVIDER_EXTERNALMCPSERVER']._serialized_end=249 - _globals['_EXTERNALTOOLPROVIDERSERROR']._serialized_start=263 - _globals['_EXTERNALTOOLPROVIDERSERROR']._serialized_end=363 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/common/tool_provider_pb2.pyi b/truffle/common/tool_provider_pb2.pyi deleted file mode 100644 index 843d87d..0000000 --- a/truffle/common/tool_provider_pb2.pyi +++ /dev/null @@ -1,33 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class ExternalToolProvider(_message.Message): - __slots__ = ("uuid", "mcp_server") - class ExternalMCPServer(_message.Message): - __slots__ = ("address", "port", "path") - ADDRESS_FIELD_NUMBER: _ClassVar[int] - PORT_FIELD_NUMBER: _ClassVar[int] - PATH_FIELD_NUMBER: _ClassVar[int] - address: str - port: int - path: str - def __init__(self, address: _Optional[str] = ..., port: _Optional[int] = ..., path: _Optional[str] = ...) -> None: ... - UUID_FIELD_NUMBER: _ClassVar[int] - MCP_SERVER_FIELD_NUMBER: _ClassVar[int] - uuid: str - mcp_server: ExternalToolProvider.ExternalMCPServer - def __init__(self, uuid: _Optional[str] = ..., mcp_server: _Optional[_Union[ExternalToolProvider.ExternalMCPServer, _Mapping]] = ...) -> None: ... - -class ExternalToolProvidersError(_message.Message): - __slots__ = ("provider_uuid", "error", "details") - PROVIDER_UUID_FIELD_NUMBER: _ClassVar[int] - ERROR_FIELD_NUMBER: _ClassVar[int] - DETAILS_FIELD_NUMBER: _ClassVar[int] - provider_uuid: str - error: str - details: str - def __init__(self, provider_uuid: _Optional[str] = ..., error: _Optional[str] = ..., details: _Optional[str] = ...) -> None: ... diff --git a/truffle/common/tool_provider_pb2_grpc.py b/truffle/common/tool_provider_pb2_grpc.py deleted file mode 100644 index a578d03..0000000 --- a/truffle/common/tool_provider_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/common/tool_provider_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/__init__.py b/truffle/os/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/truffle/os/app_queries_pb2.py b/truffle/os/app_queries_pb2.py deleted file mode 100644 index 4ea7a94..0000000 --- a/truffle/os/app_queries_pb2.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/app_queries.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/app_queries.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.app import app_pb2 as truffle_dot_app_dot_app__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/os/app_queries.proto\x12\ntruffle.os\x1a\x15truffle/app/app.proto\"\x13\n\x11GetAllAppsRequest\"4\n\x12GetAllAppsResponse\x12\x1e\n\x04\x61pps\x18\x01 \x03(\x0b\x32\x10.truffle.app.App\"$\n\x10\x44\x65leteAppRequest\x12\x10\n\x08\x61pp_uuid\x18\x01 \x01(\t\"\x13\n\x11\x44\x65leteAppResponseb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.app_queries_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_GETALLAPPSREQUEST']._serialized_start=67 - _globals['_GETALLAPPSREQUEST']._serialized_end=86 - _globals['_GETALLAPPSRESPONSE']._serialized_start=88 - _globals['_GETALLAPPSRESPONSE']._serialized_end=140 - _globals['_DELETEAPPREQUEST']._serialized_start=142 - _globals['_DELETEAPPREQUEST']._serialized_end=178 - _globals['_DELETEAPPRESPONSE']._serialized_start=180 - _globals['_DELETEAPPRESPONSE']._serialized_end=199 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/app_queries_pb2.pyi b/truffle/os/app_queries_pb2.pyi deleted file mode 100644 index e1f2157..0000000 --- a/truffle/os/app_queries_pb2.pyi +++ /dev/null @@ -1,28 +0,0 @@ -from truffle.app import app_pb2 as _app_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class GetAllAppsRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class GetAllAppsResponse(_message.Message): - __slots__ = ("apps",) - APPS_FIELD_NUMBER: _ClassVar[int] - apps: _containers.RepeatedCompositeFieldContainer[_app_pb2.App] - def __init__(self, apps: _Optional[_Iterable[_Union[_app_pb2.App, _Mapping]]] = ...) -> None: ... - -class DeleteAppRequest(_message.Message): - __slots__ = ("app_uuid",) - APP_UUID_FIELD_NUMBER: _ClassVar[int] - app_uuid: str - def __init__(self, app_uuid: _Optional[str] = ...) -> None: ... - -class DeleteAppResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... diff --git a/truffle/os/app_queries_pb2_grpc.py b/truffle/os/app_queries_pb2_grpc.py deleted file mode 100644 index e6864a7..0000000 --- a/truffle/os/app_queries_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/app_queries_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/background_feed_pb2.py b/truffle/os/background_feed_pb2.py deleted file mode 100644 index d609505..0000000 --- a/truffle/os/background_feed_pb2.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/background_feed.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/background_feed.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 -from truffle.common import content_pb2 as truffle_dot_common_dot_content__pb2 -from truffle.os import proactivity_pb2 as truffle_dot_os_dot_proactivity__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n truffle/os/background_feed.proto\x12\ntruffle.os\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1ctruffle/common/content.proto\x1a\x1ctruffle/os/proactivity.proto\"\xdc\x01\n\x08\x46\x65\x65\x64\x43\x61rd\x12\r\n\x05title\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\t\x12\x32\n\rmedia_sources\x18\x03 \x03(\x0b\x32\x1b.truffle.common.MediaSource\x12\x12\n\nsource_uri\x18\x04 \x01(\t\x12.\n\ncreated_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x42\x0b\n\t_metadata\"8\n\x0e\x42\x61\x63kgroundFeed\x12&\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x15.truffle.os.FeedEntry\"\xce\x01\n\x15\x46\x65\x65\x64\x45ntryNotification\x12\x11\n\tentry_ids\x18\x01 \x03(\x04\x12>\n\toperation\x18\x02 \x01(\x0e\x32+.truffle.os.FeedEntryNotification.Operation\"b\n\tOperation\x12\x15\n\x11OPERATION_INVALID\x10\x00\x12\x11\n\rOPERATION_ADD\x10\x01\x12\x14\n\x10OPERATION_DELETE\x10\x02\x12\x15\n\x11OPERATION_REFRESH\x10\x03\"\xb0\x01\n\tFeedEntry\x12\n\n\x02id\x18\x01 \x01(\x04\x12$\n\x04\x63\x61rd\x18\x02 \x01(\x0b\x32\x14.truffle.os.FeedCardH\x00\x12\x37\n\x10proactive_action\x18\x03 \x01(\x0b\x32\x1b.truffle.os.ProactiveActionH\x00\x12-\n\ttimestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\t\n\x07\x63ontent\">\n\x14\x46\x65\x65\x64\x45ntryTaskContext\x12&\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x15.truffle.os.FeedEntryb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.background_feed_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_FEEDCARD']._serialized_start=172 - _globals['_FEEDCARD']._serialized_end=392 - _globals['_BACKGROUNDFEED']._serialized_start=394 - _globals['_BACKGROUNDFEED']._serialized_end=450 - _globals['_FEEDENTRYNOTIFICATION']._serialized_start=453 - _globals['_FEEDENTRYNOTIFICATION']._serialized_end=659 - _globals['_FEEDENTRYNOTIFICATION_OPERATION']._serialized_start=561 - _globals['_FEEDENTRYNOTIFICATION_OPERATION']._serialized_end=659 - _globals['_FEEDENTRY']._serialized_start=662 - _globals['_FEEDENTRY']._serialized_end=838 - _globals['_FEEDENTRYTASKCONTEXT']._serialized_start=840 - _globals['_FEEDENTRYTASKCONTEXT']._serialized_end=902 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/background_feed_pb2.pyi b/truffle/os/background_feed_pb2.pyi deleted file mode 100644 index 3513f11..0000000 --- a/truffle/os/background_feed_pb2.pyi +++ /dev/null @@ -1,72 +0,0 @@ -import datetime - -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from google.protobuf import struct_pb2 as _struct_pb2 -from truffle.common import content_pb2 as _content_pb2 -from truffle.os import proactivity_pb2 as _proactivity_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class FeedCard(_message.Message): - __slots__ = ("title", "body", "media_sources", "source_uri", "created_at", "metadata") - TITLE_FIELD_NUMBER: _ClassVar[int] - BODY_FIELD_NUMBER: _ClassVar[int] - MEDIA_SOURCES_FIELD_NUMBER: _ClassVar[int] - SOURCE_URI_FIELD_NUMBER: _ClassVar[int] - CREATED_AT_FIELD_NUMBER: _ClassVar[int] - METADATA_FIELD_NUMBER: _ClassVar[int] - title: str - body: str - media_sources: _containers.RepeatedCompositeFieldContainer[_content_pb2.MediaSource] - source_uri: str - created_at: _timestamp_pb2.Timestamp - metadata: _struct_pb2.Struct - def __init__(self, title: _Optional[str] = ..., body: _Optional[str] = ..., media_sources: _Optional[_Iterable[_Union[_content_pb2.MediaSource, _Mapping]]] = ..., source_uri: _Optional[str] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., metadata: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ... - -class BackgroundFeed(_message.Message): - __slots__ = ("entries",) - ENTRIES_FIELD_NUMBER: _ClassVar[int] - entries: _containers.RepeatedCompositeFieldContainer[FeedEntry] - def __init__(self, entries: _Optional[_Iterable[_Union[FeedEntry, _Mapping]]] = ...) -> None: ... - -class FeedEntryNotification(_message.Message): - __slots__ = ("entry_ids", "operation") - class Operation(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - OPERATION_INVALID: _ClassVar[FeedEntryNotification.Operation] - OPERATION_ADD: _ClassVar[FeedEntryNotification.Operation] - OPERATION_DELETE: _ClassVar[FeedEntryNotification.Operation] - OPERATION_REFRESH: _ClassVar[FeedEntryNotification.Operation] - OPERATION_INVALID: FeedEntryNotification.Operation - OPERATION_ADD: FeedEntryNotification.Operation - OPERATION_DELETE: FeedEntryNotification.Operation - OPERATION_REFRESH: FeedEntryNotification.Operation - ENTRY_IDS_FIELD_NUMBER: _ClassVar[int] - OPERATION_FIELD_NUMBER: _ClassVar[int] - entry_ids: _containers.RepeatedScalarFieldContainer[int] - operation: FeedEntryNotification.Operation - def __init__(self, entry_ids: _Optional[_Iterable[int]] = ..., operation: _Optional[_Union[FeedEntryNotification.Operation, str]] = ...) -> None: ... - -class FeedEntry(_message.Message): - __slots__ = ("id", "card", "proactive_action", "timestamp") - ID_FIELD_NUMBER: _ClassVar[int] - CARD_FIELD_NUMBER: _ClassVar[int] - PROACTIVE_ACTION_FIELD_NUMBER: _ClassVar[int] - TIMESTAMP_FIELD_NUMBER: _ClassVar[int] - id: int - card: FeedCard - proactive_action: _proactivity_pb2.ProactiveAction - timestamp: _timestamp_pb2.Timestamp - def __init__(self, id: _Optional[int] = ..., card: _Optional[_Union[FeedCard, _Mapping]] = ..., proactive_action: _Optional[_Union[_proactivity_pb2.ProactiveAction, _Mapping]] = ..., timestamp: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... - -class FeedEntryTaskContext(_message.Message): - __slots__ = ("entries",) - ENTRIES_FIELD_NUMBER: _ClassVar[int] - entries: _containers.RepeatedCompositeFieldContainer[FeedEntry] - def __init__(self, entries: _Optional[_Iterable[_Union[FeedEntry, _Mapping]]] = ...) -> None: ... diff --git a/truffle/os/background_feed_pb2_grpc.py b/truffle/os/background_feed_pb2_grpc.py deleted file mode 100644 index cc8f5ef..0000000 --- a/truffle/os/background_feed_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/background_feed_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/background_feed_queries_pb2.py b/truffle/os/background_feed_queries_pb2.py deleted file mode 100644 index 5fa8b9f..0000000 --- a/truffle/os/background_feed_queries_pb2.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/background_feed_queries.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/background_feed_queries.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.app import background_pb2 as truffle_dot_app_dot_background__pb2 -from truffle.os import background_feed_pb2 as truffle_dot_os_dot_background__feed__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n(truffle/os/background_feed_queries.proto\x12\ntruffle.os\x1a\x1ctruffle/app/background.proto\x1a truffle/os/background_feed.proto\"\x8a\x01\n\x18GetBackgroundFeedRequest\x12\x17\n\x0ftarget_entry_id\x18\x01 \x01(\x04\x12\x12\n\nmax_before\x18\x02 \x01(\x05\x12\x11\n\tmax_after\x18\x03 \x01(\x05\x12\x17\n\x0finclude_actions\x18\x04 \x01(\x08\x12\x15\n\rinclude_cards\x18\x05 \x01(\x08\"C\n\x19GetBackgroundFeedResponse\x12&\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x15.truffle.os.FeedEntry\"\x1d\n\x1bGetLatestFeedEntryIDRequest\"<\n\x1cGetLatestFeedEntryIDResponse\x12\x1c\n\x14latest_feed_entry_id\x18\x01 \x01(\x04\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.background_feed_queries_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_GETBACKGROUNDFEEDREQUEST']._serialized_start=121 - _globals['_GETBACKGROUNDFEEDREQUEST']._serialized_end=259 - _globals['_GETBACKGROUNDFEEDRESPONSE']._serialized_start=261 - _globals['_GETBACKGROUNDFEEDRESPONSE']._serialized_end=328 - _globals['_GETLATESTFEEDENTRYIDREQUEST']._serialized_start=330 - _globals['_GETLATESTFEEDENTRYIDREQUEST']._serialized_end=359 - _globals['_GETLATESTFEEDENTRYIDRESPONSE']._serialized_start=361 - _globals['_GETLATESTFEEDENTRYIDRESPONSE']._serialized_end=421 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/background_feed_queries_pb2.pyi b/truffle/os/background_feed_queries_pb2.pyi deleted file mode 100644 index 7313d33..0000000 --- a/truffle/os/background_feed_queries_pb2.pyi +++ /dev/null @@ -1,39 +0,0 @@ -from truffle.app import background_pb2 as _background_pb2 -from truffle.os import background_feed_pb2 as _background_feed_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class GetBackgroundFeedRequest(_message.Message): - __slots__ = ("target_entry_id", "max_before", "max_after", "include_actions", "include_cards") - TARGET_ENTRY_ID_FIELD_NUMBER: _ClassVar[int] - MAX_BEFORE_FIELD_NUMBER: _ClassVar[int] - MAX_AFTER_FIELD_NUMBER: _ClassVar[int] - INCLUDE_ACTIONS_FIELD_NUMBER: _ClassVar[int] - INCLUDE_CARDS_FIELD_NUMBER: _ClassVar[int] - target_entry_id: int - max_before: int - max_after: int - include_actions: bool - include_cards: bool - def __init__(self, target_entry_id: _Optional[int] = ..., max_before: _Optional[int] = ..., max_after: _Optional[int] = ..., include_actions: bool = ..., include_cards: bool = ...) -> None: ... - -class GetBackgroundFeedResponse(_message.Message): - __slots__ = ("entries",) - ENTRIES_FIELD_NUMBER: _ClassVar[int] - entries: _containers.RepeatedCompositeFieldContainer[_background_feed_pb2.FeedEntry] - def __init__(self, entries: _Optional[_Iterable[_Union[_background_feed_pb2.FeedEntry, _Mapping]]] = ...) -> None: ... - -class GetLatestFeedEntryIDRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class GetLatestFeedEntryIDResponse(_message.Message): - __slots__ = ("latest_feed_entry_id",) - LATEST_FEED_ENTRY_ID_FIELD_NUMBER: _ClassVar[int] - latest_feed_entry_id: int - def __init__(self, latest_feed_entry_id: _Optional[int] = ...) -> None: ... diff --git a/truffle/os/background_feed_queries_pb2_grpc.py b/truffle/os/background_feed_queries_pb2_grpc.py deleted file mode 100644 index e6fa230..0000000 --- a/truffle/os/background_feed_queries_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/background_feed_queries_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/builder_pb2.py b/truffle/os/builder_pb2.py deleted file mode 100644 index dde6b87..0000000 --- a/truffle/os/builder_pb2.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/builder.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/builder.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.app import background_pb2 as truffle_dot_app_dot_background__pb2 -from truffle.app import foreground_pb2 as truffle_dot_app_dot_foreground__pb2 -from truffle.app import app_build_pb2 as truffle_dot_app_dot_app__build__pb2 -from truffle.app import app_pb2 as truffle_dot_app_dot_app__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18truffle/os/builder.proto\x12\ntruffle.os\x1a\x1ctruffle/app/background.proto\x1a\x1ctruffle/app/foreground.proto\x1a\x1btruffle/app/app_build.proto\x1a\x15truffle/app/app.proto\">\n\x18StartBuildSessionRequest\x12\x15\n\x08\x61pp_uuid\x18\x01 \x01(\tH\x00\x88\x01\x01\x42\x0b\n\t_app_uuid\"B\n\x19StartBuildSessionResponse\x12\x13\n\x0b\x61\x63\x63\x65ss_path\x18\x01 \x01(\t\x12\x10\n\x08\x61pp_uuid\x18\x02 \x01(\t\"\x84\x02\n\x19\x46inishBuildSessionRequest\x12\x10\n\x08\x61pp_uuid\x18\x01 \x01(\t\x12\x0f\n\x07\x64iscard\x18\x02 \x01(\x08\x12*\n\x08metadata\x18\x03 \x01(\x0b\x32\x18.truffle.app.AppMetadata\x12<\n\nforeground\x18\x04 \x01(\x0b\x32#.truffle.app.ForegroundAppBuildInfoH\x00\x88\x01\x01\x12<\n\nbackground\x18\x05 \x01(\x0b\x32#.truffle.app.BackgroundAppBuildInfoH\x01\x88\x01\x01\x42\r\n\x0b_foregroundB\r\n\x0b_background\"D\n\x11\x42uildSessionError\x12\r\n\x05\x65rror\x18\x01 \x01(\t\x12\x14\n\x07\x64\x65tails\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\n\n\x08_details\"Y\n\x1a\x46inishBuildSessionResponse\x12\x31\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x1d.truffle.os.BuildSessionErrorH\x00\x88\x01\x01\x42\x08\n\x06_errorb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.builder_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_STARTBUILDSESSIONREQUEST']._serialized_start=152 - _globals['_STARTBUILDSESSIONREQUEST']._serialized_end=214 - _globals['_STARTBUILDSESSIONRESPONSE']._serialized_start=216 - _globals['_STARTBUILDSESSIONRESPONSE']._serialized_end=282 - _globals['_FINISHBUILDSESSIONREQUEST']._serialized_start=285 - _globals['_FINISHBUILDSESSIONREQUEST']._serialized_end=545 - _globals['_BUILDSESSIONERROR']._serialized_start=547 - _globals['_BUILDSESSIONERROR']._serialized_end=615 - _globals['_FINISHBUILDSESSIONRESPONSE']._serialized_start=617 - _globals['_FINISHBUILDSESSIONRESPONSE']._serialized_end=706 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/builder_pb2.pyi b/truffle/os/builder_pb2.pyi deleted file mode 100644 index e62fd5a..0000000 --- a/truffle/os/builder_pb2.pyi +++ /dev/null @@ -1,52 +0,0 @@ -from truffle.app import background_pb2 as _background_pb2 -from truffle.app import foreground_pb2 as _foreground_pb2 -from truffle.app import app_build_pb2 as _app_build_pb2 -from truffle.app import app_pb2 as _app_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class StartBuildSessionRequest(_message.Message): - __slots__ = ("app_uuid",) - APP_UUID_FIELD_NUMBER: _ClassVar[int] - app_uuid: str - def __init__(self, app_uuid: _Optional[str] = ...) -> None: ... - -class StartBuildSessionResponse(_message.Message): - __slots__ = ("access_path", "app_uuid") - ACCESS_PATH_FIELD_NUMBER: _ClassVar[int] - APP_UUID_FIELD_NUMBER: _ClassVar[int] - access_path: str - app_uuid: str - def __init__(self, access_path: _Optional[str] = ..., app_uuid: _Optional[str] = ...) -> None: ... - -class FinishBuildSessionRequest(_message.Message): - __slots__ = ("app_uuid", "discard", "metadata", "foreground", "background") - APP_UUID_FIELD_NUMBER: _ClassVar[int] - DISCARD_FIELD_NUMBER: _ClassVar[int] - METADATA_FIELD_NUMBER: _ClassVar[int] - FOREGROUND_FIELD_NUMBER: _ClassVar[int] - BACKGROUND_FIELD_NUMBER: _ClassVar[int] - app_uuid: str - discard: bool - metadata: _app_pb2.AppMetadata - foreground: _foreground_pb2.ForegroundAppBuildInfo - background: _background_pb2.BackgroundAppBuildInfo - def __init__(self, app_uuid: _Optional[str] = ..., discard: bool = ..., metadata: _Optional[_Union[_app_pb2.AppMetadata, _Mapping]] = ..., foreground: _Optional[_Union[_foreground_pb2.ForegroundAppBuildInfo, _Mapping]] = ..., background: _Optional[_Union[_background_pb2.BackgroundAppBuildInfo, _Mapping]] = ...) -> None: ... - -class BuildSessionError(_message.Message): - __slots__ = ("error", "details") - ERROR_FIELD_NUMBER: _ClassVar[int] - DETAILS_FIELD_NUMBER: _ClassVar[int] - error: str - details: str - def __init__(self, error: _Optional[str] = ..., details: _Optional[str] = ...) -> None: ... - -class FinishBuildSessionResponse(_message.Message): - __slots__ = ("error",) - ERROR_FIELD_NUMBER: _ClassVar[int] - error: BuildSessionError - def __init__(self, error: _Optional[_Union[BuildSessionError, _Mapping]] = ...) -> None: ... diff --git a/truffle/os/builder_pb2_grpc.py b/truffle/os/builder_pb2_grpc.py deleted file mode 100644 index 9a3c71e..0000000 --- a/truffle/os/builder_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/builder_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/client_metadata_pb2.py b/truffle/os/client_metadata_pb2.py deleted file mode 100644 index d04c359..0000000 --- a/truffle/os/client_metadata_pb2.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/client_metadata.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/client_metadata.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n truffle/os/client_metadata.proto\x12\ntruffle.os\"d\n\x0e\x43lientMetadata\x12\x10\n\x08platform\x18\x01 \x01(\t\x12\x14\n\x07version\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06\x64\x65vice\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\n\n\x08_versionB\t\n\x07_deviceb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.client_metadata_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_CLIENTMETADATA']._serialized_start=48 - _globals['_CLIENTMETADATA']._serialized_end=148 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/client_metadata_pb2.pyi b/truffle/os/client_metadata_pb2.pyi deleted file mode 100644 index 36b1489..0000000 --- a/truffle/os/client_metadata_pb2.pyi +++ /dev/null @@ -1,15 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional - -DESCRIPTOR: _descriptor.FileDescriptor - -class ClientMetadata(_message.Message): - __slots__ = ("platform", "version", "device") - PLATFORM_FIELD_NUMBER: _ClassVar[int] - VERSION_FIELD_NUMBER: _ClassVar[int] - DEVICE_FIELD_NUMBER: _ClassVar[int] - platform: str - version: str - device: str - def __init__(self, platform: _Optional[str] = ..., version: _Optional[str] = ..., device: _Optional[str] = ...) -> None: ... diff --git a/truffle/os/client_metadata_pb2_grpc.py b/truffle/os/client_metadata_pb2_grpc.py deleted file mode 100644 index 3adeae5..0000000 --- a/truffle/os/client_metadata_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/client_metadata_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/client_session_pb2.py b/truffle/os/client_session_pb2.py deleted file mode 100644 index b8d9c42..0000000 --- a/truffle/os/client_session_pb2.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/client_session.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/client_session.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import client_metadata_pb2 as truffle_dot_os_dot_client__metadata__pb2 -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ftruffle/os/client_session.proto\x12\ntruffle.os\x1a truffle/os/client_metadata.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\"\n\x11UserRecoveryCodes\x12\r\n\x05\x63odes\x18\x01 \x03(\t\"\x88\x01\n\x19RegisterNewSessionRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12,\n\x08metadata\x18\x02 \x01(\x0b\x32\x1a.truffle.os.ClientMetadata\x12\x1a\n\rrecovery_code\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x10\n\x0e_recovery_code\"\x99\x01\n\x1aRegisterNewSessionResponse\x12\r\n\x05token\x18\x01 \x01(\t\x12\x31\n\x08verifier\x18\x02 \x01(\x0b\x32\x1a.truffle.os.ClientMetadataH\x00\x88\x01\x01\x12,\n\x06status\x18\x03 \x01(\x0b\x32\x1c.truffle.os.NewSessionStatusB\x0b\n\t_verifier\"\x9b\x01\n\x16NewSessionVerification\x12\x1a\n\x12verification_token\x18\x01 \x01(\t\x12.\n\nexpires_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x11requesting_client\x18\x03 \x01(\x0b\x32\x1a.truffle.os.ClientMetadata\"D\n\x17VerifyNewSessionRequest\x12\x1a\n\x12verification_token\x18\x01 \x01(\t\x12\r\n\x05\x61llow\x18\x02 \x01(\x08\"\xfc\x01\n\x10NewSessionStatus\x12@\n\x05\x65rror\x18\x01 \x01(\x0e\x32,.truffle.os.NewSessionStatus.NewSessionErrorH\x00\x88\x01\x01\"\x9b\x01\n\x0fNewSessionError\x12\x17\n\x13NEW_SESSION_SUCCESS\x10\x00\x12\x17\n\x13NEW_SESSION_TIMEOUT\x10\x01\x12\x18\n\x14NEW_SESSION_REJECTED\x10\x02\x12\x1f\n\x1bNEW_SESSION_TOKEN_NOT_FOUND\x10\x03\x12\x1b\n\x17NEW_SESSION_NO_REQUESTS\x10\x04\x42\x08\n\x06_errorb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.client_session_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_USERRECOVERYCODES']._serialized_start=114 - _globals['_USERRECOVERYCODES']._serialized_end=148 - _globals['_REGISTERNEWSESSIONREQUEST']._serialized_start=151 - _globals['_REGISTERNEWSESSIONREQUEST']._serialized_end=287 - _globals['_REGISTERNEWSESSIONRESPONSE']._serialized_start=290 - _globals['_REGISTERNEWSESSIONRESPONSE']._serialized_end=443 - _globals['_NEWSESSIONVERIFICATION']._serialized_start=446 - _globals['_NEWSESSIONVERIFICATION']._serialized_end=601 - _globals['_VERIFYNEWSESSIONREQUEST']._serialized_start=603 - _globals['_VERIFYNEWSESSIONREQUEST']._serialized_end=671 - _globals['_NEWSESSIONSTATUS']._serialized_start=674 - _globals['_NEWSESSIONSTATUS']._serialized_end=926 - _globals['_NEWSESSIONSTATUS_NEWSESSIONERROR']._serialized_start=761 - _globals['_NEWSESSIONSTATUS_NEWSESSIONERROR']._serialized_end=916 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/client_session_pb2.pyi b/truffle/os/client_session_pb2.pyi deleted file mode 100644 index 23dc24b..0000000 --- a/truffle/os/client_session_pb2.pyi +++ /dev/null @@ -1,74 +0,0 @@ -import datetime - -from truffle.os import client_metadata_pb2 as _client_metadata_pb2 -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class UserRecoveryCodes(_message.Message): - __slots__ = ("codes",) - CODES_FIELD_NUMBER: _ClassVar[int] - codes: _containers.RepeatedScalarFieldContainer[str] - def __init__(self, codes: _Optional[_Iterable[str]] = ...) -> None: ... - -class RegisterNewSessionRequest(_message.Message): - __slots__ = ("user_id", "metadata", "recovery_code") - USER_ID_FIELD_NUMBER: _ClassVar[int] - METADATA_FIELD_NUMBER: _ClassVar[int] - RECOVERY_CODE_FIELD_NUMBER: _ClassVar[int] - user_id: str - metadata: _client_metadata_pb2.ClientMetadata - recovery_code: str - def __init__(self, user_id: _Optional[str] = ..., metadata: _Optional[_Union[_client_metadata_pb2.ClientMetadata, _Mapping]] = ..., recovery_code: _Optional[str] = ...) -> None: ... - -class RegisterNewSessionResponse(_message.Message): - __slots__ = ("token", "verifier", "status") - TOKEN_FIELD_NUMBER: _ClassVar[int] - VERIFIER_FIELD_NUMBER: _ClassVar[int] - STATUS_FIELD_NUMBER: _ClassVar[int] - token: str - verifier: _client_metadata_pb2.ClientMetadata - status: NewSessionStatus - def __init__(self, token: _Optional[str] = ..., verifier: _Optional[_Union[_client_metadata_pb2.ClientMetadata, _Mapping]] = ..., status: _Optional[_Union[NewSessionStatus, _Mapping]] = ...) -> None: ... - -class NewSessionVerification(_message.Message): - __slots__ = ("verification_token", "expires_at", "requesting_client") - VERIFICATION_TOKEN_FIELD_NUMBER: _ClassVar[int] - EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] - REQUESTING_CLIENT_FIELD_NUMBER: _ClassVar[int] - verification_token: str - expires_at: _timestamp_pb2.Timestamp - requesting_client: _client_metadata_pb2.ClientMetadata - def __init__(self, verification_token: _Optional[str] = ..., expires_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., requesting_client: _Optional[_Union[_client_metadata_pb2.ClientMetadata, _Mapping]] = ...) -> None: ... - -class VerifyNewSessionRequest(_message.Message): - __slots__ = ("verification_token", "allow") - VERIFICATION_TOKEN_FIELD_NUMBER: _ClassVar[int] - ALLOW_FIELD_NUMBER: _ClassVar[int] - verification_token: str - allow: bool - def __init__(self, verification_token: _Optional[str] = ..., allow: bool = ...) -> None: ... - -class NewSessionStatus(_message.Message): - __slots__ = ("error",) - class NewSessionError(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - NEW_SESSION_SUCCESS: _ClassVar[NewSessionStatus.NewSessionError] - NEW_SESSION_TIMEOUT: _ClassVar[NewSessionStatus.NewSessionError] - NEW_SESSION_REJECTED: _ClassVar[NewSessionStatus.NewSessionError] - NEW_SESSION_TOKEN_NOT_FOUND: _ClassVar[NewSessionStatus.NewSessionError] - NEW_SESSION_NO_REQUESTS: _ClassVar[NewSessionStatus.NewSessionError] - NEW_SESSION_SUCCESS: NewSessionStatus.NewSessionError - NEW_SESSION_TIMEOUT: NewSessionStatus.NewSessionError - NEW_SESSION_REJECTED: NewSessionStatus.NewSessionError - NEW_SESSION_TOKEN_NOT_FOUND: NewSessionStatus.NewSessionError - NEW_SESSION_NO_REQUESTS: NewSessionStatus.NewSessionError - ERROR_FIELD_NUMBER: _ClassVar[int] - error: NewSessionStatus.NewSessionError - def __init__(self, error: _Optional[_Union[NewSessionStatus.NewSessionError, str]] = ...) -> None: ... diff --git a/truffle/os/client_session_pb2_grpc.py b/truffle/os/client_session_pb2_grpc.py deleted file mode 100644 index d94ebaf..0000000 --- a/truffle/os/client_session_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/client_session_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/client_state_pb2.py b/truffle/os/client_state_pb2.py deleted file mode 100644 index 59da1b3..0000000 --- a/truffle/os/client_state_pb2.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/client_state.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/client_state.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dtruffle/os/client_state.proto\x12\ntruffle.os\"\x1b\n\x0b\x43lientState\x12\x0c\n\x04\x62lob\x18\x01 \x01(\t\"O\n\x18UpdateClientStateRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05state\x18\x02 \x01(\x0b\x32\x17.truffle.os.ClientState\"\x1b\n\x19UpdateClientStateResponse\"$\n\x15GetClientStateRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\"@\n\x16GetClientStateResponse\x12&\n\x05state\x18\x01 \x01(\x0b\x32\x17.truffle.os.ClientState\"\x1b\n\x19GetAllClientStatesRequest\"\xa8\x01\n\x1aGetAllClientStatesResponse\x12\x42\n\x06states\x18\x01 \x03(\x0b\x32\x32.truffle.os.GetAllClientStatesResponse.StatesEntry\x1a\x46\n\x0bStatesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12&\n\x05value\x18\x02 \x01(\x0b\x32\x17.truffle.os.ClientState:\x02\x38\x01\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.client_state_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_GETALLCLIENTSTATESRESPONSE_STATESENTRY']._loaded_options = None - _globals['_GETALLCLIENTSTATESRESPONSE_STATESENTRY']._serialized_options = b'8\001' - _globals['_CLIENTSTATE']._serialized_start=45 - _globals['_CLIENTSTATE']._serialized_end=72 - _globals['_UPDATECLIENTSTATEREQUEST']._serialized_start=74 - _globals['_UPDATECLIENTSTATEREQUEST']._serialized_end=153 - _globals['_UPDATECLIENTSTATERESPONSE']._serialized_start=155 - _globals['_UPDATECLIENTSTATERESPONSE']._serialized_end=182 - _globals['_GETCLIENTSTATEREQUEST']._serialized_start=184 - _globals['_GETCLIENTSTATEREQUEST']._serialized_end=220 - _globals['_GETCLIENTSTATERESPONSE']._serialized_start=222 - _globals['_GETCLIENTSTATERESPONSE']._serialized_end=286 - _globals['_GETALLCLIENTSTATESREQUEST']._serialized_start=288 - _globals['_GETALLCLIENTSTATESREQUEST']._serialized_end=315 - _globals['_GETALLCLIENTSTATESRESPONSE']._serialized_start=318 - _globals['_GETALLCLIENTSTATESRESPONSE']._serialized_end=486 - _globals['_GETALLCLIENTSTATESRESPONSE_STATESENTRY']._serialized_start=416 - _globals['_GETALLCLIENTSTATESRESPONSE_STATESENTRY']._serialized_end=486 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/client_state_pb2.pyi b/truffle/os/client_state_pb2.pyi deleted file mode 100644 index 5c66f9b..0000000 --- a/truffle/os/client_state_pb2.pyi +++ /dev/null @@ -1,54 +0,0 @@ -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class ClientState(_message.Message): - __slots__ = ("blob",) - BLOB_FIELD_NUMBER: _ClassVar[int] - blob: str - def __init__(self, blob: _Optional[str] = ...) -> None: ... - -class UpdateClientStateRequest(_message.Message): - __slots__ = ("key", "state") - KEY_FIELD_NUMBER: _ClassVar[int] - STATE_FIELD_NUMBER: _ClassVar[int] - key: str - state: ClientState - def __init__(self, key: _Optional[str] = ..., state: _Optional[_Union[ClientState, _Mapping]] = ...) -> None: ... - -class UpdateClientStateResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class GetClientStateRequest(_message.Message): - __slots__ = ("key",) - KEY_FIELD_NUMBER: _ClassVar[int] - key: str - def __init__(self, key: _Optional[str] = ...) -> None: ... - -class GetClientStateResponse(_message.Message): - __slots__ = ("state",) - STATE_FIELD_NUMBER: _ClassVar[int] - state: ClientState - def __init__(self, state: _Optional[_Union[ClientState, _Mapping]] = ...) -> None: ... - -class GetAllClientStatesRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class GetAllClientStatesResponse(_message.Message): - __slots__ = ("states",) - class StatesEntry(_message.Message): - __slots__ = ("key", "value") - KEY_FIELD_NUMBER: _ClassVar[int] - VALUE_FIELD_NUMBER: _ClassVar[int] - key: str - value: ClientState - def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[ClientState, _Mapping]] = ...) -> None: ... - STATES_FIELD_NUMBER: _ClassVar[int] - states: _containers.MessageMap[str, ClientState] - def __init__(self, states: _Optional[_Mapping[str, ClientState]] = ...) -> None: ... diff --git a/truffle/os/client_state_pb2_grpc.py b/truffle/os/client_state_pb2_grpc.py deleted file mode 100644 index 92d9bc3..0000000 --- a/truffle/os/client_state_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/client_state_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/client_user_pb2.py b/truffle/os/client_user_pb2.py deleted file mode 100644 index d2cb9a3..0000000 --- a/truffle/os/client_user_pb2.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/client_user.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/client_user.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import client_metadata_pb2 as truffle_dot_os_dot_client__metadata__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/os/client_user.proto\x12\ntruffle.os\x1a truffle/os/client_metadata.proto\"h\n\x16RegisterNewUserRequest\x12,\n\x08metadata\x18\x01 \x01(\x0b\x32\x1a.truffle.os.ClientMetadata\x12\x14\n\x07user_id\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\n\n\x08_user_id\"9\n\x17RegisterNewUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\"&\n\x15UserIDForTokenRequest\x12\r\n\x05token\x18\x01 \x01(\t\")\n\x16UserIDForTokenResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\tb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.client_user_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_REGISTERNEWUSERREQUEST']._serialized_start=78 - _globals['_REGISTERNEWUSERREQUEST']._serialized_end=182 - _globals['_REGISTERNEWUSERRESPONSE']._serialized_start=184 - _globals['_REGISTERNEWUSERRESPONSE']._serialized_end=241 - _globals['_USERIDFORTOKENREQUEST']._serialized_start=243 - _globals['_USERIDFORTOKENREQUEST']._serialized_end=281 - _globals['_USERIDFORTOKENRESPONSE']._serialized_start=283 - _globals['_USERIDFORTOKENRESPONSE']._serialized_end=324 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/client_user_pb2.pyi b/truffle/os/client_user_pb2.pyi deleted file mode 100644 index b6add62..0000000 --- a/truffle/os/client_user_pb2.pyi +++ /dev/null @@ -1,35 +0,0 @@ -from truffle.os import client_metadata_pb2 as _client_metadata_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class RegisterNewUserRequest(_message.Message): - __slots__ = ("metadata", "user_id") - METADATA_FIELD_NUMBER: _ClassVar[int] - USER_ID_FIELD_NUMBER: _ClassVar[int] - metadata: _client_metadata_pb2.ClientMetadata - user_id: str - def __init__(self, metadata: _Optional[_Union[_client_metadata_pb2.ClientMetadata, _Mapping]] = ..., user_id: _Optional[str] = ...) -> None: ... - -class RegisterNewUserResponse(_message.Message): - __slots__ = ("user_id", "token") - USER_ID_FIELD_NUMBER: _ClassVar[int] - TOKEN_FIELD_NUMBER: _ClassVar[int] - user_id: str - token: str - def __init__(self, user_id: _Optional[str] = ..., token: _Optional[str] = ...) -> None: ... - -class UserIDForTokenRequest(_message.Message): - __slots__ = ("token",) - TOKEN_FIELD_NUMBER: _ClassVar[int] - token: str - def __init__(self, token: _Optional[str] = ...) -> None: ... - -class UserIDForTokenResponse(_message.Message): - __slots__ = ("user_id",) - USER_ID_FIELD_NUMBER: _ClassVar[int] - user_id: str - def __init__(self, user_id: _Optional[str] = ...) -> None: ... diff --git a/truffle/os/client_user_pb2_grpc.py b/truffle/os/client_user_pb2_grpc.py deleted file mode 100644 index 732739c..0000000 --- a/truffle/os/client_user_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/client_user_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/hardware_control_pb2.py b/truffle/os/hardware_control_pb2.py deleted file mode 100644 index 25f2ab5..0000000 --- a/truffle/os/hardware_control_pb2.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/hardware_control.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/hardware_control.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!truffle/os/hardware_control.proto\x12\ntruffle.os\"\xe7\x01\n\x1bHardwarePowerControlRequest\x12R\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x42.truffle.os.HardwarePowerControlRequest.HardwarePowerControlAction\"t\n\x1aHardwarePowerControlAction\x12\x1e\n\x1a\x43ONTROL_ACTION_UNSPECIFIED\x10\x00\x12\x19\n\x15\x43ONTROL_ACTION_REBOOT\x10\x01\x12\x1b\n\x17\x43ONTROL_ACTION_SHUTDOWN\x10\x02\"\x1e\n\x1cHardwarePowerControlResponseb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.hardware_control_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_HARDWAREPOWERCONTROLREQUEST']._serialized_start=50 - _globals['_HARDWAREPOWERCONTROLREQUEST']._serialized_end=281 - _globals['_HARDWAREPOWERCONTROLREQUEST_HARDWAREPOWERCONTROLACTION']._serialized_start=165 - _globals['_HARDWAREPOWERCONTROLREQUEST_HARDWAREPOWERCONTROLACTION']._serialized_end=281 - _globals['_HARDWAREPOWERCONTROLRESPONSE']._serialized_start=283 - _globals['_HARDWAREPOWERCONTROLRESPONSE']._serialized_end=313 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/hardware_control_pb2.pyi b/truffle/os/hardware_control_pb2.pyi deleted file mode 100644 index 99af986..0000000 --- a/truffle/os/hardware_control_pb2.pyi +++ /dev/null @@ -1,24 +0,0 @@ -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class HardwarePowerControlRequest(_message.Message): - __slots__ = ("action",) - class HardwarePowerControlAction(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - CONTROL_ACTION_UNSPECIFIED: _ClassVar[HardwarePowerControlRequest.HardwarePowerControlAction] - CONTROL_ACTION_REBOOT: _ClassVar[HardwarePowerControlRequest.HardwarePowerControlAction] - CONTROL_ACTION_SHUTDOWN: _ClassVar[HardwarePowerControlRequest.HardwarePowerControlAction] - CONTROL_ACTION_UNSPECIFIED: HardwarePowerControlRequest.HardwarePowerControlAction - CONTROL_ACTION_REBOOT: HardwarePowerControlRequest.HardwarePowerControlAction - CONTROL_ACTION_SHUTDOWN: HardwarePowerControlRequest.HardwarePowerControlAction - ACTION_FIELD_NUMBER: _ClassVar[int] - action: HardwarePowerControlRequest.HardwarePowerControlAction - def __init__(self, action: _Optional[_Union[HardwarePowerControlRequest.HardwarePowerControlAction, str]] = ...) -> None: ... - -class HardwarePowerControlResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... diff --git a/truffle/os/hardware_control_pb2_grpc.py b/truffle/os/hardware_control_pb2_grpc.py deleted file mode 100644 index 0cabd14..0000000 --- a/truffle/os/hardware_control_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/hardware_control_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/hardware_info_pb2.py b/truffle/os/hardware_info_pb2.py deleted file mode 100644 index f98e4da..0000000 --- a/truffle/os/hardware_info_pb2.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/hardware_info.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/hardware_info.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import hardware_network_pb2 as truffle_dot_os_dot_hardware__network__pb2 -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1etruffle/os/hardware_info.proto\x12\ntruffle.os\x1a!truffle/os/hardware_network.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xba\x02\n\x0cHardwareInfo\x12\x10\n\x08hostname\x18\x01 \x01(\t\x12\x12\n\nip_address\x18\x02 \x01(\t\x12\x13\n\x0bmac_address\x18\x03 \x01(\t\x12\x39\n\x0enetwork_status\x18\x04 \x01(\x0e\x32!.truffle.os.HardwareNetworkStatus\x12\x42\n\x14\x63urrent_wifi_network\x18\x05 \x01(\x0b\x32\x1f.truffle.os.HardwareWifiNetworkH\x00\x88\x01\x01\x12\x15\n\rserial_number\x18\n \x01(\t\x12.\n\nstart_time\x18\x0b \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x10\n\x08timezone\x18\x0c \x01(\tB\x17\n\x15_current_wifi_networkb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.hardware_info_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_HARDWAREINFO']._serialized_start=115 - _globals['_HARDWAREINFO']._serialized_end=429 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/hardware_info_pb2.pyi b/truffle/os/hardware_info_pb2.pyi deleted file mode 100644 index 5381f44..0000000 --- a/truffle/os/hardware_info_pb2.pyi +++ /dev/null @@ -1,30 +0,0 @@ -import datetime - -from truffle.os import hardware_network_pb2 as _hardware_network_pb2 -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class HardwareInfo(_message.Message): - __slots__ = ("hostname", "ip_address", "mac_address", "network_status", "current_wifi_network", "serial_number", "start_time", "timezone") - HOSTNAME_FIELD_NUMBER: _ClassVar[int] - IP_ADDRESS_FIELD_NUMBER: _ClassVar[int] - MAC_ADDRESS_FIELD_NUMBER: _ClassVar[int] - NETWORK_STATUS_FIELD_NUMBER: _ClassVar[int] - CURRENT_WIFI_NETWORK_FIELD_NUMBER: _ClassVar[int] - SERIAL_NUMBER_FIELD_NUMBER: _ClassVar[int] - START_TIME_FIELD_NUMBER: _ClassVar[int] - TIMEZONE_FIELD_NUMBER: _ClassVar[int] - hostname: str - ip_address: str - mac_address: str - network_status: _hardware_network_pb2.HardwareNetworkStatus - current_wifi_network: _hardware_network_pb2.HardwareWifiNetwork - serial_number: str - start_time: _timestamp_pb2.Timestamp - timezone: str - def __init__(self, hostname: _Optional[str] = ..., ip_address: _Optional[str] = ..., mac_address: _Optional[str] = ..., network_status: _Optional[_Union[_hardware_network_pb2.HardwareNetworkStatus, str]] = ..., current_wifi_network: _Optional[_Union[_hardware_network_pb2.HardwareWifiNetwork, _Mapping]] = ..., serial_number: _Optional[str] = ..., start_time: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., timezone: _Optional[str] = ...) -> None: ... diff --git a/truffle/os/hardware_info_pb2_grpc.py b/truffle/os/hardware_info_pb2_grpc.py deleted file mode 100644 index 0920be0..0000000 --- a/truffle/os/hardware_info_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/hardware_info_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/hardware_network_pb2.py b/truffle/os/hardware_network_pb2.py deleted file mode 100644 index a93c1d4..0000000 --- a/truffle/os/hardware_network_pb2.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/hardware_network.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/hardware_network.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n!truffle/os/hardware_network.proto\x12\ntruffle.os\"1\n\x13HardwareWifiNetwork\x12\x0c\n\x04ssid\x18\x01 \x01(\t\x12\x0c\n\x04rssi\x18\x02 \x01(\x02*g\n\x15HardwareNetworkStatus\x12\x0f\n\x0bNET_DEFAULT\x10\x00\x12\x15\n\x11NET_NOT_CONNECTED\x10\x01\x12\x13\n\x0fNET_NO_INTERNET\x10\x02\x12\x11\n\rNET_CONNECTED\x10\x03\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.hardware_network_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_HARDWARENETWORKSTATUS']._serialized_start=100 - _globals['_HARDWARENETWORKSTATUS']._serialized_end=203 - _globals['_HARDWAREWIFINETWORK']._serialized_start=49 - _globals['_HARDWAREWIFINETWORK']._serialized_end=98 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/hardware_network_pb2.pyi b/truffle/os/hardware_network_pb2.pyi deleted file mode 100644 index 8a70fed..0000000 --- a/truffle/os/hardware_network_pb2.pyi +++ /dev/null @@ -1,25 +0,0 @@ -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional - -DESCRIPTOR: _descriptor.FileDescriptor - -class HardwareNetworkStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - NET_DEFAULT: _ClassVar[HardwareNetworkStatus] - NET_NOT_CONNECTED: _ClassVar[HardwareNetworkStatus] - NET_NO_INTERNET: _ClassVar[HardwareNetworkStatus] - NET_CONNECTED: _ClassVar[HardwareNetworkStatus] -NET_DEFAULT: HardwareNetworkStatus -NET_NOT_CONNECTED: HardwareNetworkStatus -NET_NO_INTERNET: HardwareNetworkStatus -NET_CONNECTED: HardwareNetworkStatus - -class HardwareWifiNetwork(_message.Message): - __slots__ = ("ssid", "rssi") - SSID_FIELD_NUMBER: _ClassVar[int] - RSSI_FIELD_NUMBER: _ClassVar[int] - ssid: str - rssi: float - def __init__(self, ssid: _Optional[str] = ..., rssi: _Optional[float] = ...) -> None: ... diff --git a/truffle/os/hardware_network_pb2_grpc.py b/truffle/os/hardware_network_pb2_grpc.py deleted file mode 100644 index 122a5b5..0000000 --- a/truffle/os/hardware_network_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/hardware_network_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/hardware_settings_pb2.py b/truffle/os/hardware_settings_pb2.py deleted file mode 100644 index 2dc5c8b..0000000 --- a/truffle/os/hardware_settings_pb2.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/hardware_settings.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/hardware_settings.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"truffle/os/hardware_settings.proto\x12\ntruffle.os\"\xc3\x01\n\x10HardwareSettings\x12\x14\n\x0ctruffle_name\x18\x01 \x01(\t\x12\x43\n\x0cled_settings\x18\x02 \x01(\x0b\x32(.truffle.os.HardwareSettings.LEDSettingsH\x00\x88\x01\x01\x1a\x43\n\x0bLEDSettings\x12\x14\n\x07\x65nabled\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x12\n\nbrightness\x18\x02 \x01(\x02\x42\n\n\x08_enabledB\x0f\n\r_led_settingsb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.hardware_settings_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_HARDWARESETTINGS']._serialized_start=51 - _globals['_HARDWARESETTINGS']._serialized_end=246 - _globals['_HARDWARESETTINGS_LEDSETTINGS']._serialized_start=162 - _globals['_HARDWARESETTINGS_LEDSETTINGS']._serialized_end=229 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/hardware_settings_pb2.pyi b/truffle/os/hardware_settings_pb2.pyi deleted file mode 100644 index 7f3af53..0000000 --- a/truffle/os/hardware_settings_pb2.pyi +++ /dev/null @@ -1,21 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class HardwareSettings(_message.Message): - __slots__ = ("truffle_name", "led_settings") - class LEDSettings(_message.Message): - __slots__ = ("enabled", "brightness") - ENABLED_FIELD_NUMBER: _ClassVar[int] - BRIGHTNESS_FIELD_NUMBER: _ClassVar[int] - enabled: bool - brightness: float - def __init__(self, enabled: bool = ..., brightness: _Optional[float] = ...) -> None: ... - TRUFFLE_NAME_FIELD_NUMBER: _ClassVar[int] - LED_SETTINGS_FIELD_NUMBER: _ClassVar[int] - truffle_name: str - led_settings: HardwareSettings.LEDSettings - def __init__(self, truffle_name: _Optional[str] = ..., led_settings: _Optional[_Union[HardwareSettings.LEDSettings, _Mapping]] = ...) -> None: ... diff --git a/truffle/os/hardware_settings_pb2_grpc.py b/truffle/os/hardware_settings_pb2_grpc.py deleted file mode 100644 index ed803c6..0000000 --- a/truffle/os/hardware_settings_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/hardware_settings_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/hardware_stats_pb2.py b/truffle/os/hardware_stats_pb2.py deleted file mode 100644 index 6cfab76..0000000 --- a/truffle/os/hardware_stats_pb2.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/hardware_stats.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/hardware_stats.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import hardware_network_pb2 as truffle_dot_os_dot_hardware__network__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ftruffle/os/hardware_stats.proto\x12\ntruffle.os\x1a!truffle/os/hardware_network.proto\"\xf7\x05\n\rHardwareStats\x12.\n\x05usage\x18\x01 \x01(\x0b\x32\x1f.truffle.os.HardwareStats.Usage\x12.\n\x05temps\x18\x02 \x01(\x0b\x32\x1f.truffle.os.HardwareStats.Temps\x12.\n\x05power\x18\x03 \x01(\x0b\x32\x1f.truffle.os.HardwareStats.Power\x12,\n\x04misc\x18\x04 \x01(\x0b\x32\x1e.truffle.os.HardwareStats.Misc\x12\x39\n\x0enetwork_status\x18\x05 \x01(\x0e\x32!.truffle.os.HardwareNetworkStatus\x12\x17\n\x0fthermal_warning\x18\n \x01(\x08\x12\x14\n\x0c\x64isk_warning\x18\x0b \x01(\x08\x12\x15\n\rfubar_warning\x18\x0c \x01(\x08\x12 \n\x18\x63urrent_poll_interval_ms\x18\r \x01(\r\x1a\x8f\x01\n\x05Usage\x12\x0f\n\x07\x63pu_avg\x18\x01 \x01(\x02\x12\x0f\n\x07\x63pu_max\x18\x02 \x01(\x02\x12\x14\n\x0cmemory_total\x18\x03 \x01(\x02\x12\x12\n\nmemory_gpu\x18\x04 \x01(\x02\x12\x12\n\nmemory_cpu\x18\x05 \x01(\x02\x12\x0c\n\x04\x64isk\x18\x06 \x01(\x02\x12\x0b\n\x03gpu\x18\x07 \x01(\x02\x12\x0b\n\x03\x66\x61n\x18\x08 \x01(\x02\x1aZ\n\x05Temps\x12\x0f\n\x07\x63pu_avg\x18\x01 \x01(\x02\x12\x0f\n\x07\x63pu_max\x18\x02 \x01(\x02\x12\x0f\n\x07gpu_avg\x18\x03 \x01(\x02\x12\x0f\n\x07gpu_max\x18\x04 \x01(\x02\x12\r\n\x05t_max\x18\x05 \x01(\x02\x1a\x30\n\x05Power\x12\r\n\x05total\x18\x01 \x01(\x02\x12\x0b\n\x03\x63pu\x18\x02 \x01(\x02\x12\x0b\n\x03gpu\x18\x03 \x01(\x02\x1aM\n\x04Misc\x12\x0e\n\x06net_rx\x18\x01 \x01(\x03\x12\x0e\n\x06net_tx\x18\x02 \x01(\x03\x12\x11\n\tdisk_read\x18\x03 \x01(\x03\x12\x12\n\ndisk_write\x18\x04 \x01(\x03J\x04\x08\x06\x10\x07J\x04\x08\x07\x10\x08J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\x16\n\x14HardwareStatsRequestb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.hardware_stats_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_HARDWARESTATS']._serialized_start=83 - _globals['_HARDWARESTATS']._serialized_end=842 - _globals['_HARDWARESTATS_USAGE']._serialized_start=454 - _globals['_HARDWARESTATS_USAGE']._serialized_end=597 - _globals['_HARDWARESTATS_TEMPS']._serialized_start=599 - _globals['_HARDWARESTATS_TEMPS']._serialized_end=689 - _globals['_HARDWARESTATS_POWER']._serialized_start=691 - _globals['_HARDWARESTATS_POWER']._serialized_end=739 - _globals['_HARDWARESTATS_MISC']._serialized_start=741 - _globals['_HARDWARESTATS_MISC']._serialized_end=818 - _globals['_HARDWARESTATSREQUEST']._serialized_start=844 - _globals['_HARDWARESTATSREQUEST']._serialized_end=866 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/hardware_stats_pb2.pyi b/truffle/os/hardware_stats_pb2.pyi deleted file mode 100644 index 27d7c5d..0000000 --- a/truffle/os/hardware_stats_pb2.pyi +++ /dev/null @@ -1,85 +0,0 @@ -from truffle.os import hardware_network_pb2 as _hardware_network_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class HardwareStats(_message.Message): - __slots__ = ("usage", "temps", "power", "misc", "network_status", "thermal_warning", "disk_warning", "fubar_warning", "current_poll_interval_ms") - class Usage(_message.Message): - __slots__ = ("cpu_avg", "cpu_max", "memory_total", "memory_gpu", "memory_cpu", "disk", "gpu", "fan") - CPU_AVG_FIELD_NUMBER: _ClassVar[int] - CPU_MAX_FIELD_NUMBER: _ClassVar[int] - MEMORY_TOTAL_FIELD_NUMBER: _ClassVar[int] - MEMORY_GPU_FIELD_NUMBER: _ClassVar[int] - MEMORY_CPU_FIELD_NUMBER: _ClassVar[int] - DISK_FIELD_NUMBER: _ClassVar[int] - GPU_FIELD_NUMBER: _ClassVar[int] - FAN_FIELD_NUMBER: _ClassVar[int] - cpu_avg: float - cpu_max: float - memory_total: float - memory_gpu: float - memory_cpu: float - disk: float - gpu: float - fan: float - def __init__(self, cpu_avg: _Optional[float] = ..., cpu_max: _Optional[float] = ..., memory_total: _Optional[float] = ..., memory_gpu: _Optional[float] = ..., memory_cpu: _Optional[float] = ..., disk: _Optional[float] = ..., gpu: _Optional[float] = ..., fan: _Optional[float] = ...) -> None: ... - class Temps(_message.Message): - __slots__ = ("cpu_avg", "cpu_max", "gpu_avg", "gpu_max", "t_max") - CPU_AVG_FIELD_NUMBER: _ClassVar[int] - CPU_MAX_FIELD_NUMBER: _ClassVar[int] - GPU_AVG_FIELD_NUMBER: _ClassVar[int] - GPU_MAX_FIELD_NUMBER: _ClassVar[int] - T_MAX_FIELD_NUMBER: _ClassVar[int] - cpu_avg: float - cpu_max: float - gpu_avg: float - gpu_max: float - t_max: float - def __init__(self, cpu_avg: _Optional[float] = ..., cpu_max: _Optional[float] = ..., gpu_avg: _Optional[float] = ..., gpu_max: _Optional[float] = ..., t_max: _Optional[float] = ...) -> None: ... - class Power(_message.Message): - __slots__ = ("total", "cpu", "gpu") - TOTAL_FIELD_NUMBER: _ClassVar[int] - CPU_FIELD_NUMBER: _ClassVar[int] - GPU_FIELD_NUMBER: _ClassVar[int] - total: float - cpu: float - gpu: float - def __init__(self, total: _Optional[float] = ..., cpu: _Optional[float] = ..., gpu: _Optional[float] = ...) -> None: ... - class Misc(_message.Message): - __slots__ = ("net_rx", "net_tx", "disk_read", "disk_write") - NET_RX_FIELD_NUMBER: _ClassVar[int] - NET_TX_FIELD_NUMBER: _ClassVar[int] - DISK_READ_FIELD_NUMBER: _ClassVar[int] - DISK_WRITE_FIELD_NUMBER: _ClassVar[int] - net_rx: int - net_tx: int - disk_read: int - disk_write: int - def __init__(self, net_rx: _Optional[int] = ..., net_tx: _Optional[int] = ..., disk_read: _Optional[int] = ..., disk_write: _Optional[int] = ...) -> None: ... - USAGE_FIELD_NUMBER: _ClassVar[int] - TEMPS_FIELD_NUMBER: _ClassVar[int] - POWER_FIELD_NUMBER: _ClassVar[int] - MISC_FIELD_NUMBER: _ClassVar[int] - NETWORK_STATUS_FIELD_NUMBER: _ClassVar[int] - THERMAL_WARNING_FIELD_NUMBER: _ClassVar[int] - DISK_WARNING_FIELD_NUMBER: _ClassVar[int] - FUBAR_WARNING_FIELD_NUMBER: _ClassVar[int] - CURRENT_POLL_INTERVAL_MS_FIELD_NUMBER: _ClassVar[int] - usage: HardwareStats.Usage - temps: HardwareStats.Temps - power: HardwareStats.Power - misc: HardwareStats.Misc - network_status: _hardware_network_pb2.HardwareNetworkStatus - thermal_warning: bool - disk_warning: bool - fubar_warning: bool - current_poll_interval_ms: int - def __init__(self, usage: _Optional[_Union[HardwareStats.Usage, _Mapping]] = ..., temps: _Optional[_Union[HardwareStats.Temps, _Mapping]] = ..., power: _Optional[_Union[HardwareStats.Power, _Mapping]] = ..., misc: _Optional[_Union[HardwareStats.Misc, _Mapping]] = ..., network_status: _Optional[_Union[_hardware_network_pb2.HardwareNetworkStatus, str]] = ..., thermal_warning: bool = ..., disk_warning: bool = ..., fubar_warning: bool = ..., current_poll_interval_ms: _Optional[int] = ...) -> None: ... - -class HardwareStatsRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... diff --git a/truffle/os/hardware_stats_pb2_grpc.py b/truffle/os/hardware_stats_pb2_grpc.py deleted file mode 100644 index 122c216..0000000 --- a/truffle/os/hardware_stats_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/hardware_stats_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/installer_pb2.py b/truffle/os/installer_pb2.py deleted file mode 100644 index 4ac2a76..0000000 --- a/truffle/os/installer_pb2.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/installer.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/installer.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.app import app_pb2 as truffle_dot_app_dot_app__pb2 - -from truffle.app.app_pb2 import * - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1atruffle/os/installer.proto\x12\ntruffle.os\x1a\x15truffle/app/app.proto\"\x87\x01\n\x10\x41ppInstallSource\x12\x35\n\x0bsource_type\x18\x01 \x01(\x0e\x32 .truffle.os.AppInstallSourceType\x12\x10\n\x03url\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x15\n\x08git_hash\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x06\n\x04_urlB\x0b\n\t_git_hash\"\xd9\x08\n\x0f\x41ppInstallModal\x12\x12\n\nstep_index\x18\x01 \x01(\x05\x12\x11\n\tstep_name\x18\x02 \x01(\t\x12\x41\n\rwelcome_modal\x18\n \x01(\x0b\x32(.truffle.os.AppInstallModal.WelcomeModalH\x00\x12H\n\x11text_fields_modal\x18\x0b \x01(\x0b\x32+.truffle.os.AppInstallModal.TextFieldsModalH\x00\x12\x39\n\tvnc_modal\x18\x0c \x01(\x0b\x32$.truffle.os.AppInstallModal.VNCModalH\x00\x12?\n\x0c\x66inish_modal\x18\x0e \x01(\x0b\x32\'.truffle.os.AppInstallModal.FinishModalH\x00\x12H\n\x11upload_file_modal\x18\x0f \x01(\x0b\x32+.truffle.os.AppInstallModal.UploadFileModalH\x00\x12=\n\x0boauth_modal\x18\x10 \x01(\x0b\x32&.truffle.os.AppInstallModal.OAuthModalH\x00\x1a\'\n\x0cWelcomeModal\x12\x17\n\x0fwelcome_message\x18\x01 \x01(\t\x1a\xca\x02\n\x0fTextFieldsModal\x12\x14\n\x0cinstructions\x18\x01 \x01(\t\x12G\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x37.truffle.os.AppInstallModal.TextFieldsModal.FieldsEntry\x1ar\n\tTextField\x12\r\n\x05label\x18\x01 \x01(\t\x12\x13\n\x0bplaceholder\x18\x02 \x01(\t\x12\x13\n\x0bis_password\x18\x03 \x01(\x08\x12\x1a\n\rdefault_value\x18\x04 \x01(\tH\x00\x88\x01\x01\x42\x10\n\x0e_default_value\x1a\x64\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x44\n\x05value\x18\x02 \x01(\x0b\x32\x35.truffle.os.AppInstallModal.TextFieldsModal.TextField:\x02\x38\x01\x1aR\n\x08VNCModal\x12\x14\n\x0cinstructions\x18\x01 \x01(\t\x12\x14\n\x0cvnc_uri_path\x18\x02 \x01(\t\x12\x1a\n\x12\x63loses_on_complete\x18\x03 \x01(\x08\x1aU\n\nOAuthModal\x12\x14\n\x0cinstructions\x18\x01 \x01(\t\x12\x10\n\x08provider\x18\x02 \x01(\t\x12\x10\n\x08\x61uth_url\x18\x03 \x01(\t\x12\r\n\x05state\x18\x04 \x01(\t\x1a*\n\x0fUploadFileModal\x12\x17\n\x0fupload_uri_path\x18\x01 \x01(\t\x1a\x37\n\x0b\x46inishModal\x12\x10\n\x08\x61pp_uuid\x18\x01 \x01(\t\x12\x16\n\x0e\x66inish_message\x18\x02 \x01(\tB\x07\n\x05modal\"(\n\x0f\x41ppInstallError\x12\x15\n\rerror_message\x18\x01 \x01(\t\",\n\x11\x41ppInstallLoading\x12\x17\n\x0floading_message\x18\x01 \x01(\t\"\xb1\x01\n\x0e\x41ppInstallHint\x12\x34\n\x08ui_state\x18\x01 \x01(\x0e\x32\".truffle.os.AppInstallHint.UiState\"i\n\x07UiState\x12\x18\n\x14UI_STATE_UNSPECIFIED\x10\x00\x12#\n\x1fUI_STATE_USER_INTERACTION_READY\x10\x01\x12\x1f\n\x1bUI_STATE_MOVE_TO_BACKGROUND\x10\x02\"~\n\x12\x41ppInstallMetadata\x12\x0c\n\x04uuid\x18\x01 \x01(\t\x12*\n\x08metadata\x18\x02 \x01(\x0b\x32\x18.truffle.app.AppMetadata\x12\x16\n\x0ehas_foreground\x18\x03 \x01(\x08\x12\x16\n\x0ehas_background\x18\x04 \x01(\x08\"\xb8\x04\n\x14\x41ppInstallUserAction\x12;\n\x04next\x18\x01 \x01(\x0b\x32+.truffle.os.AppInstallUserAction.NextActionH\x00\x12N\n\x0btext_fields\x18\x02 \x01(\x0b\x32\x37.truffle.os.AppInstallUserAction.SubmitTextFieldsActionH\x00\x12=\n\x05\x61\x62ort\x18\x03 \x01(\x0b\x32,.truffle.os.AppInstallUserAction.AbortActionH\x00\x12\x43\n\x05oauth\x18\x04 \x01(\x0b\x32\x32.truffle.os.AppInstallUserAction.SubmitOAuthActionH\x00\x1a\x0c\n\nNextAction\x1a\xb5\x01\n\x16SubmitTextFieldsAction\x12\x64\n\x0f\x66ield_responses\x18\x01 \x03(\x0b\x32K.truffle.os.AppInstallUserAction.SubmitTextFieldsAction.FieldResponsesEntry\x1a\x35\n\x13\x46ieldResponsesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x30\n\x11SubmitOAuthAction\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\r\n\x05state\x18\x02 \x01(\t\x1a\r\n\x0b\x41\x62ortActionB\x08\n\x06\x61\x63tion\"\xde\x01\n\x11\x41ppInstallRequest\x12\x42\n\tstart_new\x18\x01 \x01(\x0b\x32-.truffle.os.AppInstallRequest.StartNewInstallH\x00\x12\x37\n\x0buser_action\x18\x03 \x01(\x0b\x32 .truffle.os.AppInstallUserActionH\x00\x1a?\n\x0fStartNewInstall\x12,\n\x06source\x18\x01 \x01(\x0b\x32\x1c.truffle.os.AppInstallSourceB\x0b\n\toperation\"\xb7\x02\n\x12\x41ppInstallResponse\x12\x34\n\rinstall_modal\x18\x01 \x01(\x0b\x32\x1b.truffle.os.AppInstallModalH\x00\x12\x34\n\rinstall_error\x18\x02 \x01(\x0b\x32\x1b.truffle.os.AppInstallErrorH\x00\x12\x38\n\x0finstall_loading\x18\x03 \x01(\x0b\x32\x1d.truffle.os.AppInstallLoadingH\x00\x12:\n\x10install_metadata\x18\x04 \x01(\x0b\x32\x1e.truffle.os.AppInstallMetadataH\x00\x12\x32\n\x0cinstall_hint\x18\x05 \x01(\x0b\x32\x1a.truffle.os.AppInstallHintH\x00\x42\x0b\n\toperation*\xa3\x01\n\x14\x41ppInstallSourceType\x12\'\n#APP_INSTALL_SOURCE_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1b\x41PP_INSTALL_SOURCE_TYPE_URL\x10\x01\x12 \n\x1c\x41PP_INSTALL_SOURCE_TYPE_FILE\x10\x02\x12\x1f\n\x1b\x41PP_INSTALL_SOURCE_TYPE_GIT\x10\x03P\x00\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.installer_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_APPINSTALLMODAL_TEXTFIELDSMODAL_FIELDSENTRY']._loaded_options = None - _globals['_APPINSTALLMODAL_TEXTFIELDSMODAL_FIELDSENTRY']._serialized_options = b'8\001' - _globals['_APPINSTALLUSERACTION_SUBMITTEXTFIELDSACTION_FIELDRESPONSESENTRY']._loaded_options = None - _globals['_APPINSTALLUSERACTION_SUBMITTEXTFIELDSACTION_FIELDRESPONSESENTRY']._serialized_options = b'8\001' - _globals['_APPINSTALLSOURCETYPE']._serialized_start=2826 - _globals['_APPINSTALLSOURCETYPE']._serialized_end=2989 - _globals['_APPINSTALLSOURCE']._serialized_start=66 - _globals['_APPINSTALLSOURCE']._serialized_end=201 - _globals['_APPINSTALLMODAL']._serialized_start=204 - _globals['_APPINSTALLMODAL']._serialized_end=1317 - _globals['_APPINSTALLMODAL_WELCOMEMODAL']._serialized_start=664 - _globals['_APPINSTALLMODAL_WELCOMEMODAL']._serialized_end=703 - _globals['_APPINSTALLMODAL_TEXTFIELDSMODAL']._serialized_start=706 - _globals['_APPINSTALLMODAL_TEXTFIELDSMODAL']._serialized_end=1036 - _globals['_APPINSTALLMODAL_TEXTFIELDSMODAL_TEXTFIELD']._serialized_start=820 - _globals['_APPINSTALLMODAL_TEXTFIELDSMODAL_TEXTFIELD']._serialized_end=934 - _globals['_APPINSTALLMODAL_TEXTFIELDSMODAL_FIELDSENTRY']._serialized_start=936 - _globals['_APPINSTALLMODAL_TEXTFIELDSMODAL_FIELDSENTRY']._serialized_end=1036 - _globals['_APPINSTALLMODAL_VNCMODAL']._serialized_start=1038 - _globals['_APPINSTALLMODAL_VNCMODAL']._serialized_end=1120 - _globals['_APPINSTALLMODAL_OAUTHMODAL']._serialized_start=1122 - _globals['_APPINSTALLMODAL_OAUTHMODAL']._serialized_end=1207 - _globals['_APPINSTALLMODAL_UPLOADFILEMODAL']._serialized_start=1209 - _globals['_APPINSTALLMODAL_UPLOADFILEMODAL']._serialized_end=1251 - _globals['_APPINSTALLMODAL_FINISHMODAL']._serialized_start=1253 - _globals['_APPINSTALLMODAL_FINISHMODAL']._serialized_end=1308 - _globals['_APPINSTALLERROR']._serialized_start=1319 - _globals['_APPINSTALLERROR']._serialized_end=1359 - _globals['_APPINSTALLLOADING']._serialized_start=1361 - _globals['_APPINSTALLLOADING']._serialized_end=1405 - _globals['_APPINSTALLHINT']._serialized_start=1408 - _globals['_APPINSTALLHINT']._serialized_end=1585 - _globals['_APPINSTALLHINT_UISTATE']._serialized_start=1480 - _globals['_APPINSTALLHINT_UISTATE']._serialized_end=1585 - _globals['_APPINSTALLMETADATA']._serialized_start=1587 - _globals['_APPINSTALLMETADATA']._serialized_end=1713 - _globals['_APPINSTALLUSERACTION']._serialized_start=1716 - _globals['_APPINSTALLUSERACTION']._serialized_end=2284 - _globals['_APPINSTALLUSERACTION_NEXTACTION']._serialized_start=2013 - _globals['_APPINSTALLUSERACTION_NEXTACTION']._serialized_end=2025 - _globals['_APPINSTALLUSERACTION_SUBMITTEXTFIELDSACTION']._serialized_start=2028 - _globals['_APPINSTALLUSERACTION_SUBMITTEXTFIELDSACTION']._serialized_end=2209 - _globals['_APPINSTALLUSERACTION_SUBMITTEXTFIELDSACTION_FIELDRESPONSESENTRY']._serialized_start=2156 - _globals['_APPINSTALLUSERACTION_SUBMITTEXTFIELDSACTION_FIELDRESPONSESENTRY']._serialized_end=2209 - _globals['_APPINSTALLUSERACTION_SUBMITOAUTHACTION']._serialized_start=2211 - _globals['_APPINSTALLUSERACTION_SUBMITOAUTHACTION']._serialized_end=2259 - _globals['_APPINSTALLUSERACTION_ABORTACTION']._serialized_start=2261 - _globals['_APPINSTALLUSERACTION_ABORTACTION']._serialized_end=2274 - _globals['_APPINSTALLREQUEST']._serialized_start=2287 - _globals['_APPINSTALLREQUEST']._serialized_end=2509 - _globals['_APPINSTALLREQUEST_STARTNEWINSTALL']._serialized_start=2433 - _globals['_APPINSTALLREQUEST_STARTNEWINSTALL']._serialized_end=2496 - _globals['_APPINSTALLRESPONSE']._serialized_start=2512 - _globals['_APPINSTALLRESPONSE']._serialized_end=2823 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/installer_pb2.pyi b/truffle/os/installer_pb2.pyi deleted file mode 100644 index 21ac0b4..0000000 --- a/truffle/os/installer_pb2.pyi +++ /dev/null @@ -1,218 +0,0 @@ -from truffle.app import app_pb2 as _app_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union -from truffle.app.app_pb2 import AppMetadata as AppMetadata -from truffle.app.app_pb2 import AppConfig as AppConfig -from truffle.app.app_pb2 import App as App -from truffle.app.app_pb2 import AppError as AppError - -DESCRIPTOR: _descriptor.FileDescriptor - -class AppInstallSourceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - APP_INSTALL_SOURCE_TYPE_UNSPECIFIED: _ClassVar[AppInstallSourceType] - APP_INSTALL_SOURCE_TYPE_URL: _ClassVar[AppInstallSourceType] - APP_INSTALL_SOURCE_TYPE_FILE: _ClassVar[AppInstallSourceType] - APP_INSTALL_SOURCE_TYPE_GIT: _ClassVar[AppInstallSourceType] -APP_INSTALL_SOURCE_TYPE_UNSPECIFIED: AppInstallSourceType -APP_INSTALL_SOURCE_TYPE_URL: AppInstallSourceType -APP_INSTALL_SOURCE_TYPE_FILE: AppInstallSourceType -APP_INSTALL_SOURCE_TYPE_GIT: AppInstallSourceType - -class AppInstallSource(_message.Message): - __slots__ = ("source_type", "url", "git_hash") - SOURCE_TYPE_FIELD_NUMBER: _ClassVar[int] - URL_FIELD_NUMBER: _ClassVar[int] - GIT_HASH_FIELD_NUMBER: _ClassVar[int] - source_type: AppInstallSourceType - url: str - git_hash: str - def __init__(self, source_type: _Optional[_Union[AppInstallSourceType, str]] = ..., url: _Optional[str] = ..., git_hash: _Optional[str] = ...) -> None: ... - -class AppInstallModal(_message.Message): - __slots__ = ("step_index", "step_name", "welcome_modal", "text_fields_modal", "vnc_modal", "finish_modal", "upload_file_modal", "oauth_modal") - class WelcomeModal(_message.Message): - __slots__ = ("welcome_message",) - WELCOME_MESSAGE_FIELD_NUMBER: _ClassVar[int] - welcome_message: str - def __init__(self, welcome_message: _Optional[str] = ...) -> None: ... - class TextFieldsModal(_message.Message): - __slots__ = ("instructions", "fields") - class TextField(_message.Message): - __slots__ = ("label", "placeholder", "is_password", "default_value") - LABEL_FIELD_NUMBER: _ClassVar[int] - PLACEHOLDER_FIELD_NUMBER: _ClassVar[int] - IS_PASSWORD_FIELD_NUMBER: _ClassVar[int] - DEFAULT_VALUE_FIELD_NUMBER: _ClassVar[int] - label: str - placeholder: str - is_password: bool - default_value: str - def __init__(self, label: _Optional[str] = ..., placeholder: _Optional[str] = ..., is_password: bool = ..., default_value: _Optional[str] = ...) -> None: ... - class FieldsEntry(_message.Message): - __slots__ = ("key", "value") - KEY_FIELD_NUMBER: _ClassVar[int] - VALUE_FIELD_NUMBER: _ClassVar[int] - key: str - value: AppInstallModal.TextFieldsModal.TextField - def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[AppInstallModal.TextFieldsModal.TextField, _Mapping]] = ...) -> None: ... - INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] - FIELDS_FIELD_NUMBER: _ClassVar[int] - instructions: str - fields: _containers.MessageMap[str, AppInstallModal.TextFieldsModal.TextField] - def __init__(self, instructions: _Optional[str] = ..., fields: _Optional[_Mapping[str, AppInstallModal.TextFieldsModal.TextField]] = ...) -> None: ... - class VNCModal(_message.Message): - __slots__ = ("instructions", "vnc_uri_path", "closes_on_complete") - INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] - VNC_URI_PATH_FIELD_NUMBER: _ClassVar[int] - CLOSES_ON_COMPLETE_FIELD_NUMBER: _ClassVar[int] - instructions: str - vnc_uri_path: str - closes_on_complete: bool - def __init__(self, instructions: _Optional[str] = ..., vnc_uri_path: _Optional[str] = ..., closes_on_complete: bool = ...) -> None: ... - class OAuthModal(_message.Message): - __slots__ = ("instructions", "provider", "auth_url", "state") - INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] - PROVIDER_FIELD_NUMBER: _ClassVar[int] - AUTH_URL_FIELD_NUMBER: _ClassVar[int] - STATE_FIELD_NUMBER: _ClassVar[int] - instructions: str - provider: str - auth_url: str - state: str - def __init__(self, instructions: _Optional[str] = ..., provider: _Optional[str] = ..., auth_url: _Optional[str] = ..., state: _Optional[str] = ...) -> None: ... - class UploadFileModal(_message.Message): - __slots__ = ("upload_uri_path",) - UPLOAD_URI_PATH_FIELD_NUMBER: _ClassVar[int] - upload_uri_path: str - def __init__(self, upload_uri_path: _Optional[str] = ...) -> None: ... - class FinishModal(_message.Message): - __slots__ = ("app_uuid", "finish_message") - APP_UUID_FIELD_NUMBER: _ClassVar[int] - FINISH_MESSAGE_FIELD_NUMBER: _ClassVar[int] - app_uuid: str - finish_message: str - def __init__(self, app_uuid: _Optional[str] = ..., finish_message: _Optional[str] = ...) -> None: ... - STEP_INDEX_FIELD_NUMBER: _ClassVar[int] - STEP_NAME_FIELD_NUMBER: _ClassVar[int] - WELCOME_MODAL_FIELD_NUMBER: _ClassVar[int] - TEXT_FIELDS_MODAL_FIELD_NUMBER: _ClassVar[int] - VNC_MODAL_FIELD_NUMBER: _ClassVar[int] - FINISH_MODAL_FIELD_NUMBER: _ClassVar[int] - UPLOAD_FILE_MODAL_FIELD_NUMBER: _ClassVar[int] - OAUTH_MODAL_FIELD_NUMBER: _ClassVar[int] - step_index: int - step_name: str - welcome_modal: AppInstallModal.WelcomeModal - text_fields_modal: AppInstallModal.TextFieldsModal - vnc_modal: AppInstallModal.VNCModal - finish_modal: AppInstallModal.FinishModal - upload_file_modal: AppInstallModal.UploadFileModal - oauth_modal: AppInstallModal.OAuthModal - def __init__(self, step_index: _Optional[int] = ..., step_name: _Optional[str] = ..., welcome_modal: _Optional[_Union[AppInstallModal.WelcomeModal, _Mapping]] = ..., text_fields_modal: _Optional[_Union[AppInstallModal.TextFieldsModal, _Mapping]] = ..., vnc_modal: _Optional[_Union[AppInstallModal.VNCModal, _Mapping]] = ..., finish_modal: _Optional[_Union[AppInstallModal.FinishModal, _Mapping]] = ..., upload_file_modal: _Optional[_Union[AppInstallModal.UploadFileModal, _Mapping]] = ..., oauth_modal: _Optional[_Union[AppInstallModal.OAuthModal, _Mapping]] = ...) -> None: ... - -class AppInstallError(_message.Message): - __slots__ = ("error_message",) - ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int] - error_message: str - def __init__(self, error_message: _Optional[str] = ...) -> None: ... - -class AppInstallLoading(_message.Message): - __slots__ = ("loading_message",) - LOADING_MESSAGE_FIELD_NUMBER: _ClassVar[int] - loading_message: str - def __init__(self, loading_message: _Optional[str] = ...) -> None: ... - -class AppInstallHint(_message.Message): - __slots__ = ("ui_state",) - class UiState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - UI_STATE_UNSPECIFIED: _ClassVar[AppInstallHint.UiState] - UI_STATE_USER_INTERACTION_READY: _ClassVar[AppInstallHint.UiState] - UI_STATE_MOVE_TO_BACKGROUND: _ClassVar[AppInstallHint.UiState] - UI_STATE_UNSPECIFIED: AppInstallHint.UiState - UI_STATE_USER_INTERACTION_READY: AppInstallHint.UiState - UI_STATE_MOVE_TO_BACKGROUND: AppInstallHint.UiState - UI_STATE_FIELD_NUMBER: _ClassVar[int] - ui_state: AppInstallHint.UiState - def __init__(self, ui_state: _Optional[_Union[AppInstallHint.UiState, str]] = ...) -> None: ... - -class AppInstallMetadata(_message.Message): - __slots__ = ("uuid", "metadata", "has_foreground", "has_background") - UUID_FIELD_NUMBER: _ClassVar[int] - METADATA_FIELD_NUMBER: _ClassVar[int] - HAS_FOREGROUND_FIELD_NUMBER: _ClassVar[int] - HAS_BACKGROUND_FIELD_NUMBER: _ClassVar[int] - uuid: str - metadata: _app_pb2.AppMetadata - has_foreground: bool - has_background: bool - def __init__(self, uuid: _Optional[str] = ..., metadata: _Optional[_Union[_app_pb2.AppMetadata, _Mapping]] = ..., has_foreground: bool = ..., has_background: bool = ...) -> None: ... - -class AppInstallUserAction(_message.Message): - __slots__ = ("next", "text_fields", "abort", "oauth") - class NextAction(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - class SubmitTextFieldsAction(_message.Message): - __slots__ = ("field_responses",) - class FieldResponsesEntry(_message.Message): - __slots__ = ("key", "value") - KEY_FIELD_NUMBER: _ClassVar[int] - VALUE_FIELD_NUMBER: _ClassVar[int] - key: str - value: str - def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... - FIELD_RESPONSES_FIELD_NUMBER: _ClassVar[int] - field_responses: _containers.ScalarMap[str, str] - def __init__(self, field_responses: _Optional[_Mapping[str, str]] = ...) -> None: ... - class SubmitOAuthAction(_message.Message): - __slots__ = ("code", "state") - CODE_FIELD_NUMBER: _ClassVar[int] - STATE_FIELD_NUMBER: _ClassVar[int] - code: str - state: str - def __init__(self, code: _Optional[str] = ..., state: _Optional[str] = ...) -> None: ... - class AbortAction(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - NEXT_FIELD_NUMBER: _ClassVar[int] - TEXT_FIELDS_FIELD_NUMBER: _ClassVar[int] - ABORT_FIELD_NUMBER: _ClassVar[int] - OAUTH_FIELD_NUMBER: _ClassVar[int] - next: AppInstallUserAction.NextAction - text_fields: AppInstallUserAction.SubmitTextFieldsAction - abort: AppInstallUserAction.AbortAction - oauth: AppInstallUserAction.SubmitOAuthAction - def __init__(self, next: _Optional[_Union[AppInstallUserAction.NextAction, _Mapping]] = ..., text_fields: _Optional[_Union[AppInstallUserAction.SubmitTextFieldsAction, _Mapping]] = ..., abort: _Optional[_Union[AppInstallUserAction.AbortAction, _Mapping]] = ..., oauth: _Optional[_Union[AppInstallUserAction.SubmitOAuthAction, _Mapping]] = ...) -> None: ... - -class AppInstallRequest(_message.Message): - __slots__ = ("start_new", "user_action") - class StartNewInstall(_message.Message): - __slots__ = ("source",) - SOURCE_FIELD_NUMBER: _ClassVar[int] - source: AppInstallSource - def __init__(self, source: _Optional[_Union[AppInstallSource, _Mapping]] = ...) -> None: ... - START_NEW_FIELD_NUMBER: _ClassVar[int] - USER_ACTION_FIELD_NUMBER: _ClassVar[int] - start_new: AppInstallRequest.StartNewInstall - user_action: AppInstallUserAction - def __init__(self, start_new: _Optional[_Union[AppInstallRequest.StartNewInstall, _Mapping]] = ..., user_action: _Optional[_Union[AppInstallUserAction, _Mapping]] = ...) -> None: ... - -class AppInstallResponse(_message.Message): - __slots__ = ("install_modal", "install_error", "install_loading", "install_metadata", "install_hint") - INSTALL_MODAL_FIELD_NUMBER: _ClassVar[int] - INSTALL_ERROR_FIELD_NUMBER: _ClassVar[int] - INSTALL_LOADING_FIELD_NUMBER: _ClassVar[int] - INSTALL_METADATA_FIELD_NUMBER: _ClassVar[int] - INSTALL_HINT_FIELD_NUMBER: _ClassVar[int] - install_modal: AppInstallModal - install_error: AppInstallError - install_loading: AppInstallLoading - install_metadata: AppInstallMetadata - install_hint: AppInstallHint - def __init__(self, install_modal: _Optional[_Union[AppInstallModal, _Mapping]] = ..., install_error: _Optional[_Union[AppInstallError, _Mapping]] = ..., install_loading: _Optional[_Union[AppInstallLoading, _Mapping]] = ..., install_metadata: _Optional[_Union[AppInstallMetadata, _Mapping]] = ..., install_hint: _Optional[_Union[AppInstallHint, _Mapping]] = ...) -> None: ... diff --git a/truffle/os/installer_pb2_grpc.py b/truffle/os/installer_pb2_grpc.py deleted file mode 100644 index 9e4d9a2..0000000 --- a/truffle/os/installer_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/installer_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/notification_pb2.py b/truffle/os/notification_pb2.py deleted file mode 100644 index 198341b..0000000 --- a/truffle/os/notification_pb2.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/notification.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/notification.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 -from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -from truffle.os import hardware_stats_pb2 as truffle_dot_os_dot_hardware__stats__pb2 -from truffle.os import client_session_pb2 as truffle_dot_os_dot_client__session__pb2 -from truffle.os import background_feed_pb2 as truffle_dot_os_dot_background__feed__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dtruffle/os/notification.proto\x12\ntruffle.os\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1ftruffle/os/hardware_stats.proto\x1a\x1ftruffle/os/client_session.proto\x1a truffle/os/background_feed.proto\"!\n\x1fSubscribeToNotificationsRequest\"\xb7\x04\n\x0cNotification\x12\x37\n\x04type\x18\x01 \x01(\x0e\x32).truffle.os.Notification.NotificationType\x12\x15\n\rassociated_id\x18\x02 \x01(\t\x12&\n\x04none\x18\x03 \x01(\x0b\x32\x16.google.protobuf.EmptyH\x00\x12\x46\n\x18new_session_verification\x18\x07 \x01(\x0b\x32\".truffle.os.NewSessionVerificationH\x00\x12\x44\n\x17\x66\x65\x65\x64_entry_notification\x18\n \x01(\x0b\x32!.truffle.os.FeedEntryNotificationH\x00\x12\x10\n\x08is_error\x18\x08 \x01(\x08\"\x86\x02\n\x10NotificationType\x12\x1d\n\x19NOTIFICATION_TYPE_INVALID\x10\x00\x12\x12\n\x0e\x42G_FEED_UPDATE\x10\x01\x12\x13\n\x0fTASK_HAS_RESULT\x10\x02\x12\x12\n\x0e\x41PP_LIST_DIRTY\x10\x0e\x12\x13\n\x0fTASK_LIST_DIRTY\x10\x10\x12\x11\n\rSESSION_READY\x10\x14\x12 \n\x1cSESSION_VERIFICATION_REQUEST\x10\x15\x12\x11\n\rSESSION_ADDED\x10\x16\x12\x12\n\x0eSESSION_DENIED\x10\x17\x12\x12\n\x0eSERVER_CLOSING\x10\x1f\x12\x11\n\rDISPLAY_TOAST\x10 B\x06\n\x04\x64\x61tab\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.notification_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_SUBSCRIBETONOTIFICATIONSREQUEST']._serialized_start=204 - _globals['_SUBSCRIBETONOTIFICATIONSREQUEST']._serialized_end=237 - _globals['_NOTIFICATION']._serialized_start=240 - _globals['_NOTIFICATION']._serialized_end=807 - _globals['_NOTIFICATION_NOTIFICATIONTYPE']._serialized_start=537 - _globals['_NOTIFICATION_NOTIFICATIONTYPE']._serialized_end=799 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/notification_pb2.pyi b/truffle/os/notification_pb2.pyi deleted file mode 100644 index dee6108..0000000 --- a/truffle/os/notification_pb2.pyi +++ /dev/null @@ -1,56 +0,0 @@ -from google.protobuf import struct_pb2 as _struct_pb2 -from google.protobuf import empty_pb2 as _empty_pb2 -from truffle.os import hardware_stats_pb2 as _hardware_stats_pb2 -from truffle.os import client_session_pb2 as _client_session_pb2 -from truffle.os import background_feed_pb2 as _background_feed_pb2 -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class SubscribeToNotificationsRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class Notification(_message.Message): - __slots__ = ("type", "associated_id", "none", "new_session_verification", "feed_entry_notification", "is_error") - class NotificationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - NOTIFICATION_TYPE_INVALID: _ClassVar[Notification.NotificationType] - BG_FEED_UPDATE: _ClassVar[Notification.NotificationType] - TASK_HAS_RESULT: _ClassVar[Notification.NotificationType] - APP_LIST_DIRTY: _ClassVar[Notification.NotificationType] - TASK_LIST_DIRTY: _ClassVar[Notification.NotificationType] - SESSION_READY: _ClassVar[Notification.NotificationType] - SESSION_VERIFICATION_REQUEST: _ClassVar[Notification.NotificationType] - SESSION_ADDED: _ClassVar[Notification.NotificationType] - SESSION_DENIED: _ClassVar[Notification.NotificationType] - SERVER_CLOSING: _ClassVar[Notification.NotificationType] - DISPLAY_TOAST: _ClassVar[Notification.NotificationType] - NOTIFICATION_TYPE_INVALID: Notification.NotificationType - BG_FEED_UPDATE: Notification.NotificationType - TASK_HAS_RESULT: Notification.NotificationType - APP_LIST_DIRTY: Notification.NotificationType - TASK_LIST_DIRTY: Notification.NotificationType - SESSION_READY: Notification.NotificationType - SESSION_VERIFICATION_REQUEST: Notification.NotificationType - SESSION_ADDED: Notification.NotificationType - SESSION_DENIED: Notification.NotificationType - SERVER_CLOSING: Notification.NotificationType - DISPLAY_TOAST: Notification.NotificationType - TYPE_FIELD_NUMBER: _ClassVar[int] - ASSOCIATED_ID_FIELD_NUMBER: _ClassVar[int] - NONE_FIELD_NUMBER: _ClassVar[int] - NEW_SESSION_VERIFICATION_FIELD_NUMBER: _ClassVar[int] - FEED_ENTRY_NOTIFICATION_FIELD_NUMBER: _ClassVar[int] - IS_ERROR_FIELD_NUMBER: _ClassVar[int] - type: Notification.NotificationType - associated_id: str - none: _empty_pb2.Empty - new_session_verification: _client_session_pb2.NewSessionVerification - feed_entry_notification: _background_feed_pb2.FeedEntryNotification - is_error: bool - def __init__(self, type: _Optional[_Union[Notification.NotificationType, str]] = ..., associated_id: _Optional[str] = ..., none: _Optional[_Union[_empty_pb2.Empty, _Mapping]] = ..., new_session_verification: _Optional[_Union[_client_session_pb2.NewSessionVerification, _Mapping]] = ..., feed_entry_notification: _Optional[_Union[_background_feed_pb2.FeedEntryNotification, _Mapping]] = ..., is_error: bool = ...) -> None: ... diff --git a/truffle/os/notification_pb2_grpc.py b/truffle/os/notification_pb2_grpc.py deleted file mode 100644 index 504a3bc..0000000 --- a/truffle/os/notification_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/notification_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/proactivity_pb2.py b/truffle/os/proactivity_pb2.py deleted file mode 100644 index b1c9d3b..0000000 --- a/truffle/os/proactivity_pb2.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/proactivity.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/proactivity.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/os/proactivity.proto\x12\ntruffle.os\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe9\x04\n\x0fProactiveAction\x12\r\n\x05title\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\x12:\n\nactionable\x18\x04 \x01(\x0b\x32&.truffle.os.ProactiveAction.Actionable\x12\x32\n\x06status\x18\x05 \x01(\x0e\x32\".truffle.os.ProactiveAction.Status\x12.\n\ncreated_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12.\n\nupdated_at\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x11\n\tapp_uuids\x18\x08 \x03(\t\x12\x1b\n\x13prompt_for_subagent\x18\t \x01(\t\x1a\x9c\x01\n\nActionable\x12J\n\x0c\x62oolean_text\x18\x01 \x01(\x0b\x32\x32.truffle.os.ProactiveAction.Actionable.BooleanTextH\x00\x1a:\n\x0b\x42ooleanText\x12\x0f\n\x07\x61pprove\x18\x01 \x01(\x08\x12\x11\n\x04text\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x07\n\x05_textB\x06\n\x04type\"\x92\x01\n\x06Status\x12\x18\n\x14\x41\x43TION_STATE_INVALID\x10\x00\x12\x18\n\x14\x41\x43TION_STATE_PENDING\x10\x01\x12\x1c\n\x18\x41\x43TION_STATE_IN_PROGRESS\x10\x02\x12\x1a\n\x16\x41\x43TION_STATE_CANCELLED\x10\x03\x12\x1a\n\x16\x41\x43TION_STATE_COMPLETED\x10\x04\"n\n\x1d\x41pproveProactiveActionRequest\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\x04\x12;\n\x0buser_action\x18\x02 \x01(\x0b\x32&.truffle.os.ProactiveAction.Actionable\"U\n\x1e\x41pproveProactiveActionResponse\x12\x33\n\x0eupdated_action\x18\x01 \x01(\x0b\x32\x1b.truffle.os.ProactiveAction\"0\n\x1c\x43\x61ncelProactiveActionRequest\x12\x10\n\x08\x65ntry_id\x18\x01 \x01(\x04\"\x1f\n\x1d\x43\x61ncelProactiveActionResponseb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.proactivity_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_PROACTIVEACTION']._serialized_start=78 - _globals['_PROACTIVEACTION']._serialized_end=695 - _globals['_PROACTIVEACTION_ACTIONABLE']._serialized_start=390 - _globals['_PROACTIVEACTION_ACTIONABLE']._serialized_end=546 - _globals['_PROACTIVEACTION_ACTIONABLE_BOOLEANTEXT']._serialized_start=480 - _globals['_PROACTIVEACTION_ACTIONABLE_BOOLEANTEXT']._serialized_end=538 - _globals['_PROACTIVEACTION_STATUS']._serialized_start=549 - _globals['_PROACTIVEACTION_STATUS']._serialized_end=695 - _globals['_APPROVEPROACTIVEACTIONREQUEST']._serialized_start=697 - _globals['_APPROVEPROACTIVEACTIONREQUEST']._serialized_end=807 - _globals['_APPROVEPROACTIVEACTIONRESPONSE']._serialized_start=809 - _globals['_APPROVEPROACTIVEACTIONRESPONSE']._serialized_end=894 - _globals['_CANCELPROACTIVEACTIONREQUEST']._serialized_start=896 - _globals['_CANCELPROACTIVEACTIONREQUEST']._serialized_end=944 - _globals['_CANCELPROACTIVEACTIONRESPONSE']._serialized_start=946 - _globals['_CANCELPROACTIVEACTIONRESPONSE']._serialized_end=977 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/proactivity_pb2.pyi b/truffle/os/proactivity_pb2.pyi deleted file mode 100644 index c2e952e..0000000 --- a/truffle/os/proactivity_pb2.pyi +++ /dev/null @@ -1,79 +0,0 @@ -import datetime - -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class ProactiveAction(_message.Message): - __slots__ = ("title", "description", "actionable", "status", "created_at", "updated_at", "app_uuids", "prompt_for_subagent") - class Status(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - ACTION_STATE_INVALID: _ClassVar[ProactiveAction.Status] - ACTION_STATE_PENDING: _ClassVar[ProactiveAction.Status] - ACTION_STATE_IN_PROGRESS: _ClassVar[ProactiveAction.Status] - ACTION_STATE_CANCELLED: _ClassVar[ProactiveAction.Status] - ACTION_STATE_COMPLETED: _ClassVar[ProactiveAction.Status] - ACTION_STATE_INVALID: ProactiveAction.Status - ACTION_STATE_PENDING: ProactiveAction.Status - ACTION_STATE_IN_PROGRESS: ProactiveAction.Status - ACTION_STATE_CANCELLED: ProactiveAction.Status - ACTION_STATE_COMPLETED: ProactiveAction.Status - class Actionable(_message.Message): - __slots__ = ("boolean_text",) - class BooleanText(_message.Message): - __slots__ = ("approve", "text") - APPROVE_FIELD_NUMBER: _ClassVar[int] - TEXT_FIELD_NUMBER: _ClassVar[int] - approve: bool - text: str - def __init__(self, approve: bool = ..., text: _Optional[str] = ...) -> None: ... - BOOLEAN_TEXT_FIELD_NUMBER: _ClassVar[int] - boolean_text: ProactiveAction.Actionable.BooleanText - def __init__(self, boolean_text: _Optional[_Union[ProactiveAction.Actionable.BooleanText, _Mapping]] = ...) -> None: ... - TITLE_FIELD_NUMBER: _ClassVar[int] - DESCRIPTION_FIELD_NUMBER: _ClassVar[int] - ACTIONABLE_FIELD_NUMBER: _ClassVar[int] - STATUS_FIELD_NUMBER: _ClassVar[int] - CREATED_AT_FIELD_NUMBER: _ClassVar[int] - UPDATED_AT_FIELD_NUMBER: _ClassVar[int] - APP_UUIDS_FIELD_NUMBER: _ClassVar[int] - PROMPT_FOR_SUBAGENT_FIELD_NUMBER: _ClassVar[int] - title: str - description: str - actionable: ProactiveAction.Actionable - status: ProactiveAction.Status - created_at: _timestamp_pb2.Timestamp - updated_at: _timestamp_pb2.Timestamp - app_uuids: _containers.RepeatedScalarFieldContainer[str] - prompt_for_subagent: str - def __init__(self, title: _Optional[str] = ..., description: _Optional[str] = ..., actionable: _Optional[_Union[ProactiveAction.Actionable, _Mapping]] = ..., status: _Optional[_Union[ProactiveAction.Status, str]] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., updated_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., app_uuids: _Optional[_Iterable[str]] = ..., prompt_for_subagent: _Optional[str] = ...) -> None: ... - -class ApproveProactiveActionRequest(_message.Message): - __slots__ = ("entry_id", "user_action") - ENTRY_ID_FIELD_NUMBER: _ClassVar[int] - USER_ACTION_FIELD_NUMBER: _ClassVar[int] - entry_id: int - user_action: ProactiveAction.Actionable - def __init__(self, entry_id: _Optional[int] = ..., user_action: _Optional[_Union[ProactiveAction.Actionable, _Mapping]] = ...) -> None: ... - -class ApproveProactiveActionResponse(_message.Message): - __slots__ = ("updated_action",) - UPDATED_ACTION_FIELD_NUMBER: _ClassVar[int] - updated_action: ProactiveAction - def __init__(self, updated_action: _Optional[_Union[ProactiveAction, _Mapping]] = ...) -> None: ... - -class CancelProactiveActionRequest(_message.Message): - __slots__ = ("entry_id",) - ENTRY_ID_FIELD_NUMBER: _ClassVar[int] - entry_id: int - def __init__(self, entry_id: _Optional[int] = ...) -> None: ... - -class CancelProactiveActionResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... diff --git a/truffle/os/proactivity_pb2_grpc.py b/truffle/os/proactivity_pb2_grpc.py deleted file mode 100644 index ebb049d..0000000 --- a/truffle/os/proactivity_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/proactivity_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/system_info_pb2.py b/truffle/os/system_info_pb2.py deleted file mode 100644 index 7a91170..0000000 --- a/truffle/os/system_info_pb2.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/system_info.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/system_info.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -from truffle.os import hardware_info_pb2 as truffle_dot_os_dot_hardware__info__pb2 - -from truffle.os.hardware_info_pb2 import * - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/os/system_info.proto\x12\ntruffle.os\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1etruffle/os/hardware_info.proto\"\"\n\x0f\x46irmwareVersion\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x88\x02\n\nSystemInfo\x12\x37\n\x0bsystem_type\x18\x01 \x01(\x0e\x32\".truffle.os.SystemInfo.TruffleType\x12\x35\n\x10\x66irmware_version\x18\x02 \x01(\x0b\x32\x1b.truffle.os.FirmwareVersion\x12\x34\n\rhardware_info\x18\x05 \x01(\x0b\x32\x18.truffle.os.HardwareInfoH\x00\x88\x01\x01\"B\n\x0bTruffleType\x12\x18\n\x14TRUFFLE_TYPE_INVALID\x10\x00\x12\x19\n\x15TRUFFLE_TYPE_HARDWARE\x10\x01\x42\x10\n\x0e_hardware_info\"\x1d\n\x1bSystemCheckForUpdateRequest\"\x1e\n\x1cSystemCheckForUpdateResponse\"\x14\n\x12SystemGetIDRequest\"@\n\x13SystemGetIDResponse\x12\x12\n\ntruffle_id\x18\x01 \x01(\t\x12\x15\n\rserial_number\x18\x02 \x01(\tP\x01\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.system_info_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_FIRMWAREVERSION']._serialized_start=109 - _globals['_FIRMWAREVERSION']._serialized_end=143 - _globals['_SYSTEMINFO']._serialized_start=146 - _globals['_SYSTEMINFO']._serialized_end=410 - _globals['_SYSTEMINFO_TRUFFLETYPE']._serialized_start=326 - _globals['_SYSTEMINFO_TRUFFLETYPE']._serialized_end=392 - _globals['_SYSTEMCHECKFORUPDATEREQUEST']._serialized_start=412 - _globals['_SYSTEMCHECKFORUPDATEREQUEST']._serialized_end=441 - _globals['_SYSTEMCHECKFORUPDATERESPONSE']._serialized_start=443 - _globals['_SYSTEMCHECKFORUPDATERESPONSE']._serialized_end=473 - _globals['_SYSTEMGETIDREQUEST']._serialized_start=475 - _globals['_SYSTEMGETIDREQUEST']._serialized_end=495 - _globals['_SYSTEMGETIDRESPONSE']._serialized_start=497 - _globals['_SYSTEMGETIDRESPONSE']._serialized_end=561 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/system_info_pb2.pyi b/truffle/os/system_info_pb2.pyi deleted file mode 100644 index c27de8b..0000000 --- a/truffle/os/system_info_pb2.pyi +++ /dev/null @@ -1,52 +0,0 @@ -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from truffle.os import hardware_info_pb2 as _hardware_info_pb2 -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union -from truffle.os.hardware_info_pb2 import HardwareInfo as HardwareInfo - -DESCRIPTOR: _descriptor.FileDescriptor - -class FirmwareVersion(_message.Message): - __slots__ = ("version",) - VERSION_FIELD_NUMBER: _ClassVar[int] - version: str - def __init__(self, version: _Optional[str] = ...) -> None: ... - -class SystemInfo(_message.Message): - __slots__ = ("system_type", "firmware_version", "hardware_info") - class TruffleType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - TRUFFLE_TYPE_INVALID: _ClassVar[SystemInfo.TruffleType] - TRUFFLE_TYPE_HARDWARE: _ClassVar[SystemInfo.TruffleType] - TRUFFLE_TYPE_INVALID: SystemInfo.TruffleType - TRUFFLE_TYPE_HARDWARE: SystemInfo.TruffleType - SYSTEM_TYPE_FIELD_NUMBER: _ClassVar[int] - FIRMWARE_VERSION_FIELD_NUMBER: _ClassVar[int] - HARDWARE_INFO_FIELD_NUMBER: _ClassVar[int] - system_type: SystemInfo.TruffleType - firmware_version: FirmwareVersion - hardware_info: _hardware_info_pb2.HardwareInfo - def __init__(self, system_type: _Optional[_Union[SystemInfo.TruffleType, str]] = ..., firmware_version: _Optional[_Union[FirmwareVersion, _Mapping]] = ..., hardware_info: _Optional[_Union[_hardware_info_pb2.HardwareInfo, _Mapping]] = ...) -> None: ... - -class SystemCheckForUpdateRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class SystemCheckForUpdateResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class SystemGetIDRequest(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class SystemGetIDResponse(_message.Message): - __slots__ = ("truffle_id", "serial_number") - TRUFFLE_ID_FIELD_NUMBER: _ClassVar[int] - SERIAL_NUMBER_FIELD_NUMBER: _ClassVar[int] - truffle_id: str - serial_number: str - def __init__(self, truffle_id: _Optional[str] = ..., serial_number: _Optional[str] = ...) -> None: ... diff --git a/truffle/os/system_info_pb2_grpc.py b/truffle/os/system_info_pb2_grpc.py deleted file mode 100644 index 1ff7692..0000000 --- a/truffle/os/system_info_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/system_info_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/system_settings_pb2.py b/truffle/os/system_settings_pb2.py deleted file mode 100644 index 2f372f5..0000000 --- a/truffle/os/system_settings_pb2.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/system_settings.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/system_settings.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import hardware_settings_pb2 as truffle_dot_os_dot_hardware__settings__pb2 - -from truffle.os.hardware_settings_pb2 import * - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n truffle/os/system_settings.proto\x12\ntruffle.os\x1a\"truffle/os/hardware_settings.proto\"\xac\x01\n\x0eSystemSettings\x12<\n\x11hardware_settings\x18\x01 \x01(\x0b\x32\x1c.truffle.os.HardwareSettingsH\x00\x88\x01\x01\x12\x34\n\rtask_settings\x18\x02 \x01(\x0b\x32\x18.truffle.os.TaskSettingsH\x01\x88\x01\x01\x42\x14\n\x12_hardware_settingsB\x10\n\x0e_task_settings\"\x0e\n\x0cTaskSettingsP\x00\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.system_settings_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_SYSTEMSETTINGS']._serialized_start=85 - _globals['_SYSTEMSETTINGS']._serialized_end=257 - _globals['_TASKSETTINGS']._serialized_start=259 - _globals['_TASKSETTINGS']._serialized_end=273 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/system_settings_pb2.pyi b/truffle/os/system_settings_pb2.pyi deleted file mode 100644 index 870865a..0000000 --- a/truffle/os/system_settings_pb2.pyi +++ /dev/null @@ -1,20 +0,0 @@ -from truffle.os import hardware_settings_pb2 as _hardware_settings_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union -from truffle.os.hardware_settings_pb2 import HardwareSettings as HardwareSettings - -DESCRIPTOR: _descriptor.FileDescriptor - -class SystemSettings(_message.Message): - __slots__ = ("hardware_settings", "task_settings") - HARDWARE_SETTINGS_FIELD_NUMBER: _ClassVar[int] - TASK_SETTINGS_FIELD_NUMBER: _ClassVar[int] - hardware_settings: _hardware_settings_pb2.HardwareSettings - task_settings: TaskSettings - def __init__(self, hardware_settings: _Optional[_Union[_hardware_settings_pb2.HardwareSettings, _Mapping]] = ..., task_settings: _Optional[_Union[TaskSettings, _Mapping]] = ...) -> None: ... - -class TaskSettings(_message.Message): - __slots__ = () - def __init__(self) -> None: ... diff --git a/truffle/os/system_settings_pb2_grpc.py b/truffle/os/system_settings_pb2_grpc.py deleted file mode 100644 index 45fd07e..0000000 --- a/truffle/os/system_settings_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/system_settings_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_actions_pb2.py b/truffle/os/task_actions_pb2.py deleted file mode 100644 index a78e2db..0000000 --- a/truffle/os/task_actions_pb2.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_actions.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_actions.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.common import file_pb2 as truffle_dot_common_dot_file__pb2 -from truffle.os import task_pb2 as truffle_dot_os_dot_task__pb2 -try: - truffle_dot_os_dot_task__info__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__info__pb2 -except AttributeError: - truffle_dot_os_dot_task__info__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_info_pb2 -try: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__user__response__pb2 -except AttributeError: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_user_response_pb2 -try: - truffle_dot_os_dot_task__step__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__step__pb2 -except AttributeError: - truffle_dot_os_dot_task__step__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_step_pb2 -try: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_common_dot_content__pb2 -except AttributeError: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__pb2.truffle.common.content_pb2 -from truffle.os import task_target_pb2 as truffle_dot_os_dot_task__target__pb2 -from truffle.os import task_options_pb2 as truffle_dot_os_dot_task__options__pb2 -from truffle.os import task_user_response_pb2 as truffle_dot_os_dot_task__user__response__pb2 -from truffle.common import tool_provider_pb2 as truffle_dot_common_dot_tool__provider__pb2 - -from truffle.os.task_pb2 import * -from truffle.os.task_target_pb2 import * -from truffle.os.task_options_pb2 import * -from truffle.os.task_user_response_pb2 import * -from truffle.common.tool_provider_pb2 import * - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dtruffle/os/task_actions.proto\x12\ntruffle.os\x1a\x19truffle/common/file.proto\x1a\x15truffle/os/task.proto\x1a\x1ctruffle/os/task_target.proto\x1a\x1dtruffle/os/task_options.proto\x1a#truffle/os/task_user_response.proto\x1a\"truffle/common/tool_provider.proto\">\n\x14InterruptTaskRequest\x12&\n\x06target\x18\x01 \x01(\x0b\x32\x16.truffle.os.TargetTask\"\x8d\x01\n\x07NewTask\x12-\n\x0cuser_message\x18\x01 \x01(\x0b\x32\x17.truffle.os.UserMessage\x12\x11\n\tapp_uuids\x18\x02 \x03(\t\x12@\n\x14\x66iles_to_be_uploaded\x18\x03 \x03(\x0b\x32\".truffle.common.AttachedFileIntent\"\xf7\x01\n\x0fOpenTaskRequest\x12/\n\rexisting_task\x18\x01 \x01(\x0b\x32\x16.truffle.os.TargetTaskH\x00\x12\'\n\x08new_task\x18\x02 \x01(\x0b\x32\x13.truffle.os.NewTaskH\x00\x12-\n\x07options\x18\x03 \x01(\x0b\x32\x17.truffle.os.TaskOptionsH\x01\x88\x01\x01\x12\x45\n\x17\x65xternal_tool_providers\x18\x04 \x03(\x0b\x32$.truffle.common.ExternalToolProviderB\x08\n\x06targetB\n\n\x08_options\"\x88\x01\n\x1bTaskSetAvailableAppsRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x11\n\tapp_uuids\x18\x02 \x03(\t\x12\x45\n\x17\x65xternal_tool_providers\x18\x03 \x03(\x0b\x32$.truffle.common.ExternalToolProvider\"\x1e\n\x1cTaskSetAvailableAppsResponse\"\x14\n\x12TaskActionResponse\"k\n#TaskTestExternalToolProviderRequest\x12\x44\n\x16\x65xternal_tool_provider\x18\x01 \x01(\x0b\x32$.truffle.common.ExternalToolProvider\"e\n$TaskTestExternalToolProviderResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x1a\n\rerror_message\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x10\n\x0e_error_messageP\x01P\x02P\x03P\x04P\x05\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_actions_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_INTERRUPTTASKREQUEST']._serialized_start=229 - _globals['_INTERRUPTTASKREQUEST']._serialized_end=291 - _globals['_NEWTASK']._serialized_start=294 - _globals['_NEWTASK']._serialized_end=435 - _globals['_OPENTASKREQUEST']._serialized_start=438 - _globals['_OPENTASKREQUEST']._serialized_end=685 - _globals['_TASKSETAVAILABLEAPPSREQUEST']._serialized_start=688 - _globals['_TASKSETAVAILABLEAPPSREQUEST']._serialized_end=824 - _globals['_TASKSETAVAILABLEAPPSRESPONSE']._serialized_start=826 - _globals['_TASKSETAVAILABLEAPPSRESPONSE']._serialized_end=856 - _globals['_TASKACTIONRESPONSE']._serialized_start=858 - _globals['_TASKACTIONRESPONSE']._serialized_end=878 - _globals['_TASKTESTEXTERNALTOOLPROVIDERREQUEST']._serialized_start=880 - _globals['_TASKTESTEXTERNALTOOLPROVIDERREQUEST']._serialized_end=987 - _globals['_TASKTESTEXTERNALTOOLPROVIDERRESPONSE']._serialized_start=989 - _globals['_TASKTESTEXTERNALTOOLPROVIDERRESPONSE']._serialized_end=1090 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_actions_pb2.pyi b/truffle/os/task_actions_pb2.pyi deleted file mode 100644 index 775b279..0000000 --- a/truffle/os/task_actions_pb2.pyi +++ /dev/null @@ -1,88 +0,0 @@ -from truffle.common import file_pb2 as _file_pb2 -from truffle.os import task_pb2 as _task_pb2 -from truffle.os import task_info_pb2 as _task_info_pb2 -from truffle.os import task_user_response_pb2 as _task_user_response_pb2 -from truffle.os import task_step_pb2 as _task_step_pb2 -from truffle.os import task_target_pb2 as _task_target_pb2 -from truffle.os import task_options_pb2 as _task_options_pb2 -from truffle.os import task_user_response_pb2 as _task_user_response_pb2_1 -from truffle.common import tool_provider_pb2 as _tool_provider_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union -from truffle.os.task_pb2 import Task as Task -from truffle.os.task_pb2 import TasksList as TasksList -from truffle.os.task_pb2 import TaskNode as TaskNode -from truffle.os.task_pb2 import StreamingTaskStepResult as StreamingTaskStepResult -from truffle.os.task_pb2 import TaskStreamUpdate as TaskStreamUpdate -from truffle.os.task_target_pb2 import TargetTask as TargetTask -from truffle.os.task_options_pb2 import TaskOptions as TaskOptions -from truffle.os.task_user_response_pb2 import UserMessage as UserMessage -from truffle.os.task_user_response_pb2 import PendingUserResponse as PendingUserResponse -from truffle.os.task_user_response_pb2 import RespondToTaskRequest as RespondToTaskRequest -from truffle.common.tool_provider_pb2 import ExternalToolProvider as ExternalToolProvider -from truffle.common.tool_provider_pb2 import ExternalToolProvidersError as ExternalToolProvidersError - -DESCRIPTOR: _descriptor.FileDescriptor - -class InterruptTaskRequest(_message.Message): - __slots__ = ("target",) - TARGET_FIELD_NUMBER: _ClassVar[int] - target: _task_target_pb2.TargetTask - def __init__(self, target: _Optional[_Union[_task_target_pb2.TargetTask, _Mapping]] = ...) -> None: ... - -class NewTask(_message.Message): - __slots__ = ("user_message", "app_uuids", "files_to_be_uploaded") - USER_MESSAGE_FIELD_NUMBER: _ClassVar[int] - APP_UUIDS_FIELD_NUMBER: _ClassVar[int] - FILES_TO_BE_UPLOADED_FIELD_NUMBER: _ClassVar[int] - user_message: _task_user_response_pb2_1.UserMessage - app_uuids: _containers.RepeatedScalarFieldContainer[str] - files_to_be_uploaded: _containers.RepeatedCompositeFieldContainer[_file_pb2.AttachedFileIntent] - def __init__(self, user_message: _Optional[_Union[_task_user_response_pb2_1.UserMessage, _Mapping]] = ..., app_uuids: _Optional[_Iterable[str]] = ..., files_to_be_uploaded: _Optional[_Iterable[_Union[_file_pb2.AttachedFileIntent, _Mapping]]] = ...) -> None: ... - -class OpenTaskRequest(_message.Message): - __slots__ = ("existing_task", "new_task", "options", "external_tool_providers") - EXISTING_TASK_FIELD_NUMBER: _ClassVar[int] - NEW_TASK_FIELD_NUMBER: _ClassVar[int] - OPTIONS_FIELD_NUMBER: _ClassVar[int] - EXTERNAL_TOOL_PROVIDERS_FIELD_NUMBER: _ClassVar[int] - existing_task: _task_target_pb2.TargetTask - new_task: NewTask - options: _task_options_pb2.TaskOptions - external_tool_providers: _containers.RepeatedCompositeFieldContainer[_tool_provider_pb2.ExternalToolProvider] - def __init__(self, existing_task: _Optional[_Union[_task_target_pb2.TargetTask, _Mapping]] = ..., new_task: _Optional[_Union[NewTask, _Mapping]] = ..., options: _Optional[_Union[_task_options_pb2.TaskOptions, _Mapping]] = ..., external_tool_providers: _Optional[_Iterable[_Union[_tool_provider_pb2.ExternalToolProvider, _Mapping]]] = ...) -> None: ... - -class TaskSetAvailableAppsRequest(_message.Message): - __slots__ = ("task_id", "app_uuids", "external_tool_providers") - TASK_ID_FIELD_NUMBER: _ClassVar[int] - APP_UUIDS_FIELD_NUMBER: _ClassVar[int] - EXTERNAL_TOOL_PROVIDERS_FIELD_NUMBER: _ClassVar[int] - task_id: str - app_uuids: _containers.RepeatedScalarFieldContainer[str] - external_tool_providers: _containers.RepeatedCompositeFieldContainer[_tool_provider_pb2.ExternalToolProvider] - def __init__(self, task_id: _Optional[str] = ..., app_uuids: _Optional[_Iterable[str]] = ..., external_tool_providers: _Optional[_Iterable[_Union[_tool_provider_pb2.ExternalToolProvider, _Mapping]]] = ...) -> None: ... - -class TaskSetAvailableAppsResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class TaskActionResponse(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - -class TaskTestExternalToolProviderRequest(_message.Message): - __slots__ = ("external_tool_provider",) - EXTERNAL_TOOL_PROVIDER_FIELD_NUMBER: _ClassVar[int] - external_tool_provider: _tool_provider_pb2.ExternalToolProvider - def __init__(self, external_tool_provider: _Optional[_Union[_tool_provider_pb2.ExternalToolProvider, _Mapping]] = ...) -> None: ... - -class TaskTestExternalToolProviderResponse(_message.Message): - __slots__ = ("success", "error_message") - SUCCESS_FIELD_NUMBER: _ClassVar[int] - ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int] - success: bool - error_message: str - def __init__(self, success: bool = ..., error_message: _Optional[str] = ...) -> None: ... diff --git a/truffle/os/task_actions_pb2_grpc.py b/truffle/os/task_actions_pb2_grpc.py deleted file mode 100644 index 1f22a7b..0000000 --- a/truffle/os/task_actions_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_actions_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_error_pb2.py b/truffle/os/task_error_pb2.py deleted file mode 100644 index 3a4e0d8..0000000 --- a/truffle/os/task_error_pb2.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_error.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_error.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.common import tool_provider_pb2 as truffle_dot_common_dot_tool__provider__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1btruffle/os/task_error.proto\x12\ntruffle.os\x1a\"truffle/common/tool_provider.proto\"\x87\x01\n\tTaskError\x12\x10\n\x08is_fatal\x18\x01 \x01(\x08\x12\x11\n\x07message\x18\x02 \x01(\tH\x00\x12L\n\x16\x65xternal_tool_provider\x18\x03 \x01(\x0b\x32*.truffle.common.ExternalToolProvidersErrorH\x00\x42\x07\n\x05\x65rrorb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_error_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_TASKERROR']._serialized_start=80 - _globals['_TASKERROR']._serialized_end=215 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_error_pb2.pyi b/truffle/os/task_error_pb2.pyi deleted file mode 100644 index f08099c..0000000 --- a/truffle/os/task_error_pb2.pyi +++ /dev/null @@ -1,17 +0,0 @@ -from truffle.common import tool_provider_pb2 as _tool_provider_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class TaskError(_message.Message): - __slots__ = ("is_fatal", "message", "external_tool_provider") - IS_FATAL_FIELD_NUMBER: _ClassVar[int] - MESSAGE_FIELD_NUMBER: _ClassVar[int] - EXTERNAL_TOOL_PROVIDER_FIELD_NUMBER: _ClassVar[int] - is_fatal: bool - message: str - external_tool_provider: _tool_provider_pb2.ExternalToolProvidersError - def __init__(self, is_fatal: bool = ..., message: _Optional[str] = ..., external_tool_provider: _Optional[_Union[_tool_provider_pb2.ExternalToolProvidersError, _Mapping]] = ...) -> None: ... diff --git a/truffle/os/task_error_pb2_grpc.py b/truffle/os/task_error_pb2_grpc.py deleted file mode 100644 index 2443ad2..0000000 --- a/truffle/os/task_error_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_error_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_info_pb2.py b/truffle/os/task_info_pb2.py deleted file mode 100644 index 4de7973..0000000 --- a/truffle/os/task_info_pb2.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_info.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_info.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -from truffle.os import task_options_pb2 as truffle_dot_os_dot_task__options__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1atruffle/os/task_info.proto\x12\ntruffle.os\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1dtruffle/os/task_options.proto\"\xd7\x03\n\x08TaskInfo\x12\x34\n\trun_state\x18\x01 \x01(\x0e\x32!.truffle.os.TaskInfo.TaskRunState\x12\x11\n\tapp_uuids\x18\x02 \x03(\t\x12\x17\n\ntask_title\x18\x05 \x01(\tH\x00\x88\x01\x01\x12(\n\x07options\x18\x06 \x01(\x0b\x32\x17.truffle.os.TaskOptions\x12+\n\x07\x63reated\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0clast_updated\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\naccess_uri\x18\x08 \x01(\tH\x01\x88\x01\x01\"\xa8\x01\n\x0cTaskRunState\x12\x1a\n\x16TASK_RUN_STATE_INVALID\x10\x00\x12\x1f\n\x1bTASK_RUN_STATE_CREATING_NEW\x10\x01\x12!\n\x1dTASK_RUN_STATE_RELOADING_PREV\x10\x02\x12\x18\n\x14TASK_RUN_STATE_READY\x10\x03\x12\x1e\n\x1aTASK_RUN_STATE_FATAL_ERROR\x10\x05\x42\r\n\x0b_task_titleB\r\n\x0b_access_uri*X\n\tTaskFlags\x12\x13\n\x0fTASK_FLAGS_NONE\x10\x00\x12\x0f\n\x0bTASK_ACTIVE\x10\x01\x12\x13\n\x0fTASK_NO_RESPOND\x10\x02\x12\x10\n\x0cTASK_BLOCKED\x10\x04\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_info_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_TASKFLAGS']._serialized_start=580 - _globals['_TASKFLAGS']._serialized_end=668 - _globals['_TASKINFO']._serialized_start=107 - _globals['_TASKINFO']._serialized_end=578 - _globals['_TASKINFO_TASKRUNSTATE']._serialized_start=380 - _globals['_TASKINFO_TASKRUNSTATE']._serialized_end=548 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_info_pb2.pyi b/truffle/os/task_info_pb2.pyi deleted file mode 100644 index b457df9..0000000 --- a/truffle/os/task_info_pb2.pyi +++ /dev/null @@ -1,53 +0,0 @@ -import datetime - -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from truffle.os import task_options_pb2 as _task_options_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class TaskFlags(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - TASK_FLAGS_NONE: _ClassVar[TaskFlags] - TASK_ACTIVE: _ClassVar[TaskFlags] - TASK_NO_RESPOND: _ClassVar[TaskFlags] - TASK_BLOCKED: _ClassVar[TaskFlags] -TASK_FLAGS_NONE: TaskFlags -TASK_ACTIVE: TaskFlags -TASK_NO_RESPOND: TaskFlags -TASK_BLOCKED: TaskFlags - -class TaskInfo(_message.Message): - __slots__ = ("run_state", "app_uuids", "task_title", "options", "created", "last_updated", "access_uri") - class TaskRunState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - TASK_RUN_STATE_INVALID: _ClassVar[TaskInfo.TaskRunState] - TASK_RUN_STATE_CREATING_NEW: _ClassVar[TaskInfo.TaskRunState] - TASK_RUN_STATE_RELOADING_PREV: _ClassVar[TaskInfo.TaskRunState] - TASK_RUN_STATE_READY: _ClassVar[TaskInfo.TaskRunState] - TASK_RUN_STATE_FATAL_ERROR: _ClassVar[TaskInfo.TaskRunState] - TASK_RUN_STATE_INVALID: TaskInfo.TaskRunState - TASK_RUN_STATE_CREATING_NEW: TaskInfo.TaskRunState - TASK_RUN_STATE_RELOADING_PREV: TaskInfo.TaskRunState - TASK_RUN_STATE_READY: TaskInfo.TaskRunState - TASK_RUN_STATE_FATAL_ERROR: TaskInfo.TaskRunState - RUN_STATE_FIELD_NUMBER: _ClassVar[int] - APP_UUIDS_FIELD_NUMBER: _ClassVar[int] - TASK_TITLE_FIELD_NUMBER: _ClassVar[int] - OPTIONS_FIELD_NUMBER: _ClassVar[int] - CREATED_FIELD_NUMBER: _ClassVar[int] - LAST_UPDATED_FIELD_NUMBER: _ClassVar[int] - ACCESS_URI_FIELD_NUMBER: _ClassVar[int] - run_state: TaskInfo.TaskRunState - app_uuids: _containers.RepeatedScalarFieldContainer[str] - task_title: str - options: _task_options_pb2.TaskOptions - created: _timestamp_pb2.Timestamp - last_updated: _timestamp_pb2.Timestamp - access_uri: str - def __init__(self, run_state: _Optional[_Union[TaskInfo.TaskRunState, str]] = ..., app_uuids: _Optional[_Iterable[str]] = ..., task_title: _Optional[str] = ..., options: _Optional[_Union[_task_options_pb2.TaskOptions, _Mapping]] = ..., created: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., last_updated: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., access_uri: _Optional[str] = ...) -> None: ... diff --git a/truffle/os/task_info_pb2_grpc.py b/truffle/os/task_info_pb2_grpc.py deleted file mode 100644 index 58c324e..0000000 --- a/truffle/os/task_info_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_info_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_options_pb2.py b/truffle/os/task_options_pb2.py deleted file mode 100644 index 099f5c7..0000000 --- a/truffle/os/task_options_pb2.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_options.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_options.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dtruffle/os/task_options.proto\x12\ntruffle.os\x1a\x1egoogle/protobuf/duration.proto\"\r\n\x0bTaskOptionsb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_options_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_TASKOPTIONS']._serialized_start=77 - _globals['_TASKOPTIONS']._serialized_end=90 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_options_pb2.pyi b/truffle/os/task_options_pb2.pyi deleted file mode 100644 index 765098f..0000000 --- a/truffle/os/task_options_pb2.pyi +++ /dev/null @@ -1,10 +0,0 @@ -from google.protobuf import duration_pb2 as _duration_pb2 -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar - -DESCRIPTOR: _descriptor.FileDescriptor - -class TaskOptions(_message.Message): - __slots__ = () - def __init__(self) -> None: ... diff --git a/truffle/os/task_options_pb2_grpc.py b/truffle/os/task_options_pb2_grpc.py deleted file mode 100644 index 8930d65..0000000 --- a/truffle/os/task_options_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_options_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_pb2.py b/truffle/os/task_pb2.py deleted file mode 100644 index c612b37..0000000 --- a/truffle/os/task_pb2.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -from truffle.common import file_pb2 as truffle_dot_common_dot_file__pb2 -from truffle.os import task_info_pb2 as truffle_dot_os_dot_task__info__pb2 -from truffle.os import task_user_response_pb2 as truffle_dot_os_dot_task__user__response__pb2 -from truffle.os import task_step_pb2 as truffle_dot_os_dot_task__step__pb2 -try: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__step__pb2.truffle_dot_common_dot_content__pb2 -except AttributeError: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__step__pb2.truffle.common.content_pb2 -from truffle.os import task_error_pb2 as truffle_dot_os_dot_task__error__pb2 - -from truffle.os.task_info_pb2 import * -from truffle.os.task_user_response_pb2 import * -from truffle.os.task_step_pb2 import * - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15truffle/os/task.proto\x12\ntruffle.os\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x19truffle/common/file.proto\x1a\x1atruffle/os/task_info.proto\x1a#truffle/os/task_user_response.proto\x1a\x1atruffle/os/task_step.proto\x1a\x1btruffle/os/task_error.proto\"t\n\x04Task\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\"\n\x04info\x18\x02 \x01(\x0b\x32\x14.truffle.os.TaskInfo\x12\x12\n\ntask_flags\x18\x03 \x01(\r\x12#\n\x05nodes\x18\x05 \x03(\x0b\x32\x14.truffle.os.TaskNode\",\n\tTasksList\x12\x1f\n\x05tasks\x18\x01 \x03(\x0b\x32\x10.truffle.os.Task\"\xf0\x01\n\x08TaskNode\x12\n\n\x02id\x18\x01 \x01(\r\x12\x11\n\tparent_id\x18\x02 \x01(\r\x12\x11\n\tchild_ids\x18\x03 \x03(\r\x12.\n\ncreated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12+\n\x05\x66iles\x18\x08 \x03(\x0b\x32\x1c.truffle.common.AttachedFile\x12 \n\x04step\x18\t \x01(\x0b\x32\x10.truffle.os.StepH\x00\x12+\n\x08user_msg\x18\n \x01(\x0b\x32\x17.truffle.os.UserMessageH\x00\x42\x06\n\x04item\"C\n\x17StreamingTaskStepResult\x12\x0f\n\x07node_id\x18\x01 \x01(\r\x12\x17\n\x0fpartial_content\x18\x02 \x01(\t\"\x92\x02\n\x10TaskStreamUpdate\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\'\n\x04info\x18\x03 \x01(\x0b\x32\x14.truffle.os.TaskInfoH\x00\x88\x01\x01\x12#\n\x05nodes\x18\x02 \x03(\x0b\x32\x14.truffle.os.TaskNode\x12)\n\x05\x65rror\x18\x05 \x01(\x0b\x32\x15.truffle.os.TaskErrorH\x01\x88\x01\x01\x12G\n\x15streaming_step_result\x18\x06 \x01(\x0b\x32#.truffle.os.StreamingTaskStepResultH\x02\x88\x01\x01\x42\x07\n\x05_infoB\x08\n\x06_errorB\x18\n\x16_streaming_step_resultP\x02P\x03P\x04\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_TASK']._serialized_start=219 - _globals['_TASK']._serialized_end=335 - _globals['_TASKSLIST']._serialized_start=337 - _globals['_TASKSLIST']._serialized_end=381 - _globals['_TASKNODE']._serialized_start=384 - _globals['_TASKNODE']._serialized_end=624 - _globals['_STREAMINGTASKSTEPRESULT']._serialized_start=626 - _globals['_STREAMINGTASKSTEPRESULT']._serialized_end=693 - _globals['_TASKSTREAMUPDATE']._serialized_start=696 - _globals['_TASKSTREAMUPDATE']._serialized_end=970 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_pb2.pyi b/truffle/os/task_pb2.pyi deleted file mode 100644 index 322c076..0000000 --- a/truffle/os/task_pb2.pyi +++ /dev/null @@ -1,84 +0,0 @@ -import datetime - -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from truffle.common import file_pb2 as _file_pb2 -from truffle.os import task_info_pb2 as _task_info_pb2 -from truffle.os import task_user_response_pb2 as _task_user_response_pb2 -from truffle.os import task_step_pb2 as _task_step_pb2 -from truffle.common import content_pb2 as _content_pb2 -from truffle.os import task_error_pb2 as _task_error_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union -from truffle.os.task_info_pb2 import TaskInfo as TaskInfo -from truffle.os.task_info_pb2 import TaskFlags as TaskFlags -from truffle.os.task_user_response_pb2 import UserMessage as UserMessage -from truffle.os.task_user_response_pb2 import PendingUserResponse as PendingUserResponse -from truffle.os.task_user_response_pb2 import RespondToTaskRequest as RespondToTaskRequest -from truffle.os.task_step_pb2 import Step as Step - -DESCRIPTOR: _descriptor.FileDescriptor -TASK_FLAGS_NONE: _task_info_pb2.TaskFlags -TASK_ACTIVE: _task_info_pb2.TaskFlags -TASK_NO_RESPOND: _task_info_pb2.TaskFlags -TASK_BLOCKED: _task_info_pb2.TaskFlags - -class Task(_message.Message): - __slots__ = ("task_id", "info", "task_flags", "nodes") - TASK_ID_FIELD_NUMBER: _ClassVar[int] - INFO_FIELD_NUMBER: _ClassVar[int] - TASK_FLAGS_FIELD_NUMBER: _ClassVar[int] - NODES_FIELD_NUMBER: _ClassVar[int] - task_id: str - info: _task_info_pb2.TaskInfo - task_flags: int - nodes: _containers.RepeatedCompositeFieldContainer[TaskNode] - def __init__(self, task_id: _Optional[str] = ..., info: _Optional[_Union[_task_info_pb2.TaskInfo, _Mapping]] = ..., task_flags: _Optional[int] = ..., nodes: _Optional[_Iterable[_Union[TaskNode, _Mapping]]] = ...) -> None: ... - -class TasksList(_message.Message): - __slots__ = ("tasks",) - TASKS_FIELD_NUMBER: _ClassVar[int] - tasks: _containers.RepeatedCompositeFieldContainer[Task] - def __init__(self, tasks: _Optional[_Iterable[_Union[Task, _Mapping]]] = ...) -> None: ... - -class TaskNode(_message.Message): - __slots__ = ("id", "parent_id", "child_ids", "created_at", "files", "step", "user_msg") - ID_FIELD_NUMBER: _ClassVar[int] - PARENT_ID_FIELD_NUMBER: _ClassVar[int] - CHILD_IDS_FIELD_NUMBER: _ClassVar[int] - CREATED_AT_FIELD_NUMBER: _ClassVar[int] - FILES_FIELD_NUMBER: _ClassVar[int] - STEP_FIELD_NUMBER: _ClassVar[int] - USER_MSG_FIELD_NUMBER: _ClassVar[int] - id: int - parent_id: int - child_ids: _containers.RepeatedScalarFieldContainer[int] - created_at: _timestamp_pb2.Timestamp - files: _containers.RepeatedCompositeFieldContainer[_file_pb2.AttachedFile] - step: _task_step_pb2.Step - user_msg: _task_user_response_pb2.UserMessage - def __init__(self, id: _Optional[int] = ..., parent_id: _Optional[int] = ..., child_ids: _Optional[_Iterable[int]] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., files: _Optional[_Iterable[_Union[_file_pb2.AttachedFile, _Mapping]]] = ..., step: _Optional[_Union[_task_step_pb2.Step, _Mapping]] = ..., user_msg: _Optional[_Union[_task_user_response_pb2.UserMessage, _Mapping]] = ...) -> None: ... - -class StreamingTaskStepResult(_message.Message): - __slots__ = ("node_id", "partial_content") - NODE_ID_FIELD_NUMBER: _ClassVar[int] - PARTIAL_CONTENT_FIELD_NUMBER: _ClassVar[int] - node_id: int - partial_content: str - def __init__(self, node_id: _Optional[int] = ..., partial_content: _Optional[str] = ...) -> None: ... - -class TaskStreamUpdate(_message.Message): - __slots__ = ("task_id", "info", "nodes", "error", "streaming_step_result") - TASK_ID_FIELD_NUMBER: _ClassVar[int] - INFO_FIELD_NUMBER: _ClassVar[int] - NODES_FIELD_NUMBER: _ClassVar[int] - ERROR_FIELD_NUMBER: _ClassVar[int] - STREAMING_STEP_RESULT_FIELD_NUMBER: _ClassVar[int] - task_id: str - info: _task_info_pb2.TaskInfo - nodes: _containers.RepeatedCompositeFieldContainer[TaskNode] - error: _task_error_pb2.TaskError - streaming_step_result: StreamingTaskStepResult - def __init__(self, task_id: _Optional[str] = ..., info: _Optional[_Union[_task_info_pb2.TaskInfo, _Mapping]] = ..., nodes: _Optional[_Iterable[_Union[TaskNode, _Mapping]]] = ..., error: _Optional[_Union[_task_error_pb2.TaskError, _Mapping]] = ..., streaming_step_result: _Optional[_Union[StreamingTaskStepResult, _Mapping]] = ...) -> None: ... diff --git a/truffle/os/task_pb2_grpc.py b/truffle/os/task_pb2_grpc.py deleted file mode 100644 index 61b5ff6..0000000 --- a/truffle/os/task_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_queries_pb2.py b/truffle/os/task_queries_pb2.py deleted file mode 100644 index 8a6eb9e..0000000 --- a/truffle/os/task_queries_pb2.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_queries.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_queries.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import task_info_pb2 as truffle_dot_os_dot_task__info__pb2 -from truffle.os import task_pb2 as truffle_dot_os_dot_task__pb2 -try: - truffle_dot_os_dot_task__info__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__info__pb2 -except AttributeError: - truffle_dot_os_dot_task__info__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_info_pb2 -try: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__user__response__pb2 -except AttributeError: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_user_response_pb2 -try: - truffle_dot_os_dot_task__step__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__step__pb2 -except AttributeError: - truffle_dot_os_dot_task__step__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_step_pb2 -try: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_common_dot_content__pb2 -except AttributeError: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__pb2.truffle.common.content_pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1dtruffle/os/task_queries.proto\x12\ntruffle.os\x1a\x1atruffle/os/task_info.proto\x1a\x15truffle/os/task.proto\"7\n\x0fGetTasksRequest\x12\x10\n\x08task_ids\x18\x01 \x03(\t\x12\x12\n\nwith_nodes\x18\x03 \x01(\x08\"8\n\x11GetOneTaskRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x12\n\nwith_nodes\x18\x03 \x01(\x08\"T\n\x13GetTaskInfosRequest\x12\x16\n\x0etarget_task_id\x18\x01 \x01(\t\x12\x12\n\nmax_before\x18\x02 \x01(\x05\x12\x11\n\tmax_after\x18\x03 \x01(\x05\"\xee\x01\n\x14GetTaskInfosResponse\x12=\n\x07\x65ntries\x18\x01 \x03(\x0b\x32,.truffle.os.GetTaskInfosResponse.TaskPreview\x12\x17\n\x0ftotal_num_tasks\x18\x02 \x01(\r\x1a~\n\x0bTaskPreview\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\"\n\x04info\x18\x02 \x01(\x0b\x32\x14.truffle.os.TaskInfo\x12,\n\tlast_node\x18\x03 \x01(\x0b\x32\x14.truffle.os.TaskNodeH\x00\x88\x01\x01\x42\x0c\n\n_last_nodeb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_queries_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_GETTASKSREQUEST']._serialized_start=96 - _globals['_GETTASKSREQUEST']._serialized_end=151 - _globals['_GETONETASKREQUEST']._serialized_start=153 - _globals['_GETONETASKREQUEST']._serialized_end=209 - _globals['_GETTASKINFOSREQUEST']._serialized_start=211 - _globals['_GETTASKINFOSREQUEST']._serialized_end=295 - _globals['_GETTASKINFOSRESPONSE']._serialized_start=298 - _globals['_GETTASKINFOSRESPONSE']._serialized_end=536 - _globals['_GETTASKINFOSRESPONSE_TASKPREVIEW']._serialized_start=410 - _globals['_GETTASKINFOSRESPONSE_TASKPREVIEW']._serialized_end=536 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_queries_pb2.pyi b/truffle/os/task_queries_pb2.pyi deleted file mode 100644 index 5957d64..0000000 --- a/truffle/os/task_queries_pb2.pyi +++ /dev/null @@ -1,55 +0,0 @@ -from truffle.os import task_info_pb2 as _task_info_pb2 -from truffle.os import task_pb2 as _task_pb2 -from truffle.os import task_info_pb2 as _task_info_pb2_1 -from truffle.os import task_user_response_pb2 as _task_user_response_pb2 -from truffle.os import task_step_pb2 as _task_step_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class GetTasksRequest(_message.Message): - __slots__ = ("task_ids", "with_nodes") - TASK_IDS_FIELD_NUMBER: _ClassVar[int] - WITH_NODES_FIELD_NUMBER: _ClassVar[int] - task_ids: _containers.RepeatedScalarFieldContainer[str] - with_nodes: bool - def __init__(self, task_ids: _Optional[_Iterable[str]] = ..., with_nodes: bool = ...) -> None: ... - -class GetOneTaskRequest(_message.Message): - __slots__ = ("task_id", "with_nodes") - TASK_ID_FIELD_NUMBER: _ClassVar[int] - WITH_NODES_FIELD_NUMBER: _ClassVar[int] - task_id: str - with_nodes: bool - def __init__(self, task_id: _Optional[str] = ..., with_nodes: bool = ...) -> None: ... - -class GetTaskInfosRequest(_message.Message): - __slots__ = ("target_task_id", "max_before", "max_after") - TARGET_TASK_ID_FIELD_NUMBER: _ClassVar[int] - MAX_BEFORE_FIELD_NUMBER: _ClassVar[int] - MAX_AFTER_FIELD_NUMBER: _ClassVar[int] - target_task_id: str - max_before: int - max_after: int - def __init__(self, target_task_id: _Optional[str] = ..., max_before: _Optional[int] = ..., max_after: _Optional[int] = ...) -> None: ... - -class GetTaskInfosResponse(_message.Message): - __slots__ = ("entries", "total_num_tasks") - class TaskPreview(_message.Message): - __slots__ = ("task_id", "info", "last_node") - TASK_ID_FIELD_NUMBER: _ClassVar[int] - INFO_FIELD_NUMBER: _ClassVar[int] - LAST_NODE_FIELD_NUMBER: _ClassVar[int] - task_id: str - info: _task_info_pb2_1.TaskInfo - last_node: _task_pb2.TaskNode - def __init__(self, task_id: _Optional[str] = ..., info: _Optional[_Union[_task_info_pb2_1.TaskInfo, _Mapping]] = ..., last_node: _Optional[_Union[_task_pb2.TaskNode, _Mapping]] = ...) -> None: ... - ENTRIES_FIELD_NUMBER: _ClassVar[int] - TOTAL_NUM_TASKS_FIELD_NUMBER: _ClassVar[int] - entries: _containers.RepeatedCompositeFieldContainer[GetTaskInfosResponse.TaskPreview] - total_num_tasks: int - def __init__(self, entries: _Optional[_Iterable[_Union[GetTaskInfosResponse.TaskPreview, _Mapping]]] = ..., total_num_tasks: _Optional[int] = ...) -> None: ... diff --git a/truffle/os/task_queries_pb2_grpc.py b/truffle/os/task_queries_pb2_grpc.py deleted file mode 100644 index 2d4e661..0000000 --- a/truffle/os/task_queries_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_queries_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_search_pb2.py b/truffle/os/task_search_pb2.py deleted file mode 100644 index 5af5440..0000000 --- a/truffle/os/task_search_pb2.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_search.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_search.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -from truffle.os import task_info_pb2 as truffle_dot_os_dot_task__info__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/os/task_search.proto\x12\ntruffle.os\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1atruffle/os/task_info.proto\"H\n\x12SearchTasksRequest\x12\r\n\x05query\x18\x01 \x01(\t\x12\x13\n\x0bmax_results\x18\x02 \x01(\x05\x12\x0e\n\x06offset\x18\x03 \x01(\x05\"\xe2\x01\n\x10TaskSearchResult\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\'\n\ttask_info\x18\x02 \x01(\x0b\x32\x14.truffle.os.TaskInfo\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12?\n\x07\x63ontent\x18\x04 \x01(\x0b\x32..truffle.os.TaskSearchResult.TaskSearchContent\x1a$\n\x11TaskSearchContent\x12\x0f\n\x07snippet\x18\x01 \x01(\t\"s\n\x13SearchTasksResponse\x12\x15\n\rtotal_results\x18\x01 \x01(\x05\x12\x16\n\x0e\x63urrent_offset\x18\x02 \x01(\x05\x12-\n\x07results\x18\x03 \x03(\x0b\x32\x1c.truffle.os.TaskSearchResultb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_search_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_SEARCHTASKSREQUEST']._serialized_start=105 - _globals['_SEARCHTASKSREQUEST']._serialized_end=177 - _globals['_TASKSEARCHRESULT']._serialized_start=180 - _globals['_TASKSEARCHRESULT']._serialized_end=406 - _globals['_TASKSEARCHRESULT_TASKSEARCHCONTENT']._serialized_start=370 - _globals['_TASKSEARCHRESULT_TASKSEARCHCONTENT']._serialized_end=406 - _globals['_SEARCHTASKSRESPONSE']._serialized_start=408 - _globals['_SEARCHTASKSRESPONSE']._serialized_end=523 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_search_pb2.pyi b/truffle/os/task_search_pb2.pyi deleted file mode 100644 index 4c64b2f..0000000 --- a/truffle/os/task_search_pb2.pyi +++ /dev/null @@ -1,48 +0,0 @@ -import datetime - -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from truffle.os import task_info_pb2 as _task_info_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class SearchTasksRequest(_message.Message): - __slots__ = ("query", "max_results", "offset") - QUERY_FIELD_NUMBER: _ClassVar[int] - MAX_RESULTS_FIELD_NUMBER: _ClassVar[int] - OFFSET_FIELD_NUMBER: _ClassVar[int] - query: str - max_results: int - offset: int - def __init__(self, query: _Optional[str] = ..., max_results: _Optional[int] = ..., offset: _Optional[int] = ...) -> None: ... - -class TaskSearchResult(_message.Message): - __slots__ = ("task_id", "task_info", "timestamp", "content") - class TaskSearchContent(_message.Message): - __slots__ = ("snippet",) - SNIPPET_FIELD_NUMBER: _ClassVar[int] - snippet: str - def __init__(self, snippet: _Optional[str] = ...) -> None: ... - TASK_ID_FIELD_NUMBER: _ClassVar[int] - TASK_INFO_FIELD_NUMBER: _ClassVar[int] - TIMESTAMP_FIELD_NUMBER: _ClassVar[int] - CONTENT_FIELD_NUMBER: _ClassVar[int] - task_id: str - task_info: _task_info_pb2.TaskInfo - timestamp: _timestamp_pb2.Timestamp - content: TaskSearchResult.TaskSearchContent - def __init__(self, task_id: _Optional[str] = ..., task_info: _Optional[_Union[_task_info_pb2.TaskInfo, _Mapping]] = ..., timestamp: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., content: _Optional[_Union[TaskSearchResult.TaskSearchContent, _Mapping]] = ...) -> None: ... - -class SearchTasksResponse(_message.Message): - __slots__ = ("total_results", "current_offset", "results") - TOTAL_RESULTS_FIELD_NUMBER: _ClassVar[int] - CURRENT_OFFSET_FIELD_NUMBER: _ClassVar[int] - RESULTS_FIELD_NUMBER: _ClassVar[int] - total_results: int - current_offset: int - results: _containers.RepeatedCompositeFieldContainer[TaskSearchResult] - def __init__(self, total_results: _Optional[int] = ..., current_offset: _Optional[int] = ..., results: _Optional[_Iterable[_Union[TaskSearchResult, _Mapping]]] = ...) -> None: ... diff --git a/truffle/os/task_search_pb2_grpc.py b/truffle/os/task_search_pb2_grpc.py deleted file mode 100644 index 15dc29f..0000000 --- a/truffle/os/task_search_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_search_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_step_pb2.py b/truffle/os/task_step_pb2.py deleted file mode 100644 index b1cf1da..0000000 --- a/truffle/os/task_step_pb2.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_step.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_step.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import task_user_response_pb2 as truffle_dot_os_dot_task__user__response__pb2 -from truffle.common import content_pb2 as truffle_dot_common_dot_content__pb2 -from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 - -from truffle.common.content_pb2 import * - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1atruffle/os/task_step.proto\x12\ntruffle.os\x1a#truffle/os/task_user_response.proto\x1a\x1ctruffle/common/content.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc5\x05\n\x04Step\x12.\n\x05state\x18\x01 \x01(\x0e\x32\x1a.truffle.os.Step.StepStateH\x00\x88\x01\x01\x12;\n\ruser_response\x18\n \x01(\x0b\x32\x1f.truffle.os.PendingUserResponseH\x01\x88\x01\x01\x12+\n\x08thinking\x18\x02 \x01(\x0b\x32\x19.truffle.os.Step.Thinking\x12-\n\ntool_calls\x18\x03 \x03(\x0b\x32\x19.truffle.os.Step.ToolCall\x12+\n\texecution\x18\x04 \x01(\x0b\x32\x18.truffle.os.Step.Execute\x12)\n\x07results\x18\x05 \x01(\x0b\x32\x18.truffle.os.Step.Results\x12\x17\n\nmodel_uuid\x18\x07 \x01(\tH\x02\x88\x01\x01\x1a\x35\n\x08Thinking\x12\x12\n\ncot_chunks\x18\x01 \x03(\t\x12\x15\n\rcot_summaries\x18\x02 \x03(\t\x1an\n\x08ToolCall\x12\x16\n\ttool_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07summary\x18\x02 \x01(\tH\x01\x88\x01\x01\x12\x11\n\x04\x61rgs\x18\x03 \x01(\tH\x02\x88\x01\x01\x42\x0c\n\n_tool_nameB\n\n\x08_summaryB\x07\n\x05_args\x1a\t\n\x07\x45xecute\x1aM\n\x07Results\x12\x14\n\x07summary\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07\x63ontent\x18\x02 \x01(\tH\x01\x88\x01\x01\x42\n\n\x08_summaryB\n\n\x08_content\"W\n\tStepState\x12\x10\n\x0cSTEP_INVALID\x10\x00\x12\x13\n\x0fSTEP_GENERATING\x10\x01\x12\x12\n\x0eSTEP_EXECUTING\x10\x02\x12\x0f\n\x0bSTEP_RESULT\x10\x03\x42\x08\n\x06_stateB\x10\n\x0e_user_responseB\r\n\x0b_model_uuidP\x01\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_step_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_STEP']._serialized_start=143 - _globals['_STEP']._serialized_end=852 - _globals['_STEP_THINKING']._serialized_start=465 - _globals['_STEP_THINKING']._serialized_end=518 - _globals['_STEP_TOOLCALL']._serialized_start=520 - _globals['_STEP_TOOLCALL']._serialized_end=630 - _globals['_STEP_EXECUTE']._serialized_start=632 - _globals['_STEP_EXECUTE']._serialized_end=641 - _globals['_STEP_RESULTS']._serialized_start=643 - _globals['_STEP_RESULTS']._serialized_end=720 - _globals['_STEP_STEPSTATE']._serialized_start=722 - _globals['_STEP_STEPSTATE']._serialized_end=809 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_step_pb2.pyi b/truffle/os/task_step_pb2.pyi deleted file mode 100644 index 9f4c8da..0000000 --- a/truffle/os/task_step_pb2.pyi +++ /dev/null @@ -1,67 +0,0 @@ -from truffle.os import task_user_response_pb2 as _task_user_response_pb2 -from truffle.common import content_pb2 as _content_pb2 -from google.protobuf import timestamp_pb2 as _timestamp_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union -from truffle.common.content_pb2 import MediaSource as MediaSource -from truffle.common.content_pb2 import WebComponent as WebComponent - -DESCRIPTOR: _descriptor.FileDescriptor - -class Step(_message.Message): - __slots__ = ("state", "user_response", "thinking", "tool_calls", "execution", "results", "model_uuid") - class StepState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): - __slots__ = () - STEP_INVALID: _ClassVar[Step.StepState] - STEP_GENERATING: _ClassVar[Step.StepState] - STEP_EXECUTING: _ClassVar[Step.StepState] - STEP_RESULT: _ClassVar[Step.StepState] - STEP_INVALID: Step.StepState - STEP_GENERATING: Step.StepState - STEP_EXECUTING: Step.StepState - STEP_RESULT: Step.StepState - class Thinking(_message.Message): - __slots__ = ("cot_chunks", "cot_summaries") - COT_CHUNKS_FIELD_NUMBER: _ClassVar[int] - COT_SUMMARIES_FIELD_NUMBER: _ClassVar[int] - cot_chunks: _containers.RepeatedScalarFieldContainer[str] - cot_summaries: _containers.RepeatedScalarFieldContainer[str] - def __init__(self, cot_chunks: _Optional[_Iterable[str]] = ..., cot_summaries: _Optional[_Iterable[str]] = ...) -> None: ... - class ToolCall(_message.Message): - __slots__ = ("tool_name", "summary", "args") - TOOL_NAME_FIELD_NUMBER: _ClassVar[int] - SUMMARY_FIELD_NUMBER: _ClassVar[int] - ARGS_FIELD_NUMBER: _ClassVar[int] - tool_name: str - summary: str - args: str - def __init__(self, tool_name: _Optional[str] = ..., summary: _Optional[str] = ..., args: _Optional[str] = ...) -> None: ... - class Execute(_message.Message): - __slots__ = () - def __init__(self) -> None: ... - class Results(_message.Message): - __slots__ = ("summary", "content") - SUMMARY_FIELD_NUMBER: _ClassVar[int] - CONTENT_FIELD_NUMBER: _ClassVar[int] - summary: str - content: str - def __init__(self, summary: _Optional[str] = ..., content: _Optional[str] = ...) -> None: ... - STATE_FIELD_NUMBER: _ClassVar[int] - USER_RESPONSE_FIELD_NUMBER: _ClassVar[int] - THINKING_FIELD_NUMBER: _ClassVar[int] - TOOL_CALLS_FIELD_NUMBER: _ClassVar[int] - EXECUTION_FIELD_NUMBER: _ClassVar[int] - RESULTS_FIELD_NUMBER: _ClassVar[int] - MODEL_UUID_FIELD_NUMBER: _ClassVar[int] - state: Step.StepState - user_response: _task_user_response_pb2.PendingUserResponse - thinking: Step.Thinking - tool_calls: _containers.RepeatedCompositeFieldContainer[Step.ToolCall] - execution: Step.Execute - results: Step.Results - model_uuid: str - def __init__(self, state: _Optional[_Union[Step.StepState, str]] = ..., user_response: _Optional[_Union[_task_user_response_pb2.PendingUserResponse, _Mapping]] = ..., thinking: _Optional[_Union[Step.Thinking, _Mapping]] = ..., tool_calls: _Optional[_Iterable[_Union[Step.ToolCall, _Mapping]]] = ..., execution: _Optional[_Union[Step.Execute, _Mapping]] = ..., results: _Optional[_Union[Step.Results, _Mapping]] = ..., model_uuid: _Optional[str] = ...) -> None: ... diff --git a/truffle/os/task_step_pb2_grpc.py b/truffle/os/task_step_pb2_grpc.py deleted file mode 100644 index 65fe0d4..0000000 --- a/truffle/os/task_step_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_step_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_target_pb2.py b/truffle/os/task_target_pb2.py deleted file mode 100644 index 2d85791..0000000 --- a/truffle/os/task_target_pb2.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_target.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_target.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1ctruffle/os/task_target.proto\x12\ntruffle.os\"?\n\nTargetTask\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x14\n\x07node_id\x18\x02 \x01(\rH\x00\x88\x01\x01\x42\n\n\x08_node_idb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_target_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_TARGETTASK']._serialized_start=44 - _globals['_TARGETTASK']._serialized_end=107 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_target_pb2.pyi b/truffle/os/task_target_pb2.pyi deleted file mode 100644 index ee8bd4d..0000000 --- a/truffle/os/task_target_pb2.pyi +++ /dev/null @@ -1,13 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Optional as _Optional - -DESCRIPTOR: _descriptor.FileDescriptor - -class TargetTask(_message.Message): - __slots__ = ("task_id", "node_id") - TASK_ID_FIELD_NUMBER: _ClassVar[int] - NODE_ID_FIELD_NUMBER: _ClassVar[int] - task_id: str - node_id: int - def __init__(self, task_id: _Optional[str] = ..., node_id: _Optional[int] = ...) -> None: ... diff --git a/truffle/os/task_target_pb2_grpc.py b/truffle/os/task_target_pb2_grpc.py deleted file mode 100644 index daa929f..0000000 --- a/truffle/os/task_target_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_target_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/task_user_response_pb2.py b/truffle/os/task_user_response_pb2.py deleted file mode 100644 index 238fc45..0000000 --- a/truffle/os/task_user_response_pb2.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/task_user_response.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/task_user_response.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from truffle.os import task_target_pb2 as truffle_dot_os_dot_task__target__pb2 -from truffle.common import file_pb2 as truffle_dot_common_dot_file__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#truffle/os/task_user_response.proto\x12\ntruffle.os\x1a\x1ctruffle/os/task_target.proto\x1a\x19truffle/common/file.proto\"?\n\x0bUserMessage\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x1f\n\x17\x61ttached_feed_entry_ids\x18\x02 \x03(\x04\"7\n\x13PendingUserResponse\x12\x0f\n\x07task_id\x18\x02 \x01(\t\x12\x0f\n\x07node_id\x18\x03 \x01(\r\"\x8f\x01\n\x14RespondToTaskRequest\x12\x0f\n\x07task_id\x18\x02 \x01(\t\x12\x0f\n\x07node_id\x18\x03 \x01(\r\x12(\n\x07message\x18\x04 \x01(\x0b\x32\x17.truffle.os.UserMessage\x12+\n\x05\x66iles\x18\x05 \x03(\x0b\x32\x1c.truffle.common.AttachedFileb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.task_user_response_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_USERMESSAGE']._serialized_start=108 - _globals['_USERMESSAGE']._serialized_end=171 - _globals['_PENDINGUSERRESPONSE']._serialized_start=173 - _globals['_PENDINGUSERRESPONSE']._serialized_end=228 - _globals['_RESPONDTOTASKREQUEST']._serialized_start=231 - _globals['_RESPONDTOTASKREQUEST']._serialized_end=374 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/task_user_response_pb2.pyi b/truffle/os/task_user_response_pb2.pyi deleted file mode 100644 index 3dbe4eb..0000000 --- a/truffle/os/task_user_response_pb2.pyi +++ /dev/null @@ -1,37 +0,0 @@ -from truffle.os import task_target_pb2 as _task_target_pb2 -from truffle.common import file_pb2 as _file_pb2 -from google.protobuf.internal import containers as _containers -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class UserMessage(_message.Message): - __slots__ = ("content", "attached_feed_entry_ids") - CONTENT_FIELD_NUMBER: _ClassVar[int] - ATTACHED_FEED_ENTRY_IDS_FIELD_NUMBER: _ClassVar[int] - content: str - attached_feed_entry_ids: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, content: _Optional[str] = ..., attached_feed_entry_ids: _Optional[_Iterable[int]] = ...) -> None: ... - -class PendingUserResponse(_message.Message): - __slots__ = ("task_id", "node_id") - TASK_ID_FIELD_NUMBER: _ClassVar[int] - NODE_ID_FIELD_NUMBER: _ClassVar[int] - task_id: str - node_id: int - def __init__(self, task_id: _Optional[str] = ..., node_id: _Optional[int] = ...) -> None: ... - -class RespondToTaskRequest(_message.Message): - __slots__ = ("task_id", "node_id", "message", "files") - TASK_ID_FIELD_NUMBER: _ClassVar[int] - NODE_ID_FIELD_NUMBER: _ClassVar[int] - MESSAGE_FIELD_NUMBER: _ClassVar[int] - FILES_FIELD_NUMBER: _ClassVar[int] - task_id: str - node_id: int - message: UserMessage - files: _containers.RepeatedCompositeFieldContainer[_file_pb2.AttachedFile] - def __init__(self, task_id: _Optional[str] = ..., node_id: _Optional[int] = ..., message: _Optional[_Union[UserMessage, _Mapping]] = ..., files: _Optional[_Iterable[_Union[_file_pb2.AttachedFile, _Mapping]]] = ...) -> None: ... diff --git a/truffle/os/task_user_response_pb2_grpc.py b/truffle/os/task_user_response_pb2_grpc.py deleted file mode 100644 index 5df8dd7..0000000 --- a/truffle/os/task_user_response_pb2_grpc.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/task_user_response_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) diff --git a/truffle/os/truffleos_pb2.py b/truffle/os/truffleos_pb2.py deleted file mode 100644 index 5d300c4..0000000 --- a/truffle/os/truffleos_pb2.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: truffle/os/truffleos.proto -# Protobuf Python Version: 6.31.1 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 31, - 1, - '', - 'truffle/os/truffleos.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -from truffle.os import hardware_stats_pb2 as truffle_dot_os_dot_hardware__stats__pb2 -from truffle.os import hardware_control_pb2 as truffle_dot_os_dot_hardware__control__pb2 -from truffle.os import system_settings_pb2 as truffle_dot_os_dot_system__settings__pb2 -try: - truffle_dot_os_dot_hardware__settings__pb2 = truffle_dot_os_dot_system__settings__pb2.truffle_dot_os_dot_hardware__settings__pb2 -except AttributeError: - truffle_dot_os_dot_hardware__settings__pb2 = truffle_dot_os_dot_system__settings__pb2.truffle.os.hardware_settings_pb2 -from truffle.os import system_info_pb2 as truffle_dot_os_dot_system__info__pb2 -try: - truffle_dot_os_dot_hardware__info__pb2 = truffle_dot_os_dot_system__info__pb2.truffle_dot_os_dot_hardware__info__pb2 -except AttributeError: - truffle_dot_os_dot_hardware__info__pb2 = truffle_dot_os_dot_system__info__pb2.truffle.os.hardware_info_pb2 -from truffle.os import notification_pb2 as truffle_dot_os_dot_notification__pb2 -from truffle.os import client_session_pb2 as truffle_dot_os_dot_client__session__pb2 -from truffle.os import client_user_pb2 as truffle_dot_os_dot_client__user__pb2 -from truffle.os import client_state_pb2 as truffle_dot_os_dot_client__state__pb2 -from truffle.os import task_pb2 as truffle_dot_os_dot_task__pb2 -try: - truffle_dot_os_dot_task__info__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__info__pb2 -except AttributeError: - truffle_dot_os_dot_task__info__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_info_pb2 -try: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__user__response__pb2 -except AttributeError: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_user_response_pb2 -try: - truffle_dot_os_dot_task__step__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_os_dot_task__step__pb2 -except AttributeError: - truffle_dot_os_dot_task__step__pb2 = truffle_dot_os_dot_task__pb2.truffle.os.task_step_pb2 -try: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__pb2.truffle_dot_common_dot_content__pb2 -except AttributeError: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__pb2.truffle.common.content_pb2 -from truffle.os import task_queries_pb2 as truffle_dot_os_dot_task__queries__pb2 -from truffle.os import task_actions_pb2 as truffle_dot_os_dot_task__actions__pb2 -try: - truffle_dot_os_dot_task__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_os_dot_task__pb2 -except AttributeError: - truffle_dot_os_dot_task__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.os.task_pb2 -try: - truffle_dot_os_dot_task__info__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_os_dot_task__info__pb2 -except AttributeError: - truffle_dot_os_dot_task__info__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.os.task_info_pb2 -try: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_os_dot_task__user__response__pb2 -except AttributeError: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.os.task_user_response_pb2 -try: - truffle_dot_os_dot_task__step__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_os_dot_task__step__pb2 -except AttributeError: - truffle_dot_os_dot_task__step__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.os.task_step_pb2 -try: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_common_dot_content__pb2 -except AttributeError: - truffle_dot_common_dot_content__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.common.content_pb2 -try: - truffle_dot_os_dot_task__target__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_os_dot_task__target__pb2 -except AttributeError: - truffle_dot_os_dot_task__target__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.os.task_target_pb2 -try: - truffle_dot_os_dot_task__options__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_os_dot_task__options__pb2 -except AttributeError: - truffle_dot_os_dot_task__options__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.os.task_options_pb2 -try: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_os_dot_task__user__response__pb2 -except AttributeError: - truffle_dot_os_dot_task__user__response__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.os.task_user_response_pb2 -try: - truffle_dot_common_dot_tool__provider__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle_dot_common_dot_tool__provider__pb2 -except AttributeError: - truffle_dot_common_dot_tool__provider__pb2 = truffle_dot_os_dot_task__actions__pb2.truffle.common.tool_provider_pb2 -from truffle.os import task_user_response_pb2 as truffle_dot_os_dot_task__user__response__pb2 -from truffle.os import task_search_pb2 as truffle_dot_os_dot_task__search__pb2 -from truffle.os import builder_pb2 as truffle_dot_os_dot_builder__pb2 -from truffle.os import background_feed_queries_pb2 as truffle_dot_os_dot_background__feed__queries__pb2 -from truffle.os import proactivity_pb2 as truffle_dot_os_dot_proactivity__pb2 -from truffle.os import app_queries_pb2 as truffle_dot_os_dot_app__queries__pb2 -from truffle.os import installer_pb2 as truffle_dot_os_dot_installer__pb2 -try: - truffle_dot_app_dot_app__pb2 = truffle_dot_os_dot_installer__pb2.truffle_dot_app_dot_app__pb2 -except AttributeError: - truffle_dot_app_dot_app__pb2 = truffle_dot_os_dot_installer__pb2.truffle.app.app_pb2 -from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 - -from truffle.os.hardware_stats_pb2 import * -from truffle.os.hardware_control_pb2 import * -from truffle.os.system_settings_pb2 import * -from truffle.os.system_info_pb2 import * -from truffle.os.notification_pb2 import * -from truffle.os.client_session_pb2 import * -from truffle.os.client_user_pb2 import * -from truffle.os.client_state_pb2 import * -from truffle.os.task_pb2 import * -from truffle.os.task_queries_pb2 import * -from truffle.os.task_actions_pb2 import * -from truffle.os.task_user_response_pb2 import * -from truffle.os.task_search_pb2 import * -from truffle.os.builder_pb2 import * -from truffle.os.background_feed_queries_pb2 import * -from truffle.os.proactivity_pb2 import * -from truffle.os.app_queries_pb2 import * - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1atruffle/os/truffleos.proto\x12\ntruffle.os\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1ftruffle/os/hardware_stats.proto\x1a!truffle/os/hardware_control.proto\x1a truffle/os/system_settings.proto\x1a\x1ctruffle/os/system_info.proto\x1a\x1dtruffle/os/notification.proto\x1a\x1ftruffle/os/client_session.proto\x1a\x1ctruffle/os/client_user.proto\x1a\x1dtruffle/os/client_state.proto\x1a\x15truffle/os/task.proto\x1a\x1dtruffle/os/task_queries.proto\x1a\x1dtruffle/os/task_actions.proto\x1a#truffle/os/task_user_response.proto\x1a\x1ctruffle/os/task_search.proto\x1a\x18truffle/os/builder.proto\x1a(truffle/os/background_feed_queries.proto\x1a\x1ctruffle/os/proactivity.proto\x1a\x1ctruffle/os/app_queries.proto\x1a\x1atruffle/os/installer.proto\x1a\x1cgoogle/api/annotations.proto2\xfd#\n\tTruffleOS\x12m\n\x0e\x41pps_DeleteApp\x12\x1c.truffle.os.DeleteAppRequest\x1a\x1d.truffle.os.DeleteAppResponse\"\x1e\x82\xd3\xe4\x93\x02\x18*\x16/v1/os/apps/{app_uuid}\x12\x65\n\x0b\x41pps_GetAll\x12\x1d.truffle.os.GetAllAppsRequest\x1a\x1e.truffle.os.GetAllAppsResponse\"\x17\x82\xd3\xe4\x93\x02\x11\x12\x0f/v1/os/apps/all\x12T\n\x0f\x41pps_InstallApp\x12\x1d.truffle.os.AppInstallRequest\x1a\x1e.truffle.os.AppInstallResponse(\x01\x30\x01\x12\x8d\x01\n\x12\x42\x61\x63kground_GetFeed\x12$.truffle.os.GetBackgroundFeedRequest\x1a%.truffle.os.GetBackgroundFeedResponse\"*\x82\xd3\xe4\x93\x02$\"\x1f/v1/os/apps/background/feed:get:\x01*\x12\xa8\x01\n\x1f\x42\x61\x63kground_GetLatestFeedEntryID\x12\'.truffle.os.GetLatestFeedEntryIDRequest\x1a(.truffle.os.GetLatestFeedEntryIDResponse\"2\x82\xd3\xe4\x93\x02,\"\'/v1/os/apps/background/latestfeedid:get:\x01*\x12\xab\x01\n!Background_ApproveProactiveAction\x12).truffle.os.ApproveProactiveActionRequest\x1a*.truffle.os.ApproveProactiveActionResponse\"/\x82\xd3\xe4\x93\x02)\"$/v1/os/apps/background/approveAction:\x01*\x12\xa7\x01\n Background_CancelProactiveAction\x12(.truffle.os.CancelProactiveActionRequest\x1a).truffle.os.CancelProactiveActionResponse\".\x82\xd3\xe4\x93\x02(\"#/v1/os/apps/background/cancelAction:\x01*\x12\x90\x01\n\x19\x42uilder_StartBuildSession\x12$.truffle.os.StartBuildSessionRequest\x1a%.truffle.os.StartBuildSessionResponse\"&\x82\xd3\xe4\x93\x02 \"\x1b/v1/os/builder/builds:start:\x01*\x12\x94\x01\n\x1a\x42uilder_FinishBuildSession\x12%.truffle.os.FinishBuildSessionRequest\x1a&.truffle.os.FinishBuildSessionResponse\"\'\x82\xd3\xe4\x93\x02!\"\x1c/v1/os/builder/builds:finish:\x01*\x12\x83\x01\n\x16\x43lient_RegisterNewUser\x12\".truffle.os.RegisterNewUserRequest\x1a#.truffle.os.RegisterNewUserResponse\" \x82\xd3\xe4\x93\x02\x1a\"\x15/v1/os/users:register:\x01*\x12\x8f\x01\n\x19\x43lient_RegisterNewSession\x12%.truffle.os.RegisterNewSessionRequest\x1a&.truffle.os.RegisterNewSessionResponse\"#\x82\xd3\xe4\x93\x02\x1d\"\x18/v1/os/sessions:register:\x01*\x12\x82\x01\n\x15\x43lient_UserIDForToken\x12!.truffle.os.UserIDForTokenRequest\x1a\".truffle.os.UserIDForTokenResponse\"\"\x82\xd3\xe4\x93\x02\x1c\x12\x1a/v1/os/tokens/{token}/user\x12\x8b\x01\n#Client_VerifyNewSessionRegistration\x12#.truffle.os.VerifyNewSessionRequest\x1a\x1c.truffle.os.NewSessionStatus\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/os/sessions:verify:\x01*\x12x\n\x1b\x43lient_GetUserRecoveryCodes\x12\x16.google.protobuf.Empty\x1a\x1d.truffle.os.UserRecoveryCodes\"\"\x82\xd3\xe4\x93\x02\x1c\x12\x1a/v1/os/users/recoveryCodes\x12\x87\x01\n\x18\x43lient_UpdateClientState\x12$.truffle.os.UpdateClientStateRequest\x1a%.truffle.os.UpdateClientStateResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/os/client/state:\x01*\x12\x81\x01\n\x15\x43lient_GetClientState\x12!.truffle.os.GetClientStateRequest\x1a\".truffle.os.GetClientStateResponse\"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/v1/os/client/state/{key}\x12\x88\x01\n\x19\x43lient_GetAllClientStates\x12%.truffle.os.GetAllClientStatesRequest\x1a&.truffle.os.GetAllClientStatesResponse\"\x1c\x82\xd3\xe4\x93\x02\x16\x12\x14/v1/os/client/states\x12\x8b\x01\n\x18SubscribeToNotifications\x12+.truffle.os.SubscribeToNotificationsRequest\x1a\x18.truffle.os.Notification\"&\x82\xd3\xe4\x93\x02 \x12\x1e/v1/os/notifications:subscribe0\x01\x12o\n\x11Hardware_GetStats\x12 .truffle.os.HardwareStatsRequest\x1a\x19.truffle.os.HardwareStats\"\x1d\x82\xd3\xe4\x93\x02\x17\x12\x15/v1/os/hardware/stats\x12|\n\x15Hardware_StatsUpdates\x12 .truffle.os.HardwareStatsRequest\x1a\x19.truffle.os.HardwareStats\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/os/hardware/stats:stream0\x01\x12\x93\x01\n\x15Hardware_PowerControl\x12\'.truffle.os.HardwarePowerControlRequest\x1a(.truffle.os.HardwarePowerControlResponse\"\'\x82\xd3\xe4\x93\x02!\"\x1c/v1/os/hardware/powerControl:\x01*\x12i\n\x0cSystem_GetID\x12\x1e.truffle.os.SystemGetIDRequest\x1a\x1f.truffle.os.SystemGetIDResponse\"\x18\x82\xd3\xe4\x93\x02\x12\x12\x10/v1/os/system/id\x12h\n\x12System_GetSettings\x12\x16.google.protobuf.Empty\x1a\x1a.truffle.os.SystemSettings\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/v1/os/system/settings\x12o\n\x12System_SetSettings\x12\x1a.truffle.os.SystemSettings\x1a\x1a.truffle.os.SystemSettings\"!\x82\xd3\xe4\x93\x02\x1b\x32\x16/v1/os/system/settings:\x01*\x12\\\n\x0eSystem_GetInfo\x12\x16.google.protobuf.Empty\x1a\x16.truffle.os.SystemInfo\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/os/system/info\x12\x8e\x01\n\x15System_CheckForUpdate\x12\'.truffle.os.SystemCheckForUpdateRequest\x1a(.truffle.os.SystemCheckForUpdateResponse\"\"\x82\xd3\xe4\x93\x02\x1c\x12\x1a/v1/os/system/check-update\x12\xb4\x01\n\x1dTask_TestExternalToolProvider\x12/.truffle.os.TaskTestExternalToolProviderRequest\x1a\x30.truffle.os.TaskTestExternalToolProviderResponse\"0\x82\xd3\xe4\x93\x02*\"%/v1/os/tasks:testExternalToolProvider:\x01*\x12j\n\rTask_OpenTask\x12\x1b.truffle.os.OpenTaskRequest\x1a\x1c.truffle.os.TaskStreamUpdate\"\x1c\x82\xd3\xe4\x93\x02\x16\"\x11/v1/os/tasks:open:\x01*0\x01\x12\x83\x01\n\x12Task_InterruptTask\x12 .truffle.os.InterruptTaskRequest\x1a\x1e.truffle.os.TaskActionResponse\"+\x82\xd3\xe4\x93\x02%\" /v1/os/tasks/{task_id}:interrupt:\x01*\x12\x81\x01\n\x12Task_RespondToTask\x12 .truffle.os.RespondToTaskRequest\x1a\x1e.truffle.os.TaskActionResponse\")\x82\xd3\xe4\x93\x02#\"\x1e/v1/os/tasks/{task_id}:respond:\x01*\x12\x9e\x01\n\x15Task_SetAvailableApps\x12\'.truffle.os.TaskSetAvailableAppsRequest\x1a(.truffle.os.TaskSetAvailableAppsResponse\"2\x82\xd3\xe4\x93\x02,\"\'/v1/os/tasks/{task_id}:setAvailableApps:\x01*\x12s\n\x10Task_SearchTasks\x12\x1e.truffle.os.SearchTasksRequest\x1a\x1f.truffle.os.SearchTasksResponse\"\x1e\x82\xd3\xe4\x93\x02\x18\"\x13/v1/os/tasks:search:\x01*\x12\x63\n\rTask_GetTasks\x12\x1b.truffle.os.GetTasksRequest\x1a\x10.truffle.os.Task\"!\x82\xd3\xe4\x93\x02\x1b\"\x16/v1/os/tasks:streamGet:\x01*0\x01\x12\x62\n\x0fTask_GetOneTask\x12\x1d.truffle.os.GetOneTaskRequest\x1a\x10.truffle.os.Task\"\x1e\x82\xd3\xe4\x93\x02\x18\x12\x16/v1/os/tasks/{task_id}\x12u\n\x11Task_GetTaskInfos\x12\x1f.truffle.os.GetTaskInfosRequest\x1a .truffle.os.GetTaskInfosResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x12/v1/os/tasks/infos:\x01*P\x01P\x02P\x03P\x04P\x05P\x06P\x07P\x08P\tP\nP\x0bP\x0cP\rP\x0eP\x0fP\x10P\x11\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'truffle.os.truffleos_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Apps_DeleteApp']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Apps_DeleteApp']._serialized_options = b'\202\323\344\223\002\030*\026/v1/os/apps/{app_uuid}' - _globals['_TRUFFLEOS'].methods_by_name['Apps_GetAll']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Apps_GetAll']._serialized_options = b'\202\323\344\223\002\021\022\017/v1/os/apps/all' - _globals['_TRUFFLEOS'].methods_by_name['Background_GetFeed']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Background_GetFeed']._serialized_options = b'\202\323\344\223\002$\"\037/v1/os/apps/background/feed:get:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Background_GetLatestFeedEntryID']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Background_GetLatestFeedEntryID']._serialized_options = b'\202\323\344\223\002,\"\'/v1/os/apps/background/latestfeedid:get:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Background_ApproveProactiveAction']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Background_ApproveProactiveAction']._serialized_options = b'\202\323\344\223\002)\"$/v1/os/apps/background/approveAction:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Background_CancelProactiveAction']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Background_CancelProactiveAction']._serialized_options = b'\202\323\344\223\002(\"#/v1/os/apps/background/cancelAction:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Builder_StartBuildSession']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Builder_StartBuildSession']._serialized_options = b'\202\323\344\223\002 \"\033/v1/os/builder/builds:start:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Builder_FinishBuildSession']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Builder_FinishBuildSession']._serialized_options = b'\202\323\344\223\002!\"\034/v1/os/builder/builds:finish:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Client_RegisterNewUser']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Client_RegisterNewUser']._serialized_options = b'\202\323\344\223\002\032\"\025/v1/os/users:register:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Client_RegisterNewSession']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Client_RegisterNewSession']._serialized_options = b'\202\323\344\223\002\035\"\030/v1/os/sessions:register:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Client_UserIDForToken']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Client_UserIDForToken']._serialized_options = b'\202\323\344\223\002\034\022\032/v1/os/tokens/{token}/user' - _globals['_TRUFFLEOS'].methods_by_name['Client_VerifyNewSessionRegistration']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Client_VerifyNewSessionRegistration']._serialized_options = b'\202\323\344\223\002\033\"\026/v1/os/sessions:verify:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Client_GetUserRecoveryCodes']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Client_GetUserRecoveryCodes']._serialized_options = b'\202\323\344\223\002\034\022\032/v1/os/users/recoveryCodes' - _globals['_TRUFFLEOS'].methods_by_name['Client_UpdateClientState']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Client_UpdateClientState']._serialized_options = b'\202\323\344\223\002\030\"\023/v1/os/client/state:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Client_GetClientState']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Client_GetClientState']._serialized_options = b'\202\323\344\223\002\033\022\031/v1/os/client/state/{key}' - _globals['_TRUFFLEOS'].methods_by_name['Client_GetAllClientStates']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Client_GetAllClientStates']._serialized_options = b'\202\323\344\223\002\026\022\024/v1/os/client/states' - _globals['_TRUFFLEOS'].methods_by_name['SubscribeToNotifications']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['SubscribeToNotifications']._serialized_options = b'\202\323\344\223\002 \022\036/v1/os/notifications:subscribe' - _globals['_TRUFFLEOS'].methods_by_name['Hardware_GetStats']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Hardware_GetStats']._serialized_options = b'\202\323\344\223\002\027\022\025/v1/os/hardware/stats' - _globals['_TRUFFLEOS'].methods_by_name['Hardware_StatsUpdates']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Hardware_StatsUpdates']._serialized_options = b'\202\323\344\223\002\036\022\034/v1/os/hardware/stats:stream' - _globals['_TRUFFLEOS'].methods_by_name['Hardware_PowerControl']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Hardware_PowerControl']._serialized_options = b'\202\323\344\223\002!\"\034/v1/os/hardware/powerControl:\001*' - _globals['_TRUFFLEOS'].methods_by_name['System_GetID']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['System_GetID']._serialized_options = b'\202\323\344\223\002\022\022\020/v1/os/system/id' - _globals['_TRUFFLEOS'].methods_by_name['System_GetSettings']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['System_GetSettings']._serialized_options = b'\202\323\344\223\002\030\022\026/v1/os/system/settings' - _globals['_TRUFFLEOS'].methods_by_name['System_SetSettings']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['System_SetSettings']._serialized_options = b'\202\323\344\223\002\0332\026/v1/os/system/settings:\001*' - _globals['_TRUFFLEOS'].methods_by_name['System_GetInfo']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['System_GetInfo']._serialized_options = b'\202\323\344\223\002\024\022\022/v1/os/system/info' - _globals['_TRUFFLEOS'].methods_by_name['System_CheckForUpdate']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['System_CheckForUpdate']._serialized_options = b'\202\323\344\223\002\034\022\032/v1/os/system/check-update' - _globals['_TRUFFLEOS'].methods_by_name['Task_TestExternalToolProvider']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_TestExternalToolProvider']._serialized_options = b'\202\323\344\223\002*\"%/v1/os/tasks:testExternalToolProvider:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Task_OpenTask']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_OpenTask']._serialized_options = b'\202\323\344\223\002\026\"\021/v1/os/tasks:open:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Task_InterruptTask']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_InterruptTask']._serialized_options = b'\202\323\344\223\002%\" /v1/os/tasks/{task_id}:interrupt:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Task_RespondToTask']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_RespondToTask']._serialized_options = b'\202\323\344\223\002#\"\036/v1/os/tasks/{task_id}:respond:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Task_SetAvailableApps']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_SetAvailableApps']._serialized_options = b'\202\323\344\223\002,\"\'/v1/os/tasks/{task_id}:setAvailableApps:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Task_SearchTasks']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_SearchTasks']._serialized_options = b'\202\323\344\223\002\030\"\023/v1/os/tasks:search:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Task_GetTasks']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_GetTasks']._serialized_options = b'\202\323\344\223\002\033\"\026/v1/os/tasks:streamGet:\001*' - _globals['_TRUFFLEOS'].methods_by_name['Task_GetOneTask']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_GetOneTask']._serialized_options = b'\202\323\344\223\002\030\022\026/v1/os/tasks/{task_id}' - _globals['_TRUFFLEOS'].methods_by_name['Task_GetTaskInfos']._loaded_options = None - _globals['_TRUFFLEOS'].methods_by_name['Task_GetTaskInfos']._serialized_options = b'\202\323\344\223\002\027\"\022/v1/os/tasks/infos:\001*' - _globals['_TRUFFLEOS']._serialized_start=667 - _globals['_TRUFFLEOS']._serialized_end=5272 -# @@protoc_insertion_point(module_scope) diff --git a/truffle/os/truffleos_pb2.pyi b/truffle/os/truffleos_pb2.pyi deleted file mode 100644 index b3e4a9a..0000000 --- a/truffle/os/truffleos_pb2.pyi +++ /dev/null @@ -1,107 +0,0 @@ -from google.protobuf import empty_pb2 as _empty_pb2 -from truffle.os import hardware_stats_pb2 as _hardware_stats_pb2 -from truffle.os import hardware_control_pb2 as _hardware_control_pb2 -from truffle.os import system_settings_pb2 as _system_settings_pb2 -from truffle.os import hardware_settings_pb2 as _hardware_settings_pb2 -from truffle.os import system_info_pb2 as _system_info_pb2 -from truffle.os import hardware_info_pb2 as _hardware_info_pb2 -from truffle.os import notification_pb2 as _notification_pb2 -from truffle.os import client_session_pb2 as _client_session_pb2 -from truffle.os import client_user_pb2 as _client_user_pb2 -from truffle.os import client_state_pb2 as _client_state_pb2 -from truffle.os import task_pb2 as _task_pb2 -from truffle.os import task_info_pb2 as _task_info_pb2 -from truffle.os import task_user_response_pb2 as _task_user_response_pb2 -from truffle.os import task_step_pb2 as _task_step_pb2 -from truffle.os import task_queries_pb2 as _task_queries_pb2 -from truffle.os import task_actions_pb2 as _task_actions_pb2 -from truffle.os import task_pb2 as _task_pb2_1 -from truffle.os import task_target_pb2 as _task_target_pb2 -from truffle.os import task_options_pb2 as _task_options_pb2 -from truffle.os import task_user_response_pb2 as _task_user_response_pb2_1 -from truffle.common import tool_provider_pb2 as _tool_provider_pb2 -from truffle.os import task_user_response_pb2 as _task_user_response_pb2_1_1 -from truffle.os import task_search_pb2 as _task_search_pb2 -from truffle.os import builder_pb2 as _builder_pb2 -from truffle.os import background_feed_queries_pb2 as _background_feed_queries_pb2 -from truffle.os import proactivity_pb2 as _proactivity_pb2 -from truffle.os import app_queries_pb2 as _app_queries_pb2 -from truffle.os import installer_pb2 as _installer_pb2 -from truffle.app import app_pb2 as _app_pb2 -from google.api import annotations_pb2 as _annotations_pb2 -from google.protobuf import descriptor as _descriptor -from typing import ClassVar as _ClassVar -from truffle.os.hardware_stats_pb2 import HardwareStats as HardwareStats -from truffle.os.hardware_stats_pb2 import HardwareStatsRequest as HardwareStatsRequest -from truffle.os.hardware_control_pb2 import HardwarePowerControlRequest as HardwarePowerControlRequest -from truffle.os.hardware_control_pb2 import HardwarePowerControlResponse as HardwarePowerControlResponse -from truffle.os.system_settings_pb2 import SystemSettings as SystemSettings -from truffle.os.system_settings_pb2 import TaskSettings as TaskSettings -from truffle.os.system_info_pb2 import FirmwareVersion as FirmwareVersion -from truffle.os.system_info_pb2 import SystemInfo as SystemInfo -from truffle.os.system_info_pb2 import SystemCheckForUpdateRequest as SystemCheckForUpdateRequest -from truffle.os.system_info_pb2 import SystemCheckForUpdateResponse as SystemCheckForUpdateResponse -from truffle.os.system_info_pb2 import SystemGetIDRequest as SystemGetIDRequest -from truffle.os.system_info_pb2 import SystemGetIDResponse as SystemGetIDResponse -from truffle.os.notification_pb2 import SubscribeToNotificationsRequest as SubscribeToNotificationsRequest -from truffle.os.notification_pb2 import Notification as Notification -from truffle.os.client_session_pb2 import UserRecoveryCodes as UserRecoveryCodes -from truffle.os.client_session_pb2 import RegisterNewSessionRequest as RegisterNewSessionRequest -from truffle.os.client_session_pb2 import RegisterNewSessionResponse as RegisterNewSessionResponse -from truffle.os.client_session_pb2 import NewSessionVerification as NewSessionVerification -from truffle.os.client_session_pb2 import VerifyNewSessionRequest as VerifyNewSessionRequest -from truffle.os.client_session_pb2 import NewSessionStatus as NewSessionStatus -from truffle.os.client_user_pb2 import RegisterNewUserRequest as RegisterNewUserRequest -from truffle.os.client_user_pb2 import RegisterNewUserResponse as RegisterNewUserResponse -from truffle.os.client_user_pb2 import UserIDForTokenRequest as UserIDForTokenRequest -from truffle.os.client_user_pb2 import UserIDForTokenResponse as UserIDForTokenResponse -from truffle.os.client_state_pb2 import ClientState as ClientState -from truffle.os.client_state_pb2 import UpdateClientStateRequest as UpdateClientStateRequest -from truffle.os.client_state_pb2 import UpdateClientStateResponse as UpdateClientStateResponse -from truffle.os.client_state_pb2 import GetClientStateRequest as GetClientStateRequest -from truffle.os.client_state_pb2 import GetClientStateResponse as GetClientStateResponse -from truffle.os.client_state_pb2 import GetAllClientStatesRequest as GetAllClientStatesRequest -from truffle.os.client_state_pb2 import GetAllClientStatesResponse as GetAllClientStatesResponse -from truffle.os.task_pb2 import Task as Task -from truffle.os.task_pb2 import TasksList as TasksList -from truffle.os.task_pb2 import TaskNode as TaskNode -from truffle.os.task_pb2 import StreamingTaskStepResult as StreamingTaskStepResult -from truffle.os.task_pb2 import TaskStreamUpdate as TaskStreamUpdate -from truffle.os.task_queries_pb2 import GetTasksRequest as GetTasksRequest -from truffle.os.task_queries_pb2 import GetOneTaskRequest as GetOneTaskRequest -from truffle.os.task_queries_pb2 import GetTaskInfosRequest as GetTaskInfosRequest -from truffle.os.task_queries_pb2 import GetTaskInfosResponse as GetTaskInfosResponse -from truffle.os.task_actions_pb2 import InterruptTaskRequest as InterruptTaskRequest -from truffle.os.task_actions_pb2 import NewTask as NewTask -from truffle.os.task_actions_pb2 import OpenTaskRequest as OpenTaskRequest -from truffle.os.task_actions_pb2 import TaskSetAvailableAppsRequest as TaskSetAvailableAppsRequest -from truffle.os.task_actions_pb2 import TaskSetAvailableAppsResponse as TaskSetAvailableAppsResponse -from truffle.os.task_actions_pb2 import TaskActionResponse as TaskActionResponse -from truffle.os.task_actions_pb2 import TaskTestExternalToolProviderRequest as TaskTestExternalToolProviderRequest -from truffle.os.task_actions_pb2 import TaskTestExternalToolProviderResponse as TaskTestExternalToolProviderResponse -from truffle.os.task_user_response_pb2 import UserMessage as UserMessage -from truffle.os.task_user_response_pb2 import PendingUserResponse as PendingUserResponse -from truffle.os.task_user_response_pb2 import RespondToTaskRequest as RespondToTaskRequest -from truffle.os.task_search_pb2 import SearchTasksRequest as SearchTasksRequest -from truffle.os.task_search_pb2 import TaskSearchResult as TaskSearchResult -from truffle.os.task_search_pb2 import SearchTasksResponse as SearchTasksResponse -from truffle.os.builder_pb2 import StartBuildSessionRequest as StartBuildSessionRequest -from truffle.os.builder_pb2 import StartBuildSessionResponse as StartBuildSessionResponse -from truffle.os.builder_pb2 import FinishBuildSessionRequest as FinishBuildSessionRequest -from truffle.os.builder_pb2 import BuildSessionError as BuildSessionError -from truffle.os.builder_pb2 import FinishBuildSessionResponse as FinishBuildSessionResponse -from truffle.os.background_feed_queries_pb2 import GetBackgroundFeedRequest as GetBackgroundFeedRequest -from truffle.os.background_feed_queries_pb2 import GetBackgroundFeedResponse as GetBackgroundFeedResponse -from truffle.os.background_feed_queries_pb2 import GetLatestFeedEntryIDRequest as GetLatestFeedEntryIDRequest -from truffle.os.background_feed_queries_pb2 import GetLatestFeedEntryIDResponse as GetLatestFeedEntryIDResponse -from truffle.os.proactivity_pb2 import ProactiveAction as ProactiveAction -from truffle.os.proactivity_pb2 import ApproveProactiveActionRequest as ApproveProactiveActionRequest -from truffle.os.proactivity_pb2 import ApproveProactiveActionResponse as ApproveProactiveActionResponse -from truffle.os.proactivity_pb2 import CancelProactiveActionRequest as CancelProactiveActionRequest -from truffle.os.proactivity_pb2 import CancelProactiveActionResponse as CancelProactiveActionResponse -from truffle.os.app_queries_pb2 import GetAllAppsRequest as GetAllAppsRequest -from truffle.os.app_queries_pb2 import GetAllAppsResponse as GetAllAppsResponse -from truffle.os.app_queries_pb2 import DeleteAppRequest as DeleteAppRequest -from truffle.os.app_queries_pb2 import DeleteAppResponse as DeleteAppResponse - -DESCRIPTOR: _descriptor.FileDescriptor diff --git a/truffle/os/truffleos_pb2_grpc.py b/truffle/os/truffleos_pb2_grpc.py deleted file mode 100644 index 86946ec..0000000 --- a/truffle/os/truffleos_pb2_grpc.py +++ /dev/null @@ -1,1591 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc -import warnings - -from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2 -from truffle.os import app_queries_pb2 as truffle_dot_os_dot_app__queries__pb2 -from truffle.os import background_feed_queries_pb2 as truffle_dot_os_dot_background__feed__queries__pb2 -from truffle.os import builder_pb2 as truffle_dot_os_dot_builder__pb2 -from truffle.os import client_session_pb2 as truffle_dot_os_dot_client__session__pb2 -from truffle.os import client_state_pb2 as truffle_dot_os_dot_client__state__pb2 -from truffle.os import client_user_pb2 as truffle_dot_os_dot_client__user__pb2 -from truffle.os import hardware_control_pb2 as truffle_dot_os_dot_hardware__control__pb2 -from truffle.os import hardware_stats_pb2 as truffle_dot_os_dot_hardware__stats__pb2 -from truffle.os import installer_pb2 as truffle_dot_os_dot_installer__pb2 -from truffle.os import notification_pb2 as truffle_dot_os_dot_notification__pb2 -from truffle.os import proactivity_pb2 as truffle_dot_os_dot_proactivity__pb2 -from truffle.os import system_info_pb2 as truffle_dot_os_dot_system__info__pb2 -from truffle.os import system_settings_pb2 as truffle_dot_os_dot_system__settings__pb2 -from truffle.os import task_actions_pb2 as truffle_dot_os_dot_task__actions__pb2 -from truffle.os import task_pb2 as truffle_dot_os_dot_task__pb2 -from truffle.os import task_queries_pb2 as truffle_dot_os_dot_task__queries__pb2 -from truffle.os import task_search_pb2 as truffle_dot_os_dot_task__search__pb2 -from truffle.os import task_user_response_pb2 as truffle_dot_os_dot_task__user__response__pb2 - -GRPC_GENERATED_VERSION = '1.76.0' -GRPC_VERSION = grpc.__version__ -_version_not_supported = False - -try: - from grpc._utilities import first_version_is_lower - _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) -except ImportError: - _version_not_supported = True - -if _version_not_supported: - raise RuntimeError( - f'The grpc package installed is at version {GRPC_VERSION},' - + ' but the generated code in truffle/os/truffleos_pb2_grpc.py depends on' - + f' grpcio>={GRPC_GENERATED_VERSION}.' - + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' - + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' - ) - - -class TruffleOSStub(object): - """Missing associated documentation comment in .proto file.""" - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Apps_DeleteApp = channel.unary_unary( - '/truffle.os.TruffleOS/Apps_DeleteApp', - request_serializer=truffle_dot_os_dot_app__queries__pb2.DeleteAppRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_app__queries__pb2.DeleteAppResponse.FromString, - _registered_method=True) - self.Apps_GetAll = channel.unary_unary( - '/truffle.os.TruffleOS/Apps_GetAll', - request_serializer=truffle_dot_os_dot_app__queries__pb2.GetAllAppsRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_app__queries__pb2.GetAllAppsResponse.FromString, - _registered_method=True) - self.Apps_InstallApp = channel.stream_stream( - '/truffle.os.TruffleOS/Apps_InstallApp', - request_serializer=truffle_dot_os_dot_installer__pb2.AppInstallRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_installer__pb2.AppInstallResponse.FromString, - _registered_method=True) - self.Background_GetFeed = channel.unary_unary( - '/truffle.os.TruffleOS/Background_GetFeed', - request_serializer=truffle_dot_os_dot_background__feed__queries__pb2.GetBackgroundFeedRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_background__feed__queries__pb2.GetBackgroundFeedResponse.FromString, - _registered_method=True) - self.Background_GetLatestFeedEntryID = channel.unary_unary( - '/truffle.os.TruffleOS/Background_GetLatestFeedEntryID', - request_serializer=truffle_dot_os_dot_background__feed__queries__pb2.GetLatestFeedEntryIDRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_background__feed__queries__pb2.GetLatestFeedEntryIDResponse.FromString, - _registered_method=True) - self.Background_ApproveProactiveAction = channel.unary_unary( - '/truffle.os.TruffleOS/Background_ApproveProactiveAction', - request_serializer=truffle_dot_os_dot_proactivity__pb2.ApproveProactiveActionRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_proactivity__pb2.ApproveProactiveActionResponse.FromString, - _registered_method=True) - self.Background_CancelProactiveAction = channel.unary_unary( - '/truffle.os.TruffleOS/Background_CancelProactiveAction', - request_serializer=truffle_dot_os_dot_proactivity__pb2.CancelProactiveActionRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_proactivity__pb2.CancelProactiveActionResponse.FromString, - _registered_method=True) - self.Builder_StartBuildSession = channel.unary_unary( - '/truffle.os.TruffleOS/Builder_StartBuildSession', - request_serializer=truffle_dot_os_dot_builder__pb2.StartBuildSessionRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_builder__pb2.StartBuildSessionResponse.FromString, - _registered_method=True) - self.Builder_FinishBuildSession = channel.unary_unary( - '/truffle.os.TruffleOS/Builder_FinishBuildSession', - request_serializer=truffle_dot_os_dot_builder__pb2.FinishBuildSessionRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_builder__pb2.FinishBuildSessionResponse.FromString, - _registered_method=True) - self.Client_RegisterNewUser = channel.unary_unary( - '/truffle.os.TruffleOS/Client_RegisterNewUser', - request_serializer=truffle_dot_os_dot_client__user__pb2.RegisterNewUserRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_client__user__pb2.RegisterNewUserResponse.FromString, - _registered_method=True) - self.Client_RegisterNewSession = channel.unary_unary( - '/truffle.os.TruffleOS/Client_RegisterNewSession', - request_serializer=truffle_dot_os_dot_client__session__pb2.RegisterNewSessionRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_client__session__pb2.RegisterNewSessionResponse.FromString, - _registered_method=True) - self.Client_UserIDForToken = channel.unary_unary( - '/truffle.os.TruffleOS/Client_UserIDForToken', - request_serializer=truffle_dot_os_dot_client__user__pb2.UserIDForTokenRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_client__user__pb2.UserIDForTokenResponse.FromString, - _registered_method=True) - self.Client_VerifyNewSessionRegistration = channel.unary_unary( - '/truffle.os.TruffleOS/Client_VerifyNewSessionRegistration', - request_serializer=truffle_dot_os_dot_client__session__pb2.VerifyNewSessionRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_client__session__pb2.NewSessionStatus.FromString, - _registered_method=True) - self.Client_GetUserRecoveryCodes = channel.unary_unary( - '/truffle.os.TruffleOS/Client_GetUserRecoveryCodes', - request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=truffle_dot_os_dot_client__session__pb2.UserRecoveryCodes.FromString, - _registered_method=True) - self.Client_UpdateClientState = channel.unary_unary( - '/truffle.os.TruffleOS/Client_UpdateClientState', - request_serializer=truffle_dot_os_dot_client__state__pb2.UpdateClientStateRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_client__state__pb2.UpdateClientStateResponse.FromString, - _registered_method=True) - self.Client_GetClientState = channel.unary_unary( - '/truffle.os.TruffleOS/Client_GetClientState', - request_serializer=truffle_dot_os_dot_client__state__pb2.GetClientStateRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_client__state__pb2.GetClientStateResponse.FromString, - _registered_method=True) - self.Client_GetAllClientStates = channel.unary_unary( - '/truffle.os.TruffleOS/Client_GetAllClientStates', - request_serializer=truffle_dot_os_dot_client__state__pb2.GetAllClientStatesRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_client__state__pb2.GetAllClientStatesResponse.FromString, - _registered_method=True) - self.SubscribeToNotifications = channel.unary_stream( - '/truffle.os.TruffleOS/SubscribeToNotifications', - request_serializer=truffle_dot_os_dot_notification__pb2.SubscribeToNotificationsRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_notification__pb2.Notification.FromString, - _registered_method=True) - self.Hardware_GetStats = channel.unary_unary( - '/truffle.os.TruffleOS/Hardware_GetStats', - request_serializer=truffle_dot_os_dot_hardware__stats__pb2.HardwareStatsRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_hardware__stats__pb2.HardwareStats.FromString, - _registered_method=True) - self.Hardware_StatsUpdates = channel.unary_stream( - '/truffle.os.TruffleOS/Hardware_StatsUpdates', - request_serializer=truffle_dot_os_dot_hardware__stats__pb2.HardwareStatsRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_hardware__stats__pb2.HardwareStats.FromString, - _registered_method=True) - self.Hardware_PowerControl = channel.unary_unary( - '/truffle.os.TruffleOS/Hardware_PowerControl', - request_serializer=truffle_dot_os_dot_hardware__control__pb2.HardwarePowerControlRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_hardware__control__pb2.HardwarePowerControlResponse.FromString, - _registered_method=True) - self.System_GetID = channel.unary_unary( - '/truffle.os.TruffleOS/System_GetID', - request_serializer=truffle_dot_os_dot_system__info__pb2.SystemGetIDRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_system__info__pb2.SystemGetIDResponse.FromString, - _registered_method=True) - self.System_GetSettings = channel.unary_unary( - '/truffle.os.TruffleOS/System_GetSettings', - request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=truffle_dot_os_dot_system__settings__pb2.SystemSettings.FromString, - _registered_method=True) - self.System_SetSettings = channel.unary_unary( - '/truffle.os.TruffleOS/System_SetSettings', - request_serializer=truffle_dot_os_dot_system__settings__pb2.SystemSettings.SerializeToString, - response_deserializer=truffle_dot_os_dot_system__settings__pb2.SystemSettings.FromString, - _registered_method=True) - self.System_GetInfo = channel.unary_unary( - '/truffle.os.TruffleOS/System_GetInfo', - request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - response_deserializer=truffle_dot_os_dot_system__info__pb2.SystemInfo.FromString, - _registered_method=True) - self.System_CheckForUpdate = channel.unary_unary( - '/truffle.os.TruffleOS/System_CheckForUpdate', - request_serializer=truffle_dot_os_dot_system__info__pb2.SystemCheckForUpdateRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_system__info__pb2.SystemCheckForUpdateResponse.FromString, - _registered_method=True) - self.Task_TestExternalToolProvider = channel.unary_unary( - '/truffle.os.TruffleOS/Task_TestExternalToolProvider', - request_serializer=truffle_dot_os_dot_task__actions__pb2.TaskTestExternalToolProviderRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__actions__pb2.TaskTestExternalToolProviderResponse.FromString, - _registered_method=True) - self.Task_OpenTask = channel.unary_stream( - '/truffle.os.TruffleOS/Task_OpenTask', - request_serializer=truffle_dot_os_dot_task__actions__pb2.OpenTaskRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__pb2.TaskStreamUpdate.FromString, - _registered_method=True) - self.Task_InterruptTask = channel.unary_unary( - '/truffle.os.TruffleOS/Task_InterruptTask', - request_serializer=truffle_dot_os_dot_task__actions__pb2.InterruptTaskRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__actions__pb2.TaskActionResponse.FromString, - _registered_method=True) - self.Task_RespondToTask = channel.unary_unary( - '/truffle.os.TruffleOS/Task_RespondToTask', - request_serializer=truffle_dot_os_dot_task__user__response__pb2.RespondToTaskRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__actions__pb2.TaskActionResponse.FromString, - _registered_method=True) - self.Task_SetAvailableApps = channel.unary_unary( - '/truffle.os.TruffleOS/Task_SetAvailableApps', - request_serializer=truffle_dot_os_dot_task__actions__pb2.TaskSetAvailableAppsRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__actions__pb2.TaskSetAvailableAppsResponse.FromString, - _registered_method=True) - self.Task_SearchTasks = channel.unary_unary( - '/truffle.os.TruffleOS/Task_SearchTasks', - request_serializer=truffle_dot_os_dot_task__search__pb2.SearchTasksRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__search__pb2.SearchTasksResponse.FromString, - _registered_method=True) - self.Task_GetTasks = channel.unary_stream( - '/truffle.os.TruffleOS/Task_GetTasks', - request_serializer=truffle_dot_os_dot_task__queries__pb2.GetTasksRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__pb2.Task.FromString, - _registered_method=True) - self.Task_GetOneTask = channel.unary_unary( - '/truffle.os.TruffleOS/Task_GetOneTask', - request_serializer=truffle_dot_os_dot_task__queries__pb2.GetOneTaskRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__pb2.Task.FromString, - _registered_method=True) - self.Task_GetTaskInfos = channel.unary_unary( - '/truffle.os.TruffleOS/Task_GetTaskInfos', - request_serializer=truffle_dot_os_dot_task__queries__pb2.GetTaskInfosRequest.SerializeToString, - response_deserializer=truffle_dot_os_dot_task__queries__pb2.GetTaskInfosResponse.FromString, - _registered_method=True) - - -class TruffleOSServicer(object): - """Missing associated documentation comment in .proto file.""" - - def Apps_DeleteApp(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Apps_GetAll(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Apps_InstallApp(self, request_iterator, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Background_GetFeed(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Background_GetLatestFeedEntryID(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Background_ApproveProactiveAction(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Background_CancelProactiveAction(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Builder_StartBuildSession(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Builder_FinishBuildSession(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Client_RegisterNewUser(self, request, context): - """NOAUTH. Registers a new user and returns a user ID and a session token. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Client_RegisterNewSession(self, request, context): - """NOAUTH. starts session verification process for a new session on another device. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Client_UserIDForToken(self, request, context): - """get the user id associated with a session token - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Client_VerifyNewSessionRegistration(self, request, context): - """called by an already authenticated client to approve or deny a new session creation request from another device. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Client_GetUserRecoveryCodes(self, request, context): - """WARNING! calling this wipes existing recovery codes, if they exist - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Client_UpdateClientState(self, request, context): - """client can store arbitrary key-value state data on the server - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Client_GetClientState(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Client_GetAllClientStates(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def SubscribeToNotifications(self, request, context): - """do this on session start to get your notification stream - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Hardware_GetStats(self, request, context): - """please don't poll this use the damn stream. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Hardware_StatsUpdates(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Hardware_PowerControl(self, request, context): - """this rpc might not return.. so treat calling it as a fire-and-forget - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def System_GetID(self, request, context): - """NO AUTH - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def System_GetSettings(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def System_SetSettings(self, request, context): - """only applies the fields that are explicitly set - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def System_GetInfo(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def System_CheckForUpdate(self, request, context): - """client guys dont use me. this is basically just for dev.. pretty useless due to ota flow. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_TestExternalToolProvider(self, request, context): - """Task == Focus == Foreground App - create or open an existing task and its update stream - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_OpenTask(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_InterruptTask(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_RespondToTask(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_SetAvailableApps(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_SearchTasks(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_GetTasks(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_GetOneTask(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Task_GetTaskInfos(self, request, context): - """Missing associated documentation comment in .proto file.""" - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_TruffleOSServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Apps_DeleteApp': grpc.unary_unary_rpc_method_handler( - servicer.Apps_DeleteApp, - request_deserializer=truffle_dot_os_dot_app__queries__pb2.DeleteAppRequest.FromString, - response_serializer=truffle_dot_os_dot_app__queries__pb2.DeleteAppResponse.SerializeToString, - ), - 'Apps_GetAll': grpc.unary_unary_rpc_method_handler( - servicer.Apps_GetAll, - request_deserializer=truffle_dot_os_dot_app__queries__pb2.GetAllAppsRequest.FromString, - response_serializer=truffle_dot_os_dot_app__queries__pb2.GetAllAppsResponse.SerializeToString, - ), - 'Apps_InstallApp': grpc.stream_stream_rpc_method_handler( - servicer.Apps_InstallApp, - request_deserializer=truffle_dot_os_dot_installer__pb2.AppInstallRequest.FromString, - response_serializer=truffle_dot_os_dot_installer__pb2.AppInstallResponse.SerializeToString, - ), - 'Background_GetFeed': grpc.unary_unary_rpc_method_handler( - servicer.Background_GetFeed, - request_deserializer=truffle_dot_os_dot_background__feed__queries__pb2.GetBackgroundFeedRequest.FromString, - response_serializer=truffle_dot_os_dot_background__feed__queries__pb2.GetBackgroundFeedResponse.SerializeToString, - ), - 'Background_GetLatestFeedEntryID': grpc.unary_unary_rpc_method_handler( - servicer.Background_GetLatestFeedEntryID, - request_deserializer=truffle_dot_os_dot_background__feed__queries__pb2.GetLatestFeedEntryIDRequest.FromString, - response_serializer=truffle_dot_os_dot_background__feed__queries__pb2.GetLatestFeedEntryIDResponse.SerializeToString, - ), - 'Background_ApproveProactiveAction': grpc.unary_unary_rpc_method_handler( - servicer.Background_ApproveProactiveAction, - request_deserializer=truffle_dot_os_dot_proactivity__pb2.ApproveProactiveActionRequest.FromString, - response_serializer=truffle_dot_os_dot_proactivity__pb2.ApproveProactiveActionResponse.SerializeToString, - ), - 'Background_CancelProactiveAction': grpc.unary_unary_rpc_method_handler( - servicer.Background_CancelProactiveAction, - request_deserializer=truffle_dot_os_dot_proactivity__pb2.CancelProactiveActionRequest.FromString, - response_serializer=truffle_dot_os_dot_proactivity__pb2.CancelProactiveActionResponse.SerializeToString, - ), - 'Builder_StartBuildSession': grpc.unary_unary_rpc_method_handler( - servicer.Builder_StartBuildSession, - request_deserializer=truffle_dot_os_dot_builder__pb2.StartBuildSessionRequest.FromString, - response_serializer=truffle_dot_os_dot_builder__pb2.StartBuildSessionResponse.SerializeToString, - ), - 'Builder_FinishBuildSession': grpc.unary_unary_rpc_method_handler( - servicer.Builder_FinishBuildSession, - request_deserializer=truffle_dot_os_dot_builder__pb2.FinishBuildSessionRequest.FromString, - response_serializer=truffle_dot_os_dot_builder__pb2.FinishBuildSessionResponse.SerializeToString, - ), - 'Client_RegisterNewUser': grpc.unary_unary_rpc_method_handler( - servicer.Client_RegisterNewUser, - request_deserializer=truffle_dot_os_dot_client__user__pb2.RegisterNewUserRequest.FromString, - response_serializer=truffle_dot_os_dot_client__user__pb2.RegisterNewUserResponse.SerializeToString, - ), - 'Client_RegisterNewSession': grpc.unary_unary_rpc_method_handler( - servicer.Client_RegisterNewSession, - request_deserializer=truffle_dot_os_dot_client__session__pb2.RegisterNewSessionRequest.FromString, - response_serializer=truffle_dot_os_dot_client__session__pb2.RegisterNewSessionResponse.SerializeToString, - ), - 'Client_UserIDForToken': grpc.unary_unary_rpc_method_handler( - servicer.Client_UserIDForToken, - request_deserializer=truffle_dot_os_dot_client__user__pb2.UserIDForTokenRequest.FromString, - response_serializer=truffle_dot_os_dot_client__user__pb2.UserIDForTokenResponse.SerializeToString, - ), - 'Client_VerifyNewSessionRegistration': grpc.unary_unary_rpc_method_handler( - servicer.Client_VerifyNewSessionRegistration, - request_deserializer=truffle_dot_os_dot_client__session__pb2.VerifyNewSessionRequest.FromString, - response_serializer=truffle_dot_os_dot_client__session__pb2.NewSessionStatus.SerializeToString, - ), - 'Client_GetUserRecoveryCodes': grpc.unary_unary_rpc_method_handler( - servicer.Client_GetUserRecoveryCodes, - request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=truffle_dot_os_dot_client__session__pb2.UserRecoveryCodes.SerializeToString, - ), - 'Client_UpdateClientState': grpc.unary_unary_rpc_method_handler( - servicer.Client_UpdateClientState, - request_deserializer=truffle_dot_os_dot_client__state__pb2.UpdateClientStateRequest.FromString, - response_serializer=truffle_dot_os_dot_client__state__pb2.UpdateClientStateResponse.SerializeToString, - ), - 'Client_GetClientState': grpc.unary_unary_rpc_method_handler( - servicer.Client_GetClientState, - request_deserializer=truffle_dot_os_dot_client__state__pb2.GetClientStateRequest.FromString, - response_serializer=truffle_dot_os_dot_client__state__pb2.GetClientStateResponse.SerializeToString, - ), - 'Client_GetAllClientStates': grpc.unary_unary_rpc_method_handler( - servicer.Client_GetAllClientStates, - request_deserializer=truffle_dot_os_dot_client__state__pb2.GetAllClientStatesRequest.FromString, - response_serializer=truffle_dot_os_dot_client__state__pb2.GetAllClientStatesResponse.SerializeToString, - ), - 'SubscribeToNotifications': grpc.unary_stream_rpc_method_handler( - servicer.SubscribeToNotifications, - request_deserializer=truffle_dot_os_dot_notification__pb2.SubscribeToNotificationsRequest.FromString, - response_serializer=truffle_dot_os_dot_notification__pb2.Notification.SerializeToString, - ), - 'Hardware_GetStats': grpc.unary_unary_rpc_method_handler( - servicer.Hardware_GetStats, - request_deserializer=truffle_dot_os_dot_hardware__stats__pb2.HardwareStatsRequest.FromString, - response_serializer=truffle_dot_os_dot_hardware__stats__pb2.HardwareStats.SerializeToString, - ), - 'Hardware_StatsUpdates': grpc.unary_stream_rpc_method_handler( - servicer.Hardware_StatsUpdates, - request_deserializer=truffle_dot_os_dot_hardware__stats__pb2.HardwareStatsRequest.FromString, - response_serializer=truffle_dot_os_dot_hardware__stats__pb2.HardwareStats.SerializeToString, - ), - 'Hardware_PowerControl': grpc.unary_unary_rpc_method_handler( - servicer.Hardware_PowerControl, - request_deserializer=truffle_dot_os_dot_hardware__control__pb2.HardwarePowerControlRequest.FromString, - response_serializer=truffle_dot_os_dot_hardware__control__pb2.HardwarePowerControlResponse.SerializeToString, - ), - 'System_GetID': grpc.unary_unary_rpc_method_handler( - servicer.System_GetID, - request_deserializer=truffle_dot_os_dot_system__info__pb2.SystemGetIDRequest.FromString, - response_serializer=truffle_dot_os_dot_system__info__pb2.SystemGetIDResponse.SerializeToString, - ), - 'System_GetSettings': grpc.unary_unary_rpc_method_handler( - servicer.System_GetSettings, - request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=truffle_dot_os_dot_system__settings__pb2.SystemSettings.SerializeToString, - ), - 'System_SetSettings': grpc.unary_unary_rpc_method_handler( - servicer.System_SetSettings, - request_deserializer=truffle_dot_os_dot_system__settings__pb2.SystemSettings.FromString, - response_serializer=truffle_dot_os_dot_system__settings__pb2.SystemSettings.SerializeToString, - ), - 'System_GetInfo': grpc.unary_unary_rpc_method_handler( - servicer.System_GetInfo, - request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, - response_serializer=truffle_dot_os_dot_system__info__pb2.SystemInfo.SerializeToString, - ), - 'System_CheckForUpdate': grpc.unary_unary_rpc_method_handler( - servicer.System_CheckForUpdate, - request_deserializer=truffle_dot_os_dot_system__info__pb2.SystemCheckForUpdateRequest.FromString, - response_serializer=truffle_dot_os_dot_system__info__pb2.SystemCheckForUpdateResponse.SerializeToString, - ), - 'Task_TestExternalToolProvider': grpc.unary_unary_rpc_method_handler( - servicer.Task_TestExternalToolProvider, - request_deserializer=truffle_dot_os_dot_task__actions__pb2.TaskTestExternalToolProviderRequest.FromString, - response_serializer=truffle_dot_os_dot_task__actions__pb2.TaskTestExternalToolProviderResponse.SerializeToString, - ), - 'Task_OpenTask': grpc.unary_stream_rpc_method_handler( - servicer.Task_OpenTask, - request_deserializer=truffle_dot_os_dot_task__actions__pb2.OpenTaskRequest.FromString, - response_serializer=truffle_dot_os_dot_task__pb2.TaskStreamUpdate.SerializeToString, - ), - 'Task_InterruptTask': grpc.unary_unary_rpc_method_handler( - servicer.Task_InterruptTask, - request_deserializer=truffle_dot_os_dot_task__actions__pb2.InterruptTaskRequest.FromString, - response_serializer=truffle_dot_os_dot_task__actions__pb2.TaskActionResponse.SerializeToString, - ), - 'Task_RespondToTask': grpc.unary_unary_rpc_method_handler( - servicer.Task_RespondToTask, - request_deserializer=truffle_dot_os_dot_task__user__response__pb2.RespondToTaskRequest.FromString, - response_serializer=truffle_dot_os_dot_task__actions__pb2.TaskActionResponse.SerializeToString, - ), - 'Task_SetAvailableApps': grpc.unary_unary_rpc_method_handler( - servicer.Task_SetAvailableApps, - request_deserializer=truffle_dot_os_dot_task__actions__pb2.TaskSetAvailableAppsRequest.FromString, - response_serializer=truffle_dot_os_dot_task__actions__pb2.TaskSetAvailableAppsResponse.SerializeToString, - ), - 'Task_SearchTasks': grpc.unary_unary_rpc_method_handler( - servicer.Task_SearchTasks, - request_deserializer=truffle_dot_os_dot_task__search__pb2.SearchTasksRequest.FromString, - response_serializer=truffle_dot_os_dot_task__search__pb2.SearchTasksResponse.SerializeToString, - ), - 'Task_GetTasks': grpc.unary_stream_rpc_method_handler( - servicer.Task_GetTasks, - request_deserializer=truffle_dot_os_dot_task__queries__pb2.GetTasksRequest.FromString, - response_serializer=truffle_dot_os_dot_task__pb2.Task.SerializeToString, - ), - 'Task_GetOneTask': grpc.unary_unary_rpc_method_handler( - servicer.Task_GetOneTask, - request_deserializer=truffle_dot_os_dot_task__queries__pb2.GetOneTaskRequest.FromString, - response_serializer=truffle_dot_os_dot_task__pb2.Task.SerializeToString, - ), - 'Task_GetTaskInfos': grpc.unary_unary_rpc_method_handler( - servicer.Task_GetTaskInfos, - request_deserializer=truffle_dot_os_dot_task__queries__pb2.GetTaskInfosRequest.FromString, - response_serializer=truffle_dot_os_dot_task__queries__pb2.GetTaskInfosResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'truffle.os.TruffleOS', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('truffle.os.TruffleOS', rpc_method_handlers) - - - # This class is part of an EXPERIMENTAL API. -class TruffleOS(object): - """Missing associated documentation comment in .proto file.""" - - @staticmethod - def Apps_DeleteApp(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Apps_DeleteApp', - truffle_dot_os_dot_app__queries__pb2.DeleteAppRequest.SerializeToString, - truffle_dot_os_dot_app__queries__pb2.DeleteAppResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Apps_GetAll(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Apps_GetAll', - truffle_dot_os_dot_app__queries__pb2.GetAllAppsRequest.SerializeToString, - truffle_dot_os_dot_app__queries__pb2.GetAllAppsResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Apps_InstallApp(request_iterator, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.stream_stream( - request_iterator, - target, - '/truffle.os.TruffleOS/Apps_InstallApp', - truffle_dot_os_dot_installer__pb2.AppInstallRequest.SerializeToString, - truffle_dot_os_dot_installer__pb2.AppInstallResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Background_GetFeed(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Background_GetFeed', - truffle_dot_os_dot_background__feed__queries__pb2.GetBackgroundFeedRequest.SerializeToString, - truffle_dot_os_dot_background__feed__queries__pb2.GetBackgroundFeedResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Background_GetLatestFeedEntryID(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Background_GetLatestFeedEntryID', - truffle_dot_os_dot_background__feed__queries__pb2.GetLatestFeedEntryIDRequest.SerializeToString, - truffle_dot_os_dot_background__feed__queries__pb2.GetLatestFeedEntryIDResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Background_ApproveProactiveAction(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Background_ApproveProactiveAction', - truffle_dot_os_dot_proactivity__pb2.ApproveProactiveActionRequest.SerializeToString, - truffle_dot_os_dot_proactivity__pb2.ApproveProactiveActionResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Background_CancelProactiveAction(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Background_CancelProactiveAction', - truffle_dot_os_dot_proactivity__pb2.CancelProactiveActionRequest.SerializeToString, - truffle_dot_os_dot_proactivity__pb2.CancelProactiveActionResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Builder_StartBuildSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Builder_StartBuildSession', - truffle_dot_os_dot_builder__pb2.StartBuildSessionRequest.SerializeToString, - truffle_dot_os_dot_builder__pb2.StartBuildSessionResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Builder_FinishBuildSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Builder_FinishBuildSession', - truffle_dot_os_dot_builder__pb2.FinishBuildSessionRequest.SerializeToString, - truffle_dot_os_dot_builder__pb2.FinishBuildSessionResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Client_RegisterNewUser(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Client_RegisterNewUser', - truffle_dot_os_dot_client__user__pb2.RegisterNewUserRequest.SerializeToString, - truffle_dot_os_dot_client__user__pb2.RegisterNewUserResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Client_RegisterNewSession(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Client_RegisterNewSession', - truffle_dot_os_dot_client__session__pb2.RegisterNewSessionRequest.SerializeToString, - truffle_dot_os_dot_client__session__pb2.RegisterNewSessionResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Client_UserIDForToken(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Client_UserIDForToken', - truffle_dot_os_dot_client__user__pb2.UserIDForTokenRequest.SerializeToString, - truffle_dot_os_dot_client__user__pb2.UserIDForTokenResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Client_VerifyNewSessionRegistration(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Client_VerifyNewSessionRegistration', - truffle_dot_os_dot_client__session__pb2.VerifyNewSessionRequest.SerializeToString, - truffle_dot_os_dot_client__session__pb2.NewSessionStatus.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Client_GetUserRecoveryCodes(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Client_GetUserRecoveryCodes', - google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - truffle_dot_os_dot_client__session__pb2.UserRecoveryCodes.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Client_UpdateClientState(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Client_UpdateClientState', - truffle_dot_os_dot_client__state__pb2.UpdateClientStateRequest.SerializeToString, - truffle_dot_os_dot_client__state__pb2.UpdateClientStateResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Client_GetClientState(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Client_GetClientState', - truffle_dot_os_dot_client__state__pb2.GetClientStateRequest.SerializeToString, - truffle_dot_os_dot_client__state__pb2.GetClientStateResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Client_GetAllClientStates(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Client_GetAllClientStates', - truffle_dot_os_dot_client__state__pb2.GetAllClientStatesRequest.SerializeToString, - truffle_dot_os_dot_client__state__pb2.GetAllClientStatesResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def SubscribeToNotifications(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_stream( - request, - target, - '/truffle.os.TruffleOS/SubscribeToNotifications', - truffle_dot_os_dot_notification__pb2.SubscribeToNotificationsRequest.SerializeToString, - truffle_dot_os_dot_notification__pb2.Notification.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Hardware_GetStats(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Hardware_GetStats', - truffle_dot_os_dot_hardware__stats__pb2.HardwareStatsRequest.SerializeToString, - truffle_dot_os_dot_hardware__stats__pb2.HardwareStats.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Hardware_StatsUpdates(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_stream( - request, - target, - '/truffle.os.TruffleOS/Hardware_StatsUpdates', - truffle_dot_os_dot_hardware__stats__pb2.HardwareStatsRequest.SerializeToString, - truffle_dot_os_dot_hardware__stats__pb2.HardwareStats.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Hardware_PowerControl(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Hardware_PowerControl', - truffle_dot_os_dot_hardware__control__pb2.HardwarePowerControlRequest.SerializeToString, - truffle_dot_os_dot_hardware__control__pb2.HardwarePowerControlResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def System_GetID(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/System_GetID', - truffle_dot_os_dot_system__info__pb2.SystemGetIDRequest.SerializeToString, - truffle_dot_os_dot_system__info__pb2.SystemGetIDResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def System_GetSettings(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/System_GetSettings', - google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - truffle_dot_os_dot_system__settings__pb2.SystemSettings.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def System_SetSettings(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/System_SetSettings', - truffle_dot_os_dot_system__settings__pb2.SystemSettings.SerializeToString, - truffle_dot_os_dot_system__settings__pb2.SystemSettings.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def System_GetInfo(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/System_GetInfo', - google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, - truffle_dot_os_dot_system__info__pb2.SystemInfo.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def System_CheckForUpdate(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/System_CheckForUpdate', - truffle_dot_os_dot_system__info__pb2.SystemCheckForUpdateRequest.SerializeToString, - truffle_dot_os_dot_system__info__pb2.SystemCheckForUpdateResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_TestExternalToolProvider(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Task_TestExternalToolProvider', - truffle_dot_os_dot_task__actions__pb2.TaskTestExternalToolProviderRequest.SerializeToString, - truffle_dot_os_dot_task__actions__pb2.TaskTestExternalToolProviderResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_OpenTask(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_stream( - request, - target, - '/truffle.os.TruffleOS/Task_OpenTask', - truffle_dot_os_dot_task__actions__pb2.OpenTaskRequest.SerializeToString, - truffle_dot_os_dot_task__pb2.TaskStreamUpdate.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_InterruptTask(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Task_InterruptTask', - truffle_dot_os_dot_task__actions__pb2.InterruptTaskRequest.SerializeToString, - truffle_dot_os_dot_task__actions__pb2.TaskActionResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_RespondToTask(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Task_RespondToTask', - truffle_dot_os_dot_task__user__response__pb2.RespondToTaskRequest.SerializeToString, - truffle_dot_os_dot_task__actions__pb2.TaskActionResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_SetAvailableApps(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Task_SetAvailableApps', - truffle_dot_os_dot_task__actions__pb2.TaskSetAvailableAppsRequest.SerializeToString, - truffle_dot_os_dot_task__actions__pb2.TaskSetAvailableAppsResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_SearchTasks(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Task_SearchTasks', - truffle_dot_os_dot_task__search__pb2.SearchTasksRequest.SerializeToString, - truffle_dot_os_dot_task__search__pb2.SearchTasksResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_GetTasks(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_stream( - request, - target, - '/truffle.os.TruffleOS/Task_GetTasks', - truffle_dot_os_dot_task__queries__pb2.GetTasksRequest.SerializeToString, - truffle_dot_os_dot_task__pb2.Task.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_GetOneTask(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Task_GetOneTask', - truffle_dot_os_dot_task__queries__pb2.GetOneTaskRequest.SerializeToString, - truffle_dot_os_dot_task__pb2.Task.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) - - @staticmethod - def Task_GetTaskInfos(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary( - request, - target, - '/truffle.os.TruffleOS/Task_GetTaskInfos', - truffle_dot_os_dot_task__queries__pb2.GetTaskInfosRequest.SerializeToString, - truffle_dot_os_dot_task__queries__pb2.GetTaskInfosResponse.FromString, - options, - channel_credentials, - insecure, - call_credentials, - compression, - wait_for_ready, - timeout, - metadata, - _registered_method=True) From 44bf787bf111a1a79dc8e7aac2ba6f0382500a0b Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sat, 4 Apr 2026 16:22:16 -0700 Subject: [PATCH 03/12] set up publishing --- .github/workflows/publish.yml | 55 +++++++++++++++++++++++++++++++++++ .gitignore | 6 +++- pyproject.toml | 1 + truffile/__init__.py | 35 ++++++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..98ba3e4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,55 @@ +name: Build and Publish + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + include_internal: + description: 'include internal modules' + type: boolean + default: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.PYFW_DEPLOY_KEY }} + + - name: clone pyfw + run: | + git clone --depth 1 git@github.com:deepshard/pyfw.git _pyfw + + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: install build deps + run: pip install grpcio-tools==1.76.0 protobuf>=6.30.0 googleapis-common-protos>=1.63.2 build setuptools-scm + + - name: build package + run: | + ARGS="--pyfw-path ./_pyfw" + if [ "${{ inputs.include_internal }}" = "true" ]; then + ARGS="$ARGS --include-internal" + fi + python scripts/build_package.py $ARGS + + - name: build wheel + run: python -m build + + - name: publish to pypi + if: startsWith(github.ref, 'refs/tags/v') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_TOKEN }} + + - name: cleanup + if: always() + run: rm -rf _pyfw diff --git a/.gitignore b/.gitignore index 4ded562..7a00e45 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ __pycache__/ .DS_Store dist whatsapp/ -docs-repo/ \ No newline at end of file +docs-repo/ +.env +truffile/app_runtime/ +truffle/ +scripts/* \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ab5869f..0c3edf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "protobuf>=6.30.0", "googleapis-common-protos>=1.63.2", "grpcio==1.76.0", + "grpcio-reflection==1.76.0", "httpx>=0.27.0", "pyyaml>=6.0", "platformdirs>=3.10.0", diff --git a/truffile/__init__.py b/truffile/__init__.py index 97f20b5..1c0066e 100644 --- a/truffile/__init__.py +++ b/truffile/__init__.py @@ -27,6 +27,33 @@ def _ensure_bundled_truffle_on_path() -> None: from .client import TruffleClient, ExecResult, UploadResult, resolve_mdns, NewSessionStatus from .schedule import parse_runtime_policy +try: + from .sdk import ( + ForegroundApp, + BackgroundWorkerApp, + tool, + ok, + err, + OAuth, + AppHarness, + ToolSpec, + ) + + # register app_runtime as an alias for truffile.app_runtime + # so old apps using "from app_runtime import ..." still work + # when only truffile is installed (no standalone app_runtime) + import truffile.app_runtime as _app_runtime + if "app_runtime" not in sys.modules: + sys.modules["app_runtime"] = _app_runtime + # register submodules so deep imports work too + for _submod in ("background", "mcp", "abrasive", "browser", "browser.web_fingerprint"): + _full = f"truffile.app_runtime.{_submod}" + _alias = f"app_runtime.{_submod}" + if _full in sys.modules and _alias not in sys.modules: + sys.modules[_alias] = sys.modules[_full] +except ImportError: + pass + __all__ = [ "__version__", "TruffleClient", @@ -35,4 +62,12 @@ def _ensure_bundled_truffle_on_path() -> None: "resolve_mdns", "NewSessionStatus", "parse_runtime_policy", + "ForegroundApp", + "BackgroundWorkerApp", + "tool", + "ok", + "err", + "OAuth", + "AppHarness", + "ToolSpec", ] From 0c532b91d7efce3240c0483601e800e2262b5c6f Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sat, 4 Apr 2026 16:28:17 -0700 Subject: [PATCH 04/12] publishing tweaks --- .github/workflows/publish.yml | 15 +---- .gitignore | 3 +- scripts/build_package.py | 114 ++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 scripts/build_package.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98ba3e4..2d91f82 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,11 +4,6 @@ on: push: tags: ['v*'] workflow_dispatch: - inputs: - include_internal: - description: 'include internal modules' - type: boolean - default: false jobs: build: @@ -23,8 +18,7 @@ jobs: ssh-private-key: ${{ secrets.PYFW_DEPLOY_KEY }} - name: clone pyfw - run: | - git clone --depth 1 git@github.com:deepshard/pyfw.git _pyfw + run: git clone --depth 1 git@github.com:deepshard/pyfw.git _pyfw - uses: actions/setup-python@v5 with: @@ -34,12 +28,7 @@ jobs: run: pip install grpcio-tools==1.76.0 protobuf>=6.30.0 googleapis-common-protos>=1.63.2 build setuptools-scm - name: build package - run: | - ARGS="--pyfw-path ./_pyfw" - if [ "${{ inputs.include_internal }}" = "true" ]; then - ARGS="$ARGS --include-internal" - fi - python scripts/build_package.py $ARGS + run: python scripts/build_package.py --pyfw-path ./_pyfw - name: build wheel run: python -m build diff --git a/.gitignore b/.gitignore index 7a00e45..b71bf9c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,4 @@ whatsapp/ docs-repo/ .env truffile/app_runtime/ -truffle/ -scripts/* \ No newline at end of file +truffle/ \ No newline at end of file diff --git a/scripts/build_package.py b/scripts/build_package.py new file mode 100644 index 0000000..c1f21bb --- /dev/null +++ b/scripts/build_package.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# if youre reading this plz go make apps lol +# copies app_runtime from pyfw and rebuilds protos for truffile packaging. +# +# usage: +# python scripts/build_package.py --pyfw-path /path/to/pyfw +# +# or set PYFW_PATH in .env: +# echo "PYFW_PATH=/Users/me/work/pyfw" > .env +# python scripts/build_package.py +# +# what it does: +# 1. copies python/app_runtime/ from pyfw into truffile/app_runtime/ +# 2. rebuilds protos in pyfw (if needed) and copies truffle/ proto package +# 3. strips internal-only modules (browser/web_fingerprint) from the public build + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +REPO_ROOT = SCRIPT_DIR.parent +TRUFFILE_PKG = REPO_ROOT / "truffile" +TRUFFLE_PROTO_PKG = REPO_ROOT / "truffle" + +def load_env(): + env_file = REPO_ROOT / ".env" + if env_file.exists(): + for line in env_file.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + key, _, value = line.partition("=") + os.environ.setdefault(key.strip(), value.strip()) + + +def resolve_pyfw_path(cli_arg: str | None) -> Path: + raw = cli_arg or os.environ.get("PYFW_PATH", "") + if not raw: + print("error: --pyfw-path not provided and PYFW_PATH not set") + print("set it via cli arg or in .env file") + sys.exit(1) + path = Path(raw).resolve() + if not (path / "python" / "app_runtime").exists(): + print(f"error: {path}/python/app_runtime does not exist") + sys.exit(1) + return path + + +def build_protos(pyfw_path: Path): + build_script = pyfw_path / "python" / "tools" / "build_protos.py" + if not build_script.exists(): + print("warning: proto build script not found, skipping proto rebuild") + return + print("building protos in pyfw...") + subprocess.run( + [sys.executable, str(build_script)], + cwd=str(pyfw_path), + check=True, + ) + print("protos built") + + +def copy_app_runtime(pyfw_path: Path): + src = pyfw_path / "python" / "app_runtime" + dst = TRUFFILE_PKG / "app_runtime" + + if dst.exists(): + shutil.rmtree(dst) + + shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) + print(f"copied app_runtime ({sum(1 for _ in dst.rglob('*.py'))} py files)") + + +def copy_protos(pyfw_path: Path): + src = pyfw_path / "python" / "truffle" + dst = TRUFFLE_PROTO_PKG + + if not src.exists(): + print("warning: pyfw truffle/ proto package not found, keeping existing vendored protos") + return + + if dst.exists(): + shutil.rmtree(dst) + + shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) + print(f"copied truffle protos ({sum(1 for _ in dst.rglob('*.py'))} py files)") + + +def main(): + load_env() + + parser = argparse.ArgumentParser(description="build truffile package with app_runtime from pyfw") + parser.add_argument("--pyfw-path", type=str, default=None) + parser.add_argument("--skip-protos", action="store_true", help="skip proto rebuild") + args = parser.parse_args() + + pyfw_path = resolve_pyfw_path(args.pyfw_path) + print(f"using pyfw at: {pyfw_path}") + + if not args.skip_protos: + build_protos(pyfw_path) + + copy_app_runtime(pyfw_path) + copy_protos(pyfw_path) + + print("\npackage ready. run: pip install -e . to test locally") + + +if __name__ == "__main__": + main() From fd5b47a0e88da7c811cb751566fdced943f68c2e Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sat, 4 Apr 2026 18:34:28 -0700 Subject: [PATCH 05/12] test --- .github/workflows/test.yml | 45 ++++++++++ tests/test_app_config.py | 154 +++++++++++++++++++++++++++++++++ tests/test_chat_helpers.py | 93 ++++++++++++++++++++ tests/test_create.py | 88 +++++++++++++++++++ tests/test_cross_platform.py | 72 +++++++++++++++ tests/test_deploy_plan.py | 100 +++++++++++++++++++++ tests/test_runtime_policy.py | 125 ++++++++++++++++++++++++++ tests/test_storage.py | 89 +++++++++++++++++++ tests/test_transport_client.py | 3 +- 9 files changed, 768 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100644 tests/test_app_config.py create mode 100644 tests/test_chat_helpers.py create mode 100644 tests/test_create.py create mode 100644 tests/test_cross_platform.py create mode 100644 tests/test_deploy_plan.py create mode 100644 tests/test_runtime_policy.py create mode 100644 tests/test_storage.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5604255 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: Tests + +on: + pull_request: + push: + branches: [main] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.12', '3.13'] + steps: + - uses: actions/checkout@v4 + + - uses: webfactory/ssh-agent@v0.9.0 + if: runner.os != 'Windows' + with: + ssh-private-key: ${{ secrets.PYFW_DEPLOY_KEY }} + + - name: clone pyfw + if: runner.os != 'Windows' + run: git clone --depth 1 git@github.com:deepshard/pyfw.git _pyfw + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: install deps + run: | + pip install grpcio-tools==1.76.0 protobuf>=6.30.0 googleapis-common-protos>=1.63.2 pytest + pip install -e . + + - name: build package + if: runner.os != 'Windows' + run: python scripts/build_package.py --pyfw-path ./_pyfw + + - name: run tests + run: python -m pytest tests/ -v + + - name: cleanup + if: always() && runner.os != 'Windows' + run: rm -rf _pyfw diff --git a/tests/test_app_config.py b/tests/test_app_config.py new file mode 100644 index 0000000..c4eb731 --- /dev/null +++ b/tests/test_app_config.py @@ -0,0 +1,154 @@ +import tempfile +import unittest +from pathlib import Path + +from truffile.schema.app_config import validate_app_dir + + +class TestValidateAppDir(unittest.TestCase): + def _make_app(self, tmp: str, yaml_content: str, files: dict[str, str] | None = None): + app_dir = Path(tmp) / "test-app" + app_dir.mkdir() + (app_dir / "truffile.yaml").write_text(yaml_content) + for name, content in (files or {}).items(): + p = app_dir / name + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(content) + return app_dir + + def test_valid_foreground_app(self): + yaml = """ +metadata: + name: Test App + bundle_id: org.test.app + description: A test app + foreground: + process: + cmd: [python, app.py] +steps: + - name: Copy files + type: files + files: + - source: ./app.py + destination: ./app.py +""" + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app(tmp, yaml, {"app.py": "print('hello')"}) + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + self.assertTrue(valid, f"errors: {errors}") + + def test_missing_truffile_yaml(self): + with tempfile.TemporaryDirectory() as tmp: + app_dir = Path(tmp) / "empty-app" + app_dir.mkdir() + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + self.assertFalse(valid) + self.assertTrue(any("truffile.yaml" in e.lower() or "not found" in e.lower() for e in errors)) + + def test_invalid_yaml(self): + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app(tmp, "{{{{invalid yaml::::") + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + self.assertFalse(valid) + + def test_missing_metadata_name(self): + yaml = """ +metadata: + bundle_id: org.test.app + foreground: + process: + cmd: [python, app.py] +""" + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app(tmp, yaml) + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + self.assertFalse(valid) + + def test_missing_process_config(self): + yaml = """ +metadata: + name: No Process App + bundle_id: org.test.noprocess + description: No fg or bg +""" + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app(tmp, yaml) + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + self.assertFalse(valid) + + def test_hybrid_app_type(self): + yaml = """ +metadata: + name: Hybrid + bundle_id: org.test.hybrid + description: Both fg and bg + foreground: + process: + cmd: [python, fg.py] + background: + process: + cmd: [python, bg.py] + default_schedule: + type: interval + interval: + duration: 30m +steps: + - name: Copy + type: files + files: + - source: ./fg.py + destination: ./fg.py + - source: ./bg.py + destination: ./bg.py +""" + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app(tmp, yaml, {"fg.py": "pass", "bg.py": "pass"}) + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + self.assertTrue(valid, f"errors: {errors}") + self.assertEqual(app_type, "hybrid") + + def test_bad_python_syntax_detected(self): + yaml = """ +metadata: + name: Bad Syntax + bundle_id: org.test.badsyntax + foreground: + process: + cmd: [python, broken.py] +steps: + - name: Copy + type: files + files: + - source: ./broken.py + destination: ./broken.py +""" + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app(tmp, yaml, {"broken.py": "def foo(\n pass"}) + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + # should have a warning about syntax + all_messages = warnings + errors + has_syntax = any("syntax" in m.lower() for m in all_messages) + self.assertTrue(has_syntax, f"expected syntax warning in: {all_messages}") + + def test_missing_source_file(self): + yaml = """ +metadata: + name: Missing File + bundle_id: org.test.missing + foreground: + process: + cmd: [python, app.py] +steps: + - name: Copy + type: files + files: + - source: ./app.py + destination: ./app.py +""" + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app(tmp, yaml) + # app.py not created โ€” should warn or error + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + all_messages = warnings + errors + has_missing = any("not found" in m.lower() or "missing" in m.lower() or "does not exist" in m.lower() for m in all_messages) + self.assertTrue(has_missing, f"expected missing file message in: {all_messages}") diff --git a/tests/test_chat_helpers.py b/tests/test_chat_helpers.py new file mode 100644 index 0000000..807c162 --- /dev/null +++ b/tests/test_chat_helpers.py @@ -0,0 +1,93 @@ +import base64 +import sys +import unittest + + +def _import_chat(): + """lazy import to handle missing deps gracefully""" + try: + from truffile.cli import chat + return chat + except ImportError: + return None + + +class TestParseOnOff(unittest.TestCase): + def setUp(self): + self.chat = _import_chat() + if self.chat is None: + self.skipTest("chat module not importable") + + def test_true_values(self): + for val in ["on", "true", "1", "yes", "ON", "True", "YES"]: + self.assertTrue(self.chat._parse_on_off(val), f"{val} should be True") + + def test_false_values(self): + for val in ["off", "false", "0", "no", "OFF", "False", "NO"]: + self.assertFalse(self.chat._parse_on_off(val), f"{val} should be False") + + def test_invalid_returns_none(self): + self.assertIsNone(self.chat._parse_on_off("maybe")) + self.assertIsNone(self.chat._parse_on_off("")) + self.assertIsNone(self.chat._parse_on_off("2")) + + +class TestMakeUserMessage(unittest.TestCase): + def setUp(self): + self.chat = _import_chat() + if self.chat is None: + self.skipTest("chat module not importable") + + def test_text_only(self): + msg = self.chat._make_user_message("hello", None) + self.assertEqual(msg["role"], "user") + + def test_with_image(self): + msg = self.chat._make_user_message("describe", "data:image/png;base64,abc") + self.assertEqual(msg["role"], "user") + content = msg["content"] + self.assertIsInstance(content, list) + + +class TestToDataUrl(unittest.TestCase): + def setUp(self): + self.chat = _import_chat() + if self.chat is None: + self.skipTest("chat module not importable") + + def test_encodes_correctly(self): + result = self.chat._to_data_url(b"hello world", "image/png") + self.assertTrue(result.startswith("data:image/png;base64,")) + encoded = result.split(",")[1] + self.assertEqual(base64.b64decode(encoded), b"hello world") + + +class TestBuildDefaultTools(unittest.TestCase): + def setUp(self): + self.chat = _import_chat() + if self.chat is None: + self.skipTest("chat module not importable") + + def test_returns_list(self): + tools = self.chat._build_default_tools() + self.assertIsInstance(tools, list) + self.assertTrue(len(tools) > 0) + + def test_tools_have_schema(self): + tools = self.chat._build_default_tools() + for tool in tools: + self.assertEqual(tool["type"], "function") + self.assertIn("name", tool["function"]) + self.assertIn("parameters", tool["function"]) + + +class TestReplCommands(unittest.TestCase): + def setUp(self): + self.chat = _import_chat() + if self.chat is None: + self.skipTest("chat module not importable") + + def test_commands_exist(self): + self.assertIn("/help", self.chat.REPL_COMMANDS) + self.assertIn("/exit", self.chat.REPL_COMMANDS) + self.assertIn("/mcp", self.chat.REPL_COMMANDS) diff --git a/tests/test_create.py b/tests/test_create.py new file mode 100644 index 0000000..6963e10 --- /dev/null +++ b/tests/test_create.py @@ -0,0 +1,88 @@ +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from truffile.cli.create import _safe_app_slug, _sample_truffile_yaml, _sample_foreground_py, _sample_background_py + + +class TestSafeAppSlug(unittest.TestCase): + def test_simple_name(self): + self.assertEqual(_safe_app_slug("My App"), "my_app") + + def test_hyphenated_name(self): + self.assertEqual(_safe_app_slug("my-cool-app"), "my_cool_app") + + def test_special_chars_stripped(self): + slug = _safe_app_slug("My App! (v2)") + self.assertTrue(slug.isidentifier(), f"{slug} is not a valid identifier") + + def test_numeric_prefix_handled(self): + slug = _safe_app_slug("123app") + self.assertTrue(slug.isidentifier() or slug.startswith("_")) + + def test_empty_name(self): + slug = _safe_app_slug("") + self.assertTrue(len(slug) > 0) + + +class TestTemplateGeneration(unittest.TestCase): + def test_truffile_yaml_contains_name(self): + yaml = _sample_truffile_yaml("My App", "my_app") + self.assertIn("My App", yaml) + self.assertIn("my_app", yaml) + + def test_truffile_yaml_is_valid_yaml(self): + import yaml as pyyaml + content = _sample_truffile_yaml("Test", "test") + parsed = pyyaml.safe_load(content) + self.assertIsInstance(parsed, dict) + self.assertIn("metadata", parsed) + + def test_foreground_template_valid_python(self): + import ast + code = _sample_foreground_py() + ast.parse(code) + + def test_background_template_valid_python(self): + import ast + code = _sample_background_py() + ast.parse(code) + + def test_generated_yaml_has_both_processes(self): + import yaml as pyyaml + content = _sample_truffile_yaml("Hybrid", "hybrid") + parsed = pyyaml.safe_load(content) + meta = parsed.get("metadata", {}) + self.assertIn("foreground", meta) + self.assertIn("background", meta) + + +class TestCmdCreate(unittest.TestCase): + def test_creates_directory_with_files(self): + with tempfile.TemporaryDirectory() as tmp: + from types import SimpleNamespace + args = SimpleNamespace(name="test-app", path=tmp) + + with patch("truffile.cli.create._load_stock_icon_bytes", return_value=(b"fake_png", "memory")): + from truffile.cli.create import cmd_create + result = cmd_create(args) + + app_dir = Path(tmp) / "test-app" + if app_dir.exists(): + self.assertTrue((app_dir / "truffile.yaml").exists()) + + def test_scaffolded_app_validates(self): + with tempfile.TemporaryDirectory() as tmp: + from types import SimpleNamespace + args = SimpleNamespace(name="valid-app", path=tmp) + + with patch("truffile.cli.create._load_stock_icon_bytes", return_value=(b"fake_png", "memory")): + from truffile.cli.create import cmd_create + cmd_create(args) + + app_dir = Path(tmp) / "valid-app" + if app_dir.exists() and (app_dir / "truffile.yaml").exists(): + from truffile.schema.app_config import validate_app_dir + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + self.assertTrue(valid, f"scaffolded app failed validation: {errors}") diff --git a/tests/test_cross_platform.py b/tests/test_cross_platform.py new file mode 100644 index 0000000..7a3297a --- /dev/null +++ b/tests/test_cross_platform.py @@ -0,0 +1,72 @@ +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from truffile.storage import StorageService, StoredState + + +class TestStoragePaths(unittest.TestCase): + def test_storage_creates_parent_dirs(self): + with tempfile.TemporaryDirectory() as tmp: + nested = Path(tmp) / "a" / "b" / "c" + s = StorageService() + s.storage_dir = nested + s.state_file = nested / "state.json" + s.state = StoredState() + s.set_token("dev-1", "tok") + self.assertTrue(s.state_file.exists()) + + def test_storage_handles_unicode_device_names(self): + with tempfile.TemporaryDirectory() as tmp: + s = StorageService() + s.storage_dir = Path(tmp) + s.state_file = Path(tmp) / "state.json" + s.state = StoredState() + s.set_token("truffle-cafรฉ-โ˜•", "tok") + self.assertEqual(s.get_token("truffle-cafรฉ-โ˜•"), "tok") + + +class TestTerminalDetection(unittest.TestCase): + def test_mushroom_disabled_when_not_tty(self): + from truffile.cli.ui import MushroomPulse + with patch("sys.stdout") as mock_stdout: + mock_stdout.isatty.return_value = False + pulse = MushroomPulse("test") + self.assertFalse(pulse.enabled) + + def test_mushroom_enabled_when_tty(self): + from truffile.cli.ui import MushroomPulse + with patch("sys.stdout") as mock_stdout: + mock_stdout.isatty.return_value = True + pulse = MushroomPulse("test") + self.assertTrue(pulse.enabled) + + +class TestPathHandling(unittest.TestCase): + def test_slug_no_path_separators(self): + from truffile.cli.create import _safe_app_slug + slug = _safe_app_slug("my/app\\name") + self.assertNotIn("/", slug) + self.assertNotIn("\\", slug) + + def test_validate_handles_spaces_in_path(self): + with tempfile.TemporaryDirectory() as tmp: + spaced = Path(tmp) / "my app dir" + spaced.mkdir() + (spaced / "truffile.yaml").write_text("metadata:\n name: test\n") + from truffile.schema.app_config import validate_app_dir + validate_app_dir(spaced) + + +class TestWindowsCompat(unittest.TestCase): + def test_storage_works_regardless_of_os(self): + with tempfile.TemporaryDirectory() as tmp: + s = StorageService() + s.storage_dir = Path(tmp) + s.state_file = Path(tmp) / "state.json" + s.state = StoredState() + s.set_token("dev-1", "tok") + self.assertEqual(s.get_token("dev-1"), "tok") diff --git a/tests/test_deploy_plan.py b/tests/test_deploy_plan.py new file mode 100644 index 0000000..5af0b62 --- /dev/null +++ b/tests/test_deploy_plan.py @@ -0,0 +1,100 @@ +import tempfile +import unittest +from pathlib import Path + +from truffile.deploy.builder import ( + build_deploy_plan, + _normalize_cmd, + _bundle_id_from_name, + _env_map_to_list, +) + + +class TestNormalizeCmd(unittest.TestCase): + def test_list_passthrough(self): + result = _normalize_cmd(["python", "app.py"]) + # returns (binary, args) tuple + self.assertIsNotNone(result) + binary = result[0] if isinstance(result, tuple) else result + self.assertIn("python", str(binary)) + + def test_string_input(self): + result = _normalize_cmd("python app.py") + self.assertIsNotNone(result) + + def test_empty_list(self): + binary, args = _normalize_cmd([]) + self.assertEqual(binary, "") + self.assertEqual(args, []) + + +class TestBundleIdFromName(unittest.TestCase): + def test_simple_name(self): + bid = _bundle_id_from_name("My App") + self.assertIsInstance(bid, str) + self.assertFalse(" " in bid) + + def test_special_chars_removed(self): + bid = _bundle_id_from_name("My App! (v2)") + self.assertFalse("!" in bid) + self.assertFalse("(" in bid) + + +class TestEnvMapToList(unittest.TestCase): + def test_converts_dict(self): + result = _env_map_to_list({"KEY": "value", "FOO": "bar"}) + self.assertIsInstance(result, list) + joined = " ".join(result) + self.assertIn("KEY", joined) + self.assertIn("value", joined) + + def test_empty_dict(self): + result = _env_map_to_list({}) + self.assertEqual(len(result), 0) + + def test_none_input(self): + result = _env_map_to_list(None) + self.assertEqual(len(result), 0) + + +class TestBuildDeployPlan(unittest.TestCase): + def _make_app_dir(self, tmp: str, yaml_content: str, files: dict[str, str] | None = None) -> Path: + app_dir = Path(tmp) / "test-app" + app_dir.mkdir() + (app_dir / "truffile.yaml").write_text(yaml_content) + for name, content in (files or {}).items(): + (app_dir / name).write_text(content) + return app_dir + + def test_foreground_only(self): + config = { + "metadata": { + "name": "FG App", + "bundle_id": "org.test.fg", + "description": "FG only", + "foreground": { + "process": {"cmd": ["python", "fg.py"]}, + }, + }, + } + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app_dir(tmp, "", {"fg.py": "pass"}) + plan = build_deploy_plan(config=config, app_dir=app_dir, app_type="focus") + self.assertIsNotNone(plan) + + def test_background_only(self): + config = { + "metadata": { + "name": "BG App", + "bundle_id": "org.test.bg", + "description": "BG only", + "background": { + "process": {"cmd": ["python", "bg.py"]}, + "default_schedule": {"type": "interval", "interval": {"duration": "30m"}}, + }, + }, + } + with tempfile.TemporaryDirectory() as tmp: + app_dir = self._make_app_dir(tmp, "", {"bg.py": "pass"}) + plan = build_deploy_plan(config=config, app_dir=app_dir, app_type="ambient") + self.assertIsNotNone(plan) diff --git a/tests/test_runtime_policy.py b/tests/test_runtime_policy.py new file mode 100644 index 0000000..fd425a4 --- /dev/null +++ b/tests/test_runtime_policy.py @@ -0,0 +1,125 @@ +import unittest + +from truffile.schema.runtime_policy import ( + parse_runtime_policy, + _parse_duration, + _parse_time_of_day, + _parse_daily_window, +) + + +class TestParseDuration(unittest.TestCase): + def test_minutes(self): + d = _parse_duration("30m", ctx="test") + self.assertEqual(d.seconds, 30 * 60) + + def test_hours(self): + d = _parse_duration("2h", ctx="test") + self.assertEqual(d.seconds, 2 * 3600) + + def test_seconds(self): + d = _parse_duration("90s", ctx="test") + self.assertEqual(d.seconds, 90) + + def test_milliseconds(self): + d = _parse_duration("500ms", ctx="test") + self.assertEqual(d.seconds, 0) + self.assertGreater(d.nanos, 0) + + def test_invalid_format_raises(self): + with self.assertRaises((ValueError, Exception)): + _parse_duration("xyz", ctx="test") + + def test_empty_raises(self): + with self.assertRaises((ValueError, Exception)): + _parse_duration("", ctx="test") + + def test_zero_duration(self): + d = _parse_duration("0s", ctx="test") + self.assertEqual(d.seconds, 0) + + +class TestParseTimeOfDay(unittest.TestCase): + def test_hhmm(self): + result = _parse_time_of_day("09:30", ctx="test") + self.assertIsNotNone(result) + + def test_hhmmss(self): + result = _parse_time_of_day("14:30:45", ctx="test") + self.assertIsNotNone(result) + + def test_midnight(self): + result = _parse_time_of_day("00:00", ctx="test") + self.assertIsNotNone(result) + + def test_end_of_day(self): + result = _parse_time_of_day("23:59", ctx="test") + self.assertIsNotNone(result) + + def test_invalid_format_raises(self): + with self.assertRaises((ValueError, Exception)): + _parse_time_of_day("noon", ctx="test") + + def test_invalid_hour_raises(self): + with self.assertRaises((ValueError, Exception)): + _parse_time_of_day("25:00", ctx="test") + + def test_invalid_minute_raises(self): + with self.assertRaises((ValueError, Exception)): + _parse_time_of_day("12:60", ctx="test") + + +class TestParseDailyWindow(unittest.TestCase): + def test_full_day(self): + result = _parse_daily_window("00:00-23:59", ctx="test") + self.assertIsNotNone(result) + + def test_business_hours(self): + result = _parse_daily_window("09:00-17:00", ctx="test") + self.assertIsNotNone(result) + + def test_invalid_format_raises(self): + with self.assertRaises((ValueError, Exception)): + _parse_daily_window("not-a-window", ctx="test") + + +class TestParseRuntimePolicy(unittest.TestCase): + def test_interval_basic(self): + config = { + "type": "interval", + "interval": {"duration": "30m"}, + } + policy = parse_runtime_policy(config) + self.assertTrue(policy.HasField("interval")) + + def test_always(self): + config = {"type": "always"} + policy = parse_runtime_policy(config) + self.assertTrue(policy.HasField("always")) + + def test_times_basic(self): + config = { + "type": "times", + "times": {"run_times": ["09:00", "18:00"]}, + } + policy = parse_runtime_policy(config) + self.assertTrue(policy.HasField("times")) + + def test_interval_with_daily_window(self): + config = { + "type": "interval", + "interval": { + "duration": "1h", + "schedule": {"daily_window": "08:00-22:00"}, + }, + } + policy = parse_runtime_policy(config) + self.assertTrue(policy.HasField("interval")) + + def test_missing_type_raises(self): + with self.assertRaises((ValueError, KeyError)): + parse_runtime_policy({}) + + def test_unknown_type_raises(self): + with self.assertRaises((ValueError, KeyError)): + parse_runtime_policy({"type": "cron"}) diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 0000000..332777b --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,89 @@ +import json +import os +import stat +import tempfile +import unittest +from pathlib import Path + +from truffile.storage import StorageService, StoredState + + +class TestStorageRoundTrip(unittest.TestCase): + def _make_storage(self, tmp_dir: str) -> StorageService: + storage = StorageService() + storage.storage_dir = Path(tmp_dir) + storage.state_file = Path(tmp_dir) / "state.json" + storage.state = StoredState() + return storage + + def test_set_and_get_token(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + s.set_token("truffle-1234", "tok_abc") + self.assertEqual(s.get_token("truffle-1234"), "tok_abc") + + def test_get_nonexistent_returns_none(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + self.assertIsNone(s.get_token("truffle-9999")) + + def test_list_devices(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + s.set_token("truffle-1", "tok_1") + s.set_token("truffle-2", "tok_2") + devices = s.list_devices() + self.assertEqual(len(devices), 2) + + def test_remove_device(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + s.set_token("truffle-1", "tok_1") + s.remove_device("truffle-1") + self.assertIsNone(s.get_token("truffle-1")) + + def test_clear_all(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + s.set_token("truffle-1", "tok_1") + s.set_token("truffle-2", "tok_2") + s.clear_all() + self.assertEqual(len(s.list_devices()), 0) + + def test_set_last_used(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + s.set_token("truffle-1", "tok_1") + s.set_last_used("truffle-1") + self.assertEqual(s.state.last_used_device, "truffle-1") + + def test_overwrite_token(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + s.set_token("truffle-1", "tok_old") + s.set_token("truffle-1", "tok_new") + self.assertEqual(s.get_token("truffle-1"), "tok_new") + + def test_state_persists_to_disk(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + s.set_token("truffle-1", "tok_1") + self.assertTrue(s.state_file.exists()) + + def test_corrupt_state_file_recovers(self): + with tempfile.TemporaryDirectory() as tmp: + s = self._make_storage(tmp) + s.state_file.write_text("not json {{{") + state = s._load_state() + self.assertIsNotNone(state) + + +class TestStorageFilePermissions(unittest.TestCase): + def test_token_file_created(self): + with tempfile.TemporaryDirectory() as tmp: + s = StorageService() + s.storage_dir = Path(tmp) + s.state_file = Path(tmp) / "state.json" + s.state = StoredState() + s.set_token("truffle-1", "tok_secret") + self.assertTrue(s.state_file.exists()) diff --git a/tests/test_transport_client.py b/tests/test_transport_client.py index db2700d..55a6286 100644 --- a/tests/test_transport_client.py +++ b/tests/test_transport_client.py @@ -1,5 +1,6 @@ import asyncio import sys +from pathlib import Path from unittest.mock import Mock import truffile @@ -36,7 +37,7 @@ def fake_insecure_channel(address, options=None): def test_init_prepends_repo_root_for_bundled_truffle(monkeypatch): - repo_root = "/Users/truffle/work/truffile" + repo_root = str(Path(truffile.__file__).resolve().parent.parent) monkeypatch.setattr(sys, "path", ["/tmp/external"]) truffile._ensure_bundled_truffle_on_path() From 2a85face13201954158bddfc612c74ed69e5f6c0 Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sat, 4 Apr 2026 20:00:36 -0700 Subject: [PATCH 06/12] bring in interactive input during deploy --- tests/test_deploy_plan.py | 2 +- truffile/_version.py | 6 +- truffile/cli/__init__.py | 2 + truffile/cli/chat.py | 1118 ----------------------------- truffile/deploy/__init__.py | 3 +- truffile/deploy/builder.py | 213 ++---- truffile/deploy/plan.py | 115 +++ truffile/deploy/steps/__init__.py | 3 + truffile/deploy/steps/bash.py | 39 + truffile/deploy/steps/files.py | 49 ++ truffile/deploy/steps/text.py | 95 +++ truffile/storage.py | 1 + 12 files changed, 361 insertions(+), 1285 deletions(-) delete mode 100644 truffile/cli/chat.py create mode 100644 truffile/deploy/plan.py create mode 100644 truffile/deploy/steps/__init__.py create mode 100644 truffile/deploy/steps/bash.py create mode 100644 truffile/deploy/steps/files.py create mode 100644 truffile/deploy/steps/text.py diff --git a/tests/test_deploy_plan.py b/tests/test_deploy_plan.py index 5af0b62..11e3949 100644 --- a/tests/test_deploy_plan.py +++ b/tests/test_deploy_plan.py @@ -2,7 +2,7 @@ import unittest from pathlib import Path -from truffile.deploy.builder import ( +from truffile.deploy.plan import ( build_deploy_plan, _normalize_cmd, _bundle_id_from_name, diff --git a/truffile/_version.py b/truffile/_version.py index b4a3585..ed2a65f 100644 --- a/truffile/_version.py +++ b/truffile/_version.py @@ -18,7 +18,7 @@ commit_id: str | None __commit_id__: str | None -__version__ = version = '0.1.36.dev1' -__version_tuple__ = version_tuple = (0, 1, 36, 'dev1') +__version__ = version = '0.1.36.dev5' +__version_tuple__ = version_tuple = (0, 1, 36, 'dev5') -__commit_id__ = commit_id = 'g6d69eecf1' +__commit_id__ = commit_id = 'gfd5b47a0e' diff --git a/truffile/cli/__init__.py b/truffile/cli/__init__.py index a39e362..9b3bf5f 100644 --- a/truffile/cli/__init__.py +++ b/truffile/cli/__init__.py @@ -46,6 +46,8 @@ def main() -> int: dep_p = sub.add_parser("deploy", help="deploy app to device") dep_p.add_argument("path", nargs="?", default=".") dep_p.add_argument("--shell", action="store_true") + dep_p.add_argument("--interactive", action="store_true") + dep_p.add_argument("--dry-run", action="store_true") dep_p.add_argument("--no-finalize", action="store_true") # list diff --git a/truffile/cli/chat.py b/truffile/cli/chat.py deleted file mode 100644 index c2392e3..0000000 --- a/truffile/cli/chat.py +++ /dev/null @@ -1,1118 +0,0 @@ -import argparse -import base64 -import contextlib -import json -import mimetypes -import os -import re -import select -import signal -import sys -import threading -import time -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Callable - -import httpx - -from .ui import C, MUSHROOM, SUPPORTED_SERVER_MIME_TYPES, MushroomPulse, error, success, info, warn -from .connect import _resolve_connected_device -from .models import _fetch_models_payload, _pick_model_interactive, _default_model - -try: - import readline -except Exception: - readline = None - -try: - import termios - import tty -except Exception: - termios = None - tty = None - -DEFAULT_SYSTEM_PROMPT = None - -REPL_COMMANDS = [ - "/help", "/", "/history", "/reset", "/models", "/config", - "/reasoning", "/stream", "/json", "/tools", "/max_tokens", - "/temperature", "/top_p", "/max_rounds", "/system", "/mcp", - "/attach", "/exit", "/quit", -] - -class ChatSettings: - model: str - system_prompt: str | None = DEFAULT_SYSTEM_PROMPT - reasoning: bool = True - stream: bool = True - json_mode: bool = False - max_tokens: int = 2048 - temperature: float | None = None - top_p: float | None = None - default_tools: bool = True - max_tool_rounds: int = 8 - - -class ChatMCPClient: - def __init__(self) -> None: - self._group: Any | None = None - self.endpoint: str | None = None - - @property - def connected(self) -> bool: - return self._group is not None - - async def connect_streamable_http(self, endpoint: str) -> None: - from mcp.client.session_group import ClientSessionGroup, StreamableHttpParameters - - await self.disconnect() - group = ClientSessionGroup() - await group.__aenter__() - try: - await group.connect_to_server(StreamableHttpParameters(url=endpoint)) - except Exception: - with contextlib.suppress(Exception): - await group.__aexit__(None, None, None) - raise - self._group = group - self.endpoint = endpoint - - async def disconnect(self) -> None: - if self._group is None: - self.endpoint = None - return - group = self._group - self._group = None - self.endpoint = None - with contextlib.suppress(Exception): - await group.__aexit__(None, None, None) - - def list_tool_names(self) -> list[str]: - if self._group is None: - return [] - names: list[str] = [] - for _server_name, tool in self._group.list_tools(): - name = getattr(tool, "name", None) - if isinstance(name, str): - names.append(name) - return sorted(set(names)) - - def has_tool(self, name: str) -> bool: - if self._group is None: - return False - try: - tool = self._group.get_tool(name) - return tool is not None - except Exception: - return False - - def build_openai_tools(self) -> list[dict[str, Any]]: - if self._group is None: - return [] - tools: list[dict[str, Any]] = [] - for _server_name, tool in self._group.list_tools(): - name = getattr(tool, "name", None) - if not isinstance(name, str) or not name: - continue - description = str(getattr(tool, "description", "") or "") - schema = getattr(tool, "inputSchema", None) - if not isinstance(schema, dict): - schema = {"type": "object", "properties": {}} - if schema.get("type") != "object": - schema = {"type": "object", "properties": {}} - tools.append( - { - "type": "function", - "function": { - "name": name, - "description": description, - "parameters": schema, - }, - } - ) - return tools - - async def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: - if self._group is None: - return {"error": "mcp not connected"} - try: - result = await self._group.call_tool(name, arguments) - content: list[dict[str, Any]] = [] - for part in result.content: - if hasattr(part, "model_dump"): - content.append(part.model_dump()) # type: ignore[call-arg] - elif isinstance(part, dict): - content.append(part) - else: - content.append({"value": str(part)}) - return { - "is_error": bool(result.isError), - "structured_content": result.structuredContent, - "content": content, - } - except Exception as exc: - return {"error": "mcp call failed", "tool": name, "detail": str(exc)} - - -def _print_chat_config(settings: ChatSettings, mcp_client: ChatMCPClient) -> None: - print(f"{C.BLUE}chat config{C.RESET}") - print(f" {C.DIM}model:{C.RESET} {settings.model}") - print(f" {C.DIM}reasoning:{C.RESET} {settings.reasoning}") - print(f" {C.DIM}stream:{C.RESET} {settings.stream}") - print(f" {C.DIM}json:{C.RESET} {settings.json_mode}") - print(f" {C.DIM}tools:{C.RESET} {settings.default_tools}") - print(f" {C.DIM}max_tokens:{C.RESET} {settings.max_tokens}") - print(f" {C.DIM}temperature:{C.RESET} {settings.temperature}") - print(f" {C.DIM}top_p:{C.RESET} {settings.top_p}") - print(f" {C.DIM}max_rounds:{C.RESET} {settings.max_tool_rounds}") - print(f" {C.DIM}system:{C.RESET} {settings.system_prompt or ''}") - print(f" {C.DIM}mcp:{C.RESET} {mcp_client.endpoint or ''}") - - -def _parse_on_off(value: str) -> bool | None: - v = value.strip().lower() - if v in {"on", "true", "1", "yes"}: - return True - if v in {"off", "false", "0", "no"}: - return False - return None - - -def _resolve_image_path(raw_path: str) -> Path: - path = Path(raw_path).expanduser().resolve() - if not path.is_file(): - raise FileNotFoundError(f"image file not found: {path}") - return path - - -def _guess_mime_type(path: Path) -> str: - mime, _ = mimetypes.guess_type(str(path)) - return mime or "image/jpeg" - - -def _normalize_image_for_server(image_bytes: bytes, mime: str) -> tuple[bytes, str, bool]: - mime_l = mime.lower() - if mime_l in SUPPORTED_SERVER_MIME_TYPES: - return image_bytes, mime_l, False - try: - from PIL import Image - except Exception as exc: - raise RuntimeError( - f"image mime {mime!r} is not supported by server decoder and Pillow is unavailable: {exc}" - ) from exc - - from io import BytesIO - - try: - with Image.open(BytesIO(image_bytes)) as im: - rgb = im.convert("RGB") - out = BytesIO() - rgb.save(out, format="PNG") - return out.getvalue(), "image/png", True - except Exception as exc: - raise RuntimeError(f"failed to transcode unsupported image mime {mime!r}: {exc}") from exc - - -def _resolve_image_bytes_and_mime(image_path_or_url: str) -> tuple[bytes, str, str]: - if image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://"): - with httpx.Client(timeout=60.0) as client: - resp = client.get(image_path_or_url) - resp.raise_for_status() - content_type = (resp.headers.get("Content-Type") or "").split(";")[0].strip().lower() - mime = content_type if content_type.startswith("image/") else "image/jpeg" - size_kib = len(resp.content) / 1024.0 - image_bytes, mime, transcoded = _normalize_image_for_server(resp.content, mime) - desc = f"url={image_path_or_url} size={size_kib:.1f} KiB mime={mime}" - if transcoded: - desc += " (transcoded)" - return image_bytes, mime, desc - - path = _resolve_image_path(image_path_or_url) - size_kib = path.stat().st_size / 1024.0 - mime = _guess_mime_type(path) - image_bytes, mime, transcoded = _normalize_image_for_server(path.read_bytes(), mime) - desc = f"path={path} size={size_kib:.1f} KiB mime={mime}" - if transcoded: - desc += " (transcoded)" - return image_bytes, mime, desc - - -def _to_data_url(image_bytes: bytes, mime: str) -> str: - payload = base64.b64encode(image_bytes).decode("ascii") - return f"data:{mime};base64,{payload}" - - -def _make_user_message(text: str, image_data_url: str | None) -> dict[str, Any]: - if image_data_url is None: - return {"role": "user", "content": text} - return { - "role": "user", - "content": [ - {"type": "text", "text": text}, - {"type": "image_url", "image_url": {"url": image_data_url}}, - ], - } - - -def _build_default_tools() -> list[dict[str, Any]]: - return [ - { - "type": "function", - "function": { - "name": "web_search", - "description": "Search the web for a query and return top results.", - "parameters": { - "type": "object", - "properties": { - "query": {"type": "string", "description": "Search query."}, - "max_results": { - "type": "integer", - "description": "Number of results to return (1-10).", - "default": 5, - }, - }, - "required": ["query"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "web_fetch", - "description": "Fetch and extract readable text from a URL.", - "parameters": { - "type": "object", - "properties": { - "url": {"type": "string", "description": "Absolute http/https URL."}, - "max_chars": { - "type": "integer", - "description": "Max number of characters to return (500-20000).", - "default": 8000, - }, - }, - "required": ["url"], - }, - }, - }, - ] - - -def _tool_web_search(arguments: dict[str, Any]) -> dict[str, Any]: - query = str(arguments.get("query", "")).strip() - if not query: - return {"error": "query is required"} - max_results = arguments.get("max_results", 5) - try: - max_results = int(max_results) - except (TypeError, ValueError): - max_results = 5 - max_results = max(1, min(max_results, 10)) - try: - from ddgs import DDGS - except Exception as exc: - return { - "error": "ddgs is not installed or failed to import", - "detail": str(exc), - "hint": "pip install ddgs", - } - rows: list[dict[str, Any]] = [] - try: - with DDGS() as ddgs: - for r in ddgs.text(query, max_results=max_results): - if len(rows) >= max_results: - break - rows.append( - { - "title": r.get("title"), - "url": r.get("href") or r.get("url"), - "snippet": r.get("body") or r.get("snippet"), - } - ) - except Exception as exc: - return {"error": "web_search failed", "detail": str(exc)} - return {"query": query, "count": len(rows), "results": rows} - - -def _tool_web_fetch(arguments: dict[str, Any]) -> dict[str, Any]: - url = str(arguments.get("url", "")).strip() - if not url: - return {"error": "url is required"} - max_chars = arguments.get("max_chars", 8000) - try: - max_chars = int(max_chars) - except (TypeError, ValueError): - max_chars = 8000 - max_chars = max(500, min(max_chars, 20000)) - try: - import trafilatura - except Exception as exc: - return { - "error": "trafilatura is not installed or failed to import", - "detail": str(exc), - "hint": "pip install trafilatura", - } - try: - downloaded = trafilatura.fetch_url(url) - if not downloaded: - return {"error": "failed to download url", "url": url} - text = trafilatura.extract(downloaded, include_links=False, include_images=False) - if not text: - return {"error": "failed to extract readable text", "url": url} - text = text.strip() - truncated = len(text) > max_chars - return { - "url": url, - "content": text[:max_chars], - "truncated": truncated, - "content_chars": min(len(text), max_chars), - } - except Exception as exc: - return {"error": "web_fetch failed", "url": url, "detail": str(exc)} - - -def _execute_default_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: - if name == "web_search": - return _tool_web_search(arguments) - if name == "web_fetch": - return _tool_web_fetch(arguments) - return {"error": f"unknown tool '{name}'"} - - -def _print_history(messages: list[dict[str, Any]]) -> None: - for idx, msg in enumerate(messages): - role = str(msg.get("role", "unknown")) - if role == "assistant" and msg.get("tool_calls"): - text = f"[tool_calls={len(msg.get('tool_calls') or [])}]" - else: - content = msg.get("content", "") - if isinstance(content, list): - text = json.dumps(content, ensure_ascii=True) - else: - text = str(content) - text = text.replace("\n", " ") - if len(text) > 160: - text = text[:157] + "..." - print(f"{idx:03d} {role:9s} {text}") - - -def _build_chat_payload( - *, - model: str, - messages: list[dict[str, Any]], - settings: ChatSettings, - stream: bool, - tools: list[dict[str, Any]] | None, -) -> dict[str, Any]: - body: dict[str, Any] = { - "model": model, - "messages": messages, - "stream": stream, - "reasoning": {"enabled": bool(settings.reasoning)}, - "max_tokens": int(settings.max_tokens), - } - if settings.temperature is not None: - body["temperature"] = settings.temperature - if settings.top_p is not None: - body["top_p"] = settings.top_p - if stream: - body["stream_options"] = {"include_usage": True} - if tools: - body["tools"] = tools - body["tool_choice"] = "auto" - return body - - -def _print_reasoning_and_response(reasoning_text: str, response_text: str, show_reasoning: bool) -> None: - if show_reasoning and reasoning_text: - print(f"{C.GRAY}thinking:{C.RESET}") - print(f"{C.GRAY}{reasoning_text}{C.RESET}") - if response_text: - print() - if response_text: - print(response_text) - - -def _print_repl_commands(prefix: str | None = None) -> None: - command_pool = [cmd for cmd in REPL_COMMANDS if cmd != "/"] - if prefix is None: - matches = command_pool - else: - matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] - if not matches: - print(f"{C.YELLOW}no command matches: {prefix}{C.RESET}") - return - rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in matches) - print(f"commands: {rendered}") - - -def _install_repl_completer(commands: list[str]) -> Callable[[], None] | None: - if readline is None: - return None - try: - prev_completer = readline.get_completer() - prev_delims = readline.get_completer_delims() - prev_display_hook = getattr(readline, "get_completion_display_matches_hook", lambda: None)() - readline.parse_and_bind("tab: complete") - readline.parse_and_bind("set show-all-if-ambiguous on") - readline.parse_and_bind("set show-all-if-unmodified on") - readline.parse_and_bind("set completion-ignore-case on") - readline.set_completer_delims(" \t\n") - matches: list[str] = [] - - def _complete(text: str, state: int) -> str | None: - nonlocal matches - if state == 0: - buffer = readline.get_line_buffer().lstrip() - if buffer.startswith("/"): - prefix = buffer.split()[0] - command_pool = [cmd for cmd in commands if cmd != "/"] - if prefix == "/": - matches = command_pool - else: - matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] - else: - matches = [] - if state < len(matches): - return matches[state] - return None - - readline.set_completer(_complete) - if hasattr(readline, "set_completion_display_matches_hook"): - def _display_matches(substitution: str, display_matches: list[str], longest_match_length: int) -> None: - del substitution, longest_match_length - if not display_matches: - return - print() - rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in display_matches) - print(f"commands: {rendered}") - try: - readline.redisplay() - except Exception: - pass - readline.set_completion_display_matches_hook(_display_matches) - - def _cleanup() -> None: - try: - readline.set_completer(prev_completer) - readline.set_completer_delims(prev_delims) - if hasattr(readline, "set_completion_display_matches_hook"): - readline.set_completion_display_matches_hook(prev_display_hook) - except Exception: - pass - - return _cleanup - except Exception: - return None - - -class StreamAbortWatcher: - def __init__(self) -> None: - self.enabled = bool(sys.stdin.isatty() and termios is not None and tty is not None) - self._fd: int | None = None - self._old_attrs: Any = None - self._thread: threading.Thread | None = None - self._stop = threading.Event() - self._abort_reason: str | None = None - - def __enter__(self) -> "StreamAbortWatcher": - if not self.enabled: - return self - try: - self._fd = sys.stdin.fileno() - self._old_attrs = termios.tcgetattr(self._fd) - tty.setcbreak(self._fd) - except Exception: - self.enabled = False - return self - self._thread = threading.Thread(target=self._watch, daemon=True) - self._thread.start() - return self - - def _watch(self) -> None: - if self._fd is None: - return - while not self._stop.is_set(): - try: - ready, _, _ = select.select([self._fd], [], [], 0.1) - except Exception: - return - if not ready: - continue - try: - ch = os.read(self._fd, 1) - except Exception: - continue - if not ch: - continue - if ch == b"\x1b": - self._abort_reason = "esc" - self._stop.set() - return - - def aborted(self) -> bool: - return self._abort_reason is not None - - def __exit__(self, exc_type, exc, tb) -> bool: - self._stop.set() - if self._thread: - self._thread.join(timeout=0.2) - if self.enabled and self._fd is not None and self._old_attrs is not None: - try: - termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_attrs) - except Exception: - pass - return False - - -def _run_single_chat_request( - *, - client: httpx.Client, - url: str, - headers: dict[str, str], - payload: dict[str, Any], - settings: ChatSettings, - stream: bool, -) -> tuple[dict[str, Any], dict[str, Any] | None, bool]: - wait_anim = MushroomPulse("thinking") - wait_anim.start() - if stream: - content_parts: list[str] = [] - reasoning_parts: list[str] = [] - usage: dict[str, Any] | None = None - tool_calls_by_index: dict[int, dict[str, Any]] = {} - reasoning_stream_started = False - interrupted = False - first_event_seen = False - - try: - with StreamAbortWatcher() as abort_watcher: - with client.stream("POST", url, headers=headers, json=payload) as resp: - resp.raise_for_status() - for raw in resp.iter_lines(): - if abort_watcher.aborted(): - interrupted = True - break - if not raw: - continue - line = raw.strip() - if not line.startswith("data:"): - continue - data = line[len("data:"):].strip() - if data == "[DONE]": - break - try: - evt = json.loads(data) - except Exception: - continue - if not first_event_seen: - wait_anim.stop() - first_event_seen = True - - if isinstance(evt.get("usage"), dict): - usage = evt.get("usage") - - choices = evt.get("choices") - if not isinstance(choices, list) or not choices: - continue - c0 = choices[0] - if not isinstance(c0, dict): - continue - delta = c0.get("delta", {}) - if not isinstance(delta, dict): - continue - - reasoning_chunk = delta.get("reasoning") - if isinstance(reasoning_chunk, str) and reasoning_chunk: - reasoning_parts.append(reasoning_chunk) - if settings.reasoning: - if not reasoning_stream_started: - print(f"{C.GRAY}thinking:{C.RESET}") - reasoning_stream_started = True - print(f"{C.GRAY}{reasoning_chunk}{C.RESET}", end="", flush=True) - - content_chunk = delta.get("content") - if isinstance(content_chunk, str) and content_chunk: - content_parts.append(content_chunk) - print(content_chunk, end="", flush=True) - - for tc in delta.get("tool_calls") or []: - if not isinstance(tc, dict): - continue - idx = tc.get("index") - if not isinstance(idx, int): - idx = len(tool_calls_by_index) - entry = tool_calls_by_index.setdefault( - idx, - { - "id": tc.get("id", ""), - "type": tc.get("type", "function"), - "function": {"name": "", "arguments": ""}, - }, - ) - if tc.get("id"): - entry["id"] = tc["id"] - if tc.get("type"): - entry["type"] = tc["type"] - fn = tc.get("function") or {} - if isinstance(fn, dict): - if fn.get("name"): - entry["function"]["name"] += str(fn["name"]) - if fn.get("arguments"): - entry["function"]["arguments"] += str(fn["arguments"]) - except KeyboardInterrupt: - interrupted = True - finally: - wait_anim.stop() - - msg: dict[str, Any] = {"role": "assistant", "content": "".join(content_parts).strip()} - reasoning_text = "".join(reasoning_parts).strip() - if reasoning_text: - msg["reasoning_content"] = reasoning_text - if tool_calls_by_index: - msg["tool_calls"] = [tool_calls_by_index[i] for i in sorted(tool_calls_by_index)] - if settings.reasoning: - if reasoning_stream_started: - print() - response_text = str(msg.get("content") or "") - if response_text: - print() - print(response_text) - elif content_parts: - print() - if interrupted: - print(f"{C.YELLOW}response interrupted{C.RESET}") - return msg, usage, interrupted - - try: - resp = client.post(url, headers=headers, json=payload, timeout=120.0) - resp.raise_for_status() - body = resp.json() - finally: - wait_anim.stop() - if settings.json_mode: - print(json.dumps(body, indent=2)) - - choices = body.get("choices", []) - c0 = choices[0] if isinstance(choices, list) and choices else {} - msg = c0.get("message", {}) if isinstance(c0, dict) else {} - if not isinstance(msg, dict): - msg = {} - out: dict[str, Any] = {"role": "assistant", "content": str(msg.get("content", "") or "")} - if isinstance(msg.get("reasoning"), str) and msg.get("reasoning"): - out["reasoning_content"] = msg["reasoning"] - if isinstance(msg.get("tool_calls"), list): - out["tool_calls"] = msg.get("tool_calls") - - _print_reasoning_and_response( - str(out.get("reasoning_content") or ""), - str(out.get("content") or ""), - bool(settings.reasoning), - ) - return out, body.get("usage") if isinstance(body.get("usage"), dict) else None, False - - -async def _run_chat_turn( - *, - client: httpx.Client, - url: str, - headers: dict[str, str], - model: str, - settings: ChatSettings, - mcp_client: ChatMCPClient, - messages: list[dict[str, Any]], - user_message: dict[str, Any], -) -> int: - messages.append(user_message) - - max_rounds = max(1, int(settings.max_tool_rounds)) - for _ in range(max_rounds): - stream = settings.stream and not settings.json_mode - tools: list[dict[str, Any]] = [] - if settings.default_tools: - tools.extend(_build_default_tools()) - if mcp_client.connected: - tools.extend(mcp_client.build_openai_tools()) - - payload = _build_chat_payload( - model=model, - messages=messages, - settings=settings, - stream=stream, - tools=tools or None, - ) - assistant_msg, usage, interrupted = _run_single_chat_request( - client=client, url=url, headers=headers, payload=payload, settings=settings, stream=stream - ) - messages.append(assistant_msg) - if isinstance(usage, dict): - image_tokens = usage.get("image_tokens") or 0 - image_tokens_part = f", image: {image_tokens}" if image_tokens else "" - image_tps_part = f", image tps: {usage.get('image_tokens_per_second') or ''}" if image_tokens else "" - print( - f"{C.DIM}[usage] tokens(prompt: {usage.get('prompt_tokens') or ''}, completion: {usage.get('completion_tokens') or ''}, total: {usage.get('total_tokens') or ''}{image_tokens_part}) usage(decode tps: {usage.get('decode_tokens_per_second') or ''}, prefill tps: {usage.get('prefill_tokens_per_second') or ''}{image_tps_part}) itl: {usage.get('itl_ms') or ''}ms ttft: {usage.get('ttft_ms') or ''}ms{C.RESET}" - ) - if interrupted: - return 130 - - tool_calls = assistant_msg.get("tool_calls") if isinstance(assistant_msg, dict) else None - if not tools or not isinstance(tool_calls, list) or not tool_calls: - return 0 - - for tool_call in tool_calls: - if not isinstance(tool_call, dict): - continue - fn = tool_call.get("function") or {} - if not isinstance(fn, dict): - continue - name = str(fn.get("name") or "") - raw_args = str(fn.get("arguments") or "{}") - try: - parsed_args = json.loads(raw_args) - except json.JSONDecodeError: - parsed_args = {"_raw": raw_args} - if name in {"web_search", "web_fetch"}: - print(f"{C.CYAN}{HAMMER} tool{C.RESET} {name}") - tool_result = _execute_default_tool(name, parsed_args) - elif mcp_client.has_tool(name): - print(f"{C.CYAN}{HAMMER} mcp{C.RESET} {name}") - tool_result = await mcp_client.call_tool(name, parsed_args) - else: - print(f"{C.YELLOW}{WARN} unknown tool{C.RESET} {name}") - tool_result = {"error": f"unknown tool '{name}'"} - messages.append( - { - "role": "tool", - "tool_call_id": tool_call.get("id", ""), - "content": json.dumps(tool_result, ensure_ascii=False), - } - ) - - warn("Reached max tool rounds without a final assistant response") - return 1 - - -async def cmd_chat(args, storage: StorageService) -> int: - prompt_words = getattr(args, "prompt_words", None) - prompt = " ".join(prompt_words).strip() if prompt_words else "" - - device, ip = await _resolve_connected_device(storage) - if not device or not ip: - return 1 - - spinner = Spinner("Resolving default model") - spinner.start() - model = await _default_model(ip) - if not model: - spinner.fail("Failed to resolve default model from IF2") - return 1 - spinner.stop(success=True) - - settings = ChatSettings(model=model) - mcp_client = ChatMCPClient() - messages: list[dict[str, Any]] = [] - pending_image_data_url: str | None = None - pending_image_desc: str | None = None - - url = f"http://{ip}/if2/v1/chat/completions" - headers = {"Content-Type": "application/json"} - - try: - spinner = Spinner(f"Connecting to {device}") - spinner.start() - with httpx.Client(timeout=None) as client: - spinner.stop(success=True) - - # REPL mode (default). - print(f"{C.DIM}model: {settings.model}{C.RESET}") - print( - f"{C.DIM}commands: /help, /history, /reset, /models, /attach, /config, /mcp, /exit{C.RESET}" - ) - - cleanup_repl = _install_repl_completer(REPL_COMMANDS) - try: - if prompt: - print(f"{C.CYAN}> {prompt}{C.RESET}") - rc = await _run_chat_turn( - client=client, - url=url, - headers=headers, - model=settings.model, - settings=settings, - mcp_client=mcp_client, - messages=messages, - user_message=_make_user_message(prompt, pending_image_data_url), - ) - if rc != 0: - if rc == 130: - return 0 - else: - return rc - else: - pending_image_data_url = None - pending_image_desc = None - - while True: - try: - line = input(f"{C.CYAN}> {C.RESET}").strip() - except EOFError: - print() - return 0 - except KeyboardInterrupt: - print() - return 0 - - if not line: - continue - if line in {"/", "/help"}: - _print_repl_commands() - continue - if line in {"/exit", "/quit"}: - return 0 - if line == "/history": - _print_history(messages) - continue - if line == "/reset": - messages = [] - if settings.system_prompt: - messages.append({"role": "system", "content": settings.system_prompt}) - pending_image_data_url = None - pending_image_desc = None - print(f"{C.YELLOW}history reset (and cleared pending attachment){C.RESET}") - continue - if line in {"/models", "/model"}: - try: - models = _fetch_models_payload(client, ip) - selected_model = _pick_model_interactive(models, settings.model) - if selected_model and selected_model != settings.model: - settings.model = selected_model - print(f"{C.GREEN}{CHECK}{C.RESET} model switched: {settings.model}") - except Exception as exc: - error(f"failed to list models: {exc}") - continue - if line == "/config": - _print_chat_config(settings, mcp_client) - continue - if line.startswith("/reasoning"): - arg = line[len("/reasoning"):].strip() - if not arg: - print(f"{C.DIM}reasoning={settings.reasoning}{C.RESET}") - continue - val = _parse_on_off(arg) - if val is None: - warn("usage: /reasoning ") - continue - settings.reasoning = val - print(f"{C.GREEN}{CHECK}{C.RESET} reasoning={settings.reasoning}") - continue - if line.startswith("/stream"): - arg = line[len("/stream"):].strip() - if not arg: - print(f"{C.DIM}stream={settings.stream}{C.RESET}") - continue - val = _parse_on_off(arg) - if val is None: - warn("usage: /stream ") - continue - settings.stream = val - print(f"{C.GREEN}{CHECK}{C.RESET} stream={settings.stream}") - continue - if line.startswith("/json"): - arg = line[len("/json"):].strip() - if not arg: - print(f"{C.DIM}json={settings.json_mode}{C.RESET}") - continue - val = _parse_on_off(arg) - if val is None: - warn("usage: /json ") - continue - settings.json_mode = val - print(f"{C.GREEN}{CHECK}{C.RESET} json={settings.json_mode}") - continue - if line.startswith("/tools"): - arg = line[len("/tools"):].strip() - if not arg: - print(f"{C.DIM}tools={settings.default_tools}{C.RESET}") - continue - val = _parse_on_off(arg) - if val is None: - warn("usage: /tools ") - continue - settings.default_tools = val - print(f"{C.GREEN}{CHECK}{C.RESET} tools={settings.default_tools}") - continue - if line.startswith("/max_tokens"): - arg = line[len("/max_tokens"):].strip() - if not arg: - print(f"{C.DIM}max_tokens={settings.max_tokens}{C.RESET}") - continue - try: - settings.max_tokens = max(1, int(arg)) - print(f"{C.GREEN}{CHECK}{C.RESET} max_tokens={settings.max_tokens}") - except ValueError: - warn("usage: /max_tokens ") - continue - if line.startswith("/temperature"): - arg = line[len("/temperature"):].strip() - if not arg: - print(f"{C.DIM}temperature={settings.temperature}{C.RESET}") - continue - if arg.lower() in {"off", "none"}: - settings.temperature = None - print(f"{C.GREEN}{CHECK}{C.RESET} temperature=None") - continue - try: - settings.temperature = float(arg) - print(f"{C.GREEN}{CHECK}{C.RESET} temperature={settings.temperature}") - except ValueError: - warn("usage: /temperature ") - continue - if line.startswith("/top_p"): - arg = line[len("/top_p"):].strip() - if not arg: - print(f"{C.DIM}top_p={settings.top_p}{C.RESET}") - continue - if arg.lower() in {"off", "none"}: - settings.top_p = None - print(f"{C.GREEN}{CHECK}{C.RESET} top_p=None") - continue - try: - settings.top_p = float(arg) - print(f"{C.GREEN}{CHECK}{C.RESET} top_p={settings.top_p}") - except ValueError: - warn("usage: /top_p ") - continue - if line.startswith("/max_rounds"): - arg = line[len("/max_rounds"):].strip() - if not arg: - print(f"{C.DIM}max_rounds={settings.max_tool_rounds}{C.RESET}") - continue - try: - settings.max_tool_rounds = max(1, int(arg)) - print(f"{C.GREEN}{CHECK}{C.RESET} max_rounds={settings.max_tool_rounds}") - except ValueError: - warn("usage: /max_rounds ") - continue - if line.startswith("/system"): - arg = line[len("/system"):].strip() - if not arg: - print(f"{C.DIM}system={settings.system_prompt or ''}{C.RESET}") - continue - if arg.lower() in {"off", "none", "clear"}: - settings.system_prompt = None - if messages and messages[0].get("role") == "system": - messages.pop(0) - print(f"{C.GREEN}{CHECK}{C.RESET} system prompt cleared") - continue - settings.system_prompt = arg - if messages and messages[0].get("role") == "system": - messages[0]["content"] = arg - else: - messages.insert(0, {"role": "system", "content": arg}) - print(f"{C.GREEN}{CHECK}{C.RESET} system prompt updated") - continue - if line.startswith("/mcp"): - parts = line.split(maxsplit=2) - if len(parts) == 1 or parts[1] == "status": - print( - f"{C.BLUE}/mcp status{C.RESET} " - f"{C.DIM}mcp={mcp_client.endpoint or ''} " - f"tools={len(mcp_client.list_tool_names())}{C.RESET}" - ) - print( - f"{C.DIM}subcommands:{C.RESET} " - f"{C.BLUE}/mcp connect {C.RESET}, " - f"{C.BLUE}/mcp tools{C.RESET}, " - f"{C.BLUE}/mcp disconnect{C.RESET}" - ) - continue - sub = parts[1].lower() - if sub == "connect": - if len(parts) < 3: - warn("usage: /mcp connect ") - continue - endpoint = parts[2].strip() - if not endpoint.startswith(("http://", "https://")): - warn("mcp endpoint must start with http:// or https://") - continue - try: - await mcp_client.connect_streamable_http(endpoint) - print( - f"{C.BLUE}/mcp connect{C.RESET} " - f"{C.GREEN}{CHECK}{C.RESET} {endpoint} " - f"({len(mcp_client.list_tool_names())} tools)" - ) - except Exception as exc: - error(f"mcp connect failed: {exc}") - continue - if sub == "disconnect": - await mcp_client.disconnect() - print(f"{C.BLUE}/mcp disconnect{C.RESET} {C.GREEN}{CHECK}{C.RESET}") - continue - if sub == "tools": - names = mcp_client.list_tool_names() - if not names: - print(f"{C.BLUE}/mcp tools{C.RESET} {C.DIM}no tools available{C.RESET}") - else: - print(f"{C.BLUE}/mcp tools{C.RESET} {', '.join(names)}") - continue - warn("usage: /mcp ") - continue - if line.startswith("/attach"): - parts = line.split(maxsplit=1) - if len(parts) != 2 or not parts[1].strip(): - warn("usage: /attach ") - continue - src = parts[1].strip() - try: - image_bytes, mime, desc = _resolve_image_bytes_and_mime(src) - pending_image_data_url = _to_data_url(image_bytes, mime) - pending_image_desc = desc - print(f"{C.GREEN}{CHECK}{C.RESET} attachment ready: {desc}") - except FileNotFoundError as exc: - error(str(exc)) - except httpx.HTTPError as exc: - error(f"failed to fetch image: {exc}") - except RuntimeError as exc: - error(str(exc)) - continue - if line.startswith("/"): - matches = [cmd for cmd in REPL_COMMANDS if cmd.startswith(line)] - if matches: - _print_repl_commands(line) - else: - warn(f"unknown command: {line}") - _print_repl_commands() - continue - - if pending_image_data_url is not None: - print(f"{C.MAGENTA}[attach]{C.RESET} sending with image: {pending_image_desc}") - rc = await _run_chat_turn( - client=client, - url=url, - headers=headers, - model=settings.model, - settings=settings, - mcp_client=mcp_client, - messages=messages, - user_message=_make_user_message(line, pending_image_data_url), - ) - if rc != 0: - if rc == 130: - return 0 - return rc - pending_image_data_url = None - pending_image_desc = None - finally: - if cleanup_repl: - cleanup_repl() - await mcp_client.disconnect() - return 0 - except Exception as e: - try: - spinner.fail(f"Chat request failed: {e}") # type: ignore[name-defined] - except Exception: - error(f"Chat request failed: {e}") - return 1 - - diff --git a/truffile/deploy/__init__.py b/truffile/deploy/__init__.py index bc40339..6826da9 100644 --- a/truffile/deploy/__init__.py +++ b/truffile/deploy/__init__.py @@ -1,3 +1,4 @@ -from .builder import build_deploy_plan, deploy_with_builder +from .plan import build_deploy_plan +from .builder import deploy_with_builder __all__ = ["build_deploy_plan", "deploy_with_builder"] diff --git a/truffile/deploy/builder.py b/truffile/deploy/builder.py index 561ccd4..e903cce 100644 --- a/truffile/deploy/builder.py +++ b/truffile/deploy/builder.py @@ -1,120 +1,18 @@ from __future__ import annotations import asyncio -import json from pathlib import Path from typing import Any, Callable from truffile.transport.client import TruffleClient +from .plan import build_deploy_plan, _env_map_to_list +from .steps import handle_bash, handle_files, handle_text - -def _normalize_cmd(cmd_list: list[str]) -> tuple[str, list[str]]: - cmd = cmd_list[0] if cmd_list[0].startswith("/") else f"/usr/bin/{cmd_list[0]}" - return cmd, cmd_list[1:] - - -def _env_map_to_list(env_dict: dict[str, str] | None) -> list[str]: - if not env_dict: - return [] - return [f"{k}={v}" for k, v in env_dict.items()] - - -def _bundle_id_from_name(name: str) -> str: - raw = "".join(ch.lower() if ch.isalnum() else "." for ch in name).strip(".") - normalized = ".".join([part for part in raw.split(".") if part]) - return normalized or "truffle.app" - - -def _extract_process(process_cfg: dict[str, Any] | None) -> tuple[str, list[str], str, list[str]]: - proc = process_cfg or {} - cmd_list = list(proc.get("cmd", ["python", "app.py"])) - cmd, args = _normalize_cmd(cmd_list) - cwd = proc.get("working_directory", proc.get("cwd", "/")) - env = _env_map_to_list(proc.get("environment", proc.get("env"))) - return cmd, args, cwd, env - - -def build_deploy_plan( - *, - config: dict[str, Any], - app_dir: Path, - app_type: str, -) -> dict[str, Any]: - meta = config["metadata"] - name = meta["name"] - description = meta.get("description", "") - bundle_id = meta.get("bundle_id") or _bundle_id_from_name(name) - icon_file = meta.get("icon_file") - icon_path = (app_dir / icon_file) if icon_file and (app_dir / icon_file).exists() else None - - fg_cfg = meta.get("foreground") - bg_cfg = meta.get("background") - new_style = isinstance(fg_cfg, dict) or isinstance(bg_cfg, dict) - - if new_style: - has_fg = isinstance(fg_cfg, dict) - has_bg = isinstance(bg_cfg, dict) - else: - has_fg = app_type == "focus" - has_bg = app_type == "ambient" - - if not has_fg and not has_bg: - raise RuntimeError("App must define foreground and/or background process config") - - fg_payload = None - bg_payload = None - exec_cwd = "/" - if has_fg: - fg_process = fg_cfg.get("process") if isinstance(fg_cfg, dict) else meta.get("process") - fg_cmd, fg_args, fg_cwd, fg_env = _extract_process(fg_process) - fg_payload = {"cmd": fg_cmd, "args": fg_args, "cwd": fg_cwd, "env": fg_env} - exec_cwd = fg_cwd - if has_bg: - bg_process = bg_cfg.get("process") if isinstance(bg_cfg, dict) else meta.get("process") - bg_cmd, bg_args, bg_cwd, bg_env = _extract_process(bg_process) - bg_payload = {"cmd": bg_cmd, "args": bg_args, "cwd": bg_cwd, "env": bg_env} - if exec_cwd == "/" and bg_cwd: - exec_cwd = bg_cwd - - if has_fg and has_bg: - finish_label = "foreground+background" - elif has_fg: - finish_label = "foreground" - else: - finish_label = "background" - - default_schedule = None - if isinstance(bg_cfg, dict): - default_schedule = bg_cfg.get("default_schedule") - elif has_bg: - default_schedule = meta.get("default_schedule") - - files_to_upload = [] - for step in config.get("steps", []): - if isinstance(step, dict) and step.get("type") == "files": - files_to_upload.extend(step.get("files", [])) - files_to_upload.extend(config.get("files", [])) - - bash_commands = [] - for step in config.get("steps", []): - if isinstance(step, dict) and step.get("type") == "bash": - bash_commands.append((step.get("name", "bash"), step["run"])) - if config.get("run"): - bash_commands.append(("Install dependencies", config["run"])) - - return { - "name": name, - "description": description, - "bundle_id": bundle_id, - "icon_path": icon_path, - "fg_payload": fg_payload, - "bg_payload": bg_payload, - "exec_cwd": exec_cwd, - "finish_label": finish_label, - "default_schedule": default_schedule, - "files_to_upload": files_to_upload, - "bash_commands": bash_commands, - } +STEP_HANDLERS = { + "bash": handle_bash, + "files": handle_files, + "text": handle_text, +} async def _wait_for_build_session_ready(client: TruffleClient, timeout_sec: float = 45.0) -> None: @@ -128,9 +26,10 @@ async def _wait_for_build_session_ready(client: TruffleClient, timeout_sec: floa except Exception as e: last_error = e await asyncio.sleep(1.0) - if last_error is not None: - raise RuntimeError(f"build session endpoint did not become ready in time: {last_error}") - raise RuntimeError("build session endpoint did not become ready in time") + msg = f"build session did not become ready in time" + if last_error: + msg += f": {last_error}" + raise RuntimeError(msg) async def deploy_with_builder( @@ -153,17 +52,6 @@ async def deploy_with_builder( interactive_shell: Callable[[str], Any], ) -> int: plan = build_deploy_plan(config=config, app_dir=app_dir, app_type=app_type) - name = plan["name"] - description = plan["description"] - bundle_id = plan["bundle_id"] - icon_path = plan["icon_path"] - fg_payload = plan["fg_payload"] - bg_payload = plan["bg_payload"] - exec_cwd = plan["exec_cwd"] - finish_label = plan["finish_label"] - default_schedule = plan["default_schedule"] - files_to_upload = plan["files_to_upload"] - bash_commands = plan["bash_commands"] spinner = spinner_cls(f"Connecting to {device}") spinner.start() @@ -177,38 +65,39 @@ async def deploy_with_builder( spinner.stop(success=True) print(f" {color_dim}Session: {client.app_uuid}{color_reset}") - for f in files_to_upload: - src = app_dir / f["source"] - dest = f["destination"] - spinner = spinner_cls(f"Uploading {src.name} {arrow} {dest}") - spinner.start() - result = await client.upload(src, dest) - spinner.stop(success=True) - print(f" {color_dim}{result.bytes} bytes, sha256={result.sha256[:12]}...{color_reset}") - - for step_name, run_cmd in bash_commands: - info(f"Running: {step_name}") - log = scrolling_log_cls(height=6, prefix=" ") - exit_code = 0 - async for ev, data in client.exec_stream(run_cmd, cwd=exec_cwd): - if ev == "log": - try: - import json - obj = json.loads(data) - line = obj.get("line", "") - except Exception: - line = data - log.add(line) - elif ev == "exit": - try: - import json - exit_code = int(json.loads(data).get("code", 0)) - except (ValueError, KeyError): - pass - log.finish() - if exit_code != 0: - error(f"Step '{step_name}' failed with exit code {exit_code}") - raise RuntimeError(f"Step '{step_name}' failed with exit code {exit_code}") + collected_env: dict[str, str] = {} + + ctx = dict( + client=client, + app_dir=app_dir, + exec_cwd=plan["exec_cwd"], + spinner_cls=spinner_cls, + scrolling_log_cls=scrolling_log_cls, + info=info, + success=success, + error=error, + color_dim=color_dim, + color_reset=color_reset, + color_bold=color_bold, + arrow=arrow, + collected_env=collected_env, + ) + + for step in plan["ordered_steps"]: + step_type = step.get("type", "") + handler = STEP_HANDLERS.get(step_type) + if handler: + await handler(step, **ctx) + + # inject collected env vars into process configs + fg_payload = plan["fg_payload"] + bg_payload = plan["bg_payload"] + if collected_env: + env_list = _env_map_to_list(collected_env) + if fg_payload: + fg_payload["env"] = fg_payload.get("env", []) + env_list + if bg_payload: + bg_payload["env"] = bg_payload.get("env", []) + env_list if interactive: print() @@ -217,20 +106,20 @@ async def deploy_with_builder( await interactive_shell(ws_url) print() - spinner = spinner_cls(f"Finishing as {finish_label} app") + spinner = spinner_cls(f"Finishing as {plan['finish_label']} app") spinner.start() await client.finish_app( - name=name, - bundle_id=bundle_id, - description=description, - icon=icon_path, + name=plan["name"], + bundle_id=plan["bundle_id"], + description=plan["description"], + icon=plan["icon_path"], foreground=fg_payload, background=bg_payload, - default_schedule=default_schedule, + default_schedule=plan["default_schedule"], ) spinner.stop(success=True) print() - success(f"Deployed: {color_bold}{name}{color_reset} ({finish_label})") + success(f"Deployed: {color_bold}{plan['name']}{color_reset} ({plan['finish_label']})") return 0 diff --git a/truffile/deploy/plan.py b/truffile/deploy/plan.py new file mode 100644 index 0000000..4e300fc --- /dev/null +++ b/truffile/deploy/plan.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + + +def _normalize_cmd(cmd_list: list[str]) -> tuple[str, list[str]]: + if not cmd_list: + return ("", []) + cmd = cmd_list[0] if cmd_list[0].startswith("/") else f"/usr/bin/{cmd_list[0]}" + return cmd, cmd_list[1:] + + +def _env_map_to_list(env_dict: dict[str, str] | None) -> list[str]: + if not env_dict: + return [] + return [f"{k}={v}" for k, v in env_dict.items()] + + +def _bundle_id_from_name(name: str) -> str: + raw = "".join(ch.lower() if ch.isalnum() else "." for ch in name).strip(".") + normalized = ".".join([part for part in raw.split(".") if part]) + return normalized or "truffle.app" + + +def _extract_process(process_cfg: dict[str, Any] | None) -> tuple[str, list[str], str, list[str]]: + proc = process_cfg or {} + cmd_list = list(proc.get("cmd", ["python", "app.py"])) + cmd, args = _normalize_cmd(cmd_list) + cwd = proc.get("working_directory", proc.get("cwd", "/")) + env = _env_map_to_list(proc.get("environment", proc.get("env"))) + return cmd, args, cwd, env + + +def build_deploy_plan( + *, + config: dict[str, Any], + app_dir: Path, + app_type: str, +) -> dict[str, Any]: + meta = config["metadata"] + name = meta["name"] + description = meta.get("description", "") + bundle_id = meta.get("bundle_id") or _bundle_id_from_name(name) + icon_file = meta.get("icon_file") + icon_path = (app_dir / icon_file) if icon_file and (app_dir / icon_file).exists() else None + + fg_cfg = meta.get("foreground") + bg_cfg = meta.get("background") + new_style = isinstance(fg_cfg, dict) or isinstance(bg_cfg, dict) + + if new_style: + has_fg = isinstance(fg_cfg, dict) + has_bg = isinstance(bg_cfg, dict) + else: + has_fg = app_type == "focus" + has_bg = app_type == "ambient" + + if not has_fg and not has_bg: + raise RuntimeError("app must define foreground and/or background process config") + + fg_payload = None + bg_payload = None + exec_cwd = "/" + + if has_fg: + fg_process = fg_cfg.get("process") if isinstance(fg_cfg, dict) else meta.get("process") + fg_cmd, fg_args, fg_cwd, fg_env = _extract_process(fg_process) + fg_payload = {"cmd": fg_cmd, "args": fg_args, "cwd": fg_cwd, "env": fg_env} + exec_cwd = fg_cwd + + if has_bg: + bg_process = bg_cfg.get("process") if isinstance(bg_cfg, dict) else meta.get("process") + bg_cmd, bg_args, bg_cwd, bg_env = _extract_process(bg_process) + bg_payload = {"cmd": bg_cmd, "args": bg_args, "cwd": bg_cwd, "env": bg_env} + if exec_cwd == "/" and bg_cwd: + exec_cwd = bg_cwd + + if has_fg and has_bg: + finish_label = "foreground+background" + elif has_fg: + finish_label = "foreground" + else: + finish_label = "background" + + default_schedule = None + if isinstance(bg_cfg, dict): + default_schedule = bg_cfg.get("default_schedule") + elif has_bg: + default_schedule = meta.get("default_schedule") + + ordered_steps = [] + for step in config.get("steps", []): + if isinstance(step, dict): + ordered_steps.append(step) + + if config.get("files"): + ordered_steps.append({"type": "files", "name": "Copy files", "files": config["files"]}) + if config.get("run"): + ordered_steps.append({"type": "bash", "name": "Install dependencies", "run": config["run"]}) + + return { + "name": name, + "description": description, + "bundle_id": bundle_id, + "icon_path": icon_path, + "fg_payload": fg_payload, + "bg_payload": bg_payload, + "exec_cwd": exec_cwd, + "finish_label": finish_label, + "default_schedule": default_schedule, + "ordered_steps": ordered_steps, + "files_to_upload": [f for s in ordered_steps if s.get("type") == "files" for f in s.get("files", [])], + "bash_commands": [(s.get("name", "bash"), s["run"]) for s in ordered_steps if s.get("type") == "bash"], + } diff --git a/truffile/deploy/steps/__init__.py b/truffile/deploy/steps/__init__.py new file mode 100644 index 0000000..d1233c7 --- /dev/null +++ b/truffile/deploy/steps/__init__.py @@ -0,0 +1,3 @@ +from .bash import handle_bash +from .files import handle_files +from .text import handle_text diff --git a/truffile/deploy/steps/bash.py b/truffile/deploy/steps/bash.py new file mode 100644 index 0000000..f942933 --- /dev/null +++ b/truffile/deploy/steps/bash.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json +from typing import Any, Callable + +from truffile.transport.client import TruffleClient + + +async def handle_bash( + step: dict[str, Any], + *, + client: TruffleClient, + exec_cwd: str, + info: Callable, + error: Callable, + scrolling_log_cls: Any, + **_kw: Any, +) -> None: + step_name = step.get("name", "bash") + run_cmd = step["run"] + info(f"Running: {step_name}") + log = scrolling_log_cls(height=6, prefix=" ") + exit_code = 0 + async for ev, data in client.exec_stream(run_cmd, cwd=exec_cwd): + if ev == "log": + try: + line = json.loads(data).get("line", "") + except Exception: + line = data + log.add(line) + elif ev == "exit": + try: + exit_code = int(json.loads(data).get("code", 0)) + except (ValueError, KeyError): + pass + log.finish() + if exit_code != 0: + error(f"Step '{step_name}' failed with exit code {exit_code}") + raise RuntimeError(f"Step '{step_name}' failed with exit code {exit_code}") diff --git a/truffile/deploy/steps/files.py b/truffile/deploy/steps/files.py new file mode 100644 index 0000000..d50a726 --- /dev/null +++ b/truffile/deploy/steps/files.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from truffile.transport.client import TruffleClient + + +async def _upload_file( + client: TruffleClient, + src: Path, + dest: str, + spinner_cls: Any, + arrow: str, + color_dim: str, + color_reset: str, +) -> None: + spinner = spinner_cls(f"Uploading {src.name} {arrow} {dest}") + spinner.start() + result = await client.upload(src, dest) + spinner.stop(success=True) + print(f" {color_dim}{result.bytes} bytes, sha256={result.sha256[:12]}...{color_reset}") + + +async def handle_files( + step: dict[str, Any], + *, + client: TruffleClient, + app_dir: Path, + spinner_cls: Any, + arrow: str, + color_dim: str, + color_reset: str, + **_kw: Any, +) -> None: + for f in step.get("files", []): + src = app_dir / f["source"] + dest = f["destination"] + + if src.is_dir(): + for child in sorted(src.rglob("*")): + if child.is_file() and "__pycache__" not in str(child): + rel = child.relative_to(src) + child_dest = f"{dest.rstrip('/')}/{rel}" + await _upload_file(client, child, child_dest, spinner_cls, arrow, color_dim, color_reset) + elif src.is_file(): + await _upload_file(client, src, dest, spinner_cls, arrow, color_dim, color_reset) + else: + raise FileNotFoundError(f"no such file: {src}") diff --git a/truffile/deploy/steps/text.py b/truffile/deploy/steps/text.py new file mode 100644 index 0000000..1ffcff5 --- /dev/null +++ b/truffile/deploy/steps/text.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import getpass +import json +from typing import Any, Callable + +from truffile.transport.client import TruffleClient + + +def _build_env_prefix(env: dict[str, str]) -> str: + if not env: + return "" + parts = [] + for key, val in env.items(): + escaped = val.replace("'", "'\\''") + parts.append(f"{key}='{escaped}'") + return " ".join(parts) + " " + + +async def handle_text( + step: dict[str, Any], + *, + client: TruffleClient, + exec_cwd: str, + info: Callable, + error: Callable, + scrolling_log_cls: Any, + collected_env: dict[str, str], + **_kw: Any, +) -> None: + step_name = step.get("name", "Configure") + content = step.get("content", "") + if content: + print() + info(step_name) + print(content.strip()) + + for field in step.get("fields", []): + label = field.get("label", field.get("name", "")) + field_type = field.get("type", "text") + placeholder = field.get("placeholder", "") + default = field.get("default", "") + prompt = f" {label}" + if placeholder: + prompt += f" ({placeholder})" + prompt += ": " + + if field_type == "password": + value = getpass.getpass(prompt) + else: + value = input(prompt) + + if not value.strip() and default: + value = default + if not value.strip() and field.get("env_default_if_empty"): + value = field["env_default_if_empty"] + + env_key = field.get("env") + if env_key: + collected_env[env_key] = value + + # run validator if present โ€” prepend env vars to the command + # since each exec call is a fresh shell + validator = step.get("validator") + if not validator: + return + + validator_cmd = validator.get("run", "") + if not validator_cmd: + return + + env_prefix = _build_env_prefix(collected_env) + full_cmd = f"{env_prefix}{validator_cmd}" + + info("Validating...") + log = scrolling_log_cls(height=4, prefix=" ") + exit_code = 0 + async for ev, data in client.exec_stream(full_cmd, cwd=exec_cwd): + if ev == "log": + try: + line = json.loads(data).get("line", "") + except Exception: + line = data + log.add(line) + elif ev == "exit": + try: + exit_code = int(json.loads(data).get("code", 0)) + except (ValueError, KeyError): + pass + log.finish() + + if exit_code != 0: + error_msg = validator.get("error_message", "Validation failed. Check your input.") + error(error_msg.strip()) + raise RuntimeError(f"Validator failed for step '{step_name}'") diff --git a/truffile/storage.py b/truffile/storage.py index b35e675..a1640fb 100644 --- a/truffile/storage.py +++ b/truffile/storage.py @@ -50,6 +50,7 @@ def save(self) -> None: "last_used_device": self.state.last_used_device, "client_user_id": self.state.client_user_id, } + self.state_file.parent.mkdir(parents=True, exist_ok=True) with open(self.state_file, "w") as f: json.dump(state_dict, f, indent=4) From a3c25b9b0e8145d27802e21c5d8f27da5981253a Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sat, 4 Apr 2026 21:59:19 -0700 Subject: [PATCH 07/12] talk to yo truff --- truffile/cli/__init__.py | 32 +- truffile/cli/chat.py | 482 +++++++++++++++ truffile/cli/infer.py | 1120 ++++++++++++++++++++++++++++++++++ truffile/cli/picker.py | 47 ++ truffile/transport/client.py | 87 +++ 5 files changed, 1760 insertions(+), 8 deletions(-) create mode 100644 truffile/cli/chat.py create mode 100644 truffile/cli/infer.py create mode 100644 truffile/cli/picker.py diff --git a/truffile/cli/__init__.py b/truffile/cli/__init__.py index 9b3bf5f..cbaedcb 100644 --- a/truffile/cli/__init__.py +++ b/truffile/cli/__init__.py @@ -20,6 +20,7 @@ def run_async(coro): def main() -> int: parser = argparse.ArgumentParser(prog="truffile", add_help=False) + parser.add_argument("--resume", action="store_true", help="resume a previous task") sub = parser.add_subparsers(dest="command") # scan @@ -61,23 +62,35 @@ def main() -> int: # models sub.add_parser("models", help="list inference models") - # chat - chat_p = sub.add_parser("chat", help="interactive chat") - chat_p.add_argument("--model", type=str, default=None) - chat_p.add_argument("--system", type=str, default=None) - chat_p.add_argument("--no-stream", action="store_true") - chat_p.add_argument("--no-tools", action="store_true") - chat_p.add_argument("--mcp", type=str, action="append", default=None) + # chat (agent runtime with apps) + chat_p = sub.add_parser("chat", help="agent chat with apps") + chat_p.add_argument("prompt", nargs="?", default=None) + chat_p.add_argument("--resume", action="store_true", help="resume a previous task") + + # infer (raw model inference) + infer_p = sub.add_parser("infer", help="raw model inference") + infer_p.add_argument("--model", type=str, default=None) + infer_p.add_argument("--system", type=str, default=None) + infer_p.add_argument("--no-stream", action="store_true") + infer_p.add_argument("--no-tools", action="store_true") + infer_p.add_argument("--mcp", type=str, action="append", default=None) # help sub.add_parser("help", help="show help") args = parser.parse_args() - if args.command is None or args.command == "help": + if args.command == "help": print_help() return 0 + if args.command is None: + from truffile.storage import StorageService + storage = StorageService() + from .chat import cmd_chat + from types import SimpleNamespace + return run_async(cmd_chat(SimpleNamespace(resume=args.resume, prompt=None), storage)) + from truffile.storage import StorageService storage = StorageService() @@ -111,6 +124,9 @@ def main() -> int: elif args.command == "chat": from .chat import cmd_chat return run_async(cmd_chat(args, storage)) + elif args.command == "infer": + from .infer import cmd_infer + return run_async(cmd_infer(args, storage)) print_help() return 1 diff --git a/truffile/cli/chat.py b/truffile/cli/chat.py new file mode 100644 index 0000000..dd08cdb --- /dev/null +++ b/truffile/cli/chat.py @@ -0,0 +1,482 @@ +from __future__ import annotations + +import asyncio +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from truffile.storage import StorageService +from truffile.client import TruffleClient, resolve_mdns +from .ui import C, MUSHROOM, DOT, CHECK, CROSS, Spinner, ScrollingLog, error, success, info, warn +from .connect import _resolve_connected_device +from .picker import pick_from_list + + +@dataclass +class TaskState: + task_id: str = "" + title: str = "" + run_state: str = "" + pending_node_id: int | None = None + result_text: str = "" + tool_calls: list[str] = field(default_factory=list) + + +CHAT_COMMANDS = { + "/help": "show available commands", + "/tasks": "list recent tasks", + "/rename ": "rename current task", + "/title": "show current task title", + "/resume": "switch to a previous task", + "/new": "start a new task", + "/apps": "list installed apps", + "/app ": "add an app to current task", + "/deploy ": "deploy an app to device", + "/delete app ": "delete an installed app", + "/delete task": "delete current task", + "/devices": "list connected devices", + "/exit": "exit chat", +} + + +def _print_update(update: Any, state: TaskState) -> None: + if update.task_id and not state.task_id: + state.task_id = update.task_id + + if update.HasField("info"): + run_state = update.info.TaskRunState.Name(update.info.run_state) if update.info.run_state else "" + if run_state: + state.run_state = run_state + title = update.info.task_title + if title: + state.title = title + + if update.HasField("error"): + msg = update.error.message if hasattr(update.error, "message") else str(update.error) + print(f"\n{C.RED}error: {msg}{C.RESET}") + + if update.HasField("streaming_step_result"): + chunk = update.streaming_step_result.partial_content + if chunk: + sys.stdout.write(chunk) + sys.stdout.flush() + + for node in update.nodes: + if not node.HasField("step"): + continue + + step = node.step + + if step.HasField("thinking"): + for s in step.thinking.cot_summaries: + print(f"{C.DIM}thinking: {s}{C.RESET}") + + for tc in step.tool_calls: + name = tc.tool_name if hasattr(tc, "tool_name") else "" + tc_summary = tc.summary if hasattr(tc, "summary") else "" + if name: + print(f"{C.CYAN}{DOT} tool: {name}{C.RESET}", end="") + if tc_summary: + print(f" {C.DIM}โ€” {tc_summary}{C.RESET}") + else: + print() + state.tool_calls.append(name) + + if step.HasField("results"): + content = step.results.content if hasattr(step.results, "content") else "" + res_summary = step.results.summary if hasattr(step.results, "summary") else "" + text = content or res_summary + if text: + state.result_text = text + + if step.HasField("user_response"): + node_id = step.user_response.node_id if hasattr(step.user_response, "node_id") else 0 + if node_id: + state.pending_node_id = node_id + + +async def _stream_task(client: TruffleClient, stream: Any, state: TaskState) -> None: + interrupted = False + try: + async for update in stream: + _print_update(update, state) + if state.pending_node_id is not None: + break + except (asyncio.CancelledError, KeyboardInterrupt): + interrupted = True + except Exception: + interrupted = True + + if interrupted and state.task_id: + print(f"\n{C.DIM}interrupting...{C.RESET}") + try: + await client.interrupt_task(state.task_id) + except Exception: + pass + + +async def _pick_task(client: TruffleClient, *, current_task_id: str = "") -> str | None: + tasks = await client.get_task_infos(max_before=15) + if not tasks: + info("no previous tasks found") + return None + + items = [ + { + "label": t["title"], + "detail": t["updated"][:16] if t["updated"] else "", + "task_id": t["task_id"], + } + for t in tasks + ] + + print() + picked = pick_from_list( + items, + label_key="label", + detail_key="detail", + active_key="task_id" if current_task_id else None, + active_value=current_task_id, + prompt="pick a task", + ) + return picked["task_id"] if picked else None + + +async def _get_apps_list(client: TruffleClient) -> list[dict]: + apps = await client.get_all_apps() + result = [] + for app in apps: + meta = app.metadata + name = meta.name if hasattr(meta, "name") else "?" + bundle_id = meta.bundle_id if hasattr(meta, "bundle_id") else "" + result.append({"name": name, "bundle_id": bundle_id, "uuid": app.uuid}) + return result + + +def _find_app_by_name(apps: list, name: str) -> dict | None: + name_lower = name.strip().lower() + for app in apps: + app_name = app.get("name", "").lower() + if app_name == name_lower or name_lower in app_name: + return app + return None + + +async def _handle_slash( + cmd: str, + client: TruffleClient, + state: TaskState, + storage: StorageService, +) -> str | None: + """returns action string or None. 'exit' to quit, 'new' for fresh task, 'switch:' to resume.""" + + parts = cmd.strip().split(maxsplit=1) + command = parts[0].lower() + arg = parts[1].strip() if len(parts) > 1 else "" + + if command == "/help": + print(f"\n{C.BOLD}commands:{C.RESET}") + for name, desc in CHAT_COMMANDS.items(): + print(f" {C.CYAN}{name}{C.RESET} โ€” {desc}") + print() + return None + + if command in ("/exit", "/quit"): + return "exit" + + if command == "/title": + print(f" {state.title or 'no title yet'}") + return None + + if command == "/rename": + if not arg: + error("usage: /rename ") + return None + if not state.task_id: + error("no active task") + return None + try: + await client.rename_task(state.task_id, arg) + state.title = arg + success(f"renamed to \"{arg}\"") + except Exception as e: + error(f"rename failed: {e}") + return None + + if command == "/tasks": + tasks = await client.get_task_infos(max_before=10) + if not tasks: + info("no tasks found") + return None + print() + for i, t in enumerate(tasks, 1): + marker = f" {C.GREEN}(current){C.RESET}" if t["task_id"] == state.task_id else "" + updated = t["updated"][:16] if t["updated"] else "" + print(f" {C.CYAN}{i}.{C.RESET} {t['title']}{marker} {C.DIM}{updated}{C.RESET}") + print() + return None + + if command in ("/resume", "/switch"): + task_id = await _pick_task(client, current_task_id=state.task_id) + return f"switch:{task_id}" if task_id else None + + if command == "/new": + return "new" + + if command == "/apps": + try: + apps_list = await _get_apps_list(client) + if not apps_list: + info("no apps installed") + return None + print(f"\n{C.BOLD}Installed apps:{C.RESET}") + for a in apps_list: + print(f" {C.CYAN}{DOT}{C.RESET} {a['name']} {C.DIM}({a['bundle_id']}){C.RESET}") + print() + except Exception as e: + error(f"failed to list apps: {e}") + return None + + if command == "/app": + if not arg: + error("usage: /app ") + return None + if not state.task_id: + error("no active task โ€” send a message first") + return None + try: + apps_list = await _get_apps_list(client) + match = _find_app_by_name(apps_list, arg) + if not match: + error(f"app \"{arg}\" not found. use /apps to see installed apps.") + return None + await client.set_task_apps(state.task_id, [match["uuid"]]) + success(f"added {match['name']} to task") + except Exception as e: + error(f"failed: {e}") + return None + + if command == "/delete": + if not arg: + error("usage: /delete app [name] or /delete task") + return None + + if arg.lower().strip() == "task" or arg.lower().startswith("task "): + task_to_delete = await _pick_task(client, current_task_id=state.task_id) + if not task_to_delete: + return None + try: + await client.delete_task(task_to_delete) + success("task deleted") + if task_to_delete == state.task_id: + return "new" + except Exception as e: + error(f"failed: {e}") + return None + + if arg.lower().strip() == "app" or arg.lower().startswith("app "): + app_name = arg[4:].strip() if len(arg) > 4 else "" + try: + apps_list = await _get_apps_list(client) + if not apps_list: + info("no apps installed") + return None + + if not app_name: + items = [{"label": a["name"], "detail": a["bundle_id"], "uuid": a["uuid"]} for a in apps_list] + print() + picked = pick_from_list(items, label_key="label", detail_key="detail", prompt="pick app to delete") + if not picked: + return None + match = {"name": picked["label"], "uuid": picked["uuid"]} + else: + match = _find_app_by_name(apps_list, app_name) + if not match: + error(f"app \"{app_name}\" not found") + return None + + await client.delete_app(match["uuid"]) + success(f"deleted {match['name']}") + except Exception as e: + error(f"failed: {e}") + return None + + error("usage: /delete app [name] or /delete task") + return None + + if command == "/deploy": + if not arg: + error("usage: /deploy ") + return None + app_dir = Path(arg).resolve() + if not app_dir.exists(): + error(f"path not found: {arg}") + return None + try: + from truffile.schema import validate_app_dir + from truffile.deploy import build_deploy_plan, deploy_with_builder + from .deploy import _interactive_shell + + valid, config, app_type, warnings, errors_list = validate_app_dir(app_dir) + if not valid: + for msg in errors_list: + error(msg) + return None + + result = await deploy_with_builder( + client=client, + config=config, + app_dir=app_dir, + app_type=app_type, + device=storage.state.last_used_device or "device", + interactive=False, + spinner_cls=Spinner, + scrolling_log_cls=ScrollingLog, + info=info, + success=success, + error=error, + color_dim=C.DIM, + color_reset=C.RESET, + color_bold=C.BOLD, + arrow="โ†’", + interactive_shell=_interactive_shell, + ) + if result == 0: + success("deploy complete") + except Exception as e: + error(f"deploy failed: {e}") + return None + + if command == "/devices": + devices = storage.list_devices() + if not devices: + info("no devices connected") + return None + current = storage.state.last_used_device + print() + for d in devices: + marker = f" {C.GREEN}(active){C.RESET}" if d == current else "" + print(f" {C.CYAN}{DOT}{C.RESET} {d}{marker}") + print() + return None + + error(f"unknown command: {command}. type /help") + return None + + +async def cmd_chat(args, storage: StorageService) -> int: + result = await _resolve_connected_device(storage) + device, ip = result + if not device or not ip: + return 1 + + token = storage.get_token(device) + if not token: + error(f"no token for {device}") + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + try: + await client.connect() + await client.check_auth() + spinner.stop(success=True) + except Exception as e: + spinner.fail(f"Could not connect to {device}") + error(str(e)) + return 1 + + resume = getattr(args, "resume", False) + state = TaskState() + stream = None + + if resume: + task_id = await _pick_task(client) + if task_id: + state.task_id = task_id + stream = client.open_existing_task_stream(task_id) + try: + async for update in stream: + _print_update(update, state) + if state.run_state: + break + except Exception: + pass + print(f"\n{C.BOLD}{MUSHROOM} truffile chat{C.RESET} โ€” resumed \"{state.title or 'task'}\"") + else: + print(f"\n{C.BOLD}{MUSHROOM} truffile chat{C.RESET} โ€” connected to {device}") + else: + print(f"\n{C.BOLD}{MUSHROOM} truffile chat{C.RESET} โ€” connected to {device}") + + print(f"{C.DIM}type a message or /help for commands. ctrl+c to interrupt. ctrl+d to exit.{C.RESET}\n") + + try: + while True: + try: + user_input = input(f"{C.BOLD}you>{C.RESET} ") + except (EOFError, KeyboardInterrupt): + print() + break + + if not user_input.strip(): + continue + + if user_input.strip().startswith("/"): + action = await _handle_slash(user_input.strip(), client, state, storage) + if action == "exit": + break + if action == "new": + state = TaskState() + stream = None + info("starting new conversation") + continue + if action and action.startswith("switch:"): + new_task_id = action.split(":", 1)[1] + state = TaskState() + state.task_id = new_task_id + stream = client.open_existing_task_stream(new_task_id) + try: + async for update in stream: + _print_update(update, state) + if state.run_state: + break + except Exception: + pass + info(f"switched to \"{state.title or 'task'}\"") + print() + continue + + print() + + if state.pending_node_id is not None: + await client.respond_to_task(state.task_id, state.pending_node_id, user_input.strip()) + state.pending_node_id = None + if stream: + await _stream_task(client, stream, state) + elif not state.task_id: + stream = client.open_task_stream(user_input.strip()) + await _stream_task(client, stream, state) + else: + state = TaskState() + stream = client.open_task_stream(user_input.strip()) + await _stream_task(client, stream, state) + + if state.result_text: + print(f"\n{state.result_text}") + print() + + except KeyboardInterrupt: + print() + if state.task_id: + try: + await client.interrupt_task(state.task_id) + except Exception: + pass + finally: + await client.close() + + return 0 diff --git a/truffile/cli/infer.py b/truffile/cli/infer.py new file mode 100644 index 0000000..0b496ee --- /dev/null +++ b/truffile/cli/infer.py @@ -0,0 +1,1120 @@ +import argparse +import base64 +import contextlib +import json +import mimetypes +import os +import re +import select +import signal +import sys +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable + +import httpx + +from .ui import C, MUSHROOM, SUPPORTED_SERVER_MIME_TYPES, Spinner, MushroomPulse, error, success, info, warn +from .connect import _resolve_connected_device +from .models import _fetch_models_payload, _pick_model_interactive, _default_model +from truffile.storage import StorageService + +try: + import readline +except Exception: + readline = None + +try: + import termios + import tty +except Exception: + termios = None + tty = None + +DEFAULT_SYSTEM_PROMPT = None + +REPL_COMMANDS = [ + "/help", "/", "/history", "/reset", "/models", "/config", + "/reasoning", "/stream", "/json", "/tools", "/max_tokens", + "/temperature", "/top_p", "/max_rounds", "/system", "/mcp", + "/attach", "/exit", "/quit", +] + +@dataclass +class ChatSettings: + model: str + system_prompt: str | None = DEFAULT_SYSTEM_PROMPT + reasoning: bool = True + stream: bool = True + json_mode: bool = False + max_tokens: int = 2048 + temperature: float | None = None + top_p: float | None = None + default_tools: bool = True + max_tool_rounds: int = 8 + + +class ChatMCPClient: + def __init__(self) -> None: + self._group: Any | None = None + self.endpoint: str | None = None + + @property + def connected(self) -> bool: + return self._group is not None + + async def connect_streamable_http(self, endpoint: str) -> None: + from mcp.client.session_group import ClientSessionGroup, StreamableHttpParameters + + await self.disconnect() + group = ClientSessionGroup() + await group.__aenter__() + try: + await group.connect_to_server(StreamableHttpParameters(url=endpoint)) + except Exception: + with contextlib.suppress(Exception): + await group.__aexit__(None, None, None) + raise + self._group = group + self.endpoint = endpoint + + async def disconnect(self) -> None: + if self._group is None: + self.endpoint = None + return + group = self._group + self._group = None + self.endpoint = None + with contextlib.suppress(Exception): + await group.__aexit__(None, None, None) + + def list_tool_names(self) -> list[str]: + if self._group is None: + return [] + names: list[str] = [] + for _server_name, tool in self._group.list_tools(): + name = getattr(tool, "name", None) + if isinstance(name, str): + names.append(name) + return sorted(set(names)) + + def has_tool(self, name: str) -> bool: + if self._group is None: + return False + try: + tool = self._group.get_tool(name) + return tool is not None + except Exception: + return False + + def build_openai_tools(self) -> list[dict[str, Any]]: + if self._group is None: + return [] + tools: list[dict[str, Any]] = [] + for _server_name, tool in self._group.list_tools(): + name = getattr(tool, "name", None) + if not isinstance(name, str) or not name: + continue + description = str(getattr(tool, "description", "") or "") + schema = getattr(tool, "inputSchema", None) + if not isinstance(schema, dict): + schema = {"type": "object", "properties": {}} + if schema.get("type") != "object": + schema = {"type": "object", "properties": {}} + tools.append( + { + "type": "function", + "function": { + "name": name, + "description": description, + "parameters": schema, + }, + } + ) + return tools + + async def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: + if self._group is None: + return {"error": "mcp not connected"} + try: + result = await self._group.call_tool(name, arguments) + content: list[dict[str, Any]] = [] + for part in result.content: + if hasattr(part, "model_dump"): + content.append(part.model_dump()) # type: ignore[call-arg] + elif isinstance(part, dict): + content.append(part) + else: + content.append({"value": str(part)}) + return { + "is_error": bool(result.isError), + "structured_content": result.structuredContent, + "content": content, + } + except Exception as exc: + return {"error": "mcp call failed", "tool": name, "detail": str(exc)} + + +def _print_chat_config(settings: ChatSettings, mcp_client: ChatMCPClient) -> None: + print(f"{C.BLUE}chat config{C.RESET}") + print(f" {C.DIM}model:{C.RESET} {settings.model}") + print(f" {C.DIM}reasoning:{C.RESET} {settings.reasoning}") + print(f" {C.DIM}stream:{C.RESET} {settings.stream}") + print(f" {C.DIM}json:{C.RESET} {settings.json_mode}") + print(f" {C.DIM}tools:{C.RESET} {settings.default_tools}") + print(f" {C.DIM}max_tokens:{C.RESET} {settings.max_tokens}") + print(f" {C.DIM}temperature:{C.RESET} {settings.temperature}") + print(f" {C.DIM}top_p:{C.RESET} {settings.top_p}") + print(f" {C.DIM}max_rounds:{C.RESET} {settings.max_tool_rounds}") + print(f" {C.DIM}system:{C.RESET} {settings.system_prompt or ''}") + print(f" {C.DIM}mcp:{C.RESET} {mcp_client.endpoint or ''}") + + +def _parse_on_off(value: str) -> bool | None: + v = value.strip().lower() + if v in {"on", "true", "1", "yes"}: + return True + if v in {"off", "false", "0", "no"}: + return False + return None + + +def _resolve_image_path(raw_path: str) -> Path: + path = Path(raw_path).expanduser().resolve() + if not path.is_file(): + raise FileNotFoundError(f"image file not found: {path}") + return path + + +def _guess_mime_type(path: Path) -> str: + mime, _ = mimetypes.guess_type(str(path)) + return mime or "image/jpeg" + + +def _normalize_image_for_server(image_bytes: bytes, mime: str) -> tuple[bytes, str, bool]: + mime_l = mime.lower() + if mime_l in SUPPORTED_SERVER_MIME_TYPES: + return image_bytes, mime_l, False + try: + from PIL import Image + except Exception as exc: + raise RuntimeError( + f"image mime {mime!r} is not supported by server decoder and Pillow is unavailable: {exc}" + ) from exc + + from io import BytesIO + + try: + with Image.open(BytesIO(image_bytes)) as im: + rgb = im.convert("RGB") + out = BytesIO() + rgb.save(out, format="PNG") + return out.getvalue(), "image/png", True + except Exception as exc: + raise RuntimeError(f"failed to transcode unsupported image mime {mime!r}: {exc}") from exc + + +def _resolve_image_bytes_and_mime(image_path_or_url: str) -> tuple[bytes, str, str]: + if image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://"): + with httpx.Client(timeout=60.0) as client: + resp = client.get(image_path_or_url) + resp.raise_for_status() + content_type = (resp.headers.get("Content-Type") or "").split(";")[0].strip().lower() + mime = content_type if content_type.startswith("image/") else "image/jpeg" + size_kib = len(resp.content) / 1024.0 + image_bytes, mime, transcoded = _normalize_image_for_server(resp.content, mime) + desc = f"url={image_path_or_url} size={size_kib:.1f} KiB mime={mime}" + if transcoded: + desc += " (transcoded)" + return image_bytes, mime, desc + + path = _resolve_image_path(image_path_or_url) + size_kib = path.stat().st_size / 1024.0 + mime = _guess_mime_type(path) + image_bytes, mime, transcoded = _normalize_image_for_server(path.read_bytes(), mime) + desc = f"path={path} size={size_kib:.1f} KiB mime={mime}" + if transcoded: + desc += " (transcoded)" + return image_bytes, mime, desc + + +def _to_data_url(image_bytes: bytes, mime: str) -> str: + payload = base64.b64encode(image_bytes).decode("ascii") + return f"data:{mime};base64,{payload}" + + +def _make_user_message(text: str, image_data_url: str | None) -> dict[str, Any]: + if image_data_url is None: + return {"role": "user", "content": text} + return { + "role": "user", + "content": [ + {"type": "text", "text": text}, + {"type": "image_url", "image_url": {"url": image_data_url}}, + ], + } + + +def _build_default_tools() -> list[dict[str, Any]]: + return [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web for a query and return top results.", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query."}, + "max_results": { + "type": "integer", + "description": "Number of results to return (1-10).", + "default": 5, + }, + }, + "required": ["query"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "web_fetch", + "description": "Fetch and extract readable text from a URL.", + "parameters": { + "type": "object", + "properties": { + "url": {"type": "string", "description": "Absolute http/https URL."}, + "max_chars": { + "type": "integer", + "description": "Max number of characters to return (500-20000).", + "default": 8000, + }, + }, + "required": ["url"], + }, + }, + }, + ] + + +def _tool_web_search(arguments: dict[str, Any]) -> dict[str, Any]: + query = str(arguments.get("query", "")).strip() + if not query: + return {"error": "query is required"} + max_results = arguments.get("max_results", 5) + try: + max_results = int(max_results) + except (TypeError, ValueError): + max_results = 5 + max_results = max(1, min(max_results, 10)) + try: + from ddgs import DDGS + except Exception as exc: + return { + "error": "ddgs is not installed or failed to import", + "detail": str(exc), + "hint": "pip install ddgs", + } + rows: list[dict[str, Any]] = [] + try: + with DDGS() as ddgs: + for r in ddgs.text(query, max_results=max_results): + if len(rows) >= max_results: + break + rows.append( + { + "title": r.get("title"), + "url": r.get("href") or r.get("url"), + "snippet": r.get("body") or r.get("snippet"), + } + ) + except Exception as exc: + return {"error": "web_search failed", "detail": str(exc)} + return {"query": query, "count": len(rows), "results": rows} + + +def _tool_web_fetch(arguments: dict[str, Any]) -> dict[str, Any]: + url = str(arguments.get("url", "")).strip() + if not url: + return {"error": "url is required"} + max_chars = arguments.get("max_chars", 8000) + try: + max_chars = int(max_chars) + except (TypeError, ValueError): + max_chars = 8000 + max_chars = max(500, min(max_chars, 20000)) + try: + import trafilatura + except Exception as exc: + return { + "error": "trafilatura is not installed or failed to import", + "detail": str(exc), + "hint": "pip install trafilatura", + } + try: + downloaded = trafilatura.fetch_url(url) + if not downloaded: + return {"error": "failed to download url", "url": url} + text = trafilatura.extract(downloaded, include_links=False, include_images=False) + if not text: + return {"error": "failed to extract readable text", "url": url} + text = text.strip() + truncated = len(text) > max_chars + return { + "url": url, + "content": text[:max_chars], + "truncated": truncated, + "content_chars": min(len(text), max_chars), + } + except Exception as exc: + return {"error": "web_fetch failed", "url": url, "detail": str(exc)} + + +def _execute_default_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + if name == "web_search": + return _tool_web_search(arguments) + if name == "web_fetch": + return _tool_web_fetch(arguments) + return {"error": f"unknown tool '{name}'"} + + +def _print_history(messages: list[dict[str, Any]]) -> None: + for idx, msg in enumerate(messages): + role = str(msg.get("role", "unknown")) + if role == "assistant" and msg.get("tool_calls"): + text = f"[tool_calls={len(msg.get('tool_calls') or [])}]" + else: + content = msg.get("content", "") + if isinstance(content, list): + text = json.dumps(content, ensure_ascii=True) + else: + text = str(content) + text = text.replace("\n", " ") + if len(text) > 160: + text = text[:157] + "..." + print(f"{idx:03d} {role:9s} {text}") + + +def _build_chat_payload( + *, + model: str, + messages: list[dict[str, Any]], + settings: ChatSettings, + stream: bool, + tools: list[dict[str, Any]] | None, +) -> dict[str, Any]: + body: dict[str, Any] = { + "model": model, + "messages": messages, + "stream": stream, + "reasoning": {"enabled": bool(settings.reasoning)}, + "max_tokens": int(settings.max_tokens), + } + if settings.temperature is not None: + body["temperature"] = settings.temperature + if settings.top_p is not None: + body["top_p"] = settings.top_p + if stream: + body["stream_options"] = {"include_usage": True} + if tools: + body["tools"] = tools + body["tool_choice"] = "auto" + return body + + +def _print_reasoning_and_response(reasoning_text: str, response_text: str, show_reasoning: bool) -> None: + if show_reasoning and reasoning_text: + print(f"{C.GRAY}thinking:{C.RESET}") + print(f"{C.GRAY}{reasoning_text}{C.RESET}") + if response_text: + print() + if response_text: + print(response_text) + + +def _print_repl_commands(prefix: str | None = None) -> None: + command_pool = [cmd for cmd in REPL_COMMANDS if cmd != "/"] + if prefix is None: + matches = command_pool + else: + matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] + if not matches: + print(f"{C.YELLOW}no command matches: {prefix}{C.RESET}") + return + rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in matches) + print(f"commands: {rendered}") + + +def _install_repl_completer(commands: list[str]) -> Callable[[], None] | None: + if readline is None: + return None + try: + prev_completer = readline.get_completer() + prev_delims = readline.get_completer_delims() + prev_display_hook = getattr(readline, "get_completion_display_matches_hook", lambda: None)() + readline.parse_and_bind("tab: complete") + readline.parse_and_bind("set show-all-if-ambiguous on") + readline.parse_and_bind("set show-all-if-unmodified on") + readline.parse_and_bind("set completion-ignore-case on") + readline.set_completer_delims(" \t\n") + matches: list[str] = [] + + def _complete(text: str, state: int) -> str | None: + nonlocal matches + if state == 0: + buffer = readline.get_line_buffer().lstrip() + if buffer.startswith("/"): + prefix = buffer.split()[0] + command_pool = [cmd for cmd in commands if cmd != "/"] + if prefix == "/": + matches = command_pool + else: + matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] + else: + matches = [] + if state < len(matches): + return matches[state] + return None + + readline.set_completer(_complete) + if hasattr(readline, "set_completion_display_matches_hook"): + def _display_matches(substitution: str, display_matches: list[str], longest_match_length: int) -> None: + del substitution, longest_match_length + if not display_matches: + return + print() + rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in display_matches) + print(f"commands: {rendered}") + try: + readline.redisplay() + except Exception: + pass + readline.set_completion_display_matches_hook(_display_matches) + + def _cleanup() -> None: + try: + readline.set_completer(prev_completer) + readline.set_completer_delims(prev_delims) + if hasattr(readline, "set_completion_display_matches_hook"): + readline.set_completion_display_matches_hook(prev_display_hook) + except Exception: + pass + + return _cleanup + except Exception: + return None + + +class StreamAbortWatcher: + def __init__(self) -> None: + self.enabled = bool(sys.stdin.isatty() and termios is not None and tty is not None) + self._fd: int | None = None + self._old_attrs: Any = None + self._thread: threading.Thread | None = None + self._stop = threading.Event() + self._abort_reason: str | None = None + + def __enter__(self) -> "StreamAbortWatcher": + if not self.enabled: + return self + try: + self._fd = sys.stdin.fileno() + self._old_attrs = termios.tcgetattr(self._fd) + tty.setcbreak(self._fd) + except Exception: + self.enabled = False + return self + self._thread = threading.Thread(target=self._watch, daemon=True) + self._thread.start() + return self + + def _watch(self) -> None: + if self._fd is None: + return + while not self._stop.is_set(): + try: + ready, _, _ = select.select([self._fd], [], [], 0.1) + except Exception: + return + if not ready: + continue + try: + ch = os.read(self._fd, 1) + except Exception: + continue + if not ch: + continue + if ch == b"\x1b": + self._abort_reason = "esc" + self._stop.set() + return + + def aborted(self) -> bool: + return self._abort_reason is not None + + def __exit__(self, exc_type, exc, tb) -> bool: + self._stop.set() + if self._thread: + self._thread.join(timeout=0.2) + if self.enabled and self._fd is not None and self._old_attrs is not None: + try: + termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_attrs) + except Exception: + pass + return False + + +def _run_single_chat_request( + *, + client: httpx.Client, + url: str, + headers: dict[str, str], + payload: dict[str, Any], + settings: ChatSettings, + stream: bool, +) -> tuple[dict[str, Any], dict[str, Any] | None, bool]: + wait_anim = MushroomPulse("thinking") + wait_anim.start() + if stream: + content_parts: list[str] = [] + reasoning_parts: list[str] = [] + usage: dict[str, Any] | None = None + tool_calls_by_index: dict[int, dict[str, Any]] = {} + reasoning_stream_started = False + interrupted = False + first_event_seen = False + + try: + with StreamAbortWatcher() as abort_watcher: + with client.stream("POST", url, headers=headers, json=payload) as resp: + resp.raise_for_status() + for raw in resp.iter_lines(): + if abort_watcher.aborted(): + interrupted = True + break + if not raw: + continue + line = raw.strip() + if not line.startswith("data:"): + continue + data = line[len("data:"):].strip() + if data == "[DONE]": + break + try: + evt = json.loads(data) + except Exception: + continue + if not first_event_seen: + wait_anim.stop() + first_event_seen = True + + if isinstance(evt.get("usage"), dict): + usage = evt.get("usage") + + choices = evt.get("choices") + if not isinstance(choices, list) or not choices: + continue + c0 = choices[0] + if not isinstance(c0, dict): + continue + delta = c0.get("delta", {}) + if not isinstance(delta, dict): + continue + + reasoning_chunk = delta.get("reasoning") + if isinstance(reasoning_chunk, str) and reasoning_chunk: + reasoning_parts.append(reasoning_chunk) + if settings.reasoning: + if not reasoning_stream_started: + print(f"{C.GRAY}thinking:{C.RESET}") + reasoning_stream_started = True + print(f"{C.GRAY}{reasoning_chunk}{C.RESET}", end="", flush=True) + + content_chunk = delta.get("content") + if isinstance(content_chunk, str) and content_chunk: + content_parts.append(content_chunk) + print(content_chunk, end="", flush=True) + + for tc in delta.get("tool_calls") or []: + if not isinstance(tc, dict): + continue + idx = tc.get("index") + if not isinstance(idx, int): + idx = len(tool_calls_by_index) + entry = tool_calls_by_index.setdefault( + idx, + { + "id": tc.get("id", ""), + "type": tc.get("type", "function"), + "function": {"name": "", "arguments": ""}, + }, + ) + if tc.get("id"): + entry["id"] = tc["id"] + if tc.get("type"): + entry["type"] = tc["type"] + fn = tc.get("function") or {} + if isinstance(fn, dict): + if fn.get("name"): + entry["function"]["name"] += str(fn["name"]) + if fn.get("arguments"): + entry["function"]["arguments"] += str(fn["arguments"]) + except KeyboardInterrupt: + interrupted = True + finally: + wait_anim.stop() + + msg: dict[str, Any] = {"role": "assistant", "content": "".join(content_parts).strip()} + reasoning_text = "".join(reasoning_parts).strip() + if reasoning_text: + msg["reasoning_content"] = reasoning_text + if tool_calls_by_index: + msg["tool_calls"] = [tool_calls_by_index[i] for i in sorted(tool_calls_by_index)] + if settings.reasoning: + if reasoning_stream_started: + print() + response_text = str(msg.get("content") or "") + if response_text: + print() + print(response_text) + elif content_parts: + print() + if interrupted: + print(f"{C.YELLOW}response interrupted{C.RESET}") + return msg, usage, interrupted + + try: + resp = client.post(url, headers=headers, json=payload, timeout=120.0) + resp.raise_for_status() + body = resp.json() + finally: + wait_anim.stop() + if settings.json_mode: + print(json.dumps(body, indent=2)) + + choices = body.get("choices", []) + c0 = choices[0] if isinstance(choices, list) and choices else {} + msg = c0.get("message", {}) if isinstance(c0, dict) else {} + if not isinstance(msg, dict): + msg = {} + out: dict[str, Any] = {"role": "assistant", "content": str(msg.get("content", "") or "")} + if isinstance(msg.get("reasoning"), str) and msg.get("reasoning"): + out["reasoning_content"] = msg["reasoning"] + if isinstance(msg.get("tool_calls"), list): + out["tool_calls"] = msg.get("tool_calls") + + _print_reasoning_and_response( + str(out.get("reasoning_content") or ""), + str(out.get("content") or ""), + bool(settings.reasoning), + ) + return out, body.get("usage") if isinstance(body.get("usage"), dict) else None, False + + +async def _run_chat_turn( + *, + client: httpx.Client, + url: str, + headers: dict[str, str], + model: str, + settings: ChatSettings, + mcp_client: ChatMCPClient, + messages: list[dict[str, Any]], + user_message: dict[str, Any], +) -> int: + messages.append(user_message) + + max_rounds = max(1, int(settings.max_tool_rounds)) + for _ in range(max_rounds): + stream = settings.stream and not settings.json_mode + tools: list[dict[str, Any]] = [] + if settings.default_tools: + tools.extend(_build_default_tools()) + if mcp_client.connected: + tools.extend(mcp_client.build_openai_tools()) + + payload = _build_chat_payload( + model=model, + messages=messages, + settings=settings, + stream=stream, + tools=tools or None, + ) + assistant_msg, usage, interrupted = _run_single_chat_request( + client=client, url=url, headers=headers, payload=payload, settings=settings, stream=stream + ) + messages.append(assistant_msg) + if isinstance(usage, dict): + image_tokens = usage.get("image_tokens") or 0 + image_tokens_part = f", image: {image_tokens}" if image_tokens else "" + image_tps_part = f", image tps: {usage.get('image_tokens_per_second') or ''}" if image_tokens else "" + print( + f"{C.DIM}[usage] tokens(prompt: {usage.get('prompt_tokens') or ''}, completion: {usage.get('completion_tokens') or ''}, total: {usage.get('total_tokens') or ''}{image_tokens_part}) usage(decode tps: {usage.get('decode_tokens_per_second') or ''}, prefill tps: {usage.get('prefill_tokens_per_second') or ''}{image_tps_part}) itl: {usage.get('itl_ms') or ''}ms ttft: {usage.get('ttft_ms') or ''}ms{C.RESET}" + ) + if interrupted: + return 130 + + tool_calls = assistant_msg.get("tool_calls") if isinstance(assistant_msg, dict) else None + if not tools or not isinstance(tool_calls, list) or not tool_calls: + return 0 + + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + fn = tool_call.get("function") or {} + if not isinstance(fn, dict): + continue + name = str(fn.get("name") or "") + raw_args = str(fn.get("arguments") or "{}") + try: + parsed_args = json.loads(raw_args) + except json.JSONDecodeError: + parsed_args = {"_raw": raw_args} + if name in {"web_search", "web_fetch"}: + print(f"{C.CYAN}{HAMMER} tool{C.RESET} {name}") + tool_result = _execute_default_tool(name, parsed_args) + elif mcp_client.has_tool(name): + print(f"{C.CYAN}{HAMMER} mcp{C.RESET} {name}") + tool_result = await mcp_client.call_tool(name, parsed_args) + else: + print(f"{C.YELLOW}{WARN} unknown tool{C.RESET} {name}") + tool_result = {"error": f"unknown tool '{name}'"} + messages.append( + { + "role": "tool", + "tool_call_id": tool_call.get("id", ""), + "content": json.dumps(tool_result, ensure_ascii=False), + } + ) + + warn("Reached max tool rounds without a final assistant response") + return 1 + + +async def cmd_infer(args, storage: StorageService) -> int: + prompt_words = getattr(args, "prompt_words", None) + prompt = " ".join(prompt_words).strip() if prompt_words else "" + + device, ip = await _resolve_connected_device(storage) + if not device or not ip: + return 1 + + spinner = Spinner("Resolving default model") + spinner.start() + model = await _default_model(ip) + if not model: + spinner.fail("Failed to resolve default model from IF2") + return 1 + spinner.stop(success=True) + + settings = ChatSettings(model=model) + mcp_client = ChatMCPClient() + messages: list[dict[str, Any]] = [] + pending_image_data_url: str | None = None + pending_image_desc: str | None = None + + url = f"http://{ip}/if2/v1/chat/completions" + headers = {"Content-Type": "application/json"} + + try: + spinner = Spinner(f"Connecting to {device}") + spinner.start() + with httpx.Client(timeout=None) as client: + spinner.stop(success=True) + + # REPL mode (default). + print(f"{C.DIM}model: {settings.model}{C.RESET}") + print( + f"{C.DIM}commands: /help, /history, /reset, /models, /attach, /config, /mcp, /exit{C.RESET}" + ) + + cleanup_repl = _install_repl_completer(REPL_COMMANDS) + try: + if prompt: + print(f"{C.CYAN}> {prompt}{C.RESET}") + rc = await _run_chat_turn( + client=client, + url=url, + headers=headers, + model=settings.model, + settings=settings, + mcp_client=mcp_client, + messages=messages, + user_message=_make_user_message(prompt, pending_image_data_url), + ) + if rc != 0: + if rc == 130: + return 0 + else: + return rc + else: + pending_image_data_url = None + pending_image_desc = None + + while True: + try: + line = input(f"{C.CYAN}> {C.RESET}").strip() + except EOFError: + print() + return 0 + except KeyboardInterrupt: + print() + return 0 + + if not line: + continue + if line in {"/", "/help"}: + _print_repl_commands() + continue + if line in {"/exit", "/quit"}: + return 0 + if line == "/history": + _print_history(messages) + continue + if line == "/reset": + messages = [] + if settings.system_prompt: + messages.append({"role": "system", "content": settings.system_prompt}) + pending_image_data_url = None + pending_image_desc = None + print(f"{C.YELLOW}history reset (and cleared pending attachment){C.RESET}") + continue + if line in {"/models", "/model"}: + try: + models = _fetch_models_payload(client, ip) + selected_model = _pick_model_interactive(models, settings.model) + if selected_model and selected_model != settings.model: + settings.model = selected_model + print(f"{C.GREEN}{CHECK}{C.RESET} model switched: {settings.model}") + except Exception as exc: + error(f"failed to list models: {exc}") + continue + if line == "/config": + _print_chat_config(settings, mcp_client) + continue + if line.startswith("/reasoning"): + arg = line[len("/reasoning"):].strip() + if not arg: + print(f"{C.DIM}reasoning={settings.reasoning}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /reasoning ") + continue + settings.reasoning = val + print(f"{C.GREEN}{CHECK}{C.RESET} reasoning={settings.reasoning}") + continue + if line.startswith("/stream"): + arg = line[len("/stream"):].strip() + if not arg: + print(f"{C.DIM}stream={settings.stream}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /stream ") + continue + settings.stream = val + print(f"{C.GREEN}{CHECK}{C.RESET} stream={settings.stream}") + continue + if line.startswith("/json"): + arg = line[len("/json"):].strip() + if not arg: + print(f"{C.DIM}json={settings.json_mode}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /json ") + continue + settings.json_mode = val + print(f"{C.GREEN}{CHECK}{C.RESET} json={settings.json_mode}") + continue + if line.startswith("/tools"): + arg = line[len("/tools"):].strip() + if not arg: + print(f"{C.DIM}tools={settings.default_tools}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /tools ") + continue + settings.default_tools = val + print(f"{C.GREEN}{CHECK}{C.RESET} tools={settings.default_tools}") + continue + if line.startswith("/max_tokens"): + arg = line[len("/max_tokens"):].strip() + if not arg: + print(f"{C.DIM}max_tokens={settings.max_tokens}{C.RESET}") + continue + try: + settings.max_tokens = max(1, int(arg)) + print(f"{C.GREEN}{CHECK}{C.RESET} max_tokens={settings.max_tokens}") + except ValueError: + warn("usage: /max_tokens ") + continue + if line.startswith("/temperature"): + arg = line[len("/temperature"):].strip() + if not arg: + print(f"{C.DIM}temperature={settings.temperature}{C.RESET}") + continue + if arg.lower() in {"off", "none"}: + settings.temperature = None + print(f"{C.GREEN}{CHECK}{C.RESET} temperature=None") + continue + try: + settings.temperature = float(arg) + print(f"{C.GREEN}{CHECK}{C.RESET} temperature={settings.temperature}") + except ValueError: + warn("usage: /temperature ") + continue + if line.startswith("/top_p"): + arg = line[len("/top_p"):].strip() + if not arg: + print(f"{C.DIM}top_p={settings.top_p}{C.RESET}") + continue + if arg.lower() in {"off", "none"}: + settings.top_p = None + print(f"{C.GREEN}{CHECK}{C.RESET} top_p=None") + continue + try: + settings.top_p = float(arg) + print(f"{C.GREEN}{CHECK}{C.RESET} top_p={settings.top_p}") + except ValueError: + warn("usage: /top_p ") + continue + if line.startswith("/max_rounds"): + arg = line[len("/max_rounds"):].strip() + if not arg: + print(f"{C.DIM}max_rounds={settings.max_tool_rounds}{C.RESET}") + continue + try: + settings.max_tool_rounds = max(1, int(arg)) + print(f"{C.GREEN}{CHECK}{C.RESET} max_rounds={settings.max_tool_rounds}") + except ValueError: + warn("usage: /max_rounds ") + continue + if line.startswith("/system"): + arg = line[len("/system"):].strip() + if not arg: + print(f"{C.DIM}system={settings.system_prompt or ''}{C.RESET}") + continue + if arg.lower() in {"off", "none", "clear"}: + settings.system_prompt = None + if messages and messages[0].get("role") == "system": + messages.pop(0) + print(f"{C.GREEN}{CHECK}{C.RESET} system prompt cleared") + continue + settings.system_prompt = arg + if messages and messages[0].get("role") == "system": + messages[0]["content"] = arg + else: + messages.insert(0, {"role": "system", "content": arg}) + print(f"{C.GREEN}{CHECK}{C.RESET} system prompt updated") + continue + if line.startswith("/mcp"): + parts = line.split(maxsplit=2) + if len(parts) == 1 or parts[1] == "status": + print( + f"{C.BLUE}/mcp status{C.RESET} " + f"{C.DIM}mcp={mcp_client.endpoint or ''} " + f"tools={len(mcp_client.list_tool_names())}{C.RESET}" + ) + print( + f"{C.DIM}subcommands:{C.RESET} " + f"{C.BLUE}/mcp connect {C.RESET}, " + f"{C.BLUE}/mcp tools{C.RESET}, " + f"{C.BLUE}/mcp disconnect{C.RESET}" + ) + continue + sub = parts[1].lower() + if sub == "connect": + if len(parts) < 3: + warn("usage: /mcp connect ") + continue + endpoint = parts[2].strip() + if not endpoint.startswith(("http://", "https://")): + warn("mcp endpoint must start with http:// or https://") + continue + try: + await mcp_client.connect_streamable_http(endpoint) + print( + f"{C.BLUE}/mcp connect{C.RESET} " + f"{C.GREEN}{CHECK}{C.RESET} {endpoint} " + f"({len(mcp_client.list_tool_names())} tools)" + ) + except Exception as exc: + error(f"mcp connect failed: {exc}") + continue + if sub == "disconnect": + await mcp_client.disconnect() + print(f"{C.BLUE}/mcp disconnect{C.RESET} {C.GREEN}{CHECK}{C.RESET}") + continue + if sub == "tools": + names = mcp_client.list_tool_names() + if not names: + print(f"{C.BLUE}/mcp tools{C.RESET} {C.DIM}no tools available{C.RESET}") + else: + print(f"{C.BLUE}/mcp tools{C.RESET} {', '.join(names)}") + continue + warn("usage: /mcp ") + continue + if line.startswith("/attach"): + parts = line.split(maxsplit=1) + if len(parts) != 2 or not parts[1].strip(): + warn("usage: /attach ") + continue + src = parts[1].strip() + try: + image_bytes, mime, desc = _resolve_image_bytes_and_mime(src) + pending_image_data_url = _to_data_url(image_bytes, mime) + pending_image_desc = desc + print(f"{C.GREEN}{CHECK}{C.RESET} attachment ready: {desc}") + except FileNotFoundError as exc: + error(str(exc)) + except httpx.HTTPError as exc: + error(f"failed to fetch image: {exc}") + except RuntimeError as exc: + error(str(exc)) + continue + if line.startswith("/"): + matches = [cmd for cmd in REPL_COMMANDS if cmd.startswith(line)] + if matches: + _print_repl_commands(line) + else: + warn(f"unknown command: {line}") + _print_repl_commands() + continue + + if pending_image_data_url is not None: + print(f"{C.MAGENTA}[attach]{C.RESET} sending with image: {pending_image_desc}") + rc = await _run_chat_turn( + client=client, + url=url, + headers=headers, + model=settings.model, + settings=settings, + mcp_client=mcp_client, + messages=messages, + user_message=_make_user_message(line, pending_image_data_url), + ) + if rc != 0: + if rc == 130: + return 0 + return rc + pending_image_data_url = None + pending_image_desc = None + finally: + if cleanup_repl: + cleanup_repl() + await mcp_client.disconnect() + return 0 + except Exception as e: + try: + spinner.fail(f"Chat request failed: {e}") # type: ignore[name-defined] + except Exception: + error(f"Chat request failed: {e}") + return 1 + + diff --git a/truffile/cli/picker.py b/truffile/cli/picker.py new file mode 100644 index 0000000..b7ae525 --- /dev/null +++ b/truffile/cli/picker.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import Any + +from .ui import C, DOT + + +def pick_from_list( + items: list[dict[str, Any]], + *, + label_key: str = "label", + detail_key: str = "detail", + active_key: str | None = None, + active_value: Any = None, + prompt: str = "pick one", +) -> dict[str, Any] | None: + if not items: + return None + + print() + for i, item in enumerate(items, 1): + label = item.get(label_key, "?") + detail = item.get(detail_key, "") + is_active = active_key and item.get(active_key) == active_value + + line = f" {C.CYAN}{i}.{C.RESET} {label}" + if is_active: + line += f" {C.GREEN}(current){C.RESET}" + if detail: + line += f" {C.DIM}{detail}{C.RESET}" + print(line) + print() + + try: + choice = input(f"{prompt} (1-{len(items)}) or enter to cancel: ").strip() + except (EOFError, KeyboardInterrupt): + return None + + if not choice: + return None + try: + idx = int(choice) - 1 + if 0 <= idx < len(items): + return items[idx] + except ValueError: + pass + return None diff --git a/truffile/transport/client.py b/truffile/transport/client.py index 41d6715..ba89b9a 100644 --- a/truffile/transport/client.py +++ b/truffile/transport/client.py @@ -25,6 +25,15 @@ from truffle.os.app_queries_pb2 import GetAllAppsRequest, GetAllAppsResponse, DeleteAppRequest, DeleteAppResponse from truffle.app.app_pb2 import App from truffle.app.background_pb2 import BackgroundApp, BackgroundAppRuntimePolicy +from truffle.os.task_actions_pb2 import ( + OpenTaskRequest, + InterruptTaskRequest, + TaskRenameRequest, + TaskDeleteRequest, + TaskSetAvailableAppsRequest, +) +from truffle.os.task_user_response_pb2 import RespondToTaskRequest +from truffle.os.task_queries_pb2 import GetTaskInfosRequest from truffile.schedule import parse_runtime_policy GRPC_MAX_MESSAGE_BYTES = 32 * 1024 * 1024 @@ -422,6 +431,84 @@ async def close(self): self.channel = None self.stub = None + # task methods + + def open_task_stream(self, prompt: str, *, app_uuids: list[str] | None = None): + if not self.stub: + raise RuntimeError("not connected") + req = OpenTaskRequest() + req.new_task.user_message.content = prompt + if app_uuids: + req.new_task.app_uuids.extend(app_uuids) + return self.stub.Task_OpenTask(req, metadata=self._metadata) + + async def respond_to_task(self, task_id: str, node_id: int, message: str) -> None: + if not self.stub: + raise RuntimeError("not connected") + req = RespondToTaskRequest() + req.task_id = task_id + req.node_id = node_id + req.message.content = message + await self.stub.Task_RespondToTask(req, metadata=self._metadata) + + async def interrupt_task(self, task_id: str) -> None: + if not self.stub: + raise RuntimeError("not connected") + req = InterruptTaskRequest() + req.target.task_id = task_id + await self.stub.Task_InterruptTask(req, metadata=self._metadata) + + def open_existing_task_stream(self, task_id: str): + if not self.stub: + raise RuntimeError("not connected") + req = OpenTaskRequest() + req.existing_task.task_id = task_id + return self.stub.Task_OpenTask(req, metadata=self._metadata) + + async def get_task_infos(self, *, max_before: int = 20) -> list[dict]: + if not self.stub: + raise RuntimeError("not connected") + req = GetTaskInfosRequest() + req.max_before = max_before + resp = await self.stub.Task_GetTaskInfos(req, metadata=self._metadata) + tasks = [] + for entry in resp.entries: + info = entry.info + title = info.task_title or "(untitled)" + created = info.created.ToDatetime().isoformat() if info.HasField("created") else "" + updated = info.last_updated.ToDatetime().isoformat() if info.HasField("last_updated") else "" + tasks.append({ + "task_id": entry.task_id, + "title": title, + "created": created, + "updated": updated, + }) + return tasks + + async def rename_task(self, task_id: str, new_name: str) -> str: + if not self.stub: + raise RuntimeError("not connected") + req = TaskRenameRequest() + req.task_id = task_id + req.new_name = new_name + resp = await self.stub.Task_Rename(req, metadata=self._metadata) + return resp.new_name + + async def delete_task(self, task_id: str) -> None: + if not self.stub: + raise RuntimeError("not connected") + req = TaskDeleteRequest() + req.task_id = task_id + await self.stub.Task_Delete(req, metadata=self._metadata) + + async def set_task_apps(self, task_id: str, app_uuids: list[str]) -> None: + if not self.stub: + raise RuntimeError("not connected") + req = TaskSetAvailableAppsRequest() + req.task_id = task_id + req.app_uuids.extend(app_uuids) + await self.stub.Task_SetAvailableApps(req, metadata=self._metadata) + async def __aenter__(self): await self.connect() await self.start_build() From e625ebdb4e799e2e2ff83fc325b9a4295f581f8c Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sat, 4 Apr 2026 23:00:22 -0700 Subject: [PATCH 08/12] skills, need polish still and need to bes testing while actually making an app --- skills/SKILL.md | 186 +++++++++++++++++++++++++++++ skills/references/app-patterns.md | 77 ++++++++++++ skills/references/auth-patterns.md | 47 ++++++++ skills/references/rules.md | 39 ++++++ skills/references/testing-guide.md | 55 +++++++++ skills/references/truffile-yaml.md | 82 +++++++++++++ skills/scripts/check_python.py | 61 ++++++++++ skills/scripts/fetch_docs.py | 62 ++++++++++ skills/scripts/fetch_repo.py | 85 +++++++++++++ skills/scripts/list_app_files.py | 53 ++++++++ skills/scripts/validate_yaml.py | 126 +++++++++++++++++++ tests/test_chat_helpers.py | 4 +- 12 files changed, 875 insertions(+), 2 deletions(-) create mode 100644 skills/SKILL.md create mode 100644 skills/references/app-patterns.md create mode 100644 skills/references/auth-patterns.md create mode 100644 skills/references/rules.md create mode 100644 skills/references/testing-guide.md create mode 100644 skills/references/truffile-yaml.md create mode 100644 skills/scripts/check_python.py create mode 100644 skills/scripts/fetch_docs.py create mode 100644 skills/scripts/fetch_repo.py create mode 100644 skills/scripts/list_app_files.py create mode 100644 skills/scripts/validate_yaml.py diff --git a/skills/SKILL.md b/skills/SKILL.md new file mode 100644 index 0000000..ec90797 --- /dev/null +++ b/skills/SKILL.md @@ -0,0 +1,186 @@ +--- +name: truffle-app-creator +description: | + Create Truffle apps from scratch or by porting existing MCP servers and open source projects. + Use when the user asks to "make an app", "create a truffle app", "build an integration", + "port this to truffle", or wants to connect a new service to their Truffle device. + Guides through research, architecture decisions, implementation, testing, and deployment. +--- + +# Truffle App Creator + +Build Truffle apps step by step โ€” from idea to deployed, tested app. + +## When to use + +Activate when the user wants to: +- create a new Truffle app for a service +- port an existing MCP server to Truffle +- connect a new API or service to their Truffle +- understand how Truffle apps work + +## Workflow + +### Step 1: Understand what the user wants + +Interview the user: +- what service or API do they want to connect? +- what should the agent be able to DO with it? (foreground tools) +- what should happen automatically in the background? (background monitoring) +- does the service need API keys or is it public? + +Explain the difference between foreground and background clearly: +- foreground: tools the agent calls during a conversation (search, send, check) +- background: scheduled jobs that submit context to the background agent running on their Truffle that decides whether submiited context is worth posting or taking an action on or not +- they run in separate containers and cannot talk to each other directly +- use app variables to share state between them + +### Step 2: Research existing solutions + +Before building from scratch, search for: +- existing MCP servers for the service (check mcp.io, github, npm) +- open source libraries or SDKs for the API +- existing Truffle apps that do something similar + +Use web search to find options. Present them to the user with pros/cons: +- if an MCP server exists: can we proxy it or wrap it? +- if an SDK exists: can we build tools on top of it? +- if nothing exists: we build from scratch using the API docs + +Ask the user which approach they prefer. + +### Step 3: Decide the app architecture + +Based on the user's answers, determine: +- app shape: foreground only, background only, or hybrid +- auth type: API key or public (OAuth and VNC not yet supported in truffile deploy) +- base image: minimal for API apps + +Reference: see references/auth-patterns.md for the decision tree. + +### Step 4: Scaffold the app + +Create the directory structure, the command truffile create does this, read the docs first: +``` +my-app/ +โ”œโ”€โ”€ truffile.yaml +โ”œโ”€โ”€ icon.png +โ”œโ”€โ”€ my_app_foreground.py (if FG) +โ”œโ”€โ”€ my_app_background.py (if BG) +โ”œโ”€โ”€ bg_worker.py (if BG) +โ”œโ”€โ”€ client.py +โ”œโ”€โ”€ config.py +โ””โ”€โ”€ tests/ + โ”œโ”€โ”€ conftest.py + โ”œโ”€โ”€ test_my_app_unit.py + โ””โ”€โ”€ test_my_app_app_shells.py +``` + +Generate truffile.yaml with the correct steps for the chosen auth type. Reference: see references/truffile-yaml.md. + +Use the app patterns from references/app-patterns.md for the code structure. + +### Step 5: Implement the client + +Build the API client FIRST with injectable auth and transport: + +```python +class MyClient: + def __init__(self, auth, http): + self._auth = auth + self._http = http +``` + +This makes everything testable from the start. + +### Step 6: Implement foreground tools (if FG) + +Register tools using ForegroundApp and ToolSpec: + +```python +from truffile import ForegroundApp, ToolSpec, ok, err + +app = ForegroundApp("my-app") + +@app.tool(ToolSpec(name="search", description="Search.", icon="magnifying-glass", readonly=True)) +async def search(query: str) -> dict: + ... +``` + +Follow the rules in references/rules.md for naming, descriptions, error handling. + +### Step 7: Implement background worker (if BG) + +Subclass BackgroundWorkerApp with the 4 required methods. Follow the rules: +- first cycle seeds state, doesn't submit +- track seen IDs to prevent duplicates +- bound tracking sets +- submit generously, let the proactivity agent curate + +### Step 8: Write tests immediately + +Don't wait until the end. Write tests as you implement: + +```bash +python scripts/check_python.py ./my-app/ # syntax check +python scripts/validate_yaml.py ./my-app/ # yaml check +python -m pytest ./my-app/tests/ -v # unit tests +``` + +Use FakeHttpTransport and AppHarness. See references/testing-guide.md. + +### Step 9: Validate and iterate + +Run the validation scripts: +- `python scripts/validate_yaml.py ./my-app/` โ€” check manifest +- `python scripts/check_python.py ./my-app/` โ€” check all python files +- `python scripts/list_app_files.py ./my-app/` โ€” verify file list matches truffile.yaml +- `python -m pytest ./my-app/tests/` โ€” run tests + +Fix any issues. Repeat until everything passes. + +### Step 10: Deploy and test + +If the user has a device connected: +```bash +truffile deploy ./my-app +``` + +Then test with the agent: +```bash +truffile chat +you> [test the app's tools] +``` + +### Step 11: Create skills (optional) + +Ask the user if they want to add skills โ€” workflow guides that teach the agent how to use the app's tools effectively. Create SKILL.md files under skills/foreground/ and skills/background/. + +## Error handling + +Common issues during app creation: +- yaml parse errors: run validate_yaml.py and fix the reported issues +- python syntax errors: run check_python.py +- missing files in truffile.yaml: run list_app_files.py and compare with the files step +- import errors: make sure all deps are in the bash install step +- auth failures during deploy: check env var names match between text step and code + +## Reference scripts + +| Script | What it does | +|--------|-------------| +| `scripts/fetch_docs.py` | pull docs from docs.truffle.net | +| `scripts/fetch_repo.py` | browse example apps from the truffile repo | +| `scripts/validate_yaml.py` | check truffile.yaml format and structure | +| `scripts/check_python.py` | syntax-check all python files | +| `scripts/list_app_files.py` | list files with sizes | + +## Reference docs + +| Doc | What it covers | +|-----|---------------| +| `references/app-patterns.md` | FG, BG, hybrid, proxy code patterns | +| `references/auth-patterns.md` | API key and public auth patterns with yaml examples | +| `references/testing-guide.md` | how to write tests with fakes and harness | +| `references/truffile-yaml.md` | full manifest format reference | +| `references/rules.md` | all rules for foreground, background, and general app development | diff --git a/skills/references/app-patterns.md b/skills/references/app-patterns.md new file mode 100644 index 0000000..cb82ea2 --- /dev/null +++ b/skills/references/app-patterns.md @@ -0,0 +1,77 @@ +# App Patterns + +## Foreground Only (MCP tools for agent) + +```python +from truffile import ForegroundApp, ToolSpec, ok, err + +app = ForegroundApp("my-app") + +@app.tool(ToolSpec(name="search", description="Search items.", icon="magnifying-glass", readonly=True)) +async def search(query: str, limit: int = 10) -> dict: + try: + items = await client.search(query, limit=limit) + return ok("found items", items=items) + except Exception as e: + return err(str(e)) + +if __name__ == "__main__": + app.run() +``` + +## Background Only (scheduled context submissions) + +```python +from truffile import BackgroundWorkerApp + +class MyApp(BackgroundWorkerApp[MyWorker, MyResult]): + def __init__(self): + super().__init__("my-app", logger_name="my-app.background") + + def build_worker(self): + return MyWorker() + + def verify_worker(self, worker): + return worker.verify() + + def run_cycle(self, worker): + return worker.run_cycle() + + def handle_cycle_result(self, ctx, result): + if result.content: + self.submit_text(ctx, content=result.content) + +app = MyApp() + +if __name__ == "__main__": + app.main() +``` + +## Hybrid (both FG tools + BG monitoring) + +Same as above but the truffile.yaml defines both foreground and background processes. They run in separate containers from the same codebase. They CANNOT communicate directly โ€” use app variables to share state. + +## Proxy MCP (wrapping an existing MCP server) + +For services that already have an MCP server. Three sub-patterns: +- HTTP proxy: forward MCP requests to remote endpoint with auth headers +- Managed subprocess: launch a binary MCP server and talk via stdin/stdout +- Remote JSON-RPC client: send HTTP requests to remote MCP endpoint, parse SSE responses + +## Client Architecture + +Always make your API client injectable for testing: + +```python +class MyClient: + def __init__(self, auth: ApiKeyProvider, http: HttpTransport): + self._auth = auth + self._http = http + + async def search(self, query: str) -> list: + headers = self._auth.get_auth_headers("GET", "/search") + resp = await self._http.request("GET", f"{BASE_URL}/search", params={"q": query}, headers=headers) + return resp.json()["results"] +``` + +This lets tests inject FakeApiKeyProvider and FakeHttpTransport. diff --git a/skills/references/auth-patterns.md b/skills/references/auth-patterns.md new file mode 100644 index 0000000..b0a4568 --- /dev/null +++ b/skills/references/auth-patterns.md @@ -0,0 +1,47 @@ +# Auth Patterns + +## Decision Tree + +``` +Does the service already have an MCP server? +โ”œโ”€โ”€ Yes โ†’ Proxy MCP app (see app-patterns.md) +โ””โ”€โ”€ No โ†’ Build tools directly + โ”‚ + โ”œโ”€โ”€ API key available? โ†’ text install step + โ”‚ User enters key. Promoted to env var. + โ”‚ Validator script verifies before install completes. + โ”‚ + โ””โ”€โ”€ No auth needed? โ†’ no auth step + Public APIs. Just bash + files steps. +``` + +Note: OAuth and VNC auth are supported in the platform but not yet available through truffile deploy. Use the Truffle client app for OAuth and VNC-based apps. + +## API Key (truffile.yaml) + +```yaml +- name: Configure API access + type: text + fields: + - name: api_key + label: API Key + type: password + env: MY_API_KEY + - name: preference + label: Optional preference + type: text + default: "" + env: USER_PREFERENCE + env_default_if_empty: "default_value" + validator: + type: bash + run: python ./verify.py + timeout: 120 + error_message: Could not verify credentials. +``` + +At runtime, the app reads credentials from env vars: `os.environ["MY_API_KEY"]`. + +## No Auth (public APIs) + +No auth step needed. Just bash + files steps in truffile.yaml. The app accesses public endpoints directly. diff --git a/skills/references/rules.md b/skills/references/rules.md new file mode 100644 index 0000000..f13d0bb --- /dev/null +++ b/skills/references/rules.md @@ -0,0 +1,39 @@ +# Rules for Truffle Apps + +## Foreground Rules + +- tools must return ok() or err() response dicts +- every tool needs a name, description, and icon +- set readonly=True for tools that only read data +- destructive tools (purchases, deletions, sends) must say so in description +- catch exceptions and return err(), don't let them propagate +- report auth failures to firmware via report_app_error with needs_intervention=True +- tool parameters should have sensible defaults for optional fields +- client should be injectable (accept auth and http transport as constructor params) + +## Background Rules + +- background workers submit context for the proactivity agent to evaluate +- be comprehensive โ€” submit everything relevant, let the agent curate +- be idempotent โ€” running twice should not produce duplicate submissions +- track seen IDs to prevent re-submitting old content +- bound your tracking sets (max 1500-5000 entries) to prevent memory growth +- persist state to JSON files, not just in-memory +- first cycle should seed state without submitting (avoid stale content on startup) +- use PRIORITY_HIGH for urgent content (mentions, alerts), PRIORITY_LOW for informational + +## FG/BG Separation + +- foreground and background run in SEPARATE containers +- they CANNOT call each other's functions or share memory +- use app variables (get_app_var/set_app_var) to share state between FG and BG + +## General Rules + +- all config from environment variables โ€” define a config.py +- don't hardcode service URLs, credentials, or user-specific values +- log to stdout with PYTHONUNBUFFERED=1 +- keep files focused: one module per concern (client, config, auth, bg_worker) +- snake_case for tool names: search_items, send_message +- use type hints +- write tests from the start, not at the end diff --git a/skills/references/testing-guide.md b/skills/references/testing-guide.md new file mode 100644 index 0000000..1a53ecc --- /dev/null +++ b/skills/references/testing-guide.md @@ -0,0 +1,55 @@ +# Testing Guide + +## Unit Tests + +Every app needs tests that run without network, auth, or a device. + +```python +from truffile import FakeHttpTransport, FakeHttpResponse, FakeApiKeyProvider, AppHarness + +class TestMyClient(unittest.IsolatedAsyncioTestCase): + async def test_search_returns_results(self): + transport = FakeHttpTransport({ + "GET /search": FakeHttpResponse(200, _json={"results": [{"id": 1}]}) + }) + auth = FakeApiKeyProvider(authenticated=True) + client = MyClient(auth=auth, http=transport) + results = await client.search("test") + self.assertEqual(len(results), 1) +``` + +## App Shell Tests + +Test tool invocation through AppHarness: + +```python +class TestMyAppShells(unittest.IsolatedAsyncioTestCase): + async def test_foreground_tool(self): + harness = AppHarness(fg_app=my_app) + with patch("my_client.search", return_value=[{"id": 1}]): + result = await harness.run_fg(calls=[("search", {"query": "test"})]) + self.assertTrue(result.success) + + async def test_background_submission(self): + harness = AppHarness(bg_app=my_bg_app) + with patch.object(my_bg_app, "run_cycle", return_value=mock_result): + result = await harness.run_bg(cycles=1) + self.assertGreater(len(result.submissions), 0) +``` + +## What to Test + +- parsing and formatting (all pure functions) +- client request construction and response handling +- background worker dedup and state management +- error paths (401, 403, timeouts) +- config loading and validation + +## Test File Structure + +``` +tests/ +โ”œโ”€โ”€ conftest.py # sys.path setup +โ”œโ”€โ”€ test__unit.py # business logic with fakes +โ””โ”€โ”€ test__app_shells.py # FG/BG through AppHarness +``` diff --git a/skills/references/truffile-yaml.md b/skills/references/truffile-yaml.md new file mode 100644 index 0000000..7e3645e --- /dev/null +++ b/skills/references/truffile-yaml.md @@ -0,0 +1,82 @@ +# truffile.yaml Reference + +## Full Structure + +```yaml +metadata: + name: # display name + bundle_id: # org.deepshard. + description: # what the app does + icon_file: # relative path to icon.png + + foreground: # optional + process: + cmd: [, ...] # e.g. [python, my_foreground.py] + working_directory: / + environment: + PYTHONUNBUFFERED: "1" + + background: # optional + process: + cmd: [, ...] # e.g. [python, my_background.py] + working_directory: / + environment: + PYTHONUNBUFFERED: "1" + default_schedule: + type: interval | times | always + interval: + duration: # dev interval e.g. "2m" + prod_duration: # production interval e.g. "60m" + daily_window: # e.g. "00:00-23:59" + +steps: + - name: Install dependencies + type: bash + run: pip install --no-cache-dir httpx>=0.27.0 + + - name: Copy app files + type: files + files: + - source: ./my_foreground.py + destination: ./my_foreground.py + - source: ./my_background.py + destination: ./my_background.py + - source: ./client.py + destination: ./client.py + + - name: Configure + type: text + fields: + - name: api_key + label: API Key + type: password + env: MY_API_KEY + validator: + type: bash + run: python ./my_background.py --verify + timeout: 120 +``` + +## Rules + +- at least one of foreground or background must be defined +- bundle_id must be globally unique: org.deepshard. +- foreground cmd must start a process that listens on port 8000 +- every source file must be listed in a files step +- use --no-cache-dir for pip installs +- don't reinstall packages in the base image (python3, grpcio, mcp, httpx) +- PYTHONUNBUFFERED=1 should be set in all process environments + +## Files Step + +Every python file your app needs must be listed. Directories are supported: +```yaml +- source: ./common/ + destination: ./common/ +``` + +## Schedule Types + +- interval: runs every N minutes/hours within optional daily window +- times: runs at specific times of day +- always: runs continuously with 8s restart delay between cycles diff --git a/skills/scripts/check_python.py b/skills/scripts/check_python.py new file mode 100644 index 0000000..26c15a3 --- /dev/null +++ b/skills/scripts/check_python.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# check python files in an app directory for syntax errors and basic import issues +# usage: python scripts/check_python.py path/to/app/ + +import argparse +import ast +import sys +from pathlib import Path + + +def check_syntax(filepath: Path) -> tuple[bool, str]: + try: + source = filepath.read_text(encoding="utf-8") + ast.parse(source, filename=str(filepath)) + return True, "ok" + except SyntaxError as e: + return False, f"line {e.lineno}: {e.msg}" + except Exception as e: + return False, str(e) + + +def check_app_dir(app_dir: Path) -> tuple[int, int, list[str]]: + passed = 0 + failed = 0 + errors: list[str] = [] + + for py_file in sorted(app_dir.rglob("*.py")): + if "__pycache__" in str(py_file): + continue + ok, msg = check_syntax(py_file) + rel = py_file.relative_to(app_dir) + if ok: + passed += 1 + else: + failed += 1 + errors.append(f" {rel}: {msg}") + + return passed, failed, errors + + +def main(): + parser = argparse.ArgumentParser(description="check python syntax in app directory") + parser.add_argument("path", nargs="?", default=".", help="path to app directory") + args = parser.parse_args() + + app_dir = Path(args.path).resolve() + passed, failed, errors = check_app_dir(app_dir) + + for e in errors: + print(e) + + total = passed + failed + if failed == 0: + print(f"\n all {total} python files ok") + else: + print(f"\n {failed}/{total} files have errors") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/scripts/fetch_docs.py b/skills/scripts/fetch_docs.py new file mode 100644 index 0000000..badfdd2 --- /dev/null +++ b/skills/scripts/fetch_docs.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# fetch truffle docs from docs.truffle.net for context +# usage: python scripts/fetch_docs.py [--section getting-started] + +import argparse +import json +import sys + +try: + import httpx +except ImportError: + print("install httpx: pip install httpx") + sys.exit(1) + +DOCS_BASE = "https://docs.truffle.net" + +SECTIONS = [ + "getting-started", + "apps/overview", + "apps/foreground", + "apps/background", + "apps/manifest", + "apps/testing", + "apps/auth", + "sdk/overview", +] + + +def fetch_section(section: str) -> str: + url = f"{DOCS_BASE}/{section}" + try: + resp = httpx.get(url, timeout=15.0, follow_redirects=True) + if resp.status_code == 200: + return resp.text + return f"error: {resp.status_code}" + except Exception as e: + return f"error: {e}" + + +def main(): + parser = argparse.ArgumentParser(description="fetch truffle docs") + parser.add_argument("--section", type=str, default=None, help="specific section to fetch") + parser.add_argument("--list", action="store_true", help="list available sections") + args = parser.parse_args() + + if args.list: + for s in SECTIONS: + print(f" {s}") + return + + if args.section: + content = fetch_section(args.section) + print(content) + else: + for s in SECTIONS: + print(f"\n--- {s} ---") + content = fetch_section(s) + print(content[:500]) + + +if __name__ == "__main__": + main() diff --git a/skills/scripts/fetch_repo.py b/skills/scripts/fetch_repo.py new file mode 100644 index 0000000..71df675 --- /dev/null +++ b/skills/scripts/fetch_repo.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# fetch example apps from the truffile repo for reference +# usage: python scripts/fetch_repo.py [--app kalshi] + +import argparse +import json +import sys + +try: + import httpx +except ImportError: + print("install httpx: pip install httpx") + sys.exit(1) + +REPO = "deepshard/truffile" +API_BASE = f"https://api.github.com/repos/{REPO}" +RAW_BASE = f"https://raw.githubusercontent.com/{REPO}/main" + + +def list_example_apps() -> list[str]: + url = f"{API_BASE}/contents/app-store" + try: + resp = httpx.get(url, timeout=15.0) + if resp.status_code != 200: + return [] + entries = resp.json() + return [e["name"] for e in entries if e["type"] == "dir"] + except Exception: + return [] + + +def fetch_app_file(app_name: str, filename: str) -> str: + url = f"{RAW_BASE}/app-store/{app_name}/{filename}" + try: + resp = httpx.get(url, timeout=15.0) + return resp.text if resp.status_code == 200 else f"not found: {url}" + except Exception as e: + return f"error: {e}" + + +def fetch_app_truffile(app_name: str) -> str: + return fetch_app_file(app_name, "truffile.yaml") + + +def list_app_files(app_name: str) -> list[str]: + url = f"{API_BASE}/contents/app-store/{app_name}" + try: + resp = httpx.get(url, timeout=15.0) + if resp.status_code != 200: + return [] + return [e["name"] for e in resp.json()] + except Exception: + return [] + + +def main(): + parser = argparse.ArgumentParser(description="fetch example apps from truffile repo") + parser.add_argument("--app", type=str, default=None, help="app name to inspect") + parser.add_argument("--file", type=str, default=None, help="specific file to fetch") + parser.add_argument("--list", action="store_true", help="list example apps") + args = parser.parse_args() + + if args.list: + apps = list_example_apps() + for a in apps: + print(f" {a}") + return + + if args.app and args.file: + print(fetch_app_file(args.app, args.file)) + elif args.app: + print(f"files in {args.app}:") + for f in list_app_files(args.app): + print(f" {f}") + print(f"\ntruffile.yaml:") + print(fetch_app_truffile(args.app)) + else: + apps = list_example_apps() + print(f"example apps ({len(apps)}):") + for a in apps: + print(f" {a}") + + +if __name__ == "__main__": + main() diff --git a/skills/scripts/list_app_files.py b/skills/scripts/list_app_files.py new file mode 100644 index 0000000..8ab6d37 --- /dev/null +++ b/skills/scripts/list_app_files.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# list all files in an app directory with their sizes +# usage: python scripts/list_app_files.py path/to/app/ + +import argparse +import sys +from pathlib import Path + + +def list_files(app_dir: Path) -> list[dict]: + files = [] + for f in sorted(app_dir.rglob("*")): + if f.is_dir() or "__pycache__" in str(f): + continue + rel = f.relative_to(app_dir) + size = f.stat().st_size + files.append({"path": str(rel), "size": size, "ext": f.suffix}) + return files + + +def main(): + parser = argparse.ArgumentParser(description="list app files") + parser.add_argument("path", nargs="?", default=".", help="path to app directory") + args = parser.parse_args() + + app_dir = Path(args.path).resolve() + files = list_files(app_dir) + + py_files = [f for f in files if f["ext"] == ".py"] + yaml_files = [f for f in files if f["ext"] in (".yaml", ".yml")] + other = [f for f in files if f not in py_files and f not in yaml_files] + + if yaml_files: + print("config:") + for f in yaml_files: + print(f" {f['path']} ({f['size']} bytes)") + + if py_files: + print("python:") + for f in py_files: + print(f" {f['path']} ({f['size']} bytes)") + + if other: + print("other:") + for f in other: + print(f" {f['path']} ({f['size']} bytes)") + + total_size = sum(f["size"] for f in files) + print(f"\ntotal: {len(files)} files, {total_size:,} bytes") + + +if __name__ == "__main__": + main() diff --git a/skills/scripts/validate_yaml.py b/skills/scripts/validate_yaml.py new file mode 100644 index 0000000..8da6ca5 --- /dev/null +++ b/skills/scripts/validate_yaml.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# validate truffile.yaml format and structure +# usage: python scripts/validate_yaml.py path/to/app/ + +import argparse +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + print("install pyyaml: pip install pyyaml") + sys.exit(1) + +REQUIRED_METADATA = ["name", "bundle_id"] +VALID_STEP_TYPES = {"welcome", "bash", "files", "text", "oauth", "vnc"} +VALID_SCHEDULE_TYPES = {"interval", "times", "always"} + + +def validate(app_dir: Path) -> tuple[bool, list[str], list[str]]: + errors: list[str] = [] + warnings: list[str] = [] + + truffile = app_dir / "truffile.yaml" + if not truffile.exists(): + errors.append("truffile.yaml not found") + return False, errors, warnings + + try: + data = yaml.safe_load(truffile.read_text()) + except yaml.YAMLError as e: + errors.append(f"yaml parse error: {e}") + return False, errors, warnings + + if not isinstance(data, dict): + errors.append("truffile.yaml must be a dict") + return False, errors, warnings + + meta = data.get("metadata") + if not isinstance(meta, dict): + errors.append("metadata section missing or not a dict") + return False, errors, warnings + + for field in REQUIRED_METADATA: + if not meta.get(field): + errors.append(f"metadata.{field} is required") + + has_fg = isinstance(meta.get("foreground"), dict) + has_bg = isinstance(meta.get("background"), dict) + if not has_fg and not has_bg: + errors.append("must define foreground and/or background in metadata") + + if has_fg: + fg = meta["foreground"] + proc = fg.get("process") + if not isinstance(proc, dict) or not proc.get("cmd"): + errors.append("foreground.process.cmd is required") + + if has_bg: + bg = meta["background"] + proc = bg.get("process") + if not isinstance(proc, dict) or not proc.get("cmd"): + errors.append("background.process.cmd is required") + schedule = bg.get("default_schedule") + if not isinstance(schedule, dict): + errors.append("background.default_schedule is required") + elif schedule.get("type") not in VALID_SCHEDULE_TYPES: + errors.append(f"invalid schedule type: {schedule.get('type')}. must be one of {VALID_SCHEDULE_TYPES}") + + icon_file = meta.get("icon_file") + if icon_file: + icon_path = app_dir / icon_file + if not icon_path.exists(): + warnings.append(f"icon_file {icon_file} not found") + else: + warnings.append("no icon_file specified") + + steps = data.get("steps", []) + if not isinstance(steps, list): + errors.append("steps must be a list") + else: + for i, step in enumerate(steps): + if not isinstance(step, dict): + errors.append(f"step {i} is not a dict") + continue + step_type = step.get("type") + if step_type not in VALID_STEP_TYPES: + errors.append(f"step {i}: invalid type '{step_type}'") + if step_type == "bash" and not step.get("run"): + errors.append(f"step {i}: bash step missing 'run' field") + if step_type == "files" and not step.get("files"): + errors.append(f"step {i}: files step missing 'files' field") + if step_type == "files": + for f in step.get("files", []): + src = f.get("source", "") + if src: + src_path = app_dir / src + if not src_path.exists(): + warnings.append(f"step {i}: source file not found: {src}") + + valid = len(errors) == 0 + return valid, errors, warnings + + +def main(): + parser = argparse.ArgumentParser(description="validate truffile.yaml") + parser.add_argument("path", nargs="?", default=".", help="path to app directory") + args = parser.parse_args() + + app_dir = Path(args.path).resolve() + valid, errors, warnings = validate(app_dir) + + for w in warnings: + print(f" warning: {w}") + for e in errors: + print(f" error: {e}") + + if valid: + print(f"\n valid: {app_dir / 'truffile.yaml'}") + else: + print(f"\n invalid: {len(errors)} error(s)") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/test_chat_helpers.py b/tests/test_chat_helpers.py index 807c162..ff56158 100644 --- a/tests/test_chat_helpers.py +++ b/tests/test_chat_helpers.py @@ -6,8 +6,8 @@ def _import_chat(): """lazy import to handle missing deps gracefully""" try: - from truffile.cli import chat - return chat + from truffile.cli import infer + return infer except ImportError: return None From e131e4e0f0900e3076be1fdaef51216b07da2efd Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sat, 4 Apr 2026 23:33:47 -0700 Subject: [PATCH 09/12] clean up --- docs/Truffle.png | Bin 8922 -> 0 bytes docs/ambient-app.mdx | 165 --------------------------- docs/building-apps.mdx | 161 -------------------------- docs/cli.mdx | 250 ----------------------------------------- docs/focus-app.mdx | 197 -------------------------------- docs/inference.mdx | 164 --------------------------- docs/installation.mdx | 143 ----------------------- 7 files changed, 1080 deletions(-) delete mode 100644 docs/Truffle.png delete mode 100644 docs/ambient-app.mdx delete mode 100644 docs/building-apps.mdx delete mode 100644 docs/cli.mdx delete mode 100644 docs/focus-app.mdx delete mode 100644 docs/inference.mdx delete mode 100644 docs/installation.mdx diff --git a/docs/Truffle.png b/docs/Truffle.png deleted file mode 100644 index 979ff73ce44802282df90fee6b713db3088d3cf5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8922 zcmeHN`9GBH_rGV1Wvn5z*d^JMtQm@;2U%yvSkff>*vghIlPu2@g)EUmMKUuO%UBx* zc@j!!#=cd^GRUs6eD6L#e_!7};QRXgFh9(>uIpU)eXet#^FHr${bOTg#>ahv8vp>l zzph@k0{|%I&yR}(lr($hi~)e`%fBvPLf(Zi#}Cs*K5S~QrZ4UnRLIdD0)GRFP#f4) zLA-3*QOJ?|mtjt_X@7&#K-f9Koy}^y*WFc3OR1U&gl2&?y{swOkJvSq%j(f9X=>S7 zIkW_T%-s(#;G!ag2Pn+JvjaAkG$?T9G!_CJ6($3~Lpb;-9WWID?*1EbSNfn zWZuX~c@?s#fNkZbU^iwz*J@Yq#T!u3 zo~5%_XQ&BhR1G5ad0n?xCJ)b~KsI42XZlCND_p7i5r_NRlkAF#Pt)iGm(shG$@_VN zl+fr<;coD7#o|p~mLl3&acuVu+7=+b&E+ zw-{9-^hSsf3-f~go-sOU@I?_8E>x5bPI?m z6}lRu2`;KZK8jTeP|>C@O2`O@mvJ7a{1DMQt0oqLwU#uujB}Y2pgi|wTxC(r5WzXZ zVcZP|Q_+^Yr1Wy(=g-b;IQbmhaq=FE`B!N7lBlyZw$iE8T;8&H^JzFrX(*9lZW(GZ ze#K^IdFgx>bpwQhN!i3zH~&ZK|v9jm-=+0yY@+ON7qane|gB2mJ<-5IfUBuzo6 zHs2t^%`^EX9K$Q8iR>*fawvtC7x8tLRC#F0zCJ?6AdTtCV(L9eh)#A;Rz#Uus^p7! zHOv!yhwMl;w-3Z=pA%^@dKOg{uc~@ETN>N<$XA3M&oH(eigSgjc$PS^rxY?J58DNd z-uh}>fFuu}!t8GhSQAjnUz?-P4cyt$->a?CH#17x(?ebB#$i9++Nf z>)+vTM^D%zF6>nsysdvTtKe;WjrPqHM%;bQZ=-79@!-m|Pps1yUm4QA8lGPDJ532H zIw*^w`@B?K311%DJ%eoZsF}(d1RW}&0vac(Bagi;A4lJO!EeX($~rUqHO`?8{kXu_ zC|EB}bPyc$aTGj*xo+jmKM{w-Ew-K!suHRdYFw-K9qj1wzKw#47GyoT)N*(bm84c+ zlH;aqm89lOiZ=i9tqP{{*+YtGEw4*C5_)*BpMjOPtn_F&aE{V2BHr0oytFuYBNovh zMstLz7^a`}RzeSV`>qJ(3%wG0b4QM%C=)d}@{it&m#BAJD(jA6iE}DYQK^qWRM%uw zj5&fA8L=J0Ep~y|C8{iFruo;8j~N+-?2qsr|BM~8r;Lm)yke|HtxnzN7CG8UEusi- zYrCc?F?x*-mkitB5Zv5a%`lpC`AR^XfRy3h5kikmtHGUEpCsyBj5_nOzlkwzq;hPv znYG;XX>x;~h=ij9BFQPDwN>XL7Plpce1hb#GuByfbi+q~lrbt_7FCM1^gsExIDgAb zAB;}g7b1swcB6>x(;=Ei$dV0GGxr0DH!ZYq1u)W!p#>5uE|VeUXU5~~*!PaYd5KPn z0ToJH$4^GDfAS1Kt0HGSi#It(v!Wd07}*>@{B0Vgpp1WKW;6F48ETgnetrL-@Um+_ zYRNip&^#2!6Xjp*;XO(67ba^f=@cT%81DqG8KazHbDhuHvtM5J_jkoHS$;Fk*)i1y z=L0hEluNikxGx0yG(F1Yf_iA#_65~;rVv@%^OI^TlGx9IEQ6LSd6kqm5Q;tO5l}|r zkGFZ0G9kZe6g+`|qFtj97$>|aBN2(q38T_j|BC@w z%^&hE5X4OW`to=aoix;q*q@+ADQw?mSg_-Y8RvTPvox#=47KX{d2d}4QEgvYERLfX zH)y0tmak>zlynSM+~9Gb*)WiO=Cm$E!KQ7iQ?B^e=Oq=kjv`#7PbZX|;0f?}QWBmb z zu8*4Gz9tQic~xVscAhA}Yt^T>qtsRnuFaHkV=YVrPM&t3xhpZMn{thIVyJW4{KAo; z#<2$j=frP!j=4I17bbIP`PTaq*~)lfTe2vU;I{bDs((H65+RRHl-p6Nm{76~{OmB} zX3hTF5e*;Qh2Y!jNHT|}TfH3xx-;-MUFE^bExXneJ>E&$L?_6e|9f9_v!>8*WYxd2 zSiVzvf6s1g3;y`)3=w&5YQB_f>XzdV&3^Odc+yfU^FRX%m4==r(9-yykS0Yrz~iYp1=I$8P#^%x~w5NlVzUnK3+NTvFF;@uv@(m z4uH#eUtizHw;#BwZQxE3SWi7DBd%YwC46O~=6qBs@b#Ea*7Q|oH=WHv8Gf#Va36U*&#SFi8&bvx{-tanMot&43zI!F&n*c_P1QQt)I*Os@HkrdZ9Uf` z^>AoPL2##s7kx|j%SoazOp?3_+VeqyQLLecfF?#0Q&pGG$86r;8inIGb@la~+>72b z_||P(pS9YRwTgd~c7c$rwSk!58}ncD(9_W{lA8sEL%3W#mW}Lcop0dyW68LzEy7z+ zv@7Fct^3dG)UFKc#1ROlB}CTs-Cd2%O!PDeS44B!N0mfK#u3n;66O6mH-1ao0t*IF zTO^jTd|6wh66T5!sd;a$yHp|SI>c|lF1gt6qLUdrv2%Me)@U>P{#DRaQP~StZ{6sE zWN!Dj2n)ZcE@^kq3Sj6B@=0Wcie*QSV}e!<7f+GaP<2XPvYLkrP+PJ6rRo0m2_=lq z*Qj1)6DGiV@^U->yY%t94jqkpBBYZTH!h7)4R1c^Qa6|_t!g0)A8D*lZ?WSSR~E7w zl<_fLwS3gMN@Qh#Df{BH$iVKjwV=pvz>-7b>EQ^f7>cxL-`eYNGA7C_=8`fasQa5x+1l4$5J# z+pY{N6aPrI3C7hJhmtFIuOL``!!B0!(5V`d(%!ERQYEfEZr5LMGekB>u6muv@yYHc zUyzlN{$fQBL$#DJ4qr4|jUqR$f(u+6uMMrc3!q3u+t$XSd~0rwQAj;0pKXPJd0G+| z`j?rR*?1Qj@pKAG4nVYMMoQ{a!;~;jGlz+i5akES6*pS)fvKv<0I7`+Qn4Z9OCe=Q zvYJfspevm?;dKo{zPG#lQzSAUbf8}mP=@@*@3S7x98`axTxojYV}^pg0Eizh+KVYn}DDXrZSzey|8;JC*RQxl(M5X~q3!WJSC(P)_s`vB+CpN0JR+4+b?A zR{&$kUZw2v?hbI*FSvR{EBGBxy@^Cu`V9wZKo+4?LxWT)qkN#yxY<6Ebf$-VVNRI* zIepDH6>-JRRqG7b#jJRutTYfjf7|pX!DCmlx|&sjB!5n8-I5ehdB6mEip`Qv`M2o=PFO~p)AUU$@~gJM z=^ss^Q@fsMPY=ULn(v}_?>uuh+}RRZxa?|&jvw5xF2~J=(b0tr5y&0izgpTcqw1IRzvw0dhQOgJ4XoF!|tB96K$0b13Azsxc@KA|IR!pR6BznFj<( z24lcLXJ7EXERvnpzdY8U`RUWWKU3ZqN+n9RCjdd|lH*53NJemQlE4FQQ9v$-#p*0% zfrzBEcTWUJ!7i^9rHu8&ERCJu!erptY4aI*Qu zY_@yM7>K`_+WpiiawOC2goCa*PtFsdV${oqnm7i+Z<3XHQ zugGW}CiPnVB0ehw5#F|3iBkhyt~4qKr&es_+?E_JXG5n-Rzku7)AQS0$=r;`)Y!v& z&+PO0dF$EE$|}R>isQ88=*obphy3)rFbQ&`UmpO-HG-i0yaJIc+;180k<0;@7GKd- zhu}=>0w?nU>}AA5Ad24+%1A+yfn%uD1WZ(b@Uk{`bH_pq6bz3|+DA?B;OZc_WjbH2 zH-HV{hO7_o+v)}XddZFr2^NF)V3}XWV&@4CKuW-}8~!*+Lf_CZf3^rbil4KtOf{^n z6%C4AfsrIeSRCF!;T2t}(epi**?I6(%^%Gs!JMRq-n&qIo*i#a>Tyyo<_Z_UDvo0Z zdfIpy{JE{*Qr?DMv*aTnzXP7i#Y*)9meBxz%&!iyf#+erW0e8r@2nt6r}+Ps=eeM@ z?RY=RkbW6tg*;_IR>;Pb_UxZWmXKpFM5m*}PLiLuz<_ljGJqeC05SM`1CS~jDm6OT zx%F&dZ2VO^9HW`rRKW-6ghodj{K-K8**>_JhlsWPY!3lmS3e9gRQiw1fgCW|j9_MQ zGc0?skhA(Cq}{jJPwx#eK#CRZS#JOv1GEuHLCG!l`DlQzT^BI^Y_{kM7p*fAS%D;n z{VV{B`11-79(?+zeFSB@!@Y?Q6I|iz>a*kZO-O z_(p+4j%A60r%e?AH`oO?c>Sy?E_Ckm3!h#X>A0R6CtmQAM7dk9GM5SMnQf}TS-=IA z3Z;!Vg_j_}G<*tt)>o6UhcV9vCfZR;`q)oY)Pv(HfT=tC^d9I!vsOdHZ!UF0vFC$1 zRYX)&+k65>;(?S?lNP}%1NGY?@s*&nitwX_pkxJ(G1gEh?O}rv3^YlK`!Zx(vLjh* zD2DcoWpZ|#^Ego6#@%KZCPkiI7WV2K&js~JPnvkDE~dE+lXn1?QpmeqxDHq$~zbXs3>Z- z&Q|;u1i$H%uS(q@d&|b1|b+=v9s2#?uC%j(fkw(=Cbz%$Ujj;L|Mw0(};~6-vjO4mncH)fYDtY zw1gijbwvuYcniw75xMawYNG`*b$;DpKetu??6x?%$moTk7O;_qS^`z)&ANZyu~))y zN*YG%eqweciwOf8{okX^qE_vpjQf%&i+qHkU+hy?Ui^t_(U!@p7e^lW2!FnocLXn4 z6}h@t(ULzq8^!#iVr}}T?QHbH-YyIXUZhJNEjN@01G5|!5w+z9#W!qv3m8Ujcz*-+ zDk8FT--zgK|1%3LjjFfoonv+FoSnL-4Oz^Lwy7Em2`~bycu>ncU2^Hyiw09D<9MXs zU>Y|%Re=ZCQuy#~<7cL~K)&^#oHFw!aU7CkQla+G*3_=wK+xj`Yl&1+W9R2O^bNp9 ze)9S*f1Yz2J+Nh8`?j_SDL9(NtQ#2Xmqm6!3+*a|O=>vR-^7JJcC`jdT|FLX18={) zW)s#Hqvmik^s1CznFZ>hROW=CHZsnd8(a?aYg`Uu!eCy#rFC;}=XB-RS4Mg-(L>B50LqGC$2`3lR`qp9|ePWU4BC;>CMAU zkZ-aEf*cV*w9(Bu)_d?x`N8Ai~M8ul=!c5ajv*s#4 z%`0YqbI@MB*0cTuWbsL)|L~Cr*?Gzd7(R4?qH5y4Oc*5mE;~Y2Q=BGvi72zJfcyXp z29#{xJ5s0BPS0&^jH_YxFD}!YyM@~q2P-7Z0Gio3^M2e6wmBIr_jGRm2SqhKEJktB zeBfwm8|tm=^;8gWZI-)rRJ0t7FkLPK|7LNn@YZfWr`k~)B}GWe<|4%KXV!{C97Vv! zfuRkx(NKhhTWC;6k5Qwx-46xVF#szwJ^i`!Xz~Ib#Ff_vKsvx+6Qui2Hoaq8`r6lL zI~XKFaL{=j7s@vOV0*$`Ml44F0p`K{Chn%%$>;b`Hbm+f-AuY%*jM(UW|g;%;VZ7& zdSf*l5)09qPVqWv0ykEk_`B;|&r5^)O$sGHUz3n?8Gp{r;8WcUNx!+B8K}9(Putp% z^i72kqwBaZIv@r#f$LK_C*)eo!>uLphS^*W>BnzO6B4=6>4D9=v+h}Df%)1%tw(8) zhjXG66rC;~b~mtw2U2^pG)RPF6z3x->PSExE4%K!DuJbD^Ex;^*hG>adT4&}>dn7q z@v(&kvIWAVzEDr@7)3I>MWS+9YXldjBijjex6`vIMSAdJ6qsiZevEYXMg$s}v8cv8 z&Pgk!YIC6=i7L=i7k;eu$+S`p$D9oIlk|s?Q|_vYtQ>=w-(R0{Rr|;&7HosWmw=H} zVo9GzfX3j}iFJ06@q8iBVj?g+r<7-=E-&!iBC$yo48~8;NlyRqrmoL*{6m z!iuBr%?e5jWdZ#k3iU|al+qo4c^B0P{|KM}EXiLB&Q?B)M-VUTsuuJuO=e%rI!pO6 zu;E1MLK7gWA*~T0T)x-;#JLgrJSeMIWnGZ$2{MLhTE-XTZ@_Z9BX?G(-y2GB5Z)23 z=r~llix*VvS~<1HS+!DT(`UB)_n*4ZLz;zrWUPg|v9{a2hn!$$F>aJsBe|Utxq8w<7E~lmV?W2#>$? zp_)KR950U0wYSIG)#k@sM)|*; z4a%Kf@OqD)@@b=2!AR_7^6pmx#&Y>=>5aKzUo9_$nT9?z_kX(U@;djs-3)<_{^|KR zY@_2;8+{E_TBhi(Rq9J2&v4vpMnAI4b^8nMta=xIeK6x}))jdlw({u{qUHZeWBIT} z689QAKNwF>(l(@VqdR)1#A z=Y@9gH6^@xCzR$C1QN(wbF;6ee)Ct%gz?qkA~TOLoVFV{Ya%*T5xT>3zLro%p{c%j z2;BemKzG31$YYtv=U9V=YyqiPBfTJ?(qE5k`8i_rZbCrn*#y|f=wtiJQ2w%esLrY5 zJd8LE!C@OT+!J2X0Gf;!&*K#-;dWnz0wXYPL-AKTlqUJw=tJNe6~p#6<_9vw0M^#d zukRUMS^ir8mRzZqbxQObM~UfSifFDiN&f9ii!#`=YXE+pvmg6 zOxsZdQPU^cQzJTs6Uq==$J$Jqd3JKDt5tzdU56anX~_4ajlRhf`5%|$SofZ z-`TtlaiQmTg8y`EJlnr|NFU$~UWJGp^0+QJ{b``+jI{92;^I>^DoomgR*;;tr! zNi4H)TC|q8N4P;$^L@EU3pi@U<;3cX%T%K02vv=c-z_i>a;irJAUH3lFZ%lWYp1A! zsx}^!8x!n=O0x?KCwTf9sRkbrSEjYjQt;9Sq0IRVGkT z1)Vi9bysgG{!UjUD=hK?S6+U(NqHY{g%_%DF|-Aw&%OV&HL7LyB_#DSsBeWxytF$` zM8D#$nY#s*-fVZS(!N=?G2X>15^9tNr~3>?4)$x<<$c%*=-c2>wTfm-F05oMUbtxF zQfJ%IV10>3WTP(}<&uc+$a3^NC>?VbXdeJWFRJ*MRn)~hX`A$#TSe!1a>HLf>^?%S zuWZjvQ>|0rH$mPD5Gmb?)AFU4ziWwMp*wI@m(;ajDqWP5VLaE9bXIL=5=Wp&*Ihl- z+jd~4G>hsp7yupo_3KyCqHasz%%z3Z*7+cc+E^z3icU#E=IEo}tJHJxlrLWc3Yq(i zGUtML#c6nXIeXc9`Fy%@j`!}q$mlcxnE$s;{Qs&={NK9}|M%sPL#aU783IA`u`PIK1^DZV L)#Y+yY~23=0.27.0" "cryptography>=42.0.0" - - name: Copy application files - type: files - files: - - source: ./config.py - destination: ./config.py - - source: ./client.py - destination: ./client.py - - source: ./bg_worker.py - destination: ./bg_worker.py - - source: ./kalshi_background.py - destination: ./kalshi_background.py -``` - ---- - -## Step 2: Runtime Pattern - -Kalshi background uses the app runtime background loop (coming soon but for now you can use the example apps to see how to use it for your apps!): - -```python -from app_runtime.background import BackgroundRunContext, run_background - -def kalshi_ambient(ctx: BackgroundRunContext) -> None: - ... - ctx.bg.submit_context(content=content, uris=[], priority=priority) - -if __name__ == "__main__": - run_background(kalshi_ambient) -``` - -Core idea: - -1. gather external state (APIs, events, account state) -2. create context messages with enough detail to be actionable -3. submit with priority (`LOW`, `DEFAULT`, `HIGH`) based on urgency - ---- - -## Step 3: Submit Rich Context (Not Generic Text) - -From Kalshi, stronger context items include concrete fields: - -- ticker and title -- absolute and relative price change -- before/after values -- settlement result and revenue impact -- order IDs and status change - -Example style: - -```text -Price alert: FED-RATE-SEP moved up 12c (was 41c, now 53c) -``` - -This is much better for proactivity than broad summaries without entity/time/value details. - ---- - -## Step 4: Reliability and Cleanup - -Kalshi background explicitly closes resources on shutdown (This is important and needs to be done for your apps! Your apps will fail at runtime otherwise): - -- `atexit.register(_cleanup)` in `kalshi_background.py` -- `_cleanup()` closes `KalshiBackgroundWorker` client and event loop -- worker itself exposes `close()` and wraps API failures - -Do the same in your app. Leaving connections open at shutdown is a common source of flaky reruns and container instability. - ---- - -## Step 5: Deploy and Verify - -```bash -truffile validate ./app-store/kalshi -truffile deploy --dry-run ./app-store/kalshi -truffile deploy ./app-store/kalshi -``` - -Then verify: - -- scheduled runs execute at expected interval/window (Tip: Keep interval small when testing to see results earlier, you can adjust it and redeploy later when you know it works!) -- context submissions are visible in runtime logs, right now there is not a away for you to go and watch runtime logs for your background apps, we will be adding this feature soon to the sdk. Till then please make sure you follow the example app templates! - ---- - -## When to Use Background - -Use background apps when you need: - -- periodic polling/monitoring -- context emission for proactive behavior, you would like Truffle to take an action! (Do not be restrained in thinking that a background app can only influence actions for that app only, if I get an instagram message regarding an amazon order, Truffle can use that to do an action of adding said item to my cart!) -- domain-specific event digestion over time - -If your app also needs callable tools, keep both blocks in one app (`metadata.foreground` + `metadata.background`) as shown in Kalshi. diff --git a/docs/building-apps.mdx b/docs/building-apps.mdx deleted file mode 100644 index a813f53..0000000 --- a/docs/building-apps.mdx +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: What are Truffle Apps? ---- - -## What Are Truffle Apps? - -Truffle apps are containerized programs that run on your Truffle and extend what the agent on your Truffle can do. - -There are three app shapes you can build today: - -| Type | When It Runs | What It Does | -|------|--------------|--------------| -| **Foreground** | On demand | Exposes MCP tools (served over `streamable-http`) the agent can call during tasks | -| **Background** | On schedule | Submits context to the proactive agent | -| **Both** | On demand + schedule | Combines MCP tools and scheduled context submission | - - - - Full example app using both foreground tools and background scheduling. - - - Background-only example that submits periodic context. - - - ---- - -## App Config Shape (Current) - -The current SDK supports app-specific foreground/background process blocks under `metadata`. -An app can have both or either! - -```yaml -metadata: - name: Kalshi - bundle_id: org.deepshard.kalshi - description: | - Have Truffle trade and monitor Kalshi prediction markets for you. - icon_file: ./icon.png - - background: - process: - cmd: ["python", "kalshi_background.py"] - working_directory: / - environment: - PYTHONUNBUFFERED: "1" - KALSHI_API_KEY: "REPLACE_WITH_KALSHI_API_KEY" - KALSHI_PRIVATE_KEY: | - REPLACE_WITH_KALSHI_PRIVATE_KEY_PEM - default_schedule: - type: interval - interval: - duration: 30m - schedule: - daily_window: "00:00-23:59" - - foreground: - process: - cmd: ["python", "kalshi_foreground.py"] - working_directory: / - environment: - PYTHONUNBUFFERED: "1" - KALSHI_API_KEY: "REPLACE_WITH_KALSHI_API_KEY" - KALSHI_PRIVATE_KEY: | - REPLACE_WITH_KALSHI_PRIVATE_KEY_PEM - -steps: - - name: Install dependencies - type: bash - run: | - apk add --no-cache gcc musl-dev libffi-dev openssl-dev - pip install --no-cache-dir "httpx>=0.27.0" "cryptography>=42.0.0" - - name: Copy application files - type: files - files: - - source: ./config.py - destination: ./config.py - - source: ./client.py - destination: ./client.py - - source: ./bg_worker.py - destination: ./bg_worker.py - - source: ./kalshi_foreground.py - destination: ./kalshi_foreground.py - - source: ./kalshi_background.py - destination: ./kalshi_background.py -``` - ---- - -## Build and Deploy Flow - -Use this flow for all app types: - -1. Validate config and source files. -2. Check deploy plan without mutating device. -3. Deploy via builder session. - -```bash -truffile validate ./my-app -truffile deploy --dry-run ./my-app -truffile deploy ./my-app -``` - -Under the hood, deploy uses a build session, uploads declared files, runs `bash` steps, and finalizes app metadata/process config. - ---- - -## Kalshi Walkthrough: - -Kalshi is the best reference because it demonstrates both paths in one app: - -- `kalshi_foreground.py` exposes MCP tools such as `get_markets`, `get_market`, `create_order`, and `get_positions`. -- `kalshi_background.py` runs on schedule and submits portfolio/market context through `ctx.bg.submit_context(...)`. - -This is the intended split: - -- **Foreground** is your callable tool surface (MCP spec). -- **Background** is your proactive context pipeline. - -The quality of your background context directly affects proactive behavior. Rich, precise submissions perform better than generic text. - ---- - -## Runtime Stability Pattern (Important) - -In Kalshi, network clients are explicitly closed on shutdown: - -- foreground: `atexit.register(_cleanup)` calls `KalshiClient.close()` -- background: `atexit.register(_cleanup)` closes the worker client and event loop - -Do this in your own apps. Closing outbound clients/sessions before process exit prevents flaky shutdown behavior and avoids container-side runtime crashes. - ---- - -## Next Steps - - - - MCP tool server patterns, examples, and deployment details. - - - Scheduling, context submission, and proactivity-oriented design. - - - ---- - -## Contribute to the Truffle App Store - -Contributors are welcome to submit apps to the Truffle App Store. - -To submit your app: - -1. Open a PR with your app under the `app-store/` folder. -2. Include a screen recording of your app in action. - -For accepted apps, the Truffle team will deploy the app to the App Store so others can use it. Your name can be credited as the app author. In some cases, changes may be required for runtime reliability. - - - Example app card in Truffle App Store with author attribution - diff --git a/docs/cli.mdx b/docs/cli.mdx deleted file mode 100644 index 61231f9..0000000 --- a/docs/cli.mdx +++ /dev/null @@ -1,250 +0,0 @@ ---- -title: Truffile CLI -description: "Connect to your Truffle and deploy apps using the truffile CLI" ---- - -## Overview - -The truffile CLI lets you discover, connect to, and deploy apps to your Truffle devices. - -```bash -truffile -``` - -``` -๐Ÿ„โ€๐ŸŸซ truffile - TruffleOS SDK - -Usage: truffile [options] - -Commands: - scan Scan network for Truffle devices - connect Connect to a Truffle device - disconnect Disconnect and clear credentials - deploy [path] Deploy an app (reads type from truffile.yaml) - validate [path] Validate app config and files - delete Delete installed apps from device - list List installed apps or devices - models List models on your Truffle - chat Chat on your Truffle (REPL) -``` - ---- - -## Connecting to Your Truffle - -### Step 1: Scan for Devices - -Find Truffle devices on your local network: - -```bash -truffile scan -``` - -``` -โœ“ Scanning for Truffle devices (5s) - -Found 6 Truffle device(s): - - 1. truffle-6070 (192.168.1.109) - 2. truffle-5970 (192.168.1.104) - 3. truffle-6272 (192.168.1.32) - 4. truffle-7280 (192.168.1.87) - 5. truffle-0148 (192.168.1.60) - 6. truffle-6239 (192.168.1.129) - -Select device to connect (1-6) or press Enter to cancel: -``` - -Select a device by entering its number. - -### Step 2: Enter Your User ID - -After selecting a device, you'll be prompted for your User ID: - -``` -โœ“ Resolving truffle-6272.local - - Make sure you have: - โ€ข Onboarded with the Truffle app - โ€ข Your User ID from the recovery codes - -? Enter your User ID: -``` - - -You can find your User ID in the **Settings โ†’ About** section of your Truffle desktop client. - - -### Step 3: Approve the Connection - -After entering your User ID, you'll see: - -``` -โœ“ Connecting to device - -โ€ข Requesting authorization... - Please approve on your Truffle device -โ ‹ Waiting for approval -``` - -A dialog will appear on your Truffle client asking you to approve the connection: - - - Approve connection dialog - - -Click **Approve** to allow the connection. - -``` -โœ“ Waiting for approval - -โœ“ Connected to truffle-6272 -``` - -### Reconnecting Later - -Once you've connected to a device, you can reconnect directly without scanning: - -```bash -truffile connect truffle-6272 -``` - -Your credentials are saved locally, so you won't need to re-enter your User ID or approve again. - ---- - -## Disconnecting - -To disconnect from all devices and clear your saved credentials: - -```bash -truffile disconnect all -``` - - -This will require you to go through the full connection process again (User ID + approval) the next time you want to connect. - - -To disconnect from a specific device: - -```bash -truffile disconnect truffle-6272 -``` - ---- - -## Deploying Apps - -Once connected, you can deploy apps to your Truffle. - - -If you haven't built an app yet, check out the [Building Apps](/sdk/building-apps) guide first. - - -### Basic Deploy - -Deploy an app from the current directory: - -```bash -truffile deploy -``` - -Or specify a path to your app folder: - -```bash -truffile deploy ./my-app -``` - -truffile will: -1. Validate your `truffile.yaml` configuration -2. Check syntax for all Python files -3. Resolve app runtime shape (**foreground**, **background**, or both) -4. Upload files and run installation steps -5. Register the app with your Truffle - -### Interactive Mode - -Deploy with an interactive terminal session for debugging: - -```bash -truffile deploy -i -``` - -This uploads your files and opens a terminal inside your app's container. You can: -- Test your app manually -- Install additional dependencies -- Debug issues in real-time - - -You can even install [Claude Code](https://claude.ai/code) inside the container to help fix or extend your app! - -```bash -curl -fsSL https://claude.ai/install.sh | bash -``` - - -Type `exit` or press `Ctrl+D` to finish the deploy. - ---- - -## Listing Apps & Devices - -### List Installed Apps - -See all apps installed on your connected Truffle: - -```bash -truffile list apps -``` - -### List Connected Devices - -See all devices you have saved credentials for: - -```bash -truffile list devices -``` - ---- - -## Inference Commands - -Inference commands available in CLI: - -```bash -truffile models -truffile chat -``` - -For `truffile chat`, runtime behavior is controlled inside REPL using slash commands (`/help`, `/config`, `/models`, `/attach`, `/reasoning`, `/mcp`, etc.). -Run `truffile chat` by itself and manage behavior in-session. -See the [Inference](/sdk/inference) page for full command details. - ---- - -## Command Reference - -| Command | Description | -|---------|-------------| -| `truffile` | Show help menu | -| `truffile -h` | Show help menu | -| `truffile scan` | Scan network for Truffle devices | -| `truffile connect ` | Connect to a specific device | -| `truffile disconnect ` | Disconnect from a specific device | -| `truffile disconnect all` | Disconnect from all devices | -| `truffile deploy` | Deploy app from current directory | -| `truffile deploy ` | Deploy app from specified path | -| `truffile deploy -i` | Deploy with interactive terminal | -| `truffile validate [path]` | Validate app config and sources | -| `truffile delete` | Delete installed apps from connected device | -| `truffile list apps` | List installed apps | -| `truffile list devices` | List connected devices | -| `truffile models` | List models on connected device | -| `truffile chat` | Start chat REPL | - ---- - -## Next Steps - -- Ready to build your own apps? Check out the [Building Apps](/sdk/building-apps) guide. -- Want to use your Truffle for AI inference? See the [Inference](/sdk/inference) guide. diff --git a/docs/focus-app.mdx b/docs/focus-app.mdx deleted file mode 100644 index dca878c..0000000 --- a/docs/focus-app.mdx +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: Build a Foreground App -description: "Give your Truffle MCP tools it can call on demand" ---- - -Foreground apps are MCP servers. They expose tools that Truffle can call during active tasks. - - -Foreground apps on Truffle must serve MCP over `streamable-http`. `stdio` transport is not supported for deployed foreground apps. - - -If you are new to MCP itself, read the protocol docs here: - -- [Model Context Protocol Documentation](https://modelcontextprotocol.io/docs) - - -Truffle foreground apps currently support the MCP tool surface (tool registration and tool invocation over streamable HTTP). Full MCP features are still work in progress - - -### Coming Soon - -- Additional MCP assets: resources, prompts, files, and skills -- Tool icons and richer tool metadata -- Read-only tool semantics -- Tool-driven user elicitation (requesting user input from a tool flow) -- Expanded authentication and session handling for stateful MCP servers - ---- - -## What We Are Building - -This walkthrough uses the Kalshi foreground service: - -- source: [`app-store/kalshi/kalshi_foreground.py`](https://github.com/deepshard/truffile/blob/main/app-store/kalshi/kalshi_foreground.py) -- config: [`app-store/kalshi/truffile.yaml`](https://github.com/deepshard/truffile/blob/main/app-store/kalshi/truffile.yaml) (`metadata.foreground.process`) -- app source repo: [`app-store/kalshi`](https://github.com/deepshard/truffile/tree/main/app-store/kalshi) - -It exposes callable tools like: - -- `get_markets` -- `get_market` -- `get_positions` -- `create_order` -- `cancel_order` -- `kalshi_health` - -These are regular Python functions decorated as MCP tools with clear descriptions and typed parameters. - ---- - -## Step 1: Foreground Config in `truffile.yaml` - -```yaml -metadata: - name: Kalshi - bundle_id: org.deepshard.kalshi - description: | - Have Truffle trade and monitor Kalshi prediction markets for you. - icon_file: ./icon.png - - foreground: - process: - cmd: - - python - - kalshi_foreground.py - working_directory: / - environment: - PYTHONUNBUFFERED: "1" - KALSHI_API_KEY: "REPLACE_WITH_KALSHI_API_KEY" - KALSHI_PRIVATE_KEY: | - REPLACE_WITH_KALSHI_PRIVATE_KEY_PEM - -steps: - - name: Install dependencies - type: bash - run: | - apk add --no-cache gcc musl-dev libffi-dev openssl-dev - pip install --no-cache-dir "httpx>=0.27.0" "cryptography>=42.0.0" - - name: Copy application files - type: files - files: - - source: ./config.py - destination: ./config.py - - source: ./client.py - destination: ./client.py - - source: ./kalshi_foreground.py - destination: ./kalshi_foreground.py -``` - -Use literal block style (`|`) for multiline secrets like PEM keys. - ---- - -## Step 2: MCP Server Bootstrap Pattern - -Kalshi foreground uses this runtime pattern: - -```python -from app_runtime.mcp import create_mcp_server, run_mcp_server - -mcp = create_mcp_server("kalshi") - -@mcp.tool("get_market", description="Get details for a specific market ticker.") -async def get_market(ticker: str) -> dict: - ... - -def main() -> None: - run_mcp_server(mcp, logger) -``` - -`create_mcp_server(...)` and `run_mcp_server(...)` are the Truffle app runtime wrappers used by foreground apps. - ---- - -## Step 3: Tool Design Guidance (from Kalshi) - -Kalshi tools follow a useful pattern worth copying: - -1. Validate inputs early (`limit`, `depth`, enum-like args). -2. Return structured success/error payloads, not plain strings. -3. Keep tool descriptions explicit so the model can route correctly. -4. Report auth/runtime issues using runtime error reporting where needed. - -Example tools in Kalshi: - -- read path: `get_markets`, `get_market`, `get_orderbook`, `get_positions` -- write path: `create_order`, `cancel_order`, `batch_cancel_orders` -- health path: `kalshi_health` - ---- - -## Step 4: Test MCP Tools in Chat Before Deploy - -You can test your MCP server with Truffle chat before shipping app changes. - -1. Start chat: - -```bash -truffile chat -``` - -2. Connect your MCP endpoint: - -- local MCP example: - - `/mcp connect http://127.0.0.1:8000/mcp` -- hosted MCP example (Exa): - - `/mcp connect https://mcp.exa.ai/mcp?exaApiKey=&tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check` - -3. Verify tool discovery: - -- `/mcp tools` - -4. Run a real tool-routing prompt: - -- `use the company research exa tool to find me information about SpaceX` - -This is the fastest way to validate tool schemas/descriptions and model routing behavior before deploying your foreground app package. - - -Do not commit or publish real API keys in docs, screenshots, logs, or recordings. - - ---- - -## Step 5: Connection Lifecycle and Cleanup - -Kalshi foreground keeps a shared API client and closes it on shutdown: - -- `atexit.register(_cleanup)` -- `_cleanup()` calls `await _api.close()` - -This matters in production. Explicitly closing connections before process exit prevents shutdown instability and reduces the chance of container runtime errors. - ---- - -## Step 6: Deploy and Test - -```bash -truffile validate ./app-store/kalshi -truffile deploy --dry-run ./app-store/kalshi -truffile deploy ./app-store/kalshi -``` - -After deploy, run a task that requires market data or order actions and verify Truffle is invoking the foreground tools. - ---- - -## When to Use Foreground - -Use foreground apps when you need: - -- request/response tools invoked during a task -- deterministic API integrations the model can call on demand -- low-latency, operation-style behaviors (search, lookup, actions) - -## Note -- Remember tool descriptions are very important, follow good practice and add tool descriptions with explination fo each parameter and example usage, this helps your Truffle in calling tools! diff --git a/docs/inference.mdx b/docs/inference.mdx deleted file mode 100644 index 401d651..0000000 --- a/docs/inference.mdx +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: Inference -description: "Use models and chat on your Truffle, including image attachments in chat" ---- - -Your Truffle exposes inference directly over `/if2/v1/*`. - -- `truffile models` โ€” list models on your connected device -- `truffile chat` โ€” interactive chat REPL (runtime config is done with `/` commands) - ---- - -## Quick Start - -### 1) List models - -```bash -truffile models -``` - -This calls the device endpoint: - -- `GET /if2/v1/models` - ---- - -### 2) Start chat REPL - -```bash -truffile chat -``` - -`truffile chat` is REPL-first. Use slash commands inside chat to configure behavior live. - -Chat requests are sent to: - -- `POST /if2/v1/chat/completions` - ---- - -## Image Input in Chat - -Use `/attach` to attach an image to the next user message. - -Local file: - -```text -truffile chat -/attach ./my-image.png -What is in this image? -``` - -Remote URL: - -```text -truffile chat -/attach https://example.com/image.jpg -Describe this scene briefly. -``` - -After the next prompt is sent, the pending attachment is cleared. - ---- - -## Chat Commands - -Use `/help` in REPL to list commands. - -### Core REPL commands - -| Command | What it does | -|---------|---------------| -| `/help` or `/` | Show available commands | -| `/history` | Show current conversation history summary | -| `/reset` | Clear current conversation state | -| `/models` | Open model picker and switch active model | -| `/attach ` | Attach image (local path or `http(s)` URL) for next message | -| `/config` | Show current chat config values | -| `/exit` or `/quit` | Exit chat | - -### Generation controls - -| Command | What it does | -|---------|---------------| -| `/reasoning on\|off` | Toggle reasoning output | -| `/stream on\|off` | Toggle streaming output | -| `/json on\|off` | Toggle JSON response mode | -| `/tools on\|off` | Toggle built-in default tools (`web_search`, `web_fetch`) | -| `/max_tokens ` | Set max output tokens | -| `/temperature ` | Set/clear temperature | -| `/top_p ` | Set/clear top-p | -| `/max_rounds ` | Set max assistant/tool loop rounds | -| `/system ` | Set system prompt | -| `/system clear` | Clear system prompt | - -### MCP in chat - -| Command | What it does | -|---------|---------------| -| `/mcp status` | Show connected MCP endpoint + tool count | -| `/mcp connect ` | Connect external MCP server | -| `/mcp tools` | List tools from connected MCP | -| `/mcp disconnect` | Disconnect MCP session | - -`/mcp connect` expects an HTTP(S) streamable MCP endpoint. - ---- - -## Direct HTTP Endpoints - -Use your device hostname/IP directly when needed. - -List models: - -```bash -curl -sS http:///if2/v1/models -``` - -Chat completion: - -```bash -curl -sS http:///if2/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{ - "model": "", - "messages": [ - {"role": "user", "content": "Give me one sentence about Truffle."} - ] - }' -``` - ---- - -## MCP Testing Workflow (Local and Hosted) - -`truffile chat` can be used as an MCP integration test harness before app deployment. - -### Local MCP test - -1. Start your local MCP server with streamable HTTP transport. -2. In chat, connect it: - - `/mcp connect http://127.0.0.1:8000/mcp` -3. Verify discovered tools: - - `/mcp tools` -4. Ask for explicit tool use and verify calls in output. - -### Hosted MCP test (Exa example) - -Use a hosted MCP endpoint exactly the same way: - -- `/mcp connect https://mcp.exa.ai/mcp?exaApiKey=&tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check` -- `/mcp tools` -- prompt example: `use the company research exa tool to find me information about OpenAI` - ---- - -## Command Reference - -| Command | Purpose | -|---------|---------| -| `truffile models` | List models on connected Truffle | -| `truffile chat` | Start interactive chat REPL | - ---- diff --git a/docs/installation.mdx b/docs/installation.mdx deleted file mode 100644 index 7dff12e..0000000 --- a/docs/installation.mdx +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: Installation -description: "Install the Truffile SDK to deploy apps to your Truffle" ---- - -## Prerequisites - -- Python 3.12 or higher -- Truffleยน -- The Symphony desktop client - -## Install Python - - - - ```bash - brew install python@3.12 - ``` - - Verify the installation: - ```bash - python3 --version - ``` - - - Download and install Python from [python.org](https://www.python.org/downloads/) or use Chocolatey: - - ```powershell - choco install python --version=3.12.0 - ``` - - Verify the installation: - ```powershell - python --version - ``` - - - ```bash - sudo apt update - sudo apt install python3.12 python3.12-venv python3-pip - ``` - - Verify the installation: - ```bash - python3 --version - ``` - - - -## Set Up Your Environment - -Choose your preferred method for managing Python environments: - - - - Create and activate a virtual environment: - - ```bash - python3 -m venv .venv - ``` - - - - ```bash - source .venv/bin/activate - ``` - - - ```powershell - .venv\Scripts\Activate.ps1 - ``` - - - - - If you prefer [uv](https://github.com/astral-sh/uv) for faster package management: - - ```bash - # Install uv if you haven't already - curl -LsSf https://astral.sh/uv/install.sh | sh - - # Create a virtual environment - uv venv - - # Activate it - source .venv/bin/activate # macOS/Linux - # or - .venv\Scripts\Activate.ps1 # Windows - ``` - - - -## Install Truffile - -With your environment activated, install the SDK: - -```bash -pip install truffile -``` - -Verify the installation: - -```bash -truffile -``` - -You should see: - -``` -๐Ÿ„โ€๐ŸŸซ truffile - TruffleOS SDK - -Usage: truffile [options] - -Commands: - scan Scan network for Truffle devices - connect Connect to a Truffle device - disconnect Disconnect and clear credentials - deploy [path] Deploy an app (reads type from truffile.yaml) - validate [path] Validate app config and files - delete Delete installed apps from device - list List installed apps or devices - models List models on your Truffle - chat Chat on your Truffle (REPL by default) - -Examples: - truffile scan # find devices on network - truffile connect truffle-6272 - truffile deploy ./my-app - truffile deploy --dry-run ./my-app - truffile deploy # uses current directory - truffile validate ./my-app - truffile list apps - truffile models # show models on your Truffle - truffile chat # open interactive REPL chat -``` - - -Make sure you've already onboarded your Truffle with the desktop client and created an account before proceeding. - - -## Next Steps - -Now that you have Truffile installed, head to the [CLI guide](/sdk/cli) to connect to your Truffle device. From 5e460b6532bdc5758c41bac4c5f7e60116353ef2 Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sun, 5 Apr 2026 18:14:01 -0700 Subject: [PATCH 10/12] publishing taken else whee, cli is nice now --- .github/workflows/publish.yml | 44 - .github/workflows/test.yml | 19 +- build/lib/truffile/__init__.py | 73 ++ build/lib/truffile/_version.py | 24 + build/lib/truffile/app_runtime/__init__.py | 102 ++ .../truffile/app_runtime/abrasive/__init__.py | 1 + .../truffile/app_runtime/abrasive/extract.py | 157 +++ .../truffile/app_runtime/abrasive/fetch.py | 48 + build/lib/truffile/app_runtime/app_client.py | 90 ++ build/lib/truffile/app_runtime/auth_modes.py | 39 + .../app_runtime/background/__init__.py | 10 + .../truffile/app_runtime/background/client.py | 64 ++ .../app_runtime/background/runtime.py | 62 + .../truffile/app_runtime/browser/__init__.py | 3 + build/lib/truffile/app_runtime/browser/cdp.py | 258 +++++ build/lib/truffile/app_runtime/core.py | 68 ++ build/lib/truffile/app_runtime/errors.py | 93 ++ build/lib/truffile/app_runtime/foreground.py | 271 +++++ .../lib/truffile/app_runtime/grpc_harness.py | 197 ++++ build/lib/truffile/app_runtime/icons.py | 10 + build/lib/truffile/app_runtime/jsonrpc.py | 67 ++ .../lib/truffile/app_runtime/mcp/__init__.py | 15 + build/lib/truffile/app_runtime/mcp/config.py | 4 + .../truffile/app_runtime/mcp/helpers_fast.py | 16 + .../truffile/app_runtime/mcp/helpers_mcp.py | 16 + build/lib/truffile/app_runtime/mcp_harness.py | 104 ++ build/lib/truffile/app_runtime/oauth.py | 126 +++ build/lib/truffile/app_runtime/protocols.py | 91 ++ build/lib/truffile/app_runtime/responses.py | 15 + build/lib/truffile/app_runtime/result.py | 79 ++ build/lib/truffile/app_runtime/stores.py | 114 ++ build/lib/truffile/app_runtime/testing.py | 334 ++++++ build/lib/truffile/app_runtime/worker.py | 199 ++++ build/lib/truffile/assets/Truffle.png | Bin 0 -> 8922 bytes build/lib/truffile/cli/__init__.py | 142 +++ build/lib/truffile/cli/apps.py | 218 ++++ build/lib/truffile/cli/art.py | 551 +++++++++ build/lib/truffile/cli/chat.py | 561 +++++++++ build/lib/truffile/cli/commands.py | 49 + build/lib/truffile/cli/connect.py | 246 ++++ build/lib/truffile/cli/create.py | 170 +++ build/lib/truffile/cli/deploy.py | 255 +++++ build/lib/truffile/cli/infer.py | 1006 +++++++++++++++++ build/lib/truffile/cli/markdown.py | 61 + build/lib/truffile/cli/models.py | 221 ++++ build/lib/truffile/cli/picker.py | 144 +++ build/lib/truffile/cli/prompt.py | 146 +++ build/lib/truffile/cli/ui.py | 226 ++++ build/lib/truffile/cli/validate.py | 24 + build/lib/truffile/cli/welcome.py | 169 +++ build/lib/truffile/client.py | 17 + build/lib/truffile/deploy/__init__.py | 4 + build/lib/truffile/deploy/builder.py | 125 ++ build/lib/truffile/deploy/plan.py | 115 ++ build/lib/truffile/deploy/steps/__init__.py | 3 + build/lib/truffile/deploy/steps/bash.py | 39 + build/lib/truffile/deploy/steps/files.py | 49 + build/lib/truffile/deploy/steps/text.py | 95 ++ build/lib/truffile/schedule.py | 215 ++++ build/lib/truffile/schema/__init__.py | 4 + build/lib/truffile/schema/app_config.py | 178 +++ build/lib/truffile/schema/runtime_policy.py | 215 ++++ build/lib/truffile/sdk.py | 75 ++ build/lib/truffile/storage.py | 95 ++ build/lib/truffile/transport/__init__.py | 15 + build/lib/truffile/transport/client.py | 520 +++++++++ pyproject.toml | 26 +- scripts/build_package.py | 114 -- tests/test_chat_helpers.py | 13 +- tests/test_cross_platform.py | 21 +- tests/test_prompt.py | 148 +++ truffile/_version.py | 25 +- truffile/cli/__init__.py | 16 +- truffile/cli/art.py | 551 +++++++++ truffile/cli/chat.py | 261 +++-- truffile/cli/commands.py | 49 + truffile/cli/connect.py | 2 +- truffile/cli/create.py | 5 +- truffile/cli/infer.py | 278 ++--- truffile/cli/markdown.py | 61 + truffile/cli/picker.py | 107 +- truffile/cli/prompt.py | 146 +++ truffile/cli/ui.py | 108 +- truffile/cli/welcome.py | 169 +++ 84 files changed, 10293 insertions(+), 573 deletions(-) delete mode 100644 .github/workflows/publish.yml create mode 100644 build/lib/truffile/__init__.py create mode 100644 build/lib/truffile/_version.py create mode 100644 build/lib/truffile/app_runtime/__init__.py create mode 100644 build/lib/truffile/app_runtime/abrasive/__init__.py create mode 100644 build/lib/truffile/app_runtime/abrasive/extract.py create mode 100644 build/lib/truffile/app_runtime/abrasive/fetch.py create mode 100644 build/lib/truffile/app_runtime/app_client.py create mode 100644 build/lib/truffile/app_runtime/auth_modes.py create mode 100644 build/lib/truffile/app_runtime/background/__init__.py create mode 100644 build/lib/truffile/app_runtime/background/client.py create mode 100644 build/lib/truffile/app_runtime/background/runtime.py create mode 100644 build/lib/truffile/app_runtime/browser/__init__.py create mode 100644 build/lib/truffile/app_runtime/browser/cdp.py create mode 100644 build/lib/truffile/app_runtime/core.py create mode 100644 build/lib/truffile/app_runtime/errors.py create mode 100644 build/lib/truffile/app_runtime/foreground.py create mode 100644 build/lib/truffile/app_runtime/grpc_harness.py create mode 100644 build/lib/truffile/app_runtime/icons.py create mode 100644 build/lib/truffile/app_runtime/jsonrpc.py create mode 100644 build/lib/truffile/app_runtime/mcp/__init__.py create mode 100644 build/lib/truffile/app_runtime/mcp/config.py create mode 100644 build/lib/truffile/app_runtime/mcp/helpers_fast.py create mode 100644 build/lib/truffile/app_runtime/mcp/helpers_mcp.py create mode 100644 build/lib/truffile/app_runtime/mcp_harness.py create mode 100644 build/lib/truffile/app_runtime/oauth.py create mode 100644 build/lib/truffile/app_runtime/protocols.py create mode 100644 build/lib/truffile/app_runtime/responses.py create mode 100644 build/lib/truffile/app_runtime/result.py create mode 100644 build/lib/truffile/app_runtime/stores.py create mode 100644 build/lib/truffile/app_runtime/testing.py create mode 100644 build/lib/truffile/app_runtime/worker.py create mode 100644 build/lib/truffile/assets/Truffle.png create mode 100644 build/lib/truffile/cli/__init__.py create mode 100644 build/lib/truffile/cli/apps.py create mode 100644 build/lib/truffile/cli/art.py create mode 100644 build/lib/truffile/cli/chat.py create mode 100644 build/lib/truffile/cli/commands.py create mode 100644 build/lib/truffile/cli/connect.py create mode 100644 build/lib/truffile/cli/create.py create mode 100644 build/lib/truffile/cli/deploy.py create mode 100644 build/lib/truffile/cli/infer.py create mode 100644 build/lib/truffile/cli/markdown.py create mode 100644 build/lib/truffile/cli/models.py create mode 100644 build/lib/truffile/cli/picker.py create mode 100644 build/lib/truffile/cli/prompt.py create mode 100644 build/lib/truffile/cli/ui.py create mode 100644 build/lib/truffile/cli/validate.py create mode 100644 build/lib/truffile/cli/welcome.py create mode 100644 build/lib/truffile/client.py create mode 100644 build/lib/truffile/deploy/__init__.py create mode 100644 build/lib/truffile/deploy/builder.py create mode 100644 build/lib/truffile/deploy/plan.py create mode 100644 build/lib/truffile/deploy/steps/__init__.py create mode 100644 build/lib/truffile/deploy/steps/bash.py create mode 100644 build/lib/truffile/deploy/steps/files.py create mode 100644 build/lib/truffile/deploy/steps/text.py create mode 100644 build/lib/truffile/schedule.py create mode 100644 build/lib/truffile/schema/__init__.py create mode 100644 build/lib/truffile/schema/app_config.py create mode 100644 build/lib/truffile/schema/runtime_policy.py create mode 100644 build/lib/truffile/sdk.py create mode 100644 build/lib/truffile/storage.py create mode 100644 build/lib/truffile/transport/__init__.py create mode 100644 build/lib/truffile/transport/client.py delete mode 100644 scripts/build_package.py create mode 100644 tests/test_prompt.py create mode 100644 truffile/cli/art.py create mode 100644 truffile/cli/commands.py create mode 100644 truffile/cli/markdown.py create mode 100644 truffile/cli/prompt.py create mode 100644 truffile/cli/welcome.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 2d91f82..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Build and Publish - -on: - push: - tags: ['v*'] - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.PYFW_DEPLOY_KEY }} - - - name: clone pyfw - run: git clone --depth 1 git@github.com:deepshard/pyfw.git _pyfw - - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: install build deps - run: pip install grpcio-tools==1.76.0 protobuf>=6.30.0 googleapis-common-protos>=1.63.2 build setuptools-scm - - - name: build package - run: python scripts/build_package.py --pyfw-path ./_pyfw - - - name: build wheel - run: python -m build - - - name: publish to pypi - if: startsWith(github.ref, 'refs/tags/v') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_TOKEN }} - - - name: cleanup - if: always() - run: rm -rf _pyfw diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5604255..127f0fc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,31 +15,14 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: webfactory/ssh-agent@v0.9.0 - if: runner.os != 'Windows' - with: - ssh-private-key: ${{ secrets.PYFW_DEPLOY_KEY }} - - - name: clone pyfw - if: runner.os != 'Windows' - run: git clone --depth 1 git@github.com:deepshard/pyfw.git _pyfw - - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: install deps run: | - pip install grpcio-tools==1.76.0 protobuf>=6.30.0 googleapis-common-protos>=1.63.2 pytest + pip install pytest pip install -e . - - name: build package - if: runner.os != 'Windows' - run: python scripts/build_package.py --pyfw-path ./_pyfw - - name: run tests run: python -m pytest tests/ -v - - - name: cleanup - if: always() && runner.os != 'Windows' - run: rm -rf _pyfw diff --git a/build/lib/truffile/__init__.py b/build/lib/truffile/__init__.py new file mode 100644 index 0000000..1c0066e --- /dev/null +++ b/build/lib/truffile/__init__.py @@ -0,0 +1,73 @@ +import os +import sys +from pathlib import Path + +# Keep gRPC from enabling fork support in this CLI process. +os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" + + +def _ensure_bundled_truffle_on_path() -> None: + repo_root = Path(__file__).resolve().parent.parent + bundled_truffle = repo_root / "truffle" + if not bundled_truffle.is_dir(): + return + + repo_root_str = str(repo_root) + if repo_root_str not in sys.path: + sys.path.insert(0, repo_root_str) + + +_ensure_bundled_truffle_on_path() + +try: + from ._version import __version__ +except ImportError: + __version__ = "0.1.dev0" + +from .client import TruffleClient, ExecResult, UploadResult, resolve_mdns, NewSessionStatus +from .schedule import parse_runtime_policy + +try: + from .sdk import ( + ForegroundApp, + BackgroundWorkerApp, + tool, + ok, + err, + OAuth, + AppHarness, + ToolSpec, + ) + + # register app_runtime as an alias for truffile.app_runtime + # so old apps using "from app_runtime import ..." still work + # when only truffile is installed (no standalone app_runtime) + import truffile.app_runtime as _app_runtime + if "app_runtime" not in sys.modules: + sys.modules["app_runtime"] = _app_runtime + # register submodules so deep imports work too + for _submod in ("background", "mcp", "abrasive", "browser", "browser.web_fingerprint"): + _full = f"truffile.app_runtime.{_submod}" + _alias = f"app_runtime.{_submod}" + if _full in sys.modules and _alias not in sys.modules: + sys.modules[_alias] = sys.modules[_full] +except ImportError: + pass + +__all__ = [ + "__version__", + "TruffleClient", + "ExecResult", + "UploadResult", + "resolve_mdns", + "NewSessionStatus", + "parse_runtime_policy", + "ForegroundApp", + "BackgroundWorkerApp", + "tool", + "ok", + "err", + "OAuth", + "AppHarness", + "ToolSpec", +] diff --git a/build/lib/truffile/_version.py b/build/lib/truffile/_version.py new file mode 100644 index 0000000..8b6fe66 --- /dev/null +++ b/build/lib/truffile/_version.py @@ -0,0 +1,24 @@ +# file generated by vcs-versioning +# don't change, don't track in version control +from __future__ import annotations + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +version: str +__version__: str +__version_tuple__: tuple[int | str, ...] +version_tuple: tuple[int | str, ...] +commit_id: str | None +__commit_id__: str | None + +__version__ = version = '0.1.36.dev9' +__version_tuple__ = version_tuple = (0, 1, 36, 'dev9') + +__commit_id__ = commit_id = 'ge131e4e0f' diff --git a/build/lib/truffile/app_runtime/__init__.py b/build/lib/truffile/app_runtime/__init__.py new file mode 100644 index 0000000..14a2298 --- /dev/null +++ b/build/lib/truffile/app_runtime/__init__.py @@ -0,0 +1,102 @@ +from .app_client import AppRuntimeClient, report_app_error, AppRuntimeErrorType +from .auth_modes import OAuthAuth, PublicAuth, TextConfigAuth, VncAuth, load_required_env +from .browser import ChromiumCDPBrowser +from .core import ( + RuntimeConnectionInfo, + build_auth_metadata, + close_channel, + init_channel, + load_runtime_connection_info, +) +from .errors import AppAuthError, AppRuntimeFailure +from .foreground import ForegroundApp, ToolSpec +from .grpc_harness import InProcessGrpcServer +from .icons import phosphor_icon_url +from .jsonrpc import HttpxResponseAdapter, parse_jsonrpc_payload +from .mcp_harness import McpTestServer, call_tool +from .oauth import OAuth +from .protocols import ( + ApiKeyProvider, + AuthProvider, + BrowserSessionProvider, + CookieStore, + HttpResponse, + HttpTransport, + OAuthProvider, + TokenStore, +) +from .responses import err, ok +from .result import truncate_items, truncate_result +from .stores import FileCookieStore, FileTokenStore, MemoryCookieStore, MemoryTokenStore +from .testing import ( + AppHarness, + FakeApiKeyProvider, + FakeAuthProvider, + FakeBackgroundRuntime, + FakeHttpResponse, + FakeHttpTransport, + FakeOAuthProvider, + HarnessResult, + RecordedBackgroundError, + RecordedSubmission, + make_background_ctx, +) +from .worker import BackgroundApp, BackgroundWorkerApp, Submission + +__all__ = [ + "ApiKeyProvider", + "AppAuthError", + "AppHarness", + "AppRuntimeClient", + "AppRuntimeErrorType", + "AppRuntimeFailure", + "AuthProvider", + "BackgroundApp", + "BackgroundWorkerApp", + "BrowserSessionProvider", + "ChromiumCDPBrowser", + "CookieStore", + "FakeApiKeyProvider", + "FakeAuthProvider", + "FakeBackgroundRuntime", + "FakeHttpResponse", + "FakeHttpTransport", + "FakeOAuthProvider", + "FileCookieStore", + "FileTokenStore", + "ForegroundApp", + "HarnessResult", + "HttpResponse", + "HttpTransport", + "HttpxResponseAdapter", + "InProcessGrpcServer", + "McpTestServer", + "MemoryCookieStore", + "MemoryTokenStore", + "OAuth", + "OAuthAuth", + "OAuthProvider", + "PublicAuth", + "RecordedBackgroundError", + "RecordedSubmission", + "RuntimeConnectionInfo", + "Submission", + "TextConfigAuth", + "TokenStore", + "ToolSpec", + "VncAuth", + "build_auth_metadata", + "call_tool", + "close_channel", + "err", + "init_channel", + "load_required_env", + "load_runtime_connection_info", + "make_background_ctx", + "ok", + "parse_jsonrpc_payload", + "phosphor_icon_url", + "report_app_error", + "truncate_items", + "truncate_result", +] diff --git a/build/lib/truffile/app_runtime/abrasive/__init__.py b/build/lib/truffile/app_runtime/abrasive/__init__.py new file mode 100644 index 0000000..4ed5c07 --- /dev/null +++ b/build/lib/truffile/app_runtime/abrasive/__init__.py @@ -0,0 +1 @@ +# general helpers for webscraping diff --git a/build/lib/truffile/app_runtime/abrasive/extract.py b/build/lib/truffile/app_runtime/abrasive/extract.py new file mode 100644 index 0000000..2f2f8ca --- /dev/null +++ b/build/lib/truffile/app_runtime/abrasive/extract.py @@ -0,0 +1,157 @@ +import trafilatura +from trafilatura.metadata import Document +from trafilatura.xml import xmltotxt +from .fetch import fetch_html +from typing import Tuple, NamedTuple +from dataclasses import dataclass, field +import json +from datetime import datetime +import logging + +def _extract_html(html: str, url : str | None = None) -> str | None: + text = trafilatura.extract( + html, + url=url, + output_format="markdown", + favor_recall=False, + deduplicate=True, + favor_precision=True, + include_comments=True, + include_links=True, + include_tables=False, + with_metadata=False + ) + return text.strip() if text else None + +def _extract_html_metadata(html: str, url : str | None = None) -> dict | None: + # this actually still includes raw text + so much extra stuff!! not just meta + # todo: look into this more! super cool! + meta = trafilatura.extract( + html, + url=url, + output_format="json", + favor_recall=False, + deduplicate=True, + favor_precision=True, + include_links=True, + include_tables=False, + with_metadata=True, + only_with_metadata=True # we probably dont want this... + + ) + if not meta: + return None + return json.loads(meta) + +def extract_text_from_url(url: str) -> str | None: + html = fetch_html(url) + if html is None: + return None + return _extract_html(html, url=url) + +def extract_text_and_metadata_from_url(url: str) -> Tuple[str | None, dict | None]: + html = fetch_html(url) + if html is None: + return None, None + metadata = _extract_html_metadata(html, url=url) + if metadata is None: + return _extract_html(html, url=url), None + text = None + if hasattr(metadata, 'raw_text'): + text = metadata['raw_text'] + metadata.pop('raw_text') + return text, metadata + +def extract_metadata_from_url(url: str) -> dict | None: + html = fetch_html(url) + if html is None: + return None + return _extract_html_metadata(html, url=url) + +@dataclass +class ExtractedContent(): + source_url: str + title : str | None + description : str | None + text: str | None + date : datetime | None + source_name : str | None = None + images: list[str] = field(default_factory=list) + comments : list[str] = field(default_factory=list) + doc : Document | None = None + +def _extract_content_from_html(html: str, url : str | None ) -> ExtractedContent | None: + DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + extracted = trafilatura.bare_extraction( + filecontent=html, + url=url, + favor_recall=False, + output_format="markdown", + deduplicate=True, + favor_precision=True, + include_comments=True, + include_tables=True, + include_links=True, #experimental + include_images=True, #experimental + with_metadata=True, + date_extraction_params={ + "original_date": True, + "max_date" : datetime.now().strftime(DATE_FORMAT), + "extensive_search": True, + "outputformat": DATE_FORMAT + } + ) + if not extracted or not isinstance(extracted, Document): + return None + doc : Document = extracted + + if doc.body is not None: + text = xmltotxt(doc.body, include_formatting=True) + else: + text = doc.text if doc.text else (doc.raw_text if doc.raw_text else None) + comments_text = doc.comments + if doc.commentsbody is not None: + comments_text = xmltotxt(doc.commentsbody, include_formatting=True) + timestamp = None + if doc.date: + try: + timestamp = datetime.strptime(doc.date, DATE_FORMAT) + except Exception: + timestamp = None + if not timestamp: + try: + timestamp = datetime.strptime(doc.date, "%Y-%m-%d") + except Exception: + timestamp = None + + + return ExtractedContent( + source_url = doc.url if doc.url else (url if url else ""), + title = doc.title, + description = doc.description, + text = text, + date= timestamp, + source_name=doc.sitename, + images=[doc.image ] if doc.image else [], + comments= [comments_text ] if comments_text else [], + doc = doc + ) + +def extract_content_from_url(url : str ) -> ExtractedContent | None: + html = fetch_html(url) + if html is None: + return None + return _extract_content_from_html(html, url=url) + + +if __name__ == "__main__": + url = "https://www.theverge.com/news/836212/openai-code-red-chatgpt" + content = extract_content_from_url(url) + + + if content: + print("Title:", content.title) + print("timestamp:", content.date) + print("Text snippet:", content.text[:256] if content.text else "No text") + print("Images:", content.images) + print("Comments:", content.comments) \ No newline at end of file diff --git a/build/lib/truffile/app_runtime/abrasive/fetch.py b/build/lib/truffile/app_runtime/abrasive/fetch.py new file mode 100644 index 0000000..a737d78 --- /dev/null +++ b/build/lib/truffile/app_runtime/abrasive/fetch.py @@ -0,0 +1,48 @@ +import requests +# wrappers around requests to fetch JSON or text from URLs +# can include any anti-detection headers, timeouts, error handling, etc. common here + +USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" + + +def _fetch(url: str, timeout: int = 10) -> requests.Response | None: + try: + resp = requests.get( + url, + timeout=timeout, + headers={ + "User-Agent": USER_AGENT, + "Connection": "close", + }, + allow_redirects=True, + ) + resp.raise_for_status() + return resp + except Exception as e: + print("Error fetching URL:", url) + print(e) + return None + +def fetch_json(url: str, timeout: int = 10) -> dict | None: + resp = _fetch(url, timeout=timeout) + if resp is None: + return None + try: + return resp.json() + except Exception as e: + print("Error parsing JSON from URL:", url) + print(e) + return None + +def fetch_html(url: str, timeout: int = 10) -> str | None: + resp = _fetch(url, timeout=timeout) + if resp is None: + return None + try: + return resp.text + except Exception as e: + print("Error reading text from URL:", url) + print(e) + return None +def fetch_text(url: str, timeout: int = 10) -> str | None: + return fetch_html(url, timeout=timeout) \ No newline at end of file diff --git a/build/lib/truffile/app_runtime/app_client.py b/build/lib/truffile/app_runtime/app_client.py new file mode 100644 index 0000000..d01d92b --- /dev/null +++ b/build/lib/truffile/app_runtime/app_client.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import grpc +from truffle.app.app_pb2 import AppError +from truffle.app.app_runtime_pb2 import AppRuntimeReportErrorRequest +from truffle.app.app_runtime_pb2_grpc import AppRuntimeServiceStub +from enum import IntEnum +from .core import RuntimeConnectionInfo, build_auth_metadata, load_runtime_connection_info + +class AppRuntimeErrorType(IntEnum): + APP_ERROR_INVALID = 0 + APP_ERROR_RUNTIME = 1 + APP_ERROR_AUTH = 2 + APP_ERROR_UNKNOWN = 3 + +def _app_runtime_error_type_to_pb(error_type: AppRuntimeErrorType | int) -> AppError.ErrorType: + if isinstance(error_type, AppRuntimeErrorType): + error_type = int(error_type) + if error_type == AppRuntimeErrorType.APP_ERROR_RUNTIME: + return AppError.APP_ERROR_RUNTIME + elif error_type == AppRuntimeErrorType.APP_ERROR_AUTH: + return AppError.APP_ERROR_AUTH + else: + return AppError.APP_ERROR_UNKNOWN + + +class AppRuntimeClient: + def __init__( + self, + channel: grpc.Channel, + *, + connection: RuntimeConnectionInfo | None = None, + ) -> None: + self._connection = connection or load_runtime_connection_info() + self._stub = AppRuntimeServiceStub(channel) + + def report_error( + self, + *, + error_type: AppRuntimeErrorType | int, + error_message: str, + needs_intervention: bool = False, + app_uuid: str | None = None, + ): + req = AppRuntimeReportErrorRequest() + req.app_uuid = app_uuid or self._connection.app_id + req.error.error_type = _app_runtime_error_type_to_pb(error_type) + req.error.error_message = error_message + req.needs_intervention = needs_intervention + return self._stub.ReportError(req, metadata=build_auth_metadata(self._connection)) + + def report_runtime_error( + self, + error_message: str, + *, + needs_intervention: bool = False, + app_uuid: str | None = None, + ): + return self.report_error( + error_type=AppError.APP_ERROR_RUNTIME, + error_message=error_message, + needs_intervention=needs_intervention, + app_uuid=app_uuid, + ) + +async def report_app_error( + error_message: str, + *, + error_type: AppRuntimeErrorType | int = AppRuntimeErrorType.APP_ERROR_RUNTIME, + needs_intervention: bool = False, + is_fatal: bool = False +) -> bool: + try: + from app_runtime.core import init_channel + with init_channel() as channel: + client = AppRuntimeClient(channel) + client.report_error( + error_type=error_type, + error_message=error_message, + needs_intervention=needs_intervention, + ) + return True + except Exception: + print(f"Failed to report app error: {error_message}") + print("Exception details:") + import traceback + traceback.print_exc() + # Don't raise exceptions from error reporting to avoid potential infinite loops. + pass + return False diff --git a/build/lib/truffile/app_runtime/auth_modes.py b/build/lib/truffile/app_runtime/auth_modes.py new file mode 100644 index 0000000..11715c1 --- /dev/null +++ b/build/lib/truffile/app_runtime/auth_modes.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from dataclasses import dataclass +import os + + +@dataclass(frozen=True, slots=True) +class PublicAuth: + mode: str = "public" + + +@dataclass(frozen=True, slots=True) +class TextConfigAuth: + fields: tuple[str, ...] + mode: str = "text" + + def __init__(self, fields: list[str] | tuple[str, ...]) -> None: + object.__setattr__(self, "fields", tuple(fields)) + object.__setattr__(self, "mode", "text") + + +@dataclass(frozen=True, slots=True) +class OAuthAuth: + provider: str + token_file: str + mode: str = "oauth" + + +@dataclass(frozen=True, slots=True) +class VncAuth: + session_dir: str + mode: str = "vnc" + + +def load_required_env(name: str) -> str: + value = str(os.getenv(name, "")).strip() + if not value: + raise RuntimeError(f"Missing required environment variable: {name}") + return value diff --git a/build/lib/truffile/app_runtime/background/__init__.py b/build/lib/truffile/app_runtime/background/__init__.py new file mode 100644 index 0000000..4932e60 --- /dev/null +++ b/build/lib/truffile/app_runtime/background/__init__.py @@ -0,0 +1,10 @@ +from .client import BackgroundAppClient +from .runtime import BackgroundRunContext, run_background +from truffle.app.background_pb2 import BackgroundContext + + +__all__ = [ + "BackgroundAppClient", + "BackgroundRunContext", + "run_background", +] diff --git a/build/lib/truffile/app_runtime/background/client.py b/build/lib/truffile/app_runtime/background/client.py new file mode 100644 index 0000000..92e5282 --- /dev/null +++ b/build/lib/truffile/app_runtime/background/client.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import Iterable + +import grpc +from truffle.app import background_pb2 +from truffle.app.background_pb2_grpc import BackgroundAppServiceStub + +from ..core import RuntimeConnectionInfo, build_auth_metadata, load_runtime_connection_info + +logger = logging.getLogger(__name__) + +BG_SUBMIT_CONTEXT_MAX_CHARS = 80_000 + + +class BackgroundAppClient: + def __init__( + self, + channel: grpc.Channel, + *, + connection: RuntimeConnectionInfo | None = None, + ) -> None: + self._connection = connection or load_runtime_connection_info() + self._stub = BackgroundAppServiceStub(channel) + + def on_run(self) -> background_pb2.BackgroundAppOnRunResponse: + req = background_pb2.BackgroundAppOnRunRequest() + return self._stub.OnRun(req, metadata=self.grpc_metadata()) + + def yield_run(self) -> datetime: + req = background_pb2.BackgroundAppYieldRequest() + resp = self._stub.Yield(req, metadata=self.grpc_metadata()) + return resp.next_scheduled_run_time.ToDatetime(tzinfo=timezone.utc) + + def submit_context( + self, + *, + content: str, + uris: Iterable[str] = (), + priority: int = background_pb2.BackgroundContext.PRIORITY_UNSPECIFIED, + max_chars: int = BG_SUBMIT_CONTEXT_MAX_CHARS, + ) -> background_pb2.BackgroundAppSubmitContextResponse: + if len(content) > max_chars: + original_len = len(content) + logger.warning( + "submit_context content is %s chars (limit: %s). Truncating.", + f"{original_len:,}", + f"{max_chars:,}", + ) + content = ( + content[:max_chars] + + f"\n\n[Content truncated: original was {original_len:,} chars, " + f"showing first {max_chars:,}.]" + ) + req = background_pb2.BackgroundAppSubmitContextRequest() + req.content.content = content + req.content.priority = priority + req.content.uris.extend(list(uris)) + return self._stub.Submit(req, metadata=self.grpc_metadata()) + + def grpc_metadata(self) -> list[tuple[str, str]]: + return build_auth_metadata(self._connection) diff --git a/build/lib/truffile/app_runtime/background/runtime.py b/build/lib/truffile/app_runtime/background/runtime.py new file mode 100644 index 0000000..08643b6 --- /dev/null +++ b/build/lib/truffile/app_runtime/background/runtime.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +import logging +import time +from typing import Callable + +from truffle.app.background_pb2 import BackgroundAppOnRunResponse + +from ..app_client import AppRuntimeClient +from ..core import close_channel, init_channel +from .client import BackgroundAppClient + +logger = logging.getLogger(__name__) + +BackgroundRunFunc = Callable[["BackgroundRunContext"], None] + + +@dataclass(slots=True) +class BackgroundRunContext: + run_num: int + run_info: BackgroundAppOnRunResponse | None + bg: BackgroundAppClient + app_runtime: AppRuntimeClient + + +def run_background( + func: BackgroundRunFunc, + *, + connection_timeout_seconds: float = 15.0, + wait_interval_seconds: float = 0.5, +) -> None: + run_num = 0 + while True: + logger.debug("starting background app run %s", run_num) + channel = init_channel(timeout_seconds=connection_timeout_seconds) + bg_client = BackgroundAppClient(channel) + app_runtime_client = AppRuntimeClient(channel) + ctx = BackgroundRunContext( + run_num=run_num, + run_info=None, + bg=bg_client, + app_runtime=app_runtime_client, + ) + try: + ctx.run_info = bg_client.on_run() + func(ctx) + except Exception as e: + logger.exception("background app user function failed in run %s", run_num) + try: + app_runtime_client.report_runtime_error(str(e), needs_intervention=False) + except Exception: + logger.exception("failed to report runtime error") + finally: + time.sleep(0.250) + wait_until = bg_client.yield_run() + close_channel(channel) + + while datetime.now(timezone.utc) < wait_until: + time.sleep(wait_interval_seconds) + run_num += 1 diff --git a/build/lib/truffile/app_runtime/browser/__init__.py b/build/lib/truffile/app_runtime/browser/__init__.py new file mode 100644 index 0000000..908a920 --- /dev/null +++ b/build/lib/truffile/app_runtime/browser/__init__.py @@ -0,0 +1,3 @@ +from .cdp import ChromiumCDPBrowser + +__all__ = ["ChromiumCDPBrowser"] diff --git a/build/lib/truffile/app_runtime/browser/cdp.py b/build/lib/truffile/app_runtime/browser/cdp.py new file mode 100644 index 0000000..e0fb286 --- /dev/null +++ b/build/lib/truffile/app_runtime/browser/cdp.py @@ -0,0 +1,258 @@ +from __future__ import annotations + +import asyncio +import contextlib +import socket +import time +from pathlib import Path +from typing import Any, Callable + +import httpx + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return int(sock.getsockname()[1]) + + +class ChromiumCDPBrowser: + def __init__( + self, + *, + browser_binary: str, + user_data_dir: str | Path, + start_url: str, + window_size: tuple[int, int] = (1920, 1080), + ) -> None: + self.browser_binary = browser_binary + self.user_data_dir = Path(user_data_dir) + self.start_url = start_url + self.window_size = window_size + + self.debug_port = _find_free_port() + self._proc: asyncio.subprocess.Process | None = None + self._cdp_client: Any = None + self._session_id: str | None = None + self._target_id: str | None = None + self._page_crashed = False + self._request_callback_registered = False + + @property + def process(self) -> asyncio.subprocess.Process | None: + return self._proc + + @property + def page_crashed(self) -> bool: + return self._page_crashed + + async def start(self) -> None: + self.user_data_dir.mkdir(parents=True, exist_ok=True) + self._clean_profile_locks() + + launch_args = self._build_launch_args() + launch_args.append(f"--remote-debugging-port={self.debug_port}") + launch_args.append("--remote-debugging-address=127.0.0.1") + + self._proc = await asyncio.create_subprocess_exec( + *launch_args, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + + debugger_url = await self._wait_for_debugger_url() + + from cdp_use import CDPClient + + self._cdp_client = CDPClient(debugger_url, max_ws_frame_size=50 * 1024 * 1024) + await self._cdp_client.start() + + def _on_target_crashed(event: dict, session_id: str | None = None) -> None: + self._page_crashed = True + + self._cdp_client.register.Target.targetCrashed(_on_target_crashed) + await self._attach_page_target() + + async def close(self) -> None: + if self._cdp_client is not None: + with contextlib.suppress(Exception): + await self._cdp_client.stop() + self._cdp_client = None + if self._proc is not None: + if self._proc.returncode is None: + self._proc.terminate() + try: + await asyncio.wait_for(self._proc.wait(), timeout=3.0) + except TimeoutError: + with contextlib.suppress(ProcessLookupError): + self._proc.kill() + with contextlib.suppress(Exception): + await self._proc.wait() + self._proc = None + + def is_alive(self) -> bool: + if self._page_crashed: + return False + return self._proc is not None and self._proc.returncode is None + + async def navigate(self, url: str) -> None: + assert self._cdp_client is not None and self._session_id is not None + await self._cdp_client.send.Page.navigate(params={"url": url}, session_id=self._session_id) + + async def evaluate(self, expression: str) -> Any: + assert self._cdp_client is not None and self._session_id is not None + result = await self._cdp_client.send.Runtime.evaluate( + params={"expression": expression, "returnByValue": True, "awaitPromise": True}, + session_id=self._session_id, + ) + if "exceptionDetails" in result: + raise RuntimeError(f"Runtime.evaluate failed: {result['exceptionDetails']}") + return result.get("result", {}).get("value") + + async def current_url(self) -> str: + return str(await self.evaluate("window.location.href") or "") + + async def title(self) -> str: + return str(await self.evaluate("document.title") or "") + + async def ready_state(self) -> str: + return str(await self.evaluate("document.readyState") or "") + + async def get_cookies(self) -> list[dict[str, Any]]: + assert self._cdp_client is not None and self._session_id is not None + result = await self._cdp_client.send.Storage.getCookies(session_id=self._session_id) + return list(result.get("cookies", []) or []) + + async def get_user_agent(self) -> str: + return str(await self.evaluate("navigator.userAgent") or "") + + async def get_language(self) -> str: + return str(await self.evaluate("navigator.language || 'en-US'") or "en-US") + + async def capture_matching_requests( + self, + *, + duration: float, + should_capture: Callable[[str], bool], + on_capture: Callable[..., None], + source: str = "cdp", + include_post_data: bool = False, + ) -> int: + if duration <= 0: + return 0 + assert self._cdp_client is not None + assert self._session_id is not None + + captured = 0 + + def _on_request(params: Any, session_id: str | None = None) -> None: + nonlocal captured + if session_id and session_id != self._session_id: + return + request = params.get("request", {}) if hasattr(params, "get") else {} + url = str(request.get("url") or "") + if not should_capture(url): + return + method = str(request.get("method") or "GET").upper() + if method == "OPTIONS": + return + headers_raw = request.get("headers") or {} + headers = {str(k): str(v) for k, v in headers_raw.items()} if isinstance(headers_raw, dict) else {} + post_data = str(request.get("postData") or "") + if include_post_data: + on_capture(method, url, headers, post_data, source) + else: + on_capture(method, url, headers, source) + captured += 1 + + if not self._request_callback_registered: + self._cdp_client.register.Network.requestWillBeSent(_on_request) + self._request_callback_registered = True + + await asyncio.sleep(duration) + return captured + + def _clean_profile_locks(self) -> None: + for pattern in ("Singleton*", "lockfile"): + for path in self.user_data_dir.glob(pattern): + with contextlib.suppress(Exception): + path.unlink() + + def _build_launch_args(self) -> list[str]: + width, height = self.window_size + return [ + self.browser_binary, + "--no-first-run", + "--no-default-browser-check", + "--disable-infobars", + "--disable-blink-features=AutomationControlled", + "--disable-dev-shm-usage", + "--no-sandbox", + "--test-type", + "--disable-gpu", + "--disable-software-rasterizer", + f"--window-size={width},{height}", + "--start-fullscreen", + f"--app={self.start_url}", + f"--user-data-dir={self.user_data_dir}", + ] + + async def _wait_for_debugger_url(self, timeout_seconds: float = 10.0) -> str: + deadline = time.monotonic() + timeout_seconds + version_url = f"http://127.0.0.1:{self.debug_port}/json/version" + + async with httpx.AsyncClient(timeout=1.0) as client: + while time.monotonic() < deadline: + if self._proc is not None and self._proc.returncode is not None: + raise RuntimeError(f"Chromium exited early with code {self._proc.returncode}") + try: + response = await client.get(version_url) + response.raise_for_status() + payload = response.json() + debugger_url = str(payload.get("webSocketDebuggerUrl") or "").strip() + if debugger_url: + return debugger_url + except Exception: + pass + await asyncio.sleep(0.2) + + raise RuntimeError("Timed out waiting for Chromium CDP endpoint") + + async def _attach_page_target(self) -> None: + assert self._cdp_client is not None + target_id = await self._choose_target_id() + attach_result = await self._cdp_client.send.Target.attachToTarget( + params={"targetId": target_id, "flatten": True} + ) + self._target_id = target_id + self._session_id = attach_result["sessionId"] + await asyncio.gather( + self._cdp_client.send.Page.enable(session_id=self._session_id), + self._cdp_client.send.Runtime.enable(session_id=self._session_id), + self._cdp_client.send.Network.enable(session_id=self._session_id), + ) + + async def _choose_target_id(self) -> str: + assert self._cdp_client is not None + deadline = time.monotonic() + 5.0 + while time.monotonic() < deadline: + targets_result = await self._cdp_client.send.Target.getTargets() + target_infos = list(targets_result.get("targetInfos", []) or []) + page_targets = [ + t for t in target_infos + if t.get("type") in {"page", "tab"} + and not str(t.get("url") or "").startswith("devtools://") + and not str(t.get("url") or "").startswith("chrome-extension://") + ] + if page_targets: + chosen = next((t for t in page_targets if self._target_matches_start_url(t)), page_targets[0]) + return str(chosen["targetId"]) + await asyncio.sleep(0.1) + + created = await self._cdp_client.send.Target.createTarget(params={"url": self.start_url}) + return str(created["targetId"]) + + def _target_matches_start_url(self, target: dict[str, Any]) -> bool: + url = str(target.get("url") or "").lower() + return self.start_url.lower().split("/", 3)[2] in url if "://" in self.start_url else self.start_url.lower() in url diff --git a/build/lib/truffile/app_runtime/core.py b/build/lib/truffile/app_runtime/core.py new file mode 100644 index 0000000..c835daf --- /dev/null +++ b/build/lib/truffile/app_runtime/core.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from dataclasses import dataclass +import os + +import grpc + +DEFAULT_GRPC_ADDRESS = "10.22.0.1:80" +_DEFAULT_CONNECT_TIMEOUT_SECONDS = 15.0 + + +@dataclass(frozen=True) +class RuntimeConnectionInfo: + app_id: str + session_token: str + grpc_address: str + + +def load_runtime_connection_info() -> RuntimeConnectionInfo: + app_id = _required_env("APP_ID") + session_token = _required_env("APP_SESSION_TOKEN") + grpc_address = _normalize_grpc_address(os.getenv("GRPC_ADDRESS", DEFAULT_GRPC_ADDRESS)) + return RuntimeConnectionInfo( + app_id=app_id, + session_token=session_token, + grpc_address=grpc_address, + ) + + +def build_auth_metadata(info: RuntimeConnectionInfo) -> list[tuple[str, str]]: + return [("session", info.session_token), ("app-id", info.app_id)] + + +def init_channel( + connection: RuntimeConnectionInfo | None = None, + *, + timeout_seconds: float = _DEFAULT_CONNECT_TIMEOUT_SECONDS, +) -> grpc.Channel: + info = connection or load_runtime_connection_info() + channel = grpc.insecure_channel(info.grpc_address) + grpc.channel_ready_future(channel).result(timeout=timeout_seconds) + return channel + + +def close_channel(channel: grpc.Channel) -> None: + channel.close() + + +def _required_env(name: str) -> str: + value = os.getenv(name, "").strip() + if value == "": + raise RuntimeError(f"missing required environment variable: {name}") + return value + + +def _normalize_grpc_address(address: str) -> str: + raw = address.strip() + if raw == "": + return DEFAULT_GRPC_ADDRESS + # Keep URI forms unchanged. + if "://" in raw: + return raw + # Preserve bracketed IPv6 form if port already present. + if raw.startswith("[") and "]:" in raw: + return raw + if ":" in raw: + return raw + return f"{raw}:80" diff --git a/build/lib/truffile/app_runtime/errors.py b/build/lib/truffile/app_runtime/errors.py new file mode 100644 index 0000000..4a3a92c --- /dev/null +++ b/build/lib/truffile/app_runtime/errors.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Any + + +class AppRuntimeFailure(RuntimeError): + pass + + +class AppAuthError(AppRuntimeFailure): + pass + + +@dataclass(frozen=True, slots=True) +class ErrorEnvelope: + error_type: int + error_message: str + needs_intervention: bool + + def as_dict(self) -> dict[str, Any]: + return { + "error_type": self.error_type, + "error_message": self.error_message, + "needs_intervention": self.needs_intervention, + } + + +def _classify_error(exc: BaseException) -> tuple[int, bool, str]: + if isinstance(exc, AppAuthError): + return 2, True, "auth" + return 1, False, "runtime" + + +class ErrorReporter: + def __init__(self, *, logger: logging.Logger, app_name: str) -> None: + self._logger = logger + self._app_name = app_name + + async def report_foreground_exception(self, exc: BaseException, *, tool_name: str) -> None: + error_type, needs_intervention, category = _classify_error(exc) + message = f"{self._app_name} foreground tool '{tool_name}' failed ({category}): {exc}" + self._logger.exception( + "Foreground tool failed", + extra={ + "app_name": self._app_name, + "tool_name": tool_name, + "error_category": category, + }, + ) + if category != "auth": + return + try: + from app_runtime import report_app_error + + await report_app_error( + message, + error_type=error_type, + needs_intervention=needs_intervention, + ) + except Exception: + self._logger.exception("Failed reporting foreground tool error") + + def report_background_exception(self, ctx: Any, exc: BaseException, *, phase: str) -> ErrorEnvelope: + error_type, needs_intervention, category = _classify_error(exc) + message = f"{self._app_name} {phase} failed ({category}): {exc}" + self._logger.exception( + "Background phase failed", + extra={ + "app_name": self._app_name, + "phase": phase, + "error_category": category, + }, + ) + + envelope = ErrorEnvelope( + error_type=error_type, + error_message=message, + needs_intervention=needs_intervention, + ) + + reporter = getattr(ctx, "app_runtime", None) + if reporter is not None: + try: + reporter.report_error( + error_type=error_type, + error_message=message, + needs_intervention=needs_intervention, + ) + except Exception: + self._logger.exception("Failed reporting background runtime error") + return envelope diff --git a/build/lib/truffile/app_runtime/foreground.py b/build/lib/truffile/app_runtime/foreground.py new file mode 100644 index 0000000..6664035 --- /dev/null +++ b/build/lib/truffile/app_runtime/foreground.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +from dataclasses import dataclass +import functools +import inspect +import logging +from types import NoneType +from typing import Any, Callable, get_args, get_origin, get_type_hints + +from .errors import ErrorReporter + + +@dataclass(frozen=True, slots=True) +class ToolSpec: + name: str + description: str + icon: str + readonly: bool = False + + +@dataclass(frozen=True, slots=True) +class _ToolRegistration: + spec: ToolSpec + handler: Callable[..., Any] + + +@dataclass(frozen=True, slots=True) +class _PromptRegistration: + name: str + description: str + handler: Callable[..., Any] + + +class ForegroundApp: + def __init__(self, name: str, *, logger_name: str | None = None) -> None: + from app_runtime.mcp import create_mcp_server + + self.name = name + self.logger = logging.getLogger(logger_name or f"{name}.foreground") + self.logger.setLevel(logging.INFO) + self._mcp = create_mcp_server(name) + self._error_reporter = ErrorReporter(logger=self.logger, app_name=name) + self._tools: dict[str, _ToolRegistration] = {} + self._prompts: dict[str, _PromptRegistration] = {} + + def tool( + self, + spec_or_name: ToolSpec | str, + *, + description: str | None = None, + icon: str | None = None, + readonly: bool = False, + ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + spec = self._normalize_tool_spec(spec_or_name, description=description, icon=icon, readonly=readonly) + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + wrapped = self._wrap_tool(spec.name, func) + kwargs: dict[str, Any] = {"description": spec.description} + if spec.icon: + from mcp.types import Icon + kwargs["icons"] = [Icon(src=spec.icon)] + if spec.readonly: + from mcp.types import ToolAnnotations + kwargs["annotations"] = ToolAnnotations(readOnlyHint=True) + registered = self._mcp.tool(spec.name, **kwargs)(wrapped) + self._tools[spec.name] = _ToolRegistration(spec=spec, handler=wrapped) + return registered + + return decorator + + def prompt( + self, + name: str, + *, + description: str | None = None, + ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + if not description: + raise ValueError("prompt description is required") + + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + wrapped = self._wrap_passthrough(func) + registered = self._mcp.prompt(name, description=description)(wrapped) + self._prompts[name] = _PromptRegistration(name=name, description=description, handler=wrapped) + return registered + + return decorator + + def run(self) -> None: + from app_runtime.mcp import run_mcp_server + + run_mcp_server(self._mcp, self.logger) + + async def invoke_tool(self, name: str, **arguments: Any) -> Any: + registration = self._tools.get(name) + if registration is None: + raise KeyError(f"Unknown tool: {name}") + return await registration.handler(**arguments) + + async def invoke_prompt(self, name: str, **arguments: Any) -> Any: + registration = self._prompts.get(name) + if registration is None: + raise KeyError(f"Unknown prompt: {name}") + result = registration.handler(**arguments) + if inspect.isawaitable(result): + return await result + return result + + def list_tools(self) -> list[dict[str, str]]: + return [ + { + "name": registration.spec.name, + "description": registration.spec.description, + "icon": registration.spec.icon, + } + for registration in sorted(self._tools.values(), key=lambda item: item.spec.name) + ] + + def openai_tools(self) -> list[dict[str, Any]]: + tools: list[dict[str, Any]] = [] + for registration in sorted(self._tools.values(), key=lambda item: item.spec.name): + tools.append( + { + "type": "function", + "function": { + "name": registration.spec.name, + "description": registration.spec.description, + "parameters": self._signature_to_json_schema(registration.handler), + }, + } + ) + return tools + + def list_prompts(self) -> list[dict[str, str]]: + return [ + {"name": reg.name, "description": reg.description} + for reg in sorted(self._prompts.values(), key=lambda item: item.name) + ] + + def logger_names(self) -> list[str]: + return [self.logger.name] + + def _normalize_tool_spec( + self, + spec_or_name: ToolSpec | str, + *, + description: str | None, + icon: str | None, + readonly: bool, + ) -> ToolSpec: + if isinstance(spec_or_name, ToolSpec): + resolved_icon = self._resolve_icon(spec_or_name.icon) + if resolved_icon != spec_or_name.icon: + return ToolSpec(name=spec_or_name.name, description=spec_or_name.description, icon=resolved_icon, readonly=spec_or_name.readonly) + return spec_or_name + if not description: + raise ValueError("tool description is required") + if not icon: + raise ValueError("tool icon is required") + return ToolSpec(name=spec_or_name, description=description, icon=self._resolve_icon(icon), readonly=readonly) + + @staticmethod + def _resolve_icon(icon: str) -> str: + if not icon or icon.startswith("http://") or icon.startswith("https://"): + return icon + from .icons import phosphor_icon_url + return phosphor_icon_url(icon) + + def _wrap_tool(self, tool_name: str, func: Callable[..., Any]) -> Callable[..., Any]: + signature = inspect.signature(func) + + @functools.wraps(func) + async def wrapped(*args: Any, **kwargs: Any) -> Any: + try: + result = func(*args, **kwargs) + if inspect.isawaitable(result): + return await result + return result + except Exception as exc: + await self._error_reporter.report_foreground_exception(exc, tool_name=tool_name) + raise + + wrapped.__signature__ = signature # type: ignore[attr-defined] + return wrapped + + def _wrap_passthrough(self, func: Callable[..., Any]) -> Callable[..., Any]: + signature = inspect.signature(func) + + @functools.wraps(func) + async def wrapped(*args: Any, **kwargs: Any) -> Any: + result = func(*args, **kwargs) + if inspect.isawaitable(result): + return await result + return result + + wrapped.__signature__ = signature # type: ignore[attr-defined] + return wrapped + + def _signature_to_json_schema(self, func: Callable[..., Any]) -> dict[str, Any]: + signature = inspect.signature(func) + type_hints = get_type_hints(func) + properties: dict[str, Any] = {} + required: list[str] = [] + + for name, param in signature.parameters.items(): + if param.kind not in ( + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ): + continue + + annotation = type_hints.get(name, param.annotation if param.annotation is not inspect._empty else Any) + schema = self._annotation_to_json_schema(annotation) + properties[name] = schema + if param.default is inspect._empty: + required.append(name) + + out: dict[str, Any] = { + "type": "object", + "properties": properties, + "additionalProperties": False, + } + if required: + out["required"] = required + return out + + def _annotation_to_json_schema(self, annotation: Any) -> dict[str, Any]: + if annotation is Any: + return {} + + origin = get_origin(annotation) + args = get_args(annotation) + + if origin is None: + return self._plain_type_schema(annotation) + + if origin in (list, tuple): + item_annotation = args[0] if args else Any + return { + "type": "array", + "items": self._annotation_to_json_schema(item_annotation), + } + + if origin is dict: + return {"type": "object"} + + non_none_args = [arg for arg in args if arg is not NoneType] + if len(non_none_args) == 1 and len(non_none_args) != len(args): + schema = self._annotation_to_json_schema(non_none_args[0]) + if not schema: + return schema + if "type" in schema and isinstance(schema["type"], str): + schema = dict(schema) + schema["type"] = [schema["type"], "null"] + return schema + + return {} + + def _plain_type_schema(self, annotation: Any) -> dict[str, Any]: + if annotation is str: + return {"type": "string"} + if annotation is int: + return {"type": "integer"} + if annotation is float: + return {"type": "number"} + if annotation is bool: + return {"type": "boolean"} + if annotation in (list, tuple): + return {"type": "array"} + if annotation is dict: + return {"type": "object"} + return {} diff --git a/build/lib/truffile/app_runtime/grpc_harness.py b/build/lib/truffile/app_runtime/grpc_harness.py new file mode 100644 index 0000000..126586b --- /dev/null +++ b/build/lib/truffile/app_runtime/grpc_harness.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +import os +from concurrent import futures +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any + +import grpc + + +_PROTO_IMPORT_ERROR = ( + "Protobuf stubs not found. Run the pyfw proto generation step before using grpc_harness." +) + + +def _import_protos() -> tuple[Any, ...]: + try: + from truffle.app import app_runtime_pb2, app_runtime_pb2_grpc + from truffle.app import background_pb2, background_pb2_grpc + except ImportError as exc: + raise ImportError(_PROTO_IMPORT_ERROR) from exc + return background_pb2, background_pb2_grpc, app_runtime_pb2, app_runtime_pb2_grpc + + + +@dataclass(slots=True) +class RecordedSubmission: + content: str = "" + uris: list[str] = field(default_factory=list) + priority: int = 0 + + +@dataclass(slots=True) +class RecordedRuntimeError: + app_uuid: str = "" + error_type: int = 0 + error_message: str = "" + needs_intervention: bool = False + + +class FakeBackgroundAppServicer: + def __init__(self) -> None: + self.on_run_calls = 0 + self.yield_calls = 0 + self.submissions: list[RecordedSubmission] = [] + self._pb2: Any = None + + def _ensure_pb2(self) -> Any: + if self._pb2 is None: + self._pb2 = _import_protos()[0] + return self._pb2 + + def OnRun(self, request: Any, context: Any) -> Any: + pb2 = self._ensure_pb2() + self.on_run_calls += 1 + return pb2.BackgroundAppOnRunResponse() + + def Yield(self, request: Any, context: Any) -> Any: + pb2 = self._ensure_pb2() + self.yield_calls += 1 + resp = pb2.BackgroundAppYieldResponse() + resp.next_scheduled_run_time.FromDatetime(datetime.now(timezone.utc)) + return resp + + def Submit(self, request: Any, context: Any) -> Any: + pb2 = self._ensure_pb2() + self.submissions.append( + RecordedSubmission( + content=request.content.content, + uris=list(request.content.uris), + priority=int(request.content.priority), + ) + ) + return pb2.BackgroundAppSubmitContextResponse() + + +class FakeAppRuntimeServicer: + def __init__(self) -> None: + self.errors: list[RecordedRuntimeError] = [] + self.app_vars: dict[str, str] = {} + self._pb2: Any = None + + def _ensure_pb2(self) -> Any: + if self._pb2 is None: + self._pb2 = _import_protos()[2] + return self._pb2 + + def ReportError(self, request: Any, context: Any) -> Any: + pb2 = self._ensure_pb2() + self.errors.append( + RecordedRuntimeError( + app_uuid=request.app_uuid, + error_type=int(request.error.error_type), + error_message=request.error.error_message, + needs_intervention=bool(request.needs_intervention), + ) + ) + return pb2.AppRuntimeReportErrorResponse() + + def GetAppVar(self, request: Any, context: Any) -> Any: + pb2 = self._ensure_pb2() + value = self.app_vars.get(request.key, "") + if not value: + context.set_code(grpc.StatusCode.NOT_FOUND) + return pb2.AppRuntimeGetAppVarResponse(value=value) + + def SetAppVar(self, request: Any, context: Any) -> Any: + pb2 = self._ensure_pb2() + self.app_vars[request.key] = request.value + return pb2.AppRuntimeSetAppVarResponse() + + def DeleteAppVar(self, request: Any, context: Any) -> Any: + pb2 = self._ensure_pb2() + self.app_vars.pop(request.key, None) + return pb2.AppRuntimeDeleteAppVarResponse() + + def ListAppVars(self, request: Any, context: Any) -> Any: + pb2 = self._ensure_pb2() + resp = pb2.AppRuntimeListAppVarsResponse() + for k, v in self.app_vars.items(): + resp.vars[k] = v + return resp + + +class InProcessGrpcServer: + def __init__( + self, + *, + app_id: str = "test-app-id", + session_token: str = "test-session-token", + max_workers: int = 4, + ) -> None: + self.app_id = app_id + self.session_token = session_token + self._max_workers = max_workers + self._server: grpc.Server | None = None + self._port = 0 + self._saved_env: dict[str, str | None] = {} + self.bg_servicer = FakeBackgroundAppServicer() + self.runtime_servicer = FakeAppRuntimeServicer() + + @property + def address(self) -> str: + return f"127.0.0.1:{self._port}" + + def __enter__(self) -> InProcessGrpcServer: + background_pb2, background_pb2_grpc, app_runtime_pb2, app_runtime_pb2_grpc = _import_protos() + del background_pb2, app_runtime_pb2 + self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=self._max_workers)) + background_pb2_grpc.add_BackgroundAppServiceServicer_to_server(self.bg_servicer, self._server) + app_runtime_pb2_grpc.add_AppRuntimeServiceServicer_to_server(self.runtime_servicer, self._server) + self._port = self._server.add_insecure_port("127.0.0.1:0") + self._server.start() + + env_overrides = { + "APP_ID": self.app_id, + "APP_SESSION_TOKEN": self.session_token, + "GRPC_ADDRESS": self.address, + } + for key, value in env_overrides.items(): + self._saved_env[key] = os.environ.get(key) + os.environ[key] = value + return self + + def __exit__(self, *exc: Any) -> None: + if self._server is not None: + self._server.stop(grace=0) + self._server = None + for key, original in self._saved_env.items(): + if original is None: + os.environ.pop(key, None) + else: + os.environ[key] = original + self._saved_env.clear() + + def run_background_once(self, func: Any, *, connection_timeout_seconds: float = 5.0) -> None: + from app_runtime.app_client import AppRuntimeClient + from app_runtime.background.client import BackgroundAppClient + from app_runtime.background.runtime import BackgroundRunContext + from app_runtime.core import close_channel, init_channel + + channel = init_channel(timeout_seconds=connection_timeout_seconds) + try: + bg_client = BackgroundAppClient(channel) + runtime_client = AppRuntimeClient(channel) + run_info = bg_client.on_run() + ctx = BackgroundRunContext( + run_num=0, + run_info=run_info, + bg=bg_client, + app_runtime=runtime_client, + ) + func(ctx) + bg_client.yield_run() + finally: + close_channel(channel) diff --git a/build/lib/truffile/app_runtime/icons.py b/build/lib/truffile/app_runtime/icons.py new file mode 100644 index 0000000..d5a2a9d --- /dev/null +++ b/build/lib/truffile/app_runtime/icons.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +PHOSPHOR_ICON_BASE_URL = "https://raw.githubusercontent.com/phosphor-icons/core/main/assets/regular" + + +def phosphor_icon_url(name: str) -> str: + slug = str(name or "").strip() + if not slug: + raise ValueError("icon name must be non-empty") + return f"{PHOSPHOR_ICON_BASE_URL}/{slug}.svg" diff --git a/build/lib/truffile/app_runtime/jsonrpc.py b/build/lib/truffile/app_runtime/jsonrpc.py new file mode 100644 index 0000000..6e212ac --- /dev/null +++ b/build/lib/truffile/app_runtime/jsonrpc.py @@ -0,0 +1,67 @@ +# shared JSON-RPC response parsing and httpx response adapter. +# used by apps that talk to remote MCP servers (arxiv, exa). + +from __future__ import annotations + +import json +from typing import Any + +import httpx + + +def parse_jsonrpc_payload(raw: str) -> dict[str, Any] | None: + """Parse a JSON-RPC response that may be plain JSON or SSE data: lines.""" + text = (raw or "").strip() + if not text: + return None + + try: + parsed = json.loads(text) + if isinstance(parsed, dict): + return parsed + except Exception: + pass + + candidate: dict[str, Any] | None = None + for line in text.splitlines(): + if not line.startswith("data:"): + continue + payload = line[len("data:"):].strip() + if not payload: + continue + try: + parsed = json.loads(payload) + except Exception: + continue + if isinstance(parsed, dict): + candidate = parsed + return candidate + + +class HttpxResponseAdapter: + """Wraps httpx.Response to match the HttpResponse protocol.""" + + def __init__(self, response: httpx.Response) -> None: + self._response = response + + @property + def status_code(self) -> int: + return int(self._response.status_code) + + @property + def is_success(self) -> bool: + return bool(self._response.is_success) + + @property + def text(self) -> str: + return self._response.text + + @property + def url(self) -> str: + return str(self._response.url) + + def json(self) -> Any: + return self._response.json() + + def raise_for_status(self) -> None: + self._response.raise_for_status() diff --git a/build/lib/truffile/app_runtime/mcp/__init__.py b/build/lib/truffile/app_runtime/mcp/__init__.py new file mode 100644 index 0000000..ea1a00d --- /dev/null +++ b/build/lib/truffile/app_runtime/mcp/__init__.py @@ -0,0 +1,15 @@ +import logging +from .config import TRUFFLE_MCP_TRANSPORT, TRUFFLE_MCP_HOST, TRUFFLE_MCP_PORT +try: + from mcp.server.fastmcp import FastMCP + from .helpers_fast import create_mcp_server, run_mcp_server + +except ImportError as e: + try: # feb 2026/v1.26.0: looks like next version of MCP will have FastMCP removed, so fall back to MCPServer if FastMCP is not found + from mcp.server.mcpserver import MCPServer # type: ignore + from .helpers_mcp import create_mcp_server, run_mcp_server + except ImportError as e2: + raise ImportError("Neither FastMCP nor MCPServer could be imported. Please ensure the MCP library is installed and up to date.") from e2 + + + diff --git a/build/lib/truffile/app_runtime/mcp/config.py b/build/lib/truffile/app_runtime/mcp/config.py new file mode 100644 index 0000000..fb368e3 --- /dev/null +++ b/build/lib/truffile/app_runtime/mcp/config.py @@ -0,0 +1,4 @@ + +TRUFFLE_MCP_HOST = "0.0.0.0" +TRUFFLE_MCP_PORT = 8000 +TRUFFLE_MCP_TRANSPORT = "streamable-http" diff --git a/build/lib/truffile/app_runtime/mcp/helpers_fast.py b/build/lib/truffile/app_runtime/mcp/helpers_fast.py new file mode 100644 index 0000000..05e0beb --- /dev/null +++ b/build/lib/truffile/app_runtime/mcp/helpers_fast.py @@ -0,0 +1,16 @@ +from mcp.server.fastmcp import FastMCP +from .config import TRUFFLE_MCP_HOST, TRUFFLE_MCP_PORT, TRUFFLE_MCP_TRANSPORT +import logging +def create_mcp_server(name : str, **kwargs) -> FastMCP: + return FastMCP(name, stateless_http=True, host=TRUFFLE_MCP_HOST, port=TRUFFLE_MCP_PORT, **kwargs) + +def run_mcp_server(mcp: FastMCP, logger : logging.Logger | None ) -> None: + if logger: + logger.info(f"Starting {TRUFFLE_MCP_TRANSPORT} MCP server on {TRUFFLE_MCP_HOST}:{TRUFFLE_MCP_PORT}") + try: + mcp.run(transport=TRUFFLE_MCP_TRANSPORT) + except KeyboardInterrupt: + if logger: logger.info("MCP server stopped by KeyboardInterrupt") + except Exception as e: + if logger: logger.exception(f"Error running MCP server: {e}") + \ No newline at end of file diff --git a/build/lib/truffile/app_runtime/mcp/helpers_mcp.py b/build/lib/truffile/app_runtime/mcp/helpers_mcp.py new file mode 100644 index 0000000..41c774e --- /dev/null +++ b/build/lib/truffile/app_runtime/mcp/helpers_mcp.py @@ -0,0 +1,16 @@ +from mcp.server.mcpserver import MCPServer # type: ignore +from .config import TRUFFLE_MCP_HOST, TRUFFLE_MCP_PORT, TRUFFLE_MCP_TRANSPORT +import logging +def create_mcp_server(name : str, **kwargs) -> MCPServer: + return MCPServer(name, stateless_http=True, host=TRUFFLE_MCP_HOST, port=TRUFFLE_MCP_PORT, **kwargs) + +def run_mcp_server(mcp: MCPServer, logger : logging.Logger | None ) -> None: + if logger: + logger.info(f"Starting {TRUFFLE_MCP_TRANSPORT} MCP server on {TRUFFLE_MCP_HOST}:{TRUFFLE_MCP_PORT}") + try: + mcp.run(transport=TRUFFLE_MCP_TRANSPORT) + except KeyboardInterrupt: + if logger: logger.info("MCP server stopped by KeyboardInterrupt") + except Exception as e: + if logger: logger.exception(f"Error running MCP server: {e}") + \ No newline at end of file diff --git a/build/lib/truffile/app_runtime/mcp_harness.py b/build/lib/truffile/app_runtime/mcp_harness.py new file mode 100644 index 0000000..daba74b --- /dev/null +++ b/build/lib/truffile/app_runtime/mcp_harness.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import asyncio +import json +import socket +from contextlib import asynccontextmanager +from typing import Any, AsyncGenerator + +from mcp.client.session import ClientSession +from mcp.client.streamable_http import streamable_http_client +from mcp.server.fastmcp import FastMCP +from mcp.types import CallToolResult, TextContent + + +async def call_tool( + mcp: FastMCP, + name: str, + arguments: dict[str, Any] | None = None, +) -> CallToolResult: + raw = await mcp._tool_manager.call_tool( + name, + arguments or {}, + context=None, + convert_result=True, + ) + if isinstance(raw, CallToolResult): + return raw + if ( + isinstance(raw, tuple) + and len(raw) == 2 + and isinstance(raw[0], list) + and isinstance(raw[1], dict) + ): + return CallToolResult(content=raw[0], structuredContent=raw[1], isError=False) + if isinstance(raw, list): + return CallToolResult(content=raw, isError=False) + if isinstance(raw, dict): + return CallToolResult( + content=[TextContent(type="text", text=json.dumps(raw, indent=2, ensure_ascii=False))], + structuredContent=raw, + isError=False, + ) + return CallToolResult(content=raw, isError=False) + + +def _pick_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) + + +class McpTestServer: + def __init__(self, mcp: FastMCP) -> None: + self._mcp = mcp + self._port = 0 + self._task: asyncio.Task[None] | None = None + + @property + def url(self) -> str: + return f"http://127.0.0.1:{self._port}" + + @property + def mcp_url(self) -> str: + return f"{self.url}/mcp" + + async def __aenter__(self) -> McpTestServer: + import uvicorn + + self._port = _pick_free_port() + app = self._mcp.streamable_http_app() + config = uvicorn.Config(app, host="127.0.0.1", port=self._port, log_level="warning") + server = uvicorn.Server(config) + self._task = asyncio.create_task(server.serve()) + + for _ in range(50): + try: + reader, writer = await asyncio.open_connection("127.0.0.1", self._port) + writer.close() + await writer.wait_closed() + break + except OSError: + await asyncio.sleep(0.1) + else: + raise RuntimeError(f"McpTestServer failed to start on port {self._port}") + return self + + async def __aexit__(self, *exc: Any) -> None: + if self._task is not None and not self._task.done(): + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + + @asynccontextmanager + async def client(self) -> AsyncGenerator[ClientSession, None]: + async with streamable_http_client(self.mcp_url) as ( + read_stream, + write_stream, + _get_session_id, + ): + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + yield session diff --git a/build/lib/truffile/app_runtime/oauth.py b/build/lib/truffile/app_runtime/oauth.py new file mode 100644 index 0000000..30b18b7 --- /dev/null +++ b/build/lib/truffile/app_runtime/oauth.py @@ -0,0 +1,126 @@ +# base class for OAuth auth with app var + file dual-write pattern. +# subclasses set APP_VAR_KEY and override token_from_payload(). + +from __future__ import annotations + +import json +import logging +import os +from pathlib import Path +from typing import Any + +LOGGER = logging.getLogger("app_runtime.oauth") + + +class OAuth: + APP_VAR_KEY: str = "oauth_state" + + def __init__(self, token_file: Path, read_only: bool = False) -> None: + self.token_file = token_file + self._read_only = read_only + + @staticmethod + def _runtime_app_vars_enabled() -> bool: + return bool( + str(os.getenv("APP_ID", "")).strip() + and str(os.getenv("APP_SESSION_TOKEN", "")).strip() + ) + + def _load_serialized_from_app_var(self) -> str | None: + if not self._runtime_app_vars_enabled(): + return None + try: + from app_runtime import AppRuntimeClient, init_channel + + with init_channel() as channel: + client = AppRuntimeClient(channel) + return client.get_app_var(self.APP_VAR_KEY) + except Exception: + LOGGER.debug("failed to load %s from app vars", self.APP_VAR_KEY, exc_info=True) + return None + + def _save_serialized_to_app_var(self, serialized: str) -> None: + if self._read_only or not self._runtime_app_vars_enabled(): + return + try: + from app_runtime import AppRuntimeClient, init_channel + + with init_channel() as channel: + client = AppRuntimeClient(channel) + client.set_app_var(self.APP_VAR_KEY, serialized) + except Exception: + LOGGER.debug("failed to save %s to app vars", self.APP_VAR_KEY, exc_info=True) + + def _delete_app_var(self) -> None: + if not self._runtime_app_vars_enabled(): + return + try: + from app_runtime import AppRuntimeClient, init_channel + + with init_channel() as channel: + client = AppRuntimeClient(channel) + client.delete_app_var(self.APP_VAR_KEY) + except Exception: + LOGGER.debug("failed to delete %s from app vars", self.APP_VAR_KEY, exc_info=True) + + + def _load_payload_from_file(self) -> dict[str, Any] | None: + try: + payload = json.loads(self.token_file.read_text(encoding="utf-8")) + except Exception: + return None + return payload if isinstance(payload, dict) else None + + def _save_payload_to_file(self, payload: dict[str, Any]) -> None: + self.token_file.parent.mkdir(parents=True, exist_ok=True) + self.token_file.write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + @staticmethod + def _serialize(payload: dict[str, Any]) -> str: + return json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + + + + @staticmethod + def token_from_payload(payload: dict[str, Any]) -> str: + """Extract the usable token string from a payload dict. Override per app.""" + return str(payload.get("access_token", "") or "").strip() + + + + def get_oauth_payload(self) -> dict[str, Any] | None: + """Load token: app var first, file fallback, sync back if needed.""" + serialized = self._load_serialized_from_app_var() + if serialized: + try: + payload = json.loads(serialized) + except Exception: + payload = None + if isinstance(payload, dict) and self.token_from_payload(payload): + if not self._read_only: + self._save_payload_to_file(payload) + return payload + + payload = self._load_payload_from_file() + if isinstance(payload, dict) and self.token_from_payload(payload): + if not self._read_only: + self._save_serialized_to_app_var(self._serialize(payload)) + return payload + + return None + + def save_oauth_payload(self, payload: dict[str, Any]) -> None: + """save to file and app var.""" + if not isinstance(payload, dict): + raise ValueError("payload must be a dict") + self._save_payload_to_file(payload) + self._save_serialized_to_app_var(self._serialize(payload)) + + def get_access_token(self) -> str: + payload = self.get_oauth_payload() + if not isinstance(payload, dict): + return "" + return self.token_from_payload(payload) diff --git a/build/lib/truffile/app_runtime/protocols.py b/build/lib/truffile/app_runtime/protocols.py new file mode 100644 index 0000000..f60dc93 --- /dev/null +++ b/build/lib/truffile/app_runtime/protocols.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from typing import Any, Protocol, runtime_checkable + + +@runtime_checkable +class AuthProvider(Protocol): + def is_authenticated(self) -> bool: ... + + def verify(self) -> tuple[bool, str]: ... + + +class OAuthProvider(AuthProvider, Protocol): + def get_access_token(self) -> str | None: ... + + def get_auth_headers(self) -> dict[str, str]: ... + + def refresh_if_needed(self) -> bool: ... + + +class ApiKeyProvider(AuthProvider, Protocol): + def get_auth_headers(self, method: str, path: str) -> dict[str, str]: ... + + +class BrowserSessionProvider(AuthProvider, Protocol): + async def connect(self) -> bool: ... + + async def disconnect(self) -> None: ... + + async def is_logged_in(self) -> bool: ... + + def cookies_for_httpx(self) -> Any: ... + + def get_default_headers(self, accept: str = "application/json") -> dict[str, str]: ... + + +@runtime_checkable +class HttpResponse(Protocol): + @property + def status_code(self) -> int: ... + + @property + def is_success(self) -> bool: ... + + @property + def text(self) -> str: ... + + @property + def url(self) -> str: ... + + def json(self) -> Any: ... + + def raise_for_status(self) -> None: ... + + +@runtime_checkable +class HttpTransport(Protocol): + async def request( + self, + method: str, + url: str, + *, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, + content: str | None = None, + ) -> HttpResponse: ... + + async def close(self) -> None: ... + + +@runtime_checkable +class TokenStore(Protocol): + def load(self) -> dict[str, Any] | None: ... + + def save(self, data: dict[str, Any]) -> None: ... + + def delete(self) -> None: ... + + +@runtime_checkable +class CookieStore(Protocol): + def load_cookies(self) -> list[dict[str, Any]]: ... + + def save_cookies(self, cookies: list[dict[str, Any]]) -> None: ... + + def load_metadata(self) -> dict[str, Any] | None: ... + + def save_metadata(self, data: dict[str, Any]) -> None: ... + + def clear(self) -> None: ... diff --git a/build/lib/truffile/app_runtime/responses.py b/build/lib/truffile/app_runtime/responses.py new file mode 100644 index 0000000..95bfdd1 --- /dev/null +++ b/build/lib/truffile/app_runtime/responses.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import Any + + +def ok(message: str, **data: Any) -> dict[str, Any]: + result: dict[str, Any] = {"status": "success", "message": message} + result.update(data) + return result + + +def err(message: str, **data: Any) -> dict[str, Any]: + result: dict[str, Any] = {"status": "error", "message": message} + result.update(data) + return result diff --git a/build/lib/truffile/app_runtime/result.py b/build/lib/truffile/app_runtime/result.py new file mode 100644 index 0000000..fb93e28 --- /dev/null +++ b/build/lib/truffile/app_runtime/result.py @@ -0,0 +1,79 @@ +"""handy utilities for managing tool result size in foreground and background apps. + +app can use these as helpers to avoid facing runtimes chopping block +""" + +from __future__ import annotations + +import json +import logging +from typing import Any, Sequence + +logger = logging.getLogger(__name__) + +DEFAULT_MAX_CHARS = 80_000 + + +def truncate_result(text: str, *, max_chars: int = DEFAULT_MAX_CHARS) -> str: + """Hard-truncate a text result with an informative suffix.""" + if len(text) <= max_chars: + return text + original_len = len(text) + logger.warning( + "Tool result is %s chars (limit: %s). Truncating.", + f"{original_len:,}", + f"{max_chars:,}", + ) + return ( + text[:max_chars] + + f"\n\n[Result truncated: original was {original_len:,} chars, " + f"showing first {max_chars:,}. Narrow your query for more specific results.]" + ) + + +def truncate_items( + items: Sequence[Any], + *, + max_chars: int = DEFAULT_MAX_CHARS, + serialize: Any | None = None, +) -> str: + """Serialize a list of items and drop trailing items that exceed the budget. + + Unlike ``truncate_result`` which chops mid-text, this drops whole items so + the output stays valid (no half-JSON or broken CSV rows). + + ``serialize`` is an optional callable ``(item) -> str``. Defaults to + ``json.dumps``. + """ + if serialize is None: + serialize = lambda item: json.dumps(item, ensure_ascii=False) + + parts: list[str] = [] + used = 0 + kept = 0 + + for item in items: + chunk = serialize(item) + if used + len(chunk) > max_chars and parts: + break + parts.append(chunk) + used += len(chunk) + kept += 1 + + dropped = len(items) - kept + body = "\n".join(parts) + + if dropped > 0: + logger.warning( + "Dropped %d of %d items to stay within %s char limit.", + dropped, + len(items), + f"{max_chars:,}", + ) + body += ( + f"\n\n[Showing {kept} of {len(items)} items. " + f"{dropped} items dropped to stay within size limit. " + f"Narrow your query for more specific results.]" + ) + + return body diff --git a/build/lib/truffile/app_runtime/stores.py b/build/lib/truffile/app_runtime/stores.py new file mode 100644 index 0000000..7a3b233 --- /dev/null +++ b/build/lib/truffile/app_runtime/stores.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + + +class FileTokenStore: + def __init__(self, path: str | Path) -> None: + self._path = Path(path) + + @property + def path(self) -> Path: + return self._path + + def load(self) -> dict[str, Any] | None: + try: + return json.loads(self._path.read_text(encoding="utf-8")) + except Exception: + return None + + def save(self, data: dict[str, Any]) -> None: + self._path.parent.mkdir(parents=True, exist_ok=True) + self._path.write_text(json.dumps(data, indent=2), encoding="utf-8") + try: + os.chmod(self._path, 0o600) + except Exception: + pass + + def delete(self) -> None: + try: + self._path.unlink(missing_ok=True) + except Exception: + pass + + +class MemoryTokenStore: + def __init__(self, initial: dict[str, Any] | None = None) -> None: + self._data = dict(initial) if initial is not None else None + + def load(self) -> dict[str, Any] | None: + return dict(self._data) if self._data is not None else None + + def save(self, data: dict[str, Any]) -> None: + self._data = dict(data) + + def delete(self) -> None: + self._data = None + + +class FileCookieStore: + def __init__(self, cookies_path: str | Path, metadata_path: str | Path) -> None: + self._cookies_path = Path(cookies_path) + self._metadata_path = Path(metadata_path) + + def load_cookies(self) -> list[dict[str, Any]]: + try: + data = json.loads(self._cookies_path.read_text(encoding="utf-8")) + return data if isinstance(data, list) else [] + except Exception: + return [] + + def save_cookies(self, cookies: list[dict[str, Any]]) -> None: + self._cookies_path.parent.mkdir(parents=True, exist_ok=True) + self._cookies_path.write_text(json.dumps(cookies, indent=2), encoding="utf-8") + try: + os.chmod(self._cookies_path, 0o600) + except Exception: + pass + + def load_metadata(self) -> dict[str, Any] | None: + try: + data = json.loads(self._metadata_path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else None + except Exception: + return None + + def save_metadata(self, data: dict[str, Any]) -> None: + self._metadata_path.parent.mkdir(parents=True, exist_ok=True) + self._metadata_path.write_text(json.dumps(data, indent=2), encoding="utf-8") + try: + os.chmod(self._metadata_path, 0o600) + except Exception: + pass + + def clear(self) -> None: + for path in (self._cookies_path, self._metadata_path): + try: + path.unlink(missing_ok=True) + except Exception: + pass + + +class MemoryCookieStore: + def __init__(self) -> None: + self._cookies: list[dict[str, Any]] = [] + self._metadata: dict[str, Any] | None = None + + def load_cookies(self) -> list[dict[str, Any]]: + return list(self._cookies) + + def save_cookies(self, cookies: list[dict[str, Any]]) -> None: + self._cookies = list(cookies) + + def load_metadata(self) -> dict[str, Any] | None: + return dict(self._metadata) if self._metadata is not None else None + + def save_metadata(self, data: dict[str, Any]) -> None: + self._metadata = dict(data) + + def clear(self) -> None: + self._cookies.clear() + self._metadata = None diff --git a/build/lib/truffile/app_runtime/testing.py b/build/lib/truffile/app_runtime/testing.py new file mode 100644 index 0000000..9f94dca --- /dev/null +++ b/build/lib/truffile/app_runtime/testing.py @@ -0,0 +1,334 @@ +from __future__ import annotations + +from contextlib import contextmanager +from dataclasses import dataclass, field +import logging +from typing import Any, Iterable +from urllib.parse import urlparse + +import httpx + +from .worker import LocalBackgroundContext +from .errors import ErrorEnvelope + + +@dataclass(slots=True) +class HarnessResult: + success: bool + logs: list[str] = field(default_factory=list) + tool_calls: list[dict[str, Any]] = field(default_factory=list) + submissions: list[dict[str, Any]] = field(default_factory=list) + errors: list[str] = field(default_factory=list) + contexts: list[LocalBackgroundContext] = field(default_factory=list) + + +@dataclass(slots=True) +class RecordedSubmission: + text: str + uris: list[str] = field(default_factory=list) + priority: int = 0 + + @classmethod + def from_context_submission(cls, submission: Any) -> RecordedSubmission: + return cls( + text=str(getattr(submission, "text", "")), + uris=list(getattr(submission, "uris", ()) or ()), + priority=int(getattr(submission, "priority", 0)), + ) + + def as_dict(self) -> dict[str, Any]: + return { + "text": self.text, + "uris": list(self.uris), + "priority": self.priority, + } + + +@dataclass(slots=True) +class RecordedBackgroundError: + error_type: int + error_message: str + needs_intervention: bool + + @classmethod + def from_envelope(cls, envelope: ErrorEnvelope) -> RecordedBackgroundError: + return cls( + error_type=envelope.error_type, + error_message=envelope.error_message, + needs_intervention=envelope.needs_intervention, + ) + + def as_dict(self) -> dict[str, Any]: + return { + "error_type": self.error_type, + "error_message": self.error_message, + "needs_intervention": self.needs_intervention, + } + + +def make_background_ctx(app: Any, *, run_num: int = 0) -> LocalBackgroundContext: + return app.run_once_for_test(run_num=run_num) + + +class FakeBackgroundRuntime: + def __init__(self, app: Any, *, cycles: int = 1) -> None: + self._app = app + self.cycles = cycles + self.contexts: list[LocalBackgroundContext] = [] + + def run(self) -> list[LocalBackgroundContext]: + self.contexts = [self._app.run_once_for_test(run_num=run_num) for run_num in range(self.cycles)] + return list(self.contexts) + + @property + def all_submissions(self) -> list[RecordedSubmission]: + out: list[RecordedSubmission] = [] + for ctx in self.contexts: + out.extend(RecordedSubmission.from_context_submission(submission) for submission in ctx.bg.submissions) + return out + + @property + def all_reported_errors(self) -> list[RecordedBackgroundError]: + out: list[RecordedBackgroundError] = [] + for ctx in self.contexts: + out.extend(RecordedBackgroundError.from_envelope(error) for error in ctx.app_runtime.errors) + return out + + +@dataclass(slots=True) +class FakeHttpResponse: + status_code: int = 200 + _json: Any = None + _text: str = "" + url: str = "" + + @property + def is_success(self) -> bool: + return 200 <= self.status_code < 400 + + @property + def text(self) -> str: + return self._text + + def json(self) -> Any: + return self._json + + def raise_for_status(self) -> None: + if not self.is_success: + request = httpx.Request("GET", self.url or "http://fake") + response = httpx.Response(self.status_code, request=request) + raise httpx.HTTPStatusError( + f"Fake {self.status_code}", + request=request, + response=response, + ) + + +class FakeHttpTransport: + def __init__(self, responses: dict[str, Any] | None = None) -> None: + self._responses: dict[str, FakeHttpResponse] = {} + self._calls: list[dict[str, Any]] = [] + for key, value in (responses or {}).items(): + self.add(key, value) + + def add(self, method_path: str, response: Any) -> None: + if isinstance(response, FakeHttpResponse): + self._responses[method_path] = response + else: + self._responses[method_path] = FakeHttpResponse(status_code=200, _json=response) + + async def request( + self, + method: str, + url: str, + *, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, + content: str | None = None, + ) -> FakeHttpResponse: + parsed = urlparse(url) + path = parsed.path.rstrip("/") or "/" + key = f"{method.upper()} {path}" + self._calls.append( + { + "method": method.upper(), + "url": url, + "path": path, + "params": params, + "json": json, + "headers": headers, + "content": content, + } + ) + if key in self._responses: + return self._responses[key] + for registered, response in self._responses.items(): + if key.startswith(registered): + return response + raise KeyError(f"FakeHttpTransport: no response for {key!r}. Registered: {list(self._responses.keys())}") + + async def close(self) -> None: + return None + + @property + def calls(self) -> list[dict[str, Any]]: + return list(self._calls) + + def reset(self) -> None: + self._calls.clear() + + +class FakeAuthProvider: + def __init__(self, *, authenticated: bool = True, verify_msg: str = "ok") -> None: + self._authenticated = authenticated + self._verify_msg = verify_msg + + def is_authenticated(self) -> bool: + return self._authenticated + + def verify(self) -> tuple[bool, str]: + return self._authenticated, self._verify_msg + + +class FakeApiKeyProvider(FakeAuthProvider): + def __init__(self, *, authenticated: bool = True, headers: dict[str, str] | None = None) -> None: + super().__init__(authenticated=authenticated) + self._headers = headers or {"X-API-Key": "fake-key"} + + def get_auth_headers(self, method: str, path: str) -> dict[str, str]: + if not self._authenticated: + return {} + return dict(self._headers) + + +class FakeOAuthProvider(FakeAuthProvider): + def __init__(self, *, authenticated: bool = True, access_token: str = "fake-access-token") -> None: + super().__init__(authenticated=authenticated) + self._access_token = access_token + + def get_access_token(self) -> str | None: + return self._access_token if self._authenticated else None + + def get_auth_headers(self) -> dict[str, str]: + if not self._authenticated: + return {} + return {"Authorization": f"Bearer {self._access_token}"} + + def refresh_if_needed(self) -> bool: + return self._authenticated + + +class _ListHandler(logging.Handler): + def __init__(self, sink: list[str]) -> None: + super().__init__() + self._sink = sink + self.setFormatter(logging.Formatter("%(name)s %(levelname)s %(message)s")) + + def emit(self, record: logging.LogRecord) -> None: + self._sink.append(self.format(record)) + + +@contextmanager +def _capture_logs(logger_names: Iterable[str]) -> Any: + log_lines: list[str] = [] + handler = _ListHandler(log_lines) + attached: list[tuple[logging.Logger, bool, int]] = [] + + for name in logger_names: + logger = logging.getLogger(name) + attached.append((logger, logger.propagate, logger.level)) + logger.addHandler(handler) + logger.propagate = False + if logger.level > logging.INFO: + logger.setLevel(logging.INFO) + + try: + yield log_lines + finally: + for logger, propagate, level in attached: + logger.removeHandler(handler) + logger.propagate = propagate + logger.setLevel(level) + + +class AppHarness: + def __init__( + self, + *, + fg_app: Any | None = None, + bg_app: Any | None = None, + logger_names: Iterable[str] = (), + ) -> None: + self._fg_app = fg_app + self._bg_app = bg_app + self._logger_names = list(logger_names) + + async def run_fg(self, *, calls: list[tuple[str, dict[str, Any]]]) -> HarnessResult: + if self._fg_app is None: + raise RuntimeError("foreground app is not configured for this harness") + + logger_names = self._merged_logger_names(self._fg_app.logger_names()) + with _capture_logs(logger_names) as logs: + tool_calls: list[dict[str, Any]] = [] + errors: list[str] = [] + success = True + + for tool_name, arguments in calls: + try: + result = await self._fg_app.invoke_tool(tool_name, **arguments) + tool_calls.append( + { + "tool": tool_name, + "arguments": arguments, + "result": result, + } + ) + except Exception as exc: + success = False + message = f"{tool_name}: {exc}" + errors.append(message) + tool_calls.append( + { + "tool": tool_name, + "arguments": arguments, + "error": str(exc), + } + ) + + return HarnessResult( + success=success, + logs=list(logs), + tool_calls=tool_calls, + errors=errors, + ) + + async def run_bg(self, *, cycles: int = 1) -> HarnessResult: + if self._bg_app is None: + raise RuntimeError("background app is not configured for this harness") + + logger_names = self._merged_logger_names(self._bg_app.logger_names()) + with _capture_logs(logger_names) as logs: + runtime = FakeBackgroundRuntime(self._bg_app, cycles=cycles) + contexts = runtime.run() + submissions = [submission.as_dict() for submission in runtime.all_submissions] + errors = [error.error_message for error in runtime.all_reported_errors] + success = not errors + + return HarnessResult( + success=success, + logs=list(logs), + submissions=submissions, + errors=errors, + contexts=contexts, + ) + + def _merged_logger_names(self, default_names: Iterable[str]) -> list[str]: + seen: set[str] = set() + merged: list[str] = [] + for name in list(default_names) + self._logger_names: + if name in seen: + continue + seen.add(name) + merged.append(name) + return merged diff --git a/build/lib/truffile/app_runtime/worker.py b/build/lib/truffile/app_runtime/worker.py new file mode 100644 index 0000000..4500ac2 --- /dev/null +++ b/build/lib/truffile/app_runtime/worker.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +from dataclasses import dataclass +import logging +import sys +from typing import Any, Generic, Iterable, TypeVar + +from truffle.app.background_pb2 import BackgroundContext + +from .errors import ErrorEnvelope, ErrorReporter + +_PRIORITY_DEFAULT = getattr( + BackgroundContext, + "PRIORITY_DEFAULT", + getattr(BackgroundContext, "PRIORITY_HIGH", 1), +) + +WorkerT = TypeVar("WorkerT") +ResultT = TypeVar("ResultT") + + +@dataclass(frozen=True, slots=True) +class Submission: + text: str + uris: tuple[str, ...] = () + priority: int = _PRIORITY_DEFAULT + + def __init__( + self, + *, + text: str, + uris: Iterable[str] = (), + priority: int = _PRIORITY_DEFAULT, + ) -> None: + object.__setattr__(self, "text", text) + object.__setattr__(self, "uris", tuple(uris)) + object.__setattr__(self, "priority", priority) + + def as_dict(self) -> dict[str, Any]: + return { + "text": self.text, + "uris": list(self.uris), + "priority": self.priority, + } + + +@dataclass(slots=True) +class _LocalRuntimeReporter: + errors: list[ErrorEnvelope] + + def report_error( + self, + *, + error_type: int, + error_message: str, + needs_intervention: bool = False, + app_uuid: str | None = None, + ) -> ErrorEnvelope: + envelope = ErrorEnvelope( + error_type=error_type, + error_message=error_message, + needs_intervention=needs_intervention, + ) + self.errors.append(envelope) + return envelope + + +@dataclass(slots=True) +class _LocalBackgroundClient: + submissions: list[Submission] + + def submit_context( + self, + *, + content: str, + uris: Iterable[str] = (), + priority: int = _PRIORITY_DEFAULT, + max_chars: int = 80_000, + ) -> Submission: + if len(content) > max_chars: + original_len = len(content) + content = ( + content[:max_chars] + + f"\n\n[Content truncated: original was {original_len:,} chars, " + f"showing first {max_chars:,}.]" + ) + submission = Submission(text=content, uris=uris, priority=priority) + self.submissions.append(submission) + return submission + + +@dataclass(slots=True) +class LocalBackgroundContext: + run_num: int + run_info: None + bg: _LocalBackgroundClient + app_runtime: _LocalRuntimeReporter + + +class BackgroundApp(Generic[WorkerT, ResultT]): + AUTH_FAILURE_THRESHOLD: int = 3 + + def __init__(self, name: str, *, logger_name: str | None = None) -> None: + self.name = name + self.logger = logging.getLogger(logger_name or f"{name}.background") + self.logger.setLevel(logging.INFO) + self._worker: WorkerT | None = None + self._error_reporter = ErrorReporter(logger=self.logger, app_name=name) + self._consecutive_auth_failures: int = 0 + + def build_worker(self) -> WorkerT: + raise NotImplementedError + + def verify_worker(self, worker: WorkerT) -> tuple[bool, str]: + verify = getattr(worker, "verify", None) + if callable(verify): + return verify() + return True, f"{self.name} background verified" + + def run_cycle(self, worker: WorkerT) -> ResultT: + raise NotImplementedError + + def handle_cycle_result(self, ctx: Any, result: ResultT) -> None: + raise NotImplementedError + + def report_auth_failure(self, ctx: Any, description: str) -> None: + """Report auth failure with dedup. Only reports after AUTH_FAILURE_THRESHOLD consecutive failures.""" + self._consecutive_auth_failures += 1 + if self._consecutive_auth_failures >= self.AUTH_FAILURE_THRESHOLD: + self.logger.error("auth failure (count=%d, threshold reached): %s", self._consecutive_auth_failures, description) + ctx.app_runtime.report_error( + error_type=2, # APP_ERROR_AUTH + error_message=f"{self.name} authentication failure: {description}", + needs_intervention=True, + ) + else: + self.logger.warning("auth failure (count=%d/%d, suppressed): %s", self._consecutive_auth_failures, self.AUTH_FAILURE_THRESHOLD, description) + + def reset_auth_failures(self) -> None: + if self._consecutive_auth_failures > 0: + self.logger.info("auth failure streak reset (was %d)", self._consecutive_auth_failures) + self._consecutive_auth_failures = 0 + + def submit_text( + self, + ctx: Any, + *, + content: str, + priority: int = _PRIORITY_DEFAULT, + uris: Iterable[str] = (), + ) -> Submission | Any: + return ctx.bg.submit_context(content=content, uris=uris, priority=priority) + + def get_worker(self) -> WorkerT: + if self._worker is None: + self._worker = self.build_worker() + return self._worker + + def reset_for_test(self) -> None: + self._worker = None + + def verify(self) -> tuple[bool, str]: + worker = self.get_worker() + return self.verify_worker(worker) + + def run_once_for_test(self, *, run_num: int = 0) -> LocalBackgroundContext: + ctx = LocalBackgroundContext( + run_num=run_num, + run_info=None, + bg=_LocalBackgroundClient(submissions=[]), + app_runtime=_LocalRuntimeReporter(errors=[]), + ) + self._execute_cycle(ctx) + return ctx + + def main(self) -> None: + if "--verify" in sys.argv: + ok, message = self.verify() + print(message, flush=True) + raise SystemExit(0 if ok else 1) + + from app_runtime.background import run_background + + run_background(self._execute_cycle) + + def logger_names(self) -> list[str]: + return [self.logger.name] + + def _execute_cycle(self, ctx: Any) -> None: + worker = self.get_worker() + try: + result = self.run_cycle(worker) + self.handle_cycle_result(ctx, result) + except Exception as exc: + self._error_reporter.report_background_exception(ctx, exc, phase="background_cycle") + + +class BackgroundWorkerApp(BackgroundApp[WorkerT, ResultT]): + pass diff --git a/build/lib/truffile/assets/Truffle.png b/build/lib/truffile/assets/Truffle.png new file mode 100644 index 0000000000000000000000000000000000000000..979ff73ce44802282df90fee6b713db3088d3cf5 GIT binary patch literal 8922 zcmeHN`9GBH_rGV1Wvn5z*d^JMtQm@;2U%yvSkff>*vghIlPu2@g)EUmMKUuO%UBx* zc@j!!#=cd^GRUs6eD6L#e_!7};QRXgFh9(>uIpU)eXet#^FHr${bOTg#>ahv8vp>l zzph@k0{|%I&yR}(lr($hi~)e`%fBvPLf(Zi#}Cs*K5S~QrZ4UnRLIdD0)GRFP#f4) zLA-3*QOJ?|mtjt_X@7&#K-f9Koy}^y*WFc3OR1U&gl2&?y{swOkJvSq%j(f9X=>S7 zIkW_T%-s(#;G!ag2Pn+JvjaAkG$?T9G!_CJ6($3~Lpb;-9WWID?*1EbSNfn zWZuX~c@?s#fNkZbU^iwz*J@Yq#T!u3 zo~5%_XQ&BhR1G5ad0n?xCJ)b~KsI42XZlCND_p7i5r_NRlkAF#Pt)iGm(shG$@_VN zl+fr<;coD7#o|p~mLl3&acuVu+7=+b&E+ zw-{9-^hSsf3-f~go-sOU@I?_8E>x5bPI?m z6}lRu2`;KZK8jTeP|>C@O2`O@mvJ7a{1DMQt0oqLwU#uujB}Y2pgi|wTxC(r5WzXZ zVcZP|Q_+^Yr1Wy(=g-b;IQbmhaq=FE`B!N7lBlyZw$iE8T;8&H^JzFrX(*9lZW(GZ ze#K^IdFgx>bpwQhN!i3zH~&ZK|v9jm-=+0yY@+ON7qane|gB2mJ<-5IfUBuzo6 zHs2t^%`^EX9K$Q8iR>*fawvtC7x8tLRC#F0zCJ?6AdTtCV(L9eh)#A;Rz#Uus^p7! zHOv!yhwMl;w-3Z=pA%^@dKOg{uc~@ETN>N<$XA3M&oH(eigSgjc$PS^rxY?J58DNd z-uh}>fFuu}!t8GhSQAjnUz?-P4cyt$->a?CH#17x(?ebB#$i9++Nf z>)+vTM^D%zF6>nsysdvTtKe;WjrPqHM%;bQZ=-79@!-m|Pps1yUm4QA8lGPDJ532H zIw*^w`@B?K311%DJ%eoZsF}(d1RW}&0vac(Bagi;A4lJO!EeX($~rUqHO`?8{kXu_ zC|EB}bPyc$aTGj*xo+jmKM{w-Ew-K!suHRdYFw-K9qj1wzKw#47GyoT)N*(bm84c+ zlH;aqm89lOiZ=i9tqP{{*+YtGEw4*C5_)*BpMjOPtn_F&aE{V2BHr0oytFuYBNovh zMstLz7^a`}RzeSV`>qJ(3%wG0b4QM%C=)d}@{it&m#BAJD(jA6iE}DYQK^qWRM%uw zj5&fA8L=J0Ep~y|C8{iFruo;8j~N+-?2qsr|BM~8r;Lm)yke|HtxnzN7CG8UEusi- zYrCc?F?x*-mkitB5Zv5a%`lpC`AR^XfRy3h5kikmtHGUEpCsyBj5_nOzlkwzq;hPv znYG;XX>x;~h=ij9BFQPDwN>XL7Plpce1hb#GuByfbi+q~lrbt_7FCM1^gsExIDgAb zAB;}g7b1swcB6>x(;=Ei$dV0GGxr0DH!ZYq1u)W!p#>5uE|VeUXU5~~*!PaYd5KPn z0ToJH$4^GDfAS1Kt0HGSi#It(v!Wd07}*>@{B0Vgpp1WKW;6F48ETgnetrL-@Um+_ zYRNip&^#2!6Xjp*;XO(67ba^f=@cT%81DqG8KazHbDhuHvtM5J_jkoHS$;Fk*)i1y z=L0hEluNikxGx0yG(F1Yf_iA#_65~;rVv@%^OI^TlGx9IEQ6LSd6kqm5Q;tO5l}|r zkGFZ0G9kZe6g+`|qFtj97$>|aBN2(q38T_j|BC@w z%^&hE5X4OW`to=aoix;q*q@+ADQw?mSg_-Y8RvTPvox#=47KX{d2d}4QEgvYERLfX zH)y0tmak>zlynSM+~9Gb*)WiO=Cm$E!KQ7iQ?B^e=Oq=kjv`#7PbZX|;0f?}QWBmb z zu8*4Gz9tQic~xVscAhA}Yt^T>qtsRnuFaHkV=YVrPM&t3xhpZMn{thIVyJW4{KAo; z#<2$j=frP!j=4I17bbIP`PTaq*~)lfTe2vU;I{bDs((H65+RRHl-p6Nm{76~{OmB} zX3hTF5e*;Qh2Y!jNHT|}TfH3xx-;-MUFE^bExXneJ>E&$L?_6e|9f9_v!>8*WYxd2 zSiVzvf6s1g3;y`)3=w&5YQB_f>XzdV&3^Odc+yfU^FRX%m4==r(9-yykS0Yrz~iYp1=I$8P#^%x~w5NlVzUnK3+NTvFF;@uv@(m z4uH#eUtizHw;#BwZQxE3SWi7DBd%YwC46O~=6qBs@b#Ea*7Q|oH=WHv8Gf#Va36U*&#SFi8&bvx{-tanMot&43zI!F&n*c_P1QQt)I*Os@HkrdZ9Uf` z^>AoPL2##s7kx|j%SoazOp?3_+VeqyQLLecfF?#0Q&pGG$86r;8inIGb@la~+>72b z_||P(pS9YRwTgd~c7c$rwSk!58}ncD(9_W{lA8sEL%3W#mW}Lcop0dyW68LzEy7z+ zv@7Fct^3dG)UFKc#1ROlB}CTs-Cd2%O!PDeS44B!N0mfK#u3n;66O6mH-1ao0t*IF zTO^jTd|6wh66T5!sd;a$yHp|SI>c|lF1gt6qLUdrv2%Me)@U>P{#DRaQP~StZ{6sE zWN!Dj2n)ZcE@^kq3Sj6B@=0Wcie*QSV}e!<7f+GaP<2XPvYLkrP+PJ6rRo0m2_=lq z*Qj1)6DGiV@^U->yY%t94jqkpBBYZTH!h7)4R1c^Qa6|_t!g0)A8D*lZ?WSSR~E7w zl<_fLwS3gMN@Qh#Df{BH$iVKjwV=pvz>-7b>EQ^f7>cxL-`eYNGA7C_=8`fasQa5x+1l4$5J# z+pY{N6aPrI3C7hJhmtFIuOL``!!B0!(5V`d(%!ERQYEfEZr5LMGekB>u6muv@yYHc zUyzlN{$fQBL$#DJ4qr4|jUqR$f(u+6uMMrc3!q3u+t$XSd~0rwQAj;0pKXPJd0G+| z`j?rR*?1Qj@pKAG4nVYMMoQ{a!;~;jGlz+i5akES6*pS)fvKv<0I7`+Qn4Z9OCe=Q zvYJfspevm?;dKo{zPG#lQzSAUbf8}mP=@@*@3S7x98`axTxojYV}^pg0Eizh+KVYn}DDXrZSzey|8;JC*RQxl(M5X~q3!WJSC(P)_s`vB+CpN0JR+4+b?A zR{&$kUZw2v?hbI*FSvR{EBGBxy@^Cu`V9wZKo+4?LxWT)qkN#yxY<6Ebf$-VVNRI* zIepDH6>-JRRqG7b#jJRutTYfjf7|pX!DCmlx|&sjB!5n8-I5ehdB6mEip`Qv`M2o=PFO~p)AUU$@~gJM z=^ss^Q@fsMPY=ULn(v}_?>uuh+}RRZxa?|&jvw5xF2~J=(b0tr5y&0izgpTcqw1IRzvw0dhQOgJ4XoF!|tB96K$0b13Azsxc@KA|IR!pR6BznFj<( z24lcLXJ7EXERvnpzdY8U`RUWWKU3ZqN+n9RCjdd|lH*53NJemQlE4FQQ9v$-#p*0% zfrzBEcTWUJ!7i^9rHu8&ERCJu!erptY4aI*Qu zY_@yM7>K`_+WpiiawOC2goCa*PtFsdV${oqnm7i+Z<3XHQ zugGW}CiPnVB0ehw5#F|3iBkhyt~4qKr&es_+?E_JXG5n-Rzku7)AQS0$=r;`)Y!v& z&+PO0dF$EE$|}R>isQ88=*obphy3)rFbQ&`UmpO-HG-i0yaJIc+;180k<0;@7GKd- zhu}=>0w?nU>}AA5Ad24+%1A+yfn%uD1WZ(b@Uk{`bH_pq6bz3|+DA?B;OZc_WjbH2 zH-HV{hO7_o+v)}XddZFr2^NF)V3}XWV&@4CKuW-}8~!*+Lf_CZf3^rbil4KtOf{^n z6%C4AfsrIeSRCF!;T2t}(epi**?I6(%^%Gs!JMRq-n&qIo*i#a>Tyyo<_Z_UDvo0Z zdfIpy{JE{*Qr?DMv*aTnzXP7i#Y*)9meBxz%&!iyf#+erW0e8r@2nt6r}+Ps=eeM@ z?RY=RkbW6tg*;_IR>;Pb_UxZWmXKpFM5m*}PLiLuz<_ljGJqeC05SM`1CS~jDm6OT zx%F&dZ2VO^9HW`rRKW-6ghodj{K-K8**>_JhlsWPY!3lmS3e9gRQiw1fgCW|j9_MQ zGc0?skhA(Cq}{jJPwx#eK#CRZS#JOv1GEuHLCG!l`DlQzT^BI^Y_{kM7p*fAS%D;n z{VV{B`11-79(?+zeFSB@!@Y?Q6I|iz>a*kZO-O z_(p+4j%A60r%e?AH`oO?c>Sy?E_Ckm3!h#X>A0R6CtmQAM7dk9GM5SMnQf}TS-=IA z3Z;!Vg_j_}G<*tt)>o6UhcV9vCfZR;`q)oY)Pv(HfT=tC^d9I!vsOdHZ!UF0vFC$1 zRYX)&+k65>;(?S?lNP}%1NGY?@s*&nitwX_pkxJ(G1gEh?O}rv3^YlK`!Zx(vLjh* zD2DcoWpZ|#^Ego6#@%KZCPkiI7WV2K&js~JPnvkDE~dE+lXn1?QpmeqxDHq$~zbXs3>Z- z&Q|;u1i$H%uS(q@d&|b1|b+=v9s2#?uC%j(fkw(=Cbz%$Ujj;L|Mw0(};~6-vjO4mncH)fYDtY zw1gijbwvuYcniw75xMawYNG`*b$;DpKetu??6x?%$moTk7O;_qS^`z)&ANZyu~))y zN*YG%eqweciwOf8{okX^qE_vpjQf%&i+qHkU+hy?Ui^t_(U!@p7e^lW2!FnocLXn4 z6}h@t(ULzq8^!#iVr}}T?QHbH-YyIXUZhJNEjN@01G5|!5w+z9#W!qv3m8Ujcz*-+ zDk8FT--zgK|1%3LjjFfoonv+FoSnL-4Oz^Lwy7Em2`~bycu>ncU2^Hyiw09D<9MXs zU>Y|%Re=ZCQuy#~<7cL~K)&^#oHFw!aU7CkQla+G*3_=wK+xj`Yl&1+W9R2O^bNp9 ze)9S*f1Yz2J+Nh8`?j_SDL9(NtQ#2Xmqm6!3+*a|O=>vR-^7JJcC`jdT|FLX18={) zW)s#Hqvmik^s1CznFZ>hROW=CHZsnd8(a?aYg`Uu!eCy#rFC;}=XB-RS4Mg-(L>B50LqGC$2`3lR`qp9|ePWU4BC;>CMAU zkZ-aEf*cV*w9(Bu)_d?x`N8Ai~M8ul=!c5ajv*s#4 z%`0YqbI@MB*0cTuWbsL)|L~Cr*?Gzd7(R4?qH5y4Oc*5mE;~Y2Q=BGvi72zJfcyXp z29#{xJ5s0BPS0&^jH_YxFD}!YyM@~q2P-7Z0Gio3^M2e6wmBIr_jGRm2SqhKEJktB zeBfwm8|tm=^;8gWZI-)rRJ0t7FkLPK|7LNn@YZfWr`k~)B}GWe<|4%KXV!{C97Vv! zfuRkx(NKhhTWC;6k5Qwx-46xVF#szwJ^i`!Xz~Ib#Ff_vKsvx+6Qui2Hoaq8`r6lL zI~XKFaL{=j7s@vOV0*$`Ml44F0p`K{Chn%%$>;b`Hbm+f-AuY%*jM(UW|g;%;VZ7& zdSf*l5)09qPVqWv0ykEk_`B;|&r5^)O$sGHUz3n?8Gp{r;8WcUNx!+B8K}9(Putp% z^i72kqwBaZIv@r#f$LK_C*)eo!>uLphS^*W>BnzO6B4=6>4D9=v+h}Df%)1%tw(8) zhjXG66rC;~b~mtw2U2^pG)RPF6z3x->PSExE4%K!DuJbD^Ex;^*hG>adT4&}>dn7q z@v(&kvIWAVzEDr@7)3I>MWS+9YXldjBijjex6`vIMSAdJ6qsiZevEYXMg$s}v8cv8 z&Pgk!YIC6=i7L=i7k;eu$+S`p$D9oIlk|s?Q|_vYtQ>=w-(R0{Rr|;&7HosWmw=H} zVo9GzfX3j}iFJ06@q8iBVj?g+r<7-=E-&!iBC$yo48~8;NlyRqrmoL*{6m z!iuBr%?e5jWdZ#k3iU|al+qo4c^B0P{|KM}EXiLB&Q?B)M-VUTsuuJuO=e%rI!pO6 zu;E1MLK7gWA*~T0T)x-;#JLgrJSeMIWnGZ$2{MLhTE-XTZ@_Z9BX?G(-y2GB5Z)23 z=r~llix*VvS~<1HS+!DT(`UB)_n*4ZLz;zrWUPg|v9{a2hn!$$F>aJsBe|Utxq8w<7E~lmV?W2#>$? zp_)KR950U0wYSIG)#k@sM)|*; z4a%Kf@OqD)@@b=2!AR_7^6pmx#&Y>=>5aKzUo9_$nT9?z_kX(U@;djs-3)<_{^|KR zY@_2;8+{E_TBhi(Rq9J2&v4vpMnAI4b^8nMta=xIeK6x}))jdlw({u{qUHZeWBIT} z689QAKNwF>(l(@VqdR)1#A z=Y@9gH6^@xCzR$C1QN(wbF;6ee)Ct%gz?qkA~TOLoVFV{Ya%*T5xT>3zLro%p{c%j z2;BemKzG31$YYtv=U9V=YyqiPBfTJ?(qE5k`8i_rZbCrn*#y|f=wtiJQ2w%esLrY5 zJd8LE!C@OT+!J2X0Gf;!&*K#-;dWnz0wXYPL-AKTlqUJw=tJNe6~p#6<_9vw0M^#d zukRUMS^ir8mRzZqbxQObM~UfSifFDiN&f9ii!#`=YXE+pvmg6 zOxsZdQPU^cQzJTs6Uq==$J$Jqd3JKDt5tzdU56anX~_4ajlRhf`5%|$SofZ z-`TtlaiQmTg8y`EJlnr|NFU$~UWJGp^0+QJ{b``+jI{92;^I>^DoomgR*;;tr! zNi4H)TC|q8N4P;$^L@EU3pi@U<;3cX%T%K02vv=c-z_i>a;irJAUH3lFZ%lWYp1A! zsx}^!8x!n=O0x?KCwTf9sRkbrSEjYjQt;9Sq0IRVGkT z1)Vi9bysgG{!UjUD=hK?S6+U(NqHY{g%_%DF|-Aw&%OV&HL7LyB_#DSsBeWxytF$` zM8D#$nY#s*-fVZS(!N=?G2X>15^9tNr~3>?4)$x<<$c%*=-c2>wTfm-F05oMUbtxF zQfJ%IV10>3WTP(}<&uc+$a3^NC>?VbXdeJWFRJ*MRn)~hX`A$#TSe!1a>HLf>^?%S zuWZjvQ>|0rH$mPD5Gmb?)AFU4ziWwMp*wI@m(;ajDqWP5VLaE9bXIL=5=Wp&*Ihl- z+jd~4G>hsp7yupo_3KyCqHasz%%z3Z*7+cc+E^z3icU#E=IEo}tJHJxlrLWc3Yq(i zGUtML#c6nXIeXc9`Fy%@j`!}q$mlcxnE$s;{Qs&={NK9}|M%sPL#aU783IA`u`PIK1^DZV L)#Y+yY~23 int: + parser = argparse.ArgumentParser(prog="truffile", add_help=False) + parser.add_argument("--resume", action="store_true", help="resume a previous task") + sub = parser.add_subparsers(dest="command") + + # scan + scan_p = sub.add_parser("scan", help="scan for truffle devices") + scan_p.add_argument("--timeout", type=int, default=5) + + # connect + conn_p = sub.add_parser("connect", help="connect to a truffle") + conn_p.add_argument("device", help="device name (e.g. truffle-1234)") + + # disconnect + disc_p = sub.add_parser("disconnect", help="disconnect from device(s)") + disc_p.add_argument("device", nargs="?", default="all") + + # create + create_p = sub.add_parser("create", help="scaffold a new app") + create_p.add_argument("name", nargs="?") + create_p.add_argument("--path", type=str, default=None, help="base directory for the app") + + # validate + val_p = sub.add_parser("validate", help="validate app directory") + val_p.add_argument("path", nargs="?", default=".") + + # deploy + dep_p = sub.add_parser("deploy", help="deploy app to device") + dep_p.add_argument("path", nargs="?", default=".") + dep_p.add_argument("--shell", action="store_true") + dep_p.add_argument("--interactive", action="store_true") + dep_p.add_argument("--dry-run", action="store_true") + dep_p.add_argument("--no-finalize", action="store_true") + + # list + list_p = sub.add_parser("list", help="list apps or devices") + list_p.add_argument("what", choices=["apps", "devices"]) + + # delete + del_p = sub.add_parser("delete", help="delete app from device") + del_p.add_argument("app", nargs="?") + + # models + sub.add_parser("models", help="list inference models") + + # chat (agent runtime with apps) + chat_p = sub.add_parser("chat", help="agent chat with apps") + chat_p.add_argument("prompt", nargs="?", default=None) + chat_p.add_argument("--resume", action="store_true", help="resume a previous task") + + # infer (raw model inference) + infer_p = sub.add_parser("infer", help="raw model inference") + infer_p.add_argument("--model", type=str, default=None) + infer_p.add_argument("--system", type=str, default=None) + infer_p.add_argument("--no-stream", action="store_true") + infer_p.add_argument("--no-tools", action="store_true") + infer_p.add_argument("--mcp", type=str, action="append", default=None) + + # help + sub.add_parser("help", help="show help") + + # easter egg + sub.add_parser("glow") + + args = parser.parse_args() + + if args.command == "help": + show_help_welcome() + return 0 + + if args.command == "glow": + render_glow_demo(duration=999999.0) + return 0 + + if args.command is None: + from truffile.storage import StorageService + storage = StorageService() + from .chat import cmd_chat + from types import SimpleNamespace + return run_async(cmd_chat(SimpleNamespace(resume=args.resume, prompt=None), storage)) + + from truffile.storage import StorageService + storage = StorageService() + + if args.command == "scan": + from .connect import cmd_scan + return run_async(cmd_scan(args, storage)) + elif args.command == "connect": + from .connect import cmd_connect + return run_async(cmd_connect(args, storage)) + elif args.command == "disconnect": + from .connect import cmd_disconnect + return cmd_disconnect(args, storage) + elif args.command == "create": + from .create import cmd_create + return cmd_create(args) + elif args.command == "validate": + from .validate import cmd_validate + return cmd_validate(args) + elif args.command == "deploy": + from .deploy import cmd_deploy + return run_async(cmd_deploy(args, storage)) + elif args.command == "list": + from .apps import cmd_list + return cmd_list(args, storage) + elif args.command == "delete": + from .apps import cmd_delete + return run_async(cmd_delete(args, storage)) + elif args.command == "models": + from .models import cmd_models + return run_async(cmd_models(storage)) + elif args.command == "chat": + from .chat import cmd_chat + return run_async(cmd_chat(args, storage)) + elif args.command == "infer": + from .infer import cmd_infer + return run_async(cmd_infer(args, storage)) + + show_help_welcome() + return 1 diff --git a/build/lib/truffile/cli/apps.py b/build/lib/truffile/cli/apps.py new file mode 100644 index 0000000..8feaca1 --- /dev/null +++ b/build/lib/truffile/cli/apps.py @@ -0,0 +1,218 @@ +import asyncio + +from truffile.storage import StorageService +from truffile.client import TruffleClient, resolve_mdns + +from .ui import C, DOT, CROSS, CHECK, Spinner, error, success + + +async def cmd_list_apps(storage: StorageService) -> int: + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + token = storage.get_token(device) + if not token: + error(f"No token for {device}") + print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") + return 1 + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError as e: + spinner.fail(str(e)) + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + + try: + await client.connect() + apps = await client.get_all_apps() + spinner.stop(success=True) + + if not apps: + print(f" {C.DIM}No apps installed{C.RESET}") + return 0 + + focus_apps = [app for app in apps if app.HasField("foreground")] + ambient_apps = [app for app in apps if app.HasField("background")] + both_apps = [app for app in apps if app.HasField("foreground") and app.HasField("background")] + + print() + if focus_apps: + print(f"{C.BOLD}Focus Apps{C.RESET}") + for app in focus_apps: + print(f" {C.CYAN}{DOT}{C.RESET} {app.metadata.name}") + setattr(app.metadata, "description", getattr(app.metadata, "description", "")) + if hasattr(app.metadata, "description") and app.metadata.description: + desc = app.metadata.description.strip().split('\n')[0][:55] + print(f" {C.DIM}{desc}{C.RESET}") + + if ambient_apps: + if focus_apps: + print() + print(f"{C.BOLD}Ambient Apps{C.RESET}") + for app in ambient_apps: + schedule = "" + policy = app.background.runtime_policy + if policy.HasField("interval"): + secs = policy.interval.duration.seconds + if secs >= 3600: + schedule = f"every {secs // 3600}h" + elif secs >= 60: + schedule = f"every {secs // 60}m" + else: + schedule = f"every {secs}s" + elif policy.HasField("always"): + schedule = "always" + print(f" {C.CYAN}{DOT}{C.RESET} {app.metadata.name} {C.DIM}({schedule}){C.RESET}") + setattr(app.metadata, "description", getattr(app.metadata, "description", "")) + if hasattr(app.metadata, "description") and app.metadata.description: + desc = app.metadata.description.strip().split('\n')[0][:55] + print(f" {C.DIM}{desc}{C.RESET}") + + print() + print( + f"{C.DIM}Total: {len(focus_apps)} focus, {len(ambient_apps)} ambient, " + f"{len(both_apps)} both{C.RESET}" + ) + return 0 + + except Exception as e: + spinner.fail(str(e)) + return 1 + finally: + await client.close() + +async def cmd_delete(args, storage: StorageService) -> int: + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + token = storage.get_token(device) + if not token: + error(f"No token for {device}") + print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") + return 1 + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError as e: + spinner.fail(str(e)) + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + + try: + await client.connect() + apps = await client.get_all_apps() + spinner.stop(success=True) + + all_apps = [] + for app in apps: + if app.HasField("foreground") and app.HasField("background"): + kind = "both" + elif app.HasField("foreground"): + kind = "focus" + elif app.HasField("background"): + kind = "ambient" + else: + kind = "unknown" + desc = app.metadata.description.strip().split('\n')[0][:55] if app.metadata.description else "" + all_apps.append((kind, app.uuid, app.metadata.name, desc)) + + if not all_apps: + print(f" {C.DIM}No apps installed{C.RESET}") + return 0 + + print() + print(f"{C.BOLD}Installed Apps:{C.RESET}") + print() + for i, (kind, uuid, name, desc) in enumerate(all_apps, 1): + print(f" {C.CYAN}{i}.{C.RESET} {name} {C.DIM}({kind}){C.RESET}") + if desc: + print(f" {C.DIM}{desc}{C.RESET}") + print() + + try: + choice = input(f"Select apps to delete (e.g. 1,3,5 or 'all'): ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + + if not choice: + return 0 + + if choice.lower() == "all": + to_delete = list(range(len(all_apps))) + else: + try: + to_delete = [int(x.strip()) - 1 for x in choice.split(",")] + for idx in to_delete: + if idx < 0 or idx >= len(all_apps): + error(f"Invalid selection: {idx + 1}") + return 1 + except ValueError: + error("Invalid input") + return 1 + + print() + deleted = 0 + for idx in to_delete: + kind, uuid, name, _ = all_apps[idx] + spinner = Spinner(f"Deleting {name}") + spinner.start() + try: + await client.delete_app(uuid) + spinner.stop(success=True) + deleted += 1 + except Exception as e: + spinner.fail(f"Failed to delete {name}: {e}") + + print() + success(f"Deleted {deleted} app(s)") + return 0 + + except Exception as e: + spinner.fail(str(e)) + return 1 + finally: + await client.close() + + +def cmd_list(args, storage: StorageService) -> int: + what = args.what + if what == "apps": + return _run_async(cmd_list_apps(storage)) + elif what == "devices": + devices = storage.list_devices() + if not devices: + print(f" {C.DIM}No connected devices{C.RESET}") + else: + print(f"{C.BOLD}Connected Devices{C.RESET}") + for d in devices: + if d == storage.state.last_used_device: + print(f" {C.GREEN}{DOT}{C.RESET} {d} {C.DIM}(active){C.RESET}") + else: + print(f" {C.CYAN}{DOT}{C.RESET} {d}") + return 0 + + +def _run_async(coro): + try: + return asyncio.run(coro) + except KeyboardInterrupt: + print(f"\r{C.RED}{CROSS} Cancelled{C.RESET} ") + return 130 diff --git a/build/lib/truffile/cli/art.py b/build/lib/truffile/cli/art.py new file mode 100644 index 0000000..7d10e6c --- /dev/null +++ b/build/lib/truffile/cli/art.py @@ -0,0 +1,551 @@ +from __future__ import annotations + +import math +import os +import sys +import threading +import time + +try: + import termios + import tty +except Exception: + termios = None + tty = None + +# truffle shape โ€” bean/mushroom cap outline +# using block characters for solid fills and edges + +TRUFFLE_BANNER = [ + "##########@@%**+++====+++**%@@##########", + "######%+=-::-===++++++++===-::-=+%######", + "####+::-+%@##################@%+-::+####", + "##%:.+@##########################@+.:%##", + "#%..@##############################@. %#", + "#: %################################% :#", + "# .##################################. #", + "#. ################################## .#", + "#= +################################= +#", + "##- =@##########@@%%%@@###########@= =##", + "###*-.-=++++===-::----::-==+++++=-.-*###", + "#####@*+====++*%@#####@@%*++====+*@#####", +] + +TRUFFLE_BANNER_BRAILLE = [ + "โ €โ €โ €โ €โ €โ €โ €โฃ€โฃ€โฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃ€โฃ€โ €โ €โ €โ €โ €โ €โ €", + "โ €โ €โ €โข€โฃคโฃถโ ฟโ ›โ ‹โ ‰โ ‰โ โ €โ €โ €โ €โ €โ ˆโ ‰โ ‰โ ™โ ›โ ฟโฃถโฃคโก€โ €โ €โ €", + "โ €โ €โฃดโกฟโ ‹โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ ™โขฟโฃฆโ €โ €", + "โ €โฃผโกŸโ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โขปโฃงโ €", + "โข โฃฟโ โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ ˆโฃฟโก„", + "โ ˜โฃฟโ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โข€โฃฟโ ƒ", + "โ €โขฟโฃ‡โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โฃผโกŸโ €", + "โ €โ ˆโ ปโฃทโฃ„โก€โ €โ €โ €โฃ€โฃ€โฃ โฃคโฃคโฃคโฃคโฃคโฃ„โฃ€โก€โ €โ €โ €โข€โฃ โฃพโ Ÿโ โ €", + "โ €โ €โ €โ ˆโ ™โ ›โ ฟโ ฟโ Ÿโ ›โ ›โ ‰โ ‰โ โ €โ ˆโ ‰โ ™โ ›โ ›โ ปโ ฟโ ฟโ ›โ ‹โ โ €โ €โ €", +] + +ORB = [ + "โข€โฃถโฃถโก€", + "โขธโฃฟโฃฟโก‡", + "โ ˆโ ฟโ ฟโ ", +] + +# color stops +PINK = (255, 105, 180) +HOT_PINK = (255, 20, 147) +BLUE = (100, 149, 237) +ROYAL_BLUE = (65, 105, 225) +ORANGE = (255, 165, 0) +CYAN = (0, 255, 255) + + +def _lerp(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> tuple[int, int, int]: + return ( + int(a[0] + (b[0] - a[0]) * t), + int(a[1] + (b[1] - a[1]) * t), + int(a[2] + (b[2] - a[2]) * t), + ) + + +def _supports_truecolor() -> bool: + ct = os.environ.get("COLORTERM", "").lower() + if ct in ("truecolor", "24bit"): + return True + term = os.environ.get("TERM_PROGRAM", "").lower() + # terminals known to support truecolor + if term in ("iterm.app", "hyper", "wezterm", "vscode", "ghostty", "alacritty"): + return True + return False + + +_TRUECOLOR = _supports_truecolor() + + +def _fg(r: int, g: int, b: int) -> str: + if _TRUECOLOR: + return f"\x1b[38;2;{r};{g};{b}m" + # fallback: use closest ANSI 256 color + return f"\x1b[38;5;{_rgb_to_256(r, g, b)}m" + + +def _rgb_to_256(r: int, g: int, b: int) -> int: + """Map RGB to nearest xterm-256 color cube index.""" + # check if close to grayscale + if r == g == b: + if r < 8: + return 16 + if r > 248: + return 231 + return round((r - 8) / 247 * 24) + 232 + # map to 6x6x6 color cube + ri = round(r / 255 * 5) + gi = round(g / 255 * 5) + bi = round(b / 255 * 5) + return 16 + 36 * ri + 6 * gi + bi + + +RESET = "\x1b[0m" +BOLD = "\x1b[1m" +DIM = "\x1b[2m" + + +def _terminal_width() -> int: + try: + import shutil + return shutil.get_terminal_size().columns + except Exception: + return 80 + + +def _center(line: str, width: int) -> str: + # braille chars are all width 1 visually + visible = len(line) + pad = max(0, (width - visible) // 2) + return " " * pad + line + + +def _color_top_row(line: str, phase: float = 0.0) -> str: + out = "" + stroke_positions = [i for i, ch in enumerate(line) if ch != BRAILLE_BLANK and ch != " "] + total = len(stroke_positions) + for i, ch in enumerate(line): + if ch == BRAILLE_BLANK or ch == " ": + out += RESET + ch + else: + idx = stroke_positions.index(i) + # mix pink and blue based on position + phase offset + t = (idx / max(total - 1, 1) + phase) % 1.0 + # use sine to blend so both colors are always present + blend = (math.sin(t * math.pi * 3) + 1.0) / 2.0 + c = _lerp(HOT_PINK, ROYAL_BLUE, blend) + out += _fg(*c) + ch + return out + RESET + + +def render_banner(*, fade_in: bool = True) -> None: + lines = TRUFFLE_BANNER_BRAILLE + label = f"{_fg(*ROYAL_BLUE)}{BOLD}Truffile{RESET} {DIM}โ€” Truffle SDK{RESET}" + banner_width = len(lines[0]) + # put label to the right of the middle row + label_row = len(lines) // 2 + + for i, line in enumerate(lines): + if i <= 1: + row = _color_top_row(line) + else: + row = line + if i == label_row: + row += " " + label + print(row) + if fade_in: + time.sleep(0.04) + + print() + + +BRAILLE_BLANK = "โ €" + + +def _color_strokes(line: str, color: tuple[int, int, int]) -> str: + out = "" + c = _fg(*color) + for ch in line: + if ch == BRAILLE_BLANK or ch == " ": + out += RESET + ch + else: + out += c + ch + return out + RESET + + +def render_small(*, color: tuple[int, int, int] = PINK) -> str: + result = "" + for line in TRUFFLE_SMALL: + result += _color_strokes(line, color) + "\n" + return result + + +class GlowBanner: + """Renders banner then keeps top rows glowing in a background thread.""" + + def __init__(self): + self._running = False + self._thread: threading.Thread | None = None + self._row0: int = 0 # absolute terminal row of banner line 0 + + @staticmethod + def _query_cursor_row() -> int | None: + """Query current cursor row via DSR escape sequence.""" + import select + try: + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) if termios else None + if tty: + tty.setcbreak(fd) + sys.stdout.write("\x1b[6n") + sys.stdout.flush() + ready, _, _ = select.select([fd], [], [], 0.1) + if not ready: + if old and termios: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + return None + resp = b"" + while True: + ready, _, _ = select.select([fd], [], [], 0.05) + if not ready: + break + ch = os.read(fd, 1) + resp += ch + if ch == b"R": + break + if old and termios: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + # response: \x1b[row;colR + decoded = resp.decode("ascii", errors="ignore") + if "[" in decoded and ";" in decoded: + row_str = decoded.split("[")[1].split(";")[0] + return int(row_str) + except Exception: + pass + return None + + def start(self) -> None: + """Print the banner and start the glow loop.""" + lines = TRUFFLE_BANNER_BRAILLE + label = f"{_fg(*ROYAL_BLUE)}{BOLD}Truffile{RESET} {DIM}โ€” Truffle SDK{RESET}" + label_row = len(lines) // 2 + + # query cursor row before printing to know where banner starts + row_before = self._query_cursor_row() if sys.stdout.isatty() else None + + for i, line in enumerate(lines): + if i <= 1: + row = _color_top_row(line) + else: + row = line + if i == label_row: + row += " " + label + print(row) + print() + + if not sys.stdout.isatty() or row_before is None: + return + + self._row0 = row_before # absolute row where line 0 of banner is + self._running = True + self._thread = threading.Thread(target=self._loop, daemon=True) + self._thread.start() + + def _loop(self) -> None: + lines = TRUFFLE_BANNER_BRAILLE + start = time.monotonic() + while self._running: + t = time.monotonic() - start + phase = t * 0.5 + # use absolute positioning to the exact rows + sys.stdout.write("\x1b7") + for i in range(2): + sys.stdout.write(f"\x1b[{self._row0 + i};1H\x1b[K{_color_top_row(lines[i], phase)}") + sys.stdout.write("\x1b8") + sys.stdout.flush() + time.sleep(0.06) + + def stop(self) -> None: + self._running = False + if self._thread: + self._thread.join(timeout=0.3) + self._thread = None + + +def render_glow_banner(*, duration: float = 1.5) -> None: + """Brief glow animation that settles into the static banner with label.""" + lines = TRUFFLE_BANNER_BRAILLE + height = len(lines) + label = f"{_fg(*ROYAL_BLUE)}{BOLD}Truffile{RESET} {DIM}โ€” Truffle SDK{RESET}" + label_row = len(lines) // 2 + start = time.monotonic() + + # print blank lines to make space + for _ in range(height): + sys.stdout.write("\n") + + try: + while time.monotonic() - start < duration: + elapsed = time.monotonic() - start + phase = elapsed * 0.8 + + sys.stdout.write(f"\x1b[{height}A") + for i, line in enumerate(lines): + if i <= 1: + row = _color_top_row(line, phase) + else: + row = line + if i == label_row: + row += " " + label + sys.stdout.write(f"\x1b[K{row}\n") + sys.stdout.flush() + time.sleep(0.04) + except KeyboardInterrupt: + pass + + # settle on final static frame + sys.stdout.write(f"\x1b[{height}A") + for i, line in enumerate(lines): + if i <= 1: + row = _color_top_row(line) + else: + row = line + if i == label_row: + row += " " + label + sys.stdout.write(f"\x1b[K{row}\n") + sys.stdout.flush() + print() + + +def render_glow_demo(*, duration: float = 8.0) -> None: + lines = TRUFFLE_BANNER_BRAILLE + height = len(lines) + start = time.monotonic() + + sys.stdout.write("\n") + for _ in range(height): + sys.stdout.write("\n") + + try: + while time.monotonic() - start < duration: + elapsed = time.monotonic() - start + phase = elapsed * 0.5 + + sys.stdout.write(f"\x1b[{height}A") + for i, line in enumerate(lines): + if i <= 1: + sys.stdout.write(f"\x1b[K{_color_top_row(line, phase)}\n") + else: + sys.stdout.write(f"\x1b[K{line}\n") + sys.stdout.flush() + time.sleep(0.05) + except KeyboardInterrupt: + pass + + sys.stdout.write(f"\x1b[{height}A") + for _ in range(height): + sys.stdout.write("\x1b[K\n") + sys.stdout.write(f"\x1b[{height}A") + sys.stdout.flush() + + +class ParticleOrb: + """particles bouncing inside an invisible circle. pink/blue when active, orange when done.""" + + STATE_ACTIVE = "active" + STATE_DONE = "done" + STATE_IDLE = "idle" + + # invisible container: 8 dot-wide x 12 dot-tall circle = 4x3 chars + DOT_W = 8 + DOT_H = 12 + CHAR_W = DOT_W // 2 + CHAR_H = DOT_H // 4 + CX = DOT_W / 2 - 0.5 + CY = DOT_H / 2 - 0.5 + RADIUS = 3.5 + + BRAILLE_BASE = 0x2800 + DOT_MAP = [ + (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (0, 3, 0x40), + (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (1, 3, 0x80), + ] + + def __init__(self, num_particles: int = 5): + import random + self._state = self.STATE_IDLE + self._running = False + self._thread: threading.Thread | None = None + self._lock = threading.Lock() + self._rng = random.Random() + # each particle: [orbit_r, speed, phase, wobble_freq, wobble_amp, color_type] + # color_type: 0=blue, 1=pink + self._particles: list[list[float]] = [] + self._orange_particles: list[list[float]] = [] + for i in range(num_particles): + orbit_r = self._rng.uniform(1.0, self.RADIUS * 0.85) + orbit_speed = self._rng.uniform(0.8, 2.2) * (1 if i % 2 == 0 else -1) + phase = self._rng.uniform(0, math.pi * 2) + wobble_freq = self._rng.uniform(1.5, 4.0) + wobble_amp = self._rng.uniform(0.3, 1.0) + self._particles.append([orbit_r, orbit_speed, phase, wobble_freq, wobble_amp, float(i % 2)]) + + def set_state(self, state: str) -> None: + with self._lock: + old = self._state + self._state = state + # when switching to done, spawn orange particles among existing ones + if state == self.STATE_DONE and old != self.STATE_DONE: + self._orange_particles = [] + for _ in range(3): + orbit_r = self._rng.uniform(0.8, self.RADIUS * 0.8) + orbit_speed = self._rng.uniform(1.0, 2.5) * self._rng.choice([1, -1]) + phase = self._rng.uniform(0, math.pi * 2) + wobble_freq = self._rng.uniform(2.0, 4.0) + wobble_amp = self._rng.uniform(0.3, 0.8) + self._orange_particles.append([orbit_r, orbit_speed, phase, wobble_freq, wobble_amp, 2.0]) + + def _get_positions(self, t: float) -> list[tuple[float, float, float]]: + positions = [] + # pink and blue always present + for p in self._particles: + orbit_r, speed, phase, wf, wa, color_type = p + angle = t * speed + phase + wobble = math.sin(t * wf + phase) * wa + r = orbit_r + wobble + r = max(0.3, min(r, self.RADIUS - 0.2)) + x = self.CX + math.cos(angle) * r + y = self.CY + math.sin(angle) * r * 0.6 + positions.append((x, y, color_type)) + # orange joins when done + with self._lock: + if self._state == self.STATE_DONE: + for p in self._orange_particles: + orbit_r, speed, phase, wf, wa, color_type = p + angle = t * speed + phase + wobble = math.sin(t * wf + phase) * wa + r = orbit_r + wobble + r = max(0.3, min(r, self.RADIUS - 0.2)) + x = self.CX + math.cos(angle) * r + y = self.CY + math.sin(angle) * r * 0.6 + positions.append((x, y, color_type)) + return positions + + def _render(self, t: float) -> list[str]: + with self._lock: + state = self._state + + positions = self._get_positions(t) + grid: dict[tuple[int, int], tuple[int, int, int]] = {} + for x, y, color_type in positions: + px, py = int(round(x)), int(round(y)) + if 0 <= px < self.DOT_W and 0 <= py < self.DOT_H: + if color_type > 1.5: + grid[(px, py)] = ORANGE + elif color_type > 0.5: + grid[(px, py)] = HOT_PINK + else: + grid[(px, py)] = ROYAL_BLUE + + lines = [] + for cy in range(self.CHAR_H): + out = "" + for cx in range(self.CHAR_W): + code = self.BRAILLE_BASE + char_color = None + for dx, dy, bit in self.DOT_MAP: + dpx = cx * 2 + dx + dpy = cy * 4 + dy + if (dpx, dpy) in grid: + code |= bit + char_color = grid[(dpx, dpy)] + if char_color and code != self.BRAILLE_BASE: + out += _fg(*char_color) + chr(code) + RESET + else: + out += BRAILLE_BLANK + lines.append(out) + return lines + + def _draw(self, t: float) -> None: + try: + import shutil + term_h = shutil.get_terminal_size().lines + except Exception: + term_h = 24 + + rendered = self._render(t) + sys.stdout.write("\x1b7") + for i, line in enumerate(rendered): + row = term_h - self.CHAR_H + i + sys.stdout.write(f"\x1b[{row};1H\x1b[K{line}") + sys.stdout.write("\x1b8") + sys.stdout.flush() + + def _loop(self) -> None: + start = time.monotonic() + while self._running: + t = time.monotonic() - start + self._draw(t) + time.sleep(0.06) + self._clear() + + def _clear(self) -> None: + try: + import shutil + term_h = shutil.get_terminal_size().lines + except Exception: + term_h = 24 + sys.stdout.write("\x1b7") + for i in range(self.CHAR_H): + row = term_h - self.CHAR_H + i + sys.stdout.write(f"\x1b[{row};1H\x1b[K") + sys.stdout.write("\x1b8") + sys.stdout.flush() + + def start(self, state: str = STATE_ACTIVE) -> None: + self._state = state + self._running = True + self._thread = threading.Thread(target=self._loop, daemon=True) + self._thread.start() + + def stop(self) -> None: + self._running = False + if self._thread: + self._thread.join(timeout=0.3) + self._thread = None + + +def render_orb_demo(*, duration: float = 6.0) -> None: + orb = ParticleOrb(num_particles=6) + orb.start(ParticleOrb.STATE_ACTIVE) + print("particles active (pink/blue)...") + time.sleep(duration * 0.6) + orb.set_state(ParticleOrb.STATE_DONE) + print("particles done (orange)...") + time.sleep(duration * 0.4) + orb.stop() + print("stopped") + + +if __name__ == "__main__": + import sys as _sys + arg = _sys.argv[1] if len(_sys.argv) > 1 else "banner" + + if arg == "banner": + render_banner() + elif arg == "small": + print(render_small()) + elif arg == "glow": + render_glow_demo(duration=8.0) + elif arg == "orb": + render_orb_demo(duration=6.0) + elif arg == "all": + render_banner() + print("glow demo (8s)...") + render_glow_demo(duration=8.0) + print("orb demo (6s)...") + render_orb_demo(duration=6.0) + print("done") diff --git a/build/lib/truffile/cli/chat.py b/build/lib/truffile/cli/chat.py new file mode 100644 index 0000000..a95de30 --- /dev/null +++ b/build/lib/truffile/cli/chat.py @@ -0,0 +1,561 @@ +from __future__ import annotations + +import asyncio +import shutil +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from truffile.storage import StorageService +from truffile.client import TruffleClient, resolve_mdns +from .ui import C, MUSHROOM, DOT, CHECK, CROSS, Spinner, ScrollingLog, StreamAbortWatcher, error, success, info, warn, create_thinking_orb +from .connect import _resolve_connected_device +from .picker import pick_from_list +from .commands import CHAT_COMMANDS, SlashCommand +from .prompt import TrufflePrompt +from .markdown import has_markdown, render_markdown, count_terminal_lines +from .art import ParticleOrb +from .welcome import show_chat_welcome + + +@dataclass +class TaskState: + task_id: str = "" + title: str = "" + run_state: str = "" + pending_node_id: int | None = None + result_text: str = "" + tool_calls: list[str] = field(default_factory=list) + thinking_summaries: list[str] = field(default_factory=list) + + +# accumulated streaming text for markdown re-render +_streaming_text: list[str] = [] + + +def _print_update(update: Any, state: TaskState) -> None: + if update.task_id and not state.task_id: + state.task_id = update.task_id + + if update.HasField("info"): + run_state = update.info.TaskRunState.Name(update.info.run_state) if update.info.run_state else "" + if run_state: + state.run_state = run_state + title = update.info.task_title + if title: + state.title = title + + if update.HasField("error"): + msg = update.error.message if hasattr(update.error, "message") else str(update.error) + print(f"\n{C.RED}error: {msg}{C.RESET}") + + # render thinking/tools BEFORE streaming content so they appear above the response + for node in update.nodes: + if not node.HasField("step"): + continue + + step = node.step + + if step.HasField("thinking"): + for s in step.thinking.cot_summaries: + state.thinking_summaries.append(s) + + for tc in step.tool_calls: + name = tc.tool_name if hasattr(tc, "tool_name") else "" + tc_summary = tc.summary if hasattr(tc, "summary") else "" + if name: + print(f"{C.CYAN}{DOT} tool: {name}{C.RESET}", end="") + if tc_summary: + print(f" {C.DIM}โ€” {tc_summary}{C.RESET}") + else: + print() + state.tool_calls.append(name) + + if step.HasField("results"): + content = step.results.content if hasattr(step.results, "content") else "" + res_summary = step.results.summary if hasattr(step.results, "summary") else "" + text = content or res_summary + if text: + state.result_text = text + + if step.HasField("user_response"): + node_id = step.user_response.node_id if hasattr(step.user_response, "node_id") else 0 + if node_id: + state.pending_node_id = node_id + + # stream content after thinking/tools are rendered + if update.HasField("streaming_step_result"): + chunk = update.streaming_step_result.partial_content + if chunk: + sys.stdout.write(chunk) + sys.stdout.flush() + _streaming_text.append(chunk) + + +async def _stream_task(client: TruffleClient, stream: Any, state: TaskState, orb: ParticleOrb | None = None) -> None: + global _streaming_text + _streaming_text = [] + interrupted = False + orb_stopped = False + + if orb: + orb.start(ParticleOrb.STATE_ACTIVE) + + with StreamAbortWatcher() as abort: + try: + async for update in stream: + if abort.aborted(): + interrupted = True + break + # stop orb only when actual visible content arrives + if orb and not orb_stopped: + has_content = ( + update.HasField("streaming_step_result") + and update.streaming_step_result.partial_content + ) + if has_content: + orb.stop() + orb_stopped = True + _print_update(update, state) + if state.pending_node_id is not None: + break + except (asyncio.CancelledError, KeyboardInterrupt): + interrupted = True + except Exception: + interrupted = True + + if orb and not orb_stopped: + orb.stop() + + if interrupted and state.task_id: + print(f"\n{C.DIM}interrupting...{C.RESET}") + try: + await client.interrupt_task(state.task_id) + except Exception: + pass + + +async def _pick_task(client: TruffleClient, *, current_task_id: str = "") -> str | None: + tasks = await client.get_task_infos(max_before=15) + if not tasks: + info("no previous tasks found") + return None + + items = [ + { + "label": t["title"], + "detail": t["updated"][:16] if t["updated"] else "", + "task_id": t["task_id"], + } + for t in tasks + ] + + print() + picked = await pick_from_list( + items, + label_key="label", + detail_key="detail", + active_key="task_id" if current_task_id else None, + active_value=current_task_id, + prompt="pick a task", + ) + return picked["task_id"] if picked else None + + +async def _get_apps_list(client: TruffleClient) -> list[dict]: + apps = await client.get_all_apps() + result = [] + for app in apps: + meta = app.metadata + name = meta.name if hasattr(meta, "name") else "?" + bundle_id = meta.bundle_id if hasattr(meta, "bundle_id") else "" + result.append({"name": name, "bundle_id": bundle_id, "uuid": app.uuid}) + return result + + +def _find_app_by_name(apps: list, name: str) -> dict | None: + name_lower = name.strip().lower() + for app in apps: + app_name = app.get("name", "").lower() + if app_name == name_lower or name_lower in app_name: + return app + return None + + +async def _handle_slash( + cmd: str, + client: TruffleClient, + state: TaskState, + storage: StorageService, + *, + app_commands: dict[str, dict] | None = None, +) -> str | None: + """returns action string or None. 'exit' to quit, 'new' for fresh task, 'switch:' to resume.""" + + parts = cmd.strip().split(maxsplit=1) + command = parts[0].lower() + arg = parts[1].strip() if len(parts) > 1 else "" + + # check if this is a / shortcut + if app_commands and command in app_commands: + app = app_commands[command] + if not state.task_id: + error("no active task โ€” send a message first") + return None + try: + await client.set_task_apps(state.task_id, [app["uuid"]]) + success(f"added {app['name']} to task") + except Exception as e: + error(f"failed: {e}") + return None + + if command == "/help": + print(f"\n{C.BOLD}commands:{C.RESET}") + for sc in CHAT_COMMANDS: + display = f"{sc.name} {sc.arg_hint}" if sc.arg_hint else sc.name + print(f" {C.CYAN}{display}{C.RESET} โ€” {sc.description}") + print(f"\n{C.DIM}alt+enter for newline ยท tab to complete commands ยท ctrl+d to exit{C.RESET}\n") + return None + + if command in ("/exit", "/quit"): + return "exit" + + if command == "/title": + print(f" {state.title or 'no title yet'}") + return None + + if command == "/rename": + if not arg: + error("usage: /rename ") + return None + if not state.task_id: + error("no active task") + return None + try: + await client.rename_task(state.task_id, arg) + state.title = arg + success(f"renamed to \"{arg}\"") + except Exception as e: + error(f"rename failed: {e}") + return None + + if command in ("/tasks", "/resume", "/switch"): + task_id = await _pick_task(client, current_task_id=state.task_id) + return f"switch:{task_id}" if task_id else None + + if command == "/new": + return "new" + + if command == "/apps": + try: + apps_list = await _get_apps_list(client) + if not apps_list: + info("no apps installed") + return None + print(f"\n{C.BOLD}installed apps:{C.RESET}") + for a in apps_list: + slug = a["name"].lower().replace(" ", "-") + print(f" {C.CYAN}/{slug}{C.RESET} {a['name']} {C.DIM}({a['bundle_id']}){C.RESET}") + print() + except Exception as e: + error(f"failed to list apps: {e}") + return None + + if command == "/delete": + if not arg: + error("usage: /delete app [name] or /delete task") + return None + + if arg.lower().strip() == "task" or arg.lower().startswith("task "): + task_to_delete = await _pick_task(client, current_task_id=state.task_id) + if not task_to_delete: + return None + try: + await client.delete_task(task_to_delete) + success("task deleted") + if task_to_delete == state.task_id: + return "new" + except Exception as e: + error(f"failed: {e}") + return None + + if arg.lower().strip() == "app" or arg.lower().startswith("app "): + app_name = arg[4:].strip() if len(arg) > 4 else "" + try: + apps_list = await _get_apps_list(client) + if not apps_list: + info("no apps installed") + return None + + if not app_name: + items = [{"label": a["name"], "detail": a["bundle_id"], "uuid": a["uuid"]} for a in apps_list] + print() + picked = await pick_from_list(items, label_key="label", detail_key="detail", prompt="pick app to delete") + if not picked: + return None + match = {"name": picked["label"], "uuid": picked["uuid"]} + else: + match = _find_app_by_name(apps_list, app_name) + if not match: + error(f"app \"{app_name}\" not found") + return None + + await client.delete_app(match["uuid"]) + success(f"deleted {match['name']}") + except Exception as e: + error(f"failed: {e}") + return None + + error("usage: /delete app [name] or /delete task") + return None + + if command == "/deploy": + if not arg: + error("usage: /deploy ") + return None + app_dir = Path(arg).resolve() + if not app_dir.exists(): + error(f"path not found: {arg}") + return None + try: + from truffile.schema import validate_app_dir + from truffile.deploy import build_deploy_plan, deploy_with_builder + from .deploy import _interactive_shell + + valid, config, app_type, warnings, errors_list = validate_app_dir(app_dir) + if not valid: + for msg in errors_list: + error(msg) + return None + + result = await deploy_with_builder( + client=client, + config=config, + app_dir=app_dir, + app_type=app_type, + device=storage.state.last_used_device or "device", + interactive=False, + spinner_cls=Spinner, + scrolling_log_cls=ScrollingLog, + info=info, + success=success, + error=error, + color_dim=C.DIM, + color_reset=C.RESET, + color_bold=C.BOLD, + arrow="โ†’", + interactive_shell=_interactive_shell, + ) + if result == 0: + success("deploy complete") + except Exception as e: + error(f"deploy failed: {e}") + return None + + if command == "/devices": + devices = storage.list_devices() + if not devices: + info("no devices connected") + return None + current = storage.state.last_used_device + print() + for d in devices: + marker = f" {C.GREEN}(active){C.RESET}" if d == current else "" + print(f" {C.CYAN}{DOT}{C.RESET} {d}{marker}") + print() + return None + + if command == "/create": + from .create import cmd_create + from types import SimpleNamespace + create_args = SimpleNamespace(name=arg or None, path=None) + cmd_create(create_args) + return None + + error(f"unknown command: {command}. type /help") + return None + + +def _maybe_render_markdown(text: str) -> None: + """If text has markdown, re-render with rich.""" + if not text or not has_markdown(text): + return + try: + width = shutil.get_terminal_size().columns + lines = count_terminal_lines(text, width) + render_markdown(text, lines) + except Exception: + pass + + +async def cmd_chat(args, storage: StorageService) -> int: + result = await _resolve_connected_device(storage) + device, ip = result + if not device or not ip: + return 1 + + token = storage.get_token(device) + if not token: + error(f"no token for {device}") + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + try: + await client.connect() + await client.check_auth() + spinner.stop(success=True) + except Exception as e: + spinner.fail(f"Could not connect to {device}") + error(str(e)) + return 1 + + resume = getattr(args, "resume", False) + state = TaskState() + stream = None + + # fetch installed apps and register as / slash commands + app_commands: dict[str, dict] = {} + app_slugs: list[str] = [] + try: + apps_list = await _get_apps_list(client) + for a in apps_list: + slug = a["name"].lower().replace(" ", "-") + app_commands[f"/{slug}"] = a + app_slugs.append(slug) + except Exception: + pass + + # welcome panel + show_chat_welcome(device=device, apps=app_slugs or None) + + if resume: + task_id = await _pick_task(client) + if task_id: + state.task_id = task_id + stream = client.open_existing_task_stream(task_id) + try: + async for update in stream: + _print_update(update, state) + if state.run_state: + break + except Exception: + pass + info(f"resumed \"{state.title or 'task'}\"") + + prompt = TrufflePrompt("you> ", CHAT_COMMANDS) + prompt.task_name = state.title + if app_commands: + prompt.add_commands([ + SlashCommand(cmd_name, f"add {a['name']} to task") + for cmd_name, a in app_commands.items() + ]) + + try: + while True: + user_input = await prompt.get_input() + if user_input is None: + print() + break + if not user_input.strip(): + continue + + if user_input.strip().startswith("/"): + action = await _handle_slash(user_input.strip(), client, state, storage, app_commands=app_commands) + if action == "exit": + break + if action == "new": + state = TaskState() + stream = None + prompt.task_name = "" + info("starting new conversation") + continue + if action and action.startswith("switch:"): + new_task_id = action.split(":", 1)[1] + state = TaskState() + state.task_id = new_task_id + stream = client.open_existing_task_stream(new_task_id) + try: + async for update in stream: + _print_update(update, state) + if state.run_state: + break + except Exception: + pass + prompt.task_name = state.title + info(f"switched to \"{state.title or 'task'}\"") + print() + continue + + print() + + orb = create_thinking_orb() + + if state.pending_node_id is not None: + await client.respond_to_task(state.task_id, state.pending_node_id, user_input.strip()) + state.pending_node_id = None + if stream: + await _stream_task(client, stream, state, orb=orb) + elif not state.task_id: + stream = client.open_task_stream(user_input.strip()) + await _stream_task(client, stream, state, orb=orb) + else: + state = TaskState() + stream = client.open_task_stream(user_input.strip()) + await _stream_task(client, stream, state, orb=orb) + + # update task name for prompt border + prompt.task_name = state.title + + streamed = "".join(_streaming_text) + + if streamed and state.thinking_summaries: + # clear raw streamed text, reprint with thinking above + try: + width = shutil.get_terminal_size().columns + except Exception: + width = 80 + from .markdown import count_terminal_lines + nlines = count_terminal_lines(streamed, width) + sys.stdout.write(f"\r\033[{nlines}A") + for _ in range(nlines + 1): + sys.stdout.write("\033[K\n") + sys.stdout.write(f"\033[{nlines + 1}A") + sys.stdout.flush() + # thinking header + combined = " ".join(state.thinking_summaries) + print(f"{C.GRAY}thinking: {combined}{C.RESET}") + # reprint response (was cleared above) + print(streamed) + _maybe_render_markdown(streamed) + elif streamed: + # content already printed during streaming, just finish the line + print() + _maybe_render_markdown(streamed) + elif state.thinking_summaries: + # thinking but no streamed content + combined = " ".join(state.thinking_summaries) + print(f"{C.GRAY}thinking: {combined}{C.RESET}") + if state.result_text: + print(state.result_text) + elif state.result_text: + # non-streaming result + print(state.result_text) + print() + + except KeyboardInterrupt: + if state.task_id: + try: + await client.interrupt_task(state.task_id) + except Exception: + pass + finally: + await client.close() + print(f"\n{MUSHROOM} goodbye!") + + return 0 diff --git a/build/lib/truffile/cli/commands.py b/build/lib/truffile/cli/commands.py new file mode 100644 index 0000000..517acb7 --- /dev/null +++ b/build/lib/truffile/cli/commands.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class SlashCommand: + name: str + description: str + has_arg: bool = False + arg_hint: str = "" + + +CHAT_COMMANDS: list[SlashCommand] = [ + SlashCommand("/help", "show available commands"), + SlashCommand("/tasks", "list recent tasks"), + SlashCommand("/rename", "rename current task", has_arg=True, arg_hint=""), + SlashCommand("/title", "show current task title"), + SlashCommand("/resume", "switch to a previous task"), + SlashCommand("/new", "start a new task"), + SlashCommand("/apps", "list installed apps"), + SlashCommand("/deploy", "deploy an app to device", has_arg=True, arg_hint=""), + SlashCommand("/delete", "delete app or task", has_arg=True, arg_hint="app|task"), + SlashCommand("/create", "scaffold a new app", has_arg=True, arg_hint=""), + SlashCommand("/devices", "list connected devices"), + SlashCommand("/exit", "exit chat"), +] + + +INFER_COMMANDS: list[SlashCommand] = [ + SlashCommand("/help", "show available commands"), + SlashCommand("/history", "show conversation history"), + SlashCommand("/reset", "clear conversation history"), + SlashCommand("/models", "switch inference model"), + SlashCommand("/config", "show current config"), + SlashCommand("/reasoning", "toggle reasoning", has_arg=True, arg_hint="on|off"), + SlashCommand("/stream", "toggle streaming", has_arg=True, arg_hint="on|off"), + SlashCommand("/json", "toggle JSON mode", has_arg=True, arg_hint="on|off"), + SlashCommand("/tools", "toggle tool use", has_arg=True, arg_hint="on|off"), + SlashCommand("/max_tokens", "set max response tokens", has_arg=True, arg_hint=""), + SlashCommand("/temperature", "set temperature", has_arg=True, arg_hint=""), + SlashCommand("/top_p", "set top_p", has_arg=True, arg_hint=""), + SlashCommand("/max_rounds", "set max tool rounds", has_arg=True, arg_hint=""), + SlashCommand("/system", "set system prompt", has_arg=True, arg_hint=""), + SlashCommand("/mcp", "manage MCP connections", has_arg=True, arg_hint="connect|disconnect|status|tools"), + SlashCommand("/attach", "attach an image", has_arg=True, arg_hint=""), + SlashCommand("/create", "scaffold a new app", has_arg=True, arg_hint=""), + SlashCommand("/exit", "exit"), +] diff --git a/build/lib/truffile/cli/connect.py b/build/lib/truffile/cli/connect.py new file mode 100644 index 0000000..bb169ed --- /dev/null +++ b/build/lib/truffile/cli/connect.py @@ -0,0 +1,246 @@ +import asyncio + +from truffile.storage import StorageService +from truffile.client import TruffleClient, resolve_mdns, NewSessionStatus + +from .ui import C, DOT, Spinner, error, success, info + + +async def cmd_connect(args, storage: StorageService) -> int: + device_name = args.device + + spinner = Spinner(f"Resolving {device_name}.local") + spinner.start() + + hostname = f"{device_name}.local" + try: + ip = await resolve_mdns(hostname) + spinner.stop(success=True) + except RuntimeError: + spinner.fail(f"Could not resolve {device_name}.local") + print() + print(f" {C.DIM}Try running:{C.RESET}") + print(f" {C.CYAN}ping {device_name}.local{C.RESET}") + print() + print(f" {C.DIM}If ping fails, check:{C.RESET}") + print(f" {C.DIM}{DOT} Device is powered on and connected to WiFi{C.RESET}") + print(f" {C.DIM}{DOT} Your computer is on the same network{C.RESET}") + print(f" {C.DIM}{DOT} mDNS is working{C.RESET}") + print() + return 1 + + address = f"{ip}:80" + existing_token = storage.get_token(device_name) + + if existing_token: + spinner = Spinner("Validating existing token") + spinner.start() + client = TruffleClient(address, existing_token) + try: + await client.connect() + if await client.check_auth(): + spinner.stop(success=True) + storage.set_last_used(device_name) + success(f"Already connected to {C.BOLD}{device_name}{C.RESET}") + await client.close() + return 0 + spinner.fail("Token invalid, re-authenticating") + except Exception: + spinner.fail("Token validation failed") + finally: + await client.close() + + print() + print(f" {C.DIM}Make sure you have:{C.RESET}") + print(f" {C.DIM}{DOT} Onboarded with the Truffle app{C.RESET}") + print(f" {C.DIM}{DOT} Your User ID from the recovery codes{C.RESET}") + print() + + try: + user_id = input(f"{C.CYAN}?{C.RESET} Enter your User ID: ").strip() + except (KeyboardInterrupt, EOFError): + print() + raise KeyboardInterrupt() + if not user_id: + error("User ID is required") + return 1 + + spinner = Spinner("Connecting to device") + spinner.start() + + client = TruffleClient(address, token="") + try: + await client.connect() + spinner.stop(success=True) + except Exception as e: + spinner.fail(f"Failed to connect: {e}") + return 1 + + print() + info("Requesting authorization...") + print(f" {C.DIM}Please approve on your Truffle device{C.RESET}") + + spinner = Spinner("Waiting for approval") + spinner.start() + + try: + status, token = await client.register_new_session(user_id) + except Exception as e: + spinner.fail(f"Failed to register: {e}") + await client.close() + return 1 + + await client.close() + + if status.error == NewSessionStatus.NEW_SESSION_SUCCESS and token: + spinner.stop(success=True) + storage.set_token(device_name, token) + storage.set_last_used(device_name) + print() + success(f"Connected to {C.BOLD}{device_name}{C.RESET}") + return 0 + elif status.error == NewSessionStatus.NEW_SESSION_TIMEOUT: + spinner.fail("Approval timed out") + return 1 + elif status.error == NewSessionStatus.NEW_SESSION_REJECTED: + spinner.fail("Request was rejected") + return 1 + else: + spinner.fail(f"Authentication failed: {status.error}") + return 1 + + +def cmd_disconnect(args, storage: StorageService) -> int: + target = getattr(args, "device", "all") + if target == "all": + storage.clear_all() + success("All device credentials cleared") + else: + if storage.remove_device(target): + success(f"Disconnected from {C.BOLD}{target}{C.RESET}") + else: + error(f"No credentials found for {target}") + return 0 + + +async def cmd_scan(args, storage: StorageService) -> int: + try: + from zeroconf import ServiceBrowser, ServiceListener, Zeroconf, IPVersion + except ImportError: + error("zeroconf package required for scanning") + print(f" {C.DIM}pip install zeroconf{C.RESET}") + return 1 + + devices: dict[str, dict] = {} + scan_done = asyncio.Event() + + class TruffleListener(ServiceListener): + def add_service(self, zc: Zeroconf, type_: str, name: str): + if name.lower().startswith("truffle-"): + info = zc.get_service_info(type_, name) + device_name = name.split(".")[0] + if info and device_name not in devices: + addresses = [addr for addr in info.parsed_addresses(IPVersion.V4Only)] + devices[device_name] = { + "name": device_name, + "addresses": addresses, + "port": info.port, + } + + def remove_service(self, zc: Zeroconf, type_: str, name: str): + pass + + def update_service(self, zc: Zeroconf, type_: str, name: str): + pass + + timeout = args.timeout if hasattr(args, 'timeout') else 5 + + spinner = Spinner(f"Scanning for Truffle devices ({timeout}s)") + spinner.start() + + try: + zc = Zeroconf(ip_version=IPVersion.V4Only) + listener = TruffleListener() + + browsers = [ + ServiceBrowser(zc, "_truffle._tcp.local.", listener), + ] + + await asyncio.sleep(timeout) + + for browser in browsers: + browser.cancel() + zc.close() + + except Exception as e: + spinner.fail(f"Scan failed: {e}") + return 1 + + spinner.stop(success=True) + + if not devices: + print() + print(f" {C.DIM}No Truffle devices found on the network{C.RESET}") + print() + print(f" {C.DIM}Make sure your Truffle is:{C.RESET}") + print(f" {C.DIM}โ€ข Powered on{C.RESET}") + print(f" {C.DIM}โ€ข Connected to the same network as this computer{C.RESET}") + print() + return 1 + + print() + print(f"{C.BOLD}Found {len(devices)} Truffle device(s):{C.RESET}") + print() + + device_list = list(devices.values()) + for i, device in enumerate(device_list, 1): + name = device["name"] + addrs = ", ".join(device["addresses"]) if device["addresses"] else "unknown" + + already_connected = storage.get_token(name) is not None + if already_connected: + print(f" {C.GREEN}{i}.{C.RESET} {C.BOLD}{name}{C.RESET} {C.DIM}({addrs}){C.RESET} {C.GREEN}[connected]{C.RESET}") + else: + print(f" {C.CYAN}{i}.{C.RESET} {C.BOLD}{name}{C.RESET} {C.DIM}({addrs}){C.RESET}") + + print() + + try: + choice = input(f"Select device to connect (1-{len(device_list)}) or press Enter to cancel: ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + + if not choice: + return 0 + + try: + idx = int(choice) - 1 + if 0 <= idx < len(device_list): + selected = device_list[idx] + print() + + class FakeArgs: + device = selected["name"] + + return await cmd_connect(FakeArgs(), storage) + else: + error("Invalid selection") + return 1 + except ValueError: + error("Invalid input") + return 1 + + +async def _resolve_connected_device(storage: StorageService) -> tuple[str, str] | tuple[None, None]: + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return None, None + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError: + error(f"Could not resolve {device}.local") + return None, None + return device, ip diff --git a/build/lib/truffile/cli/create.py b/build/lib/truffile/cli/create.py new file mode 100644 index 0000000..682e079 --- /dev/null +++ b/build/lib/truffile/cli/create.py @@ -0,0 +1,170 @@ +import json +import re +from importlib import resources as importlib_resources +from pathlib import Path + +from .ui import C, ARROW, SCAFFOLD_ICON_RESOURCE_REL, error, success + + +def _safe_app_slug(app_name: str) -> str: + slug = re.sub(r"[^a-z0-9]+", "_", app_name.lower()).strip("_") + if not slug: + return "my_app" + if slug[0].isdigit(): + return f"app_{slug}" + return slug + + +def _sample_truffile_yaml(app_name: str, slug: str) -> str: + quoted_name = json.dumps(app_name) + return ( + "metadata:\n" + f" name: {quoted_name}\n" + f" bundle_id: org.truffle.{slug.replace('_', '.')}\n" + " description: |\n" + " Describe what this app does.\n" + " icon_file: ./icon.png\n" + " foreground:\n" + " process:\n" + " cmd:\n" + " - python\n" + f" - {slug}_foreground.py\n" + " working_directory: /\n" + " environment:\n" + ' PYTHONUNBUFFERED: "1"\n' + " background:\n" + " process:\n" + " cmd:\n" + " - python\n" + f" - {slug}_background.py\n" + " working_directory: /\n" + " environment:\n" + ' PYTHONUNBUFFERED: "1"\n' + " default_schedule:\n" + " type: interval\n" + " interval:\n" + " duration: 30m\n" + " schedule:\n" + ' daily_window: "00:00-23:59"\n' + "\n" + "steps:\n" + " - name: Copy application files\n" + " type: files\n" + " files:\n" + f" - source: ./{slug}_foreground.py\n" + f" destination: ./{slug}_foreground.py\n" + f" - source: ./{slug}_background.py\n" + f" destination: ./{slug}_background.py\n" + ) + + +def _sample_foreground_py() -> str: + return ( + '"""Foreground app entrypoint (MCP-facing surface)."""\n' + "\n" + "def main() -> None:\n" + ' print(\"TODO: implement foreground MCP tool server\")\n' + "\n" + "\n" + "if __name__ == \"__main__\":\n" + " main()\n" + ) + + +def _sample_background_py() -> str: + return ( + '"""Background app entrypoint (scheduled context emitter)."""\n' + "\n" + "def main() -> None:\n" + ' print(\"TODO: implement background scheduled job\")\n' + "\n" + "\n" + "if __name__ == \"__main__\":\n" + " main()\n" + ) + + +def _load_stock_icon_bytes() -> tuple[bytes | None, str]: + try: + resource_file = importlib_resources.files("truffile").joinpath(str(SCAFFOLD_ICON_RESOURCE_REL)) + icon_bytes = resource_file.read_bytes() + return icon_bytes, f"truffile/{SCAFFOLD_ICON_RESOURCE_REL.as_posix()}" + except Exception: + pass + + local_package_path = Path(__file__).resolve().parent.parent / SCAFFOLD_ICON_RESOURCE_REL + if local_package_path.exists() and local_package_path.is_file(): + return local_package_path.read_bytes(), str(local_package_path) + + legacy_docs_path = Path(__file__).resolve().parents[2] / "docs" / "Truffle.png" + if legacy_docs_path.exists() and legacy_docs_path.is_file(): + return legacy_docs_path.read_bytes(), str(legacy_docs_path) + + return None, f"truffile/{SCAFFOLD_ICON_RESOURCE_REL.as_posix()}" + + +def cmd_create(args) -> int: + app_name = (args.name or "").strip() + if not app_name: + try: + app_name = input(f"{C.CYAN}?{C.RESET} App name: ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + if not app_name: + error("App name is required") + return 1 + if "/" in app_name or "\\" in app_name: + error("App name cannot contain path separators") + return 1 + + base_dir: Path + if args.path: + base_dir = Path(args.path).expanduser().resolve() + else: + cwd = Path.cwd() + try: + raw = input(f"{C.CYAN}?{C.RESET} Base path [{cwd}]: ").strip() + except (KeyboardInterrupt, EOFError): + print() + return 0 + base_dir = Path(raw).expanduser().resolve() if raw else cwd + + app_dir = base_dir / app_name + if app_dir.exists(): + error(f"Target directory already exists: {app_dir}") + return 1 + + slug = _safe_app_slug(app_name) + fg_file = f"{slug}_foreground.py" + bg_file = f"{slug}_background.py" + stock_icon_bytes, stock_icon_source = _load_stock_icon_bytes() + if stock_icon_bytes is None: + error(f"Stock icon not found: {stock_icon_source}") + return 1 + if len(stock_icon_bytes) == 0: + error(f"Stock icon is empty: {stock_icon_source}") + return 1 + + try: + app_dir.mkdir(parents=True, exist_ok=False) + (app_dir / "truffile.yaml").write_text(_sample_truffile_yaml(app_name, slug), encoding="utf-8") + (app_dir / fg_file).write_text(_sample_foreground_py(), encoding="utf-8") + (app_dir / bg_file).write_text(_sample_background_py(), encoding="utf-8") + (app_dir / "icon.png").write_bytes(stock_icon_bytes) + except Exception as exc: + error(f"Failed to scaffold app: {exc}") + return 1 + + # OSC 8 clickable path (supported in iTerm2, VSCode, WezTerm, etc.) + file_url = app_dir.as_uri() + link = f"\x1b]8;;{file_url}\x1b\\{app_dir}\x1b]8;;\x1b\\" + success(f"Created app scaffold: {link}") + print(f" {C.DIM}Files:{C.RESET}") + print(f" {C.DIM}{ARROW} truffile.yaml{C.RESET}") + print(f" {C.DIM}{ARROW} {fg_file}{C.RESET}") + print(f" {C.DIM}{ARROW} {bg_file}{C.RESET}") + print(f" {C.DIM}{ARROW} icon.png{C.RESET}") + print() + print(f" {C.DIM}Next:{C.RESET} truffile validate {app_dir}") + return 0 diff --git a/build/lib/truffile/cli/deploy.py b/build/lib/truffile/cli/deploy.py new file mode 100644 index 0000000..5224d8a --- /dev/null +++ b/build/lib/truffile/cli/deploy.py @@ -0,0 +1,255 @@ +import asyncio +import os +import signal +import sys +from pathlib import Path + +from truffile.storage import StorageService +from truffile.client import TruffleClient, resolve_mdns +from truffile.schema import validate_app_dir +from truffile.deploy import build_deploy_plan, deploy_with_builder + +from .ui import C, ARROW, CROSS, DOT, Spinner, ScrollingLog, error, warn, info, success + + +async def cmd_deploy(args, storage: StorageService) -> int: + app_path = args.path if args.path else "." + app_dir = Path(app_path).resolve() + interactive = args.interactive + dry_run = bool(getattr(args, "dry_run", False)) + if not app_dir.exists() or not app_dir.is_dir(): + error(f"{app_dir} is not a valid directory") + return 1 + + info(f"Validating app in {app_dir.name}") + valid, config, app_type, warnings, errors = validate_app_dir(app_dir) + if not valid or not app_type: + for msg in errors: + error(msg) + return 1 + + for w in warnings: + warn(w) + + metadata = config.get("metadata", {}) if isinstance(config, dict) else {} + icon_file = metadata.get("icon_file") if isinstance(metadata, dict) else None + if not isinstance(icon_file, str) or not icon_file.strip(): + error("Deploy requires metadata.icon_file in truffile.yaml") + return 1 + deploy_icon_path = app_dir / icon_file + if not deploy_icon_path.exists() or not deploy_icon_path.is_file(): + error(f"Deploy requires an icon file; not found: {icon_file}") + return 1 + if deploy_icon_path.stat().st_size == 0: + error(f"Deploy requires a non-empty icon file: {icon_file}") + return 1 + + if dry_run: + try: + plan = build_deploy_plan(config=config, app_dir=app_dir, app_type=app_type) + except Exception as e: + error(f"Failed to build deploy plan: {e}") + return 1 + print() + print(f"{C.BOLD}Dry Run: Deploy Plan{C.RESET}") + print(f" Name: {plan['name']}") + print(f" Bundle ID: {plan['bundle_id']}") + print(f" Mode: {plan['finish_label']}") + print(f" App Dir: {app_dir}") + print(f" Exec CWD: {plan['exec_cwd']}") + if plan["icon_path"] is not None: + print(f" Icon: {plan['icon_path']}") + else: + print(f" Icon: {C.DIM}{C.RESET}") + + fg = plan["fg_payload"] + if fg is not None: + fg_keys = [e.split("=", 1)[0] for e in fg.get("env", []) if "=" in e] + print(f" Foreground Cmd: {fg['cmd']} {' '.join(fg.get('args', []))}".rstrip()) + print(f" Foreground Env Keys: {', '.join(fg_keys) if fg_keys else ''}") + + bg = plan["bg_payload"] + if bg is not None: + bg_keys = [e.split('=', 1)[0] for e in bg.get("env", []) if "=" in e] + print(f" Background Cmd: {bg['cmd']} {' '.join(bg.get('args', []))}".rstrip()) + print(f" Background Env Keys: {', '.join(bg_keys) if bg_keys else ''}") + if plan["default_schedule"] is not None: + print(f" Background Schedule: configured") + else: + print(f" Background Schedule: {C.DIM}{C.RESET}") + + files = plan["files_to_upload"] + print(f" Files To Upload: {len(files)}") + for f in files: + src = f.get("source", "") + dst = f.get("destination", "") + print(f" - {src} {ARROW} {dst}") + + cmds = plan["bash_commands"] + print(f" Bash Steps: {len(cmds)}") + for name, _cmd in cmds: + print(f" - {name}") + print() + success("Dry run complete (no device changes made)") + return 0 + + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + token = storage.get_token(device) + if not token: + error(f"No token for {device}") + print(f" {C.DIM}Run: truffile connect {device}{C.RESET}") + return 1 + + spinner = Spinner(f"Resolving {device}") + spinner.start() + try: + ip = await resolve_mdns(f"{device}.local") + spinner.stop(success=True) + except RuntimeError: + spinner.fail(f"Could not resolve {device}.local") + print(f" {C.DIM}Try: ping {device}.local{C.RESET}") + return 1 + + address = f"{ip}:80" + client = TruffleClient(address, token=token) + deploy_task = None + + loop = asyncio.get_event_loop() + + def handle_sigint(): + print("\nInterrupted!") + if deploy_task and not deploy_task.done(): + deploy_task.cancel() + + loop.add_signal_handler(signal.SIGINT, handle_sigint) + + try: + deploy_task = asyncio.create_task( + deploy_with_builder( + client=client, + config=config, + app_dir=app_dir, + app_type=app_type, + device=device, + interactive=interactive, + spinner_cls=Spinner, + scrolling_log_cls=ScrollingLog, + info=info, + success=success, + error=error, + color_dim=C.DIM, + color_reset=C.RESET, + color_bold=C.BOLD, + arrow=ARROW, + interactive_shell=_interactive_shell, + ) + ) + return await deploy_task + except asyncio.CancelledError: + print() + spinner = Spinner("Discarding build session") + spinner.start() + if client.app_uuid: + try: + await client.discard() + spinner.stop(success=True) + except Exception: + spinner.fail("Failed to discard") + return 130 + except Exception as e: + error(str(e)) + if client.app_uuid: + spinner = Spinner("Discarding build session") + spinner.start() + try: + await client.discard() + spinner.stop(success=True) + except Exception: + spinner.fail("Failed to discard") + return 1 + finally: + loop.remove_signal_handler(signal.SIGINT) + await client.close() + + +async def _interactive_shell(ws_url: str) -> int: + print(f"{C.DIM}Opening shell... (exit with Ctrl+D or 'exit'){C.RESET}") + import os, termios, fcntl, struct, tty, contextlib, json + try: + import websockets + from websockets.exceptions import ConnectionClosed, ConnectionClosedOK + except Exception: + print(f"{C.RED}{CROSS} Error:{C.RESET} websockets package is required for terminal mode") + return 67 + + def _winsz(): + try: + h, w, _, _ = struct.unpack("HHHH", fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, b"\0"*8)) + return w, h + except Exception: + return 80, 24 + + class Raw: + def __enter__(self): + self.fd = sys.stdin.fileno() + self.old = termios.tcgetattr(self.fd) + tty.setraw(self.fd); return self + def __exit__(self, *a): + termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old) + + async def run_once(): + async with websockets.connect(ws_url, max_size=None, ping_interval=30) as ws: + cols, rows = _winsz() + await ws.send(json.dumps({"resize":[cols, rows]})) + + loop = asyncio.get_running_loop() + q: asyncio.Queue[bytes] = asyncio.Queue() + stop = asyncio.Event() + + def on_stdin(): + try: + data = os.read(sys.stdin.fileno(), 4096) + if data: q.put_nowait(data) + except BlockingIOError: + pass + loop.add_reader(sys.stdin.fileno(), on_stdin) + + async def pump_in(): + try: + while not stop.is_set(): + data = await q.get() + try: await ws.send(data) + except (ConnectionClosed, ConnectionClosedOK): break + finally: + stop.set() + async def pump_out(): + try: + async for msg in ws: + if isinstance(msg, bytes): + os.write(sys.stdout.fileno(), msg) + else: + os.write(sys.stdout.fileno(), msg.encode()) # type: ignore + except (ConnectionClosed, ConnectionClosedOK): + pass + finally: + stop.set() + + with Raw(): + t_in = asyncio.create_task(pump_in()) + t_out = asyncio.create_task(pump_out()) + try: + await asyncio.wait({t_in, t_out}, return_when=asyncio.FIRST_COMPLETED) + finally: + stop.set(); t_in.cancel(); t_out.cancel() + with contextlib.suppress(Exception): + await asyncio.gather(t_in, t_out, return_exceptions=True) + loop.remove_reader(sys.stdin.fileno()) + + + await run_once() + return 67 diff --git a/build/lib/truffile/cli/infer.py b/build/lib/truffile/cli/infer.py new file mode 100644 index 0000000..7c83593 --- /dev/null +++ b/build/lib/truffile/cli/infer.py @@ -0,0 +1,1006 @@ +import argparse +import base64 +import contextlib +import json +import mimetypes +import re +import shutil +import signal +import sys +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable + +import httpx + +from .ui import C, MUSHROOM, CHECK, HAMMER, WARN, SUPPORTED_SERVER_MIME_TYPES, Spinner, StreamAbortWatcher, error, success, info, warn, create_thinking_orb +from .connect import _resolve_connected_device +from .models import _fetch_models_payload, _pick_model_interactive, _default_model +from .commands import INFER_COMMANDS +from .prompt import TrufflePrompt +from .markdown import has_markdown, render_markdown, count_terminal_lines +from .art import ParticleOrb +from .welcome import show_infer_welcome +from truffile.storage import StorageService + + +DEFAULT_SYSTEM_PROMPT = None + +@dataclass +class ChatSettings: + model: str + system_prompt: str | None = DEFAULT_SYSTEM_PROMPT + reasoning: bool = True + stream: bool = True + json_mode: bool = False + max_tokens: int = 2048 + temperature: float | None = None + top_p: float | None = None + default_tools: bool = True + max_tool_rounds: int = 8 + + +class ChatMCPClient: + def __init__(self) -> None: + self._group: Any | None = None + self.endpoint: str | None = None + + @property + def connected(self) -> bool: + return self._group is not None + + async def connect_streamable_http(self, endpoint: str) -> None: + from mcp.client.session_group import ClientSessionGroup, StreamableHttpParameters + + await self.disconnect() + group = ClientSessionGroup() + await group.__aenter__() + try: + await group.connect_to_server(StreamableHttpParameters(url=endpoint)) + except Exception: + with contextlib.suppress(Exception): + await group.__aexit__(None, None, None) + raise + self._group = group + self.endpoint = endpoint + + async def disconnect(self) -> None: + if self._group is None: + self.endpoint = None + return + group = self._group + self._group = None + self.endpoint = None + with contextlib.suppress(Exception): + await group.__aexit__(None, None, None) + + def list_tool_names(self) -> list[str]: + if self._group is None: + return [] + names: list[str] = [] + for _server_name, tool in self._group.list_tools(): + name = getattr(tool, "name", None) + if isinstance(name, str): + names.append(name) + return sorted(set(names)) + + def has_tool(self, name: str) -> bool: + if self._group is None: + return False + try: + tool = self._group.get_tool(name) + return tool is not None + except Exception: + return False + + def build_openai_tools(self) -> list[dict[str, Any]]: + if self._group is None: + return [] + tools: list[dict[str, Any]] = [] + for _server_name, tool in self._group.list_tools(): + name = getattr(tool, "name", None) + if not isinstance(name, str) or not name: + continue + description = str(getattr(tool, "description", "") or "") + schema = getattr(tool, "inputSchema", None) + if not isinstance(schema, dict): + schema = {"type": "object", "properties": {}} + if schema.get("type") != "object": + schema = {"type": "object", "properties": {}} + tools.append( + { + "type": "function", + "function": { + "name": name, + "description": description, + "parameters": schema, + }, + } + ) + return tools + + async def call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]: + if self._group is None: + return {"error": "mcp not connected"} + try: + result = await self._group.call_tool(name, arguments) + content: list[dict[str, Any]] = [] + for part in result.content: + if hasattr(part, "model_dump"): + content.append(part.model_dump()) # type: ignore[call-arg] + elif isinstance(part, dict): + content.append(part) + else: + content.append({"value": str(part)}) + return { + "is_error": bool(result.isError), + "structured_content": result.structuredContent, + "content": content, + } + except Exception as exc: + return {"error": "mcp call failed", "tool": name, "detail": str(exc)} + + +def _print_chat_config(settings: ChatSettings, mcp_client: ChatMCPClient) -> None: + print(f"{C.BLUE}chat config{C.RESET}") + print(f" {C.DIM}model:{C.RESET} {settings.model}") + print(f" {C.DIM}reasoning:{C.RESET} {settings.reasoning}") + print(f" {C.DIM}stream:{C.RESET} {settings.stream}") + print(f" {C.DIM}json:{C.RESET} {settings.json_mode}") + print(f" {C.DIM}tools:{C.RESET} {settings.default_tools}") + print(f" {C.DIM}max_tokens:{C.RESET} {settings.max_tokens}") + print(f" {C.DIM}temperature:{C.RESET} {settings.temperature}") + print(f" {C.DIM}top_p:{C.RESET} {settings.top_p}") + print(f" {C.DIM}max_rounds:{C.RESET} {settings.max_tool_rounds}") + print(f" {C.DIM}system:{C.RESET} {settings.system_prompt or ''}") + print(f" {C.DIM}mcp:{C.RESET} {mcp_client.endpoint or ''}") + + +def _parse_on_off(value: str) -> bool | None: + v = value.strip().lower() + if v in {"on", "true", "1", "yes"}: + return True + if v in {"off", "false", "0", "no"}: + return False + return None + + +def _resolve_image_path(raw_path: str) -> Path: + path = Path(raw_path).expanduser().resolve() + if not path.is_file(): + raise FileNotFoundError(f"image file not found: {path}") + return path + + +def _guess_mime_type(path: Path) -> str: + mime, _ = mimetypes.guess_type(str(path)) + return mime or "image/jpeg" + + +def _normalize_image_for_server(image_bytes: bytes, mime: str) -> tuple[bytes, str, bool]: + mime_l = mime.lower() + if mime_l in SUPPORTED_SERVER_MIME_TYPES: + return image_bytes, mime_l, False + try: + from PIL import Image + except Exception as exc: + raise RuntimeError( + f"image mime {mime!r} is not supported by server decoder and Pillow is unavailable: {exc}" + ) from exc + + from io import BytesIO + + try: + with Image.open(BytesIO(image_bytes)) as im: + rgb = im.convert("RGB") + out = BytesIO() + rgb.save(out, format="PNG") + return out.getvalue(), "image/png", True + except Exception as exc: + raise RuntimeError(f"failed to transcode unsupported image mime {mime!r}: {exc}") from exc + + +def _resolve_image_bytes_and_mime(image_path_or_url: str) -> tuple[bytes, str, str]: + if image_path_or_url.startswith("http://") or image_path_or_url.startswith("https://"): + with httpx.Client(timeout=60.0) as client: + resp = client.get(image_path_or_url) + resp.raise_for_status() + content_type = (resp.headers.get("Content-Type") or "").split(";")[0].strip().lower() + mime = content_type if content_type.startswith("image/") else "image/jpeg" + size_kib = len(resp.content) / 1024.0 + image_bytes, mime, transcoded = _normalize_image_for_server(resp.content, mime) + desc = f"url={image_path_or_url} size={size_kib:.1f} KiB mime={mime}" + if transcoded: + desc += " (transcoded)" + return image_bytes, mime, desc + + path = _resolve_image_path(image_path_or_url) + size_kib = path.stat().st_size / 1024.0 + mime = _guess_mime_type(path) + image_bytes, mime, transcoded = _normalize_image_for_server(path.read_bytes(), mime) + desc = f"path={path} size={size_kib:.1f} KiB mime={mime}" + if transcoded: + desc += " (transcoded)" + return image_bytes, mime, desc + + +def _to_data_url(image_bytes: bytes, mime: str) -> str: + payload = base64.b64encode(image_bytes).decode("ascii") + return f"data:{mime};base64,{payload}" + + +def _make_user_message(text: str, image_data_url: str | None) -> dict[str, Any]: + if image_data_url is None: + return {"role": "user", "content": text} + return { + "role": "user", + "content": [ + {"type": "text", "text": text}, + {"type": "image_url", "image_url": {"url": image_data_url}}, + ], + } + + +def _build_default_tools() -> list[dict[str, Any]]: + return [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the web for a query and return top results.", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query."}, + "max_results": { + "type": "integer", + "description": "Number of results to return (1-10).", + "default": 5, + }, + }, + "required": ["query"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "web_fetch", + "description": "Fetch and extract readable text from a URL.", + "parameters": { + "type": "object", + "properties": { + "url": {"type": "string", "description": "Absolute http/https URL."}, + "max_chars": { + "type": "integer", + "description": "Max number of characters to return (500-20000).", + "default": 8000, + }, + }, + "required": ["url"], + }, + }, + }, + ] + + +def _tool_web_search(arguments: dict[str, Any]) -> dict[str, Any]: + query = str(arguments.get("query", "")).strip() + if not query: + return {"error": "query is required"} + max_results = arguments.get("max_results", 5) + try: + max_results = int(max_results) + except (TypeError, ValueError): + max_results = 5 + max_results = max(1, min(max_results, 10)) + try: + from ddgs import DDGS + except Exception as exc: + return { + "error": "ddgs is not installed or failed to import", + "detail": str(exc), + "hint": "pip install ddgs", + } + rows: list[dict[str, Any]] = [] + try: + with DDGS() as ddgs: + for r in ddgs.text(query, max_results=max_results): + if len(rows) >= max_results: + break + rows.append( + { + "title": r.get("title"), + "url": r.get("href") or r.get("url"), + "snippet": r.get("body") or r.get("snippet"), + } + ) + except Exception as exc: + return {"error": "web_search failed", "detail": str(exc)} + return {"query": query, "count": len(rows), "results": rows} + + +def _tool_web_fetch(arguments: dict[str, Any]) -> dict[str, Any]: + url = str(arguments.get("url", "")).strip() + if not url: + return {"error": "url is required"} + max_chars = arguments.get("max_chars", 8000) + try: + max_chars = int(max_chars) + except (TypeError, ValueError): + max_chars = 8000 + max_chars = max(500, min(max_chars, 20000)) + try: + import trafilatura + except Exception as exc: + return { + "error": "trafilatura is not installed or failed to import", + "detail": str(exc), + "hint": "pip install trafilatura", + } + try: + downloaded = trafilatura.fetch_url(url) + if not downloaded: + return {"error": "failed to download url", "url": url} + text = trafilatura.extract(downloaded, include_links=False, include_images=False) + if not text: + return {"error": "failed to extract readable text", "url": url} + text = text.strip() + truncated = len(text) > max_chars + return { + "url": url, + "content": text[:max_chars], + "truncated": truncated, + "content_chars": min(len(text), max_chars), + } + except Exception as exc: + return {"error": "web_fetch failed", "url": url, "detail": str(exc)} + + +def _execute_default_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: + if name == "web_search": + return _tool_web_search(arguments) + if name == "web_fetch": + return _tool_web_fetch(arguments) + return {"error": f"unknown tool '{name}'"} + + +def _print_history(messages: list[dict[str, Any]]) -> None: + for idx, msg in enumerate(messages): + role = str(msg.get("role", "unknown")) + if role == "assistant" and msg.get("tool_calls"): + text = f"[tool_calls={len(msg.get('tool_calls') or [])}]" + else: + content = msg.get("content", "") + if isinstance(content, list): + text = json.dumps(content, ensure_ascii=True) + else: + text = str(content) + text = text.replace("\n", " ") + if len(text) > 160: + text = text[:157] + "..." + print(f"{idx:03d} {role:9s} {text}") + + +def _build_chat_payload( + *, + model: str, + messages: list[dict[str, Any]], + settings: ChatSettings, + stream: bool, + tools: list[dict[str, Any]] | None, +) -> dict[str, Any]: + body: dict[str, Any] = { + "model": model, + "messages": messages, + "stream": stream, + "reasoning": {"enabled": bool(settings.reasoning)}, + "max_tokens": int(settings.max_tokens), + } + if settings.temperature is not None: + body["temperature"] = settings.temperature + if settings.top_p is not None: + body["top_p"] = settings.top_p + if stream: + body["stream_options"] = {"include_usage": True} + if tools: + body["tools"] = tools + body["tool_choice"] = "auto" + return body + + +def _print_reasoning_and_response(reasoning_text: str, response_text: str, show_reasoning: bool) -> None: + if show_reasoning and reasoning_text: + print(f"{C.GRAY}thinking:{C.RESET}") + print(f"{C.GRAY}{reasoning_text}{C.RESET}") + if response_text: + print() + if response_text: + print(response_text) + + +def _print_usage(usage: dict[str, Any]) -> None: + prompt_t = usage.get("prompt_tokens", "") + comp_t = usage.get("completion_tokens", "") + total_t = usage.get("total_tokens", "") + decode_tps = usage.get("decode_tokens_per_second", "") + ttft = usage.get("ttft_ms", "") + itl = usage.get("itl_ms", "") + image_t = usage.get("image_tokens") or 0 + parts = [f"tokens {C.DIM}[{prompt_t}/{comp_t}/{total_t}]{C.RESET}"] + if decode_tps: + parts.append(f"decode {C.DIM}{decode_tps} t/s{C.RESET}") + if ttft: + parts.append(f"ttft {C.DIM}{ttft}ms{C.RESET}") + if itl: + parts.append(f"itl {C.DIM}{itl}ms{C.RESET}") + if image_t: + parts.append(f"image {C.DIM}{image_t}t{C.RESET}") + sep = f" {C.DIM}|{C.RESET} " + print(f"\n{C.DIM}---{C.RESET}") + print(sep.join(parts)) + + +def _print_infer_help() -> None: + print(f"\n{C.BOLD}commands:{C.RESET}") + for sc in INFER_COMMANDS: + display = f"{sc.name} {sc.arg_hint}" if sc.arg_hint else sc.name + print(f" {C.CYAN}{display}{C.RESET} โ€” {sc.description}") + print(f"\n{C.DIM}alt+enter for newline ยท tab to complete commands ยท ctrl+d to exit{C.RESET}\n") + + +def _run_single_chat_request( + *, + client: httpx.Client, + url: str, + headers: dict[str, str], + payload: dict[str, Any], + settings: ChatSettings, + stream: bool, +) -> tuple[dict[str, Any], dict[str, Any] | None, bool]: + orb = create_thinking_orb() + orb.start(ParticleOrb.STATE_ACTIVE) + orb_stopped = False + if stream: + content_parts: list[str] = [] + reasoning_parts: list[str] = [] + usage: dict[str, Any] | None = None + tool_calls_by_index: dict[int, dict[str, Any]] = {} + reasoning_stream_started = False + interrupted = False + first_event_seen = False + + try: + with StreamAbortWatcher() as abort_watcher: + with client.stream("POST", url, headers=headers, json=payload) as resp: + resp.raise_for_status() + for raw in resp.iter_lines(): + if abort_watcher.aborted(): + interrupted = True + break + if not raw: + continue + line = raw.strip() + if not line.startswith("data:"): + continue + data = line[len("data:"):].strip() + if data == "[DONE]": + break + try: + evt = json.loads(data) + except Exception: + continue + if isinstance(evt.get("usage"), dict): + usage = evt.get("usage") + + choices = evt.get("choices") + if not isinstance(choices, list) or not choices: + continue + c0 = choices[0] + if not isinstance(c0, dict): + continue + delta = c0.get("delta", {}) + if not isinstance(delta, dict): + continue + + reasoning_chunk = delta.get("reasoning") + if isinstance(reasoning_chunk, str) and reasoning_chunk: + # stop orb before first visible output + if not orb_stopped: + orb.stop() + orb_stopped = True + reasoning_parts.append(reasoning_chunk) + if settings.reasoning: + if not reasoning_stream_started: + print(f"{C.GRAY}thinking:{C.RESET}") + reasoning_stream_started = True + print(f"{C.GRAY}{reasoning_chunk}{C.RESET}", end="", flush=True) + + content_chunk = delta.get("content") + if isinstance(content_chunk, str) and content_chunk: + # stop orb before first visible output + if not orb_stopped: + orb.stop() + orb_stopped = True + content_parts.append(content_chunk) + print(content_chunk, end="", flush=True) + + for tc in delta.get("tool_calls") or []: + if not isinstance(tc, dict): + continue + idx = tc.get("index") + if not isinstance(idx, int): + idx = len(tool_calls_by_index) + entry = tool_calls_by_index.setdefault( + idx, + { + "id": tc.get("id", ""), + "type": tc.get("type", "function"), + "function": {"name": "", "arguments": ""}, + }, + ) + if tc.get("id"): + entry["id"] = tc["id"] + if tc.get("type"): + entry["type"] = tc["type"] + fn = tc.get("function") or {} + if isinstance(fn, dict): + if fn.get("name"): + entry["function"]["name"] += str(fn["name"]) + if fn.get("arguments"): + entry["function"]["arguments"] += str(fn["arguments"]) + except KeyboardInterrupt: + interrupted = True + finally: + if not orb_stopped: + orb.stop() + orb_stopped = True + + msg: dict[str, Any] = {"role": "assistant", "content": "".join(content_parts).strip()} + reasoning_text = "".join(reasoning_parts).strip() + if reasoning_text: + msg["reasoning_content"] = reasoning_text + if tool_calls_by_index: + msg["tool_calls"] = [tool_calls_by_index[i] for i in sorted(tool_calls_by_index)] + full_content = "".join(content_parts).strip() + # content was already streamed to stdout chunk by chunk + if reasoning_stream_started or content_parts: + print() + + # markdown re-render: clear raw streamed content, replace with rich formatted + if full_content and has_markdown(full_content): + try: + width = shutil.get_terminal_size().columns + # only clear the content lines (not reasoning which was above) + lines_to_clear = count_terminal_lines(full_content, width) + 1 + render_markdown(full_content, lines_to_clear) + except Exception: + pass + if interrupted: + print(f"{C.YELLOW}response interrupted{C.RESET}") + return msg, usage, interrupted + + try: + resp = client.post(url, headers=headers, json=payload, timeout=120.0) + resp.raise_for_status() + body = resp.json() + finally: + if not orb_stopped: + orb.stop() + orb_stopped = True + if settings.json_mode: + print(json.dumps(body, indent=2)) + + choices = body.get("choices", []) + c0 = choices[0] if isinstance(choices, list) and choices else {} + msg = c0.get("message", {}) if isinstance(c0, dict) else {} + if not isinstance(msg, dict): + msg = {} + out: dict[str, Any] = {"role": "assistant", "content": str(msg.get("content", "") or "")} + if isinstance(msg.get("reasoning"), str) and msg.get("reasoning"): + out["reasoning_content"] = msg["reasoning"] + if isinstance(msg.get("tool_calls"), list): + out["tool_calls"] = msg.get("tool_calls") + + _print_reasoning_and_response( + str(out.get("reasoning_content") or ""), + str(out.get("content") or ""), + bool(settings.reasoning), + ) + return out, body.get("usage") if isinstance(body.get("usage"), dict) else None, False + + +async def _run_chat_turn( + *, + client: httpx.Client, + url: str, + headers: dict[str, str], + model: str, + settings: ChatSettings, + mcp_client: ChatMCPClient, + messages: list[dict[str, Any]], + user_message: dict[str, Any], +) -> int: + messages.append(user_message) + + max_rounds = max(1, int(settings.max_tool_rounds)) + for _ in range(max_rounds): + stream = settings.stream and not settings.json_mode + tools: list[dict[str, Any]] = [] + if settings.default_tools: + tools.extend(_build_default_tools()) + if mcp_client.connected: + tools.extend(mcp_client.build_openai_tools()) + + payload = _build_chat_payload( + model=model, + messages=messages, + settings=settings, + stream=stream, + tools=tools or None, + ) + assistant_msg, usage, interrupted = _run_single_chat_request( + client=client, url=url, headers=headers, payload=payload, settings=settings, stream=stream + ) + messages.append(assistant_msg) + if isinstance(usage, dict): + _print_usage(usage) + if interrupted: + return 130 + + tool_calls = assistant_msg.get("tool_calls") if isinstance(assistant_msg, dict) else None + if not tools or not isinstance(tool_calls, list) or not tool_calls: + return 0 + + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + fn = tool_call.get("function") or {} + if not isinstance(fn, dict): + continue + name = str(fn.get("name") or "") + raw_args = str(fn.get("arguments") or "{}") + try: + parsed_args = json.loads(raw_args) + except json.JSONDecodeError: + parsed_args = {"_raw": raw_args} + if name in {"web_search", "web_fetch"}: + print(f"{C.CYAN}{HAMMER} tool{C.RESET} {name}") + tool_result = _execute_default_tool(name, parsed_args) + elif mcp_client.has_tool(name): + print(f"{C.CYAN}{HAMMER} mcp{C.RESET} {name}") + tool_result = await mcp_client.call_tool(name, parsed_args) + else: + print(f"{C.YELLOW}{WARN} unknown tool{C.RESET} {name}") + tool_result = {"error": f"unknown tool '{name}'"} + messages.append( + { + "role": "tool", + "tool_call_id": tool_call.get("id", ""), + "content": json.dumps(tool_result, ensure_ascii=False), + } + ) + + warn("Reached max tool rounds without a final assistant response") + return 1 + + +async def cmd_infer(args, storage: StorageService) -> int: + prompt_words = getattr(args, "prompt_words", None) + prompt = " ".join(prompt_words).strip() if prompt_words else "" + + device, ip = await _resolve_connected_device(storage) + if not device or not ip: + return 1 + + spinner = Spinner("Resolving default model") + spinner.start() + model = await _default_model(ip) + if not model: + spinner.fail("Failed to resolve default model from IF2") + return 1 + spinner.stop(success=True) + + settings = ChatSettings(model=model) + mcp_client = ChatMCPClient() + messages: list[dict[str, Any]] = [] + pending_image_data_url: str | None = None + pending_image_desc: str | None = None + + url = f"http://{ip}/if2/v1/chat/completions" + headers = {"Content-Type": "application/json"} + + try: + spinner = Spinner(f"Connecting to {device}") + spinner.start() + with httpx.Client(timeout=None) as client: + spinner.stop(success=True) + + # REPL mode + show_infer_welcome(model=settings.model, device=device) + + prompt_ui = TrufflePrompt("> ", INFER_COMMANDS) + try: + if prompt: + print(f"{C.CYAN}> {prompt}{C.RESET}") + rc = await _run_chat_turn( + client=client, + url=url, + headers=headers, + model=settings.model, + settings=settings, + mcp_client=mcp_client, + messages=messages, + user_message=_make_user_message(prompt, pending_image_data_url), + ) + if rc != 0: + if rc == 130: + return 0 + else: + return rc + else: + pending_image_data_url = None + pending_image_desc = None + + while True: + raw = await prompt_ui.get_input() + if raw is None: + print(f"\n{MUSHROOM} goodbye!") + return 0 + line = raw.strip() + if not line: + continue + if line in {"/", "/help"}: + _print_infer_help() + continue + if line in {"/exit", "/quit"}: + print(f"{MUSHROOM} goodbye!") + return 0 + if line == "/history": + _print_history(messages) + continue + if line == "/reset": + messages = [] + if settings.system_prompt: + messages.append({"role": "system", "content": settings.system_prompt}) + pending_image_data_url = None + pending_image_desc = None + print(f"{C.YELLOW}history reset (and cleared pending attachment){C.RESET}") + continue + if line in {"/models", "/model"}: + try: + models = _fetch_models_payload(client, ip) + selected_model = _pick_model_interactive(models, settings.model) + if selected_model and selected_model != settings.model: + settings.model = selected_model + print(f"{C.GREEN}{CHECK}{C.RESET} model switched: {settings.model}") + except Exception as exc: + error(f"failed to list models: {exc}") + continue + if line == "/config": + _print_chat_config(settings, mcp_client) + continue + if line.startswith("/reasoning"): + arg = line[len("/reasoning"):].strip() + if not arg: + print(f"{C.DIM}reasoning={settings.reasoning}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /reasoning ") + continue + settings.reasoning = val + print(f"{C.GREEN}{CHECK}{C.RESET} reasoning={settings.reasoning}") + continue + if line.startswith("/stream"): + arg = line[len("/stream"):].strip() + if not arg: + print(f"{C.DIM}stream={settings.stream}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /stream ") + continue + settings.stream = val + print(f"{C.GREEN}{CHECK}{C.RESET} stream={settings.stream}") + continue + if line.startswith("/json"): + arg = line[len("/json"):].strip() + if not arg: + print(f"{C.DIM}json={settings.json_mode}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /json ") + continue + settings.json_mode = val + print(f"{C.GREEN}{CHECK}{C.RESET} json={settings.json_mode}") + continue + if line.startswith("/tools"): + arg = line[len("/tools"):].strip() + if not arg: + print(f"{C.DIM}tools={settings.default_tools}{C.RESET}") + continue + val = _parse_on_off(arg) + if val is None: + warn("usage: /tools ") + continue + settings.default_tools = val + print(f"{C.GREEN}{CHECK}{C.RESET} tools={settings.default_tools}") + continue + if line.startswith("/max_tokens"): + arg = line[len("/max_tokens"):].strip() + if not arg: + print(f"{C.DIM}max_tokens={settings.max_tokens}{C.RESET}") + continue + try: + settings.max_tokens = max(1, int(arg)) + print(f"{C.GREEN}{CHECK}{C.RESET} max_tokens={settings.max_tokens}") + except ValueError: + warn("usage: /max_tokens ") + continue + if line.startswith("/temperature"): + arg = line[len("/temperature"):].strip() + if not arg: + print(f"{C.DIM}temperature={settings.temperature}{C.RESET}") + continue + if arg.lower() in {"off", "none"}: + settings.temperature = None + print(f"{C.GREEN}{CHECK}{C.RESET} temperature=None") + continue + try: + settings.temperature = float(arg) + print(f"{C.GREEN}{CHECK}{C.RESET} temperature={settings.temperature}") + except ValueError: + warn("usage: /temperature ") + continue + if line.startswith("/top_p"): + arg = line[len("/top_p"):].strip() + if not arg: + print(f"{C.DIM}top_p={settings.top_p}{C.RESET}") + continue + if arg.lower() in {"off", "none"}: + settings.top_p = None + print(f"{C.GREEN}{CHECK}{C.RESET} top_p=None") + continue + try: + settings.top_p = float(arg) + print(f"{C.GREEN}{CHECK}{C.RESET} top_p={settings.top_p}") + except ValueError: + warn("usage: /top_p ") + continue + if line.startswith("/max_rounds"): + arg = line[len("/max_rounds"):].strip() + if not arg: + print(f"{C.DIM}max_rounds={settings.max_tool_rounds}{C.RESET}") + continue + try: + settings.max_tool_rounds = max(1, int(arg)) + print(f"{C.GREEN}{CHECK}{C.RESET} max_rounds={settings.max_tool_rounds}") + except ValueError: + warn("usage: /max_rounds ") + continue + if line.startswith("/system"): + arg = line[len("/system"):].strip() + if not arg: + print(f"{C.DIM}system={settings.system_prompt or ''}{C.RESET}") + continue + if arg.lower() in {"off", "none", "clear"}: + settings.system_prompt = None + if messages and messages[0].get("role") == "system": + messages.pop(0) + print(f"{C.GREEN}{CHECK}{C.RESET} system prompt cleared") + continue + settings.system_prompt = arg + if messages and messages[0].get("role") == "system": + messages[0]["content"] = arg + else: + messages.insert(0, {"role": "system", "content": arg}) + print(f"{C.GREEN}{CHECK}{C.RESET} system prompt updated") + continue + if line.startswith("/mcp"): + parts = line.split(maxsplit=2) + if len(parts) == 1 or parts[1] == "status": + print( + f"{C.BLUE}/mcp status{C.RESET} " + f"{C.DIM}mcp={mcp_client.endpoint or ''} " + f"tools={len(mcp_client.list_tool_names())}{C.RESET}" + ) + print( + f"{C.DIM}subcommands:{C.RESET} " + f"{C.BLUE}/mcp connect {C.RESET}, " + f"{C.BLUE}/mcp tools{C.RESET}, " + f"{C.BLUE}/mcp disconnect{C.RESET}" + ) + continue + sub = parts[1].lower() + if sub == "connect": + if len(parts) < 3: + warn("usage: /mcp connect ") + continue + endpoint = parts[2].strip() + if not endpoint.startswith(("http://", "https://")): + warn("mcp endpoint must start with http:// or https://") + continue + try: + await mcp_client.connect_streamable_http(endpoint) + print( + f"{C.BLUE}/mcp connect{C.RESET} " + f"{C.GREEN}{CHECK}{C.RESET} {endpoint} " + f"({len(mcp_client.list_tool_names())} tools)" + ) + except Exception as exc: + error(f"mcp connect failed: {exc}") + continue + if sub == "disconnect": + await mcp_client.disconnect() + print(f"{C.BLUE}/mcp disconnect{C.RESET} {C.GREEN}{CHECK}{C.RESET}") + continue + if sub == "tools": + names = mcp_client.list_tool_names() + if not names: + print(f"{C.BLUE}/mcp tools{C.RESET} {C.DIM}no tools available{C.RESET}") + else: + print(f"{C.BLUE}/mcp tools{C.RESET} {', '.join(names)}") + continue + warn("usage: /mcp ") + continue + if line.startswith("/attach"): + parts = line.split(maxsplit=1) + if len(parts) != 2 or not parts[1].strip(): + warn("usage: /attach ") + continue + src = parts[1].strip() + try: + image_bytes, mime, desc = _resolve_image_bytes_and_mime(src) + pending_image_data_url = _to_data_url(image_bytes, mime) + pending_image_desc = desc + print(f"{C.GREEN}{CHECK}{C.RESET} attachment ready: {desc}") + except FileNotFoundError as exc: + error(str(exc)) + except httpx.HTTPError as exc: + error(f"failed to fetch image: {exc}") + except RuntimeError as exc: + error(str(exc)) + continue + if line.startswith("/create"): + from .create import cmd_create + from types import SimpleNamespace + create_arg = line[len("/create"):].strip() or None + cmd_create(SimpleNamespace(name=create_arg, path=None)) + continue + if line.startswith("/"): + warn(f"unknown command: {line}. type /help") + continue + + if pending_image_data_url is not None: + print(f"{C.MAGENTA}[attach]{C.RESET} sending with image: {pending_image_desc}") + rc = await _run_chat_turn( + client=client, + url=url, + headers=headers, + model=settings.model, + settings=settings, + mcp_client=mcp_client, + messages=messages, + user_message=_make_user_message(line, pending_image_data_url), + ) + if rc != 0: + if rc == 130: + return 0 + return rc + pending_image_data_url = None + pending_image_desc = None + finally: + await mcp_client.disconnect() + return 0 + except Exception as e: + try: + spinner.fail(f"Chat request failed: {e}") # type: ignore[name-defined] + except Exception: + error(f"Chat request failed: {e}") + return 1 + + diff --git a/build/lib/truffile/cli/markdown.py b/build/lib/truffile/cli/markdown.py new file mode 100644 index 0000000..f63e537 --- /dev/null +++ b/build/lib/truffile/cli/markdown.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import re +import shutil +import sys + +from rich.console import Console +from rich.markdown import Markdown +from rich.theme import Theme + + +TRUFFILE_THEME = Theme({ + "markdown.h1": "bold #4169E1", + "markdown.h2": "bold #4169E1", + "markdown.h3": "#4169E1", + "markdown.link": "#FF1493", + "markdown.link_url": "dim #FF1493", + "markdown.code": "#FFA500", + "markdown.item.bullet": "#4169E1", + "markdown.item.number": "#4169E1", +}) + +_console = Console(theme=TRUFFILE_THEME, highlight=False) + +_MD_PATTERNS = [ + re.compile(r"```"), + re.compile(r"^\s*#{1,6}\s", re.MULTILINE), + re.compile(r"\*\*.+?\*\*"), + re.compile(r"^\s*[-*+]\s", re.MULTILINE), + re.compile(r"^\s*\d+\.\s", re.MULTILINE), + re.compile(r"\[.+?\]\(.+?\)"), +] + + +def has_markdown(text: str) -> bool: + for pattern in _MD_PATTERNS: + if pattern.search(text): + return True + return False + + +def count_terminal_lines(text: str, terminal_width: int) -> int: + lines = 0 + for line in text.split("\n"): + visible_len = len(line) + if visible_len == 0: + lines += 1 + else: + lines += max(1, (visible_len + terminal_width - 1) // terminal_width) + return lines + + +def render_markdown(raw_text: str, lines_to_clear: int) -> None: + if not raw_text.strip(): + return + # move cursor up and clear each line of raw output + for _ in range(lines_to_clear): + sys.stdout.write("\033[A\033[K") + sys.stdout.write("\033[K") + sys.stdout.flush() + _console.print(Markdown(raw_text)) diff --git a/build/lib/truffile/cli/models.py b/build/lib/truffile/cli/models.py new file mode 100644 index 0000000..8bfdb01 --- /dev/null +++ b/build/lib/truffile/cli/models.py @@ -0,0 +1,221 @@ +import sys +from typing import Any + +import httpx + +from truffile.storage import StorageService +from truffile.client import resolve_mdns + +from .ui import C, CHECK, MUSHROOM, WARN, Spinner, error, warn + +try: + import termios + import tty +except Exception: + termios = None # type: ignore[assignment] + tty = None # type: ignore[assignment] + + +async def cmd_models(storage: StorageService) -> int: + """List models on your Truffle.""" + device = storage.state.last_used_device + if not device: + error("No device connected") + print(f" {C.DIM}Run: truffile connect {C.RESET}") + return 1 + + spinner = Spinner(f"Connecting to {device}") + spinner.start() + + try: + ip = await resolve_mdns(f"{device}.local") + except RuntimeError: + spinner.fail(f"Could not resolve {device}.local") + return 1 + + try: + url = f"http://{ip}/if2/v1/models" + with httpx.Client(timeout=15.0) as client: + resp = client.get(url) + resp.raise_for_status() + payload = resp.json() + spinner.stop(success=True) + except Exception as e: + spinner.fail(f"Failed to get IF2 models: {e}") + return 1 + + models = payload.get("data", []) + if not isinstance(models, list): + spinner.fail("Invalid response: missing 'data' list") + return 1 + + print() + print(f"{MUSHROOM} {C.BOLD}IF2 Models on {device}{C.RESET}") + print() + + if not models: + print(f" {C.DIM}No models found{C.RESET}") + return 0 + + for m in models: + if not isinstance(m, dict): + continue + model_id = m.get("id", "") + name = m.get("name", model_id) + uuid = m.get("uuid", "") + ctx = m.get("context_length", "") + arch = m.get("architecture", {}) + tokenizer = arch.get("tokenizer", "") if isinstance(arch, dict) else "" + max_batch = m.get("max_batch_size", "") + print(f" {C.GREEN}{CHECK}{C.RESET} {name}") + print(f" {C.DIM}id: {model_id}{C.RESET}") + print(f" {C.DIM}uuid: {uuid}{C.RESET}") + print(f" {C.DIM}context: {ctx}, tokenizer: {tokenizer}, max_batch: {max_batch}{C.RESET}") + + return 0 + + +async def _default_model(ip: str) -> str | None: + try: + with httpx.Client(timeout=10.0) as client: + resp = client.get(f"http://{ip}/if2/v1/models") + resp.raise_for_status() + payload = resp.json() + models = payload.get("data", []) + if not isinstance(models, list) or not models: + return None + # sort models by name/id so default is 35b typically & list is consistent + models.sort(key=lambda m: str(m.get("name") or m.get("id") or m.get("uuid") or "")) + first = models[0] + return str(first.get("id") or first.get("uuid") or "") + except Exception: + return None + + +def _model_display_name(model: dict[str, Any]) -> str: + model_id = str(model.get("id") or "") + name = str(model.get("name") or model_id) + if name == model_id: + return name + return f"{name} ({model_id})" + + +def _model_value(model: dict[str, Any]) -> str: + return str(model.get("uuid") or model.get("id") or "") + + +def _model_matches_current(model: dict[str, Any], current_model: str) -> bool: + if not current_model: + return False + mv = _model_value(model) + mid = str(model.get("id") or "") + return current_model in {mv, mid} + + +def _pick_model_with_numbers(models: list[dict[str, Any]], current_model: str) -> str | None: + if not models: + return None + print(f"{C.BLUE}models:{C.RESET}") + default_idx = 0 + for i, m in enumerate(models, start=1): + active = f" {C.DIM}[active]{C.RESET}" if _model_matches_current(m, current_model) else "" + if active: + default_idx = i - 1 + print(f"{C.BLUE}{i}.{C.RESET} {_model_display_name(m)}{active}") + choice = input(f"{C.CYAN}?{C.RESET} select model [1-{len(models)}] (Enter to keep): ").strip() + if not choice: + return _model_value(models[default_idx]) + try: + idx = int(choice) - 1 + except ValueError: + warn("invalid model selection") + return None + if idx < 0 or idx >= len(models): + warn("invalid model selection") + return None + return _model_value(models[idx]) + + +def _pick_model_interactive(models: list[dict[str, Any]], current_model: str) -> str | None: + if not models: + return None + if not sys.stdin.isatty() or not sys.stdout.isatty() or termios is None or tty is None: + return _pick_model_with_numbers(models, current_model) + + selected = 0 + for i, m in enumerate(models): + if _model_matches_current(m, current_model): + selected = i + break + + lines_rendered = 0 + + def _render() -> None: + nonlocal lines_rendered + lines: list[str] = [] + lines.append(f"{C.BLUE}select model (โ†‘/โ†“, Enter=select, q=cancel){C.RESET}") + for i, m in enumerate(models): + pointer = "โ€บ" if i == selected else " " + active = f" {C.DIM}[active]{C.RESET}" if _model_matches_current(m, current_model) else "" + line = f" {C.CYAN}{pointer}{C.RESET} {_model_display_name(m)}{active}" + lines.append(line) + + if lines_rendered > 0: + sys.stdout.write(f"\033[{lines_rendered}A") + for line in lines: + sys.stdout.write(f"\r\033[K{line}\n") + sys.stdout.flush() + lines_rendered = len(lines) + + fd = sys.stdin.fileno() + old_attrs = termios.tcgetattr(fd) + try: + tty.setraw(fd) + _render() + while True: + ch = sys.stdin.read(1) + if ch in ("\r", "\n"): + sys.stdout.write("\r\033[K") + return _model_value(models[selected]) + if ch in ("q", "Q"): + sys.stdout.write("\r\033[K") + return None + if ch == "\x1b": + seq1 = sys.stdin.read(1) + if seq1 == "[": + seq2 = sys.stdin.read(1) + if seq2 == "A": + selected = (selected - 1) % len(models) + _render() + continue + if seq2 == "B": + selected = (selected + 1) % len(models) + _render() + continue + return None + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs) + if lines_rendered > 0: + sys.stdout.write(f"\033[{lines_rendered}A") + for _ in range(lines_rendered): + sys.stdout.write("\r\033[K\n") + sys.stdout.write(f"\033[{lines_rendered}A") + sys.stdout.flush() + + +def _fetch_models_payload(client: httpx.Client, ip: str) -> list[dict[str, Any]]: + resp = client.get(f"http://{ip}/if2/v1/models", timeout=15.0) + resp.raise_for_status() + payload = resp.json() + raw = payload.get("data", []) + if not isinstance(raw, list): + raise RuntimeError("invalid models payload") + out: list[dict[str, Any]] = [] + for m in raw: + if isinstance(m, dict): + out.append(m) + try: + out.sort(key=lambda m: str(m.get("name") or m.get("id") or m.get("uuid") or "")) + except Exception: pass + + return out diff --git a/build/lib/truffile/cli/picker.py b/build/lib/truffile/cli/picker.py new file mode 100644 index 0000000..0ba0251 --- /dev/null +++ b/build/lib/truffile/cli/picker.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import sys +from typing import Any + +from prompt_toolkit import Application +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout import Layout, HSplit +from prompt_toolkit.layout.containers import Window +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.formatted_text import FormattedText +from prompt_toolkit.styles import Style + +from .ui import C + + +PICKER_STYLE = Style.from_dict({ + "selected": "#4169E1 bold", + "label": "", + "detail": "#666666", + "current": "#22c55e", + "pointer": "#4169E1 bold", +}) + + +async def pick_from_list( + items: list[dict[str, Any]], + *, + label_key: str = "label", + detail_key: str = "detail", + active_key: str | None = None, + active_value: Any = None, + prompt: str = "pick one", +) -> dict[str, Any] | None: + if not items: + return None + + # fallback for non-tty + if not sys.stdout.isatty(): + return _fallback_pick(items, label_key=label_key, detail_key=detail_key, + active_key=active_key, active_value=active_value, prompt=prompt) + + selected_idx = [0] + result: list[dict[str, Any] | None] = [None] + + def _get_text(): + fragments: list[tuple[str, str]] = [] + fragments.append(("class:detail", f" {prompt} (โ†‘โ†“ enter esc)\n")) + for i, item in enumerate(items): + label = item.get(label_key, "?") + detail = item.get(detail_key, "") + is_active = active_key and item.get(active_key) == active_value + is_selected = i == selected_idx[0] + + if is_selected: + fragments.append(("class:pointer", " โ€บ ")) + fragments.append(("class:selected", label)) + else: + fragments.append(("", " ")) + fragments.append(("class:label", label)) + + if is_active: + fragments.append(("class:current", " (current)")) + if detail: + fragments.append(("class:detail", f" {detail}")) + fragments.append(("", "\n")) + return FormattedText(fragments) + + kb = KeyBindings() + + @kb.add("up") + def _up(event): + selected_idx[0] = (selected_idx[0] - 1) % len(items) + + @kb.add("down") + def _down(event): + selected_idx[0] = (selected_idx[0] + 1) % len(items) + + @kb.add("enter") + def _enter(event): + result[0] = items[selected_idx[0]] + event.app.exit() + + @kb.add("escape") + def _escape(event): + result[0] = None + event.app.exit() + + @kb.add("c-c") + def _ctrlc(event): + result[0] = None + event.app.exit() + + @kb.add("c-d") + def _ctrld(event): + result[0] = None + event.app.exit() + + layout = Layout(HSplit([Window(FormattedTextControl(_get_text))])) + app = Application(layout=layout, key_bindings=kb, style=PICKER_STYLE, full_screen=False) + + try: + await app.run_async() + except (EOFError, KeyboardInterrupt): + return None + + return result[0] + + +def _fallback_pick( + items: list[dict[str, Any]], + *, + label_key: str = "label", + detail_key: str = "detail", + active_key: str | None = None, + active_value: Any = None, + prompt: str = "pick one", +) -> dict[str, Any] | None: + """Simple numbered fallback for non-interactive terminals.""" + print() + for i, item in enumerate(items, 1): + label = item.get(label_key, "?") + detail = item.get(detail_key, "") + is_active = active_key and item.get(active_key) == active_value + line = f" {C.CYAN}{i}.{C.RESET} {label}" + if is_active: + line += f" {C.GREEN}(current){C.RESET}" + if detail: + line += f" {C.DIM}{detail}{C.RESET}" + print(line) + print() + try: + choice = input(f"{prompt} (1-{len(items)}) or enter to cancel: ").strip() + except (EOFError, KeyboardInterrupt): + return None + if not choice: + return None + try: + idx = int(choice) - 1 + if 0 <= idx < len(items): + return items[idx] + except ValueError: + pass + return None diff --git a/build/lib/truffile/cli/prompt.py b/build/lib/truffile/cli/prompt.py new file mode 100644 index 0000000..1030f37 --- /dev/null +++ b/build/lib/truffile/cli/prompt.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import shutil +import time + +from prompt_toolkit import PromptSession +from prompt_toolkit.completion import Completer, Completion +from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text import HTML, FormattedText +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.styles import Style + +from .commands import SlashCommand + + +TRUFFILE_STYLE = Style.from_dict({ + "prompt": "#56b6c2 bold", + "continuation": "#666666", + "bottom-toolbar": "noreverse #555555", + "bottom-toolbar.text": "noreverse #555555", + # dropdown + "completion-menu.completion": "bg:#2a2a2a #56b6c2", + "completion-menu.completion.current": "bg:#3a3a4a #56b6c2 bold", + "completion-menu.meta.completion": "bg:#2a2a2a #888888", + "completion-menu.meta.completion.current": "bg:#3a3a4a #bbbbbb", + "scrollbar.background": "bg:#2a2a2a", + "scrollbar.button": "bg:#56b6c2", +}) + +# thin horizontal rule using ANSI bright cyan (\033[96m) to match C.CYAN +_CYAN = "\033[96m" +_DIM = "\033[2m" +_RESET = "\033[0m" + + +class SlashCommandCompleter(Completer): + def __init__(self, commands: list[SlashCommand]): + self.commands = list(commands) + + def add_commands(self, commands: list[SlashCommand]) -> None: + for cmd in commands: + if not any(c.name == cmd.name for c in self.commands): + self.commands.append(cmd) + + def get_completions(self, document: Document, complete_event): + text = document.text_before_cursor.lstrip() + if not text.startswith("/"): + return + + parts = text.split(maxsplit=1) + if len(parts) > 1: + return + + prefix = parts[0].lower() + for cmd in self.commands: + if not cmd.name.lower().startswith(prefix): + continue + display = f"{cmd.name} {cmd.arg_hint}" if cmd.arg_hint else cmd.name + yield Completion( + cmd.name, + start_position=-len(prefix), + display=display, + display_meta=cmd.description, + ) + + +def _build_keybindings() -> KeyBindings: + kb = KeyBindings() + + @kb.add("enter", eager=True) + def _submit(event): + event.current_buffer.validate_and_handle() + + @kb.add("escape", "enter") + def _newline_alt_enter(event): + event.current_buffer.insert_text("\n") + + @kb.add("c-j") + def _newline_ctrl_j(event): + event.current_buffer.insert_text("\n") + + return kb + + +def _hr(task_name: str = "") -> str: + """Thin horizontal rule, optional right-aligned task name.""" + try: + width = shutil.get_terminal_size().columns + except Exception: + width = 80 + if task_name: + label = f" {task_name} " + line_len = max(0, width - len(label)) + return "โ”€" * line_len + label + return "โ”€" * width + + +class TrufflePrompt: + """Shared input component for chat and infer REPLs.""" + + def __init__(self, prompt_text: str, commands: list[SlashCommand]): + self._completer = SlashCommandCompleter(commands) + self._session = PromptSession( + completer=self._completer, + key_bindings=_build_keybindings(), + multiline=True, + prompt_continuation=self._continuation, + style=TRUFFILE_STYLE, + complete_while_typing=True, + ) + self._prompt_text = prompt_text + self._ctrlc_time: float | None = None + self.task_name: str = "" + + def add_commands(self, commands: list[SlashCommand]) -> None: + self._completer.add_commands(commands) + + @staticmethod + def _continuation(width: int, line_number: int, wrap_count: int) -> str: + return "." * (width - 1) + " " + + def _bottom_toolbar(self) -> FormattedText: + line = _hr(self.task_name) + return FormattedText([("", line)]) + + async def get_input(self) -> str | None: + """Returns user text, empty string on single Ctrl+C, None on exit.""" + try: + text = await self._session.prompt_async( + HTML(f"{self._prompt_text}"), + bottom_toolbar=self._bottom_toolbar, + ) + self._ctrlc_time = None + return text + except EOFError: + return None + except KeyboardInterrupt: + return self._handle_ctrlc() + + def _handle_ctrlc(self) -> str | None: + now = time.monotonic() + if self._ctrlc_time is not None and (now - self._ctrlc_time) < 3.0: + return None + self._ctrlc_time = now + print("\npress ctrl+c again to quit (or ctrl+d)") + return "" diff --git a/build/lib/truffile/cli/ui.py b/build/lib/truffile/cli/ui.py new file mode 100644 index 0000000..1818e10 --- /dev/null +++ b/build/lib/truffile/cli/ui.py @@ -0,0 +1,226 @@ +import os +import select +import sys +import threading +import time +from pathlib import Path +from typing import Any + +try: + import termios + import tty +except Exception: + termios = None + tty = None + + +class C: + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + MAGENTA = "\033[95m" + CYAN = "\033[96m" + GRAY = "\033[90m" + DIM = "\033[2m" + BOLD = "\033[1m" + RESET = "\033[0m" + + +MUSHROOM = "๐Ÿ„โ€๐ŸŸซ" +CHECK = "โœ“" +CROSS = "โœ—" +ARROW = "โ†’" +DOT = "โ€ข" +WARN = "โš " +HAMMER = "๐Ÿ”จ" +SUPPORTED_SERVER_MIME_TYPES = {"image/jpeg", "image/png", "image/bmp"} +SCAFFOLD_ICON_RESOURCE_REL = Path("assets") / "Truffle.png" + + +class Spinner: + FRAMES = ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "] + + def __init__(self, message: str): + self.message = message + self.running = False + self.thread = None + self.frame_idx = 0 + + def _spin(self): + while self.running: + frame = self.FRAMES[self.frame_idx % len(self.FRAMES)] + sys.stdout.write(f"\r{C.CYAN}{frame}{C.RESET} {self.message}") + sys.stdout.flush() + self.frame_idx += 1 + time.sleep(0.08) + + def start(self): + self.running = True + self.thread = threading.Thread(target=self._spin, daemon=True) + self.thread.start() + + def stop(self, success: bool = True): + self.running = False + if self.thread: + self.thread.join(timeout=0.2) + icon = f"{C.GREEN}{CHECK}{C.RESET}" if success else f"{C.RED}{CROSS}{C.RESET}" + sys.stdout.write(f"\r{icon} {self.message}\n") + sys.stdout.flush() + + def fail(self, message: str | None = None): + self.running = False + if self.thread: + self.thread.join(timeout=0.2) + msg = message or self.message + sys.stdout.write(f"\r{C.RED}{CROSS}{C.RESET} {msg}\n") + sys.stdout.flush() + + +def create_thinking_orb(): + from .art import ParticleOrb + return ParticleOrb(num_particles=5) + + +class ScrollingLog: + def __init__(self, height: int = 6, prefix: str = " "): + self.height = height + self.prefix = prefix + self.lines: list[str] = [] + self.started = False + try: + import shutil + self.width = shutil.get_terminal_size().columns - len(prefix) - 2 + except Exception: + self.width = 76 + + def _truncate(self, line: str) -> str: + if len(line) > self.width: + return line[:self.width - 3] + "..." + return line + + def _render(self): + if self.started: + sys.stdout.write(f"\033[{self.height}A") + display = self.lines[-self.height:] if len(self.lines) >= self.height else self.lines + while len(display) < self.height: + display.insert(0, "") + for line in display: + truncated = self._truncate(line) + sys.stdout.write(f"\033[K{self.prefix}{C.DIM}{truncated}{C.RESET}\n") + sys.stdout.flush() + self.started = True + + def add(self, line: str): + self.lines.append(line.rstrip()) + self._render() + + def finish(self): + if self.started: + sys.stdout.write(f"\033[{self.height}A") + for _ in range(self.height): + sys.stdout.write("\033[K\n") + sys.stdout.write(f"\033[{self.height}A") + sys.stdout.flush() + + +class StreamAbortWatcher: + """Watches for ESC key during streaming. Use as context manager.""" + + def __init__(self) -> None: + self.enabled = bool(sys.stdin.isatty() and termios is not None and tty is not None) + self._fd: int | None = None + self._old_attrs: Any = None + self._thread: threading.Thread | None = None + self._stop = threading.Event() + self._aborted = False + + def __enter__(self) -> "StreamAbortWatcher": + if not self.enabled: + return self + try: + self._fd = sys.stdin.fileno() + self._old_attrs = termios.tcgetattr(self._fd) + tty.setcbreak(self._fd) + except Exception: + self.enabled = False + return self + self._thread = threading.Thread(target=self._watch, daemon=True) + self._thread.start() + return self + + def _watch(self) -> None: + if self._fd is None: + return + while not self._stop.is_set(): + try: + ready, _, _ = select.select([self._fd], [], [], 0.1) + except Exception: + return + if not ready: + continue + try: + ch = os.read(self._fd, 1) + except Exception: + continue + if ch == b"\x1b": + self._aborted = True + self._stop.set() + return + + def aborted(self) -> bool: + return self._aborted + + def __exit__(self, exc_type, exc, tb) -> bool: + self._stop.set() + if self._thread: + self._thread.join(timeout=0.2) + if self.enabled and self._fd is not None and self._old_attrs is not None: + try: + termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_attrs) + except Exception: + pass + return False + + +def error(msg: str): + print(f"{C.RED}{CROSS} Error:{C.RESET} {msg}") + + +def warn(msg: str): + print(f"{C.YELLOW}{WARN} Warning:{C.RESET} {msg}") + + +def success(msg: str): + print(f"{C.GREEN}{CHECK}{C.RESET} {msg}") + + +def info(msg: str): + print(f"{C.CYAN}{DOT}{C.RESET} {msg}") + + +def print_help(): + print(f""" +{C.BOLD}Usage:{C.RESET} truffile [options] + +{C.BOLD}Commands:{C.RESET} + {C.CYAN}scan{C.RESET} scan for truffle devices on the network + {C.CYAN}connect{C.RESET} connect to a truffle device + {C.CYAN}disconnect{C.RESET} [device|all] disconnect from device(s) + {C.CYAN}create{C.RESET} [name] scaffold a new app + {C.CYAN}validate{C.RESET} [path] validate app directory + {C.CYAN}deploy{C.RESET} [path] deploy app to connected device + {C.CYAN}list{C.RESET} list installed apps or connected devices + {C.CYAN}delete{C.RESET} [app] delete app from device + {C.CYAN}models{C.RESET} list inference models on device + {C.CYAN}chat{C.RESET} interactive chat with device + {C.CYAN}help{C.RESET} show this help + +{C.BOLD}Examples:{C.RESET} + truffile scan + truffile connect truffle-1234 + truffile create my-app + truffile deploy ./my-app + truffile list apps + truffile chat +""") diff --git a/build/lib/truffile/cli/validate.py b/build/lib/truffile/cli/validate.py new file mode 100644 index 0000000..f29a69c --- /dev/null +++ b/build/lib/truffile/cli/validate.py @@ -0,0 +1,24 @@ +from pathlib import Path + +from truffile.schema import validate_app_dir + +from .ui import C, error, warn, info, success + + +def cmd_validate(args) -> int: + app_dir = Path(args.path).resolve() + if not app_dir.exists() or not app_dir.is_dir(): + error(f"{app_dir} is not a valid directory") + return 1 + + info(f"Validating app in {app_dir.name}") + valid, _config, app_type, warnings, errors = validate_app_dir(app_dir) + for w in warnings: + warn(w) + if not valid: + for e in errors: + error(e) + return 1 + + success(f"Validation passed ({app_type})") + return 0 diff --git a/build/lib/truffile/cli/welcome.py b/build/lib/truffile/cli/welcome.py new file mode 100644 index 0000000..6e433f6 --- /dev/null +++ b/build/lib/truffile/cli/welcome.py @@ -0,0 +1,169 @@ +"""Rich welcome panels for chat, infer, and help.""" +from __future__ import annotations + +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +from .art import TRUFFLE_BANNER_BRAILLE, _color_top_row, _fg, ROYAL_BLUE, BOLD, DIM, RESET + +_console = Console(highlight=False) + +# colors matching C.CYAN (\033[96m) +ACCENT = "#56b6c2" +DIM_C = "#5a6a6e" +TEXT_C = "#d4d4d4" + + +def _braille_banner_rich() -> str: + """Build the braille banner as a rich-markup string.""" + lines = TRUFFLE_BANNER_BRAILLE + parts = [] + for i, line in enumerate(lines): + if i <= 1: + # top rows get colored but rich markup needs plain text โ€” use raw ANSI + parts.append(_color_top_row(line)) + else: + parts.append(line) + return "\n".join(parts) + + +def _build_panel( + *, + right_lines: list[str], + title: str = "Truffile", + subtitle: str = "", +) -> Panel: + import shutil + try: + term_width = shutil.get_terminal_size().columns + except Exception: + term_width = 80 + + right = Text.from_ansi("\n".join(right_lines)) + + # two-column layout if terminal is wide enough for banner + commands + if term_width >= 75: + layout = Table.grid(padding=(0, 3)) + layout.add_column("left", justify="center") + layout.add_column("right", justify="left") + banner_text = _braille_banner_rich() + left = Text.from_ansi(banner_text + f"\n\n {_fg(*ROYAL_BLUE)}{BOLD}Truffile{RESET} {DIM}โ€” Truffle SDK{RESET}") + layout.add_row(left, right) + content = layout + else: + # narrow terminal: commands only, no banner + content = right + + return Panel( + content, + title=f"[bold {ACCENT}]{title}[/]", + subtitle=f"[dim {DIM_C}]{subtitle}[/]" if subtitle else None, + border_style=ACCENT, + padding=(1, 2), + ) + + +def show_chat_welcome(*, device: str = "", apps: list[str] | None = None) -> None: + """Welcome panel for truffile chat.""" + right: list[str] = [] + right.append(f"\033[1m\033[96mCommands\033[0m") + cmds = [ + ("/help", "show commands"), + ("/tasks", "pick a task"), + ("/new", "new conversation"), + ("/apps", "list installed apps"), + ("/deploy ", "deploy an app"), + ("/create ", "scaffold new app"), + ("/exit", "exit chat"), + ] + for name, desc in cmds: + right.append(f" \033[96m{name:<18}\033[0m \033[2m{desc}\033[0m") + + right.append("") + right.append(f"\033[1m\033[96mKeys\033[0m") + right.append(f" \033[96m{'alt+enter':<18}\033[0m \033[2mnew line\033[0m") + right.append(f" \033[96m{'tab':<18}\033[0m \033[2mcomplete command\033[0m") + right.append(f" \033[96m{'esc':<18}\033[0m \033[2minterrupt stream\033[0m") + right.append(f" \033[96m{'ctrl+d':<18}\033[0m \033[2mexit\033[0m") + + if apps: + right.append("") + right.append(f"\033[1m\033[96mApps\033[0m") + for a in apps[:6]: + right.append(f" \033[96m/{a}\033[0m") + if len(apps) > 6: + right.append(f" \033[2m...and {len(apps) - 6} more\033[0m") + + subtitle = f"connected to {device}" if device else "" + _console.print(_build_panel(right_lines=right, title="Truffile Chat", subtitle=subtitle)) + + +def show_infer_welcome(*, model: str = "", device: str = "") -> None: + """Welcome panel for truffile infer.""" + right: list[str] = [] + right.append(f"\033[1m\033[96mCommands\033[0m") + cmds = [ + ("/help", "show commands"), + ("/models", "switch model"), + ("/config", "show config"), + ("/mcp connect ", "connect MCP"), + ("/attach ", "attach image"), + ("/create ", "scaffold new app"), + ("/reset", "clear history"), + ("/exit", "exit"), + ] + for name, desc in cmds: + right.append(f" \033[96m{name:<22}\033[0m \033[2m{desc}\033[0m") + + right.append("") + right.append(f"\033[1m\033[96mKeys\033[0m") + right.append(f" \033[96m{'alt+enter':<22}\033[0m \033[2mnew line\033[0m") + right.append(f" \033[96m{'tab':<22}\033[0m \033[2mcomplete command\033[0m") + right.append(f" \033[96m{'esc':<22}\033[0m \033[2minterrupt stream\033[0m") + right.append(f" \033[96m{'ctrl+d':<22}\033[0m \033[2mexit\033[0m") + + if model: + right.append("") + right.append(f"\033[1m\033[96mModel\033[0m") + right.append(f" \033[2m{model}\033[0m") + + subtitle = f"{device} ยท {model}" if device and model else device or model or "" + _console.print(_build_panel(right_lines=right, title="Truffile Infer", subtitle=subtitle)) + + +def show_help_welcome() -> None: + """Welcome panel for truffile help.""" + right: list[str] = [] + right.append(f"\033[1m\033[96mCommands\033[0m") + cmds = [ + ("scan", "scan for devices"), + ("connect ", "connect to device"), + ("disconnect", "disconnect"), + ("create [name]", "scaffold new app"), + ("validate [path]", "validate app"), + ("deploy [path]", "deploy to device"), + ("list ", "list apps or devices"), + ("delete [app]", "delete app"), + ("models", "list models"), + ("chat", "agent chat"), + ("infer", "raw inference"), + ("help", "show this"), + ] + for name, desc in cmds: + right.append(f" \033[96m{name:<22}\033[0m \033[2m{desc}\033[0m") + + right.append("") + right.append(f"\033[1m\033[96mExamples\033[0m") + examples = [ + "truffile scan", + "truffile connect truffle-1234", + "truffile create my-app", + "truffile deploy ./my-app", + "truffile chat", + ] + for ex in examples: + right.append(f" \033[2m{ex}\033[0m") + + _console.print(_build_panel(right_lines=right, title="Truffile", subtitle="TruffleOS SDK")) diff --git a/build/lib/truffile/client.py b/build/lib/truffile/client.py new file mode 100644 index 0000000..0ad8a67 --- /dev/null +++ b/build/lib/truffile/client.py @@ -0,0 +1,17 @@ +"""import surface for transport client APIs.""" + +from truffile.transport.client import ( + ExecResult, + NewSessionStatus, + TruffleClient, + UploadResult, + resolve_mdns, +) + +__all__ = [ + "ExecResult", + "NewSessionStatus", + "TruffleClient", + "UploadResult", + "resolve_mdns", +] diff --git a/build/lib/truffile/deploy/__init__.py b/build/lib/truffile/deploy/__init__.py new file mode 100644 index 0000000..6826da9 --- /dev/null +++ b/build/lib/truffile/deploy/__init__.py @@ -0,0 +1,4 @@ +from .plan import build_deploy_plan +from .builder import deploy_with_builder + +__all__ = ["build_deploy_plan", "deploy_with_builder"] diff --git a/build/lib/truffile/deploy/builder.py b/build/lib/truffile/deploy/builder.py new file mode 100644 index 0000000..e903cce --- /dev/null +++ b/build/lib/truffile/deploy/builder.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import asyncio +from pathlib import Path +from typing import Any, Callable + +from truffile.transport.client import TruffleClient +from .plan import build_deploy_plan, _env_map_to_list +from .steps import handle_bash, handle_files, handle_text + +STEP_HANDLERS = { + "bash": handle_bash, + "files": handle_files, + "text": handle_text, +} + + +async def _wait_for_build_session_ready(client: TruffleClient, timeout_sec: float = 45.0) -> None: + deadline = asyncio.get_event_loop().time() + timeout_sec + last_error: Exception | None = None + while asyncio.get_event_loop().time() < deadline: + try: + result = await client.exec("echo ready", cwd="/") + if result.exit_code == 0: + return + except Exception as e: + last_error = e + await asyncio.sleep(1.0) + msg = f"build session did not become ready in time" + if last_error: + msg += f": {last_error}" + raise RuntimeError(msg) + + +async def deploy_with_builder( + *, + client: TruffleClient, + config: dict[str, Any], + app_dir: Path, + app_type: str, + device: str, + interactive: bool, + spinner_cls: Any, + scrolling_log_cls: Any, + info: Callable[[str], None], + success: Callable[[str], None], + error: Callable[[str], None], + color_dim: str, + color_reset: str, + color_bold: str, + arrow: str, + interactive_shell: Callable[[str], Any], +) -> int: + plan = build_deploy_plan(config=config, app_dir=app_dir, app_type=app_type) + + spinner = spinner_cls(f"Connecting to {device}") + spinner.start() + await client.connect() + spinner.stop(success=True) + + spinner = spinner_cls("Starting build session") + spinner.start() + await client.start_build() + await _wait_for_build_session_ready(client) + spinner.stop(success=True) + print(f" {color_dim}Session: {client.app_uuid}{color_reset}") + + collected_env: dict[str, str] = {} + + ctx = dict( + client=client, + app_dir=app_dir, + exec_cwd=plan["exec_cwd"], + spinner_cls=spinner_cls, + scrolling_log_cls=scrolling_log_cls, + info=info, + success=success, + error=error, + color_dim=color_dim, + color_reset=color_reset, + color_bold=color_bold, + arrow=arrow, + collected_env=collected_env, + ) + + for step in plan["ordered_steps"]: + step_type = step.get("type", "") + handler = STEP_HANDLERS.get(step_type) + if handler: + await handler(step, **ctx) + + # inject collected env vars into process configs + fg_payload = plan["fg_payload"] + bg_payload = plan["bg_payload"] + if collected_env: + env_list = _env_map_to_list(collected_env) + if fg_payload: + fg_payload["env"] = fg_payload.get("env", []) + env_list + if bg_payload: + bg_payload["env"] = bg_payload.get("env", []) + env_list + + if interactive: + print() + info("Opening interactive shell (exit with Ctrl+D or 'exit' to finish deploy)") + ws_url = str(client.http_base or "").replace("http://", "ws://").replace("https://", "wss://") + "/term" + await interactive_shell(ws_url) + print() + + spinner = spinner_cls(f"Finishing as {plan['finish_label']} app") + spinner.start() + + await client.finish_app( + name=plan["name"], + bundle_id=plan["bundle_id"], + description=plan["description"], + icon=plan["icon_path"], + foreground=fg_payload, + background=bg_payload, + default_schedule=plan["default_schedule"], + ) + + spinner.stop(success=True) + print() + success(f"Deployed: {color_bold}{plan['name']}{color_reset} ({plan['finish_label']})") + return 0 diff --git a/build/lib/truffile/deploy/plan.py b/build/lib/truffile/deploy/plan.py new file mode 100644 index 0000000..4e300fc --- /dev/null +++ b/build/lib/truffile/deploy/plan.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + + +def _normalize_cmd(cmd_list: list[str]) -> tuple[str, list[str]]: + if not cmd_list: + return ("", []) + cmd = cmd_list[0] if cmd_list[0].startswith("/") else f"/usr/bin/{cmd_list[0]}" + return cmd, cmd_list[1:] + + +def _env_map_to_list(env_dict: dict[str, str] | None) -> list[str]: + if not env_dict: + return [] + return [f"{k}={v}" for k, v in env_dict.items()] + + +def _bundle_id_from_name(name: str) -> str: + raw = "".join(ch.lower() if ch.isalnum() else "." for ch in name).strip(".") + normalized = ".".join([part for part in raw.split(".") if part]) + return normalized or "truffle.app" + + +def _extract_process(process_cfg: dict[str, Any] | None) -> tuple[str, list[str], str, list[str]]: + proc = process_cfg or {} + cmd_list = list(proc.get("cmd", ["python", "app.py"])) + cmd, args = _normalize_cmd(cmd_list) + cwd = proc.get("working_directory", proc.get("cwd", "/")) + env = _env_map_to_list(proc.get("environment", proc.get("env"))) + return cmd, args, cwd, env + + +def build_deploy_plan( + *, + config: dict[str, Any], + app_dir: Path, + app_type: str, +) -> dict[str, Any]: + meta = config["metadata"] + name = meta["name"] + description = meta.get("description", "") + bundle_id = meta.get("bundle_id") or _bundle_id_from_name(name) + icon_file = meta.get("icon_file") + icon_path = (app_dir / icon_file) if icon_file and (app_dir / icon_file).exists() else None + + fg_cfg = meta.get("foreground") + bg_cfg = meta.get("background") + new_style = isinstance(fg_cfg, dict) or isinstance(bg_cfg, dict) + + if new_style: + has_fg = isinstance(fg_cfg, dict) + has_bg = isinstance(bg_cfg, dict) + else: + has_fg = app_type == "focus" + has_bg = app_type == "ambient" + + if not has_fg and not has_bg: + raise RuntimeError("app must define foreground and/or background process config") + + fg_payload = None + bg_payload = None + exec_cwd = "/" + + if has_fg: + fg_process = fg_cfg.get("process") if isinstance(fg_cfg, dict) else meta.get("process") + fg_cmd, fg_args, fg_cwd, fg_env = _extract_process(fg_process) + fg_payload = {"cmd": fg_cmd, "args": fg_args, "cwd": fg_cwd, "env": fg_env} + exec_cwd = fg_cwd + + if has_bg: + bg_process = bg_cfg.get("process") if isinstance(bg_cfg, dict) else meta.get("process") + bg_cmd, bg_args, bg_cwd, bg_env = _extract_process(bg_process) + bg_payload = {"cmd": bg_cmd, "args": bg_args, "cwd": bg_cwd, "env": bg_env} + if exec_cwd == "/" and bg_cwd: + exec_cwd = bg_cwd + + if has_fg and has_bg: + finish_label = "foreground+background" + elif has_fg: + finish_label = "foreground" + else: + finish_label = "background" + + default_schedule = None + if isinstance(bg_cfg, dict): + default_schedule = bg_cfg.get("default_schedule") + elif has_bg: + default_schedule = meta.get("default_schedule") + + ordered_steps = [] + for step in config.get("steps", []): + if isinstance(step, dict): + ordered_steps.append(step) + + if config.get("files"): + ordered_steps.append({"type": "files", "name": "Copy files", "files": config["files"]}) + if config.get("run"): + ordered_steps.append({"type": "bash", "name": "Install dependencies", "run": config["run"]}) + + return { + "name": name, + "description": description, + "bundle_id": bundle_id, + "icon_path": icon_path, + "fg_payload": fg_payload, + "bg_payload": bg_payload, + "exec_cwd": exec_cwd, + "finish_label": finish_label, + "default_schedule": default_schedule, + "ordered_steps": ordered_steps, + "files_to_upload": [f for s in ordered_steps if s.get("type") == "files" for f in s.get("files", [])], + "bash_commands": [(s.get("name", "bash"), s["run"]) for s in ordered_steps if s.get("type") == "bash"], + } diff --git a/build/lib/truffile/deploy/steps/__init__.py b/build/lib/truffile/deploy/steps/__init__.py new file mode 100644 index 0000000..d1233c7 --- /dev/null +++ b/build/lib/truffile/deploy/steps/__init__.py @@ -0,0 +1,3 @@ +from .bash import handle_bash +from .files import handle_files +from .text import handle_text diff --git a/build/lib/truffile/deploy/steps/bash.py b/build/lib/truffile/deploy/steps/bash.py new file mode 100644 index 0000000..f942933 --- /dev/null +++ b/build/lib/truffile/deploy/steps/bash.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json +from typing import Any, Callable + +from truffile.transport.client import TruffleClient + + +async def handle_bash( + step: dict[str, Any], + *, + client: TruffleClient, + exec_cwd: str, + info: Callable, + error: Callable, + scrolling_log_cls: Any, + **_kw: Any, +) -> None: + step_name = step.get("name", "bash") + run_cmd = step["run"] + info(f"Running: {step_name}") + log = scrolling_log_cls(height=6, prefix=" ") + exit_code = 0 + async for ev, data in client.exec_stream(run_cmd, cwd=exec_cwd): + if ev == "log": + try: + line = json.loads(data).get("line", "") + except Exception: + line = data + log.add(line) + elif ev == "exit": + try: + exit_code = int(json.loads(data).get("code", 0)) + except (ValueError, KeyError): + pass + log.finish() + if exit_code != 0: + error(f"Step '{step_name}' failed with exit code {exit_code}") + raise RuntimeError(f"Step '{step_name}' failed with exit code {exit_code}") diff --git a/build/lib/truffile/deploy/steps/files.py b/build/lib/truffile/deploy/steps/files.py new file mode 100644 index 0000000..d50a726 --- /dev/null +++ b/build/lib/truffile/deploy/steps/files.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from truffile.transport.client import TruffleClient + + +async def _upload_file( + client: TruffleClient, + src: Path, + dest: str, + spinner_cls: Any, + arrow: str, + color_dim: str, + color_reset: str, +) -> None: + spinner = spinner_cls(f"Uploading {src.name} {arrow} {dest}") + spinner.start() + result = await client.upload(src, dest) + spinner.stop(success=True) + print(f" {color_dim}{result.bytes} bytes, sha256={result.sha256[:12]}...{color_reset}") + + +async def handle_files( + step: dict[str, Any], + *, + client: TruffleClient, + app_dir: Path, + spinner_cls: Any, + arrow: str, + color_dim: str, + color_reset: str, + **_kw: Any, +) -> None: + for f in step.get("files", []): + src = app_dir / f["source"] + dest = f["destination"] + + if src.is_dir(): + for child in sorted(src.rglob("*")): + if child.is_file() and "__pycache__" not in str(child): + rel = child.relative_to(src) + child_dest = f"{dest.rstrip('/')}/{rel}" + await _upload_file(client, child, child_dest, spinner_cls, arrow, color_dim, color_reset) + elif src.is_file(): + await _upload_file(client, src, dest, spinner_cls, arrow, color_dim, color_reset) + else: + raise FileNotFoundError(f"no such file: {src}") diff --git a/build/lib/truffile/deploy/steps/text.py b/build/lib/truffile/deploy/steps/text.py new file mode 100644 index 0000000..1ffcff5 --- /dev/null +++ b/build/lib/truffile/deploy/steps/text.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import getpass +import json +from typing import Any, Callable + +from truffile.transport.client import TruffleClient + + +def _build_env_prefix(env: dict[str, str]) -> str: + if not env: + return "" + parts = [] + for key, val in env.items(): + escaped = val.replace("'", "'\\''") + parts.append(f"{key}='{escaped}'") + return " ".join(parts) + " " + + +async def handle_text( + step: dict[str, Any], + *, + client: TruffleClient, + exec_cwd: str, + info: Callable, + error: Callable, + scrolling_log_cls: Any, + collected_env: dict[str, str], + **_kw: Any, +) -> None: + step_name = step.get("name", "Configure") + content = step.get("content", "") + if content: + print() + info(step_name) + print(content.strip()) + + for field in step.get("fields", []): + label = field.get("label", field.get("name", "")) + field_type = field.get("type", "text") + placeholder = field.get("placeholder", "") + default = field.get("default", "") + prompt = f" {label}" + if placeholder: + prompt += f" ({placeholder})" + prompt += ": " + + if field_type == "password": + value = getpass.getpass(prompt) + else: + value = input(prompt) + + if not value.strip() and default: + value = default + if not value.strip() and field.get("env_default_if_empty"): + value = field["env_default_if_empty"] + + env_key = field.get("env") + if env_key: + collected_env[env_key] = value + + # run validator if present โ€” prepend env vars to the command + # since each exec call is a fresh shell + validator = step.get("validator") + if not validator: + return + + validator_cmd = validator.get("run", "") + if not validator_cmd: + return + + env_prefix = _build_env_prefix(collected_env) + full_cmd = f"{env_prefix}{validator_cmd}" + + info("Validating...") + log = scrolling_log_cls(height=4, prefix=" ") + exit_code = 0 + async for ev, data in client.exec_stream(full_cmd, cwd=exec_cwd): + if ev == "log": + try: + line = json.loads(data).get("line", "") + except Exception: + line = data + log.add(line) + elif ev == "exit": + try: + exit_code = int(json.loads(data).get("code", 0)) + except (ValueError, KeyError): + pass + log.finish() + + if exit_code != 0: + error_msg = validator.get("error_message", "Validation failed. Check your input.") + error(error_msg.strip()) + raise RuntimeError(f"Validator failed for step '{step_name}'") diff --git a/build/lib/truffile/schedule.py b/build/lib/truffile/schedule.py new file mode 100644 index 0000000..bb90bb9 --- /dev/null +++ b/build/lib/truffile/schedule.py @@ -0,0 +1,215 @@ +import re +from typing import Any, Dict, List, Optional, Tuple +from google.protobuf.duration_pb2 import Duration +from truffle.app import background_pb2 + +_DAY_BIT = { + "sat": 0, + "fri": 1, + "thu": 2, + "wed": 3, + "tue": 4, + "mon": 5, + "sun": 6, +} + +_TIME_RE = re.compile(r"^\s*(\d{1,2}):(\d{2})(?::(\d{2}))?\s*$") + +def _parse_time_of_day(s: str, *, ctx: str): + m = _TIME_RE.match(s or "") + if not m: + raise ValueError(f"{ctx}: invalid time '{s}', expected HH:MM or HH:MM:SS") + hh = int(m.group(1)) + mm = int(m.group(2)) + ss = int(m.group(3) or "0") + if not (0 <= hh <= 23): raise ValueError(f"{ctx}: hour out of range: {hh}") + if not (0 <= mm <= 59): raise ValueError(f"{ctx}: minute out of range: {mm}") + if not (0 <= ss <= 59): raise ValueError(f"{ctx}: second out of range: {ss}") + return hh, mm, ss + +def _set_time_of_day(msg_time_of_day, s: str, *, ctx: str) -> None: + hh, mm, ss = _parse_time_of_day(s, ctx=ctx) + msg_time_of_day.hour = hh + msg_time_of_day.minute = mm + msg_time_of_day.second = ss + +def _parse_daily_window(v: Any, *, ctx: str) -> Optional[Tuple[str, str]]: + if v is None: + return None + if isinstance(v, str): + parts = v.split("-", 1) + if len(parts) != 2: + raise ValueError(f"{ctx}: daily_window must be 'HH:MM-HH:MM[:SS]'") + start_s = parts[0].strip() + end_s = parts[1].strip() + _parse_time_of_day(start_s, ctx=f"{ctx}.daily_window start") + _parse_time_of_day(end_s, ctx=f"{ctx}.daily_window end") + return start_s, end_s + if isinstance(v, dict): + start_s = v.get("start") + end_s = v.get("end") + if not isinstance(start_s, str) or not isinstance(end_s, str): + raise ValueError(f"{ctx}: daily_window dict must have string start/end") + _parse_time_of_day(start_s, ctx=f"{ctx}.daily_window start") + _parse_time_of_day(end_s, ctx=f"{ctx}.daily_window end") + return start_s, end_s + raise ValueError(f"{ctx}: daily_window must be string or object") + +def _day_mask_from_allowed_days(days: List[str], *, ctx: str) -> int: + forbidden = 0 + allowed_bits = set() + for d in days: + if not isinstance(d, str): + raise ValueError(f"{ctx}: day entries must be strings") + k = d.strip().lower()[:3] + if k not in _DAY_BIT: + raise ValueError(f"{ctx}: unknown day '{d}' (use sun/mon/tue/wed/thu/fri/sat)") + allowed_bits.add(_DAY_BIT[k]) + if not allowed_bits: + raise ValueError(f"{ctx}: allowed_days cannot be empty") + for k, bit in _DAY_BIT.items(): + if bit not in allowed_bits: + forbidden |= (1 << bit) + return forbidden + +def _day_mask_from_forbidden_days(days: List[str], *, ctx: str) -> int: + forbidden = 0 + for d in days: + if not isinstance(d, str): + raise ValueError(f"{ctx}: day entries must be strings") + k = d.strip().lower()[:3] + if k not in _DAY_BIT: + raise ValueError(f"{ctx}: unknown day '{d}' (use sun/mon/tue/wed/thu/fri/sat)") + forbidden |= (1 << _DAY_BIT[k]) + if forbidden == 0b1111111: + raise ValueError(f"{ctx}: forbidden_days forbids all days (invalid)") + return forbidden + +_DUR_RE = re.compile(r"^\s*(\d+)\s*(ms|s|m|h|d)\s*$", re.IGNORECASE) + +def _parse_duration(s: str, *, ctx: str) -> Duration: + if not isinstance(s, str): + raise ValueError(f"{ctx}: duration must be a string like '15m' or '2h'") + m = _DUR_RE.match(s) + if not m: + raise ValueError(f"{ctx}: invalid duration '{s}' (use ms/s/m/h/d)") + n = int(m.group(1)) + unit = m.group(2).lower() + seconds = 0 + nanos = 0 + if unit == "ms": + seconds = n // 1000 + nanos = (n % 1000) * 1_000_000 + elif unit == "s": + seconds = n + elif unit == "m": + seconds = n * 60 + elif unit == "h": + seconds = n * 3600 + elif unit == "d": + seconds = n * 86400 + dur = Duration() + dur.seconds = seconds + dur.nanos = nanos + return dur + + +def parse_runtime_policy(schedule_cfg_data: Dict[str, Any]) -> background_pb2.BackgroundAppRuntimePolicy: + if not isinstance(schedule_cfg_data, dict): + raise ValueError("default_schedule must be an object") + + policy_type = schedule_cfg_data.get("type") + if policy_type not in ("interval", "times", "always"): + raise ValueError(f"Invalid default_schedule.type: {policy_type}") + + runtime_policy = background_pb2.BackgroundAppRuntimePolicy() + + if policy_type == "always": + runtime_policy.always.SetInParent() + return runtime_policy + + if policy_type == "interval": + interval_obj = schedule_cfg_data.get("interval") + if not isinstance(interval_obj, dict): + raise ValueError("default_schedule.interval must be an object") + + dur_s = interval_obj.get("duration", None) + if not isinstance(dur_s, str): + raise ValueError("default_schedule.interval.duration must be a string") + runtime_policy.interval.duration.CopyFrom(_parse_duration(dur_s, ctx="default_schedule.interval.duration")) + + sched = interval_obj.get("schedule", {}) + if sched is None: + sched = {} + if not isinstance(sched, dict): + raise ValueError("default_schedule.interval.schedule must be an object") + + allowed_days = sched.get("allowed_days") + forbidden_days = sched.get("forbidden_days") + if allowed_days is not None and forbidden_days is not None: + raise ValueError("Provide only one of schedule.allowed_days or schedule.forbidden_days") + + if allowed_days is not None: + if not isinstance(allowed_days, list): + raise ValueError("schedule.allowed_days must be a list") + runtime_policy.interval.schedule.weekly_window.day_mask = _day_mask_from_allowed_days( + allowed_days, ctx="default_schedule.interval.schedule.allowed_days" + ) + elif forbidden_days is not None: + if not isinstance(forbidden_days, list): + raise ValueError("schedule.forbidden_days must be a list") + runtime_policy.interval.schedule.weekly_window.day_mask = _day_mask_from_forbidden_days( + forbidden_days, ctx="default_schedule.interval.schedule.forbidden_days" + ) + else: + runtime_policy.interval.schedule.weekly_window.day_mask = 0 + + dw = _parse_daily_window(sched.get("daily_window"), ctx="default_schedule.interval.schedule") + if dw is not None: + start_s, end_s = dw + runtime_policy.interval.schedule.daily_window.SetInParent() + _set_time_of_day(runtime_policy.interval.schedule.daily_window.daily_start_time, start_s, + ctx="default_schedule.interval.schedule.daily_window.start") + _set_time_of_day(runtime_policy.interval.schedule.daily_window.daily_end_time, end_s, + ctx="default_schedule.interval.schedule.daily_window.end") + + return runtime_policy + + if policy_type == "times": + times_obj = schedule_cfg_data.get("times") + if not isinstance(times_obj, dict): + raise ValueError("default_schedule.times must be an object") + + run_times = times_obj.get("run_times") + if not isinstance(run_times, list) or not run_times: + raise ValueError("default_schedule.times.run_times must be a non-empty list of time strings") + + for i, t in enumerate(run_times): + if not isinstance(t, str): + raise ValueError("default_schedule.times.run_times must contain strings") + tod = runtime_policy.times.run_times.add() + _set_time_of_day(tod, t, ctx=f"default_schedule.times.run_times[{i}]") + + allowed_days = times_obj.get("allowed_days") + forbidden_days = times_obj.get("forbidden_days") + if allowed_days is not None and forbidden_days is not None: + raise ValueError("Provide only one of times.allowed_days or times.forbidden_days") + + if allowed_days is not None: + if not isinstance(allowed_days, list): + raise ValueError("times.allowed_days must be a list") + runtime_policy.times.weekly_window.day_mask = _day_mask_from_allowed_days( + allowed_days, ctx="default_schedule.times.allowed_days" + ) + elif forbidden_days is not None: + if not isinstance(forbidden_days, list): + raise ValueError("times.forbidden_days must be a list") + runtime_policy.times.weekly_window.day_mask = _day_mask_from_forbidden_days( + forbidden_days, ctx="default_schedule.times.forbidden_days" + ) + else: + runtime_policy.times.weekly_window.day_mask = 0 + + return runtime_policy + + raise RuntimeError("unreachable") diff --git a/build/lib/truffile/schema/__init__.py b/build/lib/truffile/schema/__init__.py new file mode 100644 index 0000000..278b425 --- /dev/null +++ b/build/lib/truffile/schema/__init__.py @@ -0,0 +1,4 @@ +from .app_config import validate_app_dir +from .runtime_policy import parse_runtime_policy + +__all__ = ["validate_app_dir", "parse_runtime_policy"] diff --git a/build/lib/truffile/schema/app_config.py b/build/lib/truffile/schema/app_config.py new file mode 100644 index 0000000..29bdd3e --- /dev/null +++ b/build/lib/truffile/schema/app_config.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +import ast +import re +from pathlib import Path +from typing import Any + +import yaml + + +def _check_python_syntax(file_path: Path) -> tuple[bool, str]: + try: + source = file_path.read_text(encoding="utf-8") + ast.parse(source) + return True, "" + except SyntaxError as e: + return False, f"Line {e.lineno}: {e.msg}" + + +_ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +def _validate_process_cfg( + process: Any, + *, + path: str, + warnings: list[str], + errors: list[str], +) -> None: + if not isinstance(process, dict): + errors.append(f"{path} must be an object") + return + + cmd = process.get("cmd") + if not isinstance(cmd, list) or len(cmd) == 0: + errors.append(f"{path}.cmd must be a non-empty list") + elif not all(isinstance(v, str) and v.strip() for v in cmd): + errors.append(f"{path}.cmd must be list[str] with non-empty values") + + for key in ("working_directory", "cwd"): + if key in process and not isinstance(process.get(key), str): + errors.append(f"{path}.{key} must be a string") + + env_obj = process.get("environment", process.get("env")) + if env_obj is None: + return + if not isinstance(env_obj, dict): + errors.append(f"{path}.environment must be a map") + return + for k, v in env_obj.items(): + if not isinstance(k, str): + errors.append(f"{path}.environment keys must be strings") + continue + if not _ENV_KEY_RE.match(k): + warnings.append(f"{path}.environment key '{k}' is non-standard") + if not isinstance(v, str): + errors.append(f"{path}.environment['{k}'] must be a string") + + +def validate_app_dir(app_dir: Path) -> tuple[bool, dict[str, Any] | None, str | None, list[str], list[str]]: + """Validate app directory and return (valid, config, app_type, warnings, errors).""" + warnings: list[str] = [] + errors: list[str] = [] + + truffile_path = app_dir / "truffile.yaml" + if not truffile_path.exists(): + errors.append(f"No truffile.yaml found in {app_dir}") + return False, None, None, warnings, errors + + try: + config = yaml.safe_load(truffile_path.read_text(encoding="utf-8")) + except yaml.YAMLError as e: + errors.append(f"Invalid truffile.yaml: {e}") + return False, None, None, warnings, errors + + if not isinstance(config, dict): + errors.append("truffile.yaml root must be a mapping") + return False, None, None, warnings, errors + + meta = config.get("metadata", {}) + if not isinstance(meta, dict): + errors.append("metadata must be a mapping") + return False, None, None, warnings, errors + + if not meta.get("name"): + errors.append("metadata.name is required in truffile.yaml") + return False, None, None, warnings, errors + + fg_cfg = meta.get("foreground") + bg_cfg = meta.get("background") + has_fg_cfg = isinstance(fg_cfg, dict) + has_bg_cfg = isinstance(bg_cfg, dict) + if has_fg_cfg or has_bg_cfg: + if has_fg_cfg and has_bg_cfg: + app_type = "hybrid" + elif has_fg_cfg: + app_type = "focus" + else: + app_type = "ambient" + else: + cfg_type = str(meta.get("type", "")).lower().strip() + if cfg_type in ("background", "ambient"): + app_type = "ambient" + elif cfg_type in ("foreground", "focus"): + app_type = "focus" + else: + app_type = "focus" + warnings.append("No type specified in truffile.yaml, defaulting to focus") + + if "bundle_id" not in meta: + warnings.append("No metadata.bundle_id specified; using derived default from metadata.name") + + if has_fg_cfg: + process = fg_cfg.get("process") + _validate_process_cfg( + process, + path="metadata.foreground.process", + warnings=warnings, + errors=errors, + ) + + if has_bg_cfg: + process = bg_cfg.get("process") + _validate_process_cfg( + process, + path="metadata.background.process", + warnings=warnings, + errors=errors, + ) + if not isinstance(bg_cfg.get("default_schedule"), dict): + errors.append("metadata.background.default_schedule must be an object") + if not has_fg_cfg and not has_bg_cfg: + process = meta.get("process") + _validate_process_cfg( + process, + path="metadata.process", + warnings=warnings, + errors=errors, + ) + if app_type == "ambient" and "default_schedule" in meta and not isinstance(meta.get("default_schedule"), dict): + errors.append("metadata.default_schedule must be an object when provided") + + icon_file = meta.get("icon_file") + if icon_file: + icon_path = app_dir / str(icon_file) + if not icon_path.exists(): + warnings.append(f"Icon file not found: {icon_file}") + else: + warnings.append("No icon specified in truffile.yaml") + + files_to_check: list[dict[str, Any]] = [] + for step in config.get("steps", []): + if isinstance(step, dict) and step.get("type") == "files": + step_files = step.get("files", []) + if isinstance(step_files, list): + files_to_check.extend([f for f in step_files if isinstance(f, dict)]) + + top_files = config.get("files", []) + if isinstance(top_files, list): + files_to_check.extend([f for f in top_files if isinstance(f, dict)]) + + for f in files_to_check: + source = f.get("source") + if not isinstance(source, str): + errors.append("files entries must include a string 'source'") + continue + + src = app_dir / source + if not src.exists(): + errors.append(f"Source file not found: {src}") + continue + + if src.suffix == ".py": + ok, err = _check_python_syntax(src) + if not ok: + errors.append(f"Syntax error in {src.name}: {err}") + + return len(errors) == 0, config, app_type, warnings, errors diff --git a/build/lib/truffile/schema/runtime_policy.py b/build/lib/truffile/schema/runtime_policy.py new file mode 100644 index 0000000..bb90bb9 --- /dev/null +++ b/build/lib/truffile/schema/runtime_policy.py @@ -0,0 +1,215 @@ +import re +from typing import Any, Dict, List, Optional, Tuple +from google.protobuf.duration_pb2 import Duration +from truffle.app import background_pb2 + +_DAY_BIT = { + "sat": 0, + "fri": 1, + "thu": 2, + "wed": 3, + "tue": 4, + "mon": 5, + "sun": 6, +} + +_TIME_RE = re.compile(r"^\s*(\d{1,2}):(\d{2})(?::(\d{2}))?\s*$") + +def _parse_time_of_day(s: str, *, ctx: str): + m = _TIME_RE.match(s or "") + if not m: + raise ValueError(f"{ctx}: invalid time '{s}', expected HH:MM or HH:MM:SS") + hh = int(m.group(1)) + mm = int(m.group(2)) + ss = int(m.group(3) or "0") + if not (0 <= hh <= 23): raise ValueError(f"{ctx}: hour out of range: {hh}") + if not (0 <= mm <= 59): raise ValueError(f"{ctx}: minute out of range: {mm}") + if not (0 <= ss <= 59): raise ValueError(f"{ctx}: second out of range: {ss}") + return hh, mm, ss + +def _set_time_of_day(msg_time_of_day, s: str, *, ctx: str) -> None: + hh, mm, ss = _parse_time_of_day(s, ctx=ctx) + msg_time_of_day.hour = hh + msg_time_of_day.minute = mm + msg_time_of_day.second = ss + +def _parse_daily_window(v: Any, *, ctx: str) -> Optional[Tuple[str, str]]: + if v is None: + return None + if isinstance(v, str): + parts = v.split("-", 1) + if len(parts) != 2: + raise ValueError(f"{ctx}: daily_window must be 'HH:MM-HH:MM[:SS]'") + start_s = parts[0].strip() + end_s = parts[1].strip() + _parse_time_of_day(start_s, ctx=f"{ctx}.daily_window start") + _parse_time_of_day(end_s, ctx=f"{ctx}.daily_window end") + return start_s, end_s + if isinstance(v, dict): + start_s = v.get("start") + end_s = v.get("end") + if not isinstance(start_s, str) or not isinstance(end_s, str): + raise ValueError(f"{ctx}: daily_window dict must have string start/end") + _parse_time_of_day(start_s, ctx=f"{ctx}.daily_window start") + _parse_time_of_day(end_s, ctx=f"{ctx}.daily_window end") + return start_s, end_s + raise ValueError(f"{ctx}: daily_window must be string or object") + +def _day_mask_from_allowed_days(days: List[str], *, ctx: str) -> int: + forbidden = 0 + allowed_bits = set() + for d in days: + if not isinstance(d, str): + raise ValueError(f"{ctx}: day entries must be strings") + k = d.strip().lower()[:3] + if k not in _DAY_BIT: + raise ValueError(f"{ctx}: unknown day '{d}' (use sun/mon/tue/wed/thu/fri/sat)") + allowed_bits.add(_DAY_BIT[k]) + if not allowed_bits: + raise ValueError(f"{ctx}: allowed_days cannot be empty") + for k, bit in _DAY_BIT.items(): + if bit not in allowed_bits: + forbidden |= (1 << bit) + return forbidden + +def _day_mask_from_forbidden_days(days: List[str], *, ctx: str) -> int: + forbidden = 0 + for d in days: + if not isinstance(d, str): + raise ValueError(f"{ctx}: day entries must be strings") + k = d.strip().lower()[:3] + if k not in _DAY_BIT: + raise ValueError(f"{ctx}: unknown day '{d}' (use sun/mon/tue/wed/thu/fri/sat)") + forbidden |= (1 << _DAY_BIT[k]) + if forbidden == 0b1111111: + raise ValueError(f"{ctx}: forbidden_days forbids all days (invalid)") + return forbidden + +_DUR_RE = re.compile(r"^\s*(\d+)\s*(ms|s|m|h|d)\s*$", re.IGNORECASE) + +def _parse_duration(s: str, *, ctx: str) -> Duration: + if not isinstance(s, str): + raise ValueError(f"{ctx}: duration must be a string like '15m' or '2h'") + m = _DUR_RE.match(s) + if not m: + raise ValueError(f"{ctx}: invalid duration '{s}' (use ms/s/m/h/d)") + n = int(m.group(1)) + unit = m.group(2).lower() + seconds = 0 + nanos = 0 + if unit == "ms": + seconds = n // 1000 + nanos = (n % 1000) * 1_000_000 + elif unit == "s": + seconds = n + elif unit == "m": + seconds = n * 60 + elif unit == "h": + seconds = n * 3600 + elif unit == "d": + seconds = n * 86400 + dur = Duration() + dur.seconds = seconds + dur.nanos = nanos + return dur + + +def parse_runtime_policy(schedule_cfg_data: Dict[str, Any]) -> background_pb2.BackgroundAppRuntimePolicy: + if not isinstance(schedule_cfg_data, dict): + raise ValueError("default_schedule must be an object") + + policy_type = schedule_cfg_data.get("type") + if policy_type not in ("interval", "times", "always"): + raise ValueError(f"Invalid default_schedule.type: {policy_type}") + + runtime_policy = background_pb2.BackgroundAppRuntimePolicy() + + if policy_type == "always": + runtime_policy.always.SetInParent() + return runtime_policy + + if policy_type == "interval": + interval_obj = schedule_cfg_data.get("interval") + if not isinstance(interval_obj, dict): + raise ValueError("default_schedule.interval must be an object") + + dur_s = interval_obj.get("duration", None) + if not isinstance(dur_s, str): + raise ValueError("default_schedule.interval.duration must be a string") + runtime_policy.interval.duration.CopyFrom(_parse_duration(dur_s, ctx="default_schedule.interval.duration")) + + sched = interval_obj.get("schedule", {}) + if sched is None: + sched = {} + if not isinstance(sched, dict): + raise ValueError("default_schedule.interval.schedule must be an object") + + allowed_days = sched.get("allowed_days") + forbidden_days = sched.get("forbidden_days") + if allowed_days is not None and forbidden_days is not None: + raise ValueError("Provide only one of schedule.allowed_days or schedule.forbidden_days") + + if allowed_days is not None: + if not isinstance(allowed_days, list): + raise ValueError("schedule.allowed_days must be a list") + runtime_policy.interval.schedule.weekly_window.day_mask = _day_mask_from_allowed_days( + allowed_days, ctx="default_schedule.interval.schedule.allowed_days" + ) + elif forbidden_days is not None: + if not isinstance(forbidden_days, list): + raise ValueError("schedule.forbidden_days must be a list") + runtime_policy.interval.schedule.weekly_window.day_mask = _day_mask_from_forbidden_days( + forbidden_days, ctx="default_schedule.interval.schedule.forbidden_days" + ) + else: + runtime_policy.interval.schedule.weekly_window.day_mask = 0 + + dw = _parse_daily_window(sched.get("daily_window"), ctx="default_schedule.interval.schedule") + if dw is not None: + start_s, end_s = dw + runtime_policy.interval.schedule.daily_window.SetInParent() + _set_time_of_day(runtime_policy.interval.schedule.daily_window.daily_start_time, start_s, + ctx="default_schedule.interval.schedule.daily_window.start") + _set_time_of_day(runtime_policy.interval.schedule.daily_window.daily_end_time, end_s, + ctx="default_schedule.interval.schedule.daily_window.end") + + return runtime_policy + + if policy_type == "times": + times_obj = schedule_cfg_data.get("times") + if not isinstance(times_obj, dict): + raise ValueError("default_schedule.times must be an object") + + run_times = times_obj.get("run_times") + if not isinstance(run_times, list) or not run_times: + raise ValueError("default_schedule.times.run_times must be a non-empty list of time strings") + + for i, t in enumerate(run_times): + if not isinstance(t, str): + raise ValueError("default_schedule.times.run_times must contain strings") + tod = runtime_policy.times.run_times.add() + _set_time_of_day(tod, t, ctx=f"default_schedule.times.run_times[{i}]") + + allowed_days = times_obj.get("allowed_days") + forbidden_days = times_obj.get("forbidden_days") + if allowed_days is not None and forbidden_days is not None: + raise ValueError("Provide only one of times.allowed_days or times.forbidden_days") + + if allowed_days is not None: + if not isinstance(allowed_days, list): + raise ValueError("times.allowed_days must be a list") + runtime_policy.times.weekly_window.day_mask = _day_mask_from_allowed_days( + allowed_days, ctx="default_schedule.times.allowed_days" + ) + elif forbidden_days is not None: + if not isinstance(forbidden_days, list): + raise ValueError("times.forbidden_days must be a list") + runtime_policy.times.weekly_window.day_mask = _day_mask_from_forbidden_days( + forbidden_days, ctx="default_schedule.times.forbidden_days" + ) + else: + runtime_policy.times.weekly_window.day_mask = 0 + + return runtime_policy + + raise RuntimeError("unreachable") diff --git a/build/lib/truffile/sdk.py b/build/lib/truffile/sdk.py new file mode 100644 index 0000000..e756f99 --- /dev/null +++ b/build/lib/truffile/sdk.py @@ -0,0 +1,75 @@ +# truffile SDK โ€” re-exports from app_runtime under the truffile namespace. +# +# usage: +# from truffile.sdk import ForegroundApp, tool, ok, err +# from truffile.sdk import BackgroundWorkerApp +# from truffile.sdk import AppHarness, FakeHttpTransport + +from truffile.app_runtime import ( + # app authoring + ForegroundApp, + BackgroundApp, + BackgroundWorkerApp, + ToolSpec, + Submission, + ok, + err, + phosphor_icon_url, + + # auth + OAuth, + OAuthAuth, + PublicAuth, + TextConfigAuth, + VncAuth, + load_required_env, + + # protocols + ApiKeyProvider, + AuthProvider, + BrowserSessionProvider, + HttpResponse, + HttpTransport, + OAuthProvider, + TokenStore, + CookieStore, + + # stores + FileTokenStore, + MemoryTokenStore, + FileCookieStore, + MemoryCookieStore, + + # errors + AppAuthError, + AppRuntimeFailure, + AppRuntimeErrorType, + + # testing + AppHarness, + FakeApiKeyProvider, + FakeAuthProvider, + FakeBackgroundRuntime, + FakeHttpResponse, + FakeHttpTransport, + FakeOAuthProvider, + HarnessResult, + RecordedBackgroundError, + RecordedSubmission, + make_background_ctx, + + # mcp testing + McpTestServer, + call_tool, + InProcessGrpcServer, + + # utilities + parse_jsonrpc_payload, + HttpxResponseAdapter, + report_app_error, + truncate_result, + truncate_items, +) + +# nice alias โ€” truffile.sdk.tool is ToolSpec +tool = ToolSpec diff --git a/build/lib/truffile/storage.py b/build/lib/truffile/storage.py new file mode 100644 index 0000000..a1640fb --- /dev/null +++ b/build/lib/truffile/storage.py @@ -0,0 +1,95 @@ +import json +import platformdirs +from pathlib import Path +from dataclasses import dataclass, field + + +@dataclass +class StoredDevice: + name: str + token: str + + +@dataclass +class StoredState: + devices: list[StoredDevice] = field(default_factory=list) + last_used_device: str | None = None + client_user_id: str | None = None + + +def get_storage_dir() -> Path: + dir_path = Path(platformdirs.user_data_dir("truffile")) + dir_path.mkdir(parents=True, exist_ok=True) + return dir_path + + +class StorageService: + def __init__(self): + self.storage_dir = get_storage_dir() + self.state_file = self.storage_dir / "state.json" + self.state = self._load_state() + + def _load_state(self) -> StoredState: + if not self.state_file.exists(): + return StoredState() + try: + with open(self.state_file, "r") as f: + data = json.load(f) + devices = [StoredDevice(**d) for d in data.get("devices", [])] + return StoredState( + devices=devices, + last_used_device=data.get("last_used_device"), + client_user_id=data.get("client_user_id"), + ) + except (json.JSONDecodeError, KeyError): + return StoredState() + + def save(self) -> None: + state_dict = { + "devices": [{"name": d.name, "token": d.token} for d in self.state.devices], + "last_used_device": self.state.last_used_device, + "client_user_id": self.state.client_user_id, + } + self.state_file.parent.mkdir(parents=True, exist_ok=True) + with open(self.state_file, "w") as f: + json.dump(state_dict, f, indent=4) + + def get_token(self, device_name: str) -> str | None: + for device in self.state.devices: + if device.name == device_name: + return device.token + return None + + def has_token(self, device_name: str) -> bool: + token = self.get_token(device_name) + return token is not None and len(token) > 0 + + def set_token(self, device_name: str, token: str) -> None: + for device in self.state.devices: + if device.name == device_name: + device.token = token + self.save() + return + self.state.devices.append(StoredDevice(name=device_name, token=token)) + self.save() + + def set_last_used(self, device_name: str) -> None: + self.state.last_used_device = device_name + self.save() + + def remove_device(self, device_name: str) -> bool: + for i, device in enumerate(self.state.devices): + if device.name == device_name: + self.state.devices.pop(i) + if self.state.last_used_device == device_name: + self.state.last_used_device = None + self.save() + return True + return False + + def clear_all(self) -> None: + self.state = StoredState() + self.save() + + def list_devices(self) -> list[str]: + return [d.name for d in self.state.devices] diff --git a/build/lib/truffile/transport/__init__.py b/build/lib/truffile/transport/__init__.py new file mode 100644 index 0000000..60b0d18 --- /dev/null +++ b/build/lib/truffile/transport/__init__.py @@ -0,0 +1,15 @@ +from .client import ( + ExecResult, + NewSessionStatus, + TruffleClient, + UploadResult, + resolve_mdns, +) + +__all__ = [ + "ExecResult", + "NewSessionStatus", + "TruffleClient", + "UploadResult", + "resolve_mdns", +] diff --git a/build/lib/truffile/transport/client.py b/build/lib/truffile/transport/client.py new file mode 100644 index 0000000..ba89b9a --- /dev/null +++ b/build/lib/truffile/transport/client.py @@ -0,0 +1,520 @@ +import asyncio +import json +import platform +import socket +from dataclasses import dataclass +from pathlib import Path +from typing import AsyncIterator +import grpc +from grpc import aio +import httpx +from google.protobuf import empty_pb2 +from truffle.os.truffleos_pb2_grpc import TruffleOSStub +from truffle.os.builder_pb2 import ( + StartBuildSessionRequest, + StartBuildSessionResponse, + FinishBuildSessionRequest, + FinishBuildSessionResponse, +) +from truffle.os.client_session_pb2 import ( + RegisterNewSessionRequest, + RegisterNewSessionResponse, + NewSessionStatus, +) +from truffle.os.client_metadata_pb2 import ClientMetadata +from truffle.os.app_queries_pb2 import GetAllAppsRequest, GetAllAppsResponse, DeleteAppRequest, DeleteAppResponse +from truffle.app.app_pb2 import App +from truffle.app.background_pb2 import BackgroundApp, BackgroundAppRuntimePolicy +from truffle.os.task_actions_pb2 import ( + OpenTaskRequest, + InterruptTaskRequest, + TaskRenameRequest, + TaskDeleteRequest, + TaskSetAvailableAppsRequest, +) +from truffle.os.task_user_response_pb2 import RespondToTaskRequest +from truffle.os.task_queries_pb2 import GetTaskInfosRequest +from truffile.schedule import parse_runtime_policy + +GRPC_MAX_MESSAGE_BYTES = 32 * 1024 * 1024 + + +def get_client_metadata() -> ClientMetadata: + from truffile import __version__ + metadata = ClientMetadata() + metadata.device = platform.node() + metadata.platform = platform.platform() + metadata.version = f"truffile-{__version__}-{platform.python_version()}" + return metadata + + +async def resolve_mdns(hostname: str) -> str: + if ".local" not in hostname: + return hostname + loop = asyncio.get_event_loop() + try: + resolved = await loop.run_in_executor(None, socket.gethostbyname, hostname) + return resolved + except socket.gaierror as e: + raise RuntimeError(f"Failed to resolve {hostname} - is the device on the same network? ({e})") + + +@dataclass +class ExecResult: + exit_code: int + output: list[str] + + +@dataclass +class UploadResult: + path: str + bytes: int + sha256: str + + +class TruffleClient: + def __init__(self, address: str, token: str): + self.address = address + self.token = token + self.channel: aio.Channel | None = None + self.stub: TruffleOSStub | None = None + self.app_uuid: str | None = None + self.access_path: str | None = None + + @property + def http_base(self) -> str | None: + if not self.access_path: + return None + host = self.address if "://" in self.address else f"http://{self.address}" + return f"{host}/containers/{self.access_path}" + + @property + def _metadata(self) -> list: + return [("session", self.token)] + + async def connect(self, timeout: float = 15.0): + self.channel = aio.insecure_channel( + self.address, + options=[ + ("grpc.max_receive_message_length", GRPC_MAX_MESSAGE_BYTES), + ("grpc.max_send_message_length", GRPC_MAX_MESSAGE_BYTES), + ], + ) + await asyncio.wait_for(self.channel.channel_ready(), timeout=timeout) + self.stub = TruffleOSStub(self.channel) + + def update_token(self, token: str): + self.token = token + + async def check_auth(self) -> bool: + if not self.stub or not self.token: + return False + try: + await self.stub.System_GetInfo(empty_pb2.Empty(), metadata=self._metadata) + return True + except aio.AioRpcError as e: + if e.code() == grpc.StatusCode.UNAUTHENTICATED: + return False + raise + + async def register_new_session(self, user_id: str) -> tuple[NewSessionStatus, str | None]: + if not self.stub: + raise RuntimeError("not connected") + req = RegisterNewSessionRequest() + req.user_id = user_id + req.metadata.CopyFrom(get_client_metadata()) + resp: RegisterNewSessionResponse = await self.stub.Client_RegisterNewSession(req) + if resp.status.error == NewSessionStatus.NEW_SESSION_SUCCESS: + self.token = resp.token + return resp.status, resp.token + return resp.status, None + + async def get_all_apps(self) -> list[App]: + if not self.stub: + raise RuntimeError("not connected") + req = GetAllAppsRequest() + resp: GetAllAppsResponse = await self.stub.Apps_GetAll(req, metadata=self._metadata) + return list(resp.apps) + + async def delete_app(self, app_uuid: str) -> DeleteAppResponse: + if not self.stub: + raise RuntimeError("not connected") + req = DeleteAppRequest() + req.app_uuid = app_uuid + resp: DeleteAppResponse = await self.stub.Apps_DeleteApp(req, metadata=self._metadata) + return resp + + async def start_build(self) -> StartBuildSessionResponse: + if not self.stub: + raise RuntimeError("not connected") + req = StartBuildSessionRequest() + resp: StartBuildSessionResponse = await self.stub.Builder_StartBuildSession( + req, metadata=self._metadata + ) + self.app_uuid = resp.app_uuid + self.access_path = resp.access_path + return resp + + @staticmethod + def _build_bundle_id(name: str) -> str: + raw = "".join(ch.lower() if ch.isalnum() else "." for ch in name).strip(".") + normalized = ".".join([part for part in raw.split(".") if part]) + return normalized or "truffle.app" + + def _apply_metadata( + self, + *, + req: FinishBuildSessionRequest, + name: str, + bundle_id: str | None, + description: str, + icon: str | Path | bytes | None, + ) -> None: + req.metadata.name = name + req.metadata.bundle_id = (bundle_id or self._build_bundle_id(name)).strip() + if description: + req.metadata.description = description + icon_data = self._load_icon(icon) + if icon_data: + req.metadata.icon.png_data = icon_data + + @staticmethod + def _apply_process(process_pb, *, cmd: str, args: list[str], cwd: str, env: list[str] | None) -> None: + process_pb.cmd = cmd + process_pb.args.extend(args) + if env: + process_pb.env.extend(env) + process_pb.cwd = cwd + + async def _sse_events(self, client: httpx.AsyncClient, url: str, body: dict) -> AsyncIterator[tuple[str, str]]: + async with client.stream("POST", url, json=body, timeout=None) as r: + r.raise_for_status() + event = "message" + data_parts = [] + async for raw in r.aiter_lines(): + if raw is None: + continue + line = raw.rstrip("\r") + if line == "": + if data_parts: + yield event, "\n".join(data_parts) + event, data_parts = "message", [] + continue + if line.startswith(":"): + continue + if line.startswith("event:"): + event = line[6:].strip() + elif line.startswith("data:"): + data_parts.append(line[5:].lstrip()) + if data_parts: + yield event, "\n".join(data_parts) + + async def exec(self, cmd: str, cwd: str = "/") -> ExecResult: + if not self.http_base: + raise RuntimeError("no active build session") + url = f"{self.http_base}/exec/stream" + body = {"cmd": ["bash", "-lc", f"cd {cwd} && {cmd}"], "cwd": cwd} + output = [] + exit_code = 0 + retries = 5 + backoff = 1.0 + async with httpx.AsyncClient(timeout=None) as client: + for attempt in range(retries): + try: + async for ev, data in self._sse_events(client, url, body): + if ev == "log": + try: + obj = json.loads(data) + line = obj.get("line", "") + except Exception: + line = data + output.append(line) + elif ev == "exit": + try: + exit_code = int(json.loads(data).get("code", 0)) + except Exception: + pass + return ExecResult(exit_code=exit_code, output=output) + except httpx.HTTPStatusError as e: + if e.response.status_code == 503 and attempt < retries - 1: + await asyncio.sleep(backoff * (attempt + 1)) + continue + raise + return ExecResult(exit_code=exit_code, output=output) + + async def exec_stream(self, cmd: str, cwd: str = "/") -> AsyncIterator[tuple[str, str]]: + if not self.http_base: + raise RuntimeError("no active build session") + url = f"{self.http_base}/exec/stream" + body = {"cmd": ["bash", "-lc", f"cd {cwd} && {cmd}"], "cwd": cwd} + retries = 5 + backoff = 1.0 + async with httpx.AsyncClient(timeout=None) as client: + for attempt in range(retries): + try: + async for ev, data in self._sse_events(client, url, body): + yield ev, data + return + except httpx.HTTPStatusError as e: + if e.response.status_code == 503 and attempt < retries - 1: + await asyncio.sleep(backoff * (attempt + 1)) + continue + raise + + async def upload(self, src: str | Path, dest: str) -> UploadResult: + if not self.http_base: + raise RuntimeError("no active build session") + path = Path(src).expanduser() + if not path.exists() or not path.is_file(): + raise FileNotFoundError(f"no such file: {path}") + url = f"{self.http_base}/upload" + retries = 5 + backoff = 1.0 + async with httpx.AsyncClient(timeout=None) as client: + for attempt in range(retries): + try: + with path.open("rb") as fh: + files = {"file": (path.name, fh)} + r = await client.post(url, params={"path": dest}, files=files) + r.raise_for_status() + data = r.json() + return UploadResult( + path=data.get("path", ""), + bytes=data.get("bytes", 0), + sha256=data.get("sha256", ""), + ) + except httpx.HTTPStatusError as e: + if e.response.status_code == 503 and attempt < retries - 1: + await asyncio.sleep(backoff * (attempt + 1)) + continue + raise + raise RuntimeError("upload failed after retries") + + def _load_icon(self, icon: str | Path | bytes | None) -> bytes | None: + if icon is None: + return None + if isinstance(icon, bytes): + return icon + path = Path(icon).expanduser() + if path.exists() and path.is_file(): + return path.read_bytes() + return None + + async def finish_foreground( + self, + name: str, + bundle_id: str | None, + cmd: str, + args: list[str], + cwd: str = "/", + env: list[str] | None = None, + description: str = "", + icon: str | Path | bytes | None = None, + ) -> FinishBuildSessionResponse: + return await self.finish_app( + name=name, + bundle_id=bundle_id, + description=description, + icon=icon, + foreground={ + "cmd": cmd, + "args": args, + "cwd": cwd, + "env": env or [], + }, + background=None, + default_schedule=None, + ) + + async def finish_background( + self, + name: str, + bundle_id: str | None, + cmd: str, + args: list[str], + cwd: str = "/", + env: list[str] | None = None, + description: str = "", + icon: str | Path | bytes | None = None, + default_schedule: dict | None = None, + ) -> FinishBuildSessionResponse: + return await self.finish_app( + name=name, + bundle_id=bundle_id, + description=description, + icon=icon, + foreground=None, + background={ + "cmd": cmd, + "args": args, + "cwd": cwd, + "env": env or [], + }, + default_schedule=default_schedule, + ) + + async def finish_app( + self, + *, + name: str, + bundle_id: str | None, + description: str = "", + icon: str | Path | bytes | None = None, + foreground: dict | None, + background: dict | None, + default_schedule: dict | None, + ) -> FinishBuildSessionResponse: + if not self.stub or not self.app_uuid: + raise RuntimeError("no active build session") + if foreground is None and background is None: + raise ValueError("finish_app requires foreground and/or background config") + + req = FinishBuildSessionRequest() + req.app_uuid = self.app_uuid + req.discard = False + self._apply_metadata( + req=req, + name=name, + bundle_id=bundle_id, + description=description, + icon=icon, + ) + + if foreground is not None: + self._apply_process( + req.foreground.process, + cmd=foreground["cmd"], + args=list(foreground.get("args", [])), + cwd=foreground.get("cwd", "/"), + env=list(foreground.get("env", [])), + ) + + if background is not None: + self._apply_process( + req.background.process, + cmd=background["cmd"], + args=list(background.get("args", [])), + cwd=background.get("cwd", "/"), + env=list(background.get("env", [])), + ) + if default_schedule: + runtime_policy = parse_runtime_policy(default_schedule) + req.background.runtime_policy.CopyFrom(runtime_policy) + else: + req.background.runtime_policy.interval.duration.seconds = 60 + + resp: FinishBuildSessionResponse = await self.stub.Builder_FinishBuildSession( + req, metadata=self._metadata + ) + self.app_uuid = None + self.access_path = None + if resp.HasField("error"): + raise RuntimeError(f"finish failed: {resp.error.error} - {resp.error.details}") + return resp + + async def discard(self) -> FinishBuildSessionResponse | None: + if not self.stub or not self.app_uuid: + return None + req = FinishBuildSessionRequest() + req.app_uuid = self.app_uuid + req.discard = True + resp: FinishBuildSessionResponse = await self.stub.Builder_FinishBuildSession( + req, metadata=self._metadata + ) + self.app_uuid = None + self.access_path = None + return resp + + async def close(self): + if self.channel: + await self.channel.close() + self.channel = None + self.stub = None + + # task methods + + def open_task_stream(self, prompt: str, *, app_uuids: list[str] | None = None): + if not self.stub: + raise RuntimeError("not connected") + req = OpenTaskRequest() + req.new_task.user_message.content = prompt + if app_uuids: + req.new_task.app_uuids.extend(app_uuids) + return self.stub.Task_OpenTask(req, metadata=self._metadata) + + async def respond_to_task(self, task_id: str, node_id: int, message: str) -> None: + if not self.stub: + raise RuntimeError("not connected") + req = RespondToTaskRequest() + req.task_id = task_id + req.node_id = node_id + req.message.content = message + await self.stub.Task_RespondToTask(req, metadata=self._metadata) + + async def interrupt_task(self, task_id: str) -> None: + if not self.stub: + raise RuntimeError("not connected") + req = InterruptTaskRequest() + req.target.task_id = task_id + await self.stub.Task_InterruptTask(req, metadata=self._metadata) + + def open_existing_task_stream(self, task_id: str): + if not self.stub: + raise RuntimeError("not connected") + req = OpenTaskRequest() + req.existing_task.task_id = task_id + return self.stub.Task_OpenTask(req, metadata=self._metadata) + + async def get_task_infos(self, *, max_before: int = 20) -> list[dict]: + if not self.stub: + raise RuntimeError("not connected") + req = GetTaskInfosRequest() + req.max_before = max_before + resp = await self.stub.Task_GetTaskInfos(req, metadata=self._metadata) + tasks = [] + for entry in resp.entries: + info = entry.info + title = info.task_title or "(untitled)" + created = info.created.ToDatetime().isoformat() if info.HasField("created") else "" + updated = info.last_updated.ToDatetime().isoformat() if info.HasField("last_updated") else "" + tasks.append({ + "task_id": entry.task_id, + "title": title, + "created": created, + "updated": updated, + }) + return tasks + + async def rename_task(self, task_id: str, new_name: str) -> str: + if not self.stub: + raise RuntimeError("not connected") + req = TaskRenameRequest() + req.task_id = task_id + req.new_name = new_name + resp = await self.stub.Task_Rename(req, metadata=self._metadata) + return resp.new_name + + async def delete_task(self, task_id: str) -> None: + if not self.stub: + raise RuntimeError("not connected") + req = TaskDeleteRequest() + req.task_id = task_id + await self.stub.Task_Delete(req, metadata=self._metadata) + + async def set_task_apps(self, task_id: str, app_uuids: list[str]) -> None: + if not self.stub: + raise RuntimeError("not connected") + req = TaskSetAvailableAppsRequest() + req.task_id = task_id + req.app_uuids.extend(app_uuids) + await self.stub.Task_SetAvailableApps(req, metadata=self._metadata) + + async def __aenter__(self): + await self.connect() + await self.start_build() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.discard() + await self.close() + return False diff --git a/pyproject.toml b/pyproject.toml index 0c3edf5..f162366 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,10 @@ [build-system] -requires = [ - "setuptools>=70", - "setuptools_scm>=8", - "wheel", - "grpcio-tools==1.76.0", - "protobuf>=6.30.0", - "googleapis-common-protos>=1.63.2", -] +requires = ["setuptools>=70", "wheel"] build-backend = "setuptools.build_meta" -[tool.setuptools_scm] -version_scheme = "guess-next-dev" -local_scheme = "no-local-version" -fallback_version = "0.1.dev0" -version_file = "truffile/_version.py" - [project] name = "truffile" -dynamic = ["version"] +version = "0.2.0" description = "truffile the TruffleOS SDK - Connect and deploy apps to Truffle devices" readme = "README.md" requires-python = ">=3.12" @@ -34,16 +21,15 @@ dependencies = [ "websockets>=12.0", "zeroconf>=0.131.0", "mcp==1.26.0", + "prompt_toolkit>=3.0.43", + "rich>=14.3.2", ] [project.scripts] truffile = "truffile.cli:main" [project.optional-dependencies] -dev = [ - "pytest>=9.0.2", - "grpcio-tools==1.76.0", -] +dev = ["pytest>=9.0.2"] [tool.setuptools.packages.find] where = ["."] @@ -51,4 +37,4 @@ include = ["truffile*", "truffle*"] [tool.setuptools.package-data] "*" = ["*.pyi", "py.typed"] -"truffile" = ["assets/*.png"] +"truffile" = ["assets/*.png", "assets/**/*"] diff --git a/scripts/build_package.py b/scripts/build_package.py deleted file mode 100644 index c1f21bb..0000000 --- a/scripts/build_package.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -# if youre reading this plz go make apps lol -# copies app_runtime from pyfw and rebuilds protos for truffile packaging. -# -# usage: -# python scripts/build_package.py --pyfw-path /path/to/pyfw -# -# or set PYFW_PATH in .env: -# echo "PYFW_PATH=/Users/me/work/pyfw" > .env -# python scripts/build_package.py -# -# what it does: -# 1. copies python/app_runtime/ from pyfw into truffile/app_runtime/ -# 2. rebuilds protos in pyfw (if needed) and copies truffle/ proto package -# 3. strips internal-only modules (browser/web_fingerprint) from the public build - -import argparse -import os -import shutil -import subprocess -import sys -from pathlib import Path - -SCRIPT_DIR = Path(__file__).resolve().parent -REPO_ROOT = SCRIPT_DIR.parent -TRUFFILE_PKG = REPO_ROOT / "truffile" -TRUFFLE_PROTO_PKG = REPO_ROOT / "truffle" - -def load_env(): - env_file = REPO_ROOT / ".env" - if env_file.exists(): - for line in env_file.read_text().splitlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - key, _, value = line.partition("=") - os.environ.setdefault(key.strip(), value.strip()) - - -def resolve_pyfw_path(cli_arg: str | None) -> Path: - raw = cli_arg or os.environ.get("PYFW_PATH", "") - if not raw: - print("error: --pyfw-path not provided and PYFW_PATH not set") - print("set it via cli arg or in .env file") - sys.exit(1) - path = Path(raw).resolve() - if not (path / "python" / "app_runtime").exists(): - print(f"error: {path}/python/app_runtime does not exist") - sys.exit(1) - return path - - -def build_protos(pyfw_path: Path): - build_script = pyfw_path / "python" / "tools" / "build_protos.py" - if not build_script.exists(): - print("warning: proto build script not found, skipping proto rebuild") - return - print("building protos in pyfw...") - subprocess.run( - [sys.executable, str(build_script)], - cwd=str(pyfw_path), - check=True, - ) - print("protos built") - - -def copy_app_runtime(pyfw_path: Path): - src = pyfw_path / "python" / "app_runtime" - dst = TRUFFILE_PKG / "app_runtime" - - if dst.exists(): - shutil.rmtree(dst) - - shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) - print(f"copied app_runtime ({sum(1 for _ in dst.rglob('*.py'))} py files)") - - -def copy_protos(pyfw_path: Path): - src = pyfw_path / "python" / "truffle" - dst = TRUFFLE_PROTO_PKG - - if not src.exists(): - print("warning: pyfw truffle/ proto package not found, keeping existing vendored protos") - return - - if dst.exists(): - shutil.rmtree(dst) - - shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) - print(f"copied truffle protos ({sum(1 for _ in dst.rglob('*.py'))} py files)") - - -def main(): - load_env() - - parser = argparse.ArgumentParser(description="build truffile package with app_runtime from pyfw") - parser.add_argument("--pyfw-path", type=str, default=None) - parser.add_argument("--skip-protos", action="store_true", help="skip proto rebuild") - args = parser.parse_args() - - pyfw_path = resolve_pyfw_path(args.pyfw_path) - print(f"using pyfw at: {pyfw_path}") - - if not args.skip_protos: - build_protos(pyfw_path) - - copy_app_runtime(pyfw_path) - copy_protos(pyfw_path) - - print("\npackage ready. run: pip install -e . to test locally") - - -if __name__ == "__main__": - main() diff --git a/tests/test_chat_helpers.py b/tests/test_chat_helpers.py index ff56158..951153b 100644 --- a/tests/test_chat_helpers.py +++ b/tests/test_chat_helpers.py @@ -82,12 +82,9 @@ def test_tools_have_schema(self): class TestReplCommands(unittest.TestCase): - def setUp(self): - self.chat = _import_chat() - if self.chat is None: - self.skipTest("chat module not importable") - def test_commands_exist(self): - self.assertIn("/help", self.chat.REPL_COMMANDS) - self.assertIn("/exit", self.chat.REPL_COMMANDS) - self.assertIn("/mcp", self.chat.REPL_COMMANDS) + from truffile.cli.commands import INFER_COMMANDS + names = [c.name for c in INFER_COMMANDS] + self.assertIn("/help", names) + self.assertIn("/exit", names) + self.assertIn("/mcp", names) diff --git a/tests/test_cross_platform.py b/tests/test_cross_platform.py index 7a3297a..85be0d7 100644 --- a/tests/test_cross_platform.py +++ b/tests/test_cross_platform.py @@ -30,19 +30,16 @@ def test_storage_handles_unicode_device_names(self): class TestTerminalDetection(unittest.TestCase): - def test_mushroom_disabled_when_not_tty(self): - from truffile.cli.ui import MushroomPulse - with patch("sys.stdout") as mock_stdout: - mock_stdout.isatty.return_value = False - pulse = MushroomPulse("test") - self.assertFalse(pulse.enabled) + def test_particle_orb_creates(self): + from truffile.cli.ui import create_thinking_orb + orb = create_thinking_orb() + self.assertIsNotNone(orb) - def test_mushroom_enabled_when_tty(self): - from truffile.cli.ui import MushroomPulse - with patch("sys.stdout") as mock_stdout: - mock_stdout.isatty.return_value = True - pulse = MushroomPulse("test") - self.assertTrue(pulse.enabled) + def test_particle_orb_state_changes(self): + from truffile.cli.art import ParticleOrb + orb = ParticleOrb(num_particles=3) + orb.set_state(ParticleOrb.STATE_ACTIVE) + orb.set_state(ParticleOrb.STATE_DONE) class TestPathHandling(unittest.TestCase): diff --git a/tests/test_prompt.py b/tests/test_prompt.py new file mode 100644 index 0000000..22d8caf --- /dev/null +++ b/tests/test_prompt.py @@ -0,0 +1,148 @@ +import time +import unittest + +from prompt_toolkit.document import Document +from prompt_toolkit.completion import CompleteEvent + +from truffile.cli.commands import SlashCommand, CHAT_COMMANDS, INFER_COMMANDS +from truffile.cli.prompt import SlashCommandCompleter, TrufflePrompt +from truffile.cli.markdown import has_markdown, count_terminal_lines + + +class TestSlashCommandCompleter(unittest.TestCase): + def setUp(self): + self.commands = [ + SlashCommand("/help", "show help"), + SlashCommand("/history", "show history"), + SlashCommand("/reset", "clear history"), + SlashCommand("/models", "switch model"), + ] + self.completer = SlashCommandCompleter(self.commands) + self.event = CompleteEvent() + + def _complete(self, text: str) -> list[str]: + doc = Document(text, len(text)) + return [c.text for c in self.completer.get_completions(doc, self.event)] + + def test_slash_yields_all(self): + results = self._complete("/") + self.assertEqual(len(results), 4) + + def test_prefix_match(self): + results = self._complete("/he") + self.assertEqual(results, ["/help"]) + + def test_prefix_match_multiple(self): + results = self._complete("/h") + self.assertEqual(set(results), {"/help", "/history"}) + + def test_no_slash_yields_nothing(self): + results = self._complete("hello") + self.assertEqual(results, []) + + def test_empty_yields_nothing(self): + results = self._complete("") + self.assertEqual(results, []) + + def test_space_after_command_yields_nothing(self): + results = self._complete("/help something") + self.assertEqual(results, []) + + def test_completions_have_descriptions(self): + doc = Document("/he", 3) + completions = list(self.completer.get_completions(doc, self.event)) + self.assertEqual(len(completions), 1) + self.assertIn("show help", str(completions[0].display_meta)) + + +class TestCommandRegistries(unittest.TestCase): + def test_chat_commands_have_help_and_exit(self): + names = [c.name for c in CHAT_COMMANDS] + self.assertIn("/help", names) + self.assertIn("/exit", names) + + def test_infer_commands_have_help_and_exit(self): + names = [c.name for c in INFER_COMMANDS] + self.assertIn("/help", names) + self.assertIn("/exit", names) + + def test_all_commands_start_with_slash(self): + for cmd in CHAT_COMMANDS + INFER_COMMANDS: + self.assertTrue(cmd.name.startswith("/"), f"{cmd.name} missing /") + + def test_all_commands_have_descriptions(self): + for cmd in CHAT_COMMANDS + INFER_COMMANDS: + self.assertTrue(len(cmd.description) > 0, f"{cmd.name} missing description") + + +class TestDoubleCtrlC(unittest.TestCase): + def test_single_press_returns_empty_string(self): + prompt = TrufflePrompt("> ", []) + result = prompt._handle_ctrlc() + self.assertEqual(result, "") + + def test_double_press_within_timeout_returns_none(self): + prompt = TrufflePrompt("> ", []) + prompt._handle_ctrlc() + result = prompt._handle_ctrlc() + self.assertIsNone(result) + + def test_press_after_timeout_resets(self): + prompt = TrufflePrompt("> ", []) + prompt._handle_ctrlc() + prompt._ctrlc_time = time.monotonic() - 5.0 + result = prompt._handle_ctrlc() + self.assertEqual(result, "") + + +class TestHasMarkdown(unittest.TestCase): + def test_code_block(self): + self.assertTrue(has_markdown("hello\n```python\nprint(1)\n```")) + + def test_header(self): + self.assertTrue(has_markdown("# Header")) + + def test_h2(self): + self.assertTrue(has_markdown("## Section")) + + def test_bold(self): + self.assertTrue(has_markdown("This is **bold** text")) + + def test_unordered_list(self): + self.assertTrue(has_markdown("- item one\n- item two")) + + def test_ordered_list(self): + self.assertTrue(has_markdown("1. first\n2. second")) + + def test_link(self): + self.assertTrue(has_markdown("See [docs](https://example.com)")) + + def test_plain_text(self): + self.assertFalse(has_markdown("hello world")) + + def test_empty(self): + self.assertFalse(has_markdown("")) + + def test_single_asterisk(self): + self.assertFalse(has_markdown("I like * stars")) + + +class TestCountTerminalLines(unittest.TestCase): + def test_single_line(self): + self.assertEqual(count_terminal_lines("hello", 80), 1) + + def test_two_lines(self): + self.assertEqual(count_terminal_lines("hello\nworld", 80), 2) + + def test_empty_line(self): + self.assertEqual(count_terminal_lines("", 80), 1) + + def test_wrapping(self): + self.assertEqual(count_terminal_lines("a" * 160, 80), 2) + + def test_exact_width(self): + self.assertEqual(count_terminal_lines("a" * 80, 80), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/truffile/_version.py b/truffile/_version.py index ed2a65f..d3ec452 100644 --- a/truffile/_version.py +++ b/truffile/_version.py @@ -1,24 +1 @@ -# file generated by vcs-versioning -# don't change, don't track in version control -from __future__ import annotations - -__all__ = [ - "__version__", - "__version_tuple__", - "version", - "version_tuple", - "__commit_id__", - "commit_id", -] - -version: str -__version__: str -__version_tuple__: tuple[int | str, ...] -version_tuple: tuple[int | str, ...] -commit_id: str | None -__commit_id__: str | None - -__version__ = version = '0.1.36.dev5' -__version_tuple__ = version_tuple = (0, 1, 36, 'dev5') - -__commit_id__ = commit_id = 'gfd5b47a0e' +__version__ = "0.2.0" diff --git a/truffile/cli/__init__.py b/truffile/cli/__init__.py index cbaedcb..f858085 100644 --- a/truffile/cli/__init__.py +++ b/truffile/cli/__init__.py @@ -3,7 +3,9 @@ import sys from pathlib import Path -from .ui import C, MUSHROOM, print_help +from .ui import C, MUSHROOM +from .art import render_glow_demo +from .welcome import show_help_welcome def run_async(coro): @@ -38,6 +40,7 @@ def main() -> int: # create create_p = sub.add_parser("create", help="scaffold a new app") create_p.add_argument("name", nargs="?") + create_p.add_argument("--path", type=str, default=None, help="base directory for the app") # validate val_p = sub.add_parser("validate", help="validate app directory") @@ -78,10 +81,17 @@ def main() -> int: # help sub.add_parser("help", help="show help") + # easter egg + sub.add_parser("glow") + args = parser.parse_args() if args.command == "help": - print_help() + show_help_welcome() + return 0 + + if args.command == "glow": + render_glow_demo(duration=999999.0) return 0 if args.command is None: @@ -128,5 +138,5 @@ def main() -> int: from .infer import cmd_infer return run_async(cmd_infer(args, storage)) - print_help() + show_help_welcome() return 1 diff --git a/truffile/cli/art.py b/truffile/cli/art.py new file mode 100644 index 0000000..7d10e6c --- /dev/null +++ b/truffile/cli/art.py @@ -0,0 +1,551 @@ +from __future__ import annotations + +import math +import os +import sys +import threading +import time + +try: + import termios + import tty +except Exception: + termios = None + tty = None + +# truffle shape โ€” bean/mushroom cap outline +# using block characters for solid fills and edges + +TRUFFLE_BANNER = [ + "##########@@%**+++====+++**%@@##########", + "######%+=-::-===++++++++===-::-=+%######", + "####+::-+%@##################@%+-::+####", + "##%:.+@##########################@+.:%##", + "#%..@##############################@. %#", + "#: %################################% :#", + "# .##################################. #", + "#. ################################## .#", + "#= +################################= +#", + "##- =@##########@@%%%@@###########@= =##", + "###*-.-=++++===-::----::-==+++++=-.-*###", + "#####@*+====++*%@#####@@%*++====+*@#####", +] + +TRUFFLE_BANNER_BRAILLE = [ + "โ €โ €โ €โ €โ €โ €โ €โฃ€โฃ€โฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃคโฃ€โฃ€โ €โ €โ €โ €โ €โ €โ €", + "โ €โ €โ €โข€โฃคโฃถโ ฟโ ›โ ‹โ ‰โ ‰โ โ €โ €โ €โ €โ €โ ˆโ ‰โ ‰โ ™โ ›โ ฟโฃถโฃคโก€โ €โ €โ €", + "โ €โ €โฃดโกฟโ ‹โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ ™โขฟโฃฆโ €โ €", + "โ €โฃผโกŸโ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โขปโฃงโ €", + "โข โฃฟโ โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ ˆโฃฟโก„", + "โ ˜โฃฟโ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โข€โฃฟโ ƒ", + "โ €โขฟโฃ‡โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โ €โฃผโกŸโ €", + "โ €โ ˆโ ปโฃทโฃ„โก€โ €โ €โ €โฃ€โฃ€โฃ โฃคโฃคโฃคโฃคโฃคโฃ„โฃ€โก€โ €โ €โ €โข€โฃ โฃพโ Ÿโ โ €", + "โ €โ €โ €โ ˆโ ™โ ›โ ฟโ ฟโ Ÿโ ›โ ›โ ‰โ ‰โ โ €โ ˆโ ‰โ ™โ ›โ ›โ ปโ ฟโ ฟโ ›โ ‹โ โ €โ €โ €", +] + +ORB = [ + "โข€โฃถโฃถโก€", + "โขธโฃฟโฃฟโก‡", + "โ ˆโ ฟโ ฟโ ", +] + +# color stops +PINK = (255, 105, 180) +HOT_PINK = (255, 20, 147) +BLUE = (100, 149, 237) +ROYAL_BLUE = (65, 105, 225) +ORANGE = (255, 165, 0) +CYAN = (0, 255, 255) + + +def _lerp(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> tuple[int, int, int]: + return ( + int(a[0] + (b[0] - a[0]) * t), + int(a[1] + (b[1] - a[1]) * t), + int(a[2] + (b[2] - a[2]) * t), + ) + + +def _supports_truecolor() -> bool: + ct = os.environ.get("COLORTERM", "").lower() + if ct in ("truecolor", "24bit"): + return True + term = os.environ.get("TERM_PROGRAM", "").lower() + # terminals known to support truecolor + if term in ("iterm.app", "hyper", "wezterm", "vscode", "ghostty", "alacritty"): + return True + return False + + +_TRUECOLOR = _supports_truecolor() + + +def _fg(r: int, g: int, b: int) -> str: + if _TRUECOLOR: + return f"\x1b[38;2;{r};{g};{b}m" + # fallback: use closest ANSI 256 color + return f"\x1b[38;5;{_rgb_to_256(r, g, b)}m" + + +def _rgb_to_256(r: int, g: int, b: int) -> int: + """Map RGB to nearest xterm-256 color cube index.""" + # check if close to grayscale + if r == g == b: + if r < 8: + return 16 + if r > 248: + return 231 + return round((r - 8) / 247 * 24) + 232 + # map to 6x6x6 color cube + ri = round(r / 255 * 5) + gi = round(g / 255 * 5) + bi = round(b / 255 * 5) + return 16 + 36 * ri + 6 * gi + bi + + +RESET = "\x1b[0m" +BOLD = "\x1b[1m" +DIM = "\x1b[2m" + + +def _terminal_width() -> int: + try: + import shutil + return shutil.get_terminal_size().columns + except Exception: + return 80 + + +def _center(line: str, width: int) -> str: + # braille chars are all width 1 visually + visible = len(line) + pad = max(0, (width - visible) // 2) + return " " * pad + line + + +def _color_top_row(line: str, phase: float = 0.0) -> str: + out = "" + stroke_positions = [i for i, ch in enumerate(line) if ch != BRAILLE_BLANK and ch != " "] + total = len(stroke_positions) + for i, ch in enumerate(line): + if ch == BRAILLE_BLANK or ch == " ": + out += RESET + ch + else: + idx = stroke_positions.index(i) + # mix pink and blue based on position + phase offset + t = (idx / max(total - 1, 1) + phase) % 1.0 + # use sine to blend so both colors are always present + blend = (math.sin(t * math.pi * 3) + 1.0) / 2.0 + c = _lerp(HOT_PINK, ROYAL_BLUE, blend) + out += _fg(*c) + ch + return out + RESET + + +def render_banner(*, fade_in: bool = True) -> None: + lines = TRUFFLE_BANNER_BRAILLE + label = f"{_fg(*ROYAL_BLUE)}{BOLD}Truffile{RESET} {DIM}โ€” Truffle SDK{RESET}" + banner_width = len(lines[0]) + # put label to the right of the middle row + label_row = len(lines) // 2 + + for i, line in enumerate(lines): + if i <= 1: + row = _color_top_row(line) + else: + row = line + if i == label_row: + row += " " + label + print(row) + if fade_in: + time.sleep(0.04) + + print() + + +BRAILLE_BLANK = "โ €" + + +def _color_strokes(line: str, color: tuple[int, int, int]) -> str: + out = "" + c = _fg(*color) + for ch in line: + if ch == BRAILLE_BLANK or ch == " ": + out += RESET + ch + else: + out += c + ch + return out + RESET + + +def render_small(*, color: tuple[int, int, int] = PINK) -> str: + result = "" + for line in TRUFFLE_SMALL: + result += _color_strokes(line, color) + "\n" + return result + + +class GlowBanner: + """Renders banner then keeps top rows glowing in a background thread.""" + + def __init__(self): + self._running = False + self._thread: threading.Thread | None = None + self._row0: int = 0 # absolute terminal row of banner line 0 + + @staticmethod + def _query_cursor_row() -> int | None: + """Query current cursor row via DSR escape sequence.""" + import select + try: + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) if termios else None + if tty: + tty.setcbreak(fd) + sys.stdout.write("\x1b[6n") + sys.stdout.flush() + ready, _, _ = select.select([fd], [], [], 0.1) + if not ready: + if old and termios: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + return None + resp = b"" + while True: + ready, _, _ = select.select([fd], [], [], 0.05) + if not ready: + break + ch = os.read(fd, 1) + resp += ch + if ch == b"R": + break + if old and termios: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + # response: \x1b[row;colR + decoded = resp.decode("ascii", errors="ignore") + if "[" in decoded and ";" in decoded: + row_str = decoded.split("[")[1].split(";")[0] + return int(row_str) + except Exception: + pass + return None + + def start(self) -> None: + """Print the banner and start the glow loop.""" + lines = TRUFFLE_BANNER_BRAILLE + label = f"{_fg(*ROYAL_BLUE)}{BOLD}Truffile{RESET} {DIM}โ€” Truffle SDK{RESET}" + label_row = len(lines) // 2 + + # query cursor row before printing to know where banner starts + row_before = self._query_cursor_row() if sys.stdout.isatty() else None + + for i, line in enumerate(lines): + if i <= 1: + row = _color_top_row(line) + else: + row = line + if i == label_row: + row += " " + label + print(row) + print() + + if not sys.stdout.isatty() or row_before is None: + return + + self._row0 = row_before # absolute row where line 0 of banner is + self._running = True + self._thread = threading.Thread(target=self._loop, daemon=True) + self._thread.start() + + def _loop(self) -> None: + lines = TRUFFLE_BANNER_BRAILLE + start = time.monotonic() + while self._running: + t = time.monotonic() - start + phase = t * 0.5 + # use absolute positioning to the exact rows + sys.stdout.write("\x1b7") + for i in range(2): + sys.stdout.write(f"\x1b[{self._row0 + i};1H\x1b[K{_color_top_row(lines[i], phase)}") + sys.stdout.write("\x1b8") + sys.stdout.flush() + time.sleep(0.06) + + def stop(self) -> None: + self._running = False + if self._thread: + self._thread.join(timeout=0.3) + self._thread = None + + +def render_glow_banner(*, duration: float = 1.5) -> None: + """Brief glow animation that settles into the static banner with label.""" + lines = TRUFFLE_BANNER_BRAILLE + height = len(lines) + label = f"{_fg(*ROYAL_BLUE)}{BOLD}Truffile{RESET} {DIM}โ€” Truffle SDK{RESET}" + label_row = len(lines) // 2 + start = time.monotonic() + + # print blank lines to make space + for _ in range(height): + sys.stdout.write("\n") + + try: + while time.monotonic() - start < duration: + elapsed = time.monotonic() - start + phase = elapsed * 0.8 + + sys.stdout.write(f"\x1b[{height}A") + for i, line in enumerate(lines): + if i <= 1: + row = _color_top_row(line, phase) + else: + row = line + if i == label_row: + row += " " + label + sys.stdout.write(f"\x1b[K{row}\n") + sys.stdout.flush() + time.sleep(0.04) + except KeyboardInterrupt: + pass + + # settle on final static frame + sys.stdout.write(f"\x1b[{height}A") + for i, line in enumerate(lines): + if i <= 1: + row = _color_top_row(line) + else: + row = line + if i == label_row: + row += " " + label + sys.stdout.write(f"\x1b[K{row}\n") + sys.stdout.flush() + print() + + +def render_glow_demo(*, duration: float = 8.0) -> None: + lines = TRUFFLE_BANNER_BRAILLE + height = len(lines) + start = time.monotonic() + + sys.stdout.write("\n") + for _ in range(height): + sys.stdout.write("\n") + + try: + while time.monotonic() - start < duration: + elapsed = time.monotonic() - start + phase = elapsed * 0.5 + + sys.stdout.write(f"\x1b[{height}A") + for i, line in enumerate(lines): + if i <= 1: + sys.stdout.write(f"\x1b[K{_color_top_row(line, phase)}\n") + else: + sys.stdout.write(f"\x1b[K{line}\n") + sys.stdout.flush() + time.sleep(0.05) + except KeyboardInterrupt: + pass + + sys.stdout.write(f"\x1b[{height}A") + for _ in range(height): + sys.stdout.write("\x1b[K\n") + sys.stdout.write(f"\x1b[{height}A") + sys.stdout.flush() + + +class ParticleOrb: + """particles bouncing inside an invisible circle. pink/blue when active, orange when done.""" + + STATE_ACTIVE = "active" + STATE_DONE = "done" + STATE_IDLE = "idle" + + # invisible container: 8 dot-wide x 12 dot-tall circle = 4x3 chars + DOT_W = 8 + DOT_H = 12 + CHAR_W = DOT_W // 2 + CHAR_H = DOT_H // 4 + CX = DOT_W / 2 - 0.5 + CY = DOT_H / 2 - 0.5 + RADIUS = 3.5 + + BRAILLE_BASE = 0x2800 + DOT_MAP = [ + (0, 0, 0x01), (0, 1, 0x02), (0, 2, 0x04), (0, 3, 0x40), + (1, 0, 0x08), (1, 1, 0x10), (1, 2, 0x20), (1, 3, 0x80), + ] + + def __init__(self, num_particles: int = 5): + import random + self._state = self.STATE_IDLE + self._running = False + self._thread: threading.Thread | None = None + self._lock = threading.Lock() + self._rng = random.Random() + # each particle: [orbit_r, speed, phase, wobble_freq, wobble_amp, color_type] + # color_type: 0=blue, 1=pink + self._particles: list[list[float]] = [] + self._orange_particles: list[list[float]] = [] + for i in range(num_particles): + orbit_r = self._rng.uniform(1.0, self.RADIUS * 0.85) + orbit_speed = self._rng.uniform(0.8, 2.2) * (1 if i % 2 == 0 else -1) + phase = self._rng.uniform(0, math.pi * 2) + wobble_freq = self._rng.uniform(1.5, 4.0) + wobble_amp = self._rng.uniform(0.3, 1.0) + self._particles.append([orbit_r, orbit_speed, phase, wobble_freq, wobble_amp, float(i % 2)]) + + def set_state(self, state: str) -> None: + with self._lock: + old = self._state + self._state = state + # when switching to done, spawn orange particles among existing ones + if state == self.STATE_DONE and old != self.STATE_DONE: + self._orange_particles = [] + for _ in range(3): + orbit_r = self._rng.uniform(0.8, self.RADIUS * 0.8) + orbit_speed = self._rng.uniform(1.0, 2.5) * self._rng.choice([1, -1]) + phase = self._rng.uniform(0, math.pi * 2) + wobble_freq = self._rng.uniform(2.0, 4.0) + wobble_amp = self._rng.uniform(0.3, 0.8) + self._orange_particles.append([orbit_r, orbit_speed, phase, wobble_freq, wobble_amp, 2.0]) + + def _get_positions(self, t: float) -> list[tuple[float, float, float]]: + positions = [] + # pink and blue always present + for p in self._particles: + orbit_r, speed, phase, wf, wa, color_type = p + angle = t * speed + phase + wobble = math.sin(t * wf + phase) * wa + r = orbit_r + wobble + r = max(0.3, min(r, self.RADIUS - 0.2)) + x = self.CX + math.cos(angle) * r + y = self.CY + math.sin(angle) * r * 0.6 + positions.append((x, y, color_type)) + # orange joins when done + with self._lock: + if self._state == self.STATE_DONE: + for p in self._orange_particles: + orbit_r, speed, phase, wf, wa, color_type = p + angle = t * speed + phase + wobble = math.sin(t * wf + phase) * wa + r = orbit_r + wobble + r = max(0.3, min(r, self.RADIUS - 0.2)) + x = self.CX + math.cos(angle) * r + y = self.CY + math.sin(angle) * r * 0.6 + positions.append((x, y, color_type)) + return positions + + def _render(self, t: float) -> list[str]: + with self._lock: + state = self._state + + positions = self._get_positions(t) + grid: dict[tuple[int, int], tuple[int, int, int]] = {} + for x, y, color_type in positions: + px, py = int(round(x)), int(round(y)) + if 0 <= px < self.DOT_W and 0 <= py < self.DOT_H: + if color_type > 1.5: + grid[(px, py)] = ORANGE + elif color_type > 0.5: + grid[(px, py)] = HOT_PINK + else: + grid[(px, py)] = ROYAL_BLUE + + lines = [] + for cy in range(self.CHAR_H): + out = "" + for cx in range(self.CHAR_W): + code = self.BRAILLE_BASE + char_color = None + for dx, dy, bit in self.DOT_MAP: + dpx = cx * 2 + dx + dpy = cy * 4 + dy + if (dpx, dpy) in grid: + code |= bit + char_color = grid[(dpx, dpy)] + if char_color and code != self.BRAILLE_BASE: + out += _fg(*char_color) + chr(code) + RESET + else: + out += BRAILLE_BLANK + lines.append(out) + return lines + + def _draw(self, t: float) -> None: + try: + import shutil + term_h = shutil.get_terminal_size().lines + except Exception: + term_h = 24 + + rendered = self._render(t) + sys.stdout.write("\x1b7") + for i, line in enumerate(rendered): + row = term_h - self.CHAR_H + i + sys.stdout.write(f"\x1b[{row};1H\x1b[K{line}") + sys.stdout.write("\x1b8") + sys.stdout.flush() + + def _loop(self) -> None: + start = time.monotonic() + while self._running: + t = time.monotonic() - start + self._draw(t) + time.sleep(0.06) + self._clear() + + def _clear(self) -> None: + try: + import shutil + term_h = shutil.get_terminal_size().lines + except Exception: + term_h = 24 + sys.stdout.write("\x1b7") + for i in range(self.CHAR_H): + row = term_h - self.CHAR_H + i + sys.stdout.write(f"\x1b[{row};1H\x1b[K") + sys.stdout.write("\x1b8") + sys.stdout.flush() + + def start(self, state: str = STATE_ACTIVE) -> None: + self._state = state + self._running = True + self._thread = threading.Thread(target=self._loop, daemon=True) + self._thread.start() + + def stop(self) -> None: + self._running = False + if self._thread: + self._thread.join(timeout=0.3) + self._thread = None + + +def render_orb_demo(*, duration: float = 6.0) -> None: + orb = ParticleOrb(num_particles=6) + orb.start(ParticleOrb.STATE_ACTIVE) + print("particles active (pink/blue)...") + time.sleep(duration * 0.6) + orb.set_state(ParticleOrb.STATE_DONE) + print("particles done (orange)...") + time.sleep(duration * 0.4) + orb.stop() + print("stopped") + + +if __name__ == "__main__": + import sys as _sys + arg = _sys.argv[1] if len(_sys.argv) > 1 else "banner" + + if arg == "banner": + render_banner() + elif arg == "small": + print(render_small()) + elif arg == "glow": + render_glow_demo(duration=8.0) + elif arg == "orb": + render_orb_demo(duration=6.0) + elif arg == "all": + render_banner() + print("glow demo (8s)...") + render_glow_demo(duration=8.0) + print("orb demo (6s)...") + render_orb_demo(duration=6.0) + print("done") diff --git a/truffile/cli/chat.py b/truffile/cli/chat.py index dd08cdb..a95de30 100644 --- a/truffile/cli/chat.py +++ b/truffile/cli/chat.py @@ -1,16 +1,23 @@ from __future__ import annotations import asyncio +import shutil import sys +import time from dataclasses import dataclass, field from pathlib import Path from typing import Any from truffile.storage import StorageService from truffile.client import TruffleClient, resolve_mdns -from .ui import C, MUSHROOM, DOT, CHECK, CROSS, Spinner, ScrollingLog, error, success, info, warn +from .ui import C, MUSHROOM, DOT, CHECK, CROSS, Spinner, ScrollingLog, StreamAbortWatcher, error, success, info, warn, create_thinking_orb from .connect import _resolve_connected_device from .picker import pick_from_list +from .commands import CHAT_COMMANDS, SlashCommand +from .prompt import TrufflePrompt +from .markdown import has_markdown, render_markdown, count_terminal_lines +from .art import ParticleOrb +from .welcome import show_chat_welcome @dataclass @@ -21,23 +28,11 @@ class TaskState: pending_node_id: int | None = None result_text: str = "" tool_calls: list[str] = field(default_factory=list) + thinking_summaries: list[str] = field(default_factory=list) -CHAT_COMMANDS = { - "/help": "show available commands", - "/tasks": "list recent tasks", - "/rename ": "rename current task", - "/title": "show current task title", - "/resume": "switch to a previous task", - "/new": "start a new task", - "/apps": "list installed apps", - "/app ": "add an app to current task", - "/deploy ": "deploy an app to device", - "/delete app ": "delete an installed app", - "/delete task": "delete current task", - "/devices": "list connected devices", - "/exit": "exit chat", -} +# accumulated streaming text for markdown re-render +_streaming_text: list[str] = [] def _print_update(update: Any, state: TaskState) -> None: @@ -56,12 +51,7 @@ def _print_update(update: Any, state: TaskState) -> None: msg = update.error.message if hasattr(update.error, "message") else str(update.error) print(f"\n{C.RED}error: {msg}{C.RESET}") - if update.HasField("streaming_step_result"): - chunk = update.streaming_step_result.partial_content - if chunk: - sys.stdout.write(chunk) - sys.stdout.flush() - + # render thinking/tools BEFORE streaming content so they appear above the response for node in update.nodes: if not node.HasField("step"): continue @@ -70,7 +60,7 @@ def _print_update(update: Any, state: TaskState) -> None: if step.HasField("thinking"): for s in step.thinking.cot_summaries: - print(f"{C.DIM}thinking: {s}{C.RESET}") + state.thinking_summaries.append(s) for tc in step.tool_calls: name = tc.tool_name if hasattr(tc, "tool_name") else "" @@ -95,18 +85,49 @@ def _print_update(update: Any, state: TaskState) -> None: if node_id: state.pending_node_id = node_id + # stream content after thinking/tools are rendered + if update.HasField("streaming_step_result"): + chunk = update.streaming_step_result.partial_content + if chunk: + sys.stdout.write(chunk) + sys.stdout.flush() + _streaming_text.append(chunk) -async def _stream_task(client: TruffleClient, stream: Any, state: TaskState) -> None: + +async def _stream_task(client: TruffleClient, stream: Any, state: TaskState, orb: ParticleOrb | None = None) -> None: + global _streaming_text + _streaming_text = [] interrupted = False - try: - async for update in stream: - _print_update(update, state) - if state.pending_node_id is not None: - break - except (asyncio.CancelledError, KeyboardInterrupt): - interrupted = True - except Exception: - interrupted = True + orb_stopped = False + + if orb: + orb.start(ParticleOrb.STATE_ACTIVE) + + with StreamAbortWatcher() as abort: + try: + async for update in stream: + if abort.aborted(): + interrupted = True + break + # stop orb only when actual visible content arrives + if orb and not orb_stopped: + has_content = ( + update.HasField("streaming_step_result") + and update.streaming_step_result.partial_content + ) + if has_content: + orb.stop() + orb_stopped = True + _print_update(update, state) + if state.pending_node_id is not None: + break + except (asyncio.CancelledError, KeyboardInterrupt): + interrupted = True + except Exception: + interrupted = True + + if orb and not orb_stopped: + orb.stop() if interrupted and state.task_id: print(f"\n{C.DIM}interrupting...{C.RESET}") @@ -132,7 +153,7 @@ async def _pick_task(client: TruffleClient, *, current_task_id: str = "") -> str ] print() - picked = pick_from_list( + picked = await pick_from_list( items, label_key="label", detail_key="detail", @@ -168,6 +189,8 @@ async def _handle_slash( client: TruffleClient, state: TaskState, storage: StorageService, + *, + app_commands: dict[str, dict] | None = None, ) -> str | None: """returns action string or None. 'exit' to quit, 'new' for fresh task, 'switch:' to resume.""" @@ -175,11 +198,25 @@ async def _handle_slash( command = parts[0].lower() arg = parts[1].strip() if len(parts) > 1 else "" + # check if this is a / shortcut + if app_commands and command in app_commands: + app = app_commands[command] + if not state.task_id: + error("no active task โ€” send a message first") + return None + try: + await client.set_task_apps(state.task_id, [app["uuid"]]) + success(f"added {app['name']} to task") + except Exception as e: + error(f"failed: {e}") + return None + if command == "/help": print(f"\n{C.BOLD}commands:{C.RESET}") - for name, desc in CHAT_COMMANDS.items(): - print(f" {C.CYAN}{name}{C.RESET} โ€” {desc}") - print() + for sc in CHAT_COMMANDS: + display = f"{sc.name} {sc.arg_hint}" if sc.arg_hint else sc.name + print(f" {C.CYAN}{display}{C.RESET} โ€” {sc.description}") + print(f"\n{C.DIM}alt+enter for newline ยท tab to complete commands ยท ctrl+d to exit{C.RESET}\n") return None if command in ("/exit", "/quit"): @@ -204,20 +241,7 @@ async def _handle_slash( error(f"rename failed: {e}") return None - if command == "/tasks": - tasks = await client.get_task_infos(max_before=10) - if not tasks: - info("no tasks found") - return None - print() - for i, t in enumerate(tasks, 1): - marker = f" {C.GREEN}(current){C.RESET}" if t["task_id"] == state.task_id else "" - updated = t["updated"][:16] if t["updated"] else "" - print(f" {C.CYAN}{i}.{C.RESET} {t['title']}{marker} {C.DIM}{updated}{C.RESET}") - print() - return None - - if command in ("/resume", "/switch"): + if command in ("/tasks", "/resume", "/switch"): task_id = await _pick_task(client, current_task_id=state.task_id) return f"switch:{task_id}" if task_id else None @@ -230,33 +254,15 @@ async def _handle_slash( if not apps_list: info("no apps installed") return None - print(f"\n{C.BOLD}Installed apps:{C.RESET}") + print(f"\n{C.BOLD}installed apps:{C.RESET}") for a in apps_list: - print(f" {C.CYAN}{DOT}{C.RESET} {a['name']} {C.DIM}({a['bundle_id']}){C.RESET}") + slug = a["name"].lower().replace(" ", "-") + print(f" {C.CYAN}/{slug}{C.RESET} {a['name']} {C.DIM}({a['bundle_id']}){C.RESET}") print() except Exception as e: error(f"failed to list apps: {e}") return None - if command == "/app": - if not arg: - error("usage: /app ") - return None - if not state.task_id: - error("no active task โ€” send a message first") - return None - try: - apps_list = await _get_apps_list(client) - match = _find_app_by_name(apps_list, arg) - if not match: - error(f"app \"{arg}\" not found. use /apps to see installed apps.") - return None - await client.set_task_apps(state.task_id, [match["uuid"]]) - success(f"added {match['name']} to task") - except Exception as e: - error(f"failed: {e}") - return None - if command == "/delete": if not arg: error("usage: /delete app [name] or /delete task") @@ -286,7 +292,7 @@ async def _handle_slash( if not app_name: items = [{"label": a["name"], "detail": a["bundle_id"], "uuid": a["uuid"]} for a in apps_list] print() - picked = pick_from_list(items, label_key="label", detail_key="detail", prompt="pick app to delete") + picked = await pick_from_list(items, label_key="label", detail_key="detail", prompt="pick app to delete") if not picked: return None match = {"name": picked["label"], "uuid": picked["uuid"]} @@ -361,10 +367,29 @@ async def _handle_slash( print() return None + if command == "/create": + from .create import cmd_create + from types import SimpleNamespace + create_args = SimpleNamespace(name=arg or None, path=None) + cmd_create(create_args) + return None + error(f"unknown command: {command}. type /help") return None +def _maybe_render_markdown(text: str) -> None: + """If text has markdown, re-render with rich.""" + if not text or not has_markdown(text): + return + try: + width = shutil.get_terminal_size().columns + lines = count_terminal_lines(text, width) + render_markdown(text, lines) + except Exception: + pass + + async def cmd_chat(args, storage: StorageService) -> int: result = await _resolve_connected_device(storage) device, ip = result @@ -394,6 +419,21 @@ async def cmd_chat(args, storage: StorageService) -> int: state = TaskState() stream = None + # fetch installed apps and register as / slash commands + app_commands: dict[str, dict] = {} + app_slugs: list[str] = [] + try: + apps_list = await _get_apps_list(client) + for a in apps_list: + slug = a["name"].lower().replace(" ", "-") + app_commands[f"/{slug}"] = a + app_slugs.append(slug) + except Exception: + pass + + # welcome panel + show_chat_welcome(device=device, apps=app_slugs or None) + if resume: task_id = await _pick_task(client) if task_id: @@ -406,32 +446,33 @@ async def cmd_chat(args, storage: StorageService) -> int: break except Exception: pass - print(f"\n{C.BOLD}{MUSHROOM} truffile chat{C.RESET} โ€” resumed \"{state.title or 'task'}\"") - else: - print(f"\n{C.BOLD}{MUSHROOM} truffile chat{C.RESET} โ€” connected to {device}") - else: - print(f"\n{C.BOLD}{MUSHROOM} truffile chat{C.RESET} โ€” connected to {device}") + info(f"resumed \"{state.title or 'task'}\"") - print(f"{C.DIM}type a message or /help for commands. ctrl+c to interrupt. ctrl+d to exit.{C.RESET}\n") + prompt = TrufflePrompt("you> ", CHAT_COMMANDS) + prompt.task_name = state.title + if app_commands: + prompt.add_commands([ + SlashCommand(cmd_name, f"add {a['name']} to task") + for cmd_name, a in app_commands.items() + ]) try: while True: - try: - user_input = input(f"{C.BOLD}you>{C.RESET} ") - except (EOFError, KeyboardInterrupt): + user_input = await prompt.get_input() + if user_input is None: print() break - if not user_input.strip(): continue if user_input.strip().startswith("/"): - action = await _handle_slash(user_input.strip(), client, state, storage) + action = await _handle_slash(user_input.strip(), client, state, storage, app_commands=app_commands) if action == "exit": break if action == "new": state = TaskState() stream = None + prompt.task_name = "" info("starting new conversation") continue if action and action.startswith("switch:"): @@ -446,31 +487,68 @@ async def cmd_chat(args, storage: StorageService) -> int: break except Exception: pass + prompt.task_name = state.title info(f"switched to \"{state.title or 'task'}\"") print() continue print() + orb = create_thinking_orb() + if state.pending_node_id is not None: await client.respond_to_task(state.task_id, state.pending_node_id, user_input.strip()) state.pending_node_id = None if stream: - await _stream_task(client, stream, state) + await _stream_task(client, stream, state, orb=orb) elif not state.task_id: stream = client.open_task_stream(user_input.strip()) - await _stream_task(client, stream, state) + await _stream_task(client, stream, state, orb=orb) else: state = TaskState() stream = client.open_task_stream(user_input.strip()) - await _stream_task(client, stream, state) - - if state.result_text: - print(f"\n{state.result_text}") + await _stream_task(client, stream, state, orb=orb) + + # update task name for prompt border + prompt.task_name = state.title + + streamed = "".join(_streaming_text) + + if streamed and state.thinking_summaries: + # clear raw streamed text, reprint with thinking above + try: + width = shutil.get_terminal_size().columns + except Exception: + width = 80 + from .markdown import count_terminal_lines + nlines = count_terminal_lines(streamed, width) + sys.stdout.write(f"\r\033[{nlines}A") + for _ in range(nlines + 1): + sys.stdout.write("\033[K\n") + sys.stdout.write(f"\033[{nlines + 1}A") + sys.stdout.flush() + # thinking header + combined = " ".join(state.thinking_summaries) + print(f"{C.GRAY}thinking: {combined}{C.RESET}") + # reprint response (was cleared above) + print(streamed) + _maybe_render_markdown(streamed) + elif streamed: + # content already printed during streaming, just finish the line + print() + _maybe_render_markdown(streamed) + elif state.thinking_summaries: + # thinking but no streamed content + combined = " ".join(state.thinking_summaries) + print(f"{C.GRAY}thinking: {combined}{C.RESET}") + if state.result_text: + print(state.result_text) + elif state.result_text: + # non-streaming result + print(state.result_text) print() except KeyboardInterrupt: - print() if state.task_id: try: await client.interrupt_task(state.task_id) @@ -478,5 +556,6 @@ async def cmd_chat(args, storage: StorageService) -> int: pass finally: await client.close() + print(f"\n{MUSHROOM} goodbye!") return 0 diff --git a/truffile/cli/commands.py b/truffile/cli/commands.py new file mode 100644 index 0000000..517acb7 --- /dev/null +++ b/truffile/cli/commands.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class SlashCommand: + name: str + description: str + has_arg: bool = False + arg_hint: str = "" + + +CHAT_COMMANDS: list[SlashCommand] = [ + SlashCommand("/help", "show available commands"), + SlashCommand("/tasks", "list recent tasks"), + SlashCommand("/rename", "rename current task", has_arg=True, arg_hint=""), + SlashCommand("/title", "show current task title"), + SlashCommand("/resume", "switch to a previous task"), + SlashCommand("/new", "start a new task"), + SlashCommand("/apps", "list installed apps"), + SlashCommand("/deploy", "deploy an app to device", has_arg=True, arg_hint=""), + SlashCommand("/delete", "delete app or task", has_arg=True, arg_hint="app|task"), + SlashCommand("/create", "scaffold a new app", has_arg=True, arg_hint=""), + SlashCommand("/devices", "list connected devices"), + SlashCommand("/exit", "exit chat"), +] + + +INFER_COMMANDS: list[SlashCommand] = [ + SlashCommand("/help", "show available commands"), + SlashCommand("/history", "show conversation history"), + SlashCommand("/reset", "clear conversation history"), + SlashCommand("/models", "switch inference model"), + SlashCommand("/config", "show current config"), + SlashCommand("/reasoning", "toggle reasoning", has_arg=True, arg_hint="on|off"), + SlashCommand("/stream", "toggle streaming", has_arg=True, arg_hint="on|off"), + SlashCommand("/json", "toggle JSON mode", has_arg=True, arg_hint="on|off"), + SlashCommand("/tools", "toggle tool use", has_arg=True, arg_hint="on|off"), + SlashCommand("/max_tokens", "set max response tokens", has_arg=True, arg_hint=""), + SlashCommand("/temperature", "set temperature", has_arg=True, arg_hint=""), + SlashCommand("/top_p", "set top_p", has_arg=True, arg_hint=""), + SlashCommand("/max_rounds", "set max tool rounds", has_arg=True, arg_hint=""), + SlashCommand("/system", "set system prompt", has_arg=True, arg_hint=""), + SlashCommand("/mcp", "manage MCP connections", has_arg=True, arg_hint="connect|disconnect|status|tools"), + SlashCommand("/attach", "attach an image", has_arg=True, arg_hint=""), + SlashCommand("/create", "scaffold a new app", has_arg=True, arg_hint=""), + SlashCommand("/exit", "exit"), +] diff --git a/truffile/cli/connect.py b/truffile/cli/connect.py index fd3fb43..bb169ed 100644 --- a/truffile/cli/connect.py +++ b/truffile/cli/connect.py @@ -111,7 +111,7 @@ async def cmd_connect(args, storage: StorageService) -> int: def cmd_disconnect(args, storage: StorageService) -> int: - target = args.target + target = getattr(args, "device", "all") if target == "all": storage.clear_all() success("All device credentials cleared") diff --git a/truffile/cli/create.py b/truffile/cli/create.py index 3bc2675..dd70f65 100644 --- a/truffile/cli/create.py +++ b/truffile/cli/create.py @@ -156,7 +156,10 @@ def cmd_create(args) -> int: error(f"Failed to scaffold app: {exc}") return 1 - success(f"Created app scaffold: {app_dir}") + # OSC 8 clickable path (supported in iTerm2, VSCode, WezTerm, etc.) + file_url = app_dir.as_uri() + link = f"\x1b]8;;{file_url}\a{app_dir}\x1b]8;;\a" + success(f"Created app scaffold: {link}") print(f" {C.DIM}Files:{C.RESET}") print(f" {C.DIM}{ARROW} truffile.yaml{C.RESET}") print(f" {C.DIM}{ARROW} {fg_file}{C.RESET}") diff --git a/truffile/cli/infer.py b/truffile/cli/infer.py index 0b496ee..7c83593 100644 --- a/truffile/cli/infer.py +++ b/truffile/cli/infer.py @@ -3,9 +3,8 @@ import contextlib import json import mimetypes -import os import re -import select +import shutil import signal import sys import threading @@ -16,32 +15,19 @@ import httpx -from .ui import C, MUSHROOM, SUPPORTED_SERVER_MIME_TYPES, Spinner, MushroomPulse, error, success, info, warn +from .ui import C, MUSHROOM, CHECK, HAMMER, WARN, SUPPORTED_SERVER_MIME_TYPES, Spinner, StreamAbortWatcher, error, success, info, warn, create_thinking_orb from .connect import _resolve_connected_device from .models import _fetch_models_payload, _pick_model_interactive, _default_model +from .commands import INFER_COMMANDS +from .prompt import TrufflePrompt +from .markdown import has_markdown, render_markdown, count_terminal_lines +from .art import ParticleOrb +from .welcome import show_infer_welcome from truffile.storage import StorageService -try: - import readline -except Exception: - readline = None - -try: - import termios - import tty -except Exception: - termios = None - tty = None DEFAULT_SYSTEM_PROMPT = None -REPL_COMMANDS = [ - "/help", "/", "/history", "/reset", "/models", "/config", - "/reasoning", "/stream", "/json", "/tools", "/max_tokens", - "/temperature", "/top_p", "/max_rounds", "/system", "/mcp", - "/attach", "/exit", "/quit", -] - @dataclass class ChatSettings: model: str @@ -435,136 +421,34 @@ def _print_reasoning_and_response(reasoning_text: str, response_text: str, show_ print(response_text) -def _print_repl_commands(prefix: str | None = None) -> None: - command_pool = [cmd for cmd in REPL_COMMANDS if cmd != "/"] - if prefix is None: - matches = command_pool - else: - matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] - if not matches: - print(f"{C.YELLOW}no command matches: {prefix}{C.RESET}") - return - rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in matches) - print(f"commands: {rendered}") - - -def _install_repl_completer(commands: list[str]) -> Callable[[], None] | None: - if readline is None: - return None - try: - prev_completer = readline.get_completer() - prev_delims = readline.get_completer_delims() - prev_display_hook = getattr(readline, "get_completion_display_matches_hook", lambda: None)() - readline.parse_and_bind("tab: complete") - readline.parse_and_bind("set show-all-if-ambiguous on") - readline.parse_and_bind("set show-all-if-unmodified on") - readline.parse_and_bind("set completion-ignore-case on") - readline.set_completer_delims(" \t\n") - matches: list[str] = [] - - def _complete(text: str, state: int) -> str | None: - nonlocal matches - if state == 0: - buffer = readline.get_line_buffer().lstrip() - if buffer.startswith("/"): - prefix = buffer.split()[0] - command_pool = [cmd for cmd in commands if cmd != "/"] - if prefix == "/": - matches = command_pool - else: - matches = [cmd for cmd in command_pool if cmd.startswith(prefix)] - else: - matches = [] - if state < len(matches): - return matches[state] - return None - - readline.set_completer(_complete) - if hasattr(readline, "set_completion_display_matches_hook"): - def _display_matches(substitution: str, display_matches: list[str], longest_match_length: int) -> None: - del substitution, longest_match_length - if not display_matches: - return - print() - rendered = ", ".join(f"{C.BLUE}{cmd}{C.RESET}" for cmd in display_matches) - print(f"commands: {rendered}") - try: - readline.redisplay() - except Exception: - pass - readline.set_completion_display_matches_hook(_display_matches) - - def _cleanup() -> None: - try: - readline.set_completer(prev_completer) - readline.set_completer_delims(prev_delims) - if hasattr(readline, "set_completion_display_matches_hook"): - readline.set_completion_display_matches_hook(prev_display_hook) - except Exception: - pass - - return _cleanup - except Exception: - return None - - -class StreamAbortWatcher: - def __init__(self) -> None: - self.enabled = bool(sys.stdin.isatty() and termios is not None and tty is not None) - self._fd: int | None = None - self._old_attrs: Any = None - self._thread: threading.Thread | None = None - self._stop = threading.Event() - self._abort_reason: str | None = None - - def __enter__(self) -> "StreamAbortWatcher": - if not self.enabled: - return self - try: - self._fd = sys.stdin.fileno() - self._old_attrs = termios.tcgetattr(self._fd) - tty.setcbreak(self._fd) - except Exception: - self.enabled = False - return self - self._thread = threading.Thread(target=self._watch, daemon=True) - self._thread.start() - return self - - def _watch(self) -> None: - if self._fd is None: - return - while not self._stop.is_set(): - try: - ready, _, _ = select.select([self._fd], [], [], 0.1) - except Exception: - return - if not ready: - continue - try: - ch = os.read(self._fd, 1) - except Exception: - continue - if not ch: - continue - if ch == b"\x1b": - self._abort_reason = "esc" - self._stop.set() - return - - def aborted(self) -> bool: - return self._abort_reason is not None - - def __exit__(self, exc_type, exc, tb) -> bool: - self._stop.set() - if self._thread: - self._thread.join(timeout=0.2) - if self.enabled and self._fd is not None and self._old_attrs is not None: - try: - termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_attrs) - except Exception: - pass - return False +def _print_usage(usage: dict[str, Any]) -> None: + prompt_t = usage.get("prompt_tokens", "") + comp_t = usage.get("completion_tokens", "") + total_t = usage.get("total_tokens", "") + decode_tps = usage.get("decode_tokens_per_second", "") + ttft = usage.get("ttft_ms", "") + itl = usage.get("itl_ms", "") + image_t = usage.get("image_tokens") or 0 + parts = [f"tokens {C.DIM}[{prompt_t}/{comp_t}/{total_t}]{C.RESET}"] + if decode_tps: + parts.append(f"decode {C.DIM}{decode_tps} t/s{C.RESET}") + if ttft: + parts.append(f"ttft {C.DIM}{ttft}ms{C.RESET}") + if itl: + parts.append(f"itl {C.DIM}{itl}ms{C.RESET}") + if image_t: + parts.append(f"image {C.DIM}{image_t}t{C.RESET}") + sep = f" {C.DIM}|{C.RESET} " + print(f"\n{C.DIM}---{C.RESET}") + print(sep.join(parts)) + + +def _print_infer_help() -> None: + print(f"\n{C.BOLD}commands:{C.RESET}") + for sc in INFER_COMMANDS: + display = f"{sc.name} {sc.arg_hint}" if sc.arg_hint else sc.name + print(f" {C.CYAN}{display}{C.RESET} โ€” {sc.description}") + print(f"\n{C.DIM}alt+enter for newline ยท tab to complete commands ยท ctrl+d to exit{C.RESET}\n") def _run_single_chat_request( @@ -576,8 +460,9 @@ def _run_single_chat_request( settings: ChatSettings, stream: bool, ) -> tuple[dict[str, Any], dict[str, Any] | None, bool]: - wait_anim = MushroomPulse("thinking") - wait_anim.start() + orb = create_thinking_orb() + orb.start(ParticleOrb.STATE_ACTIVE) + orb_stopped = False if stream: content_parts: list[str] = [] reasoning_parts: list[str] = [] @@ -607,10 +492,6 @@ def _run_single_chat_request( evt = json.loads(data) except Exception: continue - if not first_event_seen: - wait_anim.stop() - first_event_seen = True - if isinstance(evt.get("usage"), dict): usage = evt.get("usage") @@ -626,6 +507,10 @@ def _run_single_chat_request( reasoning_chunk = delta.get("reasoning") if isinstance(reasoning_chunk, str) and reasoning_chunk: + # stop orb before first visible output + if not orb_stopped: + orb.stop() + orb_stopped = True reasoning_parts.append(reasoning_chunk) if settings.reasoning: if not reasoning_stream_started: @@ -635,6 +520,10 @@ def _run_single_chat_request( content_chunk = delta.get("content") if isinstance(content_chunk, str) and content_chunk: + # stop orb before first visible output + if not orb_stopped: + orb.stop() + orb_stopped = True content_parts.append(content_chunk) print(content_chunk, end="", flush=True) @@ -665,7 +554,9 @@ def _run_single_chat_request( except KeyboardInterrupt: interrupted = True finally: - wait_anim.stop() + if not orb_stopped: + orb.stop() + orb_stopped = True msg: dict[str, Any] = {"role": "assistant", "content": "".join(content_parts).strip()} reasoning_text = "".join(reasoning_parts).strip() @@ -673,15 +564,20 @@ def _run_single_chat_request( msg["reasoning_content"] = reasoning_text if tool_calls_by_index: msg["tool_calls"] = [tool_calls_by_index[i] for i in sorted(tool_calls_by_index)] - if settings.reasoning: - if reasoning_stream_started: - print() - response_text = str(msg.get("content") or "") - if response_text: - print() - print(response_text) - elif content_parts: + full_content = "".join(content_parts).strip() + # content was already streamed to stdout chunk by chunk + if reasoning_stream_started or content_parts: print() + + # markdown re-render: clear raw streamed content, replace with rich formatted + if full_content and has_markdown(full_content): + try: + width = shutil.get_terminal_size().columns + # only clear the content lines (not reasoning which was above) + lines_to_clear = count_terminal_lines(full_content, width) + 1 + render_markdown(full_content, lines_to_clear) + except Exception: + pass if interrupted: print(f"{C.YELLOW}response interrupted{C.RESET}") return msg, usage, interrupted @@ -691,7 +587,9 @@ def _run_single_chat_request( resp.raise_for_status() body = resp.json() finally: - wait_anim.stop() + if not orb_stopped: + orb.stop() + orb_stopped = True if settings.json_mode: print(json.dumps(body, indent=2)) @@ -748,12 +646,7 @@ async def _run_chat_turn( ) messages.append(assistant_msg) if isinstance(usage, dict): - image_tokens = usage.get("image_tokens") or 0 - image_tokens_part = f", image: {image_tokens}" if image_tokens else "" - image_tps_part = f", image tps: {usage.get('image_tokens_per_second') or ''}" if image_tokens else "" - print( - f"{C.DIM}[usage] tokens(prompt: {usage.get('prompt_tokens') or ''}, completion: {usage.get('completion_tokens') or ''}, total: {usage.get('total_tokens') or ''}{image_tokens_part}) usage(decode tps: {usage.get('decode_tokens_per_second') or ''}, prefill tps: {usage.get('prefill_tokens_per_second') or ''}{image_tps_part}) itl: {usage.get('itl_ms') or ''}ms ttft: {usage.get('ttft_ms') or ''}ms{C.RESET}" - ) + _print_usage(usage) if interrupted: return 130 @@ -825,13 +718,10 @@ async def cmd_infer(args, storage: StorageService) -> int: with httpx.Client(timeout=None) as client: spinner.stop(success=True) - # REPL mode (default). - print(f"{C.DIM}model: {settings.model}{C.RESET}") - print( - f"{C.DIM}commands: /help, /history, /reset, /models, /attach, /config, /mcp, /exit{C.RESET}" - ) + # REPL mode + show_infer_welcome(model=settings.model, device=device) - cleanup_repl = _install_repl_completer(REPL_COMMANDS) + prompt_ui = TrufflePrompt("> ", INFER_COMMANDS) try: if prompt: print(f"{C.CYAN}> {prompt}{C.RESET}") @@ -855,21 +745,18 @@ async def cmd_infer(args, storage: StorageService) -> int: pending_image_desc = None while True: - try: - line = input(f"{C.CYAN}> {C.RESET}").strip() - except EOFError: - print() + raw = await prompt_ui.get_input() + if raw is None: + print(f"\n{MUSHROOM} goodbye!") return 0 - except KeyboardInterrupt: - print() - return 0 - + line = raw.strip() if not line: continue if line in {"/", "/help"}: - _print_repl_commands() + _print_infer_help() continue if line in {"/exit", "/quit"}: + print(f"{MUSHROOM} goodbye!") return 0 if line == "/history": _print_history(messages) @@ -1078,13 +965,14 @@ async def cmd_infer(args, storage: StorageService) -> int: except RuntimeError as exc: error(str(exc)) continue + if line.startswith("/create"): + from .create import cmd_create + from types import SimpleNamespace + create_arg = line[len("/create"):].strip() or None + cmd_create(SimpleNamespace(name=create_arg, path=None)) + continue if line.startswith("/"): - matches = [cmd for cmd in REPL_COMMANDS if cmd.startswith(line)] - if matches: - _print_repl_commands(line) - else: - warn(f"unknown command: {line}") - _print_repl_commands() + warn(f"unknown command: {line}. type /help") continue if pending_image_data_url is not None: @@ -1106,8 +994,6 @@ async def cmd_infer(args, storage: StorageService) -> int: pending_image_data_url = None pending_image_desc = None finally: - if cleanup_repl: - cleanup_repl() await mcp_client.disconnect() return 0 except Exception as e: diff --git a/truffile/cli/markdown.py b/truffile/cli/markdown.py new file mode 100644 index 0000000..f63e537 --- /dev/null +++ b/truffile/cli/markdown.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import re +import shutil +import sys + +from rich.console import Console +from rich.markdown import Markdown +from rich.theme import Theme + + +TRUFFILE_THEME = Theme({ + "markdown.h1": "bold #4169E1", + "markdown.h2": "bold #4169E1", + "markdown.h3": "#4169E1", + "markdown.link": "#FF1493", + "markdown.link_url": "dim #FF1493", + "markdown.code": "#FFA500", + "markdown.item.bullet": "#4169E1", + "markdown.item.number": "#4169E1", +}) + +_console = Console(theme=TRUFFILE_THEME, highlight=False) + +_MD_PATTERNS = [ + re.compile(r"```"), + re.compile(r"^\s*#{1,6}\s", re.MULTILINE), + re.compile(r"\*\*.+?\*\*"), + re.compile(r"^\s*[-*+]\s", re.MULTILINE), + re.compile(r"^\s*\d+\.\s", re.MULTILINE), + re.compile(r"\[.+?\]\(.+?\)"), +] + + +def has_markdown(text: str) -> bool: + for pattern in _MD_PATTERNS: + if pattern.search(text): + return True + return False + + +def count_terminal_lines(text: str, terminal_width: int) -> int: + lines = 0 + for line in text.split("\n"): + visible_len = len(line) + if visible_len == 0: + lines += 1 + else: + lines += max(1, (visible_len + terminal_width - 1) // terminal_width) + return lines + + +def render_markdown(raw_text: str, lines_to_clear: int) -> None: + if not raw_text.strip(): + return + # move cursor up and clear each line of raw output + for _ in range(lines_to_clear): + sys.stdout.write("\033[A\033[K") + sys.stdout.write("\033[K") + sys.stdout.flush() + _console.print(Markdown(raw_text)) diff --git a/truffile/cli/picker.py b/truffile/cli/picker.py index b7ae525..0ba0251 100644 --- a/truffile/cli/picker.py +++ b/truffile/cli/picker.py @@ -1,11 +1,29 @@ from __future__ import annotations +import sys from typing import Any -from .ui import C, DOT +from prompt_toolkit import Application +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout import Layout, HSplit +from prompt_toolkit.layout.containers import Window +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.formatted_text import FormattedText +from prompt_toolkit.styles import Style +from .ui import C -def pick_from_list( + +PICKER_STYLE = Style.from_dict({ + "selected": "#4169E1 bold", + "label": "", + "detail": "#666666", + "current": "#22c55e", + "pointer": "#4169E1 bold", +}) + + +async def pick_from_list( items: list[dict[str, Any]], *, label_key: str = "label", @@ -17,12 +35,93 @@ def pick_from_list( if not items: return None + # fallback for non-tty + if not sys.stdout.isatty(): + return _fallback_pick(items, label_key=label_key, detail_key=detail_key, + active_key=active_key, active_value=active_value, prompt=prompt) + + selected_idx = [0] + result: list[dict[str, Any] | None] = [None] + + def _get_text(): + fragments: list[tuple[str, str]] = [] + fragments.append(("class:detail", f" {prompt} (โ†‘โ†“ enter esc)\n")) + for i, item in enumerate(items): + label = item.get(label_key, "?") + detail = item.get(detail_key, "") + is_active = active_key and item.get(active_key) == active_value + is_selected = i == selected_idx[0] + + if is_selected: + fragments.append(("class:pointer", " โ€บ ")) + fragments.append(("class:selected", label)) + else: + fragments.append(("", " ")) + fragments.append(("class:label", label)) + + if is_active: + fragments.append(("class:current", " (current)")) + if detail: + fragments.append(("class:detail", f" {detail}")) + fragments.append(("", "\n")) + return FormattedText(fragments) + + kb = KeyBindings() + + @kb.add("up") + def _up(event): + selected_idx[0] = (selected_idx[0] - 1) % len(items) + + @kb.add("down") + def _down(event): + selected_idx[0] = (selected_idx[0] + 1) % len(items) + + @kb.add("enter") + def _enter(event): + result[0] = items[selected_idx[0]] + event.app.exit() + + @kb.add("escape") + def _escape(event): + result[0] = None + event.app.exit() + + @kb.add("c-c") + def _ctrlc(event): + result[0] = None + event.app.exit() + + @kb.add("c-d") + def _ctrld(event): + result[0] = None + event.app.exit() + + layout = Layout(HSplit([Window(FormattedTextControl(_get_text))])) + app = Application(layout=layout, key_bindings=kb, style=PICKER_STYLE, full_screen=False) + + try: + await app.run_async() + except (EOFError, KeyboardInterrupt): + return None + + return result[0] + + +def _fallback_pick( + items: list[dict[str, Any]], + *, + label_key: str = "label", + detail_key: str = "detail", + active_key: str | None = None, + active_value: Any = None, + prompt: str = "pick one", +) -> dict[str, Any] | None: + """Simple numbered fallback for non-interactive terminals.""" print() for i, item in enumerate(items, 1): label = item.get(label_key, "?") detail = item.get(detail_key, "") is_active = active_key and item.get(active_key) == active_value - line = f" {C.CYAN}{i}.{C.RESET} {label}" if is_active: line += f" {C.GREEN}(current){C.RESET}" @@ -30,12 +129,10 @@ def pick_from_list( line += f" {C.DIM}{detail}{C.RESET}" print(line) print() - try: choice = input(f"{prompt} (1-{len(items)}) or enter to cancel: ").strip() except (EOFError, KeyboardInterrupt): return None - if not choice: return None try: diff --git a/truffile/cli/prompt.py b/truffile/cli/prompt.py new file mode 100644 index 0000000..1030f37 --- /dev/null +++ b/truffile/cli/prompt.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import shutil +import time + +from prompt_toolkit import PromptSession +from prompt_toolkit.completion import Completer, Completion +from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text import HTML, FormattedText +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.styles import Style + +from .commands import SlashCommand + + +TRUFFILE_STYLE = Style.from_dict({ + "prompt": "#56b6c2 bold", + "continuation": "#666666", + "bottom-toolbar": "noreverse #555555", + "bottom-toolbar.text": "noreverse #555555", + # dropdown + "completion-menu.completion": "bg:#2a2a2a #56b6c2", + "completion-menu.completion.current": "bg:#3a3a4a #56b6c2 bold", + "completion-menu.meta.completion": "bg:#2a2a2a #888888", + "completion-menu.meta.completion.current": "bg:#3a3a4a #bbbbbb", + "scrollbar.background": "bg:#2a2a2a", + "scrollbar.button": "bg:#56b6c2", +}) + +# thin horizontal rule using ANSI bright cyan (\033[96m) to match C.CYAN +_CYAN = "\033[96m" +_DIM = "\033[2m" +_RESET = "\033[0m" + + +class SlashCommandCompleter(Completer): + def __init__(self, commands: list[SlashCommand]): + self.commands = list(commands) + + def add_commands(self, commands: list[SlashCommand]) -> None: + for cmd in commands: + if not any(c.name == cmd.name for c in self.commands): + self.commands.append(cmd) + + def get_completions(self, document: Document, complete_event): + text = document.text_before_cursor.lstrip() + if not text.startswith("/"): + return + + parts = text.split(maxsplit=1) + if len(parts) > 1: + return + + prefix = parts[0].lower() + for cmd in self.commands: + if not cmd.name.lower().startswith(prefix): + continue + display = f"{cmd.name} {cmd.arg_hint}" if cmd.arg_hint else cmd.name + yield Completion( + cmd.name, + start_position=-len(prefix), + display=display, + display_meta=cmd.description, + ) + + +def _build_keybindings() -> KeyBindings: + kb = KeyBindings() + + @kb.add("enter", eager=True) + def _submit(event): + event.current_buffer.validate_and_handle() + + @kb.add("escape", "enter") + def _newline_alt_enter(event): + event.current_buffer.insert_text("\n") + + @kb.add("c-j") + def _newline_ctrl_j(event): + event.current_buffer.insert_text("\n") + + return kb + + +def _hr(task_name: str = "") -> str: + """Thin horizontal rule, optional right-aligned task name.""" + try: + width = shutil.get_terminal_size().columns + except Exception: + width = 80 + if task_name: + label = f" {task_name} " + line_len = max(0, width - len(label)) + return "โ”€" * line_len + label + return "โ”€" * width + + +class TrufflePrompt: + """Shared input component for chat and infer REPLs.""" + + def __init__(self, prompt_text: str, commands: list[SlashCommand]): + self._completer = SlashCommandCompleter(commands) + self._session = PromptSession( + completer=self._completer, + key_bindings=_build_keybindings(), + multiline=True, + prompt_continuation=self._continuation, + style=TRUFFILE_STYLE, + complete_while_typing=True, + ) + self._prompt_text = prompt_text + self._ctrlc_time: float | None = None + self.task_name: str = "" + + def add_commands(self, commands: list[SlashCommand]) -> None: + self._completer.add_commands(commands) + + @staticmethod + def _continuation(width: int, line_number: int, wrap_count: int) -> str: + return "." * (width - 1) + " " + + def _bottom_toolbar(self) -> FormattedText: + line = _hr(self.task_name) + return FormattedText([("", line)]) + + async def get_input(self) -> str | None: + """Returns user text, empty string on single Ctrl+C, None on exit.""" + try: + text = await self._session.prompt_async( + HTML(f"{self._prompt_text}"), + bottom_toolbar=self._bottom_toolbar, + ) + self._ctrlc_time = None + return text + except EOFError: + return None + except KeyboardInterrupt: + return self._handle_ctrlc() + + def _handle_ctrlc(self) -> str | None: + now = time.monotonic() + if self._ctrlc_time is not None and (now - self._ctrlc_time) < 3.0: + return None + self._ctrlc_time = now + print("\npress ctrl+c again to quit (or ctrl+d)") + return "" diff --git a/truffile/cli/ui.py b/truffile/cli/ui.py index c87d969..1818e10 100644 --- a/truffile/cli/ui.py +++ b/truffile/cli/ui.py @@ -1,7 +1,17 @@ +import os +import select import sys import threading import time from pathlib import Path +from typing import Any + +try: + import termios + import tty +except Exception: + termios = None + tty = None class C: @@ -67,40 +77,9 @@ def fail(self, message: str | None = None): sys.stdout.flush() -class MushroomPulse: - FRAMES = ["(๐Ÿ„ )", "(๐Ÿ„. )", "(๐Ÿ„.. )", "(๐Ÿ„...)", "(๐Ÿ„ ..)", "(๐Ÿ„ .)"] - - def __init__(self, message: str = "thinking", interval: float = 0.09): - self.message = message - self.interval = interval - self.running = False - self.thread: threading.Thread | None = None - self.frame_idx = 0 - self.enabled = bool(sys.stdout.isatty()) - - def _spin(self) -> None: - while self.running: - frame = self.FRAMES[self.frame_idx % len(self.FRAMES)] - sys.stdout.write(f"\r{C.MAGENTA}{frame}{C.RESET} {C.DIM}{self.message}{C.RESET}") - sys.stdout.flush() - self.frame_idx += 1 - time.sleep(self.interval) - - def start(self) -> None: - if not self.enabled or self.running: - return - self.running = True - self.thread = threading.Thread(target=self._spin, daemon=True) - self.thread.start() - - def stop(self) -> None: - if not self.running: - return - self.running = False - if self.thread: - self.thread.join(timeout=0.2) - sys.stdout.write("\r\033[K") - sys.stdout.flush() +def create_thinking_orb(): + from .art import ParticleOrb + return ParticleOrb(num_particles=5) class ScrollingLog: @@ -145,6 +124,65 @@ def finish(self): sys.stdout.flush() +class StreamAbortWatcher: + """Watches for ESC key during streaming. Use as context manager.""" + + def __init__(self) -> None: + self.enabled = bool(sys.stdin.isatty() and termios is not None and tty is not None) + self._fd: int | None = None + self._old_attrs: Any = None + self._thread: threading.Thread | None = None + self._stop = threading.Event() + self._aborted = False + + def __enter__(self) -> "StreamAbortWatcher": + if not self.enabled: + return self + try: + self._fd = sys.stdin.fileno() + self._old_attrs = termios.tcgetattr(self._fd) + tty.setcbreak(self._fd) + except Exception: + self.enabled = False + return self + self._thread = threading.Thread(target=self._watch, daemon=True) + self._thread.start() + return self + + def _watch(self) -> None: + if self._fd is None: + return + while not self._stop.is_set(): + try: + ready, _, _ = select.select([self._fd], [], [], 0.1) + except Exception: + return + if not ready: + continue + try: + ch = os.read(self._fd, 1) + except Exception: + continue + if ch == b"\x1b": + self._aborted = True + self._stop.set() + return + + def aborted(self) -> bool: + return self._aborted + + def __exit__(self, exc_type, exc, tb) -> bool: + self._stop.set() + if self._thread: + self._thread.join(timeout=0.2) + if self.enabled and self._fd is not None and self._old_attrs is not None: + try: + termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_attrs) + except Exception: + pass + return False + + def error(msg: str): print(f"{C.RED}{CROSS} Error:{C.RESET} {msg}") @@ -163,8 +201,6 @@ def info(msg: str): def print_help(): print(f""" -{C.BOLD}{MUSHROOM} truffile{C.RESET} โ€” TruffleOS SDK - {C.BOLD}Usage:{C.RESET} truffile [options] {C.BOLD}Commands:{C.RESET} diff --git a/truffile/cli/welcome.py b/truffile/cli/welcome.py new file mode 100644 index 0000000..6e433f6 --- /dev/null +++ b/truffile/cli/welcome.py @@ -0,0 +1,169 @@ +"""Rich welcome panels for chat, infer, and help.""" +from __future__ import annotations + +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +from .art import TRUFFLE_BANNER_BRAILLE, _color_top_row, _fg, ROYAL_BLUE, BOLD, DIM, RESET + +_console = Console(highlight=False) + +# colors matching C.CYAN (\033[96m) +ACCENT = "#56b6c2" +DIM_C = "#5a6a6e" +TEXT_C = "#d4d4d4" + + +def _braille_banner_rich() -> str: + """Build the braille banner as a rich-markup string.""" + lines = TRUFFLE_BANNER_BRAILLE + parts = [] + for i, line in enumerate(lines): + if i <= 1: + # top rows get colored but rich markup needs plain text โ€” use raw ANSI + parts.append(_color_top_row(line)) + else: + parts.append(line) + return "\n".join(parts) + + +def _build_panel( + *, + right_lines: list[str], + title: str = "Truffile", + subtitle: str = "", +) -> Panel: + import shutil + try: + term_width = shutil.get_terminal_size().columns + except Exception: + term_width = 80 + + right = Text.from_ansi("\n".join(right_lines)) + + # two-column layout if terminal is wide enough for banner + commands + if term_width >= 75: + layout = Table.grid(padding=(0, 3)) + layout.add_column("left", justify="center") + layout.add_column("right", justify="left") + banner_text = _braille_banner_rich() + left = Text.from_ansi(banner_text + f"\n\n {_fg(*ROYAL_BLUE)}{BOLD}Truffile{RESET} {DIM}โ€” Truffle SDK{RESET}") + layout.add_row(left, right) + content = layout + else: + # narrow terminal: commands only, no banner + content = right + + return Panel( + content, + title=f"[bold {ACCENT}]{title}[/]", + subtitle=f"[dim {DIM_C}]{subtitle}[/]" if subtitle else None, + border_style=ACCENT, + padding=(1, 2), + ) + + +def show_chat_welcome(*, device: str = "", apps: list[str] | None = None) -> None: + """Welcome panel for truffile chat.""" + right: list[str] = [] + right.append(f"\033[1m\033[96mCommands\033[0m") + cmds = [ + ("/help", "show commands"), + ("/tasks", "pick a task"), + ("/new", "new conversation"), + ("/apps", "list installed apps"), + ("/deploy ", "deploy an app"), + ("/create ", "scaffold new app"), + ("/exit", "exit chat"), + ] + for name, desc in cmds: + right.append(f" \033[96m{name:<18}\033[0m \033[2m{desc}\033[0m") + + right.append("") + right.append(f"\033[1m\033[96mKeys\033[0m") + right.append(f" \033[96m{'alt+enter':<18}\033[0m \033[2mnew line\033[0m") + right.append(f" \033[96m{'tab':<18}\033[0m \033[2mcomplete command\033[0m") + right.append(f" \033[96m{'esc':<18}\033[0m \033[2minterrupt stream\033[0m") + right.append(f" \033[96m{'ctrl+d':<18}\033[0m \033[2mexit\033[0m") + + if apps: + right.append("") + right.append(f"\033[1m\033[96mApps\033[0m") + for a in apps[:6]: + right.append(f" \033[96m/{a}\033[0m") + if len(apps) > 6: + right.append(f" \033[2m...and {len(apps) - 6} more\033[0m") + + subtitle = f"connected to {device}" if device else "" + _console.print(_build_panel(right_lines=right, title="Truffile Chat", subtitle=subtitle)) + + +def show_infer_welcome(*, model: str = "", device: str = "") -> None: + """Welcome panel for truffile infer.""" + right: list[str] = [] + right.append(f"\033[1m\033[96mCommands\033[0m") + cmds = [ + ("/help", "show commands"), + ("/models", "switch model"), + ("/config", "show config"), + ("/mcp connect ", "connect MCP"), + ("/attach ", "attach image"), + ("/create ", "scaffold new app"), + ("/reset", "clear history"), + ("/exit", "exit"), + ] + for name, desc in cmds: + right.append(f" \033[96m{name:<22}\033[0m \033[2m{desc}\033[0m") + + right.append("") + right.append(f"\033[1m\033[96mKeys\033[0m") + right.append(f" \033[96m{'alt+enter':<22}\033[0m \033[2mnew line\033[0m") + right.append(f" \033[96m{'tab':<22}\033[0m \033[2mcomplete command\033[0m") + right.append(f" \033[96m{'esc':<22}\033[0m \033[2minterrupt stream\033[0m") + right.append(f" \033[96m{'ctrl+d':<22}\033[0m \033[2mexit\033[0m") + + if model: + right.append("") + right.append(f"\033[1m\033[96mModel\033[0m") + right.append(f" \033[2m{model}\033[0m") + + subtitle = f"{device} ยท {model}" if device and model else device or model or "" + _console.print(_build_panel(right_lines=right, title="Truffile Infer", subtitle=subtitle)) + + +def show_help_welcome() -> None: + """Welcome panel for truffile help.""" + right: list[str] = [] + right.append(f"\033[1m\033[96mCommands\033[0m") + cmds = [ + ("scan", "scan for devices"), + ("connect ", "connect to device"), + ("disconnect", "disconnect"), + ("create [name]", "scaffold new app"), + ("validate [path]", "validate app"), + ("deploy [path]", "deploy to device"), + ("list ", "list apps or devices"), + ("delete [app]", "delete app"), + ("models", "list models"), + ("chat", "agent chat"), + ("infer", "raw inference"), + ("help", "show this"), + ] + for name, desc in cmds: + right.append(f" \033[96m{name:<22}\033[0m \033[2m{desc}\033[0m") + + right.append("") + right.append(f"\033[1m\033[96mExamples\033[0m") + examples = [ + "truffile scan", + "truffile connect truffle-1234", + "truffile create my-app", + "truffile deploy ./my-app", + "truffile chat", + ] + for ex in examples: + right.append(f" \033[2m{ex}\033[0m") + + _console.print(_build_panel(right_lines=right, title="Truffile", subtitle="TruffleOS SDK")) From a40ba414965f1c86fb526452a451277c5d96f8e0 Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sun, 5 Apr 2026 18:16:13 -0700 Subject: [PATCH 11/12] remove old work flow --- .github/workflows/publish_pip.yml | 111 ------------------------------ 1 file changed, 111 deletions(-) delete mode 100644 .github/workflows/publish_pip.yml diff --git a/.github/workflows/publish_pip.yml b/.github/workflows/publish_pip.yml deleted file mode 100644 index 8c84e4d..0000000 --- a/.github/workflows/publish_pip.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Build and Publish - -on: - push: - branches: [main] - pull_request: - branches: [main] - -permissions: - contents: write - id-token: write - -jobs: - build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: . - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install build dependencies - run: pip install build - - - name: Set version from run number - run: | - if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "SETUPTOOLS_SCM_PRETEND_VERSION=0.1.${{ github.run_number }}" >> $GITHUB_ENV - else - echo "SETUPTOOLS_SCM_PRETEND_VERSION=0.0.dev0" >> $GITHUB_ENV - fi - - - name: Build wheel - run: python -m build - - - name: Show dist contents - run: ls -lah dist - - - name: Test wheel installs and imports - run: | - pip install dist/*.whl - python -c "from truffile import TruffleClient; print('Import OK')" - truffile --help - - - name: Upload wheel artifact - uses: actions/upload-artifact@v4 - with: - name: truffile-wheel - path: dist/*.whl - - - name: Upload sdist artifact - uses: actions/upload-artifact@v4 - with: - name: truffile-sdist - path: dist/*.tar.gz - - publish-pypi: - name: Publish to PyPI - needs: build - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - environment: - name: pypi - url: https://pypi.org/p/truffile - permissions: - id-token: write - steps: - - name: Download wheel - uses: actions/download-artifact@v4 - with: - name: truffile-wheel - path: dist/ - - - name: Download sdist - uses: actions/download-artifact@v4 - with: - name: truffile-sdist - path: dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - - github-release: - name: Create GitHub Release - needs: [build, publish-pypi] - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - permissions: - contents: write - steps: - - name: Download wheel - uses: actions/download-artifact@v4 - with: - name: truffile-wheel - path: dist/ - - - name: Create Release - uses: softprops/action-gh-release@v2 - with: - tag_name: v0.1.${{ github.run_number }} - name: truffile v0.1.${{ github.run_number }} - files: dist/*.whl - generate_release_notes: true - make_latest: true From 67bac9e3f1b4dfad2e34d4b526e79c5d225478e4 Mon Sep 17 00:00:00 2001 From: notabd7-deepshard Date: Sun, 5 Apr 2026 18:18:31 -0700 Subject: [PATCH 12/12] have to sort tests out first because of publishing changes --- .github/workflows/test.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 127f0fc..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Tests - -on: - pull_request: - push: - branches: [main] - -jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.12', '3.13'] - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: install deps - run: | - pip install pytest - pip install -e . - - - name: run tests - run: python -m pytest tests/ -v