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
6 changes: 3 additions & 3 deletions docs/implementation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C
| `pkg_deps` | `registry`, `package_name`, `version?`, `lifecycle?`, `include_importers?`, `max_depth?`, `format?` | Direct runtime dependency list by default with resolved versions. Non-runtime groups are hidden with an MCP-native hint (`pass lifecycle="all"`). Use `lifecycle: "runtime"` for explicit runtime-only, a concrete non-runtime lifecycle for runtime plus matching groups, or `lifecycle: "all"` for all available groups. `max_depth` requests capped transitive output with aggregate edge counts, the preprocessed install footprint, typed conflicts and circular-dependency cycles; opt into importer provenance with `include_importers`. Pass `format: "json"` for the lean structured envelope. |
| `pkg_changelog` | `registry?`, `package_name?`, `repo_url?`, `from_version?`, `to_version?`, `limit?`, `git_ref?`, `omit_bodies?`, `verbose?`, `body_lines?`, `format?` | Release notes or changelog entries for a package or GitHub repo. Example: `{registry:"npm", package_name:"express", limit:2}`. Defaults to compact text with newest-first entries and 10-line body previews; set `body_lines` to tune text previews, `verbose:true` for full text bodies, `omit_bodies:true` for a lean timeline, or `format:"json"` for the complete envelope. `from_version` switches to range mode (no count cap). Dual addressing (spec vs repo URL) is mutually exclusive. |
| `pkg_upgrade_review` | `registry?`, `package_name?`, `current_version?`, `target_version?`, `packages?`, `skip_transitive_security?`, `include_dependency_issues?`, `min_severity?`, `verbose?`, `format?` | Evidence for dependency upgrades. Accepts a single package or repeatable batch, compares current vs target direct vulnerabilities, changelog range evidence, target deprecation metadata, peer dependency changes, dependency changes, and transitive security evidence by default. `skip_transitive_security:true` disables transitive vulnerability evidence when latency matters. Reports facts only; callers decide whether to accept the upgrade. |
| `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, targetResolution?}` in JSON mode. Dual addressing via `target.registry + target.package_name` (spec) or `target.repo_url + target.git_ref?` (repo, omitted ref means default branch intent). Selectors (`path`, `path_prefix`, `globs`) are OR-ed; the other filters intersect on top. `INDEXING` errors include immediate retry candidates in `details.availableVersions` / `details.availableRefs` when available. `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, targetResolution?}` in JSON mode. Dual addressing via `target.registry + target.package_name` (spec) or `target.repo_url + target.git_ref?` (repo, omitted ref means default branch intent). Selectors (`path`, `path_prefix`, `globs`) are OR-ed; the other filters intersect on top. `INDEXING` errors include immediate retry candidates in `details.availableVersions` / `details.availableRefs` when available; repository ref suggestions use `suggestedRefs` and are not immediate retry guarantees. `format` defaults to `text-v1` (paths-only listing); pass `format: "json"` for the structured envelope. |
| `code_read` | `target`, `path`, `start_line?`, `end_line?`, `wait_timeout_ms?`, `format?` | Read a file from an indexed dependency. `target` accepts the structured object or compact string (`npm:react@18.2.0`, `github:facebook/react#HEAD`, `github.com/facebook/react#HEAD`, `https://github.com/facebook/react#HEAD`, `github:facebook/react@HEAD`, or any repo form without `#ref`/`@ref` for default branch intent). User-facing output canonicalizes repo targets as `github:owner/repo#ref` so refs can contain `@` safely. Package compact strings require an explicit registry prefix. **MCP per-call span cap: 150 lines** — broader requests (or no range) are silently truncated to the first 150 lines from the caller's start, with a hint explaining the cap and the original request. Defaults to `text-v1` with line-numbered content; pass `format: "json"` for the structured envelope. Binary files set `isBinary: true` and omit `content`; `targetResolution` may explain fallback/indexing provenance. 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 @@ -143,7 +143,7 @@ These three indexed tools share an addressing and lifecycle contract (documented

**`code_grep` envelope**: `{registry?|name?|repoUrl?+gitRef?, pattern, patternType?, caseSensitive?, matches: [{filePath, line, matchStartByte, matchEndByte, lineContent, contextBefore?, contextAfter?, fileContentHash?, fileIntent?, symbol?}], nextCursor?, hasMore, truncatedReason?, filesScanned, filesInScope, binaryFilesSkipped?, filesTooLargeSkipped?, totalMatches, uniqueFilesMatched, indexedVersion?, resolution?, targetResolution?, filter?}`. Default-valued fields (`patternType: literal`, `caseSensitive: false`, zero skipped counters, `truncatedReason: none`) are omitted. `filter` echoes only explicit caller filters. Match entries carry `filePath` so grep output chains directly into `code_read`.

`targetResolution` is additive provenance. It explains requested, resolved-requested, and served artifacts plus `freshness` (`current`, `fallback_recent`, `indexing`, or `unavailable`), `freshnessReason`, `indexingRef`, `availableVersions`, and `availableRefs`. Existing `indexedVersion`, `resolution`, and locator fields remain served-identity compatibility fields. Text mode renders actionable notes such as `using recent index`, `indexing fresh target`, or `target unavailable`; JSON mode carries the structured object.
`targetResolution` is additive provenance. It explains requested, resolved-requested, and served artifacts plus `freshness` (`current`, `fallback_recent`, `indexing`, or `unavailable`), `freshnessReason`, `indexingRef`, `availableVersions`, `availableRefs`, and `suggestedRefs`. `availableVersions` and `availableRefs` are already-indexed artifacts that can be queried immediately. `suggestedRefs` are fuzzy upstream candidates and may require indexing before use. Existing `indexedVersion`, `resolution`, and locator fields remain served-identity compatibility fields. Text mode renders actionable notes such as `using recent index`, `indexing fresh target`, `target unavailable`, `queryable now`, or `suggested refs`; JSON mode carries the structured object.

### Indexing lifecycle (shared across `code_files`, `code_read`, `code_grep`)

Expand All @@ -163,7 +163,7 @@ All three code-navigation tools share the same indexing-retry contract. The stat
}
```

`details.availableVersions` and `details.availableRefs` are populated when the backend returned already-indexed artifacts alongside the sentinel. Agents can pick one to retry against immediately without waiting. When `details.targetResolution` is present, it is explanatory provenance only; follow-up commands still use served locators / legacy served fields rather than reconstructing targets from the originally requested identity.
`details.availableVersions` and `details.availableRefs` are populated when the backend returned already-indexed artifacts alongside the sentinel. Agents can pick one to retry against immediately without waiting. `details.suggestedRefs` appears on `REF_NOT_FOUND` and inside `details.targetResolution` when the backend has fuzzy repository-ref candidates; these are suggestions only, not immediate retry guarantees. When `details.targetResolution` is present, it is explanatory provenance only; follow-up commands still use served locators / legacy served fields rather than reconstructing targets from the originally requested identity.

**Retry default**: `DEFAULT_WAIT_TIMEOUT_MS = 20_000` (shared, defined in `packages/mcp/src/shared/code-navigation-defaults.ts`). Applied inside each request builder so both CLI and MCP surfaces get the same default by construction. CLI's `--wait <ms>` and MCP's `wait_timeout_ms` override.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ describe("CodeNavigationServiceImpl", () => {
indexingRef: "ref_xyz",
availableVersions: [],
availableRefs: [{ ref: "main" }, { ref: "v4.18.2" }],
suggestedRefs: [{ ref: "express-v4.18.2" }],
},
},
},
Expand Down Expand Up @@ -252,6 +253,9 @@ describe("CodeNavigationServiceImpl", () => {
{ ref: "v4.18.2" },
]);
expect(typed.targetResolution?.freshness).toBe("indexing");
expect(typed.targetResolution?.suggestedRefs).toEqual([
{ ref: "express-v4.18.2" },
]);
}
});

Expand Down Expand Up @@ -306,8 +310,71 @@ describe("CodeNavigationServiceImpl", () => {

expect(result.indexedVersion).toBe("v5.2.1");
expect(bodies).toHaveLength(2);
expect(bodies[0]).toContain("suggestedRefs");
expect(bodies[0]).toContain("availableRefs");
expect(bodies[1]).not.toContain("availableRefs");
expect(bodies[1]).not.toContain("suggestedRefs");
expect(bodies[1]).toContain("availableRefs");
});

it("falls back again when targetResolution availableRefs is also unsupported", async () => {
const bodies: string[] = [];
globalThis.fetch = mock((_, init?: RequestInit) => {
bodies.push(String(init?.body ?? ""));
const message =
bodies.length === 1
? 'Cannot query field "suggestedRefs" on type "TargetResolution".'
: 'Cannot query field "availableRefs" on type "TargetResolution".';
if (bodies.length < 3) {
return Promise.resolve(
new Response(
JSON.stringify({
errors: [
{
message,
extensions: { code: "GRAPHQL_VALIDATION_FAILED" },
},
],
}),
{ status: 200 },
),
);
}
return Promise.resolve(
new Response(
JSON.stringify({
data: {
listRepoFiles: {
files: [],
total: 0,
hasMore: false,
indexedVersion: "v5.2.1",
resolution: null,
diagnostics: null,
codeIndexState: "CURRENT",
},
},
}),
{ status: 200 },
),
);
}) as unknown as typeof fetch;

const service = new CodeNavigationServiceImpl(
BASE_URL,
createMockTokenProvider(),
);

const result = await service.listFiles({
target: { registry: "NPM", packageName: "express" },
});

expect(result.indexedVersion).toBe("v5.2.1");
expect(bodies).toHaveLength(3);
expect(bodies[0]).toContain("suggestedRefs");
expect(bodies[1]).not.toContain("suggestedRefs");
expect(bodies[1]).toContain("availableRefs");
expect(bodies[2]).not.toContain("suggestedRefs");
expect(bodies[2]).not.toContain("availableRefs");
});

it("surfaces diagnostics.hint on listFiles empty responses", async () => {
Expand Down Expand Up @@ -865,21 +932,22 @@ describe("CodeNavigationServiceImpl", () => {
}
});

it("classifies GraphQL REF_NOT_FOUND with available ref suggestions", async () => {
it("classifies GraphQL REF_NOT_FOUND with indexed refs and suggested refs", async () => {
mockFetch(() =>
Promise.resolve(
new Response(
JSON.stringify({
errors: [
{
message:
"Repository ref cannot be resolved for openai/codex@1.2.3. Did you mean codex@1.2.3, v1.2.3?",
"Repository ref cannot be resolved for github:openai/codex#1.2.3. Did you mean codex@1.2.3, v1.2.3?",
extensions: {
code: "REF_NOT_FOUND",
retryable: false,
repo_url: "https://github.com/openai/codex",
git_ref: "1.2.3",
available_refs: [
available_refs: [{ ref: "main", version: null }],
suggested_refs: [
{ ref: "codex@1.2.3", version: null },
{ ref: "v1.2.3", version: null },
],
Expand Down Expand Up @@ -910,6 +978,9 @@ describe("CodeNavigationServiceImpl", () => {
expect(typed.repoUrl).toBe("https://github.com/openai/codex");
expect(typed.requestedRef).toBe("1.2.3");
expect(typed.availableRefs).toEqual([
{ ref: "main", version: undefined },
]);
expect(typed.suggestedRefs).toEqual([
{ ref: "codex@1.2.3", version: undefined },
{ ref: "v1.2.3", version: undefined },
]);
Expand Down
53 changes: 48 additions & 5 deletions packages/core-internal/src/services/code-navigation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export interface TargetResolutionIdentity {
}

export type AvailableRef = AvailableVersion;
export type SuggestedRef = AvailableVersion;

export interface TargetResolution {
requested?: TargetResolutionIdentity;
Expand All @@ -127,6 +128,7 @@ export interface TargetResolution {
indexingRef?: string;
availableVersions: AvailableVersion[];
availableRefs: AvailableRef[];
suggestedRefs?: SuggestedRef[];
}

export type UnifiedSearchSource = "AUTO" | "DOCS" | "CODE" | "SYMBOL";
Expand Down Expand Up @@ -259,6 +261,7 @@ export interface UnifiedSearchProgressTarget {
targetResolution?: TargetResolution;
availableVersions?: AvailableVersion[];
availableRefs?: AvailableRef[];
suggestedRefs?: SuggestedRef[];
}

export interface UnifiedSearchRequestedTarget {
Expand Down Expand Up @@ -580,6 +583,7 @@ export class CodeNavigationRefNotFoundError extends Error {
public readonly repoUrl: string | undefined,
public readonly requestedRef: string | undefined,
public readonly availableRefs: AvailableRef[] | undefined,
public readonly suggestedRefs: SuggestedRef[] | undefined,
) {
super(message);
this.name = "CodeNavigationRefNotFoundError";
Expand Down Expand Up @@ -640,6 +644,18 @@ availableRefs {
ref
}`;

const TARGET_RESOLUTION_SUGGESTED_REFS_SELECTION = `
suggestedRefs {
version
ref
}`;

const DISCOVERY_TARGET_PROGRESS_SUGGESTED_REFS_SELECTION = `
suggestedRefs {
version
ref
}`;

const TARGET_RESOLUTION_SELECTION = `
targetResolution {
requested {
Expand Down Expand Up @@ -677,6 +693,7 @@ targetResolution {
ref
}
${TARGET_RESOLUTION_AVAILABLE_REFS_SELECTION}
${TARGET_RESOLUTION_SUGGESTED_REFS_SELECTION}
}`;

const CODE_CONTEXT_AVAILABLE_VERSIONS_SELECTION = `
Expand All @@ -693,7 +710,8 @@ availableVersions {
availableRefs {
version
ref
}`;
}
${DISCOVERY_TARGET_PROGRESS_SUGGESTED_REFS_SELECTION}`;

const UNIFIED_SEARCH_QUERY = `
query UnifiedSearch(
Expand Down Expand Up @@ -1035,6 +1053,7 @@ const targetResolutionSchema = z
indexingRef: z.string().nullable().optional(),
availableVersions: z.array(availableVersionSchema).nullable().optional(),
availableRefs: z.array(availableVersionSchema).nullable().optional(),
suggestedRefs: z.array(availableVersionSchema).nullable().optional(),
})
.nullable()
.optional();
Expand Down Expand Up @@ -1162,6 +1181,7 @@ const unifiedSearchProgressTargetSchema = z.object({
targetResolution: targetResolutionSchema,
availableVersions: z.array(availableVersionSchema).nullable().optional(),
availableRefs: z.array(availableVersionSchema).nullable().optional(),
suggestedRefs: z.array(availableVersionSchema).nullable().optional(),
});

const unifiedSearchRequestedTargetSchema = z.object({
Expand Down Expand Up @@ -1963,6 +1983,7 @@ export class CodeNavigationServiceImpl implements CodeNavigationService {
? extensions.gitRef
: undefined,
parseAvailableRefs(extensions),
parseSuggestedRefs(extensions),
);

case "NOT_FOUND":
Expand Down Expand Up @@ -2206,6 +2227,7 @@ export class CodeNavigationServiceImpl implements CodeNavigationService {
targetResolution: normaliseTargetResolution(target.targetResolution),
availableVersions: normaliseAvailableVersions(target.availableVersions),
availableRefs: normaliseAvailableVersions(target.availableRefs),
suggestedRefs: normaliseAvailableVersions(target.suggestedRefs),
})),
expiresAt: progress.expiresAt ?? undefined,
};
Expand Down Expand Up @@ -2571,11 +2593,24 @@ function parseDetail(body: string): string | undefined {
}

function buildTargetResolutionFallbackQueries(query: string): string[] {
const withoutSuggestedRefs = query
.replaceAll(TARGET_RESOLUTION_SUGGESTED_REFS_SELECTION, "")
.replaceAll(DISCOVERY_TARGET_PROGRESS_SUGGESTED_REFS_SELECTION, "");
const candidates = [
query.replaceAll(TARGET_RESOLUTION_AVAILABLE_REFS_SELECTION, ""),
query.replaceAll(DISCOVERY_TARGET_PROGRESS_RETRY_SELECTION, ""),
query.replaceAll(CODE_CONTEXT_AVAILABLE_VERSIONS_SELECTION, ""),
query
withoutSuggestedRefs,
withoutSuggestedRefs.replaceAll(
TARGET_RESOLUTION_AVAILABLE_REFS_SELECTION,
"",
),
withoutSuggestedRefs.replaceAll(
DISCOVERY_TARGET_PROGRESS_RETRY_SELECTION,
"",
),
withoutSuggestedRefs.replaceAll(
CODE_CONTEXT_AVAILABLE_VERSIONS_SELECTION,
"",
),
withoutSuggestedRefs
.replaceAll(TARGET_RESOLUTION_SELECTION, "")
.replaceAll(DISCOVERY_TARGET_PROGRESS_RETRY_SELECTION, "")
.replaceAll(CODE_CONTEXT_AVAILABLE_VERSIONS_SELECTION, ""),
Expand Down Expand Up @@ -2655,6 +2690,13 @@ function parseAvailableRefs(
return parseAvailableArtifacts(raw);
}

function parseSuggestedRefs(
extensions: Record<string, unknown> | undefined,
): SuggestedRef[] | undefined {
const raw = extensions?.suggested_refs ?? extensions?.suggestedRefs;
return parseAvailableArtifacts(raw);
}

function parseTargetResolution(
extensions: Record<string, unknown> | undefined,
): TargetResolution | undefined {
Expand Down Expand Up @@ -2708,6 +2750,7 @@ function normaliseTargetResolution(
availableVersions:
normaliseAvailableVersions(resolution.availableVersions) ?? [],
availableRefs: normaliseAvailableVersions(resolution.availableRefs) ?? [],
suggestedRefs: normaliseAvailableVersions(resolution.suggestedRefs) ?? [],
};
}

Expand Down
Loading