关于 Code Crawler
Code Crawler 将 Java、Python、TypeScript 和 JavaScript 项目索引为 SQLite 属性图,为编码 Agent 和 LLM 客户端提供代码调用关系查询。它将静态调用、动态分派及框架路由等关系纳入图中,并支持增量索引。
主要功能
- 01四种语言写入同一代码图
- 02查询调用者、被调用者和调用路径
- 03追踪动态分派与反射关系
- 04索引依赖注入和 HTTP 路由
- 05增量索引并缓存解析结果
- 06查找热点、死代码候选和符号
适用场景
- 01追踪某个方法的传递调用者
- 02评估更改方法的影响范围
- 03分析框架路由和依赖注入
- 04定位热点与潜在死代码
MCP 相关
它通过 stdio MCP 服务向客户端提供代码图工具,包括符号搜索、调用追踪、实现查找和热点分析。索引保存在 SQLite 数据库中。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y @nithin554/code-crawlerCODECRAWLER_DBPath to the SQLite graph database (default ~/.code-crawler/index.db)CODECRAWLER_EXCLUDE_DIRSComma-separated directories to skip during crawlCODECRAWLER_DEFAULT_LIMITDefault result cap for list queries (default 50)CODECRAWLER_MAX_DEPTHTraversal depth cap (default 10)CODECRAWLER_HELPFUL_ERRORSFuzzy suggestions on unknown symbols (default true)本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
27收录的工具列表 · 更新于 1 天前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string | 是 | 要编入索引的项目根目录的绝对路径。 |
| force | boolean | 否 | 重新解析每个发现的文件,忽略清单中的大小、mtime 和内容哈希快捷判断。当索引可能已过期但元数据匹配时使用——否则,同大小编辑并恢复 mtime(git checkout、rsync -t、unzip)会从缓存重放。 |
| language | string | 否 | 将索引限制为一种解析器语言:'java'、'python'、'typescript' 或 'javascript'。扩展名不属于该语言的文件将被跳过。默认根据文件扩展名检测。 |
| fileFilter | string | 否 | 可选的子字符串筛选器:仅索引路径包含此字符串的文件。 |
详细介绍
Code-Crawler
由代码库持久化属性图提供支持的静态与动态调用追踪 MCP 服务。
Code-Crawler 会将项目索引到由 SQLite 支持的属性图中,涵盖文件、包、模块、类、接口、方法、字段、导入、注解/装饰器、调用边、虚拟分派、切面通知,以及框架添加的连接关系(入口点、路由、注入的依赖项、反射查找),然后将该图作为 MCP 工具提供给大模型客户端:查找调用方、追踪调用路径、探索邻域、跟踪运行时分派,以及在整个代码库中搜索符号。
支持 Java、Python、TypeScript 和 JavaScript 项目,并可将它们全部索引到同一张图中。
📖 文档网站: https://nithin554.github.io/code-crawler/
📘 任务指南: USAGE.md
🗺️ 路线图: ROADMAP.md
┌─────────────────────── MCP (stdio) ───────────────────────┐
│ crawl_project search_symbols get_callers get_callees │
│ trace_path trace_execution find_aspects get_decorators│
│ find_implementations find_hotspots find_dead_code ... │
└───────────────┬──────────────────────────────┬───────────────┘
│ │
┌──────▼───────┐ ┌────────▼────────┐
│ crawler │ │ graph queries │
│ parse+resolve│ │ recursive CTEs │
└──────┬───────┘ └────────┬────────┘
│ │
┌──────▼──────────────────────────────▼──────┐
│ SQLite property graph (WAL) │
│ nodes (kind, fqn, props) + edges (kind) │
└─────────────────────────────────────────────┘
为什么需要图?
纯文本 grep 无法回答这类问题:
- “谁会传递调用
OrderRepository#create?” - “从
Main#main到Tracking#dispatch是否存在静态路径?” - “更改
OrderService#place的影响范围有多大?”
这些问题需要遍历图。Code-Crawler 会构建一次图(按文件幂等构建),并通过有界递归 SQL 查询来回答。
功能
- 四种语言,一个图谱:Java(
tree-sitter-java)、Python (tree-sitter-python)、TypeScript(tree-sitter-typescript、tree-sitter-tsx)和 JavaScript(tree-sitter-javascript)——全部采用 WASM,无需原生构建步骤。 按文件检测语言,因此多语言代码库会汇入同一个图谱。 - 在静态图谱上进行动态追踪——
DYNAMIC_DISPATCH边将接口/基类调用连接到项目中的每个重写实现;ADVISES边则将 AspectJ/Spring AOP advice 编织到执行轨迹中(before、after、around、after-returning、after-throwing、命名的@Pointcut引用、&&/||/!以及execution、within、@annotation、@within、target、this、args原语)。 - 运行时间接调用有名可查,不会丢失——Java 反射/
ServiceLoader、Pythongetattr/importlib和 JSrequire/动态import()在目标可静态确定时,会生成指向解析后符号的REFLECTS边;否则会生成指向__reflect__:标记的边。 - 框架装配——依赖注入(
@Autowired、NestJS 构造函数注入、FastAPIDepends)会变为INJECTS边;HTTP 路由(@GetMapping、@RequestMapping、NestJS@Controller/@Get、FastAPI/Flask/Quart 装饰器、Expressapp.get)会变为指向处理程序的ROUTES边,处理程序也会被标记为入口点。 - 调用边上的控制流——每条
CALLS边都会记录调用是否有条件,以及其外层守卫/循环的精简源码文本,因此 Agent 无需读取文件,就能区分正常路径和错误路径。 - 明确标记外部依赖——第三方导入和运行时全局项会成为包级别的
external_symbol节点,并通过REFERENCES边关联;find_external_dependencies会提供汇总结果,与项目代码明确区分。 - 注解作为一等节点——
@Transactional、@Cacheable、@Async、@Retryable、Python 装饰器和 TS 装饰器会成为annotation节点,并通过ANNOTATED_WITH边关联,因此可以查询类型、方法和字段上的装饰器。 - 超越简单字符串匹配的调用解析启发式规则:
根据局部变量、参数、字段、
new T()表达式推断接收者类型;处理self/this作用域、点分路径(service.repo.find)、循环变量元素类型、不带限定符/this/super调用、静态/限定调用以及超类链。 无法解析的调用会成为unknown_symbol幻影节点,并生成resolved:false边,确保图谱始终可遍历。 - 增量式、按文件索引——每次扫描都会记录逐文件清单
(
size、mtime、SHA-256),并对内容未变的文件重用缓存的解析结果,因此重新扫描时只会重新解析实际发生变化的文件。变更会通过filesAdded/filesUpdated/filesUnchanged/filesRemoved报告。只有发生变更时,才会重建派生动态边并清理孤立项。完整重新扫描仍会移除已消失的文件,且不会重复构建图谱;限定范围的扫描(fileFilter或强制指定的language)绝不会移除范围外的文件。如果检出操作、rsync -t、保留 mtime 的解压操作或时钟偏差导致编辑后的文件恢复为原来的大小和 mtime,此快捷判断就会漏掉变更——请传入force(CLI 中使用--force)以重新解析所有内容。 - 由 FTS5 驱动的符号搜索——符号查找基于外部内容 FTS5 索引运行,并采用 BM25 排名和精确/前缀加权;仅当 FTS 未找到结果时才回退到
LIKE扫描。随着图谱增长,搜索性能不会下降。 - 带自动迁移的版本化架构——数据库会保存架构版本,并在打开时自动原位升级。如果数据库由更新版本构建程序写入,则会明确报错,而不会假定已知其结构并继续读取。
- 每个项目一个数据库——索引位于
<project>/.code-crawler/index.db,因此两个代码库绝不会冲突。若将已填充的数据库指向其他项目,会提示使用clear_index,而不会静默清除第一个索引。 - 分析——热点(扇入/扇出/advice 数量)和死代码候选项。
- 自包含:单个 SQLite 文件,无需外部服务。
- 适合大模型:每个工具都会返回结构化 JSON;未知符号会附带模糊搜索建议。
安装和构建
需要 Node.js ≥ 22.12(better-sqlite3@13 和 Vitest 工具链都需要此版本)。
npm install
npm run build # tsc → dist/
npm test # vitest (full suite; set CODECRAWLER_BENCH=1 for the scale test)
npm run lint # biome check (lint + format, fails CI)
npm run format # biome format --write
npm run coverage # vitest run --coverage (thresholds in vitest.config.ts)
npm run bench # crawl/query benchmark on a generated ~137k-line project
命令行界面
codecrawler crawl <path> # index a project
codecrawler crawl <path> --force # re-parse every file, ignoring cached metadata
codecrawler status # index summary
codecrawler query <symbol> # fuzzy-lookup a symbol
codecrawler serve # start the MCP server over stdio (default)
环境变量:
| 变量 | 默认值 | 用途 |
|---|---|---|
CODECRAWLER_DB | <project>/.code-crawler/index.db | 数据库路径。向上从当前工作目录解析,因此每个项目都有自己的文件;如果当前工作目录不属于任何项目,则回退到 ~/.code-crawler/index.db。主目录仅用于回退,不会被视为上级项目 |
CODECRAWLER_EXCLUDE_DIRS | .git,node_modules,dist,build,target,... | 爬取时跳过的目录 |
CODECRAWLER_DEFAULT_LIMIT | 50 | 默认结果上限 |
CODECRAWLER_MAX_DEPTH | 10 | 遍历深度上限(1–100) |
CODECRAWLER_MAX_FILE_BYTES | 1000000 | 大于此大小的文件会被跳过,不会解析 |
CODECRAWLER_RESPECT_GITIGNORE | true | 发现文件时遵循项目的 .gitignore |
CODECRAWLER_HELPFUL_ERRORS | true | 找不到符号时提供模糊匹配建议 |
CODECRAWLER_PLUGIN | unset | 插件模式:在调用时为每个项目解析一个数据库,而不是在启动时打开一个文件 |
CODECRAWLER_PLUGIN_ROOT | unset | 插件安装目录,不会作为项目编入索引 |
CODECRAWLER_PLUGIN_DATA | temp dir | 无项目时回退数据库的可写目录 |
无效值不会导致服务崩溃:布尔值会回退到默认值,整数会限制在文档规定的范围内;每种情况都会向 stderr 输出一行警告。
索引内容
crawl 会遍历项目,并跳过 .git、node_modules、构建输出以及 CODECRAWLER_EXCLUDE_DIRS 中的其他条目。此外:
-
会遵循项目的
.gitignore(设置CODECRAWLER_RESPECT_GITIGNORE=false可禁用), -
项目根目录中可选的
.crawlerrc可以通过 gitignore 风格的 glob 缩小或扩大文件发现范围,并调整模块 ID 的起始位置:{ "include": ["src/", "packages/core/"], "exclude": ["**/generated/"], "moduleRoots": ["src"] }当
include非空时,只会将匹配的文件编入索引。格式错误的.crawlerrc会被忽略并发出警告,不会导致爬取失败。moduleRoots默认不启用:设置为["src"]后,src/orders/service.ts会成为模块orders.service,而不是src.orders.service,这与使用baseUrl或PYTHONPATH的项目实际导入的模块一致。系统不会自动推断该设置,因为这会悄然改变现有索引中的编号。TypeScript 和 JavaScript 项目还会自动获得路径别名支持:系统会从tsconfig.json(或jsconfig.json)读取compilerOptions.paths和baseUrl,并在项目内解析extends,因此import { x } from "@app/orders/service"会解析到实际模块,而不是记录为外部模块。更改任一设置后,下次爬取时会重新解析缓存文件。 -
超过
CODECRAWLER_MAX_FILE_BYTES的文件以及疑似二进制文件会被跳过;crawl_project会在skippedFiles中列出这些文件及其原因。 -
单个文件的解析失败不会中止爬取;相关错误会记录在
parseErrors中。
目前只读取根目录中的 .gitignore,尚不支持嵌套的 .gitignore 文件。
增量爬取
每次爬取都会为每个文件记录一行清单(size、mtime、SHA-256、语言)。在
下一次爬取时,如果文件大小和截断后的 mtime 都匹配,就会直接使用缓存的解析结果,
而不是重新读取和解析;如果文件的 mtime 发生变化但哈希值未变,也会从缓存中重放。
解析、虚拟节点创建和派生边仍会针对整个内存文件集运行,因此跨文件边仍然正确——
只有 I/O、解析和数据库写入会跳过。
crawl_project 会报告以下统计:
| 字段 | 含义 |
|---|---|
filesAdded | 本次爬取中首次发现的文件 |
filesUpdated | 内容已更改并重新解析的文件 |
filesUnchanged | 从解析缓存中重放的文件 |
filesRemoved | 已从磁盘消失并被清除的文件 |
**已知限制:**由于未更改文件的边不会重写,因此,如果某个调用仅因其他文件发生变化
而变得新近可解析(例如,某个方法新增了一个重载,或者引入了一个类,使之前有歧义的
调用现在可以绑定到该类),未触碰的文件中的该调用不会更新。触碰该文件,或运行
clear_index,即可强制执行完整解析。
基准测试
npm run bench 会生成一个合成的 TypeScript 项目(575 个模块、约 137k 行、
约 9.8k 个方法),对其进行爬取,并报告爬取各阶段的耗时和查询延迟百分位数。
tests/perf.test.ts 会断言相同的性能预算;该测试为可选测试,因为爬取真实规模的
代码库需要一些时间:
npm run bench # human-readable report
CODECRAWLER_BENCH=1 npm test -- tests/perf.test.ts # enforce the budgets
CODECRAWLER_BENCH_LINES=20000 npm run bench # smaller corpus
以下数据是在磁盘缓存预热的 Windows 开发笔记本上测得;绝对数值会有所变化, 重要的是比例和索引的使用情况:
| 阶段 | 耗时 | 新增 | 更新 | 未更改 |
|---|---|---|---|---|
| 冷启动爬取 | 22.5 s | 575 | 0 | 0 |
| 重新爬取,未发生变化 | 0.91 s | 0 | 0 | 575 |
| 编辑一个文件后重新爬取 | 0.75 s | 0 | 1 | 574 |
| 查询 | p50 | p95 | 预算(p95) |
|---|---|---|---|
search_symbols(精确) | 0.69 ms | 1.96 ms | 50 ms |
search_symbols(前缀) | 1.60 ms | 4.09 ms | 50 ms |
get_callers(深度 2) | 0.16 ms | 0.21 ms | 50 ms |
get_callees(深度 2) | 0.11 ms | 0.15 ms | 50 ms |
get_subgraph(半径 2) | 0.16 ms | 0.27 ms | 50 ms |
find_entry_points | 0.12 ms | 0.29 ms | 50 ms |
find_hotspots | 145.5 ms | 181.4 ms | 1000 ms |
find_dead_code | 99.8 ms | 290.3 ms | 1000 ms |
对于上述 14.4k 个节点和 38.8k 条边,索引大小为 66 MB。find_hotspots 和
find_dead_code 会对整个项目中的每条边和每个节点进行聚合,因此它们的性能预算
比符号查找宽松一个数量级;这两个查询都使用 EXPLAIN QUERY PLAN 进行了性能分析,
并由 idx_edges_target_kind 提供服务。
CI 与发布
GitHub Actions 会在每个拉取请求上运行(pull-request.yml):npm ci →
类型检查 → 测试 → 构建 → 对构建后的 CLI 进行冒烟测试。
合并到 main 后会触发 publish.yml,该工作流会重新运行相同的检查,然后在
全部通过后自动发布:
- 读取最近的
v*标签(首次运行时读取package.json版本), 计算下一个补丁版本,并推送vX.Y.Z标签。 - 创建对应的 GitHub 发布版本。
- 使用 provenance(OIDC 可信发布)将包发布到 npm 注册表——无需令牌或 仓库密钥。此步骤支持幂等执行和重试:跳过 npm 中已有的版本;透明日志或注册表暂时出现故障时,也不会再中止发布。
- 使用单独且可重新运行的作业,通过
ExaDev/mcp-registry-publish将server.json以io.github.nithin554/code-crawler的名称发布到 官方 MCP 注册表。 该作业在mcp-registry-publish环境中运行,此环境的部署分支策略仅允许main。 它会将server.json固定到发布版本,使用 GitHub OIDC 进行身份验证,跳过注册表中已有的版本,并在 npm 传播或注册表暂时出错时重试。所有权通过package.json中的mcpName字段验证。
用作 MCP 服务
安装为 Agent 插件
此仓库也是一个 Agent 插件,因此 Claude Code 和 Copilot CLI 可以一起安装服务和技能:
# Claude Code
/plugin marketplace add nithin554/code-crawler
/plugin install code-crawler@code-crawler
# Copilot CLI
copilot plugin marketplace add nithin554/code-crawler
copilot plugin install code-crawler@code-crawler
该插件运行 npx -y @nithin554/code-crawler serve。Agent Plugins 客户端会在插件目录中启动插件的 stdio 服务,因此服务不会将该目录视为你的检出目录:它会从 crawl_project、客户端的工作区根目录或此会话已选定的项目中解析项目,并将该项目的图谱保存在 <project>/.code-crawler/index.db 中——每个代码仓库一个数据库,布局与 CLI 使用的相同。如果你需要固定使用某个文件,仍可通过 CODECRAWLER_DB 指定。
同一技能也以 code-crawler 的名称发布在 ClawHub 上,插件也发布在那里:
- 技能 — 使用
clawhub install code-crawler安装。ClawHub 会在已发布版本的修订号上加 1 来分配此版本,因此每次只前进一个版本,而不会复制 npm 版本;如果某次发布的技能发布失败,两个版本号就会保持不一致,直到下一次发布。 - 插件 — 作为
code-crawler捆绑插件发布,版本号取自发布标签:发布v0.1.67会以0.1.67发布捆绑包。捆绑包中的plugins/code-crawler/package.json是打包元数据——ClawHub 需要包清单来检查捆绑包并构建其 ClawPack 产物,缺少清单时会发出警告——因此发布工作流会将标签中的版本写入其中、打包捆绑包并发布该 tarball;已提交的version只是占位符。
插件发布会在 Node 下直接运行官方 clawhub CLI,而不会调用上游可复用的 package-publish.yml。该工作流无法发布此捆绑包:它会使用 bun ... package validate 检查打包后的产物,而 CLI 拒绝在 Bun 下运行(“Plugin validation requires Node.js 22.13+ (22.x) or 24+”)。ClawPack 始终包含 package/package.json,且没有输入项会跳过此步骤,因此只有在捆绑包没有清单时,这个调用才曾成功——而这正是 ClawHub 会警告的问题。
发布仅限于 main。发布作业通过 clawhub-publish 环境进行部署,该环境的部署分支策略会拒绝任何其他引用,因此从功能分支手动触发时,会在上传前停止。CLAWHUB_TOKEN 是仓库机密,而不是环境机密:技能调用会将其传递给上游,上游会将其读取为 clawhub_token,而调用可复用工作流的作业无法声明环境。通过作业输出转发该值也没有用——GitHub 会拒绝输出机密值,并记录 Skip output 'clawhub_token' since it may contain secret。
一条命令
add-mcp 会将服务注册到你指定的客户端中——Claude Code、Cursor、VS Code、Claude Desktop、Codex、OpenCode、Zed 等(npx add-mcp list-agents)。请在要建立索引的仓库中运行:
npx add-mcp @nithin554/code-crawler --args serve -a cursor
这会在代码旁写入项目级配置(.mcp.json、.cursor/mcp.json、.vscode/mcp.json 等),因此每个仓库都有自己的注册配置和 .code-crawler/index.db。服务名称 code-crawler 根据包名生成。
--env CODECRAWLER_DB 是可选的——只有想将数据库放到其他位置时才添加:--env CODECRAWLER_DB=/absolute/path/to/index.db。-g 会改写入用户级配置。只有在客户端以你的检出目录作为工作目录启动服务时,才建议使用此选项:如果使用 -g 且工作目录与检出目录无关(Claude Desktop 会从其自身的安装目录启动),服务就会回退到 ~/.code-crawler/index.db,导致每个仓库都争用同一个文件。
每个项目使用一个服务
在插件模式以外,服务进程会在启动时打开一个数据库,而一个数据库只保存一个项目,因此模型是每个代码仓库注册一次——这正是上述每项目配置的作用。插件安装则会在调用时按项目解析数据库,因此一个插件即可服务于你打开的所有代码仓库。重新爬取代码仓库会复用其现有文件,只重新解析有更改的内容; 两个代码仓库绝不会共用一个文件,因此不会发生冲突。一次只会打开少量项目数据库(空闲的数据库会关闭),客户端断开连接时会释放所有句柄。
MCP Registry / npx
该服务已在 MCP Registry 中登记,名称为 io.github.nithin554/code-crawler。
任何支持 npx 的客户端都可以运行它,无需本地检出代码:
{
"mcpServers": {
"code-crawler": {
"command": "npx",
"args": ["-y", "@nithin554/code-crawler", "serve"]
}
}
}
Claude Desktop
claude_desktop_config.json:
{
"mcpServers": {
"code-crawler": {
"command": "node",
"args": ["/absolute/path/to/code-crawler/dist/index.js", "serve"],
"env": { "CODECRAWLER_DB": "/absolute/path/to/your/index.db" }
}
}
}
Cursor / 其他 stdio MCP 客户端
将客户端指向同一命令。LLM Agent 的典型工作流:
- 对代码仓库运行一次
crawl_project。 - 使用
find_entry_points查看执行实际从何处开始(路由、监听器、main 函数)。 - 使用
search_symbols定位感兴趣的符号。 - 使用
get_callers/get_callees/trace_path分析调用图。 - 使用
trace_execution跟踪运行时分发和切面增强;使用find_aspects、get_decorators和find_annotated检查会改变行为的注解。 - 使用
find_routes、find_injections、find_reflections和find_external_dependencies梳理框架接线和第三方接口。
完整的、按任务组织的指南请参阅 USAGE.md。
MCP 功能
工具
| 工具 | 用途 |
|---|---|
crawl_project | 为目录建立索引(幂等;每个数据库对应一个项目) |
index_status | 节点/边/文件数量及上次爬取时间 |
search_symbols | 按名称或 FQN 模糊查找,可选按类型筛选 |
get_node | 获取单个符号的元数据和源代码位置 |
get_callers | 获取直接或传递调用者(限制深度) |
get_callees | 获取直接或传递被调用者(限制深度) |
trace_path | 两个符号之间的静态调用路径(BFS,优先返回最短路径) |
trace_execution | 动态追踪:静态调用 + 虚拟派发 + 编织的增强逻辑 |
find_implementations | 查找接口或超类的实现类/子类 |
find_aspects | 查找切面、增强类型、切入点和被拦截的方法 |
get_decorators | 获取符号及可选成员上的注解/装饰器 |
find_annotated | 查找带有指定注解的所有符号 |
find_hotspots | 按扇入、扇出和增强拦截次数对方法排名 |
find_dead_code | 查找没有传入调用/派发/增强边的方法 |
find_entry_points | 查找由框架调用的根节点:HTTP 路由、监听器、调度器、CLI 主函数、测试 |
find_routes | HTTP 路由 → 处理程序映射(@GetMapping、NestJS、FastAPI、Express) |
find_injections | 查找 DI 提供的依赖(@Autowired、NestJS、FastAPI Depends) |
find_reflections | 查找运行时查询:Class.forName、getattr、importlib、动态 import() |
find_external_dependencies | 查找第三方包及其引用数量和文件数量 |
find_unresolved | 查找解析器无法绑定的调用点,并按失败原因分组 |
impact_analysis | 一次调用获取调用者 + 入口点 + 路由 + 重写项 + 风险等级 |
explain_symbol | 一次性摘要:成员、装饰器、调用者、被调用者、连接关系 |
find_cycles | 查找调用环(SCC),附带示例闭合路径,以及直接递归 |
export_graph | 将邻域渲染为 Mermaid 或 Graphviz DOT |
get_subgraph | 提取用于可视化的邻域 |
get_schema | 描述图模型 |
clear_index | 重置数据库 |
资源:codecrawler://schema、codecrawler://status
提示词:trace-call-path(from, to)
令牌预算。 18 个返回列表的查询工具——即每个负载为单个结果列表的工具——均接受 max_tokens(100–200000)和 cursor。传入其中任一参数都会添加一个包含 totalCount、returnedCount、truncated、estimatedTokens 和 nextCursor 的 page 块;两者都省略时,响应格式保持不变,因此现有客户端仍可正常工作。只要还有项目,分页就不会返回空页,因此续页循环一定会终止。令牌数根据 JSON 长度估算(约每个令牌 4 个字符),无需依赖分词器,并且在不同模型间保持确定性。
get_subgraph 是唯一的例外:它的 nodes 和 edges 是同一切片的两个视图,因此截断其中一个列表会导致边指向已省略的节点。该工具改用 radius 和 limit 限制结果。聚合工具(impact_analysis、explain_symbol、export_graph)和单对象工具(get_node、index_status、get_schema)均返回单个对象,不接受预算。
Agent 技能
服务会告诉 Agent 它可以调用什么;技能会告诉它何时调用。skills/code-crawler/
随 npm 包一同发布,内容为纯 Markdown——SKILL.md 用于发现技能和了解优先爬取的工作流,reference/tools.md 将全部 27 个工具映射到它们能解答的问题,reference/edge-semantics.md 则说明 confidence、external 和虚影节点。
npx skills add nithin554/code-crawler
skills CLI 会按约定找到它,并安装到你选择的任意 Agent;npx skills update 可刷新技能。若要手动安装,请将该目录复制到客户端的技能文件夹(~/.claude/skills/code-crawler/、.claude/skills/code-crawler/ 或插件的 skills/ 根目录)。无需构建步骤。请参阅 USAGE.md。
tests/skill.test.ts 会将技能的工具表与实时注册表进行断言校验,因此添加工具时表格不会过时。
图模型
节点——kind 的取值包括 file | package | module | class | interface | enum | method | constructor | field | import | external_symbol | aspect | annotation_type | annotation | unknown_symbol。每个节点都有全局唯一的 qualifiedName 和一个 JSON properties
数据块(签名、修饰符、行号、注解等)。
边——kind 的取值包括 CALLS | DECLARED_IN | CONTAINS | IMPLEMENTS | EXTENDS | IMPORTS | REFERENCES | DYNAMIC_DISPATCH | ADVISES | ANNOTATED_WITH | REFLECTS | INJECTS | ROUTES。CALLS 边包含 line、resolved、confidence,若调用位于控制流之后,还会包含 conditional、controlFlow 和 guards;ANNOTATED_WITH 边包含 line 和 arguments;ADVISES 边包含 adviceKind 和已解析的 pointcut;INJECTS 包含 injectedName 和 framework;REFLECTS 包含 mechanism;REFERENCES 包含 external。
ID 方案:
| 符号 | 示例 |
|---|---|
| 类型 | com.acme.OrderService |
| 方法 | com.acme.OrderService#place |
| 构造函数 | com.acme.OrderService#<init> |
| 字段 | com.acme.OrderService$repo |
| 注解 | @Transactional |
| 导入 | __import__:java.util.List |
| 未解析的调用 | __unknown__:System.out.println |
| Python/TS 模块 | module:app.service |
| 模块级函数 | module:app.api#read_order |
| 模块级字段 | module:app.service$AUDIT_LOG |
| 内联箭头处理函数 | module:src.routes#<anonymous@7> |
| 外部包 | ext:fastapi |
| 反射标记 | __reflect__:./plugins/${name} |
| 注入标记 | __inject__:Repo |
动态行为
静态调用边回答的是*“源代码字面上调用了什么?”*运行时行为有两处不同之处,两者都会建模为派生边,并在每次爬取时重新生成:
虚拟分派
对接口或基类方法的调用可能会执行任意一个重写方法。所有文件完成索引后,每条目标为可重写方法且已解析的 CALLS 边都会获得指向项目中找到的各个重写方法的 DYNAMIC_DISPATCH 边:
Checkout#checkout --CALLS--> Payment#pay
Payment#pay --DYNAMIC_DISPATCH--> CardPayment#pay
Payment#pay --DYNAMIC_DISPATCH--> UpiPayment#pay
构造函数以及 private/static/final 方法不在此列,因为它们不能被重写。使用 find_implementations 列出候选项,并使用 trace_execution 跟踪调用链。
切面、通知和装饰器
@Aspect 类型会被索引为 aspect 节点。通过 @Before、@After、@Around、@AfterReturning、@AfterThrowing 和 @Pointcut 识别通知方法。命名切入点(@Around("paymentCall()"))会解析为其 @Pointcut 表达式,然后与项目中的每个方法匹配,以创建 ADVISES 边:
AuditAspect#auditPayment --ADVISES--> Payment#pay
AuditAspect#auditPayment --ADVISES--> CardPayment#pay
AuditAspect#auditPayment --ADVISES--> UpiPayment#pay
AuditAspect#beforeCheckout --ADVISES--> Checkout#checkout
trace_execution 会将这些内容织入为 INTERCEPT 跳转,因此追踪结果如下:
Checkout#checkout
INTERCEPT AuditAspect#beforeCheckout
CALLS Payment#pay
INTERCEPT AuditAspect#auditPayment
DYNAMIC_DISPATCH CardPayment#pay -> authorize
DYNAMIC_DISPATCH UpiPayment#pay -> settle
所有其他注解都会被索引为带有 ANNOTATED_WITH 边的 annotation 节点。框架装饰器(@Transactional、@Cacheable、@Async、@Retryable、@PreAuthorize、@Scheduled 等)还会标记为 decorator: true,以便 get_decorators 和 find_annotated 找到具有运行时语义的注解。
Python 装饰器和 TypeScript 装饰器采用相同的建模方式:@router.get(...)、@injectable、@Controller(...)、@Get(':id') 等会作为 annotations 写入声明它们的类型或方法;框架装饰器还会额外生成路由、注入或入口点标记。
反射和运行时间接调用
编译器无法解析的查找会记录为 REFLECTS 边。当目标可以静态确定时(字符串字面量、字面量前缀加变量(importlib.import_module("app.plugins." + name))、getattr(obj, "run")、require("./x")),边会指向解析出的符号。否则,它会指向一个携带 mechanism 的 __reflect__: 标记:
module:app.dynamic#load_plugin --REFLECTS--> __reflect__:app.plugins. (importlib.import_module)
module:src.dynamic#loadPlugin --REFLECTS--> __reflect__:./plugins/${name} (import())
属于项目自身命名空间的动态模块前缀仍留在项目内;只有确实属于外部的包才会成为外部依赖。
依赖注入
由框架提供的依赖会表示为 INJECTS 边,而不是缺失的调用(find_injections):
| 框架 | 检测依据 |
|---|---|
| Spring | @Autowired 字段、方法和构造函数 |
| NestJS | 带装饰器的构造函数参数;@Injectable() 类 |
| FastAPI | Depends(...) 参数 |
| dependency-injector | @injectable 类的构造函数参数 |
| plain TS/JS | 类型为项目类的构造函数参数 |
原始类型和结构化参数类型(string、{ id: string }、list[str])会被统计,但不会生成边,因此 @Body() body: { id: string } 不会污染图。
框架入口点和路由
框架调用的方法没有源代码层面的调用方,因此标记这些方法可以让死代码分析更准确,并为 trace_execution 提供根节点(find_entry_points、find_routes):
| 语言 | 可识别项 |
|---|---|
| Java | @GetMapping、@PostMapping、@RequestMapping、@Scheduled、@EventListener、监听器注解、测试、main |
| Python | FastAPI(@router.get、@app.post、@router.websocket)、Flask(@bp.route、@app.get)、Quart(@bp.route)、if __name__ == "__main__"、pytest 测试 |
| TypeScript | NestJS @Controller + @Get/@Post/...,Express app.get('/x', handler),Jest 测试 |
内联编写的 Express 处理函数会获得一个合成方法节点(module:src.routes#<anonymous@7>),因此其函数体仍会获得调用边、包含关系和路由边。
调用边上的控制流
每条 CALLS 边都会记录它是否受条件保护:
{
"kind": "CALLS",
"source": "app.repository.OrderRepository#create",
"target": "ext:builtins",
"properties": { "line": 14, "conditional": true, "controlFlow": ["branch", "loop"],
"guards": ["row.get(\"id\") == order_id", "self.rows"] }
}
controlFlow 使用 branch | loop | exception | predicate。保护条件是源代码文本,而非符号约束:它会告诉 Agent 某次调用是有条件的,以及条件的形式,但不会假装能够建模其真值。
解析启发规则
Java:
- 构造函数调用(
new Foo(...))→ 解析Foo→#<init>(若未显式定义,则使用默认构造函数)。 - 裸调用 /
this.调用 /super.调用 → 所属类型及其父类链。 - 类型限定调用(
com.acme.Foo.bar)→ 直接解析该类型。 - 实例调用(
x.method())→ 从局部变量 → 参数 → 字段推断接收者类型, 然后搜索该类型及其祖先类型。 - 否则 → 创建虚设的
unknown_symbol节点,设为resolved:false,并将推断 失败原因附加到边上。
Python 和 TypeScript 在同一流程基础上增加了模块感知解析:
- 感知导入的作用域 —
from app.repository import OrderRepository会将本地 名称映射到app.repository.OrderRepository;相对模块和项目模块按后缀解析,裸说明符则回退到包根目录。 self.x/this.x会根据所属类的字段解析;以self/this/cls/super开头的接收者永远不会被视为外部包。- 点分接收者(
service.repo.find)会逐段遍历:第一段解析为类型,下一段解析为该类型上的字段;list.append等容器成员会被识别为内置成员,而非未知成员。 - 局部变量会根据参数、带注解的赋值、
new T()以及推断出的循环/变量元素类型确定类型(for row in self.rows:→row: dict)。 - 模块函数(
module:app.api#read_order)会像类方法一样参与分派、路由和入口点标记。
每条已解析的边都包含一个 confidence 置信度分数和一条易读的 reason 溯源说明
(例如 via local orders: OrderService),以便 LLM 客户端评估应在多大程度上信任该结果。
已实现的语言
| 语言 | 扩展名 | 提取内容 | 动态特性 |
|---|---|---|---|
| Java | .java | 包、类、接口、枚举、注解、构造函数、方法、字段 | 虚拟分派、AspectJ/Spring AOP 通知、@Autowired、@RequestMapping 系列、Class.forName、ServiceLoader |
| Python | .py, .pyi | 模块、类、函数、方法、字段、装饰器 | @injectable/Depends 注入、FastAPI/Flask/Quart 路由、getattr、importlib、__main__、测试 |
| TypeScript | .ts, .tsx, .mts, .cts | 模块、类、接口、类型别名、枚举、方法、字段、箭头函数 | NestJS/Angular 装饰器、DI 构造函数注入、NestJS/Express 路由、import()、require() |
| JavaScript | .js, .jsx, .mjs, .cjs | 模块、类、函数、字段 | 装饰器、Express 路由、require() |
爬取目录时会自动混合处理所有这些语言——根据文件扩展名为每个文件选择语言,单次爬取即可返回 languages: ["java", "python", "typescript"]。
添加一种语言需要:
src/parser/languages/<lang>.ts— 一个实现parseFile的LanguageDescriptor(提取模块/包、类型/成员、原始调用点、本地类型表),以及三个声明式字段:resolver(处理该语言的调用解析器)、frameworks(其解析器可识别的框架)和fixture(其一致性测试目录)。- 在
src/parser/languages/index.ts中添加一行。 - 在
tests/language-conformance.test.ts的CONTRACTS表中添加一行——随后测试套件会要求新语言满足与其他所有语言相同的图不变量。 - 可选:在
src/resolver/中添加解析器,并通过src/resolver/registry.ts暴露;大多数语言可使用共享的module解析器,只需覆盖语言特有的内置项。 - 添加尚未注册且应识别的任何框架——在
src/parser/frameworks/中创建模块,声明其入口点、注入和标记。这只是数据变更:无需修改解析器。
框架按文件解析:首先依据导入证据,然后按描述符的 frameworks 顺序,最后尝试所有已注册模块。导入 dependency_injector 的文件会将其 @inject 属性归属于该框架;没有此类导入的 Java 文件也会回退到注册表,而不会丢失注解。解析器只保留真正属于语法的信息:调用式路由的接收者名称、路径拼接,以及记录为入口点详情的文本。
无需编辑任何现有语言模块:src/crawler.ts 中的分派会读取描述符的 resolver,因此语言不会出现在条件判断中。
所有语言使用相同的图架构、存储、遍历查询和 MCP 接口。候选后续语言及其所需的建模工作已在 ROADMAP.md 的第 3 阶段中排序。
未建模的内容
除下文列出的共通项外,各语言特有的缺口:
| 语言 | 未建模的内容 |
|---|---|
java | record 会被索引为带有 record: true 的 class;其组件会成为 private final 字段,并各自生成一个访问器,但在调用解析到隐式规范构造函数之前,该构造函数不会获得签名。Lambda 函数体会被索引,但像 this::handle 这样的函数引用没有静态目标,因此不会生成调用边。不会提取 static {} 和实例初始化块。泛型类型参数会被擦除,因此通过 List<T> 字段进行的调用会绑定到声明的上界,而不是具体参数。 |
python | 不建模元类、描述符、__getattr__ 或 __getattribute__。装饰器生成而非显式编写的成员(如 @dataclass 生成的 __init__ 和 __eq__,以及元类添加的任何成员)没有可指向的声明,因此对这些成员的调用仍无法解析。不会跟踪导入时的猴子补丁。存储在属性中并稍后调用的 lambda 没有目标。 |
typescript | namespace X {} 代码块不会生成模块节点,因此其中声明的成员无法按名称访问。JSX 可以正常解析,但不会对元素树建模。类型级构造(条件类型、映射类型、模板字面量类型、交叉类型)不会进行求值,因此此类类型的接收者会回退为无法解析。不会提取 static {} 代码块和 accessor 字段。 |
javascript | 由 TypeScript 解析器处理,因此上述所有缺口均适用。由于没有注解,接收者只能来自 new、赋值的初始化表达式以及语言无关的启发式规则,因此动态类型代码的解析率低于带注解的代码。 |
按设计尚未建模的内容:
- 值流和分支可行性——边会说明调用是否有条件,并记录守卫文本,但不会说明实际执行哪个分支。
- 跨仓库解析——爬取目录之外的库会被概括为
ext:包,而不会被爬取。 - 完全动态的目标(
getattr(obj, name)、require(userInput))会指向__reflect__:标记,而非符号;没有可供静态指向的目标。 - 从配置文件而非代码组装的运行时 DI 图。
- 从表达式而非名称推断接收者——
getService().run()、items[0].run()、三元表达式或类型转换等调用会报告为无法解析。 - 重载选择会优先使用参数数量,然后在调用位置明确给出参数类型时(字面量、
new表达式、类型转换)使用各参数的类型。如果调用无法区分两个重载(例如f(count)对应f(int)和f(String)),仍会绑定到源码中第一个声明。重载声明在第一个之后会保留不同的 ID(Type#f、Type#f(int)、Type#<init>(String));ID 方案见USAGE.md。
完整列表以及针对各项缺口所建模的内容,见 USAGE.md。
架构
src/
├── index.ts CLI (crawl | status | query | serve)
├── server.ts MCP server: tools, resources, prompt (thin handlers)
├── crawler.ts file discovery → parse → index → resolve → persist
├── config.ts env-overridable configuration
├── graph/
│ ├── schema.ts node/edge kinds, id scheme, property shapes
│ ├── store.ts better-sqlite3 schema + idempotent file-scoped upserts
│ └── queries.ts recursive-CTE traversal (callers/callees/path/subgraph)
├── parser/
│ ├── language.ts LanguageConfig / LanguageDescriptor contracts
│ ├── treeSitter.ts WASM runtime + grammar loading + CST walk helpers
│ ├── controlFlow.ts enclosing branch/loop/try/catch + guard text
│ ├── moduleId.ts import-aware module id derivation
│ ├── methodId.ts method id scheme (overload signatures, constructor ids)
│ ├── frameworks/
│ │ ├── types.ts FrameworkModule / FrameworkScope contracts
│ │ ├── index.ts registry + scope/entry-point/injection lookups
│ │ └── <framework>.ts one module per framework (spring, nestjs, fastapi, …)
│ └── languages/
│ ├── index.ts descriptor table + per-file language detection
│ ├── java.ts Java structural extraction
│ ├── python.ts Python structural extraction
│ └── typescript.ts TypeScript, TSX and JavaScript extraction
└── resolver/
├── types.ts TypeIndex (global name/method index) + Resolver contract
├── registry.ts resolver lookup, capability query, default fallback
├── common.ts builtin types, external packages, receiver inference
├── dynamic.ts virtual dispatch + AspectJ pointcut matching/advice edges
├── genericResolver.ts the `module` resolver (imports, barrels, overloads)
└── javaResolver.ts the `java` resolver (declared types, hierarchy, overloads)
路线图
语言无关的架构、更多语言支持以及更完善的解析均在 ROADMAP.md 中跟踪。近期重点是将新增一种语言所需的改动缩减为一个模块,然后以此为基础添加 Go、C# 和 Kotlin 支持。
文档站点
docs/ 中的静态文档站点由 .github/workflows/pages.yml 部署到 GitHub Pages。站点涵盖快速入门、MCP 工具参考和实例教程。可在本地打开 docs/index.html,或访问
https://nithin554.github.io/code-crawler/。
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。