@xiaoso/dsh-tool-plus 0.1.14 → 0.1.16-beta.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/README.en.md CHANGED
@@ -1,130 +1,164 @@
1
- <div align="center">
2
- <img width="160" src="assets/icon.png" alt="dsh tool plus icon"><br>
3
- <img width="240" src="assets/wordmark.png" alt="dsh tool plus">
4
- </div>
5
-
6
- <div align="center">
7
-
8
- [简体中文](./README.md) | English
9
-
10
- </div>
11
-
12
- <div align="center">
13
-
14
- [![npm version](https://img.shields.io/npm/v/@xiaoso/dsh-tool-plus?logo=npm)](https://www.npmjs.com/package/@xiaoso/dsh-tool-plus)
15
- [![License: MIT](https://img.shields.io/github/license/xiaoso456/dsh-tool-plus)](LICENSE)
16
- [![Node](https://img.shields.io/badge/node-%3E%3D22.19%20%7C%20%3E%3D24-339933?logo=nodedotjs)](https://nodejs.org)
17
- [![Platform](https://img.shields.io/badge/platform-windows%20%7C%20macos%20%7C%20linux-lightgrey)]()
18
-
19
- </div>
20
-
21
- Essential tools enhancement for DeepSeek Harness: persistent bash, structured read, multi-mode edit, atomic write, full-text search, and direct image reading — one plugin covers it all. Ported from the [Oh My Pi](https://github.com/can1357/oh-my-pi) core; once installed it automatically takes over the official bash / pwsh / file / search tools, with optional `ast_grep` / `ast_edit` structural search and rewrite.
22
-
23
- ## Features
24
-
25
- - **bash**: persistent shell; `cd` and `export` keep state across calls; verbose logs (git/npm/cargo…) are condensed automatically; overlong output keeps only head and tail, with the full content written to disk for later retrieval; long-running commands are moved to the background automatically; optionally intercepts `cat`, `grep`, `find`, and `sed -i` to steer you toward the dedicated tools
26
- - **Safe delete**: `rm` moves files to the system trash by default instead of deleting permanently, so accidental deletes are recoverable; use `command rm` for a real delete
27
- - **read**: precise line-range reading (supports `:N-M`, `:raw`, and multiple ranges); large code files return a structural summary by default, with details expanded on demand; zip/tar archives, SQLite, notebooks, and PDFs read directly; PNG / JPEG / WebP / GIF images read directly, oversized ones scaled down automatically; can fetch web content (including images embedded in the page); SPA sites (content rendered dynamically by JS, e.g. excalidraw.com) are automatically fetched via local browser rendering
28
- - **write**: atomic writes that return a diff of the changes; supports patch-style writing, and can write directly into zip/tar archive members and SQLite data
29
- - **edit**: `replace` by default, with patch / hashline / apply-patch formats also supported; multi-hunk edits, uniqueness validation, fuzzy matching tolerant of whitespace differences
30
- - **grep / glob**: full-text search and filename matching; mtime sorting, context lines, and configurable ignore rules
31
- - **ast_grep / ast_edit** (optional): syntax-tree based structural code search and rewrite, enabled in settings
32
- - **agent presets**: two companion templates — Standard enhanced and PTC (Code Mode)
33
-
34
- Settings panel (Bash tab):
35
-
36
- ![Tool Plus settings panel (Bash)](docs/screenshots/settings-bash-en.png)
37
-
38
- ## Recommended Environment & Configuration
39
-
40
- - **Full-access mode**: the plugin is best used under `danger-full-access`, which avoids file writes being blocked by mistake in sandboxed modes.
41
-
42
- ## Installation
43
-
44
- ### Install from npm (recommended)
45
-
46
- #### Web
47
-
48
- ```sh
49
- dsh plugin --profile web add --allow-build=@xiaoso/dsh-tool-plus --allow-build=koffi @xiaoso/dsh-tool-plus
50
- ```
51
-
52
- `--allow-build` lets install scripts run (once for this package, once for `koffi`); it requires pnpm ≥ 10.4, and on pnpm 11+ omitting it fails the install with `ERR_PNPM_IGNORED_BUILDS`. If your pnpm is too old for the flag, allow the scripts first and re-run:
53
-
54
- - Append to `~/.dsh/profiles/web/pnpm-workspace.yaml`: `allowBuilds:`, ` '@xiaoso/dsh-tool-plus': true` and ` koffi: true`
55
- - Or run `cd ~/.dsh/profiles/web && pnpm approve-builds --all` (pnpm ≥ 10.32)
56
-
57
- #### Desktop
58
-
59
- The profile is owned by the desktop app, so `dsh plugin --profile desktop` is refused; install inside the profile directory instead:
60
-
61
- ```sh
62
- cd ~/.dsh/profiles/desktop
63
- pnpm add --allow-build=@xiaoso/dsh-tool-plus --allow-build=koffi @xiaoso/dsh-tool-plus
64
- ```
65
-
66
- ### Local development
67
-
68
- ```sh
69
- dsh plugin --profile web add link:<path to this repo>
70
- ```
71
-
72
- ### Presets
73
-
74
- | Preset | Base | What it is |
75
- |---|---|---|
76
- | **Tool Plus 标准增强版** | official standard | everything in standard mode, with the file/shell toolset swapped for this plugin |
77
- | **Tool Plus PTC 增强版** | official PTC (Code Mode) | same, but composing multi-step operations through Code Mode's `run_code` |
78
-
79
- Both presets are **declared by the plugin**: installing it (as a profile bundle) brings two `@deepseek-ai/dsh-agent-preset` declaration rows along in its patch layer, so they appear in the preset list immediately. There is no install step, and the plugin writes nothing at startup.
80
-
81
- **Tool Plus → Presets** in the settings page is where you change them. Since dsh 0.1.7 the official preset page is read-only and the `agent_preset` tool is gone, so this panel is the only graphical entry point:
82
-
83
- - **Minimal update** — disables just the official tool rows still mounted, touching nothing else
84
- - **Align to template** — replaces this row's whole plugin list with the selected template
85
- - **Revert to bundled** — removes the override you wrote in the profile configuration (`<profile>/cordis.patch.yml`), falling back to the bundled declaration
86
- - **View differences** — shows which entries differ before you touch anything
87
-
88
- Writes go through the harness's own `ctx.configEditor`, so they inherit the profile lock, concurrency protection, atomic write and rollback.
89
-
90
- > Upgrading from 0.1.9 or earlier: `~/.dsh/.agent-presets/` is **read by nothing** any more (0.1.7 moved presets into the patch layer), so it is safe to delete; the panel tells you when it finds one.
91
-
92
- ## Configuration
93
-
94
- Works out of the box, no configuration needed. Common tweaks: background threshold (`autoBackgroundMs`), timeouts (`defaultTimeoutMs` / `maxTimeoutMs`), the output truncation window, the default edit mode (`editMode`, default `replace`), and the structured summary toggle (`readSummarizeEnabled`).
95
-
96
- ## Requirements
97
-
98
- - **dsh CLI**: installed globally, `npm i -g @deepseek-ai/dsh`
99
- - **Node.js** ≥ 22.19 or ≥ 24
100
- - **Git Bash** (recommended): serves as the bash execution environment on Windows
101
- - Targets DeepSeek Harness `dsh` v0.2.0-rc.2 (pre-release; interfaces may change)
102
-
103
- ## Notes
104
-
105
- - No standalone `pwsh` tool: shell work is handled by the more capable persistent `bash`
106
-
107
- **Safe rm sends deleted files to the system trash.** Where they land, and whether you can see them:
108
-
109
- | | Windows | macOS | Linux |
110
- |---|---|---|---|
111
- | **Location** | Recycle Bin | Trash `~/.Trash` | `$XDG_DATA_HOME/Trash`; defaults to `~/.local/share/Trash` |
112
- | **Another volume / external disk** | That volume's Recycle Bin | That volume's `.Trashes` | `.Trash-<uid>` at that mount point |
113
- | **Visible in the OS trash** | Yes | Yes | Same volume: yes; another volume: open that location |
114
- | **File name** | Unchanged | Unchanged | A UUID; the original name comes from `.trashinfo` |
115
- | **Put back** | Yes | Yes | Depends on the file manager |
116
- | **Implementation** | Shell API | System API | Third-party XDG implementation |
117
- | **Known limits** | No Recycle Bin on network drives | Requires macOS 10.12+ | Some desktops do not list it — restore from the location above |
118
-
119
- ## Build
120
-
121
- ```sh
122
- pnpm install
123
- pnpm build # type declarations (tsc) + tsdown bundle + asset copy
124
- pnpm typecheck
125
- pnpm test # includes real-bash cases; needs Git Bash on Windows, auto-skips when missing
126
- ```
127
-
128
- ## License
129
-
130
- [MIT](LICENSE); third-party component licenses in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
1
+ <div align="center">
2
+ <img width="160" src="assets/icon.png" alt="dsh tool plus icon"><br>
3
+ <img width="240" src="assets/wordmark.png" alt="dsh tool plus">
4
+ </div>
5
+
6
+ <div align="center">
7
+
8
+ [简体中文](./README.md) | English
9
+
10
+ </div>
11
+
12
+ <div align="center">
13
+
14
+ [![npm version](https://img.shields.io/npm/v/@xiaoso/dsh-tool-plus?logo=npm)](https://www.npmjs.com/package/@xiaoso/dsh-tool-plus)
15
+ [![License: MIT](https://img.shields.io/github/license/xiaoso456/dsh-tool-plus)](LICENSE)
16
+ [![Node](https://img.shields.io/badge/node-%3E%3D22.19%20%7C%20%3E%3D24-339933?logo=nodedotjs)](https://nodejs.org)
17
+ [![Platform](https://img.shields.io/badge/platform-windows%20%7C%20macos%20%7C%20linux-lightgrey)]()
18
+
19
+ </div>
20
+
21
+ Essential tools enhancement for DeepSeek Harness: persistent bash, structured read, multi-mode edit, atomic write, full-text search, and direct image reading — one plugin covers it all. Ported from the [Oh My Pi](https://github.com/can1357/oh-my-pi) core; once installed it automatically takes over the official bash / pwsh / file / search tools, with optional `ast_grep` / `ast_edit` structural search and rewrite.
22
+
23
+ ## Features
24
+
25
+ - **bash**: persistent shell; `cd` and `export` keep state across calls; verbose logs (git/npm/cargo…) are condensed automatically; overlong output keeps only head and tail, with the full content written to disk for later retrieval; long-running commands are moved to the background automatically; optionally intercepts `cat`, `grep`, `find`, and `sed -i` to steer you toward the dedicated tools (off by default)
26
+ - **Safe delete**: `rm` moves files to the system trash by default instead of deleting permanently, so accidental deletes are recoverable; use `command rm` for a real delete
27
+ - **read**: precise line-range reading (supports `:N-M`, `:raw`, and multiple ranges); large code files return a structural summary by default, with details expanded on demand; zip/tar archives, SQLite, notebooks, and PDFs read directly; PNG / JPEG / WebP / GIF images read directly, oversized ones scaled down automatically; can fetch web content (including images embedded in the page); SPA sites (content rendered dynamically by JS, e.g. excalidraw.com) are automatically fetched via local browser rendering
28
+ - **write**: atomic writes that return a diff of the changes; supports patch-style writing, and can write directly into zip/tar archive members and SQLite data
29
+ - **edit**: `replace` by default, with patch / hashline / apply-patch formats also supported; multi-hunk edits, uniqueness validation, fuzzy matching tolerant of whitespace differences
30
+ - **grep / glob**: full-text search and filename matching; mtime sorting, context lines, and configurable ignore rules
31
+ - **ast_grep / ast_edit** (optional): syntax-tree based structural code search and rewrite, enabled in settings
32
+ - **agent presets**: two companion templates — Standard enhanced and PTC (Code Mode)
33
+
34
+ Settings panel (Bash tab):
35
+
36
+ ![Tool Plus settings panel (Bash)](docs/screenshots/settings-bash-en.png)
37
+
38
+ ## Recommended Environment & Configuration
39
+
40
+ - **Full-access mode**: the plugin is best used under `danger-full-access`, which avoids file writes being blocked by mistake in sandboxed modes.
41
+
42
+ ## Installation
43
+
44
+ ### Install from npm (recommended)
45
+
46
+ #### Web
47
+
48
+ ```sh
49
+ dsh plugin --profile web add --allow-build=@xiaoso/dsh-tool-plus --allow-build=koffi @xiaoso/dsh-tool-plus
50
+ ```
51
+
52
+ `--allow-build` lets install scripts run (once for this package, once for `koffi`); it requires pnpm ≥ 10.4, and on pnpm 11+ omitting it fails the install with `ERR_PNPM_IGNORED_BUILDS`. If your pnpm is too old for the flag, allow the scripts first and re-run:
53
+
54
+ - Append to `~/.dsh/profiles/web/pnpm-workspace.yaml`: `allowBuilds:`, ` '@xiaoso/dsh-tool-plus': true` and ` koffi: true`
55
+ - Or run `cd ~/.dsh/profiles/web && pnpm approve-builds --all` (pnpm ≥ 10.32)
56
+
57
+ #### Desktop
58
+
59
+ The profile is owned by the desktop app, so `dsh plugin --profile desktop` is refused; install inside the profile directory instead:
60
+
61
+ ```sh
62
+ cd ~/.dsh/profiles/desktop
63
+ pnpm add --allow-build=@xiaoso/dsh-tool-plus --allow-build=koffi @xiaoso/dsh-tool-plus
64
+ ```
65
+
66
+ ### Local development
67
+
68
+ ```sh
69
+ dsh plugin --profile web add link:<path to this repo>
70
+ ```
71
+
72
+ ### Presets
73
+
74
+ | Preset | Base | What it is |
75
+ |---|---|---|
76
+ | **Tool Plus 标准增强版** | official standard | everything in standard mode, with the file/shell toolset swapped for this plugin |
77
+ | **Tool Plus PTC 增强版** | official PTC (Code Mode) | same, but composing multi-step operations through Code Mode's `run_code` |
78
+
79
+ Both presets are **declared by the plugin**: installing it (as a profile bundle) brings two `@deepseek-ai/dsh-agent-preset` declaration rows along in its patch layer, so they appear in the preset list immediately. There is no install step, and the plugin writes nothing at startup.
80
+
81
+ **Tool Plus → Presets** in the settings page is where you change them. Since dsh 0.1.7 the official preset page is read-only and the `agent_preset` tool is gone, so this panel is the only graphical entry point:
82
+
83
+ - **Minimal update** — disables just the official tool rows still mounted, touching nothing else
84
+ - **Align to template** — replaces this row's whole plugin list with the selected template
85
+ - **Revert to bundled** — removes the override you wrote in the profile configuration (`<profile>/cordis.patch.yml`), falling back to the bundled declaration
86
+ - **View differences** — shows which entries differ before you touch anything
87
+
88
+ Writes go through the harness's own `ctx.configEditor`, so they inherit the profile lock, concurrency protection, atomic write and rollback.
89
+
90
+ > Upgrading from 0.1.9 or earlier: `~/.dsh/.agent-presets/` is **read by nothing** any more (0.1.7 moved presets into the patch layer), so it is safe to delete; the panel tells you when it finds one.
91
+
92
+ ## Configuration
93
+
94
+ Works out of the box, no configuration needed. Common tweaks: background threshold (`autoBackgroundMs`), timeouts (`defaultTimeoutMs` / `maxTimeoutMs`), the output truncation window, the default edit mode (`editMode`, default `replace`), and the structured summary toggle (`readSummarizeEnabled`).
95
+
96
+ ## Performance
97
+
98
+ On Linux, comparing the official standard mode with this plugin's Standard enhanced preset (SWE-bench Multilingual, 20 tasks × 3 rounds):
99
+
100
+ | Metric | Official tool surface | This plugin (command interception on) | This plugin (command interception off, default) |
101
+ |---|---|---|---|
102
+ | Resolve rate | 83.3% | **83.3%** | 81.7% (1 task lower, within normal variance) |
103
+ | Steps / task | 24.4 | 29.7 (+21.7%) | 25.3 (+3.8%) |
104
+ | Total tokens / task | **729,434** | 954,100 (+30.8%) | 779,799 (+6.9%) |
105
+ | System prompt / step (incl. tool definitions) | **4,642** | 6,305 (+35.8%) | 6,305 (+35.8%) |
106
+ | System prompt total / task (per step × steps) | **113,265** | 187,259 (+65.3%) | 159,622 (+40.9%) |
107
+ | Net tokens / task (excl. system prompt) | **616,169** | 766,841 (+24.5%) | 620,177 (+0.7%) |
108
+ | Average context length at task end (tokens) | **42,817** | 48,645 (+13.6%) | 44,781 (+4.6%) |
109
+ | `read` calls | 225 | 459 | **198** |
110
+ | `bash` time per call | 3,140 ms | **2,391 ms** | 2,045 ms |
111
+ | `bash` tokens returned per call | 790 tok | **343 tok** | 638 tok |
112
+ | Average time / task (excludes token-rate and network-speed fluctuation) | **123 s** | 135 s | 151 s |
113
+
114
+ Notes:
115
+
116
+ 1. The average time is an end-to-end measurement that does not account for token-rate or network-speed fluctuation; use the absolute values for horizontal comparison only.
117
+ 2. The figures above describe this benchmark only (its tasks use little context, so they do not represent large tasks).
118
+
119
+ Conclusions:
120
+
121
+ 1. The resolve rate matches the official tools (the interception-off group is 1 task lower, within normal variance).
122
+ 2. `bash` itself is leaner, and the tokens it saves roughly offset the extra system prompt from thicker tool definitions (the plugin adds capabilities, so its definitions are thicker than the official preset's).
123
+ 3. `bash` is faster, but the gain is cancelled out by side effects from `bash` itself and the other enhanced tools.
124
+ 4. On Linux the combined performance is on par with the official standard-mode tools, with a richer feature set.
125
+
126
+ So on Linux this plugin cannot promise lower token usage or better performance; Windows is not suitable for running the standard benchmark, so the only claim there is a **personal impression** that it is more pleasant to use than PowerShell.
127
+
128
+ Full data and the per-task matrix: [docs/benchmark/sbml-compare-ops.md](docs/benchmark/sbml-compare-ops.md) (Chinese).
129
+
130
+ ## Requirements
131
+
132
+ - **dsh CLI**: installed globally, `npm i -g @deepseek-ai/dsh`
133
+ - **Node.js** ≥ 22.19 or ≥ 24
134
+ - **Git Bash** (recommended): serves as the bash execution environment on Windows
135
+ - Targets DeepSeek Harness `dsh` v0.2.0-rc.2 (pre-release; interfaces may change)
136
+
137
+ ## Notes
138
+
139
+ - No standalone `pwsh` tool: shell work is handled by the more capable persistent `bash`
140
+
141
+ **Safe rm sends deleted files to the system trash.** Where they land, and whether you can see them:
142
+
143
+ | | Windows | macOS | Linux |
144
+ |---|---|---|---|
145
+ | **Location** | Recycle Bin | Trash `~/.Trash` | `$XDG_DATA_HOME/Trash`; defaults to `~/.local/share/Trash` |
146
+ | **Another volume / external disk** | That volume's Recycle Bin | That volume's `.Trashes` | `.Trash-<uid>` at that mount point |
147
+ | **Visible in the OS trash** | Yes | Yes | Same volume: yes; another volume: open that location |
148
+ | **File name** | Unchanged | Unchanged | A UUID; the original name comes from `.trashinfo` |
149
+ | **Put back** | Yes | Yes | Depends on the file manager |
150
+ | **Implementation** | Shell API | System API | Third-party XDG implementation |
151
+ | **Known limits** | No Recycle Bin on network drives | Requires macOS 10.12+ | Some desktops do not list it — restore from the location above |
152
+
153
+ ## Build
154
+
155
+ ```sh
156
+ pnpm install
157
+ pnpm build # type declarations (tsc) + tsdown bundle + asset copy
158
+ pnpm typecheck
159
+ pnpm test # includes real-bash cases; needs Git Bash on Windows, auto-skips when missing
160
+ ```
161
+
162
+ ## License
163
+
164
+ [MIT](LICENSE); third-party component licenses in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
package/README.md CHANGED
@@ -22,7 +22,7 @@ DeepSeek Harness 基础工具增强:持久 bash、结构化 read、多模式 e
22
22
 
23
23
  ## 功能特性
24
24
 
25
- - **bash**:持久 shell,`cd`、`export` 跨调用保持状态;冗长日志(git/npm/cargo…)自动压缩;超长输出只保留头尾,完整内容落盘可回读;长命令自动转后台;可选拦截 `cat`、`grep`、`find`、`sed -i`,引导改用专用工具
25
+ - **bash**:持久 shell,`cd`、`export` 跨调用保持状态;冗长日志(git/npm/cargo…)自动压缩;超长输出只保留头尾,完整内容落盘可回读;长命令自动转后台;可选拦截 `cat`、`grep`、`find`、`sed -i`,引导改用专用工具(默认关闭)
26
26
  - **安全删除**:`rm` 默认把文件移入系统回收站而非永久删除,误删可恢复;需要真删时用 `command rm`
27
27
  - **read**:按行区间精准读取(支持 `:N-M`、`:raw`、多区间);大代码文件默认返回结构摘要,细节按需展开;zip/tar 归档、SQLite、notebook、PDF 直接读;PNG / JPEG / WebP / GIF 图片直读,超大图自动缩放;可抓取网页内容(含网页内图片);SPA 站点(内容由 JS 动态渲染,如 excalidraw.com)自动改用本机浏览器渲染后抓取
28
28
  - **write**:原子写入,返回修改 diff;支持补丁式写入,可直接写 zip/tar 归档成员与 SQLite 数据
@@ -87,12 +87,45 @@ dsh plugin --profile web add link:<本仓库路径>
87
87
 
88
88
  写入通过宿主自己的 `ctx.configEditor` 完成,因此自带 profile 锁、并发保护、原子写与失败回滚。
89
89
 
90
- > 从 0.1.9 或更早升级过来的话:`~/.dsh/.agent-presets/` 目录**已经没有任何代码读它**(0.1.7 起预设改为补丁层声明),可以安全删除;设置页检测到它会给出提示。
91
-
92
90
  ## 配置
93
91
 
94
92
  开箱即用,无需配置。需要微调时常用项有:后台阈值(`autoBackgroundMs`)、超时(`defaultTimeoutMs` / `maxTimeoutMs`)、输出截断窗口、编辑默认模式(`editMode`,默认 `replace`)、结构化摘要开关(`readSummarizeEnabled`)。抓取相关:可切换网页转 Markdown 的后端(`fetchReader`,含浏览器 JS 渲染——SPA 页面),设置面板里可一键「探测浏览器」查看本机可用的 Chrome/Edge。
95
93
 
94
+ ## 性能
95
+
96
+ 标准模式/标准模式增强下,在 Linux 上跑 SWE-bench Multilingual(20 题 × 3 轮):
97
+
98
+ | 指标 | 官方工具面 | 本插件(命令拦截开) | 本插件(命令拦截关,默认) |
99
+ |---|---|---|---|
100
+ | 解题率 | 83.3% | **83.3%** | 81.7%(低 1 题,正常波动) |
101
+ | 步数 / 题 | 24.4 | 29.7(+21.7%) | 25.3(+3.8%) |
102
+ | 总 token / 题 | **729,434** | 954,100(+30.8%) | 779,799(+6.9%) |
103
+ | 系统提示词 / 步(含工具定义) | **4,642** | 6,305(+35.8%) | 6,305(+35.8%) |
104
+ | 系统提示词累计 / 题(单步 × 步数) | **113,265** | 187,259(+65.3%) | 159,622(+40.9%) |
105
+ | 净 token / 题(除系统提示词) | **616,169** | 766,841(+24.5%) | 620,177(+0.7%) |
106
+ | 任务结束后平均上下文长度(token) | **42,817** | 48,645(+13.6%) | 44,781(+4.6%) |
107
+ | `read` 调用次数 | 225 | 459 | **198** |
108
+ | `bash` 每次调用耗时 | 3,140 ms | **2,391 ms** | 2,045 ms |
109
+ | `bash` 每次调用返回 token | 790 tok | **343 tok** | 638 tok |
110
+ | 平均耗时 / 题(未计入 token 速率波动、网络速度带来的误差) | **123 s** | 135 s | 151 s |
111
+
112
+ 注:
113
+
114
+ 1. 平均耗时是端到端实测,未计入 `token` 速率波动、网络速度带来的误差,绝对值只作横向参考。
115
+ 2. 本节性能只代表在该基准测试下的表现(该基准任务占用上下文较低,因此无法代表大任务情况)
116
+ 3. 由大模型计算统计核对,数据可能有疏漏,但不影响整体结论
117
+
118
+ 结论:
119
+
120
+ 1. 解题率与官方持平(命令拦截关的那组低 1 题,属正常波动)
121
+ 2. `bash` 本身更省,省下的 token 基本能抵消工具定义变厚带来的系统提示词增量(功能更强,工具定义比官方预设更厚)
122
+ 3. `bash` 更快,但带来的增益会被 `bash` 自身以及其他增强工具带来的副作用抹平
123
+ 4. 在 `linux` 上,综合性能和官方默认标准模式工具相当,但功能更丰富
124
+
125
+ 因此本插件工具在 `linux` 上,无法承诺更省 `token` 或具有更强的性能;`windows` 不适合跑标准测试,只能从**个人体感**说比 `powershell` 更好用。
126
+
127
+ 完整的四组数据与逐题矩阵见 [docs/benchmark/sbml-compare-ops.md](docs/benchmark/sbml-compare-ops.md)。
128
+
96
129
  ## 环境要求
97
130
 
98
131
  - **dsh CLI**:需全局安装,`npm i -g @deepseek-ai/dsh`
package/lib/client.js CHANGED
@@ -64,7 +64,7 @@ window.__ModuleLoader__.load({
64
64
  {
65
65
  name: "interceptorEnabled",
66
66
  kind: "boolean",
67
- default: true,
67
+ default: false,
68
68
  labelKey: "interceptorEnabled",
69
69
  hintKey: "interceptorEnabledHint",
70
70
  group: "behavior",
package/lib/index.mjs CHANGED
@@ -3,7 +3,7 @@ __installBunShim();
3
3
  import { n as fs_promises_shim_exports, t as exists } from "./fs-promises-shim-B2YFWkcd.mjs";
4
4
  import { i as setRuntimeLogger, n as systemChromiumCandidatesForTest, r as runtimeLogger } from "./launch-BIWNyrBB.mjs";
5
5
  import { $ as isEnoent, A as formatCount, C as warn, D as installBunShim, F as $flag, N as $env, O as formatAge, P as $envpos, Q as hasFsCode, U as getAutoQaDbPath, V as VERSION, X as getRemoteDir, Y as getPuppeteerDir, at as findOnPath, b as debug, c as combineSignals, d as $which, et as isEnotdir, it as untilAborted, k as formatBytes$1, l as exec, m as readImageMetadata, nt as peekFile, q as getInstallId, tt as isProbablyBinary, u as render, x as error } from "./src-AELUUb5U.mjs";
6
- import { C as truncateToWidth$1, S as replaceTabs, _ as formatCodeFrameLine, a as fetchBinary, b as LRUCache, c as getParallelExtractContent, d as convertFileWithMarkit, f as Settings, g as capParseErrors, h as OMP_DEFAULTS, i as convertWithMarkit, l as ensureTool, m as settings, o as extractWithParallel, p as isSettingsInitialized, s as findParallelApiKey, u as CONVERTIBLE_EXTENSIONS, v as formatParseErrors, x as padding, y as shortenPath } from "./utils-CHnQw1zZ.mjs";
6
+ import { C as truncateToWidth$1, S as replaceTabs, _ as formatCodeFrameLine, a as fetchBinary, b as LRUCache, c as getParallelExtractContent, d as convertFileWithMarkit, f as Settings, g as capParseErrors, h as OMP_DEFAULTS, i as convertWithMarkit, l as ensureTool, m as settings, o as extractWithParallel, p as isSettingsInitialized, s as findParallelApiKey, u as CONVERTIBLE_EXTENSIONS, v as formatParseErrors, x as padding, y as shortenPath } from "./utils-eJcxzVy7.mjs";
7
7
  import { t as sanitizeText } from "./sanitize-text-BCf5KmCf.mjs";
8
8
  import { n as isRecord$5 } from "./type-guards-CttAANEx.mjs";
9
9
  import { n as ToolError, r as throwIfAborted, t as ToolAbortError } from "./tool-errors-DoyxivAR.mjs";
@@ -4202,7 +4202,7 @@ const TOOL_PLUS_FIELDS = [
4202
4202
  {
4203
4203
  name: "interceptorEnabled",
4204
4204
  kind: "boolean",
4205
- default: true,
4205
+ default: false,
4206
4206
  labelKey: "interceptorEnabled",
4207
4207
  hintKey: "interceptorEnabledHint",
4208
4208
  group: "behavior",
@@ -4958,7 +4958,7 @@ const Config = z.object({
4958
4958
  outputSinkTailBytes: z.number().default(fieldDefault("outputSinkTailBytes", 51200)).volatile(),
4959
4959
  outputSinkHeadBytes: z.number().default(fieldDefault("outputSinkHeadBytes", 20480)).volatile(),
4960
4960
  minimizerEnabled: z.boolean().default(fieldDefault("minimizerEnabled", true)).volatile(),
4961
- interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled", true)).volatile(),
4961
+ interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled", false)).volatile(),
4962
4962
  nonInteractiveEnv: z.boolean().default(fieldDefault("nonInteractiveEnv", true)).volatile(),
4963
4963
  snapshotEnabled: z.boolean().default(fieldDefault("snapshotEnabled", true)).volatile(),
4964
4964
  rmSafe: z.boolean().default(fieldDefault("rmSafe", true)).volatile(),
@@ -5069,7 +5069,7 @@ function resolveConfig(input) {
5069
5069
  outputSinkTailBytes: config.outputSinkTailBytes ?? fieldDefault("outputSinkTailBytes", 51200),
5070
5070
  outputSinkHeadBytes: config.outputSinkHeadBytes ?? fieldDefault("outputSinkHeadBytes", 20480),
5071
5071
  minimizer: resolveMinimizer(),
5072
- interceptorEnabled: config.interceptorEnabled ?? fieldDefault("interceptorEnabled", true),
5072
+ interceptorEnabled: config.interceptorEnabled ?? fieldDefault("interceptorEnabled", false),
5073
5073
  nonInteractiveEnv: config.nonInteractiveEnv ?? fieldDefault("nonInteractiveEnv", true),
5074
5074
  snapshotEnabled: config.snapshotEnabled ?? fieldDefault("snapshotEnabled", true),
5075
5075
  rmSafe: config.rmSafe ?? fieldDefault("rmSafe", true),
@@ -11479,7 +11479,7 @@ let specialHandlersPromise;
11479
11479
  * requires a special handler, so we keep them out of the cold-startup graph.
11480
11480
  */
11481
11481
  function loadSpecialHandlers() {
11482
- specialHandlersPromise ??= import("./scrapers-DSZkQist.mjs").then((m) => m.specialHandlers);
11482
+ specialHandlersPromise ??= import("./scrapers-C6oBIusN.mjs").then((m) => m.specialHandlers);
11483
11483
  return specialHandlersPromise;
11484
11484
  }
11485
11485
  /**
@@ -0,0 +1,34 @@
1
+ /**
2
+ * tool-plus package entry — pi-natives compatibility bootstrap.
3
+ *
4
+ * \`@oh-my-pi/pi-natives\` ships a Bun-only \`import.meta.dir\` in
5
+ * native/loader-state.js. A postinstall patch alone is not enough under pnpm:
6
+ * the file is a hardlink to the content-addressable store, and a later
7
+ * install that repairs or re-links the store can restore the buggy bytes.
8
+ *
9
+ * This entry runs the shared patch helper first, then dynamically imports the
10
+ * real plugin entry. It must not statically import index.mjs (or any module
11
+ * that reaches pi-natives): static imports are evaluated before this body.
12
+ *
13
+ * The helper writes through a temp file + rename, so it breaks the store
14
+ * hardlink and keeps this profile's copy private.
15
+ */
16
+ import { ensurePiNativesPatched } from "../scripts/patch-pi-natives.mjs";
17
+
18
+ const result = ensurePiNativesPatched({ detach: true });
19
+ if (result.error) {
20
+ console.warn("[tool-plus] pi-natives compat: " + result.error);
21
+ } else if (result.patched > 0 || result.detached > 0) {
22
+ console.log(
23
+ "[tool-plus] pi-natives compat: patched=" + result.patched + ", detached=" + result.detached,
24
+ );
25
+ }
26
+
27
+ // Runtime-computed URL: keep this dynamic so a bundler never inlines the
28
+ // plugin graph (and its static pi-natives imports) ahead of the patch above.
29
+ const mod = await import(new URL("./index.mjs", import.meta.url).href);
30
+
31
+ export const name = mod.name;
32
+ export const inject = mod.inject;
33
+ export const Config = mod.Config;
34
+ export const apply = mod.apply;
@@ -3,7 +3,7 @@ __installBunShim();
3
3
  import { n as fs_promises_shim_exports } from "./fs-promises-shim-B2YFWkcd.mjs";
4
4
  import { $ as isEnoent, B as USER_AGENT, C as warn, G as getDocsRsCacheDir, N as $env, c as combineSignals, j as formatNumber, k as formatBytes, l as exec, s as Snowflake, y as parseFrontmatter } from "./src-AELUUb5U.mjs";
5
5
  import { W as tryParseJson } from "./utils-cZX3f_Ls.mjs";
6
- import { a as fetchBinary, c as getParallelExtractContent, i as convertWithMarkit, l as ensureTool, m as settings, n as asRecord, o as extractWithParallel, r as asString, s as findParallelApiKey, t as asNumber } from "./utils-CHnQw1zZ.mjs";
6
+ import { a as fetchBinary, c as getParallelExtractContent, i as convertWithMarkit, l as ensureTool, m as settings, n as asRecord, o as extractWithParallel, r as asString, s as findParallelApiKey, t as asNumber } from "./utils-eJcxzVy7.mjs";
7
7
  import { n as isRecord } from "./type-guards-CttAANEx.mjs";
8
8
  import { r as throwIfAborted, t as ToolAbortError } from "./tool-errors-DoyxivAR.mjs";
9
9
  import { c as getLocalizedText, d as looksLikeHtml, i as decodeHtmlEntities, l as htmlToBasicMarkdown, o as formatIsoDate, r as buildResult, s as formatMediaDuration, u as loadPage } from "./types-BLrLohwK.mjs";
@@ -6267,7 +6267,7 @@ const settings = {
6267
6267
  };
6268
6268
  //#endregion
6269
6269
  //#region package.json
6270
- var version = "0.1.14";
6270
+ var version = "0.1.16-beta.0";
6271
6271
  /** `.tmp` files older than this are treated as orphaned writes and swept. */
6272
6272
  const TMP_ORPHAN_MAX_AGE_MS = 3e5;
6273
6273
  function markitConversionCacheKey(bytes, extension) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xiaoso/dsh-tool-plus",
3
3
  "description": "Oh My Pi bash runtime ported as a deepseek-harness tool plugin: persistent shell session, output minimizer, managed background jobs, command interception",
4
- "version": "0.1.14",
4
+ "version": "0.1.16-beta.0",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "deepseek-harness",
@@ -36,13 +36,13 @@
36
36
  "node": "^22.19.0 || >=24.0.0",
37
37
  "dsh": ">=0.2.1-0 <0.3.0-0"
38
38
  },
39
- "main": "lib/index.mjs",
39
+ "main": "lib/pi-natives-bootstrap.mjs",
40
40
  "types": "lib/types/index.d.ts",
41
41
  "icon": "./assets/icon.png",
42
42
  "exports": {
43
43
  ".": {
44
44
  "types": "./lib/types/index.d.ts",
45
- "default": "./lib/index.mjs"
45
+ "default": "./lib/pi-natives-bootstrap.mjs"
46
46
  },
47
47
  "./client": {
48
48
  "types": "./lib/types/client/index.d.ts",
@@ -1,108 +1,241 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Self-heal `@oh-my-pi/pi-natives` for Node runtimes after installs that did
4
- * NOT go through pnpm (plain npm / yarn / npx of this package).
5
- *
6
- * Background: upstream pi-natives@17.3.5 locates its prebuilt binaries with
7
- * the Bun-only API `import.meta.dir` (native/loader-state.js). Under pnpm,
8
- * `patches/@oh-my-pi__pi-natives.patch` (declared in pnpm-workspace.yaml)
9
- * fixes that line automatically; every other package manager ignores pnpm
10
- * patch declarations, so their installs keep the bug and the tool plugin
11
- * crashes on first native access. This hook folds in the SAME one-line fix,
12
- * making the dependency-tree end state identical across all managers.
13
- *
14
- * Idempotent by construction:
15
- * - file absent -> warn, exit 0 (unknown layout must not brick installs)
16
- * - line already fixed -> no-op ("already patched")
17
- * - buggy literal found -> rewrite `import.meta.dir` -> `import.meta.dirname`
18
- *
19
- * Usage: node scripts/patch-pi-natives.mjs [searchRoot]
20
- * Without argument the script walks ancestors of this file looking for a
21
- * node_modules/@oh-my-pi/pi-natives package (hoisting-safe). With an
22
- * argument, that directory is used as the walk root (for isolated tests).
23
- *
24
- * Never throws: a packaging lifecycle hook must not fail installs; if
25
- * anything unexpected happens it warns so the (pre-existing) runtime error
26
- * stays the single source of truth.
27
- */
28
-
29
- import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
30
- import { dirname, join, resolve } from "node:path";
31
- import { fileURLToPath } from "node:url";
32
-
33
- const PKG_SUBPATH = join("node_modules", "@oh-my-pi", "pi-natives");
34
- const TARGET_REL = join("native", "loader-state.js");
35
- // Exact buggy token; `\b` keeps an already-fixed `import.meta.dirname` intact.
36
- const BUGGY = /import\.meta\.dir\b/g;
37
-
38
- function log(message) {
39
- console.log(`[patch-pi-natives] ${message}`);
40
- }
41
-
42
- /** Walk upwards from `start`, yielding every candidate package directory. */
43
- function* candidatePackageDirs(start) {
44
- let current = resolve(start);
45
- for (;;) {
46
- const candidate = join(current, PKG_SUBPATH);
47
- if (existsSync(candidate)) yield candidate;
48
- const parent = dirname(current);
49
- if (parent === current) return;
50
- current = parent;
51
- }
52
- }
53
-
54
- function firstMatchedFile(packageDir) {
55
- // The known target first (byte-for-byte what the pnpm patch touches).
56
- const direct = join(packageDir, TARGET_REL);
57
- if (existsSync(direct)) return direct;
58
- // Unknown layouts: locate any loader-state.js so we can still heal them.
59
- const stack = [packageDir];
60
- while (stack.length > 0) {
61
- const entry = stack.pop();
62
- for (const item of readdirSync(entry)) {
63
- const full = join(entry, item);
64
- const stat = statSync(full);
65
- if (stat.isDirectory()) stack.push(full);
66
- else if (item === "loader-state.js") return full;
67
- }
68
- }
69
- return undefined;
70
- }
71
-
72
- function main() {
73
- // Default: walk ancestors starting at THIS script's directory (`scripts/`),
74
- // so the repo root's node_modules is covered both here and in published
75
- // layouts (where the package sits inside someone else's node_modules tree).
76
- const scriptDir = dirname(fileURLToPath(import.meta.url));
77
- const searchRoot = process.argv[2] ? resolve(process.argv[2]) : scriptDir;
78
-
79
- let inspected = 0;
80
- let healed = 0;
81
- for (const packageDir of candidatePackageDirs(searchRoot)) {
82
- inspected += 1;
83
- const file = firstMatchedFile(packageDir);
84
- if (file === undefined) {
85
- log(`WARN ${packageDir}: loader-state.js not found, skipped`);
86
- continue;
87
- }
88
- const source = readFileSync(file, "utf8");
89
- BUGGY.lastIndex = 0;
90
- if (!BUGGY.test(source)) {
91
- log(`${file}: already patched`);
92
- continue;
93
- }
94
- BUGGY.lastIndex = 0;
95
- writeFileSync(file, source.replace(BUGGY, "import.meta.dirname"));
96
- healed += 1;
97
- log(`${file}: patched import.meta.dir -> import.meta.dirname`);
98
- }
99
-
100
- if (inspected === 0) log(`WARN no @oh-my-pi/pi-natives found under ${searchRoot}`);
101
- else log(`done: ${inspected} package(s) seen, ${healed} patched`);
102
- }
103
-
104
- try {
105
- main();
106
- } catch (error) {
107
- log(`WARN self-heal skipped: ${error?.message ?? error}`);
108
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Self-heal \`@oh-my-pi/pi-natives\` for Node runtimes.
4
+ *
5
+ * The upstream package locates its prebuilt binaries with the Bun-only
6
+ * \`import.meta.dir\` (native/loader-state.js). Node has no such property, so
7
+ * \`path.join(undefined, "..", "native")\` throws ERR_INVALID_ARG_TYPE before
8
+ * any tool can load.
9
+ *
10
+ * Two callers share this module:
11
+ * - the package postinstall (plain npm / yarn / npx installs), and
12
+ * - lib/pi-natives-bootstrap.mjs, which runs before the real plugin entry
13
+ * so a pnpm store repair/re-link cannot break the next dsh start.
14
+ *
15
+ * Copy-on-patch is intentional. pnpm links package files from its
16
+ * content-addressable store with hardlinks; writing the linked file in place
17
+ * mutates the store and every profile that shares that inode. Writing a temp
18
+ * file and renaming it over the target gives this profile a private inode and
19
+ * leaves the store untouched. When the file is already patched but still
20
+ * shared, the same operation detaches it once.
21
+ *
22
+ * Resolution prefers \`import.meta.resolve("@oh-my-pi/pi-natives")\` because the
23
+ * package is ESM-only; createRequire.resolve throws
24
+ * ERR_PACKAGE_PATH_NOT_EXPORTED for it. The historical ancestor walk remains
25
+ * as a fallback for unknown layouts, and can be forced with a searchRoot.
26
+ *
27
+ * Idempotent; the CLI never throws.
28
+ *
29
+ * Usage: node scripts/patch-pi-natives.mjs [searchRoot]
30
+ */
31
+
32
+ import {
33
+ existsSync,
34
+ readdirSync,
35
+ readFileSync,
36
+ realpathSync,
37
+ renameSync,
38
+ rmSync,
39
+ statSync,
40
+ writeFileSync,
41
+ } from "node:fs";
42
+ import { dirname, join, resolve } from "node:path";
43
+ import { fileURLToPath, pathToFileURL } from "node:url";
44
+
45
+ const PKG_SUBPATH = join("node_modules", "@oh-my-pi", "pi-natives");
46
+ const TARGET_REL = join("native", "loader-state.js");
47
+ // Exact buggy token; \b keeps an already-fixed \`import.meta.dirname\` intact.
48
+ const BUGGY = /import\.meta\.dir\b/g;
49
+ const PATCHED_LITERAL = "import.meta.dirname";
50
+ const PKG_NAME = "@oh-my-pi/pi-natives";
51
+
52
+ function log(message) {
53
+ console.log("[patch-pi-natives] " + message);
54
+ }
55
+
56
+ function warn(message) {
57
+ console.warn("[patch-pi-natives] " + message);
58
+ }
59
+
60
+ /** Walk upwards from start, yielding every candidate package directory. */
61
+ function* candidatePackageDirs(start) {
62
+ let current = resolve(start);
63
+ for (;;) {
64
+ const candidate = join(current, PKG_SUBPATH);
65
+ if (existsSync(candidate)) yield candidate;
66
+ const parent = dirname(current);
67
+ if (parent === current) return;
68
+ current = parent;
69
+ }
70
+ }
71
+
72
+ function firstMatchedFile(packageDir) {
73
+ const direct = join(packageDir, TARGET_REL);
74
+ if (existsSync(direct)) return direct;
75
+ const stack = [packageDir];
76
+ while (stack.length > 0) {
77
+ const entry = stack.pop();
78
+ for (const item of readdirSync(entry)) {
79
+ const full = join(entry, item);
80
+ const stat = statSync(full);
81
+ if (stat.isDirectory()) stack.push(full);
82
+ else if (item === "loader-state.js") return full;
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ /** Walk up from a resolved entry until the pi-natives package root. */
89
+ function packageRootFor(entry) {
90
+ let dir = dirname(entry);
91
+ for (;;) {
92
+ const manifest = join(dir, "package.json");
93
+ if (existsSync(manifest)) {
94
+ try {
95
+ const pkg = JSON.parse(readFileSync(manifest, "utf8"));
96
+ if (pkg.name === PKG_NAME) return dir;
97
+ } catch {
98
+ // Keep walking: a malformed package.json is not our target.
99
+ }
100
+ }
101
+ const parent = dirname(dir);
102
+ if (parent === dir) return undefined;
103
+ dir = parent;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * @param {string | undefined} searchRoot
109
+ * @param {boolean} preferExact
110
+ * @returns {{ file: string | undefined, inspected: number }}
111
+ */
112
+ function resolveTarget(searchRoot, preferExact) {
113
+ if (preferExact) {
114
+ try {
115
+ const entry = fileURLToPath(import.meta.resolve(PKG_NAME));
116
+ const packageRoot = packageRootFor(entry);
117
+ if (packageRoot !== undefined) {
118
+ const direct = join(packageRoot, TARGET_REL);
119
+ if (existsSync(direct)) return { file: direct, inspected: 1 };
120
+ }
121
+ } catch {
122
+ // Fall through to the ancestor walk.
123
+ }
124
+ }
125
+
126
+ const start = searchRoot ? resolve(searchRoot) : dirname(fileURLToPath(import.meta.url));
127
+ let inspected = 0;
128
+ for (const packageDir of candidatePackageDirs(start)) {
129
+ inspected += 1;
130
+ const file = firstMatchedFile(packageDir);
131
+ if (file !== undefined) return { file, inspected };
132
+ }
133
+ return { file: undefined, inspected };
134
+ }
135
+
136
+ /**
137
+ * Replace the target with a private inode. rename() removes this path's link
138
+ * to the old inode (the store and other profiles keep theirs); the temp file
139
+ * becomes the new content.
140
+ */
141
+ function atomicReplace(file, content) {
142
+ const real = realpathSync(file);
143
+ const mode = statSync(real).mode;
144
+ const tmp = real + "." + process.pid + "." + Date.now() + ".tmp";
145
+ writeFileSync(tmp, content, { mode });
146
+ try {
147
+ renameSync(tmp, real);
148
+ } catch (error) {
149
+ const code = error && error.code;
150
+ if (code !== "EEXIST" && code !== "EPERM" && code !== "EXDEV") throw error;
151
+ rmSync(real, { force: true });
152
+ renameSync(tmp, real);
153
+ } finally {
154
+ rmSync(tmp, { force: true });
155
+ }
156
+ return real;
157
+ }
158
+
159
+ /**
160
+ * @param {{
161
+ * target?: string,
162
+ * searchRoot?: string,
163
+ * detach?: boolean,
164
+ * copyOnPatch?: boolean,
165
+ * }} [options]
166
+ * @returns {{ inspected: number, patched: number, detached: number, files: string[], error?: string }}
167
+ */
168
+ export function ensurePiNativesPatched(options = {}) {
169
+ const { target, searchRoot, detach = true, copyOnPatch = true } = options;
170
+
171
+ try {
172
+ let found;
173
+ if (target) {
174
+ found = { file: existsSync(target) ? target : undefined, inspected: existsSync(target) ? 1 : 0 };
175
+ } else {
176
+ found = resolveTarget(searchRoot, searchRoot === undefined);
177
+ }
178
+
179
+ if (found.file === undefined) {
180
+ return {
181
+ inspected: found.inspected,
182
+ patched: 0,
183
+ detached: 0,
184
+ files: [],
185
+ error: "no " + PKG_NAME + "/" + TARGET_REL + " found",
186
+ };
187
+ }
188
+
189
+ const real = realpathSync(found.file);
190
+ const source = readFileSync(real, "utf8");
191
+ const next = source.replace(BUGGY, PATCHED_LITERAL);
192
+ const patched = next !== source;
193
+ const shared = statSync(real).nlink > 1;
194
+ const shouldDetach = !patched && detach && shared;
195
+
196
+ if (!patched && !shouldDetach) {
197
+ return { inspected: found.inspected, patched: 0, detached: 0, files: [real] };
198
+ }
199
+
200
+ if (copyOnPatch) atomicReplace(real, next);
201
+ else writeFileSync(real, next);
202
+
203
+ return {
204
+ inspected: found.inspected,
205
+ patched: patched ? 1 : 0,
206
+ detached: shouldDetach ? 1 : 0,
207
+ files: [real],
208
+ };
209
+ } catch (error) {
210
+ return {
211
+ inspected: 0,
212
+ patched: 0,
213
+ detached: 0,
214
+ files: [],
215
+ error: error && error.message ? error.message : String(error),
216
+ };
217
+ }
218
+ }
219
+
220
+ function main() {
221
+ const searchRoot = process.argv[2] ? resolve(process.argv[2]) : undefined;
222
+ const result = ensurePiNativesPatched({ detach: true, searchRoot });
223
+ if (result.error) {
224
+ warn("WARN " + result.error);
225
+ return;
226
+ }
227
+ for (const file of result.files) {
228
+ if (result.patched > 0) log(file + ": patched import.meta.dir -> import.meta.dirname");
229
+ else if (result.detached > 0) log(file + ": detached hardlink (private copy)");
230
+ else log(file + ": already patched");
231
+ }
232
+ log("done: patched=" + result.patched + ", detached=" + result.detached);
233
+ }
234
+
235
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
236
+ try {
237
+ main();
238
+ } catch (error) {
239
+ warn("WARN self-heal skipped: " + (error && error.message ? error.message : String(error)));
240
+ }
241
+ }