{ config, pkgs, ... }:
let
  # Pi injects @earendil-works/* and typebox at runtime, so drop its dev/peer
  # deps and their transitive graph from the install.
  slimSrc = pkgs.applyPatches {
    name = "pi-web-access-slim";
    src = pkgs.npinsed.ai.pi.pi-web-access;
    nativeBuildInputs = [ pkgs.jq ];
    postPatch = ''
      jq 'del(.devDependencies, .peerDependencies, .peerDependenciesMeta, .scripts)' package.json > package.json.tmp
      mv package.json.tmp package.json
      cp ${./package-lock.json} package-lock.json
    '';
  };
in {
  # Search-provider keys live in Pi's agent dir, not in Nix. Written by yq-merge
  # (a real file, not a store symlink) so the extension's `/websearch` edits keep
  # working; the keys still end up in the store, same as auth.json.
  yq-merge.".pi/agent/web-search.json" = {
    generator = builtins.toJSON;
    expr = {
      tavilyApiKey = config.ai.tavily.api_key;
      deepseekApiKey = config.ai.deepseek.api_key;
    };
  };

  home.file.".pi/agent/extensions/pi-web-access".source = pkgs.buildNpmPackage {
    name = "pi-web-access";
    src = slimSrc;
    npmDepsHash = "sha256-aoS9mSkbGjjnhm7rwWoPBgK2XWJCmg0OmUMuuwAI/yc=";
    dontNpmBuild = true;
    npmRebuildFlags = [ "--ignore-scripts" ];
    nativeBuildInputs = [ pkgs.jq ];
    postInstall = ''
      # `bin` points at dist/mcp-cli.js, which only upstream's publish build
      # creates; drop the dead wrapper and the field that promises it.
      rm -rf $out/bin
      dir=$out/lib/node_modules/pi-web-access
      jq 'del(.bin)' "$dir/package.json" > "$dir/package.json.tmp"
      mv "$dir/package.json.tmp" "$dir/package.json"
      # Guard the one contract Pi needs: every declared entry file must exist.
      jq -r '.pi.extensions[]' "$dir/package.json" | while read -r e; do test -f "$dir/$e"; done
    '';
  } + /lib/node_modules/pi-web-access;
}