dsh-tool-ast-grep 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 +112 -0
- package/README.zh.md +114 -0
- package/cordis.patch.yml +8 -0
- package/index.js +439 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jaylor Wang (Jaylor-Wang)
|
|
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,112 @@
|
|
|
1
|
+
# dsh-tool-ast-grep
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
**AST-based structural code search and outline plugin for DeepSeek Harness (DSH)**, powered by [ast-grep](https://ast-grep.github.io/).
|
|
6
|
+
|
|
7
|
+
Provides structural code searching and outline inspection capabilities for coding agents, dramatically outperforming traditional regular expression search (`grep`) in token efficiency and semantic accuracy.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **`ast_search`**: Search code structures using AST patterns and meta-variables (`$NAME`, `$$$`).
|
|
14
|
+
- Finds structural patterns across multiple lines regardless of formatting or whitespace.
|
|
15
|
+
- Supports node kind queries (e.g. `function_declaration`, `interface_declaration`, `class_declaration`).
|
|
16
|
+
- Supports strictness modes (`smart`, `relaxed`, `ast`, `cst`).
|
|
17
|
+
- Supports file path and glob filtering.
|
|
18
|
+
- **`ast_outline`**: Extract an ultra-compact structural outline (functions, classes, types, interfaces, exports) from a source file.
|
|
19
|
+
- Saves 80%–95% tokens compared to reading the entire file.
|
|
20
|
+
- Supports depth control (top-level vs. nested methods).
|
|
21
|
+
- **Zero Shell Quoting Issues**: Invoked safely via child processes with JSON streaming — immune to Windows shell argument escaping and quoting pitfalls.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
This plugin requires `ast-grep` CLI installed on your machine:
|
|
28
|
+
|
|
29
|
+
- **npm**:
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g @ast-grep/cli
|
|
32
|
+
```
|
|
33
|
+
- **pip**:
|
|
34
|
+
```bash
|
|
35
|
+
pip install ast-grep-cli
|
|
36
|
+
```
|
|
37
|
+
- **cargo**:
|
|
38
|
+
```bash
|
|
39
|
+
cargo install ast-grep --locked
|
|
40
|
+
```
|
|
41
|
+
- **brew** (macOS/Linux):
|
|
42
|
+
```bash
|
|
43
|
+
brew install ast-grep
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Verify with:
|
|
47
|
+
```bash
|
|
48
|
+
ast-grep --version
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Installation in DSH
|
|
54
|
+
|
|
55
|
+
### Option 1: Via DSH Market (Recommended)
|
|
56
|
+
|
|
57
|
+
Once published and indexed, search for `dsh-tool-ast-grep` in **Settings → Plugin Market** and click **Install**.
|
|
58
|
+
|
|
59
|
+
Or run in terminal:
|
|
60
|
+
```bash
|
|
61
|
+
dsh plugin --profile web add dsh-tool-ast-grep
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Option 2: Mount in an Agent Preset
|
|
65
|
+
|
|
66
|
+
In your agent configuration (e.g. `~/.dsh/.agent-presets/your-agent/agent.cordis.yml`):
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
id: my-agent
|
|
70
|
+
name: My Agent
|
|
71
|
+
tools:
|
|
72
|
+
- id: tool-ast-grep
|
|
73
|
+
name: dsh-tool-ast-grep
|
|
74
|
+
config:
|
|
75
|
+
maxResults: 50
|
|
76
|
+
timeoutMs: 60000
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Tool API Overview
|
|
82
|
+
|
|
83
|
+
### 1. `ast_search`
|
|
84
|
+
|
|
85
|
+
| Parameter | Type | Description |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `pattern` | `string` | AST pattern, e.g. `function $NAME($$$) { $$$ }` or `$OBJ.push($ITEM)` |
|
|
88
|
+
| `kind` | `string` | AST node kind, e.g. `class_declaration`, `function_declaration` |
|
|
89
|
+
| `path` | `string` | Directory or file path to search in (defaults to workspace root) |
|
|
90
|
+
| `lang` | `string` | Language override (`ts`, `tsx`, `js`, `jsx`, `python`, `rust`, `go`, etc.) |
|
|
91
|
+
| `globs` | `string` | File glob pattern to include/exclude (e.g. `src/**/*.ts`) |
|
|
92
|
+
| `limit` | `integer` | Max matches to return (default: 50) |
|
|
93
|
+
|
|
94
|
+
### 2. `ast_outline`
|
|
95
|
+
|
|
96
|
+
| Parameter | Type | Description |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `file_path` | `string` | Path of the file to inspect |
|
|
99
|
+
| `lang` | `string` | Language override (auto-detected if omitted) |
|
|
100
|
+
| `depth` | `integer` | `1` for top-level only (default), `2` to include class methods |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Supported Languages
|
|
105
|
+
|
|
106
|
+
TypeScript (`ts`, `tsx`), JavaScript (`js`, `jsx`), Python (`py`), Rust (`rs`), Go (`go`), Java (`java`), C (`c`, `h`), C++ (`cpp`, `hpp`, `cc`).
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT © Jaylor Wang (Jaylor-Wang)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# dsh-tool-ast-grep
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
**专为 DeepSeek Harness (DSH) 打造的语法级 AST 代码检索与大纲提取插件**,基于 Rust 高性能语法解析工具 [ast-grep](https://ast-grep.github.io/) 驱动。
|
|
6
|
+
|
|
7
|
+
为 Coding Agent 提供精准的代码语法树(AST)检索能力与文件架构提取能力,在语义精度和 Token 消耗控制上彻底超越传统的纯文本正则搜索(`grep`)。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 核心特性
|
|
12
|
+
|
|
13
|
+
- **`ast_search`(AST 结构化检索)**:
|
|
14
|
+
- 使用 AST 模式语法与元变量(如 `$NAME` 匹配单节点,`$$$` 匹配任意多节点/参数)精准定位目标代码。
|
|
15
|
+
- 语法无关排版:不论是单行书写、换行还是空格格式差异,均能准确匹配。
|
|
16
|
+
- 支持节点类型(Kind)检索(如 `class_declaration`, `interface_declaration`, `method_definition`)。
|
|
17
|
+
- 支持严格度模式(`smart`, `relaxed`, `ast`, `cst`)及文件 Glob 过滤。
|
|
18
|
+
- **`ast_outline`(极速代码大纲提取)**:
|
|
19
|
+
- 一键提取源码文件的结构化大纲(顶层函数、类、接口、类型别名、导出语句)。
|
|
20
|
+
- 相比把整个长文件塞进大模型上下文,**可节省 80%–95% 的 Token 消耗**。
|
|
21
|
+
- 支持深度控制:默认只看顶层接口,可展开类内部方法与构造函数。
|
|
22
|
+
- **消灭 Shell 引号与转义地狱**:
|
|
23
|
+
- 采用 Node.js 原生参数列表调用,彻底规避 Windows Shell 下特殊字符(如 `$VAR` 被吞、引号被截断)引发的运行报错。
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 前置要求
|
|
28
|
+
|
|
29
|
+
使用本插件需要在系统中安装 `ast-grep` 命令行工具(二选一即可):
|
|
30
|
+
|
|
31
|
+
- **通过 npm 全局安装**:
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g @ast-grep/cli
|
|
34
|
+
```
|
|
35
|
+
- **通过 Python pip 安装**:
|
|
36
|
+
```bash
|
|
37
|
+
pip install ast-grep-cli
|
|
38
|
+
```
|
|
39
|
+
- **通过 Cargo 安装 (Rust)**:
|
|
40
|
+
```bash
|
|
41
|
+
cargo install ast-grep --locked
|
|
42
|
+
```
|
|
43
|
+
- **macOS / Homebrew**:
|
|
44
|
+
```bash
|
|
45
|
+
brew install ast-grep
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
验证安装:
|
|
49
|
+
```bash
|
|
50
|
+
ast-grep --version
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 在 DeepSeek Harness 中安装
|
|
56
|
+
|
|
57
|
+
### 方式 1:通过插件市场一键安装(推荐)
|
|
58
|
+
|
|
59
|
+
在 Web UI 导航至 **设置 → 插件市场**,搜索 `dsh-tool-ast-grep`,点击安装即可。
|
|
60
|
+
|
|
61
|
+
或在终端中运行:
|
|
62
|
+
```bash
|
|
63
|
+
dsh plugin --profile web add dsh-tool-ast-grep
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 方式 2:在智能体预设(Preset)中引入
|
|
67
|
+
|
|
68
|
+
在你的预设文件(如 `~/.dsh/.agent-presets/my-agent/agent.cordis.yml`)中挂载:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
id: my-agent
|
|
72
|
+
name: My Agent
|
|
73
|
+
tools:
|
|
74
|
+
- id: tool-ast-grep
|
|
75
|
+
name: dsh-tool-ast-grep
|
|
76
|
+
config:
|
|
77
|
+
maxResults: 50
|
|
78
|
+
timeoutMs: 60000
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 工具参数说明
|
|
84
|
+
|
|
85
|
+
### 1. `ast_search`
|
|
86
|
+
|
|
87
|
+
| 参数 | 类型 | 说明 |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `pattern` | `string` | AST 匹配模式,如 `function $NAME($$$) { $$$ }` 或 `$OBJ.push($ITEM)` |
|
|
90
|
+
| `kind` | `string` | AST 语法节点类型,如 `class_declaration`, `function_declaration` |
|
|
91
|
+
| `path` | `string` | 搜索的目标目录或文件路径(默认为工作区根目录) |
|
|
92
|
+
| `lang` | `string` | 指定语言(`ts`, `tsx`, `js`, `jsx`, `python`, `rust`, `go` 等) |
|
|
93
|
+
| `globs` | `string` | 文件过滤规则(如 `src/**/*.ts` 或 `!**/*.test.ts`) |
|
|
94
|
+
| `limit` | `integer` | 最大返回条数(默认 50) |
|
|
95
|
+
|
|
96
|
+
### 2. `ast_outline`
|
|
97
|
+
|
|
98
|
+
| 参数 | 类型 | 说明 |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `file_path` | `string` | 目标源码文件路径(相对路径或绝对路径) |
|
|
101
|
+
| `lang` | `string` | 指定语言(如省略则根据文件后缀自动推断) |
|
|
102
|
+
| `depth` | `integer` | 大纲深度:`1` 仅展示顶层定义(默认),`2` 展开类方法与构造器 |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 支持的语言
|
|
107
|
+
|
|
108
|
+
TypeScript (`ts`, `tsx`), JavaScript (`js`, `jsx`), Python (`py`), Rust (`rs`), Go (`go`), Java (`java`), C (`c`, `h`), C++ (`cpp`, `hpp`, `cc`)。
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 许可证
|
|
113
|
+
|
|
114
|
+
MIT © Jaylor Wang (Jaylor-Wang)
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
import { execFile } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export const name = "dsh-tool-ast-grep";
|
|
11
|
+
export const Config = z.object({
|
|
12
|
+
maxResults: z.number().default(50).description("Maximum search results to return"),
|
|
13
|
+
timeoutMs: z.number().default(60000).description("Execution timeout in milliseconds"),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const inject = ["tools"];
|
|
17
|
+
|
|
18
|
+
const OUTLINE_KINDS = {
|
|
19
|
+
ts: {
|
|
20
|
+
topLevel:
|
|
21
|
+
"function_declaration, generator_function_declaration, class_declaration, abstract_class_declaration, interface_declaration, type_alias_declaration, enum_declaration, export_statement, lexical_declaration",
|
|
22
|
+
nested: "method_definition, public_field_definition",
|
|
23
|
+
},
|
|
24
|
+
tsx: {
|
|
25
|
+
topLevel:
|
|
26
|
+
"function_declaration, generator_function_declaration, class_declaration, abstract_class_declaration, interface_declaration, type_alias_declaration, enum_declaration, export_statement, lexical_declaration",
|
|
27
|
+
nested: "method_definition, public_field_definition",
|
|
28
|
+
},
|
|
29
|
+
js: {
|
|
30
|
+
topLevel:
|
|
31
|
+
"function_declaration, generator_function_declaration, class_declaration, export_statement, lexical_declaration, variable_declaration",
|
|
32
|
+
nested: "method_definition",
|
|
33
|
+
},
|
|
34
|
+
jsx: {
|
|
35
|
+
topLevel:
|
|
36
|
+
"function_declaration, generator_function_declaration, class_declaration, export_statement, lexical_declaration, variable_declaration",
|
|
37
|
+
nested: "method_definition",
|
|
38
|
+
},
|
|
39
|
+
python: {
|
|
40
|
+
topLevel:
|
|
41
|
+
"function_definition, async_function_definition, class_definition, decorated_definition",
|
|
42
|
+
nested: "function_definition, async_function_definition",
|
|
43
|
+
},
|
|
44
|
+
rust: {
|
|
45
|
+
topLevel:
|
|
46
|
+
"function_item, struct_item, enum_item, trait_item, impl_item, type_item, mod_item, macro_definition",
|
|
47
|
+
nested: "function_item",
|
|
48
|
+
},
|
|
49
|
+
go: {
|
|
50
|
+
topLevel: "function_declaration, method_declaration, type_declaration",
|
|
51
|
+
nested: "method_declaration",
|
|
52
|
+
},
|
|
53
|
+
java: {
|
|
54
|
+
topLevel: "class_declaration, interface_declaration, enum_declaration",
|
|
55
|
+
nested: "method_declaration, constructor_declaration",
|
|
56
|
+
},
|
|
57
|
+
c: {
|
|
58
|
+
topLevel: "function_definition, struct_specifier, enum_specifier, type_definition",
|
|
59
|
+
nested: "field_declaration",
|
|
60
|
+
},
|
|
61
|
+
cpp: {
|
|
62
|
+
topLevel: "function_definition, class_specifier, struct_specifier, enum_specifier, type_definition",
|
|
63
|
+
nested: "function_definition, field_declaration",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function detectLang(filePath) {
|
|
68
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
69
|
+
switch (ext) {
|
|
70
|
+
case ".ts":
|
|
71
|
+
case ".mts":
|
|
72
|
+
case ".cts":
|
|
73
|
+
return "ts";
|
|
74
|
+
case ".tsx":
|
|
75
|
+
return "tsx";
|
|
76
|
+
case ".js":
|
|
77
|
+
case ".mjs":
|
|
78
|
+
case ".cjs":
|
|
79
|
+
return "js";
|
|
80
|
+
case ".jsx":
|
|
81
|
+
return "jsx";
|
|
82
|
+
case ".py":
|
|
83
|
+
case ".pyi":
|
|
84
|
+
return "python";
|
|
85
|
+
case ".rs":
|
|
86
|
+
return "rust";
|
|
87
|
+
case ".go":
|
|
88
|
+
return "go";
|
|
89
|
+
case ".java":
|
|
90
|
+
return "java";
|
|
91
|
+
case ".c":
|
|
92
|
+
case ".h":
|
|
93
|
+
return "c";
|
|
94
|
+
case ".cpp":
|
|
95
|
+
case ".hpp":
|
|
96
|
+
case ".cc":
|
|
97
|
+
case ".cxx":
|
|
98
|
+
return "cpp";
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function truncateSnippet(text, maxLines = 8) {
|
|
105
|
+
const lines = text.split("\n");
|
|
106
|
+
if (lines.length <= maxLines) {
|
|
107
|
+
return lines.map((l) => " " + l).join("\n");
|
|
108
|
+
}
|
|
109
|
+
const head = lines.slice(0, 4).map((l) => " " + l);
|
|
110
|
+
const tail = lines.slice(-2).map((l) => " " + l);
|
|
111
|
+
const omitted = lines.length - 6;
|
|
112
|
+
return [...head, ` ... (${omitted} lines omitted) ...`, ...tail].join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let cachedAstGrepBinary = null;
|
|
116
|
+
|
|
117
|
+
function resolveBinary() {
|
|
118
|
+
if (cachedAstGrepBinary) return cachedAstGrepBinary;
|
|
119
|
+
|
|
120
|
+
// 1. Check direct binary in PATH
|
|
121
|
+
const binaryName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
122
|
+
|
|
123
|
+
// 2. Check common paths on Windows
|
|
124
|
+
if (process.platform === "win32") {
|
|
125
|
+
const userProfile = process.env.USERPROFILE || "";
|
|
126
|
+
const localAppData = process.env.LOCALAPPDATA || "";
|
|
127
|
+
const candidates = [
|
|
128
|
+
binaryName,
|
|
129
|
+
path.join(userProfile, "AppData", "Local", "Programs", "Python", "Python313", "Scripts", "ast-grep.exe"),
|
|
130
|
+
path.join(userProfile, "AppData", "Local", "Programs", "Python", "Python312", "Scripts", "ast-grep.exe"),
|
|
131
|
+
path.join(userProfile, "AppData", "Local", "Programs", "Python", "Python311", "Scripts", "ast-grep.exe"),
|
|
132
|
+
path.join(localAppData, "Programs", "Python", "Python313", "Scripts", "ast-grep.exe"),
|
|
133
|
+
path.join(userProfile, "AppData", "Roaming", "npm", "ast-grep.cmd"),
|
|
134
|
+
path.join(userProfile, "AppData", "Roaming", "npm", "ast-grep.exe"),
|
|
135
|
+
path.join(userProfile, ".cargo", "bin", "ast-grep.exe"),
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
for (const cand of candidates) {
|
|
139
|
+
if (cand === binaryName) continue;
|
|
140
|
+
if (fs.existsSync(cand)) {
|
|
141
|
+
cachedAstGrepBinary = cand;
|
|
142
|
+
return cand;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
cachedAstGrepBinary = binaryName;
|
|
148
|
+
return binaryName;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function runAstGrep(args, cwd, signal, timeoutMs = 60000) {
|
|
152
|
+
const binary = resolveBinary();
|
|
153
|
+
try {
|
|
154
|
+
const res = await execFileAsync(binary, args, {
|
|
155
|
+
cwd,
|
|
156
|
+
signal,
|
|
157
|
+
timeout: timeoutMs,
|
|
158
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
159
|
+
windowsHide: true,
|
|
160
|
+
});
|
|
161
|
+
return res.stdout || "[]";
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (err.code === 1 && typeof err.stdout === "string" && err.stdout.trim().startsWith("[")) {
|
|
164
|
+
return err.stdout;
|
|
165
|
+
}
|
|
166
|
+
if (err.name === "AbortError" || signal?.aborted) {
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
if (err.code === "ENOENT" || err.message?.includes("ENOENT")) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`ast-grep binary was not found in PATH or standard directories.\n` +
|
|
172
|
+
`Please install ast-grep to use this plugin:\n` +
|
|
173
|
+
`- npm: npm install -g @ast-grep/cli\n` +
|
|
174
|
+
`- pip: pip install ast-grep-cli\n` +
|
|
175
|
+
`- cargo: cargo install ast-grep --locked\n` +
|
|
176
|
+
`- brew: brew install ast-grep (macOS/Linux)`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const errText = err.stderr?.trim() || err.message;
|
|
180
|
+
throw new Error(`ast-grep execution failed: ${errText}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function apply(ctx, config = {}) {
|
|
185
|
+
const maxResultsDefault = config.maxResults || 50;
|
|
186
|
+
const timeoutMsDefault = config.timeoutMs || 60000;
|
|
187
|
+
|
|
188
|
+
const systemPrompt = ctx.get("systemPrompt");
|
|
189
|
+
if (systemPrompt) {
|
|
190
|
+
try {
|
|
191
|
+
systemPrompt.section({
|
|
192
|
+
name: "tool:ast-grep",
|
|
193
|
+
order: systemPrompt.getSectionOrder?.("TOOLS_SDK") ?? 5000,
|
|
194
|
+
text: "When searching for code structures (e.g. function/class/interface definitions, API usages) or inspecting file outline, prefer `ast_search` and `ast_outline` over plain-text grep or reading entire files. Use $VAR for single nodes and $$$ for multi-node wildcards.",
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
// safe fallback if systemPrompt not mounted
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 1. ast_search
|
|
202
|
+
ctx.tools.register(
|
|
203
|
+
defineTool({
|
|
204
|
+
name: "ast_search",
|
|
205
|
+
description:
|
|
206
|
+
"Perform structural AST code search across the workspace or a directory using ast-grep. Matches code by syntax tree patterns instead of regex strings. Use $VAR for single node meta-variables (e.g. $NAME) and $$$ for multiple nodes/statements/arguments. Example patterns: 'function $NAME($$$) { $$$ }', 'interface $NAME { $$$ }', 'class $NAME extends $BASE { $$$ }', '$OBJ.push($ITEM)'.",
|
|
207
|
+
parameters: {
|
|
208
|
+
pattern: {
|
|
209
|
+
type: "string",
|
|
210
|
+
description:
|
|
211
|
+
"AST pattern to match. Use $VAR for single node meta-variables and $$$ for wildcard nodes. Example: 'function $NAME($$$) { $$$ }'. Either pattern or kind must be provided.",
|
|
212
|
+
},
|
|
213
|
+
kind: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description:
|
|
216
|
+
"AST node kind to match (e.g. 'function_declaration', 'class_declaration', 'interface_declaration', 'method_definition').",
|
|
217
|
+
},
|
|
218
|
+
selector: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description:
|
|
221
|
+
"Sub-syntax node kind selector for the pattern (see ast-grep --selector).",
|
|
222
|
+
},
|
|
223
|
+
path: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Target directory or file path. Defaults to workspace root.",
|
|
226
|
+
},
|
|
227
|
+
lang: {
|
|
228
|
+
type: "string",
|
|
229
|
+
description:
|
|
230
|
+
"Programming language (ts, tsx, js, jsx, rust, python, go, c, cpp, java). Auto-detected if omitted.",
|
|
231
|
+
},
|
|
232
|
+
globs: {
|
|
233
|
+
type: "string",
|
|
234
|
+
description: 'File glob pattern to filter, e.g. "src/**/*.ts" or "!**/*.test.ts".',
|
|
235
|
+
},
|
|
236
|
+
strictness: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description:
|
|
239
|
+
'Strictness mode: "smart" (default), "cst", "ast", "relaxed", "signature", "template".',
|
|
240
|
+
},
|
|
241
|
+
limit: {
|
|
242
|
+
type: "integer",
|
|
243
|
+
description: "Maximum number of matches to return (default 50).",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
output: {
|
|
247
|
+
schema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
additionalProperties: false,
|
|
250
|
+
properties: {
|
|
251
|
+
text: { type: "string", required: true },
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
render: (_args, value) => [{ type: "text", text: value.text }],
|
|
255
|
+
},
|
|
256
|
+
async execute(args, exec) {
|
|
257
|
+
if (!args.pattern && !args.kind) {
|
|
258
|
+
throw new Error("ast_search requires either 'pattern' or 'kind' to be specified.");
|
|
259
|
+
}
|
|
260
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
261
|
+
const limit = args.limit || maxResultsDefault;
|
|
262
|
+
const targetPath = args.path ? path.resolve(cwd, args.path) : cwd;
|
|
263
|
+
|
|
264
|
+
const cmdArgs = ["run", "--json=compact"];
|
|
265
|
+
if (args.pattern) cmdArgs.push("--pattern", args.pattern);
|
|
266
|
+
if (args.kind) cmdArgs.push("--kind", args.kind);
|
|
267
|
+
if (args.selector) cmdArgs.push("--selector", args.selector);
|
|
268
|
+
if (args.lang) cmdArgs.push("--lang", args.lang);
|
|
269
|
+
if (args.globs) cmdArgs.push("--globs", args.globs);
|
|
270
|
+
if (args.strictness) cmdArgs.push("--strictness", args.strictness);
|
|
271
|
+
cmdArgs.push(targetPath);
|
|
272
|
+
|
|
273
|
+
const rawJson = await runAstGrep(cmdArgs, cwd, exec.signal, timeoutMsDefault);
|
|
274
|
+
let matches = [];
|
|
275
|
+
try {
|
|
276
|
+
matches = JSON.parse(rawJson);
|
|
277
|
+
} catch {
|
|
278
|
+
matches = [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (matches.length === 0) {
|
|
282
|
+
const query = args.pattern ? `pattern '${args.pattern}'` : `kind '${args.kind}'`;
|
|
283
|
+
return { text: `No AST matches found for ${query}` };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const total = matches.length;
|
|
287
|
+
const sliced = matches.slice(0, limit);
|
|
288
|
+
|
|
289
|
+
// Group by file
|
|
290
|
+
const byFile = new Map();
|
|
291
|
+
for (const m of sliced) {
|
|
292
|
+
const rel = path.relative(cwd, m.file).replace(/\\/g, "/");
|
|
293
|
+
const list = byFile.get(rel) || [];
|
|
294
|
+
list.push(m);
|
|
295
|
+
byFile.set(rel, list);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const sections = [];
|
|
299
|
+
for (const [filePath, fileMatches] of byFile) {
|
|
300
|
+
const lines = [`### ${filePath}`];
|
|
301
|
+
for (const m of fileMatches) {
|
|
302
|
+
const startLine = m.range.start.line + 1;
|
|
303
|
+
const endLine = m.range.end.line + 1;
|
|
304
|
+
const lineStr = startLine === endLine ? `Line ${startLine}` : `Lines ${startLine}-${endLine}`;
|
|
305
|
+
|
|
306
|
+
// Meta variables summary
|
|
307
|
+
let metaStr = "";
|
|
308
|
+
if (m.metaVariables?.single) {
|
|
309
|
+
const vars = Object.entries(m.metaVariables.single)
|
|
310
|
+
.map(([k, v]) => `$${k} = ${JSON.stringify(v.text)}`)
|
|
311
|
+
.join(", ");
|
|
312
|
+
if (vars) metaStr = ` [${vars}]`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
lines.push(`- **${lineStr}**${metaStr}:\n${truncateSnippet(m.text)}`);
|
|
316
|
+
}
|
|
317
|
+
sections.push(lines.join("\n"));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let output = sections.join("\n\n");
|
|
321
|
+
if (total > limit) {
|
|
322
|
+
output += `\n\n(Showing ${limit} of ${total} matches. Narrow your search with \`path\` or \`globs\` if needed.)`;
|
|
323
|
+
}
|
|
324
|
+
return { text: output };
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// 2. ast_outline
|
|
330
|
+
ctx.tools.register(
|
|
331
|
+
defineTool({
|
|
332
|
+
name: "ast_outline",
|
|
333
|
+
description:
|
|
334
|
+
"Extract a concise structural outline (functions, classes, interfaces, types, exports) from a source file using AST. Much faster and saves significant tokens compared to reading the entire file.",
|
|
335
|
+
parameters: {
|
|
336
|
+
file_path: {
|
|
337
|
+
type: "string",
|
|
338
|
+
required: true,
|
|
339
|
+
description: "Path of the source file to inspect (relative to workspace or absolute).",
|
|
340
|
+
},
|
|
341
|
+
lang: {
|
|
342
|
+
type: "string",
|
|
343
|
+
description: "Language override (ts, tsx, js, jsx, rust, python, go, java, c, cpp).",
|
|
344
|
+
},
|
|
345
|
+
depth: {
|
|
346
|
+
type: "integer",
|
|
347
|
+
description:
|
|
348
|
+
"Outline depth: 1 for top-level definitions only (default), 2 to include class methods and inner declarations.",
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
output: {
|
|
352
|
+
schema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
additionalProperties: false,
|
|
355
|
+
properties: {
|
|
356
|
+
text: { type: "string", required: true },
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
render: (_args, value) => [{ type: "text", text: value.text }],
|
|
360
|
+
},
|
|
361
|
+
async execute(args, exec) {
|
|
362
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
363
|
+
const filePath = path.isAbsolute(args.file_path)
|
|
364
|
+
? args.file_path
|
|
365
|
+
: path.resolve(cwd, args.file_path);
|
|
366
|
+
|
|
367
|
+
if (!fs.existsSync(filePath)) {
|
|
368
|
+
throw new Error(`File not found: ${args.file_path}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const lang = args.lang || detectLang(filePath);
|
|
372
|
+
if (!lang) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Unable to determine language for ${args.file_path}. Please provide the 'lang' parameter.`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const langConfig = OUTLINE_KINDS[lang];
|
|
379
|
+
if (!langConfig) {
|
|
380
|
+
throw new Error(`Unsupported outline language: ${lang}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const depth = args.depth || 1;
|
|
384
|
+
let kinds = langConfig.topLevel;
|
|
385
|
+
if (depth >= 2 && langConfig.nested) {
|
|
386
|
+
kinds = `${langConfig.topLevel}, ${langConfig.nested}`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const cmdArgs = ["run", "--kind", kinds, "--lang", lang, "--json=compact", filePath];
|
|
390
|
+
const rawJson = await runAstGrep(cmdArgs, cwd, exec.signal, timeoutMsDefault);
|
|
391
|
+
|
|
392
|
+
let items = [];
|
|
393
|
+
try {
|
|
394
|
+
items = JSON.parse(rawJson);
|
|
395
|
+
} catch {
|
|
396
|
+
items = [];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (items.length === 0) {
|
|
400
|
+
return { text: `No outline symbols found in ${args.file_path}` };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
items.sort(
|
|
404
|
+
(a, b) =>
|
|
405
|
+
a.range.start.line - b.range.start.line || a.range.start.column - b.range.start.column
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const lines = [`Outline for \`${path.relative(cwd, filePath).replace(/\\/g, "/")}\` (${lang}):`];
|
|
409
|
+
const seenLines = new Set();
|
|
410
|
+
|
|
411
|
+
for (const item of items) {
|
|
412
|
+
const col = item.range.start.column;
|
|
413
|
+
if (depth === 1 && col > 2) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const startLine = item.range.start.line + 1;
|
|
418
|
+
const endLine = item.range.end.line + 1;
|
|
419
|
+
|
|
420
|
+
if (seenLines.has(startLine)) continue;
|
|
421
|
+
seenLines.add(startLine);
|
|
422
|
+
|
|
423
|
+
const rawFirstLine = item.text.split("\n")[0].trim();
|
|
424
|
+
const cleanSig = rawFirstLine.replace(/\{$/, "").replace(/:$/, "").trim();
|
|
425
|
+
|
|
426
|
+
const indent = col > 2 ? " └─ " : "- ";
|
|
427
|
+
const lineRef = startLine === endLine ? `Line ${startLine}` : `Lines ${startLine}-${endLine}`;
|
|
428
|
+
lines.push(`${indent}**${lineRef}**: \`${cleanSig}\``);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (lines.length === 1) {
|
|
432
|
+
return { text: `No top-level outline symbols matched in ${args.file_path}` };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { text: lines.join("\n") };
|
|
436
|
+
},
|
|
437
|
+
})
|
|
438
|
+
);
|
|
439
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-tool-ast-grep",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AST-based structural code search and syntax outline tool for DeepSeek Harness powered by ast-grep",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"dsh",
|
|
10
|
+
"dsh-plugin",
|
|
11
|
+
"cordis",
|
|
12
|
+
"ast-grep",
|
|
13
|
+
"ast",
|
|
14
|
+
"code-search",
|
|
15
|
+
"outline"
|
|
16
|
+
],
|
|
17
|
+
"author": "Jaylor-Wang",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://github.com/Jaylor-Wang/dsh-tool-ast-grep#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Jaylor-Wang/dsh-tool-ast-grep.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Jaylor-Wang/dsh-tool-ast-grep/issues"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
37
|
+
"@deepseek-ai/schemastery": "*"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"index.js",
|
|
41
|
+
"cordis.patch.yml",
|
|
42
|
+
"README.md",
|
|
43
|
+
"README.zh.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
]
|
|
46
|
+
}
|