@qvac/harness 0.8.9 → 0.8.11
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 +73 -1
- package/dist-lib/index.d.ts +1 -1
- package/dist-lib/index.js +51 -50
- package/dist-lib/lib/audio-transcode-bare.js +2 -2
- package/dist-lib/lib/capabilities/completion.d.ts +1 -2
- package/dist-lib/lib/capabilities/completion.js +29 -25
- package/dist-lib/lib/capabilities/tool-prompt-log.js +3 -3
- package/dist-lib/lib/capabilities/voice.js +7 -6
- package/dist-lib/lib/connect.js +18 -17
- package/dist-lib/lib/errors.d.ts +266 -0
- package/dist-lib/lib/errors.js +189 -0
- package/dist-lib/lib/helpers/load-stall.d.ts +1 -1
- package/dist-lib/lib/helpers/load-stall.js +2 -2
- package/dist-lib/lib/helpers/race-abort.d.ts +2 -1
- package/dist-lib/lib/helpers/race-abort.js +3 -4
- package/dist-lib/lib/hooks/built-in/arg-repair.js +2 -2
- package/dist-lib/lib/hooks/pipeline.js +4 -4
- package/dist-lib/lib/hooks/run.js +2 -2
- package/dist-lib/lib/log.d.ts +26 -2
- package/dist-lib/lib/log.js +66 -16
- package/dist-lib/lib/models/model-pool.js +11 -6
- package/dist-lib/lib/models/model-source.js +3 -2
- package/dist-lib/lib/no-redirect-fetch-bare.js +2 -1
- package/dist-lib/lib/no-redirect-fetch-impl.js +2 -1
- package/dist-lib/lib/office/extract.js +4 -3
- package/dist-lib/lib/office/index.js +2 -1
- package/dist-lib/lib/resolution-detector.js +3 -3
- package/dist-lib/lib/rpc-session.js +2 -1
- package/dist-lib/lib/sdk-loader.js +2 -2
- package/dist-lib/lib/serve.js +33 -29
- package/dist-lib/lib/spawn.d.ts +2 -4
- package/dist-lib/lib/spawn.js +14 -7
- package/dist-lib/lib/tools/edit-image.js +2 -1
- package/dist-lib/lib/tools/exec/adapters/python/attachments.js +8 -8
- package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.js +7 -6
- package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +8 -8
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +11 -7
- package/dist-lib/lib/tools/exec/adapters/python/wheels.js +5 -5
- package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +9 -8
- package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.d.ts +2 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +4 -4
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn.d.ts +2 -1
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn.js +2 -1
- package/dist-lib/lib/tools/exec/obsidian/note-name.js +2 -1
- package/dist-lib/lib/tools/exec/obsidian/safe-path.js +5 -4
- package/dist-lib/lib/tools/exec/obsidian/vault-create.js +3 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +3 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-read.js +6 -5
- package/dist-lib/lib/tools/exec/obsidian/vault-write.js +13 -12
- package/dist-lib/lib/tools/exec/runner.js +2 -1
- package/dist-lib/lib/tools/files/edit-file.d.ts +3 -0
- package/dist-lib/lib/tools/files/edit-file.js +73 -0
- package/dist-lib/lib/tools/files/glob.d.ts +3 -0
- package/dist-lib/lib/tools/files/glob.js +63 -0
- package/dist-lib/lib/tools/files/grep.d.ts +3 -0
- package/dist-lib/lib/tools/files/grep.js +114 -0
- package/dist-lib/lib/tools/files/index.d.ts +3 -0
- package/dist-lib/lib/tools/files/index.js +23 -0
- package/dist-lib/lib/tools/files/list-dir.d.ts +5 -0
- package/dist-lib/lib/tools/files/list-dir.js +130 -0
- package/dist-lib/lib/tools/files/make-dir.d.ts +3 -0
- package/dist-lib/lib/tools/files/make-dir.js +47 -0
- package/dist-lib/lib/tools/files/read-file.d.ts +3 -0
- package/dist-lib/lib/tools/files/read-file.js +103 -0
- package/dist-lib/lib/tools/files/transfer.d.ts +4 -0
- package/dist-lib/lib/tools/files/transfer.js +82 -0
- package/dist-lib/lib/tools/files/trash-file.d.ts +4 -0
- package/dist-lib/lib/tools/files/trash-file.js +61 -0
- package/dist-lib/lib/tools/files/write-file.d.ts +3 -0
- package/dist-lib/lib/tools/files/write-file.js +55 -0
- package/dist-lib/lib/tools/fs/document-text.d.ts +2 -0
- package/dist-lib/lib/tools/fs/document-text.js +38 -0
- package/dist-lib/lib/tools/fs/edit-match.d.ts +11 -0
- package/dist-lib/lib/tools/fs/edit-match.js +85 -0
- package/dist-lib/lib/tools/fs/remote.d.ts +2 -1
- package/dist-lib/lib/tools/fs/remote.js +4 -4
- package/dist-lib/lib/tools/fs/scope.d.ts +1 -1
- package/dist-lib/lib/tools/fs/scope.js +3 -2
- package/dist-lib/lib/tools/fs/targets.d.ts +10 -0
- package/dist-lib/lib/tools/fs/targets.js +32 -0
- package/dist-lib/lib/tools/fs/tool-path.d.ts +24 -0
- package/dist-lib/lib/tools/fs/tool-path.js +58 -0
- package/dist-lib/lib/tools/fs/walk.js +3 -3
- package/dist-lib/lib/tools/generate-image.d.ts +2 -2
- package/dist-lib/lib/tools/generate-image.js +5 -6
- package/dist-lib/lib/tools/office.js +2 -1
- package/dist-lib/lib/tools/skill.js +2 -1
- package/dist-lib/lib/tools/skills/bundled-dir.js +4 -4
- package/dist-lib/lib/tools/skills/catalog.js +2 -2
- package/dist-lib/lib/tools/skills/cli-argv.js +25 -5
- package/dist-lib/lib/tools/skills/credential-health.js +3 -3
- package/dist-lib/lib/tools/skills/overlay-crud.js +3 -2
- package/dist-lib/lib/tools/translate.d.ts +2 -2
- package/dist-lib/lib/tools/translate.js +5 -6
- package/dist-lib/lib/tools/weather-lookup/geocode.js +5 -4
- package/dist-lib/lib/web-search/engines/bing.js +2 -1
- package/dist-lib/lib/web-search/engines/google.js +2 -1
- package/dist-lib/lib/web-search/fetch-html.d.ts +5 -5
- package/dist-lib/lib/web-search/fetch-html.js +12 -15
- package/package.json +67 -25
- package/worker/harness-sidecar.mjs +12 -13
- package/dist-lib/lib/tools/create-documents.d.ts +0 -3
- package/dist-lib/lib/tools/create-documents.js +0 -95
- package/dist-lib/lib/tools/edit-documents.d.ts +0 -3
- package/dist-lib/lib/tools/edit-documents.js +0 -132
- package/dist-lib/lib/tools/file-glob.d.ts +0 -14
- package/dist-lib/lib/tools/file-glob.js +0 -118
- package/dist-lib/lib/tools/grep.d.ts +0 -5
- package/dist-lib/lib/tools/grep.js +0 -184
- package/dist-lib/lib/tools/read-files.d.ts +0 -5
- package/dist-lib/lib/tools/read-files.js +0 -157
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { errorMessage } from '../guards.js';
|
|
2
|
-
import { hlog } from '../log.js';
|
|
3
|
-
import { searchTargets } from './file-glob.js';
|
|
4
|
-
import { readLines } from './fs/read.js';
|
|
5
|
-
import { isDriveReadTruncated } from './fs/remote.js';
|
|
6
|
-
import { canonicalDenyRoots, defaultRealpath, isDeniedPath } from './fs/scope.js';
|
|
7
|
-
import { walkFiles } from './fs/walk.js';
|
|
8
|
-
export const DEFAULT_GREP_MATCHES = 200;
|
|
9
|
-
export const MAX_GREP_MATCHES = 1000;
|
|
10
|
-
const MAX_LINE_CHARS = 500;
|
|
11
|
-
const BINARY_PROBE_CHARS = 1024;
|
|
12
|
-
async function scanFile(drive, key, { path, tests, room }) {
|
|
13
|
-
const hits = [];
|
|
14
|
-
let probed = 0;
|
|
15
|
-
let binary = false;
|
|
16
|
-
let index = 0;
|
|
17
|
-
try {
|
|
18
|
-
for await (const line of readLines(drive, key)) {
|
|
19
|
-
index++;
|
|
20
|
-
if (probed < BINARY_PROBE_CHARS && line.includes('\0')) {
|
|
21
|
-
binary = true;
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
if (probed < BINARY_PROBE_CHARS) {
|
|
25
|
-
probed += line.length + 1;
|
|
26
|
-
}
|
|
27
|
-
if (!tests.some((test) => test(line))) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
hits.push({ file: path, line: index, text: line.slice(0, MAX_LINE_CHARS) });
|
|
31
|
-
if (hits.length >= room) {
|
|
32
|
-
return { hits, binary, end: 'capped' };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (isDriveReadTruncated(error)) {
|
|
38
|
-
return { hits, binary, end: 'cut' };
|
|
39
|
-
}
|
|
40
|
-
return { hits, binary, end: 'unreadable', error };
|
|
41
|
-
}
|
|
42
|
-
return { hits, binary, end: 'complete' };
|
|
43
|
-
}
|
|
44
|
-
function stringArray(raw) {
|
|
45
|
-
if (!Array.isArray(raw) || raw.length === 0)
|
|
46
|
-
return null;
|
|
47
|
-
const values = raw.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
48
|
-
return values.length === raw.length ? values : null;
|
|
49
|
-
}
|
|
50
|
-
function compileQueries(queries, regex, caseSensitive) {
|
|
51
|
-
if (regex) {
|
|
52
|
-
try {
|
|
53
|
-
return queries.map((q) => {
|
|
54
|
-
const rx = new RegExp(q, caseSensitive ? '' : 'i');
|
|
55
|
-
return (line) => rx.test(line);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
return { error: `invalid regex: ${errorMessage(err)}` };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return queries.map((q) => {
|
|
63
|
-
const needle = caseSensitive ? q : q.toLowerCase();
|
|
64
|
-
return caseSensitive
|
|
65
|
-
? (line) => line.includes(needle)
|
|
66
|
-
: (line) => line.toLowerCase().includes(needle);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
export function createGrepTool({ realpath = defaultRealpath, deniedRoots } = {}) {
|
|
70
|
-
return {
|
|
71
|
-
name: 'grep',
|
|
72
|
-
toolset: 'files',
|
|
73
|
-
description: 'Search file contents inside the granted directories. Queries are literal ' +
|
|
74
|
-
'substrings by default (case-insensitive); set regex=true for patterns. ' +
|
|
75
|
-
'Returns matching lines with file and line number. Symlinks are never ' +
|
|
76
|
-
'followed; skippedLinks counts the ones left out.',
|
|
77
|
-
parameters: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: {
|
|
80
|
-
queries: {
|
|
81
|
-
type: 'array',
|
|
82
|
-
description: 'Text or patterns to search for',
|
|
83
|
-
items: { type: 'string' }
|
|
84
|
-
},
|
|
85
|
-
dir: {
|
|
86
|
-
type: 'string',
|
|
87
|
-
description: 'Optional directory to search (default: every granted directory)'
|
|
88
|
-
},
|
|
89
|
-
regex: {
|
|
90
|
-
type: 'boolean',
|
|
91
|
-
description: 'Treat queries as regular expressions (default false)'
|
|
92
|
-
},
|
|
93
|
-
caseSensitive: { type: 'boolean', description: 'Match case exactly (default false)' },
|
|
94
|
-
maxMatches: {
|
|
95
|
-
type: 'number',
|
|
96
|
-
description: `Optional match cap (default ${DEFAULT_GREP_MATCHES}, max ${MAX_GREP_MATCHES})`
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
required: ['queries']
|
|
100
|
-
},
|
|
101
|
-
effect: () => 'read',
|
|
102
|
-
stableRead: true,
|
|
103
|
-
autoApprove: true,
|
|
104
|
-
rootsRequired: true,
|
|
105
|
-
validate(rawArgs) {
|
|
106
|
-
const queries = stringArray(rawArgs.queries);
|
|
107
|
-
if (!queries) {
|
|
108
|
-
return 'queries must be a non-empty array of strings';
|
|
109
|
-
}
|
|
110
|
-
const compiled = compileQueries(queries, rawArgs.regex === true, rawArgs.caseSensitive === true);
|
|
111
|
-
if ('error' in compiled) {
|
|
112
|
-
return compiled.error;
|
|
113
|
-
}
|
|
114
|
-
const maxMatches = rawArgs.maxMatches;
|
|
115
|
-
if (maxMatches !== undefined && maxMatches !== null) {
|
|
116
|
-
const n = Number(maxMatches);
|
|
117
|
-
if (!Number.isInteger(n) || n <= 0 || n > MAX_GREP_MATCHES) {
|
|
118
|
-
return `maxMatches must be a positive integer ≤ ${MAX_GREP_MATCHES}`;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
},
|
|
123
|
-
resource(args) {
|
|
124
|
-
const queries = stringArray(args.queries);
|
|
125
|
-
return queries ? { kind: 'file', label: `grep ${queries.join(', ')}` } : null;
|
|
126
|
-
},
|
|
127
|
-
async execute(rawArgs, ctx) {
|
|
128
|
-
const roots = ctx?.fsRoots ?? [];
|
|
129
|
-
const queries = stringArray(rawArgs.queries);
|
|
130
|
-
if (!queries) {
|
|
131
|
-
return { error: 'queries must be a non-empty array of strings' };
|
|
132
|
-
}
|
|
133
|
-
const compiled = compileQueries(queries, rawArgs.regex === true, rawArgs.caseSensitive === true);
|
|
134
|
-
if ('error' in compiled) {
|
|
135
|
-
return compiled;
|
|
136
|
-
}
|
|
137
|
-
const maxMatches = rawArgs.maxMatches ? Number(rawArgs.maxMatches) : DEFAULT_GREP_MATCHES;
|
|
138
|
-
const deny = await canonicalDenyRoots(deniedRoots, realpath);
|
|
139
|
-
const scoped = await searchTargets(rawArgs.dir, roots, { realpath, deny });
|
|
140
|
-
if ('error' in scoped) {
|
|
141
|
-
return scoped;
|
|
142
|
-
}
|
|
143
|
-
const matches = [];
|
|
144
|
-
let truncated = false;
|
|
145
|
-
let skippedLinks = 0;
|
|
146
|
-
for (const target of scoped.targets) {
|
|
147
|
-
const walk = await walkFiles(target.root, target.folder);
|
|
148
|
-
truncated ||= walk.truncated;
|
|
149
|
-
skippedLinks += walk.skippedLinks;
|
|
150
|
-
for (const file of walk.files) {
|
|
151
|
-
if (isDeniedPath(file.path, deny))
|
|
152
|
-
continue;
|
|
153
|
-
const scan = await scanFile(target.root.drive, file.key, {
|
|
154
|
-
path: file.path,
|
|
155
|
-
tests: compiled,
|
|
156
|
-
room: maxMatches - matches.length
|
|
157
|
-
});
|
|
158
|
-
if (scan.end === 'capped') {
|
|
159
|
-
matches.push(...scan.hits);
|
|
160
|
-
return { matches, truncated: true, skippedLinks: skippedLinks || undefined };
|
|
161
|
-
}
|
|
162
|
-
if (scan.end === 'unreadable') {
|
|
163
|
-
hlog('warn: grep skipped a file', {
|
|
164
|
-
file: file.path,
|
|
165
|
-
err: errorMessage(scan.error)
|
|
166
|
-
});
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (scan.end === 'cut') {
|
|
170
|
-
truncated = true;
|
|
171
|
-
}
|
|
172
|
-
if (!scan.binary) {
|
|
173
|
-
matches.push(...scan.hits);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return {
|
|
178
|
-
matches,
|
|
179
|
-
truncated: truncated || undefined,
|
|
180
|
-
skippedLinks: skippedLinks || undefined
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { ToolDefinition } from '../capabilities/completion.ts';
|
|
2
|
-
import { type FsToolOptions } from './fs/scope.ts';
|
|
3
|
-
export declare const DEFAULT_READ_CHARS = 8192;
|
|
4
|
-
export declare const MAX_READ_CHARS = 65536;
|
|
5
|
-
export declare function createReadFilesTool({ realpath, deniedRoots }?: FsToolOptions): ToolDefinition;
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { errorMessage, isRecord } from '../guards.js';
|
|
2
|
-
import { readLines } from './fs/read.js';
|
|
3
|
-
import { isDriveReadTruncated } from './fs/remote.js';
|
|
4
|
-
import { canonicalDenyRoots, defaultRealpath, resolveKey } from './fs/scope.js';
|
|
5
|
-
export const DEFAULT_READ_CHARS = 8192;
|
|
6
|
-
export const MAX_READ_CHARS = 65_536;
|
|
7
|
-
function lineIndex(value) {
|
|
8
|
-
if (value === undefined || value === null)
|
|
9
|
-
return undefined;
|
|
10
|
-
const n = Number(value);
|
|
11
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
return n;
|
|
15
|
-
}
|
|
16
|
-
function fileRequests(raw) {
|
|
17
|
-
if (!Array.isArray(raw) || raw.length === 0)
|
|
18
|
-
return null;
|
|
19
|
-
const files = [];
|
|
20
|
-
for (const entry of raw) {
|
|
21
|
-
if (!isRecord(entry) || typeof entry.path !== 'string' || !entry.path.trim()) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
const startLine = lineIndex(entry.startLine);
|
|
25
|
-
const endLine = lineIndex(entry.endLine);
|
|
26
|
-
if (startLine === null || endLine === null) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
files.push({ path: entry.path, startLine, endLine });
|
|
30
|
-
}
|
|
31
|
-
return files;
|
|
32
|
-
}
|
|
33
|
-
async function slice(drive, key, { request, maxChars }) {
|
|
34
|
-
const first = request.startLine ?? 1;
|
|
35
|
-
const last = request.endLine ?? Infinity;
|
|
36
|
-
const kept = [];
|
|
37
|
-
let length = 0;
|
|
38
|
-
let index = 0;
|
|
39
|
-
let capped = false;
|
|
40
|
-
try {
|
|
41
|
-
for await (const line of readLines(drive, key)) {
|
|
42
|
-
if (++index < first)
|
|
43
|
-
continue;
|
|
44
|
-
if (index > last)
|
|
45
|
-
break;
|
|
46
|
-
kept.push(line);
|
|
47
|
-
length += line.length + (kept.length > 1 ? 1 : 0);
|
|
48
|
-
if (length > maxChars) {
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
if (!isDriveReadTruncated(err)) {
|
|
55
|
-
throw err;
|
|
56
|
-
}
|
|
57
|
-
capped = true;
|
|
58
|
-
}
|
|
59
|
-
const text = kept.join('\n');
|
|
60
|
-
return text.length > maxChars || capped
|
|
61
|
-
? { content: text.slice(0, maxChars), truncated: true }
|
|
62
|
-
: { content: text };
|
|
63
|
-
}
|
|
64
|
-
export function createReadFilesTool({ realpath = defaultRealpath, deniedRoots } = {}) {
|
|
65
|
-
return {
|
|
66
|
-
name: 'read_files',
|
|
67
|
-
toolset: 'files',
|
|
68
|
-
description: 'Read the contents of one or more files inside the granted directories. ' +
|
|
69
|
-
'Optionally slice by 1-indexed line range. Failures are reported per file.',
|
|
70
|
-
parameters: {
|
|
71
|
-
type: 'object',
|
|
72
|
-
properties: {
|
|
73
|
-
files: {
|
|
74
|
-
type: 'array',
|
|
75
|
-
description: 'Files to read',
|
|
76
|
-
items: {
|
|
77
|
-
type: 'object',
|
|
78
|
-
properties: {
|
|
79
|
-
path: { type: 'string', description: 'File path' },
|
|
80
|
-
startLine: { type: 'number', description: 'Optional 1-indexed first line' },
|
|
81
|
-
endLine: { type: 'number', description: 'Optional 1-indexed last line (inclusive)' }
|
|
82
|
-
},
|
|
83
|
-
required: ['path']
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
maxChars: {
|
|
87
|
-
type: 'number',
|
|
88
|
-
description: `Optional per-file cap in characters (default ${DEFAULT_READ_CHARS}, max ${MAX_READ_CHARS})`
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
required: ['files']
|
|
92
|
-
},
|
|
93
|
-
effect: () => 'read',
|
|
94
|
-
stableRead: true,
|
|
95
|
-
autoApprove: true,
|
|
96
|
-
rootsRequired: true,
|
|
97
|
-
validate(rawArgs) {
|
|
98
|
-
if (!fileRequests(rawArgs.files)) {
|
|
99
|
-
return 'files must be a non-empty array of { path } with optional positive integer startLine/endLine';
|
|
100
|
-
}
|
|
101
|
-
const maxChars = rawArgs.maxChars;
|
|
102
|
-
if (maxChars !== undefined && maxChars !== null) {
|
|
103
|
-
const n = Number(maxChars);
|
|
104
|
-
if (!Number.isInteger(n) || n <= 0 || n > MAX_READ_CHARS) {
|
|
105
|
-
return `maxChars must be a positive integer ≤ ${MAX_READ_CHARS}`;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
},
|
|
110
|
-
resource(args) {
|
|
111
|
-
const files = fileRequests(args.files);
|
|
112
|
-
if (!files) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
const label = files
|
|
116
|
-
.map((f) => f.path)
|
|
117
|
-
.slice(0, 3)
|
|
118
|
-
.join(', ');
|
|
119
|
-
return {
|
|
120
|
-
kind: 'file',
|
|
121
|
-
label: files.length > 3 ? `${label} (+${files.length - 3} more)` : label
|
|
122
|
-
};
|
|
123
|
-
},
|
|
124
|
-
async execute(rawArgs, ctx) {
|
|
125
|
-
const roots = ctx?.fsRoots ?? [];
|
|
126
|
-
const requests = fileRequests(rawArgs.files);
|
|
127
|
-
if (!requests) {
|
|
128
|
-
return { error: 'files must be a non-empty array of { path }' };
|
|
129
|
-
}
|
|
130
|
-
const maxChars = rawArgs.maxChars ? Number(rawArgs.maxChars) : DEFAULT_READ_CHARS;
|
|
131
|
-
const files = [];
|
|
132
|
-
const failed = [];
|
|
133
|
-
const deny = await canonicalDenyRoots(deniedRoots, realpath);
|
|
134
|
-
for (const request of requests) {
|
|
135
|
-
const resolved = await resolveKey(request.path, roots, { realpath, deny });
|
|
136
|
-
if ('error' in resolved) {
|
|
137
|
-
failed.push({ path: request.path, error: resolved.error });
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
if ((await resolved.root.drive.entry(resolved.key)) === null) {
|
|
142
|
-
failed.push({ path: request.path, error: `no such file: ${request.path}` });
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
files.push({
|
|
146
|
-
path: request.path,
|
|
147
|
-
...(await slice(resolved.root.drive, resolved.key, { request, maxChars }))
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
failed.push({ path: request.path, error: errorMessage(err) });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { files, failed: failed.length ? failed : undefined };
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|