dsh-code-index 0.1.0
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/LICENSE +21 -0
- package/README.md +125 -0
- package/README.zh.md +125 -0
- package/cordis.patch.yml +5 -0
- package/dist/index.js +792 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-code-index contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# dsh-code-index
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Semantic repo index — a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) plugin that gives the agent a **codebase map**: a tree-sitter symbol index, ranked symbol search, and a bounded auto-updating repo map in the system prompt.
|
|
6
|
+
|
|
7
|
+
Fills a real ecosystem gap: search of the `dsh-plugin` topic (2026-08) shows git/voice/browser/memory plugins galore, but **no native, model-usable code index / repo-map capability** — the kind of thing aider's repo-map and Cursor's `@Codebase` do for their agents.
|
|
8
|
+
|
|
9
|
+
## What the model gets
|
|
10
|
+
|
|
11
|
+
| Tool | Purpose |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `code_index` | Status / (re)build the index for the current workspace |
|
|
14
|
+
| `code_symbols` | List symbols (functions, classes, interfaces, types, methods…) with file:line — filtered by name, path, kind, exported |
|
|
15
|
+
| `code_search` | Ranked lookup: exact > prefix > substring, exports first, relevance score + file:line |
|
|
16
|
+
| `code_map` | Bounded ranked repo map (top files by symbol density, key symbols + lines) |
|
|
17
|
+
|
|
18
|
+
Plus an optional **auto-injected system prompt section** (`code-index:repo-map`, order 60): a compact ranked map of the default workspace, refreshed with a 60s TTL. Set `autoInject: false` to disable and rely on the `code_map` tool only.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Requires `dsh` (any install path — npx, npm, or source) and Node ≥ 22.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
# from npm (prebuilt)
|
|
26
|
+
npx @deepseek-ai/dsh plugin --profile web add dsh-code-index
|
|
27
|
+
|
|
28
|
+
# or from a directory containing this checkout
|
|
29
|
+
npx @deepseek-ai/dsh plugin --profile web add ./dsh-code-index
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Restart the Web UI (`npx @deepseek-ai/dsh web`) — startup logs confirm each tool:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
[dsh-code-index] plugin loaded
|
|
36
|
+
[dsh-code-index] registered tool: code_index
|
|
37
|
+
...
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Verify the composed config without booting: `dsh --profile web --dump-config`.
|
|
41
|
+
|
|
42
|
+
## Using it
|
|
43
|
+
|
|
44
|
+
In a workspace session, ask the agent:
|
|
45
|
+
|
|
46
|
+
- "Which repo are we in — run code_map first."
|
|
47
|
+
- "Find every function whose name contains `parse` and where it lives."
|
|
48
|
+
- "List the exported symbols in src/core."
|
|
49
|
+
- "Rebuild the code index."
|
|
50
|
+
|
|
51
|
+
No API key is needed to *index*; the model must of course be configured to call the tools.
|
|
52
|
+
|
|
53
|
+
## Example (input → output)
|
|
54
|
+
|
|
55
|
+
User prompt:
|
|
56
|
+
|
|
57
|
+
> Which repo are we in? Run `code_map` first, then find where `extractSymbols` is defined.
|
|
58
|
+
|
|
59
|
+
The agent calls the tools in turn:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
code_map
|
|
63
|
+
# repo map
|
|
64
|
+
## src/extract.ts (14)
|
|
65
|
+
function extractSymbols(code, id) :121
|
|
66
|
+
function languageForFile(filePath) :37
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
code_search { query: "extractSymbols" }
|
|
70
|
+
export function extractSymbols(code, id) — src/extract.ts:121
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The index builds lazily on first use; later calls are served from the on-disk cache with mtime-incremental refresh.
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
Options are passed as the plugin row's `config` in the profile patch (or defaults are used if absent):
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# $DSH_HOME/profiles/<name>/cordis.patch.yml — a bare row overrides by id.
|
|
81
|
+
- id: code-index
|
|
82
|
+
config:
|
|
83
|
+
excludeDirs: [generated, playground]
|
|
84
|
+
mapTopFiles: 30
|
|
85
|
+
mapMaxChars: 4000
|
|
86
|
+
autoInject: true
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Key | Default | Meaning |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `excludeDirs` | `[]` | Extra dirs appended to the built-in excludes (`node_modules`, `.git`, `dist`, `build`, `out`, `coverage`, `.next`, `.nuxt`, `.cache`, `target`, `vendor`, …) |
|
|
92
|
+
| `mapTopFiles` | `24` | Max files in a ranked map |
|
|
93
|
+
| `mapMaxChars` | `3200` | Hard cap on rendered map characters |
|
|
94
|
+
| `autoInject` | `true` | Register the system prompt section |
|
|
95
|
+
|
|
96
|
+
## Supported languages
|
|
97
|
+
|
|
98
|
+
TypeScript + JavaScript (`.ts .tsx .mts .cts .js .jsx .mjs .cjs`) via tree-sitter WASM — pure parsing, no native build. The symbol provider seam (`src/extract.ts` + grammars) is where other languages/embeddings plug in later.
|
|
99
|
+
|
|
100
|
+
## How it works
|
|
101
|
+
|
|
102
|
+
- **Index build** (`src/buildIndex.ts`): recursive scan (excludes applied), per-file tree-sitter extraction (`src/extract.ts`), JSON cache under `<repo>/.dsh-code-index/`, incremental refresh by mtime (only touched files re-parse).
|
|
103
|
+
- **Search** (`src/search.ts`): pure scoring — exact `1` / prefix `0.8` / substring `0.5`, export boost, name order tiebreak.
|
|
104
|
+
- **Repo map** (`src/repomap.ts`): density-aware file score (class/interface/function weighted, mild anti-bloat), top-N files, per-file symbol cap, hard char truncation.
|
|
105
|
+
- **Workspace resolution**: each tool resolves the session cwd (`agent.session.header.cwd`) and walks up to the nearest `.git` (bounded — a directory without a repo marker is never indexed).
|
|
106
|
+
|
|
107
|
+
## Known limitations
|
|
108
|
+
|
|
109
|
+
- **web-tree-sitter pinned to `^0.20.8`** — newer releases expect dylinked grammar wasm while `tree-sitter-wasms` ships static builds; this pair is verified working under Node ≥ 22/24.
|
|
110
|
+
- Auto-injected section targets the **default workspace** (launch directory, matching headless/CLI mode). Multi-workspace Web UI sessions should use `code_map`/`code_symbols` (they resolve per-session cwd).
|
|
111
|
+
- Local variables are indexed too — recall over precision; `code_search` ranking keeps them low.
|
|
112
|
+
- Developer-preview harness: expect breaking harness/plugin API changes upstream.
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
pnpm install
|
|
118
|
+
pnpm test # vitest — extractor, scan, cache, search, repo map (35 tests)
|
|
119
|
+
pnpm typecheck
|
|
120
|
+
pnpm build # tsup → dist/index.js (ESM, external deps)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT. Not affiliated with DeepSeek; built on the public `dsh` plugin surface.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# dsh-code-index
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
语义仓库索引 —— 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)插件,为 agent 提供**代码库地图**:基于 tree-sitter 的符号索引、带排名的符号搜索,以及注入到系统提示词里的限量自动更新仓库地图。
|
|
6
|
+
|
|
7
|
+
填补了一个真实的生态缺口:检索 `dsh-plugin` 话题(2026-08)会发现 git/语音/浏览器/记忆类插件琳琅满目,但**缺少原生、模型可用的代码索引 / 仓库地图能力**——也就是 aider 的 repo-map 和 Cursor 的 `@Codebase` 为各自 agent 提供的同类能力。
|
|
8
|
+
|
|
9
|
+
## 模型能得到什么
|
|
10
|
+
|
|
11
|
+
| 工具 | 用途 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `code_index` | 查看 / (重)建当前工作区的索引 |
|
|
14
|
+
| `code_symbols` | 列出符号(函数、类、接口、类型、方法……),带 file:line——支持按名称、路径、类型、是否导出过滤 |
|
|
15
|
+
| `code_search` | 排名检索:精确 > 前缀 > 子串,导出优先,带相关度分数与 file:line |
|
|
16
|
+
| `code_map` | 限量排名仓库地图(按符号密度取核心文件 + 关键符号与行号) |
|
|
17
|
+
|
|
18
|
+
外加一个可选的**自动注入系统提示词段**(`code-index:repo-map`,序 60):默认工作区的精简排名地图,60 秒 TTL 自动刷新。将 `autoInject: false` 可关闭,只依赖 `code_map` 工具。
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
需要 `dsh`(任意安装方式——npx、npm 或源码)与 Node ≥ 22。
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
# 从 npm(预编译)
|
|
26
|
+
npx @deepseek-ai/dsh plugin --profile web add dsh-code-index
|
|
27
|
+
|
|
28
|
+
# 或从包含本仓库检查副本的目录
|
|
29
|
+
npx @deepseek-ai/dsh plugin --profile web add ./dsh-code-index
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
重启 Web UI(`npx @deepseek-ai/dsh web`)——启动日志会确认每个工具:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
[dsh-code-index] plugin loaded
|
|
36
|
+
[dsh-code-index] registered tool: code_index
|
|
37
|
+
...
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
不启动即可核对组合配置:`dsh --profile web --dump-config`。
|
|
41
|
+
|
|
42
|
+
## 使用
|
|
43
|
+
|
|
44
|
+
在工作区会话中,向 agent 提这类请求:
|
|
45
|
+
|
|
46
|
+
- "我们现在在哪个仓库?先跑 code_map。"
|
|
47
|
+
- "找出所有名字含 `parse` 的函数及其位置。"
|
|
48
|
+
- "列出 src/core 里的导出符号。"
|
|
49
|
+
- "重建代码索引。"
|
|
50
|
+
|
|
51
|
+
*索引*本身不需要 API key;模型当然要配置好才能调用这些工具。
|
|
52
|
+
|
|
53
|
+
## 示例(输入 → 输出)
|
|
54
|
+
|
|
55
|
+
用户提示:
|
|
56
|
+
|
|
57
|
+
> 我们现在在哪个仓库?先跑 `code_map`,然后找出 `extractSymbols` 定义在哪。
|
|
58
|
+
|
|
59
|
+
agent 依次调用工具:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
code_map
|
|
63
|
+
# repo map
|
|
64
|
+
## src/extract.ts (14)
|
|
65
|
+
function extractSymbols(code, id) :121
|
|
66
|
+
function languageForFile(filePath) :37
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
code_search { query: "extractSymbols" }
|
|
70
|
+
export function extractSymbols(code, id) — src/extract.ts:121
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
索引在首次使用时惰性构建;后续调用由磁盘缓存提供,并按 mtime 增量刷新。
|
|
74
|
+
|
|
75
|
+
## 配置
|
|
76
|
+
|
|
77
|
+
选项通过插件行的 `config` 在 profile 补丁中传入(缺省时使用默认值):
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# $DSH_HOME/profiles/<name>/cordis.patch.yml —— 裸行按 id 覆盖。
|
|
81
|
+
- id: code-index
|
|
82
|
+
config:
|
|
83
|
+
excludeDirs: [generated, playground]
|
|
84
|
+
mapTopFiles: 30
|
|
85
|
+
mapMaxChars: 4000
|
|
86
|
+
autoInject: true
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| 键 | 默认 | 含义 |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `excludeDirs` | `[]` | 追加到内置排除列表(`node_modules`、`.git`、`dist`、`build`、`out`、`coverage`、`.next`、`.nuxt`、`.cache`、`target`、`vendor`……)之外的额外目录 |
|
|
92
|
+
| `mapTopFiles` | `24` | 排名地图中的最大文件数 |
|
|
93
|
+
| `mapMaxChars` | `3200` | 渲染地图的硬性字符上限 |
|
|
94
|
+
| `autoInject` | `true` | 是否注册系统提示词段 |
|
|
95
|
+
|
|
96
|
+
## 支持的语言
|
|
97
|
+
|
|
98
|
+
TypeScript + JavaScript(`.ts .tsx .mts .cts .js .jsx .mjs .cjs`),通过 tree-sitter WASM 解析——纯解析,无需原生编译。符号提供方的接缝(`src/extract.ts` + 语法文件)预留了后续接入其他语言/嵌入检索的位置。
|
|
99
|
+
|
|
100
|
+
## 工作原理
|
|
101
|
+
|
|
102
|
+
- **索引构建**(`src/buildIndex.ts`):递归扫描(应用排除规则),逐文件 tree-sitter 提取(`src/extract.ts`),JSON 缓存置于 `<repo>/.dsh-code-index/`,按 mtime 增量刷新(只有被改动的文件才重新解析)。
|
|
103
|
+
- **搜索**(`src/search.ts`):纯打分——精确 `1` / 前缀 `0.8` / 子串 `0.5`,导出加权,名称序平局裁决。
|
|
104
|
+
- **仓库地图**(`src/repomap.ts`):密度感知的文件打分(class/interface/function 加权,轻微反膨胀),取 Top-N 文件,每文件符号上限,硬截断。
|
|
105
|
+
- **工作区解析**:每个工具解析会话 cwd(`agent.session.header.cwd`)并向上查找最近的 `.git`(有界——没有仓库标记的目录绝不会被索引)。
|
|
106
|
+
|
|
107
|
+
## 已知限制
|
|
108
|
+
|
|
109
|
+
- **web-tree-sitter 固定为 `^0.20.8`** —— 新版本期望 dylink 语法的 wasm,而 `tree-sitter-wasms` 提供静态构建;此组合在 Node ≥ 22/24 下验证可用。
|
|
110
|
+
- 自动注入段针对**默认工作区**(启动目录,与 headless/CLI 模式一致)。多工作区 Web UI 会话应使用 `code_map`/`code_symbols`(它们按会话 cwd 解析)。
|
|
111
|
+
- 局部变量也会被索引——召回优先于精确;`code_search` 的排名会压低它们。
|
|
112
|
+
- 开发者预览版 harness:上游 harness/插件 API 大概率有破坏性变更。
|
|
113
|
+
|
|
114
|
+
## 开发
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
pnpm install
|
|
118
|
+
pnpm test # vitest —— 提取器、扫描、缓存、搜索、仓库地图(35 项测试)
|
|
119
|
+
pnpm typecheck
|
|
120
|
+
pnpm build # tsup → dist/index.js(ESM,外部依赖)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 许可证
|
|
124
|
+
|
|
125
|
+
MIT。与 DeepSeek 无关;基于公开的 `dsh` 插件接口构建。
|
package/cordis.patch.yml
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
// src/buildIndex.ts
|
|
2
|
+
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
3
|
+
import path4 from "path";
|
|
4
|
+
|
|
5
|
+
// src/extract.ts
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import Parser from "web-tree-sitter";
|
|
10
|
+
var require2 = createRequire(import.meta.url);
|
|
11
|
+
var WASM_DIR = path.dirname(require2.resolve("tree-sitter-wasms/out/tree-sitter-typescript.wasm"));
|
|
12
|
+
var EXT_TO_LANG = {
|
|
13
|
+
".ts": "typescript",
|
|
14
|
+
".tsx": "typescript",
|
|
15
|
+
".mts": "typescript",
|
|
16
|
+
".cts": "typescript",
|
|
17
|
+
".js": "javascript",
|
|
18
|
+
".jsx": "javascript",
|
|
19
|
+
".mjs": "javascript",
|
|
20
|
+
".cjs": "javascript"
|
|
21
|
+
};
|
|
22
|
+
function languageForFile(filePath) {
|
|
23
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
24
|
+
return EXT_TO_LANG[ext] ?? null;
|
|
25
|
+
}
|
|
26
|
+
var QUERIES = {
|
|
27
|
+
typescript: `
|
|
28
|
+
(function_declaration) @function
|
|
29
|
+
(generator_function_declaration) @function
|
|
30
|
+
(method_definition) @method
|
|
31
|
+
(class_declaration) @class
|
|
32
|
+
(interface_declaration) @interface
|
|
33
|
+
(type_alias_declaration) @type
|
|
34
|
+
(enum_declaration) @enum
|
|
35
|
+
(variable_declarator) @variable
|
|
36
|
+
(public_field_definition) @field
|
|
37
|
+
(abstract_class_declaration) @class
|
|
38
|
+
`,
|
|
39
|
+
// JS shares the same query surface; type-related patterns simply never match.
|
|
40
|
+
javascript: `
|
|
41
|
+
(function_declaration) @function
|
|
42
|
+
(generator_function_declaration) @function
|
|
43
|
+
(method_definition) @method
|
|
44
|
+
(class_declaration) @class
|
|
45
|
+
(variable_declarator) @variable
|
|
46
|
+
`
|
|
47
|
+
};
|
|
48
|
+
var CAPTURE_KINDS = {
|
|
49
|
+
function: { kind: "function" },
|
|
50
|
+
method: { kind: "method" },
|
|
51
|
+
class: { kind: "class" },
|
|
52
|
+
interface: { kind: "interface" },
|
|
53
|
+
type: { kind: "type" },
|
|
54
|
+
enum: { kind: "enum" },
|
|
55
|
+
variable: { kind: "variable" },
|
|
56
|
+
field: { kind: "field" }
|
|
57
|
+
};
|
|
58
|
+
var parserPromise = null;
|
|
59
|
+
async function getParser() {
|
|
60
|
+
if (!parserPromise) {
|
|
61
|
+
parserPromise = (async () => {
|
|
62
|
+
const wasm = path.join(path.dirname(require2.resolve("web-tree-sitter")), "tree-sitter.wasm");
|
|
63
|
+
await Parser.init({ locateFile: () => wasm });
|
|
64
|
+
const p = new Parser();
|
|
65
|
+
return p;
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
return parserPromise;
|
|
69
|
+
}
|
|
70
|
+
var languageCache = /* @__PURE__ */ new Map();
|
|
71
|
+
function getLanguage(id) {
|
|
72
|
+
let entry = languageCache.get(id);
|
|
73
|
+
if (!entry) {
|
|
74
|
+
entry = getParser().then(async (parser) => {
|
|
75
|
+
const grammarName = id === "typescript" ? "tree-sitter-typescript" : "tree-sitter-javascript";
|
|
76
|
+
const grammarPath = path.join(WASM_DIR, `${grammarName}.wasm`);
|
|
77
|
+
const bytes = await readFile(grammarPath);
|
|
78
|
+
const lang = await Parser.Language.load(bytes);
|
|
79
|
+
const q = lang.query(QUERIES[id]);
|
|
80
|
+
q.delete();
|
|
81
|
+
return lang;
|
|
82
|
+
});
|
|
83
|
+
languageCache.set(id, entry);
|
|
84
|
+
}
|
|
85
|
+
return entry;
|
|
86
|
+
}
|
|
87
|
+
async function extractSymbols(code, id) {
|
|
88
|
+
const lang = await getLanguage(id);
|
|
89
|
+
const parser = await getParser();
|
|
90
|
+
parser.setLanguage(lang);
|
|
91
|
+
const tree = parser.parse(code);
|
|
92
|
+
try {
|
|
93
|
+
const query = lang.query(QUERIES[id]);
|
|
94
|
+
const rows = [];
|
|
95
|
+
try {
|
|
96
|
+
const captures = query.captures(tree.rootNode);
|
|
97
|
+
for (const cap of captures) {
|
|
98
|
+
const def = CAPTURE_KINDS[cap.name];
|
|
99
|
+
if (!def) continue;
|
|
100
|
+
const node = cap.node;
|
|
101
|
+
const name2 = nameOf(node);
|
|
102
|
+
if (!name2) continue;
|
|
103
|
+
rows.push({
|
|
104
|
+
name: name2,
|
|
105
|
+
kind: def.kind,
|
|
106
|
+
file: "",
|
|
107
|
+
// set by the caller (extractor is file-agnostic)
|
|
108
|
+
line: node.startPosition.row + 1,
|
|
109
|
+
endLine: node.endPosition.row + 1,
|
|
110
|
+
exported: isExported(node),
|
|
111
|
+
signature: signatureFor(node)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
query.delete();
|
|
116
|
+
}
|
|
117
|
+
rows.sort((a, b) => a.line - b.line);
|
|
118
|
+
return rows;
|
|
119
|
+
} finally {
|
|
120
|
+
tree.delete();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function nameOf(node) {
|
|
124
|
+
const field = node.childForFieldName?.("name");
|
|
125
|
+
if (field) return field.text.trim();
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
function signatureFor(node) {
|
|
129
|
+
const name2 = nameOf(node);
|
|
130
|
+
const params = node.namedChildren.find(
|
|
131
|
+
(c) => c.type === "formal_parameters" || c.type === "method_parameters"
|
|
132
|
+
);
|
|
133
|
+
if (params) {
|
|
134
|
+
return `${name2}${params.text}`;
|
|
135
|
+
}
|
|
136
|
+
const first = node.namedChildren[0];
|
|
137
|
+
return first ? first.text.trim() : name2;
|
|
138
|
+
}
|
|
139
|
+
function isExported(node) {
|
|
140
|
+
let parent = node.parent;
|
|
141
|
+
let depth = 0;
|
|
142
|
+
while (parent && depth < 3) {
|
|
143
|
+
if (parent.type === "export_statement") return true;
|
|
144
|
+
if (parent.type === "statement_block") return false;
|
|
145
|
+
parent = parent.parent;
|
|
146
|
+
depth++;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
async function parseFileToSymbols(filePath, repoRoot, code) {
|
|
151
|
+
const lang = languageForFile(filePath);
|
|
152
|
+
if (!lang) return [];
|
|
153
|
+
const text = code ?? await readFile(filePath, "utf8");
|
|
154
|
+
const rows = await extractSymbols(text, lang);
|
|
155
|
+
const rel = path.relative(repoRoot, filePath).split(path.sep).join("/");
|
|
156
|
+
return rows.map((r) => ({ ...r, file: rel }));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/scan.ts
|
|
160
|
+
import { readdir, stat } from "fs/promises";
|
|
161
|
+
import path2 from "path";
|
|
162
|
+
var DEFAULT_EXCLUDED_DIRS = [
|
|
163
|
+
"node_modules",
|
|
164
|
+
".git",
|
|
165
|
+
".idea",
|
|
166
|
+
".vscode",
|
|
167
|
+
"dist",
|
|
168
|
+
"build",
|
|
169
|
+
"out",
|
|
170
|
+
"coverage",
|
|
171
|
+
".next",
|
|
172
|
+
".nuxt",
|
|
173
|
+
".cache",
|
|
174
|
+
"target",
|
|
175
|
+
"vendor",
|
|
176
|
+
".dsh-code-index"
|
|
177
|
+
];
|
|
178
|
+
var SUPPORTED_EXTS = /* @__PURE__ */ new Set([
|
|
179
|
+
".ts",
|
|
180
|
+
".tsx",
|
|
181
|
+
".mts",
|
|
182
|
+
".cts",
|
|
183
|
+
".js",
|
|
184
|
+
".jsx",
|
|
185
|
+
".mjs",
|
|
186
|
+
".cjs"
|
|
187
|
+
]);
|
|
188
|
+
async function scanRepo(root, options = {}) {
|
|
189
|
+
const excluded = /* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]);
|
|
190
|
+
const results = [];
|
|
191
|
+
const queue = [[root, ""]];
|
|
192
|
+
while (queue.length) {
|
|
193
|
+
const [absDir, relDir] = queue.pop();
|
|
194
|
+
let entries;
|
|
195
|
+
try {
|
|
196
|
+
entries = await readdir(absDir, { withFileTypes: true });
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
const abs = path2.join(absDir, entry.name);
|
|
202
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
if (excluded.has(entry.name)) continue;
|
|
205
|
+
queue.push([abs, rel]);
|
|
206
|
+
} else if (entry.isFile() && SUPPORTED_EXTS.has(path2.extname(entry.name).toLowerCase())) {
|
|
207
|
+
try {
|
|
208
|
+
const st = await stat(abs);
|
|
209
|
+
results.push({ abs, rel, mtimeMs: st.mtimeMs });
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
results.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/store.ts
|
|
220
|
+
import { mkdir, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
221
|
+
import path3 from "path";
|
|
222
|
+
import { createHash, randomUUID } from "crypto";
|
|
223
|
+
var CACHE_DIR_NAME = ".dsh-code-index";
|
|
224
|
+
function cacheKeyForRoot(root) {
|
|
225
|
+
const resolved = path3.resolve(root);
|
|
226
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
227
|
+
}
|
|
228
|
+
function defaultCachePath(root, cacheDir) {
|
|
229
|
+
const dir = cacheDir ?? path3.join(root, CACHE_DIR_NAME);
|
|
230
|
+
const hash = createHash("sha1").update(cacheKeyForRoot(root)).digest("hex").slice(0, 12);
|
|
231
|
+
return path3.join(dir, `${hash}.json`);
|
|
232
|
+
}
|
|
233
|
+
function legacyCachePath(root, cacheDir) {
|
|
234
|
+
const dir = cacheDir ?? path3.join(root, CACHE_DIR_NAME);
|
|
235
|
+
const hash = createHash("sha1").update(root).digest("hex").slice(0, 12);
|
|
236
|
+
return path3.join(dir, `${hash}.json`);
|
|
237
|
+
}
|
|
238
|
+
async function loadIndex(cachePath) {
|
|
239
|
+
try {
|
|
240
|
+
const raw = await readFile2(cachePath, "utf8");
|
|
241
|
+
const parsed = JSON.parse(raw);
|
|
242
|
+
if (!parsed || typeof parsed.root !== "string" || !Array.isArray(parsed.files)) return null;
|
|
243
|
+
return parsed;
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function saveIndex(cachePath, index) {
|
|
249
|
+
await mkdir(path3.dirname(cachePath), { recursive: true });
|
|
250
|
+
const temporaryPath = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
251
|
+
try {
|
|
252
|
+
await writeFile(temporaryPath, JSON.stringify(index, null, 2), { encoding: "utf8", flag: "wx" });
|
|
253
|
+
await rename(temporaryPath, cachePath);
|
|
254
|
+
} finally {
|
|
255
|
+
await rm(temporaryPath, { force: true });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/buildIndex.ts
|
|
260
|
+
async function buildIndex(root, options = {}, previous = null) {
|
|
261
|
+
const scanned = await scanRepo(root, options);
|
|
262
|
+
const prevByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
|
|
263
|
+
const prevMtime = new Map((previous?.files ?? []).map((f) => [f.path, f.mtimeMs]));
|
|
264
|
+
const files = [];
|
|
265
|
+
const BATCH = 8;
|
|
266
|
+
for (let i = 0; i < scanned.length; i += BATCH) {
|
|
267
|
+
const batch = scanned.slice(i, i + BATCH);
|
|
268
|
+
const rows = await Promise.all(
|
|
269
|
+
batch.map(async (f) => {
|
|
270
|
+
const lang = languageForFile(f.abs);
|
|
271
|
+
if (!lang) return null;
|
|
272
|
+
if (prevMtime.get(f.rel) === f.mtimeMs) {
|
|
273
|
+
const cached = prevByPath.get(f.rel);
|
|
274
|
+
return {
|
|
275
|
+
...cached,
|
|
276
|
+
mtimeMs: f.mtimeMs,
|
|
277
|
+
symbols: cached.symbols.map((symbol) => ({ ...symbol, file: f.rel }))
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
let code;
|
|
281
|
+
try {
|
|
282
|
+
code = await readFile3(f.abs, "utf8");
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const symbols = await extractSymbols(code, lang);
|
|
287
|
+
return {
|
|
288
|
+
path: f.rel,
|
|
289
|
+
lang,
|
|
290
|
+
mtimeMs: f.mtimeMs,
|
|
291
|
+
symbols: symbols.map((s) => ({ ...s, file: f.rel }))
|
|
292
|
+
};
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
for (const f of rows) {
|
|
296
|
+
if (f) files.push(f);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
root,
|
|
301
|
+
generatedAt: Date.now(),
|
|
302
|
+
files,
|
|
303
|
+
excludedDirs: [...DEFAULT_EXCLUDED_DIRS, ...options.excludeDirs ?? []]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function buildIndexWithCache(root, options = {}, cacheDir) {
|
|
307
|
+
const cachePath = defaultCachePath(root, cacheDir);
|
|
308
|
+
let prev = await loadIndex(cachePath);
|
|
309
|
+
let loadedLegacy = false;
|
|
310
|
+
if (!prev) {
|
|
311
|
+
const oldPath = legacyCachePath(root, cacheDir);
|
|
312
|
+
if (oldPath !== cachePath) {
|
|
313
|
+
prev = await loadIndex(oldPath);
|
|
314
|
+
loadedLegacy = prev !== null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const reusable = prev && cacheKeyForRoot(prev.root) === cacheKeyForRoot(root) ? prev : null;
|
|
318
|
+
const fresh = await buildIndex(root, options, reusable);
|
|
319
|
+
if (prev && indexesEqual(prev, fresh)) {
|
|
320
|
+
if (loadedLegacy) await saveIndex(cachePath, fresh);
|
|
321
|
+
return loadedLegacy ? fresh : prev;
|
|
322
|
+
}
|
|
323
|
+
await saveIndex(cachePath, fresh);
|
|
324
|
+
return fresh;
|
|
325
|
+
}
|
|
326
|
+
function indexesEqual(left, right) {
|
|
327
|
+
if (cacheKeyForRoot(left.root) !== cacheKeyForRoot(right.root)) return false;
|
|
328
|
+
if (left.files.length !== right.files.length) return false;
|
|
329
|
+
if (left.excludedDirs.length !== right.excludedDirs.length) return false;
|
|
330
|
+
if (left.excludedDirs.some((value, index) => value !== right.excludedDirs[index])) return false;
|
|
331
|
+
return left.files.every((file, fileIndex) => {
|
|
332
|
+
const other = right.files[fileIndex];
|
|
333
|
+
if (!other || file.path !== other.path || file.lang !== other.lang || file.mtimeMs !== other.mtimeMs) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
if (file.symbols.length !== other.symbols.length) return false;
|
|
337
|
+
return file.symbols.every((symbol, symbolIndex) => {
|
|
338
|
+
const candidate = other.symbols[symbolIndex];
|
|
339
|
+
return candidate !== void 0 && symbol.name === candidate.name && symbol.kind === candidate.kind && symbol.file === candidate.file && symbol.line === candidate.line && symbol.endLine === candidate.endLine && symbol.exported === candidate.exported && symbol.signature === candidate.signature;
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
async function findRepoRoot(startDir, maxLevels = 12) {
|
|
344
|
+
let dir = path4.resolve(startDir);
|
|
345
|
+
for (let level = 0; level < maxLevels; level++) {
|
|
346
|
+
try {
|
|
347
|
+
await stat2(path4.join(dir, ".git"));
|
|
348
|
+
return dir;
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
const parent = path4.dirname(dir);
|
|
352
|
+
if (parent === dir) return null;
|
|
353
|
+
dir = parent;
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/types.ts
|
|
359
|
+
function symbolCount(index) {
|
|
360
|
+
return index.files.reduce((n, f) => n + f.symbols.length, 0);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/search.ts
|
|
364
|
+
function matchScore(name2, query) {
|
|
365
|
+
const q = query.toLowerCase();
|
|
366
|
+
const n = name2.toLowerCase();
|
|
367
|
+
if (n === q) return 1;
|
|
368
|
+
if (n.startsWith(q)) return 0.8;
|
|
369
|
+
if (n.includes(q)) return 0.5;
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
function searchSymbols(index, filter, limit = 50) {
|
|
373
|
+
const q = (filter.query ?? "").trim();
|
|
374
|
+
const filePat = filter.file?.trim().toLowerCase();
|
|
375
|
+
const hits = [];
|
|
376
|
+
for (const file of index.files) {
|
|
377
|
+
if (filePat && !file.path.toLowerCase().includes(filePat)) continue;
|
|
378
|
+
for (const sym of file.symbols) {
|
|
379
|
+
if (filter.kind && sym.kind !== filter.kind) continue;
|
|
380
|
+
if (filter.exportedOnly && !sym.exported) continue;
|
|
381
|
+
const score = q ? matchScore(sym.name, q) : 0.4;
|
|
382
|
+
if (q && score === 0) continue;
|
|
383
|
+
hits.push({ ...sym, score });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
hits.sort((a, b) => {
|
|
387
|
+
const ab = Number(b.exported) - Number(a.exported) || b.score - a.score || a.name.localeCompare(b.name) || a.file.localeCompare(b.file);
|
|
388
|
+
return ab;
|
|
389
|
+
});
|
|
390
|
+
return hits.slice(0, limit);
|
|
391
|
+
}
|
|
392
|
+
function renderHit(hit) {
|
|
393
|
+
const exportMark = hit.exported ? "export " : "";
|
|
394
|
+
const sig = hit.signature || hit.name;
|
|
395
|
+
const score = hit.score < 1 ? ` [${hit.score.toFixed(2)}]` : "";
|
|
396
|
+
return `${exportMark}${hit.kind} ${sig}${score} \u2014 ${hit.file}:${hit.line}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/repomap.ts
|
|
400
|
+
var KIND_WEIGHT = {
|
|
401
|
+
class: 1,
|
|
402
|
+
interface: 1,
|
|
403
|
+
type: 0.8,
|
|
404
|
+
function: 0.8,
|
|
405
|
+
method: 0.7,
|
|
406
|
+
enum: 0.9,
|
|
407
|
+
variable: 0.25,
|
|
408
|
+
field: 0.2,
|
|
409
|
+
import: 0.05,
|
|
410
|
+
module: 0.3
|
|
411
|
+
};
|
|
412
|
+
function scoreFile(file) {
|
|
413
|
+
let score = 0;
|
|
414
|
+
for (const sym of file.symbols) {
|
|
415
|
+
score += KIND_WEIGHT[sym.kind] ?? 0.3;
|
|
416
|
+
if (sym.exported) score += 0.3;
|
|
417
|
+
}
|
|
418
|
+
return score / (1 + file.symbols.length * 0.04);
|
|
419
|
+
}
|
|
420
|
+
function rankRepoMap(index, options = {}) {
|
|
421
|
+
const topFiles = options.topFiles ?? 24;
|
|
422
|
+
const perFile = options.symbolsPerFile ?? 18;
|
|
423
|
+
const ranked = index.files.filter((f) => f.symbols.length > 0).map((f) => ({ file: f, score: scoreFile(f) })).sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path)).slice(0, topFiles);
|
|
424
|
+
return ranked.map(({ file, score }) => ({
|
|
425
|
+
path: file.path,
|
|
426
|
+
score,
|
|
427
|
+
symbols: file.symbols.slice(0, perFile).map((s) => ({
|
|
428
|
+
name: s.name,
|
|
429
|
+
kind: s.kind,
|
|
430
|
+
line: s.line,
|
|
431
|
+
signature: s.signature
|
|
432
|
+
}))
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
function renderRepoMap(entries, options = {}) {
|
|
436
|
+
const maxChars = options.maxChars ?? 3200;
|
|
437
|
+
const lines = ["# repo map"];
|
|
438
|
+
let totalSymbols = 0;
|
|
439
|
+
for (const e of entries) {
|
|
440
|
+
totalSymbols += e.symbols.length;
|
|
441
|
+
lines.push(`## ${e.path} (${e.symbols.length})`);
|
|
442
|
+
for (const s of e.symbols) {
|
|
443
|
+
const label = s.signature || s.name;
|
|
444
|
+
lines.push(` ${s.kind} ${label} :${s.line}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
let text = lines.join("\n");
|
|
448
|
+
if (text.length > maxChars) {
|
|
449
|
+
text = `${text.slice(0, maxChars)}
|
|
450
|
+
\u2026 truncated`;
|
|
451
|
+
}
|
|
452
|
+
return totalSymbols > 0 ? text : "";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/tools.ts
|
|
456
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
457
|
+
import path5 from "path";
|
|
458
|
+
|
|
459
|
+
// src/config.ts
|
|
460
|
+
var DEFAULTS = {
|
|
461
|
+
excludeDirs: [],
|
|
462
|
+
mapTopFiles: 24,
|
|
463
|
+
mapMaxChars: 3200,
|
|
464
|
+
autoInject: true
|
|
465
|
+
};
|
|
466
|
+
var state = { current: { ...DEFAULTS } };
|
|
467
|
+
function applyConfig(partial) {
|
|
468
|
+
state.current = {
|
|
469
|
+
...DEFAULTS,
|
|
470
|
+
...partial ?? {},
|
|
471
|
+
excludeDirs: [...DEFAULTS.excludeDirs, ...partial?.excludeDirs ?? []]
|
|
472
|
+
};
|
|
473
|
+
if (!Number.isFinite(state.current.mapTopFiles) || state.current.mapTopFiles < 1) {
|
|
474
|
+
state.current.mapTopFiles = DEFAULTS.mapTopFiles;
|
|
475
|
+
}
|
|
476
|
+
if (!Number.isFinite(state.current.mapMaxChars) || state.current.mapMaxChars < 200) {
|
|
477
|
+
state.current.mapMaxChars = DEFAULTS.mapMaxChars;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function getConfig() {
|
|
481
|
+
return state.current;
|
|
482
|
+
}
|
|
483
|
+
function indexOptions() {
|
|
484
|
+
return { excludeDirs: state.current.excludeDirs };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/tools.ts
|
|
488
|
+
function createIndexCache(load, ttlMs = 6e4, now = Date.now) {
|
|
489
|
+
const completed = /* @__PURE__ */ new Map();
|
|
490
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
491
|
+
let epoch = 0;
|
|
492
|
+
function startLoad(key, root, previous) {
|
|
493
|
+
const loadEpoch = epoch;
|
|
494
|
+
const begin = previous ? previous.catch(() => void 0).then(() => load(root)) : load(root);
|
|
495
|
+
const promise = begin.then((index) => {
|
|
496
|
+
if (loadEpoch === epoch) {
|
|
497
|
+
completed.delete(key);
|
|
498
|
+
completed.set(key, { index, at: now(), epoch: loadEpoch });
|
|
499
|
+
while (completed.size > 16) completed.delete(completed.keys().next().value);
|
|
500
|
+
}
|
|
501
|
+
return index;
|
|
502
|
+
}).finally(() => {
|
|
503
|
+
if (inFlight.get(key)?.promise === promise) inFlight.delete(key);
|
|
504
|
+
});
|
|
505
|
+
inFlight.set(key, { promise, epoch: loadEpoch });
|
|
506
|
+
return promise;
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
get(root, force = false) {
|
|
510
|
+
const resolvedRoot = path5.resolve(root);
|
|
511
|
+
const key = cacheKeyForRoot(resolvedRoot);
|
|
512
|
+
const running = inFlight.get(key);
|
|
513
|
+
if (running) {
|
|
514
|
+
if (running.epoch === epoch && !force) return running.promise;
|
|
515
|
+
return startLoad(key, resolvedRoot, running.promise);
|
|
516
|
+
}
|
|
517
|
+
if (!force) {
|
|
518
|
+
const cached = completed.get(key);
|
|
519
|
+
if (cached && cached.epoch === epoch && now() - cached.at < ttlMs) {
|
|
520
|
+
completed.delete(key);
|
|
521
|
+
completed.set(key, cached);
|
|
522
|
+
return Promise.resolve(cached.index);
|
|
523
|
+
}
|
|
524
|
+
if (cached) completed.delete(key);
|
|
525
|
+
} else {
|
|
526
|
+
completed.delete(key);
|
|
527
|
+
}
|
|
528
|
+
return startLoad(key, resolvedRoot);
|
|
529
|
+
},
|
|
530
|
+
invalidate() {
|
|
531
|
+
epoch++;
|
|
532
|
+
completed.clear();
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
var indexCache = createIndexCache((root) => buildIndexWithCache(root, indexOptions()));
|
|
537
|
+
function getIndex(root, force = false) {
|
|
538
|
+
return indexCache.get(root, force);
|
|
539
|
+
}
|
|
540
|
+
function invalidateIndexCache() {
|
|
541
|
+
indexCache.invalidate();
|
|
542
|
+
}
|
|
543
|
+
async function resolveRoot(arg, exec) {
|
|
544
|
+
const cwd = exec.agent?.session?.header?.cwd;
|
|
545
|
+
const base = arg ?? cwd ?? process.cwd();
|
|
546
|
+
const root = await findRepoRoot(base);
|
|
547
|
+
if (!root) throw new Error(`no git repository found from ${path5.resolve(base)}`);
|
|
548
|
+
return root;
|
|
549
|
+
}
|
|
550
|
+
var tools = [
|
|
551
|
+
defineTool({
|
|
552
|
+
name: "code_index",
|
|
553
|
+
description: "Manage the semantic repo index: status or (re)build it for the current workspace. Auto-builds the first time it is queried. Returns file/symbol counts and the index location.",
|
|
554
|
+
parameters: {
|
|
555
|
+
action: {
|
|
556
|
+
type: "string",
|
|
557
|
+
enum: ["status", "build"],
|
|
558
|
+
description: '"status" (default) reports without forcing a rebuild; "build" forces a fresh scan.'
|
|
559
|
+
},
|
|
560
|
+
repoRoot: {
|
|
561
|
+
type: "string",
|
|
562
|
+
description: "Optional absolute repo path. Defaults to the workspace root of the current session."
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
output: {
|
|
566
|
+
schema: { type: "string" },
|
|
567
|
+
render: (_args, value) => [{ type: "text", text: value }]
|
|
568
|
+
},
|
|
569
|
+
async execute(args, exec) {
|
|
570
|
+
try {
|
|
571
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
572
|
+
const index = await getIndex(root, args.action === "build");
|
|
573
|
+
const total = index.files.reduce((n, f) => n + f.symbols.length, 0);
|
|
574
|
+
const langs = new Set(index.files.map((f) => f.lang));
|
|
575
|
+
return [
|
|
576
|
+
`repo: ${root}`,
|
|
577
|
+
`files indexed: ${index.files.length}`,
|
|
578
|
+
`symbols: ${total}`,
|
|
579
|
+
`languages: ${[...langs].join(", ")}`,
|
|
580
|
+
index.files.length === 0 ? "no supported files found" : "index up to date"
|
|
581
|
+
].join("\n");
|
|
582
|
+
} catch (error) {
|
|
583
|
+
return `code_index: ${error.message ?? String(error)}`;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}),
|
|
587
|
+
defineTool({
|
|
588
|
+
name: "code_symbols",
|
|
589
|
+
description: "List symbols (functions, classes, interfaces, types, methods, variables) in the current repo. Filter by name substring, file path substring, or symbol kind. Results are exported-first, alphabetically ordered.",
|
|
590
|
+
parameters: {
|
|
591
|
+
query: {
|
|
592
|
+
type: "string",
|
|
593
|
+
description: "Substring of the symbol name to match (case-insensitive). Omit to list all."
|
|
594
|
+
},
|
|
595
|
+
file: {
|
|
596
|
+
type: "string",
|
|
597
|
+
description: 'Substring of the repo-relative file path to match, e.g. "src/core".'
|
|
598
|
+
},
|
|
599
|
+
kind: {
|
|
600
|
+
type: "string",
|
|
601
|
+
enum: ["function", "method", "class", "interface", "type", "enum", "variable", "field"],
|
|
602
|
+
description: "Only return symbols of this kind."
|
|
603
|
+
},
|
|
604
|
+
exportedOnly: {
|
|
605
|
+
type: "boolean",
|
|
606
|
+
description: "Only exported (module-level public) symbols."
|
|
607
|
+
},
|
|
608
|
+
limit: {
|
|
609
|
+
type: "number",
|
|
610
|
+
description: "Max rows (default 50)."
|
|
611
|
+
},
|
|
612
|
+
repoRoot: {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "Optional absolute repo path; defaults to the session workspace root."
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
output: {
|
|
618
|
+
schema: { type: "array" },
|
|
619
|
+
render: (_args, value) => value.map((v) => ({
|
|
620
|
+
type: "text",
|
|
621
|
+
text: renderHit(v)
|
|
622
|
+
}))
|
|
623
|
+
},
|
|
624
|
+
async execute(args, exec) {
|
|
625
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
626
|
+
const index = await getIndex(root);
|
|
627
|
+
const hits = searchSymbols(
|
|
628
|
+
index,
|
|
629
|
+
{ query: args.query, file: args.file, kind: args.kind, exportedOnly: args.exportedOnly },
|
|
630
|
+
args.limit ?? 50
|
|
631
|
+
);
|
|
632
|
+
return hits;
|
|
633
|
+
}
|
|
634
|
+
}),
|
|
635
|
+
defineTool({
|
|
636
|
+
name: "code_search",
|
|
637
|
+
description: "Ranked symbol search over the repo index: exact > prefix > substring name matches, exported symbols first. Results carry a relevance score and file:line, so the model can locate definitions quickly.",
|
|
638
|
+
parameters: {
|
|
639
|
+
query: {
|
|
640
|
+
type: "string",
|
|
641
|
+
required: true,
|
|
642
|
+
description: "Symbol name (or fragment) to find."
|
|
643
|
+
},
|
|
644
|
+
limit: {
|
|
645
|
+
type: "number",
|
|
646
|
+
description: "Max hits (default 20)."
|
|
647
|
+
},
|
|
648
|
+
repoRoot: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description: "Optional absolute repo path; defaults to the session workspace root."
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
output: {
|
|
654
|
+
schema: { type: "array" },
|
|
655
|
+
render: (_args, value) => value.map((v) => ({
|
|
656
|
+
type: "text",
|
|
657
|
+
text: renderHit(v)
|
|
658
|
+
}))
|
|
659
|
+
},
|
|
660
|
+
async execute(args, exec) {
|
|
661
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
662
|
+
const index = await getIndex(root);
|
|
663
|
+
const hits = searchSymbols(index, { query: args.query }, args.limit ?? 20);
|
|
664
|
+
return hits;
|
|
665
|
+
}
|
|
666
|
+
}),
|
|
667
|
+
defineTool({
|
|
668
|
+
name: "code_map",
|
|
669
|
+
description: "Return a bounded, ranked map of the current repo (top files by symbol density, with their key symbols and lines). The model can call this once per session to build an internal model of the codebase before browsing files.",
|
|
670
|
+
parameters: {
|
|
671
|
+
repoRoot: {
|
|
672
|
+
type: "string",
|
|
673
|
+
description: "Optional absolute repo path; defaults to the session workspace root."
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
output: {
|
|
677
|
+
schema: { type: "string" },
|
|
678
|
+
render: (_args, value) => [{ type: "text", text: value }]
|
|
679
|
+
},
|
|
680
|
+
async execute(args, exec) {
|
|
681
|
+
try {
|
|
682
|
+
const root = await resolveRoot(args.repoRoot, exec);
|
|
683
|
+
const index = await getIndex(root);
|
|
684
|
+
const cfg = getConfig();
|
|
685
|
+
const map = renderRepoMap(
|
|
686
|
+
rankRepoMap(index, { topFiles: cfg.mapTopFiles }),
|
|
687
|
+
{ maxChars: cfg.mapMaxChars }
|
|
688
|
+
);
|
|
689
|
+
if (!map) return "no indexable symbols found in this repo";
|
|
690
|
+
return map;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
return `code_index: ${error.message ?? String(error)}`;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
})
|
|
696
|
+
];
|
|
697
|
+
|
|
698
|
+
// src/index.ts
|
|
699
|
+
var name = "dsh-code-index";
|
|
700
|
+
var inject = ["tools", "systemPrompt"];
|
|
701
|
+
var MAP_TTL_MS = 6e4;
|
|
702
|
+
function apply(ctx, pluginConfig) {
|
|
703
|
+
applyConfig(pluginConfig);
|
|
704
|
+
invalidateIndexCache();
|
|
705
|
+
ctx.effect(() => {
|
|
706
|
+
const disposers = [];
|
|
707
|
+
console.log("[dsh-code-index] plugin loaded");
|
|
708
|
+
for (const tool of tools) {
|
|
709
|
+
disposers.push(ctx.tools.register(tool));
|
|
710
|
+
console.log(`[dsh-code-index] registered tool: ${tool.name}`);
|
|
711
|
+
}
|
|
712
|
+
let cached = null;
|
|
713
|
+
async function warmMap() {
|
|
714
|
+
const now = Date.now();
|
|
715
|
+
const cfg = getConfig();
|
|
716
|
+
try {
|
|
717
|
+
const root = await findRepoRoot(process.cwd());
|
|
718
|
+
if (!root) {
|
|
719
|
+
cached = { root: "", at: now, text: "" };
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const index = await getIndex(root);
|
|
723
|
+
const text = renderRepoMap(
|
|
724
|
+
rankRepoMap(index, { topFiles: cfg.mapTopFiles }),
|
|
725
|
+
{ maxChars: cfg.mapMaxChars }
|
|
726
|
+
);
|
|
727
|
+
const stats = symbolCount(index);
|
|
728
|
+
cached = {
|
|
729
|
+
root: index.root,
|
|
730
|
+
at: Date.now(),
|
|
731
|
+
text: text ? `${text}
|
|
732
|
+
|
|
733
|
+
(summary: ${index.files.length} files, ${stats} symbols)` : ""
|
|
734
|
+
};
|
|
735
|
+
} catch {
|
|
736
|
+
cached = { root: "", at: now, text: "" };
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
void warmMap();
|
|
740
|
+
if (getConfig().autoInject) {
|
|
741
|
+
disposers.push(ctx.systemPrompt.section({
|
|
742
|
+
name: "code-index:repo-map",
|
|
743
|
+
order: 60,
|
|
744
|
+
// before tool guidance (100–199), after persona (0)
|
|
745
|
+
text: () => {
|
|
746
|
+
const now = Date.now();
|
|
747
|
+
if (cached && now - cached.at < MAP_TTL_MS) return cached.text;
|
|
748
|
+
void warmMap();
|
|
749
|
+
return cached?.text ?? "";
|
|
750
|
+
}
|
|
751
|
+
}));
|
|
752
|
+
}
|
|
753
|
+
return () => {
|
|
754
|
+
const errors = [];
|
|
755
|
+
for (const dispose of disposers.reverse()) {
|
|
756
|
+
try {
|
|
757
|
+
dispose();
|
|
758
|
+
} catch (error) {
|
|
759
|
+
errors.push(error);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
console.log("[dsh-code-index] plugin unloaded");
|
|
763
|
+
if (errors.length > 0) throw new AggregateError(errors, "failed to unload dsh-code-index");
|
|
764
|
+
};
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
export {
|
|
768
|
+
CACHE_DIR_NAME,
|
|
769
|
+
DEFAULT_EXCLUDED_DIRS,
|
|
770
|
+
SUPPORTED_EXTS,
|
|
771
|
+
apply,
|
|
772
|
+
buildIndex,
|
|
773
|
+
buildIndexWithCache,
|
|
774
|
+
defaultCachePath,
|
|
775
|
+
extractSymbols,
|
|
776
|
+
findRepoRoot,
|
|
777
|
+
inject,
|
|
778
|
+
languageForFile,
|
|
779
|
+
loadIndex,
|
|
780
|
+
name,
|
|
781
|
+
parseFileToSymbols,
|
|
782
|
+
rankRepoMap,
|
|
783
|
+
renderHit,
|
|
784
|
+
renderRepoMap,
|
|
785
|
+
saveIndex,
|
|
786
|
+
scanRepo,
|
|
787
|
+
scoreFile,
|
|
788
|
+
searchSymbols,
|
|
789
|
+
symbolCount,
|
|
790
|
+
tools
|
|
791
|
+
};
|
|
792
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/buildIndex.ts","../src/extract.ts","../src/scan.ts","../src/store.ts","../src/types.ts","../src/search.ts","../src/repomap.ts","../src/tools.ts","../src/config.ts","../src/index.ts"],"sourcesContent":["/** Build a RepoIndex for a workspace: scan -> extract -> persist. */\n\nimport { readFile, stat } from 'node:fs/promises'\nimport path from 'node:path'\nimport { extractSymbols, languageForFile } from './extract.js'\nimport { scanRepo, DEFAULT_EXCLUDED_DIRS } from './scan.js'\nimport {\n cacheKeyForRoot,\n defaultCachePath,\n legacyCachePath,\n loadIndex,\n saveIndex,\n} from './store.js'\nimport type { IndexOptions, IndexedFile, RepoIndex } from './types.js'\n\n/**\n * Full build: scan + extract every supported file. Existing per-file\n * extraction is reused when `previous` holds an identical mtime for the\n * same rel path (incremental refresh), so touched files alone re-parse.\n */\nexport async function buildIndex(\n root: string,\n options: IndexOptions = {},\n previous: RepoIndex | null = null,\n): Promise<RepoIndex> {\n const scanned = await scanRepo(root, options)\n\n // Index stale/current state from the previous run by rel path.\n const prevByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]))\n const prevMtime = new Map((previous?.files ?? []).map((f) => [f.path, f.mtimeMs]))\n\n const files: IndexedFile[] = []\n const BATCH = 8\n for (let i = 0; i < scanned.length; i += BATCH) {\n const batch = scanned.slice(i, i + BATCH)\n const rows = await Promise.all(\n batch.map(async (f) => {\n const lang = languageForFile(f.abs)\n if (!lang) return null\n if (prevMtime.get(f.rel) === f.mtimeMs) {\n const cached = prevByPath.get(f.rel)!\n return {\n ...cached,\n mtimeMs: f.mtimeMs,\n symbols: cached.symbols.map((symbol) => ({ ...symbol, file: f.rel })),\n }\n }\n let code: string\n try {\n code = await readFile(f.abs, 'utf8')\n } catch {\n return null\n }\n const symbols = await extractSymbols(code, lang)\n // Backfill the repo-relative path: the extractor is file-agnostic and\n // leaves SymbolInfo.file empty, but search/render depend on it.\n return {\n path: f.rel,\n lang,\n mtimeMs: f.mtimeMs,\n symbols: symbols.map((s) => ({ ...s, file: f.rel })),\n } satisfies IndexedFile\n }),\n )\n for (const f of rows) {\n if (f) files.push(f)\n }\n }\n\n return {\n root,\n generatedAt: Date.now(),\n files,\n excludedDirs: [...DEFAULT_EXCLUDED_DIRS, ...(options.excludeDirs ?? [])],\n }\n}\n\n/** Convenience wrapper: evolve an on-disk cache if `root` exists. */\nexport async function buildIndexWithCache(\n root: string,\n options: IndexOptions = {},\n cacheDir?: string,\n): Promise<RepoIndex> {\n const cachePath = defaultCachePath(root, cacheDir)\n let prev = await loadIndex(cachePath)\n let loadedLegacy = false\n if (!prev) {\n const oldPath = legacyCachePath(root, cacheDir)\n if (oldPath !== cachePath) {\n prev = await loadIndex(oldPath)\n loadedLegacy = prev !== null\n }\n }\n const reusable = prev && cacheKeyForRoot(prev.root) === cacheKeyForRoot(root) ? prev : null\n const fresh = await buildIndex(root, options, reusable)\n if (prev && indexesEqual(prev, fresh)) {\n if (loadedLegacy) await saveIndex(cachePath, fresh)\n return loadedLegacy ? fresh : prev\n }\n await saveIndex(cachePath, fresh)\n return fresh\n}\n\nfunction indexesEqual(left: RepoIndex, right: RepoIndex): boolean {\n if (cacheKeyForRoot(left.root) !== cacheKeyForRoot(right.root)) return false\n if (left.files.length !== right.files.length) return false\n if (left.excludedDirs.length !== right.excludedDirs.length) return false\n if (left.excludedDirs.some((value, index) => value !== right.excludedDirs[index])) return false\n\n return left.files.every((file, fileIndex) => {\n const other = right.files[fileIndex]\n if (!other || file.path !== other.path || file.lang !== other.lang || file.mtimeMs !== other.mtimeMs) {\n return false\n }\n if (file.symbols.length !== other.symbols.length) return false\n return file.symbols.every((symbol, symbolIndex) => {\n const candidate = other.symbols[symbolIndex]\n return candidate !== undefined\n && symbol.name === candidate.name\n && symbol.kind === candidate.kind\n && symbol.file === candidate.file\n && symbol.line === candidate.line\n && symbol.endLine === candidate.endLine\n && symbol.exported === candidate.exported\n && symbol.signature === candidate.signature\n })\n })\n}\n\n/**\n * Locate the git repo root for a path by walking up to the nearest `.git`\n * directory. Bounded walk (max `maxLevels` levels); returns `null` when no\n * repo marker is found — callers must NOT index an untagged directory\n * (the fs root is the classic footgun: indexing `C:\\` by accident).\n */\nexport async function findRepoRoot(startDir: string, maxLevels = 12): Promise<string | null> {\n let dir = path.resolve(startDir)\n for (let level = 0; level < maxLevels; level++) {\n try {\n // stat() accepts BOTH a `.git` directory (normal repos) and a `.git`\n // file (worktrees / submodules) — readFile only handled the file form.\n await stat(path.join(dir, '.git'))\n return dir\n } catch {\n // not a repo here — keep walking\n }\n const parent = path.dirname(dir)\n if (parent === dir) return null\n dir = parent\n }\n return null\n}\n","/**\n * tree-sitter symbol extraction.\n *\n * Parses a source text with a per-language grammar (web-tree-sitter WASM,\n * static grammars from tree-sitter-wasms) and returns flat SymbolInfo rows.\n *\n * NOTE on the dependency pin: web-tree-sitter must stay at ^0.20.x — newer\n * releases expect dylinked grammar wasm, while tree-sitter-wasms ships\n * static builds. Verified working pair: web-tree-sitter@0.20.8 + tree-sitter-wasms@0.1.13.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport Parser from 'web-tree-sitter'\nimport type { SymbolInfo, SymbolKind } from './types.js'\n\nconst require = createRequire(import.meta.url)\n// web-tree-sitter@0.20.8 is CJS `export = Parser`; default-interop gives the class directly.\n\nexport type LanguageId = 'typescript' | 'javascript'\n\nconst WASM_DIR = path.dirname(require.resolve('tree-sitter-wasms/out/tree-sitter-typescript.wasm'))\n\nconst EXT_TO_LANG: Record<string, LanguageId> = {\n '.ts': 'typescript',\n '.tsx': 'typescript',\n '.mts': 'typescript',\n '.cts': 'typescript',\n '.js': 'javascript',\n '.jsx': 'javascript',\n '.mjs': 'javascript',\n '.cjs': 'javascript',\n}\n\nexport function languageForFile(filePath: string): LanguageId | null {\n const ext = path.extname(filePath).toLowerCase()\n return EXT_TO_LANG[ext] ?? null\n}\n\n/** Kind of a capture per query. */\ntype CaptureDef = { kind: SymbolKind; exported?: boolean }\n\n// Query definitions per language. Captures bind to the DECLARATION node\n// (capture OUTSIDE the pattern: `(function_declaration) @function` — the\n// 0.20 query parser rejects a capture sitting directly after the node type\n// inside the parens). Names/signatures/export come from node fields.\nconst QUERIES: Record<LanguageId, string> = {\n typescript: `\n (function_declaration) @function\n (generator_function_declaration) @function\n (method_definition) @method\n (class_declaration) @class\n (interface_declaration) @interface\n (type_alias_declaration) @type\n (enum_declaration) @enum\n (variable_declarator) @variable\n (public_field_definition) @field\n (abstract_class_declaration) @class\n `,\n // JS shares the same query surface; type-related patterns simply never match.\n javascript: `\n (function_declaration) @function\n (generator_function_declaration) @function\n (method_definition) @method\n (class_declaration) @class\n (variable_declarator) @variable\n `,\n}\n\nconst CAPTURE_KINDS: Record<string, CaptureDef> = {\n function: { kind: 'function' },\n method: { kind: 'method' },\n class: { kind: 'class' },\n interface: { kind: 'interface' },\n type: { kind: 'type' },\n enum: { kind: 'enum' },\n variable: { kind: 'variable' },\n field: { kind: 'field' },\n}\n\nlet parserPromise: Promise<Parser> | null = null\n\nasync function getParser(): Promise<Parser> {\n if (!parserPromise) {\n parserPromise = (async () => {\n const wasm = path.join(path.dirname(require.resolve('web-tree-sitter')), 'tree-sitter.wasm')\n await Parser.init({ locateFile: () => wasm })\n const p = new Parser()\n return p\n })()\n }\n return parserPromise\n}\n\nconst languageCache = new Map<LanguageId, Promise<Parser.Language>>()\n\nfunction getLanguage(id: LanguageId): Promise<Parser.Language> {\n let entry = languageCache.get(id)\n if (!entry) {\n entry = getParser().then(async (parser) => {\n const grammarName = id === 'typescript' ? 'tree-sitter-typescript' : 'tree-sitter-javascript'\n const grammarPath = path.join(WASM_DIR, `${grammarName}.wasm`)\n const bytes = await readFile(grammarPath)\n const lang = await Parser.Language.load(bytes)\n // Precompile queries per language to catch authoring errors early.\n const q = lang.query(QUERIES[id])\n q.delete()\n return lang\n })\n languageCache.set(id, entry)\n }\n return entry\n}\n\n/**\n * Extract all top-level symbols from source text of the given language.\n * Returns rows ordered by file line. Never throws for parse errors — a\n * failed parse yields an empty list (the caller logs and continues).\n */\nexport async function extractSymbols(code: string, id: LanguageId): Promise<SymbolInfo[]> {\n const lang = await getLanguage(id)\n const parser = await getParser()\n parser.setLanguage(lang)\n const tree = parser.parse(code)\n try {\n const query = lang.query(QUERIES[id])\n const rows: SymbolInfo[] = []\n try {\n const captures = query.captures(tree.rootNode)\n for (const cap of captures) {\n const def = CAPTURE_KINDS[cap.name]\n if (!def) continue\n const node = cap.node\n const name = nameOf(node)\n if (!name) continue\n rows.push({\n name,\n kind: def.kind,\n file: '', // set by the caller (extractor is file-agnostic)\n line: node.startPosition.row + 1,\n endLine: node.endPosition.row + 1,\n exported: isExported(node),\n signature: signatureFor(node),\n })\n }\n } finally {\n query.delete()\n }\n rows.sort((a, b) => a.line - b.line)\n return rows\n } finally {\n tree.delete()\n }\n}\n\n/** The declared name of a declaration node, via its `name` field. */\nfunction nameOf(node: Parser.SyntaxNode): string {\n const field = node.childForFieldName?.('name')\n if (field) return field.text.trim()\n // e.g. an anonymous default export — skip those.\n return ''\n}\n\n/** Best-effort declaration signature: `name` + parameter list, if any. */\nfunction signatureFor(node: Parser.SyntaxNode): string {\n const name = nameOf(node)\n const params = node.namedChildren.find(\n (c) => c.type === 'formal_parameters' || c.type === 'method_parameters',\n )\n if (params) {\n return `${name}${params.text}`\n }\n const first = node.namedChildren[0]\n return first ? first.text.trim() : name\n}\n\n/** A node is exported when it sits directly inside an `export_statement`. */\nfunction isExported(node: Parser.SyntaxNode): boolean {\n let parent = node.parent\n let depth = 0\n while (parent && depth < 3) {\n if (parent.type === 'export_statement') return true\n if (parent.type === 'statement_block') return false\n parent = parent.parent\n depth++\n }\n return false\n}\n\n/** Compile a symbol query for a code sample (used by tests). */\nexport async function parseFileToSymbols(\n filePath: string,\n repoRoot: string,\n code?: string,\n): Promise<SymbolInfo[]> {\n const lang = languageForFile(filePath)\n if (!lang) return []\n const text = code ?? (await readFile(filePath, 'utf8'))\n const rows = await extractSymbols(text, lang)\n const rel = path.relative(repoRoot, filePath).split(path.sep).join('/')\n return rows.map((r) => ({ ...r, file: rel }))\n}","/** Workspace file discovery with dir exclusions and mtime tracking. */\n\nimport { readdir, stat } from 'node:fs/promises'\nimport path from 'node:path'\nimport type { IndexOptions } from './types.js'\n\nexport const DEFAULT_EXCLUDED_DIRS = [\n 'node_modules',\n '.git',\n '.idea',\n '.vscode',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n '.next',\n '.nuxt',\n '.cache',\n 'target',\n 'vendor',\n '.dsh-code-index',\n]\n\n/** Language-agnostic source extensions we index. */\nexport const SUPPORTED_EXTS = new Set([\n '.ts',\n '.tsx',\n '.mts',\n '.cts',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n])\n\nexport interface ScannedFile {\n /** Absolute path on disk. */\n abs: string\n /** Repo-relative path (forward-slash). */\n rel: string\n mtimeMs: number\n}\n\n/**\n * Recursively walk `root` and return indexed files, skipping excluded dir\n * components at any depth. Uses readdir with `withFileTypes` so we never\n * stat every entry; mtime comes from a targeted stat per candidate file.\n */\nexport async function scanRepo(\n root: string,\n options: IndexOptions = {},\n): Promise<ScannedFile[]> {\n const excluded = new Set([...DEFAULT_EXCLUDED_DIRS, ...(options.excludeDirs ?? [])])\n const results: ScannedFile[] = []\n const queue: Array<[string, string]> = [[root, '']] // [absDir, relDir]\n\n while (queue.length) {\n const [absDir, relDir] = queue.pop()!\n let entries\n try {\n entries = await readdir(absDir, { withFileTypes: true })\n } catch {\n continue // unreadable dir: skip into the void\n }\n for (const entry of entries) {\n const abs = path.join(absDir, entry.name)\n const rel = relDir ? `${relDir}/${entry.name}` : entry.name\n if (entry.isDirectory()) {\n if (excluded.has(entry.name)) continue\n queue.push([abs, rel])\n } else if (entry.isFile() && SUPPORTED_EXTS.has(path.extname(entry.name).toLowerCase())) {\n try {\n const st = await stat(abs)\n results.push({ abs, rel, mtimeMs: st.mtimeMs })\n } catch {\n // race: file deleted mid-scan — ignore\n }\n }\n }\n }\n\n results.sort((a, b) => a.rel.localeCompare(b.rel))\n return results\n}","/** On-disk JSON cache for RepoIndex, keyed by repo root. */\n\nimport { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport { createHash, randomUUID } from 'node:crypto'\nimport type { RepoIndex } from './types.js'\n\nexport const CACHE_DIR_NAME = '.dsh-code-index'\n\n/** Stable root identity for cache hashing, including Windows case aliases. */\nexport function cacheKeyForRoot(root: string): string {\n const resolved = path.resolve(root)\n return process.platform === 'win32' ? resolved.toLowerCase() : resolved\n}\n\n/** Default cache location: `<repoRoot>/.dsh-code-index/index.json`. */\nexport function defaultCachePath(root: string, cacheDir?: string): string {\n const dir = cacheDir ?? path.join(root, CACHE_DIR_NAME)\n const hash = createHash('sha1').update(cacheKeyForRoot(root)).digest('hex').slice(0, 12)\n return path.join(dir, `${hash}.json`)\n}\n\n/** Cache filename used before Windows root casing was canonicalized. */\nexport function legacyCachePath(root: string, cacheDir?: string): string {\n const dir = cacheDir ?? path.join(root, CACHE_DIR_NAME)\n const hash = createHash('sha1').update(root).digest('hex').slice(0, 12)\n return path.join(dir, `${hash}.json`)\n}\n\nexport async function loadIndex(cachePath: string): Promise<RepoIndex | null> {\n try {\n const raw = await readFile(cachePath, 'utf8')\n const parsed = JSON.parse(raw) as RepoIndex\n if (!parsed || typeof parsed.root !== 'string' || !Array.isArray(parsed.files)) return null\n return parsed\n } catch {\n return null // missing or corrupt cache == no cache\n }\n}\n\nexport async function saveIndex(cachePath: string, index: RepoIndex): Promise<void> {\n await mkdir(path.dirname(cachePath), { recursive: true })\n const temporaryPath = `${cachePath}.${process.pid}.${randomUUID()}.tmp`\n try {\n await writeFile(temporaryPath, JSON.stringify(index, null, 2), { encoding: 'utf8', flag: 'wx' })\n await rename(temporaryPath, cachePath)\n } finally {\n await rm(temporaryPath, { force: true })\n }\n}\n","/** Shared data model for dsh-code-index. */\n\nexport type SymbolKind =\n | 'function'\n | 'method'\n | 'class'\n | 'interface'\n | 'type'\n | 'enum'\n | 'variable'\n | 'field'\n | 'import'\n | 'module'\n\nexport interface SymbolInfo {\n /** Symbol name (identifier / type id / member name). */\n name: string\n kind: SymbolKind\n /** Repo-relative path of the containing file, always forward-slash. */\n file: string\n /** 1-based start line. */\n line: number\n /** 1-based end line (inclusive). */\n endLine: number\n /** True when the symbol is exported at module level (export .. / export default). */\n exported: boolean\n /** Short human-readable signature, e.g. `greet(name: string)` — empty when n/a. */\n signature: string\n}\n\nexport interface IndexedFile {\n /** Repo-relative path (forward-slash). */\n path: string\n lang: string\n /** fs mtime of the source file at index time (milliseconds). */\n mtimeMs: number\n symbols: SymbolInfo[]\n}\n\nexport interface RepoIndex {\n /** Absolute source root this index describes. */\n root: string\n /** Unix ms when the index was generated. */\n generatedAt: number\n files: IndexedFile[]\n excludedDirs: string[]\n}\n\nexport function symbolCount(index: RepoIndex): number {\n return index.files.reduce((n, f) => n + f.symbols.length, 0)\n}\n\n/** Optional config consumed by the index build pipeline. */\nexport interface IndexOptions {\n /** Extra directories to exclude, appended to the defaults. */\n excludeDirs?: string[]\n /** Minimum mtime delta (ms) that forces a re-extract in refresh mode. */\n staleToleranceMs?: number\n}","/** Pure search / filter logic over a RepoIndex — unit-testable, no IO. */\n\nimport type { RepoIndex, SymbolInfo, SymbolKind } from './types.js'\n\nexport interface SymbolFilter {\n query?: string\n file?: string\n kind?: SymbolKind\n exportedOnly?: boolean\n}\n\nexport interface RankedHit extends SymbolInfo {\n /** 0..1 relevance. 1 = exact name match; lower = fuzzier. */\n score: number\n}\n\nfunction matchScore(name: string, query: string): number {\n const q = query.toLowerCase()\n const n = name.toLowerCase()\n if (n === q) return 1\n if (n.startsWith(q)) return 0.8\n if (n.includes(q)) return 0.5\n return 0\n}\n\n/** Filter symbols by file/kind/export, then score by name against query (if any). */\nexport function searchSymbols(\n index: RepoIndex,\n filter: SymbolFilter,\n limit = 50,\n): RankedHit[] {\n const q = (filter.query ?? '').trim()\n const filePat = filter.file?.trim().toLowerCase()\n const hits: RankedHit[] = []\n\n for (const file of index.files) {\n if (filePat && !file.path.toLowerCase().includes(filePat)) continue\n for (const sym of file.symbols) {\n if (filter.kind && sym.kind !== filter.kind) continue\n if (filter.exportedOnly && !sym.exported) continue\n const score = q ? matchScore(sym.name, q) : 0.4\n if (q && score === 0) continue\n hits.push({ ...sym, score })\n }\n }\n\n // Export boost, then relevance, then stable name order.\n hits.sort((a, b) => {\n const ab =\n Number(b.exported) - Number(a.exported) ||\n b.score - a.score ||\n a.name.localeCompare(b.name) ||\n a.file.localeCompare(b.file)\n return ab\n })\n return hits.slice(0, limit)\n}\n\n/** Neat one-line rendering of a hit for terminal/chat output. */\nexport function renderHit(hit: RankedHit): string {\n const exportMark = hit.exported ? 'export ' : ''\n const sig = hit.signature || hit.name\n const score = hit.score < 1 ? ` [${hit.score.toFixed(2)}]` : ''\n return `${exportMark}${hit.kind} ${sig}${score} — ${hit.file}:${hit.line}`\n}","/**\n * Bounded, ranked repo map generation — the \"aider-style\" overview the model\n * can turn to before browsing files. Pure logic, no IO.\n */\n\nimport type { IndexedFile, RepoIndex, SymbolInfo, SymbolKind } from './types.js'\n\n/** Higher-weight symbols pull their file up the map. */\nexport const KIND_WEIGHT: Record<SymbolKind, number> = {\n class: 1.0,\n interface: 1.0,\n type: 0.8,\n function: 0.8,\n method: 0.7,\n enum: 0.9,\n variable: 0.25,\n field: 0.2,\n import: 0.05,\n module: 0.3,\n}\n\nexport interface RepoMapOptions {\n /** Max files to include (default 24). */\n topFiles?: number\n /** Max symbols per file (default 18). */\n symbolsPerFile?: number\n /** Hard cap on rendered characters (default 3200). */\n maxChars?: number\n}\n\nexport interface RepoMapEntry {\n path: string\n score: number\n symbols: Array<{ name: string; kind: SymbolKind; line: number; signature: string }>\n}\n\n/** Density-aware file score: weighted symbols minus bloat. */\nexport function scoreFile(file: IndexedFile): number {\n let score = 0\n for (const sym of file.symbols) {\n score += KIND_WEIGHT[sym.kind] ?? 0.3\n if (sym.exported) score += 0.3\n }\n return score / (1 + file.symbols.length * 0.04)\n}\n\n/** Rank files, take the top slice, cap per-file symbols. */\nexport function rankRepoMap(index: RepoIndex, options: RepoMapOptions = {}): RepoMapEntry[] {\n const topFiles = options.topFiles ?? 24\n const perFile = options.symbolsPerFile ?? 18\n const ranked = index.files\n .filter((f) => f.symbols.length > 0)\n .map((f) => ({ file: f, score: scoreFile(f) }))\n .sort((a, b) => b.score - a.score || a.file.path.localeCompare(b.file.path))\n .slice(0, topFiles)\n\n return ranked.map(({ file, score }) => ({\n path: file.path,\n score,\n symbols: file.symbols.slice(0, perFile).map((s) => ({\n name: s.name,\n kind: s.kind,\n line: s.line,\n signature: s.signature,\n })),\n }))\n}\n\n/** Render the ranked map as markdown, hard-truncated to maxChars. */\nexport function renderRepoMap(entries: RepoMapEntry[], options: RepoMapOptions = {}): string {\n const maxChars = options.maxChars ?? 3200\n const lines: string[] = ['# repo map']\n let totalSymbols = 0\n for (const e of entries) {\n totalSymbols += e.symbols.length\n lines.push(`## ${e.path} (${e.symbols.length})`)\n for (const s of e.symbols) {\n const label = s.signature || s.name\n lines.push(` ${s.kind} ${label} :${s.line}`)\n }\n }\n let text = lines.join('\\n')\n if (text.length > maxChars) {\n text = `${text.slice(0, maxChars)}\\n… truncated`\n }\n return totalSymbols > 0 ? text : ''\n}","/** Model-visible tools: code_index, code_symbols, code_search. */\n\nimport { defineTool, type JsonValue } from '@deepseek-ai/dsh-tools'\nimport path from 'node:path'\nimport { buildIndexWithCache, findRepoRoot } from './buildIndex.js'\nimport { renderHit, searchSymbols } from './search.js'\nimport { rankRepoMap, renderRepoMap } from './repomap.js'\nimport { getConfig, indexOptions } from './config.js'\nimport { cacheKeyForRoot } from './store.js'\nimport type { RepoIndex } from './types.js'\n\n/** Minimal structural types for the harness surfaces we touch. */\ninterface ToolCwdContext {\n agent?: {\n session?: { header?: { cwd?: string } }\n }\n}\ntype ToolRunExec = ToolCwdContext & { signal?: AbortSignal }\n\ninterface TextBlock {\n type: 'text'\n text: string\n}\n\nexport interface IndexCache {\n get(root: string, force?: boolean): Promise<RepoIndex>\n invalidate(): void\n}\n\n/** Retain completed indexes briefly while still coalescing concurrent refreshes. */\nexport function createIndexCache(\n load: (root: string) => Promise<RepoIndex>,\n ttlMs = 60_000,\n now: () => number = Date.now,\n): IndexCache {\n interface CompletedEntry {\n index: RepoIndex\n at: number\n epoch: number\n }\n interface LoadEntry {\n promise: Promise<RepoIndex>\n epoch: number\n }\n\n const completed = new Map<string, CompletedEntry>()\n const inFlight = new Map<string, LoadEntry>()\n let epoch = 0\n\n function startLoad(key: string, root: string, previous?: Promise<RepoIndex>): Promise<RepoIndex> {\n const loadEpoch = epoch\n const begin = previous\n ? previous.catch(() => undefined).then(() => load(root))\n : load(root)\n const promise = begin\n .then((index) => {\n if (loadEpoch === epoch) {\n completed.delete(key)\n completed.set(key, { index, at: now(), epoch: loadEpoch })\n while (completed.size > 16) completed.delete(completed.keys().next().value!)\n }\n return index\n })\n .finally(() => {\n if (inFlight.get(key)?.promise === promise) inFlight.delete(key)\n })\n inFlight.set(key, { promise, epoch: loadEpoch })\n return promise\n }\n\n return {\n get(root, force = false) {\n const resolvedRoot = path.resolve(root)\n const key = cacheKeyForRoot(resolvedRoot)\n const running = inFlight.get(key)\n if (running) {\n if (running.epoch === epoch && !force) return running.promise\n return startLoad(key, resolvedRoot, running.promise)\n }\n\n if (!force) {\n const cached = completed.get(key)\n if (cached && cached.epoch === epoch && now() - cached.at < ttlMs) {\n completed.delete(key)\n completed.set(key, cached)\n return Promise.resolve(cached.index)\n }\n if (cached) completed.delete(key)\n } else {\n completed.delete(key)\n }\n return startLoad(key, resolvedRoot)\n },\n invalidate() {\n epoch++\n completed.clear()\n },\n }\n}\n\nconst indexCache = createIndexCache((root) => buildIndexWithCache(root, indexOptions()))\n\nexport function getIndex(root: string, force = false): Promise<RepoIndex> {\n return indexCache.get(root, force)\n}\n\nexport function invalidateIndexCache(): void {\n indexCache.invalidate()\n}\n\nasync function resolveRoot(arg: string | undefined, exec: ToolRunExec): Promise<string> {\n const cwd = exec.agent?.session?.header?.cwd\n const base = arg ?? cwd ?? process.cwd()\n const root = await findRepoRoot(base)\n if (!root) throw new Error(`no git repository found from ${path.resolve(base)}`)\n return root\n}\n\nexport const tools = [\n defineTool({\n name: 'code_index',\n description:\n 'Manage the semantic repo index: status or (re)build it for the current workspace. Auto-builds the first time it is queried. Returns file/symbol counts and the index location.',\n parameters: {\n action: {\n type: 'string',\n enum: ['status', 'build'],\n description: '\"status\" (default) reports without forcing a rebuild; \"build\" forces a fresh scan.',\n },\n repoRoot: {\n type: 'string',\n description:\n 'Optional absolute repo path. Defaults to the workspace root of the current session.',\n },\n },\n output: {\n schema: { type: 'string' },\n render: (_args, value: string): TextBlock[] => [{ type: 'text', text: value }],\n },\n async execute(args: { action?: string; repoRoot?: string }, exec: ToolRunExec): Promise<string> {\n try {\n const root = await resolveRoot(args.repoRoot, exec)\n const index = await getIndex(root, args.action === 'build')\n const total = index.files.reduce((n, f) => n + f.symbols.length, 0)\n const langs = new Set(index.files.map((f) => f.lang))\n return [\n `repo: ${root}`,\n `files indexed: ${index.files.length}`,\n `symbols: ${total}`,\n `languages: ${[...langs].join(', ')}`,\n index.files.length === 0 ? 'no supported files found' : 'index up to date',\n ].join('\\n')\n } catch (error) {\n return `code_index: ${(error as Error).message ?? String(error)}`\n }\n },\n }),\n\n defineTool({\n name: 'code_symbols',\n description:\n 'List symbols (functions, classes, interfaces, types, methods, variables) in the current repo. Filter by name substring, file path substring, or symbol kind. Results are exported-first, alphabetically ordered.',\n parameters: {\n query: {\n type: 'string',\n description: 'Substring of the symbol name to match (case-insensitive). Omit to list all.',\n },\n file: {\n type: 'string',\n description: 'Substring of the repo-relative file path to match, e.g. \"src/core\".',\n },\n kind: {\n type: 'string',\n enum: ['function', 'method', 'class', 'interface', 'type', 'enum', 'variable', 'field'],\n description: 'Only return symbols of this kind.',\n },\n exportedOnly: {\n type: 'boolean',\n description: 'Only exported (module-level public) symbols.',\n },\n limit: {\n type: 'number',\n description: 'Max rows (default 50).',\n },\n repoRoot: {\n type: 'string',\n description: 'Optional absolute repo path; defaults to the session workspace root.',\n },\n },\n output: {\n schema: { type: 'array' },\n render: (_args, value: JsonValue[]): TextBlock[] =>\n value.map((v) => ({\n type: 'text' as const,\n text: renderHit(v as unknown as Parameters<typeof renderHit>[0]),\n })),\n },\n async execute(\n args: {\n query?: string\n file?: string\n kind?: 'function' | 'method' | 'class' | 'interface' | 'type' | 'enum' | 'variable' | 'field'\n exportedOnly?: boolean\n limit?: number\n repoRoot?: string\n },\n exec: ToolRunExec,\n ): Promise<JsonValue[]> {\n const root = await resolveRoot(args.repoRoot, exec)\n const index = await getIndex(root)\n const hits = searchSymbols(\n index,\n { query: args.query, file: args.file, kind: args.kind, exportedOnly: args.exportedOnly },\n args.limit ?? 50,\n )\n return hits as unknown as JsonValue[]\n },\n }),\n\n defineTool({\n name: 'code_search',\n description:\n 'Ranked symbol search over the repo index: exact > prefix > substring name matches, exported symbols first. Results carry a relevance score and file:line, so the model can locate definitions quickly.',\n parameters: {\n query: {\n type: 'string',\n required: true,\n description: 'Symbol name (or fragment) to find.',\n },\n limit: {\n type: 'number',\n description: 'Max hits (default 20).',\n },\n repoRoot: {\n type: 'string',\n description: 'Optional absolute repo path; defaults to the session workspace root.',\n },\n },\n output: {\n schema: { type: 'array' },\n render: (_args, value: JsonValue[]): TextBlock[] =>\n value.map((v) => ({\n type: 'text' as const,\n text: renderHit(v as unknown as Parameters<typeof renderHit>[0]),\n })),\n },\n async execute(\n args: { query: string; limit?: number; repoRoot?: string },\n exec: ToolRunExec,\n ): Promise<JsonValue[]> {\n const root = await resolveRoot(args.repoRoot, exec)\n const index = await getIndex(root)\n const hits = searchSymbols(index, { query: args.query }, args.limit ?? 20)\n return hits as unknown as JsonValue[]\n },\n }),\n\n defineTool({\n name: 'code_map',\n description:\n 'Return a bounded, ranked map of the current repo (top files by symbol density, with their key symbols and lines). The model can call this once per session to build an internal model of the codebase before browsing files.',\n parameters: {\n repoRoot: {\n type: 'string',\n description: 'Optional absolute repo path; defaults to the session workspace root.',\n },\n },\n output: {\n schema: { type: 'string' },\n render: (_args, value: string): TextBlock[] => [{ type: 'text', text: value }],\n },\n async execute(args: { repoRoot?: string }, exec: ToolRunExec): Promise<string> {\n try {\n const root = await resolveRoot(args.repoRoot, exec)\n const index = await getIndex(root)\n const cfg = getConfig()\n const map = renderRepoMap(\n rankRepoMap(index, { topFiles: cfg.mapTopFiles }),\n { maxChars: cfg.mapMaxChars },\n )\n if (!map) return 'no indexable symbols found in this repo'\n return map\n } catch (error) {\n return `code_index: ${(error as Error).message ?? String(error)}`\n }\n },\n }),\n]\n","/** Plugin configuration: merged once at apply time, read wherever needed. */\n\nimport type { IndexOptions } from './types.js'\n\nexport interface PluginConfig {\n /** Extra directories to exclude from indexing (appended to defaults). */\n excludeDirs?: string[]\n /** Max files in the ranked repo map (code_map / auto section). */\n mapTopFiles?: number\n /** Hard char cap for rendered maps. */\n mapMaxChars?: number\n /** Set false to disable the auto-injected system section. */\n autoInject?: boolean\n}\n\ninterface EffectiveConfig {\n excludeDirs: string[]\n mapTopFiles: number\n mapMaxChars: number\n autoInject: boolean\n}\n\nconst DEFAULTS: EffectiveConfig = {\n excludeDirs: [],\n mapTopFiles: 24,\n mapMaxChars: 3200,\n autoInject: true,\n}\n\nconst state: { current: EffectiveConfig } = { current: { ...DEFAULTS } }\n\n/** Merge a plugin-provided partial config over the defaults (idempotent). */\nexport function applyConfig(partial?: PluginConfig): void {\n state.current = {\n ...DEFAULTS,\n ...(partial ?? {}),\n excludeDirs: [...DEFAULTS.excludeDirs, ...(partial?.excludeDirs ?? [])],\n }\n // Coerce obviously wrong inputs.\n if (!Number.isFinite(state.current.mapTopFiles) || state.current.mapTopFiles < 1) {\n state.current.mapTopFiles = DEFAULTS.mapTopFiles\n }\n if (!Number.isFinite(state.current.mapMaxChars) || state.current.mapMaxChars < 200) {\n state.current.mapMaxChars = DEFAULTS.mapMaxChars\n }\n}\n\nexport function getConfig(): Readonly<EffectiveConfig> {\n return state.current\n}\n\n/** Map the effective config onto the index pipeline options. */\nexport function indexOptions(): IndexOptions {\n return { excludeDirs: state.current.excludeDirs }\n}","/**\n * dsh-code-index — DeepSeek Harness bundle entry.\n *\n * Registers four model-visible tools (code_index / code_symbols /\n * code_search / code_map) backed by a tree-sitter symbol index, and\n * injects a bounded auto-updating repo map for the default workspace\n * into the system prompt.\n */\n\ntype Disposer = void | (() => void)\n\n/** Minimal structural Context; the real @deepseek-ai/cordis type is a\n * runtime dependency we intentionally do not import in the bundle entry. */\ninterface MinimalContext {\n effect(fn: () => Disposer): void\n tools: { register(t: unknown): () => void }\n systemPrompt: {\n section(section: {\n name: string\n order: number\n text: string | ((context: unknown) => string)\n }): () => void\n }\n}\n\nexport const name = 'dsh-code-index'\n\n// Public API surface (consumable by other bundles / tests).\nexport { buildIndex, buildIndexWithCache, findRepoRoot } from './buildIndex.js'\nexport { extractSymbols, languageForFile, parseFileToSymbols } from './extract.js'\nexport { scanRepo, DEFAULT_EXCLUDED_DIRS, SUPPORTED_EXTS } from './scan.js'\nexport { loadIndex, saveIndex, defaultCachePath, CACHE_DIR_NAME } from './store.js'\nexport { symbolCount } from './types.js'\nexport type { RepoIndex, IndexedFile, SymbolInfo, SymbolKind, IndexOptions } from './types.js'\nexport { searchSymbols, renderHit } from './search.js'\nexport { rankRepoMap, renderRepoMap, scoreFile } from './repomap.js'\nexport { tools } from './tools.js'\n\nimport { getIndex, invalidateIndexCache, tools } from './tools.js'\nimport { findRepoRoot } from './buildIndex.js'\nimport { rankRepoMap, renderRepoMap } from './repomap.js'\nimport { symbolCount } from './types.js'\nimport { applyConfig, getConfig, type PluginConfig } from './config.js'\n\nexport const inject = ['tools', 'systemPrompt'] as const\n\n/** TTL for the auto-injected repo map (seconds). */\nconst MAP_TTL_MS = 60_000\n\nexport function apply(ctx: MinimalContext, pluginConfig?: PluginConfig) {\n applyConfig(pluginConfig)\n invalidateIndexCache()\n ctx.effect(() => {\n const disposers: Array<() => void> = []\n console.log('[dsh-code-index] plugin loaded')\n for (const tool of tools) {\n disposers.push(ctx.tools.register(tool))\n console.log(`[dsh-code-index] registered tool: ${tool.name}`)\n }\n\n // Auto-inject a bounded repo map for the DEFAULT workspace (the dsh\n // launch directory, per the harness docs). Multi-workspace web sessions\n // should rely on the `code_map` tool, which resolves the per-session cwd.\n let cached: { root: string; at: number; text: string } | null = null\n\n async function warmMap(): Promise<void> {\n const now = Date.now()\n const cfg = getConfig()\n try {\n const root = await findRepoRoot(process.cwd())\n if (!root) {\n cached = { root: '', at: now, text: '' }\n return\n }\n const index = await getIndex(root)\n const text = renderRepoMap(\n rankRepoMap(index, { topFiles: cfg.mapTopFiles }),\n { maxChars: cfg.mapMaxChars },\n )\n const stats = symbolCount(index)\n cached = {\n root: index.root,\n at: Date.now(),\n text: text ? `${text}\\n\\n(summary: ${index.files.length} files, ${stats} symbols)` : '',\n }\n } catch {\n cached = { root: '', at: now, text: '' } // never let injection fail the boot\n }\n }\n\n // Warm eagerly at load so the first assembly already has the map.\n void warmMap()\n\n if (getConfig().autoInject) {\n disposers.push(ctx.systemPrompt.section({\n name: 'code-index:repo-map',\n order: 60, // before tool guidance (100–199), after persona (0)\n text: () => {\n const now = Date.now()\n if (cached && now - cached.at < MAP_TTL_MS) return cached.text\n void warmMap()\n return cached?.text ?? ''\n },\n }))\n }\n\n return () => {\n const errors: unknown[] = []\n for (const dispose of disposers.reverse()) {\n try {\n dispose()\n } catch (error) {\n errors.push(error)\n }\n }\n console.log('[dsh-code-index] plugin unloaded')\n if (errors.length > 0) throw new AggregateError(errors, 'failed to unload dsh-code-index')\n }\n })\n}\n"],"mappings":"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","names":["readFile","stat","path","require","name","path","readFile","path","readFile","path","stat","name","path","path"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-code-index",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Semantic repo index for DeepSeek Harness — tree-sitter symbol index, code search, and a bounded ranked repo map injected into agent context.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"cordis.patch.yml"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"prepare": "tsup",
|
|
18
|
+
"prepublishOnly": "tsup"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"dsh-plugin",
|
|
22
|
+
"deepseek-harness",
|
|
23
|
+
"code-index",
|
|
24
|
+
"repo-map",
|
|
25
|
+
"tree-sitter",
|
|
26
|
+
"code-search"
|
|
27
|
+
],
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.2.0",
|
|
38
|
+
"tsup": "^8.5.0",
|
|
39
|
+
"typescript": "^6.0.3",
|
|
40
|
+
"vitest": "^4.1.8"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.8",
|
|
45
|
+
"tree-sitter-wasms": "^0.1.13",
|
|
46
|
+
"web-tree-sitter": "^0.20.8"
|
|
47
|
+
}
|
|
48
|
+
}
|