pi-monofold 0.3.0 → 0.3.2

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/package.json CHANGED
@@ -1,58 +1,61 @@
1
- {
2
- "scripts": {
3
- "typecheck": "tsc --noEmit",
4
- "test": "node --import tsx --test tests/**/*.test.ts",
5
- "check": "npm run typecheck && npm test && npm pack --dry-run"
6
- },
7
- "peerDependencies": {
8
- "typebox": "*",
9
- "@earendil-works/pi-coding-agent": "*",
10
- "@earendil-works/pi-ai": "*"
11
- },
12
- "description": "Pi extension that folds multiple repositories and folders into a guarded virtual monorepo for AI agents.",
13
- "type": "module",
14
- "version": "0.3.0",
15
- "pi": {
16
- "extensions": [
17
- "./index.ts"
18
- ]
19
- },
20
- "files": [
21
- "README.md",
22
- "LICENSE",
23
- "index.ts",
24
- "focus-preset.ts",
25
- "validation.ts"
26
- ],
27
- "name": "pi-monofold",
28
- "devDependencies": {
29
- "typescript": "^6.0.3",
30
- "typebox": "^1.1.38",
31
- "@types/node": "^25.9.1",
32
- "tsx": "^4.20.5",
33
- "@earendil-works/pi-coding-agent": "^0.75.4",
34
- "@earendil-works/pi-ai": "^0.75.4"
35
- },
36
- "dependencies": {
37
- "yaml": "^2.8.1"
38
- },
39
- "keywords": [
40
- "pi-package",
41
- "pi-extension",
42
- "ai-coding",
43
- "virtual-monorepo",
44
- "multi-repo"
45
- ],
46
- "repository": {
47
- "type": "git",
48
- "url": "git+https://github.com/eiei114/pi-monofold.git"
49
- },
50
- "bugs": {
51
- "url": "https://github.com/eiei114/pi-monofold/issues"
52
- },
53
- "homepage": "https://github.com/eiei114/pi-monofold#readme",
54
- "publishConfig": {
55
- "access": "public"
56
- },
57
- "license": "MIT"
58
- }
1
+ {
2
+ "scripts": {
3
+ "typecheck": "tsc --noEmit",
4
+ "test": "node --import tsx --test tests/**/*.test.ts",
5
+ "check": "npm run typecheck && npm test && npm pack --dry-run"
6
+ },
7
+ "peerDependencies": {
8
+ "typebox": "*",
9
+ "@earendil-works/pi-coding-agent": "*",
10
+ "@earendil-works/pi-ai": "*"
11
+ },
12
+ "description": "Pi extension that folds multiple repositories and folders into a guarded virtual monorepo for AI agents.",
13
+ "type": "module",
14
+ "version": "0.3.2",
15
+ "pi": {
16
+ "extensions": [
17
+ "./index.ts"
18
+ ]
19
+ },
20
+ "files": [
21
+ "README.md",
22
+ "LICENSE",
23
+ "index.ts",
24
+ "path-normalize.ts",
25
+ "file-read-preview.ts",
26
+ "focus-preset.ts",
27
+ "read-caps.ts",
28
+ "validation.ts"
29
+ ],
30
+ "name": "pi-monofold",
31
+ "devDependencies": {
32
+ "typescript": "^6.0.3",
33
+ "typebox": "^1.1.38",
34
+ "@types/node": "^25.9.1",
35
+ "tsx": "^4.20.5",
36
+ "@earendil-works/pi-coding-agent": "^0.75.4",
37
+ "@earendil-works/pi-ai": "^0.75.4"
38
+ },
39
+ "dependencies": {
40
+ "yaml": "^2.8.1"
41
+ },
42
+ "keywords": [
43
+ "pi-package",
44
+ "pi-extension",
45
+ "ai-coding",
46
+ "virtual-monorepo",
47
+ "multi-repo"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/eiei114/pi-monofold.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/eiei114/pi-monofold/issues"
55
+ },
56
+ "homepage": "https://github.com/eiei114/pi-monofold#readme",
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "license": "MIT"
61
+ }
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+
3
+ /** Converts MSYS/Git-Bash style paths into canonical absolute Windows paths. */
4
+ function repairMsysPath(value: string): string {
5
+ let normalized = value.replace(/\\/g, "/");
6
+
7
+ const unixDrive = normalized.match(/^\/([a-zA-Z])\/(.*)$/);
8
+ if (unixDrive) {
9
+ normalized = `${unixDrive[1].toUpperCase()}:/${unixDrive[2]}`;
10
+ }
11
+
12
+ const mixedDrive = normalized.match(/^([a-zA-Z]):\/?(?:c|C)\/(.*)$/);
13
+ if (mixedDrive) {
14
+ normalized = `${mixedDrive[1].toUpperCase()}:/${mixedDrive[2]}`;
15
+ }
16
+
17
+ return normalized;
18
+ }
19
+
20
+ /** Normalizes local paths before workspace guard comparisons. */
21
+ export function normalizeGuardPath(input: string): string {
22
+ const trimmed = input.trim();
23
+ if (!trimmed) return trimmed;
24
+ return path.resolve(repairMsysPath(trimmed));
25
+ }
package/read-caps.ts ADDED
@@ -0,0 +1,137 @@
1
+ export const DEFAULT_MAX_MATCHES = 50;
2
+ export const DEFAULT_MAX_CHARS = 8_000;
3
+ export const DEFAULT_MAX_ENTRIES = 200;
4
+
5
+ export type ResolvedSearchCaps = {
6
+ maxMatches: number;
7
+ maxChars: number;
8
+ };
9
+
10
+ export type ResolvedTreeCaps = {
11
+ maxEntries: number;
12
+ };
13
+
14
+ export type SearchCapResult = {
15
+ text: string;
16
+ matchCount: number;
17
+ returnedMatchCount: number;
18
+ maxMatches: number;
19
+ maxChars: number;
20
+ truncated: boolean;
21
+ hint?: string;
22
+ };
23
+
24
+ export type TreeCapResult = {
25
+ text: string;
26
+ entryCount: number;
27
+ returnedEntryCount: number;
28
+ maxEntries: number;
29
+ truncated: boolean;
30
+ hint?: string;
31
+ };
32
+
33
+ function assertPositiveInt(name: string, value: number): number {
34
+ if (!Number.isInteger(value) || value <= 0) {
35
+ throw new Error(`${name} must be a positive integer`);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ export function resolveSearchCaps(input?: { maxMatches?: number; maxChars?: number }): ResolvedSearchCaps {
41
+ return {
42
+ maxMatches: input?.maxMatches === undefined ? DEFAULT_MAX_MATCHES : assertPositiveInt("maxMatches", input.maxMatches),
43
+ maxChars: input?.maxChars === undefined ? DEFAULT_MAX_CHARS : assertPositiveInt("maxChars", input.maxChars),
44
+ };
45
+ }
46
+
47
+ export function resolveTreeCaps(input?: { maxEntries?: number }): ResolvedTreeCaps {
48
+ return {
49
+ maxEntries:
50
+ input?.maxEntries === undefined ? DEFAULT_MAX_ENTRIES : assertPositiveInt("maxEntries", input.maxEntries),
51
+ };
52
+ }
53
+
54
+ export function searchTruncationHint(matchCount: number, returnedMatchCount: number, caps: ResolvedSearchCaps): string {
55
+ return (
56
+ `[truncated: showing ${returnedMatchCount} of ${matchCount} matches ` +
57
+ `(maxMatches=${caps.maxMatches}, maxChars=${caps.maxChars}). ` +
58
+ "Narrow path/query or pass higher maxMatches/maxChars intentionally.]"
59
+ );
60
+ }
61
+
62
+ export function treeTruncationHint(entryCount: number, returnedEntryCount: number, caps: ResolvedTreeCaps): string {
63
+ return (
64
+ `[truncated: showing ${returnedEntryCount} of ${entryCount} entries ` +
65
+ `(maxEntries=${caps.maxEntries}). ` +
66
+ "Narrow path/depth or pass a higher maxEntries intentionally.]"
67
+ );
68
+ }
69
+
70
+ export function capSearchOutput(rawOutput: string, caps: ResolvedSearchCaps): SearchCapResult {
71
+ const trimmed = rawOutput.trim();
72
+ if (!trimmed) {
73
+ return {
74
+ text: "No matches",
75
+ matchCount: 0,
76
+ returnedMatchCount: 0,
77
+ maxMatches: caps.maxMatches,
78
+ maxChars: caps.maxChars,
79
+ truncated: false,
80
+ };
81
+ }
82
+
83
+ const lines = trimmed.split("\n");
84
+ const matchCount = lines.length;
85
+ const matchLimited = lines.slice(0, caps.maxMatches);
86
+ let truncated = matchLimited.length < matchCount;
87
+
88
+ const charLimited: string[] = [];
89
+ let charTotal = 0;
90
+ for (const line of matchLimited) {
91
+ const separator = charLimited.length > 0 ? 1 : 0;
92
+ if (charTotal + separator + line.length > caps.maxChars) {
93
+ truncated = true;
94
+ break;
95
+ }
96
+ charLimited.push(line);
97
+ charTotal += separator + line.length;
98
+ }
99
+
100
+ const returnedMatchCount = charLimited.length;
101
+ const hint = truncated ? searchTruncationHint(matchCount, returnedMatchCount, caps) : undefined;
102
+ const body = charLimited.join("\n");
103
+ const text = hint ? `${body}\n\n${hint}` : body;
104
+
105
+ return {
106
+ text,
107
+ matchCount,
108
+ returnedMatchCount,
109
+ maxMatches: caps.maxMatches,
110
+ maxChars: caps.maxChars,
111
+ truncated,
112
+ hint,
113
+ };
114
+ }
115
+
116
+ export function capTreeLines(
117
+ lines: string[],
118
+ caps: ResolvedTreeCaps,
119
+ traversalTruncated = false,
120
+ ): TreeCapResult {
121
+ const entryCount = traversalTruncated ? lines.length + 1 : lines.length;
122
+ const limited = lines.slice(0, caps.maxEntries);
123
+ const truncated = traversalTruncated || limited.length < lines.length;
124
+ const returnedEntryCount = limited.length;
125
+ const hint = truncated ? treeTruncationHint(entryCount, returnedEntryCount, caps) : undefined;
126
+ const body = limited.join("\n");
127
+ const text = hint ? `${body}\n\n${hint}` : body;
128
+
129
+ return {
130
+ text,
131
+ entryCount,
132
+ returnedEntryCount,
133
+ maxEntries: caps.maxEntries,
134
+ truncated,
135
+ hint,
136
+ };
137
+ }