diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6f5e373f7..94bcde0c32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,71 @@ jobs: uv run --frozen graphify --help uv run --frozen graphify install + omp-bridge: + # tests/omp.test.ts drives the real OMP extension: bun resolves the + # path-utils helpers it imports from the pinned @oh-my-pi/pi-coding-agent + # devDependency in graphify/omp/ (the only package the host provides; + # graphify/omp/index.ts vendors isReadableUrlPath locally instead of + # importing it from @oh-my-pi/pi-tui, which the host never resolves), and + # the policy assertions run against the real installed Python CLI, so + # this job needs both toolchains. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + with: + python-version: "3.12" + + - name: Install bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: uv sync --frozen + + - name: Install OMP extension dependencies + run: bun install --frozen-lockfile + working-directory: graphify/omp + + - name: Run OMP bridge tests + run: GRAPHIFY_TEST_CLI=$PWD/.venv/bin/graphify bun test tests/omp.test.ts + + - name: Check for upstream symbol moves (advisory, no execution) + # The frozen run above only proves the bridge works against the + # exact pinned version. Upstream OMP has moved exported symbols + # between minor releases before (isReadableUrlPath moved from + # pi-coding-agent to pi-tui) without the frozen lockfile ever seeing + # it; index.ts now vendors that predicate locally instead of + # tracking whichever package currently exports it. This step used + # to install @latest AND execute the whole suite against it -- + # unpinned, mutable third-party code running on every CI invocation. + # It now only downloads the published package (--ignore-scripts: + # no install hooks run) and statically compares index.ts's imported + # path-utils symbols against that file's declared exports as plain + # text -- the downloaded code is never imported or executed, but a + # future symbol move still surfaces here. + # continue-on-error: an unrelated upstream release must not block a + # merge; the frozen run above stays the blocking gate. + continue-on-error: true + run: | + bun add --cwd graphify/omp -d @oh-my-pi/pi-coding-agent@latest --ignore-scripts + bun -e ' + const fs = require("node:fs"); + const index = fs.readFileSync("graphify/omp/index.ts", "utf8"); + const block = index.match(/import\s*{([^}]+)}\s*from\s*"@oh-my-pi\/pi-coding-agent\/tools\/path-utils"/); + if (!block) throw new Error("no path-utils import block found in index.ts"); + const imported = block[1].split(",").map((s) => s.trim()).filter(Boolean); + const source = fs.readFileSync("graphify/omp/node_modules/@oh-my-pi/pi-coding-agent/src/tools/path-utils.ts", "utf8"); + const exported = new Set([...source.matchAll(/^export\s+(?:async\s+function|function)\s+(\w+)/gm)].map((m) => m[1])); + const missing = imported.filter((name) => !exported.has(name)); + if (missing.length) { + console.error(`@oh-my-pi/pi-coding-agent@latest no longer exports: ${missing.join(", ")}`); + process.exit(1); + } + console.log(`ok: every imported symbol is exported by @latest (${imported.join(", ")})`); + ' + security-scan: # The dev deps include bandit and pip-audit. Run them in CI so a new # HIGH-severity finding or vulnerable dependency is caught on the PR that diff --git a/.gitignore b/.gitignore index 0a6775b2a8..b5fec288b9 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,9 @@ paper/ # macOS Finder metadata .DS_Store + +# OMP extension test dependency tree (graphify/omp) +graphify/omp/node_modules/ + +# Local symlink for the root-level bun tests (temporary, per AGENTS.md) +/node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index f4feab51f9..54433e86c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.58 (2026-09-10) +- Feature: ship a native Oh My Pi guard package with `graphify omp install`; reuse the installed hook-guard policy for bounded tool-call denials and per-call guidance delivered with each tool result as a leading `` block, matching how Claude Code labels PreToolUse context, without building indexes. - Fix: a call to a Python function defined nested inside another function now resolves to that inner definition per lexical scope, instead of leaking to a same-named function elsewhere; direct recursion is preserved as a self-loop (#3410, thanks @hopstreax). - Fix: submodule imports inside a PEP 420 namespace package (a directory with no `__init__.py`) now resolve to the target module instead of being dropped (#3429, thanks @flaukowski). - Fix: a bare-name import of a module sitting next to the importing file (a flat script dir with no package) now resolves to that sibling — matching CPython's `sys.path[0]` behavior — without over-resolving a genuine third-party name (#3430, thanks @hopstreax). diff --git a/README.md b/README.md index cada9528b4..6b9c7d2033 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,7 @@ for example `graphify claude install --project` or `graphify codex install --pro | Agent Skills (cross-framework) | `graphify install --platform agents` (alias `--platform skills`) | | Kiro IDE/CLI | `graphify kiro install` | | Pi coding agent | `graphify install --platform pi` | +| Oh My Pi (native guard) | `graphify omp install` | | Cursor | `graphify cursor install` | | Devin CLI | `graphify devin install` | | Google Antigravity | `graphify antigravity install` | @@ -308,6 +309,7 @@ Run this once in your project after building a graph: | Agent Skills (cross-framework) | `graphify agents install` (alias `graphify skills install`) | | Kiro IDE/CLI | `graphify kiro install` | | Pi coding agent | `graphify pi install` | +| Oh My Pi (native guard) | `graphify omp install` | | Devin CLI | `graphify devin install` | | Google Antigravity | `graphify antigravity install` | @@ -326,7 +328,55 @@ This writes a small config file that tells your assistant to consult the knowled **Cursor** writes `.cursor/rules/graphify.mdc` with `alwaysApply: true`, so Cursor includes it in every conversation automatically, no hook needed. -To remove graphify from all platforms at once: `graphify uninstall` (add `--purge` to also delete `graphify-out/`). Or use the per-platform command (e.g. `graphify claude uninstall`). +### Oh My Pi (OMP) native guard + +Install Graphify into a persistent environment with `uv tool install graphifyy` or +`pipx install graphifyy`, and install OMP separately. Then run: + +```bash +graphify omp install +``` + +This delegates to OMP's supported `omp plugin install ` +route. The Python wheel and source distribution contain `graphify/omp/package.json` +with explicit `omp.extensions` and its TypeScript entry point; OMP links that +directory and discovers the extension. No npm adapter package, shell hook, Pi API +shim, or hand-edited OMP settings are required. `graphify omp path` prints the same +directory for manual `omp plugin install` or one-session `omp -e` use. Restart OMP +after installing/upgrading, and rerun the installer if the Python environment +moves. Do not link from an ephemeral `uvx` environment. + +Before native `read`, `glob`, `grep`, and search-style `bash` calls, the extension +runs the installed `graphify hook-guard read|search` CLI. The existing Python +policy owns fresh/stale graph decisions and strict-mode denials: start OMP with +`GRAPHIFY_HOOK_STRICT=1` to enable its once-per-session indexed-read block. +Denials become OMP `block`/`reason`; each qualifying call carries its own +guidance, appended to that call's persisted tool result (Claude +`PreToolUse` additionalContext parity), and pending deliveries are cleared for +each new user run and session navigation. In-flight hooks +are cancelled on these boundaries. No graph is created or updated automatically. +The guard package intentionally declares no skills; the existing cross-framework +skill remains available separately through `graphify agents install`. + +Only local filesystem targets are inspected, using OMP's selector/path helpers. +URLs and internal resources are excluded. Hooks require an installed `graphify` +on an absolute PATH entry outside the project (including outside a project-local +virtualenv); there is no project Python or command fallback. Each native tool call +has a 256 KiB JSON-input cap, a 64 KiB output cap and a two-second subprocess budget +with forced termination. Missing commands, invalid output and failures fail open. + +The extension checks `ctx.isProjectTrusted()` before execution and context +injection, but **current OMP exposes this compatibility method as always true**: +it is not an enforced trust sandbox. Enable this integration only in projects you +trust. The lifecycle contract is based on OMP integration commit +`6aef0e8ad51b3bc5ea7a5f2a255c3d48e4c5af72`; the `18.1.17` version string alone +does not establish those lifecycle fixes. + +Remove this host-managed link with `omp plugin uninstall graphify-omp` **before** +uninstalling the Python package. `graphify uninstall` handles Graphify-managed +platform files, not OMP's plugin registry. + +To remove Graphify-managed platform files at once: `graphify uninstall` (add `--purge` to also delete `graphify-out/`). Or use the per-platform command (e.g. `graphify claude uninstall`). OMP links are removed separately as described above. --- @@ -773,6 +823,9 @@ graphify kiro install # .kiro/skills/ + .kiro/steering/graphify.md graphify kiro uninstall graphify pi install # skill file (Pi coding agent) graphify pi uninstall +graphify omp install # native guard package, registered by OMP +graphify omp path # shipped package directory for manual linking +omp plugin uninstall graphify-omp # remove the host-managed link graphify devin install # skill file + .windsurf/rules/graphify.md (Devin CLI) graphify devin uninstall graphify antigravity install # .agents/rules + .agents/workflows (Google Antigravity) diff --git a/graphify/__main__.py b/graphify/__main__.py index 736f9f1e92..338adc54a5 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -591,7 +591,7 @@ def _run_cli() -> None: # Skip during install/uninstall (hook writes trigger a fresh check anyway). # Skip during hook-check — it runs on every editor tool use and must be silent. # Deduplicate paths so platforms sharing the same install dir don't warn twice. - _silent_cmds = {"install", "uninstall", "hook-check", "hook-guard"} + _silent_cmds = {"install", "uninstall", "hook-check", "hook-guard", "omp"} if not any(arg in _silent_cmds for arg in sys.argv): # Resolve each platform's real user-scope destination so per-platform # overrides (gemini, opencode, devin, antigravity, amp) check the dir @@ -611,6 +611,7 @@ def _run_cli() -> None: print(" install [--platform P] copy skill to platform config dir (claude|windows|codebuddy|codex|opencode|aider|amp|agents|claw|droid|trae|trae-cn|gemini|cursor|antigravity|hermes|kiro|pi|devin)") print(" uninstall remove graphify from all detected platforms in one shot") print(" --purge also delete graphify-out/ directory") + print(" omp [install|path] install the native Oh My Pi guard, or print its package path") print(" path \"A\" \"B\" shortest path between two nodes in graph.json") print(" --graph path to graph.json (default graphify-out/graph.json)") print(" explain \"X\" plain-language explanation of a node and its neighbors") diff --git a/graphify/cli.py b/graphify/cli.py index 458280d72d..679f2488ac 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -13,6 +13,7 @@ import time from graphify.paths import GRAPHIFY_OUT as _GRAPHIFY_OUT from pathlib import Path, PurePosixPath, PureWindowsPath +from urllib.parse import unquote, urlsplit _SEARCH_NUDGE = json.dumps({ @@ -894,10 +895,26 @@ def _run_hook_guard(kind: str, strict: bool = False) -> None: if explicit: in_project = False for v in explicit: + v = _normalize_hook_path(v) + # A bare www.host/... value is remote only when nothing + # identically named exists locally, mirroring OMP's own + # "an existing local path wins over URL" precedence + # (resolveToolSearchScope). Decided here, not inside + # _is_cwd_relative, which has no cwd/root of its own. + if _is_external_www_target(v, root): + continue p = Path(v) if _is_cwd_relative(v): in_project = True # relative -> anchored at cwd == in project break + # _is_cwd_relative already rejects a *whole-value* + # scheme://... prefix above (a rootless URL never reaches + # here). This catches the same URL after some upstream + # host has already glued it onto an absolute prefix + # (`/local:/x`), which looks exactly like a real + # file to the containment check below. + if _has_embedded_url_scheme_segment(v): + continue try: p.resolve().relative_to(root) in_project = True @@ -942,6 +959,73 @@ def _run_hook_guard(kind: str, strict: bool = False) -> None: pass +_URL_SCHEME_PREFIX_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*://") +_URL_SCHEME_SEGMENT_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*:$") +_FILE_URL_RE = re.compile(r"^file://", re.IGNORECASE) +_WWW_HOST_RE = re.compile(r"^www\.", re.IGNORECASE) + + +def _normalize_hook_path(value: str) -> str: + r"""Trim padding whitespace and a matching pair of outer double quotes, + then strip a *local* leading ``file://`` scheme down to the local path + it names. + + Mirrors what OMP's own path pipeline already does to a raw tool argument + before a hook would ever see it: ``normalizePathLikeInput`` (trim + + de-quote) and ``file://`` -> ``url.fileURLToPath`` (its own + ``strictExternalUrlRe`` deliberately omits ``file``, routing it through + the ordinary local-file path instead of the external-URL one). The guard + must classify identical text identically whether or not a given host + bothered to normalize it first -- a classifier that only agrees with its + own host after trimming/de-quoting is exactly the kind of gap a prior + review flagged. + + Per RFC 8089, a ``file://`` URL is local only when its authority is + empty (``file:///path``) or ``localhost``; Node's own + ``url.fileURLToPath`` enforces exactly this, throwing + ``ERR_INVALID_FILE_URL_HOST`` for any other host. Any other authority + names a *remote* host, not a local path -- reducing it here would + discard the host and let ``file://evil.com/`` alias a + real local file, so it is left untouched for ``_is_foreign_url_scheme`` + to classify instead. + """ + value = value.strip() + if len(value) > 1 and value[0] == value[-1] == '"': + value = value[1:-1] + if _FILE_URL_RE.match(value): + split = urlsplit(value) + if split.hostname and split.hostname.lower() != "localhost": + return value + path = unquote(split.path) or "/" + # file:///C:/proj/a.py -> C:/proj/a.py: drop the URL's extra root + # slash in front of a Windows drive letter. + if os.name == "nt" and re.match(r"^/[A-Za-z]:", path): + path = path[1:] + return path + return value + + +def _is_foreign_url_scheme(value: str) -> bool: + """Whether *value* is itself a ``scheme://...`` value -- everything + except a *local* ``file://`` (empty or ``localhost`` authority, RFC + 8089), which ``_normalize_hook_path`` has already reduced to a plain + local path by the time this runs, so it never matches here. A + ``file://`` with any other authority is NOT reduced by + ``_normalize_hook_path`` and so matches here like any other foreign + scheme, naming a remote host rather than a local file. + + Deliberately does not enumerate OMP's (or any other harness's) internal + scheme allow-list (``local://``, ``artifact://``, ...): this guard is + embedded by multiple hosts (#522), and hard-coding one of them would + silently stop matching the day that host adds a scheme. Any scheme this + function doesn't specifically know to be local -- only a local + ``file://`` is -- is treated as not-a-local-source-file: a missed nudge + on a remote URL is a far safer wrong answer than a wrong nudge on an + unrelated file. + """ + return bool(_URL_SCHEME_PREFIX_RE.match(value)) + + def _is_cwd_relative(value: str) -> bool: r"""Whether *value* is anchored at the current working directory. @@ -964,11 +1048,63 @@ def _is_cwd_relative(value: str) -> bool: so ``paths.is_absolute_any_platform`` (for stored, portable paths) is deliberately not used. On POSIX ``root`` is set exactly when the path is absolute and ``drive`` is always empty, so this is unchanged there. + + A URL is rootless and driveless by that exact same test -- ``https://x`` + and ``myscheme://x`` have no ``root`` and no ``drive`` in either flavour + -- so without checking the scheme first, this rule alone short-circuits + the caller straight to "in project" for a value that names no filesystem + path at all (the bug this guards against, #522 follow-up). + ``_normalize_hook_path`` and ``_is_foreign_url_scheme`` reject that + shape, and strip a ``file://`` wrapper down to its local path, before + the root/drive test below ever runs. """ + value = _normalize_hook_path(value) + if _is_foreign_url_scheme(value): + return False pure = PureWindowsPath(value) if os.name == "nt" else PurePosixPath(value) return not pure.root and not pure.drive +def _has_embedded_url_scheme_segment(value: str) -> bool: + """Whether an already-absolute *value* still carries a collapsed + ``scheme://`` marker as one of its OWN path segments, e.g. + ``/local:/x`` -- what a naive host-side path-join leaves behind + when it glues a rootless ``scheme://...`` value onto a root/cwd prefix + instead of routing it through a URL handler, defeating the whole-value + check in ``_is_cwd_relative`` above. + + Only meaningful once a value is already known to have a root or drive + (``_is_cwd_relative`` returned ``False``): the leading component IS that + root/drive marker, so it is skipped here on purpose -- a genuinely + relative, colon-bearing POSIX filename like ``C:/proj/a.py`` never + reaches this function at all (it takes the cwd-relative shortcut above + and stays in-project, matching POSIX semantics where a colon has no + special meaning). + """ + pure = PureWindowsPath(value) if os.name == "nt" else PurePosixPath(value) + parts = pure.parts[1:] if (pure.root or pure.drive) else pure.parts + return any(_URL_SCHEME_SEGMENT_RE.match(part) for part in parts) + + +def _is_external_www_target(value: str, root: "Path") -> bool: + """Whether *value* is a bare ``www.host/...`` external-read shape with no + identically named local file to override it. + + Mirrors OMP's own ``isReadableUrlPath``/``resolveToolSearchScope`` + precedence: a ``www.`` value carries no ``://`` at all, so no scheme rule + catches it, and it is remote only when nothing local shares its exact + name ("an existing local path wins over URL"). A project that genuinely + has a top-level path named e.g. ``www.example.com`` stays classified as + local; only the common case -- no such path -- is treated as external. + """ + if not _WWW_HOST_RE.match(value): + return False + try: + return not (root / value).exists() + except OSError: + return False + + def _target_is_indexed(file_path: str, root: "Path") -> bool: """Guard the strict deny: only block a read of a file the graph actually indexes. Reads manifest.json (cheap, capped); on any doubt (missing/corrupt/oversized diff --git a/graphify/install.py b/graphify/install.py index 47def84540..6e8f0e98e5 100644 --- a/graphify/install.py +++ b/graphify/install.py @@ -497,6 +497,14 @@ def _register_always_on_block(target: Path, prefix: str, registration: str) -> N "claude_md": False, "skill_refs": "pi", }, + "omp": { + # OMP (Oh My Pi) mirrors pi's agent layout under ~/.omp, which the + # legacy ~/.pi path is no longer read from. + "skill_file": "skill-pi.md", + "skill_dst": Path(".omp") / "agent" / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + "skill_refs": "pi", + }, "codebuddy": { # Reuses claude's split bundle (shares skill.md). "skill_file": "skill.md", @@ -1705,7 +1713,7 @@ def _project_install(platform_name: str, project_dir: Path | None = None, strict skill_dst = _copy_skill_file("antigravity", project=True, project_dir=project_dir) _antigravity_finalize(skill_dst, project_dir) _print_project_git_add_hint([_project_scope_root(skill_dst, project_dir), project_dir / ".agents"]) - elif platform_name in ("copilot", "pi", "kimi", "agents"): + elif platform_name in ("copilot", "pi", "kimi", "agents", "omp"): # Skill-only project install: drop SKILL.md (+ references) at the scope # root. `agents` -> ./.agents/skills/graphify/SKILL.md. skill_dst = _copy_skill_file(platform_name, project=True, project_dir=project_dir) @@ -1738,7 +1746,7 @@ def _project_uninstall(platform_name: str, project_dir: Path | None = None) -> N _devin_rules_uninstall(project_dir) if not removed: print("nothing to remove") - elif platform_name in ("copilot", "pi", "kimi", "agents"): + elif platform_name in ("copilot", "pi", "kimi", "agents", "omp"): removed = _remove_skill_file(platform_name, project=True, project_dir=project_dir) if not removed: print("nothing to remove") @@ -1927,6 +1935,10 @@ def uninstall_all(project_dir: Path | None = None, purge: bool = False) -> None: # The generic agents platform's user-scope skill lives at ~/.agents/skills, # which neither the AGENTS.md cleanup nor amp's removal reaches. _remove_skill_file("agents") + # OMP's user-scope skill lives at ~/.omp/agent/skills. The `graphify omp` + # subcommand is the plugin installer, so the skill's removal rides + # uninstall_all, like amp and agents. + _remove_skill_file("omp") _uninstall_opencode_plugin(pd) _uninstall_codex_hook(pd) @@ -2133,6 +2145,7 @@ def codebuddy_uninstall(project_dir: Path | None = None, *, project: bool = Fals "kilo", "kiro", "opencode", + "omp", "pi", "skills", "trae", @@ -2150,6 +2163,25 @@ def dispatch_install_cli(cmd: str) -> bool: """ if cmd not in _CLI_INSTALL_COMMANDS: return False + if cmd == "omp": + # OMP owns package registration; do not duplicate its config/paths here. + args = sys.argv[2:] + if args not in (["path"], ["install"]): + print("Usage: graphify omp [path|install]", file=sys.stderr) + sys.exit(1) + package_path = Path(__file__).resolve().parent / "omp" + if args == ["path"]: + print(package_path) + return True + omp = shutil.which("omp") + if not omp: + print("error: install Oh My Pi (omp) and add it to PATH first", file=sys.stderr) + sys.exit(1) + import subprocess + result = subprocess.run([omp, "plugin", "install", str(package_path)], check=False) + if result.returncode: + sys.exit(result.returncode) + return True if cmd == "install": # Default to windows platform on Windows, claude elsewhere default_platform = "windows" if platform.system() == "Windows" else "claude" diff --git a/graphify/omp/bun.lock b/graphify/omp/bun.lock new file mode 100644 index 0000000000..88a2bc7fc8 --- /dev/null +++ b/graphify/omp/bun.lock @@ -0,0 +1,385 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "graphify-omp", + "devDependencies": { + "@oh-my-pi/pi-coding-agent": "^18.2.6", + }, + }, + }, + "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@huggingface/jinja": ["@huggingface/jinja@0.5.10", "", {}, "sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw=="], + + "@huggingface/tokenizers": ["@huggingface/tokenizers@0.2.0", "", {}, "sha512-LidMHe1FpcSYH4vcSjXooda34pC0M8m1gmDOv7SQi6Iv+ib1zX7J3sHBCC9/3FPCkn2k1b55FneVqMRxrr99pg=="], + + "@huggingface/transformers": ["@huggingface/transformers@4.3.0", "", { "dependencies": { "@huggingface/jinja": "^0.5.10", "@huggingface/tokenizers": "^0.2.0", "onnxruntime-node": "1.30.0", "onnxruntime-web": "1.31.0-dev.20260914-8d85527a0", "sharp": "^0.35.4" } }, "sha512-fL1A/WUZwouPrOlYxU5dzIwD2T5J781JiB2jDR8bFe5DwCj0Gfudq+NEXCMno49kQgajHA7xQkrRLJlqG1veEA=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + + "@oh-my-pi/omp-stats": ["@oh-my-pi/omp-stats@18.2.6", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.2.6", "@oh-my-pi/pi-catalog": "18.2.6", "@oh-my-pi/pi-utils": "18.2.6", "@tailwindcss/node": "^4.3.2", "chart.js": "^4.5.1", "lucide-react": "^1.24.0", "react": "19.2.7", "react-chartjs-2": "^5.3.1", "react-dom": "19.2.7", "tailwindcss": "^4.3.2" }, "bin": { "omp-stats": "src/index.ts" } }, "sha512-5jh/b4hDF2rK67gSpMKR7qBCTMQyI7FLU0jtDScBY6ILdCMQnrHIs6BsP09j984lR/HnnrLBGFOWZqMMkXZsvA=="], + + "@oh-my-pi/omptype": ["@oh-my-pi/omptype@18.2.6", "", {}, "sha512-4VUGOSr791W1lXNpU1hQXrKd4BqaEQlz2OHXYecZ6MTENb6Hx9JC00Sf7Db4dxPjkEzyibD1YjVUXqK3DQeO+g=="], + + "@oh-my-pi/pi-agent-core": ["@oh-my-pi/pi-agent-core@18.2.6", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.2.6", "@oh-my-pi/pi-catalog": "18.2.6", "@oh-my-pi/pi-natives": "18.2.6", "@oh-my-pi/pi-utils": "18.2.6", "@oh-my-pi/pi-wire": "18.2.6", "@oh-my-pi/snapcompact": "18.2.6", "@opentelemetry/api": "^1.9.1" } }, "sha512-BeakwQjJaF/w+8ISIQSbPRJJy+hSMM4XVB5kgx+qUKpykyrKXbw4zgG+tOF9+3VyCnDZZemv92FQ0Wdnmabchg=="], + + "@oh-my-pi/pi-ai": ["@oh-my-pi/pi-ai@18.2.6", "", { "dependencies": { "@oh-my-pi/omptype": "18.2.6", "@oh-my-pi/pi-catalog": "18.2.6", "@oh-my-pi/pi-natives": "18.2.6", "@oh-my-pi/pi-utils": "18.2.6", "@oh-my-pi/pi-wire": "18.2.6" } }, "sha512-SkQ5Uc5ABS5WiKPVdPAmxawhZf1kDSH53CrXrjlgshpCT6r6jDs/CI+VvwdhQHoEQcokM8pmWDGPzGfKTa7K4w=="], + + "@oh-my-pi/pi-catalog": ["@oh-my-pi/pi-catalog@18.2.6", "", { "dependencies": { "@oh-my-pi/omptype": "18.2.6", "@oh-my-pi/pi-utils": "18.2.6" } }, "sha512-jYg/cqztVXPWa3soLvjGxKs/4oIm8vDwuvue0tfPv/qiEWzxPy+PsI+9zbKZZEipfVlQUAyD+8LnkQdGlbqIDg=="], + + "@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@18.2.6", "", { "dependencies": { "@babel/parser": "^7.29.7", "@oh-my-pi/omp-stats": "18.2.6", "@oh-my-pi/omptype": "18.2.6", "@oh-my-pi/pi-agent-core": "18.2.6", "@oh-my-pi/pi-ai": "18.2.6", "@oh-my-pi/pi-catalog": "18.2.6", "@oh-my-pi/pi-mnemopi": "18.2.6", "@oh-my-pi/pi-natives": "18.2.6", "@oh-my-pi/pi-tui": "18.2.6", "@oh-my-pi/pi-utils": "18.2.6", "@oh-my-pi/pi-wire": "18.2.6", "@oh-my-pi/snapcompact": "18.2.6", "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.220.0", "@opentelemetry/context-async-hooks": "^2.9.0", "@opentelemetry/exporter-logs-otlp-proto": "^0.220.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.220.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-logs": "^0.220.0", "@opentelemetry/sdk-metrics": "^2.9.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@opentelemetry/sdk-trace-node": "^2.9.0", "puppeteer-core": "25.3.0" }, "optionalDependencies": { "@huggingface/transformers": "^4.2.0", "sherpa-onnx-node": "1.13.2" }, "bin": { "omp": "dist/cli.js" } }, "sha512-dk298PZNnD1QigR4lKvQpWVAAbIo2bCfdNOlZwGFZsxHbP+XYrUS+AxYxfWkfqSnFRliP9Ho+h94DHV3o6lxRg=="], + + "@oh-my-pi/pi-mnemopi": ["@oh-my-pi/pi-mnemopi@18.2.6", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.2.6", "@oh-my-pi/pi-catalog": "18.2.6", "@oh-my-pi/pi-natives": "18.2.6", "@oh-my-pi/pi-utils": "18.2.6" }, "peerDependencies": { "fastembed": "2.1.0", "onnxruntime-node": "1.21.0" }, "optionalPeers": ["fastembed", "onnxruntime-node"], "bin": { "mnemopi": "src/cli.ts" } }, "sha512-JD4iVlqUVnoSsMJn7Fy+Hn3zUMKH8i6qpWImoTTnyuEJ0iqL1n6H1W96GqVnhfEDoBf98/mPwoZM2mDHNKEfYg=="], + + "@oh-my-pi/pi-natives": ["@oh-my-pi/pi-natives@18.2.6", "", { "optionalDependencies": { "@oh-my-pi/pi-natives-darwin-arm64": "18.2.6", "@oh-my-pi/pi-natives-darwin-x64": "18.2.6", "@oh-my-pi/pi-natives-linux-arm64": "18.2.6", "@oh-my-pi/pi-natives-linux-x64": "18.2.6", "@oh-my-pi/pi-natives-win32-arm64": "18.2.6", "@oh-my-pi/pi-natives-win32-x64": "18.2.6" } }, "sha512-oZtxz7cU6CJvwdGJgpeHDIvhKOBoGVwRzmb7RQt+NdFD7tVe7PrObgbQdAOpmo3LwbJ3Md/jiicWmaxboFSRTQ=="], + + "@oh-my-pi/pi-natives-darwin-arm64": ["@oh-my-pi/pi-natives-darwin-arm64@18.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PVrTHpeVc8n3kOCNUd5Qq5bUyo1y8BtFRcm1UKL8LGF8OzHOrob280iz6K0LIiv8ovbfSjZSvCjYI/KSaM3YMQ=="], + + "@oh-my-pi/pi-natives-darwin-x64": ["@oh-my-pi/pi-natives-darwin-x64@18.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-qfZeZ0Xz9aONtp6x7eiWUQMlm2blEqauth2SntLplV9cdYTKailWHjpGBoyvfs/yptddA9Pbj6iN9GlzmpbgOQ=="], + + "@oh-my-pi/pi-natives-linux-arm64": ["@oh-my-pi/pi-natives-linux-arm64@18.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-hHdJnF0pw3GlgTpscPCeRT9fcf/yXbrwcvdFgVg482y6xDq8KBT5rzG+V/ntUbcjj/qfBQQiH1do2XlrbL6nFw=="], + + "@oh-my-pi/pi-natives-linux-x64": ["@oh-my-pi/pi-natives-linux-x64@18.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-mV2DTOE1Hn96iXNIbrzYHzLy9CbeAbjLCGPtc5tzFzzuD7UJ3TMfP4pieSo0WtgZnPNxisT6ncR8c/BqNbkXdg=="], + + "@oh-my-pi/pi-natives-win32-arm64": ["@oh-my-pi/pi-natives-win32-arm64@18.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-nk1ZUvF27HZJNwL8TwUMLUdmFuf8vsv1StAMsHQ9zdgLuRJ1NwWESol6wXzRntvDzsmpDQbnf3R8HcY5CzUf9w=="], + + "@oh-my-pi/pi-natives-win32-x64": ["@oh-my-pi/pi-natives-win32-x64@18.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-syDZJnTiTxFIkTteT4U5edq/hdPrPjOYmyG8Kq4a/LwHoD59eDpkE3+kqVy33GRfOwzJb0b6d6Hxrp8we75b9g=="], + + "@oh-my-pi/pi-tui": ["@oh-my-pi/pi-tui@18.2.6", "", { "dependencies": { "@oh-my-pi/omptype": "18.2.6", "@oh-my-pi/pi-agent-core": "18.2.6", "@oh-my-pi/pi-ai": "18.2.6", "@oh-my-pi/pi-catalog": "18.2.6", "@oh-my-pi/pi-natives": "18.2.6", "@oh-my-pi/pi-utils": "18.2.6", "@oh-my-pi/pi-wire": "18.2.6", "@oh-my-pi/snapcompact": "18.2.6" } }, "sha512-CsmP4SM1Qt19s2aIJ0gBTnSFLm2nXNHkaLFqL5m+3+bTcF3++G+CAEImotsjSnmajeH/Q2pq2w+FlNUD2ZjByA=="], + + "@oh-my-pi/pi-utils": ["@oh-my-pi/pi-utils@18.2.6", "", { "dependencies": { "@oh-my-pi/pi-natives": "18.2.6" } }, "sha512-jvj8w/Q9aECXpHVjUc4rWaQlFPV+uWCgII6QZjDfroo5xvt0d8ks64cH8gINmMwRieCLz7KU6JKGReuFT54aTw=="], + + "@oh-my-pi/pi-wire": ["@oh-my-pi/pi-wire@18.2.6", "", {}, "sha512-CfKosiyS2L3EfqVAidDYjfpBEVRuHAueYIlydEySARSLj61euYSqCZFMNhCPcA+QFXhfG0J0N5xGRuMk5FzRaA=="], + + "@oh-my-pi/snapcompact": ["@oh-my-pi/snapcompact@18.2.6", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.2.6", "@oh-my-pi/pi-catalog": "18.2.6", "@oh-my-pi/pi-natives": "18.2.6", "@oh-my-pi/pi-utils": "18.2.6", "@oh-my-pi/pi-wire": "18.2.6" } }, "sha512-hPL6N9YNxejTY1jOm8+FbcbuLjtgPI/v591tunKed7gIcQgOOuztNvTcM/Xbc5xc50CBcWoD6q+j4Hm0iuChlQ=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.11.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Tr79DyWI8itsBdg+jH+opjfrwLzX+erk1/ExkIwhWoAVjVrJIn2y5+cGjTC0Vy8fyNIA/y8wuJPZwr1T3xCZeQ=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.9.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw=="], + + "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.220.0", "", { "dependencies": { "@opentelemetry/otlp-exporter-base": "0.220.0", "@opentelemetry/otlp-transformer": "0.220.0", "@opentelemetry/sdk-logs": "0.220.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.220.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/otlp-exporter-base": "0.220.0", "@opentelemetry/otlp-transformer": "0.220.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-metrics": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.220.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/exporter-metrics-otlp-http": "0.220.0", "@opentelemetry/otlp-exporter-base": "0.220.0", "@opentelemetry/otlp-transformer": "0.220.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-metrics": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA=="], + + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.220.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/otlp-exporter-base": "0.220.0", "@opentelemetry/otlp-transformer": "0.220.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-trace": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.220.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/otlp-transformer": "0.220.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/sdk-logs": "0.220.0", "@opentelemetry/sdk-metrics": "2.9.0", "@opentelemetry/sdk-trace": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/resources": "2.11.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A=="], + + "@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/resources": "2.11.0", "@opentelemetry/sdk-trace": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.11.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.11.0", "@opentelemetry/core": "2.11.0", "@opentelemetry/sdk-trace-base": "2.11.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + + "@puppeteer/browsers": ["@puppeteer/browsers@3.0.6", "", { "dependencies": { "modern-tar": "^0.7.6", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], + + "@types/node": ["@types/node@22.20.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A=="], + + "adm-zip": ["adm-zip@0.6.1", "", {}, "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ=="], + + "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + + "chromium-bidi": ["chromium-bidi@16.0.1", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devtools-protocol": ["devtools-protocol@0.0.1638949", "", {}, "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "flatbuffers": ["flatbuffers@25.9.23", "", {}, "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "global-agent": ["global-agent@4.1.3", "", { "dependencies": { "globalthis": "^1.0.2", "matcher": "^4.0.0", "semver": "^7.3.5", "serialize-error": "^8.1.0" } }, "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lucide-react": ["lucide-react@1.46.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Bv+FZXgZPrxc/NCl1e7JJVQFLdiCxYgxNVhqoV7X0p6I8ADJo8DxBnK1auH0fZz4AmqOJ3jgneL4f1i8LJQRAA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "matcher": ["matcher@4.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "modern-tar": ["modern-tar@0.7.7", "", {}, "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "onnxruntime-common": ["onnxruntime-common@1.30.0", "", {}, "sha512-7fdVWjAID1dVhH/G8qK3APARunV4VkBFoCQAP7qp4Wkab0mrorvmc+sqiT+mKXOzDqdjN5j+/Z9nb4gzNPWcyA=="], + + "onnxruntime-node": ["onnxruntime-node@1.30.0", "", { "dependencies": { "adm-zip": "^0.6.0", "global-agent": "^4.1.3", "onnxruntime-common": "1.30.0" }, "os": [ "linux", "win32", "darwin", ] }, "sha512-twhs1C2C/BFkz1yc5OY0KIU2GUq6DURO7hD4bx5Q2Qy3nAMJwRXW8xU3NVczE29VA9lolLOYepoD8fjTGOfIqw=="], + + "onnxruntime-web": ["onnxruntime-web@1.31.0-dev.20260914-8d85527a0", "", { "dependencies": { "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", "onnxruntime-common": "1.31.0-dev.20260911-2a43ec07e", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "sha512-Iy7rtadoBgxS/LLvDr3QW38DB1PNXRnr0GJMcL0TAt7c9qjgVQl83UlGCVyeAnK2InpmW8Uc3PL8XIuqtDeF6g=="], + + "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], + + "protobufjs": ["protobufjs@7.6.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg=="], + + "puppeteer-core": ["puppeteer-core@25.3.0", "", { "dependencies": { "@puppeteer/browsers": "3.0.6", "chromium-bidi": "16.0.1", "devtools-protocol": "0.0.1638949", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.0" } }, "sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA=="], + + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "serialize-error": ["serialize-error@8.1.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ=="], + + "sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linux-riscv64": "1.3.3", "@img/sharp-libvips-linux-s390x": "1.3.3", "@img/sharp-libvips-linux-x64": "1.3.3", "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", "@img/sharp-libvips-linuxmusl-x64": "1.3.3", "@img/sharp-linux-arm": "0.35.4", "@img/sharp-linux-arm64": "0.35.4", "@img/sharp-linux-ppc64": "0.35.4", "@img/sharp-linux-riscv64": "0.35.4", "@img/sharp-linux-s390x": "0.35.4", "@img/sharp-linux-x64": "0.35.4", "@img/sharp-linuxmusl-arm64": "0.35.4", "@img/sharp-linuxmusl-x64": "0.35.4", "@img/sharp-webcontainers-wasm32": "0.35.4", "@img/sharp-win32-arm64": "0.35.4", "@img/sharp-win32-ia32": "0.35.4", "@img/sharp-win32-x64": "0.35.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA=="], + + "sherpa-onnx-darwin-arm64": ["sherpa-onnx-darwin-arm64@1.13.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FPNgJMgnWVl/KhRTIhG3KL3A4Om63Rn4YKXc9/uHY7SzLcvqLJLc/h7UBWJwduXvv7K18t5NpxHR6XgXn4sjWw=="], + + "sherpa-onnx-darwin-x64": ["sherpa-onnx-darwin-x64@1.13.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-7BLRpjM6w4f9W46/nmkmq8lEKUayhebvcpslCVQ+6QN2uReYlZEMDZlSpXMjme+hUFrPfRz8P3UNq8ep/4d19g=="], + + "sherpa-onnx-linux-arm64": ["sherpa-onnx-linux-arm64@1.13.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Tlg7a70b/Wge3OF8IgTHF9jhSVCsLyKQKhwc4BsJ5A+dL/SrFtGBjzuHp4XeLhiiOT7afCxX5PdSn/D4c8Lnuw=="], + + "sherpa-onnx-linux-x64": ["sherpa-onnx-linux-x64@1.13.8", "", { "os": "linux", "cpu": "x64" }, "sha512-6plnhjagsSeTntCgnlag86hWbs/uZE9Crms1LgOb68/1nKsIQjMd+WG519m+aPwT6TrsBOiEMzrx41t8sL5L5g=="], + + "sherpa-onnx-node": ["sherpa-onnx-node@1.13.2", "", { "optionalDependencies": { "sherpa-onnx-darwin-arm64": "^1.13.2", "sherpa-onnx-darwin-x64": "^1.13.2", "sherpa-onnx-linux-arm64": "^1.13.2", "sherpa-onnx-linux-x64": "^1.13.2", "sherpa-onnx-win-ia32": "^1.13.2", "sherpa-onnx-win-x64": "^1.13.2" } }, "sha512-uIH6SA5Or4pb8HlCYWB3K54XkMtzdef4/tkw1amtIf8GB1tt6hQLpur9p2jSFNfTYRyzZ8XrXofxefXQ0A7EUA=="], + + "sherpa-onnx-win-ia32": ["sherpa-onnx-win-ia32@1.13.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-H0Ojln9hfvM+pxWqW0cnjB/XK8/JPq4/k0IWu2JpY5Z458M3zv+XC3Hu+wmot3AoH/K1Bgt23n/tTKtPV9x8qg=="], + + "sherpa-onnx-win-x64": ["sherpa-onnx-win-x64@1.13.8", "", { "os": "win32", "cpu": "x64" }, "sha512-oZF1c9VPOKtMwn83Bboc5XSWL+76BRoyB3eUuVnCknBKxwSULZU2Foia9VHWzU+n4I12rPsP6z6H9Rp1hD9o8g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q=="], + + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/resources": "2.9.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q=="], + + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + + "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "@opentelemetry/sdk-trace/@opentelemetry/resources": ["@opentelemetry/resources@2.9.0", "", { "dependencies": { "@opentelemetry/core": "2.9.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/resources": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA=="], + + "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "onnxruntime-web/onnxruntime-common": ["onnxruntime-common@1.31.0-dev.20260911-2a43ec07e", "", {}, "sha512-gBuF6U32YErKIAt+yD7DeGBpjRxhJ1uJwako6ygjokSZULDU6Pd+KWComX8Tumk1hYVHRxjPj7mdMnD0ryAPOw=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + } +} diff --git a/graphify/omp/index.ts b/graphify/omp/index.ts new file mode 100644 index 0000000000..60b2657ce7 --- /dev/null +++ b/graphify/omp/index.ts @@ -0,0 +1,160 @@ +import { execFile } from "node:child_process"; +import { realpathSync } from "node:fs"; +import { delimiter, isAbsolute, relative, sep } from "node:path"; +import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; +import { InternalUrlRouter } from "@oh-my-pi/pi-coding-agent/internal-urls"; +// resolveReadPath, not the async sibling: the async variant is missing from +// older host-provided pi-coding-agent copies -- a blocked statSync beats an unloadable extension. +import { + expandDelimitedPathEntries, + normalizePathLikeInput, + resolveReadPath, + splitPathAndSelPreferringLiteral, +} from "@oh-my-pi/pi-coding-agent/tools/path-utils"; + +// Mirrors the upstream isReadableUrlPath predicate, kept as a same-named +// local copy (not imported): the wheel ships no node_modules, so this +// extension may import only host-provided packages, and the host provides +// @oh-my-pi/pi-coding-agent alone -- a scoped subpath import of any other +// package is unresolvable at runtime. Named to match so a future reader can +// diff them. +function isReadableUrlPath(value: string): boolean { + return /^https?:\/\/?/i.test(value) || /^www\./i.test(value); +} + +const INPUT_LIMIT = 256 * 1024; +const OUTPUT_LIMIT = 64 * 1024; +const TIMEOUT_MS = 2000; +const TOOL_NAMES = { bash: "Bash", grep: "Grep", read: "Read", glob: "Glob" } as const; +const FILE_URL_RE = /^file:\/\//i; + +// InternalUrlRouter/isReadableUrlPath still earn their place even though the +// guard now defends itself against URL-shaped input: resolveReadPath +// below pre-resolves a rootless value to an absolute path +// (`path.resolve(cwd, x)`) before the guard ever sees it, which for a +// *foreign*-scheme value glues it onto cwd into something that looks +// exactly like a real in-project file (`/https:/example.com/x`, +// `/local:/x`) -- a guard fix the bridge then defeats by mangling its +// input first is not a fix. A *local* file:// is the one exception: OMP's +// own resolveReadPath already strips it down to the real local path (its +// resolveToCwd -> expandPath -> stripFileUrl, deliberately excluded from +// the external-URL fast path), so routing it through here yields the +// correct absolute path rather than a mangled one. + +// RFC 8089 / Node's url.fileURLToPath (throws ERR_INVALID_FILE_URL_HOST for +// anything else): a file:// URL is local only when its authority is empty +// or `localhost`. Any other authority names a remote host and must fall +// through to the generic `scheme://` check below like any other foreign +// scheme -- treating every file:// as local let +// `file://evil.com/` alias a real local file. +function isRemote(path: string): boolean { + if (FILE_URL_RE.test(path)) { + try { + const { hostname } = new URL(path); + if (hostname === "" || hostname.toLowerCase() === "localhost") return false; + } catch { + // Malformed file:// URL: not a trustworthy local path either, fall + // through to the generic checks below. + } + } + const router = InternalUrlRouter.instance(); + const normalized = path.replace(/^(local:)\/(?!\/)/, "$1//"); + return router.canHandle(normalized) || isReadableUrlPath(path) || path.includes("://"); +} + +function installedCommand(cwd: string): string | undefined { + // Never resolve a bare/relative PATH entry against the project or use a + // project-supplied Python module as a fallback for an absent installation. + const path = (process.env.PATH ?? "").split(delimiter).filter(isAbsolute).join(delimiter); + const found = path ? Bun.which("graphify", { PATH: path }) : null; + if (!found) return; + const command = realpathSync(found); + const within = relative(realpathSync(cwd), command); + if (!within || (!within.startsWith(`..${sep}`) && !isAbsolute(within))) return; + return command; +} + +function runGuard(command: string, kind: string, payload: string, cwd: string, signal: AbortSignal, timeout: number, maxBuffer: number): Promise { + if (Buffer.byteLength(payload) > INPUT_LIMIT || signal.aborted) return Promise.resolve(undefined); + const { promise, resolve } = Promise.withResolvers(); + const child = execFile(command, ["hook-guard", kind], { + cwd, encoding: "utf8", timeout, killSignal: "SIGKILL", maxBuffer, signal, + env: { ...process.env, CLAUDE_PROJECT_DIR: cwd }, + }, (error, stdout) => resolve(error ? undefined : stdout.trim() || undefined)); + child.stdin?.on("error", () => {}); + child.stdin?.end(payload); + return promise; +} + +export default function graphify(api: ExtensionAPI): void { + // OMP's passive tool-call context channel emits trusted extension guidance as + // a separate developer message after the batch. Raw tool output stays + // untouched, so a file or command cannot forge this instruction channel. + // Every qualifying call carries its own nudge (no dedup). + let generation = 0; + let controller = new AbortController(); + const reset = () => { + generation++; + controller.abort(); + controller = new AbortController(); + }; + api.on("session_start", reset); + api.on("session_switch", reset); + api.on("session_branch", reset); + api.on("session_tree", reset); + api.on("session_shutdown", reset); + api.on("before_agent_start", reset); + + api.on("tool_call", async (event, ctx) => { + if (!ctx.isProjectTrusted()) { reset(); return; } + if (!Object.hasOwn(TOOL_NAMES, event.toolName)) return; + const current = generation; + const signal = controller.signal; + const deadline = performance.now() + TIMEOUT_MS; + const contexts: string[] = []; + try { + if (Buffer.byteLength(JSON.stringify(event.input)) > INPUT_LIMIT) return; + const command = installedCommand(ctx.cwd); + if (!command) return; + const toolName = TOOL_NAMES[event.toolName as keyof typeof TOOL_NAMES]; + const rawPath = typeof event.input.path === "string" ? normalizePathLikeInput(event.input.path) : "."; + if (isRemote(rawPath)) return; + const paths = toolName === "Read" || toolName === "Bash" ? [rawPath] : + await expandDelimitedPathEntries([rawPath], ctx.cwd); + for (const path of paths) { + if (isRemote(path)) continue; + const input: Record = { ...event.input }; + if (toolName !== "Bash") { + const target = toolName === "Glob" ? path : (await splitPathAndSelPreferringLiteral(path, ctx.cwd)).path; + const resolved = resolveReadPath(target, ctx.cwd); + delete input.path; + if (toolName === "Read") input.file_path = resolved; + else if (toolName === "Glob") input.pattern = resolved; + else input.path = resolved; + } + const timeout = Math.floor(deadline - performance.now()); + if (current !== generation || !ctx.isProjectTrusted() || timeout <= 0) return; + const output = await runGuard(command, toolName === "Bash" || toolName === "Grep" ? "search" : "read", JSON.stringify({ + session_id: ctx.sessionManager.getSessionId(), cwd: ctx.cwd, tool_name: toolName, tool_input: input, + }), ctx.cwd, signal, timeout, OUTPUT_LIMIT); + if (current !== generation || !ctx.isProjectTrusted()) return; + if (!output) continue; + const hook = JSON.parse(output)?.hookSpecificOutput; + if (hook?.hookEventName !== "PreToolUse") continue; + if (hook.permissionDecision === "deny" && typeof hook.permissionDecisionReason === "string" && hook.permissionDecisionReason.trim()) { + return { block: true, reason: hook.permissionDecisionReason }; + } + if (typeof hook.additionalContext === "string" && hook.additionalContext.trim()) { + contexts.push(hook.additionalContext); + } + // The search CLI does not inspect individual targets; one call suffices. + if (toolName === "Grep") break; + } + } catch { + // Optional guidance must not break native tools on missing executables, + // invalid paths, malformed hook output, timeout, or cancellation. + } + if (contexts.length > 0) return { additionalContext: contexts.join("\n\n") }; + }); + +} diff --git a/graphify/omp/package.json b/graphify/omp/package.json new file mode 100644 index 0000000000..2c4dd6cca8 --- /dev/null +++ b/graphify/omp/package.json @@ -0,0 +1,16 @@ +{ + "name": "graphify-omp", + "version": "0.9.72", + "private": true, + "description": "Native Oh My Pi bridge to Graphify's installed hook-guard CLI", + "license": "Apache-2.0", + "type": "module", + "files": ["index.ts"], + "devDependencies": { + "@oh-my-pi/pi-coding-agent": "^18.2.6" + }, + "omp": { + "extensions": ["./index.ts"], + "skills": [] + } +} diff --git a/pyproject.toml b/pyproject.toml index 2d8ac83d1d..831a13aae9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,8 @@ include-package-data = false # under graphify/skills//references/, and the always-on injection blocks # under graphify/always_on/. There is no graphify/skills//SKILL.md in the # repo, so no SKILL.md glob is needed here. -graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-kilo.md", "command-kilo.md", "skill-aider.md", "skill-amp.md", "skill-agents.md", "skill-copilot.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md", "skill-kiro.md", "skill-vscode.md", "skill-pi.md", "skill-devin.md", "skills/*/references/*.md", "always_on/*.md"] +# Native OMP package: linked by OMP itself from the Python installation. +graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-kilo.md", "command-kilo.md", "skill-aider.md", "skill-amp.md", "skill-agents.md", "skill-copilot.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md", "skill-kiro.md", "skill-vscode.md", "skill-pi.md", "skill-devin.md", "skills/*/references/*.md", "always_on/*.md", "omp/package.json", "omp/index.ts"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/tests/omp.test.ts b/tests/omp.test.ts new file mode 100644 index 0000000000..da0259449b --- /dev/null +++ b/tests/omp.test.ts @@ -0,0 +1,248 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { execFileSync } from "node:child_process"; +import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"; +import graphify from "../graphify/omp/index.ts"; + +// Use the real installed Python CLI for policy tests. No mock of OMP path helpers. +const realCLI = process.env.GRAPHIFY_TEST_CLI ?? Bun.which("graphify"); +if (!realCLI) throw new Error("Install graphifyy or set GRAPHIFY_TEST_CLI to its executable before running this test"); +const savedPath = process.env.PATH; +const savedStrict = process.env.GRAPHIFY_HOOK_STRICT; +let root: string; +let cwd: string; +let control: string; +let started: string; +let fixture: string; + +type Result = { block?: boolean; reason?: string; additionalContext?: string } | undefined; +type Handler = (event: Record, ctx: object) => Result | Promise; +function harness() { + const handlers = new Map(); + let trusted = true; + const ctx = { cwd, isProjectTrusted: () => trusted, sessionManager: { getSessionId: () => "omp-test-session" } }; + // Only the event registry is needed; every callback is the real extension's. + const api = { on: (event: string, handler: Handler) => handlers.set(event, handler) } as unknown as ExtensionAPI; + graphify(api); + return { + trust(value: boolean) { trusted = value; }, + async emit(event: string, payload: Record = {}) { return await handlers.get(event)?.({ type: event, ...payload }, ctx); }, + }; +} + + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "graphify-omp-")); + cwd = join(root, "project"); + mkdirSync(cwd); + mkdirSync(join(root, "bin")); + control = join(root, "control.json"); + started = join(root, "started"); + fixture = join(root, "bin", "graphify"); + writeFileSync(control, JSON.stringify({ mode: "real" })); + // A controlled installed executable outside the project exercises the real + // subprocess boundary (including cancellation and output limits). + // Real subprocess timeouts cannot be advanced by the parent test's fake clock. + writeFileSync(fixture, `#!${process.execPath}\nimport {execFileSync} from "node:child_process"; +import {readFileSync,writeFileSync,existsSync} from "node:fs"; +const input = await Bun.stdin.text(); +const config = JSON.parse(readFileSync(${JSON.stringify(control)}, "utf8")); +writeFileSync(${JSON.stringify(started)}, input); +if (config.mode === "real") process.stdout.write(execFileSync(${JSON.stringify(realCLI)}, process.argv.slice(2), {input})); +else if (config.mode === "invalid") process.stdout.write("not json"); +else if (config.mode === "overflow") process.stdout.write("x".repeat(65537)); +else if (config.mode === "delayed") { + while (!existsSync(${JSON.stringify(join(root, "release"))})) await Bun.sleep(5); + process.stdout.write(JSON.stringify({hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: "stale guidance"}})); +} else if (config.mode === "varying") { + const counterFile = ${JSON.stringify(join(root, "count"))}; + const n = existsSync(counterFile) ? Number(readFileSync(counterFile, "utf8")) || 0 : 0; + writeFileSync(counterFile, String(n + 1)); + process.stdout.write(JSON.stringify({hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: "guidance " + (n + 1)}})); +} else if (config.mode === "hung") { + process.on("SIGTERM", () => {}); + await Bun.sleep(60000); +} +`); + chmodSync(fixture, 0o755); + process.env.PATH = dirname(fixture); + delete process.env.GRAPHIFY_HOOK_STRICT; + writeFileSync(join(cwd, "source.py"), "def example(): pass\n"); + mkdirSync(join(cwd, "graphify-out")); + writeFileSync(join(cwd, "graphify-out", "graph.json"), JSON.stringify({ nodes: [], links: [] })); + writeFileSync(join(cwd, "graphify-out", "manifest.json"), JSON.stringify({ "source.py": {} })); + const fresh = new Date(Date.now() + 1000); + utimesSync(join(cwd, "graphify-out", "graph.json"), fresh, fresh); +}); + +afterEach(() => { + process.env.PATH = savedPath; + if (savedStrict === undefined) delete process.env.GRAPHIFY_HOOK_STRICT; + else process.env.GRAPHIFY_HOOK_STRICT = savedStrict; + rmSync(root, { recursive: true, force: true }); +}); + +test("real CLI strict denial blocks once; later calls return trusted passive context", async () => { + process.env.GRAPHIFY_HOOK_STRICT = "1"; + const api = harness(); + await api.emit("before_agent_start"); + const event = { toolName: "read", input: { path: "source.py:1-5" }, toolCallId: "call-1" }; + const result = await api.emit("tool_call", event); + expect(result?.block).toBe(true); + expect(result?.reason).toContain("graphify"); + expect(existsSync(join(cwd, "graphify-out", "cache", "hook_sessions", "omp-test-session.denied"))).toBe(true); + // After the one-time deny, every qualifying call carries its own guidance. + const second = await api.emit("tool_call", { ...event, toolCallId: "call-2" }); + expect(second?.additionalContext).toContain("graphify"); + await api.emit("before_agent_start"); + const third = await api.emit("tool_call", { ...event, toolCallId: "call-3" }); + expect(third?.additionalContext).toContain("graphify"); +}); + +test("native grep, bash search, and glob expose the installed CLI's actual guidance", async () => { + for (const [toolName, input, kind, legacyInput] of [ + ["grep", { pattern: "example", path: "source.py:1" }, "search", { pattern: "example" }], + ["bash", { command: "rg example ." }, "search", { command: "rg example ." }], + ["glob", { path: "**/*.py" }, "read", { pattern: "**/*.py" }], + ] as const) { + const api = harness(); + const expected = JSON.parse(execFileSync(realCLI!, ["hook-guard", kind], { + cwd, input: JSON.stringify({ tool_input: legacyInput }), encoding: "utf8", + })).hookSpecificOutput.additionalContext; + const result = await api.emit("tool_call", { toolName, input, toolCallId: `${toolName}-1` }); + expect(result?.additionalContext).toBe(expected); + } +}); + +test("multi-target glob calls surface every target's guidance", async () => { + // The real CLI returns identical fresh text per glob target (its stale check + // keys on file_path, which glob payloads do not carry), so a varying fixture + // mode distinguishes the per-target guard invocations the extension must join. + writeFileSync(control, JSON.stringify({ mode: "varying" })); + const api = harness(); + const result = await api.emit("tool_call", { toolName: "glob", input: { path: "source.py;stale.py" }, toolCallId: "call-1" }); + expect(result?.additionalContext).toContain("guidance 1"); + expect(result?.additionalContext).toContain("guidance 2"); +}); + +test("URLs, internal resources, literal selector-like names and false trust do not run project hooks", async () => { + const api = harness(); + for (const path of ["https://example.com/source.py", "www.example.com/source.py", "skill://graphify", "local:/source.py", "source.py; ssh://host/source.py"]) { + expect(await api.emit("tool_call", { toolName: "read", input: { path }, toolCallId: "call-1" })).toBeUndefined(); + expect(existsSync(started)).toBe(false); + } + api.trust(false); + expect(await api.emit("tool_call", { toolName: "read", input: { path: "source.py" }, toolCallId: "call-2" })).toBeUndefined(); + expect(existsSync(started)).toBe(false); + api.trust(true); + writeFileSync(join(cwd, "source.py:12"), "a real filename, not a selector"); + expect(await api.emit("tool_call", { toolName: "read", input: { path: "source.py:12" }, toolCallId: "call-3" })).toBeUndefined(); +}); + +test("a file:// read target resolves to the real local path and still reaches the guard", async () => { + // file:// is the one scheme isRemote lets through to resolveReadPath + // (which OMP's own path pipeline also resolves down to a local path, not + // an external URL) -- an in-project file:// target must still nudge... + const api = harness(); + const inProject = await api.emit("tool_call", { toolName: "read", input: { path: `file://${join(cwd, "source.py")}` }, toolCallId: "call-1" }); + expect(inProject?.additionalContext).toContain("graphify"); + // ...while one outside the project still resolves (the CLI subprocess + // runs), but the guard's own containment check stays silent, same as any + // other out-of-project absolute path. + expect(await api.emit("tool_call", { toolName: "read", input: { path: `file://${join(root, "outside.py")}` }, toolCallId: "call-2" })).toBeUndefined(); +}); + +test("file:// read targets distinguish remote authorities from localhost", async () => { + const api = harness(); + expect(await api.emit("tool_call", { + toolName: "read", + input: { path: `file://evil.com${join(cwd, "source.py")}` }, + toolCallId: "call-1", + })).toBeUndefined(); + expect(existsSync(started)).toBe(false); + + const local = await api.emit("tool_call", { + toolName: "read", + input: { path: `file://localhost${join(cwd, "source.py")}` }, + toolCallId: "call-2", + }); + expect(local?.additionalContext).toContain("graphify"); +}); + +test("navigation cancels in-flight guidance before the next session", async () => { + for (const navigation of ["session_start", "session_switch", "session_tree", "session_branch", "session_shutdown", "before_agent_start"]) { + rmSync(started, { force: true }); + writeFileSync(control, JSON.stringify({ mode: "delayed" })); + const api = harness(); + const pending = api.emit("tool_call", { toolName: "read", input: { path: "source.py" }, toolCallId: "call-1" }); + const deadline = Date.now() + 1500; + while (!existsSync(started) && Date.now() < deadline) await Bun.sleep(5); + expect(existsSync(started)).toBe(true); + await api.emit(navigation); + expect(await pending).toBeUndefined(); + } +}); + +test("oversized input never spawns, invalid/oversized output fails open, and a hung child is killed", async () => { + const api = harness(); + expect(await api.emit("tool_call", { toolName: "bash", input: { command: "x".repeat(256 * 1024) }, toolCallId: "call-1" })).toBeUndefined(); + expect(existsSync(started)).toBe(false); + for (const mode of ["invalid", "overflow", "hung"]) { + writeFileSync(control, JSON.stringify({ mode })); + const start = performance.now(); + expect(await api.emit("tool_call", { toolName: "read", input: { path: "source.py" }, toolCallId: "call-2" })).toBeUndefined(); + expect(performance.now() - start).toBeLessThan(3500); + } +}, 10000); + +test("missing or project-local executables never fall back to project Python code", async () => { + process.env.PATH = cwd; + const api = harness(); + await api.emit("tool_call", { toolName: "read", input: { path: "source.py" }, toolCallId: "call-1" }); + expect(existsSync(started)).toBe(false); + writeFileSync(join(cwd, "graphify"), readFileSync(fixture)); + chmodSync(join(cwd, "graphify"), 0o755); + await api.emit("tool_call", { toolName: "read", input: { path: "source.py" }, toolCallId: "call-2" }); + expect(existsSync(started)).toBe(false); +}); + +test("index.ts imports only packages the host actually provides -- no dependency the wheel's node_modules-less install can never resolve", () => { + // The extension loads from a site-packages install with no node_modules at + // all (see graphify/omp/package.json's "files" list). The host supplies + // exactly @oh-my-pi/pi-coding-agent to legacy extensions; anything else + // bare-imported here -- however cleanly `bun test` resolves it via this + // repo's own dev node_modules -- is unresolvable in production. + const source = readFileSync(join(import.meta.dir, "../graphify/omp/index.ts"), "utf8"); + const specifiers = [...source.matchAll(/from\s+"([^"]+)"/g)].map(m => m[1]); + const allowed = (specifier: string) => + specifier.startsWith("node:") || specifier === "@oh-my-pi/pi-coding-agent" || specifier.startsWith("@oh-my-pi/pi-coding-agent/"); + expect(specifiers.length).toBeGreaterThan(0); + expect(specifiers.filter(specifier => !allowed(specifier))).toEqual([]); +}); + +test("every named import from @oh-my-pi/pi-coding-agent/tools/path-utils is exported by the oldest host-provided copy", () => { + // Being on the allow-list above is not enough: @oh-my-pi/pi-coding-agent + // itself is allow-listed, but a symbol added to the bridge's import list + // can still be missing from an older copy the live host actually binds + // (see resolveReadPathAsync, absent from 18.1.17). Parse the real import + // list out of index.ts -- never hardcode it -- so a future added symbol + // is checked automatically. + const oldestRoot = "/root/.omp/plugins/node_modules/@oh-my-pi/pi-coding-agent"; + const oldestPathUtils = join(oldestRoot, "src/tools/path-utils.ts"); + if (!existsSync(oldestPathUtils)) { + console.warn(`skipping: oldest pi-coding-agent copy not found at ${oldestPathUtils}`); + return; + } + const source = readFileSync(join(import.meta.dir, "../graphify/omp/index.ts"), "utf8"); + const importBlock = source.match(/import\s*{([^}]+)}\s*from\s*"@oh-my-pi\/pi-coding-agent\/tools\/path-utils"/); + expect(importBlock).not.toBeNull(); + const imported = importBlock![1].split(",").map(name => name.trim()).filter(Boolean); + expect(imported.length).toBeGreaterThan(0); + const exportedSource = readFileSync(oldestPathUtils, "utf8"); + const exported = new Set( + [...exportedSource.matchAll(/^export\s+(?:async\s+function|function)\s+(\w+)/gm)].map(m => m[1]), + ); + expect(imported.filter(name => !exported.has(name))).toEqual([]); +}); diff --git a/tests/test_hook_url_paths.py b/tests/test_hook_url_paths.py new file mode 100644 index 0000000000..5d88381200 --- /dev/null +++ b/tests/test_hook_url_paths.py @@ -0,0 +1,243 @@ +r"""URL-shaped read targets must not fool the out-of-project guard. + +`_is_cwd_relative` answers "no root and no drive" to decide whether a +`file_path`/`path` value is cwd-anchored. A URL is rootless and driveless by +that exact same test: `https://example.com/source.py`, +`myscheme://x.py` and even a bare `www.example.com/source.py` all have no +`root` and no `drive`, so -- unmodified -- they short-circuit `_run_hook_guard` +straight to "in project" and the (correct, untouched) containment check below +never runs. Reproduced live against `graphify hook-guard read`, cwd at a +graphed project: + + https://example.com/source.py -> NUDGE (wrong) + www.example.com/source.py -> NUDGE (wrong) + /local:/source.py -> NUDGE (wrong; a naive + host-side path-join + glues a rootless + local://source.py onto + an absolute prefix, + which the containment + check can't tell apart + from a real file) + /artifact:/5.py -> NUDGE (wrong, same) + /etc/elsewhere/source.py -> silent (right) + src/usr/local/pkg/pfblockerng/pfb_unbound.py -> NUDGE (right) + +Three traps a naive "rootless value carrying `scheme://` is not cwd-relative" +rule would get wrong: + + 1. `file://` is LOCAL. OMP's own pipeline strips it and resolves the + underlying path (its `strictExternalUrlRe` deliberately omits `file`), + so `file:////src/foo.py` must still nudge -- silencing it would + be a false negative worse than the bug this fixes. + 2. `www.host/path` carries no `://` at all, so no scheme rule fires on it. + Decision recorded here: it IS handled, existence-gated exactly like + OMP's own `resolveToolSearchScope` ("an existing local path wins over + URL") -- `_is_external_www_target` below, not `_is_cwd_relative` itself, + which has no cwd/root context to make that call. + 3. An unrecognized scheme (`myscheme://x`) is LOCAL to OMP's own + classifier, but this guard deliberately diverges: harness-agnostic + silence beats a harness-specific scheme allow-list. + +Input is normalized (trim + de-quote, `_normalize_hook_path`) before any +shape test, so a quoted or whitespace-padded value classifies identically to +its literal form. +""" +import io +import json +import sys +import time + +import pytest + +import graphify.cli as cli +from graphify.cli import ( + _has_embedded_url_scheme_segment, + _is_cwd_relative, + _is_external_www_target, + _normalize_hook_path, +) + + +# --------------------------------------------------------------------------- +# Classification units +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize("value", [ + "https://example.com/source.py", + "myscheme://x.py", # unrecognized scheme (trap 3): still rejected + ' "https://example.com/source.py" ', # quoted + whitespace-padded +]) +def test_url_prefixed_values_are_not_cwd_relative(value): + assert _is_cwd_relative(value) is False, value + + +def test_file_url_is_not_naively_cwd_relative(tmp_path): + """file:// (trap 1) strips to an absolute local path, so it is NOT the + naive "no root, no drive -> in project" shortcut -- it goes through the + real containment check below, exercised end-to-end further down.""" + target = tmp_path / "src" / "foo.py" + assert _is_cwd_relative(f"file://{target.as_posix()}") is False + + +def test_in_project_relative_paths_are_unaffected(): + for value in ["src/mod.py", "a.py", "./rel.py", ' "src/mod.py" ']: + assert _is_cwd_relative(value) is True, value + + +def test_absolute_out_of_project_path_is_unaffected(): + """Control: an ordinary absolute path outside the project, no URL + involved -- must keep working exactly as before.""" + assert _is_cwd_relative("/etc/elsewhere/source.py") is False + + +def test_embedded_scheme_segment_detects_a_mangled_absolute_url(): + """/local:/source.py and /artifact:/5.py: what a naive + host-side path-join leaves behind when it glues a rootless scheme://... + value onto an absolute prefix instead of routing it through a URL + handler. _is_cwd_relative alone can't catch these (they DO have a root), + so this is the companion check the call site also runs.""" + assert _has_embedded_url_scheme_segment("/root/git/pfBlockerNG/local:/source.py") is True + assert _has_embedded_url_scheme_segment("/root/git/pfBlockerNG/artifact:/5.py") is True + + +def test_embedded_scheme_segment_is_silent_on_ordinary_absolute_paths(): + assert _has_embedded_url_scheme_segment("/etc/elsewhere/source.py") is False + assert _has_embedded_url_scheme_segment("/root/git/pfBlockerNG/src/mod.py") is False + + +def test_www_target_is_external_only_without_a_matching_local_file(tmp_path): + """Trap 2, decided: www. IS handled, gated on local existence (mirrors + OMP's own "an existing local path wins over URL" precedence).""" + assert _is_external_www_target("www.example.com/source.py", tmp_path) is True + local = tmp_path / "www.example.com" / "source.py" + local.parent.mkdir(parents=True) + local.write_text("# real file", encoding="utf-8") + assert _is_external_www_target("www.example.com/source.py", tmp_path) is False + assert _is_external_www_target("src/mod.py", tmp_path) is False + + +def test_normalize_hook_path_strips_padding_quotes_and_file_scheme(): + assert _normalize_hook_path(' "src/mod.py" ') == "src/mod.py" + assert _normalize_hook_path("src/mod.py") == "src/mod.py" + assert _normalize_hook_path("file:///abs/proj/foo.py") == "/abs/proj/foo.py" + + +def test_normalize_hook_path_file_url_authority_rows(): + """RFC 8089 / Node's ``url.fileURLToPath`` (throws + ``ERR_INVALID_FILE_URL_HOST`` for anything else): a ``file://`` URL is + local only when its authority is empty or ``localhost``. Only those two + forms may be reduced to their bare path component; any other authority + names a remote host and must be left intact for `_is_foreign_url_scheme` + to classify.""" + assert _normalize_hook_path("file:///abs/proj/foo.py") == "/abs/proj/foo.py" + assert _normalize_hook_path("file://localhost/abs/proj/foo.py") == "/abs/proj/foo.py" + assert _normalize_hook_path("file://evil.com/abs/proj/foo.py") == "file://evil.com/abs/proj/foo.py" + + +# --------------------------------------------------------------------------- +# End-to-end through the guard (real stdin JSON, cwd at a graphed project) +# --------------------------------------------------------------------------- + +def _project(tmp_path): + src = tmp_path / "src" + src.mkdir() + f = src / "mod.py" + f.write_text("def x():\n return 1\n", encoding="utf-8") + out = tmp_path / "graphify-out" + out.mkdir() + (out / "manifest.json").write_text( + json.dumps({"src/mod.py": {"mtime": 1}}), encoding="utf-8") + time.sleep(0.02) + (out / "graph.json").write_text('{"nodes":[],"links":[]}', encoding="utf-8") + return f + + +def _invoke(tmp_path, monkeypatch, file_path): + monkeypatch.chdir(tmp_path) + payload = {"session_id": "s1", "tool_name": "Read", + "tool_input": {"file_path": str(file_path)}} + + class _Stdin: + buffer = io.BytesIO(json.dumps(payload).encode()) + monkeypatch.setattr(sys, "stdin", _Stdin()) + buf = io.StringIO() + monkeypatch.setattr(sys, "stdout", buf) + cli._run_hook_guard("read") + return buf.getvalue() + + +def test_https_url_is_silent(tmp_path, monkeypatch): + _project(tmp_path) + assert _invoke(tmp_path, monkeypatch, "https://example.com/source.py").strip() == "" + + +def test_www_host_with_no_local_match_is_silent(tmp_path, monkeypatch): + _project(tmp_path) + assert _invoke(tmp_path, monkeypatch, "www.example.com/source.py").strip() == "" + + +def test_mangled_absolute_local_scheme_url_is_silent(tmp_path, monkeypatch): + _project(tmp_path) + target = f"{tmp_path}/local:/source.py" + assert _invoke(tmp_path, monkeypatch, target).strip() == "" + + +def test_mangled_absolute_artifact_scheme_url_is_silent(tmp_path, monkeypatch): + _project(tmp_path) + target = f"{tmp_path}/artifact:/5.py" + assert _invoke(tmp_path, monkeypatch, target).strip() == "" + + +def test_unrecognized_scheme_is_silent(tmp_path, monkeypatch): + _project(tmp_path) + assert _invoke(tmp_path, monkeypatch, "myscheme://x.py").strip() == "" + + +def test_absolute_out_of_project_path_stays_silent(tmp_path, monkeypatch): + """Control (right today, must stay right): containment already works.""" + _project(tmp_path) + assert _invoke(tmp_path, monkeypatch, "/etc/elsewhere/source.py").strip() == "" + + +def test_in_project_relative_path_still_nudges(tmp_path, monkeypatch): + """Control (right today, must stay right).""" + _project(tmp_path) + assert "MANDATORY" in _invoke(tmp_path, monkeypatch, "src/mod.py") + + +def test_file_url_to_an_indexed_in_project_file_still_nudges(tmp_path, monkeypatch): + """Trap 1: file:// must NOT regress to silent.""" + f = _project(tmp_path) + assert "MANDATORY" in _invoke(tmp_path, monkeypatch, f"file://{f.as_posix()}") + + +def test_file_url_with_remote_authority_is_silent(tmp_path, monkeypatch): + """VALID #4: a non-local authority (RFC 8089) names a remote host, not + the in-project file it happens to share a path with -- must NOT nudge.""" + f = _project(tmp_path) + assert _invoke(tmp_path, monkeypatch, f"file://evil.com{f.as_posix()}").strip() == "" + + +def test_file_url_with_localhost_authority_still_nudges(tmp_path, monkeypatch): + """localhost is local per RFC 8089 / Node's url.fileURLToPath.""" + f = _project(tmp_path) + assert "MANDATORY" in _invoke(tmp_path, monkeypatch, f"file://localhost{f.as_posix()}") + + +def test_quoted_and_padded_https_url_is_silent(tmp_path, monkeypatch): + _project(tmp_path) + assert _invoke(tmp_path, monkeypatch, ' "https://example.com/source.py" ').strip() == "" + + +def test_padded_in_project_path_still_nudges(tmp_path, monkeypatch): + """Leading whitespace only (not quotes, not trailing padding): the + extension allow-list this guard must not touch splits on the final "/" + and matches the tail verbatim, so trailing junk after ".py" fails IT + regardless of classification -- that is pre-existing, out-of-scope + behaviour, not the URL-classification bug this file covers. Quoted and + doubly-padded input is proven at the classification layer directly in + test_in_project_relative_paths_are_unaffected above. + """ + _project(tmp_path) + assert "MANDATORY" in _invoke(tmp_path, monkeypatch, " src/mod.py") diff --git a/tests/test_omp_install.py b/tests/test_omp_install.py new file mode 100644 index 0000000000..98a75edede --- /dev/null +++ b/tests/test_omp_install.py @@ -0,0 +1,52 @@ +"""OMP owns registration; Graphify supplies a wheel-contained native package.""" +import json +from pathlib import Path +import sys +from types import SimpleNamespace + +import pytest + +from graphify.install import dispatch_install_cli + +try: + import tomllib +except ModuleNotFoundError: # Python 3.10 + import tomli as tomllib # type: ignore[no-redef] + + +def test_omp_path_is_a_complete_native_package(monkeypatch, capsys): + monkeypatch.setattr(sys, "argv", ["graphify", "omp", "path"]) + assert dispatch_install_cli("omp") + package = Path(capsys.readouterr().out.strip()) + manifest = json.loads((package / "package.json").read_text(encoding="utf-8")) + assert manifest["omp"]["extensions"] + pyproject = tomllib.loads((package.parents[1] / "pyproject.toml").read_text(encoding="utf-8")) + assert manifest["version"] == pyproject["project"]["version"] + for entry in manifest["omp"]["extensions"]: + assert (package / entry).is_file() + assert (package / entry).resolve().is_relative_to(package) + + +def test_omp_install_delegates_to_host_and_propagates_failure(monkeypatch): + monkeypatch.setattr(sys, "argv", ["graphify", "omp", "install"]) + monkeypatch.setattr("graphify.install.shutil.which", lambda name: "/usr/bin/omp") + + def host(argv, *, check): + assert argv[:3] == ["/usr/bin/omp", "plugin", "install"] + assert (Path(argv[3]) / "package.json").is_file() + return SimpleNamespace(returncode=23) + + monkeypatch.setattr("subprocess.run", host) + with pytest.raises(SystemExit) as error: + dispatch_install_cli("omp") + assert error.value.code == 23 + + +def test_omp_install_without_host_does_not_create_configuration(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(sys, "argv", ["graphify", "omp", "install"]) + monkeypatch.setattr("graphify.install.shutil.which", lambda name: None) + with pytest.raises(SystemExit) as error: + dispatch_install_cli("omp") + assert error.value.code == 1 + assert not list(tmp_path.iterdir()) diff --git a/tests/test_omp_platform.py b/tests/test_omp_platform.py new file mode 100644 index 0000000000..2c804e23f1 --- /dev/null +++ b/tests/test_omp_platform.py @@ -0,0 +1,130 @@ +"""Tests for the OMP (Oh My Pi) install platform. + +`graphify install --platform omp` installs the skill into OMP's agent +directory — ``~/.omp/agent/skills`` globally, ``./.omp/agent/skills`` in +project scope — reusing pi's skill bundle, because OMP mirrors pi's agent +layout. The ``graphify omp`` subcommand is the plugin installer (PR #3506), so skill removal rides ``uninstall_all``. +""" +import os +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +import graphify.__main__ as mainmod + + +# --- destination map ----------------------------------------------------------- + + +def test_omp_user_destination_is_omp_agent_skills(tmp_path): + """Global omp skill lands at ~/.omp/agent/skills — the active OMP agent + directory, NOT the legacy ~/.pi path OMP no longer reads.""" + with patch("graphify.__main__.Path.home", return_value=tmp_path): + dst = mainmod._platform_skill_destination("omp", project=False) + assert dst == tmp_path / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + + +def test_omp_project_destination_is_omp_agent_skills(tmp_path): + """Project omp skill lands at ./.omp/agent/skills.""" + dst = mainmod._platform_skill_destination("omp", project=True, project_dir=tmp_path) + assert dst == tmp_path / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + + +def test_omp_reuses_the_pi_bundle(): + """omp installs pi's skill file and references sidecar verbatim: OMP is a + pi successor with the same skill format, so a second bundle would only + drift.""" + cfg = mainmod._PLATFORM_CONFIG["omp"] + assert cfg["skill_file"] == mainmod._PLATFORM_CONFIG["pi"]["skill_file"] + assert cfg["skill_refs"] == "pi" + assert cfg["claude_md"] is False + + +# --- end-to-end install / uninstall via the CLI -------------------------------- + + +def _run(tmp_path, argv, home): + """Drive main() with argv, cwd at tmp_path, and Path.home redirected.""" + old_cwd = Path.cwd() + try: + os.chdir(tmp_path) + with patch.object(sys, "argv", ["graphify", *argv]): + with patch("graphify.__main__.Path.home", return_value=home): + mainmod.main() + finally: + os.chdir(old_cwd) + + +def test_install_platform_omp_writes_user_skill(tmp_path): + """`graphify install --platform omp` writes ~/.omp/agent/skills/... + SKILL.md (+ pi's references) and nothing else — no CLAUDE.md (skill-only).""" + home = tmp_path / "home" + cwd = tmp_path / "cwd" + home.mkdir() + cwd.mkdir() + + _run(cwd, ["install", "--platform", "omp"], home) + + skill = home / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + assert skill.exists() + assert (skill.parent / ".graphify_version").read_text() == mainmod.__version__ + assert (skill.parent / "references" / "extraction-spec.md").exists() + # Skill-only: the --platform path must not write a CLAUDE.md. + assert not (cwd / "CLAUDE.md").exists() + + +def test_uninstall_platform_flag_global_removes_skill(tmp_path): + """`graphify uninstall --platform omp` (global) clears ~/.omp/agent/skills. + + The global uninstall dispatch always runs uninstall_all, which carries + `_remove_skill_file("omp")` (the `graphify omp` subcommand is the plugin + installer, so the skill's removal rides uninstall_all, like amp/agents). + """ + home = tmp_path / "home" + cwd = tmp_path / "cwd" + home.mkdir() + cwd.mkdir() + + _run(cwd, ["install", "--platform", "omp"], home) + skill = home / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + assert skill.exists() + + _run(cwd, ["uninstall", "--platform", "omp"], home) + assert not skill.exists() + + +def test_install_platform_omp_project_writes_omp_agent_skills(tmp_path): + """`graphify install --project --platform omp` writes ./.omp/agent/skills and + leaves user scope untouched.""" + home = tmp_path / "home" + proj = tmp_path / "proj" + home.mkdir() + proj.mkdir() + + _run(proj, ["install", "--project", "--platform", "omp"], home) + + project_skill = proj / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + assert project_skill.exists() + assert (project_skill.parent / "references" / "extraction-spec.md").exists() + # User scope was not touched. + assert not (home / ".omp" / "agent").exists() + + _run(proj, ["uninstall", "--project", "--platform", "omp"], home) + assert not project_skill.exists() + + +def test_unknown_platform_error_lists_omp(capsys, tmp_path): + """The unknown-platform error is built from _PLATFORM_CONFIG, so the new + platform must appear in its choices — a guard against a config entry the + CLI error path cannot see.""" + home = tmp_path / "home" + cwd = tmp_path / "cwd" + home.mkdir() + cwd.mkdir() + + with pytest.raises(SystemExit): + _run(cwd, ["install", "--platform", "not-a-platform"], home) + + assert "omp" in capsys.readouterr().err