dsh-everything 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +7 -2
- package/README.zh-CN.md +7 -4
- package/index.js +126 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [1.3.1] - 2026-10-27
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- Multi-word queries (`*.docx dm:thisweek`) silently returned 0 results: the
|
|
10
|
+
whole query string was handed to es.exe as ONE argument, which Everything
|
|
11
|
+
then matched literally. Queries are now tokenized on whitespace (outside
|
|
12
|
+
double quotes) and each term is passed as its own argument, restoring AND
|
|
13
|
+
semantics. Quoted phrases without spaces keep their phrase meaning; quoted
|
|
14
|
+
phrases containing spaces degrade to ANDed words (es.exe cannot express
|
|
15
|
+
them positionally — use `regex=true` for exact multi-word matching).
|
|
16
|
+
- Non-ASCII file names came back as mojibake (`?????`): es.exe pipes results
|
|
17
|
+
in the console/ANSI codepage, not UTF-8. Results are now read from es.exe's
|
|
18
|
+
UTF-8 `-export-json` file instead, so Chinese/Japanese/emoji names survive
|
|
19
|
+
the round trip on every codepage (with a decoded-stdout fallback for es.exe
|
|
20
|
+
builds without `-export-json`).
|
|
21
|
+
|
|
6
22
|
## [1.3.0] - 2026-10-26
|
|
7
23
|
|
|
8
24
|
### Added
|
package/README.md
CHANGED
|
@@ -37,16 +37,18 @@ Start-Process "C:\Program Files\Everything\Everything.exe"
|
|
|
37
37
|
In a DSH session, ask the agent or call the plugin manager:
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
plugin_manager install_bundle with target =
|
|
40
|
+
plugin_manager install_bundle with target = dsh-everything
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
The plugin activates as `dsh-everything` and registers the `everything_search` tool for every agent in the profile.
|
|
44
44
|
|
|
45
|
+
Published on npm as [`dsh-everything`](https://www.npmjs.com/package/dsh-everything); installing from the package directory works too.
|
|
46
|
+
|
|
45
47
|
## Tool: `everything_search`
|
|
46
48
|
|
|
47
49
|
| Parameter | Type | Description |
|
|
48
50
|
|---|---|---|
|
|
49
|
-
| `query` *(required)* | string | Everything search expression: `report ext:docx`, `*.sln dm:thisweek`, `path:\projects\ ext:py`. Space = AND, `\|` = OR, `!` = NOT
|
|
51
|
+
| `query` *(required)* | string | Everything search expression: `report ext:docx`, `*.sln dm:thisweek`, `path:\projects\ ext:py`. Space = AND, `\|` = OR, `!` = NOT. Multi-word queries are split into es.exe terms automatically. |
|
|
50
52
|
| `path` | string | Restrict to a directory and its subtree (absolute path). |
|
|
51
53
|
| `maxResults` | integer | Result cap (default 100, ceiling 1000). |
|
|
52
54
|
| `offset` | integer | Skip leading results (pagination). |
|
|
@@ -62,6 +64,9 @@ Returns `{ query, total, truncated, results: [{ path, size, dateModified, dateCr
|
|
|
62
64
|
|
|
63
65
|
Typical queries: `report ext:docx` · `*.sln dm:thisweek` · `size:>500mb` · `notes !archive` · recently changed files with `sort: date-modified-descending` + `dm:thisweek`.
|
|
64
66
|
|
|
67
|
+
> **Phrase quirks:** a quoted phrase only matches the *name* part, so `"report.docx"` matches nothing — pass it unquoted. Phrases containing spaces cannot be expressed through the es.exe CLI (they degrade to ANDed words); use `regex=true` for exact multi-word matching.
|
|
68
|
+
> **Encoding:** results are read from es.exe's UTF-8 `-export-json` output, so non-ASCII file names (Chinese, Japanese, emoji, …) survive the round trip intact.
|
|
69
|
+
|
|
65
70
|
> **Scope note:** searches names and paths only, never file contents — content search stays with `grep`.
|
|
66
71
|
> **Coverage note:** results follow the local Everything index — NTFS/reFS drives and folders explicitly added to the index. FAT/exFAT removable drives and network shares are typically not covered.
|
|
67
72
|
|
package/README.zh-CN.md
CHANGED
|
@@ -34,19 +34,19 @@ Start-Process "C:\Program Files\Everything\Everything.exe"
|
|
|
34
34
|
|
|
35
35
|
## 安装
|
|
36
36
|
|
|
37
|
-
在 DSH
|
|
37
|
+
在 DSH 会话中让插件管理器安装 npm 包名:
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
plugin_manager install_bundle,target =
|
|
40
|
+
plugin_manager install_bundle,target = dsh-everything
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
插件以 `dsh-everything` 激活,并为该 profile 下的所有 Agent 注册 `everything_search`
|
|
43
|
+
插件以 `dsh-everything` 激活,并为该 profile 下的所有 Agent 注册 `everything_search` 工具。已发布到 npm([`dsh-everything`](https://www.npmjs.com/package/dsh-everything)),直接用本包目录安装也可以。
|
|
44
44
|
|
|
45
45
|
## 工具:everything_search
|
|
46
46
|
|
|
47
47
|
| 参数 | 类型 | 说明 |
|
|
48
48
|
|---|---|---|
|
|
49
|
-
| `query` *(必填)* | string | Everything 搜索表达式:`report ext:docx`、`*.sln dm:thisweek`、`path:\projects\ ext:py`。空格 = 与,`\|` = 或,`!` =
|
|
49
|
+
| `query` *(必填)* | string | Everything 搜索表达式:`report ext:docx`、`*.sln dm:thisweek`、`path:\projects\ ext:py`。空格 = 与,`\|` = 或,`!` = 非。多词查询会自动拆分为 es.exe 的独立搜索词。 |
|
|
50
50
|
| `path` | string | 限定在某目录及其子目录内(绝对路径)。 |
|
|
51
51
|
| `maxResults` | integer | 返回条数上限(默认 100,封顶 1000)。 |
|
|
52
52
|
| `offset` | integer | 跳过前 N 条(分页)。 |
|
|
@@ -62,6 +62,9 @@ plugin_manager install_bundle,target = 本包目录的绝对路径
|
|
|
62
62
|
|
|
63
63
|
典型查询:`report ext:docx` · `*.sln dm:thisweek` · `size:>500mb` · `notes !archive` · 最近改动文件用 `sort: date-modified-descending` + `dm:thisweek`。
|
|
64
64
|
|
|
65
|
+
> **短语怪癖**:带引号的短语只匹配文件名的“名字部分”,所以 `"report.docx"` 永远搜不到——直接写不带引号的 `report.docx`。含空格的短语无法经 es.exe CLI 表达(会退化为逐词 AND);需要精确多词匹配请用 `regex=true`。
|
|
66
|
+
> **编码**:结果改从 es.exe 的 UTF-8 `-export-json` 输出读取,非 ASCII 文件名(中文、日文、emoji 等)完整回传、不再乱码。
|
|
67
|
+
|
|
65
68
|
> **范围说明**:只搜文件名和路径,不搜文件内容——内容搜索请继续用 `grep`。
|
|
66
69
|
> **覆盖说明**:结果取决于本机 Everything 索引——NTFS/reFS 磁盘与显式加入索引的目录;FAT/exFAT 可移动盘与网络共享通常不在覆盖范围。
|
|
67
70
|
|
package/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* dsh-everything: model-facing `everything_search` tool backed by the local
|
|
3
3
|
* Everything (voidtools) index through the official ES command line (es.exe).
|
|
4
4
|
*
|
|
5
|
-
* The tool spawns es.exe with a fixed argv vector (no shell layer),
|
|
6
|
-
* `-json`
|
|
5
|
+
* The tool spawns es.exe with a fixed argv vector (no shell layer), reads its
|
|
6
|
+
* results from a UTF-8 `-export-json` file (immune to console codepages), and
|
|
7
|
+
* projects results onto the built-in search-paths card.
|
|
7
8
|
* No external npm dependencies. Windows only: Everything itself is a Windows
|
|
8
9
|
* application, so on other platforms a stub tool is registered that fails with
|
|
9
10
|
* a clear platform message instead of a confusing spawn error.
|
|
@@ -18,8 +19,9 @@
|
|
|
18
19
|
* timeoutMs per-spawn timeout in milliseconds (default: 8000)
|
|
19
20
|
*/
|
|
20
21
|
import { execFile, spawn } from 'node:child_process';
|
|
21
|
-
import { existsSync } from 'node:fs';
|
|
22
|
-
import {
|
|
22
|
+
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { join as joinPath, win32 as win32path } from 'node:path';
|
|
23
25
|
import { ensureBackend, EverythingError } from './setup.js';
|
|
24
26
|
|
|
25
27
|
const DEFAULT_MAX_RESULTS = 100;
|
|
@@ -64,7 +66,7 @@ function resolveEverythingPath(config, esPath) {
|
|
|
64
66
|
return 'Everything.exe';
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
/** Run es.exe once and resolve with its raw stdout. Errors are classified into actionable codes. */
|
|
69
|
+
/** Run es.exe once and resolve with its raw stdout Buffer. Errors are classified into actionable codes. */
|
|
68
70
|
function runEs(esPath, argv, timeoutMs, signal) {
|
|
69
71
|
return new Promise((resolve, reject) => {
|
|
70
72
|
let child;
|
|
@@ -72,7 +74,7 @@ function runEs(esPath, argv, timeoutMs, signal) {
|
|
|
72
74
|
child = execFile(
|
|
73
75
|
esPath,
|
|
74
76
|
argv,
|
|
75
|
-
{ timeout: timeoutMs, maxBuffer: MAX_BUFFER, windowsHide: true, signal },
|
|
77
|
+
{ timeout: timeoutMs, maxBuffer: MAX_BUFFER, windowsHide: true, signal, encoding: 'buffer' },
|
|
76
78
|
(error, stdout, stderr) => {
|
|
77
79
|
const stderrText = typeof stderr === 'string' ? stderr.trim() : '';
|
|
78
80
|
if (error) {
|
|
@@ -104,7 +106,7 @@ function runEs(esPath, argv, timeoutMs, signal) {
|
|
|
104
106
|
reject(new EverythingError('everything_search: Everything is not running.', 'ES_NOT_RUNNING'));
|
|
105
107
|
return;
|
|
106
108
|
}
|
|
107
|
-
resolve(
|
|
109
|
+
resolve(stdout); // Buffer (execFile encoding: 'buffer')
|
|
108
110
|
}
|
|
109
111
|
);
|
|
110
112
|
} catch (error) {
|
|
@@ -146,6 +148,84 @@ async function autoStartEverything(everythingPath, esPath, signal) {
|
|
|
146
148
|
throw new EverythingError('everything_search: Everything was started but did not answer within 15s; retry the search.', 'ES_NOT_RUNNING');
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Split an Everything search expression into es.exe search terms.
|
|
153
|
+
*
|
|
154
|
+
* es.exe takes each command-line argument as ONE search term; an argument
|
|
155
|
+
* that itself contains a space is matched literally and never matches, so
|
|
156
|
+
* `*.docx dm:thisweek` MUST be passed as two arguments. Quoted phrases
|
|
157
|
+
* without spaces keep their quotes (phrase semantics); a quoted phrase that
|
|
158
|
+
* contains a space cannot be expressed positionally, so it degrades to its
|
|
159
|
+
* component words ANDed together (use regex=true for exact multi-word
|
|
160
|
+
* matching).
|
|
161
|
+
*/
|
|
162
|
+
function tokenizeQuery(query) {
|
|
163
|
+
const tokens = [];
|
|
164
|
+
let current = '';
|
|
165
|
+
let inQuotes = false;
|
|
166
|
+
const flush = () => {
|
|
167
|
+
if (current.length === 0) return;
|
|
168
|
+
if (/\s/.test(current)) {
|
|
169
|
+
// Only reachable for a quoted phrase with internal spaces: es.exe cannot
|
|
170
|
+
// take it as one term, so degrade to its component words (quotes stripped).
|
|
171
|
+
for (const word of current.split(/\s+/)) {
|
|
172
|
+
const bare = word.replace(/"/g, '');
|
|
173
|
+
if (bare.length > 0) tokens.push(bare);
|
|
174
|
+
}
|
|
175
|
+
} else if (current.replace(/"/g, '').length > 0) {
|
|
176
|
+
tokens.push(current);
|
|
177
|
+
}
|
|
178
|
+
current = '';
|
|
179
|
+
};
|
|
180
|
+
for (const ch of query) {
|
|
181
|
+
if (ch === '"') {
|
|
182
|
+
inQuotes = !inQuotes;
|
|
183
|
+
current += ch;
|
|
184
|
+
} else if (!inQuotes && /\s/.test(ch)) {
|
|
185
|
+
flush();
|
|
186
|
+
} else {
|
|
187
|
+
current += ch;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
flush();
|
|
191
|
+
return tokens;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Decode es.exe stdout: it emits the console/ANSI codepage, not UTF-8. */
|
|
195
|
+
function decodeEsStdout(buf) {
|
|
196
|
+
if (!buf || buf.length === 0) return '';
|
|
197
|
+
const utf8 = buf.toString('utf8');
|
|
198
|
+
if (!utf8.includes('\uFFFD')) return utf8;
|
|
199
|
+
try {
|
|
200
|
+
return new TextDecoder('gbk').decode(buf);
|
|
201
|
+
} catch {
|
|
202
|
+
return utf8;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let exportFileCounter = 0;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Read the UTF-8 file written by `es.exe -export-json <file>`.
|
|
210
|
+
* The file occasionally exists but is still empty right after the process
|
|
211
|
+
* exits (flush race), so an empty read is retried briefly.
|
|
212
|
+
* Returns { exists, text } — `exists: false` lets the caller fall back to
|
|
213
|
+
* decoding stdout (e.g. an es.exe build that ignores -export-json).
|
|
214
|
+
*/
|
|
215
|
+
function readExportText(filePath) {
|
|
216
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
217
|
+
try {
|
|
218
|
+
const text = readFileSync(filePath, 'utf8');
|
|
219
|
+
if (text.length > 0) return { exists: true, text: text.replace(/^\uFEFF/, '') };
|
|
220
|
+
} catch {
|
|
221
|
+
return { exists: false, text: '' };
|
|
222
|
+
}
|
|
223
|
+
// exists but empty: brief wait for the flush race
|
|
224
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
225
|
+
}
|
|
226
|
+
return { exists: true, text: '' };
|
|
227
|
+
}
|
|
228
|
+
|
|
149
229
|
/**
|
|
150
230
|
* Shared search flag vector (everything except output formatting and result
|
|
151
231
|
* window). NOTE: `-regex` consumes the search text as its own value, so the
|
|
@@ -166,7 +246,13 @@ function buildSearchFlags(args, query) {
|
|
|
166
246
|
argv.push('-sort', sort);
|
|
167
247
|
}
|
|
168
248
|
if (args.regex === true) argv.push('-regex', query);
|
|
169
|
-
else
|
|
249
|
+
else {
|
|
250
|
+
const terms = tokenizeQuery(query);
|
|
251
|
+
if (terms.length === 0) {
|
|
252
|
+
throw new EverythingError('everything_search: "query" must contain at least one search term (not only quotes or whitespace).', 'ES_INVALID_ARGS');
|
|
253
|
+
}
|
|
254
|
+
argv.push(...terms);
|
|
255
|
+
}
|
|
170
256
|
return argv;
|
|
171
257
|
}
|
|
172
258
|
|
|
@@ -286,7 +372,7 @@ const PARAMETERS_SCHEMA = {
|
|
|
286
372
|
};
|
|
287
373
|
|
|
288
374
|
const TOOL_DESCRIPTION =
|
|
289
|
-
'Locate files and folders across every indexed drive on this machine through the local Everything (voidtools) index — answers in milliseconds, far faster than glob or a recursive directory scan. Use this FIRST whenever you need to find a file or folder by name, extension, or path, especially outside the workspace. The query uses Everything search syntax, not a glob: plain words match names, `*`/`?` wildcards, `ext:js;ts` filters extensions, `dm:today` / `dm:thisweek` filter date modified, `size:>10mb` filters size, space means AND, `|` means OR, `!` means NOT
|
|
375
|
+
'Locate files and folders across every indexed drive on this machine through the local Everything (voidtools) index — answers in milliseconds, far faster than glob or a recursive directory scan. Use this FIRST whenever you need to find a file or folder by name, extension, or path, especially outside the workspace. The query uses Everything search syntax, not a glob: plain words match names, `*`/`?` wildcards, `ext:js;ts` filters extensions, `dm:today` / `dm:thisweek` filter date modified, `size:>10mb` filters size, space means AND, `|` means OR, `!` means NOT. Two quoting quirks: do NOT quote a filename-with-extension like "report.docx" — quoted phrases only match the name part, so pass it unquoted; and quoted phrases must not contain spaces (use regex=true for exact multi-word matching). Combine sort="date-modified-descending" with dm:thisweek for recently changed files, or sort="size-descending" to find the biggest ones first. Restrict scope with the dedicated path parameter rather than a path: prefix inside query. Set regex=true for a full regular expression over the whole path, countOnly=true for a fast match count without paths, dateCreated=true to also return creation dates. Returns absolute paths with size and date modified; non-ASCII names come back intact. Names and paths only, never file contents — use grep for content. Coverage follows the local Everything index: NTFS/reFS drives and explicitly added folders; FAT/exFAT removable drives and network shares are typically not indexed.';
|
|
290
376
|
|
|
291
377
|
const OUTPUT_SCHEMA = {
|
|
292
378
|
type: 'object',
|
|
@@ -416,8 +502,34 @@ export function apply(ctx, config = {}) {
|
|
|
416
502
|
if (input.dateCreated === true) searchArgv.push('-dc');
|
|
417
503
|
if (offset > 0) searchArgv.push('-viewport-offset', String(offset));
|
|
418
504
|
searchArgv.push(...flags);
|
|
419
|
-
|
|
420
|
-
|
|
505
|
+
|
|
506
|
+
// Primary path: es.exe writes results to a UTF-8 file. Piping stdout
|
|
507
|
+
// instead emits the ANSI codepage and mangles non-ASCII names (GBK on a
|
|
508
|
+
// zh-CN machine, question marks elsewhere).
|
|
509
|
+
const exportPath = joinPath(tmpdir(), `dsh-everything-${process.pid}-${Date.now()}-${++exportFileCounter}.json`);
|
|
510
|
+
try {
|
|
511
|
+
let stdoutBuf;
|
|
512
|
+
try {
|
|
513
|
+
stdoutBuf = await runEsWithRetry([...searchArgv, '-export-json', exportPath], signal);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
// es.exe builds that reject the option: retry once in stdout mode.
|
|
516
|
+
if (!(error instanceof EverythingError) || error.code !== 'ES_FAILED' || !/export|unknown|invalid|option/i.test(error.message ?? '')) throw error;
|
|
517
|
+
stdoutBuf = await runEsWithRetry(searchArgv, signal);
|
|
518
|
+
return { flags, results: parseResults(decodeEsStdout(stdoutBuf)) };
|
|
519
|
+
}
|
|
520
|
+
const exported = readExportText(exportPath);
|
|
521
|
+
if (!exported.exists) {
|
|
522
|
+
// No file written: an es.exe that ignores the option printed to stdout.
|
|
523
|
+
return { flags, results: parseResults(decodeEsStdout(stdoutBuf)) };
|
|
524
|
+
}
|
|
525
|
+
return { flags, results: parseResults(exported.text) };
|
|
526
|
+
} finally {
|
|
527
|
+
try {
|
|
528
|
+
unlinkSync(exportPath);
|
|
529
|
+
} catch {
|
|
530
|
+
/* best effort */
|
|
531
|
+
}
|
|
532
|
+
}
|
|
421
533
|
}
|
|
422
534
|
|
|
423
535
|
async function execute(args, exec) {
|
|
@@ -447,7 +559,7 @@ export function apply(ctx, config = {}) {
|
|
|
447
559
|
if (results.length === limit) {
|
|
448
560
|
try {
|
|
449
561
|
const countOut = await runEs(esPath, ['-get-result-count', ...flags], timeoutMs, signal);
|
|
450
|
-
const parsed = Number.parseInt(countOut.trim(), 10);
|
|
562
|
+
const parsed = Number.parseInt(String(countOut).trim(), 10);
|
|
451
563
|
if (Number.isFinite(parsed) && parsed >= total) total = parsed;
|
|
452
564
|
} catch {
|
|
453
565
|
/* count is cosmetic; keep the page-based estimate */
|
|
@@ -482,10 +594,12 @@ export function apply(ctx, config = {}) {
|
|
|
482
594
|
export {
|
|
483
595
|
buildSearchFlags,
|
|
484
596
|
clampInt,
|
|
597
|
+
decodeEsStdout,
|
|
485
598
|
formatSize,
|
|
486
599
|
parseResults,
|
|
487
600
|
presentCall,
|
|
488
601
|
presentResult,
|
|
489
602
|
renderResults,
|
|
490
603
|
resolveEsPath,
|
|
604
|
+
tokenizeQuery,
|
|
491
605
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-everything",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "DeepSeek Harness plugin: millisecond whole-disk file search for agents via the local Everything (voidtools) index",
|
|
5
5
|
"keywords": ["dsh", "deepseek-harness", "everything", "voidtools", "file-search", "agent-tool"],
|
|
6
6
|
"license": "MIT",
|