@myagentroam/node 0.1.3 → 0.1.6

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.
Files changed (195) hide show
  1. package/README.md +9 -1
  2. package/dist/capabilities.d.ts +2 -0
  3. package/dist/capabilities.js +15 -4
  4. package/dist/claude-agent-sdk.d.ts +4 -16
  5. package/dist/claude-agent-sdk.js +18 -79
  6. package/dist/claude-channel.js +0 -1
  7. package/dist/codex-app-server.d.ts +5 -0
  8. package/dist/codex-app-server.js +26 -5
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +18 -2
  11. package/dist/connector/node-connector-options.d.ts +26 -0
  12. package/dist/connector/node-connector-options.js +1 -0
  13. package/dist/connector/node-operation-router.d.ts +9 -0
  14. package/dist/connector/node-operation-router.js +22 -0
  15. package/dist/connector.d.ts +62 -356
  16. package/dist/connector.js +714 -6176
  17. package/dist/control-outbound-scheduler.d.ts +26 -0
  18. package/dist/control-outbound-scheduler.js +121 -0
  19. package/dist/database.d.ts +4 -0
  20. package/dist/database.js +15 -10
  21. package/dist/event-buffer.js +0 -1
  22. package/dist/fake-runner.js +0 -1
  23. package/dist/health.js +0 -1
  24. package/dist/main.js +6 -1
  25. package/dist/migrations/v001.js +0 -1
  26. package/dist/migrations/v002.js +0 -1
  27. package/dist/migrations/v003.d.ts +4 -0
  28. package/dist/migrations/v003.js +44 -0
  29. package/dist/native-jsonl-reader.d.ts +14 -0
  30. package/dist/native-jsonl-reader.js +97 -0
  31. package/dist/native-session-history.d.ts +41 -4
  32. package/dist/native-session-history.js +340 -159
  33. package/dist/opencode-runtime.d.ts +33 -0
  34. package/dist/opencode-runtime.js +80 -0
  35. package/dist/opencode-server.d.ts +47 -0
  36. package/dist/opencode-server.js +250 -0
  37. package/dist/operational.js +10 -3
  38. package/dist/process-tree.js +0 -1
  39. package/dist/runner/abstract-runner.d.ts +153 -0
  40. package/dist/runner/abstract-runner.js +154 -0
  41. package/dist/runner/claude/managed-run-controller.d.ts +52 -0
  42. package/dist/runner/claude/managed-run-controller.js +326 -0
  43. package/dist/runner/claude-code-runner.d.ts +70 -0
  44. package/dist/runner/claude-code-runner.js +286 -0
  45. package/dist/runner/codex/conversation-parser.d.ts +59 -0
  46. package/dist/runner/codex/conversation-parser.js +816 -0
  47. package/dist/runner/codex/managed-run-controller.d.ts +80 -0
  48. package/dist/runner/codex/managed-run-controller.js +388 -0
  49. package/dist/runner/codex-runner.d.ts +85 -0
  50. package/dist/runner/codex-runner.js +365 -0
  51. package/dist/runner/fake-test-runner.d.ts +26 -0
  52. package/dist/runner/fake-test-runner.js +47 -0
  53. package/dist/runner/opencode/conversation-parser.d.ts +27 -0
  54. package/dist/runner/opencode/conversation-parser.js +217 -0
  55. package/dist/runner/opencode/managed-run-controller.d.ts +76 -0
  56. package/dist/runner/opencode/managed-run-controller.js +451 -0
  57. package/dist/runner/opencode-runner.d.ts +60 -0
  58. package/dist/runner/opencode-runner.js +330 -0
  59. package/dist/runner/runner-registry.d.ts +19 -0
  60. package/dist/runner/runner-registry.js +75 -0
  61. package/dist/runner-command-engine.js +0 -1
  62. package/dist/runner-profiles.d.ts +4 -0
  63. package/dist/runner-profiles.js +76 -3
  64. package/dist/runner-usage.d.ts +2 -2
  65. package/dist/runner-usage.js +3 -6
  66. package/dist/runtime-state.js +26 -14
  67. package/dist/service/attachment-domain-state.d.ts +5 -0
  68. package/dist/service/attachment-domain-state.js +5 -0
  69. package/dist/service/conversation-history-service.d.ts +34 -0
  70. package/dist/service/conversation-history-service.js +158 -0
  71. package/dist/service/external-session-resume-service.d.ts +32 -0
  72. package/dist/service/external-session-resume-service.js +98 -0
  73. package/dist/service/fake-managed-run-service.d.ts +29 -0
  74. package/dist/service/fake-managed-run-service.js +62 -0
  75. package/dist/service/managed-runner-session-service.d.ts +12 -0
  76. package/dist/service/managed-runner-session-service.js +35 -0
  77. package/dist/service/native-session-projection-service.d.ts +51 -0
  78. package/dist/service/native-session-projection-service.js +271 -0
  79. package/dist/service/native-session-watch-service.d.ts +37 -0
  80. package/dist/service/native-session-watch-service.js +150 -0
  81. package/dist/service/node-connection-lifecycle-service.d.ts +37 -0
  82. package/dist/service/node-connection-lifecycle-service.js +85 -0
  83. package/dist/service/node-control-channel.d.ts +32 -0
  84. package/dist/service/node-control-channel.js +85 -0
  85. package/dist/service/node-control-message-service.d.ts +28 -0
  86. package/dist/service/node-control-message-service.js +104 -0
  87. package/dist/service/node-request-service.d.ts +14 -0
  88. package/dist/service/node-request-service.js +50 -0
  89. package/dist/service/node-run-event-bridge.d.ts +40 -0
  90. package/dist/service/node-run-event-bridge.js +83 -0
  91. package/dist/service/node-terminal-channel.d.ts +29 -0
  92. package/dist/service/node-terminal-channel.js +182 -0
  93. package/dist/service/node-upgrade-coordinator.d.ts +29 -0
  94. package/dist/service/node-upgrade-coordinator.js +137 -0
  95. package/dist/service/node-workspace-coordinator.d.ts +32 -0
  96. package/dist/service/node-workspace-coordinator.js +80 -0
  97. package/dist/service/run-attachment-service.d.ts +37 -0
  98. package/dist/service/run-attachment-service.js +160 -0
  99. package/dist/service/run-domain-state.d.ts +4 -0
  100. package/dist/service/run-domain-state.js +4 -0
  101. package/dist/service/run-event-service.d.ts +31 -0
  102. package/dist/service/run-event-service.js +102 -0
  103. package/dist/service/run-workbench-service.d.ts +27 -0
  104. package/dist/service/run-workbench-service.js +113 -0
  105. package/dist/service/runner-channel-message-service.d.ts +13 -0
  106. package/dist/service/runner-channel-message-service.js +20 -0
  107. package/dist/service/runner-interaction-service.d.ts +40 -0
  108. package/dist/service/runner-interaction-service.js +246 -0
  109. package/dist/service/runner-service.d.ts +44 -0
  110. package/dist/service/runner-service.js +68 -0
  111. package/dist/service/session-catalog-service.d.ts +45 -0
  112. package/dist/service/session-catalog-service.js +224 -0
  113. package/dist/service/session-command-service.d.ts +23 -0
  114. package/dist/service/session-command-service.js +69 -0
  115. package/dist/service/session-context-service.d.ts +34 -0
  116. package/dist/service/session-context-service.js +87 -0
  117. package/dist/service/session-domain-state.d.ts +8 -0
  118. package/dist/service/session-domain-state.js +8 -0
  119. package/dist/service/session-identity-service.d.ts +24 -0
  120. package/dist/service/session-identity-service.js +86 -0
  121. package/dist/service/session-lifecycle-service.d.ts +31 -0
  122. package/dist/service/session-lifecycle-service.js +242 -0
  123. package/dist/service/session-message-service.d.ts +33 -0
  124. package/dist/service/session-message-service.js +157 -0
  125. package/dist/service/session-presentation-service.d.ts +33 -0
  126. package/dist/service/session-presentation-service.js +36 -0
  127. package/dist/service/session-query-service.d.ts +31 -0
  128. package/dist/service/session-query-service.js +77 -0
  129. package/dist/service/terminal-relay-state.d.ts +6 -0
  130. package/dist/service/terminal-relay-state.js +3 -0
  131. package/dist/service/terminal-service.d.ts +12 -0
  132. package/dist/service/terminal-service.js +52 -0
  133. package/dist/service/workbench-event-service.d.ts +21 -0
  134. package/dist/service/workbench-event-service.js +53 -0
  135. package/dist/service/workbench-manifest-service.d.ts +89 -0
  136. package/dist/service/workbench-manifest-service.js +70 -0
  137. package/dist/service/workspace-change-service.d.ts +9 -0
  138. package/dist/service/workspace-change-service.js +86 -0
  139. package/dist/service/workspace-content-search-service.d.ts +14 -0
  140. package/dist/service/workspace-content-search-service.js +477 -0
  141. package/dist/service/workspace-domain-state.d.ts +75 -0
  142. package/dist/service/workspace-domain-state.js +40 -0
  143. package/dist/service/workspace-file-service.d.ts +17 -0
  144. package/dist/service/workspace-file-service.js +109 -0
  145. package/dist/service/workspace-queue-workbench-service.d.ts +55 -0
  146. package/dist/service/workspace-queue-workbench-service.js +226 -0
  147. package/dist/service/workspace-service.d.ts +15 -0
  148. package/dist/service/workspace-service.js +67 -0
  149. package/dist/service/workspace-session-service.d.ts +23 -0
  150. package/dist/service/workspace-session-service.js +139 -0
  151. package/dist/service/workspace-upload-service.d.ts +37 -0
  152. package/dist/service/workspace-upload-service.js +240 -0
  153. package/dist/service/workspace-watch-service.d.ts +18 -0
  154. package/dist/service/workspace-watch-service.js +120 -0
  155. package/dist/service/workspace-workbench-service.d.ts +47 -0
  156. package/dist/service/workspace-workbench-service.js +223 -0
  157. package/dist/service.js +2 -3
  158. package/dist/storage.js +0 -1
  159. package/dist/supervisor.d.ts +1 -0
  160. package/dist/supervisor.js +154 -0
  161. package/dist/terminal.d.ts +1 -0
  162. package/dist/terminal.js +1 -2
  163. package/dist/util/node-operation-parsers.d.ts +92 -0
  164. package/dist/util/node-operation-parsers.js +382 -0
  165. package/dist/util/runner-native-session-parsers.d.ts +72 -0
  166. package/dist/util/runner-native-session-parsers.js +381 -0
  167. package/dist/util/secret-environment.d.ts +1 -0
  168. package/dist/util/secret-environment.js +25 -0
  169. package/dist/workspace.d.ts +62 -2
  170. package/dist/workspace.js +539 -57
  171. package/package.json +3 -2
  172. package/dist/capabilities.js.map +0 -1
  173. package/dist/claude-agent-sdk.js.map +0 -1
  174. package/dist/claude-channel.js.map +0 -1
  175. package/dist/codex-app-server.js.map +0 -1
  176. package/dist/config.js.map +0 -1
  177. package/dist/connector.js.map +0 -1
  178. package/dist/database.js.map +0 -1
  179. package/dist/event-buffer.js.map +0 -1
  180. package/dist/fake-runner.js.map +0 -1
  181. package/dist/health.js.map +0 -1
  182. package/dist/main.js.map +0 -1
  183. package/dist/migrations/v001.js.map +0 -1
  184. package/dist/migrations/v002.js.map +0 -1
  185. package/dist/native-session-history.js.map +0 -1
  186. package/dist/operational.js.map +0 -1
  187. package/dist/process-tree.js.map +0 -1
  188. package/dist/runner-command-engine.js.map +0 -1
  189. package/dist/runner-profiles.js.map +0 -1
  190. package/dist/runner-usage.js.map +0 -1
  191. package/dist/runtime-state.js.map +0 -1
  192. package/dist/service.js.map +0 -1
  193. package/dist/storage.js.map +0 -1
  194. package/dist/terminal.js.map +0 -1
  195. package/dist/workspace.js.map +0 -1
@@ -0,0 +1,477 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createReadStream } from 'node:fs';
3
+ import { lstat, realpath } from 'node:fs/promises';
4
+ import { isAbsolute, join, relative, sep } from 'node:path';
5
+ const RESULT_LIMIT = 200;
6
+ const PER_FILE_LIMIT = 20;
7
+ const FILE_BYTE_LIMIT = 2 * 1024 * 1024;
8
+ const SCAN_BYTE_LIMIT = 128 * 1024 * 1024;
9
+ const OUTPUT_BYTE_LIMIT = 2 * 1024 * 1024;
10
+ const TIMEOUT_MS = 5_000;
11
+ const WINDOWS_ARGUMENT_BATCH_BYTES = 24 * 1024;
12
+ const POSIX_ARGUMENT_BATCH_BYTES = 96 * 1024;
13
+ const SEARCH_CONCURRENCY = 2;
14
+ const SEARCH_QUEUE_LIMIT = 16;
15
+ const CANDIDATE_VALIDATION_CONCURRENCY = 32;
16
+ let activeSearches = 0;
17
+ const searchWaiters = [];
18
+ async function acquireSearch(deadline) {
19
+ if (activeSearches >= SEARCH_CONCURRENCY) {
20
+ if (searchWaiters.length >= SEARCH_QUEUE_LIMIT)
21
+ throw new Error('FILE_CONTENT_SEARCH_BUSY');
22
+ await new Promise((resolve, reject) => {
23
+ const resume = () => {
24
+ clearTimeout(timer);
25
+ resolve();
26
+ };
27
+ const timer = setTimeout(() => {
28
+ const index = searchWaiters.indexOf(resume);
29
+ if (index >= 0)
30
+ searchWaiters.splice(index, 1);
31
+ reject(new Error('CONTENT_SEARCH_TIMEOUT'));
32
+ }, Math.max(1, deadline - Date.now()));
33
+ searchWaiters.push(resume);
34
+ });
35
+ }
36
+ activeSearches += 1;
37
+ return () => {
38
+ activeSearches -= 1;
39
+ searchWaiters.shift()?.();
40
+ };
41
+ }
42
+ export class WorkspaceContentSearchService {
43
+ files;
44
+ platform;
45
+ pending = new Map();
46
+ constructor(files, platform = process.platform) {
47
+ this.files = files;
48
+ this.platform = platform;
49
+ }
50
+ search(workspace, input) {
51
+ if (typeof input.query !== 'string' || input.query.length < 1 || input.query.length > 256)
52
+ throw new Error('FILE_CONTENT_SEARCH_INVALID');
53
+ const key = `${workspace.id}\0${input.query.toLocaleLowerCase()}`;
54
+ const existing = this.pending.get(key);
55
+ if (existing !== undefined)
56
+ return existing;
57
+ const task = this.perform(workspace, input.query).finally(() => this.pending.delete(key));
58
+ this.pending.set(key, task);
59
+ return task;
60
+ }
61
+ async perform(workspace, query) {
62
+ const deadline = Date.now() + TIMEOUT_MS;
63
+ const release = await acquireSearch(deadline);
64
+ try {
65
+ const prepared = await this.candidates(workspace, deadline);
66
+ if (prepared.files.length === 0)
67
+ return {
68
+ matches: [],
69
+ engine: 'NODE',
70
+ truncated: prepared.truncated || Date.now() >= deadline,
71
+ skippedFiles: prepared.skippedFiles
72
+ };
73
+ for (const engine of this.engines()) {
74
+ try {
75
+ const matches = await this.nativeSearch(engine, workspace.path, prepared.files, query, deadline);
76
+ return {
77
+ matches: matches.slice(0, RESULT_LIMIT),
78
+ engine,
79
+ truncated: prepared.truncated || matches.length > RESULT_LIMIT || Date.now() >= deadline,
80
+ skippedFiles: prepared.skippedFiles
81
+ };
82
+ }
83
+ catch {
84
+ // Missing, incompatible or failed native tools safely fall through.
85
+ }
86
+ }
87
+ const fallback = await nodeSearch(prepared.files, query, deadline);
88
+ return {
89
+ matches: fallback.matches.slice(0, RESULT_LIMIT),
90
+ engine: 'NODE',
91
+ truncated: prepared.truncated || fallback.matches.length > RESULT_LIMIT || Date.now() >= deadline,
92
+ skippedFiles: prepared.skippedFiles + fallback.skippedFiles
93
+ };
94
+ }
95
+ finally {
96
+ release();
97
+ }
98
+ }
99
+ engines() {
100
+ return this.platform === 'win32' ? ['RIPGREP', 'POWERSHELL'] : ['RIPGREP', 'GREP'];
101
+ }
102
+ async candidates(workspace, deadline) {
103
+ const root = await realpath(workspace.path);
104
+ let candidates;
105
+ try {
106
+ candidates = await beforeDeadline(this.files.index(workspace).contentCandidates(), deadline);
107
+ }
108
+ catch (error) {
109
+ if (error instanceof Error && error.message === 'CONTENT_SEARCH_TIMEOUT')
110
+ return { files: [], skippedFiles: 0, truncated: true };
111
+ throw error;
112
+ }
113
+ const files = [];
114
+ let skippedFiles = 0;
115
+ let bytes = 0;
116
+ let truncated = candidates.truncated;
117
+ for (let offset = 0; offset < candidates.entries.length; offset += CANDIDATE_VALIDATION_CONCURRENCY) {
118
+ if (Date.now() >= deadline) {
119
+ truncated = true;
120
+ break;
121
+ }
122
+ const validated = await Promise.all(candidates.entries
123
+ .slice(offset, offset + CANDIDATE_VALIDATION_CONCURRENCY)
124
+ .map((entry) => validateCandidate(root, entry.path)));
125
+ for (const candidate of validated) {
126
+ if (candidate === undefined) {
127
+ skippedFiles += 1;
128
+ continue;
129
+ }
130
+ if (bytes + candidate.size > SCAN_BYTE_LIMIT) {
131
+ truncated = true;
132
+ break;
133
+ }
134
+ bytes += candidate.size;
135
+ files.push({ path: candidate.path, absolutePath: candidate.absolutePath });
136
+ }
137
+ if (truncated)
138
+ break;
139
+ }
140
+ return { files, skippedFiles, truncated };
141
+ }
142
+ async nativeSearch(engine, root, files, query, deadline) {
143
+ const matches = [];
144
+ const argumentBatchBytes = this.platform === 'win32' ? WINDOWS_ARGUMENT_BATCH_BYTES : POSIX_ARGUMENT_BATCH_BYTES;
145
+ for (const batch of batches(files, argumentBatchBytes)) {
146
+ if (Date.now() >= deadline || matches.length > RESULT_LIMIT)
147
+ break;
148
+ const command = commandFor(engine, batch, query);
149
+ const result = await runCommand(command.file, command.args, root, deadline, engine === 'RIPGREP');
150
+ if (!command.validCodes.includes(result.code))
151
+ throw new Error('CONTENT_SEARCH_COMMAND_FAILED');
152
+ const paths = new Map(batch.flatMap((file) => [
153
+ [normalizeResultPath(file.path, this.platform), file.path],
154
+ [normalizeResultPath(file.absolutePath, this.platform), file.path]
155
+ ]));
156
+ matches.push(...parseOutput(engine, result.stdout, query).map((match) => ({
157
+ ...match,
158
+ path: paths.get(normalizeResultPath(match.path, this.platform)) ??
159
+ (() => {
160
+ throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
161
+ })()
162
+ })));
163
+ }
164
+ return limitPerFile(matches);
165
+ }
166
+ }
167
+ async function validateCandidate(root, path) {
168
+ try {
169
+ const absolutePath = await realpath(join(root, path));
170
+ const inside = relative(root, absolutePath);
171
+ if (inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside))
172
+ return undefined;
173
+ const metadata = await lstat(absolutePath);
174
+ return metadata.isFile() && metadata.size <= FILE_BYTE_LIMIT
175
+ ? { path, absolutePath, size: metadata.size }
176
+ : undefined;
177
+ }
178
+ catch {
179
+ return undefined;
180
+ }
181
+ }
182
+ async function beforeDeadline(promise, deadline) {
183
+ return new Promise((resolve, reject) => {
184
+ const timer = setTimeout(() => reject(new Error('CONTENT_SEARCH_TIMEOUT')), Math.max(1, deadline - Date.now()));
185
+ promise.then((value) => {
186
+ clearTimeout(timer);
187
+ resolve(value);
188
+ }, (error) => {
189
+ clearTimeout(timer);
190
+ reject(error);
191
+ });
192
+ });
193
+ }
194
+ function normalizeResultPath(path, platform) {
195
+ const normalized = path.replaceAll('\\', '/');
196
+ return platform === 'win32' ? normalized.toLocaleLowerCase() : normalized;
197
+ }
198
+ function batches(files, maximumBytes) {
199
+ const result = [];
200
+ let current = [];
201
+ let bytes = 0;
202
+ for (const file of files) {
203
+ const size = Buffer.byteLength(file.path) + 1;
204
+ if (current.length > 0 && bytes + size > maximumBytes) {
205
+ result.push(current);
206
+ current = [];
207
+ bytes = 0;
208
+ }
209
+ current.push(file);
210
+ bytes += size;
211
+ }
212
+ if (current.length > 0)
213
+ result.push(current);
214
+ return result;
215
+ }
216
+ function commandFor(engine, files, query) {
217
+ const paths = files.map((file) => file.path);
218
+ if (engine === 'RIPGREP')
219
+ return {
220
+ file: 'rg',
221
+ args: [
222
+ '--json',
223
+ '--no-config',
224
+ '--fixed-strings',
225
+ '--ignore-case',
226
+ '--max-count',
227
+ String(PER_FILE_LIMIT),
228
+ '--',
229
+ query,
230
+ ...paths
231
+ ],
232
+ validCodes: [0, 1]
233
+ };
234
+ if (engine === 'GREP')
235
+ return {
236
+ file: 'grep',
237
+ args: [
238
+ '-I',
239
+ '-i',
240
+ '-n',
241
+ '-H',
242
+ '-F',
243
+ '-Z',
244
+ '-m',
245
+ String(PER_FILE_LIMIT),
246
+ '-e',
247
+ query,
248
+ '--',
249
+ ...paths
250
+ ],
251
+ validCodes: [0, 1]
252
+ };
253
+ const script = [
254
+ '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;',
255
+ '$q=$args[0];',
256
+ '$args | Select-Object -Skip 1 | ForEach-Object {',
257
+ 'Select-String -LiteralPath $_ -SimpleMatch -CaseSensitive:$false -Pattern $q | Select-Object -First 20 | ForEach-Object {',
258
+ '[Console]::Out.WriteLine((@{path=$_.Path;line=$_.LineNumber;column=($_.Line.ToLowerInvariant().IndexOf($q.ToLowerInvariant())+1);preview=$_.Line}|ConvertTo-Json -Compress))',
259
+ '}}'
260
+ ].join('');
261
+ return {
262
+ file: 'powershell.exe',
263
+ args: ['-NoProfile', '-NonInteractive', '-Command', script, query, ...paths],
264
+ validCodes: [0]
265
+ };
266
+ }
267
+ async function runCommand(file, args, cwd, deadline, stopAfterResultLimit = false) {
268
+ return new Promise((resolve, reject) => {
269
+ const child = spawn(file, args, {
270
+ cwd,
271
+ windowsHide: true,
272
+ stdio: ['ignore', 'pipe', 'ignore']
273
+ });
274
+ const chunks = [];
275
+ let pending = Buffer.alloc(0);
276
+ let bytes = 0;
277
+ let matches = 0;
278
+ let stopped = false;
279
+ const timer = setTimeout(() => child.kill(), Math.max(1, deadline - Date.now()));
280
+ child.once('error', (error) => {
281
+ clearTimeout(timer);
282
+ reject(error);
283
+ });
284
+ child.stdout.on('data', (chunk) => {
285
+ bytes += chunk.length;
286
+ if (bytes > OUTPUT_BYTE_LIMIT) {
287
+ child.kill();
288
+ return;
289
+ }
290
+ if (!stopAfterResultLimit) {
291
+ chunks.push(chunk);
292
+ return;
293
+ }
294
+ pending = Buffer.concat([pending, chunk]);
295
+ let newline = pending.indexOf(0x0a);
296
+ while (newline >= 0) {
297
+ const record = pending.subarray(0, newline + 1);
298
+ chunks.push(record);
299
+ if (record.includes(Buffer.from('"type":"match"')))
300
+ matches += 1;
301
+ pending = pending.subarray(newline + 1);
302
+ if (matches > RESULT_LIMIT) {
303
+ stopped = true;
304
+ child.kill();
305
+ return;
306
+ }
307
+ newline = pending.indexOf(0x0a);
308
+ }
309
+ });
310
+ child.once('close', (code, signal) => {
311
+ clearTimeout(timer);
312
+ if (stopped)
313
+ resolve({ code: 0, stdout: Buffer.concat(chunks) });
314
+ else if (signal !== null || bytes > OUTPUT_BYTE_LIMIT)
315
+ reject(new Error('CONTENT_SEARCH_COMMAND_LIMIT'));
316
+ else {
317
+ if (stopAfterResultLimit && pending.length > 0)
318
+ chunks.push(pending);
319
+ resolve({ code: code ?? -1, stdout: Buffer.concat(chunks) });
320
+ }
321
+ });
322
+ });
323
+ }
324
+ function parseOutput(engine, output, query) {
325
+ if (engine === 'RIPGREP')
326
+ return parseRipgrep(output, query);
327
+ if (engine === 'GREP')
328
+ return parseGrep(output, query);
329
+ return parsePowerShell(output, query);
330
+ }
331
+ function parseRipgrep(output, query) {
332
+ return output
333
+ .toString('utf8')
334
+ .split('\n')
335
+ .flatMap((line) => {
336
+ if (line === '')
337
+ return [];
338
+ const value = JSON.parse(line);
339
+ if (value.type !== 'match')
340
+ return [];
341
+ const data = value.data;
342
+ const preview = String(data?.lines?.text).replace(/[\r\n]+$/u, '');
343
+ const column = lowerIndex(preview, query);
344
+ if (typeof data?.path?.text !== 'string' || !Number.isInteger(data.line_number) || column < 0)
345
+ throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
346
+ return [
347
+ {
348
+ path: data.path.text,
349
+ line: data.line_number,
350
+ column: column + 1,
351
+ preview: preview.slice(0, 8192)
352
+ }
353
+ ];
354
+ });
355
+ }
356
+ function parseGrep(output, query) {
357
+ const matches = [];
358
+ let offset = 0;
359
+ while (offset < output.length) {
360
+ const separator = output.indexOf(0, offset);
361
+ const end = output.indexOf(10, separator + 1);
362
+ if (separator < 0 || end < 0)
363
+ throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
364
+ const path = output.subarray(offset, separator).toString('utf8');
365
+ const record = output.subarray(separator + 1, end).toString('utf8');
366
+ const colon = record.indexOf(':');
367
+ const preview = record.slice(colon + 1).replace(/\r$/u, '');
368
+ const column = lowerIndex(preview, query);
369
+ if (colon < 1 || column < 0)
370
+ throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
371
+ matches.push({
372
+ path,
373
+ line: Number(record.slice(0, colon)),
374
+ column: column + 1,
375
+ preview: preview.slice(0, 8192)
376
+ });
377
+ offset = end + 1;
378
+ }
379
+ return matches;
380
+ }
381
+ function parsePowerShell(output, query) {
382
+ return output
383
+ .toString('utf8')
384
+ .split(/\r?\n/u)
385
+ .flatMap((line) => {
386
+ if (line === '')
387
+ return [];
388
+ const match = JSON.parse(line);
389
+ if (typeof match.path !== 'string' ||
390
+ !Number.isInteger(match.line) ||
391
+ typeof match.preview !== 'string')
392
+ throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
393
+ const column = lowerIndex(match.preview, query);
394
+ if (column < 0)
395
+ throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
396
+ return [
397
+ {
398
+ path: match.path,
399
+ line: match.line,
400
+ column: column + 1,
401
+ preview: match.preview.slice(0, 8192)
402
+ }
403
+ ];
404
+ });
405
+ }
406
+ async function nodeSearch(files, query, deadline) {
407
+ const matches = [];
408
+ let skippedFiles = 0;
409
+ for (const file of files) {
410
+ if (Date.now() >= deadline || matches.length > RESULT_LIMIT)
411
+ break;
412
+ const fileResults = [];
413
+ const stream = createReadStream(file.absolutePath);
414
+ const decoder = new TextDecoder('utf-8', { fatal: true });
415
+ let pending = '';
416
+ let line = 0;
417
+ let fileMatches = 0;
418
+ const addLine = (value) => {
419
+ line += 1;
420
+ const preview = value.endsWith('\r') ? value.slice(0, -1) : value;
421
+ const column = lowerIndex(preview, query);
422
+ if (column >= 0) {
423
+ fileResults.push({
424
+ path: file.path,
425
+ line,
426
+ column: column + 1,
427
+ preview: preview.slice(0, 8192)
428
+ });
429
+ fileMatches += 1;
430
+ }
431
+ return (fileMatches >= PER_FILE_LIMIT ||
432
+ matches.length + fileResults.length > RESULT_LIMIT ||
433
+ Date.now() >= deadline);
434
+ };
435
+ try {
436
+ let stopped = false;
437
+ for await (const chunk of stream) {
438
+ pending += decoder.decode(chunk, { stream: true });
439
+ let newline = pending.indexOf('\n');
440
+ while (newline >= 0) {
441
+ if (addLine(pending.slice(0, newline))) {
442
+ stopped = true;
443
+ break;
444
+ }
445
+ pending = pending.slice(newline + 1);
446
+ newline = pending.indexOf('\n');
447
+ }
448
+ if (stopped)
449
+ break;
450
+ }
451
+ if (!stopped) {
452
+ pending += decoder.decode();
453
+ if (pending !== '')
454
+ addLine(pending);
455
+ }
456
+ matches.push(...fileResults);
457
+ }
458
+ catch {
459
+ // Files that disappear, become unreadable or contain invalid UTF-8 are skipped atomically.
460
+ skippedFiles += 1;
461
+ }
462
+ }
463
+ return { matches, skippedFiles };
464
+ }
465
+ function lowerIndex(source, query) {
466
+ return source.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
467
+ }
468
+ function limitPerFile(matches) {
469
+ const counts = new Map();
470
+ return matches.filter((match) => {
471
+ const count = counts.get(match.path) ?? 0;
472
+ if (count >= PER_FILE_LIMIT)
473
+ return false;
474
+ counts.set(match.path, count + 1);
475
+ return true;
476
+ });
477
+ }
@@ -0,0 +1,75 @@
1
+ import type { FileHandle } from 'node:fs/promises';
2
+ import type { NodeAgentSession } from '../database.js';
3
+ import type { WorkspaceFileIndex } from '../workspace.js';
4
+ export interface WorkspaceWatch {
5
+ readonly workspaceId: string;
6
+ readonly watchId: string;
7
+ readonly sessionHash: string;
8
+ expiresAt: number;
9
+ readonly topics: {
10
+ readonly sessions: boolean;
11
+ readonly changes: boolean;
12
+ readonly files: {
13
+ readonly path: string;
14
+ readonly cursor?: string;
15
+ readonly limit?: number;
16
+ } | undefined;
17
+ };
18
+ timer: NodeJS.Timeout;
19
+ }
20
+ export interface ActiveWorkspaceUpload {
21
+ readonly id: string;
22
+ readonly workspaceId: string;
23
+ readonly path: string;
24
+ readonly targetPath: string;
25
+ readonly temporaryPath: string;
26
+ readonly size: number;
27
+ readonly overwrite: boolean;
28
+ readonly composerAttachment?: {
29
+ readonly id: string;
30
+ readonly name: string;
31
+ readonly mime: string;
32
+ };
33
+ readonly handle: FileHandle;
34
+ receivedBytes: number;
35
+ readonly timer: NodeJS.Timeout;
36
+ }
37
+ export interface CompletedComposerAttachment {
38
+ readonly id: string;
39
+ readonly workspaceId: string;
40
+ readonly path: string;
41
+ readonly targetPath: string;
42
+ readonly name: string;
43
+ readonly mime: string;
44
+ readonly size: number;
45
+ }
46
+ export declare class WorkspaceDomainState {
47
+ readonly uploads: Map<string, ActiveWorkspaceUpload>;
48
+ readonly composerAttachments: Map<string, CompletedComposerAttachment>;
49
+ readonly changesReads: Map<string, Promise<unknown>>;
50
+ readonly changesCache: Map<string, {
51
+ readonly value: unknown;
52
+ readonly expiresAt: number;
53
+ }>;
54
+ readonly changesVersions: Map<string, number>;
55
+ readonly fileIndexes: Map<string, WorkspaceFileIndex>;
56
+ readonly watches: Map<string, Map<string, WorkspaceWatch>>;
57
+ readonly watchRevisions: Map<string, number>;
58
+ readonly sessionWatchTimers: Map<string, NodeJS.Timeout>;
59
+ readonly sessionWatchSignatures: Map<string, string>;
60
+ readonly sessionWatchReads: Map<string, Promise<void>>;
61
+ readonly sessionCache: Map<string, {
62
+ readonly sessions: readonly NodeAgentSession[];
63
+ readonly expiresAt: number;
64
+ }>;
65
+ readonly sessionCacheReads: Map<string, Promise<readonly NodeAgentSession[]>>;
66
+ readonly changesLimiter: AsyncLimiter;
67
+ }
68
+ declare class AsyncLimiter {
69
+ private readonly limit;
70
+ private active;
71
+ private readonly waiters;
72
+ constructor(limit: number);
73
+ run<T>(task: () => Promise<T>): Promise<T>;
74
+ }
75
+ export {};
@@ -0,0 +1,40 @@
1
+ export class WorkspaceDomainState {
2
+ uploads = new Map();
3
+ composerAttachments = new Map();
4
+ changesReads = new Map();
5
+ changesCache = new Map();
6
+ changesVersions = new Map();
7
+ fileIndexes = new Map();
8
+ watches = new Map();
9
+ watchRevisions = new Map();
10
+ sessionWatchTimers = new Map();
11
+ sessionWatchSignatures = new Map();
12
+ sessionWatchReads = new Map();
13
+ sessionCache = new Map();
14
+ sessionCacheReads = new Map();
15
+ changesLimiter = new AsyncLimiter(3);
16
+ }
17
+ class AsyncLimiter {
18
+ limit;
19
+ active = 0;
20
+ waiters = [];
21
+ constructor(limit) {
22
+ this.limit = limit;
23
+ }
24
+ async run(task) {
25
+ if (this.active < this.limit)
26
+ this.active += 1;
27
+ else
28
+ await new Promise((resolve) => this.waiters.push(resolve));
29
+ try {
30
+ return await task();
31
+ }
32
+ finally {
33
+ const next = this.waiters.shift();
34
+ if (next === undefined)
35
+ this.active -= 1;
36
+ else
37
+ next();
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,17 @@
1
+ import type { NodeWorkspace } from '../database.js';
2
+ import { WorkspaceFileIndex } from '../workspace.js';
3
+ import { WorkspaceDomainState } from './workspace-domain-state.js';
4
+ export declare class WorkspaceFileService {
5
+ private readonly state;
6
+ private readonly allowedRoots;
7
+ private readonly onInvalidated;
8
+ private readonly mutationTails;
9
+ constructor(state: WorkspaceDomainState, allowedRoots: () => readonly string[], onInvalidated: (workspaceId: string) => void);
10
+ list(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
11
+ read(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
12
+ search(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
13
+ mutate(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
14
+ private serializeMutation;
15
+ index(workspace: NodeWorkspace): WorkspaceFileIndex;
16
+ dispose(workspaceId?: string): void;
17
+ }