Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ GitHits also exposes indexed package/source tools:
| `pkg_info` | Quick package triage: version, license, repository popularity, downloads, vulnerabilities |
| `pkg_vulns` | CVE / OSV advisories for a package or specific version, with filter echo and compact/verbose modes |
| `pkg_deps` | Direct dependencies, dependency groups, and optional transitive graph |
| `pkg_changelog` | Release notes / changelog entries for a package or GitHub repo |
| `pkg_changelog` | Release notes / changelog entries for a package or GitHub repo, with compact timeline and body-preview controls |
| `code_files` | Discover what files a dependency or repo contains |
| `code_read` | Read a dependency file by path |
| `code_grep` | Deterministic text grep across indexed dependency or repo files |
Expand Down
2 changes: 1 addition & 1 deletion docs/implementation/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ Fetches release notes or changelog entries for a package or GitHub repository. O

**Pre-release versions.** Normalised versions flow through unchanged (`5.0.0-rc.1`, `2.32.0.dev0`, `1.7.0-rc.5` round-trip cleanly on `--from` / `--to`). Tag-style `v`-prefixed inputs are rejected on any version flag, consistent with `pkg vulns` / `pkg deps`.

**Default terminal output.** Summary header (`name · registry · source · mode · entry count`) followed by each entry's `version date url` header plus the first 10 lines of its markdown body, indented and dimmed. Bodies longer than the cap show a footer ` (+N more lines use --verbose for the full body)`. Missing dates render as ``; missing versions render as `(unversioned)`. The version column is padded to the longest entry in the current response (no fixed width).
**Default terminal output.** Summary header (`name | registry | source | mode | entry count`) followed by each entry's `version date url` header plus the first 10 lines of its markdown body, indented and dimmed. Bodies longer than the cap show a footer `... (+N more lines - use --verbose for the full body)`. Missing dates render as `-`; missing versions render as `(unversioned)`. The version column is padded to the longest entry in the current response (no fixed width).

**`--verbose`.** Uncaps the body preview — every entry's full markdown body renders, indented and dimmed, with no truncation footer. Terminal-only — does not change `--json` output.

Expand Down
8 changes: 4 additions & 4 deletions docs/implementation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C
| `pkg_info` | `registry`, `package_name`, `verbose?`, `format?` | Latest-version package triage: license, description, repository popularity (stars/forks/issues and `[ARCHIVED]` when applicable), downloads, publish age, and vulnerability status. Example: `{registry:"npm", package_name:"express"}`. Set `verbose: true` for GitHub language/topics/last-pushed, recent advisories, and recent changes. Pass `format: "json"` for structured fields. |
| `pkg_vulns` | `registry`, `package_name`, `version?`, `min_severity?`, `include_withdrawn?`, `verbose?`, `format?` | Known vulnerabilities for a package on npm, PyPI, Hex, or Crates. Example: `{registry:"npm", package_name:"lodash", version:"4.17.20", min_severity:"high"}`. Defaults to compact text capped at 5 advisory rows with active filter echo; set `verbose: true` to show all text rows or pass `format: "json"` for the complete per-advisory envelope. |
| `pkg_deps` | `registry`, `package_name`, `version?`, `lifecycle?`, `include_transitive?`, `include_importers?`, `max_depth?`, `format?` | Direct runtime dependency list by default with resolved versions. Non-runtime groups are hidden with an MCP-native hint (`pass lifecycle="all"`). Use `lifecycle: "runtime"` for explicit runtime-only, a concrete non-runtime lifecycle for runtime plus matching groups, or `lifecycle: "all"` for all available groups. Optional transitive output includes aggregate edge counts, the preprocessed install footprint, typed conflicts and circular-dependency cycles; opt into importer provenance with `include_importers`. Pass `format: "json"` for the lean structured envelope. |
| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `include_bodies?`, `format?` | Release notes or changelog entries for a package or GitHub repo. Defaults to compact text with newest-first entries and capped body previews; pass `format: "json"` for the structured envelope with full markdown bodies, or set `include_bodies: false` for a lean version / date / URL timeline. `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) is mutually exclusive. |
| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `include_bodies?`, `verbose?`, `body_lines?`, `format?` | Release notes or changelog entries for a package or GitHub repo. Example: `{registry:"npm", package_name:"express", limit:2}`. Defaults to compact text with newest-first entries and 10-line body previews; set `body_lines` to tune text previews, `verbose:true` for full text bodies, `include_bodies:false` for a lean timeline, or `format:"json"` for the complete envelope. `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) is mutually exclusive. |
| `code_files` | `target`, `path?`, `path_prefix?`, `globs?`, `extensions?`, `file_types?`, `languages?`, `file_intent?`, `file_intents?`, `exclude_file_intents?`, `exclude_doc_files?`, `exclude_test_files?`, `include_hidden?`, `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). Selectors (`path`, `path_prefix`, `globs`) are OR-ed; the other filters intersect on top. 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?`, `format?` | Read a file from an indexed dependency. `target` accepts the structured object or compact string (`npm:react@18.2.0`, `https://github.com/facebook/react#HEAD`). **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 explaining the cap and the original request. Defaults to `text-v1` with line-numbered content; pass `format: "json"` for the structured envelope. 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. |
Expand Down Expand Up @@ -102,7 +102,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C

**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.

**`include_bodies` lever and body previews.** Release bodies on large packages (Kubernetes, Node) can run 10 KB+ per entry; a 100-entry range could produce a multi-hundred-KB envelope. `include_bodies: false` opts out explicitly in JSON and text — not silent truncation. Other fields (version / normalizedVersion / publishedAt / htmlUrl) remain so agents still get the release timeline. MCP text mode caps each body preview at 10 lines and tells agents to pass `format="json"` for full bodies. CLI terminal output uses the same preview cap but gives the CLI-native `--verbose` hint; `--verbose` uncaps terminal previews but does not change `--json` output.
**`include_bodies` lever and body previews.** Release bodies on large packages (Kubernetes, Node) can run 10 KB+ per entry; a 100-entry range could produce a multi-hundred-KB envelope. `include_bodies: false` opts out explicitly in JSON and text — not silent truncation. Other fields (version / normalizedVersion / publishedAt / htmlUrl) remain so agents still get the release timeline. Text mode caps each body preview at 10 lines by default. MCP adds text-only `body_lines` (1-50) to tune the cap and `verbose:true` to uncap text bodies; both are ignored for JSON. `verbose:true` conflicts with `include_bodies:false` and `body_lines`. CLI terminal output uses the same default preview cap and gives the CLI-native `--verbose` hint; `--verbose` uncaps terminal previews but does not change `--json` output.

**`filter.*` echo.** `filter` is emitted only when the caller explicitly supplied at least one of `from_version`, `to_version`, `limit`, or `git_ref`. Backend-default `limit: 10` / `toVersion: <latest>` is never echoed. The request builder tracks explicit-vs-defaulted via an `explicitFilterFields` set so defaults don't round-trip as caller intent.

Expand All @@ -112,7 +112,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C

**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`.

`pkg_changelog` shares its envelope builder and text formatter with the CLI `githits pkg changelog` command via `src/shared/package-changelog-request.ts` and `src/shared/package-changelog-response.ts`. MCP defaults to compact text with MCP-native `format="json"` hints for full bodies. The parity test (`src/tools/package-changelog-parity.test.ts`) passes `format: "json"`, asserts `toEqual` across every service-sourced success / error fixture (happy latest, range mode, repo-URL addressing, no-source package-version entries, `--no-body` / `include_bodies: false`, default bodies, empty entries, NOT_FOUND, PackageIntelligenceTargetNotFoundError, VERSION_NOT_FOUND, BACKEND_ERROR), and uses `toMatchObject` for builder-sourced `INVALID_ARGUMENT`.
`pkg_changelog` shares its envelope builder and text formatter with the CLI `githits pkg changelog` command via `src/shared/package-changelog-request.ts` and `src/shared/package-changelog-response.ts`. MCP defaults to compact text with MCP-native `verbose=true`, `body_lines=<n>`, and `format="json"` hints for full bodies. The parity test (`src/tools/package-changelog-parity.test.ts`) passes `format: "json"`, asserts `toEqual` across every service-sourced success / error fixture (happy latest, range mode, repo-URL addressing, no-source package-version entries, `--no-body` / `include_bodies: false`, default bodies, empty entries, NOT_FOUND, PackageIntelligenceTargetNotFoundError, VERSION_NOT_FOUND, BACKEND_ERROR), and uses `toMatchObject` for builder-sourced `INVALID_ARGUMENT`.

### `code_files` / `code_read` / `code_grep` response shapes

Expand Down Expand Up @@ -163,7 +163,7 @@ The `hint` field is emitted only when the cap *actually truncated* the response

**Example-search anatomy.** `get_example` text mode returns markdown directly, followed by `solution_id: <id>` when the REST response includes an app URL. This avoids JSON-wrapped markdown while preserving the `feedback` workflow. `search_language` text mode returns one match per line as `name (Display Name) aliases: a, b`; agents should pass the `name` value to `get_example.language`.

**Package metadata anatomy.** `pkg_info`, `pkg_vulns`, `pkg_deps`, and `pkg_changelog` text mode reuse the shared no-color terminal formatters but inject MCP-native hints. `pkg_deps` hides non-runtime groups by default and says `pass lifecycle="all"` when groups exist. `pkg_changelog` caps body previews and says `pass format="json" for full bodies` when text omitted lines. Package tools keep JSON errors in all formats because agents can reliably branch on `{error, code, retryable, details?}`.
**Package metadata anatomy.** `pkg_info`, `pkg_vulns`, `pkg_deps`, and `pkg_changelog` text mode reuse the shared no-color terminal formatters but inject MCP-native hints. `pkg_deps` hides non-runtime groups by default and says `pass lifecycle="all"` when groups exist. `pkg_changelog` caps body previews and says `pass verbose=true`, `body_lines=<n>`, or `format="json"` when text omitted lines. Package tools keep JSON errors in all formats because agents can reliably branch on `{error, code, retryable, details?}`.

**Hit anatomy** (`search` text-v1):

Expand Down
2 changes: 1 addition & 1 deletion eval/agentic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ least one agent for quick iteration.
| Unified `search` / `search_status` behavior | `unified-search-investigation.md` |
| Package overview or vulnerability UX, `pkg_info`, `pkg_vulns` | `package-overview-vulnerabilities.md` |
| Dependency graph UX, `pkg_deps` | `package-dependencies.md` |
| Release notes UX, `pkg_changelog` | `package-changelog.md` |
| Release notes UX, `pkg_changelog` | `package-changelog.md`; use `package-changelog-range.md` for range/body-preview behavior |
| Documentation browsing, `docs_list`, `docs_read` | `docs-discovery.md` |
| File listing / file read UX, `code_files`, `code_read` | `code-file-navigation.md` |
| Deterministic source search UX, `code_grep` | `code-grep-investigation.md` |
Expand Down
6 changes: 6 additions & 0 deletions eval/agentic/workloads/package-changelog-range.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Workload: Package Changelog Range Summary

You are evaluating Express upgrade risk from `4.17.0` through `4.18.2` for a
Node.js service. Summarize the notable behavioral, dependency, and security
changes in that version range. Prefer concise evidence over copying raw release
notes, and call out any caveats about the changelog source.
4 changes: 4 additions & 0 deletions scripts/cli-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,10 @@ async function runLiveSmoke(): Promise<void> {
changelogText.includes("express") || changelogText.includes("changelog"),
"pkg changelog terminal missing context",
);
assert(
!/[·…—–→]/.test(changelogText),
"pkg changelog terminal contains non-ASCII punctuation",
);

const changelogJson = assertJsonOutput(
await runCli(["pkg", "changelog", "npm:express", "--limit", "1", "--json"]),
Expand Down
38 changes: 37 additions & 1 deletion scripts/mcp-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,11 +429,33 @@ async function runLiveSmoke(client: Client): Promise<void> {
changelogText.includes("full bodies")
) {
assert(
changelogText.includes('pass format="json" for full bodies'),
changelogText.includes(
'pass verbose=true, body_lines=<n>, or format="json"',
),
"pkg_changelog truncation hint is not MCP-native",
);
}

const changelogBodyLinesText = assertDefaultText(
await callTool(client, "pkg_changelog", {
registry: "npm",
package_name: "express",
limit: 2,
body_lines: 3,
}),
"pkg_changelog body_lines",
);
assert(
changelogBodyLinesText.includes(
'pass verbose=true, body_lines=<n>, or format="json"',
),
"pkg_changelog body_lines missing MCP-native truncation hint",
);
assert(
!changelogBodyLinesText.includes("--verbose"),
"pkg_changelog body_lines leaked CLI verbose flag",
);

const changelogJson = assertJsonResult(
await callTool(client, "pkg_changelog", {
registry: "npm",
Expand All @@ -446,6 +468,20 @@ async function runLiveSmoke(client: Client): Promise<void> {
assertRecord(changelogJson, "pkg_changelog json");
assertRecord(changelogJson.entries, "pkg_changelog json entries");

const changelogTimeline = assertDefaultText(
await callTool(client, "pkg_changelog", {
registry: "npm",
package_name: "express",
limit: 2,
include_bodies: false,
}),
"pkg_changelog timeline",
);
assert(
!changelogTimeline.includes("What's Changed"),
"pkg_changelog include_bodies=false still emitted bodies",
);

const docsText = assertDefaultText(
await callTool(client, "docs_list", {
registry: "npm",
Expand Down
2 changes: 1 addition & 1 deletion src/commands/mcp-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const PKG_DEPS_BULLET =
'- `pkg_deps` — compact direct runtime deps by default. Use `lifecycle: "runtime"` for explicit runtime-only, a concrete lifecycle for runtime plus matching non-runtime deps, or `lifecycle: "all"` for all groups. Use `include_transitive` for the full graph and `include_importers` for provenance. Pass `format: "json"` for the structured envelope.';

const PKG_CHANGELOG_BULLET =
'- `pkg_changelog` — compact release notes for a package or GitHub repo, newest-first. Default latest mode returns recent entries with markdown body previews; `from_version` switches to range mode. Set `include_bodies: false` for a compact timeline or pass `format: "json"` for full bodies.';
'- `pkg_changelog` — compact release notes for a package or GitHub repo, newest-first (e.g. `npm` + `express` + `limit: 2`). Default latest mode returns recent entries with 10-line markdown previews; `from_version` switches to range mode. Set `body_lines` to tune text previews, `verbose: true` for full text bodies, `include_bodies: false` for a compact timeline, or `format: "json"` for the complete envelope.';

/**
* Combined strategy tip. Replaces the earlier
Expand Down
8 changes: 5 additions & 3 deletions src/commands/pkg/changelog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ describe("pkgChangelogAction", () => {
await pkgChangelogAction("npm:express", {}, createDeps());

const combined = writes.join("");
expect(combined).toContain("express · npm");
expect(combined).toContain("express | npm");
expect(combined).toContain("source: GitHub Releases");
expect(combined).toContain("2 entries");
expect(combined).toContain("5.2.1");
// Default now shows bodies (capped at 10 lines). Fixture bodies
// are well under the cap, so full content appears without the
// " more lines" footer.
// "... more lines" footer.
expect(combined).toContain("## Patch");
expect(combined).toContain("- fixed a thing");
expect(combined).not.toContain("use --verbose");
Expand Down Expand Up @@ -99,7 +99,9 @@ describe("pkgChangelogAction", () => {
expect(combined).toContain("line 10");
expect(combined).not.toContain("line 11");
expect(combined).toContain("+15 more lines");
expect(combined).toContain("use --verbose for the full body");
expect(combined).toContain(
"... (+15 more lines - use --verbose for the full body)",
);
writeSpy.mockRestore();
});

Expand Down
2 changes: 1 addition & 1 deletion src/commands/pkg/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ function formatChangelogTerminalError(mapped: MappedError): string {
if (available && available.length > 0) {
const sample = available.slice(0, 5).join(", ");
const more = available.length - 5;
const suffix = more > 0 ? `, (+${more} more)` : "";
const suffix = more > 0 ? `, ... (+${more} more)` : "";
lines.push(` available: ${sample}${suffix}`);
}
return lines.join("\n");
Expand Down
Loading
Loading