dsh-graft-plugin 0.2.1 → 0.3.1
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 -21
- package/README.md +46 -6
- package/README.zh-CN.md +218 -0
- package/assets/icon.png +0 -0
- package/package.json +16 -9
- package/scripts/test-autosync.mjs +66 -1
- package/scripts/test-guidance.mjs +160 -0
- package/scripts/test-harness.mjs +184 -2
- package/src/auto-sync.js +34 -2
- package/src/client.js +436 -67
- package/src/graft-tools.js +11 -0
- package/src/index.js +190 -36
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adithya N Raj
|
|
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
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/banner-dark.png">
|
|
3
|
+
<img alt="graft" src="assets/banner-light.png" width="1012">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
6
|
+
<br>
|
|
7
|
+
|
|
1
8
|
# dsh-graft-plugin
|
|
2
9
|
|
|
10
|
+
English | [中文](README.zh-CN.md)
|
|
11
|
+
|
|
3
12
|
      
|
|
4
13
|
|
|
5
14
|
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that puts [graft](https://github.com/trailhq/Graft) — a prebuilt graph of every symbol, its `file:line` span, and who calls what — in front of both you and the model.
|
|
6
15
|
|
|
7
16
|
It does four things:
|
|
8
17
|
|
|
9
|
-
- **A chip in the composer** naming the graft index the current session is actually serving, whether it still matches the code, and how many tokens graft has saved in that repo.
|
|
10
|
-
- **Five tools for the model** — `graft_ask`, `graft_grep`, `graft_callers`, `graft_skeleton`, `graft_map` — resolved per session, so one dsh can serve several workspaces at once.
|
|
18
|
+
- **A chip in the composer** naming the graft index the current session is actually serving, whether it still matches the code, and how many tokens graft has saved in that repo. Its card carries a **Build** button — rebuild the index, or create the first one.
|
|
19
|
+
- **Five tools for the model** — `graft_ask`, `graft_grep`, `graft_callers`, `graft_skeleton`, `graft_map` — resolved per session, so one dsh can serve several workspaces at once, git worktrees included, each with its own graph.
|
|
11
20
|
- **`/graft`** to build or rebuild the index, and **auto-rebuild** after the model edits files.
|
|
12
21
|
- **A `graft viz` button** that opens the graph in dsh's built-in right sidebar (0.1.5+), in [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) if you still run it, and in a browser tab as the last resort.
|
|
13
22
|
|
|
@@ -48,11 +57,32 @@ cd <your repo>
|
|
|
48
57
|
graft build
|
|
49
58
|
```
|
|
50
59
|
|
|
60
|
+
### In the desktop app
|
|
61
|
+
|
|
62
|
+
The desktop dsh installs plugins from its own **Plugins** settings — no CLI, no profile directory:
|
|
63
|
+
|
|
64
|
+
1. Open **Plugins** (settings, left sidebar) and click **Add plugin**.
|
|
65
|
+
2. Paste the GitHub repository address into the field (the package name `dsh-graft-plugin` works too):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
https://github.com/adithyanraj03/dsh-graft-plugin
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
3. Click **Install**.
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
The dialog switches to the **Installed** confirmation, and the plugin appears under **Installed** at the bottom of the settings page:
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
> The dialog notes that installed plugins **do not update automatically yet** — to upgrade, uninstall and install again. The graft CLI requirement (`npm install -g @nanonets/graft`) is the same.
|
|
80
|
+
|
|
51
81
|
### The sidebar tab
|
|
52
82
|
|
|
53
83
|
On dsh `0.1.5` and later the button opens the graph in **dsh's own right sidebar** — a tab titled Graft, also listed on the sidebar's guide page as "The graft graph for this workspace" — and a second click focuses the tab that is already open. No install needed for that path: the registration is soft, so an older dsh without the right sidebar falls through to `dsh-better-sidebar` when it is mounted, and to a browser tab when neither is there.
|
|
54
84
|
|
|
55
|
-
If your profile still runs `dsh-better-sidebar`, that path is tried **first** and behaves exactly as before.
|
|
85
|
+
If your profile still runs `dsh-better-sidebar`, that path is tried **first** and behaves exactly as before. From 0.3.0 it is an **optional peer dependency** — dsh no longer downloads it for you, so add it yourself when you want it (on dsh `0.1.5-rc.2` or later it cannot mount anyway, so most profiles should skip this):
|
|
56
86
|
|
|
57
87
|
```sh
|
|
58
88
|
dsh plugin --profile web add dsh-better-sidebar
|
|
@@ -80,6 +110,14 @@ Hovering (or clicking) opens a card with the resolved root, node and edge counts
|
|
|
80
110
|
|
|
81
111
|

|
|
82
112
|
|
|
113
|
+
The card's header carries a **Build** button beside the status pill. In a repo that already has an index it runs the same rebuild the auto-rebuild runs — graft's own sync — and holds "Building…" until the pill reads *in sync*, giving up after two minutes and showing the reason. In a workspace with no index yet, Build first asks in dsh's own dialog ("Build graft index?" — Cancel / Build index; Cancel, Escape or clicking outside builds nothing), then runs a plain offline `graft build`, which writes a `graft/` directory there.
|
|
114
|
+
|
|
115
|
+
While a `graft_*` tool is running, the chip's dot blinks violet.
|
|
116
|
+
|
|
117
|
+
Chip, card and viz tab are painted with dsh's own `--dsw-*` theme variables, so they follow the active theme — including theme plugins.
|
|
118
|
+
|
|
119
|
+
A linked git worktree gets its own graph, never the main checkout's: the card and the viz bar name the main checkout it belongs to (`git worktree of <repo>`). With no graph yet, `/graft` in the worktree seeds the first one from it.
|
|
120
|
+
|
|
83
121
|
The root is resolved from **the session's own workspace**, not from wherever dsh was launched. That is the whole reason this exists rather than the `graft mcp` server: `graft mcp` resolves its repo once, from its own working directory at startup, and nothing can re-point it afterwards. Launched from a home directory it answers *"no graph found"* for every workspace you open.
|
|
84
122
|
|
|
85
123
|
### The visualiser
|
|
@@ -125,20 +163,22 @@ All optional, all on the plugin's row in your profile's `cordis.patch.yml`:
|
|
|
125
163
|
cwd: 'C:/path/to/repo' # pin to one repo instead of resolving per session
|
|
126
164
|
tools: false # drop the graft_* tools, keep the chip
|
|
127
165
|
command: false # drop /graft
|
|
128
|
-
promptSection: false # stop telling the model to
|
|
129
|
-
autoBuild: false # stop rebuilding after edits
|
|
166
|
+
promptSection: false # stop telling the model to use graft
|
|
167
|
+
autoBuild: false # stop rebuilding after edits (the card's Build button still works)
|
|
130
168
|
```
|
|
131
169
|
|
|
132
170
|
## Requirements
|
|
133
171
|
|
|
134
172
|
| dsh | status |
|
|
135
173
|
|---|---|
|
|
174
|
+
| `0.2.1-rc.2` | test suites pass against the `0.2.1-rc.2` packages — chip, tools, `/graft`, auto-rebuild and the model-facing guidance; the dual-shape codecs load on both 0.1 and 0.2 hosts |
|
|
175
|
+
| `0.1.7-rc.2` | tested on a live profile (0.3.0) — chip, tools, `/graft`, auto-rebuild and the model-facing guidance all verified |
|
|
136
176
|
| `0.1.2-rc.1` – `0.1.0-rc.7` | tested, full feature set — the `graft viz` tab via `dsh-better-sidebar` |
|
|
137
177
|
| `0.1.5-rc.2` / `0.1.5` | tested — chip, tools, `/graft` and auto-rebuild all work; on `0.1.5` the `graft viz` button opens the graph in the built-in right sidebar |
|
|
138
178
|
| `0.2.0-rc.2` | needs plugin `0.2.1`+ — earlier releases fail to activate (see Troubleshooting) |
|
|
139
179
|
|
|
140
180
|
- Node 22+
|
|
141
|
-
- the `graft` CLI on PATH (`npm install -g @nanonets/graft`)
|
|
181
|
+
- the `graft` CLI on PATH (`npm install -g @nanonets/graft`). A bun global install (`bun add -g @nanonets/graft`) is detected too, via `$BUN_INSTALL` or `~/.bun`.
|
|
142
182
|
|
|
143
183
|
The client bundle requires only `react`, which is why it survives the dsh `0.1.5-rc.2` client-package removals that broke several neighbouring plugins.
|
|
144
184
|
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/banner-dark.png">
|
|
3
|
+
<img alt="graft" src="assets/banner-light.png" width="1012">
|
|
4
|
+
</picture>
|
|
5
|
+
|
|
6
|
+
<br>
|
|
7
|
+
|
|
8
|
+
# dsh-graft-plugin
|
|
9
|
+
|
|
10
|
+
[English](README.md) | 中文
|
|
11
|
+
|
|
12
|
+
      
|
|
13
|
+
|
|
14
|
+
一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件,把 [graft](https://github.com/trailhq/Graft)——一张预先构建的符号图(每个符号的 `file:line` 位置、谁调用了谁)——同时摆到你和模型面前。
|
|
15
|
+
|
|
16
|
+
它做四件事:
|
|
17
|
+
|
|
18
|
+
- **输入框里的徽章(chip)**:说出当前会话实际在用哪个 graft 索引、它是否还与代码一致、这个仓库里 graft 已经省了多少 token。徽章的卡片上有一个 **Build** 按钮——重建索引,或者创建第一个索引。
|
|
19
|
+
- **给模型五个工具** — `graft_ask`、`graft_grep`、`graft_callers`、`graft_skeleton`、`graft_map`——按会话解析,所以一个 dsh 可以同时服务多个工作区,git worktree 也不例外,每个都有自己的图。
|
|
20
|
+
- **`/graft`** 构建或重建索引,模型改完文件后**自动重建**。
|
|
21
|
+
- **`graft viz` 按钮**:在 dsh 内置的右侧边栏(0.1.5+)打开图;如果你还在用 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar),就在那里打开;两者都没有时退到浏览器标签页。
|
|
22
|
+
|
|
23
|
+
## 安装
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
dsh plugin --profile web add dsh-graft-plugin
|
|
27
|
+
dsh --profile web
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**pnpm 10 及以后会默认拦截未批准包的 `postinstall` 脚本**,而只要安装以非零退出,dsh 就会跳过写入插件的 bundle 行——依赖装上了,插件却隐形,dsh 还启动得干干净净,没有任何可搜索的报错。如果 `add` 以 `ERR_PNPM_IGNORED_BUILDS` 结尾,先批准再重跑:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
cd ~/.dsh/profiles/web && pnpm approve-builds # 勾选 dsh-graft-plugin
|
|
34
|
+
dsh plugin --profile web add dsh-graft-plugin # 重跑,让 bundle 行写入
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
确认它挂载了——插件必须同时出现在 profile 的 `package.json` 的 `dependencies` 和 `dsh.profile.bundles` 里:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
cd ~/.dsh/profiles/web && node -e "const p=require('./package.json');
|
|
41
|
+
console.log(p.dependencies['dsh-graft-plugin'],
|
|
42
|
+
p.dsh.profile.bundles.includes('dsh-graft-plugin'))"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
这里的每个工具都要调用 `graft` CLI,所以 PATH 上必须有 `@nanonets/graft`:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npm install -g @nanonets/graft
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`postinstall` 会在**你的包管理器愿意运行生命周期脚本时**替你装好——pnpm 10+ 默认拦截它们,所以在 pnpm 下请把上面这条命令当作必需步骤,而不是后备。若全局 npm 前缀需要提权,postinstall 会说明并打印那条需要手动执行的命令。
|
|
52
|
+
|
|
53
|
+
然后给仓库建一次索引:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
cd <your repo>
|
|
57
|
+
graft build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 桌面应用
|
|
61
|
+
|
|
62
|
+
桌面版 dsh 从自己的 **Plugins** 设置页安装插件——不用 CLI,不用 profile 目录:
|
|
63
|
+
|
|
64
|
+
1. 打开 **Plugins**(设置页左侧栏),点 **Add plugin**。
|
|
65
|
+
2. 把 GitHub 仓库地址粘进输入框(包名 `dsh-graft-plugin` 也可以):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
https://github.com/adithyanraj03/dsh-graft-plugin
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
3. 点 **Install**。
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
对话框会切换到 **Installed** 确认页,插件同时出现在设置页底部的 **Installed** 列表里:
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
> 对话框也提示:已安装的插件**目前不会自动更新**——升级请先卸载再重装。graft CLI 的要求(`npm install -g @nanonets/graft`)不变。
|
|
80
|
+
|
|
81
|
+
### 侧边栏标签页
|
|
82
|
+
|
|
83
|
+
在 dsh `0.1.5` 及以后,这个按钮会在 **dsh 自己的右侧边栏**里打开图——一个名为 Graft 的标签页,同时列在边栏引导页里,说明是 "The graft graph for this workspace"——再次点击会聚焦已打开的标签页。这条路径不需要安装任何东西:注册是软性的,所以没有右侧边栏的旧版 dsh 会退到已挂载的 `dsh-better-sidebar`,两者都没有时退到浏览器标签页。
|
|
84
|
+
|
|
85
|
+
如果你的 profile 还在用 `dsh-better-sidebar`,会**优先**尝试那条路径,行为和以前完全一样。从 0.3.0 起它是**可选对等依赖(optional peer dependency)**——dsh 不再替你下载,想要就自己加(在 dsh `0.1.5-rc.2` 及以后它本来就挂不起来,多数 profile 可以跳过):
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
dsh plugin --profile web add dsh-better-sidebar
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> **兼容性。** `dsh-better-sidebar` 0.18.x 需要 `@deepseek-ai/dsh-client-ui-primitives`,而 dsh 在 `0.1.5-rc.2` 移除了它。在该版本及以后它挂不起来——那些 profile 正好由内置右侧边栏接管。本插件其余部分——徽章、工具、`/graft`、自动重建——都不受影响。
|
|
92
|
+
|
|
93
|
+
徽章、工具、`/graft` 和自动重建在完全没有侧边栏时也都工作;按钮只是在两个都没有时退到浏览器标签页。
|
|
94
|
+
|
|
95
|
+
> 这个插件**故意不**自己插入 `dsh-better-sidebar` 行。那个包自己的 bundle 行带一个守卫:当已有另一个启用的行挂载同一个包时,它会自我禁用。第二条不同 id 的行会让这个守卫变成循环判断,输掉的那一边是*你现有的边栏悄悄自我禁用*。一条显式命令比这个风险值钱。
|
|
96
|
+
|
|
97
|
+
## 你会得到什么
|
|
98
|
+
|
|
99
|
+
### 徽章
|
|
100
|
+
|
|
101
|
+
待在输入框里模型名的旁边:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
● graft · my-repo in sync
|
|
105
|
+
◐ graft · my-repo syncing
|
|
106
|
+
▲ graft · my-repo stale
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
悬停(或点击)打开一张卡片:解析出的根目录、节点与边数、新鲜度、累计省下的 token,以及 `graft viz` 按钮。
|
|
110
|
+
|
|
111
|
+

|
|
112
|
+
|
|
113
|
+
卡片头部在状态胶囊旁边有一个 **Build** 按钮。在已有索引的仓库里,它跑的就是自动重建用的那套同步——graft 自己的 sync——并停在 "Building…" 直到胶囊显示 *in sync*,两分钟后放弃并给出原因。在还没有索引的工作区里,Build 会先用 dsh 自己的对话框确认("Build graft index?" — Cancel / Build index;取消、Escape 或点框外都不会构建),然后跑一次纯离线的 `graft build`,在那里写出一个 `graft/` 目录。
|
|
114
|
+
|
|
115
|
+
`graft_*` 工具运行期间,徽章的圆点会闪紫色。
|
|
116
|
+
|
|
117
|
+
徽章、卡片和可视化标签页全部用 dsh 自己的 `--dsw-*` 主题变量绘制,因此跟随当前主题——包括主题插件。
|
|
118
|
+
|
|
119
|
+
链接的 git worktree 拥有自己的图,绝不借用主检出(main checkout)的:卡片和边栏标签条都会标出它所属的主检出(`git worktree of <repo>`)。还没有图时,在 worktree 里 `/graft` 会从主检出播种出第一张图。
|
|
120
|
+
|
|
121
|
+
根目录是从**会话自己的工作区**解析的,不是从 dsh 的启动位置。这正是本插件存在、而不是用 `graft mcp` 服务器的原因:`graft mcp` 在启动时从自己的工作目录解析一次仓库,之后没有任何办法改指。从主目录启动时,它对你打开的每个工作区都回答*"no graph found"*。
|
|
122
|
+
|
|
123
|
+
### 可视化
|
|
124
|
+
|
|
125
|
+
`graft viz` 按钮把图作为一个标签页打开——在 `0.1.5`+ 的 dsh 内置右侧边栏里,或者在你仍在使用的 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 里。上下文视图铺开整张图:
|
|
126
|
+
|
|
127
|
+

|
|
128
|
+
|
|
129
|
+
代码视图下钻到单个文件——它包含什么、依赖什么、被谁依赖:
|
|
130
|
+
|
|
131
|
+

|
|
132
|
+
|
|
133
|
+
### 工具
|
|
134
|
+
|
|
135
|
+
| 工具 | 回答什么 |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `graft_ask` | "X 是怎么工作的" / "Y 在哪"——带排序的命中,代码直接内联在精确的 `file:line` |
|
|
138
|
+
| `graft_grep` | 每一处出现,按所在符号分组(`graft_ask` 只给 top-N,会漏) |
|
|
139
|
+
| `graft_callers` | 谁调用一个符号、它调用谁,或改名前的完整爆炸半径 |
|
|
140
|
+
| `graft_skeleton` | 一个文件的完整 API,约 200 token |
|
|
141
|
+
| `graft_map` | 在陌生目录树里定位方向 |
|
|
142
|
+
|
|
143
|
+
它们都不接受布尔参数,而是用字符串枚举——`detail: "full"` 而不是 `full: true`——对模型更好读,也绕开了一些 OpenAI 兼容服务器上的一个真实故障:*设置*了布尔值的工具调用会被发成字面的 `<tool_call>` 文本,永远不执行。
|
|
144
|
+
|
|
145
|
+
### `/graft`
|
|
146
|
+
|
|
147
|
+
- `/graft` — 构建索引,已有则重建
|
|
148
|
+
- `/graft status` — 把徽章知道的东西打成文字
|
|
149
|
+
- `/graft deep` — 完整重建
|
|
150
|
+
|
|
151
|
+
### 自动重建
|
|
152
|
+
|
|
153
|
+
模型写入或编辑文件后,索引在后台重建,并做了防抖,长回合不会每次击键都重建。这里复用 graft 自己的 sync runner,而不是重新实现一遍,所以它不会和 `graft build` 的行为漂移。
|
|
154
|
+
|
|
155
|
+
## 配置
|
|
156
|
+
|
|
157
|
+
全部可选,全部写在 profile 的 `cordis.patch.yml` 里本插件的那一行上:
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
- id: graft-status
|
|
161
|
+
name: 'dsh-graft-plugin'
|
|
162
|
+
config:
|
|
163
|
+
cwd: 'C:/path/to/repo' # 固定到一个仓库,而不是按会话解析
|
|
164
|
+
tools: false # 去掉 graft_* 工具,保留徽章
|
|
165
|
+
command: false # 去掉 /graft
|
|
166
|
+
promptSection: false # 不再告诉模型使用 graft
|
|
167
|
+
autoBuild: false # 不再在编辑后重建(卡片上的 Build 按钮仍然可用)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 系统要求
|
|
171
|
+
|
|
172
|
+
| dsh | 状态 |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `0.2.1-rc.2` | 测试套件针对 `0.2.1-rc.2` 的包全部通过——徽章、工具、`/graft`、自动重建和面向模型的指引;双形态 codec 在 0.1 和 0.2 宿主上都能加载 |
|
|
175
|
+
| `0.1.7-rc.2` | 在真实 profile 上测试过(0.3.0)——徽章、工具、`/graft`、自动重建和面向模型的指引全部验证 |
|
|
176
|
+
| `0.1.2-rc.1` – `0.1.0-rc.7` | 测试过,完整功能——`graft viz` 标签页经由 `dsh-better-sidebar` |
|
|
177
|
+
| `0.1.5-rc.2` / `0.1.5` | 测试过——徽章、工具、`/graft`、自动重建全部工作;在 `0.1.5` 上 `graft viz` 按钮在内置右侧边栏打开图 |
|
|
178
|
+
| `0.2.0-rc.2` | 需要插件 `0.2.1`+——更早的发行版无法激活(见故障排查) |
|
|
179
|
+
|
|
180
|
+
- Node 22+
|
|
181
|
+
- PATH 上的 `graft` CLI(`npm install -g @nanonets/graft`)。bun 的全局安装(`bun add -g @nanonets/graft`)也能被识别,经由 `$BUN_INSTALL` 或 `~/.bun`。
|
|
182
|
+
|
|
183
|
+
客户端 bundle 只依赖 `react`——这就是它能在 dsh `0.1.5-rc.2` 移除一批客户端包、弄坏了好几个相邻插件的情况下存活的原因。
|
|
184
|
+
|
|
185
|
+
## 开发
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
npm install
|
|
189
|
+
npm test # 可移植:在临时目录里构建一个合成的 graft 索引
|
|
190
|
+
npm run test:live # 用真实 graft CLI 对着真实仓库跑
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`npm test` 不需要任何 graft 索引,机器上也不需要装 graft。唯一会真建索引的那个套件在 graft 缺失时会大声地自我跳过——缺 CLI 是一种环境状态,不是缺陷。
|
|
194
|
+
|
|
195
|
+
### 两个标识符,故意不同
|
|
196
|
+
|
|
197
|
+
cordis 行的 `id`(`graft-status`)是本插件自己的把手——它键控存储文件、prompt 区块和每个 effect 标签,并且在改名中保持稳定。
|
|
198
|
+
|
|
199
|
+
客户端 bundle 的 `__ModuleLoader__.load({ id })` **不是**那个把手。它必须是已发布的包名:dsh 用 `entry.options.name`——即 `cordis.patch.yml` 里的 `name:`——构建每条客户端行,把 bundle 服务在 `/plugins/<name>/client.js`,然后断言有一个以同一名字注册的工厂。在那里用行 id 会让每次启动都失败:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
bundle /plugins/dsh-graft-plugin/client.js loaded without
|
|
203
|
+
registering "dsh-graft-plugin" via __ModuleLoader__.load
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`npm test` 双向守着这一点(这个守卫随 0.1.1 的修复加入)。
|
|
207
|
+
|
|
208
|
+
## 故障排查
|
|
209
|
+
|
|
210
|
+
**徽章没出现,而 dsh 启动毫无报错。** 插件在 profile 的 `package.json` 的 `dependencies` 里,却不在 `dsh.profile.bundles` 里——pnpm 10+ 下 `add` 以 `ERR_PNPM_IGNORED_BUILDS` 非零退出时就是这样:dsh 跳过 bundle 行,启动看起来一切正常。修法:在 profile 目录里 `pnpm approve-builds`(勾选 `dsh-graft-plugin`),然后重跑 `dsh plugin --profile web add dsh-graft-plugin`。如果 `pnpm approve-builds` 说没有待批准的包,去 profile 的 `pnpm-workspace.yaml` 里找未回答的 `dsh-graft-plugin` 条目(pnpm 的批准占位符),先把它的值改成 `true`。
|
|
211
|
+
|
|
212
|
+
**在 dsh 0.2 上插件加载失败,报 `strict codec has no create() factory`,dsh 还提议移除它。** dsh 0.2 要求每个 strict remote codec 都带一个 `create()` schema 工厂;`0.2.1` 之前的插件发行版只有 0.1 的 `schema` 键。升级到 `0.2.1` 或更新版本——它两种都带,0.1 和 0.2 上都能加载。
|
|
213
|
+
|
|
214
|
+
**`/graft` 说 graft CLI 不在 PATH 上。** postinstall 被拦截或被跳过了(见安装):`npm install -g @nanonets/graft`。
|
|
215
|
+
|
|
216
|
+
## 许可证
|
|
217
|
+
|
|
218
|
+
MIT
|
package/assets/icon.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-graft-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Shows which graft index a dsh session is serving and whether it still matches the code, and gives the model graft_ask, graft_grep, graft_callers, graft_skeleton and graft_map as native tools.",
|
|
5
|
+
"icon": "assets/icon.png",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "src/index.js",
|
|
7
8
|
"exports": {
|
|
@@ -25,12 +26,14 @@
|
|
|
25
26
|
"scripts/test-tools.mjs",
|
|
26
27
|
"scripts/test-autosync.mjs",
|
|
27
28
|
"scripts/test-command.mjs",
|
|
29
|
+
"scripts/test-guidance.mjs",
|
|
28
30
|
"README.md",
|
|
29
31
|
"LICENSE",
|
|
30
|
-
"screenshots.json"
|
|
32
|
+
"screenshots.json",
|
|
33
|
+
"assets/icon.png"
|
|
31
34
|
],
|
|
32
35
|
"scripts": {
|
|
33
|
-
"test": "node scripts/test-harness.mjs && node scripts/test-autosync.mjs && node scripts/test-command.mjs",
|
|
36
|
+
"test": "node scripts/test-harness.mjs && node scripts/test-autosync.mjs && node scripts/test-command.mjs && node scripts/test-guidance.mjs",
|
|
34
37
|
"test:live": "node scripts/test-tools.mjs",
|
|
35
38
|
"postinstall": "node scripts/postinstall.mjs"
|
|
36
39
|
},
|
|
@@ -45,15 +48,19 @@
|
|
|
45
48
|
]
|
|
46
49
|
}
|
|
47
50
|
},
|
|
48
|
-
"dependencies": {
|
|
49
|
-
"dsh-better-sidebar": "^0.18.0"
|
|
50
|
-
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.6"
|
|
52
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.6",
|
|
53
|
+
"dsh-better-sidebar": ">=0.18.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"dsh-better-sidebar": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
53
59
|
},
|
|
54
60
|
"devDependencies": {
|
|
55
|
-
"@deepseek-ai/
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
61
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
62
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.2",
|
|
63
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2"
|
|
57
64
|
},
|
|
58
65
|
"keywords": [
|
|
59
66
|
"dsh-plugin",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { resolveGraftCommand, savedTokensIn } from '../src/graft-tools.js'
|
|
2
3
|
import { createAutoSync } from '../src/auto-sync.js'
|
|
3
4
|
import { totalSaved } from '../src/index.js'
|
|
4
5
|
|
|
@@ -76,6 +77,17 @@ console.log('\n--- graft missing is not an error ---')
|
|
|
76
77
|
const none = createAutoSync({ graft: null, now: () => now, spawn: () => { throw new Error('should not spawn') } })
|
|
77
78
|
check('no graft, no rebuild, no throw', (await none.syncIfDirty('C:/x')) === 'graft-unavailable')
|
|
78
79
|
|
|
80
|
+
console.log('\n--- a manual Build forces a rebuild, like the popup button ---')
|
|
81
|
+
// The popup's Build must not go through syncIfDirty's gates: a repo whose
|
|
82
|
+
// stats already read clean (plain `graft build` never patches stats.json, so
|
|
83
|
+
// `dirty` is whatever the last sync-run left) would answer 'clean' and spawn
|
|
84
|
+
// nothing — the exact "button press changes nothing" shipped once.
|
|
85
|
+
state.lock = true
|
|
86
|
+
now += 60_000
|
|
87
|
+
check('a forced rebuild skips the clean gate', (await sync.rebuild('C:/y')) === 'started')
|
|
88
|
+
check('and it spawned graft\'s sync-run for that root', spawned.some((a) => a[1][1] === 'C:/y'), JSON.stringify(spawned.at(-1)))
|
|
89
|
+
check('it set syncing so the chip can show it', state.stats.syncing === true)
|
|
90
|
+
|
|
79
91
|
console.log('\n--- mid-turn rebuild: a long turn must not sit on a stale graph ---')
|
|
80
92
|
// The turn boundary alone left a graph dirty for 36 minutes of one agentic
|
|
81
93
|
// turn while the model kept querying it. A write now also arms a debounce.
|
|
@@ -106,6 +118,11 @@ check('a second write replaces the timer, not adds one', pendingTimers.size ===
|
|
|
106
118
|
const fire = [...pendingTimers.values()][0]
|
|
107
119
|
pendingTimers.clear()
|
|
108
120
|
await fire()
|
|
121
|
+
// The timer callback fires syncIfDirty without awaiting it (fire-and-forget
|
|
122
|
+
// by design — a throw inside a timer must never surface), so flush before
|
|
123
|
+
// asserting the spawn landed. This needed one more hop once syncIfDirty
|
|
124
|
+
// started delegating to rebuild().
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
109
126
|
check('the quiet period rebuilds without waiting for the turn', fired.length === 1, String(fired.length))
|
|
110
127
|
|
|
111
128
|
const off = createAutoSync({ graft: { state, syncRun: 'x' }, now: () => now, spawn: () => ({ on() {}, unref() {} }), debounceMs: 0, setTimeout: () => { throw new Error('should not arm') } })
|
|
@@ -113,5 +130,53 @@ let armed = true
|
|
|
113
130
|
try { await off.markDirty('C:/y') } catch { armed = false }
|
|
114
131
|
check('debounceMs 0 restores turn-end-only', armed === true)
|
|
115
132
|
|
|
133
|
+
console.log('\n--- a graft cli we cannot locate must never crash dsh ---')
|
|
134
|
+
// THE ORIGINAL FATALITY: graft installed as a bare shim (bun global) left
|
|
135
|
+
// `cliPath` undefined, `dirname(undefined)` in loadGraftState escaped the
|
|
136
|
+
// `void`-ed markDirty as an unhandled rejection, and dsh died on the FIRST
|
|
137
|
+
// successful edit in an indexed repo. These checks pin the guarantee: no
|
|
138
|
+
// input, no throw, no rejection, no spawn.
|
|
139
|
+
const noCli = createAutoSync({ now: () => now, spawn: (...a) => { spawned.push(a); return { on() {}, unref() {} } }, debounceMs: 0 })
|
|
140
|
+
let crashed = false
|
|
141
|
+
const spawnedBefore = spawned.length
|
|
142
|
+
try { await noCli.markDirty('C:/no-cli') } catch { crashed = true }
|
|
143
|
+
check('markDirty resolves without a cli', crashed === false)
|
|
144
|
+
check('and never spawns a rebuild', spawned.length === spawnedBefore, String(spawned.length - spawnedBefore))
|
|
145
|
+
check('syncIfDirty reports graft unavailable', (await noCli.syncIfDirty('C:/no-cli')) === 'graft-unavailable')
|
|
146
|
+
|
|
147
|
+
// Even a loader that REJECTS is swallowed — graft() awaits the injected stub
|
|
148
|
+
// inside its own try, so no input shape can turn into an unhandled rejection.
|
|
149
|
+
const throwing = createAutoSync({ graft: Promise.reject(new Error('boom')), now: () => now, spawn: () => ({ on() {}, unref() {} }), debounceMs: 0 })
|
|
150
|
+
crashed = false
|
|
151
|
+
try { await throwing.markDirty('C:/throwing') } catch { crashed = true }
|
|
152
|
+
check('a rejecting graft loader is swallowed', crashed === false)
|
|
153
|
+
check('and sync stays unavailable, not fatal', (await throwing.syncIfDirty('C:/throwing')) === 'graft-unavailable')
|
|
154
|
+
|
|
155
|
+
console.log('\n--- resolveGraftCommand finds graft however it was installed ---')
|
|
156
|
+
const fakeExists = (set) => (p) => set.has(p)
|
|
157
|
+
const bunCli = join('/home/x', '.bun', 'install', 'global', 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js')
|
|
158
|
+
// A bun global install: only the shim is on PATH, the package lives under
|
|
159
|
+
// $BUN_INSTALL/install/global. The PATH-only scan returned prefix: [] here,
|
|
160
|
+
// which is what fed undefined into loadGraftState above.
|
|
161
|
+
const viaBunInstall = resolveGraftCommand(
|
|
162
|
+
{ PATH: '/home/x/.bun/bin', BUN_INSTALL: '/home/x/.bun', HOME: '/home/x' },
|
|
163
|
+
fakeExists(new Set([bunCli])),
|
|
164
|
+
)
|
|
165
|
+
check('a bun global install resolves to its dist cli', viaBunInstall.prefix[0] === bunCli && viaBunInstall.shell === false, viaBunInstall.prefix[0])
|
|
166
|
+
const viaHome = resolveGraftCommand(
|
|
167
|
+
{ PATH: '', HOME: '/home/x' },
|
|
168
|
+
fakeExists(new Set([bunCli])),
|
|
169
|
+
)
|
|
170
|
+
check('or via HOME/.bun when BUN_INSTALL is unset', viaHome.prefix[0] === bunCli, viaHome.prefix[0])
|
|
171
|
+
const viaUserprofile = resolveGraftCommand(
|
|
172
|
+
{ PATH: '', USERPROFILE: 'C:\\Users\\x' },
|
|
173
|
+
// The fixture path is built with the same join() the source uses, so this
|
|
174
|
+
// checks WHERE the lookup happens, not how a host renders separators.
|
|
175
|
+
fakeExists(new Set([join('C:\\Users\\x', '.bun', 'install', 'global', 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js')])),
|
|
176
|
+
)
|
|
177
|
+
check('or via USERPROFILE/.bun on Windows', viaUserprofile.prefix[0] === join('C:\\Users\\x', '.bun', 'install', 'global', 'node_modules', '@nanonets', 'graft', 'dist', 'cli.js'), viaUserprofile.prefix[0])
|
|
178
|
+
const bare = resolveGraftCommand({ PATH: '', HOME: '/home/x' }, fakeExists(new Set()))
|
|
179
|
+
check('nothing installed falls back to the bare command', bare.command === 'graft' && bare.prefix.length === 0)
|
|
180
|
+
|
|
116
181
|
console.log(failures === 0 ? '\nALL CHECKS PASSED' : `\n${failures} CHECK(S) FAILED`)
|
|
117
182
|
process.exit(failures === 0 ? 0 : 1)
|