@huanlin/dsh-plugin-better-glob 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/cordis.patch.yml +14 -0
- package/lib/index.js +469 -0
- package/lib/types/argv.d.ts +69 -0
- package/lib/types/caps.d.ts +47 -0
- package/lib/types/index.d.ts +71 -0
- package/lib/types/tool.d.ts +64 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 huanlinoto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://dshfind.com/zh/plugins/huanlinoto/dsh-plugin-better-glob"><img src="https://dshfind.com/api/card/huanlinoto/dsh-plugin-better-glob?lang=zh" alt="dsh-plugin-better-glob card"></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# dsh-plugin-better-glob
|
|
6
|
+
|
|
7
|
+
以 **per-agent 阴影**顶替 DSH 内置 `glob` 工具:自动排除无底洞目录(`node_modules`、`dist`、`build`、`.venv` 等),模型要搜这些目录时必须显式传 `include` 白名单。`grep` 不受影响,不改任何内置插件注册面。
|
|
8
|
+
|
|
9
|
+
## 为什么是 per-agent 阴影
|
|
10
|
+
|
|
11
|
+
- 工具注册表按 scope 分层解析:**agent 自己层的注册遮蔽一切继承层**(headless 的全局层、web profile 里 preset 挂载的 standing scope 层)。
|
|
12
|
+
- 全局注册在 web profile 下会输给 preset 挂载;bundle patch 够不到 preset 文件。所以唯一全覆盖的机制是监听 `agent/session-start`,把同名工具经 `agent.ctx` 注册进 agent 自己的层 —— 同时注册同名 `tool:glob` 提示词 section(同 order)遮蔽内置文案。
|
|
13
|
+
- `startup`/`resume`/`clear`/`compact` 都会触发 session-start,WeakSet 保证幂等;插件配置热重载时对存活 agent 做dispose-再注册的 resync。
|
|
14
|
+
|
|
15
|
+
## 工具行为
|
|
16
|
+
|
|
17
|
+
- `pattern`(必填)/ `path`(可选):与内置 glob 语义一致,mtime 排序、含隐藏与被 ignore 文件、100 条封顶 + spill 恢复。
|
|
18
|
+
- **`include`(可选,string[])**:白名单。include 模式的路径段命中某排除目录名 → 该目录在本调用中被放行(argv 构造期摘掉它的否定 glob);`include: ["**"]` 放行全部。include 永不过滤结果 —— `pattern` 才是匹配器。
|
|
19
|
+
- 排除的实现在 argv 构造期而非后置 glob:ripgrep 在遍历期 prune 掉被排除目录,后置 glob 救不回被剪的子树。
|
|
20
|
+
- VCS 目录(`.git/.svn/.hg/.bzr/.jj/.sl`)永远排除,不配置化。
|
|
21
|
+
- 执行底座复用内置 `@deepseek-ai/dsh-tool-fs-search` 的打包 ripgrep 二进制、subprocess 缝、错误词汇表与 spill 设施。
|
|
22
|
+
|
|
23
|
+
### 默认排除清单(`excludeDirs` 整体替换式)
|
|
24
|
+
|
|
25
|
+
`node_modules` `bower_components` `vendor` `Pods` `.yarn` `dist` `build` `out` `target` `obj` `.next` `.nuxt` `.output` `.svelte-kit` `.turbo` `.parcel-cache` `.cache` `coverage` `__pycache__` `.venv` `venv` `.tox` `.mypy_cache` `.pytest_cache` `.ruff_cache` `.gradle` `.terraform` `.idea`
|
|
26
|
+
|
|
27
|
+
### 配置
|
|
28
|
+
|
|
29
|
+
| 字段 | 默认 | 说明 |
|
|
30
|
+
|------|------|------|
|
|
31
|
+
| `excludeDirs` | 上表 | 排除目录名(裸名,不允许分隔符),整体替换默认清单 |
|
|
32
|
+
| `sampleOverCapGlobResults` | `false` | 超帽页是否按顶层条目抽样(同内置开关) |
|
|
33
|
+
| `globMaxResults` | `100` | 单次内联保留路径数,超出部分 spill |
|
|
34
|
+
| `globMetaMaxBytes` | `65536` | 卡片 meta 字节预算 |
|
|
35
|
+
| `rawOutputMaxBytes` | `20000000` | 单次解析的 rg stdout 上限 |
|
|
36
|
+
| `graceMs` / `stderrMaxBytes` / `timeoutMs` | `3000` / `65536` / `30000` | 同内置语义 |
|
|
37
|
+
|
|
38
|
+
## 开发
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pnpm install # pnpm-workspace.yaml 锚定本目录(防止向上并入 D:\Projects 的 workspace)
|
|
42
|
+
pnpm exec node scripts/relink-deps.mjs # 把 @deepseek-ai/* junction 到 ~/.dsh/source/current 的已构建包(pretest/prebuild 自动跑)
|
|
43
|
+
pnpm run typecheck # tsc --noEmit(类型经 tsconfig paths 解析 source/current 的 lib/types)
|
|
44
|
+
pnpm test # vitest:argv/meta 纯逻辑 + 真实 ToolRuntime 组合 preflight
|
|
45
|
+
pnpm run build # tsdown(lib/index.js)+ tsc(lib/types/*.d.ts)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
peer 依赖(`@deepseek-ai/cordis`、`dsh-tools`、`dsh-tool-fs-search`、`dsh-system-prompt`、`dsh-agent`)由宿主提供,均为 optional peer;`schemastery` 是直接依赖。
|
|
49
|
+
|
|
50
|
+
## 运行
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
dsh plugin --profile web add "link:D:/Projects/deepseek-harness/dsh-plugin-better-glob"
|
|
54
|
+
# 之后重启 dsh web(由人类执行)+ 浏览器硬刷新
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`link:` 引用下改源码后 `pnpm run build` 重建 `lib/` 即生效,无需重装。发布形态:GitHub `huanlinoto/dsh-plugin-better-glob`(预构建 `lib/` 入库,无 prepare)/ npm `@huanlin/dsh-plugin-better-glob`。
|
|
58
|
+
|
|
59
|
+
## 检查
|
|
60
|
+
|
|
61
|
+
- `pnpm run typecheck` — 严格模式类型门禁
|
|
62
|
+
- `pnpm test` — 40 用例:argv 构造(默认排除、include 按段提升、`**` 全提升、VCS 恒排除)、参数与配置校验、meta 投影/收窄、render 页脚;组合 preflight 用真实 `ToolRuntime` + `SystemPrompt` 验证阴影生效、全局视图不受污染、re-fire 幂等、reload resync
|
|
63
|
+
- `pnpm run build` — 产物 `lib/index.js`(peer 全 external)+ `lib/types/`
|
|
64
|
+
|
|
65
|
+
## 已知限制
|
|
66
|
+
|
|
67
|
+
- 插件停用后,已存活 agent 的阴影注册会保留到该 agent 销毁(注册挂在 agent scope 上,随其 unwind)。
|
|
68
|
+
- 配置热重载对「已在会话中的 agent」经 resync 生效;若加载器将来改为 cache-bust 重导入模块(同进程新模块实例),resync 会因 agent 层重名而失败报错(fail loud,不会静默失效)。
|
|
69
|
+
- `include` 的提升粒度是「目录名」,include 模式中目录名之后的子路径不影响遍历范围(`node_modules/a/**` 放行整个 `node_modules`);精确到子目录的放行需缩小 `excludeDirs` 配置。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# better-glob bundle layer: inserts the better-glob plugin row.
|
|
2
|
+
# Loaded by `dsh plugin --profile <name> add @huanlin/dsh-plugin-better-glob`.
|
|
3
|
+
# The `name` is the package entry; Loader resolves it from profile node_modules.
|
|
4
|
+
#
|
|
5
|
+
# No built-in rows are disabled: the plugin shadows the built-in `glob` tool
|
|
6
|
+
# (and its `tool:glob` prompt section) per agent through agent-scope
|
|
7
|
+
# registration, which beats both the host-plane row (headless) and the
|
|
8
|
+
# preset-mounted row (web) by scope-layer precedence. `grep` is untouched.
|
|
9
|
+
# Plugin defaults (excludeDirs, caps) live in the plugin's Config schema;
|
|
10
|
+
# override them here when a deployment needs different values.
|
|
11
|
+
- insert:
|
|
12
|
+
- id: better-glob
|
|
13
|
+
name: '@huanlin/dsh-plugin-better-glob'
|
|
14
|
+
config: {}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { FIRST_PARTY_SECTION_ORDER } from "@deepseek-ai/dsh-system-prompt";
|
|
2
|
+
import z from "schemastery";
|
|
3
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
import { runRipgrep, sampleAcrossTopLevel, toWorkdirRelative, trySaveFormattedResult } from "@deepseek-ai/dsh-tool-fs-search";
|
|
5
|
+
//#region src/argv.ts
|
|
6
|
+
/**
|
|
7
|
+
* Pure argument handling for the better `glob` tool: model-argument
|
|
8
|
+
* validation, include-whitelist lift computation, and ripgrep argv
|
|
9
|
+
* construction. No I/O — every function here is testable without a host.
|
|
10
|
+
* @module @huanlin/dsh-plugin-better-glob/argv
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Directory names the tool never descends into, regardless of the
|
|
14
|
+
* `excludeDirs` configuration: VCS metadata stores (the built-in tool's
|
|
15
|
+
* fixed exclusion set).
|
|
16
|
+
*/
|
|
17
|
+
const VCS_EXCLUDES = [
|
|
18
|
+
".git",
|
|
19
|
+
".svn",
|
|
20
|
+
".hg",
|
|
21
|
+
".bzr",
|
|
22
|
+
".jj",
|
|
23
|
+
".sl"
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Default bottomless directories excluded from every search: dependency
|
|
27
|
+
* installs, build outputs, framework state, caches, and virtualenvs that
|
|
28
|
+
* routinely hold hundreds of thousands of files. The `excludeDirs` config
|
|
29
|
+
* replaces this list wholesale.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_EXCLUDE_DIRS = [
|
|
32
|
+
"node_modules",
|
|
33
|
+
"bower_components",
|
|
34
|
+
"vendor",
|
|
35
|
+
"Pods",
|
|
36
|
+
".yarn",
|
|
37
|
+
"dist",
|
|
38
|
+
"build",
|
|
39
|
+
"out",
|
|
40
|
+
"target",
|
|
41
|
+
"obj",
|
|
42
|
+
".next",
|
|
43
|
+
".nuxt",
|
|
44
|
+
".output",
|
|
45
|
+
".svelte-kit",
|
|
46
|
+
".turbo",
|
|
47
|
+
".parcel-cache",
|
|
48
|
+
".cache",
|
|
49
|
+
"coverage",
|
|
50
|
+
"__pycache__",
|
|
51
|
+
".venv",
|
|
52
|
+
"venv",
|
|
53
|
+
".tox",
|
|
54
|
+
".mypy_cache",
|
|
55
|
+
".pytest_cache",
|
|
56
|
+
".ruff_cache",
|
|
57
|
+
".gradle",
|
|
58
|
+
".terraform",
|
|
59
|
+
".idea"
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Validate the value constraints the schema DSL cannot express: a non-blank
|
|
63
|
+
* `pattern`, a non-blank `path` when given, and non-blank `include` entries
|
|
64
|
+
* when given. Throws a plain `Error` (an ordinary tool argument error)
|
|
65
|
+
* otherwise.
|
|
66
|
+
* @param args - the schema-validated `glob` arguments.
|
|
67
|
+
* @returns the accepted input, unchanged.
|
|
68
|
+
*/
|
|
69
|
+
function parseGlobArgs(args) {
|
|
70
|
+
if (args.pattern.trim().length === 0) throw new Error("pattern must be a non-empty string");
|
|
71
|
+
if (args.path !== void 0 && args.path.trim().length === 0) throw new Error("path must be a non-empty string when given");
|
|
72
|
+
if (args.include !== void 0) {
|
|
73
|
+
for (const entry of args.include) if (entry.trim().length === 0) throw new Error("include entries must be non-empty strings");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
pattern: args.pattern,
|
|
77
|
+
...args.path !== void 0 ? { path: args.path } : {},
|
|
78
|
+
...args.include !== void 0 ? { include: args.include } : {}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Whether one include pattern names no concrete directory (every segment is a wildcard or empty), lifting every excluded directory. */
|
|
82
|
+
function liftsEveryDirectory(pattern) {
|
|
83
|
+
return pattern.split("/").every((segment) => segment === "**" || segment === "*" || segment.length === 0);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The excluded directory names the include whitelist lifts back into one
|
|
87
|
+
* call's search. A name lifts when an include pattern carries it as a path
|
|
88
|
+
* segment (`node_modules/**` + `/package.json` lifts `node_modules`); a
|
|
89
|
+
* pattern of only wildcard segments (`**`) lifts every candidate. Segments
|
|
90
|
+
* that name no configured exclusion are ignored.
|
|
91
|
+
*
|
|
92
|
+
* The lift happens at argv construction time, not as a later ripgrep glob:
|
|
93
|
+
* ripgrep prunes an excluded directory during traversal, so a glob emitted
|
|
94
|
+
* after the exclusion could never re-enter the pruned subtree.
|
|
95
|
+
* @param include - the call's include whitelist, when the model passed one.
|
|
96
|
+
* @param excludeDirs - the configured exclusion names.
|
|
97
|
+
* @returns the subset of `excludeDirs` the whitelist lifts.
|
|
98
|
+
*/
|
|
99
|
+
function liftedExcludeNames(include, excludeDirs) {
|
|
100
|
+
if (include === void 0) return /* @__PURE__ */ new Set();
|
|
101
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
102
|
+
for (const pattern of include) {
|
|
103
|
+
if (liftsEveryDirectory(pattern)) {
|
|
104
|
+
for (const name of excludeDirs) candidates.add(name);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const segment of pattern.split("/")) {
|
|
108
|
+
if (segment === "**" || segment === "*" || segment.length === 0) continue;
|
|
109
|
+
candidates.add(segment);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const lifted = /* @__PURE__ */ new Set();
|
|
113
|
+
for (const name of excludeDirs) if (candidates.has(name)) lifted.add(name);
|
|
114
|
+
return lifted;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the fixed `rg --files` argv for one better-`glob` call. Every
|
|
118
|
+
* model-controlled value is a plain argv element — no shell layer exists, so
|
|
119
|
+
* no quoting applies; the search root rides behind `--` so a leading-dash
|
|
120
|
+
* path can never parse as a flag. `--sort=modified` orders by modification
|
|
121
|
+
* time and `--no-ignore --hidden` searches ignored and hidden files, matching
|
|
122
|
+
* the built-in tool's contract. Each excluded name carries TWO negated globs
|
|
123
|
+
* (the bare form prunes the directory during traversal; the contents form
|
|
124
|
+
* still excludes the internals when the search root sits at or inside the
|
|
125
|
+
* directory). VCS excludes are unconditional; a lifted name omits both of
|
|
126
|
+
* its globs, which is what lets ripgrep descend into it again.
|
|
127
|
+
* @param input - the validated arguments.
|
|
128
|
+
* @param excludeDirs - the configured exclusion names.
|
|
129
|
+
* @returns the complete ripgrep argument vector (excluding the binary itself).
|
|
130
|
+
*/
|
|
131
|
+
function buildGlobArgv(input, excludeDirs) {
|
|
132
|
+
const lifted = liftedExcludeNames(input.include, excludeDirs);
|
|
133
|
+
const parts = [
|
|
134
|
+
"--files",
|
|
135
|
+
`--glob=${input.pattern}`,
|
|
136
|
+
"--sort=modified",
|
|
137
|
+
"--no-ignore",
|
|
138
|
+
"--hidden"
|
|
139
|
+
];
|
|
140
|
+
const pushExclusion = (name) => {
|
|
141
|
+
parts.push(`--glob=!**/${name}`, `--glob=!**/${name}/**`);
|
|
142
|
+
};
|
|
143
|
+
for (const name of VCS_EXCLUDES) pushExclusion(name);
|
|
144
|
+
for (const name of excludeDirs) if (!lifted.has(name)) pushExclusion(name);
|
|
145
|
+
if (input.path !== void 0) parts.push("--", input.path);
|
|
146
|
+
return parts;
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/caps.ts
|
|
150
|
+
/**
|
|
151
|
+
* Plugin configuration: the Schemastery schema, the resolved caps, and the
|
|
152
|
+
* load-time validation. Every field defaults, so a patch insert row without
|
|
153
|
+
* a `config` block boots.
|
|
154
|
+
* @module @huanlin/dsh-plugin-better-glob/caps
|
|
155
|
+
*/
|
|
156
|
+
const Config = z.object({
|
|
157
|
+
excludeDirs: z.array(z.string()).default([...DEFAULT_EXCLUDE_DIRS]).description("Bottomless directory names excluded from every glob search. Replaces the default list wholesale; VCS directories (.git etc.) are always excluded on top. include lifts a name back into one call."),
|
|
158
|
+
sampleOverCapGlobResults: z.boolean().default(false).description("Whether an over-cap glob page is sampled across top-level entries instead of taking the modification-time head."),
|
|
159
|
+
globMaxResults: z.number().default(100).description("Max paths one glob call retains inline; the complete list spills to a file past it."),
|
|
160
|
+
globMetaMaxBytes: z.number().default(65536).description("Max bytes of one glob result's serialized presentationMeta; trailing paths drop past it."),
|
|
161
|
+
rawOutputMaxBytes: z.number().default(2e7).description("Max complete raw rg stdout one call parses."),
|
|
162
|
+
graceMs: z.number().default(3e3).description("Terminate-escalation grace period (ms) for the search process."),
|
|
163
|
+
stderrMaxBytes: z.number().default(65536).description("Max bytes of the retained stderr diagnostic tail."),
|
|
164
|
+
timeoutMs: z.number().default(3e4).description("Cooperative tool-call timeout budget (ms).")
|
|
165
|
+
});
|
|
166
|
+
/** The `@deepseek-ai/dsh-timeout` MAX_TIMER_DELAY_MS (2^31-1), inlined to keep the peer set minimal. */
|
|
167
|
+
const MAX_GRACE_MS = 2147483647;
|
|
168
|
+
/** Every cap counts items/bytes/milliseconds — a positive integer, or retention and timeout arithmetic misbehaves silently. */
|
|
169
|
+
function assertPositiveInteger(name, value) {
|
|
170
|
+
if (!Number.isInteger(value) || value < 1) throw new Error(`better-glob: ${name} must be a positive integer`);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Validate the schemastery-defaulted config into caps. Fails loud at load:
|
|
174
|
+
* excludeDirs entries must be bare directory names (a separator or a blank
|
|
175
|
+
* name would silently never match a directory in the traversal globs), caps
|
|
176
|
+
* must be positive integers, and graceMs must fit a timer delay.
|
|
177
|
+
* @param config - the schemastery-defaulted plugin config.
|
|
178
|
+
* @returns the resolved caps.
|
|
179
|
+
*/
|
|
180
|
+
function resolveConfig(config) {
|
|
181
|
+
const excludeDirs = config.excludeDirs ?? [...DEFAULT_EXCLUDE_DIRS];
|
|
182
|
+
for (const name of excludeDirs) if (name.length === 0 || name.includes("/") || name.includes("\\")) throw new Error(`better-glob: excludeDirs entries must be bare directory names without path separators, got ${JSON.stringify(name)}`);
|
|
183
|
+
const maxResults = config.globMaxResults ?? 100;
|
|
184
|
+
const maxMetaBytes = config.globMetaMaxBytes ?? 65536;
|
|
185
|
+
const rawOutputMaxBytes = config.rawOutputMaxBytes ?? 2e7;
|
|
186
|
+
const graceMs = config.graceMs ?? 3e3;
|
|
187
|
+
const stderrMaxBytes = config.stderrMaxBytes ?? 65536;
|
|
188
|
+
const timeoutMs = config.timeoutMs ?? 3e4;
|
|
189
|
+
assertPositiveInteger("globMaxResults", maxResults);
|
|
190
|
+
assertPositiveInteger("globMetaMaxBytes", maxMetaBytes);
|
|
191
|
+
assertPositiveInteger("rawOutputMaxBytes", rawOutputMaxBytes);
|
|
192
|
+
assertPositiveInteger("graceMs", graceMs);
|
|
193
|
+
if (graceMs > MAX_GRACE_MS) throw new Error(`better-glob: graceMs must be no greater than ${MAX_GRACE_MS}`);
|
|
194
|
+
assertPositiveInteger("stderrMaxBytes", stderrMaxBytes);
|
|
195
|
+
assertPositiveInteger("timeoutMs", timeoutMs);
|
|
196
|
+
return {
|
|
197
|
+
excludeDirs,
|
|
198
|
+
sampleOverCapGlobResults: config.sampleOverCapGlobResults ?? false,
|
|
199
|
+
maxResults,
|
|
200
|
+
maxMetaBytes,
|
|
201
|
+
rawOutputMaxBytes,
|
|
202
|
+
graceMs,
|
|
203
|
+
stderrMaxBytes,
|
|
204
|
+
timeoutMs
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/tool.ts
|
|
209
|
+
/** The serialized UTF-8 byte size of one meta payload (the size persisted and re-sent). */
|
|
210
|
+
function metaBytes(meta) {
|
|
211
|
+
return Buffer.byteLength(JSON.stringify(meta), "utf8");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Drop trailing paths until the serialized meta fits `maxMetaBytes`, marking
|
|
215
|
+
* the result `truncated` when anything was dropped. `total` is preserved. A
|
|
216
|
+
* single path too large to fit on its own is kept: the invariant is a bounded
|
|
217
|
+
* payload wherever droppable, never an empty card that hides a real result.
|
|
218
|
+
*/
|
|
219
|
+
function capMetaBytes(meta, maxMetaBytes) {
|
|
220
|
+
if (metaBytes(meta) <= maxMetaBytes) return meta;
|
|
221
|
+
const paths = [...meta.paths];
|
|
222
|
+
while (paths.length > 1 && metaBytes({
|
|
223
|
+
...meta,
|
|
224
|
+
paths,
|
|
225
|
+
truncated: true
|
|
226
|
+
}) > maxMetaBytes) paths.pop();
|
|
227
|
+
return {
|
|
228
|
+
...meta,
|
|
229
|
+
paths,
|
|
230
|
+
truncated: true
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Project one canonical value into the bounded `presentationMeta` the search card renders. */
|
|
234
|
+
function pathsMeta(value, maxMetaBytes) {
|
|
235
|
+
return capMetaBytes({
|
|
236
|
+
shape: "paths",
|
|
237
|
+
paths: value.paths,
|
|
238
|
+
truncated: value.truncated,
|
|
239
|
+
total: value.total
|
|
240
|
+
}, maxMetaBytes);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Narrow opaque live or replayed result metadata to a {@link SearchResultView}.
|
|
244
|
+
* Malformed metadata returns `undefined` so `presentResult` falls back to the
|
|
245
|
+
* generic card instead of throwing during replay of an older or hand-edited
|
|
246
|
+
* log. A zero-result meta narrows to a valid empty card.
|
|
247
|
+
* @param meta - result metadata (the {@link PathsSearchMeta} the tool projected).
|
|
248
|
+
* @returns the search view, or `undefined` for absent or malformed metadata.
|
|
249
|
+
*/
|
|
250
|
+
function pathsViewFromMeta(meta) {
|
|
251
|
+
if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
|
|
252
|
+
const record = meta;
|
|
253
|
+
const { truncated, total } = record;
|
|
254
|
+
if (typeof truncated !== "boolean" || typeof total !== "number") return void 0;
|
|
255
|
+
if (record.shape !== "paths") return void 0;
|
|
256
|
+
const { paths } = record;
|
|
257
|
+
if (!Array.isArray(paths) || !paths.every((path) => typeof path === "string")) return void 0;
|
|
258
|
+
return {
|
|
259
|
+
card: "search",
|
|
260
|
+
shape: "paths",
|
|
261
|
+
paths,
|
|
262
|
+
truncated,
|
|
263
|
+
total
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** Format one canonical value for the Native surface: the page, plus the cap basis and recovery path when capped. */
|
|
267
|
+
function renderGlobValue(value) {
|
|
268
|
+
if (value.paths.length === 0) return "No files found";
|
|
269
|
+
const body = value.paths.join("\n");
|
|
270
|
+
if (!value.truncated) return body;
|
|
271
|
+
const basis = value.sampled ? ", sampled across top-level entries instead of taken in modification-time order" : ", the modification-time-ordered head";
|
|
272
|
+
const recovery = value.spill !== void 0 ? `Full sorted result stored at: ${value.spill.locator}. ${value.spill.hint}` : "The complete result could not be saved; narrow pattern or path to see more.";
|
|
273
|
+
return `${body}\n\n(Showing ${value.paths.length} of ${value.total} paths${basis}. ${recovery})`;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Build the better `glob` tool definition.
|
|
277
|
+
* @param ctx - the plugin context; execution reads its `subprocess` service and opportunistic `spillStore`.
|
|
278
|
+
* @param caps - the deployment's resolved caps (plugin config after defaulting).
|
|
279
|
+
* @returns the registry-ready definition; the caller registers it into agent scopes.
|
|
280
|
+
*/
|
|
281
|
+
function defineBetterGlobTool(ctx, caps) {
|
|
282
|
+
const overCapDescription = caps.sampleOverCapGlobResults ? `a larger result instead returns ${caps.maxResults} paths sampled across top-level entries` : `a larger result returns the first ${caps.maxResults} paths in modification-time order`;
|
|
283
|
+
const excludedNote = caps.excludeDirs.length > 0 ? `Bottomless directories are excluded automatically (${caps.excludeDirs.join(", ")}); pass include to search inside one.` : "No directories are excluded by configuration.";
|
|
284
|
+
return defineTool({
|
|
285
|
+
name: "glob",
|
|
286
|
+
description: `Find files whose paths match a glob pattern. Returns matching file paths — never directories — in modification-time order, including hidden and ignored files (VCS metadata directories are always excluded). Up to ${caps.maxResults} paths come back inline; ${overCapDescription}, says so, and reports where the complete sorted list was saved. This tool does not enumerate directory entries. ` + excludedNote,
|
|
287
|
+
parameters: {
|
|
288
|
+
pattern: {
|
|
289
|
+
type: "string",
|
|
290
|
+
required: true,
|
|
291
|
+
description: "Glob pattern to match file paths against (e.g. \"**/*.ts\", \"src/**/*.test.js\"). A pattern with no \"/\" matches the basename at any depth, so \"*\" and \"*.ts\" both search the whole tree; include a separator to anchor the depth."
|
|
292
|
+
},
|
|
293
|
+
path: {
|
|
294
|
+
type: "string",
|
|
295
|
+
description: "Directory to search in. Defaults to the session workspace; a relative path resolves against it."
|
|
296
|
+
},
|
|
297
|
+
include: {
|
|
298
|
+
type: "array",
|
|
299
|
+
items: { type: "string" },
|
|
300
|
+
description: "Whitelist that lifts excluded directories back into THIS search: an include pattern naming an excluded directory as a path segment (e.g. \"node_modules/**/package.json\") removes that directory from the exclusion set; a bare wildcard pattern (\"**\") lifts every excluded directory. include never filters results — pattern does. Omit it for ordinary searches."
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
timeoutMs: caps.timeoutMs,
|
|
304
|
+
output: {
|
|
305
|
+
schema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
additionalProperties: false,
|
|
308
|
+
properties: {
|
|
309
|
+
root: {
|
|
310
|
+
type: "string",
|
|
311
|
+
required: true
|
|
312
|
+
},
|
|
313
|
+
paths: {
|
|
314
|
+
type: "array",
|
|
315
|
+
required: true,
|
|
316
|
+
items: { type: "string" }
|
|
317
|
+
},
|
|
318
|
+
total: {
|
|
319
|
+
type: "integer",
|
|
320
|
+
required: true
|
|
321
|
+
},
|
|
322
|
+
truncated: {
|
|
323
|
+
type: "boolean",
|
|
324
|
+
required: true
|
|
325
|
+
},
|
|
326
|
+
sampled: {
|
|
327
|
+
type: "boolean",
|
|
328
|
+
required: true
|
|
329
|
+
},
|
|
330
|
+
spill: {
|
|
331
|
+
type: "object",
|
|
332
|
+
additionalProperties: false,
|
|
333
|
+
properties: {
|
|
334
|
+
locator: {
|
|
335
|
+
type: "string",
|
|
336
|
+
required: true
|
|
337
|
+
},
|
|
338
|
+
hint: {
|
|
339
|
+
type: "string",
|
|
340
|
+
required: true
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
render: (_args, value) => [{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: renderGlobValue(value)
|
|
349
|
+
}],
|
|
350
|
+
presentationMeta: (_args, value) => pathsMeta(value, caps.maxMetaBytes)
|
|
351
|
+
},
|
|
352
|
+
async execute(args, exec) {
|
|
353
|
+
const input = parseGlobArgs(args);
|
|
354
|
+
const run = await runRipgrep(ctx, exec, "glob", buildGlobArgv(input, caps.excludeDirs), caps.rawOutputMaxBytes, caps.graceMs, caps.stderrMaxBytes);
|
|
355
|
+
const root = input.path === void 0 ? "." : toWorkdirRelative(input.path, run.workdir);
|
|
356
|
+
const all = [];
|
|
357
|
+
if (!run.noMatches) for (const line of run.stdout.split("\n")) {
|
|
358
|
+
if (line.length === 0) continue;
|
|
359
|
+
all.push(toWorkdirRelative(line, run.workdir));
|
|
360
|
+
}
|
|
361
|
+
if (all.length <= caps.maxResults) return {
|
|
362
|
+
root,
|
|
363
|
+
paths: all,
|
|
364
|
+
total: all.length,
|
|
365
|
+
truncated: false,
|
|
366
|
+
sampled: false
|
|
367
|
+
};
|
|
368
|
+
const page = caps.sampleOverCapGlobResults ? sampleAcrossTopLevel(all, caps.maxResults, root).items : all.slice(0, caps.maxResults);
|
|
369
|
+
const spillRef = await trySaveFormattedResult(ctx, exec, "glob-results.txt", all.join("\n"));
|
|
370
|
+
return {
|
|
371
|
+
root,
|
|
372
|
+
paths: page,
|
|
373
|
+
total: all.length,
|
|
374
|
+
truncated: true,
|
|
375
|
+
sampled: caps.sampleOverCapGlobResults,
|
|
376
|
+
...spillRef !== void 0 ? { spill: {
|
|
377
|
+
locator: spillRef.locator,
|
|
378
|
+
hint: spillRef.retrievalHint
|
|
379
|
+
} } : {}
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
presentCall: (args) => {
|
|
383
|
+
const where = args.path !== void 0 ? ` in ${args.path}` : "";
|
|
384
|
+
return {
|
|
385
|
+
card: "generic",
|
|
386
|
+
title: `Glob ${args.pattern}${where}`,
|
|
387
|
+
kind: "search",
|
|
388
|
+
rawInput: args.pattern
|
|
389
|
+
};
|
|
390
|
+
},
|
|
391
|
+
presentResult: (_args, result) => {
|
|
392
|
+
if (result.isError) return void 0;
|
|
393
|
+
return pathsViewFromMeta(result.meta);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/index.ts
|
|
399
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
400
|
+
const name = "better-glob";
|
|
401
|
+
/**
|
|
402
|
+
* Services required by the plugin: `tools`/`systemPrompt` register the
|
|
403
|
+
* shadow into agent scopes, `subprocess` executes ripgrep, `agents` lists
|
|
404
|
+
* live agents for the reload resync.
|
|
405
|
+
*/
|
|
406
|
+
const inject = [
|
|
407
|
+
"tools",
|
|
408
|
+
"systemPrompt",
|
|
409
|
+
"subprocess",
|
|
410
|
+
"agents"
|
|
411
|
+
];
|
|
412
|
+
/**
|
|
413
|
+
* The prompt section the shadow registers per agent. Same name and order as
|
|
414
|
+
* the built-in `tool:glob` section, so the agent-layer registration shadows
|
|
415
|
+
* the built-in text; the section text names the configured exclusions and
|
|
416
|
+
* the include whitelist escape hatch.
|
|
417
|
+
* @param caps - the deployment's resolved caps.
|
|
418
|
+
* @returns the section to register through `agent.ctx.systemPrompt`.
|
|
419
|
+
*/
|
|
420
|
+
function shadowSection(caps) {
|
|
421
|
+
const excluded = caps.excludeDirs.length > 0 ? `Bottomless directories (${caps.excludeDirs.join(", ")}) are excluded automatically — pass include (for example ["node_modules/**"]) when a search must look inside one.` : "No directories are excluded by configuration.";
|
|
422
|
+
return {
|
|
423
|
+
name: "tool:glob",
|
|
424
|
+
order: FIRST_PARTY_SECTION_ORDER.TOOL_GLOB,
|
|
425
|
+
text: "Use the glob tool — not shell find — to discover files by path pattern. A pattern with no \"/\" matches basenames at any depth, so \"*\" matches every file in the tree rather than its top level. Results are files only, never directories, and include hidden and ignored files. " + excluded + " A result that fits comes back in modification-time order."
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
/** Register the shadow into one agent's own layer; returns the combined disposer for reload resync. */
|
|
429
|
+
function shadowAgent(agent, tool, section) {
|
|
430
|
+
const disposeTool = agent.ctx.tools.register(tool);
|
|
431
|
+
const disposeSection = agent.ctx.systemPrompt.section(section);
|
|
432
|
+
return () => {
|
|
433
|
+
disposeTool();
|
|
434
|
+
disposeSection();
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Per-agent shadow registrations, module-level so a config reload (the same
|
|
439
|
+
* module instance re-running `apply`) can dispose the previous fiber's
|
|
440
|
+
* registration before re-registering with the fresh config.
|
|
441
|
+
*/
|
|
442
|
+
const shadowed = /* @__PURE__ */ new WeakMap();
|
|
443
|
+
/**
|
|
444
|
+
* Register the better-`glob` shadow: a `agent/session-start` listener that
|
|
445
|
+
* mounts the tool and prompt section into every agent's own layer, plus a
|
|
446
|
+
* resync over already-live agents so a config reload takes effect without a
|
|
447
|
+
* restart.
|
|
448
|
+
* @param ctx - the plugin context; registrations are effects scoped to it.
|
|
449
|
+
* @param config - the (schemastery-defaulted) plugin configuration.
|
|
450
|
+
*/
|
|
451
|
+
function apply(ctx, config) {
|
|
452
|
+
const caps = resolveConfig(config);
|
|
453
|
+
const tool = defineBetterGlobTool(ctx, caps);
|
|
454
|
+
const section = shadowSection(caps);
|
|
455
|
+
const shadow = (agent) => {
|
|
456
|
+
if (shadowed.has(agent)) return;
|
|
457
|
+
shadowed.set(agent, shadowAgent(agent, tool, section));
|
|
458
|
+
};
|
|
459
|
+
ctx.on("agent/session-start", ({ agent }) => {
|
|
460
|
+
shadow(agent);
|
|
461
|
+
});
|
|
462
|
+
for (const agent of ctx.agents.list()) {
|
|
463
|
+
shadowed.get(agent)?.();
|
|
464
|
+
shadowed.delete(agent);
|
|
465
|
+
shadow(agent);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
//#endregion
|
|
469
|
+
export { Config, DEFAULT_EXCLUDE_DIRS, VCS_EXCLUDES, apply, buildGlobArgv, defineBetterGlobTool, inject, liftedExcludeNames, name, parseGlobArgs, pathsMeta, pathsViewFromMeta, renderGlobValue, resolveConfig, shadowSection };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure argument handling for the better `glob` tool: model-argument
|
|
3
|
+
* validation, include-whitelist lift computation, and ripgrep argv
|
|
4
|
+
* construction. No I/O — every function here is testable without a host.
|
|
5
|
+
* @module @huanlin/dsh-plugin-better-glob/argv
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Directory names the tool never descends into, regardless of the
|
|
9
|
+
* `excludeDirs` configuration: VCS metadata stores (the built-in tool's
|
|
10
|
+
* fixed exclusion set).
|
|
11
|
+
*/
|
|
12
|
+
export declare const VCS_EXCLUDES: readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Default bottomless directories excluded from every search: dependency
|
|
15
|
+
* installs, build outputs, framework state, caches, and virtualenvs that
|
|
16
|
+
* routinely hold hundreds of thousands of files. The `excludeDirs` config
|
|
17
|
+
* replaces this list wholesale.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_EXCLUDE_DIRS: readonly string[];
|
|
20
|
+
/** Validated `glob` arguments after value-constraint checks. */
|
|
21
|
+
export interface GlobInput {
|
|
22
|
+
pattern: string;
|
|
23
|
+
path?: string;
|
|
24
|
+
include?: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate the value constraints the schema DSL cannot express: a non-blank
|
|
28
|
+
* `pattern`, a non-blank `path` when given, and non-blank `include` entries
|
|
29
|
+
* when given. Throws a plain `Error` (an ordinary tool argument error)
|
|
30
|
+
* otherwise.
|
|
31
|
+
* @param args - the schema-validated `glob` arguments.
|
|
32
|
+
* @returns the accepted input, unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseGlobArgs(args: {
|
|
35
|
+
pattern: string;
|
|
36
|
+
path?: string;
|
|
37
|
+
include?: string[];
|
|
38
|
+
}): GlobInput;
|
|
39
|
+
/**
|
|
40
|
+
* The excluded directory names the include whitelist lifts back into one
|
|
41
|
+
* call's search. A name lifts when an include pattern carries it as a path
|
|
42
|
+
* segment (`node_modules/**` + `/package.json` lifts `node_modules`); a
|
|
43
|
+
* pattern of only wildcard segments (`**`) lifts every candidate. Segments
|
|
44
|
+
* that name no configured exclusion are ignored.
|
|
45
|
+
*
|
|
46
|
+
* The lift happens at argv construction time, not as a later ripgrep glob:
|
|
47
|
+
* ripgrep prunes an excluded directory during traversal, so a glob emitted
|
|
48
|
+
* after the exclusion could never re-enter the pruned subtree.
|
|
49
|
+
* @param include - the call's include whitelist, when the model passed one.
|
|
50
|
+
* @param excludeDirs - the configured exclusion names.
|
|
51
|
+
* @returns the subset of `excludeDirs` the whitelist lifts.
|
|
52
|
+
*/
|
|
53
|
+
export declare function liftedExcludeNames(include: readonly string[] | undefined, excludeDirs: readonly string[]): ReadonlySet<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Build the fixed `rg --files` argv for one better-`glob` call. Every
|
|
56
|
+
* model-controlled value is a plain argv element — no shell layer exists, so
|
|
57
|
+
* no quoting applies; the search root rides behind `--` so a leading-dash
|
|
58
|
+
* path can never parse as a flag. `--sort=modified` orders by modification
|
|
59
|
+
* time and `--no-ignore --hidden` searches ignored and hidden files, matching
|
|
60
|
+
* the built-in tool's contract. Each excluded name carries TWO negated globs
|
|
61
|
+
* (the bare form prunes the directory during traversal; the contents form
|
|
62
|
+
* still excludes the internals when the search root sits at or inside the
|
|
63
|
+
* directory). VCS excludes are unconditional; a lifted name omits both of
|
|
64
|
+
* its globs, which is what lets ripgrep descend into it again.
|
|
65
|
+
* @param input - the validated arguments.
|
|
66
|
+
* @param excludeDirs - the configured exclusion names.
|
|
67
|
+
* @returns the complete ripgrep argument vector (excluding the binary itself).
|
|
68
|
+
*/
|
|
69
|
+
export declare function buildGlobArgv(input: GlobInput, excludeDirs: readonly string[]): string[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration: the Schemastery schema, the resolved caps, and the
|
|
3
|
+
* load-time validation. Every field defaults, so a patch insert row without
|
|
4
|
+
* a `config` block boots.
|
|
5
|
+
* @module @huanlin/dsh-plugin-better-glob/caps
|
|
6
|
+
*/
|
|
7
|
+
import z from 'schemastery';
|
|
8
|
+
/** Plugin config; every field has a default. */
|
|
9
|
+
export interface BetterGlobConfig {
|
|
10
|
+
/** Bottomless directory NAMES excluded from every search; replaces the built-in default list wholesale (VCS directories are always excluded on top). */
|
|
11
|
+
excludeDirs?: string[];
|
|
12
|
+
/** Whether an over-cap page is sampled across top-level entries instead of taking the modification-time head (the built-in deployment's switch). */
|
|
13
|
+
sampleOverCapGlobResults?: boolean;
|
|
14
|
+
/** Max paths one `glob` call retains inline; the complete list spills to a file past it. */
|
|
15
|
+
globMaxResults?: number;
|
|
16
|
+
/** Max bytes of serialized `presentationMeta`; trailing paths drop past it. */
|
|
17
|
+
globMetaMaxBytes?: number;
|
|
18
|
+
/** Max complete raw `rg` stdout one call parses. */
|
|
19
|
+
rawOutputMaxBytes?: number;
|
|
20
|
+
/** Terminate-escalation grace period (ms) for the search process. */
|
|
21
|
+
graceMs?: number;
|
|
22
|
+
/** Max bytes of the retained stderr diagnostic tail. */
|
|
23
|
+
stderrMaxBytes?: number;
|
|
24
|
+
/** Cooperative tool-call timeout budget (ms). */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare const Config: z<BetterGlobConfig>;
|
|
28
|
+
/** Resolved caps after schemastery defaulting — every field required. */
|
|
29
|
+
export interface BetterGlobCaps {
|
|
30
|
+
excludeDirs: readonly string[];
|
|
31
|
+
sampleOverCapGlobResults: boolean;
|
|
32
|
+
maxResults: number;
|
|
33
|
+
maxMetaBytes: number;
|
|
34
|
+
rawOutputMaxBytes: number;
|
|
35
|
+
graceMs: number;
|
|
36
|
+
stderrMaxBytes: number;
|
|
37
|
+
timeoutMs: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate the schemastery-defaulted config into caps. Fails loud at load:
|
|
41
|
+
* excludeDirs entries must be bare directory names (a separator or a blank
|
|
42
|
+
* name would silently never match a directory in the traversal globs), caps
|
|
43
|
+
* must be positive integers, and graceMs must fit a timer delay.
|
|
44
|
+
* @param config - the schemastery-defaulted plugin config.
|
|
45
|
+
* @returns the resolved caps.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveConfig(config: BetterGlobConfig): BetterGlobCaps;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-better-glob — per-agent shadow replacement for the built-in
|
|
3
|
+
* `glob` tool.
|
|
4
|
+
*
|
|
5
|
+
* ## Why per-agent registration
|
|
6
|
+
*
|
|
7
|
+
* The tools registry resolves a scope's view by layering: a scope's OWN
|
|
8
|
+
* registrations shadow every inherited one (the global layer in headless
|
|
9
|
+
* compositions, the preset's standing scope in web profiles, where the
|
|
10
|
+
* web-app bundle disables the host-plane row and the `standard` preset mounts
|
|
11
|
+
* `tool-fs-search` per session). A global registration cannot win in either
|
|
12
|
+
* composition, and a bundle patch cannot reach preset files — so the shadow
|
|
13
|
+
* registers into each agent's own layer (`agent.ctx`), which beats both.
|
|
14
|
+
* `grep` is untouched, and no built-in rows are disabled.
|
|
15
|
+
*
|
|
16
|
+
* ## Mount points
|
|
17
|
+
*
|
|
18
|
+
* `agent/session-start` fires for every agent (main and subagents) before the
|
|
19
|
+
* first prompt assembly, across startup/resume/clear/compact — each agent
|
|
20
|
+
* gets the shadow exactly once, guarded by a WeakSet. On plugin reload
|
|
21
|
+
* (config change), `apply` re-runs on the same module instance: live agents
|
|
22
|
+
* listed from the `agents` registry have their previous shadow disposed and
|
|
23
|
+
* re-registered so the fresh config applies.
|
|
24
|
+
*
|
|
25
|
+
* ## Execution
|
|
26
|
+
*
|
|
27
|
+
* The tool reuses the built-in search suite's spawn plumbing (`runRipgrep`,
|
|
28
|
+
* `toWorkdirRelative`, `trySaveFormattedResult`, `sampleAcrossTopLevel`) —
|
|
29
|
+
* same packaged ripgrep binary, same subprocess seam, same error vocabulary.
|
|
30
|
+
* Its own contribution is the exclusion set (VCS always + configurable
|
|
31
|
+
* bottomless directories) and the `include` whitelist that lifts an excluded
|
|
32
|
+
* directory back into one call at argv-construction time.
|
|
33
|
+
* @module @huanlin/dsh-plugin-better-glob
|
|
34
|
+
*/
|
|
35
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
36
|
+
import type { PromptSection } from '@deepseek-ai/dsh-system-prompt';
|
|
37
|
+
import type { BetterGlobCaps, BetterGlobConfig } from './caps.ts';
|
|
38
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
39
|
+
export declare const name = "better-glob";
|
|
40
|
+
/**
|
|
41
|
+
* Services required by the plugin: `tools`/`systemPrompt` register the
|
|
42
|
+
* shadow into agent scopes, `subprocess` executes ripgrep, `agents` lists
|
|
43
|
+
* live agents for the reload resync.
|
|
44
|
+
*/
|
|
45
|
+
export declare const inject: string[];
|
|
46
|
+
export { Config } from './caps.ts';
|
|
47
|
+
export type { BetterGlobConfig } from './caps.ts';
|
|
48
|
+
export { buildGlobArgv, DEFAULT_EXCLUDE_DIRS, liftedExcludeNames, parseGlobArgs, VCS_EXCLUDES } from './argv.ts';
|
|
49
|
+
export type { GlobInput } from './argv.ts';
|
|
50
|
+
export type { GlobOutput, PathsSearchMeta } from './tool.ts';
|
|
51
|
+
export { defineBetterGlobTool, pathsMeta, pathsViewFromMeta, renderGlobValue } from './tool.ts';
|
|
52
|
+
export { resolveConfig } from './caps.ts';
|
|
53
|
+
export type { BetterGlobCaps } from './caps.ts';
|
|
54
|
+
/**
|
|
55
|
+
* The prompt section the shadow registers per agent. Same name and order as
|
|
56
|
+
* the built-in `tool:glob` section, so the agent-layer registration shadows
|
|
57
|
+
* the built-in text; the section text names the configured exclusions and
|
|
58
|
+
* the include whitelist escape hatch.
|
|
59
|
+
* @param caps - the deployment's resolved caps.
|
|
60
|
+
* @returns the section to register through `agent.ctx.systemPrompt`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function shadowSection(caps: BetterGlobCaps): PromptSection;
|
|
63
|
+
/**
|
|
64
|
+
* Register the better-`glob` shadow: a `agent/session-start` listener that
|
|
65
|
+
* mounts the tool and prompt section into every agent's own layer, plus a
|
|
66
|
+
* resync over already-live agents so a config reload takes effect without a
|
|
67
|
+
* restart.
|
|
68
|
+
* @param ctx - the plugin context; registrations are effects scoped to it.
|
|
69
|
+
* @param config - the (schemastery-defaulted) plugin configuration.
|
|
70
|
+
*/
|
|
71
|
+
export declare function apply(ctx: Context, config: BetterGlobConfig): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The better `glob` tool definition: the model-facing schema (with the
|
|
3
|
+
* `include` whitelist), execution over the packaged ripgrep binary through
|
|
4
|
+
* the built-in search suite's spawn plumbing, inline-cap + spill retention,
|
|
5
|
+
* and the model/card projections. Registration happens per agent (see
|
|
6
|
+
* `index.ts`); this module only builds the definition.
|
|
7
|
+
* @module @huanlin/dsh-plugin-better-glob/tool
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import type { SearchResultView, ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
11
|
+
import type { BetterGlobCaps } from './caps.ts';
|
|
12
|
+
/**
|
|
13
|
+
* The canonical value of one `glob` call: the retained inline page plus the
|
|
14
|
+
* recovery info for a capped result. The complete modification-time-ordered
|
|
15
|
+
* list exists only in the spill file (when the cap bit and the spill backend
|
|
16
|
+
* accepted it); the canonical value stays bounded for the session log.
|
|
17
|
+
*
|
|
18
|
+
* Object-literal type aliases rather than interfaces: only an alias is
|
|
19
|
+
* assignable to the `JsonValue` index signature the tool contract uses.
|
|
20
|
+
*/
|
|
21
|
+
export type GlobOutput = {
|
|
22
|
+
/** The search root in the same display-path space as `paths`. */
|
|
23
|
+
root: string;
|
|
24
|
+
/** The retained page of matching paths, in modification-time order. */
|
|
25
|
+
paths: string[];
|
|
26
|
+
/** How many paths the complete search found. */
|
|
27
|
+
total: number;
|
|
28
|
+
/** Whether `paths` is a capped page rather than the complete result. */
|
|
29
|
+
truncated: boolean;
|
|
30
|
+
/** Whether an over-cap page was sampled across top-level entries (only meaningful when `truncated`). */
|
|
31
|
+
sampled: boolean;
|
|
32
|
+
/** Where the complete sorted result was saved, when the spill backend accepted it. */
|
|
33
|
+
spill?: {
|
|
34
|
+
locator: string;
|
|
35
|
+
hint: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** The `glob` tool's private `tool/result` meta payload: the search card's path list (object-literal alias for `JsonValue` assignability). */
|
|
39
|
+
export type PathsSearchMeta = {
|
|
40
|
+
shape: 'paths';
|
|
41
|
+
paths: string[];
|
|
42
|
+
truncated: boolean;
|
|
43
|
+
total: number;
|
|
44
|
+
};
|
|
45
|
+
/** Project one canonical value into the bounded `presentationMeta` the search card renders. */
|
|
46
|
+
export declare function pathsMeta(value: GlobOutput, maxMetaBytes: number): PathsSearchMeta;
|
|
47
|
+
/**
|
|
48
|
+
* Narrow opaque live or replayed result metadata to a {@link SearchResultView}.
|
|
49
|
+
* Malformed metadata returns `undefined` so `presentResult` falls back to the
|
|
50
|
+
* generic card instead of throwing during replay of an older or hand-edited
|
|
51
|
+
* log. A zero-result meta narrows to a valid empty card.
|
|
52
|
+
* @param meta - result metadata (the {@link PathsSearchMeta} the tool projected).
|
|
53
|
+
* @returns the search view, or `undefined` for absent or malformed metadata.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pathsViewFromMeta(meta: unknown): SearchResultView | undefined;
|
|
56
|
+
/** Format one canonical value for the Native surface: the page, plus the cap basis and recovery path when capped. */
|
|
57
|
+
export declare function renderGlobValue(value: GlobOutput): string;
|
|
58
|
+
/**
|
|
59
|
+
* Build the better `glob` tool definition.
|
|
60
|
+
* @param ctx - the plugin context; execution reads its `subprocess` service and opportunistic `spillStore`.
|
|
61
|
+
* @param caps - the deployment's resolved caps (plugin config after defaulting).
|
|
62
|
+
* @returns the registry-ready definition; the caller registers it into agent scopes.
|
|
63
|
+
*/
|
|
64
|
+
export declare function defineBetterGlobTool(ctx: Context, caps: BetterGlobCaps): ToolDefinition;
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huanlin/dsh-plugin-better-glob",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh-plugin"
|
|
9
|
+
],
|
|
10
|
+
"description": "Shadow-replaces the built-in glob tool: auto-excludes bottomless directories (node_modules, dist, build, .venv, ...) and requires an explicit include whitelist to search inside them.",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"main": "./lib/index.js",
|
|
14
|
+
"types": "./lib/types/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./lib/types/index.d.ts",
|
|
18
|
+
"import": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib/",
|
|
24
|
+
"cordis.patch.yml"
|
|
25
|
+
],
|
|
26
|
+
"dsh": {
|
|
27
|
+
"bundle": {
|
|
28
|
+
"patch": "./cordis.patch.yml"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"schemastery": "^3.18.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
36
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.1",
|
|
37
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.1",
|
|
38
|
+
"@deepseek-ai/dsh-tool-fs-search": "^0.1.2-alpha.1",
|
|
39
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.1"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@deepseek-ai/cordis": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"@deepseek-ai/dsh-agent": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"@deepseek-ai/dsh-system-prompt": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@deepseek-ai/dsh-tool-fs-search": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@deepseek-ai/dsh-tools": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^22.20.0",
|
|
60
|
+
"tsdown": "^0.22.2",
|
|
61
|
+
"typescript": "^5.9.0",
|
|
62
|
+
"vitest": "^3.2.0"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22.0.0"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"pretest": "node scripts/relink-deps.mjs",
|
|
69
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"prebuild": "node scripts/relink-deps.mjs",
|
|
72
|
+
"build": "tsdown -c tsdown.config.ts && tsc -p tsconfig.json"
|
|
73
|
+
}
|
|
74
|
+
}
|