Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, `verify-translation-pairing --write` for the touched bilingual pairs, `gen-doc-graphs`, and one typert snapshot whose ids embed character offsets. `pnpm run rescope-vendor --check` verifies the result. Renames nine vendored packages (cordis, cosmokit, schemastery and the six @cordisjs plugins) and every reference that resolves them: manifest names and dependency keys, module specifiers including declare-module merges, cordis.yml plugin names, tsconfig paths, every Markdown fence, and `docs/` prose. Directory names, upstream versions, and dependency ranges are unchanged, so vendor/README.md still reads as an upstream snapshot; its manifest table gains an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed at each fork's origin. The tutorial tier follows the rename end to end: its yaml fences named plugins the Loader can no longer resolve, its `ts ignore-check` fences disagreed with the compiled fences beside them, and its prose quoted both. The contracts that told readers to keep upstream names — the root convention and the vendoring cookbook's tree comment and manifest invariant — now say to rescope instead. Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle purity gate now names the vendored libraries a browser bundle inlines, and the files where a bare `cordis` is an agent-preset id keep that product data.
244 lines
9.1 KiB
TypeScript
244 lines
9.1 KiB
TypeScript
/**
|
|
* Model-facing `lsp` tool over `ctx.lsp`. One read-only tool with four operations
|
|
* (`goToDefinition`/`findReferences`/`goToImplementation`/`hover`); it converts one-based UTF-16
|
|
* cursor coordinates to the seam's zero-based positions, requires the session workspace with no
|
|
* fallback, caps and renders results, and attaches a configurable timeout budget for
|
|
* `dsh-timeout-policy` to enforce. It runtime-injects only `tools`, `lsp`, and `systemPrompt` and
|
|
* imports no provider.
|
|
*
|
|
* Namespace plugin (named exports, no default export).
|
|
* @module @deepseek-ai/dsh-tool-lsp
|
|
*/
|
|
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import z from '@deepseek-ai/schemastery'
|
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
import { assertNever } from '@deepseek-ai/dsh-llm'
|
|
import { LspError } from '@deepseek-ai/dsh-lsp'
|
|
import type {} from '@deepseek-ai/dsh-lsp'
|
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
|
import {
|
|
DEFAULT_MAX_LOCATIONS,
|
|
DEFAULT_MAX_RESULT_CHARS,
|
|
formatHover,
|
|
formatLocations,
|
|
LSP_OPERATIONS,
|
|
parseLspArgs,
|
|
presentLspCall,
|
|
} from './render.ts'
|
|
import { sessionCwd } from './session-cwd.ts'
|
|
|
|
export {
|
|
DEFAULT_MAX_LOCATIONS,
|
|
DEFAULT_MAX_RESULT_CHARS,
|
|
formatHover,
|
|
formatLocations,
|
|
LSP_OPERATIONS,
|
|
parseLspArgs,
|
|
presentLspCall,
|
|
renderUri,
|
|
} from './render.ts'
|
|
export { sessionCwd } from './session-cwd.ts'
|
|
|
|
/** Cordis plugin name for loader diagnostics. */
|
|
export const name = 'tool-lsp'
|
|
|
|
/** Services required by this plugin. */
|
|
export const inject = ['tools', 'lsp', 'systemPrompt']
|
|
|
|
/** Default tool-call timeout budget (ms), covering the queued open/query/close lifecycle. */
|
|
export const DEFAULT_LSP_TOOL_TIMEOUT_MS = 60_000
|
|
|
|
/** The stable system-prompt guidance positioning LSP as a precision aid. */
|
|
export const LSP_PROMPT_TEXT =
|
|
'Use search/read for ordinary navigation. Use lsp when textual matches are ambiguous or before a change requires precise definitions, implementations, or references. Positions are one-based line and character (UTF-16) at the cursor; an off-symbol position may return no results. findReferences always includes the declaration.'
|
|
|
|
/** Plugin configuration: result caps and the timeout budget. */
|
|
export interface Config {
|
|
/** Largest number of rendered locations before an omission marker (default 100). */
|
|
maxLocations?: number
|
|
/** Largest complete rendered result in characters, including truncation metadata (default 16000). */
|
|
maxResultChars?: number
|
|
/** Tool-call timeout budget in ms (default 60000). */
|
|
timeoutMs?: number
|
|
}
|
|
|
|
export const Config: z<Config> = z.object({
|
|
maxLocations: z.number().default(DEFAULT_MAX_LOCATIONS),
|
|
maxResultChars: z.number().default(DEFAULT_MAX_RESULT_CHARS),
|
|
timeoutMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_LSP_TOOL_TIMEOUT_MS),
|
|
})
|
|
|
|
type ResolvedConfig = Required<Config>
|
|
|
|
const LSP_POSITION_OUTPUT_SCHEMA = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
line: { type: 'integer', required: true },
|
|
character: { type: 'integer', required: true },
|
|
},
|
|
} as const
|
|
|
|
const LSP_RANGE_OUTPUT_SCHEMA = {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
start: { ...LSP_POSITION_OUTPUT_SCHEMA, required: true },
|
|
end: { ...LSP_POSITION_OUTPUT_SCHEMA, required: true },
|
|
},
|
|
} as const
|
|
|
|
/**
|
|
* Register the `lsp` tool and its system-prompt guidance.
|
|
* @param ctx - the plugin context (must inject `tools`, `lsp`, `systemPrompt`).
|
|
* @param config - the resolved plugin configuration.
|
|
*/
|
|
export function apply(ctx: Context, config: Config): void {
|
|
const resolved = config as ResolvedConfig
|
|
assertPositiveInteger('maxLocations', resolved.maxLocations)
|
|
assertPositiveInteger('maxResultChars', resolved.maxResultChars)
|
|
assertTimer('timeoutMs', resolved.timeoutMs)
|
|
|
|
ctx.systemPrompt.section({ name: 'tool:lsp', order: 112, text: LSP_PROMPT_TEXT })
|
|
|
|
ctx.tools.register(defineTool({
|
|
name: 'lsp',
|
|
description:
|
|
'Query a language server for precise code navigation. operation is one of goToDefinition, findReferences, goToImplementation, hover. line and character are one-based UTF-16 cursor coordinates. findReferences includes the declaration.',
|
|
parameters: {
|
|
operation: {
|
|
type: 'string',
|
|
required: true,
|
|
enum: [...LSP_OPERATIONS],
|
|
description: 'goToDefinition, findReferences, goToImplementation, or hover.',
|
|
},
|
|
file_path: { type: 'string', required: true, description: 'The source file to query, relative to the workspace or absolute.' },
|
|
line: { type: 'number', required: true, description: 'One-based line of the cursor.' },
|
|
character: { type: 'number', required: true, description: 'One-based UTF-16 column of the cursor.' },
|
|
},
|
|
output: {
|
|
schema: {
|
|
oneOf: [
|
|
{
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
kind: { type: 'string', required: true, const: 'locations' },
|
|
locations: {
|
|
type: 'array',
|
|
required: true,
|
|
items: {
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
uri: { type: 'string', required: true },
|
|
range: { ...LSP_RANGE_OUTPUT_SCHEMA, required: true },
|
|
},
|
|
},
|
|
},
|
|
resolvedWorkspaceUri: { type: 'string', required: true },
|
|
},
|
|
},
|
|
{
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
kind: { type: 'string', required: true, const: 'hover' },
|
|
hover: {
|
|
required: true,
|
|
oneOf: [
|
|
{ type: 'null' },
|
|
{
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
contents: { type: 'string', required: true },
|
|
range: LSP_RANGE_OUTPUT_SCHEMA,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
render: (_args, value) => {
|
|
switch (value.kind) {
|
|
case 'locations':
|
|
return [{ type: 'text', text: formatLocations(value.locations, value.resolvedWorkspaceUri, resolved.maxLocations, resolved.maxResultChars) }]
|
|
case 'hover':
|
|
return [{ type: 'text', text: formatHover(value.hover, resolved.maxResultChars) }]
|
|
/* v8 ignore next -- exhaustive over the output schema's closed union; unreachable. */
|
|
default:
|
|
return assertNever(value, 'tool-lsp output')
|
|
}
|
|
},
|
|
},
|
|
timeoutMs: resolved.timeoutMs,
|
|
async execute(args, exec) {
|
|
const input = parseLspArgs(args)
|
|
const workspaceRoot = sessionCwd(exec)
|
|
if (workspaceRoot === undefined) {
|
|
throw new LspError('the lsp tool requires a session workspace cwd', 'LSP_WORKSPACE_REQUIRED')
|
|
}
|
|
const result = await ctx.lsp.query({
|
|
operation: input.operation,
|
|
filePath: input.filePath,
|
|
position: input.position,
|
|
workspaceRoot,
|
|
}, exec.signal)
|
|
switch (result.kind) {
|
|
case 'locations':
|
|
return {
|
|
kind: 'locations' as const,
|
|
locations: result.locations.map(location => ({
|
|
uri: location.uri,
|
|
range: {
|
|
start: { line: location.range.start.line, character: location.range.start.character },
|
|
end: { line: location.range.end.line, character: location.range.end.character },
|
|
},
|
|
})),
|
|
resolvedWorkspaceUri: result.resolvedWorkspaceUri,
|
|
}
|
|
case 'hover':
|
|
return {
|
|
kind: 'hover' as const,
|
|
hover: result.hover === null
|
|
? null
|
|
: {
|
|
contents: result.hover.contents,
|
|
...result.hover.range === undefined
|
|
? {}
|
|
: {
|
|
range: {
|
|
start: { line: result.hover.range.start.line, character: result.hover.range.start.character },
|
|
end: { line: result.hover.range.end.line, character: result.hover.range.end.character },
|
|
},
|
|
},
|
|
},
|
|
}
|
|
/* v8 ignore next -- exhaustive over the closed LspQueryResult union; unreachable. */
|
|
default:
|
|
return assertNever(result, 'tool-lsp result')
|
|
}
|
|
},
|
|
presentCall: presentLspCall,
|
|
}))
|
|
}
|
|
|
|
/** Reject a non-positive-integer config value at load, so misconfiguration fails loud. */
|
|
function assertPositiveInteger(name: string, value: number): void {
|
|
if (!Number.isInteger(value) || value < 1) {
|
|
throw new Error(`tool-lsp: ${name} must be a positive integer`)
|
|
}
|
|
}
|
|
|
|
/** Reject a timer value Node would clamp instead of scheduling as configured. */
|
|
function assertTimer(name: string, value: number): void {
|
|
if (!Number.isInteger(value) || value < 1 || value > MAX_TIMER_DELAY_MS) {
|
|
throw new Error(`tool-lsp: ${name} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`)
|
|
}
|
|
}
|