@arnilo/prism-coding-agent 0.2.4 → 0.2.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.
- package/CHANGELOG.md +16 -0
- package/README.md +10 -0
- package/dist/coding-checkpoint.js +4 -0
- package/dist/diagnostics.d.ts +83 -0
- package/dist/diagnostics.js +179 -0
- package/dist/git.d.ts +27 -6
- package/dist/git.js +58 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.js +13 -2
- package/dist/language/client.d.ts +25 -0
- package/dist/language/client.js +54 -0
- package/dist/language/framing.d.ts +9 -1
- package/dist/language/framing.js +89 -17
- package/dist/language/index.d.ts +1 -1
- package/dist/language/intelligence.js +58 -0
- package/dist/language/types.d.ts +32 -0
- package/dist/limits.d.ts +59 -0
- package/dist/limits.js +59 -0
- package/dist/process/index.d.ts +4 -1
- package/dist/process/index.js +1 -0
- package/dist/process/recovery.d.ts +174 -0
- package/dist/process/recovery.js +320 -0
- package/dist/process/sessions.js +714 -25
- package/dist/process/types.d.ts +128 -4
- package/dist/process/types.js +7 -1
- package/dist/repository/glob.d.ts +4 -0
- package/dist/repository/glob.js +143 -0
- package/dist/repository/indexed-search.d.ts +121 -0
- package/dist/repository/indexed-search.js +313 -0
- package/dist/repository/list.d.ts +3 -0
- package/dist/repository/list.js +119 -0
- package/dist/repository/operations.d.ts +5 -0
- package/dist/repository/operations.js +14 -0
- package/dist/repository/path.d.ts +18 -0
- package/dist/repository/path.js +91 -0
- package/dist/repository/search.d.ts +9 -0
- package/dist/repository/search.js +284 -0
- package/dist/repository/types.d.ts +138 -0
- package/dist/repository/types.js +31 -0
- package/dist/repository/walk.d.ts +22 -0
- package/dist/repository/walk.js +99 -0
- package/dist/repository.d.ts +11 -172
- package/dist/repository.js +11 -748
- package/dist/review.d.ts +150 -0
- package/dist/review.js +222 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +42 -7
- package/dist/workspace-lifecycle.d.ts +153 -0
- package/dist/workspace-lifecycle.js +629 -0
- package/package.json +3 -3
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 26 Task 2: host-indexed repository search seam.
|
|
3
|
+
*
|
|
4
|
+
* `createIndexedRepositoryOperations` composes a host-owned incremental index
|
|
5
|
+
* backend with an existing literal `RepositoryOperations` fallback. `repo_search`
|
|
6
|
+
* stays bounded literal by default; `indexed_literal`/`semantic` modes are
|
|
7
|
+
* explicit and never fall back silently when the index is missing, stale, or
|
|
8
|
+
* failed. Index output is untrusted: every hit is containment-checked,
|
|
9
|
+
* bounded, and labeled `untrusted_index` on the result.
|
|
10
|
+
*/
|
|
11
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
12
|
+
import { DEFAULT_MAX_INDEX_QUERY_TIMEOUT_MS, DEFAULT_MAX_INDEX_SNIPPET_BYTES, DEFAULT_MAX_INDEX_STALE_MAX_AGE_MS, DEFAULT_MAX_INDEX_UPDATE_BYTES, DEFAULT_MAX_INDEX_UPDATE_FILES, DEFAULT_MAX_REPO_RESULTS, HARD_MAX_INDEX_QUERY_TIMEOUT_MS, HARD_MAX_INDEX_SNIPPET_BYTES, HARD_MAX_INDEX_STALE_MAX_AGE_MS, HARD_MAX_INDEX_UPDATE_BYTES, HARD_MAX_INDEX_UPDATE_FILES, HARD_MAX_REPO_RESULTS, validateCodingLimit, } from "../limits.js";
|
|
13
|
+
import { isPathInsideRoot } from "./path.js";
|
|
14
|
+
export const INDEX_STATES = ["empty", "building", "ready", "stale", "failed"];
|
|
15
|
+
export function resolveIndexLimits(options) {
|
|
16
|
+
return {
|
|
17
|
+
maxUpdateFiles: validateCodingLimit("maxUpdateFiles", options?.maxUpdateFiles ?? DEFAULT_MAX_INDEX_UPDATE_FILES, HARD_MAX_INDEX_UPDATE_FILES),
|
|
18
|
+
maxUpdateBytes: validateCodingLimit("maxUpdateBytes", options?.maxUpdateBytes ?? DEFAULT_MAX_INDEX_UPDATE_BYTES, HARD_MAX_INDEX_UPDATE_BYTES),
|
|
19
|
+
maxResults: validateCodingLimit("maxResults", options?.maxResults ?? DEFAULT_MAX_REPO_RESULTS, HARD_MAX_REPO_RESULTS),
|
|
20
|
+
maxSnippetBytes: validateCodingLimit("maxSnippetBytes", options?.maxSnippetBytes ?? DEFAULT_MAX_INDEX_SNIPPET_BYTES, HARD_MAX_INDEX_SNIPPET_BYTES),
|
|
21
|
+
staleMaxAgeMs: validateCodingLimit("staleMaxAgeMs", options?.staleMaxAgeMs ?? DEFAULT_MAX_INDEX_STALE_MAX_AGE_MS, HARD_MAX_INDEX_STALE_MAX_AGE_MS),
|
|
22
|
+
queryTimeoutMs: validateCodingLimit("queryTimeoutMs", options?.queryTimeoutMs ?? DEFAULT_MAX_INDEX_QUERY_TIMEOUT_MS, HARD_MAX_INDEX_QUERY_TIMEOUT_MS),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export class IndexError extends Error {
|
|
26
|
+
code;
|
|
27
|
+
constructor(code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.name = "IndexError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const MAX_IDENTITY_BYTES = 512;
|
|
34
|
+
const MAX_REVISION_BYTES = 4096;
|
|
35
|
+
function assertRelativeRepoPath(path) {
|
|
36
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
37
|
+
throw new IndexError("ERR_PRISM_INDEX_UNTRUSTED", "index reported an invalid path");
|
|
38
|
+
}
|
|
39
|
+
if (path.startsWith("/") || path.includes("\\") || isAbsolute(path) || path === ".." || path.startsWith("../") || path.includes("/../")) {
|
|
40
|
+
throw new IndexError("ERR_PRISM_INDEX_UNTRUSTED", "index reported a path outside the repository");
|
|
41
|
+
}
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
function assertBoundedIdentity(value, label) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > MAX_IDENTITY_BYTES) {
|
|
48
|
+
throw new IndexError("ERR_PRISM_INDEX_LIMIT", `${label} exceeds the identity bound`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function assertBoundedRevision(value) {
|
|
53
|
+
if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value, "utf8") > MAX_REVISION_BYTES) {
|
|
54
|
+
throw new IndexError("ERR_PRISM_INDEX_LIMIT", "sourceRevision is required and bounded");
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function truncateUtf8(value, maxBytes) {
|
|
59
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
60
|
+
return value;
|
|
61
|
+
let end = 0;
|
|
62
|
+
let bytes = 0;
|
|
63
|
+
for (const char of value) {
|
|
64
|
+
const size = Buffer.byteLength(char, "utf8");
|
|
65
|
+
if (bytes + size > maxBytes)
|
|
66
|
+
break;
|
|
67
|
+
bytes += size;
|
|
68
|
+
end += char.length;
|
|
69
|
+
}
|
|
70
|
+
return value.slice(0, end);
|
|
71
|
+
}
|
|
72
|
+
function isFresh(status, staleMaxAgeMs, requireSourceRevision) {
|
|
73
|
+
switch (status.state) {
|
|
74
|
+
case "failed":
|
|
75
|
+
return new IndexError("ERR_PRISM_INDEX_FAILED", "index is failed; no search is served");
|
|
76
|
+
case "empty":
|
|
77
|
+
return new IndexError("ERR_PRISM_INDEX_STALE", "index has no data; update it before querying");
|
|
78
|
+
case "building":
|
|
79
|
+
return new IndexError("ERR_PRISM_INDEX_STALE", "index is still building");
|
|
80
|
+
case "ready":
|
|
81
|
+
case "stale":
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
return new IndexError("ERR_PRISM_INDEX_FAILED", "index reported an unknown state");
|
|
85
|
+
}
|
|
86
|
+
if (status.updatedAt === undefined) {
|
|
87
|
+
return new IndexError("ERR_PRISM_INDEX_STALE", "index does not attest a freshness timestamp");
|
|
88
|
+
}
|
|
89
|
+
if (Date.now() - status.updatedAt > staleMaxAgeMs) {
|
|
90
|
+
return new IndexError("ERR_PRISM_INDEX_STALE", "index is stale; refresh it before querying");
|
|
91
|
+
}
|
|
92
|
+
if (requireSourceRevision && status.sourceRevision === undefined) {
|
|
93
|
+
return new IndexError("ERR_PRISM_INDEX_STALE", "index does not attest a source revision");
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function isIndexMode(mode) {
|
|
98
|
+
return mode === "indexed_literal" || mode === "semantic";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Compose a host index with a literal fallback. Mode "literal" is routed to
|
|
102
|
+
* `fallback` unchanged; indexed modes are served only by the host backend with
|
|
103
|
+
* freshness checks, result validation, and no silent fallback.
|
|
104
|
+
*/
|
|
105
|
+
export function createIndexedRepositoryOperations(cwd, options) {
|
|
106
|
+
const allowed = new Set(options.allowedModes ?? ["literal"]);
|
|
107
|
+
const resolved = resolveIndexLimits(options.limits);
|
|
108
|
+
const staleMaxAgeMs = options.stale?.maxAgeMs ?? resolved.staleMaxAgeMs;
|
|
109
|
+
const requireSourceRevision = options.stale?.requireSourceRevision === true;
|
|
110
|
+
const root = resolve(cwd);
|
|
111
|
+
const backend = options.index;
|
|
112
|
+
async function checkFresh() {
|
|
113
|
+
let status;
|
|
114
|
+
try {
|
|
115
|
+
status = await backend.status();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new IndexError("ERR_PRISM_INDEX_FAILED", "index status failed");
|
|
119
|
+
}
|
|
120
|
+
const stale = isFresh(status, staleMaxAgeMs, requireSourceRevision);
|
|
121
|
+
if (stale)
|
|
122
|
+
throw stale;
|
|
123
|
+
return status;
|
|
124
|
+
}
|
|
125
|
+
function validateHit(hit, scope) {
|
|
126
|
+
const path = assertRelativeRepoPath(hit.path);
|
|
127
|
+
const absolute = join(root, ...path.split("/"));
|
|
128
|
+
if (!isPathInsideRoot(root, absolute)) {
|
|
129
|
+
throw new IndexError("ERR_PRISM_INDEX_UNTRUSTED", "index reported a path outside the repository root");
|
|
130
|
+
}
|
|
131
|
+
if (scope !== undefined && scope !== "." && path !== scope && !path.startsWith(`${scope}/`)) {
|
|
132
|
+
throw new IndexError("ERR_PRISM_INDEX_UNTRUSTED", "index reported a result outside the requested path scope");
|
|
133
|
+
}
|
|
134
|
+
if (typeof hit.score !== "number" || !Number.isFinite(hit.score) || hit.score < 0 || hit.score > 1) {
|
|
135
|
+
throw new IndexError("ERR_PRISM_INDEX_UNTRUSTED", "index reported an invalid score");
|
|
136
|
+
}
|
|
137
|
+
const snippet = typeof hit.snippet === "string" ? truncateUtf8(hit.snippet, resolved.maxSnippetBytes) : "";
|
|
138
|
+
return {
|
|
139
|
+
path,
|
|
140
|
+
line: 0,
|
|
141
|
+
column: 0,
|
|
142
|
+
text: snippet,
|
|
143
|
+
before: [],
|
|
144
|
+
after: [],
|
|
145
|
+
score: hit.score,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
async search(request) {
|
|
150
|
+
const mode = request.mode ?? "literal";
|
|
151
|
+
if (!isIndexMode(mode))
|
|
152
|
+
return options.fallback.search(request);
|
|
153
|
+
if (!allowed.has(mode)) {
|
|
154
|
+
throw new IndexError("ERR_PRISM_INDEX_UNSUPPORTED", `search mode ${mode} is not enabled on this composite`);
|
|
155
|
+
}
|
|
156
|
+
if (mode === "semantic" && backend.capabilities.semantic !== true) {
|
|
157
|
+
throw new IndexError("ERR_PRISM_INDEX_UNSUPPORTED", "semantic search is not supported by this index backend");
|
|
158
|
+
}
|
|
159
|
+
const status = await checkFresh();
|
|
160
|
+
let scope;
|
|
161
|
+
if (request.path !== undefined && request.path !== "" && request.path !== ".") {
|
|
162
|
+
scope = assertRelativeRepoPath(request.path);
|
|
163
|
+
}
|
|
164
|
+
const timeoutMs = Math.min(request.deadlineMs ?? resolved.queryTimeoutMs, resolved.queryTimeoutMs);
|
|
165
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
166
|
+
const maxResults = Math.min(resolved.maxResults, validateCodingLimit("maxMatches", request.maxMatches ?? resolved.maxResults, HARD_MAX_REPO_RESULTS));
|
|
167
|
+
let queryResult;
|
|
168
|
+
try {
|
|
169
|
+
queryResult = await Promise.race([
|
|
170
|
+
backend.search({
|
|
171
|
+
query: request.query,
|
|
172
|
+
mode,
|
|
173
|
+
path: scope,
|
|
174
|
+
maxResults,
|
|
175
|
+
signal: request.signal,
|
|
176
|
+
deadlineMs: timeoutMs,
|
|
177
|
+
}),
|
|
178
|
+
new Promise((_, reject) => {
|
|
179
|
+
const timer = setTimeout(() => reject(new IndexError("ERR_PRISM_INDEX_TIMEOUT", "index query timed out")), timeoutMs + 5);
|
|
180
|
+
timer.unref();
|
|
181
|
+
}),
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (error instanceof IndexError)
|
|
186
|
+
throw error;
|
|
187
|
+
if (request.signal?.aborted)
|
|
188
|
+
throw new IndexError("ERR_PRISM_INDEX_TIMEOUT", "index query aborted");
|
|
189
|
+
throw new IndexError("ERR_PRISM_INDEX_FAILED", "index query failed");
|
|
190
|
+
}
|
|
191
|
+
if (Date.now() > deadlineAt) {
|
|
192
|
+
throw new IndexError("ERR_PRISM_INDEX_TIMEOUT", "index query exceeded the deadline");
|
|
193
|
+
}
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
const matches = [];
|
|
196
|
+
for (const hit of queryResult.hits) {
|
|
197
|
+
if (matches.length >= maxResults)
|
|
198
|
+
break;
|
|
199
|
+
const match = validateHit(hit, scope);
|
|
200
|
+
if (seen.has(match.path))
|
|
201
|
+
continue; // duplicate paths: keep first
|
|
202
|
+
seen.add(match.path);
|
|
203
|
+
matches.push(match);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
matches,
|
|
207
|
+
truncated: queryResult.truncated || queryResult.hits.length > matches.length,
|
|
208
|
+
truncatedBy: queryResult.truncated || queryResult.hits.length > matches.length ? "index" : null,
|
|
209
|
+
scannedBytes: 0,
|
|
210
|
+
scannedFiles: 0,
|
|
211
|
+
scannedEntries: 0,
|
|
212
|
+
filesSkippedBinary: 0,
|
|
213
|
+
filesSkippedOversize: 0,
|
|
214
|
+
indexed: {
|
|
215
|
+
mode,
|
|
216
|
+
state: status.state,
|
|
217
|
+
sourceRevision: status.sourceRevision,
|
|
218
|
+
updatedAt: status.updatedAt,
|
|
219
|
+
},
|
|
220
|
+
untrusted_index: true,
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
list: (request) => options.fallback.list(request),
|
|
224
|
+
glob: (request) => options.fallback.glob(request),
|
|
225
|
+
index: {
|
|
226
|
+
async update(request) {
|
|
227
|
+
assertBoundedRevision(request.sourceRevision);
|
|
228
|
+
assertBoundedIdentity(request.repositoryId, "repositoryId");
|
|
229
|
+
assertBoundedIdentity(request.worktreeId, "worktreeId");
|
|
230
|
+
if (!Array.isArray(request.changes) || request.changes.length > resolved.maxUpdateFiles) {
|
|
231
|
+
throw new IndexError("ERR_PRISM_INDEX_LIMIT", `update exceeds the ${resolved.maxUpdateFiles} change cap`);
|
|
232
|
+
}
|
|
233
|
+
let totalBytes = Buffer.byteLength(request.sourceRevision, "utf8");
|
|
234
|
+
const updates = [];
|
|
235
|
+
const removals = [];
|
|
236
|
+
for (const change of request.changes) {
|
|
237
|
+
assertRelativeRepoPath(change.path);
|
|
238
|
+
if (change.oldPath !== undefined)
|
|
239
|
+
assertRelativeRepoPath(change.oldPath);
|
|
240
|
+
if (change.bytes !== undefined && (typeof change.bytes !== "number" || !Number.isFinite(change.bytes) || change.bytes < 0)) {
|
|
241
|
+
throw new IndexError("ERR_PRISM_INDEX_LIMIT", "change bytes must be a non-negative finite number");
|
|
242
|
+
}
|
|
243
|
+
totalBytes += Buffer.byteLength(change.path, "utf8") + (change.oldPath ? Buffer.byteLength(change.oldPath, "utf8") : 0);
|
|
244
|
+
if (totalBytes > resolved.maxUpdateBytes) {
|
|
245
|
+
throw new IndexError("ERR_PRISM_INDEX_LIMIT", `update exceeds the ${resolved.maxUpdateBytes} byte cap`);
|
|
246
|
+
}
|
|
247
|
+
if (change.kind === "delete") {
|
|
248
|
+
removals.push(change.path);
|
|
249
|
+
}
|
|
250
|
+
else if (change.kind === "rename") {
|
|
251
|
+
removals.push(change.oldPath);
|
|
252
|
+
updates.push({ path: change.path, kind: "add", bytes: change.bytes });
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
updates.push(change);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
if (updates.length > 0)
|
|
260
|
+
await backend.update({
|
|
261
|
+
repositoryId: request.repositoryId,
|
|
262
|
+
worktreeId: request.worktreeId,
|
|
263
|
+
sourceRevision: request.sourceRevision,
|
|
264
|
+
changes: updates,
|
|
265
|
+
});
|
|
266
|
+
if (removals.length > 0)
|
|
267
|
+
await backend.remove({ paths: removals });
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
throw new IndexError("ERR_PRISM_INDEX_FAILED", "index update failed");
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
async remove(request) {
|
|
274
|
+
if (!Array.isArray(request.paths) || request.paths.length > resolved.maxUpdateFiles) {
|
|
275
|
+
throw new IndexError("ERR_PRISM_INDEX_LIMIT", `remove exceeds the ${resolved.maxUpdateFiles} path cap`);
|
|
276
|
+
}
|
|
277
|
+
const paths = request.paths.map((p) => assertRelativeRepoPath(p));
|
|
278
|
+
try {
|
|
279
|
+
await backend.remove({ paths });
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
throw new IndexError("ERR_PRISM_INDEX_FAILED", "index remove failed");
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
async status() {
|
|
286
|
+
let status;
|
|
287
|
+
try {
|
|
288
|
+
status = await backend.status();
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
throw new IndexError("ERR_PRISM_INDEX_FAILED", "index status failed");
|
|
292
|
+
}
|
|
293
|
+
if (!INDEX_STATES.includes(status.state)) {
|
|
294
|
+
throw new IndexError("ERR_PRISM_INDEX_FAILED", "index reported an unknown state");
|
|
295
|
+
}
|
|
296
|
+
return status;
|
|
297
|
+
},
|
|
298
|
+
async dispose() {
|
|
299
|
+
try {
|
|
300
|
+
await backend.dispose();
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
throw new IndexError("ERR_PRISM_INDEX_FAILED", "index dispose failed");
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/** Stable repo-relative label used in errors; kept for tests and docs. */
|
|
310
|
+
export function indexErrorCode(error) {
|
|
311
|
+
return error instanceof IndexError ? error.code : undefined;
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=indexed-search.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RepositoryListRequest, RepositoryListResult, ResolvedRepositoryLimits } from "./types.js";
|
|
2
|
+
import type { RepositoryWalk } from "./walk.js";
|
|
3
|
+
export declare function listLocal(request: RepositoryListRequest, defaults: ResolvedRepositoryLimits, walk: RepositoryWalk): Promise<RepositoryListResult>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Repository list family (0.2.5 plan 025 Task 1 split).
|
|
2
|
+
* Moved verbatim from repository.ts; public surface unchanged behind the barrel. */
|
|
3
|
+
import { HARD_MAX_REPO_DEPTH, HARD_MAX_REPO_ENTRIES, HARD_MAX_REPO_RESULTS, validateCodingLimit, validateCodingLimitAllowZero, } from "../limits.js";
|
|
4
|
+
import { lstat } from "node:fs/promises";
|
|
5
|
+
import { RepositoryError } from "./types.js";
|
|
6
|
+
import { resolveRepoPath } from "./path.js";
|
|
7
|
+
export async function listLocal(request, defaults, walk) {
|
|
8
|
+
const resolved = await resolveRepoPath(request.root, request.path);
|
|
9
|
+
const maxResults = validateCodingLimit("maxResults", request.maxResults ?? defaults.maxResults, HARD_MAX_REPO_RESULTS);
|
|
10
|
+
const offset = validateCodingLimitAllowZero("offset", request.offset ?? 0, HARD_MAX_REPO_ENTRIES);
|
|
11
|
+
const maxDepth = validateCodingLimit("maxDepth", request.maxDepth ?? defaults.maxDepth, HARD_MAX_REPO_DEPTH);
|
|
12
|
+
const exclude = new Set(request.exclude ?? defaults.exclude);
|
|
13
|
+
const deadlineAt = request.deadlineMs !== undefined ? Date.now() + request.deadlineMs : Date.now() + defaults.maxTimeMs;
|
|
14
|
+
const collected = [];
|
|
15
|
+
let scannedEntries = 0;
|
|
16
|
+
let scannedFiles = 0;
|
|
17
|
+
let seen = 0;
|
|
18
|
+
let truncated = false;
|
|
19
|
+
let truncatedBy = null;
|
|
20
|
+
// Single-file start: return that entry when it falls within the page window.
|
|
21
|
+
try {
|
|
22
|
+
const startStat = await lstat(resolved.absolute);
|
|
23
|
+
if (!startStat.isDirectory()) {
|
|
24
|
+
let kind = "other";
|
|
25
|
+
if (startStat.isSymbolicLink())
|
|
26
|
+
kind = "symlink";
|
|
27
|
+
else if (startStat.isFile())
|
|
28
|
+
kind = "file";
|
|
29
|
+
const entry = kind === "file" ? { path: resolved.relative, kind, size: startStat.size } : { path: resolved.relative, kind };
|
|
30
|
+
scannedEntries = 1;
|
|
31
|
+
scannedFiles = kind === "file" ? 1 : 0;
|
|
32
|
+
if (offset === 0 && maxResults > 0)
|
|
33
|
+
collected.push(entry);
|
|
34
|
+
else if (offset === 0 && maxResults === 0) {
|
|
35
|
+
truncated = true;
|
|
36
|
+
truncatedBy = "results";
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
entries: collected,
|
|
40
|
+
truncated,
|
|
41
|
+
truncatedBy,
|
|
42
|
+
scannedEntries,
|
|
43
|
+
scannedFiles,
|
|
44
|
+
offset,
|
|
45
|
+
nextOffset: undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
throw new RepositoryError(`cannot open path: ${message}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
for await (const event of walk(resolved.rootReal, resolved.absolute, {
|
|
55
|
+
maxDepth,
|
|
56
|
+
maxEntries: defaults.maxEntries,
|
|
57
|
+
maxFiles: defaults.maxFiles,
|
|
58
|
+
exclude,
|
|
59
|
+
includeHidden: request.includeHidden === true,
|
|
60
|
+
signal: request.signal,
|
|
61
|
+
deadlineAt,
|
|
62
|
+
})) {
|
|
63
|
+
if (event.type === "limit") {
|
|
64
|
+
truncated = true;
|
|
65
|
+
truncatedBy = event.truncatedBy;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
scannedEntries++;
|
|
69
|
+
if (event.entry.kind === "file")
|
|
70
|
+
scannedFiles++;
|
|
71
|
+
if (seen < offset) {
|
|
72
|
+
seen++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (collected.length >= maxResults) {
|
|
76
|
+
truncated = true;
|
|
77
|
+
truncatedBy = "results";
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
collected.push(event.entry);
|
|
81
|
+
seen++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof RepositoryError && error.message === "Operation aborted") {
|
|
86
|
+
return {
|
|
87
|
+
entries: collected,
|
|
88
|
+
truncated: true,
|
|
89
|
+
truncatedBy: "abort",
|
|
90
|
+
scannedEntries,
|
|
91
|
+
scannedFiles,
|
|
92
|
+
offset,
|
|
93
|
+
nextOffset: collected.length > 0 || offset > 0 ? offset + collected.length : undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (error instanceof RepositoryError && error.message === "Repository operation exceeded time limit") {
|
|
97
|
+
return {
|
|
98
|
+
entries: collected,
|
|
99
|
+
truncated: true,
|
|
100
|
+
truncatedBy: "time",
|
|
101
|
+
scannedEntries,
|
|
102
|
+
scannedFiles,
|
|
103
|
+
offset,
|
|
104
|
+
nextOffset: offset + collected.length,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
entries: collected,
|
|
111
|
+
truncated,
|
|
112
|
+
truncatedBy,
|
|
113
|
+
scannedEntries,
|
|
114
|
+
scannedFiles,
|
|
115
|
+
offset,
|
|
116
|
+
nextOffset: truncated ? offset + collected.length : undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=list.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Repository operations family (0.2.5 plan 025 Task 1 split).
|
|
2
|
+
* Moved verbatim from repository.ts; public surface unchanged behind the barrel. */
|
|
3
|
+
import type { RepositoryLimitOptions, RepositoryOperations } from "./types.js";
|
|
4
|
+
import type { RepositoryWalk } from "./walk.js";
|
|
5
|
+
export declare function createLocalRepositoryOperations(limits?: RepositoryLimitOptions, walk?: RepositoryWalk): RepositoryOperations;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolveRepositoryLimits } from "./types.js";
|
|
2
|
+
import { walkRepository } from "./walk.js";
|
|
3
|
+
import { globLocal } from "./glob.js";
|
|
4
|
+
import { listLocal } from "./list.js";
|
|
5
|
+
import { searchLocal } from "./search.js";
|
|
6
|
+
export function createLocalRepositoryOperations(limits, walk = walkRepository) {
|
|
7
|
+
const resolved = resolveRepositoryLimits(limits);
|
|
8
|
+
return {
|
|
9
|
+
list: (request) => listLocal(request, resolved, walk),
|
|
10
|
+
search: (request) => searchLocal(request, resolved, walk),
|
|
11
|
+
glob: (request) => globLocal(request, resolved, walk),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=operations.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import type { RepoEntryKind } from "./types.js";
|
|
3
|
+
export declare function toRepoRelative(root: string, absolutePath: string): string;
|
|
4
|
+
export declare function isPathInsideRoot(root: string, target: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a list/search start path under the workspace root.
|
|
7
|
+
* Symlink escapes fail closed after realpath when the path exists.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveRepoPath(root: string, inputPath: string | undefined): Promise<{
|
|
10
|
+
absolute: string;
|
|
11
|
+
relative: string;
|
|
12
|
+
rootReal: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function shouldSkipName(name: string, includeHidden: boolean, exclude: ReadonlySet<string>): boolean;
|
|
15
|
+
export declare function kindFromDirent(dirent: Dirent): RepoEntryKind;
|
|
16
|
+
export declare function assertNotAborted(signal: AbortSignal | undefined): void;
|
|
17
|
+
export declare function assertDeadline(deadlineAt: number | undefined): void;
|
|
18
|
+
export declare function isBinaryBuffer(buffer: Buffer): boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Repository path family (0.2.5 plan 025 Task 1 split).
|
|
2
|
+
* Moved verbatim from repository.ts; public surface unchanged behind the barrel. */
|
|
3
|
+
import { DEFAULT_BINARY_SNIFF_BYTES } from "../limits.js";
|
|
4
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { realpath } from "node:fs/promises";
|
|
6
|
+
import { resolveToCwd } from "../path-utils.js";
|
|
7
|
+
import { RepositoryError } from "./types.js";
|
|
8
|
+
export function toRepoRelative(root, absolutePath) {
|
|
9
|
+
const rel = relative(root, absolutePath);
|
|
10
|
+
if (rel === "")
|
|
11
|
+
return ".";
|
|
12
|
+
return rel.split(sep).join("/");
|
|
13
|
+
}
|
|
14
|
+
export function isPathInsideRoot(root, target) {
|
|
15
|
+
const from = resolve(root);
|
|
16
|
+
const to = resolve(target);
|
|
17
|
+
if (to === from)
|
|
18
|
+
return true;
|
|
19
|
+
const rel = relative(from, to);
|
|
20
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a list/search start path under the workspace root.
|
|
24
|
+
* Symlink escapes fail closed after realpath when the path exists.
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveRepoPath(root, inputPath) {
|
|
27
|
+
const rootResolved = resolve(root);
|
|
28
|
+
let rootReal;
|
|
29
|
+
try {
|
|
30
|
+
rootReal = await realpath(rootResolved);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new RepositoryError(`workspace root is missing or unreadable: ${rootResolved}`);
|
|
34
|
+
}
|
|
35
|
+
if (!inputPath || inputPath === "." || inputPath === "./") {
|
|
36
|
+
return { absolute: rootReal, relative: ".", rootReal };
|
|
37
|
+
}
|
|
38
|
+
const candidate = resolveToCwd(inputPath, rootReal);
|
|
39
|
+
if (!isPathInsideRoot(rootReal, candidate)) {
|
|
40
|
+
throw new RepositoryError(`path escapes workspace root: ${inputPath}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const real = await realpath(candidate);
|
|
44
|
+
if (!isPathInsideRoot(rootReal, real)) {
|
|
45
|
+
throw new RepositoryError(`path resolves outside workspace root: ${inputPath}`);
|
|
46
|
+
}
|
|
47
|
+
return { absolute: real, relative: toRepoRelative(rootReal, real), rootReal };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof RepositoryError)
|
|
51
|
+
throw error;
|
|
52
|
+
// ENOENT: allow listing a missing path to fail later with a clear error.
|
|
53
|
+
return { absolute: candidate, relative: toRepoRelative(rootReal, candidate), rootReal };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function shouldSkipName(name, includeHidden, exclude) {
|
|
57
|
+
if (name === "." || name === "..")
|
|
58
|
+
return true;
|
|
59
|
+
if (exclude.has(name))
|
|
60
|
+
return true;
|
|
61
|
+
if (!includeHidden && name.startsWith("."))
|
|
62
|
+
return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
export function kindFromDirent(dirent) {
|
|
66
|
+
if (dirent.isSymbolicLink())
|
|
67
|
+
return "symlink";
|
|
68
|
+
if (dirent.isDirectory())
|
|
69
|
+
return "directory";
|
|
70
|
+
if (dirent.isFile())
|
|
71
|
+
return "file";
|
|
72
|
+
return "other";
|
|
73
|
+
}
|
|
74
|
+
export function assertNotAborted(signal) {
|
|
75
|
+
if (signal?.aborted)
|
|
76
|
+
throw new RepositoryError("Operation aborted");
|
|
77
|
+
}
|
|
78
|
+
export function assertDeadline(deadlineAt) {
|
|
79
|
+
if (deadlineAt !== undefined && Date.now() >= deadlineAt) {
|
|
80
|
+
throw new RepositoryError("Repository operation exceeded time limit");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function isBinaryBuffer(buffer) {
|
|
84
|
+
const length = Math.min(buffer.length, DEFAULT_BINARY_SNIFF_BYTES);
|
|
85
|
+
for (let i = 0; i < length; i++) {
|
|
86
|
+
if (buffer[i] === 0)
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=path.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RepositorySearchRequest, RepositorySearchResult, ResolvedRepositoryLimits } from "./types.js";
|
|
2
|
+
import type { RepositoryWalk } from "./walk.js";
|
|
3
|
+
export declare function compileSearchPattern(query: string, caseSensitive: boolean, maxPatternBytes: number): {
|
|
4
|
+
testLine: (line: string) => {
|
|
5
|
+
column: number;
|
|
6
|
+
} | null;
|
|
7
|
+
patternBytes: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function searchLocal(request: RepositorySearchRequest, defaults: ResolvedRepositoryLimits, walk: RepositoryWalk): Promise<RepositorySearchResult>;
|