Cline SDK TypeScript LSP 插件实战:为 Agent 注入语言服务级的 goto_definition 导航工具
本文以 Cline SDK 官方示例插件 typescript-lsp 为对象,完整讲解如何用 TypeScript Language Service API 为 Cline Agent 注册一个 goto_definition(file, line) 工具:从插件的注册结构(AgentPlugin + createTool)、Language Service 的构建与缓存,到通过 CLI 安装和直接运行 demo 的完整流程。读完后你可以独立编写类似"封装内部 API 为插件工具"的扩展,并理解它为何比 grep/文本搜索精准得多。
插件解决了什么问题
Agent 在大代码库中定位符号时,如果只有文本搜索能力,会出现三类典型错误:找不到真正的定义(命中的是引用或注释)、无法区分原始定义与 re-export、无法解析包别名(如 @cline/shared)背后的真实源文件。
Cline SDK 自带的 typescript-lsp 插件给出的方案是:注册一个由 TypeScript Language Service API 驱动的工具,让 Agent 走和 IDE "Go to Definition" 完全相同的语义通道——穿透 import、re-export、类型别名和声明合并来解析符号。
插件只向 Agent 暴露一个工具 goto_definition(file, line):给定文件路径和行号,它会找出该行上的所有标识符,并解析它们真正的定义位置。以一条 import 行为例:
import { disposeAll, initVcr } from "@cline/shared"
插件会沿着 workspace 的包别名把两个符号都解析到源码文件:
disposeAll -> packages/shared/src/dispose.ts:19
initVcr -> packages/shared/src/vcr.ts:699
这也是官方插件示例集中把它列为重点示例的原因。在 插件示例总览中,typescript-lsp 被描述为"加载目标项目自己的 TypeScript 版本、找出某行上的标识符、并借助语言服务语义解析定义"的工具型插件,与 weather-metrics、background-terminal 等共同覆盖 Cline 插件的四大能力:注册工具、生命周期 Hook、消息重写、自动化事件。
插件结构:AgentPlugin + createTool
整个插件就是一个 TypeScript 文件,入口在 index.ts。它的顶层结构是一个 AgentPlugin 对象(文件末尾同时以具名和默认方式导出,第 297–298 行):
const plugin: AgentPlugin = {
name: "typescript-lsp",
manifest: {
capabilities: ["tools"],
},
setup(api) {
api.registerTool(
createTool({
name: "goto_definition",
description: "Find where TypeScript/JavaScript symbols on a given line are defined...",
inputSchema: {
type: "object",
properties: {
file: { type: "string", description: "Absolute path to the file." },
line: { type: "integer", description: "Line number (1-based)." },
},
required: ["file", "line"],
},
async execute(input) {
// 1. 从文件向上查找 tsconfig.json
// 2. 创建(或复用缓存的)TypeScript Language Service
// 3. 扫描 AST 找出目标行上的所有标识符
// 4. 通过 Language Service 解析每个标识符的定义
// 5. 过滤掉自引用,返回位置信息
},
}),
);
},
};
这里的每个字段都能在 SDK 共享类型中找到对应实现:
manifest.capabilities:插件必须声明自己使用哪些能力。在 contribution-registry.ts 中,合法能力值为"hooks" | "tools" | "commands" | "rules" | "skills" | "messageBuilders" | "providers" | "automationEvents" | "mcp",其中声明tools才会解锁api.registerTool()。注册表在setup()执行前会校验"声明的能力必须存在对应实现,反之也不允许出现未声明的处理器"(见ContributionRegistryExtension的 JSDoc,第 250–276 行)。createTool:由@cline/core导出,实现在 shared 包。它接收name、description、inputSchema(JSON Schema 对象或 Zod schema)、execute四个核心参数,另有三个有默认值的可选项——timeoutMs默认30_000,retryable默认true,maxRetries默认3。typescript-lsp 显式传了timeoutMs: 30000, retryable: false(index.ts 第 186–187 行),含义是:Language Service 解析最多等 30 秒,且失败时不自动重试(解析失败通常是文件未包含在 tsconfig 中等确定性错误,重试无意义)。inputSchema约束:createTool内部的normalizeToolInputSchema会强制顶层必须是 object 形状,并剥掉 Zod 序列化产生的$schema元键;对oneOf/anyOf/allOf分支,要求每个分支都显式断言type: "object",否则在注册期直接抛错。这解释了为什么插件里inputSchema直接写成{ type: "object", properties: {...}, required: [...], additionalProperties: false }这种严格的 object schema。
内部实现:六个步骤构建精准导航
execute 的完整流程(index.ts 第 188–291 行)可以拆成六个环节,下面逐一对应源码实现。
1. findTsConfig:向上查找最近的 tsconfig.json
function findTsConfig(startDir: string): string | undefined {
let dir = startDir;
while (true) {
const candidate = join(dir, "tsconfig.json");
if (existsSync(candidate)) return candidate;
const parent = dirname(dir);
if (parent === dir) return undefined;
dir = parent;
}
}
从目标文件所在目录逐级向上,直到找到 tsconfig.json 或到达文件系统根。找不到会抛出 No tsconfig.json found in any parent directory 错误——这界定了插件的适用前提:目标项目必须是 TypeScript 项目且存在 tsconfig。
2. loadTypeScript:复用目标项目的 TypeScript 版本
function loadTypeScript(projectDir: string) {
const req = createRequire(resolve(projectDir, "package.json"));
const tsPath = req.resolve("typescript");
return req(tsPath) as typeof import("typescript");
}
关键点是用 createRequire() 以目标项目的 package.json 为基准解析 typescript 模块。也就是说,插件自身零依赖(只用到 node:fs、node:module、node:path 等 Node 内建模块),但运行时使用的 TS 版本与目标项目编译所用版本一致,避免"用新 TS 解析老项目"带来的 API 行为差异。
3. createLanguageService:用项目编译选项构建 Language Service
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, projectDir);
const host: import("typescript").LanguageServiceHost = {
getScriptFileNames: () => parsed.fileNames,
getScriptVersion: () => "1",
getScriptSnapshot: (fileName) => {
const content = ts.sys.readFile(fileName);
if (content === undefined) return undefined;
return ts.ScriptSnapshot.fromString(content);
},
getCurrentDirectory: () => projectDir,
getCompilationSettings: () => parsed.options,
// ...
};
return ts.createLanguageService(host, ts.createDocumentRegistry());
LanguageServiceHost 是 TS 语言服务与文件系统之间的适配层:脚本列表直接取 parsed.fileNames(即 tsconfig 的 include 展开结果),编译选项取 parsed.options。因此插件的解析行为——包括包别名、路径映射、模块解析策略——完全继承项目自身的 tsconfig 配置,这正是它能解析 @cline/shared 这类 workspace 包别名的根本原因。tsconfig.json 读取失败时会把诊断信息扁平化后抛出,错误可定位到具体配置问题。
4. 缓存:同一会话内复用 Service
function getOrCreateService(tsconfigPath: string) {
if (cache && cache.tsconfigPath === tsconfigPath) {
return cache;
}
const projectDir = dirname(tsconfigPath);
const ts = loadTypeScript(projectDir);
const service = createLanguageService(ts, tsconfigPath);
cache = { tsconfigPath, service, ts };
return cache;
}
模块级变量 cache 记录 { tsconfigPath, service, ts } 三元组,只有 tsconfig 路径变化时才重建。第一次调用要付出解析整个项目的代价,后续调用命中缓存。
这里有一个值得注意的生命周期细节:官方插件文档说明了沙箱插件在空闲 30 分钟(可用 CLINE_PLUGIN_IDLE_TIMEOUT_MS 调整)后子进程会被回收,下次调用会重新执行 setup()。因此这个模块级缓存应当视为缓存而非持久存储——空闲驱逐、宿主重启或沙箱崩溃后都会重置,需要跨这些边界存活的状态应落盘。
5. getIdentifiersOnLine:AST 扫描目标行
function getIdentifiersOnLine(ts, sourceFile, targetLine) {
const identifiers: Array<{ offset: number; name: string }> = [];
function visit(node) {
if (ts.isIdentifier(node)) {
const lc = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
if (lc.line + 1 === targetLine) {
identifiers.push({ offset: node.getStart(sourceFile), name: node.text });
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return identifiers;
}
它遍历整个 SourceFile 的 AST,收集 ts.isIdentifier 命中的节点,并按"节点起始偏移换算出的行号(1-based)等于目标行"过滤。相比按文本切行,AST 扫描天然排除了字符串、注释中的干扰文本——"找一行里的标识符"本身也是语义行为。
6. getDefinitionAtPosition:解析定义并过滤自引用
const definitions = service.getDefinitionAtPosition(fileName, offset);
const nonSelfDefs = definitions.filter((def) => {
if (def.fileName !== fileName) return true;
const defLine = offsetToLineCol(sourceFile, ts, def.textSpan.start);
return defLine.line !== line;
});
service.getDefinitionAtPosition() 是语言服务的核心查询入口,负责穿透 import、re-export、类型别名等语义。返回的每个定义被转换回 1-based 的行列号(内部 getLineAndCharacterOfPosition 是 0-based,offsetToLineCol 统一 +1),并附带 kind(定义符号的种类)和 containerName(外层容器名,如类/命名空间)。过滤自引用的规则是:定义若与查询位置同文件且同行为同一行,则丢弃——避免"定义就在此行"这类对 Agent 无信息量的结果。同一行的同名标识符还会经 seen Set 去重。
工具的输出形态有三种分支:无标识符时返回 { found: false, message: "No identifiers found on this line." };有标识符但都解析不到外部定义时返回相应的说明性消息;成功时返回 { found: true, query, tsconfig, results },其中 results 是 { symbol, definitions: [{ file, line, column, kind, name, containerName? }] } 数组。另外两个前置校验也值得注意:文件不存在直接抛错;文件未被 tsconfig 的 include 覆盖时,program.getSourceFile(fileName) 返回空并抛出 File not found in TypeScript program. Make sure it is included by tsconfig.json。
两种使用方式:CLI 安装与直接运行 demo
方式一:通过 Cline CLI 安装
cline plugin install https://github.com/cline/cline/blob/main/sdk/examples/plugins/typescript-lsp/index.ts
cline -i "Find where createTool is defined"
cline plugin install 支持单文件、GitHub 文件 URL、包目录、git 仓库和 npm 包等多种形式(见 插件示例总览 中 "Try it with the CLI" 一节)。CLI 会自动从工作区的 .cline/plugins、用户主目录的 ~/.cline/plugins 以及系统 Plugins 文件夹发现插件。由于插件运行时从目标项目的 node_modules 解析 typescript,安装后无需在插件侧添加任何依赖。
方式二:直接运行 demo 脚本
ANTHROPIC_API_KEY=sk-... bun run examples/plugins/typescript-lsp/index.ts
index.ts 同时是一份可执行的 demo 脚本(bun run 时以 @cline/sdk 的 ClineCore 创建 host 并把插件挂进 extensions),无需预先安装为插件即可观察 goto_definition 的端到端行为。
方式三:以 SDK 扩展的形式接入
如果基于 @cline/core 构建自己的 Agent 宿主,把插件对象直接放进 ClineCore.start() 的 config.extensions 即可:
const host = await ClineCore.create({ backendMode: "local" });
await host.start({
config: {
providerId: "anthropic",
modelId: "claude-sonnet-4-6",
apiKey: process.env.ANTHROPIC_API_KEY ?? "",
cwd: process.cwd(),
enableTools: true,
systemPrompt: "You are a helpful assistant.",
extensions: [plugin],
},
prompt: "Find where createTool is defined",
interactive: false,
});
从源码结构看,extensions 与 AgentConfig 中的扩展列表类型贯通(agents/types.ts 定义了 extensions?: AgentExtension[]),注册表在初始化阶段执行每个扩展的 setup(api, ctx) 并汇总其贡献的工具,随后这些工具与 Core 内置工具一样进入模型的 tool 列表。
适用边界与限制
结合 index.ts 源码,可以归纳出该插件的明确边界:
- 仅面向 TypeScript/JavaScript 项目:无
tsconfig.json(且不在任意父目录中)时直接报错,纯 Python/Go 仓库不适用。 - 文件必须在 tsconfig 的包含范围内:
program.getSourceFile()查不到文件会报 "Make sure it is included by tsconfig.json",被排除(exclude)的目录解析不了。 - 30 秒超时、不重试:超大 monorepo 首次构建 Language Service 可能接近
timeoutMs上限,但缓存后后续调用显著变快。 - 模块级缓存在沙箱回收后失效:按上文生命周期说明,空闲驱逐后状态重置,插件行为仍是正确的(只是重新付出首次构建代价),但插件作者不应依赖它保存业务状态。
- 一次查询一行:
goto_definition只解析传入的那一行上的标识符,跨多行的 import 块需要按行分别查询。
参考模式:把内部系统封装成 Agent 工具
官方文档把这个示例的意义概括为:文本搜索能找到符号名,但无法区分定义、引用、re-export 和被遮蔽的变量,而 TypeScript Language Service 能处理这些。同一个模式可以直接迁移到企业内部场景——把内部 API、部署系统、功能开关、事件管理、CI 流水线等团队日常操作的系统封装成插件工具。
落地时只需照搬本示例的三段式结构:
- 声明最小能力:
manifest.capabilities: ["tools"],声明即契约,注册表会做双向校验; - 严格定义工具契约:
inputSchema保持顶层 object 形状、显式required与additionalProperties: false,并针对"确定性失败不重试、耗时长操作设超时"调整retryable/timeoutMs(默认分别为true/30000,可对照 createTool 签名 取值); - 把昂贵的资源构建放进模块级缓存:如 Language Service 之于本项目,注意它是"缓存而非存储",跨沙箱边界的持久化另作设计。
一个插件就是一个 TypeScript 文件——不需要托管和维护一个 MCP server,cline plugin install 一条命令即可让所有 Cline 宿主(CLI、VS Code 等)获得新能力。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00