@myagentroam/node 0.1.4 → 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 (210) 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 +3 -1
  5. package/dist/claude-agent-sdk.js +9 -2
  6. package/dist/claude-channel.js +0 -1
  7. package/dist/codex-app-server.d.ts +1 -0
  8. package/dist/codex-app-server.js +8 -1
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +18 -2
  11. package/dist/connector/node-connector-options.d.ts +3 -0
  12. package/dist/connector/node-connector-options.js +0 -1
  13. package/dist/connector/node-operation-router.js +0 -1
  14. package/dist/connector.d.ts +3 -0
  15. package/dist/connector.js +109 -7
  16. package/dist/control-outbound-scheduler.js +0 -1
  17. package/dist/database.d.ts +4 -0
  18. package/dist/database.js +14 -9
  19. package/dist/event-buffer.js +0 -1
  20. package/dist/fake-runner.js +0 -1
  21. package/dist/health.js +0 -1
  22. package/dist/main.js +6 -1
  23. package/dist/migrations/v001.js +0 -1
  24. package/dist/migrations/v002.js +0 -1
  25. package/dist/migrations/v003.d.ts +4 -0
  26. package/dist/migrations/v003.js +44 -0
  27. package/dist/native-jsonl-reader.js +0 -1
  28. package/dist/native-session-history.d.ts +19 -1
  29. package/dist/native-session-history.js +65 -3
  30. package/dist/opencode-runtime.d.ts +33 -0
  31. package/dist/opencode-runtime.js +80 -0
  32. package/dist/opencode-server.d.ts +47 -0
  33. package/dist/opencode-server.js +250 -0
  34. package/dist/operational.js +10 -3
  35. package/dist/process-tree.js +0 -1
  36. package/dist/runner/abstract-runner.d.ts +7 -2
  37. package/dist/runner/abstract-runner.js +17 -2
  38. package/dist/runner/claude/managed-run-controller.js +20 -1
  39. package/dist/runner/claude-code-runner.d.ts +3 -1
  40. package/dist/runner/claude-code-runner.js +26 -8
  41. package/dist/runner/codex/conversation-parser.d.ts +1 -3
  42. package/dist/runner/codex/conversation-parser.js +174 -34
  43. package/dist/runner/codex/managed-run-controller.d.ts +2 -1
  44. package/dist/runner/codex/managed-run-controller.js +13 -4
  45. package/dist/runner/codex-runner.d.ts +4 -0
  46. package/dist/runner/codex-runner.js +27 -4
  47. package/dist/runner/fake-test-runner.js +0 -1
  48. package/dist/runner/opencode/conversation-parser.d.ts +27 -0
  49. package/dist/runner/opencode/conversation-parser.js +217 -0
  50. package/dist/runner/opencode/managed-run-controller.d.ts +76 -0
  51. package/dist/runner/opencode/managed-run-controller.js +451 -0
  52. package/dist/runner/opencode-runner.d.ts +60 -0
  53. package/dist/runner/opencode-runner.js +330 -0
  54. package/dist/runner/runner-registry.d.ts +3 -1
  55. package/dist/runner/runner-registry.js +12 -3
  56. package/dist/runner-command-engine.js +0 -1
  57. package/dist/runner-profiles.d.ts +4 -0
  58. package/dist/runner-profiles.js +76 -3
  59. package/dist/runner-usage.js +0 -1
  60. package/dist/runtime-state.js +20 -9
  61. package/dist/service/attachment-domain-state.js +0 -1
  62. package/dist/service/conversation-history-service.js +0 -1
  63. package/dist/service/external-session-resume-service.js +0 -1
  64. package/dist/service/fake-managed-run-service.js +0 -1
  65. package/dist/service/managed-runner-session-service.d.ts +3 -1
  66. package/dist/service/managed-runner-session-service.js +8 -4
  67. package/dist/service/native-session-projection-service.d.ts +2 -1
  68. package/dist/service/native-session-projection-service.js +8 -5
  69. package/dist/service/native-session-watch-service.js +0 -1
  70. package/dist/service/node-connection-lifecycle-service.js +3 -2
  71. package/dist/service/node-control-channel.d.ts +1 -1
  72. package/dist/service/node-control-channel.js +5 -4
  73. package/dist/service/node-control-message-service.js +0 -1
  74. package/dist/service/node-request-service.js +0 -1
  75. package/dist/service/node-run-event-bridge.d.ts +2 -0
  76. package/dist/service/node-run-event-bridge.js +4 -1
  77. package/dist/service/node-terminal-channel.js +16 -10
  78. package/dist/service/node-upgrade-coordinator.d.ts +29 -0
  79. package/dist/service/node-upgrade-coordinator.js +137 -0
  80. package/dist/service/node-workspace-coordinator.js +0 -1
  81. package/dist/service/run-attachment-service.js +0 -1
  82. package/dist/service/run-domain-state.js +0 -1
  83. package/dist/service/run-event-service.js +0 -1
  84. package/dist/service/run-workbench-service.js +0 -1
  85. package/dist/service/runner-channel-message-service.js +0 -1
  86. package/dist/service/runner-interaction-service.js +2 -1
  87. package/dist/service/runner-service.d.ts +5 -4
  88. package/dist/service/runner-service.js +19 -6
  89. package/dist/service/session-catalog-service.d.ts +16 -1
  90. package/dist/service/session-catalog-service.js +82 -3
  91. package/dist/service/session-command-service.js +0 -1
  92. package/dist/service/session-context-service.js +0 -1
  93. package/dist/service/session-domain-state.js +0 -1
  94. package/dist/service/session-identity-service.js +0 -1
  95. package/dist/service/session-lifecycle-service.js +13 -7
  96. package/dist/service/session-message-service.js +0 -1
  97. package/dist/service/session-presentation-service.js +0 -1
  98. package/dist/service/session-query-service.js +0 -1
  99. package/dist/service/terminal-relay-state.js +0 -1
  100. package/dist/service/terminal-service.js +0 -1
  101. package/dist/service/workbench-event-service.js +0 -1
  102. package/dist/service/workbench-manifest-service.d.ts +5 -4
  103. package/dist/service/workbench-manifest-service.js +3 -2
  104. package/dist/service/workspace-change-service.js +2 -2
  105. package/dist/service/workspace-content-search-service.d.ts +14 -0
  106. package/dist/service/workspace-content-search-service.js +477 -0
  107. package/dist/service/workspace-domain-state.js +0 -1
  108. package/dist/service/workspace-file-service.d.ts +3 -0
  109. package/dist/service/workspace-file-service.js +35 -3
  110. package/dist/service/workspace-queue-workbench-service.js +0 -1
  111. package/dist/service/workspace-service.js +23 -10
  112. package/dist/service/workspace-session-service.js +0 -1
  113. package/dist/service/workspace-upload-service.js +0 -1
  114. package/dist/service/workspace-watch-service.js +0 -1
  115. package/dist/service/workspace-workbench-service.d.ts +9 -0
  116. package/dist/service/workspace-workbench-service.js +69 -2
  117. package/dist/service.js +2 -3
  118. package/dist/storage.js +0 -1
  119. package/dist/supervisor.d.ts +1 -0
  120. package/dist/supervisor.js +154 -0
  121. package/dist/terminal.js +0 -1
  122. package/dist/util/node-operation-parsers.d.ts +1 -0
  123. package/dist/util/node-operation-parsers.js +3 -1
  124. package/dist/util/runner-native-session-parsers.js +117 -73
  125. package/dist/util/secret-environment.js +0 -1
  126. package/dist/workspace.d.ts +62 -2
  127. package/dist/workspace.js +539 -57
  128. package/package.json +3 -2
  129. package/dist/capabilities.js.map +0 -1
  130. package/dist/claude-agent-sdk.js.map +0 -1
  131. package/dist/claude-channel.js.map +0 -1
  132. package/dist/codex-app-server.js.map +0 -1
  133. package/dist/config.js.map +0 -1
  134. package/dist/connector/node-connector-options.js.map +0 -1
  135. package/dist/connector/node-operation-router.js.map +0 -1
  136. package/dist/connector.js.map +0 -1
  137. package/dist/control-outbound-scheduler.js.map +0 -1
  138. package/dist/database.js.map +0 -1
  139. package/dist/event-buffer.js.map +0 -1
  140. package/dist/fake-runner.js.map +0 -1
  141. package/dist/health.js.map +0 -1
  142. package/dist/main.js.map +0 -1
  143. package/dist/migrations/v001.js.map +0 -1
  144. package/dist/migrations/v002.js.map +0 -1
  145. package/dist/native-jsonl-reader.js.map +0 -1
  146. package/dist/native-session-history.js.map +0 -1
  147. package/dist/operational.js.map +0 -1
  148. package/dist/process-tree.js.map +0 -1
  149. package/dist/runner/abstract-runner.js.map +0 -1
  150. package/dist/runner/claude/managed-run-controller.js.map +0 -1
  151. package/dist/runner/claude-code-runner.js.map +0 -1
  152. package/dist/runner/codex/conversation-parser.js.map +0 -1
  153. package/dist/runner/codex/managed-run-controller.js.map +0 -1
  154. package/dist/runner/codex-runner.js.map +0 -1
  155. package/dist/runner/fake-test-runner.js.map +0 -1
  156. package/dist/runner/runner-registry.js.map +0 -1
  157. package/dist/runner-command-engine.js.map +0 -1
  158. package/dist/runner-profiles.js.map +0 -1
  159. package/dist/runner-usage.js.map +0 -1
  160. package/dist/runtime-state.js.map +0 -1
  161. package/dist/service/attachment-domain-state.js.map +0 -1
  162. package/dist/service/conversation-history-service.js.map +0 -1
  163. package/dist/service/external-session-resume-service.js.map +0 -1
  164. package/dist/service/fake-managed-run-service.js.map +0 -1
  165. package/dist/service/managed-runner-session-service.js.map +0 -1
  166. package/dist/service/native-session-projection-service.js.map +0 -1
  167. package/dist/service/native-session-watch-service.js.map +0 -1
  168. package/dist/service/node-connection-lifecycle-service.js.map +0 -1
  169. package/dist/service/node-control-channel.js.map +0 -1
  170. package/dist/service/node-control-message-service.js.map +0 -1
  171. package/dist/service/node-request-service.js.map +0 -1
  172. package/dist/service/node-run-event-bridge.js.map +0 -1
  173. package/dist/service/node-terminal-channel.js.map +0 -1
  174. package/dist/service/node-workspace-coordinator.js.map +0 -1
  175. package/dist/service/run-attachment-service.js.map +0 -1
  176. package/dist/service/run-domain-state.js.map +0 -1
  177. package/dist/service/run-event-service.js.map +0 -1
  178. package/dist/service/run-workbench-service.js.map +0 -1
  179. package/dist/service/runner-channel-message-service.js.map +0 -1
  180. package/dist/service/runner-interaction-service.js.map +0 -1
  181. package/dist/service/runner-service.js.map +0 -1
  182. package/dist/service/session-catalog-service.js.map +0 -1
  183. package/dist/service/session-command-service.js.map +0 -1
  184. package/dist/service/session-context-service.js.map +0 -1
  185. package/dist/service/session-domain-state.js.map +0 -1
  186. package/dist/service/session-identity-service.js.map +0 -1
  187. package/dist/service/session-lifecycle-service.js.map +0 -1
  188. package/dist/service/session-message-service.js.map +0 -1
  189. package/dist/service/session-presentation-service.js.map +0 -1
  190. package/dist/service/session-query-service.js.map +0 -1
  191. package/dist/service/terminal-relay-state.js.map +0 -1
  192. package/dist/service/terminal-service.js.map +0 -1
  193. package/dist/service/workbench-event-service.js.map +0 -1
  194. package/dist/service/workbench-manifest-service.js.map +0 -1
  195. package/dist/service/workspace-change-service.js.map +0 -1
  196. package/dist/service/workspace-domain-state.js.map +0 -1
  197. package/dist/service/workspace-file-service.js.map +0 -1
  198. package/dist/service/workspace-queue-workbench-service.js.map +0 -1
  199. package/dist/service/workspace-service.js.map +0 -1
  200. package/dist/service/workspace-session-service.js.map +0 -1
  201. package/dist/service/workspace-upload-service.js.map +0 -1
  202. package/dist/service/workspace-watch-service.js.map +0 -1
  203. package/dist/service/workspace-workbench-service.js.map +0 -1
  204. package/dist/service.js.map +0 -1
  205. package/dist/storage.js.map +0 -1
  206. package/dist/terminal.js.map +0 -1
  207. package/dist/util/node-operation-parsers.js.map +0 -1
  208. package/dist/util/runner-native-session-parsers.js.map +0 -1
  209. package/dist/util/secret-environment.js.map +0 -1
  210. 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
+ }
@@ -38,4 +38,3 @@ class AsyncLimiter {
38
38
  }
39
39
  }
40
40
  }
41
- //# sourceMappingURL=workspace-domain-state.js.map
@@ -5,10 +5,13 @@ export declare class WorkspaceFileService {
5
5
  private readonly state;
6
6
  private readonly allowedRoots;
7
7
  private readonly onInvalidated;
8
+ private readonly mutationTails;
8
9
  constructor(state: WorkspaceDomainState, allowedRoots: () => readonly string[], onInvalidated: (workspaceId: string) => void);
9
10
  list(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
10
11
  read(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
11
12
  search(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
13
+ mutate(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
14
+ private serializeMutation;
12
15
  index(workspace: NodeWorkspace): WorkspaceFileIndex;
13
16
  dispose(workspaceId?: string): void;
14
17
  }
@@ -1,10 +1,11 @@
1
1
  import { isAbsolute } from 'node:path';
2
- import { listWorkspaceFiles, readAllowedTextFile, readWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
2
+ import { listWorkspaceFiles, mutateWorkspaceFile, readAllowedTextFile, readWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
3
3
  const INDEX_CACHE_LIMIT = 8;
4
4
  export class WorkspaceFileService {
5
5
  state;
6
6
  allowedRoots;
7
7
  onInvalidated;
8
+ mutationTails = new Map();
8
9
  constructor(state, allowedRoots, onInvalidated) {
9
10
  this.state = state;
10
11
  this.allowedRoots = allowedRoots;
@@ -38,12 +39,44 @@ export class WorkspaceFileService {
38
39
  async search(workspace, input) {
39
40
  if (typeof input.query !== 'string' ||
40
41
  (input.cursor !== undefined && typeof input.cursor !== 'string') ||
42
+ (input.includeDirectories !== undefined && typeof input.includeDirectories !== 'boolean') ||
41
43
  (input.limit !== undefined &&
42
44
  (!Number.isInteger(input.limit) ||
43
45
  input.limit < 1 ||
44
46
  input.limit > 200)))
45
47
  throw new Error('FILE_SEARCH_LIMIT_EXCEEDED');
46
- return this.index(workspace).search(input.query, typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined);
48
+ return this.index(workspace).search(input.query, typeof input.cursor === 'string' ? input.cursor : undefined, typeof input.limit === 'number' ? input.limit : undefined, input.includeDirectories === true);
49
+ }
50
+ async mutate(workspace, input) {
51
+ const action = input.action;
52
+ if ((action !== 'CREATE_FILE' &&
53
+ action !== 'CREATE_DIRECTORY' &&
54
+ action !== 'MOVE' &&
55
+ action !== 'DELETE') ||
56
+ typeof input.path !== 'string' ||
57
+ (action === 'MOVE' && typeof input.destination !== 'string'))
58
+ throw new Error('FILE_MUTATION_INVALID');
59
+ const path = input.path;
60
+ const destination = typeof input.destination === 'string' ? input.destination : undefined;
61
+ return this.serializeMutation(workspace.id, async () => {
62
+ const result = await mutateWorkspaceFile(workspace.path, action === 'MOVE' ? { action, path, destination: destination } : { action, path });
63
+ this.dispose(workspace.id);
64
+ this.onInvalidated(workspace.id);
65
+ return result;
66
+ });
67
+ }
68
+ async serializeMutation(workspaceId, operation) {
69
+ const previous = this.mutationTails.get(workspaceId) ?? Promise.resolve();
70
+ const result = previous.then(operation, operation);
71
+ const tail = result.then(() => undefined, () => undefined);
72
+ this.mutationTails.set(workspaceId, tail);
73
+ try {
74
+ return await result;
75
+ }
76
+ finally {
77
+ if (this.mutationTails.get(workspaceId) === tail)
78
+ this.mutationTails.delete(workspaceId);
79
+ }
47
80
  }
48
81
  index(workspace) {
49
82
  let index = this.state.fileIndexes.get(workspace.id);
@@ -74,4 +107,3 @@ export class WorkspaceFileService {
74
107
  this.state.fileIndexes.clear();
75
108
  }
76
109
  }
77
- //# sourceMappingURL=workspace-file-service.js.map
@@ -224,4 +224,3 @@ export class WorkspaceQueueWorkbenchService {
224
224
  function record(value) {
225
225
  return isPlainRecord(value) ? value : {};
226
226
  }
227
- //# sourceMappingURL=workspace-queue-workbench-service.js.map
@@ -1,4 +1,5 @@
1
- import { inspectWorkspace, listWorkspaceDirectories } from '../workspace.js';
1
+ import { rmdir } from 'node:fs/promises';
2
+ import { createWorkspaceDirectory, inspectWorkspace, listWorkspaceDirectories } from '../workspace.js';
2
3
  export class WorkspaceService {
3
4
  database;
4
5
  config;
@@ -22,14 +23,27 @@ export class WorkspaceService {
22
23
  const input = data;
23
24
  if (typeof input.path !== 'string')
24
25
  throw new Error('WORKSPACE_INVALID');
25
- const inspected = await inspectWorkspace(input.path, this.config().allowedRoots);
26
- return {
27
- workspace: this.database().saveWorkspace({
28
- path: inspected.path,
29
- kind: inspected.kind,
30
- gitSummary: inspected.gitSummary ?? null
31
- })
32
- };
26
+ if (input.createDirectoryName !== undefined && typeof input.createDirectoryName !== 'string')
27
+ throw new Error('WORKSPACE_DIRECTORY_NAME_INVALID');
28
+ let createdPath;
29
+ try {
30
+ const path = typeof input.createDirectoryName === 'string'
31
+ ? (createdPath = await createWorkspaceDirectory(input.path, input.createDirectoryName, this.config().allowedRoots))
32
+ : input.path;
33
+ const inspected = await inspectWorkspace(path, this.config().allowedRoots);
34
+ return {
35
+ workspace: this.database().saveWorkspace({
36
+ path: inspected.path,
37
+ kind: inspected.kind,
38
+ gitSummary: inspected.gitSummary ?? null
39
+ })
40
+ };
41
+ }
42
+ catch (error) {
43
+ if (createdPath !== undefined)
44
+ await rmdir(createdPath).catch(() => undefined);
45
+ throw error;
46
+ }
33
47
  }
34
48
  async listDirectories(data) {
35
49
  const input = data;
@@ -51,4 +65,3 @@ export class WorkspaceService {
51
65
  return { workspace };
52
66
  }
53
67
  }
54
- //# sourceMappingURL=workspace-service.js.map
@@ -137,4 +137,3 @@ async function within(promise, timeoutMs) {
137
137
  new Promise((resolve) => setTimeout(resolve, timeoutMs))
138
138
  ]);
139
139
  }
140
- //# sourceMappingURL=workspace-session-service.js.map
@@ -238,4 +238,3 @@ async function safeDirectory(path) {
238
238
  function plain(value) {
239
239
  return typeof value === 'object' && value !== null && !Array.isArray(value);
240
240
  }
241
- //# sourceMappingURL=workspace-upload-service.js.map
@@ -118,4 +118,3 @@ export class WorkspaceWatchService {
118
118
  function plain(value) {
119
119
  return typeof value === 'object' && value !== null && !Array.isArray(value);
120
120
  }
121
- //# sourceMappingURL=workspace-watch-service.js.map
@@ -17,6 +17,7 @@ export interface WorkspaceWorkbenchServiceOptions {
17
17
  }
18
18
  export declare class WorkspaceWorkbenchService {
19
19
  private readonly options;
20
+ private readonly contentSearch;
20
21
  constructor(options: WorkspaceWorkbenchServiceOptions);
21
22
  operations(): Readonly<Record<string, NodeOperationHandler>>;
22
23
  private activity;
@@ -25,6 +26,8 @@ export declare class WorkspaceWorkbenchService {
25
26
  private listFiles;
26
27
  private readFile;
27
28
  private searchFiles;
29
+ private mutateFile;
30
+ private searchContent;
28
31
  private preflight;
29
32
  private createUpload;
30
33
  private writeUpload;
@@ -33,6 +36,12 @@ export declare class WorkspaceWorkbenchService {
33
36
  private currentChanges;
34
37
  private changeDiff;
35
38
  private restoreChange;
39
+ private gitRepository;
40
+ private refs;
41
+ private gitRefs;
42
+ private gitHistory;
43
+ private gitCommitFiles;
44
+ private gitCommitFileDiff;
36
45
  private diff;
37
46
  private gitWorkspace;
38
47
  }