@myagentroam/node 0.11.35 → 0.11.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/execution-lifecycle.js +54 -0
- package/dist/application/node-lifecycle.js +104 -0
- package/dist/application/node-operation-admission.js +51 -0
- package/dist/application/node-operation-contracts.js +1 -0
- package/dist/application/runner-composition.js +22 -0
- package/dist/application/session-projection-composition.js +26 -0
- package/dist/application/workspace-application.js +61 -0
- package/dist/application/workspace-composition.js +21 -0
- package/dist/capabilities.js +2 -2
- package/dist/codex-app-server.js +1 -471
- package/dist/connection/authorization.js +26 -0
- package/dist/{service → connection}/direct-transfer-service.js +1 -1
- package/dist/connection/endpoints.js +32 -0
- package/dist/connector.js +144 -252
- package/dist/database.js +34 -14
- package/dist/git-askpass.js +1 -1
- package/dist/git-execution-helper.js +1 -1
- package/dist/hosting/hosted-conversation-file-identity.js +27 -0
- package/dist/hosting/hosted-conversation-file-operations.js +44 -0
- package/dist/hosting/hosted-conversation-node-service.js +974 -0
- package/dist/hosting/hosted-conversation-runs.js +599 -0
- package/dist/hosting/hosted-conversation-templates.js +280 -0
- package/dist/hosting/hosted-conversation-uploads.js +344 -0
- package/dist/hosting/hosted-conversation-watch.js +194 -0
- package/dist/hosting/runtime-contracts.js +1 -0
- package/dist/marctl-guidance.js +1 -1
- package/dist/runner/abstract-runner.js +4 -13
- package/dist/{claude-agent-sdk.js → runner/claude/claude-agent-sdk.js} +2 -2
- package/dist/runner/claude/conversation-parser.js +2 -1
- package/dist/runner/claude/managed-run-controller.js +6 -7
- package/dist/runner/claude/message-parsers.js +2 -1
- package/dist/runner/claude/native-transcript.js +2 -1
- package/dist/runner/claude/tool-normalizer.js +1 -1
- package/dist/runner/claude-code-runner.js +2 -2
- package/dist/runner/codex/codex-app-server.js +471 -0
- package/dist/runner/codex/conversation-parser.js +4 -2
- package/dist/runner/codex/managed-run-controller.js +7 -9
- package/dist/runner/codex/native-transcript.js +1 -1
- package/dist/runner/codex/tool-normalizer.js +1 -1
- package/dist/runner/codex-runner.js +9 -27
- package/dist/runner/contracts.js +1 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +2 -2
- package/dist/runner/mar-agent/history-projection.js +1 -1
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +6 -10
- package/dist/runner/mar-agent/managed-run-controller.js +85 -61
- package/dist/runner/mar-agent-runner.js +1 -1
- package/dist/runner/normalization.js +42 -0
- package/dist/runner/opencode/conversation-parser.js +1 -1
- package/dist/runner/opencode/managed-run-controller.js +6 -7
- package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
- package/dist/runner/opencode/tool-normalizer.js +1 -1
- package/dist/runner/opencode-runner.js +4 -5
- package/dist/runner/runner-registry.js +4 -1
- package/dist/runtime-state.js +48 -372
- package/dist/service/computer-use-enablement-service.js +1 -1
- package/dist/service/continuous-run-coordinator.js +52 -7
- package/dist/service/conversation-history-service.js +4 -4
- package/dist/service/conversation-hosting-capability-service.js +1 -1
- package/dist/service/conversation-hosting-enablement-service.js +1 -1
- package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
- package/dist/service/conversation-hosting-maintenance-service.js +1 -1
- package/dist/service/conversation-observation.js +1 -1
- package/dist/service/conversation-segment-service.js +1 -1
- package/dist/service/external-session-resume-service.js +47 -27
- package/dist/service/fake-managed-run-service.js +3 -7
- package/dist/service/git-operation-service.js +1 -1
- package/dist/service/goal-continuation-coordinator.js +14 -7
- package/dist/service/mar-agent-model-transport-test-service.js +1 -1
- package/dist/service/native-session-watch-service.js +1 -1
- package/dist/service/node-control-channel.js +1 -5
- package/dist/service/node-request-service.js +1 -1
- package/dist/service/node-terminal-channel.js +2 -1
- package/dist/service/node-workspace-coordinator.js +3 -2
- package/dist/service/notification-mcp-bridge.js +6 -2
- package/dist/service/recent-run-projection-service.js +1 -1
- package/dist/service/restart-session-restorer.js +1 -1
- package/dist/service/run-event-service.js +49 -42
- package/dist/service/run-workbench-service.js +3 -3
- package/dist/service/runner-interaction-service.js +6 -5
- package/dist/service/runner-service.js +1 -1
- package/dist/service/session-recent-activity.js +2 -1
- package/dist/service/skill-directory-service.js +1 -1
- package/dist/service/skill-install-service.js +2 -2
- package/dist/service/terminal-service.js +1 -1
- package/dist/service/workbench-event-projection.js +3 -2
- package/dist/service/workbench-event-service.js +1 -1
- package/dist/service/workspace-service.js +3 -2
- package/dist/session/attachments.js +110 -0
- package/dist/session/contracts.js +1 -0
- package/dist/{util → session}/conversation-identity.js +1 -1
- package/dist/session/input-validation.js +15 -0
- package/dist/{service → session}/native-session-projection-service.js +40 -11
- package/dist/session/pagination.js +77 -0
- package/dist/{util → session}/runner-native-session-parsers.js +2 -1
- package/dist/session/runtime/approvals.js +90 -0
- package/dist/session/runtime/conversation-projection.js +310 -0
- package/dist/session/runtime/type-guards.js +19 -0
- package/dist/{service → session}/session-catalog-service.js +5 -4
- package/dist/{service → session}/session-command-service.js +3 -3
- package/dist/{service → session}/session-identity-service.js +1 -1
- package/dist/{service → session}/session-lifecycle-service.js +18 -7
- package/dist/{service → session}/session-message-service.js +6 -5
- package/dist/{service → session}/session-presentation-service.js +1 -1
- package/dist/{service → session}/session-query-service.js +2 -2
- package/dist/{service → session}/session-schedule-service.js +2 -2
- package/dist/util/records.js +3 -0
- package/dist/workspace/contracts.js +1 -0
- package/dist/workspace/file-index.js +534 -0
- package/dist/workspace/files.js +466 -0
- package/dist/workspace/git.js +991 -0
- package/dist/workspace/path-access.js +184 -0
- package/dist/workspace/utf8.js +12 -0
- package/dist/{service → workspace}/workspace-change-service.js +45 -21
- package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
- package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
- package/dist/{service → workspace}/workspace-file-service.js +16 -16
- package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
- package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
- package/dist/{service → workspace}/workspace-session-service.js +45 -40
- package/dist/{service → workspace}/workspace-upload-service.js +28 -28
- package/dist/{service → workspace}/workspace-watch-service.js +29 -28
- package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
- package/package.json +4 -4
- package/dist/service/hosted-conversation-node-service.js +0 -2252
- package/dist/service/run-domain-state.js +0 -4
- package/dist/service/session-domain-state.js +0 -9
- package/dist/service/workspace-domain-state.js +0 -41
- package/dist/util/node-operation-parsers.js +0 -320
- package/dist/workspace.js +0 -2168
- /package/dist/{connector → application}/node-connector-options.js +0 -0
- /package/dist/{util → connection}/node-operation-data.js +0 -0
- /package/dist/{connector → connection}/node-operation-router.js +0 -0
- /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
- /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
- /package/dist/{util → mcp}/secret-environment.js +0 -0
- /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
- /package/dist/{util → runner}/personal-instructions.js +0 -0
- /package/dist/{util → runner}/runner-error.js +0 -0
- /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
- /package/dist/{service → session}/session-context-service.js +0 -0
- /package/dist/{util → workspace}/git-credential-client.js +0 -0
- /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
- /package/dist/{service → workspace}/workspace-watch-snapshot-service.js +0 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { opendir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { platform as hostPlatform } from 'node:os';
|
|
3
|
+
import { posix, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { Minimatch } from 'minimatch';
|
|
5
|
+
import { resolveRegisteredWorkspaceRoot, resolveWorkspaceEntryFromCanonicalRoot, resolveWorkspaceEntryMetadata } from './path-access.js';
|
|
6
|
+
const SEARCH_RESULT_LIMIT = 200;
|
|
7
|
+
const FILE_INDEX_IDLE_TTL_MS = 60 * 60 * 1_000;
|
|
8
|
+
const FILE_INDEX_FRESHNESS_TTL_MS = 30_000;
|
|
9
|
+
const FILE_INDEX_ENTRY_LIMIT = 100_000;
|
|
10
|
+
const FILE_INDEX_METADATA_CONCURRENCY = 32;
|
|
11
|
+
const FILE_INDEX_BYTE_LIMIT = 64 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_QUICK_OPEN_EXCLUDES = [
|
|
13
|
+
'**/.git/**',
|
|
14
|
+
'**/node_modules/**',
|
|
15
|
+
'**/bower_components/**',
|
|
16
|
+
'**/.pnpm/**',
|
|
17
|
+
'**/.yarn/**',
|
|
18
|
+
'**/.cache/**',
|
|
19
|
+
'**/.parcel-cache/**',
|
|
20
|
+
'**/.turbo/**',
|
|
21
|
+
'**/dist/**',
|
|
22
|
+
'**/build/**',
|
|
23
|
+
'**/out/**',
|
|
24
|
+
'**/coverage/**',
|
|
25
|
+
'**/test/runs/**',
|
|
26
|
+
'**/.next/**',
|
|
27
|
+
'**/.nuxt/**',
|
|
28
|
+
'**/.svelte-kit/**',
|
|
29
|
+
'**/.angular/**',
|
|
30
|
+
'**/target/**',
|
|
31
|
+
'**/.gradle/**',
|
|
32
|
+
'**/bin/**',
|
|
33
|
+
'**/obj/**',
|
|
34
|
+
'**/vendor/**',
|
|
35
|
+
'**/__pycache__/**',
|
|
36
|
+
'**/.venv/**',
|
|
37
|
+
'**/venv/**'
|
|
38
|
+
];
|
|
39
|
+
/** A per-workspace, process-local index for the Quick Open file picker. */
|
|
40
|
+
export class WorkspaceFileIndex {
|
|
41
|
+
workspaceRoot;
|
|
42
|
+
limits;
|
|
43
|
+
entries = [];
|
|
44
|
+
fileEntries = [];
|
|
45
|
+
retainedBytes = 0;
|
|
46
|
+
failure;
|
|
47
|
+
foregroundRequested = false;
|
|
48
|
+
preparationConsumers = new Set();
|
|
49
|
+
truncated = false;
|
|
50
|
+
root;
|
|
51
|
+
loading;
|
|
52
|
+
stale = true;
|
|
53
|
+
expiryTimer;
|
|
54
|
+
refreshedAt = 0;
|
|
55
|
+
refreshAttemptedAt = 0;
|
|
56
|
+
lifecycleGeneration = 0;
|
|
57
|
+
constructor(workspaceRoot, limits = {}) {
|
|
58
|
+
this.workspaceRoot = workspaceRoot;
|
|
59
|
+
this.limits = limits;
|
|
60
|
+
}
|
|
61
|
+
get accountedBytes() {
|
|
62
|
+
return this.retainedBytes;
|
|
63
|
+
}
|
|
64
|
+
/** Prepares only metadata; a queued background scan may lose its consumer. */
|
|
65
|
+
prepare(needed = () => true) {
|
|
66
|
+
if (!this.stale && this.root !== undefined) {
|
|
67
|
+
this.touch();
|
|
68
|
+
return Promise.resolve();
|
|
69
|
+
}
|
|
70
|
+
if (!needed())
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
this.preparationConsumers.add(needed);
|
|
73
|
+
this.startLoading(true, () => {
|
|
74
|
+
for (const consumer of this.preparationConsumers) {
|
|
75
|
+
if (consumer())
|
|
76
|
+
return true;
|
|
77
|
+
this.preparationConsumers.delete(consumer);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
return this.loading;
|
|
82
|
+
}
|
|
83
|
+
async contentCandidates(options = {}) {
|
|
84
|
+
await this.ensureLoaded(options.requireFresh === true);
|
|
85
|
+
return this.currentContentCandidates();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns the current candidate snapshot without synchronously rebuilding an
|
|
89
|
+
* empty or explicitly invalidated index. A stale loaded snapshot schedules
|
|
90
|
+
* its normal single-flight refresh before it is returned.
|
|
91
|
+
*/
|
|
92
|
+
peekContentCandidates() {
|
|
93
|
+
if (this.stale || this.root === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (Date.now() - this.refreshAttemptedAt >=
|
|
96
|
+
(this.limits.freshnessTtlMs ?? FILE_INDEX_FRESHNESS_TTL_MS))
|
|
97
|
+
this.refreshInBackground();
|
|
98
|
+
return this.currentContentCandidates();
|
|
99
|
+
}
|
|
100
|
+
/** Starts a single candidate refresh without making the caller await it. */
|
|
101
|
+
refreshContentCandidates(requireFresh = false) {
|
|
102
|
+
if (this.loading !== undefined)
|
|
103
|
+
return this.loading;
|
|
104
|
+
if (this.stale || this.root === undefined) {
|
|
105
|
+
this.startLoading();
|
|
106
|
+
return this.loading;
|
|
107
|
+
}
|
|
108
|
+
if (requireFresh) {
|
|
109
|
+
this.refreshAttemptedAt = Date.now();
|
|
110
|
+
this.startLoading();
|
|
111
|
+
return this.loading;
|
|
112
|
+
}
|
|
113
|
+
if (Date.now() - this.refreshAttemptedAt >=
|
|
114
|
+
(this.limits.freshnessTtlMs ?? FILE_INDEX_FRESHNESS_TTL_MS))
|
|
115
|
+
this.refreshInBackground();
|
|
116
|
+
return this.loading ?? Promise.resolve();
|
|
117
|
+
}
|
|
118
|
+
currentContentCandidates() {
|
|
119
|
+
this.touch();
|
|
120
|
+
return {
|
|
121
|
+
entries: this.fileEntries,
|
|
122
|
+
truncated: this.truncated
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
dispose() {
|
|
126
|
+
this.lifecycleGeneration += 1;
|
|
127
|
+
if (this.expiryTimer !== undefined)
|
|
128
|
+
clearTimeout(this.expiryTimer);
|
|
129
|
+
this.expiryTimer = undefined;
|
|
130
|
+
this.entries = [];
|
|
131
|
+
this.fileEntries = [];
|
|
132
|
+
this.root = undefined;
|
|
133
|
+
this.retainedBytes = 0;
|
|
134
|
+
this.failure = undefined;
|
|
135
|
+
this.foregroundRequested = false;
|
|
136
|
+
this.preparationConsumers.clear();
|
|
137
|
+
this.truncated = false;
|
|
138
|
+
this.stale = true;
|
|
139
|
+
this.refreshedAt = 0;
|
|
140
|
+
this.refreshAttemptedAt = 0;
|
|
141
|
+
}
|
|
142
|
+
async search(query, cursor, limit = SEARCH_RESULT_LIMIT, includeDirectories = false, waitForIndex = true) {
|
|
143
|
+
const generation = this.lifecycleGeneration;
|
|
144
|
+
if (query.trim().length === 0 ||
|
|
145
|
+
query.length > 256 ||
|
|
146
|
+
!Number.isSafeInteger(limit) ||
|
|
147
|
+
limit < 1 ||
|
|
148
|
+
limit > SEARCH_RESULT_LIMIT)
|
|
149
|
+
throw new Error('FILE_SEARCH_LIMIT_EXCEEDED');
|
|
150
|
+
const after = decodeSearchCursor(cursor, query);
|
|
151
|
+
if (!waitForIndex && (this.stale || this.root === undefined)) {
|
|
152
|
+
if (this.failure !== undefined) {
|
|
153
|
+
const failure = this.failure;
|
|
154
|
+
this.failure = undefined;
|
|
155
|
+
throw failure;
|
|
156
|
+
}
|
|
157
|
+
this.foregroundRequested = true;
|
|
158
|
+
this.startLoading();
|
|
159
|
+
return { entries: [], nextCursor: null, truncated: false, indexState: 'BUILDING' };
|
|
160
|
+
}
|
|
161
|
+
await this.ensureLoaded();
|
|
162
|
+
if (generation !== this.lifecycleGeneration || this.root === undefined)
|
|
163
|
+
throw new Error('FILE_INDEX_INVALIDATED');
|
|
164
|
+
this.touch();
|
|
165
|
+
const terms = query.trim().toLocaleLowerCase().split(/\s+/u);
|
|
166
|
+
const afterEntry = after === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: this.entries.find((entry) => entry.path === after && (includeDirectories || entry.kind === 'FILE'));
|
|
169
|
+
const afterScore = afterEntry === undefined ? -1 : fuzzyPathScore(afterEntry.path, terms);
|
|
170
|
+
const afterCandidate = afterEntry === undefined || afterScore < 0
|
|
171
|
+
? undefined
|
|
172
|
+
: { entry: afterEntry, score: afterScore };
|
|
173
|
+
const ranked = new RankedCandidateHeap(limit + 1);
|
|
174
|
+
for (const entry of this.entries) {
|
|
175
|
+
if (!includeDirectories && entry.kind !== 'FILE')
|
|
176
|
+
continue;
|
|
177
|
+
const score = fuzzyPathScore(entry.path, terms);
|
|
178
|
+
if (score < 0)
|
|
179
|
+
continue;
|
|
180
|
+
const candidate = { entry, score };
|
|
181
|
+
if (afterCandidate !== undefined && compareRankedCandidate(candidate, afterCandidate) <= 0)
|
|
182
|
+
continue;
|
|
183
|
+
ranked.add(candidate);
|
|
184
|
+
}
|
|
185
|
+
const candidates = ranked.sorted();
|
|
186
|
+
const page = candidates.slice(0, limit).map((candidate) => ({
|
|
187
|
+
...candidate.entry,
|
|
188
|
+
score: candidate.score
|
|
189
|
+
}));
|
|
190
|
+
const next = candidates[limit]?.entry.path;
|
|
191
|
+
return {
|
|
192
|
+
entries: page,
|
|
193
|
+
nextCursor: next === undefined ? null : encodeSearchCursor(query, page.at(-1)?.path ?? after ?? ''),
|
|
194
|
+
truncated: next !== undefined || this.truncated,
|
|
195
|
+
...(!waitForIndex ? { indexState: 'READY' } : {})
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async ensureLoaded(requireFresh = false) {
|
|
199
|
+
const generation = this.lifecycleGeneration;
|
|
200
|
+
if (!this.stale && this.root !== undefined) {
|
|
201
|
+
if (Date.now() - this.refreshAttemptedAt >=
|
|
202
|
+
(this.limits.freshnessTtlMs ?? FILE_INDEX_FRESHNESS_TTL_MS)) {
|
|
203
|
+
if (requireFresh) {
|
|
204
|
+
this.foregroundRequested = true;
|
|
205
|
+
if (this.loading === undefined) {
|
|
206
|
+
this.refreshAttemptedAt = Date.now();
|
|
207
|
+
this.startLoading();
|
|
208
|
+
}
|
|
209
|
+
await this.loading;
|
|
210
|
+
if (generation !== this.lifecycleGeneration)
|
|
211
|
+
throw new Error('FILE_INDEX_INVALIDATED');
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
this.refreshInBackground();
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.startLoading();
|
|
219
|
+
await this.loading;
|
|
220
|
+
if (generation !== this.lifecycleGeneration)
|
|
221
|
+
throw new Error('FILE_INDEX_INVALIDATED');
|
|
222
|
+
}
|
|
223
|
+
startLoading(background = false, needed = () => true) {
|
|
224
|
+
if (!background)
|
|
225
|
+
this.foregroundRequested = true;
|
|
226
|
+
if (this.loading !== undefined)
|
|
227
|
+
return;
|
|
228
|
+
const generation = this.lifecycleGeneration;
|
|
229
|
+
const current = () => generation === this.lifecycleGeneration;
|
|
230
|
+
const required = () => current() && (this.foregroundRequested || needed());
|
|
231
|
+
const run = async () => {
|
|
232
|
+
if (!current())
|
|
233
|
+
throw new Error('FILE_INDEX_INVALIDATED');
|
|
234
|
+
if (required())
|
|
235
|
+
await this.rebuild();
|
|
236
|
+
if (!current())
|
|
237
|
+
throw new Error('FILE_INDEX_INVALIDATED');
|
|
238
|
+
};
|
|
239
|
+
const pending = (this.limits.schedule?.(run, () => !this.foregroundRequested, required) ?? run())
|
|
240
|
+
.catch((error) => {
|
|
241
|
+
if (current())
|
|
242
|
+
this.failure = error;
|
|
243
|
+
throw error;
|
|
244
|
+
})
|
|
245
|
+
.finally(() => {
|
|
246
|
+
if (this.loading === pending) {
|
|
247
|
+
this.loading = undefined;
|
|
248
|
+
this.foregroundRequested = false;
|
|
249
|
+
this.preparationConsumers.clear();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
this.loading = pending;
|
|
253
|
+
// A fast acknowledgement has no awaiting consumer; retain failure for its next query.
|
|
254
|
+
void this.loading.catch(() => undefined);
|
|
255
|
+
}
|
|
256
|
+
touch() {
|
|
257
|
+
if (this.expiryTimer !== undefined)
|
|
258
|
+
clearTimeout(this.expiryTimer);
|
|
259
|
+
this.expiryTimer = setTimeout(() => this.dispose(), this.limits.idleTtlMs ?? FILE_INDEX_IDLE_TTL_MS);
|
|
260
|
+
this.expiryTimer.unref();
|
|
261
|
+
}
|
|
262
|
+
async rebuild() {
|
|
263
|
+
const generation = this.lifecycleGeneration;
|
|
264
|
+
const root = await resolveRegisteredWorkspaceRoot(this.workspaceRoot);
|
|
265
|
+
if (generation !== this.lifecycleGeneration)
|
|
266
|
+
return;
|
|
267
|
+
const excludeOptions = { dot: true, nocase: hostPlatform() !== 'linux' };
|
|
268
|
+
const excludes = [
|
|
269
|
+
// Fixed directory globs can use their compiled regex without splitting every path.
|
|
270
|
+
...DEFAULT_QUICK_OPEN_EXCLUDES.map((pattern) => {
|
|
271
|
+
const matcher = new Minimatch(pattern, excludeOptions);
|
|
272
|
+
const regex = matcher.makeRe();
|
|
273
|
+
// Globstar also matches line terminators in valid filesystem names.
|
|
274
|
+
return regex ? new RegExp(regex.source, `${regex.flags}s`) : matcher;
|
|
275
|
+
}),
|
|
276
|
+
...(await workspaceExcludePatterns(root)).map((pattern) => new Minimatch(pattern, excludeOptions))
|
|
277
|
+
];
|
|
278
|
+
if (generation !== this.lifecycleGeneration)
|
|
279
|
+
return;
|
|
280
|
+
const indexed = [];
|
|
281
|
+
const entryLimit = this.limits.entryLimit ?? FILE_INDEX_ENTRY_LIMIT;
|
|
282
|
+
const byteLimit = this.limits.byteLimit ?? FILE_INDEX_BYTE_LIMIT;
|
|
283
|
+
let bytes = 0;
|
|
284
|
+
let truncated = false;
|
|
285
|
+
const directories = ['.'];
|
|
286
|
+
let directoryCursor = 0;
|
|
287
|
+
while (generation === this.lifecycleGeneration &&
|
|
288
|
+
directoryCursor < directories.length &&
|
|
289
|
+
!truncated) {
|
|
290
|
+
const relativeDirectory = directories[directoryCursor];
|
|
291
|
+
directoryCursor += 1;
|
|
292
|
+
const directory = await resolveWorkspaceEntryFromCanonicalRoot(root, relativeDirectory, true).catch(() => undefined);
|
|
293
|
+
if (directory === undefined)
|
|
294
|
+
continue;
|
|
295
|
+
const stream = await opendir(directory).catch(() => undefined);
|
|
296
|
+
if (stream === undefined)
|
|
297
|
+
continue;
|
|
298
|
+
let pending = [];
|
|
299
|
+
const appendPending = async () => {
|
|
300
|
+
const resolvedEntries = await Promise.all(pending);
|
|
301
|
+
pending = [];
|
|
302
|
+
if (generation !== this.lifecycleGeneration)
|
|
303
|
+
return;
|
|
304
|
+
for (const entry of resolvedEntries) {
|
|
305
|
+
if (entry === undefined)
|
|
306
|
+
continue;
|
|
307
|
+
if (entry.path === '.git' || entry.path.startsWith('.git/'))
|
|
308
|
+
continue;
|
|
309
|
+
const entryBytes = 160 + 2 * (entry.path.length + entry.name.length);
|
|
310
|
+
if (bytes + entryBytes > byteLimit) {
|
|
311
|
+
truncated = true;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
bytes += entryBytes;
|
|
315
|
+
if (entry.kind === 'DIRECTORY')
|
|
316
|
+
directories.push(entry.path);
|
|
317
|
+
indexed.push(entry);
|
|
318
|
+
if (indexed.length >= entryLimit) {
|
|
319
|
+
truncated = true;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
for await (const child of stream) {
|
|
325
|
+
if (generation !== this.lifecycleGeneration)
|
|
326
|
+
return;
|
|
327
|
+
const candidatePath = relativeDirectory === '.'
|
|
328
|
+
? child.name
|
|
329
|
+
: posix.join(relativeDirectory.replaceAll('\\', '/'), child.name);
|
|
330
|
+
if (candidatePath === '.git' ||
|
|
331
|
+
candidatePath.startsWith('.git/') ||
|
|
332
|
+
isExcluded(candidatePath, excludes))
|
|
333
|
+
continue;
|
|
334
|
+
pending.push(inspectWorkspaceIndexEntry(root, candidatePath, child.name, excludes));
|
|
335
|
+
if (pending.length >= FILE_INDEX_METADATA_CONCURRENCY)
|
|
336
|
+
await appendPending();
|
|
337
|
+
if (truncated)
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
if (!truncated && pending.length > 0)
|
|
341
|
+
await appendPending();
|
|
342
|
+
}
|
|
343
|
+
if (generation !== this.lifecycleGeneration)
|
|
344
|
+
return;
|
|
345
|
+
this.root = root;
|
|
346
|
+
this.entries = indexed;
|
|
347
|
+
this.fileEntries = indexed.filter((entry) => entry.kind === 'FILE');
|
|
348
|
+
this.retainedBytes = bytes;
|
|
349
|
+
this.failure = undefined;
|
|
350
|
+
this.truncated = truncated;
|
|
351
|
+
this.stale = false;
|
|
352
|
+
this.refreshedAt = Date.now();
|
|
353
|
+
this.refreshAttemptedAt = this.refreshedAt;
|
|
354
|
+
this.touch();
|
|
355
|
+
this.limits.onRebuilt?.();
|
|
356
|
+
}
|
|
357
|
+
refreshInBackground() {
|
|
358
|
+
if (this.loading !== undefined)
|
|
359
|
+
return;
|
|
360
|
+
this.refreshAttemptedAt = Date.now();
|
|
361
|
+
this.startLoading(true);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function workspaceExcludePatterns(root) {
|
|
365
|
+
const settingsPath = resolve(root, '.vscode/settings.json');
|
|
366
|
+
const settings = (await readFile(settingsPath, 'utf8')
|
|
367
|
+
.then(JSON.parse)
|
|
368
|
+
.catch(() => undefined));
|
|
369
|
+
if (typeof settings !== 'object' || settings === null)
|
|
370
|
+
return [];
|
|
371
|
+
return ['files.exclude', 'search.exclude'].flatMap((key) => {
|
|
372
|
+
const value = settings[key];
|
|
373
|
+
return typeof value === 'object' && value !== null
|
|
374
|
+
? Object.entries(value).flatMap(([pattern, enabled]) => (enabled === true ? [pattern] : []))
|
|
375
|
+
: [];
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function isExcluded(path, patterns) {
|
|
379
|
+
return patterns.some((pattern) => pattern instanceof RegExp
|
|
380
|
+
? pattern.test(path) || pattern.test(`${path}/`)
|
|
381
|
+
: pattern.match(path) || pattern.match(`${path}/`));
|
|
382
|
+
}
|
|
383
|
+
function compareRankedCandidate(left, right) {
|
|
384
|
+
return right.score - left.score || left.entry.path.localeCompare(right.entry.path);
|
|
385
|
+
}
|
|
386
|
+
/** Keeps only the best page-sized candidate set, with the current worst item at the root. */
|
|
387
|
+
class RankedCandidateHeap {
|
|
388
|
+
capacity;
|
|
389
|
+
values = [];
|
|
390
|
+
constructor(capacity) {
|
|
391
|
+
this.capacity = capacity;
|
|
392
|
+
}
|
|
393
|
+
add(candidate) {
|
|
394
|
+
if (this.values.length < this.capacity) {
|
|
395
|
+
this.values.push(candidate);
|
|
396
|
+
this.bubbleUp(this.values.length - 1);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (compareRankedCandidate(candidate, this.values[0]) >= 0)
|
|
400
|
+
return;
|
|
401
|
+
this.values[0] = candidate;
|
|
402
|
+
this.sinkDown(0);
|
|
403
|
+
}
|
|
404
|
+
sorted() {
|
|
405
|
+
return [...this.values].sort(compareRankedCandidate);
|
|
406
|
+
}
|
|
407
|
+
bubbleUp(start) {
|
|
408
|
+
let index = start;
|
|
409
|
+
while (index > 0) {
|
|
410
|
+
const parent = Math.floor((index - 1) / 2);
|
|
411
|
+
if (compareRankedCandidate(this.values[index], this.values[parent]) <= 0)
|
|
412
|
+
break;
|
|
413
|
+
[this.values[index], this.values[parent]] = [
|
|
414
|
+
this.values[parent],
|
|
415
|
+
this.values[index]
|
|
416
|
+
];
|
|
417
|
+
index = parent;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
sinkDown(start) {
|
|
421
|
+
let index = start;
|
|
422
|
+
while (true) {
|
|
423
|
+
const left = index * 2 + 1;
|
|
424
|
+
if (left >= this.values.length)
|
|
425
|
+
return;
|
|
426
|
+
const right = left + 1;
|
|
427
|
+
const worse = right < this.values.length &&
|
|
428
|
+
compareRankedCandidate(this.values[right], this.values[left]) > 0
|
|
429
|
+
? right
|
|
430
|
+
: left;
|
|
431
|
+
if (compareRankedCandidate(this.values[worse], this.values[index]) <= 0)
|
|
432
|
+
return;
|
|
433
|
+
[this.values[index], this.values[worse]] = [
|
|
434
|
+
this.values[worse],
|
|
435
|
+
this.values[index]
|
|
436
|
+
];
|
|
437
|
+
index = worse;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function fuzzyPathScore(path, terms) {
|
|
442
|
+
const source = path.toLocaleLowerCase();
|
|
443
|
+
const name = source.slice(source.lastIndexOf('/') + 1);
|
|
444
|
+
let total = 0;
|
|
445
|
+
for (const needle of terms) {
|
|
446
|
+
const score = fuzzyTermScore(source, name, needle);
|
|
447
|
+
if (score < 0)
|
|
448
|
+
return -1;
|
|
449
|
+
total += score;
|
|
450
|
+
}
|
|
451
|
+
return total - path.length / 1_000;
|
|
452
|
+
}
|
|
453
|
+
function fuzzyTermScore(source, name, needle) {
|
|
454
|
+
if (name === needle || source === needle)
|
|
455
|
+
return 1_000;
|
|
456
|
+
if (name.startsWith(needle) || source.startsWith(needle))
|
|
457
|
+
return 700;
|
|
458
|
+
if (source.endsWith(`/${needle}`) || source.includes(`/${needle}/`))
|
|
459
|
+
return 600;
|
|
460
|
+
if (name.includes(needle) || source.includes(needle))
|
|
461
|
+
return 500;
|
|
462
|
+
let cursor = 0;
|
|
463
|
+
let score = 0;
|
|
464
|
+
let previous = -2;
|
|
465
|
+
for (const character of needle) {
|
|
466
|
+
const index = source.indexOf(character, cursor);
|
|
467
|
+
if (index === -1)
|
|
468
|
+
return -1;
|
|
469
|
+
score += index === previous + 1 ? 12 : 1;
|
|
470
|
+
if (index === 0 || '/._-'.includes(source[index - 1] ?? ''))
|
|
471
|
+
score += 8;
|
|
472
|
+
previous = index;
|
|
473
|
+
cursor = index + 1;
|
|
474
|
+
}
|
|
475
|
+
return 100 + score / 1_000;
|
|
476
|
+
}
|
|
477
|
+
async function inspectWorkspaceIndexEntry(root, requestedPath, name, excludes) {
|
|
478
|
+
try {
|
|
479
|
+
const entry = await resolveWorkspaceEntryMetadata(root, requestedPath);
|
|
480
|
+
if (!entry.metadata.isFile() && !entry.metadata.isDirectory())
|
|
481
|
+
return undefined;
|
|
482
|
+
const safePath = relative(root, entry.path).split(sep).join('/');
|
|
483
|
+
// The requested path was already checked; aliases need the canonical-path check too.
|
|
484
|
+
if (safePath !== requestedPath && isExcluded(safePath, excludes))
|
|
485
|
+
return undefined;
|
|
486
|
+
return entry.metadata.isDirectory()
|
|
487
|
+
? {
|
|
488
|
+
path: safePath,
|
|
489
|
+
name,
|
|
490
|
+
kind: 'DIRECTORY',
|
|
491
|
+
size: null,
|
|
492
|
+
modifiedAt: entry.metadata.mtimeMs
|
|
493
|
+
}
|
|
494
|
+
: {
|
|
495
|
+
path: safePath,
|
|
496
|
+
name,
|
|
497
|
+
kind: 'FILE',
|
|
498
|
+
size: entry.metadata.size,
|
|
499
|
+
modifiedAt: entry.metadata.mtimeMs
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function encodeSearchCursor(query, path) {
|
|
507
|
+
return Buffer.from(JSON.stringify({ query, path })).toString('base64url');
|
|
508
|
+
}
|
|
509
|
+
function decodeSearchCursor(cursor, expectedQuery) {
|
|
510
|
+
if (cursor === undefined)
|
|
511
|
+
return undefined;
|
|
512
|
+
try {
|
|
513
|
+
const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
514
|
+
if (typeof value !== 'object' ||
|
|
515
|
+
value === null ||
|
|
516
|
+
value.query !== expectedQuery ||
|
|
517
|
+
typeof value.path !== 'string')
|
|
518
|
+
throw new Error();
|
|
519
|
+
return value.path;
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
throw new Error('DIRECTORY_CURSOR_INVALID');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/** Searches file names only, so an exploratory request cannot exfiltrate file contents. */
|
|
526
|
+
export async function searchWorkspaceFiles(workspaceRoot, query, cursor, limit = SEARCH_RESULT_LIMIT, includeDirectories = false) {
|
|
527
|
+
const index = new WorkspaceFileIndex(workspaceRoot);
|
|
528
|
+
try {
|
|
529
|
+
return await index.search(query, cursor, limit, includeDirectories);
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
index.dispose();
|
|
533
|
+
}
|
|
534
|
+
}
|