Files
orcs-code/src/tools/RepoMapTool/RepoMapTool.ts
2026-04-14 18:57:46 +05:30

177 lines
5.2 KiB
TypeScript

import { z } from 'zod/v4'
import { buildTool, type ToolDef } from '../../Tool.js'
import { getCwd } from '../../utils/cwd.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { checkReadPermissionForTool } from '../../utils/permissions/filesystem.js'
import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
import { buildRepoMap } from '../../context/repoMap/index.js'
import { REPO_MAP_TOOL_NAME, getDescription } from './prompt.js'
import {
getToolUseSummary,
renderToolResultMessage,
renderToolUseErrorMessage,
renderToolUseMessage,
} from './UI.js'
const inputSchema = lazySchema(() =>
z.strictObject({
max_tokens: z
.number()
.int()
.min(256)
.max(16384)
.optional()
.describe(
'Maximum token budget for the rendered map. Higher values include more files. Default: 1024.',
),
focus_files: z
.array(z.string())
.optional()
.describe(
'Relative file or directory paths to boost in the ranking (e.g. ["src/tools/", "src/context.ts"]).',
),
focus_symbols: z
.array(z.string())
.optional()
.describe(
'Symbol names to boost — files defining these symbols rank higher (e.g. ["buildTool", "ToolUseContext"]).',
),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
const outputSchema = lazySchema(() =>
z.object({
rendered: z.string(),
token_count: z.number(),
file_count: z.number(),
total_file_count: z.number(),
cache_hit: z.boolean(),
build_time_ms: z.number(),
}),
)
type OutputSchema = ReturnType<typeof outputSchema>
type Output = z.infer<OutputSchema>
export const RepoMapTool = buildTool({
name: REPO_MAP_TOOL_NAME,
searchHint: 'structural map of repository files and symbols',
maxResultSizeChars: 50_000,
async description() {
return getDescription()
},
userFacingName() {
return 'Repository map'
},
getToolUseSummary,
getActivityDescription(input) {
if (input?.focus_files?.length) {
return `Building repository map (focus: ${input.focus_files.join(', ')})`
}
return 'Building repository map'
},
get inputSchema(): InputSchema {
return inputSchema()
},
get outputSchema(): OutputSchema {
return outputSchema()
},
isConcurrencySafe() {
return true
},
isReadOnly() {
return true
},
isSearchOrReadCommand() {
return { isSearch: false, isRead: true }
},
toAutoClassifierInput(input) {
const parts: string[] = ['repomap']
if (input.focus_files?.length) parts.push(`focus: ${input.focus_files.join(',')}`)
return parts.join(' ')
},
async checkPermissions(input, context): Promise<PermissionDecision> {
const appState = context.getAppState()
return checkReadPermissionForTool(
RepoMapTool,
input,
appState.toolPermissionContext,
)
},
async prompt() {
return getDescription()
},
renderToolUseMessage,
renderToolUseErrorMessage,
renderToolResultMessage,
extractSearchText({ rendered }) {
return rendered
},
mapToolResultToToolResultBlockParam(output, toolUseID) {
const summary = [
`Repository map: ${output.file_count} files ranked (${output.total_file_count} total), ${output.token_count} tokens`,
output.cache_hit ? '(cached)' : `(built in ${output.build_time_ms}ms)`,
].join(' ')
return {
tool_use_id: toolUseID,
type: 'tool_result',
content: `${summary}\n\n${output.rendered}`,
}
},
async call(
{ max_tokens = 1024, focus_files, focus_symbols },
{ abortController },
) {
const root = getCwd()
// Resolve focus_symbols to file paths by searching the tag cache
let resolvedFocusFiles = focus_files ?? []
if (focus_symbols?.length) {
// Import the symbol lookup dynamically to avoid circular deps at module load
const { getRepoFiles } = await import('../../context/repoMap/gitFiles.js')
const { extractTags } = await import('../../context/repoMap/symbolExtractor.js')
const { initParser } = await import('../../context/repoMap/parser.js')
await initParser()
const files = await getRepoFiles(root)
const symbolFiles: string[] = []
const symbolSet = new Set(focus_symbols)
// Scan files for matching symbol definitions
for (const file of files) {
if (abortController.signal.aborted) break
const tags = await extractTags(file, root)
if (tags) {
const hasMatch = tags.tags.some(
t => t.kind === 'def' && symbolSet.has(t.name),
)
if (hasMatch) {
symbolFiles.push(file)
}
}
}
resolvedFocusFiles = [...resolvedFocusFiles, ...symbolFiles]
}
const result = await buildRepoMap({
root,
maxTokens: max_tokens,
focusFiles: resolvedFocusFiles.length > 0 ? resolvedFocusFiles : undefined,
})
const output: Output = {
rendered: result.map,
token_count: result.tokenCount,
file_count: result.fileCount,
total_file_count: result.totalFileCount,
cache_hit: result.cacheHit,
build_time_ms: result.buildTimeMs,
}
return { data: output }
},
} satisfies ToolDef<InputSchema, Output>)