feat(pkg-intel): add package_vulnerabilities (pkg vulns + MCP) - #14
Merged
Conversation
jlitola
force-pushed
the
feat/pkg-intel-vulnerabilities
branch
from
April 20, 2026 12:34
4f0a9f6 to
2dc48e9
Compare
Adds the `package_vulnerabilities` MCP tool and `githits pkg vulns`
CLI command behind the existing `code_navigation` capability gate.
Both surfaces share a single request builder, envelope builder, and
error classifier; a parity test asserts `toEqual` JSON across the
two surfaces for every service-sourced fixture.
Key design choices:
- Server-side filtering. `--severity` / `min_severity` map to a CVSS
float on the wire; `--include-withdrawn` passes through untouched.
The backend returns a filter-aware `vulnerabilityCount`, so
`summary.total` reflects whatever survived the filter. No
client-side filtering, no dual-summary block.
- Partitioning bySeverity buckets. `summary.bySeverity` splits
returned advisories into disjoint `malware` (isMalicious === true),
CVSS bands (`critical` / `high` / `medium` / `low`), and `unrated`
(non-malicious, no CVSS score) buckets. The client-side guarantee
is that every returned advisory lands in exactly one bucket, so
the header breakdown reconciles with the total even on
registries (RUSTSEC, PYSEC) where unbanded advisories are common.
- Permissive MCP schema + in-handler validation. `registry` and
`package_name` accept any string; validation runs in
`buildPackageVulnerabilitiesParams` and emits the shared
`{ error, code, retryable, details? }` envelope on invalid input.
`min_severity` tolerates uppercase input.
- Canonical versions only. Tag-style refs with a leading `v`
(e.g. `v4.18.0`) are rejected client-side with `INVALID_ARGUMENT`
before the backend call — the `v` prefix is a git-tag convention,
not a canonical version on any supported registry. Proper
ecosystem-aware version parsing belongs in the backend, not in
ad hoc CLI normalisation rules.
- Typed `VERSION_NOT_FOUND`. When the backend emits
`extensions.code = "VERSION_NOT_FOUND"` with structured fields,
the classifier surfaces `details: { package, requestedVersion,
availableVersions[] }` in the envelope. The CLI terminal error
expands this into `package:` / `requested:` / `available:`
detail lines so users see the mistake and the alternatives.
A narrow message-string fallback promotes generic BackendErrors
with matching text to the typed error while the backend catches
up; guarded to only fire when no `graphqlCode` is present so
real server faults never have their retryability flipped.
- Registry coverage. Only npm, PyPI, Hex, and Crates have
vulnerability data. The other five known registries are rejected
client-side with a tool-specific message.
- Malware marker. Malicious advisories render with a red/bold
`MALWARE` column (optionally combined as `MALWARE · crit`) and
sort to the top of the advisory list regardless of score. The
malware count in the breakdown line is always coloured red.
Terminal UX refinements:
- `latest affected` is the only yellow token in the summary line;
`latest clean` stays plain so it doesn't read as a caution signal.
- Unrated advisories render with a dim `unrated` label in the
severity gutter rather than an empty column.
- Affected-range lists cap at 4 / 6 / 8 entries on narrow /
standard-wide / ultrawide terminals (≥120 / ≥160 cols) with a
`… (+N more; use -v)` hint; verbose mode shows every range. JSON
output is never truncated.
- Upgrade options are de-duplicated and sorted ascending by
semver-ish comparison (pre-releases rank below their base),
presenting the minimum-churn upgrade first.
- Literal `No summary available` placeholders from upstream are
stripped from both the JSON envelope and the terminal row.
- `pkg vulns` accepts `@version` in the spec (unlike `pkg info`,
which always returns latest).
MCP server instructions: `src/commands/mcp-instructions.ts` gains a
one-line bullet for `package_vulnerabilities` in the package-tools
section, gated on `packageIntelligenceService` presence. The
mention↔registration invariant test covers the new tool across
gate-closed, gate-open, half-open, and opaque-token scenarios.
Tests: 810 pass. Parity test covers happy, zero-vulns, filtered,
versioned-match, `v`-prefixed (rejected), versioned real-diff,
`NOT_FOUND`, `VERSION_NOT_FOUND` with structured details,
`BACKEND_ERROR`, plus `INVALID_ARGUMENT` fixtures for unsupported
registry and invalid severity label.
Live-verified against production pkgseer across npm, PyPI, Crates,
Hex, scoped-npm, and MCP stdio.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jlitola
force-pushed
the
feat/pkg-intel-vulnerabilities
branch
from
April 20, 2026 15:28
f569005 to
679ea6a
Compare
7 tasks
jlitola
added a commit
that referenced
this pull request
Apr 20, 2026
Fourth Wave 1 package-intelligence tool, following `package_summary` (#13), `package_vulnerabilities` (#14), `package_dependencies` (#16 + follow-up #17). Behind the existing `code_navigation` capability gate. Both surfaces share a single request builder, envelope builder, and error classifier; a parity test asserts `toEqual` JSON across the two surfaces for every service-sourced fixture. ## Surfaces - **`githits pkg changelog [spec]`** — default latest-mode output is a summary row (identity · source · mode · entry count) plus a one-line `version date url` per entry, newest-first. `--from <v>` switches to range mode (all entries between `--from` and `--to`/latest; `--limit` rejected). `--to <v>` / `--limit <n>` shape latest mode. `--verbose` expands each entry with its full markdown body, indented and dimmed. `--no-body` drops body fields from both terminal and JSON. `--git-ref` targets a specific branch/tag for CHANGELOG.md source. `--repo-url <url>` is an alternative addressing mode (mutually exclusive with `<spec>`). - **`package_changelog`** — MCP tool with the same envelope. Dual addressing: `registry` + `package_name` XOR `repo_url`. Permissive Zod schema (in-handler validation). `include_bodies` (default true) mirrors CLI's `--no-body`. Description is self-contained — no CLI-flag references. ## Design choices - **Dual addressing is unique to this tool.** P1 / P2 / P3 all accept only `registry` + `package_name` because their underlying backend queries are registry-metadata APIs. `packageChangelog` is intrinsically repo-level (its sources are GitHub Releases, CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode in the GraphQL signature, not a bolt-on. Exposing it on MCP was non-negotiable: `packageSummary` cannot resolve a repo URL → spec, so agents starting from a repo URL had no path in. Documented in `tools.md` so future tool authors don't cargo-cult the asymmetry. - **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat `@version` as "for this exact version"; changelog has no single- version query. Silently remapping to `toVersion` would be a client-invented semantic shift. Rejected client-side with `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`. - **Data-first envelope.** `{registry|repoUrl, source, mode, entries: {count, items}, filter?}`. `source` always present here (null-source case is promoted to `NOT_FOUND` at the service boundary and never reaches the envelope). `mode` derived from request — `"range"` iff `fromVersion` non-null after normalisation, `"latest"` otherwise. `entries.count` computed client-side from `items.length`; the backend's count isn't selected on the wire so the invariant holds by construction. - **`version` kept when null, other per-entry nullables stripped.** `version` is the primary key agents index by, so the slot is always present (possibly null); stripping other nullables keeps the envelope lean. `body` additionally stripped under `include_bodies: false`. - **`filter.*` echo tracks explicit fields only.** Request builder tracks `explicitFilterFields` set; envelope emits `filter.*` only for caller-supplied inputs. Backend defaults (`limit: 10`, `toVersion: <latest>`) never round-trip as caller intent. - **`include_bodies` lever.** Release bodies on large packages can run 10 KB+ per entry; `include_bodies: false` drops `body` from all items explicitly (not silent truncation). Other fields preserved so agents still get the version / date / URL timeline. - **`metadata` dropped from envelope in v1.** Source-specific opaque `GenericJSON`; revisit via agent feedback with a `TODO(pkgseer-backend)` anchor if demand surfaces. - **No client-side registry restriction.** Unlike P3's 6-registry client-side gate, `packageChangelog` is source-pull rather than registry-query; the backend returns `NOT_FOUND` for registries without changelog sources. Live-smoke matrix documented in the plan to verify behaviour per registry. - **Mode mutual exclusion enforced client-side.** `--from` / `from_version` + `--limit` / `limit` together → `INVALID_ARGUMENT` with actionable hint. - **`source: null` promoted to NOT_FOUND.** Typed `PackageIntelligenceChangelogSourceNotFoundError` at the service boundary; shared classifier routes to `NOT_FOUND` with a message naming the sources tried (GitHub Releases, CHANGELOG.md, HexDocs). Empty `entries.items: []` with a valid `source` is success, not error. - **`--verbose` vs `--no-body` vs `--json` interaction.** `--verbose` is terminal-only (expands markdown bodies per entry); does not change `--json`. `--no-body` affects both terminal and `--json`. `--json` output shape is independent of `--verbose`. - **Shared `promoteGenericVersionNotFound` extended.** The helper now recognises `fromVersion` / `toVersion` in addition to `version`; preference order `version → fromVersion → toVersion`. `registry` / `packageName` made optional so repo-URL-addressed requests flow through without a spec. P2 / P3 regressions guarded by the existing tests plus 4 new helper tests. ## Tests - `bun test` — 1053 pass, 0 fail (was 966; +87 new tests). - `bun run typecheck` / `bun run build` / `bun run lint` — clean. - Unit tests: request-builder (addressing XOR, `<spec>@<version>` rejection, `--from`/`--limit` mutex, limit bounds, tag-style rejection, pre-release versions on `--from`/`--to`, explicit-flag tracking), envelope builder (shape, null handling, `include_bodies`, mode derivation, filter echo, empty entries), terminal formatter (default one-liners, `--verbose` expansion, missing fields), CLI action (13 cases incl. addressing, errors, flag interactions), MCP tool (15 cases incl. validation + service errors). - Parity test: 12 fixtures (happy latest, range mode, repo-URL addressing, `--no-body` / `include_bodies: false`, default bodies, empty entries, `NOT_FOUND` no-source, `TargetNotFoundError`, `VERSION_NOT_FOUND` with structured details, `BACKEND_ERROR`, `INVALID_ARGUMENT` for `<spec>@<version>` and `--from`+`--limit`). - Live-smoke deferred to review cycle (local non-interactive bash keychain prompt blocks the CLI startup handshake; shape correctness is locked in via the verbatim schema Zod parse + parity test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
jlitola
added a commit
that referenced
this pull request
Apr 21, 2026
Fourth Wave 1 package-intelligence tool, following `package_summary` (#13), `package_vulnerabilities` (#14), `package_dependencies` (#16 + follow-up #17). Behind the existing `code_navigation` capability gate. Both surfaces share a single request builder, envelope builder, and error classifier; a parity test asserts `toEqual` JSON across the two surfaces for every service-sourced fixture. ## Surfaces - **`githits pkg changelog [spec]`** — default latest-mode output is a summary row (identity · source · mode · entry count) plus a one-line `version date url` per entry, newest-first. `--from <v>` switches to range mode (all entries between `--from` and `--to`/latest; `--limit` rejected). `--to <v>` / `--limit <n>` shape latest mode. `--verbose` expands each entry with its full markdown body, indented and dimmed. `--no-body` drops body fields from both terminal and JSON. `--git-ref` targets a specific branch/tag for CHANGELOG.md source. `--repo-url <url>` is an alternative addressing mode (mutually exclusive with `<spec>`). - **`package_changelog`** — MCP tool with the same envelope. Dual addressing: `registry` + `package_name` XOR `repo_url`. Permissive Zod schema (in-handler validation). `include_bodies` (default true) mirrors CLI's `--no-body`. Description is self-contained — no CLI-flag references. ## Design choices - **Dual addressing is unique to this tool.** P1 / P2 / P3 all accept only `registry` + `package_name` because their underlying backend queries are registry-metadata APIs. `packageChangelog` is intrinsically repo-level (its sources are GitHub Releases, CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode in the GraphQL signature, not a bolt-on. Exposing it on MCP was non-negotiable: `packageSummary` cannot resolve a repo URL → spec, so agents starting from a repo URL had no path in. Documented in `tools.md` so future tool authors don't cargo-cult the asymmetry. - **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat `@version` as "for this exact version"; changelog has no single- version query. Silently remapping to `toVersion` would be a client-invented semantic shift. Rejected client-side with `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`. - **Data-first envelope.** `{registry|repoUrl, source, mode, entries: {count, items}, filter?}`. `source` always present here (null-source case is promoted to `NOT_FOUND` at the service boundary and never reaches the envelope). `mode` derived from request — `"range"` iff `fromVersion` non-null after normalisation, `"latest"` otherwise. `entries.count` computed client-side from `items.length`; the backend's count isn't selected on the wire so the invariant holds by construction. - **`version` kept when null, other per-entry nullables stripped.** `version` is the primary key agents index by, so the slot is always present (possibly null); stripping other nullables keeps the envelope lean. `body` additionally stripped under `include_bodies: false`. - **`filter.*` echo tracks explicit fields only.** Request builder tracks `explicitFilterFields` set; envelope emits `filter.*` only for caller-supplied inputs. Backend defaults (`limit: 10`, `toVersion: <latest>`) never round-trip as caller intent. - **`include_bodies` lever.** Release bodies on large packages can run 10 KB+ per entry; `include_bodies: false` drops `body` from all items explicitly (not silent truncation). Other fields preserved so agents still get the version / date / URL timeline. - **`metadata` dropped from envelope in v1.** Source-specific opaque `GenericJSON`; revisit via agent feedback with a `TODO(pkgseer-backend)` anchor if demand surfaces. - **No client-side registry restriction.** Unlike P3's 6-registry client-side gate, `packageChangelog` is source-pull rather than registry-query; the backend returns `NOT_FOUND` for registries without changelog sources. Live-smoke matrix documented in the plan to verify behaviour per registry. - **Mode mutual exclusion enforced client-side.** `--from` / `from_version` + `--limit` / `limit` together → `INVALID_ARGUMENT` with actionable hint. - **`source: null` promoted to NOT_FOUND.** Typed `PackageIntelligenceChangelogSourceNotFoundError` at the service boundary; shared classifier routes to `NOT_FOUND` with a message naming the sources tried (GitHub Releases, CHANGELOG.md, HexDocs). Empty `entries.items: []` with a valid `source` is success, not error. - **`--verbose` vs `--no-body` vs `--json` interaction.** `--verbose` is terminal-only (expands markdown bodies per entry); does not change `--json`. `--no-body` affects both terminal and `--json`. `--json` output shape is independent of `--verbose`. - **Shared `promoteGenericVersionNotFound` extended.** The helper now recognises `fromVersion` / `toVersion` in addition to `version`; preference order `version → fromVersion → toVersion`. `registry` / `packageName` made optional so repo-URL-addressed requests flow through without a spec. P2 / P3 regressions guarded by the existing tests plus 4 new helper tests. ## Tests - `bun test` — 1053 pass, 0 fail (was 966; +87 new tests). - `bun run typecheck` / `bun run build` / `bun run lint` — clean. - Unit tests: request-builder (addressing XOR, `<spec>@<version>` rejection, `--from`/`--limit` mutex, limit bounds, tag-style rejection, pre-release versions on `--from`/`--to`, explicit-flag tracking), envelope builder (shape, null handling, `include_bodies`, mode derivation, filter echo, empty entries), terminal formatter (default one-liners, `--verbose` expansion, missing fields), CLI action (13 cases incl. addressing, errors, flag interactions), MCP tool (15 cases incl. validation + service errors). - Parity test: 12 fixtures (happy latest, range mode, repo-URL addressing, `--no-body` / `include_bodies: false`, default bodies, empty entries, `NOT_FOUND` no-source, `TargetNotFoundError`, `VERSION_NOT_FOUND` with structured details, `BACKEND_ERROR`, `INVALID_ARGUMENT` for `<spec>@<version>` and `--from`+`--limit`). - Live-smoke deferred to review cycle (local non-interactive bash keychain prompt blocks the CLI startup handshake; shape correctness is locked in via the verbatim schema Zod parse + parity test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jlitola
added a commit
that referenced
this pull request
Apr 21, 2026
Fourth Wave 1 package-intelligence tool, following `package_summary` (#13), `package_vulnerabilities` (#14), `package_dependencies` (#16 + follow-up #17). Behind the existing `code_navigation` capability gate. Both surfaces share a single request builder, envelope builder, and error classifier; a parity test asserts `toEqual` JSON across the two surfaces for every service-sourced fixture. ## Surfaces - **`githits pkg changelog [spec]`** — default latest-mode output is a summary row (identity · source · mode · entry count) plus, per entry, a `version date url` header followed by the first 10 lines of the markdown body (with a `… (+N more lines — use --verbose for the full body)` footer when the body is longer). `--verbose` uncaps the body preview; `--no-body` drops bodies entirely from both terminal and `--json`. `--from <v>` switches to range mode (all entries between `--from` and `--to`/latest; `--limit` rejected). `--to <v>` / `--limit <n>` shape latest mode. `--git-ref` targets a branch/tag for CHANGELOG.md source. `--repo-url <url>` is an alternative addressing mode, mutually exclusive with `<spec>`. - **`package_changelog`** — MCP tool with the same envelope. Dual addressing: `registry` + `package_name` XOR `repo_url`. Permissive Zod schema + in-handler validation. `include_bodies` (default true) mirrors CLI's `--no-body` and controls JSON envelope bodies. ## Design choices - **Dual addressing is unique to this tool.** P1 / P2 / P3 all accept only `registry` + `package_name` because their underlying backend queries are registry-metadata APIs. `packageChangelog` is intrinsically repo-level (sources: GitHub Releases, CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode in the GraphQL signature. Exposing it on MCP was non-negotiable: `packageSummary` cannot resolve repo-URL → spec, so agents starting from a repo URL had no path in. Documented in `tools.md` so future tool authors don't cargo-cult the asymmetry. - **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat `@version` as "for this exact version"; changelog has no single-version query. Silently remapping to `toVersion` would be a client-invented semantic shift. Rejected with `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`. - **Body preview capped at 10 lines by default.** Release bodies routinely run 50-100+ lines — showing them unbounded would swamp the terminal; showing none would make the whole command useless for answering "what changed". 10-line cap shows the first one or two sections plus preamble, which is usually enough to answer the question; `--verbose` lifts the cap when full context is needed; `--no-body` drops bodies entirely for a pure timeline view. JSON envelope is always full-body unless `--no-body` / `include_bodies: false` is set — `--verbose` is terminal-only. - **Data-first envelope.** `{registry|repoUrl, source, mode, entries: {count, items}, filter?}`. `source` always present here (null-source case promoted to `NOT_FOUND` at the service boundary; envelope builder has a defence-in-depth throw if the invariant is ever violated upstream). `mode` derived from request — `"range"` iff `fromVersion` non-null, `"latest"` otherwise. `entries.count` computed client-side from `items.length`; backend count not selected on the wire. - **`version` kept when null, other per-entry nullables stripped.** `version` is the primary key agents index by. Empty-string values (e.g. `body: ""`) are preserved — distinct from absent — so agents can tell "empty release notes" from "no notes field". Terminal formatter renders `(empty release notes)` sentinel for empty-string bodies. - **`filter.*` tracks explicit fields only.** Builder returns an `explicitFilterFields` set; envelope emits `filter.*` only for caller-supplied inputs. Backend defaults (`limit: 10`, `toVersion: latest`) never round-trip as caller intent. - **`include_bodies` lever.** Default true. `false` drops `body` from every item explicitly; other fields preserved so agents still get the version / date / URL timeline. Measured 5.13× envelope size reduction on a 20-entry `npm:typescript` request (17.3 KB → 3.4 KB). - **`--no-body` + `--verbose` is an error.** Contradictory intents; CLI rejects with an actionable hint rather than silently ignoring `--verbose`. - **MCP `limit` validation in the shared builder, not Zod.** If Zod hard-rejected `limit: 51` at the SDK level, the agent would see a raw SDK error instead of our shared `{error, code, retryable}` envelope. Schema is permissive; builder enforces bounds. - **Registry coverage — all 9 on the wire, no client-side restriction.** `packageChangelog` is source-pull, not registry-query. Live-smoke confirmed npm / pypi / hex / crates / vcpkg / maven / packagist return useful data; nuget returns a generic `BACKEND_ERROR`; zig packages weren't in the backend index. All paths produce the shared envelope correctly. - **Mode mutex enforced client-side.** `--from` / `from_version` + `--limit` / `limit` → `INVALID_ARGUMENT` with actionable hint. - **`metadata` dropped in v1.** Source-specific opaque `GenericJSON`; revisit via agent feedback. - **Shared `promoteGenericVersionNotFound` extended.** Now recognises `fromVersion` / `toVersion` in addition to `version`. `registry` / `packageName` made optional so repo-URL-addressed requests flow through. Preference order `version → fromVersion → toVersion`. P2 / P3 regressions guarded by existing tests + 5 new helper tests. ## Tests - `bun test` — 1062 pass, 0 fail. - `bun run typecheck` / `bun run build` / `bun run lint` — clean. - Request builder, envelope builder, terminal formatter, MCP tool, CLI action, parity fixtures (12) — see docs for full matrix. - Live-smoke against production pkgseer: latest / range / repo-URL / `--no-body` / error paths / registry matrix / body-preview cap / `--verbose` uncap / truncation footer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jlitola
added a commit
that referenced
this pull request
Apr 21, 2026
Fourth Wave 1 package-intelligence tool, following `package_summary` (#13), `package_vulnerabilities` (#14), `package_dependencies` (#16 + follow-up #17). Behind the existing `code_navigation` capability gate. Both surfaces share a single request builder, envelope builder, and error classifier; a parity test asserts `toEqual` JSON across the two surfaces for every service-sourced fixture. ## Surfaces - **`githits pkg changelog [spec]`** — default latest-mode output is a summary row (identity · source · mode · entry count) plus, per entry, a `version date url` header followed by the first 10 lines of the markdown body (with a `… (+N more lines — use --verbose for the full body)` footer when the body is longer). `--verbose` uncaps the body preview; `--no-body` drops bodies entirely from both terminal and `--json`. `--from <v>` switches to range mode (all entries between `--from` and `--to`/latest; `--limit` rejected). `--to <v>` / `--limit <n>` shape latest mode. `--git-ref` targets a branch/tag for CHANGELOG.md source. `--repo-url <url>` is an alternative addressing mode, mutually exclusive with `<spec>`. - **`package_changelog`** — MCP tool with the same envelope. Dual addressing: `registry` + `package_name` XOR `repo_url`. Permissive Zod schema + in-handler validation. `include_bodies` (default true) mirrors CLI's `--no-body` and controls JSON envelope bodies. ## Design choices - **Dual addressing is unique to this tool.** P1 / P2 / P3 all accept only `registry` + `package_name` because their underlying backend queries are registry-metadata APIs. `packageChangelog` is intrinsically repo-level (sources: GitHub Releases, CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode in the GraphQL signature. Exposing it on MCP was non-negotiable: `packageSummary` cannot resolve repo-URL → spec, so agents starting from a repo URL had no path in. Documented in `tools.md` so future tool authors don't cargo-cult the asymmetry. - **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat `@version` as "for this exact version"; changelog has no single-version query. Silently remapping to `toVersion` would be a client-invented semantic shift. Rejected with `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`. - **Body preview capped at 10 lines by default.** Release bodies routinely run 50-100+ lines — showing them unbounded would swamp the terminal; showing none would make the whole command useless for answering "what changed". 10-line cap shows the first one or two sections plus preamble, which is usually enough to answer the question; `--verbose` lifts the cap when full context is needed; `--no-body` drops bodies entirely for a pure timeline view. JSON envelope is always full-body unless `--no-body` / `include_bodies: false` is set — `--verbose` is terminal-only. - **Data-first envelope.** `{registry|repoUrl, source, mode, entries: {count, items}, filter?}`. `source` always present here (null-source case promoted to `NOT_FOUND` at the service boundary; envelope builder has a defence-in-depth throw if the invariant is ever violated upstream). `mode` derived from request — `"range"` iff `fromVersion` non-null, `"latest"` otherwise. `entries.count` computed client-side from `items.length`; backend count not selected on the wire. - **`version` kept when null, other per-entry nullables stripped.** `version` is the primary key agents index by. Empty-string values (e.g. `body: ""`) are preserved — distinct from absent — so agents can tell "empty release notes" from "no notes field". Terminal formatter renders `(empty release notes)` sentinel for empty-string bodies. - **`filter.*` tracks explicit fields only.** Builder returns an `explicitFilterFields` set; envelope emits `filter.*` only for caller-supplied inputs. Backend defaults (`limit: 10`, `toVersion: latest`) never round-trip as caller intent. - **`include_bodies` lever.** Default true. `false` drops `body` from every item explicitly; other fields preserved so agents still get the version / date / URL timeline. Measured 5.13× envelope size reduction on a 20-entry `npm:typescript` request (17.3 KB → 3.4 KB). - **`--no-body` + `--verbose` is an error.** Contradictory intents; CLI rejects with an actionable hint rather than silently ignoring `--verbose`. - **MCP `limit` validation in the shared builder, not Zod.** If Zod hard-rejected `limit: 51` at the SDK level, the agent would see a raw SDK error instead of our shared `{error, code, retryable}` envelope. Schema is permissive; builder enforces bounds. - **Registry coverage — all 9 on the wire, no client-side restriction.** `packageChangelog` is source-pull, not registry-query. Live-smoke confirmed npm / pypi / hex / crates / vcpkg / maven / packagist return useful data; nuget returns a generic `BACKEND_ERROR`; zig packages weren't in the backend index. All paths produce the shared envelope correctly. - **Mode mutex enforced client-side.** `--from` / `from_version` + `--limit` / `limit` → `INVALID_ARGUMENT` with actionable hint. - **`metadata` dropped in v1.** Source-specific opaque `GenericJSON`; revisit via agent feedback. - **Shared `promoteGenericVersionNotFound` extended.** Now recognises `fromVersion` / `toVersion` in addition to `version`. `registry` / `packageName` made optional so repo-URL-addressed requests flow through. Preference order `version → fromVersion → toVersion`. P2 / P3 regressions guarded by existing tests + 5 new helper tests. ## Tests - `bun test` — 1062 pass, 0 fail. - `bun run typecheck` / `bun run build` / `bun run lint` — clean. - Request builder, envelope builder, terminal formatter, MCP tool, CLI action, parity fixtures (12) — see docs for full matrix. - Live-smoke against production pkgseer: latest / range / repo-URL / `--no-body` / error paths / registry matrix / body-preview cap / `--verbose` uncap / truncation footer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the
package_vulnerabilitiesMCP tool andgithits pkg vulnsCLI command behind the existingcode_navigationcapability gate. Both surfaces share a single request builder, envelope builder, and error classifier; a parity test assertstoEqualJSON across the two surfaces for every service-sourced fixture.Surface
githits pkg vulns <spec>— CLI listing known CVE / OSV advisories with severity, affected ranges, fix versions, and upgrade paths. Supports@versionin the spec (unlikepkg info), plus--severity low|medium|high|critical(uppercase tolerated),--include-withdrawn,--verbose,--json.package_vulnerabilities— MCP tool with the same lean envelope. Permissive Zod schema, validation in-handler viabuildPackageVulnerabilitiesParams. Advertised in the MCP server-level instructions under the package-tools section, gated onpackageIntelligenceService.The zero-vulns happy path renders as a single-line summary body (
No known vulnerabilities.) to keep agent token usage minimal on the common case.Design choices
--severitymaps to a CVSS float on the wire;--include-withdrawnpasses through. The backend returns a filter-awarevulnerabilityCount, sosummary.totalreflects whatever survived the filter. No client-side filtering, no dual-summary block.bySeveritybuckets. Every returned advisory lands in exactly one ofmalware/critical/high/medium/low/unrated. The client-side guarantee isMALWARE + crit + high + medium + low + unrated = advisories.length, so the header breakdown always reconciles with the total — even on RUSTSEC / PYSEC where unbanded advisories are common.v(e.g.v4.18.0) are rejected client-side withINVALID_ARGUMENTbefore the backend call. Thevprefix is a git-tag convention, not a canonical version on any supported registry; proper ecosystem-aware version parsing belongs in the backend, not in ad hoc CLI normalisation rules.VERSION_NOT_FOUND. When the backend emitsextensions.code = "VERSION_NOT_FOUND"with structured fields, the classifier surfacesdetails: { package, requestedVersion, availableVersions[] }. The CLI terminal error expands this intopackage:/requested:/available:detail lines so users see the mistake and the alternatives. A narrow message-string fallback promotes genericBackendErrors with matching text to the typed error while the backend catches up; it's guarded to only fire when nographqlCodeis present, so real server faults never have their retryability flipped.pkg vulns only supports npm, pypi, hex, and crates. Got: ${registry}.before the backend call.isMalicious: truerender with a red/boldMALWAREcolumn (optionally combined asMALWARE · critwhen both flags are set) and sort to the top of the advisory list regardless of CVSS score. The malware count in the breakdown line is always coloured red.--jsonand MCPcontent[0].text; the terminal formatter is CLI-only (MCP always emits JSON). Documented inmcp-cli-parity.md.Terminal UX refinements
latest affectedis the only yellow token in the summary line;latest cleanstays plain so it doesn't read as a caution.unratedlabel in the severity gutter rather than an empty column.… (+N more; use -v)hint. Verbose shows every range. JSON is never truncated — machine consumers get the full list.No summary availableplaceholders from upstream are stripped from both the JSON envelope and the terminal row — absence ofsummaryis the signal.Tests
bun test— 810 pass, 0 fail, 1784expect()calls.bun run typecheck/bun run build/bun run lint— clean.v-prefixed (rejected client-side), versioned real-diff,NOT_FOUND,VERSION_NOT_FOUNDwith structured details,BACKEND_ERROR, plusINVALID_ARGUMENTfixtures for unsupported registry and invalid severity label.Test plan
bun testpassesbun run typecheckcleanbun run buildcleanbun run lintcleanpkg vulnsscenarios smoked against a token withcode_navigationcapability (happy, zero-vulns, version pin, malware fixture, severity filter, withdrawn toggle, tag-style rejection, scoped-npm name, registry rejection)tools/listshowspackage_vulnerabilitiesunder an open-gate token, absent under a gate-closed tokeninstructionsinclude thepackage_vulnerabilitiesbullet under an open-gate tokenpkg vulns --helprenders under an open-gate token