pi-monofold 0.12.4 → 0.12.10

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/README.md CHANGED
@@ -1,243 +1,247 @@
1
- # pi-monofold
2
-
3
- [![Join dotfield.xyz on Discord](https://img.shields.io/badge/Join%20dotfield.xyz%20on%20Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/4945dXZVW5)
4
-
5
- [![CI](https://github.com/eiei114/pi-monofold/actions/workflows/ci.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/ci.yml)
6
- [![Publish](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml)
7
- [![npm version](https://img.shields.io/npm/v/pi-monofold?color=cb3837&logo=npm)](https://www.npmjs.com/package/pi-monofold)
8
- [![npm downloads](https://img.shields.io/npm/dw/pi-monofold)](https://www.npmjs.com/package/pi-monofold)
9
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
10
- [![Pi Package](https://img.shields.io/badge/Pi-package-6f42c1)](https://github.com/eiei114/pi-monofold)
11
- [![Trusted Publishing](https://img.shields.io/badge/npm-provenance-yellow)](https://docs.npmjs.com/generating-provenance-statements)
12
- <a href="https://buymeacoffee.com/ekawano114m"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" height="60"></a>
13
-
14
- Pi extension that folds multiple local repositories and folders into a guarded **Virtual Monorepo** for AI agents.
15
-
16
- ## What this is
17
-
18
- Pi Monofold (`pi-monofold`) keeps repositories physically separate while giving Pi a lightweight manifest, routed writes, workspace-aware reads, guarded commands, and explicit git flows. Documentation, rules, product context, and implementation code can appear as one connected system without migrating everything into a single git repository.
19
-
20
- See [docs/usage.md](./docs/usage.md) for configuration, commands, agent tools, and guard behavior.
21
-
22
- ## Features
23
-
24
- - **Virtual monorepo manifest** — declare workspaces and project workspaces in `.pi/monofold.yaml`
25
- - **Multi-runtime path overlays** — keep one logical workspace definition while swapping absolute repo roots per runtime
26
- - **Routed Markdown writes** — route PRDs, progress notes, and other doc types to configured folders
27
- - **Workspace-aware reads** — list, read, search, and tree views scoped to readable workspaces, with bounded previews by default
28
- - **Capability guard** — block or confirm `read` / `write` / `edit` / `grep` / `find` / `bash` based on workspace tags
29
- - **Focus presets** — tag-based focus targets for the control workspace
30
- - **Natural-language commands** — `/monofold:explore`, `/monofold:write`, `/monofold:config`, `/monofold:git`, and more
31
- - **Strict agent tools** — `monofold_*` tools for programmatic access behind the command surface
32
- - **Config migration** — upgrade legacy `.pi/monofold.yml` with backups and validation
33
-
34
- ## Install
35
-
36
- Pi Monofold is a Pi package. Install it with Pi's package installer from git or npm.
37
-
38
- > Security: Pi packages run with full system access. Review packages before installing third-party code.
39
-
40
- ### From git
41
-
42
- ```powershell
43
- pi install git:github.com/eiei114/pi-monofold
44
- ```
45
-
46
- Project-local install:
47
-
48
- ```powershell
49
- pi install -l git:github.com/eiei114/pi-monofold
50
- ```
51
-
52
- Pin a version:
53
-
54
- ```powershell
55
- pi install git:github.com/eiei114/pi-monofold@v0.12.4
56
- ```
57
-
58
- Try without installing:
59
-
60
- ```powershell
61
- pi -e git:github.com/eiei114/pi-monofold
62
- ```
63
-
64
- ### From npm
65
-
66
- ```powershell
67
- pi install npm:pi-monofold
68
- ```
69
-
70
- Project-local install:
71
-
72
- ```powershell
73
- pi install -l npm:pi-monofold
74
- ```
75
-
76
- Pin a version:
77
-
78
- ```powershell
79
- pi install npm:pi-monofold@0.12.4
80
- ```
81
-
82
- Try without installing:
83
-
84
- ```powershell
85
- pi -e npm:pi-monofold
86
- ```
87
-
88
- ## Quick start
89
-
90
- 1. Install the extension (see [Install](#install)).
91
- 2. In your control repository, create `.pi/monofold.yaml` with at least one workspace entry (or run `/monofold:init`).
92
- 3. Start Pi in the control repository and run `/monofold:explore show the project workspaces`.
93
- 4. Use `/monofold:focus`, `ctrl+shift+m`, or `shift+ctrl+f` to switch focus presets when `focusPresets` are configured. Active Focus is restored automatically on the next Pi session in the same control repository.
94
- 5. Use `/monofold:write` for routed Markdown outputs and `/monofold:git` for guarded git workflows.
95
-
96
- Example command flows: [docs/examples.md](./docs/examples.md).
97
-
98
- ## Usage summary
99
-
100
- | Surface | Purpose |
101
- |---------|---------|
102
- | `/monofold:explore` | List, read, search, or inspect workspace trees |
103
- | `/monofold:write` | Create routed Markdown outputs |
104
- | `/monofold:config` | Add or change workspaces and project workspaces |
105
- | `/monofold:git` | Run guarded git status, commit, push, or commit+push |
106
- | `/monofold:focus` | Select the active focus preset from a TUI list |
107
- | `/monofold:focus-prev` | Cycle Active Focus backward through `focusPresets` YAML order |
108
- | `/monofold:guide` | Interactive guide for common flows |
109
- | `/monofold:init` | Create or update `.pi/monofold.yaml` |
110
- | `/monofold:update` | Migrate legacy config and optionally request config edits |
111
-
112
- Default focus shortcuts: `ctrl+shift+m` cycles Active Focus forward and `shift+ctrl+f` cycles backward through `focusPresets` YAML order. Both wrap at the start/end of the list.
113
-
114
- When Active Focus is set, Pi Monofold injects the active preset's `contextFiles` into each agent turn under **Focus Context Injection** and recomposes the manifest so active Workspace Targets are shown first while non-active targets are collapsed to one-line summaries. Tag-based target inference in `monofold_read`, `monofold_write`, and `monofold_git` also prefers Workspace Targets that belong to the active preset when a tag query would otherwise match multiple candidates; explicit `targetId` / workspace name selectors and uniquely matching targets are unchanged. If multiple in-focus targets still tie, the existing workspace selection flow applies. The MVP uses provisional context-injection caps that are intentionally temporary and exposed as constants for future tuning:
115
-
116
- - Max **6** context files per active preset.
117
- - Max **6,000** characters per file, with `… [truncated]` appended when a file is cut.
118
- - Max **12,000** injected file-content characters per turn; remaining files are skipped and a warning is surfaced once for that turn.
119
-
120
- Agent tools (`monofold_list`, `monofold_read`, `monofold_write`, `monofold_git`, `monofold_init`) sit behind these commands. Use `monofold_list` as the first-line Active Focus health check (preset, route override, unresolved targets, and warnings). Full reference: [docs/usage.md](./docs/usage.md).
121
-
122
- ## Safe read defaults
123
-
124
- `monofold_read` can reach files across multiple configured workspaces. Returning full file bodies or unbounded search/tree output by default would flood the agent chat and can bias later turns. Pi Monofold therefore uses **preview-first, capped-by-default** reads.
125
-
126
- | `monofold_read` mode | Default output |
127
- |----------------------|----------------|
128
- | **file** | Path, size, line/character counts, modified time, then a bounded preview (first **20** lines, up to **2,000** characters). Files that already fit those bounds are shown in full without a truncation marker. |
129
- | **search** | Up to **50** match lines and **8,000** characters of ripgrep output. |
130
- | **tree** | Up to **200** entries; traversal depth is capped at **5**. |
131
-
132
- When output is cut, the tool response includes a **`[truncated]`** marker (file mode) or a **`[truncated: …]`** footer (search/tree) that states what was shown and how to request more.
133
-
134
- **Request more content intentionally:**
135
-
136
- | Goal | `monofold_read` parameters |
137
- |------|----------------------------|
138
- | Full file body | `mode: "file"`, `includeContent: true` |
139
- | Larger bounded file slice | `head`, `tail`, and/or `maxChars` (positive integers) |
140
- | More search results | Higher `maxMatches` and/or `maxChars`, or a narrower `path` / `query` |
141
- | Larger directory tree | Higher `maxEntries`, lower `depth`, or a narrower `path` |
142
-
143
- Agents should call **`monofold_read`** (not guess at raw Pi `read`). Humans should use **`/monofold:explore`** with natural language. Legacy slash commands such as `/monofold:read` apply the same caps for compatibility but are **not** the preferred human-facing surface—see [docs/usage.md](./docs/usage.md#safe-read-contract-monofold_read).
144
-
145
- ## Package contents
146
-
147
- ```text
148
- pi-monofold/
149
- ├── .github/workflows/
150
- │ ├── auto-release.yml # Auto-tag + release on merge to main
151
- │ ├── ci.yml # Validate on PR / push
152
- │ └── publish.yml # Publish to npm (Trusted Publishing)
153
- ├── docs/
154
- │ ├── usage.md # Config, commands, agent API, guard
155
- │ ├── examples.md # Command examples
156
- │ └── release.md # Release and publish flow
157
- ├── scripts/
158
- │ └── check-version-bump.mjs # PR version bump guard (CI)
159
- ├── tests/
160
- │ ├── file-read-preview.test.ts
161
- │ ├── focus-command.test.ts
162
- │ ├── focus-context-injection.test.ts
163
- │ ├── focus-decision-note-integration.test.ts
164
- │ ├── focus-decision-note.test.ts
165
- │ ├── focus-inference-bias.test.ts
166
- │ ├── focus-preset.test.ts
167
- │ ├── focus-route-override-integration.test.ts
168
- │ ├── focus-route-override.test.ts
169
- │ ├── focus-session-restore.test.ts
170
- │ ├── focus-session-state.test.ts
171
- │ ├── focus-skills-auto-load.test.ts
172
- │ ├── focus-skills.test.ts
173
- │ ├── changelog-hygiene.test.ts
174
- │ ├── github-actions-pin.test.ts
175
- │ ├── monofold-read-caps.test.ts
176
- │ ├── monofold-read-ops.test.ts
177
- │ ├── path-normalize.test.ts
178
- │ ├── readme-package-contents.test.ts
179
- │ ├── release-docs.test.ts
180
- │ ├── readme-version-pin.test.ts
181
- │ ├── runtime-path-overlays.test.ts
182
- │ └── unknown-path-allows.test.ts
183
- ├── CHANGELOG.md
184
- ├── SECURITY.md
185
- ├── file-read-preview.ts
186
- ├── focus-decision-note.ts
187
- ├── focus-preset.ts
188
- ├── focus-route-override.ts
189
- ├── focus-session-state.ts
190
- ├── focus-skills.ts
191
- ├── index.ts
192
- ├── LICENSE
193
- ├── monofold-read-ops.ts
194
- ├── package.json
195
- ├── path-normalize.ts
196
- ├── read-caps.ts
197
- ├── README.md
198
- ├── unknown-path-allows.ts
199
- ├── validation.ts
200
- └── tsconfig.json
201
- ```
202
-
203
- ## Development
204
-
205
- Clone and validate:
206
-
207
- ```powershell
208
- git clone https://github.com/eiei114/pi-monofold.git
209
- cd pi-monofold
210
- npm install
211
- npm run check
212
- ```
213
-
214
- Try the local checkout without installing:
215
-
216
- ```powershell
217
- pi -e .
218
- ```
219
-
220
- ## Release
221
-
222
- Releases are automated. See [docs/release.md](./docs/release.md) for details.
223
-
224
- 1. Bump `version` in `package.json` and update `CHANGELOG.md`.
225
- 2. Merge to `main`.
226
- 3. **Auto Release** tags `v<version>` and creates a GitHub release when the tag is new.
227
- 4. The tag triggers **Publish**, which publishes to npm with OIDC provenance.
228
-
229
- ## Security
230
-
231
- Pi Monofold intercepts standard Pi tool calls when monofold config is present. Writes and shell commands are allowed only when the resolved workspace grants the matching capability. Git commit/push via raw `bash` is blocked; use `/monofold:git` or `monofold_git` instead.
232
-
233
- Report vulnerabilities per [SECURITY.md](./SECURITY.md).
234
-
235
- ## Links
236
-
237
- - **Repository**: <https://github.com/eiei114/pi-monofold>
238
- - **npm**: <https://www.npmjs.com/package/pi-monofold>
239
- - **Issues**: <https://github.com/eiei114/pi-monofold/issues>
240
-
241
- ## License
242
-
243
- [MIT](LICENSE)
1
+ # pi-monofold
2
+
3
+ [![Join dotfield.xyz on Discord](https://img.shields.io/badge/Join%20dotfield.xyz%20on%20Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/4945dXZVW5)
4
+
5
+ [![CI](https://github.com/eiei114/pi-monofold/actions/workflows/ci.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/ci.yml)
6
+ [![Publish](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml)
7
+ [![npm version](https://img.shields.io/npm/v/pi-monofold?color=cb3837&logo=npm)](https://www.npmjs.com/package/pi-monofold)
8
+ [![npm downloads](https://img.shields.io/npm/dw/pi-monofold)](https://www.npmjs.com/package/pi-monofold)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
10
+ [![Pi Package](https://img.shields.io/badge/Pi-package-6f42c1)](https://github.com/eiei114/pi-monofold)
11
+ [![Trusted Publishing](https://img.shields.io/badge/npm-provenance-yellow)](https://docs.npmjs.com/generating-provenance-statements)
12
+ <a href="https://buymeacoffee.com/ekawano114m"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" height="60"></a>
13
+
14
+ Pi extension that folds multiple local repositories and folders into a guarded **Virtual Monorepo** for AI agents.
15
+
16
+ ## What this is
17
+
18
+ Pi Monofold (`pi-monofold`) keeps repositories physically separate while giving Pi a lightweight manifest, routed writes, workspace-aware reads, guarded commands, and explicit git flows. Documentation, rules, product context, and implementation code can appear as one connected system without migrating everything into a single git repository.
19
+
20
+ See [docs/usage.md](./docs/usage.md) for configuration, commands, agent tools, and guard behavior.
21
+
22
+ ## Features
23
+
24
+ - **Virtual monorepo manifest** — declare workspaces and project workspaces in `.pi/monofold.yaml`
25
+ - **Multi-runtime path overlays** — keep one logical workspace definition while swapping absolute repo roots per runtime
26
+ - **Routed Markdown writes** — route PRDs, progress notes, and other doc types to configured folders
27
+ - **Workspace-aware reads** — list, read, search, and tree views scoped to readable workspaces, with bounded previews by default
28
+ - **Capability guard** — block or confirm `read` / `write` / `edit` / `grep` / `find` / `bash` based on workspace tags
29
+ - **Focus presets** — tag-based focus targets for the control workspace
30
+ - **Natural-language commands** — `/monofold:explore`, `/monofold:write`, `/monofold:config`, `/monofold:git`, and more
31
+ - **Strict agent tools** — `monofold_*` tools for programmatic access behind the command surface
32
+ - **Config migration** — upgrade legacy `.pi/monofold.yml` with backups and validation
33
+
34
+ ## Install
35
+
36
+ Pi Monofold is a Pi package. Install it with Pi's package installer from git or npm.
37
+
38
+ > Security: Pi packages run with full system access. Review packages before installing third-party code.
39
+
40
+ ### From git
41
+
42
+ ```powershell
43
+ pi install git:github.com/eiei114/pi-monofold
44
+ ```
45
+
46
+ Project-local install:
47
+
48
+ ```powershell
49
+ pi install -l git:github.com/eiei114/pi-monofold
50
+ ```
51
+
52
+ Pin a version:
53
+
54
+ ```powershell
55
+ pi install git:github.com/eiei114/pi-monofold@v0.12.10
56
+ ```
57
+
58
+ Try without installing:
59
+
60
+ ```powershell
61
+ pi -e git:github.com/eiei114/pi-monofold
62
+ ```
63
+
64
+ ### From npm
65
+
66
+ ```powershell
67
+ pi install npm:pi-monofold
68
+ ```
69
+
70
+ Project-local install:
71
+
72
+ ```powershell
73
+ pi install -l npm:pi-monofold
74
+ ```
75
+
76
+ Pin a version:
77
+
78
+ ```powershell
79
+ pi install npm:pi-monofold@0.12.10
80
+ ```
81
+
82
+ Try without installing:
83
+
84
+ ```powershell
85
+ pi -e npm:pi-monofold
86
+ ```
87
+
88
+ ## Quick start
89
+
90
+ 1. Install the extension (see [Install](#install)).
91
+ 2. In your control repository, create `.pi/monofold.yaml` with at least one workspace entry (or run `/monofold:init`).
92
+ 3. Start Pi in the control repository and run `/monofold:explore show the project workspaces`.
93
+ 4. Use `/monofold:focus`, `ctrl+shift+m`, or `shift+ctrl+f` to switch focus presets when `focusPresets` are configured. Active Focus is restored automatically on the next Pi session in the same control repository.
94
+ 5. Use `/monofold:write` for routed Markdown outputs and `/monofold:git` for guarded git workflows.
95
+
96
+ Example command flows: [docs/examples.md](./docs/examples.md).
97
+
98
+ ## Usage summary
99
+
100
+ | Surface | Purpose |
101
+ |---------|---------|
102
+ | `/monofold:explore` | List, read, search, or inspect workspace trees |
103
+ | `/monofold:write` | Create routed Markdown outputs |
104
+ | `/monofold:config` | Add or change workspaces and project workspaces |
105
+ | `/monofold:git` | Run guarded git status, commit, push, or commit+push |
106
+ | `/monofold:focus` | Select the active focus preset from a TUI list |
107
+ | `/monofold:focus-prev` | Cycle Active Focus backward through `focusPresets` YAML order |
108
+ | `/monofold:guide` | Interactive guide for common flows |
109
+ | `/monofold:init` | Create or update `.pi/monofold.yaml` |
110
+ | `/monofold:update` | Migrate legacy config and optionally request config edits |
111
+
112
+ Default focus shortcuts: `ctrl+shift+m` cycles Active Focus forward and `shift+ctrl+f` cycles backward through `focusPresets` YAML order. Both wrap at the start/end of the list.
113
+
114
+ When Active Focus is set, Pi Monofold injects the active preset's `contextFiles` into each agent turn under **Focus Context Injection** and recomposes the manifest so active Workspace Targets are shown first while non-active targets are collapsed to one-line summaries. Tag-based target inference in `monofold_read`, `monofold_write`, and `monofold_git` also prefers Workspace Targets that belong to the active preset when a tag query would otherwise match multiple candidates; explicit `targetId` / workspace name selectors and uniquely matching targets are unchanged. If multiple in-focus targets still tie, the existing workspace selection flow applies. The MVP uses provisional context-injection caps that are intentionally temporary and exposed as constants for future tuning:
115
+
116
+ - Max **6** context files per active preset.
117
+ - Max **6,000** characters per file, with `… [truncated]` appended when a file is cut.
118
+ - Max **12,000** injected file-content characters per turn; remaining files are skipped and a warning is surfaced once for that turn.
119
+
120
+ Agent tools (`monofold_list`, `monofold_read`, `monofold_write`, `monofold_git`, `monofold_init`) sit behind these commands. Use `monofold_list` as the first-line Active Focus health check (preset, route override, unresolved targets, and warnings). Full reference: [docs/usage.md](./docs/usage.md).
121
+
122
+ ## Safe read defaults
123
+
124
+ `monofold_read` can reach files across multiple configured workspaces. Returning full file bodies or unbounded search/tree output by default would flood the agent chat and can bias later turns. Pi Monofold therefore uses **preview-first, capped-by-default** reads.
125
+
126
+ | `monofold_read` mode | Default output |
127
+ |----------------------|----------------|
128
+ | **file** | Path, size, line/character counts, modified time, then a bounded preview (first **20** lines, up to **2,000** characters). Files that already fit those bounds are shown in full without a truncation marker. |
129
+ | **search** | Up to **50** match lines and **8,000** characters of ripgrep output. |
130
+ | **tree** | Up to **200** entries; traversal depth is capped at **5**. |
131
+
132
+ When output is cut, the tool response includes a **`[truncated]`** marker (file mode) or a **`[truncated: …]`** footer (search/tree) that states what was shown and how to request more.
133
+
134
+ **Request more content intentionally:**
135
+
136
+ | Goal | `monofold_read` parameters |
137
+ |------|----------------------------|
138
+ | Full file body | `mode: "file"`, `includeContent: true` |
139
+ | Larger bounded file slice | `head`, `tail`, and/or `maxChars` (positive integers) |
140
+ | More search results | Higher `maxMatches` and/or `maxChars`, or a narrower `path` / `query` |
141
+ | Larger directory tree | Higher `maxEntries`, lower `depth`, or a narrower `path` |
142
+
143
+ Agents should call **`monofold_read`** (not guess at raw Pi `read`). Humans should use **`/monofold:explore`** with natural language. Legacy slash commands such as `/monofold:read` apply the same caps for compatibility but are **not** the preferred human-facing surface—see [docs/usage.md](./docs/usage.md#safe-read-contract-monofold_read).
144
+
145
+ ## Package contents
146
+
147
+ ```text
148
+ pi-monofold/
149
+ ├── .github/workflows/
150
+ │ ├── auto-release.yml # Auto-tag + release on merge to main
151
+ │ ├── ci.yml # Validate on PR / push
152
+ │ └── publish.yml # Publish to npm (Trusted Publishing)
153
+ ├── docs/
154
+ │ ├── usage.md # Config, commands, agent API, guard
155
+ │ ├── examples.md # Command examples
156
+ │ └── release.md # Release and publish flow
157
+ ├── scripts/
158
+ │ └── check-version-bump.mjs # PR version bump guard (CI)
159
+ ├── tests/
160
+ │ ├── file-read-preview.test.ts
161
+ │ ├── focus-command.test.ts
162
+ │ ├── focus-context-injection.test.ts
163
+ │ ├── focus-decision-note-integration.test.ts
164
+ │ ├── focus-decision-note.test.ts
165
+ │ ├── focus-inference-bias.test.ts
166
+ │ ├── focus-preset.test.ts
167
+ │ ├── focus-route-override-integration.test.ts
168
+ │ ├── focus-route-override.test.ts
169
+ │ ├── focus-session-restore.test.ts
170
+ │ ├── focus-session-state.test.ts
171
+ │ ├── focus-skills-auto-load.test.ts
172
+ │ ├── focus-skills.test.ts
173
+ │ ├── changelog-hygiene.test.ts
174
+ │ ├── contributing-docs.test.ts
175
+ │ ├── github-actions-pin.test.ts
176
+ │ ├── monofold-read-caps.test.ts
177
+ │ ├── monofold-read-ops.test.ts
178
+ │ ├── path-normalize.test.ts
179
+ │ ├── readme-package-contents.test.ts
180
+ │ ├── release-docs.test.ts
181
+ │ ├── readme-version-pin.test.ts
182
+ │ ├── runtime-path-overlays.test.ts
183
+ │ ├── tsconfig-publishable-files.test.ts
184
+ │ ├── unknown-path-allows.test.ts
185
+ │ └── workspace-path-lookup.test.ts
186
+ ├── CHANGELOG.md
187
+ ├── SECURITY.md
188
+ ├── file-read-preview.ts
189
+ ├── focus-decision-note.ts
190
+ ├── focus-preset.ts
191
+ ├── focus-route-override.ts
192
+ ├── focus-session-state.ts
193
+ ├── focus-skills.ts
194
+ ├── index.ts
195
+ ├── LICENSE
196
+ ├── monofold-read-ops.ts
197
+ ├── package.json
198
+ ├── path-normalize.ts
199
+ ├── read-caps.ts
200
+ ├── README.md
201
+ ├── unknown-path-allows.ts
202
+ ├── validation.ts
203
+ ├── workspace-path-lookup.ts
204
+ └── tsconfig.json
205
+ ```
206
+
207
+ ## Development
208
+
209
+ Clone and validate:
210
+
211
+ ```powershell
212
+ git clone https://github.com/eiei114/pi-monofold.git
213
+ cd pi-monofold
214
+ npm install
215
+ npm run check
216
+ ```
217
+
218
+ Try the local checkout without installing:
219
+
220
+ ```powershell
221
+ pi -e .
222
+ ```
223
+
224
+ ## Release
225
+
226
+ Releases are automated. See [docs/release.md](./docs/release.md) for details.
227
+
228
+ 1. Bump `version` in `package.json` and update `CHANGELOG.md`.
229
+ 2. Merge to `main`.
230
+ 3. **Auto Release** tags `v<version>` and creates a GitHub release when the tag is new.
231
+ 4. The tag triggers **Publish**, which publishes to npm with OIDC provenance.
232
+
233
+ ## Security
234
+
235
+ Pi Monofold intercepts standard Pi tool calls when monofold config is present. Writes and shell commands are allowed only when the resolved workspace grants the matching capability. Git commit/push via raw `bash` is blocked; use `/monofold:git` or `monofold_git` instead.
236
+
237
+ Report vulnerabilities per [SECURITY.md](./SECURITY.md).
238
+
239
+ ## Links
240
+
241
+ - **Repository**: <https://github.com/eiei114/pi-monofold>
242
+ - **npm**: <https://www.npmjs.com/package/pi-monofold>
243
+ - **Issues**: <https://github.com/eiei114/pi-monofold/issues>
244
+
245
+ ## License
246
+
247
+ [MIT](LICENSE)
package/index.ts CHANGED
@@ -25,6 +25,12 @@ import {
25
25
  setActiveFocusPresetId,
26
26
  warnZeroTargetMatchesForPreset,
27
27
  } from "./focus-preset.js";
28
+ import {
29
+ buildPathLookupWorkspaces,
30
+ findWorkspaceForAbsolutePath,
31
+ isPathInsideWorkspace,
32
+ resolveAbsoluteTargetPath,
33
+ } from "./workspace-path-lookup.js";
28
34
  import {
29
35
  resolveWriteRouteType,
30
36
  type MonofoldRouteType,
@@ -130,6 +136,7 @@ type LoadedConfig = {
130
136
  root: string;
131
137
  raw: MultiWorkspaceConfig;
132
138
  workspaces: ResolvedWorkspace[];
139
+ pathLookupWorkspaces: ResolvedWorkspace[];
133
140
  activeRuntime: RuntimeInfo;
134
141
  };
135
142
 
@@ -278,10 +285,7 @@ function asPathOverlayMap(label: string, value: unknown): RuntimePathOverlayMap
278
285
  }
279
286
 
280
287
  function isInside(parent: string, child: string): boolean {
281
- const normalizedParent = normalizeGuardPath(parent);
282
- const normalizedChild = normalizeGuardPath(child);
283
- const relative = path.relative(normalizedParent, normalizedChild);
284
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
288
+ return isPathInsideWorkspace(parent, child);
285
289
  }
286
290
 
287
291
  function assertWorkspaceInternalRelative(label: string, value: string): void {
@@ -585,6 +589,7 @@ async function validateConfigObject(cwd: string, configPath: string, parsed: unk
585
589
  workspaces: workspaces.filter((workspace) => workspace.kind === "workspace"),
586
590
  },
587
591
  workspaces,
592
+ pathLookupWorkspaces: buildPathLookupWorkspaces(workspaces),
588
593
  activeRuntime,
589
594
  };
590
595
  }
@@ -1498,8 +1503,8 @@ function classifyPath(targetPath: string): "docs" | "code" | "unknown" {
1498
1503
  }
1499
1504
 
1500
1505
  function findWorkspaceForPath(loaded: LoadedConfig, targetPath: string): ResolvedWorkspace | undefined {
1501
- const absolute = normalizeGuardPath(path.isAbsolute(targetPath) ? targetPath : path.resolve(loaded.root, targetPath));
1502
- return [...loaded.workspaces].sort((a, b) => b.resolvedPath.length - a.resolvedPath.length).find((workspace) => isInside(workspace.resolvedPath, absolute));
1506
+ const absolute = resolveAbsoluteTargetPath(loaded.root, targetPath);
1507
+ return findWorkspaceForAbsolutePath(loaded.pathLookupWorkspaces, absolute);
1503
1508
  }
1504
1509
 
1505
1510
  async function confirm(ctx: ExtensionContext, title: string, body: string): Promise<boolean> {
@@ -51,7 +51,8 @@ export async function shallowTree(
51
51
  }
52
52
  const entries = await readdir(path.join(root, prefix), { withFileTypes: true });
53
53
  const lines: string[] = [];
54
- for (const entry of entries.filter((e) => e.name !== ".git" && e.name !== "node_modules")) {
54
+ for (const entry of entries) {
55
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
55
56
  if (budget && budget.remaining <= 0) {
56
57
  budget.truncated = true;
57
58
  break;
@@ -60,7 +61,10 @@ export async function shallowTree(
60
61
  lines.push(entry.isDirectory() ? `${rel}/` : rel);
61
62
  if (budget) budget.remaining -= 1;
62
63
  if (entry.isDirectory() && depth > 0) {
63
- lines.push(...(await shallowTree(root, depth - 1, rel, budget)));
64
+ const childLines = await shallowTree(root, depth - 1, rel, budget);
65
+ for (const childLine of childLines) {
66
+ lines.push(childLine);
67
+ }
64
68
  }
65
69
  }
66
70
  return lines;
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "description": "Pi extension that folds multiple repositories and folders into a guarded virtual monorepo for AI agents.",
14
14
  "type": "module",
15
- "version": "0.12.4",
15
+ "version": "0.12.10",
16
16
  "pi": {
17
17
  "extensions": [
18
18
  "./index.ts"
@@ -32,7 +32,8 @@
32
32
  "focus-session-state.ts",
33
33
  "monofold-read-ops.ts",
34
34
  "read-caps.ts",
35
- "validation.ts"
35
+ "validation.ts",
36
+ "workspace-path-lookup.ts"
36
37
  ],
37
38
  "name": "pi-monofold",
38
39
  "devDependencies": {
package/path-normalize.ts CHANGED
@@ -18,8 +18,8 @@ function repairMsysPath(value: string): string {
18
18
  }
19
19
 
20
20
  /** Normalizes local paths before workspace guard comparisons. */
21
- export function normalizeGuardPath(input: string): string {
21
+ export function normalizeGuardPath(input: string, impl: Pick<typeof path, "resolve"> = path): string {
22
22
  const trimmed = input.trim();
23
23
  if (!trimmed) return trimmed;
24
- return path.resolve(repairMsysPath(trimmed));
24
+ return impl.resolve(repairMsysPath(trimmed));
25
25
  }
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ import { normalizeGuardPath } from "./path-normalize.js";
3
+
4
+ type PathImpl = Pick<typeof path, "isAbsolute" | "relative" | "resolve" | "sep">;
5
+
6
+ /** Detects Windows drive-letter (`C:/...`) or UNC (`\\...`) syntax on any host. */
7
+ function isWindowsStylePath(value: string): boolean {
8
+ return /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("\\\\");
9
+ }
10
+
11
+ /** Selects the path implementation matching the given path syntax. */
12
+ function selectPathImpl(...values: string[]): PathImpl {
13
+ return values.some(isWindowsStylePath) ? path.win32 : path;
14
+ }
15
+
16
+ /** Returns true when `child` is inside or equal to `parent`. */
17
+ export function isPathInsideWorkspace(parent: string, child: string): boolean {
18
+ const impl = selectPathImpl(parent, child);
19
+ const normalizedParent = normalizeGuardPath(parent, impl);
20
+ const normalizedChild = normalizeGuardPath(child, impl);
21
+ const relative = impl.relative(normalizedParent, normalizedChild);
22
+ return (
23
+ relative === "" ||
24
+ (relative !== ".." && !relative.startsWith(`..${impl.sep}`) && !impl.isAbsolute(relative))
25
+ );
26
+ }
27
+
28
+ /** Pre-sorts workspaces once so path guards prefer the longest matching root. */
29
+ export function buildPathLookupWorkspaces<T extends { resolvedPath: string }>(workspaces: T[]): T[] {
30
+ return [...workspaces].sort((a, b) => b.resolvedPath.length - a.resolvedPath.length);
31
+ }
32
+
33
+ export function resolveAbsoluteTargetPath(root: string, targetPath: string): string {
34
+ const impl = selectPathImpl(root, targetPath);
35
+ return normalizeGuardPath(impl.isAbsolute(targetPath) ? targetPath : impl.resolve(root, targetPath), impl);
36
+ }
37
+
38
+ export function findWorkspaceForAbsolutePath<T extends { resolvedPath: string }>(
39
+ pathLookupWorkspaces: readonly T[],
40
+ absolutePath: string,
41
+ ): T | undefined {
42
+ const impl = selectPathImpl(absolutePath, ...pathLookupWorkspaces.map((workspace) => workspace.resolvedPath));
43
+ const absolute = normalizeGuardPath(absolutePath, impl);
44
+ return pathLookupWorkspaces.find((workspace) => {
45
+ const relative = impl.relative(normalizeGuardPath(workspace.resolvedPath, impl), absolute);
46
+ return (
47
+ relative === "" ||
48
+ (relative !== ".." && !relative.startsWith(`..${impl.sep}`) && !impl.isAbsolute(relative))
49
+ );
50
+ });
51
+ }