@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
- const failure = buildFailure(error);
281
- this.failures.set(location.directory, failure);
282
- nodeLog('workspace.code_navigation.index.failed', {
283
- nodeId: this.options.nodeId(),
284
- failureCode: failure,
285
- sourceFiles: input.entries.length,
286
- candidateScanMs: input.candidateScanMs
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
- const key = `${workspace.id}\0${input.query.toLocaleLowerCase()}`;
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, input.query).finally(() => this.pending.delete(key));
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, query) {
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, query, deadline);
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, query, deadline) {
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, query, this.ripgrep ?? undefined);
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, query).map((match) => ({
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, query, ripgrep) {
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, query) {
326
+ function parseOutput(engine, output, search) {
305
327
  if (engine === 'RIPGREP')
306
- return parseRipgrep(output, query);
328
+ return parseRipgrep(output, search);
307
329
  if (engine === 'GREP')
308
- return parseGrep(output, query);
309
- return parsePowerShell(output, query);
330
+ return parseGrep(output, search);
331
+ return parsePowerShell(output, search);
310
332
  }
311
- function parseRipgrep(output, query) {
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 = lowerIndex(preview, query);
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, query) {
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, query) {
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 = 8;
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.95",
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.95",
34
- "@myagentroam/protocol": "0.9.95"
33
+ "@myagentroam/agent": "0.9.96",
34
+ "@myagentroam/protocol": "0.9.96"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/ws": "^8.18.1"