@narumitw/pi-files 0.0.0-stage → 0.1.0

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 (64) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -2
  4. package/dist/chunks/chunk-3GQ7VS22.ts +273 -0
  5. package/dist/chunks/chunk-3GQ7VS22.ts.map +7 -0
  6. package/dist/chunks/chunk-AXW73HDJ.ts +31 -0
  7. package/dist/chunks/chunk-AXW73HDJ.ts.map +7 -0
  8. package/dist/chunks/chunk-F5QFC255.ts +101 -0
  9. package/dist/chunks/chunk-F5QFC255.ts.map +7 -0
  10. package/dist/chunks/chunk-NFNGJETD.ts +77 -0
  11. package/dist/chunks/chunk-NFNGJETD.ts.map +7 -0
  12. package/dist/chunks/chunk-OPGTWNJI.ts +258 -0
  13. package/dist/chunks/chunk-OPGTWNJI.ts.map +7 -0
  14. package/dist/chunks/chunk-WOTDNRTG.ts +242 -0
  15. package/dist/chunks/chunk-WOTDNRTG.ts.map +7 -0
  16. package/dist/chunks/file-context-explorer-WR6GQZHF.ts +1793 -0
  17. package/dist/chunks/file-context-explorer-WR6GQZHF.ts.map +7 -0
  18. package/dist/chunks/file-context-menu-VPLHUUQ6.ts +302 -0
  19. package/dist/chunks/file-context-menu-VPLHUUQ6.ts.map +7 -0
  20. package/dist/chunks/files-menu-MES4MQ4F.ts +119 -0
  21. package/dist/chunks/files-menu-MES4MQ4F.ts.map +7 -0
  22. package/dist/chunks/files-settings-NYXQU2HV.ts +295 -0
  23. package/dist/chunks/files-settings-NYXQU2HV.ts.map +7 -0
  24. package/dist/chunks/menu-P6IBMROH.ts +876 -0
  25. package/dist/chunks/menu-P6IBMROH.ts.map +7 -0
  26. package/dist/index.ts +1789 -0
  27. package/dist/index.ts.map +7 -0
  28. package/docs/edit-scope-reference.md +142 -0
  29. package/docs/file-context-reference.md +91 -0
  30. package/docs/migration.md +105 -0
  31. package/docs/verification.md +99 -0
  32. package/package.json +58 -4
  33. package/src/content-search-session.ts +204 -0
  34. package/src/content-search-ui.ts +159 -0
  35. package/src/content-search.ts +170 -0
  36. package/src/context-service.ts +515 -0
  37. package/src/external-editor.ts +143 -0
  38. package/src/file-browser-ui.ts +128 -0
  39. package/src/file-browser.ts +76 -0
  40. package/src/file-context-explorer.ts +992 -0
  41. package/src/file-context-menu.ts +360 -0
  42. package/src/file-context-preview-ui.ts +354 -0
  43. package/src/file-context-settings.ts +301 -0
  44. package/src/file-context.ts +321 -0
  45. package/src/file-search.ts +246 -0
  46. package/src/files-menu.ts +125 -0
  47. package/src/files-settings.ts +314 -0
  48. package/src/files-widget.ts +65 -0
  49. package/src/files.ts +148 -0
  50. package/src/git-context.ts +485 -0
  51. package/src/index.ts +1 -0
  52. package/src/scope/contracts.ts +243 -0
  53. package/src/scope/display.ts +27 -0
  54. package/src/scope/edit-scope.ts +435 -0
  55. package/src/scope/files.ts +118 -0
  56. package/src/scope/input.ts +207 -0
  57. package/src/scope/menu.ts +277 -0
  58. package/src/scope/paths.ts +142 -0
  59. package/src/scope/picker-keys.ts +154 -0
  60. package/src/scope/picker.ts +484 -0
  61. package/src/scope/policy.ts +55 -0
  62. package/src/scope/settings.ts +138 -0
  63. package/src/scope/state.ts +91 -0
  64. package/src/scope/ui.ts +106 -0
@@ -0,0 +1,77 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+
4
+ // src/scope/state.ts
5
+ import { isAbsolute, resolve } from "node:path";
6
+ var STATE_TYPE = "scope-state";
7
+ var MAX_TARGETS = 100;
8
+ var MAX_STATE_BYTES = 24e3;
9
+ function absolutePath(value) {
10
+ return typeof value === "string" && !value.includes("\0") && isAbsolute(value) && resolve(value) === value;
11
+ }
12
+ function createSnapshot(cwd, targets) {
13
+ if (!absolutePath(cwd)) throw new Error("Scope workspace must be an absolute normalized path");
14
+ if (targets !== null && (targets.length === 0 || targets.length > MAX_TARGETS)) {
15
+ throw new Error(`Select between 1 and ${MAX_TARGETS} files; use /scope off to end the restriction`);
16
+ }
17
+ const unique = /* @__PURE__ */ new Map();
18
+ for (const target of targets ?? []) {
19
+ if (!absolutePath(target?.path) || !absolutePath(target?.canonical)) {
20
+ throw new Error("Scope targets must contain absolute normalized paths");
21
+ }
22
+ if (!unique.has(target.canonical)) unique.set(target.canonical, { path: target.path, canonical: target.canonical });
23
+ }
24
+ const snapshot = {
25
+ version: 1,
26
+ cwd,
27
+ targets: targets === null ? null : [...unique.values()].sort((a, b) => a.canonical < b.canonical ? -1 : a.canonical > b.canonical ? 1 : 0)
28
+ };
29
+ if (Buffer.byteLength(JSON.stringify(snapshot)) > MAX_STATE_BYTES) {
30
+ throw new Error(`Scope selection exceeds the ${MAX_STATE_BYTES}-byte notice limit; select fewer files`);
31
+ }
32
+ return snapshot;
33
+ }
34
+ function parseSnapshot(value) {
35
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Invalid scope snapshot");
36
+ const data = value;
37
+ if (data.version !== 1 || typeof data.cwd !== "string" || data.targets !== null && !Array.isArray(data.targets)) {
38
+ throw new Error("Invalid or unsupported scope snapshot");
39
+ }
40
+ return createSnapshot(data.cwd, data.targets);
41
+ }
42
+ function restoreScope(branch, cwd) {
43
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
44
+ const entry = branch[index];
45
+ if (entry.type !== "custom" || entry.customType !== STATE_TYPE) continue;
46
+ try {
47
+ const snapshot = parseSnapshot(entry.data);
48
+ if (snapshot.cwd !== resolve(cwd)) throw new Error("Saved scope belongs to another workspace");
49
+ return { state: snapshot.targets === null ? "inactive" : "active", snapshot, entryId: entry.id };
50
+ } catch (error) {
51
+ return {
52
+ state: "blocked",
53
+ entryId: entry.id,
54
+ error: `${error instanceof Error ? error.message : "Scope restoration failed"}. Edits remain blocked; repair with /scope or exit with /scope off.`
55
+ };
56
+ }
57
+ }
58
+ return { state: "inactive" };
59
+ }
60
+ function sameScope(left, right) {
61
+ if (!left || left.cwd !== right.cwd || left.targets === null !== (right.targets === null)) return false;
62
+ if (left.targets === null || right.targets === null) return left.targets === right.targets;
63
+ return left.targets.length === right.targets.length && left.targets.every((target, index) => {
64
+ const other = right.targets?.[index];
65
+ return target.canonical === other?.canonical && target.path === other.path;
66
+ });
67
+ }
68
+
69
+ export {
70
+ STATE_TYPE,
71
+ MAX_TARGETS,
72
+ createSnapshot,
73
+ parseSnapshot,
74
+ restoreScope,
75
+ sameScope
76
+ };
77
+ //# sourceMappingURL=chunk-NFNGJETD.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/scope/state.ts"],
4
+ "sourcesContent": ["import { isAbsolute, resolve } from \"node:path\";\nimport type { SessionEntry } from \"@earendil-works/pi-coding-agent\";\nimport type { ScopeTarget } from \"./paths.js\";\n\n// Persisted session identity must survive package renames or active guards would be lost.\nexport const STATE_TYPE = \"scope-state\";\nexport const MAX_TARGETS = 100;\nexport const MAX_STATE_BYTES = 24_000;\n\nexport interface ScopeSnapshot {\n version: 1;\n cwd: string;\n targets: ScopeTarget[] | null;\n}\n\nexport interface ScopeState {\n state: \"inactive\" | \"active\" | \"blocked\";\n entryId?: string;\n snapshot?: ScopeSnapshot;\n error?: string;\n}\n\nfunction absolutePath(value: unknown): value is string {\n return typeof value === \"string\" && !value.includes(\"\\0\") && isAbsolute(value) && resolve(value) === value;\n}\n\nexport function createSnapshot(cwd: string, targets: readonly ScopeTarget[] | null): ScopeSnapshot {\n if (!absolutePath(cwd)) throw new Error(\"Scope workspace must be an absolute normalized path\");\n if (targets !== null && (targets.length === 0 || targets.length > MAX_TARGETS)) {\n throw new Error(`Select between 1 and ${MAX_TARGETS} files; use /scope off to end the restriction`);\n }\n const unique = new Map<string, ScopeTarget>();\n for (const target of targets ?? []) {\n if (!absolutePath(target?.path) || !absolutePath(target?.canonical)) {\n throw new Error(\"Scope targets must contain absolute normalized paths\");\n }\n if (!unique.has(target.canonical)) unique.set(target.canonical, { path: target.path, canonical: target.canonical });\n }\n const snapshot: ScopeSnapshot = {\n version: 1,\n cwd,\n targets:\n targets === null\n ? null\n : [...unique.values()].sort((a, b) => (a.canonical < b.canonical ? -1 : a.canonical > b.canonical ? 1 : 0)),\n };\n if (Buffer.byteLength(JSON.stringify(snapshot)) > MAX_STATE_BYTES) {\n throw new Error(`Scope selection exceeds the ${MAX_STATE_BYTES}-byte notice limit; select fewer files`);\n }\n return snapshot;\n}\n\nexport function parseSnapshot(value: unknown): ScopeSnapshot {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new Error(\"Invalid scope snapshot\");\n const data = value as Record<string, unknown>;\n if (data.version !== 1 || typeof data.cwd !== \"string\" || (data.targets !== null && !Array.isArray(data.targets))) {\n throw new Error(\"Invalid or unsupported scope snapshot\");\n }\n return createSnapshot(data.cwd, data.targets as ScopeTarget[] | null);\n}\n\nexport function restoreScope(branch: readonly SessionEntry[], cwd: string): ScopeState {\n for (let index = branch.length - 1; index >= 0; index -= 1) {\n const entry = branch[index];\n if (entry.type !== \"custom\" || entry.customType !== STATE_TYPE) continue;\n try {\n const snapshot = parseSnapshot(entry.data);\n if (snapshot.cwd !== resolve(cwd)) throw new Error(\"Saved scope belongs to another workspace\");\n return { state: snapshot.targets === null ? \"inactive\" : \"active\", snapshot, entryId: entry.id };\n } catch (error) {\n return {\n state: \"blocked\",\n entryId: entry.id,\n error: `${error instanceof Error ? error.message : \"Scope restoration failed\"}. Edits remain blocked; repair with /scope or exit with /scope off.`,\n };\n }\n }\n return { state: \"inactive\" };\n}\n\nexport function sameScope(left: ScopeSnapshot | undefined, right: ScopeSnapshot): boolean {\n if (!left || left.cwd !== right.cwd || (left.targets === null) !== (right.targets === null)) return false;\n if (left.targets === null || right.targets === null) return left.targets === right.targets;\n return (\n left.targets.length === right.targets.length &&\n left.targets.every((target, index) => {\n const other = right.targets?.[index];\n return target.canonical === other?.canonical && target.path === other.path;\n })\n );\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,258 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+
4
+ // src/file-context-settings.ts
5
+ import { randomUUID } from "node:crypto";
6
+ import { constants } from "node:fs";
7
+ import { mkdir, open, rename, rm, writeFile } from "node:fs/promises";
8
+ import { basename, dirname, join } from "node:path";
9
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
10
+ var FILE_CONTEXT_SETTINGS_FILE = "pi-file-context.json";
11
+ var MAX_SETTINGS_BYTES = 64 * 1024;
12
+ var DEFAULT_FILE_CONTEXT_SETTINGS = {
13
+ openShortcut: "ctrl+shift+x"
14
+ };
15
+ var MODIFIERS = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "super"]);
16
+ var BASE_KEYS = /* @__PURE__ */ new Set([
17
+ ..."abcdefghijklmnopqrstuvwxyz0123456789",
18
+ "`",
19
+ "-",
20
+ "=",
21
+ "[",
22
+ "]",
23
+ "\\",
24
+ ";",
25
+ "'",
26
+ ",",
27
+ ".",
28
+ "/",
29
+ "!",
30
+ "@",
31
+ "#",
32
+ "$",
33
+ "%",
34
+ "^",
35
+ "&",
36
+ "*",
37
+ "(",
38
+ ")",
39
+ "_",
40
+ "+",
41
+ "|",
42
+ "~",
43
+ "{",
44
+ "}",
45
+ ":",
46
+ "<",
47
+ ">",
48
+ "?",
49
+ "escape",
50
+ "esc",
51
+ "enter",
52
+ "return",
53
+ "tab",
54
+ "space",
55
+ "backspace",
56
+ "delete",
57
+ "insert",
58
+ "clear",
59
+ "home",
60
+ "end",
61
+ "pageup",
62
+ "pagedown",
63
+ "up",
64
+ "down",
65
+ "left",
66
+ "right",
67
+ ...Array.from({ length: 12 }, (_unused, index) => `f${index + 1}`)
68
+ ]);
69
+ var mutationQueues = /* @__PURE__ */ new Map();
70
+ function fileContextSettingsPath() {
71
+ return join(getAgentDir(), FILE_CONTEXT_SETTINGS_FILE);
72
+ }
73
+ async function loadFileContextSettings(settingsPath = fileContextSettingsPath()) {
74
+ await awaitFileContextSettingsWrites(settingsPath);
75
+ const snapshot = await readSettingsSnapshot(settingsPath);
76
+ if (snapshot.kind === "loaded") return { settings: snapshot.settings };
77
+ if (snapshot.kind === "missing") {
78
+ return { settings: { ...DEFAULT_FILE_CONTEXT_SETTINGS } };
79
+ }
80
+ return {
81
+ settings: { ...DEFAULT_FILE_CONTEXT_SETTINGS },
82
+ warning: snapshot.warning,
83
+ invalidReason: snapshot.reason
84
+ };
85
+ }
86
+ function updateFileContextSettings(openShortcut, options = {}) {
87
+ const settingsPath = options.settingsPath ?? fileContextSettingsPath();
88
+ return enqueueMutation(settingsPath, async () => {
89
+ options.signal?.throwIfAborted();
90
+ const snapshot = await readSettingsSnapshot(settingsPath);
91
+ if (snapshot.kind === "invalid") {
92
+ throw new Error(`File Context settings are invalid: ${snapshot.reason}`);
93
+ }
94
+ const updated = {
95
+ ...snapshot.kind === "loaded" ? snapshot.document : {},
96
+ openShortcut
97
+ };
98
+ const settings = normalizeFileContextSettings(updated);
99
+ if (!settings) throw new Error("File Context settings update is invalid");
100
+ await publishSettings(settingsPath, updated, options);
101
+ return settings;
102
+ });
103
+ }
104
+ async function awaitFileContextSettingsWrites(settingsPath = fileContextSettingsPath()) {
105
+ await mutationQueues.get(settingsPath);
106
+ }
107
+ function normalizeKeyId(value) {
108
+ if (typeof value !== "string") return void 0;
109
+ const normalized = value.trim().toLowerCase();
110
+ const base = [...BASE_KEYS].sort((left, right) => right.length - left.length).find((candidate) => normalized === candidate || normalized.endsWith(`+${candidate}`));
111
+ if (!base) return void 0;
112
+ const prefix = normalized.slice(0, normalized.length - base.length);
113
+ if (!prefix) return base;
114
+ if (/^f(?:[1-9]|1[0-2])$/.test(base) || !prefix.endsWith("+")) return void 0;
115
+ const modifiers = prefix.slice(0, -1).split("+");
116
+ if (modifiers.length === 0 || modifiers.some((modifier) => !MODIFIERS.has(modifier)) || new Set(modifiers).size !== modifiers.length) {
117
+ return void 0;
118
+ }
119
+ return normalized;
120
+ }
121
+ function normalizeFileContextSettings(value) {
122
+ if (!isSettingsDocument(value)) return void 0;
123
+ if (!Object.hasOwn(value, "openShortcut")) {
124
+ return { ...DEFAULT_FILE_CONTEXT_SETTINGS };
125
+ }
126
+ const openShortcut = Reflect.get(value, "openShortcut");
127
+ if (openShortcut === null) return { openShortcut: null };
128
+ const normalized = normalizeKeyId(openShortcut);
129
+ return normalized ? { openShortcut: normalized } : void 0;
130
+ }
131
+ function enqueueMutation(settingsPath, mutation) {
132
+ const previous = mutationQueues.get(settingsPath) ?? Promise.resolve();
133
+ const result = previous.then(mutation, mutation);
134
+ const settled = result.then(
135
+ () => void 0,
136
+ () => void 0
137
+ );
138
+ mutationQueues.set(settingsPath, settled);
139
+ void settled.finally(() => {
140
+ if (mutationQueues.get(settingsPath) === settled) mutationQueues.delete(settingsPath);
141
+ });
142
+ return result;
143
+ }
144
+ async function readSettingsSnapshot(settingsPath) {
145
+ let source;
146
+ try {
147
+ source = await readSettingsContents(settingsPath);
148
+ } catch (error) {
149
+ if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
150
+ const reason = safeReadError(error);
151
+ return {
152
+ kind: "invalid",
153
+ reason,
154
+ warning: `Cannot read File Context settings: ${reason}`
155
+ };
156
+ }
157
+ let document;
158
+ try {
159
+ document = JSON.parse(source);
160
+ } catch (error) {
161
+ return {
162
+ kind: "invalid",
163
+ reason: "invalid JSON",
164
+ warning: `Cannot parse File Context settings: ${formatError(error)}`
165
+ };
166
+ }
167
+ if (!isSettingsDocument(document)) {
168
+ return {
169
+ kind: "invalid",
170
+ reason: "settings must contain a JSON object",
171
+ warning: "File Context settings must contain a JSON object."
172
+ };
173
+ }
174
+ const settings = normalizeFileContextSettings(document);
175
+ if (!settings) {
176
+ return {
177
+ kind: "invalid",
178
+ reason: 'setting "openShortcut" must be a valid Pi key string or null',
179
+ warning: 'File Context setting "openShortcut" must be a valid Pi key string or null.'
180
+ };
181
+ }
182
+ return { kind: "loaded", document, settings };
183
+ }
184
+ async function readSettingsContents(settingsPath) {
185
+ const flags = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0);
186
+ const handle = await open(settingsPath, flags);
187
+ try {
188
+ const stats = await handle.stat();
189
+ if (!stats.isFile()) throw new Error("settings path is not a regular file");
190
+ if (stats.size > MAX_SETTINGS_BYTES) {
191
+ throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
192
+ }
193
+ const buffer = Buffer.alloc(MAX_SETTINGS_BYTES + 1);
194
+ let offset = 0;
195
+ while (offset < buffer.byteLength) {
196
+ const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
197
+ if (bytesRead === 0) break;
198
+ offset += bytesRead;
199
+ }
200
+ if (offset > MAX_SETTINGS_BYTES) {
201
+ throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
202
+ }
203
+ try {
204
+ return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, offset));
205
+ } catch {
206
+ throw new Error("settings file is not valid UTF-8");
207
+ }
208
+ } finally {
209
+ await handle.close();
210
+ }
211
+ }
212
+ async function publishSettings(settingsPath, document, options) {
213
+ options.signal?.throwIfAborted();
214
+ const contents = `${JSON.stringify(document, null, 2)}
215
+ `;
216
+ if (Buffer.byteLength(contents, "utf8") > MAX_SETTINGS_BYTES) {
217
+ throw new Error(`settings document exceeds ${MAX_SETTINGS_BYTES} bytes`);
218
+ }
219
+ const directory = dirname(settingsPath);
220
+ await mkdir(directory, { recursive: true });
221
+ options.signal?.throwIfAborted();
222
+ const temporaryPath = join(directory, `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`);
223
+ try {
224
+ await writeFile(temporaryPath, contents, {
225
+ encoding: "utf8",
226
+ flag: "wx",
227
+ mode: 384,
228
+ signal: options.signal
229
+ });
230
+ await options.beforeRename?.(temporaryPath, settingsPath);
231
+ options.signal?.throwIfAborted();
232
+ await rename(temporaryPath, settingsPath);
233
+ } finally {
234
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
235
+ }
236
+ }
237
+ function isSettingsDocument(value) {
238
+ return typeof value === "object" && value !== null && !Array.isArray(value);
239
+ }
240
+ function isNodeError(error) {
241
+ return error instanceof Error && "code" in error;
242
+ }
243
+ function safeReadError(error) {
244
+ if (isNodeError(error) && error.code === "ELOOP") return "settings path is not a regular file";
245
+ return formatError(error);
246
+ }
247
+ function formatError(error) {
248
+ return error instanceof Error ? error.message : String(error);
249
+ }
250
+
251
+ export {
252
+ fileContextSettingsPath,
253
+ loadFileContextSettings,
254
+ updateFileContextSettings,
255
+ awaitFileContextSettingsWrites,
256
+ normalizeKeyId
257
+ };
258
+ //# sourceMappingURL=chunk-OPGTWNJI.ts.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/file-context-settings.ts"],
4
+ "sourcesContent": ["import { randomUUID } from \"node:crypto\";\nimport { constants } from \"node:fs\";\nimport { mkdir, open, rename, rm, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join } from \"node:path\";\nimport { getAgentDir } from \"@earendil-works/pi-coding-agent\";\nimport type { KeyId } from \"@earendil-works/pi-tui\";\n\nexport const FILE_CONTEXT_SETTINGS_FILE = \"pi-file-context.json\";\nconst MAX_SETTINGS_BYTES = 64 * 1024;\n\nexport interface FileContextSettings {\n openShortcut: KeyId | null;\n}\n\nexport interface LoadedFileContextSettings {\n settings: FileContextSettings;\n warning?: string;\n invalidReason?: string;\n}\n\nexport interface UpdateFileContextSettingsOptions {\n settingsPath?: string;\n signal?: AbortSignal;\n beforeRename?: (temporaryPath: string, settingsPath: string) => Promise<void>;\n}\n\nexport const DEFAULT_FILE_CONTEXT_SETTINGS: FileContextSettings = {\n openShortcut: \"ctrl+shift+x\",\n};\n\nconst MODIFIERS = new Set([\"ctrl\", \"shift\", \"alt\", \"super\"]);\nconst BASE_KEYS = new Set([\n ...\"abcdefghijklmnopqrstuvwxyz0123456789\",\n \"`\",\n \"-\",\n \"=\",\n \"[\",\n \"]\",\n \"\\\\\",\n \";\",\n \"'\",\n \",\",\n \".\",\n \"/\",\n \"!\",\n \"@\",\n \"#\",\n \"$\",\n \"%\",\n \"^\",\n \"&\",\n \"*\",\n \"(\",\n \")\",\n \"_\",\n \"+\",\n \"|\",\n \"~\",\n \"{\",\n \"}\",\n \":\",\n \"<\",\n \">\",\n \"?\",\n \"escape\",\n \"esc\",\n \"enter\",\n \"return\",\n \"tab\",\n \"space\",\n \"backspace\",\n \"delete\",\n \"insert\",\n \"clear\",\n \"home\",\n \"end\",\n \"pageup\",\n \"pagedown\",\n \"up\",\n \"down\",\n \"left\",\n \"right\",\n ...Array.from({ length: 12 }, (_unused, index) => `f${index + 1}`),\n]);\n\ntype SettingsDocument = Record<string, unknown>;\ntype SettingsSnapshot =\n | { kind: \"missing\" }\n | { kind: \"invalid\"; reason: string; warning: string }\n | { kind: \"loaded\"; document: SettingsDocument; settings: FileContextSettings };\n\nconst mutationQueues = new Map<string, Promise<void>>();\n\nexport function fileContextSettingsPath(): string {\n return join(getAgentDir(), FILE_CONTEXT_SETTINGS_FILE);\n}\n\nexport async function loadFileContextSettings(\n settingsPath = fileContextSettingsPath(),\n): Promise<LoadedFileContextSettings> {\n await awaitFileContextSettingsWrites(settingsPath);\n const snapshot = await readSettingsSnapshot(settingsPath);\n if (snapshot.kind === \"loaded\") return { settings: snapshot.settings };\n if (snapshot.kind === \"missing\") {\n return { settings: { ...DEFAULT_FILE_CONTEXT_SETTINGS } };\n }\n return {\n settings: { ...DEFAULT_FILE_CONTEXT_SETTINGS },\n warning: snapshot.warning,\n invalidReason: snapshot.reason,\n };\n}\n\nexport function updateFileContextSettings(\n openShortcut: KeyId | null,\n options: UpdateFileContextSettingsOptions = {},\n): Promise<FileContextSettings> {\n const settingsPath = options.settingsPath ?? fileContextSettingsPath();\n return enqueueMutation(settingsPath, async () => {\n options.signal?.throwIfAborted();\n const snapshot = await readSettingsSnapshot(settingsPath);\n if (snapshot.kind === \"invalid\") {\n throw new Error(`File Context settings are invalid: ${snapshot.reason}`);\n }\n const updated: SettingsDocument = {\n ...(snapshot.kind === \"loaded\" ? snapshot.document : {}),\n openShortcut,\n };\n const settings = normalizeFileContextSettings(updated);\n if (!settings) throw new Error(\"File Context settings update is invalid\");\n await publishSettings(settingsPath, updated, options);\n return settings;\n });\n}\n\nexport async function awaitFileContextSettingsWrites(settingsPath = fileContextSettingsPath()): Promise<void> {\n await mutationQueues.get(settingsPath);\n}\n\nexport function normalizeKeyId(value: unknown): KeyId | undefined {\n if (typeof value !== \"string\") return undefined;\n const normalized = value.trim().toLowerCase();\n const base = [...BASE_KEYS]\n .sort((left, right) => right.length - left.length)\n .find((candidate) => normalized === candidate || normalized.endsWith(`+${candidate}`));\n if (!base) return undefined;\n const prefix = normalized.slice(0, normalized.length - base.length);\n if (!prefix) return base as KeyId;\n if (/^f(?:[1-9]|1[0-2])$/.test(base) || !prefix.endsWith(\"+\")) return undefined;\n const modifiers = prefix.slice(0, -1).split(\"+\");\n if (\n modifiers.length === 0 ||\n modifiers.some((modifier) => !MODIFIERS.has(modifier)) ||\n new Set(modifiers).size !== modifiers.length\n ) {\n return undefined;\n }\n return normalized as KeyId;\n}\n\nfunction normalizeFileContextSettings(value: unknown): FileContextSettings | undefined {\n if (!isSettingsDocument(value)) return undefined;\n if (!Object.hasOwn(value, \"openShortcut\")) {\n return { ...DEFAULT_FILE_CONTEXT_SETTINGS };\n }\n const openShortcut = Reflect.get(value, \"openShortcut\");\n if (openShortcut === null) return { openShortcut: null };\n const normalized = normalizeKeyId(openShortcut);\n return normalized ? { openShortcut: normalized } : undefined;\n}\n\nfunction enqueueMutation<T>(settingsPath: string, mutation: () => Promise<T>): Promise<T> {\n const previous = mutationQueues.get(settingsPath) ?? Promise.resolve();\n const result = previous.then(mutation, mutation);\n const settled = result.then(\n () => undefined,\n () => undefined,\n );\n mutationQueues.set(settingsPath, settled);\n void settled.finally(() => {\n if (mutationQueues.get(settingsPath) === settled) mutationQueues.delete(settingsPath);\n });\n return result;\n}\n\nasync function readSettingsSnapshot(settingsPath: string): Promise<SettingsSnapshot> {\n let source: string;\n try {\n source = await readSettingsContents(settingsPath);\n } catch (error: unknown) {\n if (isNodeError(error) && error.code === \"ENOENT\") return { kind: \"missing\" };\n const reason = safeReadError(error);\n return {\n kind: \"invalid\",\n reason,\n warning: `Cannot read File Context settings: ${reason}`,\n };\n }\n\n let document: unknown;\n try {\n document = JSON.parse(source) as unknown;\n } catch (error: unknown) {\n return {\n kind: \"invalid\",\n reason: \"invalid JSON\",\n warning: `Cannot parse File Context settings: ${formatError(error)}`,\n };\n }\n if (!isSettingsDocument(document)) {\n return {\n kind: \"invalid\",\n reason: \"settings must contain a JSON object\",\n warning: \"File Context settings must contain a JSON object.\",\n };\n }\n const settings = normalizeFileContextSettings(document);\n if (!settings) {\n return {\n kind: \"invalid\",\n reason: 'setting \"openShortcut\" must be a valid Pi key string or null',\n warning: 'File Context setting \"openShortcut\" must be a valid Pi key string or null.',\n };\n }\n return { kind: \"loaded\", document, settings };\n}\n\nasync function readSettingsContents(settingsPath: string): Promise<string> {\n const flags = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0);\n const handle = await open(settingsPath, flags);\n try {\n const stats = await handle.stat();\n if (!stats.isFile()) throw new Error(\"settings path is not a regular file\");\n if (stats.size > MAX_SETTINGS_BYTES) {\n throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);\n }\n const buffer = Buffer.alloc(MAX_SETTINGS_BYTES + 1);\n let offset = 0;\n while (offset < buffer.byteLength) {\n const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);\n if (bytesRead === 0) break;\n offset += bytesRead;\n }\n if (offset > MAX_SETTINGS_BYTES) {\n throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);\n }\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(buffer.subarray(0, offset));\n } catch {\n throw new Error(\"settings file is not valid UTF-8\");\n }\n } finally {\n await handle.close();\n }\n}\n\nasync function publishSettings(\n settingsPath: string,\n document: SettingsDocument,\n options: UpdateFileContextSettingsOptions,\n): Promise<void> {\n options.signal?.throwIfAborted();\n const contents = `${JSON.stringify(document, null, 2)}\\n`;\n if (Buffer.byteLength(contents, \"utf8\") > MAX_SETTINGS_BYTES) {\n throw new Error(`settings document exceeds ${MAX_SETTINGS_BYTES} bytes`);\n }\n const directory = dirname(settingsPath);\n await mkdir(directory, { recursive: true });\n options.signal?.throwIfAborted();\n const temporaryPath = join(directory, `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`);\n try {\n await writeFile(temporaryPath, contents, {\n encoding: \"utf8\",\n flag: \"wx\",\n mode: 0o600,\n signal: options.signal,\n });\n await options.beforeRename?.(temporaryPath, settingsPath);\n options.signal?.throwIfAborted();\n await rename(temporaryPath, settingsPath);\n } finally {\n await rm(temporaryPath, { force: true }).catch(() => undefined);\n }\n}\n\nfunction isSettingsDocument(value: unknown): value is SettingsDocument {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isNodeError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error;\n}\n\nfunction safeReadError(error: unknown): string {\n if (isNodeError(error) && error.code === \"ELOOP\") return \"settings path is not a regular file\";\n return formatError(error);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],
5
+ "mappings": ";;;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,OAAO,MAAM,QAAQ,IAAI,iBAAiB;AACnD,SAAS,UAAU,SAAS,YAAY;AACxC,SAAS,mBAAmB;AAGrB,IAAM,6BAA6B;AAC1C,IAAM,qBAAqB,KAAK;AAkBzB,IAAM,gCAAqD;AAAA,EAChE,cAAc;AAChB;AAEA,IAAM,YAAY,oBAAI,IAAI,CAAC,QAAQ,SAAS,OAAO,OAAO,CAAC;AAC3D,IAAM,YAAY,oBAAI,IAAI;AAAA,EACxB,GAAG;AAAA,EACH;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,CAAC,SAAS,UAAU,IAAI,QAAQ,CAAC,EAAE;AACnE,CAAC;AAQD,IAAM,iBAAiB,oBAAI,IAA2B;AAE/C,SAAS,0BAAkC;AAChD,SAAO,KAAK,YAAY,GAAG,0BAA0B;AACvD;AAEA,eAAsB,wBACpB,eAAe,wBAAwB,GACH;AACpC,QAAM,+BAA+B,YAAY;AACjD,QAAM,WAAW,MAAM,qBAAqB,YAAY;AACxD,MAAI,SAAS,SAAS,SAAU,QAAO,EAAE,UAAU,SAAS,SAAS;AACrE,MAAI,SAAS,SAAS,WAAW;AAC/B,WAAO,EAAE,UAAU,EAAE,GAAG,8BAA8B,EAAE;AAAA,EAC1D;AACA,SAAO;AAAA,IACL,UAAU,EAAE,GAAG,8BAA8B;AAAA,IAC7C,SAAS,SAAS;AAAA,IAClB,eAAe,SAAS;AAAA,EAC1B;AACF;AAEO,SAAS,0BACd,cACA,UAA4C,CAAC,GACf;AAC9B,QAAM,eAAe,QAAQ,gBAAgB,wBAAwB;AACrE,SAAO,gBAAgB,cAAc,YAAY;AAC/C,YAAQ,QAAQ,eAAe;AAC/B,UAAM,WAAW,MAAM,qBAAqB,YAAY;AACxD,QAAI,SAAS,SAAS,WAAW;AAC/B,YAAM,IAAI,MAAM,sCAAsC,SAAS,MAAM,EAAE;AAAA,IACzE;AACA,UAAM,UAA4B;AAAA,MAChC,GAAI,SAAS,SAAS,WAAW,SAAS,WAAW,CAAC;AAAA,MACtD;AAAA,IACF;AACA,UAAM,WAAW,6BAA6B,OAAO;AACrD,QAAI,CAAC,SAAU,OAAM,IAAI,MAAM,yCAAyC;AACxE,UAAM,gBAAgB,cAAc,SAAS,OAAO;AACpD,WAAO;AAAA,EACT,CAAC;AACH;AAEA,eAAsB,+BAA+B,eAAe,wBAAwB,GAAkB;AAC5G,QAAM,eAAe,IAAI,YAAY;AACvC;AAEO,SAAS,eAAe,OAAmC;AAChE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,QAAM,OAAO,CAAC,GAAG,SAAS,EACvB,KAAK,CAAC,MAAM,UAAU,MAAM,SAAS,KAAK,MAAM,EAChD,KAAK,CAAC,cAAc,eAAe,aAAa,WAAW,SAAS,IAAI,SAAS,EAAE,CAAC;AACvF,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,SAAS,WAAW,MAAM,GAAG,WAAW,SAAS,KAAK,MAAM;AAClE,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,sBAAsB,KAAK,IAAI,KAAK,CAAC,OAAO,SAAS,GAAG,EAAG,QAAO;AACtE,QAAM,YAAY,OAAO,MAAM,GAAG,EAAE,EAAE,MAAM,GAAG;AAC/C,MACE,UAAU,WAAW,KACrB,UAAU,KAAK,CAAC,aAAa,CAAC,UAAU,IAAI,QAAQ,CAAC,KACrD,IAAI,IAAI,SAAS,EAAE,SAAS,UAAU,QACtC;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,6BAA6B,OAAiD;AACrF,MAAI,CAAC,mBAAmB,KAAK,EAAG,QAAO;AACvC,MAAI,CAAC,OAAO,OAAO,OAAO,cAAc,GAAG;AACzC,WAAO,EAAE,GAAG,8BAA8B;AAAA,EAC5C;AACA,QAAM,eAAe,QAAQ,IAAI,OAAO,cAAc;AACtD,MAAI,iBAAiB,KAAM,QAAO,EAAE,cAAc,KAAK;AACvD,QAAM,aAAa,eAAe,YAAY;AAC9C,SAAO,aAAa,EAAE,cAAc,WAAW,IAAI;AACrD;AAEA,SAAS,gBAAmB,cAAsB,UAAwC;AACxF,QAAM,WAAW,eAAe,IAAI,YAAY,KAAK,QAAQ,QAAQ;AACrE,QAAM,SAAS,SAAS,KAAK,UAAU,QAAQ;AAC/C,QAAM,UAAU,OAAO;AAAA,IACrB,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AACA,iBAAe,IAAI,cAAc,OAAO;AACxC,OAAK,QAAQ,QAAQ,MAAM;AACzB,QAAI,eAAe,IAAI,YAAY,MAAM,QAAS,gBAAe,OAAO,YAAY;AAAA,EACtF,CAAC;AACD,SAAO;AACT;AAEA,eAAe,qBAAqB,cAAiD;AACnF,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,qBAAqB,YAAY;AAAA,EAClD,SAAS,OAAgB;AACvB,QAAI,YAAY,KAAK,KAAK,MAAM,SAAS,SAAU,QAAO,EAAE,MAAM,UAAU;AAC5E,UAAM,SAAS,cAAc,KAAK;AAClC,WAAO;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,SAAS,sCAAsC,MAAM;AAAA,IACvD;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,eAAW,KAAK,MAAM,MAAM;AAAA,EAC9B,SAAS,OAAgB;AACvB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS,uCAAuC,YAAY,KAAK,CAAC;AAAA,IACpE;AAAA,EACF;AACA,MAAI,CAAC,mBAAmB,QAAQ,GAAG;AACjC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS;AAAA,IACX;AAAA,EACF;AACA,QAAM,WAAW,6BAA6B,QAAQ;AACtD,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS;AAAA,IACX;AAAA,EACF;AACA,SAAO,EAAE,MAAM,UAAU,UAAU,SAAS;AAC9C;AAEA,eAAe,qBAAqB,cAAuC;AACzE,QAAM,QAAQ,UAAU,YAAY,UAAU,cAAc,MAAM,UAAU,cAAc;AAC1F,QAAM,SAAS,MAAM,KAAK,cAAc,KAAK;AAC7C,MAAI;AACF,UAAM,QAAQ,MAAM,OAAO,KAAK;AAChC,QAAI,CAAC,MAAM,OAAO,EAAG,OAAM,IAAI,MAAM,qCAAqC;AAC1E,QAAI,MAAM,OAAO,oBAAoB;AACnC,YAAM,IAAI,MAAM,yBAAyB,kBAAkB,QAAQ;AAAA,IACrE;AACA,UAAM,SAAS,OAAO,MAAM,qBAAqB,CAAC;AAClD,QAAI,SAAS;AACb,WAAO,SAAS,OAAO,YAAY;AACjC,YAAM,EAAE,UAAU,IAAI,MAAM,OAAO,KAAK,QAAQ,QAAQ,OAAO,aAAa,QAAQ,MAAM;AAC1F,UAAI,cAAc,EAAG;AACrB,gBAAU;AAAA,IACZ;AACA,QAAI,SAAS,oBAAoB;AAC/B,YAAM,IAAI,MAAM,yBAAyB,kBAAkB,QAAQ;AAAA,IACrE;AACA,QAAI;AACF,aAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAAO,OAAO,SAAS,GAAG,MAAM,CAAC;AAAA,IACpF,QAAQ;AACN,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAAA,EACF,UAAE;AACA,UAAM,OAAO,MAAM;AAAA,EACrB;AACF;AAEA,eAAe,gBACb,cACA,UACA,SACe;AACf,UAAQ,QAAQ,eAAe;AAC/B,QAAM,WAAW,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAAA;AACrD,MAAI,OAAO,WAAW,UAAU,MAAM,IAAI,oBAAoB;AAC5D,UAAM,IAAI,MAAM,6BAA6B,kBAAkB,QAAQ;AAAA,EACzE;AACA,QAAM,YAAY,QAAQ,YAAY;AACtC,QAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAC1C,UAAQ,QAAQ,eAAe;AAC/B,QAAM,gBAAgB,KAAK,WAAW,IAAI,SAAS,YAAY,CAAC,IAAI,QAAQ,GAAG,IAAI,WAAW,CAAC,MAAM;AACrG,MAAI;AACF,UAAM,UAAU,eAAe,UAAU;AAAA,MACvC,UAAU;AAAA,MACV,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ,QAAQ;AAAA,IAClB,CAAC;AACD,UAAM,QAAQ,eAAe,eAAe,YAAY;AACxD,YAAQ,QAAQ,eAAe;AAC/B,UAAM,OAAO,eAAe,YAAY;AAAA,EAC1C,UAAE;AACA,UAAM,GAAG,eAAe,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EAChE;AACF;AAEA,SAAS,mBAAmB,OAA2C;AACrE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,YAAY,OAAgD;AACnE,SAAO,iBAAiB,SAAS,UAAU;AAC7C;AAEA,SAAS,cAAc,OAAwB;AAC7C,MAAI,YAAY,KAAK,KAAK,MAAM,SAAS,QAAS,QAAO;AACzD,SAAO,YAAY,KAAK;AAC1B;AAEA,SAAS,YAAY,OAAwB;AAC3C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;",
6
+ "names": []
7
+ }
@@ -0,0 +1,242 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+
4
+ // src/file-context.ts
5
+ import { createHash } from "node:crypto";
6
+ import { constants } from "node:fs";
7
+ import { lstat, open, readdir, realpath } from "node:fs/promises";
8
+ import { isAbsolute, relative, resolve, sep } from "node:path";
9
+ var DEFAULT_MAX_FILES = 5e3;
10
+ var DEFAULT_MAX_BYTES = 1e6;
11
+ var MAX_QUOTE_BYTES = 5e4;
12
+ var MAX_QUOTE_LINES = 500;
13
+ var MAX_PENDING_QUOTES = 8;
14
+ var MAX_PENDING_QUOTE_BYTES = 1e5;
15
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
16
+ ".git",
17
+ ".hg",
18
+ ".svn",
19
+ ".next",
20
+ "build",
21
+ "coverage",
22
+ "dist",
23
+ "node_modules",
24
+ "target"
25
+ ]);
26
+ async function discoverProjectFiles(root, options = {}) {
27
+ const maxFiles = Math.max(1, options.maxFiles ?? DEFAULT_MAX_FILES);
28
+ const files = [];
29
+ options.signal?.throwIfAborted();
30
+ const canonicalRoot = await realpath(root);
31
+ const directories = [{ directory: canonicalRoot, prefix: "" }];
32
+ options.signal?.throwIfAborted();
33
+ for (let index = 0; index < directories.length && files.length < maxFiles; index += 1) {
34
+ options.signal?.throwIfAborted();
35
+ const current = directories[index];
36
+ if (!current) break;
37
+ const { directory, prefix } = current;
38
+ const entries = (await readdir(directory, { withFileTypes: true })).sort(
39
+ (left, right) => compareStrings(left.name, right.name)
40
+ );
41
+ options.signal?.throwIfAborted();
42
+ for (const entry of entries) {
43
+ options.signal?.throwIfAborted();
44
+ if (IGNORED_DIRECTORIES.has(entry.name) || entry.isSymbolicLink()) continue;
45
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
46
+ const absolutePath = resolve(directory, entry.name);
47
+ if (entry.isDirectory()) {
48
+ directories.push({ directory: absolutePath, prefix: relativePath });
49
+ } else if (entry.isFile()) {
50
+ files.push(relativePath);
51
+ if (files.length >= maxFiles) break;
52
+ }
53
+ }
54
+ }
55
+ options.signal?.throwIfAborted();
56
+ return files.sort(compareStrings);
57
+ }
58
+ async function loadProjectTextFile(root, projectPath, options = {}) {
59
+ options.signal?.throwIfAborted();
60
+ if (!projectPath || isAbsolute(projectPath)) throw new Error("File path is outside the project");
61
+ const canonicalRoot = await realpath(root);
62
+ options.signal?.throwIfAborted();
63
+ const candidate = resolve(canonicalRoot, projectPath);
64
+ if (!isInside(canonicalRoot, candidate)) throw new Error("File path is outside the project");
65
+ let canonicalFile;
66
+ try {
67
+ canonicalFile = await realpath(candidate);
68
+ } catch (error) {
69
+ throw new Error(`Cannot open ${projectPath}: ${formatError(error)}`);
70
+ }
71
+ options.signal?.throwIfAborted();
72
+ if (!isInside(canonicalRoot, canonicalFile)) throw new Error("File path is outside the project");
73
+ const info = await lstat(canonicalFile);
74
+ options.signal?.throwIfAborted();
75
+ if (!info.isFile()) throw new Error(`${projectPath} is not a regular file`);
76
+ const maxBytes = Math.max(1, options.maxBytes ?? DEFAULT_MAX_BYTES);
77
+ if (info.size > maxBytes) throw new Error(`${projectPath} exceeds ${maxBytes} bytes`);
78
+ await options.beforeOpen?.();
79
+ options.signal?.throwIfAborted();
80
+ let file;
81
+ try {
82
+ file = await open(canonicalFile, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
83
+ } catch (error) {
84
+ throw new Error(`Cannot safely open ${projectPath}: ${formatError(error)}`);
85
+ }
86
+ try {
87
+ options.signal?.throwIfAborted();
88
+ const openedInfo = await file.stat();
89
+ options.signal?.throwIfAborted();
90
+ if (!openedInfo.isFile()) throw new Error(`${projectPath} is not a regular file`);
91
+ if (openedInfo.dev !== info.dev || openedInfo.ino !== info.ino) {
92
+ throw new Error(`${projectPath} changed while it was being opened safely`);
93
+ }
94
+ if (openedInfo.size > maxBytes) throw new Error(`${projectPath} exceeds ${maxBytes} bytes`);
95
+ const buffer = Buffer.alloc(maxBytes + 1);
96
+ let offset = 0;
97
+ while (offset < buffer.length) {
98
+ options.signal?.throwIfAborted();
99
+ const { bytesRead } = await file.read(buffer, offset, buffer.length - offset, offset);
100
+ options.signal?.throwIfAborted();
101
+ if (bytesRead === 0) break;
102
+ offset += bytesRead;
103
+ }
104
+ if (offset > maxBytes) throw new Error(`${projectPath} exceeds ${maxBytes} bytes`);
105
+ const contents = buffer.subarray(0, offset);
106
+ if (contents.includes(0)) throw new Error(`${projectPath} appears to be binary`);
107
+ return {
108
+ path: projectPath.replaceAll("\\", "/"),
109
+ lines: normalizeTextLines(contents.toString("utf8"))
110
+ };
111
+ } finally {
112
+ await file.close();
113
+ }
114
+ }
115
+ function createFileQuote(path, lines, anchorIndex, cursorIndex, git) {
116
+ if (lines.length === 0) throw new Error("Cannot quote an empty file");
117
+ const startIndex = Math.max(0, Math.min(anchorIndex, cursorIndex, lines.length - 1));
118
+ const endIndex = Math.max(0, Math.min(Math.max(anchorIndex, cursorIndex), lines.length - 1));
119
+ const text = lines.slice(startIndex, endIndex + 1).join("\n");
120
+ return createFileQuoteSnapshot(path, startIndex + 1, endIndex + 1, text, git);
121
+ }
122
+ function createFileQuoteSnapshot(path, startLine, endLine, text, git) {
123
+ if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1) {
124
+ throw new Error("Quote lines must be positive integers");
125
+ }
126
+ if (endLine < startLine) throw new Error("Quote end line precedes its start line");
127
+ if (text.split("\n").length > MAX_QUOTE_LINES) {
128
+ throw new Error(`Quote exceeds ${MAX_QUOTE_LINES} lines`);
129
+ }
130
+ if (Buffer.byteLength(text, "utf8") > MAX_QUOTE_BYTES) {
131
+ throw new Error(`Quote exceeds ${MAX_QUOTE_BYTES} bytes`);
132
+ }
133
+ return {
134
+ path,
135
+ startLine,
136
+ endLine,
137
+ text,
138
+ ...git ? {
139
+ git: {
140
+ head: git.head,
141
+ ...git.branch !== void 0 ? { branch: git.branch } : {},
142
+ ...git.status !== void 0 ? { status: git.status } : {},
143
+ ...git.revision !== void 0 ? { revision: git.revision } : {},
144
+ ...git.blob !== void 0 ? { blob: git.blob } : {},
145
+ contentSha256: createHash("sha256").update(text, "utf8").digest("hex"),
146
+ ...git.source !== void 0 ? { source: git.source } : {},
147
+ ...git.base !== void 0 ? { base: git.base } : {}
148
+ }
149
+ } : {}
150
+ };
151
+ }
152
+ function createPreviewQuote(file, anchor, cursor, project, git, revision) {
153
+ return createFileQuote(
154
+ file.path,
155
+ file.lines,
156
+ anchor,
157
+ cursor,
158
+ project ? {
159
+ head: project.head,
160
+ branch: project.branch,
161
+ status: revision ? "historical" : git?.status?.label ?? "clean",
162
+ revision: revision?.revision,
163
+ blob: revision?.blob ?? git?.blob,
164
+ source: revision ? "revision" : "worktree",
165
+ base: revision ? void 0 : "HEAD"
166
+ } : void 0
167
+ );
168
+ }
169
+ function appendPendingQuote(current, quote) {
170
+ if (current.length >= MAX_PENDING_QUOTES) {
171
+ throw new Error(`File Context supports at most ${MAX_PENDING_QUOTES} pending quotes`);
172
+ }
173
+ const totalBytes = [...current, quote].reduce((total, item) => total + Buffer.byteLength(item.text, "utf8"), 0);
174
+ if (totalBytes > MAX_PENDING_QUOTE_BYTES) {
175
+ throw new Error(`Pending quotes exceed ${MAX_PENDING_QUOTE_BYTES} bytes`);
176
+ }
177
+ return [...current, quote];
178
+ }
179
+ function formatQuoteContext(quotes) {
180
+ const blocks = quotes.map((quote) => {
181
+ const path = escapeXml(quote.path);
182
+ const text = escapeXml(quote.text);
183
+ const attributes = [
184
+ `path="${path}"`,
185
+ `lines="${quote.startLine}-${quote.endLine}"`,
186
+ ...formatGitAttributes(quote.git)
187
+ ].join(" ");
188
+ return `<user_file_quote ${attributes}>
189
+ ${text}
190
+ </user_file_quote>`;
191
+ });
192
+ const description = quotes.length === 1 ? "The user intentionally selected the file excerpt above." : "The user intentionally selected the file excerpts above.";
193
+ return `${blocks.join("\n\n")}
194
+
195
+ ${description}`;
196
+ }
197
+ function normalizeTextLines(contents) {
198
+ if (contents === "") return [];
199
+ const lines = contents.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
200
+ if (lines.at(-1) === "") lines.pop();
201
+ return lines;
202
+ }
203
+ function compareStrings(left, right) {
204
+ return left < right ? -1 : left > right ? 1 : 0;
205
+ }
206
+ function isInside(root, candidate) {
207
+ const result = relative(root, candidate);
208
+ return result === "" || !result.startsWith(`..${sep}`) && result !== ".." && !isAbsolute(result);
209
+ }
210
+ function formatGitAttributes(git) {
211
+ if (!git) return [];
212
+ return [
213
+ ["git_head", git.head],
214
+ ["git_branch", git.branch],
215
+ ["git_status", git.status],
216
+ ["git_revision", git.revision],
217
+ ["git_blob", git.blob],
218
+ ["content_sha256", git.contentSha256],
219
+ ["source", git.source],
220
+ ["git_base", git.base]
221
+ ].flatMap(([name, value]) => value ? [`${name}="${escapeXml(value)}"`] : []);
222
+ }
223
+ function escapeXml(value) {
224
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
225
+ }
226
+ function formatError(error) {
227
+ return error instanceof Error ? error.message : String(error);
228
+ }
229
+
230
+ export {
231
+ MAX_QUOTE_BYTES,
232
+ MAX_QUOTE_LINES,
233
+ MAX_PENDING_QUOTES,
234
+ MAX_PENDING_QUOTE_BYTES,
235
+ discoverProjectFiles,
236
+ loadProjectTextFile,
237
+ createFileQuoteSnapshot,
238
+ createPreviewQuote,
239
+ appendPendingQuote,
240
+ formatQuoteContext
241
+ };
242
+ //# sourceMappingURL=chunk-WOTDNRTG.ts.map