@myagentroam/node 0.9.95 → 0.9.96
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.
|
@@ -88,6 +88,7 @@ const buildWaiters = [];
|
|
|
88
88
|
export class WorkspaceCodeNavigationService {
|
|
89
89
|
options;
|
|
90
90
|
builds = new Map();
|
|
91
|
+
preparations = new Map();
|
|
91
92
|
candidateRefreshes = new Map();
|
|
92
93
|
locations = new Map();
|
|
93
94
|
failures = new Map();
|
|
@@ -96,6 +97,49 @@ export class WorkspaceCodeNavigationService {
|
|
|
96
97
|
constructor(options) {
|
|
97
98
|
this.options = options;
|
|
98
99
|
}
|
|
100
|
+
async prepare(workspace, input) {
|
|
101
|
+
if (typeof input.path !== 'string' || input.path.length === 0 || input.path.length > 4096)
|
|
102
|
+
throw new Error('SYMBOL_DEFINITION_INVALID');
|
|
103
|
+
if (!isNavigablePath(input.path))
|
|
104
|
+
return { state: 'READY' };
|
|
105
|
+
if (!(await this.available()))
|
|
106
|
+
return { state: 'UNAVAILABLE' };
|
|
107
|
+
const location = await this.indexLocation(workspace);
|
|
108
|
+
this.locations.set(workspace.id, location);
|
|
109
|
+
this.forgotten.delete(location.directory);
|
|
110
|
+
const manifest = await readManifest(location.manifest);
|
|
111
|
+
const tags = manifest === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: await tagsPath(location, manifest).catch(() => undefined);
|
|
114
|
+
const reusable = manifest !== undefined &&
|
|
115
|
+
tags !== undefined &&
|
|
116
|
+
(await tagsAvailable(tags)) &&
|
|
117
|
+
manifest.sourceFiles !== undefined;
|
|
118
|
+
const current = reusable &&
|
|
119
|
+
manifest.schemaVersion === INDEX_SCHEMA_VERSION &&
|
|
120
|
+
manifest.optionsVersion === INDEX_OPTIONS_VERSION &&
|
|
121
|
+
manifest.ctagsVersion === BUNDLED_CTAGS_VERSION;
|
|
122
|
+
const cachedCandidates = this.options.files.index(workspace).peekContentCandidates();
|
|
123
|
+
const forceRefreshGeneration = this.forceFreshCandidates.get(workspace.id);
|
|
124
|
+
if (reusable) {
|
|
125
|
+
if (!current || cachedCandidates === undefined || forceRefreshGeneration !== undefined)
|
|
126
|
+
this.refreshCandidatesInBackground(workspace, location, manifest, {
|
|
127
|
+
requireFresh: forceRefreshGeneration !== undefined,
|
|
128
|
+
mustBuild: !current || forceRefreshGeneration !== undefined,
|
|
129
|
+
forceRefreshGeneration
|
|
130
|
+
});
|
|
131
|
+
return { state: current ? 'READY' : 'STALE' };
|
|
132
|
+
}
|
|
133
|
+
const failure = this.failures.get(location.directory);
|
|
134
|
+
if (failure !== undefined &&
|
|
135
|
+
!this.builds.has(location.directory) &&
|
|
136
|
+
!this.preparations.has(location.directory)) {
|
|
137
|
+
this.failures.delete(location.directory);
|
|
138
|
+
return { state: 'FAILED', failureCode: failure };
|
|
139
|
+
}
|
|
140
|
+
this.startInitialBuild(workspace, location);
|
|
141
|
+
return { state: 'BUILDING' };
|
|
142
|
+
}
|
|
99
143
|
async definition(workspace, input) {
|
|
100
144
|
const path = input.path;
|
|
101
145
|
const symbol = input.symbol;
|
|
@@ -276,15 +320,32 @@ export class WorkspaceCodeNavigationService {
|
|
|
276
320
|
};
|
|
277
321
|
}
|
|
278
322
|
startBuild(input, location, previous) {
|
|
279
|
-
void this.ensureBuild(input, location, previous).catch((error) =>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
323
|
+
void this.ensureBuild(input, location, previous).catch((error) => this.recordBuildFailure(location, error, input));
|
|
324
|
+
}
|
|
325
|
+
startInitialBuild(workspace, location) {
|
|
326
|
+
if (this.builds.has(location.directory) || this.preparations.has(location.directory))
|
|
327
|
+
return;
|
|
328
|
+
const preparation = this.indexInput(workspace, true)
|
|
329
|
+
.then((input) => {
|
|
330
|
+
if (this.forgotten.has(location.directory))
|
|
331
|
+
return;
|
|
332
|
+
return this.ensureBuild(input, location);
|
|
333
|
+
})
|
|
334
|
+
.catch((error) => this.recordBuildFailure(location, error))
|
|
335
|
+
.finally(() => {
|
|
336
|
+
if (this.preparations.get(location.directory) === preparation)
|
|
337
|
+
this.preparations.delete(location.directory);
|
|
338
|
+
});
|
|
339
|
+
this.preparations.set(location.directory, preparation);
|
|
340
|
+
}
|
|
341
|
+
recordBuildFailure(location, error, input) {
|
|
342
|
+
const failure = buildFailure(error);
|
|
343
|
+
this.failures.set(location.directory, failure);
|
|
344
|
+
nodeLog('workspace.code_navigation.index.failed', {
|
|
345
|
+
nodeId: this.options.nodeId(),
|
|
346
|
+
failureCode: failure,
|
|
347
|
+
sourceFiles: input?.entries.length ?? 0,
|
|
348
|
+
candidateScanMs: input?.candidateScanMs ?? 0
|
|
288
349
|
});
|
|
289
350
|
}
|
|
290
351
|
refreshCandidatesInBackground(workspace, location, manifest, options = {
|
|
@@ -53,29 +53,38 @@ export class WorkspaceContentSearchService {
|
|
|
53
53
|
search(workspace, input) {
|
|
54
54
|
if (typeof input.query !== 'string' || input.query.length < 1 || input.query.length > 256)
|
|
55
55
|
throw new Error('FILE_CONTENT_SEARCH_INVALID');
|
|
56
|
-
|
|
56
|
+
if (input.regex !== undefined && typeof input.regex !== 'boolean')
|
|
57
|
+
throw new Error('FILE_CONTENT_SEARCH_INVALID');
|
|
58
|
+
const search = { query: input.query, regex: input.regex === true };
|
|
59
|
+
const queryKey = search.regex ? search.query : search.query.toLocaleLowerCase();
|
|
60
|
+
const key = `${workspace.id}\0${search.regex}\0${queryKey}`;
|
|
57
61
|
const existing = this.pending.get(key);
|
|
58
62
|
if (existing !== undefined)
|
|
59
63
|
return existing;
|
|
60
|
-
const task = this.perform(workspace,
|
|
64
|
+
const task = this.perform(workspace, search).finally(() => this.pending.delete(key));
|
|
61
65
|
this.pending.set(key, task);
|
|
62
66
|
return task;
|
|
63
67
|
}
|
|
64
|
-
async perform(workspace,
|
|
68
|
+
async perform(workspace, search) {
|
|
65
69
|
const deadline = Date.now() + TIMEOUT_MS;
|
|
66
70
|
const release = await acquireSearch(deadline);
|
|
67
71
|
try {
|
|
72
|
+
if (search.regex && this.ripgrep === null)
|
|
73
|
+
throw new Error('RIPGREP_RESOURCE_UNAVAILABLE');
|
|
68
74
|
const prepared = await this.candidates(workspace, deadline);
|
|
69
|
-
if (prepared.files.length === 0)
|
|
75
|
+
if (prepared.files.length === 0) {
|
|
76
|
+
if (search.regex)
|
|
77
|
+
await this.validateNativeRegex(workspace.path, search, deadline);
|
|
70
78
|
return {
|
|
71
79
|
matches: [],
|
|
72
80
|
engine: 'NODE',
|
|
73
81
|
truncated: prepared.truncated || Date.now() >= deadline,
|
|
74
82
|
skippedFiles: prepared.skippedFiles
|
|
75
83
|
};
|
|
84
|
+
}
|
|
76
85
|
if (this.ripgrep !== null) {
|
|
77
86
|
try {
|
|
78
|
-
const matches = await this.nativeSearch('RIPGREP', workspace.path, prepared.files,
|
|
87
|
+
const matches = await this.nativeSearch('RIPGREP', workspace.path, prepared.files, search, deadline);
|
|
79
88
|
return {
|
|
80
89
|
matches: matches.slice(0, RESULT_LIMIT),
|
|
81
90
|
engine: 'RIPGREP',
|
|
@@ -83,11 +92,13 @@ export class WorkspaceContentSearchService {
|
|
|
83
92
|
skippedFiles: prepared.skippedFiles
|
|
84
93
|
};
|
|
85
94
|
}
|
|
86
|
-
catch {
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (search.regex)
|
|
97
|
+
throw error;
|
|
87
98
|
// Missing, incompatible or failed bundled resources safely fall through.
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
|
-
const fallback = await nodeSearch(prepared.files, query, deadline);
|
|
101
|
+
const fallback = await nodeSearch(prepared.files, search.query, deadline);
|
|
91
102
|
return {
|
|
92
103
|
matches: fallback.matches.slice(0, RESULT_LIMIT),
|
|
93
104
|
engine: 'NODE',
|
|
@@ -99,6 +110,14 @@ export class WorkspaceContentSearchService {
|
|
|
99
110
|
release();
|
|
100
111
|
}
|
|
101
112
|
}
|
|
113
|
+
async validateNativeRegex(root, search, deadline) {
|
|
114
|
+
const command = commandFor('RIPGREP', [], search, this.ripgrep ?? undefined);
|
|
115
|
+
const result = await runCommand(command.file, [...command.args, '-'], root, deadline);
|
|
116
|
+
if (result.code === 2)
|
|
117
|
+
throw new Error('FILE_CONTENT_SEARCH_INVALID');
|
|
118
|
+
if (!command.validCodes.includes(result.code))
|
|
119
|
+
throw new Error('CONTENT_SEARCH_COMMAND_FAILED');
|
|
120
|
+
}
|
|
102
121
|
async candidates(workspace, deadline) {
|
|
103
122
|
const root = await realpath(workspace.path);
|
|
104
123
|
let candidates;
|
|
@@ -139,21 +158,24 @@ export class WorkspaceContentSearchService {
|
|
|
139
158
|
}
|
|
140
159
|
return { files, skippedFiles, truncated };
|
|
141
160
|
}
|
|
142
|
-
async nativeSearch(engine, root, files,
|
|
161
|
+
async nativeSearch(engine, root, files, search, deadline) {
|
|
143
162
|
const matches = [];
|
|
144
163
|
const argumentBatchBytes = this.platform === 'win32' ? WINDOWS_ARGUMENT_BATCH_BYTES : POSIX_ARGUMENT_BATCH_BYTES;
|
|
145
164
|
for (const batch of batches(files, argumentBatchBytes)) {
|
|
146
165
|
if (Date.now() >= deadline || matches.length > RESULT_LIMIT)
|
|
147
166
|
break;
|
|
148
|
-
const command = commandFor(engine, batch,
|
|
167
|
+
const command = commandFor(engine, batch, search, this.ripgrep ?? undefined);
|
|
149
168
|
const result = await runCommand(command.file, command.args, root, deadline, engine === 'RIPGREP');
|
|
150
|
-
if (!command.validCodes.includes(result.code))
|
|
169
|
+
if (!command.validCodes.includes(result.code)) {
|
|
170
|
+
if (search.regex && result.code === 2)
|
|
171
|
+
throw new Error('FILE_CONTENT_SEARCH_INVALID');
|
|
151
172
|
throw new Error('CONTENT_SEARCH_COMMAND_FAILED');
|
|
173
|
+
}
|
|
152
174
|
const paths = new Map(batch.flatMap((file) => [
|
|
153
175
|
[normalizeResultPath(file.path, this.platform), file.path],
|
|
154
176
|
[normalizeResultPath(file.absolutePath, this.platform), file.path]
|
|
155
177
|
]));
|
|
156
|
-
matches.push(...parseOutput(engine, result.stdout,
|
|
178
|
+
matches.push(...parseOutput(engine, result.stdout, search).map((match) => ({
|
|
157
179
|
...match,
|
|
158
180
|
path: paths.get(normalizeResultPath(match.path, this.platform)) ??
|
|
159
181
|
(() => {
|
|
@@ -213,7 +235,7 @@ function batches(files, maximumBytes) {
|
|
|
213
235
|
result.push(current);
|
|
214
236
|
return result;
|
|
215
237
|
}
|
|
216
|
-
function commandFor(engine, files,
|
|
238
|
+
function commandFor(engine, files, search, ripgrep) {
|
|
217
239
|
const paths = files.map((file) => file.path);
|
|
218
240
|
if (engine === 'RIPGREP')
|
|
219
241
|
return {
|
|
@@ -224,12 +246,12 @@ function commandFor(engine, files, query, ripgrep) {
|
|
|
224
246
|
args: [
|
|
225
247
|
'--json',
|
|
226
248
|
'--no-config',
|
|
227
|
-
'--fixed-strings',
|
|
228
249
|
'--ignore-case',
|
|
229
250
|
'--max-count',
|
|
230
251
|
String(PER_FILE_LIMIT),
|
|
252
|
+
...(search.regex ? [] : ['--fixed-strings']),
|
|
231
253
|
'--',
|
|
232
|
-
query,
|
|
254
|
+
search.query,
|
|
233
255
|
...paths
|
|
234
256
|
],
|
|
235
257
|
validCodes: [0, 1]
|
|
@@ -301,14 +323,14 @@ async function runCommand(file, args, cwd, deadline, stopAfterResultLimit = fals
|
|
|
301
323
|
});
|
|
302
324
|
});
|
|
303
325
|
}
|
|
304
|
-
function parseOutput(engine, output,
|
|
326
|
+
function parseOutput(engine, output, search) {
|
|
305
327
|
if (engine === 'RIPGREP')
|
|
306
|
-
return parseRipgrep(output,
|
|
328
|
+
return parseRipgrep(output, search);
|
|
307
329
|
if (engine === 'GREP')
|
|
308
|
-
return parseGrep(output,
|
|
309
|
-
return parsePowerShell(output,
|
|
330
|
+
return parseGrep(output, search);
|
|
331
|
+
return parsePowerShell(output, search);
|
|
310
332
|
}
|
|
311
|
-
function parseRipgrep(output,
|
|
333
|
+
function parseRipgrep(output, search) {
|
|
312
334
|
return output
|
|
313
335
|
.toString('utf8')
|
|
314
336
|
.split('\n')
|
|
@@ -320,7 +342,9 @@ function parseRipgrep(output, query) {
|
|
|
320
342
|
return [];
|
|
321
343
|
const data = value.data;
|
|
322
344
|
const preview = String(data?.lines?.text).replace(/[\r\n]+$/u, '');
|
|
323
|
-
const column =
|
|
345
|
+
const column = search.regex
|
|
346
|
+
? regexRipgrepIndex(preview, data?.submatches)
|
|
347
|
+
: lowerIndex(preview, search.query);
|
|
324
348
|
if (typeof data?.path?.text !== 'string' || !Number.isInteger(data.line_number) || column < 0)
|
|
325
349
|
throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
|
|
326
350
|
return [
|
|
@@ -333,7 +357,7 @@ function parseRipgrep(output, query) {
|
|
|
333
357
|
];
|
|
334
358
|
});
|
|
335
359
|
}
|
|
336
|
-
function parseGrep(output,
|
|
360
|
+
function parseGrep(output, search) {
|
|
337
361
|
const matches = [];
|
|
338
362
|
let offset = 0;
|
|
339
363
|
while (offset < output.length) {
|
|
@@ -345,7 +369,7 @@ function parseGrep(output, query) {
|
|
|
345
369
|
const record = output.subarray(separator + 1, end).toString('utf8');
|
|
346
370
|
const colon = record.indexOf(':');
|
|
347
371
|
const preview = record.slice(colon + 1).replace(/\r$/u, '');
|
|
348
|
-
const column = lowerIndex(preview, query);
|
|
372
|
+
const column = lowerIndex(preview, search.query);
|
|
349
373
|
if (colon < 1 || column < 0)
|
|
350
374
|
throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
|
|
351
375
|
matches.push({
|
|
@@ -358,7 +382,7 @@ function parseGrep(output, query) {
|
|
|
358
382
|
}
|
|
359
383
|
return matches;
|
|
360
384
|
}
|
|
361
|
-
function parsePowerShell(output,
|
|
385
|
+
function parsePowerShell(output, search) {
|
|
362
386
|
return output
|
|
363
387
|
.toString('utf8')
|
|
364
388
|
.split(/\r?\n/u)
|
|
@@ -370,7 +394,7 @@ function parsePowerShell(output, query) {
|
|
|
370
394
|
!Number.isInteger(match.line) ||
|
|
371
395
|
typeof match.preview !== 'string')
|
|
372
396
|
throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
|
|
373
|
-
const column = lowerIndex(match.preview, query);
|
|
397
|
+
const column = lowerIndex(match.preview, search.query);
|
|
374
398
|
if (column < 0)
|
|
375
399
|
throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
|
|
376
400
|
return [
|
|
@@ -445,6 +469,12 @@ async function nodeSearch(files, query, deadline) {
|
|
|
445
469
|
function lowerIndex(source, query) {
|
|
446
470
|
return source.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
|
|
447
471
|
}
|
|
472
|
+
function regexRipgrepIndex(preview, submatches) {
|
|
473
|
+
const start = submatches?.[0]?.start;
|
|
474
|
+
if (typeof start !== 'number' || !Number.isInteger(start) || start < 0)
|
|
475
|
+
return -1;
|
|
476
|
+
return Buffer.from(preview, 'utf8').subarray(0, start).toString('utf8').length;
|
|
477
|
+
}
|
|
448
478
|
function limitPerFile(matches) {
|
|
449
479
|
const counts = new Map();
|
|
450
480
|
return matches.filter((match) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os';
|
|
2
2
|
import { isAbsolute, win32 } from 'node:path';
|
|
3
3
|
import { listWorkspaceFiles, mutateWorkspaceFile, normalizeFilePreviewPath, readAllowedFileContentRange, readWorkspaceFileContentRange, readAllowedTextFile, readWorkspaceTextFile, writeWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
|
|
4
|
-
const INDEX_CACHE_LIMIT =
|
|
4
|
+
const INDEX_CACHE_LIMIT = 32;
|
|
5
5
|
export class WorkspaceFileService {
|
|
6
6
|
state;
|
|
7
7
|
allowedRoots;
|
|
@@ -37,6 +37,7 @@ export class WorkspaceWorkbenchService {
|
|
|
37
37
|
'workspace.files.search': (data) => this.searchFiles(record(data)),
|
|
38
38
|
'workspace.files.mutate': (data) => this.mutateFile(record(data)),
|
|
39
39
|
'workspace.content.search': (data) => this.searchContent(record(data)),
|
|
40
|
+
'workspace.symbol.prepare': (data) => this.prepareSymbolIndex(record(data)),
|
|
40
41
|
'workspace.symbol.definition': (data) => this.symbolDefinition(record(data)),
|
|
41
42
|
'workspace.files.upload.preflight': (data) => this.preflight(record(data)),
|
|
42
43
|
'workspace.files.upload.create': (data) => this.createUpload(record(data)),
|
|
@@ -116,6 +117,10 @@ export class WorkspaceWorkbenchService {
|
|
|
116
117
|
const workspace = this.options.requireWorkspace(input.workspaceId);
|
|
117
118
|
return { definition: await this.codeNavigation.definition(workspace, input) };
|
|
118
119
|
}
|
|
120
|
+
async prepareSymbolIndex(input) {
|
|
121
|
+
const workspace = this.options.requireWorkspace(input.workspaceId);
|
|
122
|
+
return { preparation: await this.codeNavigation.prepare(workspace, input) };
|
|
123
|
+
}
|
|
119
124
|
async preflight(input) {
|
|
120
125
|
return {
|
|
121
126
|
uploads: await this.options.uploads.preflight(this.options.requireWorkspace(input.workspaceId), input)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.96",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"npm": "11.12.1",
|
|
31
31
|
"ws": "^8.21.3",
|
|
32
32
|
"zod": "4.4.3",
|
|
33
|
-
"@myagentroam/agent": "0.9.
|
|
34
|
-
"@myagentroam/protocol": "0.9.
|
|
33
|
+
"@myagentroam/agent": "0.9.96",
|
|
34
|
+
"@myagentroam/protocol": "0.9.96"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/ws": "^8.18.1"
|