diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efcc3a998..ccea311c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,9 +28,27 @@ jobs: - name: Setup uv virtual environment run: uv venv - - name: Install dependencies + - name: Install lean dependencies run: uv pip install -e . + - name: Verify lean install keeps native extras optional + run: | + uv run python - <<'PY' + import importlib.util + + optional_modules = ["PIL", "playwright", "regex", "ripgrep", "tiktoken"] + installed = [name for name in optional_modules if importlib.util.find_spec(name)] + if installed: + raise SystemExit(f"Lean install unexpectedly included optional modules: {installed}") + print("Lean install optional-native smoke check passed") + PY + + - name: Install CI test extras + # The full historical test suite exercises image/search behavior. + # macOS CI can use the PyPI ripgrep [search] extra; Android/Termux uses + # system rg from `pkg install ripgrep` instead. + run: uv pip install -e ".[images,search]" + - name: Install pexpect for integration tests run: uv pip install pexpect>=4.9.0 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 07b100d2a..bc26a8cde 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,11 +34,29 @@ jobs: - name: Setup uv virtual environment run: uv venv - - name: Install dependencies + - name: Install lean durable dependencies # [durable] extras pulls in dbos, required by integration test # tests/integration/test_dbos_enabled.py run: uv pip install -e ".[durable]" + - name: Verify lean install keeps native extras optional + run: | + uv run python - <<'PY' + import importlib.util + + optional_modules = ["PIL", "playwright", "regex", "ripgrep", "tiktoken"] + installed = [name for name in optional_modules if importlib.util.find_spec(name)] + if installed: + raise SystemExit(f"Lean install unexpectedly included optional modules: {installed}") + print("Lean install optional-native smoke check passed") + PY + + - name: Install CI test extras + # The full historical test suite exercises image/search behavior. + # macOS CI can use the PyPI ripgrep [search] extra; Android/Termux uses + # system rg from `pkg install ripgrep` instead. + run: uv pip install -e ".[durable,images,search]" + - name: Install pexpect for integration tests run: uv pip install pexpect>=4.9.0 diff --git a/README.md b/README.md index acc9cb7ae..c7f4e1141 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,73 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | ie uvx code-puppy ``` +#### Android / Termux + +Code Puppy installs lean on Android/Termux. Read the full guide at +[`docs/ANDROID_INSTALL.md`](docs/ANDROID_INSTALL.md). Android's Python wheel +availability is different from desktop Linux, so the installer keeps optional +native-heavy features detached and makes the required Termux build toolchain +explicit. + +**The one-command path (recommended):** the interactive bootstrap *wizard* +detects the device and walks you through every remaining step (native helper +packages, Code Puppy) with a `[Y/n]` confirmation before each one, then verifies +the install. Install `uv rust clang` first because `uvx --from code-puppy ...` +must resolve Code Puppy's required Python dependencies before the wizard can run: + +```bash +pkg update +pkg install python git uv rust clang +uvx --from code-puppy code-puppy-bootstrap wizard +``` + +The wizard is stdlib-only, gates every state-changing step behind a prompt, and +finishes with a verification + reconciliation summary. Add `--yes` to +auto-confirm for scripted installs, or `--dry-run` to preview without executing +anything. You can also inspect the plan without running it: + +```bash +uvx --from code-puppy code-puppy-bootstrap detect # what the wizard sees +uvx --from code-puppy code-puppy-bootstrap plan # the lean install plan +``` + +
+Prefer to drive it by hand? Here is the manual flow the wizard runs. + +```bash +# In Termux +pkg update +pkg install python git uv rust clang ripgrep proot +uv tool install --refresh code-puppy +code-puppy -i +``` + +
+ +Install `ripgrep` from Termux (`pkg install ripgrep`) so the file-search and +recursive-listing tools work — Code Puppy uses the system `rg` binary. Android +support does not remove search support; it only prevents the PyPI `ripgrep` +package from being installed on Android/Termux, where it is known to fail. +Desktop and CI installs using the optional `[search]` extra remain unchanged. + +#### Optional: Browser automation + +Browser tools are powered by [Playwright](https://playwright.dev/), which has no +wheels for some platforms (e.g. Android/Termux). It is therefore an optional +extra and is **not** part of the base install. On supported platforms, opt in: + +```bash +pip install "code-puppy[browser]" +# or +uv tool install "code-puppy[browser]" + +# then download the browser binaries +playwright install chromium +``` + +Without this extra, Code Puppy runs normally; browser tools simply report that +the optional `browser` dependency is missing when they are invoked. + #### Optional: DBOS durable execution Code Puppy ships with an optional [DBOS](https://github.com/dbos-inc/dbos-transact-py)-backed diff --git a/code_puppy/bootstrap.py b/code_puppy/bootstrap.py new file mode 100644 index 000000000..3c71510d1 --- /dev/null +++ b/code_puppy/bootstrap.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import argparse +import json +import sys +from typing import Any + +from code_puppy.bootstrap_profiles import ( + available_profiles, + build_install_plan, + detect_environment, +) + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Code Puppy bootstrap planner for lean environment-aware installs" + ) + subparsers = parser.add_subparsers(dest="command") + + detect_parser = subparsers.add_parser( + "detect", + help="Inspect the current environment without importing the runtime CLI", + ) + detect_parser.add_argument("--json", action="store_true", help="Print JSON output") + + plan_parser = subparsers.add_parser( + "plan", + help="Build a lean install/reattach plan from a builtin profile and optional manifest", + ) + plan_parser.add_argument( + "--profile", + default="auto", + choices=["auto", *available_profiles()], + help="Builtin install profile to use", + ) + plan_parser.add_argument( + "--manifest-json", + default="", + help="Inline JSON manifest with extras_add/extras_remove/notes overrides", + ) + plan_parser.add_argument( + "--manifest-file", + default="", + help="Path to a JSON manifest file with plan overrides", + ) + plan_parser.add_argument("--json", action="store_true", help="Print JSON output") + + wizard_parser = subparsers.add_parser( + "wizard", + help="Interactively install Code Puppy step-by-step with confirmation", + ) + wizard_parser.add_argument( + "--profile", + default="auto", + choices=["auto", *available_profiles()], + help="Builtin install profile to use", + ) + wizard_parser.add_argument( + "--yes", + action="store_true", + help="Auto-confirm every step (non-interactive automation)", + ) + wizard_parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would run without executing anything", + ) + + return parser + + +def _human_detect(environment: dict[str, Any]) -> str: + lines = [ + "Code Puppy bootstrap environment", + f"- python: {environment['python_executable']}", + f"- version: {environment['python_version']}", + f"- platform: {environment['platform_system']} {environment['platform_release']} ({environment['platform_machine']})", + f"- termux: {'yes' if environment['is_termux'] else 'no'}", + f"- android: {'yes' if environment['is_android'] else 'no'}", + f"- uv: {'yes' if environment['has_uv'] else 'no'}", + f"- uvx: {'yes' if environment['has_uvx'] else 'no'}", + f"- proot: {'yes' if environment['has_proot'] else 'no'}", + f"- ripgrep: {'yes' if environment['has_ripgrep'] else 'no'}", + f"- rust: {'yes' if environment['has_rust'] else 'no'}", + f"- clang: {'yes' if environment['has_clang'] else 'no'}", + ] + return "\n".join(lines) + + +def _human_plan(plan: dict[str, Any]) -> str: + lines = [ + f"Profile: {plan['profile']}", + f"Package spec: {plan['package_spec']}", + f"Install: {plan['install_command']}", + f"Reattach: {plan['reattach_command']}", + f"Run: {plan['run_command']}", + ] + if plan["extras"]: + lines.append(f"Extras: {', '.join(plan['extras'])}") + if plan["degraded_capabilities"]: + lines.append("Degraded until reattach:") + lines.extend(f"- {item}" for item in plan["degraded_capabilities"]) + if plan["system_packages"]: + lines.append("Suggested system packages:") + lines.extend(f"- {item}" for item in plan["system_packages"]) + if plan["notes"]: + lines.append("Notes:") + lines.extend(f"- {item}" for item in plan["notes"]) + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + parser = _build_parser() + args = parser.parse_args(argv) + + command = args.command or "plan" + + try: + if command == "wizard": + from code_puppy.bootstrap_wizard import run_wizard + + return run_wizard( + profile=getattr(args, "profile", "auto"), + assume_yes=getattr(args, "yes", False), + dry_run=getattr(args, "dry_run", False), + ) + + if command == "detect": + environment = detect_environment() + if args.json: + print(json.dumps(environment, indent=2, sort_keys=True)) + else: + print(_human_detect(environment)) + return 0 + + plan = build_install_plan( + requested_profile=getattr(args, "profile", "auto"), + manifest_json=getattr(args, "manifest_json", ""), + manifest_file=getattr(args, "manifest_file", ""), + ) + if getattr(args, "json", False): + print(json.dumps(plan, indent=2, sort_keys=True)) + else: + print(_human_plan(plan)) + return 0 + except ValueError as exc: + parser.exit(status=2, message=f"error: {exc}\n") + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/code_puppy/bootstrap_profiles.py b/code_puppy/bootstrap_profiles.py new file mode 100644 index 000000000..09f3aef7b --- /dev/null +++ b/code_puppy/bootstrap_profiles.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +import json +import os +import platform +import shlex +import shutil +import sys +from copy import deepcopy +from pathlib import Path +from typing import Any + +DEFAULT_PACKAGE_NAME = "code-puppy" + +_BUILTIN_PROFILES: dict[str, dict[str, Any]] = { + "core": { + "description": "Lean base runtime with no optional extras attached.", + "extras": [], + "notes": [ + "Use this when a launcher only needs the core CLI/runtime.", + "Optional capabilities should reattach later based on deployment policy.", + ], + "system_packages": [], + }, + "android-termux-lean": { + "description": "Android/Termux-safe profile that keeps Python deps lean.", + "extras": [], + "notes": [ + "Install the Termux build toolchain first; pydantic-core has no Android wheel.", + "Avoid browser/image/search extras during initial attach.", + "Reattach optional capabilities only after the target environment is known.", + ], + "system_packages": [ + "pkg install rust clang", + "pkg install ripgrep proot", + ], + }, + "desktop-browser": { + "description": "Desktop-oriented profile with local browser, image, and search helpers.", + "extras": ["browser", "images", "search"], + "notes": [ + "Useful for developer laptops and desktop automation environments.", + ], + "system_packages": [], + }, + "developer-full": { + "description": "Broad developer profile with provider and tooling extras attached.", + "extras": [ + "bedrock", + "browser", + "durable", + "images", + "search", + ], + "notes": [ + "This is intentionally heavy; use it only when the environment is known-good.", + ], + "system_packages": [], + }, +} + + +def available_profiles() -> list[str]: + return sorted(_BUILTIN_PROFILES) + + +def detect_environment() -> dict[str, Any]: + executable = Path(sys.executable).expanduser().resolve() + release = platform.release() + system_name = platform.system() + termux_prefix = os.environ.get("PREFIX", "") + termux_version = os.environ.get("TERMUX_VERSION", "") + is_termux = ( + bool(termux_version) + or "com.termux" in str(executable) + or (system_name == "Linux" and "com.termux" in termux_prefix) + ) + is_android = is_termux or "android" in release.lower() + + return { + "python_executable": str(executable), + "python_version": platform.python_version(), + "platform_system": system_name, + "platform_release": release, + "platform_machine": platform.machine(), + "is_android": is_android, + "is_termux": is_termux, + "is_windows": system_name == "Windows", + "is_macos": system_name == "Darwin", + "is_linux": system_name == "Linux", + "has_uv": shutil.which("uv") is not None, + "has_uvx": shutil.which("uvx") is not None, + "has_proot": shutil.which("proot") is not None, + "has_ripgrep": shutil.which("rg") is not None, + "has_rust": shutil.which("rustc") is not None + and shutil.which("cargo") is not None, + "has_clang": shutil.which("clang") is not None, + "has_git": shutil.which("git") is not None, + } + + +def auto_profile_name(environment: dict[str, Any]) -> str: + if environment.get("is_termux") or environment.get("is_android"): + return "android-termux-lean" + if environment.get("is_windows") or environment.get("is_macos"): + return "desktop-browser" + if environment.get("is_linux"): + return "desktop-browser" + return "core" + + +def resolve_profile_name( + requested_profile: str | None, environment: dict[str, Any] +) -> str: + raw = ( + requested_profile or os.environ.get("CODE_PUPPY_INSTALL_PROFILE", "auto") + ).strip() + if not raw or raw == "auto": + return auto_profile_name(environment) + if raw not in _BUILTIN_PROFILES: + choices = ", ".join(available_profiles()) + raise ValueError(f"unknown install profile '{raw}'. Choices: auto, {choices}") + return raw + + +def _parse_manifest_json(raw: str) -> dict[str, Any]: + data = json.loads(raw) + if not isinstance(data, dict): + raise ValueError("bootstrap manifest must be a JSON object") + return data + + +def load_manifest( + *, + manifest_json: str = "", + manifest_file: str = "", +) -> dict[str, Any]: + raw_json = ( + manifest_json.strip() + or os.environ.get("CODE_PUPPY_BOOTSTRAP_MANIFEST_JSON", "").strip() + ) + raw_file = ( + manifest_file.strip() + or os.environ.get("CODE_PUPPY_BOOTSTRAP_MANIFEST_FILE", "").strip() + ) + + if raw_json and raw_file: + raise ValueError("choose manifest_json or manifest_file, not both") + if raw_json: + return _parse_manifest_json(raw_json) + if raw_file: + return _parse_manifest_json( + Path(raw_file).expanduser().read_text(encoding="utf-8") + ) + return {} + + +def _normalize_list(value: Any, *, field_name: str) -> list[str]: + if value in (None, ""): + return [] + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise ValueError( + f"bootstrap manifest field '{field_name}' must be a list of strings" + ) + return [item.strip() for item in value if item.strip()] + + +def merge_profile( + profile_name: str, + manifest: dict[str, Any] | None = None, +) -> tuple[dict[str, Any], list[str]]: + profile = deepcopy(_BUILTIN_PROFILES[profile_name]) + reasons = [f"base profile: {profile_name}"] + manifest = manifest or {} + if not manifest: + return profile, reasons + + profile_override = manifest.get("profile", "") + if profile_override and profile_override != profile_name: + raise ValueError( + f"manifest profile '{profile_override}' does not match resolved profile '{profile_name}'" + ) + + extras = set(profile["extras"]) + extras.update(_normalize_list(manifest.get("extras_add"), field_name="extras_add")) + extras.difference_update( + _normalize_list(manifest.get("extras_remove"), field_name="extras_remove") + ) + profile["extras"] = sorted(extras) + + for key in ("notes", "system_packages"): + profile[key] = profile[key] + _normalize_list(manifest.get(key), field_name=key) + + description = manifest.get("description", "") + if description: + if not isinstance(description, str): + raise ValueError("bootstrap manifest field 'description' must be a string") + profile["description"] = description.strip() + + package_name = manifest.get("package_name", "") + if package_name: + if not isinstance(package_name, str): + raise ValueError("bootstrap manifest field 'package_name' must be a string") + profile["package_name"] = package_name.strip() + + if manifest: + reasons.append("manifest overrides applied") + return profile, reasons + + +def package_spec(package_name: str, extras: list[str]) -> str: + if not extras: + return package_name + return f"{package_name}[{','.join(sorted(extras))}]" + + +def install_command(spec: str, environment: dict[str, Any]) -> str: + if environment.get("has_uv"): + return f"uv tool install --refresh {shlex.quote(spec)}" + return f"python -m pip install {shlex.quote(spec)}" + + +def reattach_command(spec: str, environment: dict[str, Any]) -> str: + if environment.get("has_uv"): + return f"uv tool install --refresh {shlex.quote(spec)}" + return f"python -m pip install --upgrade {shlex.quote(spec)}" + + +def build_install_plan( + *, + requested_profile: str | None = None, + manifest_json: str = "", + manifest_file: str = "", +) -> dict[str, Any]: + environment = detect_environment() + resolved_profile = resolve_profile_name(requested_profile, environment) + manifest = load_manifest(manifest_json=manifest_json, manifest_file=manifest_file) + profile, reasons = merge_profile(resolved_profile, manifest) + selected_package = profile.get("package_name", DEFAULT_PACKAGE_NAME) + selected_extras = profile["extras"] + spec = package_spec(selected_package, selected_extras) + + degraded = [] + if resolved_profile == "android-termux-lean": + degraded = [ + "browser automation extras detached", + "image/search extras detached", + ] + + missing_system_packages = [] + if resolved_profile == "android-termux-lean" and not environment.get("has_rust"): + missing_system_packages.append("rust") + if resolved_profile == "android-termux-lean" and not environment.get("has_clang"): + missing_system_packages.append("clang") + if resolved_profile == "android-termux-lean" and not environment.get("has_ripgrep"): + missing_system_packages.append("ripgrep") + if resolved_profile == "android-termux-lean" and not environment.get("has_proot"): + missing_system_packages.append("proot") + + return { + "profile": resolved_profile, + "package_name": selected_package, + "package_spec": spec, + "description": profile["description"], + "extras": selected_extras, + "notes": profile["notes"], + "system_packages": profile["system_packages"], + "missing_system_packages": missing_system_packages, + "environment": environment, + "reasons": reasons, + "install_command": install_command(spec, environment), + "reattach_command": reattach_command(spec, environment), + "run_command": "code-puppy -i", + "degraded_capabilities": degraded, + "manifest_applied": bool(manifest), + } diff --git a/code_puppy/bootstrap_wizard.py b/code_puppy/bootstrap_wizard.py new file mode 100644 index 000000000..bb61991b0 --- /dev/null +++ b/code_puppy/bootstrap_wizard.py @@ -0,0 +1,259 @@ +"""Interactive, environment-aware install wizard for Code Puppy. + +The bootstrap *planner* (``code_puppy.bootstrap_profiles``) decides *what* +should be installed. This module is the *wizard*: it walks an operator -- +especially an Android/Termux newcomer -- through *doing* it, one confirmed +step at a time, then verifies and reconciles the result. + +Design rules honored here: +- stdlib-only (``subprocess``/``shutil``) so it runs before any heavy deps +- destructive/state-altering steps are gated behind explicit confirmation +- ``--dry-run`` never executes; ``--yes`` auto-confirms for automation +- the wizard always ends with a verification + reconciliation summary +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from dataclasses import dataclass, field +from typing import Any, Callable + +from code_puppy.bootstrap_profiles import build_install_plan, detect_environment + + +@dataclass +class WizardStep: + """A single confirmable unit of install work.""" + + key: str + title: str + command: str + explanation: str + # Returns True when the step's goal is *already* satisfied (skip it). + is_satisfied: Callable[[], bool] = field(default=lambda: False) + required: bool = True + + +@dataclass +class StepOutcome: + key: str + status: str # satisfied | done | skipped | failed | dry-run + detail: str = "" + + +def _has(binary: str) -> bool: + return shutil.which(binary) is not None + + +def build_steps(profile: str | None = None) -> tuple[list[WizardStep], dict[str, Any]]: + """Derive the ordered wizard steps from the canonical install plan. + + Reusing :func:`build_install_plan` keeps a single source of truth for + *what* to install -- the wizard only decides *how* to walk it. + """ + + plan = build_install_plan(requested_profile=profile) + env = plan["environment"] + steps: list[WizardStep] = [] + + # 1. Package manager (uv) -- the engine everything else rides on. + if not env.get("has_uv"): + if env.get("is_termux") or env.get("is_android"): + uv_cmd = "pkg install -y uv" + else: + uv_cmd = "curl -LsSf https://astral.sh/uv/install.sh | sh" + steps.append( + WizardStep( + key="uv", + title="Install the uv package manager", + command=uv_cmd, + explanation=( + "uv installs and runs Code Puppy in an isolated tool env; " + "Termux should use its packaged uv instead of building uv from PyPI." + ), + is_satisfied=lambda: _has("uv"), + ) + ) + + # 2. System packages: Android build toolchain + native helper binaries. + missing = plan.get("missing_system_packages") or [] + if missing: + steps.append( + WizardStep( + key="system_packages", + title=f"Install Termux system packages: {', '.join(missing)}", + command=f"pkg install -y {' '.join(missing)}", + explanation=( + "Termux needs rust/clang to build unavoidable native Python " + "deps (pydantic-core), plus helper binaries like ripgrep/proot." + ), + is_satisfied=lambda missing=missing: all( + _has({"ripgrep": "rg", "rust": "rustc"}.get(m, m)) for m in missing + ), + ) + ) + + # 3. Code Puppy itself. + steps.append( + WizardStep( + key="code_puppy", + title=f"Install Code Puppy ({plan['package_spec']})", + command=plan["install_command"], + explanation=plan["description"], + is_satisfied=lambda: False, # always (re)attach; --refresh is idempotent + ) + ) + + return steps, plan + + +def _confirm(prompt: str, *, assume_yes: bool) -> bool: + if assume_yes: + print(f"{prompt} [auto-yes]") + return True + if not sys.stdin.isatty(): + # Non-interactive without --yes: refuse to act, don't hang. + print(f"{prompt} [no TTY -- skipping; pass --yes to auto-run]") + return False + try: + answer = input(f"{prompt} [Y/n] ").strip().lower() + except (EOFError, KeyboardInterrupt): + print() + return False + return answer in ("", "y", "yes") + + +def _run_command(command: str) -> tuple[int, str]: + """Run a shell command, streaming nothing, capturing combined output.""" + + try: + proc = subprocess.run( + command, + shell=True, + capture_output=True, + text=True, + timeout=1800, + ) + except subprocess.TimeoutExpired: + return 124, "timed out after 1800s" + except Exception as exc: # never crash the wizard + return 1, f"failed to launch: {exc}" + output = (proc.stdout or "") + (proc.stderr or "") + return proc.returncode, output.strip() + + +def _verify(plan: dict[str, Any]) -> StepOutcome: + """Confirm Code Puppy actually landed and can answer for itself.""" + + if not _has("code-puppy"): + return StepOutcome( + "verify", + "failed", + "`code-puppy` not found on PATH after install.", + ) + code, out = _run_command("code-puppy --help") + if code != 0: + tail = out.splitlines()[-3:] if out else [] + return StepOutcome("verify", "failed", "; ".join(tail) or f"exit {code}") + return StepOutcome("verify", "done", "`code-puppy --help` runs cleanly.") + + +def run_wizard( + *, + profile: str | None = None, + assume_yes: bool = False, + dry_run: bool = False, +) -> int: + """Execute the interactive install wizard. Returns a process exit code.""" + + steps, plan = build_steps(profile) + env = plan["environment"] + + print("Code Puppy install wizard") + print(f" profile : {plan['profile']}") + print( + " device : " + f"{env['platform_system']} {env['platform_release']} ({env['platform_machine']})" + ) + print(f" python : {env['python_version']}") + print(f" steps : {len(steps)}") + if dry_run: + print(" mode : DRY-RUN (nothing will be executed)") + print() + + outcomes: list[StepOutcome] = [] + + for index, step in enumerate(steps, start=1): + print(f"[{index}/{len(steps)}] {step.title}") + print(f" why: {step.explanation}") + print(f" run: {step.command}") + + if step.is_satisfied(): + print(" -> already satisfied, skipping.\n") + outcomes.append(StepOutcome(step.key, "satisfied")) + continue + + if dry_run: + print(" -> dry-run, not executed.\n") + outcomes.append(StepOutcome(step.key, "dry-run")) + continue + + if not _confirm(" proceed?", assume_yes=assume_yes): + status = "skipped" + print(f" -> {status}.\n") + outcomes.append(StepOutcome(step.key, status)) + if step.required: + print( + " (this step is required -- later steps may fail without it)\n" + ) + continue + + code, out = _run_command(step.command) + if code == 0: + print(" -> done.\n") + outcomes.append(StepOutcome(step.key, "done")) + else: + tail = "\n ".join(out.splitlines()[-5:]) if out else f"exit {code}" + print(f" -> FAILED (exit {code}):\n {tail}\n") + outcomes.append(StepOutcome(step.key, "failed", f"exit {code}")) + if step.required: + print("Required step failed -- stopping. Fix the above and re-run.\n") + _print_summary(outcomes, verify=None) + return 1 + + verify = None + if not dry_run: + verify = _verify(plan) + symbol = "OK" if verify.status == "done" else "FAILED" + print(f"Verification: {symbol} -- {verify.detail}\n") + + _print_summary(outcomes, verify=verify) + + failed = any(o.status == "failed" for o in outcomes) + if verify is not None and verify.status == "failed": + failed = True + return 1 if failed else 0 + + +def _print_summary(outcomes: list[StepOutcome], *, verify: StepOutcome | None) -> None: + print("Summary (state reconciliation):") + for outcome in outcomes: + line = f" - {outcome.key}: {outcome.status}" + if outcome.detail: + line += f" ({outcome.detail})" + print(line) + if verify is not None: + line = f" - verify: {verify.status}" + if verify.detail: + line += f" ({verify.detail})" + print(line) + if verify is not None and verify.status == "done": + print("\nAll set -- run it with: code-puppy -i") + + +def detect_summary() -> dict[str, Any]: + """Thin re-export so callers can pre-flight without importing profiles.""" + + return detect_environment() diff --git a/code_puppy/tools/browser/browser_manager.py b/code_puppy/tools/browser/browser_manager.py index d1b4da07d..ca1b7be08 100644 --- a/code_puppy/tools/browser/browser_manager.py +++ b/code_puppy/tools/browser/browser_manager.py @@ -7,14 +7,58 @@ import atexit import contextvars import os +import sys +import types from pathlib import Path -from typing import Callable, Dict, Optional - -from playwright.async_api import Browser, BrowserContext, Page +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional from code_puppy import config from code_puppy.messaging import emit_info, emit_success, emit_warning +if TYPE_CHECKING: + from playwright.async_api import Browser, BrowserContext, Page +else: + Browser = BrowserContext = Page = Any + + +def _missing_playwright(*_args, **_kwargs): + """Raise a friendly error when Playwright is unavailable.""" + raise RuntimeError( + "Playwright is not installed in this environment. " + "Browser tools require the optional 'playwright' dependency " + "(install code-puppy[browser]) on a supported platform." + ) + + +def _install_playwright_stub() -> None: + """Register a minimal stub so browser modules stay importable. + + Keeps optional browser support from crashing startup on platforms where + Playwright wheels are unavailable (e.g. Android/Termux). Browser tools then + fail loudly only when actually invoked, instead of at import time. + """ + playwright_module = sys.modules.get("playwright") + if playwright_module is None: + playwright_module = types.ModuleType("playwright") + sys.modules["playwright"] = playwright_module + + async_api_module = sys.modules.get("playwright.async_api") + if async_api_module is None: + async_api_module = types.ModuleType("playwright.async_api") + async_api_module.Browser = Browser + async_api_module.BrowserContext = BrowserContext + async_api_module.Page = Page + async_api_module.async_playwright = _missing_playwright + sys.modules["playwright.async_api"] = async_api_module + + setattr(playwright_module, "async_api", async_api_module) + + +try: + import playwright.async_api as _playwright_async_api # noqa: F401 +except ImportError: + _install_playwright_stub() + # Registry for custom browser types from plugins (e.g., Camoufox for stealth browsing) _CUSTOM_BROWSER_TYPES: Dict[str, Callable] = {} _BROWSER_TYPES_LOADED: bool = False diff --git a/code_puppy/tools/image_tools.py b/code_puppy/tools/image_tools.py index 9081a09db..57e8a434c 100644 --- a/code_puppy/tools/image_tools.py +++ b/code_puppy/tools/image_tools.py @@ -16,7 +16,18 @@ from pathlib import Path from typing import Any, Dict, Union -from PIL import Image, UnidentifiedImageError +try: + from PIL import Image, UnidentifiedImageError + + PIL_AVAILABLE = True +except ImportError: # Pillow is optional; install code-puppy[images] + Image = None + + class UnidentifiedImageError(Exception): + """Fallback used only when Pillow is not installed.""" + + PIL_AVAILABLE = False + from pydantic_ai import BinaryContent, RunContext, ToolReturn from code_puppy.messaging import emit_error, emit_info, emit_success @@ -40,6 +51,11 @@ def _validate_and_prepare_image( file extension. If the image is resized, the output is normalized to PNG so the returned bytes and MIME type stay in sync like civilized software. """ + if not PIL_AVAILABLE or Image is None: + raise ValueError( + "Image tools require Pillow. Install with: pip install 'code-puppy[images]'" + ) + try: with Image.open(io.BytesIO(image_bytes)) as verified_image: verified_image.verify() diff --git a/docs/ANDROID_INSTALL.md b/docs/ANDROID_INSTALL.md new file mode 100644 index 000000000..aaee3603e --- /dev/null +++ b/docs/ANDROID_INSTALL.md @@ -0,0 +1,160 @@ +# Code Puppy on Android / Termux + +This guide is for a **fresh Termux install on Android**. Code Puppy works on +Android, but it is not a normal desktop Python install: several dependencies use +native code, and Android does not receive the same PyPI wheels as Linux/macOS. + +The installer path below is intentionally conservative: install the Termux build +toolchain first, keep optional heavyweight features detached, then install the +lean Code Puppy runtime. + +## 0. Use the real Termux + +Install Termux from F-Droid or GitHub releases. The Play Store build is stale and +will cause weird package failures. Weird as in "why is my phone haunted?" weird. + +## 1. Recommended: run the bootstrap wizard + +After installing Termux, run: + +```bash +pkg update +pkg install python git uv rust clang +uvx --from code-puppy code-puppy-bootstrap wizard +``` + +Install `uv` from Termux (`pkg install uv`) instead of PyPI. On Android, PyPI may +fall back to building `uv` from source, which needs Rust before you even have the +installer. That is a very silly bootstrapping sandwich, so we skip it. + +The wizard detects your device, explains each step, asks before changing state, +and finishes by verifying `code-puppy --help`. + +For scripted installs: + +```bash +uvx --from code-puppy code-puppy-bootstrap wizard --yes +``` + +To preview only: + +```bash +uvx --from code-puppy code-puppy-bootstrap wizard --dry-run +``` + +## 2. Manual install path + +If you prefer to drive the meat wagon yourself: + +```bash +pkg update +pkg install python git uv rust clang ripgrep proot +uv tool install --refresh code-puppy +code-puppy -i +``` + +### Why `rust clang` are required + +`pydantic-core` is a required dependency of `pydantic`, which Code Puppy uses +through `pydantic-ai`. PyPI does not currently publish Android/Termux wheels for +`pydantic-core`, and Termux does not ship a prebuilt `python-pydantic` package in +the default repo. A fresh Android user therefore needs the Termux Rust/C build +toolchain so pip/uv can build that dependency locally. Install it before running +`uvx --from code-puppy ...`, because `uvx` must install Code Puppy before the +wizard code can execute. + +The base install deliberately avoids other native-heavy optional packages: + +- Playwright is in the `[browser]` extra. +- PyPI's bundled `ripgrep` package is in the `[search]` extra; the Android path + uses Termux's system `rg` from `pkg install ripgrep` instead. +- Pillow is in the `[images]` extra. +- `pydantic-ai-slim[openai]` is intentionally avoided in base because it pulls + `tiktoken` and `regex`, which require additional native builds on Android. + +Android support does not remove search support; it only prevents the PyPI +`ripgrep` package from being installed on Android/Termux, where it is known to +fail. Desktop and CI installs using `[search]` remain unchanged. + +## 3. Optional features + +Install these only after the lean runtime is working. + +```bash +# Image loading / resizing tools +uv tool install --refresh 'code-puppy[images]' + +# From a source checkout / editable install on Android, keep search desktop-only. +# Plain `uv sync` installs dev tools such as Ruff and may try to build them +# from source on Android; use --no-dev for runtime validation. +uv sync --no-dev --python "$(command -v python)" + +# Or, from an activated lean venv: +uv pip install -e '.[images]' + +# Desktop/browser automation environments only; not recommended on native Android +uv tool install --refresh 'code-puppy[browser]' + +# Desktop/CI only: PyPI ripgrep bundle for non-Termux systems. +# Android/Termux should prefer: pkg install ripgrep +uv tool install --refresh 'code-puppy[search]' + +# DBOS-backed durable execution +uv tool install --refresh 'code-puppy[durable]' +``` + +## 4. Verify the install + +```bash +code-puppy --help +code-puppy-bootstrap detect +code-puppy-bootstrap plan --profile auto +``` + +On Android/Termux, the plan should select `android-termux-lean`. + +## Troubleshooting + +### `aarch64-linux-android-clang: No such file or directory` + +Install the compiler: + +```bash +pkg install clang +``` + +### `Rust not found` or `maturin` fails building `pydantic-core` + +Install Rust: + +```bash +pkg install rust +``` + +Then retry: + +```bash +uv tool install --refresh code-puppy +``` + +### `rg` / search tools are unavailable + +Install Termux ripgrep: + +```bash +pkg install ripgrep +``` + +### Image tools say Pillow is missing + +That is expected on the lean Android install. Reattach the optional image extra: + +```bash +uv tool install --refresh 'code-puppy[images]' +``` + +### The install is slow + +Compiling Rust/C dependencies on a phone can take a while. Keep Termux in the +foreground, keep the screen awake if needed, and avoid running it while Android +is aggressively saving battery. diff --git a/pyproject.toml b/pyproject.toml index fb69e0450..47f07b7e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Code generation agent" readme = "README.md" requires-python = ">=3.11,<3.15" dependencies = [ - "pydantic-ai-slim[openai,anthropic,mcp]==1.56.0", + "pydantic-ai-slim[anthropic,mcp]==1.56.0", "typer>=0.12.0", "mcp>=1.9.4", "httpx[http2]>=0.24.1", @@ -22,10 +22,7 @@ dependencies = [ "pyfiglet>=0.8.post1", "rapidfuzz>=3.13.0", "openai>=1.99.1", - "ripgrep==14.1.0", - "playwright>=1.40.0", "termflow-md>=0.1.11", - "Pillow>=10.0.0", "azure-identity>=1.15.0", "anthropic==0.79.0", "boto3>=1.43.9", @@ -53,7 +50,10 @@ classifiers = [ [project.optional-dependencies] bedrock = ["boto3>=1.35.0"] +browser = ["playwright>=1.40.0"] durable = ["dbos>=2.11.0"] +images = ["Pillow>=10.0.0"] +search = ["ripgrep==14.1.0"] [project.urls] repository = "https://github.com/mpfaffenberger/code_puppy" @@ -63,6 +63,7 @@ HomePage = "https://github.com/mpfaffenberger/code_puppy" [project.scripts] code-puppy = "code_puppy.main:main_entry" pup = "code_puppy.main:main_entry" +code-puppy-bootstrap = "code_puppy.bootstrap:main" [tool.logfire] ignore_no_config = true diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py new file mode 100644 index 000000000..840b27c58 --- /dev/null +++ b/tests/test_bootstrap.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +from code_puppy import bootstrap, bootstrap_profiles + + +def test_bootstrap_import_does_not_pull_cli_runner(): + result = subprocess.run( + [ + sys.executable, + "-c", + "import sys; import code_puppy.bootstrap; print('code_puppy.cli_runner' in sys.modules)", + ], + capture_output=True, + text=True, + check=True, + ) + assert result.stdout.strip() == "False" + + +def test_detect_environment_termux(monkeypatch): + monkeypatch.setenv("TERMUX_VERSION", "0.119") + monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr") + environment = bootstrap_profiles.detect_environment() + assert environment["is_termux"] is True + assert environment["is_android"] is True + + +def test_auto_profile_prefers_android_termux(monkeypatch): + monkeypatch.setenv("TERMUX_VERSION", "0.119") + monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr") + plan = bootstrap_profiles.build_install_plan(requested_profile="auto") + assert plan["profile"] == "android-termux-lean" + assert plan["extras"] == [] + assert "browser automation extras detached" in plan["degraded_capabilities"] + + +def test_build_install_plan_applies_manifest_overrides(tmp_path: Path): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text( + json.dumps( + { + "profile": "android-termux-lean", + "extras_add": ["browser", "search"], + "extras_remove": ["search"], + "notes": ["cloud picked a browser-friendly lane"], + } + ) + ) + plan = bootstrap_profiles.build_install_plan( + requested_profile="android-termux-lean", + manifest_file=str(manifest_path), + ) + assert plan["extras"] == ["browser"] + assert plan["manifest_applied"] is True + assert "cloud picked a browser-friendly lane" in plan["notes"] + assert plan["package_spec"] == "code-puppy[browser]" + + +def test_build_install_plan_rejects_profile_mismatch(tmp_path: Path): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps({"profile": "desktop-browser"})) + with pytest.raises(ValueError, match="does not match"): + bootstrap_profiles.build_install_plan( + requested_profile="android-termux-lean", + manifest_file=str(manifest_path), + ) + + +def test_cli_detect_json(capsys): + exit_code = bootstrap.main(["detect", "--json"]) + assert exit_code == 0 + payload = json.loads(capsys.readouterr().out) + assert "python_executable" in payload + assert "is_termux" in payload + + +def test_cli_plan_defaults_to_auto_json(capsys): + exit_code = bootstrap.main(["plan", "--json"]) + assert exit_code == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["profile"] in bootstrap_profiles.available_profiles() + assert payload["package_spec"].startswith("code-puppy") + + +def test_build_install_plan_rejects_unknown_profile(): + with pytest.raises(ValueError, match="unknown install profile"): + bootstrap_profiles.build_install_plan(requested_profile="not-a-real-profile") + + +def test_build_install_plan_rejects_multiple_manifest_sources(tmp_path: Path): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text("{}") + with pytest.raises(ValueError, match="choose manifest_json or manifest_file"): + bootstrap_profiles.build_install_plan( + requested_profile="core", + manifest_json="{}", + manifest_file=str(manifest_path), + ) + + +def test_build_install_plan_falls_back_to_pip_without_uv(monkeypatch): + monkeypatch.setattr( + bootstrap_profiles, + "detect_environment", + lambda: { + "has_uv": False, + "has_uvx": False, + "has_proot": True, + "has_ripgrep": True, + "has_rust": True, + "has_clang": True, + "is_android": False, + "is_termux": False, + "is_linux": True, + "is_macos": False, + "is_windows": False, + }, + ) + plan = bootstrap_profiles.build_install_plan(requested_profile="core") + assert plan["install_command"] == "python -m pip install code-puppy" + assert plan["reattach_command"] == "python -m pip install --upgrade code-puppy" + + +def test_build_install_plan_reports_missing_android_system_packages(monkeypatch): + monkeypatch.setattr( + bootstrap_profiles, + "detect_environment", + lambda: { + "has_uv": True, + "has_uvx": True, + "has_proot": False, + "has_ripgrep": False, + "has_rust": False, + "has_clang": False, + "is_android": True, + "is_termux": True, + "is_linux": True, + "is_macos": False, + "is_windows": False, + }, + ) + plan = bootstrap_profiles.build_install_plan(requested_profile="auto") + assert plan["profile"] == "android-termux-lean" + assert plan["missing_system_packages"] == ["rust", "clang", "ripgrep", "proot"] + + +def test_cli_plan_human_output(capsys): + exit_code = bootstrap.main(["plan"]) + assert exit_code == 0 + output = capsys.readouterr().out + assert "Profile:" in output + assert "Install:" in output + assert "Run:" in output + + +def test_cli_plan_rejects_bad_manifest_json(capsys): + with pytest.raises(SystemExit) as exc_info: + bootstrap.main(["plan", "--manifest-json", "[]"]) + assert exc_info.value.code == 2 + assert "bootstrap manifest must be a JSON object" in capsys.readouterr().err diff --git a/tests/test_bootstrap_wizard.py b/tests/test_bootstrap_wizard.py new file mode 100644 index 000000000..1e8894784 --- /dev/null +++ b/tests/test_bootstrap_wizard.py @@ -0,0 +1,156 @@ +from __future__ import annotations + + +from code_puppy import bootstrap, bootstrap_profiles, bootstrap_wizard + + +def _fake_env(**overrides): + base = { + "python_executable": "/usr/bin/python3", + "python_version": "3.13.13", + "platform_system": "Linux", + "platform_release": "android", + "platform_machine": "aarch64", + "has_uv": True, + "has_uvx": True, + "has_proot": True, + "has_ripgrep": True, + "has_rust": True, + "has_clang": True, + "has_git": True, + "is_android": False, + "is_termux": False, + "is_linux": True, + "is_macos": False, + "is_windows": False, + } + base.update(overrides) + return base + + +def _patch_env(monkeypatch, **overrides): + monkeypatch.setattr( + bootstrap_profiles, + "detect_environment", + lambda: _fake_env(**overrides), + ) + + +def test_bare_termux_device_builds_three_steps(monkeypatch): + """A fresh phone with nothing installed gets packaged uv -> packages -> code-puppy.""" + + _patch_env( + monkeypatch, + is_android=True, + is_termux=True, + has_uv=False, + has_proot=False, + has_ripgrep=False, + has_rust=False, + has_clang=False, + ) + steps, plan = bootstrap_wizard.build_steps(profile="auto") + keys = [s.key for s in steps] + assert keys == ["uv", "system_packages", "code_puppy"] + + by_key = {s.key: s for s in steps} + # Termux should use its packaged uv instead of building uv from PyPI. + assert by_key["uv"].command == "pkg install -y uv" + assert ( + by_key["system_packages"].command == "pkg install -y rust clang ripgrep proot" + ) + assert plan["profile"] == "android-termux-lean" + + +def test_loaded_device_collapses_to_single_step(monkeypatch): + """When uv + native packages are present, only the install step remains.""" + + _patch_env(monkeypatch, is_android=True, is_termux=True) + steps, _ = bootstrap_wizard.build_steps(profile="auto") + assert [s.key for s in steps] == ["code_puppy"] + + +def test_desktop_missing_uv_uses_curl_installer(monkeypatch): + _patch_env(monkeypatch, has_uv=False) + steps, _ = bootstrap_wizard.build_steps(profile="core") + uv_step = next(s for s in steps if s.key == "uv") + assert "astral.sh/uv/install.sh" in uv_step.command + + +def test_dry_run_executes_nothing(monkeypatch, capsys): + _patch_env(monkeypatch, is_android=True, is_termux=True) + + def _boom(_command): # pragma: no cover - must never be called + raise AssertionError("dry-run must not execute commands") + + monkeypatch.setattr(bootstrap_wizard, "_run_command", _boom) + code = bootstrap_wizard.run_wizard(profile="auto", dry_run=True) + out = capsys.readouterr().out + assert code == 0 + assert "DRY-RUN" in out + assert "dry-run, not executed" in out + + +def test_yes_runs_steps_and_verifies(monkeypatch, capsys): + _patch_env( + monkeypatch, + is_android=True, + is_termux=True, + has_ripgrep=False, + has_proot=False, + has_rust=False, + has_clang=False, + ) + + calls = [] + + def _fake_run(command): + calls.append(command) + return 0, "ok" + + monkeypatch.setattr(bootstrap_wizard, "_run_command", _fake_run) + # Native packages still absent (so the step runs); code-puppy present after install. + monkeypatch.setattr(bootstrap_wizard, "_has", lambda b: b == "code-puppy") + + code = bootstrap_wizard.run_wizard(profile="auto", assume_yes=True) + out = capsys.readouterr().out + assert code == 0 + # system packages + code-puppy install + the verify --help call + assert any("pkg install -y rust clang ripgrep proot" in c for c in calls) + assert any("uv tool install" in c for c in calls) + assert "Verification: OK" in out + + +def test_required_failure_stops_and_returns_one(monkeypatch, capsys): + _patch_env( + monkeypatch, + is_android=True, + is_termux=True, + has_ripgrep=False, + has_proot=False, + has_rust=False, + has_clang=False, + ) + + def _fail(_command): + return 1, "pkg: network unreachable" + + monkeypatch.setattr(bootstrap_wizard, "_run_command", _fail) + code = bootstrap_wizard.run_wizard(profile="auto", assume_yes=True) + out = capsys.readouterr().out + assert code == 1 + assert "FAILED" in out + assert "stopping" in out.lower() + + +def test_verify_fails_when_binary_absent(monkeypatch): + _patch_env(monkeypatch) + monkeypatch.setattr(bootstrap_wizard, "_has", lambda _b: False) + outcome = bootstrap_wizard._verify({}) + assert outcome.status == "failed" + + +def test_cli_wizard_dry_run_via_main(capsys): + exit_code = bootstrap.main(["wizard", "--dry-run"]) + assert exit_code == 0 + assert "install wizard" in capsys.readouterr().out.lower() diff --git a/tests/test_browser_optional.py b/tests/test_browser_optional.py new file mode 100644 index 000000000..e759d4d8d --- /dev/null +++ b/tests/test_browser_optional.py @@ -0,0 +1,45 @@ +"""Browser automation must be an optional capability. + +Playwright has no wheels for some platforms (e.g. Android/Termux), so it is an +optional ``[browser]`` extra. Importing ``code_puppy.tools`` must not crash when +Playwright is unavailable; browser tools should only fail when actually invoked. +""" + +from __future__ import annotations + +import subprocess +import sys +import textwrap + + +def test_tools_import_without_playwright(): + script = textwrap.dedent( + """ + import sys + + # Simulate a platform where Playwright is not installed. + sys.modules["playwright"] = None + sys.modules["playwright.async_api"] = None + + # Must not raise even though Playwright is unavailable. + import code_puppy.tools # noqa: F401 + from code_puppy.tools.browser import browser_manager # noqa: F401 + + # The stub keeps the module importable but inert. + from playwright.async_api import async_playwright + + try: + async_playwright() + except RuntimeError: + print("OK") + else: + raise AssertionError("expected RuntimeError from stubbed async_playwright") + """ + ) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip().splitlines()[-1] == "OK" diff --git a/uv.lock b/uv.lock index 4776a4596..37f441922 100644 --- a/uv.lock +++ b/uv.lock @@ -310,17 +310,14 @@ dependencies = [ { name = "json-repair" }, { name = "mcp" }, { name = "openai" }, - { name = "pillow" }, - { name = "playwright" }, { name = "prompt-toolkit" }, { name = "pydantic" }, - { name = "pydantic-ai-slim", extra = ["anthropic", "mcp", "openai"] }, + { name = "pydantic-ai-slim", extra = ["anthropic", "mcp"] }, { name = "pyfiglet" }, { name = "python-dotenv" }, { name = "rapidfuzz" }, { name = "requests" }, { name = "rich" }, - { name = "ripgrep" }, { name = "termflow-md" }, { name = "typer" }, ] @@ -329,9 +326,18 @@ dependencies = [ bedrock = [ { name = "boto3" }, ] +browser = [ + { name = "playwright" }, +] durable = [ { name = "dbos" }, ] +images = [ + { name = "pillow" }, +] +search = [ + { name = "ripgrep" }, +] [package.dev-dependencies] dev = [ @@ -353,21 +359,21 @@ requires-dist = [ { name = "json-repair", specifier = ">=0.46.2" }, { name = "mcp", specifier = ">=1.9.4" }, { name = "openai", specifier = ">=1.99.1" }, - { name = "pillow", specifier = ">=10.0.0" }, - { name = "playwright", specifier = ">=1.40.0" }, + { name = "pillow", marker = "extra == 'images'", specifier = ">=10.0.0" }, + { name = "playwright", marker = "extra == 'browser'", specifier = ">=1.40.0" }, { name = "prompt-toolkit", specifier = ">=3.0.52" }, { name = "pydantic", specifier = ">=2.4.0" }, - { name = "pydantic-ai-slim", extras = ["openai", "anthropic", "mcp"], specifier = "==1.56.0" }, + { name = "pydantic-ai-slim", extras = ["anthropic", "mcp"], specifier = "==1.56.0" }, { name = "pyfiglet", specifier = ">=0.8.post1" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "rapidfuzz", specifier = ">=3.13.0" }, { name = "requests", specifier = ">=2.28.0" }, { name = "rich", specifier = ">=13.4.2" }, - { name = "ripgrep", specifier = "==14.1.0" }, + { name = "ripgrep", marker = "extra == 'search'", specifier = "==14.1.0" }, { name = "termflow-md", specifier = ">=0.1.11" }, { name = "typer", specifier = ">=0.12.0" }, ] -provides-extras = ["bedrock", "durable"] +provides-extras = ["bedrock", "browser", "durable", "images", "search"] [package.metadata.requires-dev] dev = [ @@ -1325,10 +1331,6 @@ anthropic = [ mcp = [ { name = "mcp" }, ] -openai = [ - { name = "openai" }, - { name = "tiktoken" }, -] [[package]] name = "pydantic-core" @@ -1745,110 +1747,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] -[[package]] -name = "regex" -version = "2026.5.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, - { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, - { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, - { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, - { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, - { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, - { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, - { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, - { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, - { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, - { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, - { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, - { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, - { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, - { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, - { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, - { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, - { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, - { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, - { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, - { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, - { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, - { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, - { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, - { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, - { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, - { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, - { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, - { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, - { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, -] - [[package]] name = "requests" version = "2.34.2" @@ -2168,60 +2066,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/49/0fa8324879351c59ceb169f443c578fd6f8272ee3e71268f0b9140589614/termflow_md-0.1.11-py3-none-any.whl", hash = "sha256:4fd6f375b4a06aa06987dfc80d2910fed2b27e9576c50d49c7f3903c67195b2a", size = 58710, upload-time = "2026-04-18T17:05:23.738Z" }, ] -[[package]] -name = "tiktoken" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" }, - { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, - { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, - { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, - { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, - { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, - { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, - { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, - { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, - { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, - { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, - { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, - { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, - { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, - { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, - { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, - { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, - { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, - { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, - { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, - { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, - { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, - { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, - { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, - { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, - { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, - { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, -] - [[package]] name = "tomli" version = "2.4.1"