From 0c34450e9329a2ca62bd4c12ec6c178b83b60535 Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Wed, 29 Apr 2026 10:26:53 +0300 Subject: [PATCH 1/2] chore: prepare 0.3.0 release --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .plugin/plugin.json | 2 +- README.md | 2 +- commands/help.md | 7 ++++--- docs/implementation/cli-commands.md | 6 +++--- docs/implementation/mcp-cli-parity.md | 3 ++- docs/implementation/tools.md | 12 ++++++------ gemini-extension.json | 2 +- package.json | 2 +- plugins/claude/.claude-plugin/plugin.json | 2 +- plugins/claude/commands/help.md | 7 ++++--- src/commands/mcp-instructions.ts | 2 +- src/services/package-intelligence-service.ts | 2 +- src/shared/package-changelog-response.ts | 4 +++- src/tools/package-changelog.ts | 2 +- 16 files changed, 32 insertions(+), 27 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e47e07f8..db4e79ac 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "GitHits plugins for Claude Code - code examples from global open source", - "version": "0.2.3" + "version": "0.3.0" }, "plugins": [ { diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 06441cc7..0623dd16 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "githits", - "version": "0.2.3", + "version": "0.3.0", "description": "Code examples from global open source for developers and AI assistants", "author": { "name": "GitHits" diff --git a/.plugin/plugin.json b/.plugin/plugin.json index 06441cc7..0623dd16 100644 --- a/.plugin/plugin.json +++ b/.plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "githits", - "version": "0.2.3", + "version": "0.3.0", "description": "Code examples from global open source for developers and AI assistants", "author": { "name": "GitHits" diff --git a/README.md b/README.md index 120d5f46..bd9f27dd 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ npx githits init `init` authenticates with your [GitHits](https://githits.com) account, then auto-detects your installed coding tools and configures each one with GitHits MCP. -Supported tools: Claude Code, Cursor, Windsurf, VS Code / Copilot, Cline, Claude Desktop, Codex CLI, Gemini CLI, and Google Antigravity. +Supported tools: Claude Code, Cursor, Windsurf, VS Code / Copilot, Cline, Claude Desktop, Codex CLI, Gemini CLI, Google Antigravity, and OpenCode. If you are using a tool that is not listed above, use the manual MCP setup instructions near the end of this README. diff --git a/commands/help.md b/commands/help.md index 97ec0e3b..7a97b077 100644 --- a/commands/help.md +++ b/commands/help.md @@ -25,7 +25,7 @@ context summary: ## MCP Tools -This plugin connects to the GitHits MCP server and always exposes three core tools: +This plugin connects to the GitHits MCP server and always exposes these core tools: - **get_example** — Find code examples by describing what you need in natural language. Requires `query`; `language` is optional and inferred when omitted. @@ -33,8 +33,9 @@ This plugin connects to the GitHits MCP server and always exposes three core too need to force a specific language. - **feedback** — Rate a search result to improve future quality. -Additional indexed dependency/package tools such as `search`, `pkg_info`, -`code_files`, and `code_grep` are available by default. +Additional indexed dependency/package tools are available by default: +`search`, `search_status`, `docs_list`, `docs_read`, `pkg_info`, `pkg_vulns`, +`pkg_deps`, `pkg_changelog`, `code_files`, `code_read`, and `code_grep`. ## Authentication diff --git a/docs/implementation/cli-commands.md b/docs/implementation/cli-commands.md index 35cf19c8..28ad6965 100644 --- a/docs/implementation/cli-commands.md +++ b/docs/implementation/cli-commands.md @@ -17,7 +17,7 @@ The CLI exposes `example`, `languages`, `feedback`, top-level indexed `search` / | `pkg info ` | package spec | `--verbose`, `--json` | Show a package overview (latest version, downloads, license, vulnerabilities) | | `pkg vulns ` | package spec (optional `@version`) | `--severity`, `--include-withdrawn`, `--verbose`, `--json` | List known vulnerabilities for a package (npm/pypi/hex/crates) | | `pkg deps ` | package spec (optional `@version`) | `--groups`, `--lifecycle`, `--transitive`, `--depth`, `--verbose`, `--json` | Analyse dependencies: direct runtime deps, structured groups, optional transitive graph (npm/pypi/hex/crates/vcpkg/zig) | -| `pkg changelog [spec]` | package spec OR `--repo-url` | `--from`, `--to`, `--limit`, `--git-ref`, `--no-body`, `--verbose`, `--json` | Release notes / changelog entries for a package or GitHub repo (GitHub Releases, CHANGELOG.md, or HexDocs). Default shows each entry with a 10-line body preview; `--verbose` uncaps, `--no-body` drops. | +| `pkg changelog [spec]` | package spec OR `--repo-url` | `--from`, `--to`, `--limit`, `--git-ref`, `--no-body`, `--verbose`, `--json` | Release notes / changelog entries for a package or GitHub repo (GitHub Releases, CHANGELOG.md, HexDocs, or version-spine timeline). Default shows each entry with a 10-line body preview; `--verbose` uncaps, `--no-body` drops. | | `docs list ` | package spec (optional `@version`) | `--limit`, `--after`, `--verbose`, `--json` | List hosted/crawled and repository-backed documentation pages for a package. Entries include page IDs for `docs read`; JSON includes exact repo-file follow-up metadata when available. | | `docs read ` | page ID from `docs list` or search results | `--lines`, `--verbose`, `--json` | Read a documentation page by page ID. Default output is content-only; `--lines` fetches a bounded range for long pages. | | `code files [spec] [path-prefix]` | package spec OR `--repo-url` + `--git-ref`; optional `[path-prefix]` | `--limit`, `--wait`, `--verbose`, `--json` | List files in an indexed dependency. `[path-prefix]` is a literal directory prefix (not a glob). Plain output is one path per line; `--verbose` adds language / type / size annotations. Indexing-retry via `--wait` or the `availableVersions` hint in the error envelope. | @@ -34,7 +34,7 @@ githits init --skip-login # Skip authentication, configure tools only Authenticates with GitHits (via OAuth in the browser), then scans for available coding agents, checks which are already configured, and sets up unconfigured ones with your confirmation. All agents are pre-checked before any setup begins, so the status display is fully resolved. CLI agents are considered available only when their executable is on `PATH`; related dot-directories alone do not count. Config-file agents remain filesystem-detected using their known app/config directories. If already authenticated, the login step is skipped automatically. If login fails, the user is prompted to continue with tool setup anyway. If all detected agents are already configured, exits early with a summary. -Supports Claude Code, Cursor, Windsurf, VS Code / Copilot, Cline, Claude Desktop, Codex CLI, Gemini CLI, and Google Antigravity. Uses plugin install (Claude Code), CLI commands (Codex, Gemini CLI), and atomic config file writes (Cursor, Windsurf, VS Code, Cline, Claude Desktop, Google Antigravity). CLI agents use read-only check commands (e.g., `claude plugin list`) to determine configuration status before prompting. +Supports Claude Code, Cursor, Windsurf, VS Code / Copilot, Cline, Claude Desktop, Codex CLI, Gemini CLI, Google Antigravity, and OpenCode. Uses plugin install (Claude Code), CLI commands (Codex, Gemini CLI), and atomic config file writes (Cursor, Windsurf, VS Code, Cline, Claude Desktop, Google Antigravity, OpenCode). CLI agents use read-only check commands (e.g., `claude plugin list`) to determine configuration status before prompting. The command uses `createContainer()` lazily for the login step. Tool detection and configuration use lightweight dependencies that don't require auth. @@ -215,7 +215,7 @@ githits pkg changelog npm:express --json githits pkg changelog pypi:requests --no-body --json # lean timeline ``` -Fetches release notes or changelog entries for a package or GitHub repository. Output is a newest-first list with a summary header identifying the source (GitHub Releases, CHANGELOG.md, or HexDocs). +Fetches release notes or changelog entries for a package or GitHub repository. Output is a newest-first list with a summary header identifying the source (GitHub Releases, CHANGELOG.md, HexDocs, or version-spine timeline). **Addressing.** `` (`registry:name`, same parser as `pkg info` / `pkg vulns` / `pkg deps`) **or** `--repo-url `, mutually exclusive. Unlike the other `pkg` commands, `pkg changelog` is intrinsically repo-level, so repo-URL addressing is a first-class peer mode. diff --git a/docs/implementation/mcp-cli-parity.md b/docs/implementation/mcp-cli-parity.md index 2d0a224b..56c3e4cd 100644 --- a/docs/implementation/mcp-cli-parity.md +++ b/docs/implementation/mcp-cli-parity.md @@ -256,7 +256,8 @@ When a new tool lands with both MCP and CLI surfaces: - **`source: null` promoted to `NOT_FOUND`.** The service layer promotes the null-source case to a typed `PackageIntelligenceChangelogSourceNotFoundError` with a message - naming the sources tried (GitHub Releases, CHANGELOG.md, HexDocs). + naming the sources tried (GitHub Releases, CHANGELOG.md, HexDocs, + version-spine timeline). - **`--verbose` / `--no-body` / `--json` interaction.** Default terminal output truncates each entry's body at 10 lines. `--verbose` lifts the cap (terminal-only). `--no-body` mirrors diff --git a/docs/implementation/tools.md b/docs/implementation/tools.md index e3375fe0..27c31e6b 100644 --- a/docs/implementation/tools.md +++ b/docs/implementation/tools.md @@ -8,10 +8,10 @@ The CLI exposes MCP tools that mirror the backend's MCP server. This document ex GitHits has two MCP server implementations: -- **Backend** (`githits-backend`) — Python/FastMCP, runs as a hosted service at `mcp.githits.com`. Surfaces only the example-search workflow (`get_example`, `search_language`, `feedback`). -- **CLI** (`githits-cli`) — TypeScript/MCP SDK, runs locally via `githits mcp start`. Surfaces the full set, including unified `search`, package intelligence (`pkg_*`), and code navigation (`code_*`). +- **Backend** (`githits-backend` / PkgSeer MCP) — Python/FastMCP, runs as hosted MCP services. Production exposes both the core example-search workflow (`get_example`, `search_language`, `feedback`) and indexed package/source tooling. +- **CLI** (`githits-cli`) — TypeScript/MCP SDK, runs locally via `githits mcp start`. Surfaces the same public tool families, including unified `search`, package intelligence (`pkg_*`), docs (`docs_*`), and code navigation (`code_*`). -The two surfaces overlap on the example-search workflow only; for the overlapping tools the parameters and descriptions stay aligned. Tools that exist only on the CLI surface (code navigation, package intelligence, unified `search`) are not constrained by backend parity. +The CLI mirrors the production MCP tool contract where equivalent tools exist. Core example-search tool descriptions are kept aligned with GitHits backend wording; indexed package/source tool descriptions are kept aligned with the PkgSeer/GitHits indexed-service contract. ## Current Tools @@ -27,7 +27,7 @@ The two surfaces overlap on the example-search workflow only; for the overlappin | `pkg_info` | `registry`, `package_name` | Package overview: latest version, license, description, repository, downloads, GitHub metadata, install command, and known vulnerabilities. Always returns the latest published version. | | `pkg_vulns` | `registry`, `package_name`, `version?`, `min_severity?`, `include_withdrawn?` | Known vulnerabilities for a package on npm, PyPI, Hex, or Crates. Count summary, per-advisory OSV ID + severity + affected/fix ranges, and upgrade paths. Malware is surfaced in a disjoint bucket. | | `pkg_deps` | `registry`, `package_name`, `version?`, `lifecycle?`, `include_transitive?`, `include_importers?`, `max_depth?` | Direct runtime dependency list (each `{name, version, constraint}` — the backend resolves each constraint to a concrete version) plus, when the backend has them, structured groups for dev / peer / build / optional with registry-specific condition metadata (PyPI extras, Crates features). Optional transitive block with aggregate edge counts, the preprocessed install footprint as `{name, version}`, typed conflicts and circular-dependency cycles; opt into per-package importer provenance with `include_importers`. | -| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `include_bodies?` | Release notes or changelog entries for a package or GitHub repo. Default latest mode returns the ten most recent entries; `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) mutually exclusive. Response always includes `source` (`"releases"` / `"changelog_file"` / `"hexdocs"`), `mode` (`"latest"` / `"range"`), and `entries: { count, items }` with full markdown bodies by default; set `include_bodies: false` for a lean version / date / URL timeline. | +| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `include_bodies?` | Release notes or changelog entries for a package or GitHub repo. Default latest mode returns the ten most recent entries; `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) mutually exclusive. Response always includes `source` (`"releases"` / `"changelog_file"` / `"hexdocs"` / `"version_spine"`), `mode` (`"latest"` / `"range"`), and `entries: { count, items }` with full markdown bodies by default; set `include_bodies: false` for a lean version / date / URL timeline. | | `code_files` | `target`, `path_prefix?`, `limit?`, `wait_timeout_ms?`, `format?` | List files in an indexed dependency. Returns `{total, hasMore, files: [{path, name, language, fileType, byteSize}], resolution, indexedVersion}` in JSON mode. Dual addressing via `target.registry + target.package_name` (spec) or `target.repo_url + target.git_ref` (repo). `path_prefix` is a literal directory prefix — NOT a glob (`*.ts` won't match); glob / pattern filtering is an upstream enhancement. Emits an `INDEXING` error envelope when the dependency is being indexed on-demand — retry with a longer `wait_timeout_ms` or pick a version from `details.availableVersions`. `format` defaults to `text-v1` (paths-only listing); pass `format: "json"` for the structured envelope. | | `code_read` | `target`, `path`, `start_line?`, `end_line?`, `wait_timeout_ms?` | Read a file from an indexed dependency. **MCP per-call span cap: 150 lines** — broader requests (or no range) are silently truncated to the first 150 lines from the caller's start, with a `hint` field explaining the cap and the original request. Use `start_line` / `end_line` to pick a focused window — typical 80-150 lines around a known symbol from `search` / `code_grep`. Binary files set `isBinary: true` and omit `content`. On `NOT_FOUND` / `FILE_NOT_FOUND` call `code_files` to discover the actual path. The cap is MCP-only; the CLI command `githits code read` honors arbitrary ranges. | | `code_grep` | `target`, `pattern`, `path?`, `path_prefix?`, `globs?`, `extensions?`, `pattern_type?`, `case_sensitive?`, `exclude_doc_files?`, `exclude_test_files?`, `context_lines?`, `context_lines_before?`, `context_lines_after?`, `max_matches?`, `max_matches_per_file?`, `cursor?`, `symbol_fields?`, `wait_timeout_ms?`, `format?` | Deterministic text grep over indexed dependency or repository source. Defaults to literal, ASCII case-insensitive matching across the whole target; non-ASCII letters match case-sensitively. Narrow with `path`, `path_prefix`, `globs`, or `extensions`. `pattern_type: "regex"` uses RE2 syntax; whole-target regexes must include at least one literal substring for index pre-filtering. Returns matches plus pagination and scan counters; `symbol_fields` hydrates enclosing symbol metadata on each match. `format` defaults to `text-v1` (matches grouped by file, grep -A/-B notation for context); pass `format: "json"` for the structured envelope. | @@ -90,11 +90,11 @@ The two surfaces overlap on the example-search workflow only; for the overlappin ### `pkg_changelog` response shape -**Data-first envelope.** The top level carries addressing (`registry` + `name` for spec addressing, or `repoUrl` for repo-URL addressing), `source` (`"releases"` / `"changelog_file"` / `"hexdocs"`), and `mode` (`"latest"` or `"range"`). Entries live under `entries: { count, items }` — matching the `{count, items}` shape used by `pkg_deps.runtime`. `count` is computed client-side from `items.length`, so the invariant holds regardless of backend drift. +**Data-first envelope.** The top level carries addressing (`registry` + `name` for spec addressing, or `repoUrl` for repo-URL addressing), `source` (`"releases"` / `"changelog_file"` / `"hexdocs"` / `"version_spine"`), and `mode` (`"latest"` or `"range"`). Entries live under `entries: { count, items }` — matching the `{count, items}` shape used by `pkg_deps.runtime`. `count` is computed client-side from `items.length`, so the invariant holds regardless of backend drift. **Per-entry shape.** `{version, normalizedVersion?, publishedAt?, htmlUrl?, body?}`. `version` is kept in the envelope even when `null` so agents can write `items.map(e => e.version)` without guarding; every other nullable field is stripped when absent. `body` is additionally stripped when the caller set `include_bodies: false`. The backend's opaque per-entry `metadata` GenericJSON is deliberately dropped from the envelope in v1 — revisit via agent feedback. -**Dual addressing (`registry` + `package_name` XOR `repo_url`).** `pkg_changelog` is the only metadata-side MCP tool with dual addressing. `pkg_info` / `pkg_vulns` / `pkg_deps` all accept only `registry` + `package_name` because they are registry-metadata lookups without repo-URL alternatives. `pkg_changelog` is intrinsically repo-level — its sources are GitHub Releases, CHANGELOG.md, and HexDocs — so `repoUrl` is a peer addressing mode, not a bolt-on. Future tool authors should not cargo-cult the asymmetry without reading this rationale. +**Dual addressing (`registry` + `package_name` XOR `repo_url`).** `pkg_changelog` is the only metadata-side MCP tool with dual addressing. `pkg_info` / `pkg_vulns` / `pkg_deps` all accept only `registry` + `package_name` because they are registry-metadata lookups without repo-URL alternatives. `pkg_changelog` is intrinsically repo-level — its sources are GitHub Releases, CHANGELOG.md, HexDocs, and package version-spine timelines — so `repoUrl` is a peer addressing mode, not a bolt-on. Future tool authors should not cargo-cult the asymmetry without reading this rationale. **Mode selection.** `from_version` triggers range mode (returns every entry in `[fromVersion, toVersion]` with no cap). Latest mode is the default, capped by `limit` (1–50, backend default 10). `from_version` + `limit` is rejected client-side with `INVALID_ARGUMENT` rather than silently routed to one mode. diff --git a/gemini-extension.json b/gemini-extension.json index 3646628a..e271a3ab 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "githits", - "version": "0.2.3", + "version": "0.3.0", "description": "Code examples from global open source for developers and AI assistants.", "mcpServers": { "githits": { diff --git a/package.json b/package.json index d3d2e455..7e0136ac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "githits", "description": "CLI companion for GitHits - code examples from global open source for developers and AI assistants", - "version": "0.2.3", + "version": "0.3.0", "type": "module", "files": [ "dist", diff --git a/plugins/claude/.claude-plugin/plugin.json b/plugins/claude/.claude-plugin/plugin.json index 06441cc7..0623dd16 100644 --- a/plugins/claude/.claude-plugin/plugin.json +++ b/plugins/claude/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "githits", - "version": "0.2.3", + "version": "0.3.0", "description": "Code examples from global open source for developers and AI assistants", "author": { "name": "GitHits" diff --git a/plugins/claude/commands/help.md b/plugins/claude/commands/help.md index 97ec0e3b..7a97b077 100644 --- a/plugins/claude/commands/help.md +++ b/plugins/claude/commands/help.md @@ -25,7 +25,7 @@ context summary: ## MCP Tools -This plugin connects to the GitHits MCP server and always exposes three core tools: +This plugin connects to the GitHits MCP server and always exposes these core tools: - **get_example** — Find code examples by describing what you need in natural language. Requires `query`; `language` is optional and inferred when omitted. @@ -33,8 +33,9 @@ This plugin connects to the GitHits MCP server and always exposes three core too need to force a specific language. - **feedback** — Rate a search result to improve future quality. -Additional indexed dependency/package tools such as `search`, `pkg_info`, -`code_files`, and `code_grep` are available by default. +Additional indexed dependency/package tools are available by default: +`search`, `search_status`, `docs_list`, `docs_read`, `pkg_info`, `pkg_vulns`, +`pkg_deps`, `pkg_changelog`, `code_files`, `code_read`, and `code_grep`. ## Authentication diff --git a/src/commands/mcp-instructions.ts b/src/commands/mcp-instructions.ts index 30a6471e..700f310c 100644 --- a/src/commands/mcp-instructions.ts +++ b/src/commands/mcp-instructions.ts @@ -70,7 +70,7 @@ const MULTI_TURN_TIP = * Mirrors `getMcpToolDefinitions` so the instructions stay aligned * with the registered tool surface. */ -export function buildMcpInstructions(deps: Dependencies): string { +export function buildMcpInstructions(_deps: Dependencies): string { const sections = [CORE_BLOCK]; const bullets: string[] = []; diff --git a/src/services/package-intelligence-service.ts b/src/services/package-intelligence-service.ts index 30c5a3b9..5687a354 100644 --- a/src/services/package-intelligence-service.ts +++ b/src/services/package-intelligence-service.ts @@ -358,7 +358,7 @@ export interface ChangelogEntryDetail { export interface ChangelogReport { /** Echo of addressing + filter as the backend saw it. */ package?: ChangelogPackageInfo; - /** `"releases"` | `"changelog_file"` | `"hexdocs"` when resolved; null otherwise. */ + /** `"releases"` | `"changelog_file"` | `"hexdocs"` | `"version_spine"` when resolved; null otherwise. */ source?: string; /** Entries, newest-first. Empty array = resolved source but nothing in range. */ entries: ChangelogEntryDetail[]; diff --git a/src/shared/package-changelog-response.ts b/src/shared/package-changelog-response.ts index 6d97805c..5a5378bb 100644 --- a/src/shared/package-changelog-response.ts +++ b/src/shared/package-changelog-response.ts @@ -88,7 +88,7 @@ export interface LeanChangelogEnvelope { name?: string; /** Present for repo-URL addressing. */ repoUrl?: string; - /** `"releases"` | `"changelog_file"` | `"hexdocs"`. Never null here + /** `"releases"` | `"changelog_file"` | `"hexdocs"` | `"version_spine"`. Never null here * (null is promoted to NOT_FOUND at the service boundary). */ source: string; /** Derived from request params. */ @@ -332,6 +332,8 @@ function humanizeSource(source: string): string { return "CHANGELOG.md"; case "hexdocs": return "HexDocs"; + case "version_spine": + return "Version spine"; default: return source; } diff --git a/src/tools/package-changelog.ts b/src/tools/package-changelog.ts index ee71df10..9f848e10 100644 --- a/src/tools/package-changelog.ts +++ b/src/tools/package-changelog.ts @@ -90,7 +90,7 @@ const DESCRIPTION = "`[from_version, to_version]` range (range mode, no count cap). " + "Address via `registry` + `package_name` or `repo_url` (mutually " + 'exclusive). Response: `source` (`"releases"` / `"changelog_file"` ' + - '/ `"hexdocs"`), `mode` (`"latest"` or `"range"`), ' + + '/ `"hexdocs"` / `"version_spine"`), `mode` (`"latest"` or `"range"`), ' + "`entries: { count, items }` with full markdown bodies. Set " + "`include_bodies: false` for a version / date / URL timeline only. " + "Supports npm, PyPI, Hex, Crates, vcpkg, Zig, NuGet, Maven, " + From 2d5e5d28870035ed8b5628a360893661faa1a72b Mon Sep 17 00:00:00 2001 From: Juha Litola Date: Wed, 29 Apr 2026 10:57:40 +0300 Subject: [PATCH 2/2] fix: align release smoke contracts --- docs/implementation/cli-commands.md | 8 ++--- docs/implementation/mcp-cli-parity.md | 7 ++--- docs/implementation/tools.md | 10 +++---- src/commands/search.test.ts | 24 +++++++++++++++ src/commands/search.ts | 2 +- .../package-intelligence-service.test.ts | 30 +++++++++++++++++++ src/services/package-intelligence-service.ts | 13 ++++---- src/services/test-helpers.ts | 2 +- src/shared/package-changelog-response.ts | 8 ++--- src/shared/unified-search-request.test.ts | 2 +- src/shared/unified-search-request.ts | 4 ++- src/shared/unified-search-response.test.ts | 4 +-- src/shared/unified-search-response.ts | 3 +- src/tools/package-changelog.ts | 2 +- src/tools/search.test.ts | 1 + src/tools/search.ts | 8 ++++- 16 files changed, 94 insertions(+), 34 deletions(-) diff --git a/docs/implementation/cli-commands.md b/docs/implementation/cli-commands.md index 28ad6965..36618e25 100644 --- a/docs/implementation/cli-commands.md +++ b/docs/implementation/cli-commands.md @@ -10,14 +10,14 @@ The CLI exposes `example`, `languages`, `feedback`, top-level indexed `search` / |---|---|---|---| | `init` | — | `-y, --yes`, `--skip-login` | Authenticate and set up MCP server for coding agents | | `example ` | `` | `-l, --lang `, `--license `, `--explain`, `--json` | Search for code examples | -| `search ` | `--in ` | `--source `, `--kind `, `--category `, `--path-prefix `, `--intent `, `--public`, `--name `, `--lang `, `--allow-partial`, `--limit `, `--offset `, `--wait `, `--json` | Unified indexed search across dependency/repository code, docs, and symbols | +| `search ` | `--in ` | `--source `, `--kind `, `--category `, `--path-prefix `, `--intent `, `--public`, `--name `, `--lang `, `--allow-partial`, `--limit `, `--offset `, `--wait `, `--json` | Unified indexed search across dependency/repository code, docs, and symbols. Defaults to 10 results. | | `search-status ` | `` | `--json` | Check progress, fetch partial hits, or fetch final results for a prior unified search | | `languages [query]` | — | `--json` | List or filter supported languages | | `feedback ` | `--accept` or `--reject` | `-m, --message `, `--json` | Submit feedback on a search result | | `pkg info ` | package spec | `--verbose`, `--json` | Show a package overview (latest version, downloads, license, vulnerabilities) | | `pkg vulns ` | package spec (optional `@version`) | `--severity`, `--include-withdrawn`, `--verbose`, `--json` | List known vulnerabilities for a package (npm/pypi/hex/crates) | | `pkg deps ` | package spec (optional `@version`) | `--groups`, `--lifecycle`, `--transitive`, `--depth`, `--verbose`, `--json` | Analyse dependencies: direct runtime deps, structured groups, optional transitive graph (npm/pypi/hex/crates/vcpkg/zig) | -| `pkg changelog [spec]` | package spec OR `--repo-url` | `--from`, `--to`, `--limit`, `--git-ref`, `--no-body`, `--verbose`, `--json` | Release notes / changelog entries for a package or GitHub repo (GitHub Releases, CHANGELOG.md, HexDocs, or version-spine timeline). Default shows each entry with a 10-line body preview; `--verbose` uncaps, `--no-body` drops. | +| `pkg changelog [spec]` | package spec OR `--repo-url` | `--from`, `--to`, `--limit`, `--git-ref`, `--no-body`, `--verbose`, `--json` | Release notes / changelog entries for a package or GitHub repo (GitHub Releases, CHANGELOG.md, or HexDocs). Default shows each entry with a 10-line body preview; `--verbose` uncaps, `--no-body` drops. | | `docs list ` | package spec (optional `@version`) | `--limit`, `--after`, `--verbose`, `--json` | List hosted/crawled and repository-backed documentation pages for a package. Entries include page IDs for `docs read`; JSON includes exact repo-file follow-up metadata when available. | | `docs read ` | page ID from `docs list` or search results | `--lines`, `--verbose`, `--json` | Read a documentation page by page ID. Default output is content-only; `--lines` fetches a bounded range for long pages. | | `code files [spec] [path-prefix]` | package spec OR `--repo-url` + `--git-ref`; optional `[path-prefix]` | `--limit`, `--wait`, `--verbose`, `--json` | List files in an indexed dependency. `[path-prefix]` is a literal directory prefix (not a glob). Plain output is one path per line; `--verbose` adds language / type / size annotations. Indexing-retry via `--wait` or the `availableVersions` hint in the error envelope. | @@ -72,7 +72,7 @@ Unified search spans indexed dependency and repository code, docs, and explicit **Intent filter.** When `--intent` is omitted, unified search sends no file-intent filter. Pass `--intent production` or another specific intent only when you want to narrow the result set. Some sources can still ignore `fileIntent`; when they do, the JSON `sourceStatus` block and terminal notes report that explicitly. -**Complete-by-default results.** The CLI sends `allowPartialResults: false` unless `--allow-partial` is passed. If indexing does not complete within the wait window, the default behavior returns a `searchRef` and progress summary instead of partial hits. With `--allow-partial`, available hits are included while remaining sources continue indexing. `--wait` is in seconds (0-60, default 20). +**Complete-by-default results.** The CLI sends `allowPartialResults: false` unless `--allow-partial` is passed. If indexing does not complete within the wait window, the default behavior returns a `searchRef` and progress summary instead of partial hits. With `--allow-partial`, available hits are included while remaining sources continue indexing. `--limit` defaults to 10 results. `--wait` is in seconds (0-60, default 20). The original unified-search plan envisaged hiding partial mode entirely in v1 to make results trustworthy by default. We kept the flag exposed because some agent and CLI flows benefit from "show me what you have so far"; the trust contract is preserved by keeping the *default* false. Users and agents must explicitly opt in, so a vanilla `search` call still cannot return incomplete results by surprise. @@ -215,7 +215,7 @@ githits pkg changelog npm:express --json githits pkg changelog pypi:requests --no-body --json # lean timeline ``` -Fetches release notes or changelog entries for a package or GitHub repository. Output is a newest-first list with a summary header identifying the source (GitHub Releases, CHANGELOG.md, HexDocs, or version-spine timeline). +Fetches release notes or changelog entries for a package or GitHub repository. Output is a newest-first list with a summary header identifying the source (GitHub Releases, CHANGELOG.md, or HexDocs). **Addressing.** `` (`registry:name`, same parser as `pkg info` / `pkg vulns` / `pkg deps`) **or** `--repo-url `, mutually exclusive. Unlike the other `pkg` commands, `pkg changelog` is intrinsically repo-level, so repo-URL addressing is a first-class peer mode. diff --git a/docs/implementation/mcp-cli-parity.md b/docs/implementation/mcp-cli-parity.md index 56c3e4cd..88932561 100644 --- a/docs/implementation/mcp-cli-parity.md +++ b/docs/implementation/mcp-cli-parity.md @@ -253,11 +253,10 @@ When a new tool lands with both MCP and CLI surfaces: values never round-trip as caller intent. - **`entries: { count, items }` shape.** Mirrors `runtime: {count, items}` from `pkg_deps`. -- **`source: null` promoted to `NOT_FOUND`.** The service layer - promotes the null-source case to a typed +- **Missing source promoted to `NOT_FOUND`.** The service layer + promotes null or empty `source` to a typed `PackageIntelligenceChangelogSourceNotFoundError` with a message - naming the sources tried (GitHub Releases, CHANGELOG.md, HexDocs, - version-spine timeline). + naming the sources tried (GitHub Releases, CHANGELOG.md, HexDocs). - **`--verbose` / `--no-body` / `--json` interaction.** Default terminal output truncates each entry's body at 10 lines. `--verbose` lifts the cap (terminal-only). `--no-body` mirrors diff --git a/docs/implementation/tools.md b/docs/implementation/tools.md index 27c31e6b..8b7c297f 100644 --- a/docs/implementation/tools.md +++ b/docs/implementation/tools.md @@ -20,14 +20,14 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C | `get_example` | `query`, `language?`, `license_mode?` | Search for canonical code examples. Returns markdown-formatted results. If `language` is omitted, the backend infers it from the query. | | `search_language` | `query` | Find supported programming language names before searching. | | `feedback` | `solution_id`, `accepted`, `feedback_text?` | Submit feedback on a search result to improve quality. | -| `search` | `query`, `target?`, `targets?`, `sources?`, `category?`, `kind?`, `path_prefix?`, `file_intent?`, `public_only?`, `name?`, `language?`, `allow_partial_results?`, `limit?`, `offset?`, `wait_timeout_ms?`, `format?` | Unified indexed dependency/repository discovery search across code, docs, and symbols. Omit `file_intent` to search across all intents; set it only when you want to narrow results, and note that some sources may ignore it and report that in `sourceStatus`. Prefer `sources:["symbol"]` for symbol-shaped unified search. Complete-by-default; set `allow_partial_results: true` to receive available partial hits while indexing continues. `format` defaults to `text-v1` for compact agent output; pass `format: "json"` for the structured envelope. | +| `search` | `query`, `target?`, `targets?`, `sources?`, `category?`, `kind?`, `path_prefix?`, `file_intent?`, `public_only?`, `name?`, `language?`, `allow_partial_results?`, `limit?`, `offset?`, `wait_timeout_ms?`, `format?` | Unified indexed dependency/repository discovery search across code, docs, and symbols. Omit `file_intent` to search across all intents; set it only when you want to narrow results, and note that some sources may ignore it and report that in `sourceStatus`. Prefer `sources:["symbol"]` for symbol-shaped unified search. Complete-by-default; `limit` defaults to 10 to keep agent output compact. Set `allow_partial_results: true` to receive available partial hits while indexing continues. `format` defaults to `text-v1` for compact agent output; pass `format: "json"` for the structured envelope. | | `search_status` | `search_ref` | Check progress, fetch partial hits when the original request used `allow_partial_results: true`, or fetch final results for a prior unified search. | | `docs_list` | `registry`, `package_name`, `version?`, `limit?`, `after?` | List hosted/crawled and repository-backed documentation pages for a package. Entries include stable page IDs for `docs_read`; repo-backed entries include exact source metadata for `code_read` follow-up. | | `docs_read` | `page_id`, `start_line?`, `end_line?` | Read a documentation page by page ID. Supports bounded line ranges for long pages; repo-backed pages include exact file follow-up metadata. | | `pkg_info` | `registry`, `package_name` | Package overview: latest version, license, description, repository, downloads, GitHub metadata, install command, and known vulnerabilities. Always returns the latest published version. | | `pkg_vulns` | `registry`, `package_name`, `version?`, `min_severity?`, `include_withdrawn?` | Known vulnerabilities for a package on npm, PyPI, Hex, or Crates. Count summary, per-advisory OSV ID + severity + affected/fix ranges, and upgrade paths. Malware is surfaced in a disjoint bucket. | | `pkg_deps` | `registry`, `package_name`, `version?`, `lifecycle?`, `include_transitive?`, `include_importers?`, `max_depth?` | Direct runtime dependency list (each `{name, version, constraint}` — the backend resolves each constraint to a concrete version) plus, when the backend has them, structured groups for dev / peer / build / optional with registry-specific condition metadata (PyPI extras, Crates features). Optional transitive block with aggregate edge counts, the preprocessed install footprint as `{name, version}`, typed conflicts and circular-dependency cycles; opt into per-package importer provenance with `include_importers`. | -| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `include_bodies?` | Release notes or changelog entries for a package or GitHub repo. Default latest mode returns the ten most recent entries; `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) mutually exclusive. Response always includes `source` (`"releases"` / `"changelog_file"` / `"hexdocs"` / `"version_spine"`), `mode` (`"latest"` / `"range"`), and `entries: { count, items }` with full markdown bodies by default; set `include_bodies: false` for a lean version / date / URL timeline. | +| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `include_bodies?` | Release notes or changelog entries for a package or GitHub repo. Default latest mode returns the ten most recent entries; `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) mutually exclusive. Response always includes `source` (`"releases"` / `"changelog_file"` / `"hexdocs"`), `mode` (`"latest"` / `"range"`), and `entries: { count, items }` with full markdown bodies by default; set `include_bodies: false` for a lean version / date / URL timeline. | | `code_files` | `target`, `path_prefix?`, `limit?`, `wait_timeout_ms?`, `format?` | List files in an indexed dependency. Returns `{total, hasMore, files: [{path, name, language, fileType, byteSize}], resolution, indexedVersion}` in JSON mode. Dual addressing via `target.registry + target.package_name` (spec) or `target.repo_url + target.git_ref` (repo). `path_prefix` is a literal directory prefix — NOT a glob (`*.ts` won't match); glob / pattern filtering is an upstream enhancement. Emits an `INDEXING` error envelope when the dependency is being indexed on-demand — retry with a longer `wait_timeout_ms` or pick a version from `details.availableVersions`. `format` defaults to `text-v1` (paths-only listing); pass `format: "json"` for the structured envelope. | | `code_read` | `target`, `path`, `start_line?`, `end_line?`, `wait_timeout_ms?` | Read a file from an indexed dependency. **MCP per-call span cap: 150 lines** — broader requests (or no range) are silently truncated to the first 150 lines from the caller's start, with a `hint` field explaining the cap and the original request. Use `start_line` / `end_line` to pick a focused window — typical 80-150 lines around a known symbol from `search` / `code_grep`. Binary files set `isBinary: true` and omit `content`. On `NOT_FOUND` / `FILE_NOT_FOUND` call `code_files` to discover the actual path. The cap is MCP-only; the CLI command `githits code read` honors arbitrary ranges. | | `code_grep` | `target`, `pattern`, `path?`, `path_prefix?`, `globs?`, `extensions?`, `pattern_type?`, `case_sensitive?`, `exclude_doc_files?`, `exclude_test_files?`, `context_lines?`, `context_lines_before?`, `context_lines_after?`, `max_matches?`, `max_matches_per_file?`, `cursor?`, `symbol_fields?`, `wait_timeout_ms?`, `format?` | Deterministic text grep over indexed dependency or repository source. Defaults to literal, ASCII case-insensitive matching across the whole target; non-ASCII letters match case-sensitively. Narrow with `path`, `path_prefix`, `globs`, or `extensions`. `pattern_type: "regex"` uses RE2 syntax; whole-target regexes must include at least one literal substring for index pre-filtering. Returns matches plus pagination and scan counters; `symbol_fields` hydrates enclosing symbol metadata on each match. `format` defaults to `text-v1` (matches grouped by file, grep -A/-B notation for context); pass `format: "json"` for the structured envelope. | @@ -90,11 +90,11 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C ### `pkg_changelog` response shape -**Data-first envelope.** The top level carries addressing (`registry` + `name` for spec addressing, or `repoUrl` for repo-URL addressing), `source` (`"releases"` / `"changelog_file"` / `"hexdocs"` / `"version_spine"`), and `mode` (`"latest"` or `"range"`). Entries live under `entries: { count, items }` — matching the `{count, items}` shape used by `pkg_deps.runtime`. `count` is computed client-side from `items.length`, so the invariant holds regardless of backend drift. +**Data-first envelope.** The top level carries addressing (`registry` + `name` for spec addressing, or `repoUrl` for repo-URL addressing), `source` (`"releases"` / `"changelog_file"` / `"hexdocs"`), and `mode` (`"latest"` or `"range"`). Entries live under `entries: { count, items }` — matching the `{count, items}` shape used by `pkg_deps.runtime`. `count` is computed client-side from `items.length`, so the invariant holds regardless of backend drift. **Per-entry shape.** `{version, normalizedVersion?, publishedAt?, htmlUrl?, body?}`. `version` is kept in the envelope even when `null` so agents can write `items.map(e => e.version)` without guarding; every other nullable field is stripped when absent. `body` is additionally stripped when the caller set `include_bodies: false`. The backend's opaque per-entry `metadata` GenericJSON is deliberately dropped from the envelope in v1 — revisit via agent feedback. -**Dual addressing (`registry` + `package_name` XOR `repo_url`).** `pkg_changelog` is the only metadata-side MCP tool with dual addressing. `pkg_info` / `pkg_vulns` / `pkg_deps` all accept only `registry` + `package_name` because they are registry-metadata lookups without repo-URL alternatives. `pkg_changelog` is intrinsically repo-level — its sources are GitHub Releases, CHANGELOG.md, HexDocs, and package version-spine timelines — so `repoUrl` is a peer addressing mode, not a bolt-on. Future tool authors should not cargo-cult the asymmetry without reading this rationale. +**Dual addressing (`registry` + `package_name` XOR `repo_url`).** `pkg_changelog` is the only metadata-side MCP tool with dual addressing. `pkg_info` / `pkg_vulns` / `pkg_deps` all accept only `registry` + `package_name` because they are registry-metadata lookups without repo-URL alternatives. `pkg_changelog` is intrinsically repo-level — its sources are GitHub Releases, CHANGELOG.md, and HexDocs — so `repoUrl` is a peer addressing mode, not a bolt-on. Future tool authors should not cargo-cult the asymmetry without reading this rationale. **Mode selection.** `from_version` triggers range mode (returns every entry in `[fromVersion, toVersion]` with no cap). Latest mode is the default, capped by `limit` (1–50, backend default 10). `from_version` + `limit` is rejected client-side with `INVALID_ARGUMENT` rather than silently routed to one mode. @@ -104,7 +104,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C **Version validation.** Same rule as `pkg_vulns` / `pkg_deps`: tag-style `v`-prefixed inputs on `from_version` / `to_version` are rejected client-side with `INVALID_ARGUMENT`. `@` is also rejected — the `pkg changelog` family has no single-version query, and silently remapping to `to_version` would be a client-invented semantic shift. Hint text redirects callers to `--to` / `to_version`. -**NOT_FOUND semantics.** Backend `source === null` is promoted to a typed `PackageIntelligenceChangelogSourceNotFoundError` at the service boundary, which the shared classifier routes to the `NOT_FOUND` envelope with a message naming the sources that were tried. Empty `entries.items: []` with a valid `source` is a success, not an error — "no entries in this range" is a legitimate neutral outcome. +**NOT_FOUND semantics.** Backend `source === null` or `source === ""` is promoted to a typed `PackageIntelligenceChangelogSourceNotFoundError` at the service boundary, which the shared classifier routes to the `NOT_FOUND` envelope with a message naming the sources that were tried. Empty `entries.items: []` with a valid `source` is a success, not an error — "no entries in this range" is a legitimate neutral outcome. **Overlap with `pkg_info`.** `pkg_info` already surfaces a short-form `recentChanges` block (from the backend's `latestChangelogs` field on `PackageSummaryResult`). For a quick "what shipped recently" glance embedded in a package overview, use `pkg_info`. For the full range-capable, body-rich, `include_bodies`-toggleable changelog with `--no-body` timeline mode and repo-URL addressing, use `pkg_changelog`. diff --git a/src/commands/search.test.ts b/src/commands/search.test.ts index efd45e0a..ebbab108 100644 --- a/src/commands/search.test.ts +++ b/src/commands/search.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, mock, spyOn } from "bun:test"; import type { UnifiedSearchIncomplete, UnifiedSearchOutcome, + UnifiedSearchParams, UnifiedSearchProgress, UnifiedSearchSessionStatus, } from "../services/code-navigation-service.js"; @@ -108,6 +109,29 @@ describe("searchAction", () => { consoleSpy.mockRestore(); }); + it("uses the shared search default limit and preserves explicit limits", async () => { + const search = mock((_: UnifiedSearchParams) => + Promise.resolve(defaultUnifiedSearchOutcome), + ); + const deps = createDeps({ + codeNavigationService: createMockCodeNavigationService({ search }), + }); + const consoleSpy = spyOn(console, "log").mockImplementation(() => {}); + + await searchAction("router middleware", { in: ["npm:express"] }, deps); + expect(search.mock.calls[0]?.[0]?.limit).toBe(10); + + search.mockClear(); + await searchAction( + "router middleware", + { in: ["npm:express"], limit: "25" }, + deps, + ); + expect(search.mock.calls[0]?.[0]?.limit).toBe(25); + + consoleSpy.mockRestore(); + }); + it("does not send a file-intent filter unless the caller explicitly set one", async () => { const search = mock< ( diff --git a/src/commands/search.ts b/src/commands/search.ts index 4792c0c3..c9812abd 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -225,7 +225,7 @@ export function registerSearchCommand(program: Command) { "--allow-partial", "Include hits already available while indexing continues; a searchRef is still returned so search-status can fetch the rest", ) - .option("--limit ", "Max results (1-100)") + .option("--limit ", "Max results (1-100, default: 10)") .option("--offset ", "Result offset") .option( "--wait ", diff --git a/src/services/package-intelligence-service.test.ts b/src/services/package-intelligence-service.test.ts index 618c711e..4f28f2c6 100644 --- a/src/services/package-intelligence-service.test.ts +++ b/src/services/package-intelligence-service.test.ts @@ -5,6 +5,7 @@ import { MalformedPackageIntelligenceResponseError, PackageIntelligenceAccessError, PackageIntelligenceBackendError, + PackageIntelligenceChangelogSourceNotFoundError, PackageIntelligenceFeatureFlagRequiredError, PackageIntelligenceNetworkError, PackageIntelligenceServiceImpl, @@ -1291,6 +1292,35 @@ describe("PackageIntelligenceServiceImpl.packageVulnerabilities", () => { }); }); +describe("PackageIntelligenceServiceImpl — packageChangelog", () => { + const ENDPOINT = "https://pkgseer.dev"; + + it("treats an empty source as no changelog data", async () => { + const fetchFn = mock(() => + Promise.resolve( + jsonResponse({ + data: { + packageChangelog: { + package: { name: "express", registry: "NPM" }, + source: "", + entries: [], + }, + }, + }), + ), + ); + const service = new PackageIntelligenceServiceImpl( + ENDPOINT, + createMockTokenProvider(), + asFetchFn(fetchFn), + ); + + await expect( + service.packageChangelog({ registry: "NPM", packageName: "express" }), + ).rejects.toBeInstanceOf(PackageIntelligenceChangelogSourceNotFoundError); + }); +}); + describe("PackageIntelligenceServiceImpl — packageDependencies", () => { const ENDPOINT = "https://pkgseer.dev"; diff --git a/src/services/package-intelligence-service.ts b/src/services/package-intelligence-service.ts index 5687a354..827cc807 100644 --- a/src/services/package-intelligence-service.ts +++ b/src/services/package-intelligence-service.ts @@ -358,7 +358,7 @@ export interface ChangelogEntryDetail { export interface ChangelogReport { /** Echo of addressing + filter as the backend saw it. */ package?: ChangelogPackageInfo; - /** `"releases"` | `"changelog_file"` | `"hexdocs"` | `"version_spine"` when resolved; null otherwise. */ + /** `"releases"` | `"changelog_file"` | `"hexdocs"` when resolved; null/empty otherwise. */ source?: string; /** Entries, newest-first. Empty array = resolved source but nothing in range. */ entries: ChangelogEntryDetail[]; @@ -2023,12 +2023,11 @@ export class PackageIntelligenceServiceImpl data: z.infer, params: PackageChangelogParams, ): ChangelogReport { - // Backend returns `source: null` when no changelog source could - // be resolved for the package/repo. Distinct from `entries: []` - // which means "source resolved but produced no entries in this - // range". Promote the null-source case to a typed error at the - // service boundary so the envelope builder never has to think - // about it. + // Backend returns a null or empty source when no changelog data could + // be resolved for the package/repo. Distinct from `entries: []` with + // a valid source, which means "source resolved but produced no entries + // in this range". Promote the no-source case to a typed error at the + // service boundary so the envelope builder never has to think about it. const source = data.source ?? undefined; if (!source) { const target = diff --git a/src/services/test-helpers.ts b/src/services/test-helpers.ts index 08191e2b..dec8778d 100644 --- a/src/services/test-helpers.ts +++ b/src/services/test-helpers.ts @@ -140,7 +140,7 @@ export const defaultUnifiedSearchOutcome: UnifiedSearchOutcome = { ], page: { offset: 0, - limit: 20, + limit: 10, returned: 1, hasMore: false, }, diff --git a/src/shared/package-changelog-response.ts b/src/shared/package-changelog-response.ts index 5a5378bb..79835c50 100644 --- a/src/shared/package-changelog-response.ts +++ b/src/shared/package-changelog-response.ts @@ -10,7 +10,7 @@ * the backend returned and what the caller asked for, not by * additional caller flags. `entries` is `{count, items}` whenever * the backend resolved a `source` (even `{count: 0, items: []}` - * for an empty range); `source === null` is promoted to a + * for an empty range); null/empty source is promoted to a * `NOT_FOUND` error at the service boundary and never reaches the * envelope builder. * - **Mode derived from request.** `mode: "range"` iff `fromVersion` @@ -88,8 +88,8 @@ export interface LeanChangelogEnvelope { name?: string; /** Present for repo-URL addressing. */ repoUrl?: string; - /** `"releases"` | `"changelog_file"` | `"hexdocs"` | `"version_spine"`. Never null here - * (null is promoted to NOT_FOUND at the service boundary). */ + /** `"releases"` | `"changelog_file"` | `"hexdocs"`. Never null/empty here + * (no-source responses are promoted to NOT_FOUND at the service boundary). */ source: string; /** Derived from request params. */ mode: ChangelogMode; @@ -332,8 +332,6 @@ function humanizeSource(source: string): string { return "CHANGELOG.md"; case "hexdocs": return "HexDocs"; - case "version_spine": - return "Version spine"; default: return source; } diff --git a/src/shared/unified-search-request.test.ts b/src/shared/unified-search-request.test.ts index 08cef82b..2103531a 100644 --- a/src/shared/unified-search-request.test.ts +++ b/src/shared/unified-search-request.test.ts @@ -11,7 +11,7 @@ describe("buildUnifiedSearchParams", () => { expect(built.rawQuery).toBe("router middleware"); expect(built.compiledQuery).toBe("router middleware"); expect(built.params.filters).toBeUndefined(); - expect(built.params.limit).toBe(20); + expect(built.params.limit).toBe(10); expect(built.params.offset).toBe(0); expect(built.params.waitTimeoutMs).toBe(20_000); }); diff --git a/src/shared/unified-search-request.ts b/src/shared/unified-search-request.ts index 13b5d0d9..5fdd3bee 100644 --- a/src/shared/unified-search-request.ts +++ b/src/shared/unified-search-request.ts @@ -10,6 +10,8 @@ import type { import { DEFAULT_WAIT_TIMEOUT_MS } from "./code-navigation-defaults.js"; import { InvalidArgumentError } from "./package-spec.js"; +export const DEFAULT_UNIFIED_SEARCH_LIMIT = 10; + export interface UnifiedSearchRequestInput { target?: CodeNavigationTarget; targets?: CodeNavigationTarget[]; @@ -40,7 +42,7 @@ export function buildUnifiedSearchParams( const targets = resolveTargets(input.target, input.targets); const rawQuery = normaliseRequiredQuery(input.query); - const limit = input.limit ?? 20; + const limit = input.limit ?? DEFAULT_UNIFIED_SEARCH_LIMIT; const offset = input.offset ?? 0; const waitTimeoutMs = input.waitTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS; diff --git a/src/shared/unified-search-response.test.ts b/src/shared/unified-search-response.test.ts index fa16eb66..850d558d 100644 --- a/src/shared/unified-search-response.test.ts +++ b/src/shared/unified-search-response.test.ts @@ -15,7 +15,7 @@ describe("buildUnifiedSearchSuccessPayload", () => { const params: UnifiedSearchParams = { targets: [{ registry: "NPM", packageName: "express" }], query: "router middleware", - limit: 20, + limit: 10, offset: 0, waitTimeoutMs: 20_000, }; @@ -228,7 +228,7 @@ describe("buildUnifiedSearchSuccessPayload — sourceStatus warnings on complete targets: [{ registry: "NPM", packageName: "zod" }], query: "parse kind:function", sources: ["DOCS"], - limit: 20, + limit: 10, offset: 0, waitTimeoutMs: 20_000, }; diff --git a/src/shared/unified-search-response.ts b/src/shared/unified-search-response.ts index db8ad07e..42053136 100644 --- a/src/shared/unified-search-response.ts +++ b/src/shared/unified-search-response.ts @@ -9,6 +9,7 @@ import type { import { MalformedCodeNavigationResponseError } from "../services/index.js"; import { DEFAULT_WAIT_TIMEOUT_MS } from "./code-navigation-defaults.js"; import { mapCodeNavigationError } from "./code-navigation-error-map.js"; +import { DEFAULT_UNIFIED_SEARCH_LIMIT } from "./unified-search-request.js"; /** * Default values folded out of the JSON envelope when the caller did @@ -16,7 +17,7 @@ import { mapCodeNavigationError } from "./code-navigation-error-map.js"; * back wastes tokens. The defaults must stay aligned with * `buildUnifiedSearchParams`. */ -const DEFAULT_LIMIT = 20; +const DEFAULT_LIMIT = DEFAULT_UNIFIED_SEARCH_LIMIT; const DEFAULT_OFFSET = 0; export interface UnifiedSearchQueryEcho { diff --git a/src/tools/package-changelog.ts b/src/tools/package-changelog.ts index 9f848e10..ee71df10 100644 --- a/src/tools/package-changelog.ts +++ b/src/tools/package-changelog.ts @@ -90,7 +90,7 @@ const DESCRIPTION = "`[from_version, to_version]` range (range mode, no count cap). " + "Address via `registry` + `package_name` or `repo_url` (mutually " + 'exclusive). Response: `source` (`"releases"` / `"changelog_file"` ' + - '/ `"hexdocs"` / `"version_spine"`), `mode` (`"latest"` or `"range"`), ' + + '/ `"hexdocs"`), `mode` (`"latest"` or `"range"`), ' + "`entries: { count, items }` with full markdown bodies. Set " + "`include_bodies: false` for a version / date / URL timeline only. " + "Supports npm, PyPI, Hex, Crates, vcpkg, Zig, NuGet, Maven, " + diff --git a/src/tools/search.test.ts b/src/tools/search.test.ts index 39d45d2b..430d6108 100644 --- a/src/tools/search.test.ts +++ b/src/tools/search.test.ts @@ -44,6 +44,7 @@ describe("searchTool", () => { expect.objectContaining({ query: "(handler) AND (lang:typescript)", allowPartialResults: true, + limit: 10, filters: expect.objectContaining({ kind: "FUNCTION" }), }), ); diff --git a/src/tools/search.ts b/src/tools/search.ts index 7a0ede98..fa4aef2b 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -150,7 +150,13 @@ const schema = { .describe( "Default false waits for all sources; if the wait window expires, returns only searchRef/progress. When true, includes hits from sources that finished so far and still returns searchRef for continuation. Partial payloads support normal pagination via nextOffset.", ), - limit: z.coerce.number().int().min(1).max(100).optional(), + limit: z.coerce + .number() + .int() + .min(1) + .max(100) + .optional() + .describe("Maximum results to return (default 10, max 100)."), offset: z.coerce.number().int().min(0).optional(), wait_timeout_ms: z.coerce.number().int().min(0).max(60000).optional(), format: z