@xiaoso/dsh-tool-plus 0.1.0-beta.7 → 0.1.2-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 +13 -5
- package/README.md +13 -5
- package/lib/client.js +151 -20
- package/lib/index.mjs +350 -19
- package/lib/{scrapers-DrSYgPyf.mjs → scrapers-B3AO6nqq.mjs} +1 -1
- package/lib/trash-cli.mjs +281 -0
- package/lib/{utils-DSEEKy3G.mjs → utils-DvRrWOyM.mjs} +1 -1
- package/package.json +2 -1
- package/presets/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[简体中文](./README.md) | English
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@xiaoso/dsh-tool-plus)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
[]()
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
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.
|
|
8
11
|
|
|
9
12
|
## Features
|
|
10
13
|
|
|
11
14
|
- **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
|
|
15
|
+
- **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
|
|
12
16
|
- **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
|
|
13
17
|
- **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
|
|
14
18
|
- **edit**: `replace` by default, with patch / hashline / apply-patch formats also supported; multi-hunk edits, uniqueness validation, fuzzy matching tolerant of whitespace differences
|
|
@@ -16,6 +20,10 @@ Essential tools enhancement for DeepSeek Harness: persistent bash, structured re
|
|
|
16
20
|
- **ast_grep / ast_edit** (optional): syntax-tree based structural code search and rewrite, enabled in settings
|
|
17
21
|
- **agent presets**: two companion templates — Standard enhanced and PTC (Code Mode) — installable in one command (see Installation)
|
|
18
22
|
|
|
23
|
+
Settings panel (Bash tab):
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
19
27
|
## Recommended Environment & Configuration
|
|
20
28
|
|
|
21
29
|
- **Full-access mode**: the plugin is best used under `danger-full-access`, which avoids file writes being blocked by mistake in sandboxed modes.
|
|
@@ -30,12 +38,12 @@ Install **both** the plugin and the presets:
|
|
|
30
38
|
### Install from npm (recommended)
|
|
31
39
|
|
|
32
40
|
```sh
|
|
33
|
-
dsh plugin --profile web add @xiaoso/dsh-tool-plus
|
|
41
|
+
dsh plugin --profile web add @xiaoso/dsh-tool-plus
|
|
34
42
|
```
|
|
35
43
|
|
|
36
44
|
### Install from GitHub
|
|
37
45
|
|
|
38
|
-
Track the latest development build
|
|
46
|
+
Track the latest development build:
|
|
39
47
|
|
|
40
48
|
```sh
|
|
41
49
|
dsh plugin --profile web add github:xiaoso456/dsh-tool-plus
|
|
@@ -52,7 +60,7 @@ dsh plugin --profile web add link:<path to this repo>
|
|
|
52
60
|
**Option 1 · Install with the npx script**
|
|
53
61
|
|
|
54
62
|
```sh
|
|
55
|
-
npx @xiaoso/dsh-tool-plus-presets
|
|
63
|
+
npx @xiaoso/dsh-tool-plus-presets
|
|
56
64
|
```
|
|
57
65
|
|
|
58
66
|
**Option 2 · Have an AI session configure the presets** (no dependency on the package above) — paste the following into an AI session that can edit files on your machine:
|
package/README.md
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
简体中文 | [English](./README.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@xiaoso/dsh-tool-plus)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
[]()
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
DeepSeek Harness 基础工具增强:持久 bash、结构化 read、多模式 edit、原子 write、全文搜索、图像直读,一个插件全覆盖。基于 [Oh My Pi](https://github.com/can1357/oh-my-pi) 内核移植,安装后自动接管官方 bash / pwsh / 文件 / 搜索工具,另有可选的 `ast_grep` / `ast_edit` 结构化搜索与重写。
|
|
8
11
|
|
|
9
12
|
## 功能特性
|
|
10
13
|
|
|
11
14
|
- **bash**:持久 shell,`cd`、`export` 跨调用保持状态;冗长日志(git/npm/cargo…)自动压缩;超长输出只保留头尾,完整内容落盘可回读;长命令自动转后台;可选拦截 `cat`、`grep`、`find`、`sed -i`,引导改用专用工具
|
|
15
|
+
- **安全删除**:`rm` 默认把文件移入系统回收站而非永久删除,误删可恢复;需要真删时用 `command rm`
|
|
12
16
|
- **read**:按行区间精准读取(支持 `:N-M`、`:raw`、多区间);大代码文件默认返回结构摘要,细节按需展开;zip/tar 归档、SQLite、notebook、PDF 直接读;PNG / JPEG / WebP / GIF 图片直读,超大图自动缩放;可抓取网页内容(含网页内图片);SPA 站点(内容由 JS 动态渲染,如 excalidraw.com)自动改用本机浏览器渲染后抓取
|
|
13
17
|
- **write**:原子写入,返回修改 diff;支持补丁式写入,可直接写 zip/tar 归档成员与 SQLite 数据
|
|
14
18
|
- **edit**:replace 默认,另支持 patch / hashline / apply-patch 三种补丁格式;多段编辑、唯一性校验、空白差异模糊匹配
|
|
@@ -16,6 +20,10 @@ DeepSeek Harness 基础工具增强:持久 bash、结构化 read、多模式 e
|
|
|
16
20
|
- **ast_grep / ast_edit**(可选):基于语法树的结构化代码搜索与重写,可在设置中开启
|
|
17
21
|
- **agent 预设**:标准增强版 / PTC(Code Mode)两套配套模板,一条命令安装(见安装章节)
|
|
18
22
|
|
|
23
|
+
设置面板(Bash 工具页):
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
19
27
|
## 推荐环境与配置
|
|
20
28
|
|
|
21
29
|
- **完全权限模式**:建议在 `danger-full-access` 下使用本插件,可避免沙箱模式下文件写入被误拦。
|
|
@@ -30,12 +38,12 @@ DeepSeek Harness 基础工具增强:持久 bash、结构化 read、多模式 e
|
|
|
30
38
|
### 从 npm 安装(推荐)
|
|
31
39
|
|
|
32
40
|
```sh
|
|
33
|
-
dsh plugin --profile web add @xiaoso/dsh-tool-plus
|
|
41
|
+
dsh plugin --profile web add @xiaoso/dsh-tool-plus
|
|
34
42
|
```
|
|
35
43
|
|
|
36
44
|
### 从 GitHub 安装
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
跟踪最新开发版:
|
|
39
47
|
|
|
40
48
|
```sh
|
|
41
49
|
dsh plugin --profile web add github:xiaoso456/dsh-tool-plus
|
|
@@ -52,7 +60,7 @@ dsh plugin --profile web add link:<本仓库路径>
|
|
|
52
60
|
**方法一 · 使用 npx 脚本安装**
|
|
53
61
|
|
|
54
62
|
```sh
|
|
55
|
-
npx @xiaoso/dsh-tool-plus-presets
|
|
63
|
+
npx @xiaoso/dsh-tool-plus-presets
|
|
56
64
|
```
|
|
57
65
|
|
|
58
66
|
**方法二 · 使用大模型配置预设**(不依赖上面的包)——把下面这段发给一个能改你电脑上文件的 AI 会话:
|
package/lib/client.js
CHANGED
|
@@ -22,6 +22,7 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
editMode: "groupEditMode",
|
|
23
23
|
guard: "groupGuard",
|
|
24
24
|
grep: "groupGrep",
|
|
25
|
+
searchDefaults: "groupSearchDefaults",
|
|
25
26
|
ast: "groupAst"
|
|
26
27
|
};
|
|
27
28
|
/**
|
|
@@ -75,6 +76,15 @@ window.__ModuleLoader__.load({
|
|
|
75
76
|
group: "behavior",
|
|
76
77
|
tool: "bash"
|
|
77
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: "rmSafe",
|
|
81
|
+
kind: "boolean",
|
|
82
|
+
default: true,
|
|
83
|
+
labelKey: "rmSafe",
|
|
84
|
+
hintKey: "rmSafeHint",
|
|
85
|
+
group: "behavior",
|
|
86
|
+
tool: "bash"
|
|
87
|
+
},
|
|
78
88
|
{
|
|
79
89
|
name: "useShellCommandWrapper",
|
|
80
90
|
kind: "boolean",
|
|
@@ -663,6 +673,42 @@ window.__ModuleLoader__.load({
|
|
|
663
673
|
group: "grep",
|
|
664
674
|
tool: "grep"
|
|
665
675
|
},
|
|
676
|
+
{
|
|
677
|
+
name: "grepCaseDefault",
|
|
678
|
+
kind: "boolean",
|
|
679
|
+
default: true,
|
|
680
|
+
labelKey: "grepCaseDefault",
|
|
681
|
+
hintKey: "grepCaseDefaultHint",
|
|
682
|
+
group: "searchDefaults",
|
|
683
|
+
tool: "grep"
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: "grepGitignoreDefault",
|
|
687
|
+
kind: "boolean",
|
|
688
|
+
default: true,
|
|
689
|
+
labelKey: "grepGitignoreDefault",
|
|
690
|
+
hintKey: "grepGitignoreDefaultHint",
|
|
691
|
+
group: "searchDefaults",
|
|
692
|
+
tool: "grep"
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "globGitignoreDefault",
|
|
696
|
+
kind: "boolean",
|
|
697
|
+
default: true,
|
|
698
|
+
labelKey: "globGitignoreDefault",
|
|
699
|
+
hintKey: "globGitignoreDefaultHint",
|
|
700
|
+
group: "searchDefaults",
|
|
701
|
+
tool: "glob"
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
name: "globHiddenDefault",
|
|
705
|
+
kind: "boolean",
|
|
706
|
+
default: true,
|
|
707
|
+
labelKey: "globHiddenDefault",
|
|
708
|
+
hintKey: "globHiddenDefaultHint",
|
|
709
|
+
group: "searchDefaults",
|
|
710
|
+
tool: "glob"
|
|
711
|
+
},
|
|
666
712
|
{
|
|
667
713
|
name: "astGrepEnabled",
|
|
668
714
|
kind: "boolean",
|
|
@@ -704,6 +750,11 @@ window.__ModuleLoader__.load({
|
|
|
704
750
|
labelKey: "tabGrep",
|
|
705
751
|
fields: TOOL_PLUS_FIELDS.filter((f) => f.tool === "grep").map((f) => f.name)
|
|
706
752
|
},
|
|
753
|
+
{
|
|
754
|
+
id: "glob",
|
|
755
|
+
labelKey: "tabGlob",
|
|
756
|
+
fields: TOOL_PLUS_FIELDS.filter((f) => f.tool === "glob").map((f) => f.name)
|
|
757
|
+
},
|
|
707
758
|
{
|
|
708
759
|
id: "astGrep",
|
|
709
760
|
labelKey: "tabAstGrep",
|
|
@@ -745,7 +796,6 @@ window.__ModuleLoader__.load({
|
|
|
745
796
|
const SETTINGS_ROWS_CSS = `
|
|
746
797
|
.tp-row{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1);transition:background .16s ease}
|
|
747
798
|
.tp-row:last-child{border-bottom:none}
|
|
748
|
-
.tp-rowClickable{cursor:pointer}
|
|
749
799
|
.tp-rowText{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;padding-right:24px}
|
|
750
800
|
.tp-rowTitle{font-size:14px;font-weight:400;line-height:22px;color:var(--dsw-alias-label-primary)}
|
|
751
801
|
.tp-rowDesc{font-size:12px;font-weight:400;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
@@ -824,18 +874,16 @@ window.__ModuleLoader__.load({
|
|
|
824
874
|
}
|
|
825
875
|
/**
|
|
826
876
|
* Number row: same left-text / right-control anatomy as toggle and select
|
|
827
|
-
* rows, with the input right-aligned at a fixed width. The
|
|
828
|
-
* the input (htmlFor)
|
|
877
|
+
* rows, with the input right-aligned at a fixed width. The title is wired to
|
|
878
|
+
* the input (htmlFor), so clicking the title focuses the input; the
|
|
879
|
+
* description and the rest of the row are inert — each side manages its own
|
|
880
|
+
* interaction.
|
|
829
881
|
*/
|
|
830
882
|
function NumberRow(props) {
|
|
831
883
|
const { t, control, disabled, onEdit, onReset } = props;
|
|
832
884
|
const inputId = (0, react.useId)();
|
|
833
885
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
834
|
-
className: "tp-row"
|
|
835
|
-
onClick: (event) => {
|
|
836
|
-
if (event.target.closest("input,button,select,textarea,a")) return;
|
|
837
|
-
event.currentTarget.querySelector(".tp-input")?.focus();
|
|
838
|
-
},
|
|
886
|
+
className: "tp-row",
|
|
839
887
|
children: [
|
|
840
888
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
841
889
|
className: "tp-rowText",
|
|
@@ -883,11 +931,7 @@ window.__ModuleLoader__.load({
|
|
|
883
931
|
}));
|
|
884
932
|
const close = () => onOpenChange(null);
|
|
885
933
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
886
|
-
className: "tp-row"
|
|
887
|
-
onClick: (event) => {
|
|
888
|
-
if (event.target.closest("input,button,select,textarea,a")) return;
|
|
889
|
-
event.currentTarget.querySelector(".tp-selectTrigger")?.click();
|
|
890
|
-
},
|
|
934
|
+
className: "tp-row",
|
|
891
935
|
children: [
|
|
892
936
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
893
937
|
className: "tp-rowText",
|
|
@@ -941,11 +985,7 @@ window.__ModuleLoader__.load({
|
|
|
941
985
|
function ToggleRow(props) {
|
|
942
986
|
const { t, control, disabled, onToggle, onReset } = props;
|
|
943
987
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
944
|
-
className: "tp-row"
|
|
945
|
-
onClick: (event) => {
|
|
946
|
-
if (event.target.closest("input,button,select,textarea,a")) return;
|
|
947
|
-
event.currentTarget.querySelector(".tp-switch input")?.click();
|
|
948
|
-
},
|
|
988
|
+
className: "tp-row",
|
|
949
989
|
children: [
|
|
950
990
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
951
991
|
className: "tp-rowText",
|
|
@@ -1641,6 +1681,8 @@ window.__ModuleLoader__.load({
|
|
|
1641
1681
|
*/
|
|
1642
1682
|
/** Logical RPC channel registered by the host half of this plugin. */
|
|
1643
1683
|
const TOOL_PLUS_RPC_CHANNEL = "/tool-plus";
|
|
1684
|
+
/** Endpoint that reports the rmSafe injection status (query = ensure + verify). */
|
|
1685
|
+
const RM_SAFE_STATUS_ENDPOINT = "rmSafe/status";
|
|
1644
1686
|
//#endregion
|
|
1645
1687
|
//#region src/client/web-connection-rpc.ts
|
|
1646
1688
|
/**
|
|
@@ -2029,6 +2071,27 @@ window.__ModuleLoader__.load({
|
|
|
2029
2071
|
};
|
|
2030
2072
|
}
|
|
2031
2073
|
//#endregion
|
|
2074
|
+
//#region src/client/rm-safe-status-text.ts
|
|
2075
|
+
/** Locale key of one rmSafe failure reason. */
|
|
2076
|
+
function rmSafeReasonKey(reason) {
|
|
2077
|
+
switch (reason) {
|
|
2078
|
+
case "snapshot-unavailable": return "rmSafeReasonSnapshotUnavailable";
|
|
2079
|
+
case "cli-missing": return "rmSafeReasonCliMissing";
|
|
2080
|
+
case "script-write-failed": return "rmSafeReasonScriptWriteFailed";
|
|
2081
|
+
case "snapshot-write-failed": return "rmSafeReasonSnapshotWriteFailed";
|
|
2082
|
+
case "runtime-not-effective": return "rmSafeReasonRuntimeNotEffective";
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Localized text of one rmSafe/status result. `disabled` maps to the empty
|
|
2087
|
+
* string — the caller decides not to surface it.
|
|
2088
|
+
*/
|
|
2089
|
+
function rmSafeStatusText(t, value) {
|
|
2090
|
+
if (value.status === "failed") return t("rmSafeStatusFailed").replace("{reason}", t(rmSafeReasonKey(value.reason)));
|
|
2091
|
+
if (value.status === "disabled") return "";
|
|
2092
|
+
return value.runtime === "function" ? t("rmSafeStatusInjected") : t("rmSafeStatusInjectedUnverified");
|
|
2093
|
+
}
|
|
2094
|
+
//#endregion
|
|
2032
2095
|
//#region src/client/ToolPlusSection.tsx
|
|
2033
2096
|
/**
|
|
2034
2097
|
* Tool Plus settings section — the plugin's own page in the Settings panel,
|
|
@@ -2272,6 +2335,27 @@ window.__ModuleLoader__.load({
|
|
|
2272
2335
|
const [openSelect, setOpenSelect] = (0, react.useState)(null);
|
|
2273
2336
|
const [actionState, setActionState] = (0, react.useState)({});
|
|
2274
2337
|
const [actionResults, setActionResults] = (0, react.useState)({});
|
|
2338
|
+
const [rmSafeToast, setRmSafeToast] = (0, react.useState)(null);
|
|
2339
|
+
const handleSave = (0, react.useCallback)(async () => {
|
|
2340
|
+
const before = Boolean(scope.getSnapshot().value?.rmSafe ?? true);
|
|
2341
|
+
await form.actions.save();
|
|
2342
|
+
const after = Boolean(scope.getSnapshot().value?.rmSafe ?? true);
|
|
2343
|
+
if (before === after || !after) return;
|
|
2344
|
+
try {
|
|
2345
|
+
const result = await createWebConnectionRpc().call(TOOL_PLUS_RPC_CHANNEL, RM_SAFE_STATUS_ENDPOINT, {});
|
|
2346
|
+
if (!result.ok) return;
|
|
2347
|
+
const value = result.value;
|
|
2348
|
+
if (value.status === "disabled") return;
|
|
2349
|
+
setRmSafeToast({
|
|
2350
|
+
text: rmSafeStatusText(t, value),
|
|
2351
|
+
ok: value.status === "injected"
|
|
2352
|
+
});
|
|
2353
|
+
} catch {}
|
|
2354
|
+
}, [
|
|
2355
|
+
scope,
|
|
2356
|
+
form,
|
|
2357
|
+
t
|
|
2358
|
+
]);
|
|
2275
2359
|
const runAction = (0, react.useCallback)((field) => {
|
|
2276
2360
|
const actionKey = field.actionKey;
|
|
2277
2361
|
if (!actionKey) return;
|
|
@@ -2520,10 +2604,17 @@ window.__ModuleLoader__.load({
|
|
|
2520
2604
|
type: "button",
|
|
2521
2605
|
className: "tps-save",
|
|
2522
2606
|
disabled: blocked,
|
|
2523
|
-
onClick: () => void
|
|
2607
|
+
onClick: () => void handleSave(),
|
|
2524
2608
|
children: form.shell.saving ? t("saving") : t("save")
|
|
2525
2609
|
})
|
|
2526
2610
|
]
|
|
2611
|
+
}),
|
|
2612
|
+
rmSafeToast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
2613
|
+
text: rmSafeToast.text,
|
|
2614
|
+
icon: rmSafeToast.ok ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}),
|
|
2615
|
+
onDone: () => {
|
|
2616
|
+
setRmSafeToast(null);
|
|
2617
|
+
}
|
|
2527
2618
|
})
|
|
2528
2619
|
]
|
|
2529
2620
|
});
|
|
@@ -2541,6 +2632,7 @@ window.__ModuleLoader__.load({
|
|
|
2541
2632
|
tabRead: "Read",
|
|
2542
2633
|
tabWriteEdit: "Write & Edit",
|
|
2543
2634
|
tabGrep: "Grep",
|
|
2635
|
+
tabGlob: "Glob",
|
|
2544
2636
|
tabAstGrep: "Ast Grep",
|
|
2545
2637
|
tabAstEdit: "Ast Edit",
|
|
2546
2638
|
tabReadImage: "Read Image",
|
|
@@ -2627,11 +2719,20 @@ window.__ModuleLoader__.load({
|
|
|
2627
2719
|
grepContextBeforeHint: "每个匹配前展示的上下文行数。",
|
|
2628
2720
|
grepContextAfter: "匹配后上下文行数",
|
|
2629
2721
|
grepContextAfterHint: "每个匹配后展示的上下文行数。",
|
|
2722
|
+
grepCaseDefault: "大小写敏感(默认)",
|
|
2723
|
+
grepCaseDefaultHint: "开启(默认)时按大小写严格匹配;关闭后忽略大小写。调用时显式传 case 参数会覆盖此默认。",
|
|
2724
|
+
grepGitignoreDefault: "跳过被忽略的文件(默认)",
|
|
2725
|
+
grepGitignoreDefaultHint: "开启(默认)时,.gitignore 里列出的文件/目录会自动跳过,不出现在搜索结果里;关闭后它们也会被搜到。调用时显式传 gitignore 参数会覆盖此默认。",
|
|
2726
|
+
globGitignoreDefault: "跳过被忽略的文件(默认)",
|
|
2727
|
+
globGitignoreDefaultHint: "开启(默认)时,.gitignore 里列出的文件/目录会自动跳过,不会出现在结果里;关闭后它们也会被列出。调用时显式传 gitignore 参数会覆盖此默认。",
|
|
2728
|
+
globHiddenDefault: "包含隐藏文件(默认)",
|
|
2729
|
+
globHiddenDefaultHint: "开启(默认)时以 . 开头的文件/目录(如 .env、.git)也会出现在结果里;关闭后自动跳过。调用时显式传 hidden 参数会覆盖此默认。",
|
|
2630
2730
|
astGrepEnabled: "启用 ast_grep",
|
|
2631
2731
|
astGrepEnabledHint: "启用 ast_grep 工具做结构化 AST 搜索(默认关闭)。",
|
|
2632
2732
|
astEditEnabled: "启用 ast_edit",
|
|
2633
2733
|
astEditEnabledHint: "启用 ast_edit 工具做结构化 AST 重写(默认开启)。",
|
|
2634
2734
|
groupGrep: "匹配上下文",
|
|
2735
|
+
groupSearchDefaults: "搜索默认值",
|
|
2635
2736
|
groupAst: "AST 工具",
|
|
2636
2737
|
optReplace: "replace(字面量替换)",
|
|
2637
2738
|
optPatch: "patch(unified diff)",
|
|
@@ -2663,6 +2764,16 @@ window.__ModuleLoader__.load({
|
|
|
2663
2764
|
nonInteractiveEnvHint: "对非交互使用加固环境:关闭分页器与彩色输出,不弹交互提示。",
|
|
2664
2765
|
snapshotEnabled: "Shell 快照",
|
|
2665
2766
|
snapshotEnabledHint: "把用户的 rc 文件(别名、函数、选项)快照进会话 shell。",
|
|
2767
|
+
rmSafe: "安全 rm",
|
|
2768
|
+
rmSafeHint: "在会话 shell 里重定义 rm:删除的文件移入系统回收站而非永久删除;command rm 仍走真删除。",
|
|
2769
|
+
rmSafeStatusInjected: "安全 rm 已注入并验证:rm 进入回收站",
|
|
2770
|
+
rmSafeStatusInjectedUnverified: "安全 rm 已注入(运行时验证不可用)",
|
|
2771
|
+
rmSafeStatusFailed: "安全 rm 注入失败:{reason},rm 将永久删除",
|
|
2772
|
+
rmSafeReasonSnapshotUnavailable: "快照不可用",
|
|
2773
|
+
rmSafeReasonCliMissing: "trash-cli 产物缺失",
|
|
2774
|
+
rmSafeReasonScriptWriteFailed: "脚本写入失败",
|
|
2775
|
+
rmSafeReasonSnapshotWriteFailed: "快照不可写",
|
|
2776
|
+
rmSafeReasonRuntimeNotEffective: "运行时未生效",
|
|
2666
2777
|
useShellCommandWrapper: "用 bash -c 包裹命令",
|
|
2667
2778
|
useShellCommandWrapperHint: "把每条命令用 bash -c 包裹,以获得完整的 bash 环境。",
|
|
2668
2779
|
outputTruncateStrategy: "截断按",
|
|
@@ -2707,11 +2818,12 @@ window.__ModuleLoader__.load({
|
|
|
2707
2818
|
description: "Persistent shell tool: timeouts, backgrounding, interception, and environment.",
|
|
2708
2819
|
nav: "Tool Plus",
|
|
2709
2820
|
pageTitle: "Tool Plus tool configuration",
|
|
2710
|
-
pageDescription: "Switch by tool to adjust its global settings; values apply
|
|
2821
|
+
pageDescription: "Switch by tool to adjust its global settings; values apply …",
|
|
2711
2822
|
tabBash: "Bash",
|
|
2712
2823
|
tabRead: "Read",
|
|
2713
2824
|
tabWriteEdit: "Write & Edit",
|
|
2714
2825
|
tabGrep: "Grep",
|
|
2826
|
+
tabGlob: "Glob",
|
|
2715
2827
|
tabAstGrep: "Ast Grep",
|
|
2716
2828
|
tabAstEdit: "Ast Edit",
|
|
2717
2829
|
tabReadImage: "Read Image",
|
|
@@ -2798,11 +2910,20 @@ window.__ModuleLoader__.load({
|
|
|
2798
2910
|
grepContextBeforeHint: "Context lines shown before each match.",
|
|
2799
2911
|
grepContextAfter: "Context lines after match",
|
|
2800
2912
|
grepContextAfterHint: "Context lines shown after each match.",
|
|
2913
|
+
grepCaseDefault: "Case-sensitive (default)",
|
|
2914
|
+
grepCaseDefaultHint: "When on (default), matches strictly by case; when off, case is ignored. An explicit case argument on the call overrides this default.",
|
|
2915
|
+
grepGitignoreDefault: "Skip ignored files (default)",
|
|
2916
|
+
grepGitignoreDefaultHint: "When on (default), files and directories listed in .gitignore are skipped and never appear in results; when off, they are searched too. An explicit gitignore argument on the call overrides this default.",
|
|
2917
|
+
globGitignoreDefault: "Skip ignored files (default)",
|
|
2918
|
+
globGitignoreDefaultHint: "When on (default), files and directories listed in .gitignore are skipped and never appear in results; when off, they are listed too. An explicit gitignore argument on the call overrides this default.",
|
|
2919
|
+
globHiddenDefault: "Include hidden files (default)",
|
|
2920
|
+
globHiddenDefaultHint: "When on (default), dot-files and dot-directories (e.g. .env, .git) appear in results; when off, they are skipped. An explicit hidden argument on the call overrides this default.",
|
|
2801
2921
|
astGrepEnabled: "Enable ast_grep",
|
|
2802
2922
|
astGrepEnabledHint: "Enable the ast_grep tool for structural AST search (off by default).",
|
|
2803
2923
|
astEditEnabled: "Enable ast_edit",
|
|
2804
2924
|
astEditEnabledHint: "Enable the ast_edit tool for structural AST rewrites (on by default).",
|
|
2805
2925
|
groupGrep: "Match context",
|
|
2926
|
+
groupSearchDefaults: "Search defaults",
|
|
2806
2927
|
groupAst: "AST tools",
|
|
2807
2928
|
optReplace: "replace (literal)",
|
|
2808
2929
|
optPatch: "patch (unified diff)",
|
|
@@ -2834,6 +2955,16 @@ window.__ModuleLoader__.load({
|
|
|
2834
2955
|
nonInteractiveEnvHint: "Harden the environment for non-interactive use: no pagers, no prompts, no color.",
|
|
2835
2956
|
snapshotEnabled: "Shell snapshot",
|
|
2836
2957
|
snapshotEnabledHint: "Capture the user's rc file (aliases, functions, options) into the session shell.",
|
|
2958
|
+
rmSafe: "Safe rm",
|
|
2959
|
+
rmSafeHint: "Redefine rm in the session shell: deleted files move to the system trash instead of being permanently removed; `command rm` still deletes for real.",
|
|
2960
|
+
rmSafeStatusInjected: "Safe rm injected and verified: rm moves to trash",
|
|
2961
|
+
rmSafeStatusInjectedUnverified: "Safe rm injected (runtime verification unavailable)",
|
|
2962
|
+
rmSafeStatusFailed: "Safe rm injection failed: {reason}, rm deletes permanently",
|
|
2963
|
+
rmSafeReasonSnapshotUnavailable: "snapshot unavailable",
|
|
2964
|
+
rmSafeReasonCliMissing: "trash-cli binary missing",
|
|
2965
|
+
rmSafeReasonScriptWriteFailed: "script write failed",
|
|
2966
|
+
rmSafeReasonSnapshotWriteFailed: "snapshot not writable",
|
|
2967
|
+
rmSafeReasonRuntimeNotEffective: "not effective at runtime",
|
|
2837
2968
|
useShellCommandWrapper: "Wrap commands in bash -c",
|
|
2838
2969
|
useShellCommandWrapperHint: "Wrap every command in bash -c for a full bash environment.",
|
|
2839
2970
|
outputTruncateStrategy: "Truncate by",
|
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-Bx0fpxuk.mjs";
|
|
4
4
|
import { i as setRuntimeLogger, n as systemChromiumCandidatesForTest, r as runtimeLogger } from "./launch-DwpE70Pv.mjs";
|
|
5
5
|
import { $ as peekFile, A as formatAge, C as error, F as $envpos, I as $flag, M as formatCount, P as $env, Q as isProbablyBinary, S as debug, T as warn, U as getAutoQaDbPath, V as VERSION, X as getRemoteDir, Y as getPuppeteerDir, b as isEnotdir, c as combineSignals, d as $which, j as formatBytes$1, k as installBunShim, l as exec, p as readImageMetadata, q as getInstallId, tt as untilAborted, u as render, v as hasFsCode, y as isEnoent } from "./src-D_e1qMwi.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-
|
|
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-DvRrWOyM.mjs";
|
|
7
7
|
import { t as sanitizeText } from "./sanitize-text-CfcXzw5X.mjs";
|
|
8
8
|
import { n as isRecord$2 } from "./type-guards-B9VVjRDK.mjs";
|
|
9
9
|
import { n as ToolError, r as throwIfAborted, t as ToolAbortError } from "./tool-errors-DoyxivAR.mjs";
|
|
@@ -719,7 +719,26 @@ async function getOrCreateSnapshot(shell, env, timeoutMs = SNAPSHOT_TIMEOUT_MS)
|
|
|
719
719
|
const cached = cachedSnapshotPaths.get(cacheKey);
|
|
720
720
|
if (cached && fs.existsSync(cached)) return cached;
|
|
721
721
|
if (cached) cachedSnapshotPaths.delete(cacheKey);
|
|
722
|
-
if (process.platform === "win32")
|
|
722
|
+
if (process.platform === "win32") {
|
|
723
|
+
const uid = process.getuid?.();
|
|
724
|
+
const snapshotDir = path.join(os.tmpdir(), uid === void 0 ? "omp-shell-snapshots" : `omp-shell-snapshots-${uid}`);
|
|
725
|
+
const snapshotPath = path.join(snapshotDir, `snapshot-bash-${crypto.randomUUID()}.sh`);
|
|
726
|
+
try {
|
|
727
|
+
fs.mkdirSync(snapshotDir, {
|
|
728
|
+
recursive: true,
|
|
729
|
+
mode: 448
|
|
730
|
+
});
|
|
731
|
+
fs.writeFileSync(snapshotPath, "# Shell snapshot - generated by omp agent (windows minimal)\n", { mode: 384 });
|
|
732
|
+
cachedSnapshotPaths.set(cacheKey, snapshotPath);
|
|
733
|
+
return snapshotPath;
|
|
734
|
+
} catch (err) {
|
|
735
|
+
runtimeLogger().debug("shell-snapshot: windows snapshot dir unusable", {
|
|
736
|
+
dir: snapshotDir,
|
|
737
|
+
err: String(err)
|
|
738
|
+
});
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
723
742
|
const rcFile = getShellConfigFile(shell, env);
|
|
724
743
|
const uid = process.getuid?.();
|
|
725
744
|
const snapshotDir = path.join(os.tmpdir(), uid === void 0 ? "omp-shell-snapshots" : `omp-shell-snapshots-${uid}`);
|
|
@@ -788,6 +807,85 @@ function cleanupSnapshots() {
|
|
|
788
807
|
cachedSnapshotPaths.clear();
|
|
789
808
|
}
|
|
790
809
|
//#endregion
|
|
810
|
+
//#region src/tools/bash/rm-safe.ts
|
|
811
|
+
/**
|
|
812
|
+
* rmSafe: rm 重定义脚本的生成与快照注入。
|
|
813
|
+
*
|
|
814
|
+
* 生成一个 bash 函数定义脚本(rm → `node <trash-cli.mjs> "$@"`),
|
|
815
|
+
* 通过快照文件(会话 shell 创建时 source 一次)注入:
|
|
816
|
+
* - POSIX:追加到用户环境快照末尾(幂等,带标记行);
|
|
817
|
+
* - Windows:getOrCreateSnapshot 生成"仅注入"快照文件后同样追加。
|
|
818
|
+
*
|
|
819
|
+
* rmSafe 关闭时调用方不注入,快照保持原样,系统 rm 生效。
|
|
820
|
+
* @module @xiaoso/dsh-tool-plus/bash/rm-safe
|
|
821
|
+
*/
|
|
822
|
+
/** 注入标记行:injectRmSafe 以此判断是否已注入(幂等)。 */
|
|
823
|
+
const RM_SAFE_MARKER = "# dsh-tool-plus rmSafe";
|
|
824
|
+
/** rm-safe 脚本缓存目录(tmpdir 下,跨会话复用)。 */
|
|
825
|
+
function rmSafeScriptDir() {
|
|
826
|
+
return path.join(os.tmpdir(), "dsh-bash-plus");
|
|
827
|
+
}
|
|
828
|
+
/** trash-cli 构建产物路径(lib/trash-cli.mjs,与主 bundle 同目录)。 */
|
|
829
|
+
function rmSafeCliPath() {
|
|
830
|
+
return fileURLToPath(new URL("./trash-cli.mjs", import.meta.url));
|
|
831
|
+
}
|
|
832
|
+
/** 单引号转义(bash 单引号内 `'\''` 表示一个字面单引号)。 */
|
|
833
|
+
function quote(s) {
|
|
834
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* 生成 rm 重定义脚本内容:bash 函数 `rm()` 把参数原样转交 trash-cli。
|
|
838
|
+
* 逃生口:`command rm`(bash 的 command 关键字跳过函数/alias 查找)或
|
|
839
|
+
* 绝对路径(/usr/bin/rm)走系统真删除。注意 `\rm` 反斜杠只抑制 alias
|
|
840
|
+
* 展开、不抑制函数查找,对函数重定义无效——不是逃生口。
|
|
841
|
+
*/
|
|
842
|
+
function rmSafeScript(nodePath, cliPath) {
|
|
843
|
+
return [
|
|
844
|
+
RM_SAFE_MARKER,
|
|
845
|
+
"rm() {",
|
|
846
|
+
` ${quote(nodePath)} ${quote(cliPath)} "$@"`,
|
|
847
|
+
"}",
|
|
848
|
+
""
|
|
849
|
+
].join("\n");
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* 确保 rm-safe 脚本存在(内容不变时幂等,不重写文件)。
|
|
853
|
+
* 返回脚本路径;目录/写入失败时返回 null(不抛异常,调用方降级并告警)。
|
|
854
|
+
*/
|
|
855
|
+
function ensureRmSafeScript(dir, nodePath, cliPath) {
|
|
856
|
+
const target = path.join(dir, "rm-safe.sh");
|
|
857
|
+
const content = rmSafeScript(nodePath, cliPath);
|
|
858
|
+
try {
|
|
859
|
+
if (fs.readFileSync(target, "utf8") === content) return target;
|
|
860
|
+
} catch {}
|
|
861
|
+
try {
|
|
862
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
863
|
+
fs.writeFileSync(target, content, { mode: 384 });
|
|
864
|
+
return target;
|
|
865
|
+
} catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* 向快照文件追加 `source <rm-safe.sh>`(幂等:已有标记则跳过)。
|
|
871
|
+
* 返回是否注入成功;快照文件不可读/不可写时返回 false(不抛异常)。
|
|
872
|
+
*/
|
|
873
|
+
function injectRmSafe(snapshotPath, scriptPath) {
|
|
874
|
+
let content;
|
|
875
|
+
try {
|
|
876
|
+
content = fs.readFileSync(snapshotPath, "utf8");
|
|
877
|
+
} catch {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
if (content.includes("# dsh-tool-plus rmSafe")) return true;
|
|
881
|
+
try {
|
|
882
|
+
fs.appendFileSync(snapshotPath, `\n${RM_SAFE_MARKER}\nsource ${quote(scriptPath)}\n`);
|
|
883
|
+
return true;
|
|
884
|
+
} catch {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
//#endregion
|
|
791
889
|
//#region node_modules/.pnpm/@oh-my-pi+pi-utils@17.3.5/node_modules/@oh-my-pi/pi-utils/src/path.ts
|
|
792
890
|
const WINDOWS_DRIVE_EXTENDED_PREFIX = /^\\\\[?]\\([A-Za-z]:[\\/].*)$/;
|
|
793
891
|
const WINDOWS_UNC_EXTENDED_PREFIX = /^\\\\[?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
|
|
@@ -2325,7 +2423,19 @@ async function executeBash(command, options) {
|
|
|
2325
2423
|
const baseShellConfig = getShellConfig();
|
|
2326
2424
|
const { shell, args, env: shellEnv, prefix } = options?.useUserShell === true ? resolveUserShellConfig(baseShellConfig) : baseShellConfig;
|
|
2327
2425
|
const bashShell = isBashShell(shell);
|
|
2328
|
-
|
|
2426
|
+
let snapshotPath = bashShell && options?.snapshotEnabled !== false ? await getOrCreateSnapshot(shell, shellEnv) : null;
|
|
2427
|
+
let rmSafeInjectionFailed = false;
|
|
2428
|
+
if (snapshotPath !== null && options?.rmSafe === true) {
|
|
2429
|
+
const scriptPath = ensureRmSafeScript(rmSafeScriptDir(), process.execPath, rmSafeCliPath());
|
|
2430
|
+
if (scriptPath === null || !injectRmSafe(snapshotPath, scriptPath)) {
|
|
2431
|
+
rmSafeInjectionFailed = true;
|
|
2432
|
+
runtimeLogger().warn("rmSafe: injection failed, rm will delete permanently", {
|
|
2433
|
+
scriptPath: scriptPath ?? null,
|
|
2434
|
+
snapshotPath
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
const injectionFailedMarker = rmSafeInjectionFailed ? { rmSafeInjectionFailed: true } : {};
|
|
2329
2439
|
const minimizerSettings = options?.minimizerSettings ?? DEFAULT_MINIMIZER_SETTINGS;
|
|
2330
2440
|
const minimizer = options?.minimizerEnabled ?? minimizerSettings.enabled ? buildMinimizerOptions(minimizerSettings) : void 0;
|
|
2331
2441
|
const commandCwd = resolveShellCwd(options?.cwd);
|
|
@@ -2354,6 +2464,7 @@ async function executeBash(command, options) {
|
|
|
2354
2464
|
if (options?.signal?.aborted) return {
|
|
2355
2465
|
exitCode: void 0,
|
|
2356
2466
|
cancelled: true,
|
|
2467
|
+
...injectionFailedMarker,
|
|
2357
2468
|
...await settle("Command cancelled")
|
|
2358
2469
|
};
|
|
2359
2470
|
const shellOptions = {
|
|
@@ -2432,6 +2543,7 @@ async function executeBash(command, options) {
|
|
|
2432
2543
|
return {
|
|
2433
2544
|
exitCode: void 0,
|
|
2434
2545
|
cancelled: true,
|
|
2546
|
+
...injectionFailedMarker,
|
|
2435
2547
|
...winner.kind === "timeout" ? { timedOut: true } : {},
|
|
2436
2548
|
...await settle(winner.kind === "timeout" && deadlineTimeoutMs !== void 0 ? `Command timed out after ${Math.round(deadlineTimeoutMs / 1e3)} seconds` : "Command cancelled")
|
|
2437
2549
|
};
|
|
@@ -2448,6 +2560,7 @@ async function executeBash(command, options) {
|
|
|
2448
2560
|
exitCode: void 0,
|
|
2449
2561
|
cancelled: true,
|
|
2450
2562
|
timedOut: true,
|
|
2563
|
+
...injectionFailedMarker,
|
|
2451
2564
|
...await settle(annotation)
|
|
2452
2565
|
};
|
|
2453
2566
|
}
|
|
@@ -2457,6 +2570,7 @@ async function executeBash(command, options) {
|
|
|
2457
2570
|
return {
|
|
2458
2571
|
exitCode: void 0,
|
|
2459
2572
|
cancelled: true,
|
|
2573
|
+
...injectionFailedMarker,
|
|
2460
2574
|
...await settle("Command cancelled")
|
|
2461
2575
|
};
|
|
2462
2576
|
}
|
|
@@ -2473,6 +2587,7 @@ async function executeBash(command, options) {
|
|
|
2473
2587
|
return {
|
|
2474
2588
|
exitCode: winner.result.exitCode,
|
|
2475
2589
|
cancelled: false,
|
|
2590
|
+
...injectionFailedMarker,
|
|
2476
2591
|
minimized: minimized && minimized.text !== minimized.originalText ? {
|
|
2477
2592
|
filter: minimized.filter,
|
|
2478
2593
|
inputBytes: minimized.inputBytes,
|
|
@@ -2643,6 +2758,7 @@ function startBashJob(options) {
|
|
|
2643
2758
|
headBytes: config.outputSinkHeadBytes,
|
|
2644
2759
|
useShellCommandWrapper: config.useShellCommandWrapper,
|
|
2645
2760
|
snapshotEnabled: config.snapshotEnabled,
|
|
2761
|
+
rmSafe: config.rmSafe,
|
|
2646
2762
|
nonInteractiveEnv: config.nonInteractiveEnv,
|
|
2647
2763
|
artifactPath: spillPath,
|
|
2648
2764
|
onMinimizedSave: (originalText) => saveOriginalText(originalText),
|
|
@@ -3166,6 +3282,7 @@ function renderBashResult(value) {
|
|
|
3166
3282
|
const truncation = formatTruncationNotice(value.output);
|
|
3167
3283
|
if (truncation !== void 0) notices.push(...truncation);
|
|
3168
3284
|
else if (value.output.spillPath !== void 0) notices.push(`[full raw stream saved: ${value.output.spillPath}]`);
|
|
3285
|
+
if (value.rmSafeInjectionFailed === true) notices.push("[Safe rm injection failed: rm deletes permanently — use `command rm` to bypass]");
|
|
3169
3286
|
const markers = [];
|
|
3170
3287
|
if (value.timedOut) markers.push(`[timed out after ${value.timeoutMs ?? "?"}ms]`);
|
|
3171
3288
|
else if (value.exitCode !== 0) markers.push(`[exit code: ${value.exitCode}]`);
|
|
@@ -3229,6 +3346,15 @@ const TOOL_PLUS_FIELDS = [
|
|
|
3229
3346
|
group: "behavior",
|
|
3230
3347
|
tool: "bash"
|
|
3231
3348
|
},
|
|
3349
|
+
{
|
|
3350
|
+
name: "rmSafe",
|
|
3351
|
+
kind: "boolean",
|
|
3352
|
+
default: true,
|
|
3353
|
+
labelKey: "rmSafe",
|
|
3354
|
+
hintKey: "rmSafeHint",
|
|
3355
|
+
group: "behavior",
|
|
3356
|
+
tool: "bash"
|
|
3357
|
+
},
|
|
3232
3358
|
{
|
|
3233
3359
|
name: "useShellCommandWrapper",
|
|
3234
3360
|
kind: "boolean",
|
|
@@ -3817,6 +3943,42 @@ const TOOL_PLUS_FIELDS = [
|
|
|
3817
3943
|
group: "grep",
|
|
3818
3944
|
tool: "grep"
|
|
3819
3945
|
},
|
|
3946
|
+
{
|
|
3947
|
+
name: "grepCaseDefault",
|
|
3948
|
+
kind: "boolean",
|
|
3949
|
+
default: true,
|
|
3950
|
+
labelKey: "grepCaseDefault",
|
|
3951
|
+
hintKey: "grepCaseDefaultHint",
|
|
3952
|
+
group: "searchDefaults",
|
|
3953
|
+
tool: "grep"
|
|
3954
|
+
},
|
|
3955
|
+
{
|
|
3956
|
+
name: "grepGitignoreDefault",
|
|
3957
|
+
kind: "boolean",
|
|
3958
|
+
default: true,
|
|
3959
|
+
labelKey: "grepGitignoreDefault",
|
|
3960
|
+
hintKey: "grepGitignoreDefaultHint",
|
|
3961
|
+
group: "searchDefaults",
|
|
3962
|
+
tool: "grep"
|
|
3963
|
+
},
|
|
3964
|
+
{
|
|
3965
|
+
name: "globGitignoreDefault",
|
|
3966
|
+
kind: "boolean",
|
|
3967
|
+
default: true,
|
|
3968
|
+
labelKey: "globGitignoreDefault",
|
|
3969
|
+
hintKey: "globGitignoreDefaultHint",
|
|
3970
|
+
group: "searchDefaults",
|
|
3971
|
+
tool: "glob"
|
|
3972
|
+
},
|
|
3973
|
+
{
|
|
3974
|
+
name: "globHiddenDefault",
|
|
3975
|
+
kind: "boolean",
|
|
3976
|
+
default: true,
|
|
3977
|
+
labelKey: "globHiddenDefault",
|
|
3978
|
+
hintKey: "globHiddenDefaultHint",
|
|
3979
|
+
group: "searchDefaults",
|
|
3980
|
+
tool: "glob"
|
|
3981
|
+
},
|
|
3820
3982
|
{
|
|
3821
3983
|
name: "astGrepEnabled",
|
|
3822
3984
|
kind: "boolean",
|
|
@@ -3836,7 +3998,7 @@ const TOOL_PLUS_FIELDS = [
|
|
|
3836
3998
|
tool: "astEdit"
|
|
3837
3999
|
}
|
|
3838
4000
|
];
|
|
3839
|
-
TOOL_PLUS_FIELDS.filter((f) => f.tool === "bash").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "read").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "writeEdit").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "grep").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "astGrep").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "astEdit").map((f) => f.name);
|
|
4001
|
+
TOOL_PLUS_FIELDS.filter((f) => f.tool === "bash").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "read").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "writeEdit").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "grep").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "glob").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "astGrep").map((f) => f.name), TOOL_PLUS_FIELDS.filter((f) => f.tool === "astEdit").map((f) => f.name);
|
|
3840
4002
|
/** Look up one field definition by its schema key. */
|
|
3841
4003
|
function toolPlusField(name) {
|
|
3842
4004
|
return TOOL_PLUS_FIELDS.find((field) => field.name === name);
|
|
@@ -3895,6 +4057,7 @@ const Config = z.object({
|
|
|
3895
4057
|
interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled", true)),
|
|
3896
4058
|
nonInteractiveEnv: z.boolean().default(fieldDefault("nonInteractiveEnv", true)),
|
|
3897
4059
|
snapshotEnabled: z.boolean().default(fieldDefault("snapshotEnabled", true)),
|
|
4060
|
+
rmSafe: z.boolean().default(fieldDefault("rmSafe", true)),
|
|
3898
4061
|
useShellCommandWrapper: z.boolean().default(fieldDefault("useShellCommandWrapper", false)),
|
|
3899
4062
|
maxBackgroundJobs: z.number().default(fieldDefault("maxBackgroundJobs", 15)),
|
|
3900
4063
|
outputTruncateStrategy: z.union(["bytes", "lines"]).default(fieldDefault("outputTruncateStrategy", "bytes")),
|
|
@@ -3937,6 +4100,10 @@ const Config = z.object({
|
|
|
3937
4100
|
readConcurrentSafe: z.boolean().default(fieldDefault("readConcurrentSafe", true)),
|
|
3938
4101
|
grepContextBefore: z.number().default(fieldDefault("grepContextBefore", 1)),
|
|
3939
4102
|
grepContextAfter: z.number().default(fieldDefault("grepContextAfter", 3)),
|
|
4103
|
+
grepCaseDefault: z.boolean().default(fieldDefault("grepCaseDefault", true)),
|
|
4104
|
+
grepGitignoreDefault: z.boolean().default(fieldDefault("grepGitignoreDefault", true)),
|
|
4105
|
+
globGitignoreDefault: z.boolean().default(fieldDefault("globGitignoreDefault", true)),
|
|
4106
|
+
globHiddenDefault: z.boolean().default(fieldDefault("globHiddenDefault", true)),
|
|
3940
4107
|
astGrepEnabled: z.boolean().default(fieldDefault("astGrepEnabled", false)),
|
|
3941
4108
|
astEditEnabled: z.boolean().default(fieldDefault("astEditEnabled", true)),
|
|
3942
4109
|
fetchEnabled: z.boolean().default(fieldDefault("fetchEnabled", true)),
|
|
@@ -3987,6 +4154,7 @@ function resolveConfig(config) {
|
|
|
3987
4154
|
interceptorEnabled: config.interceptorEnabled ?? fieldDefault("interceptorEnabled", true),
|
|
3988
4155
|
nonInteractiveEnv: config.nonInteractiveEnv ?? fieldDefault("nonInteractiveEnv", true),
|
|
3989
4156
|
snapshotEnabled: config.snapshotEnabled ?? fieldDefault("snapshotEnabled", true),
|
|
4157
|
+
rmSafe: config.rmSafe ?? fieldDefault("rmSafe", true),
|
|
3990
4158
|
useShellCommandWrapper: config.useShellCommandWrapper ?? fieldDefault("useShellCommandWrapper", false),
|
|
3991
4159
|
maxBackgroundJobs: config.maxBackgroundJobs ?? fieldDefault("maxBackgroundJobs", 15),
|
|
3992
4160
|
outputTruncate: {
|
|
@@ -4022,6 +4190,10 @@ function resolveConfig(config) {
|
|
|
4022
4190
|
readConcurrentSafe: config.readConcurrentSafe ?? fieldDefault("readConcurrentSafe", true),
|
|
4023
4191
|
grepContextBefore: config.grepContextBefore ?? fieldDefault("grepContextBefore", 1),
|
|
4024
4192
|
grepContextAfter: config.grepContextAfter ?? fieldDefault("grepContextAfter", 3),
|
|
4193
|
+
grepCaseDefault: config.grepCaseDefault ?? fieldDefault("grepCaseDefault", true),
|
|
4194
|
+
grepGitignoreDefault: config.grepGitignoreDefault ?? fieldDefault("grepGitignoreDefault", true),
|
|
4195
|
+
globGitignoreDefault: config.globGitignoreDefault ?? fieldDefault("globGitignoreDefault", true),
|
|
4196
|
+
globHiddenDefault: config.globHiddenDefault ?? fieldDefault("globHiddenDefault", true),
|
|
4025
4197
|
astGrepEnabled: config.astGrepEnabled ?? fieldDefault("astGrepEnabled", false),
|
|
4026
4198
|
astEditEnabled: config.astEditEnabled ?? fieldDefault("astEditEnabled", true),
|
|
4027
4199
|
fetchEnabled: config.fetchEnabled ?? fieldDefault("fetchEnabled", true),
|
|
@@ -4196,27 +4368,145 @@ function probeBrowsers(env = defaultBrowserDetectEnv()) {
|
|
|
4196
4368
|
return found;
|
|
4197
4369
|
}
|
|
4198
4370
|
//#endregion
|
|
4371
|
+
//#region src/tools/bash/rm-safe-status.ts
|
|
4372
|
+
/**
|
|
4373
|
+
* rmSafe 注入状态查询:文件层(快照标记 + 脚本 + trash-cli 产物)与
|
|
4374
|
+
* 运行时层(一次性 bash 会话 source 快照后探测 `rm` 是否被重定义)。
|
|
4375
|
+
*
|
|
4376
|
+
* 分层语义:
|
|
4377
|
+
* - 文件层成功只证明"注入已写入快照",不证明"会话里生效"——快照是会话
|
|
4378
|
+
* 创建时 source 的,注入只对之后创建的会话生效;trash-cli 产物缺失或
|
|
4379
|
+
* node 路径不可用时函数调用会直接失败。
|
|
4380
|
+
* - 运行时层用一次性 bash 会话模拟"新会话创建"(source 快照 + `type rm`),
|
|
4381
|
+
* 验证的正是注入承诺的语义:新会话里 `rm` 是函数而非系统命令。探测
|
|
4382
|
+
* 无副作用,不触碰任何真实会话。
|
|
4383
|
+
* @module @xiaoso/dsh-tool-plus/bash/rm-safe-status
|
|
4384
|
+
*/
|
|
4385
|
+
/**
|
|
4386
|
+
* 解析 `type rm` / `declare -f rm` 探测输出。
|
|
4387
|
+
* `rm is a function` → 函数重定义生效;其余(`rm is /usr/bin/rm`、
|
|
4388
|
+
* `rm is hashed (...)`、空输出)→ 系统命令。
|
|
4389
|
+
*/
|
|
4390
|
+
function parseProbeOutput(stdout) {
|
|
4391
|
+
return /rm is a function/.test(stdout) ? "function" : "system";
|
|
4392
|
+
}
|
|
4393
|
+
/**
|
|
4394
|
+
* Windows 路径转 Git Bash 形式(`C:\Users\x` → `/c/Users/x`)。
|
|
4395
|
+
* 非 Windows 原样返回;非盘符路径仅把反斜杠转正斜杠。
|
|
4396
|
+
*/
|
|
4397
|
+
function toGitBashPath(p) {
|
|
4398
|
+
if (process.platform !== "win32") return p;
|
|
4399
|
+
const drive = /^([A-Za-z]):[\\/](.*)$/.exec(p);
|
|
4400
|
+
if (drive === null) return p.replace(/\\/g, "/");
|
|
4401
|
+
return `/${drive[1].toLowerCase()}/${drive[2].replace(/\\/g, "/")}`;
|
|
4402
|
+
}
|
|
4403
|
+
/** 默认 spawn 实现:收集 stdout,超时 kill 并标记 timedOut。 */
|
|
4404
|
+
const defaultSpawnDeps = { spawnShell: (shell, args, timeoutMs) => new Promise((resolve, reject) => {
|
|
4405
|
+
const child = spawn(shell, args, { stdio: [
|
|
4406
|
+
"ignore",
|
|
4407
|
+
"pipe",
|
|
4408
|
+
"pipe"
|
|
4409
|
+
] });
|
|
4410
|
+
let stdout = "";
|
|
4411
|
+
let timedOut = false;
|
|
4412
|
+
const timer = setTimeout(() => {
|
|
4413
|
+
timedOut = true;
|
|
4414
|
+
child.kill();
|
|
4415
|
+
}, timeoutMs);
|
|
4416
|
+
child.stdout.on("data", (chunk) => {
|
|
4417
|
+
stdout += chunk.toString();
|
|
4418
|
+
});
|
|
4419
|
+
child.on("error", (error) => {
|
|
4420
|
+
clearTimeout(timer);
|
|
4421
|
+
reject(error);
|
|
4422
|
+
});
|
|
4423
|
+
child.on("close", () => {
|
|
4424
|
+
clearTimeout(timer);
|
|
4425
|
+
resolve({
|
|
4426
|
+
stdout,
|
|
4427
|
+
timedOut
|
|
4428
|
+
});
|
|
4429
|
+
});
|
|
4430
|
+
}) };
|
|
4431
|
+
/**
|
|
4432
|
+
* 运行时探测:在一次性 bash 会话里 source 快照(模拟新会话创建),
|
|
4433
|
+
* 然后 `type rm` + `declare -f rm` 看 `rm` 是否被重定义为函数。
|
|
4434
|
+
* spawn 失败或超时 → 'unknown'(探测不可用,不代表注入失败)。
|
|
4435
|
+
*/
|
|
4436
|
+
async function probeRmSafeRuntime(shell, snapshotPath, deps = defaultSpawnDeps, timeoutMs = 5e3) {
|
|
4437
|
+
const script = `source ${quote(toGitBashPath(snapshotPath))}; type rm; declare -f rm`;
|
|
4438
|
+
try {
|
|
4439
|
+
const { stdout, timedOut } = await deps.spawnShell(shell, ["-c", script], timeoutMs);
|
|
4440
|
+
if (timedOut) return "unknown";
|
|
4441
|
+
return parseProbeOutput(stdout);
|
|
4442
|
+
} catch {
|
|
4443
|
+
return "unknown";
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
/**
|
|
4447
|
+
* 查询 rmSafe 注入状态(查询即确保注入:幂等,与 executeBash 的注入
|
|
4448
|
+
* 路径一致)。rmSafe 关闭时调用方不应调用本函数(返回 disabled 由
|
|
4449
|
+
* 调用方短路);此处仍兜底返回 disabled 语义由调用方保证。
|
|
4450
|
+
*/
|
|
4451
|
+
async function queryRmSafeStatus(deps) {
|
|
4452
|
+
const snapshotPath = await deps.getOrCreateSnapshot();
|
|
4453
|
+
if (snapshotPath === null) return {
|
|
4454
|
+
status: "failed",
|
|
4455
|
+
reason: "snapshot-unavailable"
|
|
4456
|
+
};
|
|
4457
|
+
if (!deps.cliExists()) return {
|
|
4458
|
+
status: "failed",
|
|
4459
|
+
reason: "cli-missing"
|
|
4460
|
+
};
|
|
4461
|
+
const scriptPath = deps.ensureScript();
|
|
4462
|
+
if (scriptPath === null) return {
|
|
4463
|
+
status: "failed",
|
|
4464
|
+
reason: "script-write-failed"
|
|
4465
|
+
};
|
|
4466
|
+
if (!deps.inject(snapshotPath, scriptPath)) return {
|
|
4467
|
+
status: "failed",
|
|
4468
|
+
reason: "snapshot-write-failed"
|
|
4469
|
+
};
|
|
4470
|
+
const runtime = await deps.probe(snapshotPath);
|
|
4471
|
+
if (runtime === "system") return {
|
|
4472
|
+
status: "failed",
|
|
4473
|
+
reason: "runtime-not-effective"
|
|
4474
|
+
};
|
|
4475
|
+
return {
|
|
4476
|
+
status: "injected",
|
|
4477
|
+
runtime
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
//#endregion
|
|
4199
4481
|
//#region src/host/browser-probe-rpc.ts
|
|
4200
4482
|
/**
|
|
4201
|
-
*
|
|
4483
|
+
* Host half of the tool-plus RPC channel: registers the `/tool-plus` logical
|
|
4484
|
+
* channel on the official Connection transport and answers
|
|
4485
|
+
* `browser/detect` (probe the machine for usable browsers) and
|
|
4486
|
+
* `rmSafe/status` (query = ensure + verify the rmSafe injection). The
|
|
4487
|
+
* settings panel (client half) calls these through
|
|
4488
|
+
* `createWebConnectionRpc().call(...)` — the documented public RPC surface
|
|
4489
|
+
* of `@deepseek-ai/dsh-client-connection`, not a private plugin hook.
|
|
4490
|
+
*
|
|
4491
|
+
* Trust policy is `loopback`: the probe only reports local filesystem paths,
|
|
4492
|
+
* so it must never be reachable from a non-loopback host. Registration is
|
|
4493
|
+
* best-effort — if `connection` is missing (non-web deployments) or the
|
|
4494
|
+
* channel is already taken, the endpoints are simply unavailable and the
|
|
4495
|
+
* panel shows "Unavailable".
|
|
4496
|
+
* @module @xiaoso/dsh-tool-plus/host/browser-probe-rpc
|
|
4497
|
+
*/
|
|
4498
|
+
/**
|
|
4499
|
+
* Install the tool-plus RPC endpoints. Returns a disposer that removes
|
|
4202
4500
|
* the registration (call it from the plugin's `ctx.effect` teardown).
|
|
4203
4501
|
* Never throws for "unavailable" — the return value tells the caller whether
|
|
4204
4502
|
* the channel is actually live.
|
|
4205
4503
|
*/
|
|
4206
|
-
function installBrowserProbeRpc(ctx) {
|
|
4504
|
+
function installBrowserProbeRpc(ctx, deps) {
|
|
4207
4505
|
let disposer;
|
|
4208
4506
|
ctx.inject(["connection"], (connectionCtx) => {
|
|
4209
4507
|
try {
|
|
4210
4508
|
const handler = async (endpoint, _payload, _signal) => {
|
|
4211
|
-
if (endpoint
|
|
4212
|
-
ok: false,
|
|
4213
|
-
error: {
|
|
4214
|
-
code: "bad-request",
|
|
4215
|
-
message: `Unknown tool-plus endpoint: ${endpoint}`,
|
|
4216
|
-
details: { issues: [] }
|
|
4217
|
-
}
|
|
4218
|
-
};
|
|
4219
|
-
try {
|
|
4509
|
+
if (endpoint === "browser/detect") try {
|
|
4220
4510
|
return {
|
|
4221
4511
|
ok: true,
|
|
4222
4512
|
value: { found: probeBrowsers() }
|
|
@@ -4231,6 +4521,42 @@ function installBrowserProbeRpc(ctx) {
|
|
|
4231
4521
|
}
|
|
4232
4522
|
};
|
|
4233
4523
|
}
|
|
4524
|
+
if (endpoint === "rmSafe/status") {
|
|
4525
|
+
if (!deps.getRmSafe()) return {
|
|
4526
|
+
ok: true,
|
|
4527
|
+
value: { status: "disabled" }
|
|
4528
|
+
};
|
|
4529
|
+
try {
|
|
4530
|
+
const { shell, env } = getShellConfig();
|
|
4531
|
+
return {
|
|
4532
|
+
ok: true,
|
|
4533
|
+
value: await queryRmSafeStatus({
|
|
4534
|
+
getOrCreateSnapshot: () => getOrCreateSnapshot(shell, env),
|
|
4535
|
+
cliExists: () => fs.existsSync(rmSafeCliPath()),
|
|
4536
|
+
ensureScript: () => ensureRmSafeScript(rmSafeScriptDir(), process.execPath, rmSafeCliPath()),
|
|
4537
|
+
inject: (snapshotPath, scriptPath) => injectRmSafe(snapshotPath, scriptPath),
|
|
4538
|
+
probe: (snapshotPath) => probeRmSafeRuntime(shell, snapshotPath)
|
|
4539
|
+
})
|
|
4540
|
+
};
|
|
4541
|
+
} catch (error) {
|
|
4542
|
+
return {
|
|
4543
|
+
ok: false,
|
|
4544
|
+
error: {
|
|
4545
|
+
code: "internal",
|
|
4546
|
+
message: error instanceof Error ? error.message : String(error),
|
|
4547
|
+
details: {}
|
|
4548
|
+
}
|
|
4549
|
+
};
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
return {
|
|
4553
|
+
ok: false,
|
|
4554
|
+
error: {
|
|
4555
|
+
code: "bad-request",
|
|
4556
|
+
message: `Unknown tool-plus endpoint: ${endpoint}`,
|
|
4557
|
+
details: { issues: [] }
|
|
4558
|
+
}
|
|
4559
|
+
};
|
|
4234
4560
|
};
|
|
4235
4561
|
disposer = connectionCtx.connection.rpc.handle(TOOL_PLUS_RPC_CHANNEL, handler, { authority: "loopback" });
|
|
4236
4562
|
} catch {
|
|
@@ -14555,7 +14881,7 @@ let specialHandlersPromise;
|
|
|
14555
14881
|
* requires a special handler, so we keep them out of the cold-startup graph.
|
|
14556
14882
|
*/
|
|
14557
14883
|
function loadSpecialHandlers() {
|
|
14558
|
-
specialHandlersPromise ??= import("./scrapers-
|
|
14884
|
+
specialHandlersPromise ??= import("./scrapers-B3AO6nqq.mjs").then((m) => m.specialHandlers);
|
|
14559
14885
|
return specialHandlersPromise;
|
|
14560
14886
|
}
|
|
14561
14887
|
/**
|
|
@@ -24309,7 +24635,9 @@ async function executeGrepTool(exec, cfg, args, ctx) {
|
|
|
24309
24635
|
const params = { pattern: String(args.pattern ?? "") };
|
|
24310
24636
|
if (args.path !== void 0 && args.path !== null && args.path !== "") params.path = String(args.path);
|
|
24311
24637
|
if (typeof args.case === "boolean") params.case = args.case;
|
|
24638
|
+
else params.case = cfg.grepCaseDefault;
|
|
24312
24639
|
if (typeof args.gitignore === "boolean") params.gitignore = args.gitignore;
|
|
24640
|
+
else params.gitignore = cfg.grepGitignoreDefault;
|
|
24313
24641
|
if (typeof args.skip === "number") params.skip = args.skip;
|
|
24314
24642
|
return toGrepToolResult(await tool.execute("grep", params, exec.signal));
|
|
24315
24643
|
}
|
|
@@ -24726,8 +25054,8 @@ function toGlobToolResult(result, args) {
|
|
|
24726
25054
|
async function executeGlobTool(exec, cfg, args, ctx) {
|
|
24727
25055
|
return toGlobToolResult(await new GlobTool(createToolSession$2(exec, cfg)).execute("glob", {
|
|
24728
25056
|
path: args.path !== void 0 ? String(args.path) : void 0,
|
|
24729
|
-
hidden: args.hidden,
|
|
24730
|
-
gitignore: args.gitignore,
|
|
25057
|
+
hidden: typeof args.hidden === "boolean" ? args.hidden : cfg.globHiddenDefault,
|
|
25058
|
+
gitignore: typeof args.gitignore === "boolean" ? args.gitignore : cfg.globGitignoreDefault,
|
|
24731
25059
|
limit: args.limit
|
|
24732
25060
|
}, exec.signal), args);
|
|
24733
25061
|
}
|
|
@@ -25704,6 +26032,7 @@ function buildForeground(result, wallTimeMs, timeoutMs) {
|
|
|
25704
26032
|
wallTimeMs,
|
|
25705
26033
|
...result.workingDir !== void 0 ? { workingDir: result.workingDir } : {},
|
|
25706
26034
|
...result.minimized !== void 0 ? { minimized: result.minimized } : {},
|
|
26035
|
+
...result.rmSafeInjectionFailed === true ? { rmSafeInjectionFailed: true } : {},
|
|
25707
26036
|
output: {
|
|
25708
26037
|
text: result.output,
|
|
25709
26038
|
truncated: result.truncated,
|
|
@@ -25772,7 +26101,7 @@ function apply(ctx, config = {}) {
|
|
|
25772
26101
|
cfg = current();
|
|
25773
26102
|
registerModeSensitive();
|
|
25774
26103
|
});
|
|
25775
|
-
const disposeBrowserProbe = installBrowserProbeRpc(ctx);
|
|
26104
|
+
const disposeBrowserProbe = installBrowserProbeRpc(ctx, { getRmSafe: () => cfg.rmSafe });
|
|
25776
26105
|
ctx.systemPrompt.section({
|
|
25777
26106
|
name: "tool:bash",
|
|
25778
26107
|
order: 105,
|
|
@@ -26028,6 +26357,7 @@ function apply(ctx, config = {}) {
|
|
|
26028
26357
|
headBytes: cfg.outputSinkHeadBytes,
|
|
26029
26358
|
useShellCommandWrapper: cfg.useShellCommandWrapper,
|
|
26030
26359
|
snapshotEnabled: cfg.snapshotEnabled,
|
|
26360
|
+
rmSafe: cfg.rmSafe,
|
|
26031
26361
|
nonInteractiveEnv: cfg.nonInteractiveEnv,
|
|
26032
26362
|
artifactPath: allocateSpillFile()
|
|
26033
26363
|
});
|
|
@@ -26085,6 +26415,7 @@ function apply(ctx, config = {}) {
|
|
|
26085
26415
|
headBytes: cfg.outputSinkHeadBytes,
|
|
26086
26416
|
useShellCommandWrapper: cfg.useShellCommandWrapper,
|
|
26087
26417
|
snapshotEnabled: cfg.snapshotEnabled,
|
|
26418
|
+
rmSafe: cfg.rmSafe,
|
|
26088
26419
|
nonInteractiveEnv: cfg.nonInteractiveEnv,
|
|
26089
26420
|
artifactPath: allocateSpillFile()
|
|
26090
26421
|
});
|
|
@@ -3,7 +3,7 @@ __installBunShim();
|
|
|
3
3
|
import { n as fs_promises_shim_exports } from "./fs-promises-shim-Bx0fpxuk.mjs";
|
|
4
4
|
import { B as USER_AGENT, G as getDocsRsCacheDir, N as formatNumber, P as $env, T as warn, c as combineSignals, j as formatBytes, l as exec, s as Snowflake, x as parseFrontmatter, y as isEnoent } from "./src-D_e1qMwi.mjs";
|
|
5
5
|
import { V as tryParseJson } from "./utils-fnV2zzp3.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-
|
|
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-DvRrWOyM.mjs";
|
|
7
7
|
import { n as isRecord } from "./type-guards-B9VVjRDK.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-BUO0JXO-.mjs";
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
//#region src/tools/bash/trash-cli.ts
|
|
3
|
+
const SHORT_OPTIONS = {
|
|
4
|
+
r: "recursive",
|
|
5
|
+
R: "recursive",
|
|
6
|
+
f: "force",
|
|
7
|
+
i: "interactive",
|
|
8
|
+
I: "interactive",
|
|
9
|
+
v: "verbose",
|
|
10
|
+
d: "dir"
|
|
11
|
+
};
|
|
12
|
+
const LONG_OPTIONS = {
|
|
13
|
+
"--recursive": {
|
|
14
|
+
field: "recursive",
|
|
15
|
+
value: true
|
|
16
|
+
},
|
|
17
|
+
"--force": {
|
|
18
|
+
field: "force",
|
|
19
|
+
value: true
|
|
20
|
+
},
|
|
21
|
+
"--interactive": {
|
|
22
|
+
field: "interactive",
|
|
23
|
+
value: true
|
|
24
|
+
},
|
|
25
|
+
"--verbose": {
|
|
26
|
+
field: "verbose",
|
|
27
|
+
value: true
|
|
28
|
+
},
|
|
29
|
+
"--dir": {
|
|
30
|
+
field: "dir",
|
|
31
|
+
value: true
|
|
32
|
+
},
|
|
33
|
+
"--preserve-root": {
|
|
34
|
+
field: "preserveRoot",
|
|
35
|
+
value: true
|
|
36
|
+
},
|
|
37
|
+
"--no-preserve-root": {
|
|
38
|
+
field: "preserveRoot",
|
|
39
|
+
value: false
|
|
40
|
+
},
|
|
41
|
+
"--one-file-system": {
|
|
42
|
+
field: "oneFileSystem",
|
|
43
|
+
value: true
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/** --interactive=WHEN 的合法取值(GNU rm 语义)。 */
|
|
47
|
+
const INTERACTIVE_WHEN = /* @__PURE__ */ new Set([
|
|
48
|
+
"once",
|
|
49
|
+
"always",
|
|
50
|
+
"never"
|
|
51
|
+
]);
|
|
52
|
+
/**
|
|
53
|
+
* 解析 rm 风格参数(coreutils rm 语义):
|
|
54
|
+
* - 短选项支持组合(`-rf`),`-r`/`-R`/`--recursive` 等价,`-i`/`-I` 等价;
|
|
55
|
+
* - `--` 结束符后全部视为路径(允许 `-` 开头的文件名);
|
|
56
|
+
* - `--interactive[=once|always|never]` 接受(回收站模式忽略交互);
|
|
57
|
+
* - 未知短选项 → invalid-option,未知长选项 → unrecognized-option;
|
|
58
|
+
* - 无路径 → missing-operand。
|
|
59
|
+
*/
|
|
60
|
+
function parseRmArgs(argv) {
|
|
61
|
+
const parsed = {
|
|
62
|
+
recursive: false,
|
|
63
|
+
force: false,
|
|
64
|
+
interactive: false,
|
|
65
|
+
verbose: false,
|
|
66
|
+
dir: false,
|
|
67
|
+
preserveRoot: true,
|
|
68
|
+
oneFileSystem: false,
|
|
69
|
+
paths: []
|
|
70
|
+
};
|
|
71
|
+
let afterDoubleDash = false;
|
|
72
|
+
for (const arg of argv) {
|
|
73
|
+
if (afterDoubleDash) {
|
|
74
|
+
parsed.paths.push(arg);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === "--") {
|
|
78
|
+
afterDoubleDash = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg.startsWith("--")) {
|
|
82
|
+
const eq = arg.indexOf("=");
|
|
83
|
+
const name = eq >= 0 ? arg.slice(0, eq) : arg;
|
|
84
|
+
const value = eq >= 0 ? arg.slice(eq + 1) : void 0;
|
|
85
|
+
if (name === "---presume-input-tty") continue;
|
|
86
|
+
const flag = LONG_OPTIONS[name];
|
|
87
|
+
if (flag === void 0) {
|
|
88
|
+
parsed.error = {
|
|
89
|
+
kind: "unrecognized-option",
|
|
90
|
+
option: name
|
|
91
|
+
};
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
if (value !== void 0) {
|
|
95
|
+
if (name === "--interactive") {
|
|
96
|
+
if (!INTERACTIVE_WHEN.has(value)) {
|
|
97
|
+
parsed.error = {
|
|
98
|
+
kind: "invalid-argument",
|
|
99
|
+
option: name,
|
|
100
|
+
value
|
|
101
|
+
};
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
parsed.interactive = true;
|
|
105
|
+
} else if (name === "--preserve-root") {
|
|
106
|
+
if (value === "all") parsed.preserveRoot = true;
|
|
107
|
+
else {
|
|
108
|
+
parsed.error = {
|
|
109
|
+
kind: "invalid-argument",
|
|
110
|
+
option: name,
|
|
111
|
+
value
|
|
112
|
+
};
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
parsed.error = {
|
|
117
|
+
kind: "option-argument",
|
|
118
|
+
option: name
|
|
119
|
+
};
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
} else parsed[flag.field] = flag.value;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (arg.startsWith("-") && arg.length > 1) {
|
|
126
|
+
for (const ch of arg.slice(1)) {
|
|
127
|
+
const flag = SHORT_OPTIONS[ch];
|
|
128
|
+
if (flag === void 0) {
|
|
129
|
+
parsed.error = {
|
|
130
|
+
kind: "invalid-option",
|
|
131
|
+
option: ch
|
|
132
|
+
};
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
parsed[flag] = true;
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
parsed.paths.push(arg);
|
|
140
|
+
}
|
|
141
|
+
if (parsed.paths.length === 0 && parsed.error === void 0) parsed.error = { kind: "missing-operand" };
|
|
142
|
+
return parsed;
|
|
143
|
+
}
|
|
144
|
+
/** 规范化 verbose 输出的目录尾部斜杠(`a///` → `a/`,对齐 rm)。 */
|
|
145
|
+
function normalizeTrailingSlash(p) {
|
|
146
|
+
return p.replace(/[\\/]+$/, "/");
|
|
147
|
+
}
|
|
148
|
+
function errorMessage(err) {
|
|
149
|
+
return err instanceof Error ? err.message : String(err);
|
|
150
|
+
}
|
|
151
|
+
/** 路径规范化后以 `.` 或 `..` 结尾(r-4 移植:拒绝删除)。 */
|
|
152
|
+
function isDotOrDotDot(p) {
|
|
153
|
+
const last = p.replace(/[\\/]+$/, "").split(/[\\/]/).pop();
|
|
154
|
+
return last === "." || last === "..";
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 路径是 `/` 或其同义词(`//`、`///`、符号链接/junction 指向根目录)。
|
|
158
|
+
* 根判定用 dev/ino 比较(GNU preserve-root 的精确语义):字符串比较在
|
|
159
|
+
* Windows 上会漏判(junction 指向 C:\ 时 realpath 是 "C:\\" 而非 "/"),
|
|
160
|
+
* 而 dev/ino 在 Windows 与 POSIX 上都正确。空串不是根。
|
|
161
|
+
*
|
|
162
|
+
* Windows 语义说明:Node 的 `/` 解析为当前盘根(随 cwd 变化),而 Git
|
|
163
|
+
* bash 的 `/` 是 MSYS 根——两者都只保护"字面 `/` 及其 dev/ino 等价物",
|
|
164
|
+
* 与 GNU rm 在 Git bash 的行为一致(GNU 同样不保护指向盘根的链接)。
|
|
165
|
+
*/
|
|
166
|
+
async function isRootPath(p, deps) {
|
|
167
|
+
if (p.length > 0 && p.replace(/[\\/]+$/, "") === "") return true;
|
|
168
|
+
const st = await deps.stat(p);
|
|
169
|
+
if (st === null) return false;
|
|
170
|
+
const root = await deps.stat("/");
|
|
171
|
+
return root !== null && st.dev === root.dev && st.ino === root.ino;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 执行一次 rm 语义的回收站删除。
|
|
175
|
+
*
|
|
176
|
+
* 行为对齐 coreutils rm:
|
|
177
|
+
* - 无 -r/-d 时目录报 `Is a directory` 并保留(rm4);
|
|
178
|
+
* - 不存在路径:-f 静默忽略(f-1),否则报 `No such file or directory`;
|
|
179
|
+
* - `.`/`..` 结尾路径拒绝(r-4);`/` 及同义词拒绝(r-root,preserve-root 默认开);
|
|
180
|
+
* - 符号链接按链接本身处理(lstat,dangling-symlink 移植);
|
|
181
|
+
* - trash 失败:-f 静默但退出 1,否则报 `cannot remove`;
|
|
182
|
+
* - -v 输出 `removed 'x'` / `removed directory 'x'`(r-1/d-1/v-slash);
|
|
183
|
+
* - 退出码:全部成功 0,任一失败 1。
|
|
184
|
+
*/
|
|
185
|
+
async function runTrashCli(argv, deps) {
|
|
186
|
+
const parsed = parseRmArgs(argv);
|
|
187
|
+
if (parsed.error !== void 0) {
|
|
188
|
+
if (parsed.error.kind === "missing-operand") deps.stderr("rm: missing operand");
|
|
189
|
+
else if (parsed.error.kind === "invalid-option") deps.stderr(`rm: invalid option -- '${parsed.error.option}'`);
|
|
190
|
+
else if (parsed.error.kind === "unrecognized-option") deps.stderr(`rm: unrecognized option '${parsed.error.option}'`);
|
|
191
|
+
else if (parsed.error.kind === "invalid-argument") deps.stderr(`rm: invalid argument '${parsed.error.value}' for '${parsed.error.option}'`);
|
|
192
|
+
else deps.stderr(`rm: option '${parsed.error.option}' doesn't allow an argument`);
|
|
193
|
+
deps.exit(1);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const { recursive, force, verbose, dir, preserveRoot, paths } = parsed;
|
|
197
|
+
const items = [];
|
|
198
|
+
let failed = false;
|
|
199
|
+
for (const p of paths) {
|
|
200
|
+
if (isDotOrDotDot(p)) {
|
|
201
|
+
deps.stderr(`rm: refusing to remove '.' or '..' directory: skipping '${p}'`);
|
|
202
|
+
failed = true;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (preserveRoot && await isRootPath(p, deps)) {
|
|
206
|
+
deps.stderr("rm: it is dangerous to operate recursively on '/'");
|
|
207
|
+
deps.stderr("rm: use --no-preserve-root to override this failsafe");
|
|
208
|
+
failed = true;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const st = await deps.lstat(p);
|
|
212
|
+
if (st === null) {
|
|
213
|
+
if (!force) {
|
|
214
|
+
deps.stderr(`rm: cannot remove '${p}': No such file or directory`);
|
|
215
|
+
failed = true;
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (st.isDirectory && !recursive && !dir) {
|
|
220
|
+
deps.stderr(`rm: cannot remove '${p}': Is a directory`);
|
|
221
|
+
failed = true;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
items.push({
|
|
225
|
+
path: p,
|
|
226
|
+
isDir: st.isDirectory
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (items.length > 0) try {
|
|
230
|
+
await deps.trash(items.map((item) => item.path));
|
|
231
|
+
} catch (err) {
|
|
232
|
+
deps.stderr(`rm: cannot remove '${items[0].path}': ${errorMessage(err)}`);
|
|
233
|
+
failed = true;
|
|
234
|
+
}
|
|
235
|
+
if (verbose && !failed) for (const { path: p, isDir } of items) deps.stdout(isDir ? `removed directory '${normalizeTrailingSlash(p)}'` : `removed '${p}'`);
|
|
236
|
+
deps.exit(failed ? 1 : 0);
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/tools/bash/trash-cli-main.ts
|
|
240
|
+
/**
|
|
241
|
+
* rmSafe CLI 入口(构建产物 lib/trash-cli.mjs,被注入的 bash `rm()` 函数调用)。
|
|
242
|
+
*
|
|
243
|
+
* trash 包在此动态加载:单测只 import trash-cli.ts(纯逻辑),
|
|
244
|
+
* 不会触发 trash 依赖解析。
|
|
245
|
+
* @module @xiaoso/dsh-tool-plus/bash/trash-cli-main
|
|
246
|
+
*/
|
|
247
|
+
async function main() {
|
|
248
|
+
const { default: trash } = await import("trash");
|
|
249
|
+
await runTrashCli(process.argv.slice(2), {
|
|
250
|
+
trash,
|
|
251
|
+
lstat: async (p) => {
|
|
252
|
+
try {
|
|
253
|
+
return { isDirectory: (await fs.promises.lstat(p)).isDirectory() };
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
stat: async (p) => {
|
|
259
|
+
try {
|
|
260
|
+
const st = await fs.promises.stat(p);
|
|
261
|
+
return {
|
|
262
|
+
dev: st.dev,
|
|
263
|
+
ino: st.ino
|
|
264
|
+
};
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
stdout: (s) => process.stdout.write(`${s}\n`),
|
|
270
|
+
stderr: (s) => process.stderr.write(`${s}\n`),
|
|
271
|
+
exit: (c) => {
|
|
272
|
+
process.exitCode = c;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
main().catch((err) => {
|
|
277
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
});
|
|
280
|
+
//#endregion
|
|
281
|
+
export {};
|
|
@@ -7202,7 +7202,7 @@ const settings = {
|
|
|
7202
7202
|
};
|
|
7203
7203
|
//#endregion
|
|
7204
7204
|
//#region package.json
|
|
7205
|
-
var version = "0.1.
|
|
7205
|
+
var version = "0.1.2-beta.0";
|
|
7206
7206
|
/** `.tmp` files older than this are treated as orphaned writes and swept. */
|
|
7207
7207
|
const TMP_ORPHAN_MAX_AGE_MS = 3e5;
|
|
7208
7208
|
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.
|
|
4
|
+
"version": "0.1.2-beta.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek-harness",
|
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
"@oh-my-pi/pi-utils": "17.3.5",
|
|
110
110
|
"json5": "^2.2.3",
|
|
111
111
|
"puppeteer-core": "^25.3.0",
|
|
112
|
+
"trash": "^10.1.1",
|
|
112
113
|
"yaml": "^2.9.0"
|
|
113
114
|
},
|
|
114
115
|
"devDependencies": {
|
package/presets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoso/dsh-tool-plus-presets",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2-beta.0",
|
|
4
4
|
"description": "Agent-preset installer for dsh-tool-plus: installs the standard / PTC enhanced preset templates into a DeepSeek Harness home.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|