dsh-everything 0.0.0-stage → 1.3.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/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/README.zh-CN.md +95 -0
- package/cordis.patch.yml +8 -0
- package/icon.svg +6 -0
- package/index.js +491 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +22 -4
- package/setup.js +522 -0
package/index.js
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-everything: model-facing `everything_search` tool backed by the local
|
|
3
|
+
* Everything (voidtools) index through the official ES command line (es.exe).
|
|
4
|
+
*
|
|
5
|
+
* The tool spawns es.exe with a fixed argv vector (no shell layer), parses its
|
|
6
|
+
* `-json` output, and projects results onto the built-in search-paths card.
|
|
7
|
+
* No external npm dependencies. Windows only: Everything itself is a Windows
|
|
8
|
+
* application, so on other platforms a stub tool is registered that fails with
|
|
9
|
+
* a clear platform message instead of a confusing spawn error.
|
|
10
|
+
*
|
|
11
|
+
* Config (set in cordis.patch.yml):
|
|
12
|
+
* esPath absolute path to es.exe (default: well-known locations, then auto-install)
|
|
13
|
+
* everythingPath absolute path to Everything.exe (default: sibling of esPath / standard locations)
|
|
14
|
+
* autoStart launch Everything.exe when the index is not running (default: true)
|
|
15
|
+
* autoInstall download & install Everything + ES CLI when missing (default: true)
|
|
16
|
+
* installDir where the auto-installed ES CLI lives (default: %LOCALAPPDATA%\dsh-everything)
|
|
17
|
+
* maxResults default per-call result cap (default: 100, ceiling: 1000)
|
|
18
|
+
* timeoutMs per-spawn timeout in milliseconds (default: 8000)
|
|
19
|
+
*/
|
|
20
|
+
import { execFile, spawn } from 'node:child_process';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { win32 as win32path } from 'node:path';
|
|
23
|
+
import { ensureBackend, EverythingError } from './setup.js';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_MAX_RESULTS = 100;
|
|
26
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
27
|
+
const MAX_RESULTS_CEILING = 1000;
|
|
28
|
+
const MAX_BUFFER = 32 * 1024 * 1024;
|
|
29
|
+
const AUTOSTART_WAIT_MS = 15000;
|
|
30
|
+
const SETUP_BUDGET_MS = 240000;
|
|
31
|
+
|
|
32
|
+
/** Fallback candidates when the patch config does not pin `esPath`. */
|
|
33
|
+
const ES_CANDIDATES = [
|
|
34
|
+
'D:\\Everything\\es.exe',
|
|
35
|
+
'C:\\Program Files\\Everything\\es.exe',
|
|
36
|
+
'C:\\Program Files (x86)\\Everything\\es.exe',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const SORT_NAME_RE = /^[a-z][a-z0-9-]*(-(ascending|descending))?$/i;
|
|
40
|
+
|
|
41
|
+
export { EverythingError };
|
|
42
|
+
|
|
43
|
+
function clampInt(value, min, max, fallback) {
|
|
44
|
+
const n = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : NaN;
|
|
45
|
+
if (Number.isNaN(n)) return fallback;
|
|
46
|
+
return Math.min(max, Math.max(min, n));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveEsPath(config) {
|
|
50
|
+
if (config && typeof config.esPath === 'string' && config.esPath.trim().length > 0) {
|
|
51
|
+
return config.esPath.trim();
|
|
52
|
+
}
|
|
53
|
+
for (const candidate of ES_CANDIDATES) {
|
|
54
|
+
if (existsSync(candidate)) return candidate;
|
|
55
|
+
}
|
|
56
|
+
return null; // nothing found — auto-install or PATH fallback decides
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveEverythingPath(config, esPath) {
|
|
60
|
+
if (config && typeof config.everythingPath === 'string' && config.everythingPath.trim().length > 0) {
|
|
61
|
+
return config.everythingPath.trim();
|
|
62
|
+
}
|
|
63
|
+
if (typeof esPath === 'string' && /es\.exe$/i.test(esPath)) return win32path.join(win32path.dirname(esPath), 'Everything.exe');
|
|
64
|
+
return 'Everything.exe';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Run es.exe once and resolve with its raw stdout. Errors are classified into actionable codes. */
|
|
68
|
+
function runEs(esPath, argv, timeoutMs, signal) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
let child;
|
|
71
|
+
try {
|
|
72
|
+
child = execFile(
|
|
73
|
+
esPath,
|
|
74
|
+
argv,
|
|
75
|
+
{ timeout: timeoutMs, maxBuffer: MAX_BUFFER, windowsHide: true, signal },
|
|
76
|
+
(error, stdout, stderr) => {
|
|
77
|
+
const stderrText = typeof stderr === 'string' ? stderr.trim() : '';
|
|
78
|
+
if (error) {
|
|
79
|
+
const detail = `${stderrText} ${error.message ?? ''}`;
|
|
80
|
+
if (signal?.aborted) {
|
|
81
|
+
reject(new EverythingError('everything_search: the call was aborted (tool timeout or caller cancellation).', 'ES_ABORTED'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (error.killed || /timed out/i.test(error.message ?? '')) {
|
|
85
|
+
reject(new EverythingError(`everything_search: es.exe timed out after ${timeoutMs}ms; the index may still be building on first run — retry shortly, or narrow the query.`, 'ES_TIMEOUT'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (error.signal === 'SIGTERM' || /abort/i.test(error.message ?? '')) {
|
|
89
|
+
reject(new EverythingError('everything_search: the call was aborted (tool timeout or caller cancellation).', 'ES_ABORTED'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (/ENOENT/.test(detail)) {
|
|
93
|
+
reject(new EverythingError(`everything_search: es.exe not found at "${esPath}". Install Everything 1.4 plus the ES command line tool, or set esPath in the plugin config.`, 'ES_NOT_FOUND'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (/not running|IPC|find Everything|connect/i.test(detail)) {
|
|
97
|
+
reject(new EverythingError('everything_search: Everything is not running.', 'ES_NOT_RUNNING'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
reject(new EverythingError(`everything_search: es.exe failed (${error.code ?? 'unknown'})${stderrText ? `: ${stderrText}` : ''}`, 'ES_FAILED'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (/not running|IPC|find Everything/i.test(stderrText)) {
|
|
104
|
+
reject(new EverythingError('everything_search: Everything is not running.', 'ES_NOT_RUNNING'));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
resolve(typeof stdout === 'string' ? stdout : '');
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
reject(new EverythingError(`everything_search: could not launch es.exe at "${esPath}": ${error?.message ?? error}`, 'ES_SPAWN_FAILED'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
void child;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sleep(ms) {
|
|
119
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Launch Everything.exe detached and poll its IPC until it answers or the budget runs out. */
|
|
123
|
+
async function autoStartEverything(everythingPath, esPath, signal) {
|
|
124
|
+
if (!existsSync(everythingPath)) {
|
|
125
|
+
throw new EverythingError(
|
|
126
|
+
`everything_search: Everything is not running and Everything.exe was not found at "${everythingPath}". Start Everything manually or set everythingPath in the plugin config.`,
|
|
127
|
+
'ES_NOT_RUNNING'
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
spawn(everythingPath, ['-startup'], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new EverythingError(`everything_search: failed to launch Everything.exe: ${error?.message ?? error}`, 'ES_SPAWN_FAILED');
|
|
134
|
+
}
|
|
135
|
+
const deadline = Date.now() + AUTOSTART_WAIT_MS;
|
|
136
|
+
while (Date.now() < deadline) {
|
|
137
|
+
if (signal?.aborted) throw new EverythingError('everything_search: the call was aborted (tool timeout or caller cancellation).', 'ES_ABORTED');
|
|
138
|
+
try {
|
|
139
|
+
await runEs(esPath, ['-get-everything-version'], 3000, signal);
|
|
140
|
+
return;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (!(error instanceof EverythingError) || error.code !== 'ES_NOT_RUNNING') throw error;
|
|
143
|
+
await sleep(500);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw new EverythingError('everything_search: Everything was started but did not answer within 15s; retry the search.', 'ES_NOT_RUNNING');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Shared search flag vector (everything except output formatting and result
|
|
151
|
+
* window). NOTE: `-regex` consumes the search text as its own value, so the
|
|
152
|
+
* query must be appended here, last.
|
|
153
|
+
*/
|
|
154
|
+
function buildSearchFlags(args, query) {
|
|
155
|
+
const argv = [];
|
|
156
|
+
if (args.matchCase === true) argv.push('-case');
|
|
157
|
+
if (args.wholeWord === true) argv.push('-whole-words');
|
|
158
|
+
if (args.foldersOnly === true) argv.push('/ad');
|
|
159
|
+
else if (args.filesOnly === true) argv.push('/a-d');
|
|
160
|
+
if (typeof args.path === 'string' && args.path.trim().length > 0) argv.push('-path', args.path.trim());
|
|
161
|
+
if (typeof args.sort === 'string' && args.sort.trim().length > 0) {
|
|
162
|
+
const sort = args.sort.trim();
|
|
163
|
+
if (!SORT_NAME_RE.test(sort)) {
|
|
164
|
+
throw new EverythingError(`everything_search: invalid sort "${sort}"; use e.g. "date-modified-descending", "size", "name-ascending".`, 'ES_INVALID_ARGS');
|
|
165
|
+
}
|
|
166
|
+
argv.push('-sort', sort);
|
|
167
|
+
}
|
|
168
|
+
if (args.regex === true) argv.push('-regex', query);
|
|
169
|
+
else argv.push(query);
|
|
170
|
+
return argv;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseResults(stdout) {
|
|
174
|
+
const text = stdout.trim();
|
|
175
|
+
if (text.length === 0) return [];
|
|
176
|
+
let parsed;
|
|
177
|
+
try {
|
|
178
|
+
parsed = JSON.parse(text);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new EverythingError(`everything_search: could not parse es.exe JSON output: ${error?.message ?? error}`, 'ES_PARSE_FAILED');
|
|
181
|
+
}
|
|
182
|
+
if (!Array.isArray(parsed)) return [];
|
|
183
|
+
const results = [];
|
|
184
|
+
for (const item of parsed) {
|
|
185
|
+
if (typeof item !== 'object' || item === null || typeof item.filename !== 'string') continue;
|
|
186
|
+
const entry = { path: item.filename };
|
|
187
|
+
if (typeof item.size === 'number') entry.size = item.size;
|
|
188
|
+
if (typeof item.date_modified === 'string') entry.dateModified = item.date_modified;
|
|
189
|
+
if (typeof item.date_created === 'string') entry.dateCreated = item.date_created;
|
|
190
|
+
results.push(entry);
|
|
191
|
+
}
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatSize(bytes) {
|
|
196
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
197
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
198
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
199
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function renderResults(value) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
const { query, total, truncated, results, countOnly } = value;
|
|
205
|
+
if (countOnly) {
|
|
206
|
+
lines.push(total === 0 ? `Everything "${query}": no matches.` : `Everything "${query}": ${total} match(es) (count only).`);
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
if (results.length === 0) {
|
|
210
|
+
lines.push(`Everything "${query}": no matches.`);
|
|
211
|
+
return lines.join('\n');
|
|
212
|
+
}
|
|
213
|
+
lines.push(`Everything "${query}": ${results.length} of ${total} result(s)${truncated ? ' (truncated, refine the query, raise maxResults, or page with offset)' : ''}:`);
|
|
214
|
+
for (const entry of results) {
|
|
215
|
+
const meta = [entry.size !== undefined ? formatSize(entry.size) : null, entry.dateModified ?? null, entry.dateCreated ? `created ${entry.dateCreated}` : null]
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.join(', ');
|
|
218
|
+
lines.push(meta ? `${entry.path} [${meta}]` : entry.path);
|
|
219
|
+
}
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function presentCall(args) {
|
|
224
|
+
const query = typeof args?.query === 'string' ? args.query : '';
|
|
225
|
+
const where = typeof args?.path === 'string' && args.path.trim() ? ` in ${args.path.trim()}` : '';
|
|
226
|
+
return { card: 'generic', title: `Everything ${query}${where}`, kind: 'search', rawInput: query };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function presentResult(_args, result) {
|
|
230
|
+
if (result.isError) return undefined;
|
|
231
|
+
const meta = result.meta;
|
|
232
|
+
if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined;
|
|
233
|
+
const { shape, paths, truncated, total } = meta;
|
|
234
|
+
if (shape !== 'paths' || !Array.isArray(paths) || !paths.every((p) => typeof p === 'string')) return undefined;
|
|
235
|
+
return {
|
|
236
|
+
card: 'search',
|
|
237
|
+
shape: 'paths',
|
|
238
|
+
paths,
|
|
239
|
+
truncated: truncated === true,
|
|
240
|
+
total: typeof total === 'number' ? total : paths.length,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Parameters JSON Schema, shared by the real tool and the non-Windows stub. */
|
|
245
|
+
const PARAMETERS_SCHEMA = {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
query: {
|
|
249
|
+
type: 'string',
|
|
250
|
+
description:
|
|
251
|
+
'Everything search expression, e.g. "report ext:docx", "*.sln dm:thisweek", "path:\\\\projects\\\\ ext:py". Uses Everything search syntax, not a glob.',
|
|
252
|
+
},
|
|
253
|
+
path: {
|
|
254
|
+
type: 'string',
|
|
255
|
+
description: 'Restrict the search to this directory and everything below it (absolute path).',
|
|
256
|
+
},
|
|
257
|
+
maxResults: {
|
|
258
|
+
type: 'integer',
|
|
259
|
+
description: 'Maximum number of results to return (default from plugin config, ceiling 1000).',
|
|
260
|
+
},
|
|
261
|
+
offset: {
|
|
262
|
+
type: 'integer',
|
|
263
|
+
description: 'Skip this many leading results (pagination). Default 0.',
|
|
264
|
+
},
|
|
265
|
+
sort: {
|
|
266
|
+
type: 'string',
|
|
267
|
+
description:
|
|
268
|
+
'Sort order, e.g. "date-modified-descending", "size-descending", "name-ascending". Names: name, path, size, extension, date-created, date-modified, date-accessed, run-count.',
|
|
269
|
+
},
|
|
270
|
+
regex: { type: 'boolean', description: 'Treat query as a regular expression matched against the full path.' },
|
|
271
|
+
matchCase: { type: 'boolean', description: 'Match case (default: insensitive).' },
|
|
272
|
+
wholeWord: { type: 'boolean', description: 'Match whole words only.' },
|
|
273
|
+
foldersOnly: { type: 'boolean', description: 'Return folders only.' },
|
|
274
|
+
filesOnly: { type: 'boolean', description: 'Return files only.' },
|
|
275
|
+
dateCreated: {
|
|
276
|
+
type: 'boolean',
|
|
277
|
+
description: 'Also return the creation date of each result (costs a little extra time per result).',
|
|
278
|
+
},
|
|
279
|
+
countOnly: {
|
|
280
|
+
type: 'boolean',
|
|
281
|
+
description: 'Return only the total number of matches, without listing paths (fastest way to check existence or estimate volume).',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
required: ['query'],
|
|
285
|
+
additionalProperties: false,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
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, double quotes give an exact phrase (avoid combining quoted phrases with modifiers such as ext:). 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. 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
|
+
|
|
291
|
+
const OUTPUT_SCHEMA = {
|
|
292
|
+
type: 'object',
|
|
293
|
+
additionalProperties: false,
|
|
294
|
+
required: ['query', 'total', 'truncated', 'results'],
|
|
295
|
+
properties: {
|
|
296
|
+
query: { type: 'string' },
|
|
297
|
+
total: { type: 'integer' },
|
|
298
|
+
truncated: { type: 'boolean' },
|
|
299
|
+
countOnly: { type: 'boolean' },
|
|
300
|
+
results: {
|
|
301
|
+
type: 'array',
|
|
302
|
+
items: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
additionalProperties: false,
|
|
305
|
+
required: ['path'],
|
|
306
|
+
properties: {
|
|
307
|
+
path: { type: 'string' },
|
|
308
|
+
size: { type: 'integer' },
|
|
309
|
+
dateModified: { type: 'string' },
|
|
310
|
+
dateCreated: { type: 'string' },
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const inject = ['tools'];
|
|
318
|
+
|
|
319
|
+
export function apply(ctx, config = {}) {
|
|
320
|
+
// Everything is a Windows-only application. On other platforms register a
|
|
321
|
+
// stub that fails loudly and clearly instead of a spawn error deep inside.
|
|
322
|
+
if (process.platform !== 'win32') {
|
|
323
|
+
ctx.tools.register({
|
|
324
|
+
name: 'everything_search',
|
|
325
|
+
description: TOOL_DESCRIPTION,
|
|
326
|
+
parameters: PARAMETERS_SCHEMA,
|
|
327
|
+
isConcurrencySafe: () => true,
|
|
328
|
+
output: { schema: OUTPUT_SCHEMA, render: () => [{ type: 'text', text: 'everything_search is unavailable on this platform.' }] },
|
|
329
|
+
async execute() {
|
|
330
|
+
throw new EverythingError('everything_search: this tool requires Windows (Everything by voidtools is a Windows-only application).', 'ES_UNSUPPORTED_PLATFORM');
|
|
331
|
+
},
|
|
332
|
+
presentCall,
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const autoStart = config.autoStart !== false;
|
|
338
|
+
const autoInstall = config.autoInstall !== false;
|
|
339
|
+
const maxResults = clampInt(config.maxResults, 1, MAX_RESULTS_CEILING, DEFAULT_MAX_RESULTS);
|
|
340
|
+
const timeoutMs = clampInt(config.timeoutMs, 1000, 60000, DEFAULT_TIMEOUT_MS);
|
|
341
|
+
|
|
342
|
+
// Mutable: auto-install may re-resolve both paths after provisioning.
|
|
343
|
+
let esPath = resolveEsPath(config);
|
|
344
|
+
let everythingPath = resolveEverythingPath(config, esPath ?? 'es.exe');
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* One shared provisioning run. Rejects clear the cache so the next call can
|
|
348
|
+
* retry (e.g. after the user approves a UAC prompt on a second attempt).
|
|
349
|
+
*/
|
|
350
|
+
let setupPromise = null;
|
|
351
|
+
function ensureReady() {
|
|
352
|
+
if (!setupPromise) {
|
|
353
|
+
setupPromise = ensureBackend({
|
|
354
|
+
config,
|
|
355
|
+
log: ctx.logger,
|
|
356
|
+
probe: async (es, evPath) => {
|
|
357
|
+
try {
|
|
358
|
+
await runEs(es, ['-get-everything-version'], 3000);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error instanceof EverythingError && error.code === 'ES_NOT_RUNNING' && autoStart && existsSync(evPath)) {
|
|
361
|
+
await autoStartEverything(evPath, es);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
})
|
|
368
|
+
.then((result) => {
|
|
369
|
+
esPath = result.esPath;
|
|
370
|
+
everythingPath = result.everythingPath;
|
|
371
|
+
if (result.installed.length > 0) ctx.logger?.info?.(`everything_search: auto-installed ${result.installed.join(', ')} under ${result.installDir}`);
|
|
372
|
+
return result;
|
|
373
|
+
})
|
|
374
|
+
.catch((error) => {
|
|
375
|
+
setupPromise = null;
|
|
376
|
+
throw error;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return setupPromise;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Kick provisioning in the background so the first search usually finds a
|
|
383
|
+
// ready backend; failures are logged, never thrown from here.
|
|
384
|
+
if (autoInstall && (!esPath || !existsSync(everythingPath))) {
|
|
385
|
+
ensureReady().catch((error) => {
|
|
386
|
+
ctx.logger?.warn?.(`everything_search: auto-install failed (${error?.message ?? error}). Install Everything + ES CLI from https://www.voidtools.com/ or set esPath in the plugin config.`);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Run one es.exe argv with backend recovery: provision the CLI/Everything
|
|
392
|
+
* when missing (autoInstall), start Everything when it is not running
|
|
393
|
+
* (autoStart). Shared by the search path and count-only mode.
|
|
394
|
+
*/
|
|
395
|
+
async function runEsWithRetry(argv, signal) {
|
|
396
|
+
try {
|
|
397
|
+
return await runEs(esPath ?? 'es.exe', argv, timeoutMs, signal);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (!(error instanceof EverythingError)) throw error;
|
|
400
|
+
if (autoInstall && (error.code === 'ES_NOT_FOUND' || (error.code === 'ES_NOT_RUNNING' && !existsSync(everythingPath)))) {
|
|
401
|
+
// Backend not installed yet — provision (downloads + one possible UAC
|
|
402
|
+
// prompt), then fall through to the normal start-and-retry path.
|
|
403
|
+
await ensureReady();
|
|
404
|
+
}
|
|
405
|
+
if (!autoStart) throw error;
|
|
406
|
+
if (error.code === 'ES_NOT_FOUND' && !esPath) throw error;
|
|
407
|
+
if (error.code !== 'ES_NOT_RUNNING' && error.code !== 'ES_NOT_FOUND') throw error;
|
|
408
|
+
await autoStartEverything(everythingPath, esPath ?? 'es.exe', signal);
|
|
409
|
+
return runEs(esPath ?? 'es.exe', argv, timeoutMs, signal);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function searchOnce(input, query, limit, offset, signal) {
|
|
414
|
+
const flags = buildSearchFlags(input, query);
|
|
415
|
+
const searchArgv = ['-n', String(offset + limit), '-json', '-size', '-dm', '-date-format', '1'];
|
|
416
|
+
if (input.dateCreated === true) searchArgv.push('-dc');
|
|
417
|
+
if (offset > 0) searchArgv.push('-viewport-offset', String(offset));
|
|
418
|
+
searchArgv.push(...flags);
|
|
419
|
+
const stdout = await runEsWithRetry(searchArgv, signal);
|
|
420
|
+
return { flags, results: parseResults(stdout) };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function execute(args, exec) {
|
|
424
|
+
const input = typeof args === 'object' && args !== null ? args : {};
|
|
425
|
+
const signal = exec?.signal;
|
|
426
|
+
const query = typeof input.query === 'string' ? input.query.trim() : '';
|
|
427
|
+
if (query.length === 0) throw new EverythingError('everything_search: "query" must be a non-empty Everything search expression.', 'ES_INVALID_ARGS');
|
|
428
|
+
const limit = clampInt(input.maxResults, 1, MAX_RESULTS_CEILING, maxResults);
|
|
429
|
+
const offset = clampInt(input.offset, 0, Number.MAX_SAFE_INTEGER, 0);
|
|
430
|
+
|
|
431
|
+
// Count-only mode: one IPC round-trip, no paths listed.
|
|
432
|
+
if (input.countOnly === true) {
|
|
433
|
+
const flags = buildSearchFlags(input, query);
|
|
434
|
+
const countOut = await runEsWithRetry(['-get-result-count', ...flags], signal);
|
|
435
|
+
const total = Number.parseInt(String(countOut).trim(), 10);
|
|
436
|
+
if (!Number.isFinite(total) || total < 0) {
|
|
437
|
+
throw new EverythingError(`everything_search: could not parse the result count ("${countOut}").`, 'ES_PARSE_FAILED');
|
|
438
|
+
}
|
|
439
|
+
return { query, total, truncated: false, results: [], countOnly: true };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const { flags, results } = await searchOnce(input, query, limit, offset, signal);
|
|
443
|
+
|
|
444
|
+
// The exact total costs a second IPC round-trip; only fetch it when the
|
|
445
|
+
// page is full and truncation is actually possible.
|
|
446
|
+
let total = offset + results.length;
|
|
447
|
+
if (results.length === limit) {
|
|
448
|
+
try {
|
|
449
|
+
const countOut = await runEs(esPath, ['-get-result-count', ...flags], timeoutMs, signal);
|
|
450
|
+
const parsed = Number.parseInt(countOut.trim(), 10);
|
|
451
|
+
if (Number.isFinite(parsed) && parsed >= total) total = parsed;
|
|
452
|
+
} catch {
|
|
453
|
+
/* count is cosmetic; keep the page-based estimate */
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { query, total, truncated: total > offset + results.length, results };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
ctx.tools.register({
|
|
461
|
+
name: 'everything_search',
|
|
462
|
+
description: TOOL_DESCRIPTION,
|
|
463
|
+
parameters: PARAMETERS_SCHEMA,
|
|
464
|
+
timeoutMs: timeoutMs * 2 + AUTOSTART_WAIT_MS + 5000 + (autoInstall ? SETUP_BUDGET_MS : 0),
|
|
465
|
+
isConcurrencySafe: () => true,
|
|
466
|
+
output: {
|
|
467
|
+
schema: OUTPUT_SCHEMA,
|
|
468
|
+
render: (_args, value) => [{ type: 'text', text: renderResults(value) }],
|
|
469
|
+
presentationMeta: (_args, value) => ({
|
|
470
|
+
shape: 'paths',
|
|
471
|
+
paths: value.results.map((entry) => entry.path),
|
|
472
|
+
truncated: value.truncated,
|
|
473
|
+
total: value.total,
|
|
474
|
+
}),
|
|
475
|
+
},
|
|
476
|
+
execute,
|
|
477
|
+
presentCall,
|
|
478
|
+
presentResult,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export {
|
|
483
|
+
buildSearchFlags,
|
|
484
|
+
clampInt,
|
|
485
|
+
formatSize,
|
|
486
|
+
parseResults,
|
|
487
|
+
presentCall,
|
|
488
|
+
presentResult,
|
|
489
|
+
renderResults,
|
|
490
|
+
resolveEsPath,
|
|
491
|
+
};
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-everything",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "DeepSeek Harness plugin: millisecond whole-disk file search for agents via the local Everything (voidtools) index",
|
|
5
|
+
"keywords": ["dsh", "deepseek-harness", "everything", "voidtools", "file-search", "agent-tool"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": { "type": "git", "url": "git+https://github.com/S1n7T/dsh-everything.git" },
|
|
8
|
+
"bugs": "https://github.com/S1n7T/dsh-everything/issues",
|
|
9
|
+
"homepage": "https://github.com/S1n7T/dsh-everything#readme",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.js",
|
|
13
|
+
"./locale/*.json": "./locale/*.json",
|
|
14
|
+
"./icon": "./icon.svg"
|
|
15
|
+
},
|
|
16
|
+
"files": ["index.js", "setup.js", "cordis.patch.yml", "locale/*.json", "icon.svg", "README.md", "README.zh-CN.md", "CHANGELOG.md", "LICENSE"],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
|
|
24
|
+
}
|