diff --git a/GEMINI.md b/GEMINI.md index d3e8474b..63db6013 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -66,13 +66,11 @@ Do not use `get_example` for: - Keep queries focused: 3-4 technical terms maximum - Submit `feedback` after every search result you use or discard -## Feature-Gated Tools +## Indexed Package/Source Tools -When the authenticated token carries the `code_navigation` feature flag, GitHits -also exposes indexed dependency/package tools such as `search`, -`search_status`, `package_summary`, `package_vulnerabilities`, -`package_dependencies`, `package_changelog`, `list_files`, `read_file`, and -`grep_repo`. +GitHits also exposes indexed dependency/package tools such as `search`, +`search_status`, `docs_list`, `docs_read`, `pkg_info`, `pkg_vulns`, +`pkg_deps`, `pkg_changelog`, `code_files`, `code_read`, and `code_grep`. ## License Filtering diff --git a/README.md b/README.md index d8ed89af..74e9150b 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ For plugin-based hosts, install from npm/GitHub using your agent's plugin workfl - **GitHub Copilot**: supports Open Plugin components - **Gemini CLI**: supports `gemini-extension.json` and `GEMINI.md` -That's it. Your assistant now has GitHits example-search tools, and on accounts with package/source access enabled it also gets indexed dependency/package inspection tools. +That's it. Your assistant now has GitHits example-search tools and indexed dependency/package inspection tools. ## How It Works @@ -98,21 +98,21 @@ Core tools available in every authenticated session: The assistant decides when to call these tools on its own — typically when it's stuck, needs a working example for an unfamiliar API, or encounters an error it can't resolve from its training data alone. -When package/source access is enabled for the current token, GitHits also exposes these capability-gated tools: +GitHits also exposes indexed package/source tools: | Tool | Purpose | |---|---| | `search` | Unified indexed search across dependency/repository code, docs, and symbols | | `search_status` | Follow up a prior indexed `search` by `searchRef` | -| `package_summary` | Quick package overview: version, license, downloads, quickstart, advisories | -| `package_vulnerabilities` | CVE / OSV advisories for a package or specific version | -| `package_dependencies` | Direct dependencies, dependency groups, and optional transitive graph | -| `package_changelog` | Release notes / changelog entries for a package or GitHub repo | -| `list_files` | Discover what files a dependency or repo contains | -| `read_file` | Read a dependency file by path | -| `grep_repo` | Deterministic text grep across indexed dependency or repo files | - -These advanced tools remain feature-gated. The MCP server advertises them only when the authenticated token explicitly carries the `code_navigation` feature flag. +| `docs_list` | Browse mixed package documentation pages | +| `docs_read` | Read a documentation page by page ID | +| `pkg_info` | Quick package overview: version, license, downloads, quickstart, advisories | +| `pkg_vulns` | CVE / OSV advisories for a package or specific version | +| `pkg_deps` | Direct dependencies, dependency groups, and optional transitive graph | +| `pkg_changelog` | Release notes / changelog entries for a package or GitHub repo | +| `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 | ### License Filtering @@ -162,12 +162,13 @@ githits languages List or filter supported language names githits feedback Send feedback on a returned example ``` -When the current token explicitly carries `code_navigation`, these extra commands are also available: +These indexed package/source commands are also available: ``` githits search ... Unified indexed dependency/repository search githits search-status Follow up a prior indexed search githits pkg ... Package metadata: overview, advisories, deps, changelog +githits docs ... Package documentation: browse pages and read content githits code ... Dependency source inspection: search, files, read, grep ``` @@ -178,15 +179,14 @@ githits code ... Dependency source inspection: search, files, read, grep | `GITHITS_API_TOKEN` | API token for authentication | — | | `GITHITS_MCP_URL` | Override MCP server URL | `https://mcp.githits.com` | | `GITHITS_API_URL` | Override REST API URL | `https://api.githits.com` | -| `GITHITS_CODE_NAV_URL` | Override package/source service URL | environment-specific | -| `GITHITS_CODE_NAVIGATION` | Expose hidden `search` / `pkg` / `code` CLI surfaces locally for development | — | +| `GITHITS_CODE_NAV_URL` | Override package/source service URL | `https://pkgseer.dev` | | `GITHITS_TELEMETRY` | Emit end-of-run timing spans to stderr for local profiling | — | ## Manual Setup If your tool is not in the supported `githits init` list, configure GitHits manually. -The same MCP server command exposes both the core example-search tools and, when your token carries `code_navigation`, the indexed package/source inspection tools. No separate install is required. +The same MCP server command exposes both the core example-search tools and the indexed package/source inspection tools. No separate install is required. Use this MCP server command in your tool's MCP config (the host/agent runs this command): diff --git a/commands/help.md b/commands/help.md index 0e7d8235..97ec0e3b 100644 --- a/commands/help.md +++ b/commands/help.md @@ -33,9 +33,8 @@ 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`, `package_summary`, -`list_files`, and `grep_repo` stay hidden unless the authenticated token carries -the `code_navigation` feature flag. +Additional indexed dependency/package tools such as `search`, `pkg_info`, +`code_files`, and `code_grep` are available by default. ## Authentication diff --git a/docs/implementation/auth.md b/docs/implementation/auth.md index 37068fa6..7db059a2 100644 --- a/docs/implementation/auth.md +++ b/docs/implementation/auth.md @@ -2,7 +2,7 @@ ## Purpose -The CLI supports two authentication methods with different capabilities. Understanding which one is active and how the OAuth flow works is essential for modifying auth-related code without breaking the login experience. +The CLI supports two authentication methods. Understanding which one is active and how the OAuth flow works is essential for modifying auth-related code without breaking the login experience. ## Background @@ -19,8 +19,6 @@ The two methods exist because OAuth provides full access but requires a browser, > **The container resolves auth at startup.** The `createContainer()` function checks for `GITHITS_API_TOKEN` first — if set, it takes precedence even when OAuth tokens are stored. If not set, it loads stored OAuth tokens and attempts auto-refresh if expired. See `src/container.ts` for the resolution logic. -Some hidden package/source tooling is conditionally exposed based on the active auth context. - ## OAuth PKCE Flow The login command (`src/commands/login.ts`) orchestrates a 9-step OAuth flow (matching the `// Step N:` comments in the code): @@ -162,7 +160,6 @@ The `hasValidToken` flag is checked by `requireAuth()` in `src/commands/mcp.ts` | `src/services/token-manager.ts` | `TokenProvider` interface, `TokenManager` (proactive refresh, coalescing) | | `src/services/refreshing-githits-service.ts` | `GitHitsService` decorator with token refresh and 401 retry | | `src/services/execute-with-token-refresh.ts` | Shared helper for token-authenticated retry-on-refresh flows | -| `src/services/code-navigation-capability.ts` | Local package/source access gating helpers | | `src/services/code-navigation-service.ts` | Package/source service client using the shared refresh helper | | `src/services/auth-service.ts` | OAuth operations (DCR, PKCE, token exchange, callback server) | | `src/services/auth-storage.ts` | `AuthStorage` interface and file-based implementation | diff --git a/docs/implementation/cli-commands.md b/docs/implementation/cli-commands.md index 4a29f4b3..35cf19c8 100644 --- a/docs/implementation/cli-commands.md +++ b/docs/implementation/cli-commands.md @@ -2,7 +2,7 @@ ## Purpose -The CLI exposes three always-on top-level commands: `example`, `languages`, and `feedback`. Indexed dependency/package surfaces are capability-gated: top-level `search` / `search-status` plus the `code` and `pkg` command groups are shown only when the startup token explicitly carries `code_navigation`, or when `GITHITS_CODE_NAVIGATION=1` is set locally for development. All of these commands share business logic with the MCP tools through the same service interfaces and shared utilities, but format output for terminal consumption instead of MCP tool results. +The CLI exposes `example`, `languages`, `feedback`, top-level indexed `search` / `search-status`, and the `code`, `docs`, and `pkg` command groups by default. All of these commands share business logic with the MCP tools through the same service interfaces and shared utilities, but format output for terminal consumption instead of MCP tool results. ## Commands @@ -18,6 +18,8 @@ The CLI exposes three always-on top-level commands: `example`, `languages`, and | `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. | +| `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. | | `code read ` | package spec OR `--repo-url` + `--git-ref`; plus `` | `--lines`, `--start`, `--end`, `--wait`, `--verbose`, `--json` | Read a file's contents. Plain output is the raw file bytes (pipe-friendly); `--verbose` adds a header and a line-number gutter. `--lines 10-40` concise form; `--start`/`--end` equivalent. Binary files show a sentinel line. | | `code grep [spec] [path-prefix]` | package spec OR `--repo-url` + `--git-ref`; plus `` and optional `[path-prefix]` | `--path`, repeatable `--glob`, repeatable `--ext`, `--regex`, `--case-sensitive`, `-C/-A/-B`, `--exclude-docs`, `--exclude-tests`, `--limit`, `--per-file-limit`, `--cursor`, `--symbol-field`, `--wait`, `--verbose`, `--json` | Deterministic text grep over indexed dependency or repository source. Defaults to whole-target, literal, ASCII case-insensitive matching; non-ASCII letters match case-sensitively. Narrow with `[path-prefix]`, `--path`, `--glob`, or `--ext`. Plain output is `file:line:text`; `--verbose` groups matches by file. | @@ -130,8 +132,6 @@ Shows a concise overview for a single package: latest version, license, descript **Output envelope.** Success payload is hand-crafted for agent token efficiency: `{registry, name, version, description?, license?, homepage?, repository?, publishedAt?, downloads?, github?, install?, usage?, vulnerabilities?, recentChanges?}`. Omitted fields reflect backend nulls, not dropped data. Error envelope: `{error, code, retryable, details?}` — shared classifier family. Under `--json` the error envelope is written to **stderr** so stdout stays clean for `jq`. -**Capability gate.** Same as `code`. - **Troubleshooting.** `GITHITS_DEBUG=pkg-intel` emits PII-safe classified-error diagnostics (area, event, code, error class, detail keys). Use `GITHITS_DEBUG=*` to enable all non-sensitive package/source diagnostics. ### `githits pkg vulns` @@ -168,8 +168,6 @@ Lists known CVE / OSV advisories for a package: severity, affected version range **Exit codes.** 0 on success including zero-vulns; 1 on any error. Under `--json`, the error envelope is written to **stderr**. -**Capability gate.** Same as `pkg info`. - **Troubleshooting.** Same debug areas as `pkg info`. ### `githits pkg deps` @@ -204,8 +202,6 @@ Analyses dependencies for a package on npm, PyPI, Hex, Crates, vcpkg, or Zig. De **Exit codes.** 0 on success including zero-dep packages; 1 on any error. Under `--json`, the error envelope is written to **stderr**. -**Capability gate.** Same as `pkg info` / `pkg vulns`. - **Troubleshooting.** Same debug areas as `pkg info` / `pkg vulns`. ### `githits pkg changelog` @@ -241,10 +237,41 @@ Fetches release notes or changelog entries for a package or GitHub repository. O **Errors.** `NOT_FOUND` covers both the backend's "package not found" case and the distinct "package exists but no changelog source resolved" case (typed `PackageIntelligenceChangelogSourceNotFoundError`; message names the sources that were tried). `VERSION_NOT_FOUND` enriches with structured `package` / `requested` / `available` detail lines from the shared `promoteGenericVersionNotFound` helper — which was extended in this PR to recognise `--from` and `--to` as promotable version inputs. -**Capability gate.** Same as the rest of the `pkg` family. - **Troubleshooting.** Same debug areas as the rest of the `pkg` family. +### `githits docs list` + +``` +githits docs list npm:express +githits docs list npm:express --limit 20 +githits docs list npm:express --json +``` + +Lists hosted/crawled and repository-backed documentation pages for a package. Each row includes a stable page ID for `docs read`, a source badge, and the source location. JSON output also includes repo URL / git ref / file path for repository-backed docs so callers can follow up with `code read` when source context is needed. + +**Pagination.** `--limit ` accepts 1-500. When `hasMore` is true, pass the returned `nextCursor` to `--after`. + +**Output envelope.** `{registry, name, version?, pages, total?, hasMore, nextCursor?, stale?, filter?}`. Each page has `{pageId, title, sourceKind, sourceUrl?, linkName?, repoUrl?, gitRef?, filePath?, lastUpdatedAt?}`. + +**Troubleshooting.** Same debug areas as the `pkg` family. + +### `githits docs read` + +``` +githits docs read +githits docs read --lines 10-80 +githits docs read --verbose +githits docs read --json +``` + +Reads a documentation page returned by `docs list` or search results. Default output is content-only for easy piping; `--verbose` adds a metadata header. + +**Line ranges.** `--lines 10-40`, `--lines 10-`, and `--lines -40` are supported. Use ranges to inspect long pages incrementally. + +**Output envelope.** `{pageId, title?, sourceKind?, sourceUrl?, repoUrl?, gitRef?, filePath?, totalLines?, startLine?, endLine?, content}`. Repo-backed docs include exact source metadata for `code read` follow-up. + +**Troubleshooting.** Same debug areas as the `pkg` family. + ### `githits code files` ``` @@ -338,7 +365,7 @@ Each command follows this pattern: | Shared Module | Used By | |---|---| | `GitHitsService` (via container) | `example`, `languages`, `feedback`, and always-on MCP tools | -| `CodeNavigationService` (via container) | top-level unified `search` / `search-status` plus capability-gated MCP indexed-search tools (`search`, `search_status`, `code_files`, `code_read`, `code_grep`) and the `githits code` command group | +| `CodeNavigationService` (via container) | top-level unified `search` / `search-status`, MCP indexed-search tools (`search`, `search_status`, `code_files`, `code_read`, `code_grep`), and the `githits code` command group | | `filterLanguages()` from `src/shared/language-filter.ts` | `search_language` MCP tool + `languages` CLI command | | `requireAuth()` from `src/shared/require-auth.ts` | all CLI commands and auth-required MCP tool handlers | @@ -371,7 +398,7 @@ All commands support two output modes: ## Runtime Diagnostics -- **`GITHITS_TELEMETRY=1`** — Emits an end-of-run timing summary to stderr without polluting normal stdout. Current spans cover gated command registration, startup auth lookup, container creation, token loading/refresh, and the outbound API/package-intelligence request. +- **`GITHITS_TELEMETRY=1`** — Emits an end-of-run timing summary to stderr without polluting normal stdout. Current spans cover command registration, container creation, token loading/refresh, and the outbound API/package-intelligence request. ## Key Reference Files diff --git a/docs/implementation/config.md b/docs/implementation/config.md index f0cd3cf8..01fa153d 100644 --- a/docs/implementation/config.md +++ b/docs/implementation/config.md @@ -14,9 +14,9 @@ GitHits separates its MCP server (which handles OAuth discovery and the MCP prot |---|---|---|---| | **MCP URL** | `https://mcp.githits.com` | `GITHITS_MCP_URL` | OAuth discovery (`.well-known`), DCR registration, auth flow | | **API URL** | `https://api.githits.com` | `GITHITS_API_URL` | REST endpoints (`/search`, `/languages`, `/feedbacks`) | -| **Package/source URL** | `https://pkgseer.dev` in the default production environment; otherwise unset | `GITHITS_CODE_NAV_URL` | Package/source service endpoint used by hidden indexed `search` / `pkg` / `code` tooling | +| **Package/source URL** | `https://pkgseer.dev` | `GITHITS_CODE_NAV_URL` | Package/source service endpoint used by indexed `search` / `pkg` / `docs` / `code` tooling | -> **These are different services.** Setting only one won't work for custom environments. Both must be overridden together when pointing to a non-production backend. +> **These are different services.** Override every URL that differs from production when pointing to a non-production backend. The MCP URL is also used as the storage key for tokens and client registrations in `~/.githits/` (trailing slashes are stripped for consistent key matching). This means tokens from one environment don't leak into another. @@ -38,12 +38,7 @@ The container (`src/container.ts`) resolves authentication in priority order: | `/languages` | Full access | Full access | Blocked | | `/feedbacks` | Full access | Full access | Blocked | -Package/source access is different from the REST endpoints above: - -- the CLI resolves the package/source service URL from `GITHITS_CODE_NAV_URL`; in the default production environment it falls back to `https://pkgseer.dev`, but custom GitHits environments must set this explicitly -- MCP registration for `search`, `search_status`, `package_*`, `code_files`, `code_read`, and `code_grep` happens only when the current token explicitly carries `code_navigation` -- CLI registration for top-level `search` / `search-status` plus the hidden `githits code` / `githits pkg` groups uses the same capability check, with one local-development escape hatch: `GITHITS_CODE_NAVIGATION=1` -- if the capability is absent or unknown, those indexed tools and command groups are omitted from the surfaced interface +Package/source access uses the package/source service URL from `GITHITS_CODE_NAV_URL`, defaulting to `https://pkgseer.dev`. MCP registration for `search`, `search_status`, `docs_*`, `pkg_*`, `code_files`, `code_read`, and `code_grep` is always on; CLI registration for top-level `search` / `search-status` plus the `githits code`, `githits pkg`, and `githits docs` groups is also always on. ## Environment Variables @@ -53,7 +48,6 @@ Package/source access is different from the REST endpoints above: | `GITHITS_API_URL` | Override REST API URL | `http://localhost:8000` | | `GITHITS_CODE_NAV_URL` | Override package/source service URL | `http://localhost:4000` | | `GITHITS_API_TOKEN` | API token for authentication | `ghi-abc123...` | -| `GITHITS_CODE_NAVIGATION` | Override capability gate and expose hidden indexed `search` / `code` / `pkg` CLI surfaces locally | `1` | | `GITHITS_TELEMETRY` | Emit end-of-run timing spans to stderr for local profiling | `1` | ## Local Storage @@ -72,14 +66,13 @@ The secure file permissions prevent other users from reading tokens. When writin ``` Environment variables - └─ src/services/config.ts (getMcpUrl, getApiUrl, getEnvApiToken) + └─ src/services/config.ts (getMcpUrl, getApiUrl, getCodeNavigationUrl, getEnvApiToken) └─ src/container.ts (createContainer) ├─ mcpUrl → passed to auth commands, used as storage key ├─ apiUrl → passed to GitHitsServiceImpl constructor - ├─ codeNavigationUrl → passed to CodeNavigationServiceImpl when configured + ├─ codeNavigationUrl → passed to CodeNavigationServiceImpl and PackageIntelligenceServiceImpl ├─ apiToken → resolved from env var or OAuth storage - ├─ hasValidToken → gates authenticated commands - └─ codeNavigationCapability / CLI override → gates code navigation exposure + └─ hasValidToken → gates authenticated commands ``` Commands receive the full `Dependencies` object. Services receive only what they need (e.g., `GitHitsServiceImpl` gets `apiUrl` and `token`). diff --git a/docs/implementation/tools.md b/docs/implementation/tools.md index 591d11bb..e3375fe0 100644 --- a/docs/implementation/tools.md +++ b/docs/implementation/tools.md @@ -22,6 +22,8 @@ The two surfaces overlap on the example-search workflow only; for the overlappin | `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_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`. | @@ -30,7 +32,7 @@ The two surfaces overlap on the example-search workflow only; for the overlappin | `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. | -`search`, `search_status`, `pkg_info`, `pkg_vulns`, `pkg_deps`, `pkg_changelog`, `code_files`, `code_read`, and `code_grep` are only registered when the current token explicitly carries the `code_navigation` feature flag. The package/source service URL defaults to `https://pkgseer.dev` in the default production environment and can be overridden via `GITHITS_CODE_NAV_URL` for local development. +`search`, `search_status`, `docs_list`, `docs_read`, `pkg_info`, `pkg_vulns`, `pkg_deps`, `pkg_changelog`, `code_files`, `code_read`, and `code_grep` are registered by default. The package/source service URL defaults to `https://pkgseer.dev` and can be overridden via `GITHITS_CODE_NAV_URL` for local development. **Unified `search` query syntax.** The `search.query` field is the backend discovery query syntax, not a raw pass-through to a per-source search engine. It supports implicit `AND`, uppercase `OR`, parentheses, unary `-`, quoted phrases, semantic qualifiers (`kind:`, `category:`, `path:`, `lang:`, `name:`, `intent:`), and routing qualifiers (`registry:`, `package:`, `version:`, `repo:`). The backend parses the query once and compiles it per source. Structured `name` and `language` inputs are compiled into `name:` / `lang:` qualifiers and AND-ed with the query before sending. Per-source support, ignored features, and incompatibilities are reported in `sourceStatus`. @@ -110,7 +112,7 @@ The two surfaces overlap on the example-search workflow only; for the overlappin ### `code_files` / `code_read` / `code_grep` response shapes -These three indexing-gated tools share an addressing and lifecycle contract (documented below) and then each projects its own data-first envelope. All three reuse the shipped `codeTargetSchema` + `resolveCodeTarget` from `src/tools/code-navigation-shared.ts` — no parallel addressing module. +These three indexed tools share an addressing and lifecycle contract (documented below) and then each projects its own data-first envelope. All three reuse the shipped `codeTargetSchema` + `resolveCodeTarget` from `src/tools/code-navigation-shared.ts` — no parallel addressing module. **`code_files` envelope**: `{registry?|repoUrl?+gitRef?, total, hasMore, indexedVersion?, resolution?, files: [{path, name?, language?, fileType?, byteSize?}], hint?, filter?}`. `fileType` values preserve the service vocabulary (`CONFIG`, `SOURCE`, `DOC`, `TEST`). `total` is capped at returned count when `hasMore: true` — the terminal formatter renders `N+ files` in that case to avoid misleading users. `filter.pathPrefix` / `filter.limit` echo only when the caller supplied them explicitly; default limit (200) never round-trips. @@ -217,14 +219,12 @@ The MCP server advertises a short, cross-tool orientation via the protocol's ser `src/commands/mcp-instructions.ts` owns two sections: - **Core block** — always loaded. Introduces GitHits, expands trigger criteria to include comparative cross-OSS questions and "how does X actually implement this" archaeology, and walks through the `get_example` / `search_language` / `feedback` workflow. -- **Package-tools block** — appended when `isPackageToolsCapabilityOpen(deps)` is true. Contains a preamble plus one bullet per package tool whose backing service is actually wired (so half-open service configurations never advertise a tool that was not registered), plus three cross-tool tips emitted only when code navigation is wired: +- **Package-tools block** — always appended. Contains a preamble plus one bullet per package/code tool, plus three cross-tool tips: - **Reference-first, content-second**: locate symbols and lines first, then read narrowly with `code_read` using `start_line` / `end_line` windows around the match. - **Multi-turn discovery**: anticipate 3+ calls? Delegate to a sub-task / sub-agent and ask for a compact synthesis rather than pulling raw `code_read` / `code_files` output into the main conversation. - **Tool-selection tip**: contrasts `get_example` vs unified `search` vs `code_grep`/`code_read`. -`isPackageToolsCapabilityOpen` is the single source of truth for whether package tools should surface in the current session. Both `getMcpToolDefinitions` and `buildMcpInstructions` import it so tool registration and the instruction text cannot drift. - -When adding a new package tool, extend the composer with a one-line bullet (`\`tool_name\` — one-sentence purpose`) in the same PR that registers the tool, and gate the bullet on the tool's backing service. Keep the bullet terse; argument and response detail belong in the tool's `description`. `mcp-instructions.test.ts` enforces both directions of the mention↔registration invariant across gate-open, gate-closed, and half-open scenarios. +When adding a new package tool, extend the composer with a one-line bullet (`\`tool_name\` — one-sentence purpose`) in the same PR that registers the tool. Keep the bullet terse; argument and response detail belong in the tool's `description`. `mcp-instructions.test.ts` enforces both directions of the mention↔registration invariant. ## Entry Points diff --git a/plugins/claude/commands/help.md b/plugins/claude/commands/help.md index 0e7d8235..97ec0e3b 100644 --- a/plugins/claude/commands/help.md +++ b/plugins/claude/commands/help.md @@ -33,9 +33,8 @@ 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`, `package_summary`, -`list_files`, and `grep_repo` stay hidden unless the authenticated token carries -the `code_navigation` feature flag. +Additional indexed dependency/package tools such as `search`, `pkg_info`, +`code_files`, and `code_grep` are available by default. ## Authentication diff --git a/plugins/claude/skills/search/SKILL.md b/plugins/claude/skills/search/SKILL.md index 1317b7e6..797d2df7 100644 --- a/plugins/claude/skills/search/SKILL.md +++ b/plugins/claude/skills/search/SKILL.md @@ -31,8 +31,8 @@ Guidelines: - Pass `language` only when you need to force a specific language; use `search_language` first if the exact language name is uncertain. - Use `get_example` for one focused example-search question at a time. -- When the task is about indexed dependency or repository internals and the - capability-gated tools are available, prefer unified `search` instead of +- When the task is about indexed dependency or repository internals, prefer + unified `search` instead of `get_example`. - After using results, send `feedback` with helpful/unhelpful outcome. diff --git a/skills/search/SKILL.md b/skills/search/SKILL.md index 1317b7e6..797d2df7 100644 --- a/skills/search/SKILL.md +++ b/skills/search/SKILL.md @@ -31,8 +31,8 @@ Guidelines: - Pass `language` only when you need to force a specific language; use `search_language` first if the exact language name is uncertain. - Use `get_example` for one focused example-search question at a time. -- When the task is about indexed dependency or repository internals and the - capability-gated tools are available, prefer unified `search` instead of +- When the task is about indexed dependency or repository internals, prefer + unified `search` instead of `get_example`. - After using results, send `feedback` with helpful/unhelpful outcome. diff --git a/src/cli.ts b/src/cli.ts index 13dd0eb0..e6c92c5c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -84,30 +84,25 @@ registerLanguagesCommand(program); registerFeedbackCommand(program); const argv = process.argv.slice(2); const registrationArgv = stripRootRegistrationOptions(argv); -const shouldLoadGatedHelpRegistration = - needsGatedHelpRegistration(registrationArgv); -const helpRegistrationOptions = shouldLoadGatedHelpRegistration - ? await loadHelpRegistrationOptions(registrationArgv) - : undefined; if (shouldEagerLoadSearchCommands(registrationArgv)) { await withTelemetrySpan("cli.register.search", () => - registerUnifiedSearchCommands(program, helpRegistrationOptions), + registerUnifiedSearchCommands(program), ); } if (shouldEagerLoadGatedCommandGroup(registrationArgv, "code")) { await withTelemetrySpan("cli.register.code-group", () => - registerCodeCommandGroup(program, helpRegistrationOptions), + registerCodeCommandGroup(program), ); } if (shouldEagerLoadGatedCommandGroup(registrationArgv, "pkg")) { await withTelemetrySpan("cli.register.pkg-group", () => - registerPkgCommandGroup(program, helpRegistrationOptions), + registerPkgCommandGroup(program), ); } if (shouldEagerLoadGatedCommandGroup(registrationArgv, "docs")) { await withTelemetrySpan("cli.register.docs-group", () => - registerDocsCommandGroup(program, helpRegistrationOptions), + registerDocsCommandGroup(program), ); } @@ -123,7 +118,7 @@ await withTelemetrySpan("cli.parse", () => program.parseAsync()); /** * Commander supports root options before subcommands, e.g. * `githits --no-color pkg info`. Registration happens before Commander - * parses argv, so the lightweight gated-command sniff must ignore root-only + * parses argv, so the lightweight command sniff must ignore root-only * flags or it will misclassify `--no-color` as the requested command. */ function stripRootRegistrationOptions(args: string[]): string[] { @@ -131,12 +126,10 @@ function stripRootRegistrationOptions(args: string[]): string[] { } /** - * Argv-sniff optimisation for gated command groups. Returns `true` + * Argv-sniff optimisation for command groups. Returns `true` * when the user's invocation might need the group registered — i.e. - * they typed the group name or asked for help. This is NOT a - * capability gate; the actual gate lives inside - * `registerXxxCommandGroup`. Here we only decide whether to build - * the container eagerly so registration can run. + * they typed the group name or asked for help. Here we only decide + * whether to build the command group eagerly so registration can run. */ function shouldEagerLoadGatedCommandGroup( args: string[], @@ -164,72 +157,10 @@ function shouldEagerLoadSearchCommands(args: string[]): boolean { ); } -function isHelpInvocation(args: string[]): boolean { - return ( - args.length === 0 || - args[0] === "help" || - args.includes("--help") || - args.includes("-h") - ); -} - -function needsGatedHelpRegistration(args: string[]): boolean { - if (!isHelpInvocation(args)) { - return false; - } - - const [firstArg, secondArg] = args; - if (firstArg === "help") { - return ( - isSearchHelpTarget(secondArg) || - secondArg === "code" || - secondArg === "pkg" || - secondArg === "docs" - ); - } - - return ( - isSearchHelpTarget(firstArg) || - firstArg === "code" || - firstArg === "pkg" || - firstArg === "docs" - ); -} - function isSearchHelpTarget(value: string | undefined): boolean { return value === "search" || value === "search-status"; } -async function loadHelpRegistrationOptions(args: string[]) { - const { resolveStartupCodeNavigationRegistrationState } = await import( - "./container.js" - ); - const registrationState = - await resolveStartupCodeNavigationRegistrationState(); - return { - capability: registrationState.capability, - expiredStoredAuth: shouldUseExpiredStoredAuthFallbackForHelp(args) - ? registrationState.expiredStoredAuth - : false, - }; -} - -function shouldUseExpiredStoredAuthFallbackForHelp(args: string[]): boolean { - const [firstArg, secondArg] = args; - return ( - firstArg === "search" || - firstArg === "search-status" || - firstArg === "code" || - firstArg === "pkg" || - firstArg === "docs" || - (firstArg === "help" && - (isSearchHelpTarget(secondArg) || - secondArg === "code" || - secondArg === "pkg" || - secondArg === "docs")) - ); -} - function getTelemetryCommandName(command: Command): string { const names: string[] = []; let current: Command | null = command; diff --git a/src/commands/auth-status.test.ts b/src/commands/auth-status.test.ts index db66e024..78552f39 100644 --- a/src/commands/auth-status.test.ts +++ b/src/commands/auth-status.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, mock, spyOn } from "bun:test"; import { - createJwtToken, createMockAuthService, createMockAuthStorage, createValidTokenData, @@ -16,7 +15,6 @@ describe("authStatusAction", () => { authStorage?: ReturnType; authService?: ReturnType; envApiToken?: string; - codeNavigationCliOverrideEnabled?: boolean; } = {}, ) { return { @@ -24,8 +22,6 @@ describe("authStatusAction", () => { authService: overrides.authService ?? createMockAuthService(), mcpUrl, envApiToken: overrides.envApiToken ?? undefined, - codeNavigationCliOverrideEnabled: - overrides.codeNavigationCliOverrideEnabled ?? false, }; } @@ -46,7 +42,6 @@ describe("authStatusAction", () => { loadTokens: mock(() => Promise.resolve( createValidTokenData({ - accessToken: createJwtToken({ feature_flags: ["code_navigation"] }), expiresAt: new Date(Date.now() + 3600_000).toISOString(), }), ), @@ -61,7 +56,6 @@ describe("authStatusAction", () => { expect(output).toContain(mcpUrl); expect(output).toContain("Storage:"); expect(output).toContain("System keychain (githits)"); - expect(output).toContain("Code navigation: enabled"); consoleSpy.mockRestore(); }); @@ -93,7 +87,6 @@ describe("authStatusAction", () => { expiresAt: new Date(Date.now() - 3600_000).toISOString(), }); const refreshedToken = createValidTokenData({ - accessToken: createJwtToken({ feature_flags: ["code_navigation"] }), expiresAt: new Date(Date.now() + 3600_000).toISOString(), }); @@ -114,15 +107,12 @@ describe("authStatusAction", () => { expect(output).toContain("Authenticated (token refreshed)"); expect(output).not.toContain("Token expired"); expect(output).toContain("Storage:"); - expect(output).toContain("Code navigation: enabled"); consoleSpy.mockRestore(); }); it("shows env token info when envApiToken is provided", async () => { const consoleSpy = spyOn(console, "log").mockImplementation(() => {}); - const envApiToken = createJwtToken({ - feature_flags: ["code_navigation"], - }); + const envApiToken = "ghi-env-token"; await authStatusAction( createDeps({ @@ -135,20 +125,6 @@ describe("authStatusAction", () => { expect(output).toContain("GITHITS_API_TOKEN"); expect(output).not.toContain("Token:"); expect(output).not.toContain(envApiToken.slice(0, 8)); - expect(output).toContain("Code navigation: enabled"); - consoleSpy.mockRestore(); - }); - - it("shows unknown capability and active CLI override when unauthenticated", async () => { - const consoleSpy = spyOn(console, "log").mockImplementation(() => {}); - - await authStatusAction( - createDeps({ codeNavigationCliOverrideEnabled: true }), - ); - - const output = consoleSpy.mock.calls.map((c) => c[0]).join("\n"); - expect(output).toContain("Code navigation: unknown"); - expect(output).toContain("CLI override: enabled"); consoleSpy.mockRestore(); }); diff --git a/src/commands/auth-status.ts b/src/commands/auth-status.ts index 1a777929..40eb0e18 100644 --- a/src/commands/auth-status.ts +++ b/src/commands/auth-status.ts @@ -1,21 +1,13 @@ import type { Command } from "commander"; import { createContainer } from "../container.js"; -import type { - AuthService, - AuthStorage, - CodeNavigationCapability, -} from "../services/index.js"; -import { - getCodeNavigationCapability, - refreshExpiredToken, -} from "../services/index.js"; +import type { AuthService, AuthStorage } from "../services/index.js"; +import { refreshExpiredToken } from "../services/index.js"; export interface AuthStatusDependencies { authStorage: AuthStorage; authService: AuthService; mcpUrl: string; envApiToken: string | undefined; - codeNavigationCliOverrideEnabled: boolean; } /** @@ -41,36 +33,18 @@ function displayExpiry(expiresAt: string | null): void { } } -function displayCodeNavigationStatus( - capability: CodeNavigationCapability, - overrideEnabled: boolean, -): void { - console.log(` Code navigation: ${capability}`); - console.log(` CLI override: ${overrideEnabled ? "enabled" : "disabled"}`); -} - /** * Core auth status logic, separated for testability. */ export async function authStatusAction( deps: AuthStatusDependencies, ): Promise { - const { - authStorage, - authService, - mcpUrl, - envApiToken, - codeNavigationCliOverrideEnabled, - } = deps; + const { authStorage, authService, mcpUrl, envApiToken } = deps; // Check for env API token if (envApiToken) { console.log("Authenticated via environment variable.\n"); console.log(` Source: GITHITS_API_TOKEN`); - displayCodeNavigationStatus( - getCodeNavigationCapability(envApiToken), - codeNavigationCliOverrideEnabled, - ); return; } @@ -79,7 +53,6 @@ export async function authStatusAction( if (!auth) { console.log("Not authenticated.\n"); console.log(` Environment: ${mcpUrl}\n`); - displayCodeNavigationStatus("unknown", codeNavigationCliOverrideEnabled); console.log(""); console.log("To authenticate:"); console.log(" githits login"); @@ -96,13 +69,9 @@ export async function authStatusAction( if (refreshed) { // Reload tokens to get updated expiry const refreshedAuth = await authStorage.loadTokens(mcpUrl); - const capability = getCodeNavigationCapability( - refreshedAuth?.accessToken, - ); console.log("Authenticated (token refreshed).\n"); console.log(` Environment: ${mcpUrl}`); displayExpiry(refreshedAuth?.expiresAt ?? null); - displayCodeNavigationStatus(capability, codeNavigationCliOverrideEnabled); console.log(`\n Storage: ${authStorage.getStorageLocation()}`); return; } @@ -112,7 +81,6 @@ export async function authStatusAction( console.log( ` Expired: ${new Date(auth.expiresAt).toLocaleDateString()}\n`, ); - displayCodeNavigationStatus("unknown", codeNavigationCliOverrideEnabled); console.log(""); console.log("Run `githits login` to re-authenticate."); return; @@ -121,10 +89,6 @@ export async function authStatusAction( console.log("Authenticated.\n"); console.log(` Environment: ${mcpUrl}`); displayExpiry(auth.expiresAt); - displayCodeNavigationStatus( - getCodeNavigationCapability(auth.accessToken), - codeNavigationCliOverrideEnabled, - ); console.log(`\n Storage: ${authStorage.getStorageLocation()}`); } diff --git a/src/commands/code/code-nav-cli-helpers.ts b/src/commands/code/code-nav-cli-helpers.ts index b729828a..900a18c6 100644 --- a/src/commands/code/code-nav-cli-helpers.ts +++ b/src/commands/code/code-nav-cli-helpers.ts @@ -1,5 +1,5 @@ /** - * Shared CLI helpers for the indexing-gated `code files` / `code read` + * Shared CLI helpers for the indexed `code files` / `code read` * / `code grep` commands. Each command parses its own positionals * (because the shape varies — `[spec] [path]` vs `[spec] [pattern] * [path]`), but addressing resolution, numeric-option parsing, and @@ -23,7 +23,7 @@ import { } from "../../shared/index.js"; /** - * Fields every `pkg` indexing-gated command shares. + * Fields every indexed `code` command shares. */ export interface SharedCodeNavCliDependencies { codeNavigationService: CodeNavigationService | undefined; @@ -33,7 +33,7 @@ export interface SharedCodeNavCliDependencies { } /** - * Fields every `pkg` indexing-gated command's options carry. + * Fields every indexed `code` command's options carry. */ export interface SharedCodeNavCliOptions { repoUrl?: string; @@ -193,7 +193,7 @@ function looksLikeMissingNavpackMessage(message: string): boolean { /** * Shared error-printing + `process.exit` path used by every - * indexing-gated `pkg` command. JSON callers get the shared + * indexed `code` command. JSON callers get the shared * `{error, code, retryable, details?}` envelope on stderr; * terminal callers get a per-command renderer wrapper. * diff --git a/src/commands/code/index.test.ts b/src/commands/code/index.test.ts index 65fe2c7c..409da831 100644 --- a/src/commands/code/index.test.ts +++ b/src/commands/code/index.test.ts @@ -3,12 +3,10 @@ import { Command } from "commander"; import { registerCodeCommandGroup } from "./index.js"; describe("registerCodeCommandGroup", () => { - it("does not register code commands without override or capability", async () => { + it("does not register code commands with an explicitly empty code navigation URL", async () => { const program = new Command(); await registerCodeCommandGroup(program, { - codeNavigationUrl: "https://nav.example.com", - overrideEnabled: false, - capability: "disabled", + codeNavigationUrl: "", }); expect(program.commands.some((command) => command.name() === "code")).toBe( @@ -16,12 +14,10 @@ describe("registerCodeCommandGroup", () => { ); }); - it("registers the code command group when capability is enabled", async () => { + it("registers the code command group when code navigation URL is configured", async () => { const program = new Command(); await registerCodeCommandGroup(program, { codeNavigationUrl: "https://nav.example.com", - overrideEnabled: false, - capability: "enabled", }); const codeCommand = program.commands.find( @@ -32,48 +28,4 @@ describe("registerCodeCommandGroup", () => { codeCommand?.commands.some((command) => command.name() === "files"), ).toBe(true); }); - - it("registers the code command group when override and URL are set", async () => { - const program = new Command(); - await registerCodeCommandGroup(program, { - codeNavigationUrl: "https://nav.example.com", - overrideEnabled: true, - capability: "disabled", - }); - - const codeCommand = program.commands.find( - (command) => command.name() === "code", - ); - expect(codeCommand).toBeDefined(); - expect( - codeCommand?.commands.some((command) => command.name() === "files"), - ).toBe(true); - }); - - it("does not register the code command group for opaque env tokens without the capability claim", async () => { - const program = new Command(); - await registerCodeCommandGroup(program, { - codeNavigationUrl: "https://nav.example.com", - overrideEnabled: false, - capability: "unknown", - }); - - expect(program.commands.some((command) => command.name() === "code")).toBe( - false, - ); - }); - - it("registers the code command group for expired stored auth so direct invocation can refresh", async () => { - const program = new Command(); - await registerCodeCommandGroup(program, { - codeNavigationUrl: "https://nav.example.com", - overrideEnabled: false, - capability: "unknown", - expiredStoredAuth: true, - }); - - expect(program.commands.some((command) => command.name() === "code")).toBe( - true, - ); - }); }); diff --git a/src/commands/code/index.ts b/src/commands/code/index.ts index 5f09882b..9ba76795 100644 --- a/src/commands/code/index.ts +++ b/src/commands/code/index.ts @@ -1,5 +1,4 @@ import type { Command } from "commander"; -import type { CodeNavigationCapability } from "../../services/index.js"; import { type GatedCommandGroupOptions, resolveGatedCommandGroupRegistrationState, @@ -8,16 +7,10 @@ import { registerCodeFilesCommand } from "./files.js"; import { registerCodeGrepCommand } from "./grep.js"; import { registerCodeReadCommand } from "./read.js"; -export interface CodeCommandGroupOptions extends GatedCommandGroupOptions { - capability?: CodeNavigationCapability; -} +export interface CodeCommandGroupOptions extends GatedCommandGroupOptions {} /** - * Registers the capability-gated code-navigation command group. - * Only exposed when the token advertises `code_navigation`, the - * user sets `GITHITS_CODE_NAVIGATION=1` for local development, or - * stored auth has expired and the direct command path needs a chance - * to refresh before the CLI can re-evaluate capability. + * Registers the code-navigation command group. */ export async function registerCodeCommandGroup( program: Command, diff --git a/src/commands/docs/index.test.ts b/src/commands/docs/index.test.ts index 666e75c0..0ffbaada 100644 --- a/src/commands/docs/index.test.ts +++ b/src/commands/docs/index.test.ts @@ -3,12 +3,10 @@ import { Command } from "commander"; import { registerDocsCommandGroup } from "./index.js"; describe("registerDocsCommandGroup", () => { - it("does not register docs group without override or capability", async () => { + it("does not register docs group with an explicitly empty code navigation URL", async () => { const program = new Command(); await registerDocsCommandGroup(program, { - codeNavigationUrl: "https://pkgseer.dev", - overrideEnabled: false, - capability: "disabled", + codeNavigationUrl: "", }); expect(program.commands.some((command) => command.name() === "docs")).toBe( @@ -16,12 +14,10 @@ describe("registerDocsCommandGroup", () => { ); }); - it("registers docs group when capability is enabled", async () => { + it("registers docs group when code navigation URL is configured", async () => { const program = new Command(); await registerDocsCommandGroup(program, { codeNavigationUrl: "https://pkgseer.dev", - overrideEnabled: false, - capability: "enabled", }); const docsCommand = program.commands.find( diff --git a/src/commands/docs/index.ts b/src/commands/docs/index.ts index 19474c97..51f9ab29 100644 --- a/src/commands/docs/index.ts +++ b/src/commands/docs/index.ts @@ -1,5 +1,4 @@ import type { Command } from "commander"; -import type { CodeNavigationCapability } from "../../services/index.js"; import { type GatedCommandGroupOptions, resolveGatedCommandGroupRegistrationState, @@ -7,9 +6,7 @@ import { import { registerDocsListCommand } from "./list.js"; import { registerDocsReadCommand } from "./read.js"; -export interface DocsCommandGroupOptions extends GatedCommandGroupOptions { - capability?: CodeNavigationCapability; -} +export interface DocsCommandGroupOptions extends GatedCommandGroupOptions {} export async function registerDocsCommandGroup( program: Command, diff --git a/src/commands/gated-command-group.ts b/src/commands/gated-command-group.ts index 870404e4..845b4632 100644 --- a/src/commands/gated-command-group.ts +++ b/src/commands/gated-command-group.ts @@ -1,17 +1,7 @@ -import { resolveStartupCodeNavigationRegistrationState } from "../container.js"; -import { - type CodeNavigationCapability, - getCodeNavigationUrl, - getEnvApiToken, - isCodeNavigationCliOverrideEnabled, -} from "../services/index.js"; +import { getCodeNavigationUrl } from "../services/index.js"; export interface GatedCommandGroupOptions { codeNavigationUrl?: string; - overrideEnabled?: boolean; - capability?: CodeNavigationCapability; - envTokenPresent?: boolean; - expiredStoredAuth?: boolean; } export interface GatedCommandGroupRegistrationState { @@ -23,27 +13,9 @@ export async function resolveGatedCommandGroupRegistrationState( options: GatedCommandGroupOptions = {}, ): Promise { const codeNavigationUrl = options.codeNavigationUrl ?? getCodeNavigationUrl(); - if (!codeNavigationUrl) { - return { codeNavigationUrl: undefined, shouldRegister: false }; - } - - const overrideEnabled = - options.overrideEnabled ?? isCodeNavigationCliOverrideEnabled(); - const registrationState = - options.capability !== undefined || options.expiredStoredAuth !== undefined - ? { - capability: options.capability ?? "unknown", - expiredStoredAuth: options.expiredStoredAuth ?? false, - } - : await resolveStartupCodeNavigationRegistrationState(); - const envTokenPresent = options.envTokenPresent ?? Boolean(getEnvApiToken()); return { codeNavigationUrl, - shouldRegister: - overrideEnabled || - registrationState.capability === "enabled" || - envTokenPresent || - registrationState.expiredStoredAuth, + shouldRegister: codeNavigationUrl.length > 0, }; } diff --git a/src/commands/mcp-instructions.test.ts b/src/commands/mcp-instructions.test.ts index cfc2229a..cc5d3bcb 100644 --- a/src/commands/mcp-instructions.test.ts +++ b/src/commands/mcp-instructions.test.ts @@ -10,10 +10,7 @@ import { createMockPackageIntelligenceService, } from "../services/test-helpers.js"; import { getMcpToolDefinitions } from "./mcp.js"; -import { - buildMcpInstructions, - isPackageToolsCapabilityOpen, -} from "./mcp-instructions.js"; +import { buildMcpInstructions } from "./mcp-instructions.js"; function createTestDeps(overrides: Partial = {}): Dependencies { return { @@ -26,22 +23,14 @@ function createTestDeps(overrides: Partial = {}): Dependencies { apiToken: "test-token", hasValidToken: true, envApiToken: undefined, - codeNavigationCapability: "disabled", - codeNavigationCliOverrideEnabled: false, - codeNavigationUrl: undefined, - codeNavigationService: undefined, - packageIntelligenceService: undefined, + codeNavigationUrl: "https://pkgseer.dev", + codeNavigationService: createMockCodeNavigationService(), + packageIntelligenceService: createMockPackageIntelligenceService(), githitsService: createMockGitHitsService(), ...overrides, }; } -/** - * Tool names this CLI knows how to register. Each is separately - * probed by the mention↔registration invariant so the composer - * cannot advertise a tool that `getMcpToolDefinitions` omits for - * the same deps. - */ const KNOWN_TOOLS = [ "search", "get_example", @@ -73,70 +62,13 @@ function registeredTools(deps: Dependencies): Set { return new Set(getMcpToolDefinitions(deps).map((tool) => tool.name)); } -describe("isPackageToolsCapabilityOpen", () => { - it("is true when capability is enabled", () => { - const deps = createTestDeps({ codeNavigationCapability: "enabled" }); - expect(isPackageToolsCapabilityOpen(deps)).toBe(true); - }); - - it("is false when capability is disabled", () => { - const deps = createTestDeps({ codeNavigationCapability: "disabled" }); - expect(isPackageToolsCapabilityOpen(deps)).toBe(false); - }); - - it("is true when local override is enabled", () => { - const deps = createTestDeps({ - codeNavigationCapability: "disabled", - codeNavigationCliOverrideEnabled: true, - }); - expect(isPackageToolsCapabilityOpen(deps)).toBe(true); - }); - - it("is true when capability unknown but env token provides opaque grant", () => { - const deps = createTestDeps({ - codeNavigationCapability: "unknown", - envApiToken: "ghi-opaque-token", - }); - expect(isPackageToolsCapabilityOpen(deps)).toBe(true); - }); - - it("is false when capability unknown and no env token", () => { - const deps = createTestDeps({ - codeNavigationCapability: "unknown", - envApiToken: undefined, - }); - expect(isPackageToolsCapabilityOpen(deps)).toBe(false); - }); -}); - describe("buildMcpInstructions", () => { - it("returns only the core block when gate is closed", () => { + it("returns core + package/code tools section by default", () => { const deps = createTestDeps(); const instructions = buildMcpInstructions(deps); - expect(instructions).toContain("GitHits surfaces verified"); - expect(instructions).toContain("search_language"); - expect(instructions).toContain("feedback"); - expect(instructions).toContain("get_example"); - expect(instructions).not.toContain("Package tools"); - expect(instructions).not.toContain("pkg_info"); - expect(instructions).not.toContain("pkg_vulns"); - expect(instructions).not.toContain("pkg_deps"); - expect(instructions).not.toContain("pkg_changelog"); - expect(instructions).not.toContain("search_status"); - }); - - it("returns core + package-tools section when capability enabled and both services wired", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://nav.example.com", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); - const instructions = buildMcpInstructions(deps); - - expect(instructions).toContain("GitHits surfaces verified"); - expect(instructions).toContain("Package tools"); + expect(instructions).toContain("GitHits provides verified"); + expect(instructions).toContain("Indexed package/source tools"); expect(instructions).toContain("`pkg_info`"); expect(instructions).toContain("`docs_list`"); expect(instructions).toContain("`docs_read`"); @@ -145,22 +77,8 @@ describe("buildMcpInstructions", () => { expect(instructions).toContain("`pkg_changelog`"); expect(instructions).toContain("`search`"); expect(instructions).toContain("`search_status`"); - expect(instructions).toContain("allow_partial_results"); - expect(instructions).toContain("partial hits"); - // Reference-first and multi-turn strategy tips appear when - // code-navigation is wired so agents do not pull raw source into - // the main conversation by default. expect(instructions).toContain("reference-first"); expect(instructions).toContain("Delegate multi-call work to a sub-agent"); - expect(instructions).toContain("inherently multi-call"); - // Delegation framing leads the gated section so it lands before - // the per-tool bullets. - const multiTurnIdx = instructions.indexOf( - "Delegate multi-call work to a sub-agent", - ); - const firstBulletIdx = instructions.indexOf("- `"); - expect(multiTurnIdx).toBeGreaterThan(0); - expect(firstBulletIdx).toBeGreaterThan(multiTurnIdx); }); it("expands core trigger criteria to cover comparative cross-OSS questions", () => { @@ -170,158 +88,43 @@ describe("buildMcpInstructions", () => { expect(instructions).toContain("how a real codebase implements"); }); - it("does not surface the strategy tips when code-navigation is not wired", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - packageIntelligenceService: createMockPackageIntelligenceService(), - }); - const instructions = buildMcpInstructions(deps); - expect(instructions).not.toContain("reference-first"); - expect(instructions).not.toContain("Delegate multi-call work"); - }); - - it("keeps the core block first when both sections are present", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); + it("keeps the core block first", () => { + const deps = createTestDeps(); const instructions = buildMcpInstructions(deps); - const coreIdx = instructions.indexOf("GitHits surfaces verified"); - const gatedIdx = instructions.indexOf("Package tools"); + const coreIdx = instructions.indexOf("GitHits provides verified"); + const packageToolsIdx = instructions.indexOf( + "Indexed package/source tools", + ); expect(coreIdx).toBeGreaterThanOrEqual(0); - expect(gatedIdx).toBeGreaterThan(coreIdx); + expect(packageToolsIdx).toBeGreaterThan(coreIdx); }); - it("omits the package-tools section when capability enabled but no services wired", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationService: undefined, - packageIntelligenceService: undefined, - }); - const instructions = buildMcpInstructions(deps); - - expect(instructions).not.toContain("Package tools"); - }); - - it("half-open: only code navigation service wired → mentions search/search_status but not package_summary", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: undefined, - }); - const instructions = buildMcpInstructions(deps); - - expect(instructions).toContain("Package tools"); - expect(instructions).toContain("`search`"); - expect(instructions).toContain("`search_status`"); - expect(instructions).not.toContain("`pkg_info`"); - expect(instructions).toContain("canonical example retrieval"); - }); - - it("half-open: only package intelligence service wired → mentions every package tool but not unified search", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationService: undefined, - packageIntelligenceService: createMockPackageIntelligenceService(), - }); - const instructions = buildMcpInstructions(deps); + it("keeps mentioned package/code tools aligned with registration", () => { + const deps = createTestDeps(); + const mentioned = mentionedTools(buildMcpInstructions(deps)); + const registered = registeredTools(deps); - expect(instructions).toContain("Package tools"); - expect(instructions).toContain("`pkg_info`"); - expect(instructions).toContain("`docs_list`"); - expect(instructions).toContain("`docs_read`"); - expect(instructions).toContain("`pkg_vulns`"); - expect(instructions).toContain("`pkg_deps`"); - expect(instructions).toContain("`pkg_changelog`"); - expect(instructions).not.toContain("`search_status`"); - // The decision tip references unified search, so it must not - // appear when unified search isn't registered. - expect(instructions).not.toContain("canonical example retrieval"); - }); + for (const name of mentioned) { + expect(registered.has(name)).toBe(true); + } - describe("mention↔registration invariant", () => { - const scenarios: { label: string; overrides: Partial }[] = [ - { label: "gate closed", overrides: {} }, - { - label: "gate open, no services wired", - overrides: { codeNavigationCapability: "enabled" }, - }, - { - label: "gate open, only code navigation service", - overrides: { - codeNavigationCapability: "enabled", - codeNavigationService: createMockCodeNavigationService(), - }, - }, - { - label: "override enabled, both services wired", - overrides: { - codeNavigationCliOverrideEnabled: true, - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }, - }, - { - label: "gate open, only package intelligence service", - overrides: { - codeNavigationCapability: "enabled", - packageIntelligenceService: createMockPackageIntelligenceService(), - }, - }, - { - label: "gate open, both services", - overrides: { - codeNavigationCapability: "enabled", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }, - }, - { - label: "opaque env token, both services", - overrides: { - codeNavigationCapability: "unknown", - envApiToken: "ghi-opaque-token", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }, - }, + const packageAndCodeTools = [ + "search", + "search_status", + "code_files", + "code_read", + "code_grep", + "docs_list", + "docs_read", + "pkg_info", + "pkg_vulns", + "pkg_deps", + "pkg_changelog", ]; - - for (const { label, overrides } of scenarios) { - it(`${label}: every mentioned tool is registered, and every package tool registered is mentioned`, () => { - const deps = createTestDeps(overrides); - const mentioned = mentionedTools(buildMcpInstructions(deps)); - const registered = registeredTools(deps); - - // Forward: no ghost mentions. - for (const name of mentioned) { - expect(registered.has(name)).toBe(true); - } - // Reverse: every package tool that is registered must also - // be mentioned in the composed instructions. Core tools - // (search/search_language/feedback) are exempt — the core - // block narrates the workflow rather than bulleting them - // individually, so `search_language` and `feedback` won't - // appear in backtick form. - const packageTools = [ - "search", - "search_status", - "code_files", - "code_read", - "code_grep", - "pkg_info", - "pkg_vulns", - "pkg_deps", - "pkg_changelog", - ]; - for (const name of packageTools) { - if (registered.has(name)) { - expect(mentioned.has(name)).toBe(true); - } - } - }); + for (const name of packageAndCodeTools) { + expect(registered.has(name)).toBe(true); + expect(mentioned.has(name)).toBe(true); } }); }); diff --git a/src/commands/mcp-instructions.ts b/src/commands/mcp-instructions.ts index e4bda816..30a6471e 100644 --- a/src/commands/mcp-instructions.ts +++ b/src/commands/mcp-instructions.ts @@ -9,17 +9,15 @@ import type { Dependencies } from "../container.js"; * their own. Per upstream guidance it should stay terse: anything * that belongs to a single tool lives in that tool's description. * - * Composition mirrors tool registration in `mcp.ts`. Each bullet is - * emitted only when its backing service is wired, so MCP clients - * never see guidance for a tool that isn't registered in the - * current session. + * Composition mirrors tool registration in `mcp.ts` so clients see + * guidance for the same always-on tool surface the server registers. */ -const CORE_BLOCK = `GitHits surfaces verified, canonical code examples from global open source. Use it when you're stuck, the user is frustrated by repeated failed attempts, you need up-to-date API usage, the question is comparative across OSS projects (e.g. "how does X vs Y handle Z"), the answer requires reading how a real codebase implements a feature, or the user mentions GitHits. +const CORE_BLOCK = `GitHits provides verified, canonical code examples from global open source. Use it when you're stuck, repeated attempts failed, you need up-to-date API usage, the question is comparative across OSS projects (e.g. "how does X vs Y handle Z"), the answer requires reading how a real codebase implements a feature, or the user mentions GitHits. -Workflow: call \`get_example\` with one focused question, optionally passing \`language\` when the desired language is known; call \`search_language\` first only if you need to force a language and the exact name is uncertain. Send \`feedback\` on the returned solution_id so quality improves. Each search addresses a single issue; reuse context from prior results before re-searching.`; +Example workflow: call \`get_example\` with one focused question; pass \`language\` only when you know the exact language name, otherwise call \`search_language\` first. Send \`feedback\` on the returned solution_id. Reuse prior results before searching again.`; -const PACKAGE_TOOLS_PREAMBLE = `Package tools work with third-party dependency source plus registry metadata. Use them when a stack trace points into a dependency, you need to verify how a library actually works, or you're evaluating whether to add or upgrade a package. +const PACKAGE_TOOLS_PREAMBLE = `Indexed package/source tools inspect third-party dependency source, docs, and registry metadata. Use them when a stack trace points into a dependency, you need to verify how a library works, or you're evaluating whether to add or upgrade a package. Package spec: \`registry:name[@version]\`.`; @@ -27,116 +25,76 @@ const PKG_INFO_BULLET = "- `pkg_info` — instant package overview: latest version, license, downloads, quickstart, and active advisory count."; const DOCS_LIST_BULLET = - "- `docs_list` — browse mixed package documentation pages from hosted docs and repository-backed docs. Each entry includes a stable pageId, source kind, source URL, and for repo docs exact file follow-up metadata."; + "- `docs_list` — browse hosted and repository-backed package docs. Entries include stable pageIds, source URLs, and repo-file follow-up metadata when available."; const DOCS_READ_BULLET = "- `docs_read` — read a documentation page by pageId. Works for both hosted docs and repo-backed docs."; const PKG_VULNS_BULLET = - "- `pkg_vulns` — known CVE / OSV advisories for npm, PyPI, Hex, or Crates packages (optionally pinned to `@version`). Malicious-package advisories surface in a disjoint `malware` bucket; filter with `min_severity` or include retracted advisories with `include_withdrawn`."; + "- `pkg_vulns` — known CVE / OSV advisories for npm, PyPI, Hex, or Crates packages, optionally pinned to `@version`. Filter with `min_severity`; include retracted advisories with `include_withdrawn`."; const PKG_DEPS_BULLET = - "- `pkg_deps` — direct runtime deps plus, when the backend has them, dev / peer / optional / feature groups. Pass `lifecycle` to filter groups server-side, `include_transitive` for the full graph, and `include_importers` when you also need per-package provenance. Supports npm, PyPI, Hex, Crates, vcpkg, and Zig."; + "- `pkg_deps` — direct runtime deps plus lifecycle/feature groups when available. Use `lifecycle` to filter groups, `include_transitive` for the full graph, and `include_importers` for provenance. Supports npm, PyPI, Hex, Crates, vcpkg, and Zig."; const PKG_CHANGELOG_BULLET = - "- `pkg_changelog` — release notes for a package or GitHub repo, newest-first. Default latest mode returns the 10 most recent entries with full markdown bodies; `from_version` switches to range mode between two versions. Addressable via `registry` + `package_name` or `repo_url`. Set `include_bodies: false` for a version / date / URL timeline when bodies aren't needed."; + "- `pkg_changelog` — release notes for a package or GitHub repo, newest-first. Default latest mode returns recent entries with markdown bodies; `from_version` switches to range mode. Set `include_bodies: false` for a compact timeline."; const SEARCH_BULLET = - '- `search` — unified search across indexed dependency code, docs, and explicit symbols. Structured fields are the primary UX; omit `sources` for AUTO. Default response is a compact text listing (`text-v1`); pass `format: "json"` for the structured envelope with full locator fields, highlights, and source status. Returns only trustworthy complete results by default; opt into partial hits with `allow_partial_results: true`. If indexing is still in progress, the response carries a `searchRef`.'; + '- `search` — unified search across indexed dependency code, docs, and symbols. Omit `sources` for AUTO. Default output is compact `text-v1`; pass `format: "json"` for locators, highlights, and source status. Complete by default; opt into partial hits with `allow_partial_results: true`. Incomplete responses carry a `searchRef`.'; const SEARCH_STATUS_BULLET = - "- `search_status` — follow up a prior unified search by `searchRef`. Use it after `search` returns incomplete state to check progress, fetch partial hits when the original request used `allow_partial_results: true`, or fetch final results."; + "- `search_status` — follow up a prior `search` by `searchRef` to check progress, fetch partial hits, or fetch final results."; const CODE_FILES_BULLET = - '- `code_files` — discover what files a dependency ships. Use `path_prefix` to scope to a subdirectory. Default response is a paths-only listing (`text-v1`); pass `format: "json"` for the full envelope with each file\'s language, type, and byte size. Returned paths feed directly into `code_read` and help scope `code_grep`.'; + '- `code_files` — list files in an indexed dependency. Use `path_prefix` to scope to a directory. Returned paths feed into `code_read` and help scope `code_grep`; pass `format: "json"` for language/type/size metadata.'; const CODE_READ_BULLET = - "- `code_read` — fetch a file's contents from a dependency. Pass the same `path` emitted by `code_files`. **MCP cap: 150 lines per call** — broader requests (or no range) silently truncate to the first 150 lines from your start, with a `hint` describing what was returned vs. requested. Pick a focused window from a `search` / `code_grep` match. Binary files set `isBinary: true` and omit `content`. A `FILE_NOT_FOUND` (or `NOT_FOUND`) response is the signal to call `code_files` for the actual path."; + "- `code_read` — read a dependency file by `path`. **MCP cap: 150 lines per call**; choose focused `start_line` / `end_line` windows from `search` or `code_grep`. Binary files set `isBinary: true` and omit `content`. On `FILE_NOT_FOUND` / `NOT_FOUND`, call `code_files` for the actual path."; const CODE_GREP_BULLET = - '- `code_grep` — deterministic text grep over indexed source files. Use it when you know the exact text or regex to match; use `search` for discovery. Whole-target grep is the default; narrow with `path`, `path_prefix`, `globs`, or `extensions`. Default response groups matches by file with line numbers (`text-v1`); pass `format: "json"` for the `matches[]` array with byte offsets and symbol metadata. Each match\'s `path:line` chains directly into `code_read`.'; + "- `code_grep` — deterministic text or regex grep over indexed source. Use it when you know the pattern; use `search` for discovery. Narrow with `path`, `path_prefix`, `globs`, or `extensions`. Each match's `path:line` chains into `code_read`."; const SEARCH_VS_SYMBOLS_TIP = 'Prefer `get_example` for canonical example retrieval; prefer unified `search` for indexed dependency and repository discovery; use `sources:["symbol"]` when you want symbol-shaped results. Use `code_grep` for deterministic text matching and `code_read` for focused-window inspection of a known file.'; const REFERENCE_FIRST_TIP = - "Strategy — reference-first, content-second. Locate symbols and lines with `search` / `code_grep` first, then read only the lines you actually need with `code_read` using explicit `start_line` / `end_line` windows around the match (typically 80-150 lines). The MCP `code_read` surface caps each call at 150 lines; bigger requests are silently truncated. Each turn — including retries to widen or re-narrow — costs context, so pick a focused window the first time rather than starting wide and trimming."; + "Strategy — reference-first, content-second. Locate symbols and lines with `search` / `code_grep` first, then read only the needed window with `code_read` using explicit `start_line` / `end_line` values, typically 80-150 lines around the match. The MCP `code_read` surface caps each call at 150 lines."; const MULTI_TURN_TIP = - '**Delegate multi-call work to a sub-agent.** Code-navigation tools (`search`, `code_grep`, `code_read`, `code_files`) are inherently multi-call — answering even simple questions usually takes 3-10 tool calls, and reading raw source into the main conversation compounds quickly. The default approach for any cross-project comparison, codebase mapping, pattern survey, or "how does X actually work" investigation is to spawn a sub-task / sub-agent that does the digging and returns only a compact synthesis. Do the work in the main conversation only when the result genuinely belongs there (e.g., the user asked for a specific snippet to paste into their own code). When in doubt, delegate.'; - -/** - * Whether the MCP session should register and describe package tools. - * - * Agents must not see tools or guidance that would silently fail, so - * only an explicit `code_navigation` capability enables the surface. - * - * Single source of truth — tool registration in `mcp.ts` and the - * package-tools fragment in `buildMcpInstructions` must share this - * predicate to prevent drift between what's advertised and what's - * documented. - */ -export function isPackageToolsCapabilityOpen(deps: Dependencies): boolean { - return ( - deps.codeNavigationCliOverrideEnabled || - deps.codeNavigationCapability === "enabled" || - (deps.codeNavigationCapability === "unknown" && - deps.envApiToken !== undefined) - ); -} + '**Delegate multi-call work to a sub-agent.** Code-navigation work (`search`, `code_grep`, `code_read`, `code_files`) often takes 3-10 calls. For cross-project comparisons, codebase mapping, pattern surveys, or "how does X actually work" investigations, spawn a sub-task/sub-agent and ask for a compact synthesis. Do it inline only when the raw snippet belongs in the main conversation.'; /** * Build the server-level instructions string for the current session. * - * Emits the core block unconditionally. Appends a package-tools - * section composed of a preamble plus one bullet per service that - * is actually wired. Mirrors `getMcpToolDefinitions` so the - * instructions never reference a tool that isn't registered, even - * in half-open states where only one service is available. + * Emits the core block plus the package/code-tools section. + * Mirrors `getMcpToolDefinitions` so the instructions stay aligned + * with the registered tool surface. */ export function buildMcpInstructions(deps: Dependencies): string { const sections = [CORE_BLOCK]; - if (!isPackageToolsCapabilityOpen(deps)) { - return sections.join("\n\n"); - } - const bullets: string[] = []; - if (deps.packageIntelligenceService) { - bullets.push(DOCS_LIST_BULLET); - bullets.push(DOCS_READ_BULLET); - bullets.push(PKG_INFO_BULLET); - bullets.push(PKG_VULNS_BULLET); - bullets.push(PKG_DEPS_BULLET); - bullets.push(PKG_CHANGELOG_BULLET); - } - if (deps.codeNavigationService) { - bullets.push(SEARCH_BULLET); - bullets.push(SEARCH_STATUS_BULLET); - bullets.push(CODE_FILES_BULLET); - bullets.push(CODE_READ_BULLET); - bullets.push(CODE_GREP_BULLET); - } - - if (bullets.length === 0) { - return sections.join("\n\n"); - } + bullets.push(DOCS_LIST_BULLET); + bullets.push(DOCS_READ_BULLET); + bullets.push(PKG_INFO_BULLET); + bullets.push(PKG_VULNS_BULLET); + bullets.push(PKG_DEPS_BULLET); + bullets.push(PKG_CHANGELOG_BULLET); + bullets.push(SEARCH_BULLET); + bullets.push(SEARCH_STATUS_BULLET); + bullets.push(CODE_FILES_BULLET); + bullets.push(CODE_READ_BULLET); + bullets.push(CODE_GREP_BULLET); const parts = [PACKAGE_TOOLS_PREAMBLE]; - // The multi-turn tip leads when code-navigation is wired — it is - // the highest-leverage decision the agent makes (delegate vs. - // run inline) and reading it before the per-tool bullets means - // the framing arrives before any specific tool fires. The - // reference-first / decision tips follow the bullets where they - // act as workflow guidance for the chosen tool. - if (deps.codeNavigationService) { - parts.push(MULTI_TURN_TIP); - } + // Lead with delegation because it is the highest-leverage decision + // for code-navigation work. The reference-first / decision tips + // follow the bullets where they act as workflow guidance for the + // chosen tool. + parts.push(MULTI_TURN_TIP); parts.push(bullets.join("\n")); - if (deps.codeNavigationService) { - parts.push(REFERENCE_FIRST_TIP); - parts.push(SEARCH_VS_SYMBOLS_TIP); - } + parts.push(REFERENCE_FIRST_TIP); + parts.push(SEARCH_VS_SYMBOLS_TIP); sections.push(parts.join("\n\n")); return sections.join("\n\n"); diff --git a/src/commands/mcp.test.ts b/src/commands/mcp.test.ts index 61a7aa53..d3f77095 100644 --- a/src/commands/mcp.test.ts +++ b/src/commands/mcp.test.ts @@ -30,11 +30,9 @@ function createTestDeps(overrides: Partial = {}): Dependencies { apiToken: "test-token", hasValidToken: true, envApiToken: undefined, - codeNavigationCapability: "disabled", - codeNavigationCliOverrideEnabled: false, - codeNavigationUrl: undefined, - codeNavigationService: undefined, - packageIntelligenceService: undefined, + codeNavigationUrl: "https://pkgseer.dev", + codeNavigationService: createMockCodeNavigationService(), + packageIntelligenceService: createMockPackageIntelligenceService(), githitsService: createMockGitHitsService(), ...overrides, }; @@ -49,31 +47,23 @@ describe("createMcpServer", () => { expect(server).toBeDefined(); }); - it("creates server with instructions wired in the gate-open state", () => { + it("creates server with instructions wired", () => { // Exercises the composer through createMcpServer so any breakage // in the instructions pipeline (composer import, SDK options // shape) surfaces here even though the SDK hides `instructions` // behind a private field. - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://pkgseer.dev", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); + const deps = createTestDeps(); const server = createMcpServer(deps); expect(server).toBeDefined(); }); - it("adds unified search tools when capability is enabled", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://nav.example.com", - codeNavigationService: createMockCodeNavigationService(), - }); + it("adds unified search tools by default", () => { + const deps = createTestDeps(); const tools = getMcpToolDefinitions(deps); - expect(tools.map((tool) => tool.name)).toEqual([ + const names = tools.map((tool) => tool.name); + for (const name of [ "get_example", "search_language", "feedback", @@ -82,29 +72,13 @@ describe("createMcpServer", () => { "code_files", "code_read", "code_grep", - ]); - }); - - it("adds unified search tools for opaque env tokens when the gate is otherwise open", () => { - const deps = createTestDeps({ - envApiToken: "ghi-opaque-token", - codeNavigationCapability: "unknown", - codeNavigationUrl: "https://nav.example.com", - codeNavigationService: createMockCodeNavigationService(), - }); - - const tools = getMcpToolDefinitions(deps); - expect(tools.some((tool) => tool.name === "search")).toBe(true); - expect(tools.some((tool) => tool.name === "search_status")).toBe(true); + ]) { + expect(names).toContain(name); + } }); - it("adds package_summary when capability is enabled and service wired", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://pkgseer.dev", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); + it("adds package_summary by default", () => { + const deps = createTestDeps(); const tools = getMcpToolDefinitions(deps); expect(tools.map((tool) => tool.name)).toContain("docs_list"); @@ -112,61 +86,8 @@ describe("createMcpServer", () => { expect(tools.map((tool) => tool.name)).toContain("pkg_info"); }); - it("omits package_summary when capability is disabled", () => { - const deps = createTestDeps({ - codeNavigationCapability: "disabled", - codeNavigationUrl: "https://pkgseer.dev", - packageIntelligenceService: createMockPackageIntelligenceService(), - }); - - const tools = getMcpToolDefinitions(deps); - expect(tools.map((tool) => tool.name)).not.toContain("pkg_info"); - }); - - it("omits package_summary when service is missing even if capability enabled", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://pkgseer.dev", - packageIntelligenceService: undefined, - }); - - const tools = getMcpToolDefinitions(deps); - expect(tools.map((tool) => tool.name)).not.toContain("pkg_info"); - }); - - it("adds package_summary for opaque env tokens when the gate is otherwise open", () => { - const deps = createTestDeps({ - envApiToken: "ghi-opaque-token", - codeNavigationCapability: "unknown", - codeNavigationUrl: "https://pkgseer.dev", - packageIntelligenceService: createMockPackageIntelligenceService(), - }); - - const tools = getMcpToolDefinitions(deps); - expect(tools.some((tool) => tool.name === "pkg_info")).toBe(true); - }); - - it("adds package and code-nav tools when local override is enabled", () => { - const deps = createTestDeps({ - codeNavigationCliOverrideEnabled: true, - codeNavigationUrl: "https://pkgseer.dev", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); - - const names = getMcpToolDefinitions(deps).map((tool) => tool.name); - expect(names).toContain("search"); - expect(names).toContain("docs_list"); - expect(names).toContain("docs_read"); - }); - - it("preserves half-open invariant: whenever package_summary is advertised, unified search is too (enabled path)", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://pkgseer.dev", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); + it("advertises package_summary with unified search", () => { + const deps = createTestDeps(); const names = getMcpToolDefinitions(deps).map((t) => t.name); if (names.includes("pkg_info")) { @@ -175,36 +96,15 @@ describe("createMcpServer", () => { } }); - it("adds package_vulnerabilities when capability is enabled and service wired", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://pkgseer.dev", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); + it("adds package_vulnerabilities by default", () => { + const deps = createTestDeps(); const tools = getMcpToolDefinitions(deps); expect(tools.map((tool) => tool.name)).toContain("pkg_vulns"); }); - it("omits package_vulnerabilities when capability is disabled", () => { - const deps = createTestDeps({ - codeNavigationCapability: "disabled", - codeNavigationUrl: "https://pkgseer.dev", - packageIntelligenceService: createMockPackageIntelligenceService(), - }); - - const tools = getMcpToolDefinitions(deps); - expect(tools.map((tool) => tool.name)).not.toContain("pkg_vulns"); - }); - it("advertises package_summary and package_vulnerabilities together (shared predicate)", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://pkgseer.dev", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); + const deps = createTestDeps(); const names = getMcpToolDefinitions(deps).map((t) => t.name); if (names.includes("pkg_info")) { @@ -212,35 +112,15 @@ describe("createMcpServer", () => { } }); - it("adds package_dependencies when capability is enabled and service wired", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://pkgseer.dev", - packageIntelligenceService: createMockPackageIntelligenceService(), - }); + it("adds package_dependencies by default", () => { + const deps = createTestDeps(); const tools = getMcpToolDefinitions(deps); expect(tools.map((tool) => tool.name)).toContain("pkg_deps"); }); - it("omits package_dependencies when capability is disabled", () => { - const deps = createTestDeps({ - codeNavigationCapability: "disabled", - codeNavigationUrl: "https://pkgseer.dev", - packageIntelligenceService: createMockPackageIntelligenceService(), - }); - - const tools = getMcpToolDefinitions(deps); - expect(tools.map((tool) => tool.name)).not.toContain("pkg_deps"); - }); - it("advertises every package tool together (shared predicate covers deps too)", () => { - const deps = createTestDeps({ - codeNavigationCapability: "enabled", - codeNavigationUrl: "https://pkgseer.dev", - codeNavigationService: createMockCodeNavigationService(), - packageIntelligenceService: createMockPackageIntelligenceService(), - }); + const deps = createTestDeps(); const names = getMcpToolDefinitions(deps).map((t) => t.name); if (names.includes("pkg_info")) { diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts index 641b3f62..e602bb49 100644 --- a/src/commands/mcp.ts +++ b/src/commands/mcp.ts @@ -26,10 +26,7 @@ import { type ToolDefinition, type ZodRawShape, } from "../tools/index.js"; -import { - buildMcpInstructions, - isPackageToolsCapabilityOpen, -} from "./mcp-instructions.js"; +import { buildMcpInstructions } from "./mcp-instructions.js"; /** * Returns the MCP tools enabled for the current startup state. @@ -43,38 +40,31 @@ export function getMcpToolDefinitions( eraseTool(createFeedbackTool(deps.githitsService)), ]; - const gateOpen = isPackageToolsCapabilityOpen(deps); - - if (gateOpen && deps.codeNavigationService) { - tools.push(eraseTool(createSearchTool(deps.codeNavigationService))); - tools.push(eraseTool(createSearchStatusTool(deps.codeNavigationService))); - tools.push(eraseTool(createListFilesTool(deps.codeNavigationService))); - tools.push(eraseTool(createReadFileTool(deps.codeNavigationService))); - tools.push(eraseTool(createGrepRepoTool(deps.codeNavigationService))); - } - - if (gateOpen && deps.packageIntelligenceService) { - tools.push( - eraseTool(createListPackageDocsTool(deps.packageIntelligenceService)), - ); - tools.push( - eraseTool(createReadPackageDocTool(deps.packageIntelligenceService)), - ); - tools.push( - eraseTool(createPackageSummaryTool(deps.packageIntelligenceService)), - ); - tools.push( - eraseTool( - createPackageVulnerabilitiesTool(deps.packageIntelligenceService), - ), - ); - tools.push( - eraseTool(createPackageDependenciesTool(deps.packageIntelligenceService)), - ); - tools.push( - eraseTool(createPackageChangelogTool(deps.packageIntelligenceService)), - ); - } + tools.push(eraseTool(createSearchTool(deps.codeNavigationService))); + tools.push(eraseTool(createSearchStatusTool(deps.codeNavigationService))); + tools.push(eraseTool(createListFilesTool(deps.codeNavigationService))); + tools.push(eraseTool(createReadFileTool(deps.codeNavigationService))); + tools.push(eraseTool(createGrepRepoTool(deps.codeNavigationService))); + tools.push( + eraseTool(createListPackageDocsTool(deps.packageIntelligenceService)), + ); + tools.push( + eraseTool(createReadPackageDocTool(deps.packageIntelligenceService)), + ); + tools.push( + eraseTool(createPackageSummaryTool(deps.packageIntelligenceService)), + ); + tools.push( + eraseTool( + createPackageVulnerabilitiesTool(deps.packageIntelligenceService), + ), + ); + tools.push( + eraseTool(createPackageDependenciesTool(deps.packageIntelligenceService)), + ); + tools.push( + eraseTool(createPackageChangelogTool(deps.packageIntelligenceService)), + ); return tools; } @@ -211,7 +201,7 @@ export function registerMcpCommand(program: Command) { When run interactively (TTY), shows setup instructions. When run via stdio (non-TTY), starts the MCP server. -Available tools depend on the current authentication state and enabled features.`, +Authenticated tool calls require a valid GitHits token.`, ) .action(async () => { if (process.stdout.isTTY && process.stdin.isTTY) { diff --git a/src/commands/pkg/index.test.ts b/src/commands/pkg/index.test.ts index 6dbfeed5..b782ecc3 100644 --- a/src/commands/pkg/index.test.ts +++ b/src/commands/pkg/index.test.ts @@ -3,12 +3,10 @@ import { Command } from "commander"; import { registerPkgCommandGroup } from "./index.js"; describe("registerPkgCommandGroup", () => { - it("does not register the pkg group without override or capability", async () => { + it("does not register the pkg group with an explicitly empty code navigation URL", async () => { const program = new Command(); await registerPkgCommandGroup(program, { - codeNavigationUrl: "https://pkgseer.dev", - overrideEnabled: false, - capability: "disabled", + codeNavigationUrl: "", }); expect(program.commands.some((command) => command.name() === "pkg")).toBe( @@ -16,12 +14,10 @@ describe("registerPkgCommandGroup", () => { ); }); - it("registers the pkg command group when capability is enabled", async () => { + it("registers the pkg command group when code navigation URL is configured", async () => { const program = new Command(); await registerPkgCommandGroup(program, { codeNavigationUrl: "https://pkgseer.dev", - overrideEnabled: false, - capability: "enabled", }); const pkgCommand = program.commands.find( @@ -38,48 +34,4 @@ describe("registerPkgCommandGroup", () => { pkgCommand?.commands.some((command) => command.name() === "deps"), ).toBe(true); }); - - it("registers the pkg command group when override and URL are set", async () => { - const program = new Command(); - await registerPkgCommandGroup(program, { - codeNavigationUrl: "https://pkgseer.dev", - overrideEnabled: true, - capability: "disabled", - }); - - const pkgCommand = program.commands.find( - (command) => command.name() === "pkg", - ); - expect(pkgCommand).toBeDefined(); - expect( - pkgCommand?.commands.some((command) => command.name() === "info"), - ).toBe(true); - }); - - it("does not register the pkg command group for opaque env tokens without the capability claim", async () => { - const program = new Command(); - await registerPkgCommandGroup(program, { - codeNavigationUrl: "https://pkgseer.dev", - overrideEnabled: false, - capability: "unknown", - }); - - expect(program.commands.some((command) => command.name() === "pkg")).toBe( - false, - ); - }); - - it("registers the pkg command group for expired stored auth so direct invocation can refresh", async () => { - const program = new Command(); - await registerPkgCommandGroup(program, { - codeNavigationUrl: "https://pkgseer.dev", - overrideEnabled: false, - capability: "unknown", - expiredStoredAuth: true, - }); - - expect(program.commands.some((command) => command.name() === "pkg")).toBe( - true, - ); - }); }); diff --git a/src/commands/pkg/index.ts b/src/commands/pkg/index.ts index 0a4a1ace..d0c161be 100644 --- a/src/commands/pkg/index.ts +++ b/src/commands/pkg/index.ts @@ -1,5 +1,4 @@ import type { Command } from "commander"; -import type { CodeNavigationCapability } from "../../services/index.js"; import { type GatedCommandGroupOptions, resolveGatedCommandGroupRegistrationState, @@ -9,26 +8,10 @@ import { registerPkgDepsCommand } from "./deps.js"; import { registerPkgInfoCommand } from "./info.js"; import { registerPkgVulnsCommand } from "./vulns.js"; -export interface PkgCommandGroupOptions extends GatedCommandGroupOptions { - capability?: CodeNavigationCapability; -} +export interface PkgCommandGroupOptions extends GatedCommandGroupOptions {} /** - * Registers the capability-gated `pkg` command group. Structurally - * mirrors `registerCodeCommandGroup`: - * - * 1. URL early-exit — if the pkgseer endpoint isn't configured (no - * `GITHITS_CODE_NAV_URL`, no sensible default), skip registration - * entirely. - * 2. Capability gate — register only when the token advertises - * `code_navigation`, or when `GITHITS_CODE_NAVIGATION=1` is set - * for local development. Direct command invocation also keeps the - * expired-stored-auth fallback so a refreshable session can reach - * the token refresh path before capability is re-evaluated. - * - * The capability check is intentionally duplicated with - * `registerCodeCommandGroup` rather than factored out — two tiny - * sites today, extract only once a third group arrives. + * Registers the `pkg` command group. */ export async function registerPkgCommandGroup( program: Command, diff --git a/src/commands/search-registration.test.ts b/src/commands/search-registration.test.ts index 182db6f8..0780417c 100644 --- a/src/commands/search-registration.test.ts +++ b/src/commands/search-registration.test.ts @@ -3,12 +3,10 @@ import { Command } from "commander"; import { registerUnifiedSearchCommands } from "./search.js"; describe("registerUnifiedSearchCommands", () => { - it("does not register search commands without code navigation URL", async () => { + it("does not register search commands with an explicitly empty code navigation URL", async () => { const program = new Command(); await registerUnifiedSearchCommands(program, { codeNavigationUrl: "", - overrideEnabled: false, - capability: "enabled", }); expect( @@ -19,25 +17,10 @@ describe("registerUnifiedSearchCommands", () => { ).toBe(false); }); - it("does not register search commands without override or explicit capability", async () => { + it("registers search commands when code navigation URL is configured", async () => { const program = new Command(); await registerUnifiedSearchCommands(program, { codeNavigationUrl: "https://nav.example.com", - overrideEnabled: false, - capability: "disabled", - }); - - expect( - program.commands.some((command) => command.name() === "search"), - ).toBe(false); - }); - - it("registers search commands when capability is enabled", async () => { - const program = new Command(); - await registerUnifiedSearchCommands(program, { - codeNavigationUrl: "https://nav.example.com", - overrideEnabled: false, - capability: "enabled", }); expect( @@ -47,44 +30,4 @@ describe("registerUnifiedSearchCommands", () => { program.commands.some((command) => command.name() === "search-status"), ).toBe(true); }); - - it("registers search commands when override is enabled", async () => { - const program = new Command(); - await registerUnifiedSearchCommands(program, { - codeNavigationUrl: "https://nav.example.com", - overrideEnabled: true, - capability: "disabled", - }); - - expect( - program.commands.some((command) => command.name() === "search"), - ).toBe(true); - }); - - it("does not register search commands for opaque env tokens without the capability claim", async () => { - const program = new Command(); - await registerUnifiedSearchCommands(program, { - codeNavigationUrl: "https://nav.example.com", - overrideEnabled: false, - capability: "unknown", - }); - - expect( - program.commands.some((command) => command.name() === "search"), - ).toBe(false); - }); - - it("registers search commands for expired stored auth so direct invocation can refresh", async () => { - const program = new Command(); - await registerUnifiedSearchCommands(program, { - codeNavigationUrl: "https://nav.example.com", - overrideEnabled: false, - capability: "unknown", - expiredStoredAuth: true, - }); - - expect( - program.commands.some((command) => command.name() === "search"), - ).toBe(true); - }); }); diff --git a/src/commands/search.ts b/src/commands/search.ts index dbbc9612..4792c0c3 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -1,13 +1,9 @@ import { type Command, Option } from "commander"; -import type { CodeNavigationCapability } from "../services/code-navigation-capability.js"; import type { CodeNavigationService, UnifiedSearchSource, } from "../services/code-navigation-service.js"; -import { - getCodeNavigationUrl, - isCodeNavigationCliOverrideEnabled, -} from "../services/config.js"; +import { getCodeNavigationUrl } from "../services/config.js"; import { buildUnifiedSearchErrorPayload, buildUnifiedSearchParams, @@ -52,9 +48,6 @@ export interface SearchStatusCommandOptions { export interface SearchCommandRegistrationOptions { codeNavigationUrl?: string; - overrideEnabled?: boolean; - capability?: CodeNavigationCapability; - expiredStoredAuth?: boolean; } export interface SearchCommandDependencies { @@ -265,24 +258,6 @@ export async function registerUnifiedSearchCommands( return; } - const overrideEnabled = - options.overrideEnabled ?? isCodeNavigationCliOverrideEnabled(); - const registrationState = - options.capability !== undefined || options.expiredStoredAuth !== undefined - ? { - capability: options.capability ?? "unknown", - expiredStoredAuth: options.expiredStoredAuth ?? false, - } - : await loadStartupCodeNavigationRegistrationState(); - - if ( - !overrideEnabled && - registrationState.capability !== "enabled" && - !registrationState.expiredStoredAuth - ) { - return; - } - registerSearchCommand(program); } @@ -303,13 +278,6 @@ async function loadContainer() { return createContainer(); } -async function loadStartupCodeNavigationRegistrationState() { - const { resolveStartupCodeNavigationRegistrationState } = await import( - "../container.js" - ); - return resolveStartupCodeNavigationRegistrationState(); -} - function parseTargetSpecs(specs: string[] | undefined) { if (!specs || specs.length === 0) { throw new InvalidArgumentError("Provide at least one --in target."); diff --git a/src/container.ts b/src/container.ts index 73fbe12d..28e39754 100644 --- a/src/container.ts +++ b/src/container.ts @@ -7,18 +7,15 @@ import { type BrowserService, BrowserServiceImpl, ChunkingKeyringService, - type CodeNavigationCapability, type CodeNavigationService, CodeNavigationServiceImpl, type FileSystemService, FileSystemServiceImpl, GitHitsServiceImpl, getApiUrl, - getCodeNavigationCapability, getCodeNavigationUrl, getEnvApiToken, getMcpUrl, - isCodeNavigationCliOverrideEnabled, KeychainAuthStorage, KeychainUnavailableError, KeyringServiceImpl, @@ -26,7 +23,6 @@ import { type PackageIntelligenceService, PackageIntelligenceServiceImpl, RefreshingGitHitsService, - type TokenData, TokenManager, type TokenProvider, WINDOWS_MAX_ENTRY_SIZE, @@ -77,21 +73,16 @@ export interface Dependencies { hasValidToken: boolean; /** Raw GITHITS_API_TOKEN env var value (for auth status display) */ envApiToken: string | undefined; - /** Code navigation capability derived from the startup token snapshot */ - codeNavigationCapability: CodeNavigationCapability; - /** Whether GITHITS_CODE_NAVIGATION is set to force-expose gated search/code/pkg CLI surfaces locally */ - codeNavigationCliOverrideEnabled: boolean; - /** Code navigation backend URL when configured */ - codeNavigationUrl: string | undefined; - /** Optional code navigation service used by gated CLI/MCP paths */ - codeNavigationService: CodeNavigationService | undefined; + /** Code navigation backend URL */ + codeNavigationUrl: string; + /** Code navigation service used by CLI/MCP paths */ + codeNavigationService: CodeNavigationService; /** - * Optional package intelligence service — reads registry metadata, + * Package intelligence service — reads registry metadata, * vulnerabilities, dependencies, and changelogs from the pkgseer - * endpoint. Shares the `code_navigation` capability gate and the - * same endpoint URL as the code-navigation service. + * endpoint shared with the code-navigation service. */ - packageIntelligenceService: PackageIntelligenceService | undefined; + packageIntelligenceService: PackageIntelligenceService; /** GitHits REST API service */ githitsService: GitHitsService; } @@ -112,8 +103,6 @@ export async function createContainer(): Promise { const mcpUrl = getMcpUrl(); const apiUrl = getApiUrl(); const codeNavigationUrl = getCodeNavigationUrl(); - const codeNavigationCliOverrideEnabled = - isCodeNavigationCliOverrideEnabled(); const fileSystemService = new FileSystemServiceImpl(); const authStorage = createAuthStorage(fileSystemService); const authService = new AuthServiceImpl(); @@ -123,12 +112,14 @@ export async function createContainer(): Promise { const envToken = getEnvApiToken(); if (envToken) { const tokenProvider = createStaticTokenProvider(envToken); - const codeNavigationService = codeNavigationUrl - ? new CodeNavigationServiceImpl(codeNavigationUrl, tokenProvider) - : undefined; - const packageIntelligenceService = codeNavigationUrl - ? new PackageIntelligenceServiceImpl(codeNavigationUrl, tokenProvider) - : undefined; + const codeNavigationService = new CodeNavigationServiceImpl( + codeNavigationUrl, + tokenProvider, + ); + const packageIntelligenceService = new PackageIntelligenceServiceImpl( + codeNavigationUrl, + tokenProvider, + ); return { authStorage, @@ -140,8 +131,6 @@ export async function createContainer(): Promise { apiToken: envToken, hasValidToken: true, envApiToken: envToken, - codeNavigationCapability: getCodeNavigationCapability(envToken), - codeNavigationCliOverrideEnabled, codeNavigationUrl, codeNavigationService, packageIntelligenceService, @@ -154,12 +143,14 @@ export async function createContainer(): Promise { const apiToken = await withTelemetrySpan("container.token.get", () => tokenManager.getToken(), ); - const codeNavigationService = codeNavigationUrl - ? new CodeNavigationServiceImpl(codeNavigationUrl, tokenManager) - : undefined; - const packageIntelligenceService = codeNavigationUrl - ? new PackageIntelligenceServiceImpl(codeNavigationUrl, tokenManager) - : undefined; + const codeNavigationService = new CodeNavigationServiceImpl( + codeNavigationUrl, + tokenManager, + ); + const packageIntelligenceService = new PackageIntelligenceServiceImpl( + codeNavigationUrl, + tokenManager, + ); return { authStorage, @@ -171,8 +162,6 @@ export async function createContainer(): Promise { apiToken, hasValidToken: apiToken !== undefined, envApiToken: undefined, - codeNavigationCapability: getCodeNavigationCapability(apiToken), - codeNavigationCliOverrideEnabled, codeNavigationUrl, codeNavigationService, packageIntelligenceService, @@ -180,72 +169,3 @@ export async function createContainer(): Promise { }; }); } - -/** - * Resolves the startup capability snapshot without triggering token refresh. - */ -export async function resolveStartupCodeNavigationCapability(): Promise { - const state = await resolveStartupCodeNavigationRegistrationState(); - return state.capability; -} - -export interface StartupCodeNavigationRegistrationState { - capability: CodeNavigationCapability; - expiredStoredAuth: boolean; -} - -/** - * Resolves CLI registration state without triggering token refresh. - */ -export async function resolveStartupCodeNavigationRegistrationState(): Promise { - return withTelemetrySpan( - "startup.resolve-code-nav-registration-state", - async () => { - const envToken = getEnvApiToken(); - if (envToken) { - return { - capability: getCodeNavigationCapability(envToken), - expiredStoredAuth: false, - }; - } - - const tokens = await loadStartupTokens(getMcpUrl()); - if (tokens?.expiresAt && new Date(tokens.expiresAt) < new Date()) { - return { capability: "unknown", expiredStoredAuth: true }; - } - - return { - capability: getCodeNavigationCapability(tokens?.accessToken), - expiredStoredAuth: false, - }; - }, - ); -} - -async function loadStartupTokens(mcpUrl: string): Promise { - return withTelemetrySpan("startup.load-tokens", async () => { - const fileSystemService = new FileSystemServiceImpl(); - const fileStorage = new AuthStorageImpl(fileSystemService); - - try { - // Avoid createAuthStorage() here: command registration/help should stay read-only - // and must not trigger the keychain probe write+delete cycle or migration writes. - const rawKeyring = new KeyringServiceImpl(); - const keyring = - process.platform === "win32" - ? new ChunkingKeyringService(rawKeyring, WINDOWS_MAX_ENTRY_SIZE) - : rawKeyring; - const keychainStorage = new KeychainAuthStorage(keyring); - const keychainTokens = await keychainStorage.loadTokens(mcpUrl); - if (keychainTokens) { - return keychainTokens; - } - } catch (error) { - if (!(error instanceof KeychainUnavailableError)) { - throw error; - } - } - - return fileStorage.loadTokens(mcpUrl); - }); -} diff --git a/src/services/code-navigation-capability.test.ts b/src/services/code-navigation-capability.test.ts deleted file mode 100644 index c1482df5..00000000 --- a/src/services/code-navigation-capability.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { getCodeNavigationCapability } from "./code-navigation-capability.js"; -import { createJwtToken } from "./test-helpers.js"; - -describe("getCodeNavigationCapability", () => { - it("returns enabled for top-level feature_flags claim", () => { - const token = createJwtToken({ feature_flags: ["code_navigation"] }); - expect(getCodeNavigationCapability(token)).toBe("enabled"); - }); - - it("returns enabled for nested claims.feature_flags", () => { - const token = createJwtToken({ - claims: { feature_flags: ["code_navigation"] }, - }); - expect(getCodeNavigationCapability(token)).toBe("enabled"); - }); - - it("returns disabled when feature flag array exists without the flag", () => { - const token = createJwtToken({ feature_flags: ["other_feature"] }); - expect(getCodeNavigationCapability(token)).toBe("disabled"); - }); - - it("returns unknown for opaque tokens", () => { - expect(getCodeNavigationCapability("ghi-opaque-token")).toBe("unknown"); - }); - - it("returns unknown for malformed jwt payload", () => { - expect(getCodeNavigationCapability("a.broken.c")).toBe("unknown"); - }); -}); diff --git a/src/services/code-navigation-capability.ts b/src/services/code-navigation-capability.ts deleted file mode 100644 index 6151c8cb..00000000 --- a/src/services/code-navigation-capability.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Capability state derived from the current bearer token. - */ -export type CodeNavigationCapability = "enabled" | "disabled" | "unknown"; - -interface JwtPayloadShape { - feature_flags?: unknown; - claims?: { feature_flags?: unknown }; -} - -/** - * Derives code navigation capability from a Supabase JWT. - * - * The CLI does not have the signing secret, so this decodes the JWT payload - * without verifying the signature. Opaque or malformed tokens return unknown. - */ -export function getCodeNavigationCapability( - token: string | undefined, -): CodeNavigationCapability { - if (!token) return "unknown"; - - const payload = parseJwtPayload(token); - if (!payload) return "unknown"; - - const featureFlags = extractFeatureFlags(payload); - if (!featureFlags) return "unknown"; - - return featureFlags.includes("code_navigation") ? "enabled" : "disabled"; -} - -function extractFeatureFlags(payload: JwtPayloadShape): string[] | null { - if (isStringArray(payload.feature_flags)) { - return payload.feature_flags; - } - - if (payload.claims && isStringArray(payload.claims.feature_flags)) { - return payload.claims.feature_flags; - } - - return null; -} - -function isStringArray(value: unknown): value is string[] { - return ( - Array.isArray(value) && value.every((entry) => typeof entry === "string") - ); -} - -function parseJwtPayload(token: string): JwtPayloadShape | null { - const parts = token.split("."); - if (parts.length < 2) return null; - - try { - const decoded = Buffer.from(toBase64(parts[1] ?? ""), "base64").toString( - "utf8", - ); - const parsed: unknown = JSON.parse(decoded); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return null; - } - return parsed as JwtPayloadShape; - } catch { - return null; - } -} - -function toBase64(base64Url: string): string { - const normalized = base64Url.replace(/-/g, "+").replace(/_/g, "/"); - const padding = normalized.length % 4; - if (padding === 0) return normalized; - return normalized.padEnd(normalized.length + (4 - padding), "="); -} diff --git a/src/services/code-navigation-service.ts b/src/services/code-navigation-service.ts index 90aada68..b75c2b54 100644 --- a/src/services/code-navigation-service.ts +++ b/src/services/code-navigation-service.ts @@ -479,8 +479,8 @@ export class CodeNavigationValidationError extends Error { } /** - * Raised when the caller hit a backend feature flag they don't have - * access to. Same user-facing handling as ACCESS_DENIED. + * Raised when the backend reports an authorization feature check failure. + * Same user-facing handling as ACCESS_DENIED. */ export class CodeNavigationFeatureFlagRequiredError extends Error { constructor(message: string) { diff --git a/src/services/config.test.ts b/src/services/config.test.ts index 8c15dc98..7c5e7d2b 100644 --- a/src/services/config.test.ts +++ b/src/services/config.test.ts @@ -1,20 +1,15 @@ import { afterEach, describe, expect, it } from "bun:test"; -import { - getCodeNavigationUrl, - isCodeNavigationCliOverrideEnabled, -} from "./config.js"; +import { getCodeNavigationUrl } from "./config.js"; describe("code navigation config", () => { const originalCodeNavUrl = process.env.GITHITS_CODE_NAV_URL; const originalPkgseerUrl = process.env.PKGSEER_URL; - const originalCodeNavigation = process.env.GITHITS_CODE_NAVIGATION; const originalMcpUrl = process.env.GITHITS_MCP_URL; const originalApiUrl = process.env.GITHITS_API_URL; afterEach(() => { restoreEnv("GITHITS_CODE_NAV_URL", originalCodeNavUrl); restoreEnv("PKGSEER_URL", originalPkgseerUrl); - restoreEnv("GITHITS_CODE_NAVIGATION", originalCodeNavigation); restoreEnv("GITHITS_MCP_URL", originalMcpUrl); restoreEnv("GITHITS_API_URL", originalApiUrl); }); @@ -40,27 +35,12 @@ describe("code navigation config", () => { expect(getCodeNavigationUrl()).toBe("https://pkgseer.dev"); }); - it("does not default to pkgseer.dev for custom GitHits environments", () => { + it("keeps the package/source default independent from custom GitHits environments", () => { delete process.env.GITHITS_CODE_NAV_URL; delete process.env.PKGSEER_URL; process.env.GITHITS_MCP_URL = "https://mcp.staging.githits.test"; - expect(getCodeNavigationUrl()).toBeUndefined(); - }); - - it("returns false when CLI override is unset", () => { - delete process.env.GITHITS_CODE_NAVIGATION; - expect(isCodeNavigationCliOverrideEnabled()).toBe(false); - }); - - it("returns true when CLI override is set to 1", () => { - process.env.GITHITS_CODE_NAVIGATION = "1"; - expect(isCodeNavigationCliOverrideEnabled()).toBe(true); - }); - - it("returns false when CLI override is set to false", () => { - process.env.GITHITS_CODE_NAVIGATION = "false"; - expect(isCodeNavigationCliOverrideEnabled()).toBe(false); + expect(getCodeNavigationUrl()).toBe("https://pkgseer.dev"); }); }); diff --git a/src/services/config.ts b/src/services/config.ts index a5969c35..6dc5225b 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,9 +1,10 @@ /** * Base URL configuration for GitHits services. * - * Two separate URLs are needed: + * Three separate URLs are needed: * - MCP URL: For OAuth discovery (.well-known endpoints) and auth flow * - API URL: For REST API calls (search, languages, feedbacks) + * - Code navigation URL: For indexed package/source calls */ const DEFAULT_MCP_URL = "https://mcp.githits.com"; @@ -32,16 +33,14 @@ export function getApiUrl(): string { * development parity with older environments but is not publicly * documented. */ -export function getCodeNavigationUrl(): string | undefined { +export function getCodeNavigationUrl(): string { const explicitUrl = process.env.GITHITS_CODE_NAV_URL ?? process.env.PKGSEER_URL; if (explicitUrl) { return explicitUrl; } - const usingDefaultEnvironment = - getMcpUrl() === DEFAULT_MCP_URL && getApiUrl() === DEFAULT_API_URL; - return usingDefaultEnvironment ? DEFAULT_CODE_NAV_URL : undefined; + return DEFAULT_CODE_NAV_URL; } /** @@ -50,17 +49,3 @@ export function getCodeNavigationUrl(): string | undefined { export function getEnvApiToken(): string | undefined { return process.env.GITHITS_API_TOKEN; } - -/** - * Whether `GITHITS_CODE_NAVIGATION` forces the capability-gated - * code-navigation CLI surfaces (`search`, `code`, `pkg`) to be - * exposed locally regardless of the startup token's - * `code_navigation` claim. - */ -export function isCodeNavigationCliOverrideEnabled(): boolean { - const raw = process.env.GITHITS_CODE_NAVIGATION; - if (!raw) return false; - - const normalized = raw.toLowerCase(); - return normalized !== "0" && normalized !== "false" && normalized !== ""; -} diff --git a/src/services/index.ts b/src/services/index.ts index 3939e23c..f162ba8b 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -22,8 +22,6 @@ export { ChunkingKeyringService, WINDOWS_MAX_ENTRY_SIZE, } from "./chunking-keyring-service.js"; -export type { CodeNavigationCapability } from "./code-navigation-capability.js"; -export { getCodeNavigationCapability } from "./code-navigation-capability.js"; export type { AvailableVersion, CodeNavigationRegistry, @@ -82,7 +80,6 @@ export { getCodeNavigationUrl, getEnvApiToken, getMcpUrl, - isCodeNavigationCliOverrideEnabled, } from "./config.js"; export type { ExecResult, ExecService } from "./exec-service.js"; export { ExecServiceImpl } from "./exec-service.js"; diff --git a/src/tools/shared.ts b/src/tools/shared.ts index ed4ae45f..a8150040 100644 --- a/src/tools/shared.ts +++ b/src/tools/shared.ts @@ -5,7 +5,7 @@ import { errorResult, type ToolResult } from "./types.js"; * Wraps a tool handler with the shared structured `{error, code, * retryable}` error envelope. Used by always-on tools (`get_example`, * `search_language`, `feedback`) so agents can branch on `code` - * uniformly with capability-gated tools instead of text-parsing. + * uniformly with code-navigation tools instead of text-parsing. */ export async function withErrorHandling( operation: string,