pi-advisor-flow 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.
@@ -0,0 +1,98 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { lstat, open, realpath } from "node:fs/promises";
3
+ import { isAbsolute, relative, resolve } from "node:path";
4
+ import { redactAndCapText } from "./conversation.js";
5
+
6
+ export const UNTRACKED_FILE_MAX_BYTES = 8 * 1024;
7
+ export const UNTRACKED_TOTAL_MAX_BYTES = 24 * 1024;
8
+ export interface UntrackedAttachment {
9
+ bytes: number;
10
+ text: string;
11
+ }
12
+
13
+ const PATH_SEGMENTS = /[\\/]/;
14
+
15
+ const within = (root: string, candidate: string) => {
16
+ const path = relative(root, candidate);
17
+ return path !== "" && !path.startsWith("..") && !path.includes("../");
18
+ };
19
+ const untracked = (cwd: string, path: string) => {
20
+ const output = execFileSync(
21
+ "git",
22
+ ["ls-files", "--others", "--exclude-standard", "--", path],
23
+ { cwd, encoding: "utf8", shell: false }
24
+ );
25
+ return output.split("\n").includes(path);
26
+ };
27
+
28
+ /** Returns only exact, permitted untracked regular-file bodies. Refusals are silent. */
29
+ export const readUntrackedFiles = async (
30
+ cwd: string,
31
+ requested: string[],
32
+ enabled: boolean,
33
+ redact: boolean
34
+ ): Promise<UntrackedAttachment[]> => {
35
+ if (!(enabled && Array.isArray(requested))) {
36
+ return [];
37
+ }
38
+ const root = await realpath(cwd).catch(() => undefined);
39
+ if (!root) {
40
+ return [];
41
+ }
42
+ const unique = new Set<string>();
43
+ const attachments: UntrackedAttachment[] = [];
44
+ let total = 0;
45
+ for (const name of requested) {
46
+ if (
47
+ typeof name !== "string" ||
48
+ !name ||
49
+ isAbsolute(name) ||
50
+ name.split(PATH_SEGMENTS).includes("..") ||
51
+ unique.has(name)
52
+ ) {
53
+ continue;
54
+ }
55
+ unique.add(name);
56
+ try {
57
+ const absolute = resolve(root, name);
58
+ if (!(within(root, absolute) && untracked(root, name))) {
59
+ continue;
60
+ }
61
+ // Sequentially enforce the aggregate disclosure budget.
62
+ // biome-ignore lint/performance/noAwaitInLoops: each accepted file consumes the remaining budget.
63
+ const stats = await lstat(absolute);
64
+ if (stats.isSymbolicLink() || !stats.isFile()) {
65
+ continue;
66
+ }
67
+ const resolved = await realpath(absolute);
68
+ if (!within(root, resolved)) {
69
+ continue;
70
+ }
71
+ const file = await open(resolved, "r");
72
+ try {
73
+ const available = Math.min(
74
+ UNTRACKED_FILE_MAX_BYTES,
75
+ UNTRACKED_TOTAL_MAX_BYTES - total
76
+ );
77
+ if (available <= 0) {
78
+ break;
79
+ }
80
+ const buffer = Buffer.alloc(available + 1);
81
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
82
+ const text = redactAndCapText(
83
+ buffer.subarray(0, bytesRead).toString("utf8"),
84
+ available,
85
+ redact
86
+ );
87
+ const bytes = Buffer.byteLength(text, "utf8");
88
+ attachments.push({ bytes, text });
89
+ total += bytes;
90
+ } finally {
91
+ await file.close();
92
+ }
93
+ } catch {
94
+ /* refuse unreadable or non-git paths */
95
+ }
96
+ }
97
+ return attachments;
98
+ };