preflight-mcp 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,47 +3,81 @@
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
5
5
  [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io/)
6
+ [![npm version](https://img.shields.io/npm/v/preflight-mcp)](https://www.npmjs.com/package/preflight-mcp)
6
7
 
7
8
  > **English** | [中文](./README.zh-CN.md)
8
9
 
9
- An MCP (Model Context Protocol) **stdio** server that creates evidence-based preflight bundles for GitHub repositories and library documentation.
10
-
11
- Each bundle contains:
12
- - A local copy of repo docs + code (normalized text)
13
- - A lightweight **full-text search index** (SQLite FTS5)
14
- - Agent-facing entry files: `START_HERE.md`, `AGENTS.md`, and `OVERVIEW.md` (factual-only, with evidence pointers)
15
-
16
- ## Features
17
-
18
- - **13 MCP tools** to create/update/repair/search/read bundles, generate evidence graphs, and manage trace links
19
- - **5 MCP prompts** for interactive guidance: menu, analyze guide, search guide, manage guide, trace guide
20
- - **LLM-friendly outputs**: After bundle creation, prompts user to generate dependency graph for deeper analysis
21
- - **Proactive trace links**: LLM automatically discovers and records code↔test, code↔doc relationships
22
- - **Auto-export trace.json**: Trace links are automatically exported to JSON for direct LLM reading (no API needed)
23
- - **Progress tracking**: Real-time progress reporting for long-running operations (create/update bundles)
24
- - **Bundle integrity check**: Prevents operations on incomplete bundles with helpful error messages
25
- - **De-duplication with in-progress lock**: Prevent duplicate bundle creation even during MCP timeouts
26
- - **Global dependency graph**: Generate project-wide import relationship graphs
27
- - **Batch file reading**: Read all key bundle files in a single call
28
- - **Resilient GitHub fetching**: configurable git clone timeout + GitHub archive (zipball) fallback
29
- - **Offline repair**: rebuild missing/empty derived artifacts (index/guides/overview) without re-fetching
30
- - **Static facts extraction** via `analysis/FACTS.json` (non-LLM)
31
- - **Resources** to read bundle files via `preflight://...` URIs
32
- - **Multi-path mirror backup** for cloud storage redundancy
33
- - **Resilient storage** with automatic failover when mounts are unavailable
34
- - **Atomic bundle creation** with crash-safety and zero orphans
35
- - **Fast background deletion** with 100-300x performance improvement
36
- - **Auto-cleanup** on startup for historical orphan bundles
10
+ **Give your AI assistant deep knowledge of any codebase in seconds.**
11
+
12
+ Preflight-MCP creates searchable, indexed knowledge bundles from GitHub repos, so Claude/GPT/Cursor can understand your project structure, find relevant code, and trace dependencies — without copy-pasting or token limits.
13
+
14
+ ## Why Preflight?
15
+
16
+ | Problem | Preflight Solution |
17
+ |---------|--------------------|
18
+ | 🤯 AI forgets your codebase context | Persistent, searchable bundles |
19
+ | 📋 Copy-pasting code into chat | One command: `"index this repo"` |
20
+ | 🔍 AI can't find related files | Full-text search + dependency graph |
21
+ | 🧩 Lost in large projects | Auto-generated `START_HERE.md` & `OVERVIEW.md` |
22
+ | 🔗 No idea what tests cover what | Trace links: code↔test↔doc |
23
+
24
+ ## Demo
25
+
26
+ ```
27
+ You: "Create a bundle for the repository facebook/react"
28
+
29
+ Preflight: Cloned, indexed 2,847 files, generated overview
30
+
31
+ You: "Search for 'useState' implementation"
32
+
33
+ Preflight: 📍 Found 23 matches:
34
+ packages/react/src/ReactHooks.js:24
35
+ packages/react-reconciler/src/ReactFiberHooks.js:1042
36
+ ...
37
+
38
+ You: "Show me what tests cover useState"
39
+
40
+ Preflight: 🔗 Trace links:
41
+ → ReactHooks.js tested_by ReactHooksTest.js
42
+ ...
43
+ ```
44
+
45
+ ## Core Features
46
+
47
+ - 🚀 **One-command indexing** — `"index owner/repo"` creates a complete knowledge bundle
48
+ - 🔍 **Full-text search** — SQLite FTS5 search across all code and docs
49
+ - 🗺️ **Dependency graph** — Visualize imports and file relationships
50
+ - 🔗 **Trace links** — Track code↔test↔doc relationships
51
+ - 📖 **Auto-generated guides** — `START_HERE.md`, `AGENTS.md`, `OVERVIEW.md`
52
+ - ☁️ **Cloud sync** — Multi-path mirror backup for redundancy
53
+ - ⚡ **15 MCP tools + 5 prompts** — Complete toolkit for code exploration
54
+
55
+ <details>
56
+ <summary><b>All Features (click to expand)</b></summary>
57
+
58
+ - **Progress tracking**: Real-time progress for long-running operations
59
+ - **Bundle integrity check**: Prevents operations on incomplete bundles
60
+ - **De-duplication**: Prevent duplicate bundle creation even during timeouts
61
+ - **Resilient GitHub fetching**: Git clone timeout + archive fallback
62
+ - **Offline repair**: Rebuild derived artifacts without re-fetching
63
+ - **Static facts extraction**: `analysis/FACTS.json` (non-LLM)
64
+ - **Resources**: Read bundle files via `preflight://...` URIs
65
+ - **Atomic operations**: Crash-safety with zero orphans
66
+ - **Fast deletion**: 100-300x performance improvement
67
+ - **Auto-cleanup**: Removes orphan bundles on startup
68
+
69
+ </details>
37
70
 
38
71
  ## Table of Contents
39
72
 
40
- - [Requirements](#requirements)
41
- - [Installation](#installation)
73
+ - [Why Preflight?](#why-preflight)
74
+ - [Demo](#demo)
75
+ - [Core Features](#core-features)
42
76
  - [Quick Start](#quick-start)
43
- - [Tools](#tools-12-total)
77
+ - [Tools](#tools-15-total)
78
+ - [Prompts](#prompts-5-total)
44
79
  - [Environment Variables](#environment-variables)
45
80
  - [Contributing](#contributing)
46
- - [License](#license)
47
81
 
48
82
  ## Requirements
49
83
 
@@ -125,7 +159,7 @@ Run end-to-end smoke test:
125
159
  npm run smoke
126
160
  ```
127
161
 
128
- ## Tools (13 total)
162
+ ## Tools (15 total)
129
163
 
130
164
  ### `preflight_list_bundles`
131
165
  List bundle IDs in storage.
@@ -156,10 +190,15 @@ Input (example):
156
190
  ### `preflight_read_file`
157
191
  Read file(s) from bundle. Two modes:
158
192
  - **Batch mode** (omit `file`): Returns ALL key files (OVERVIEW.md, START_HERE.md, AGENTS.md, manifest.json, deps/dependency-graph.json, repo READMEs) in one call
159
- - **Single file mode** (provide `file`): Returns that specific file (e.g., `deps/dependency-graph.json` for dependency graph)
193
+ - **Single file mode** (provide `file`): Returns that specific file
194
+ - **Evidence citation**: Use `withLineNumbers: true` to get `N|line` format; use `ranges: ["20-80"]` to read specific lines
160
195
  - Triggers: "查看bundle", "bundle概览", "项目信息", "show bundle", "读取依赖图"
161
- - Use `file: "manifest.json"` to get bundle metadata (repos, timestamps, tags, etc.)
162
- - Use `file: "deps/dependency-graph.json"` to read the dependency graph (generated by `preflight_evidence_dependency_graph`)
196
+
197
+ ### `preflight_repo_tree`
198
+ Get repository structure overview without wasting tokens on search.
199
+ - Returns: ASCII directory tree, file count by extension/directory, entry point candidates
200
+ - Use BEFORE deep analysis to understand project layout
201
+ - Triggers: "show project structure", "what files are in this repo", "项目结构", "文件分布"
163
202
 
164
203
  ### `preflight_delete_bundle`
165
204
  Delete/remove a bundle permanently.
@@ -216,9 +255,14 @@ Create or update traceability links (code↔test, code↔doc, file↔requirement
216
255
  ### `preflight_trace_query`
217
256
  Query traceability links (code↔test, code↔doc, commit↔ticket).
218
257
  - **Proactive use**: LLM automatically queries trace links when analyzing specific files
219
- - Helps answer: "Does this code have tests?", "What requirements does this implement?"
258
+ - Returns `reason` and `nextSteps` when no edges found (helps LLM decide next action)
220
259
  - Fast when `bundleId` is provided; can scan across bundles when omitted.
221
260
 
261
+ ### `preflight_trace_export`
262
+ Export trace links to `trace/trace.json` for direct LLM reading.
263
+ - Note: Auto-exported after each `trace_upsert`, so only needed to manually refresh
264
+ - Triggers: "export trace", "refresh trace.json", "导出trace"
265
+
222
266
  ### `preflight_cleanup_orphans`
223
267
  Remove incomplete or corrupted bundles (bundles without valid manifest.json).
224
268
  - Triggers: "clean up broken bundles", "remove orphans", "清理孤儿bundle"
package/README.zh-CN.md CHANGED
@@ -3,41 +3,81 @@
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
5
5
  [![MCP Compatible](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io/)
6
+ [![npm version](https://img.shields.io/npm/v/preflight-mcp)](https://www.npmjs.com/package/preflight-mcp)
6
7
 
7
8
  > [English](./README.md) | **中文**
8
9
 
9
- 一个 MCP (Model Context Protocol) **stdio** 服务器,用于为 GitHub 仓库与库文档生成"基于证据"的 preflight bundles。
10
-
11
- 每个 bundle 包含:
12
- - 仓库文档 + 代码的本地副本(规范化文本)
13
- - 轻量级 **全文搜索索引**(SQLite FTS5)
14
- - 面向 Agent 的入口文件:`START_HERE.md`、`AGENTS.md`、`OVERVIEW.md`(仅事实,带证据指针)
15
-
16
- ## Features
17
-
18
- - **13 MCP 工具**:create/update/repair/search/evidence/trace/read/cleanup(外加 resources)
19
- - **5 MCP prompts**:交互式引导(菜单、分析指南、搜索指南、管理指南、追溯指南)
20
- - **去重**:避免对相同的规范化输入重复索引
21
- - **可靠的 GitHub 获取**:可配置 git clone 超时 + GitHub archive(zipball)兜底
22
- - **离线修复**:无需重新抓取,重建缺失/为空的派生物(index/guides/overview)
23
- - **静态事实提取**:生成 `analysis/FACTS.json`(非 LLM)
24
- - **Resources**:通过 `preflight://...` URI 读取 bundle 文件
25
- - **多路径镜像备份**:云存储冗余
26
- - **弹性存储**:挂载点不可用时自动故障转移
27
- - **原子创建 + 零孤儿**:临时目录 + 原子重命名,崩溃安全
28
- - **后台快速删除**:<100ms 响应,实际删除在后台进行
29
- - **启动自动清理**:历史孤儿目录自动清理(非阻塞)
30
-
31
- ## Table of Contents
32
-
33
- - [Requirements](#requirements)
34
- - [Installation](#installation)
35
- - [Quick Start](#quick-start)
36
- - [Architecture](#architecture)
37
- - [Tools](#tools-12-total)
38
- - [Environment Variables](#environment-variables)
39
- - [Contributing](#contributing)
40
- - [License](#license)
10
+ **让你的 AI 助手秒懂任何代码仓库。**
11
+
12
+ Preflight-MCP GitHub 仓库创建可搜索的知识库,让 Claude/GPT/Cursor 理解你的项目结构、快速定位代码、追踪依赖关系 —— 无需复制粘贴,不受 token 限制。
13
+
14
+ ## 为什么需要 Preflight?
15
+
16
+ | 痛点 | Preflight 解决方案 |
17
+ |------|--------------------|
18
+ | 🤯 AI 记不住你的代码库 | 持久化、可搜索的知识包 |
19
+ | 📋 反复复制粘贴代码 | 一句话:「索引这个仓库」 |
20
+ | 🔍 AI 找不到相关文件 | 全文搜索 + 依赖图 |
21
+ | 🧩 大项目里迷失方向 | 自动生成 `START_HERE.md` 和 `OVERVIEW.md` |
22
+ | 🔗 不知道哪些测试覆盖哪些代码 | 追溯链接:代码↔测试↔文档 |
23
+
24
+ ## 效果演示
25
+
26
+ ```
27
+ 你:「为 facebook/react 创建 bundle」
28
+
29
+ Preflight:✅ 已克隆,索引了 2,847 个文件,生成概览完成
30
+
31
+ 你:「搜索 useState 的实现」
32
+
33
+ Preflight:📍 找到 23 处匹配:
34
+ packages/react/src/ReactHooks.js:24
35
+ → packages/react-reconciler/src/ReactFiberHooks.js:1042
36
+ ...
37
+
38
+ 你:「哪些测试覆盖了 useState」
39
+
40
+ Preflight:🔗 追溯链接:
41
+ ReactHooks.js tested_by ReactHooksTest.js
42
+ ...
43
+ ```
44
+
45
+ ## 核心功能
46
+
47
+ - 🚀 **一句话索引** — 「索引 owner/repo」即可创建完整知识包
48
+ - 🔍 **全文搜索** — SQLite FTS5 搜索全部代码和文档
49
+ - 🗺️ **依赖图** — 可视化 import 关系和文件依赖
50
+ - 🔗 **追溯链接** — 追踪代码↔测试↔文档关系
51
+ - 📖 **自动生成指南** — `START_HERE.md`、`AGENTS.md`、`OVERVIEW.md`
52
+ - ☁️ **云端同步** — 多路径镜像备份
53
+ - ⚡ **15 个 MCP 工具 + 5 个 prompts** — 完整的代码探索工具集
54
+
55
+ <details>
56
+ <summary><b>全部功能(点击展开)</b></summary>
57
+
58
+ - **进度追踪**:长时间操作的实时进度显示
59
+ - **Bundle 完整性检查**:防止对不完整 bundle 进行操作
60
+ - **去重机制**:即使超时也能防止重复创建
61
+ - **可靠的 GitHub 抓取**:git clone 超时 + archive 兜底
62
+ - **离线修复**:无需重新抓取即可重建派生文件
63
+ - **静态事实提取**:`analysis/FACTS.json`(非 LLM)
64
+ - **Resources**:通过 `preflight://...` URI 读取文件
65
+ - **原子操作**:崩溃安全,零孤儿目录
66
+ - **快速删除**:100-300 倍性能提升
67
+ - **自动清理**:启动时自动清理孤儿 bundle
68
+
69
+ </details>
70
+
71
+ ## 目录
72
+
73
+ - [为什么需要 Preflight](#为什么需要-preflight)
74
+ - [效果演示](#效果演示)
75
+ - [核心功能](#核心功能)
76
+ - [快速开始](#quick-start)
77
+ - [工具](#tools-15-total)
78
+ - [Prompts](#prompts-5-total)
79
+ - [环境变量](#environment-variables)
80
+ - [贡献指南](#contributing)
41
81
 
42
82
  ## Requirements
43
83
 
@@ -140,7 +180,7 @@ npm run smoke
140
180
  - 列表与清理逻辑只接受 UUID v4 作为 bundleId
141
181
  - 会自动过滤 `#recycle`、`tmp`、`.deleting` 等非 bundle 目录
142
182
 
143
- ## Tools (12 total)
183
+ ## Tools (15 total)
144
184
 
145
185
  ### `preflight_list_bundles`
146
186
  列出所有 bundle。
@@ -170,11 +210,16 @@ npm run smoke
170
210
 
171
211
  ### `preflight_read_file`
172
212
  从 bundle 读取文件。两种模式:
173
- - **批量模式**(省略 `file`):返回所有关键文件(OVERVIEW.md、START_HERE.md、AGENTS.md、manifest.json、deps/dependency-graph.json、repo READMEs)
174
- - **单文件模式**(提供 `file`):返回指定文件(如 `deps/dependency-graph.json` 获取依赖图)
213
+ - **批量模式**(省略 `file`):返回所有关键文件
214
+ - **单文件模式**(提供 `file`):返回指定文件
215
+ - **证据引用**:使用 `withLineNumbers: true` 获取 `N|行` 格式;使用 `ranges: ["20-80"]` 读取指定行
175
216
  - 触发词:「查看概览」「项目概览」「bundle详情」「读取依赖图」
176
- - 使用 `file: "manifest.json"` 获取 bundle 元数据
177
- - 使用 `file: "deps/dependency-graph.json"` 读取依赖图(由 `preflight_evidence_dependency_graph` 生成)
217
+
218
+ ### `preflight_repo_tree`
219
+ 获取仓库结构概览,避免浪费 token 搜索。
220
+ - 返回:ASCII 目录树、按扩展名/目录统计文件数、入口点候选
221
+ - 在深入分析前使用,了解项目布局
222
+ - 触发词:「项目结构」「文件分布」「show tree」
178
223
 
179
224
  ### `preflight_delete_bundle`
180
225
  永久删除/移除一个 bundle。
@@ -225,7 +270,14 @@ npm run smoke
225
270
  写入/更新 bundle 级 traceability links(commit↔ticket、symbol↔test、code↔doc 等)。
226
271
 
227
272
  ### `preflight_trace_query`
228
- 查询 traceability links(提供 `bundleId` 时更快;省略时可跨 bundle 扫描,带上限)。
273
+ 查询 traceability links
274
+ - 无匹配边时返回 `reason` 和 `nextSteps`(帮助 LLM 决定下一步)
275
+ - 提供 `bundleId` 时更快;省略时可跨 bundle 扫描
276
+
277
+ ### `preflight_trace_export`
278
+ 导出 trace links 到 `trace/trace.json`。
279
+ - 注意:每次 `trace_upsert` 后会自动导出,此工具仅用于手动刷新
280
+ - 触发词:「导出trace」「刷新trace.json」
229
281
 
230
282
  ### `preflight_cleanup_orphans`
231
283
  删除不完整或损坏的 bundle(缺少有效 manifest.json)。
@@ -0,0 +1,224 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const ENTRY_POINT_PATTERNS = [
4
+ { pattern: /^readme\.md$/i, type: 'readme', priority: 100 },
5
+ { pattern: /^readme$/i, type: 'readme', priority: 95 },
6
+ { pattern: /^index\.(ts|js|tsx|jsx|py|go|rs)$/i, type: 'index', priority: 90 },
7
+ { pattern: /^main\.(ts|js|tsx|jsx|py|go|rs)$/i, type: 'main', priority: 85 },
8
+ { pattern: /^app\.(ts|js|tsx|jsx|py)$/i, type: 'app', priority: 80 },
9
+ { pattern: /^server\.(ts|js|tsx|jsx|py|go)$/i, type: 'server', priority: 75 },
10
+ { pattern: /^cli\.(ts|js|py)$/i, type: 'cli', priority: 70 },
11
+ { pattern: /^__init__\.py$/i, type: 'index', priority: 60 },
12
+ { pattern: /^mod\.rs$/i, type: 'index', priority: 60 },
13
+ { pattern: /^lib\.rs$/i, type: 'main', priority: 85 },
14
+ { pattern: /^package\.json$/i, type: 'config', priority: 50 },
15
+ { pattern: /^pyproject\.toml$/i, type: 'config', priority: 50 },
16
+ { pattern: /^cargo\.toml$/i, type: 'config', priority: 50 },
17
+ { pattern: /^go\.mod$/i, type: 'config', priority: 50 },
18
+ { pattern: /\.test\.(ts|js|tsx|jsx)$/i, type: 'test', priority: 30 },
19
+ { pattern: /_test\.(py|go)$/i, type: 'test', priority: 30 },
20
+ { pattern: /^test_.*\.py$/i, type: 'test', priority: 30 },
21
+ ];
22
+ function matchesGlob(filename, patterns) {
23
+ if (patterns.length === 0)
24
+ return true;
25
+ for (const pattern of patterns) {
26
+ // Simple glob matching: * matches any sequence, ** matches any path
27
+ const regexPattern = pattern
28
+ .replace(/\./g, '\\.')
29
+ .replace(/\*\*/g, '.*')
30
+ .replace(/\*/g, '[^/]*');
31
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
32
+ if (regex.test(filename))
33
+ return true;
34
+ }
35
+ return false;
36
+ }
37
+ function shouldExclude(relativePath, excludePatterns) {
38
+ if (excludePatterns.length === 0)
39
+ return false;
40
+ for (const pattern of excludePatterns) {
41
+ // Simple exclusion matching
42
+ if (relativePath.includes(pattern))
43
+ return true;
44
+ if (pattern.startsWith('*') && relativePath.endsWith(pattern.slice(1)))
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+ export async function generateRepoTree(bundleRootDir, bundleId, options = {}) {
50
+ const depth = options.depth ?? 4;
51
+ const includePatterns = options.include ?? [];
52
+ const excludePatterns = options.exclude ?? ['node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build', '*.pyc'];
53
+ const reposDir = path.join(bundleRootDir, 'repos');
54
+ const stats = {
55
+ totalFiles: 0,
56
+ totalDirs: 0,
57
+ byExtension: {},
58
+ byTopDir: {},
59
+ };
60
+ const entryPointCandidates = [];
61
+ // Build tree recursively
62
+ async function buildTree(dir, currentDepth, relativePath) {
63
+ if (currentDepth > depth)
64
+ return null;
65
+ try {
66
+ const stat = await fs.stat(dir);
67
+ const name = path.basename(dir);
68
+ if (stat.isFile()) {
69
+ // Check include/exclude patterns
70
+ if (includePatterns.length > 0 && !matchesGlob(name, includePatterns)) {
71
+ return null;
72
+ }
73
+ if (shouldExclude(relativePath, excludePatterns)) {
74
+ return null;
75
+ }
76
+ stats.totalFiles++;
77
+ // Track extension stats
78
+ const ext = path.extname(name).toLowerCase() || '(no ext)';
79
+ stats.byExtension[ext] = (stats.byExtension[ext] ?? 0) + 1;
80
+ // Track top directory stats
81
+ const topDir = relativePath.split('/')[0] ?? '(root)';
82
+ stats.byTopDir[topDir] = (stats.byTopDir[topDir] ?? 0) + 1;
83
+ // Check for entry point candidates
84
+ for (const ep of ENTRY_POINT_PATTERNS) {
85
+ if (ep.pattern.test(name)) {
86
+ entryPointCandidates.push({
87
+ path: relativePath,
88
+ type: ep.type,
89
+ priority: ep.priority,
90
+ });
91
+ break;
92
+ }
93
+ }
94
+ return { name, type: 'file', size: stat.size };
95
+ }
96
+ if (stat.isDirectory()) {
97
+ // Check exclude patterns for directories
98
+ if (shouldExclude(name, excludePatterns) || shouldExclude(relativePath, excludePatterns)) {
99
+ return null;
100
+ }
101
+ stats.totalDirs++;
102
+ const entries = await fs.readdir(dir, { withFileTypes: true });
103
+ const children = [];
104
+ // Sort: directories first, then files, alphabetically
105
+ const sortedEntries = entries.sort((a, b) => {
106
+ if (a.isDirectory() && !b.isDirectory())
107
+ return -1;
108
+ if (!a.isDirectory() && b.isDirectory())
109
+ return 1;
110
+ return a.name.localeCompare(b.name);
111
+ });
112
+ for (const entry of sortedEntries) {
113
+ const childPath = path.join(dir, entry.name);
114
+ const childRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
115
+ const childNode = await buildTree(childPath, currentDepth + 1, childRelPath);
116
+ if (childNode) {
117
+ children.push(childNode);
118
+ }
119
+ }
120
+ return { name, type: 'dir', children: children.length > 0 ? children : undefined };
121
+ }
122
+ return null;
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ // Build tree starting from repos directory
129
+ let rootNode = null;
130
+ try {
131
+ await fs.access(reposDir);
132
+ rootNode = await buildTree(reposDir, 0, '');
133
+ }
134
+ catch {
135
+ // repos directory doesn't exist, try bundle root
136
+ rootNode = await buildTree(bundleRootDir, 0, '');
137
+ }
138
+ // Generate ASCII tree
139
+ function renderTree(node, prefix = '', isLast = true) {
140
+ const lines = [];
141
+ const connector = isLast ? '└── ' : '├── ';
142
+ const extension = isLast ? ' ' : '│ ';
143
+ lines.push(`${prefix}${connector}${node.name}${node.type === 'dir' ? '/' : ''}`);
144
+ if (node.children) {
145
+ const childCount = node.children.length;
146
+ node.children.forEach((child, index) => {
147
+ const childLines = renderTree(child, prefix + extension, index === childCount - 1);
148
+ lines.push(...childLines);
149
+ });
150
+ }
151
+ return lines;
152
+ }
153
+ let treeText = '';
154
+ if (rootNode) {
155
+ if (rootNode.children && rootNode.children.length > 0) {
156
+ treeText = `${rootNode.name}/\n`;
157
+ rootNode.children.forEach((child, index) => {
158
+ const childLines = renderTree(child, '', index === rootNode.children.length - 1);
159
+ treeText += childLines.join('\n') + '\n';
160
+ });
161
+ }
162
+ else {
163
+ treeText = `${rootNode.name}/ (empty or filtered out)`;
164
+ }
165
+ }
166
+ else {
167
+ treeText = '(no files found)';
168
+ }
169
+ // Sort entry point candidates by priority
170
+ entryPointCandidates.sort((a, b) => b.priority - a.priority);
171
+ return {
172
+ bundleId,
173
+ tree: treeText.trim(),
174
+ stats,
175
+ entryPointCandidates: entryPointCandidates.slice(0, 20), // Limit to top 20
176
+ };
177
+ }
178
+ /**
179
+ * Format tree result as human-readable text
180
+ */
181
+ export function formatTreeResult(result) {
182
+ const lines = [];
183
+ lines.push(`📂 Repository Structure for bundle: ${result.bundleId}`);
184
+ lines.push('');
185
+ lines.push('## Directory Tree');
186
+ lines.push('```');
187
+ lines.push(result.tree);
188
+ lines.push('```');
189
+ lines.push('');
190
+ lines.push('## Statistics');
191
+ lines.push(`- Total files: ${result.stats.totalFiles}`);
192
+ lines.push(`- Total directories: ${result.stats.totalDirs}`);
193
+ lines.push('');
194
+ // By extension (top 10)
195
+ const extEntries = Object.entries(result.stats.byExtension)
196
+ .sort((a, b) => b[1] - a[1])
197
+ .slice(0, 10);
198
+ if (extEntries.length > 0) {
199
+ lines.push('### Files by Extension');
200
+ for (const [ext, count] of extEntries) {
201
+ lines.push(`- ${ext}: ${count}`);
202
+ }
203
+ lines.push('');
204
+ }
205
+ // By top directory (top 10)
206
+ const dirEntries = Object.entries(result.stats.byTopDir)
207
+ .sort((a, b) => b[1] - a[1])
208
+ .slice(0, 10);
209
+ if (dirEntries.length > 0) {
210
+ lines.push('### Files by Top Directory');
211
+ for (const [dir, count] of dirEntries) {
212
+ lines.push(`- ${dir}: ${count}`);
213
+ }
214
+ lines.push('');
215
+ }
216
+ // Entry point candidates
217
+ if (result.entryPointCandidates.length > 0) {
218
+ lines.push('## Entry Point Candidates');
219
+ for (const ep of result.entryPointCandidates.slice(0, 10)) {
220
+ lines.push(`- \`${ep.path}\` (${ep.type})`);
221
+ }
222
+ }
223
+ return lines.join('\n');
224
+ }