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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/implementation/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The CLI exposes `example`, `languages`, `feedback`, top-level indexed `search` /
| `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 <spec>` | 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>` | 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 files [spec] [path-prefix]` | package spec OR `--repo-url` + `--git-ref`; optional `[path-prefix]` | `--path`, repeatable `--glob`, repeatable `--ext`, repeatable `--file-type`, repeatable `--language`, repeatable `--file-intent`, repeatable `--exclude-intent`, `--exclude-docs`, `--exclude-tests`, `--hidden`, `--limit`, `--wait`, `--verbose`, `--json` | List files in an indexed dependency. Selectors (`[path-prefix]`, `--path`, `--glob`) are OR-ed; the other flags filter that scope down further. 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 <spec?> <path>` | package spec OR `--repo-url` + `--git-ref`; plus `<path>` | `--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] <pattern> [path-prefix]` | package spec OR `--repo-url` + `--git-ref`; plus `<pattern>` 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. |

Expand Down
8 changes: 4 additions & 4 deletions docs/implementation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C
| `pkg_vulns` | `registry`, `package_name`, `version?`, `min_severity?`, `include_withdrawn?` | Known vulnerabilities for a package on npm, PyPI, Hex, or Crates. Count summary, per-advisory OSV ID + severity + affected/fix ranges, and upgrade paths. Malware is surfaced in a disjoint bucket. |
| `pkg_deps` | `registry`, `package_name`, `version?`, `lifecycle?`, `include_transitive?`, `include_importers?`, `max_depth?` | Direct runtime dependency list (each `{name, version, constraint}` — the backend resolves each constraint to a concrete version) plus, when the backend has them, structured groups for dev / peer / build / optional with registry-specific condition metadata (PyPI extras, Crates features). Optional transitive block with aggregate edge counts, the preprocessed install footprint as `{name, version}`, typed conflicts and circular-dependency cycles; opt into per-package importer provenance with `include_importers`. |
| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `include_bodies?` | Release notes or changelog entries for a package or GitHub repo. Default latest mode returns the ten most recent entries; `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) mutually exclusive. Response includes optional `source` (`"releases"` / `"changelog_file"` / `"hexdocs"`) when a concrete changelog source exists, `mode` (`"latest"` / `"range"`), and `entries: { count, items }` with full markdown bodies by default; set `include_bodies: false` for a lean version / date / URL timeline. Package-version entries without changelog text succeed with `source` omitted. |
| `code_files` | `target`, `path_prefix?`, `limit?`, `wait_timeout_ms?`, `format?` | List files in an indexed dependency. Returns `{total, hasMore, files: [{path, name, language, fileType, byteSize}], resolution, indexedVersion}` in JSON mode. Dual addressing via `target.registry + target.package_name` (spec) or `target.repo_url + target.git_ref` (repo). `path_prefix` is a literal directory prefix — NOT a glob (`*.ts` won't match); glob / pattern filtering is an upstream enhancement. Emits an `INDEXING` error envelope when the dependency is being indexed on-demand — retry with a longer `wait_timeout_ms` or pick a version from `details.availableVersions`. `format` defaults to `text-v1` (paths-only listing); pass `format: "json"` for the structured envelope. |
| `code_files` | `target`, `path?`, `path_prefix?`, `globs?`, `extensions?`, `file_types?`, `languages?`, `file_intent?`, `file_intents?`, `exclude_file_intents?`, `exclude_doc_files?`, `exclude_test_files?`, `include_hidden?`, `limit?`, `wait_timeout_ms?`, `format?` | List files in an indexed dependency. Returns `{total, hasMore, files: [{path, name, language, fileType, byteSize}], resolution, indexedVersion}` in JSON mode. Dual addressing via `target.registry + target.package_name` (spec) or `target.repo_url + target.git_ref` (repo). Selectors (`path`, `path_prefix`, `globs`) are OR-ed; the other filters intersect on top. Emits an `INDEXING` error envelope when the dependency is being indexed on-demand — retry with a longer `wait_timeout_ms` or pick a version from `details.availableVersions`. `format` defaults to `text-v1` (paths-only listing); pass `format: "json"` for the structured envelope. |
| `code_read` | `target`, `path`, `start_line?`, `end_line?`, `wait_timeout_ms?` | 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. |

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

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.
**`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` echoes only explicit caller filters (`path`, `pathPrefix`, `globs`, `extensions`, `fileTypes`, `languages`, file-intent filters, booleans, and `limit`); default limit (200) never round-trips.

**`code_read` envelope**: `{registry?|repoUrl?+gitRef?, path, language?, totalLines?, startLine?, endLine?, content?, isBinary?, hint?}`. `path` (not `filePath`) so the key matches `code_files.files[].path` and `code_grep.filter.path` when exact-file grep is used. Binary files set `isBinary: true` and **omit** `content` (not `null`); agents branch on the flag. `hint` is emitted only when the MCP span cap actually truncated the response — see "code_read span cap" below.

Expand Down Expand Up @@ -178,13 +178,13 @@ More hits available. Pass offset=N for the next page or limit=N to widen.
**Listing anatomy** (`code_files` text-v1):

```
code_files | <N>[+] paths | <identity> [path_prefix="..."] [limit=N]
code_files | <N>[+] paths | <identity> [path="..."] [path_prefix="..."] [globs=...] [exts=...] [...]
[blank]
<path1>
<path2>
...
[blank]
More files available. Pass limit=N to widen or refine path_prefix.
More files available. Pass limit=N or refine the filter.
```

`<identity>` is `<registry>:<name>@<version>` for spec addressing or `<repoUrl>@<gitRef>` for repo addressing. Filter echoes appear in the header only when the caller supplied them explicitly (defaults never echo).
Expand Down
92 changes: 92 additions & 0 deletions src/commands/code/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,63 @@ describe("pkgFilesAction", () => {
writeSpy.mockRestore();
});

it("forwards advanced file filters to the service", async () => {
const listFiles = mock(() => Promise.resolve(defaultListFilesResult));
const service = createMockCodeNavigationService({ listFiles });
const writeSpy = spyOn(process.stdout, "write").mockImplementation(
(() => true) as typeof process.stdout.write,
);
await pkgFilesAction(
"npm:express",
"src/",
{
path: "README.md",
glob: ["test/**/*.js"],
ext: ["js"],
fileType: ["source"],
language: ["JavaScript"],
fileIntent: ["production", "test"],
excludeIntent: ["generated"],
excludeDocs: true,
excludeTests: false,
hidden: true,
},
createDeps({ codeNavigationService: service }),
);
const calls = listFiles.mock.calls as unknown as Array<
[
{
pathSelectors?: Array<{ kind: string; value: string }>;
pathPrefix?: string;
extensions?: string[];
fileTypes?: string[];
languages?: string[];
fileIntents?: string[];
excludeFileIntents?: string[];
excludeDocFiles?: boolean;
excludeTestFiles?: boolean;
includeHidden?: boolean;
},
]
>;
expect(calls[0]?.[0]).toMatchObject({
pathSelectors: [
{ kind: "EXACT", value: "README.md" },
{ kind: "GLOB", value: "test/**/*.js" },
],
pathPrefix: "src/",
extensions: ["js"],
fileTypes: ["source"],
languages: ["JavaScript"],
fileIntents: ["PRODUCTION", "TEST"],
excludeFileIntents: ["GENERATED"],
excludeDocFiles: true,
excludeTestFiles: false,
includeHidden: true,
});
writeSpy.mockRestore();
});

it("rejects a second positional in --repo-url mode", async () => {
const errorSpy = spyOn(console, "error").mockImplementation(() => {});
const exitSpy = spyOn(process, "exit").mockImplementation(() => {
Expand Down Expand Up @@ -172,6 +229,41 @@ describe("pkgFilesAction", () => {
logSpy.mockRestore();
});

it("echoes advanced filters in the JSON envelope", async () => {
const logSpy = spyOn(console, "log").mockImplementation(() => {});
await pkgFilesAction(
"npm:express",
"src/",
{
path: "README.md",
glob: ["test/**/*.js"],
ext: ["js"],
fileType: ["source"],
language: ["JavaScript"],
fileIntent: ["production", "test"],
excludeIntent: ["generated"],
excludeDocs: true,
hidden: true,
json: true,
},
createDeps(),
);
const payload = JSON.parse(logSpy.mock.calls[0]?.[0] as string);
expect(payload.filter).toEqual({
path: "README.md",
pathPrefix: "src/",
globs: ["test/**/*.js"],
extensions: ["js"],
fileTypes: ["source"],
languages: ["JavaScript"],
fileIntents: ["production", "test"],
excludeFileIntents: ["generated"],
excludeDocFiles: true,
includeHidden: true,
});
logSpy.mockRestore();
});

it("sends waitTimeoutMs defaulting to DEFAULT_WAIT_TIMEOUT_MS (20000)", async () => {
const listFiles = mock(() => Promise.resolve(defaultListFilesResult));
const service = createMockCodeNavigationService({ listFiles });
Expand Down
Loading
Loading