pi-monofold 0.3.1 → 0.3.3

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,172 +1,195 @@
1
- # pi-monofold
2
-
3
- [![CI](https://github.com/eiei114/pi-monofold/actions/workflows/ci.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/ci.yml)
4
- [![Publish](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml)
5
- [![npm version](https://img.shields.io/npm/v/pi-monofold?color=cb3837&logo=npm)](https://www.npmjs.com/package/pi-monofold)
6
- [![npm downloads](https://img.shields.io/npm/dw/pi-monofold)](https://www.npmjs.com/package/pi-monofold)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
8
- [![Pi Package](https://img.shields.io/badge/Pi-package-6f42c1)](https://github.com/eiei114/pi-monofold)
9
- [![Trusted Publishing](https://img.shields.io/badge/npm-provenance-yellow)](https://docs.npmjs.com/generating-provenance-statements)
10
-
11
- Pi extension that folds multiple local repositories and folders into a guarded **Virtual Monorepo** for AI agents.
12
-
13
- ## What this is
14
-
15
- 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.
16
-
17
- See [docs/usage.md](./docs/usage.md) for configuration, commands, agent tools, and guard behavior.
18
-
19
- ## Features
20
-
21
- - **Virtual monorepo manifest** — declare workspaces and project workspaces in `.pi/monofold.yaml`
22
- - **Routed Markdown writes** — route PRDs, progress notes, and other doc types to configured folders
23
- - **Workspace-aware reads** — list, read, search, and tree views scoped to readable workspaces
24
- - **Capability guard** — block or confirm `read` / `write` / `edit` / `grep` / `find` / `bash` based on workspace tags
25
- - **Focus presets** — tag-based focus targets for the control workspace
26
- - **Natural-language commands** — `/monofold:explore`, `/monofold:write`, `/monofold:config`, `/monofold:git`, and more
27
- - **Strict agent tools** — `monofold_*` tools for programmatic access behind the command surface
28
- - **Config migration** — upgrade legacy `.pi/monofold.yml` with backups and validation
29
-
30
- ## Install
31
-
32
- Pi Monofold is a Pi package. Install it with Pi's package installer from git or npm.
33
-
34
- > Security: Pi packages run with full system access. Review packages before installing third-party code.
35
-
36
- ### From git
37
-
38
- ```powershell
39
- pi install git:github.com/eiei114/pi-monofold
40
- ```
41
-
42
- Project-local install:
43
-
44
- ```powershell
45
- pi install -l git:github.com/eiei114/pi-monofold
46
- ```
47
-
48
- Pin a version:
49
-
50
- ```powershell
51
- pi install git:github.com/eiei114/pi-monofold@v0.3.1
52
- ```
53
-
54
- Try without installing:
55
-
56
- ```powershell
57
- pi -e git:github.com/eiei114/pi-monofold
58
- ```
59
-
60
- ### From npm
61
-
62
- ```powershell
63
- pi install npm:pi-monofold
64
- ```
65
-
66
- Project-local install:
67
-
68
- ```powershell
69
- pi install -l npm:pi-monofold
70
- ```
71
-
72
- Pin a version:
73
-
74
- ```powershell
75
- pi install npm:pi-monofold@0.3.1
76
- ```
77
-
78
- Try without installing:
79
-
80
- ```powershell
81
- pi -e npm:pi-monofold
82
- ```
83
-
84
- ## Quick start
85
-
86
- 1. Install the extension (see [Install](#install)).
87
- 2. In your control repository, create `.pi/monofold.yaml` with at least one workspace entry (or run `/monofold:init`).
88
- 3. Start Pi in the control repository and run `/monofold:explore show the project workspaces`.
89
- 4. Use `/monofold:write` for routed Markdown outputs and `/monofold:git` for guarded git workflows.
90
-
91
- Example command flows: [docs/examples.md](./docs/examples.md).
92
-
93
- ## Usage summary
94
-
95
- | Surface | Purpose |
96
- |---------|---------|
97
- | `/monofold:explore` | List, read, search, or inspect workspace trees |
98
- | `/monofold:write` | Create routed Markdown outputs |
99
- | `/monofold:config` | Add or change workspaces and project workspaces |
100
- | `/monofold:git` | Run guarded git status, commit, push, or commit+push |
101
- | `/monofold:guide` | Interactive guide for common flows |
102
- | `/monofold:init` | Create or update `.pi/monofold.yaml` |
103
- | `/monofold:update` | Migrate legacy config and optionally request config edits |
104
-
105
- Agent tools (`monofold_list`, `monofold_read`, `monofold_write`, `monofold_git`, `monofold_init`) sit behind these commands. Full reference: [docs/usage.md](./docs/usage.md).
106
-
107
- ## Package contents
108
-
109
- ```text
110
- pi-monofold/
111
- ├── .github/workflows/
112
- │ ├── auto-release.yml # Auto-tag + release on merge to main
113
- │ ├── ci.yml # Validate on PR / push
114
- │ └── publish.yml # Publish to npm (Trusted Publishing)
115
- ├── docs/
116
- │ ├── usage.md # Config, commands, agent API, guard
117
- │ ├── examples.md # Command examples
118
- │ └── release.md # Release and publish flow
119
- ├── tests/
120
- │ └── focus-preset.test.ts
121
- ├── CHANGELOG.md
122
- ├── SECURITY.md
123
- ├── focus-preset.ts
124
- ├── index.ts
125
- ├── LICENSE
126
- ├── package.json
127
- ├── README.md
128
- ├── validation.ts
129
- └── tsconfig.json
130
- ```
131
-
132
- ## Development
133
-
134
- Clone and validate:
135
-
136
- ```powershell
137
- git clone https://github.com/eiei114/pi-monofold.git
138
- cd pi-monofold
139
- npm install
140
- npm run check
141
- ```
142
-
143
- Try the local checkout without installing:
144
-
145
- ```powershell
146
- pi -e .
147
- ```
148
-
149
- ## Release
150
-
151
- Releases are automated. See [docs/release.md](./docs/release.md) for details.
152
-
153
- 1. Bump `version` in `package.json` and update `CHANGELOG.md`.
154
- 2. Merge to `main`.
155
- 3. **Auto Release** tags `v<version>` and creates a GitHub release when the tag is new.
156
- 4. The tag triggers **Publish**, which publishes to npm with OIDC provenance.
157
-
158
- ## Security
159
-
160
- 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.
161
-
162
- Report vulnerabilities per [SECURITY.md](./SECURITY.md).
163
-
164
- ## Links
165
-
166
- - **Repository**: <https://github.com/eiei114/pi-monofold>
167
- - **npm**: <https://www.npmjs.com/package/pi-monofold>
168
- - **Issues**: <https://github.com/eiei114/pi-monofold/issues>
169
-
170
- ## License
171
-
172
- [MIT](LICENSE)
1
+ # pi-monofold
2
+
3
+ [![CI](https://github.com/eiei114/pi-monofold/actions/workflows/ci.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/ci.yml)
4
+ [![Publish](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml)
5
+ [![npm version](https://img.shields.io/npm/v/pi-monofold?color=cb3837&logo=npm)](https://www.npmjs.com/package/pi-monofold)
6
+ [![npm downloads](https://img.shields.io/npm/dw/pi-monofold)](https://www.npmjs.com/package/pi-monofold)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
8
+ [![Pi Package](https://img.shields.io/badge/Pi-package-6f42c1)](https://github.com/eiei114/pi-monofold)
9
+ [![Trusted Publishing](https://img.shields.io/badge/npm-provenance-yellow)](https://docs.npmjs.com/generating-provenance-statements)
10
+
11
+ Pi extension that folds multiple local repositories and folders into a guarded **Virtual Monorepo** for AI agents.
12
+
13
+ ## What this is
14
+
15
+ 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.
16
+
17
+ See [docs/usage.md](./docs/usage.md) for configuration, commands, agent tools, and guard behavior.
18
+
19
+ ## Features
20
+
21
+ - **Virtual monorepo manifest** — declare workspaces and project workspaces in `.pi/monofold.yaml`
22
+ - **Routed Markdown writes** — route PRDs, progress notes, and other doc types to configured folders
23
+ - **Workspace-aware reads** — list, read, search, and tree views scoped to readable workspaces, with bounded previews by default
24
+ - **Capability guard** — block or confirm `read` / `write` / `edit` / `grep` / `find` / `bash` based on workspace tags
25
+ - **Focus presets** — tag-based focus targets for the control workspace
26
+ - **Natural-language commands** — `/monofold:explore`, `/monofold:write`, `/monofold:config`, `/monofold:git`, and more
27
+ - **Strict agent tools** — `monofold_*` tools for programmatic access behind the command surface
28
+ - **Config migration** — upgrade legacy `.pi/monofold.yml` with backups and validation
29
+
30
+ ## Install
31
+
32
+ Pi Monofold is a Pi package. Install it with Pi's package installer from git or npm.
33
+
34
+ > Security: Pi packages run with full system access. Review packages before installing third-party code.
35
+
36
+ ### From git
37
+
38
+ ```powershell
39
+ pi install git:github.com/eiei114/pi-monofold
40
+ ```
41
+
42
+ Project-local install:
43
+
44
+ ```powershell
45
+ pi install -l git:github.com/eiei114/pi-monofold
46
+ ```
47
+
48
+ Pin a version:
49
+
50
+ ```powershell
51
+ pi install git:github.com/eiei114/pi-monofold@v0.3.2
52
+ ```
53
+
54
+ Try without installing:
55
+
56
+ ```powershell
57
+ pi -e git:github.com/eiei114/pi-monofold
58
+ ```
59
+
60
+ ### From npm
61
+
62
+ ```powershell
63
+ pi install npm:pi-monofold
64
+ ```
65
+
66
+ Project-local install:
67
+
68
+ ```powershell
69
+ pi install -l npm:pi-monofold
70
+ ```
71
+
72
+ Pin a version:
73
+
74
+ ```powershell
75
+ pi install npm:pi-monofold@0.3.2
76
+ ```
77
+
78
+ Try without installing:
79
+
80
+ ```powershell
81
+ pi -e npm:pi-monofold
82
+ ```
83
+
84
+ ## Quick start
85
+
86
+ 1. Install the extension (see [Install](#install)).
87
+ 2. In your control repository, create `.pi/monofold.yaml` with at least one workspace entry (or run `/monofold:init`).
88
+ 3. Start Pi in the control repository and run `/monofold:explore show the project workspaces`.
89
+ 4. Use `/monofold:write` for routed Markdown outputs and `/monofold:git` for guarded git workflows.
90
+
91
+ Example command flows: [docs/examples.md](./docs/examples.md).
92
+
93
+ ## Usage summary
94
+
95
+ | Surface | Purpose |
96
+ |---------|---------|
97
+ | `/monofold:explore` | List, read, search, or inspect workspace trees |
98
+ | `/monofold:write` | Create routed Markdown outputs |
99
+ | `/monofold:config` | Add or change workspaces and project workspaces |
100
+ | `/monofold:git` | Run guarded git status, commit, push, or commit+push |
101
+ | `/monofold:guide` | Interactive guide for common flows |
102
+ | `/monofold:init` | Create or update `.pi/monofold.yaml` |
103
+ | `/monofold:update` | Migrate legacy config and optionally request config edits |
104
+
105
+ Agent tools (`monofold_list`, `monofold_read`, `monofold_write`, `monofold_git`, `monofold_init`) sit behind these commands. Full reference: [docs/usage.md](./docs/usage.md).
106
+
107
+ ## Safe read defaults
108
+
109
+ `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.
110
+
111
+ | `monofold_read` mode | Default output |
112
+ |----------------------|----------------|
113
+ | **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. |
114
+ | **search** | Up to **50** match lines and **8,000** characters of ripgrep output. |
115
+ | **tree** | Up to **200** entries; traversal depth is capped at **5**. |
116
+
117
+ 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.
118
+
119
+ **Request more content intentionally:**
120
+
121
+ | Goal | `monofold_read` parameters |
122
+ |------|----------------------------|
123
+ | Full file body | `mode: "file"`, `includeContent: true` |
124
+ | Larger bounded file slice | `head`, `tail`, and/or `maxChars` (positive integers) |
125
+ | More search results | Higher `maxMatches` and/or `maxChars`, or a narrower `path` / `query` |
126
+ | Larger directory tree | Higher `maxEntries`, lower `depth`, or a narrower `path` |
127
+
128
+ 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).
129
+
130
+ ## Package contents
131
+
132
+ ```text
133
+ pi-monofold/
134
+ ├── .github/workflows/
135
+ │ ├── auto-release.yml # Auto-tag + release on merge to main
136
+ │ ├── ci.yml # Validate on PR / push
137
+ │ └── publish.yml # Publish to npm (Trusted Publishing)
138
+ ├── docs/
139
+ │ ├── usage.md # Config, commands, agent API, guard
140
+ │ ├── examples.md # Command examples
141
+ │ └── release.md # Release and publish flow
142
+ ├── tests/
143
+ │ └── focus-preset.test.ts
144
+ ├── CHANGELOG.md
145
+ ├── SECURITY.md
146
+ ├── focus-preset.ts
147
+ ├── index.ts
148
+ ├── LICENSE
149
+ ├── package.json
150
+ ├── README.md
151
+ ├── validation.ts
152
+ └── tsconfig.json
153
+ ```
154
+
155
+ ## Development
156
+
157
+ Clone and validate:
158
+
159
+ ```powershell
160
+ git clone https://github.com/eiei114/pi-monofold.git
161
+ cd pi-monofold
162
+ npm install
163
+ npm run check
164
+ ```
165
+
166
+ Try the local checkout without installing:
167
+
168
+ ```powershell
169
+ pi -e .
170
+ ```
171
+
172
+ ## Release
173
+
174
+ Releases are automated. See [docs/release.md](./docs/release.md) for details.
175
+
176
+ 1. Bump `version` in `package.json` and update `CHANGELOG.md`.
177
+ 2. Merge to `main`.
178
+ 3. **Auto Release** tags `v<version>` and creates a GitHub release when the tag is new.
179
+ 4. The tag triggers **Publish**, which publishes to npm with OIDC provenance.
180
+
181
+ ## Security
182
+
183
+ 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.
184
+
185
+ Report vulnerabilities per [SECURITY.md](./SECURITY.md).
186
+
187
+ ## Links
188
+
189
+ - **Repository**: <https://github.com/eiei114/pi-monofold>
190
+ - **npm**: <https://www.npmjs.com/package/pi-monofold>
191
+ - **Issues**: <https://github.com/eiei114/pi-monofold/issues>
192
+
193
+ ## License
194
+
195
+ [MIT](LICENSE)
@@ -0,0 +1,208 @@
1
+ export const DEFAULT_PREVIEW_LINES = 20;
2
+ export const DEFAULT_PREVIEW_CHARS = 2_000;
3
+
4
+ export type FileReadOptions = {
5
+ includeContent?: boolean;
6
+ maxChars?: number;
7
+ head?: number;
8
+ tail?: number;
9
+ };
10
+
11
+ export type FileReadFileStat = {
12
+ size: number;
13
+ mtime: Date;
14
+ };
15
+
16
+ export type FileReadDetails = {
17
+ byteSize: number;
18
+ characterCount: number;
19
+ lineCount: number;
20
+ modifiedTime: string;
21
+ truncated: boolean;
22
+ previewLineCount: number;
23
+ previewCharacterCount: number;
24
+ includeContent: boolean;
25
+ maxChars?: number;
26
+ head?: number;
27
+ tail?: number;
28
+ };
29
+
30
+ export type FileReadResponse = {
31
+ text: string;
32
+ details: FileReadDetails;
33
+ };
34
+
35
+ export function assertPositiveInt(value: number, label: string): number {
36
+ if (!Number.isInteger(value) || value <= 0) {
37
+ throw new Error(`${label} must be a positive integer`);
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function splitLines(content: string): string[] {
43
+ if (content.length === 0) {
44
+ return [""];
45
+ }
46
+ const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
47
+ const lines = normalized.split("\n");
48
+ if (lines.at(-1) === "" && (content.endsWith("\n") || content.endsWith("\r"))) {
49
+ lines.pop();
50
+ }
51
+ return lines.length === 0 ? [""] : lines;
52
+ }
53
+
54
+ function takeHeadLines(lines: string[], count: number): string[] {
55
+ return lines.slice(0, count);
56
+ }
57
+
58
+ function takeTailLines(lines: string[], count: number): string[] {
59
+ if (count >= lines.length) {
60
+ return lines;
61
+ }
62
+ return lines.slice(lines.length - count);
63
+ }
64
+
65
+ function truncateChars(text: string, maxChars: number): { text: string; truncated: boolean } {
66
+ if (text.length <= maxChars) {
67
+ return { text, truncated: false };
68
+ }
69
+ return { text: text.slice(0, maxChars), truncated: true };
70
+ }
71
+
72
+ export function truncationHint(details: FileReadDetails): string {
73
+ const parts: string[] = [];
74
+ if (details.truncated) {
75
+ parts.push(
76
+ `Showing ${details.previewLineCount} of ${details.lineCount} lines and ${details.previewCharacterCount} of ${details.characterCount} characters.`,
77
+ );
78
+ }
79
+ parts.push(
80
+ "Pass includeContent: true for full content, or use head, tail, and/or maxChars for a larger bounded range.",
81
+ );
82
+ return parts.join(" ");
83
+ }
84
+
85
+ function buildHeadTailPreview(lines: string[], head: number | undefined, tail: number | undefined): string {
86
+ const headCount = head ?? 0;
87
+ const tailCount = tail ?? 0;
88
+ if (headCount > 0 && tailCount > 0) {
89
+ const headLines = takeHeadLines(lines, headCount);
90
+ const tailLines = takeTailLines(lines, tailCount);
91
+ if (headCount + tailCount >= lines.length) {
92
+ return lines.join("\n");
93
+ }
94
+ const omitted = lines.length - headLines.length - tailLines.length;
95
+ return [
96
+ ...headLines,
97
+ `... [${omitted} line${omitted === 1 ? "" : "s"} omitted] ...`,
98
+ ...tailLines,
99
+ ].join("\n");
100
+ }
101
+ if (headCount > 0) {
102
+ return takeHeadLines(lines, headCount).join("\n");
103
+ }
104
+ if (tailCount > 0) {
105
+ return takeTailLines(lines, tailCount).join("\n");
106
+ }
107
+ return takeHeadLines(lines, DEFAULT_PREVIEW_LINES).join("\n");
108
+ }
109
+
110
+ function resolveExplicitLimits(options: FileReadOptions): {
111
+ includeContent: boolean;
112
+ maxChars?: number;
113
+ head?: number;
114
+ tail?: number;
115
+ } {
116
+ const includeContent = options.includeContent === true;
117
+ const maxChars = options.maxChars === undefined ? undefined : assertPositiveInt(options.maxChars, "maxChars");
118
+ const head = options.head === undefined ? undefined : assertPositiveInt(options.head, "head");
119
+ const tail = options.tail === undefined ? undefined : assertPositiveInt(options.tail, "tail");
120
+ return { includeContent, maxChars, head, tail };
121
+ }
122
+
123
+ export function buildFileReadResponse(
124
+ content: string,
125
+ options: FileReadOptions,
126
+ fileStat: FileReadFileStat,
127
+ context: { relativePath: string },
128
+ ): FileReadResponse {
129
+ const limits = resolveExplicitLimits(options);
130
+ const lines = splitLines(content);
131
+ const lineCount = lines.length;
132
+ const characterCount = content.length;
133
+
134
+ let previewText: string;
135
+ let truncated: boolean;
136
+ let previewLineCount: number;
137
+ let previewCharacterCount: number;
138
+
139
+ if (limits.includeContent && limits.maxChars === undefined && limits.head === undefined && limits.tail === undefined) {
140
+ previewText = content;
141
+ truncated = false;
142
+ previewLineCount = lineCount;
143
+ previewCharacterCount = characterCount;
144
+ } else if (limits.head !== undefined || limits.tail !== undefined) {
145
+ const headCount = limits.head ?? 0;
146
+ const tailCount = limits.tail ?? 0;
147
+ previewText = buildHeadTailPreview(lines, limits.head, limits.tail);
148
+ const maxChars = limits.maxChars ?? Number.POSITIVE_INFINITY;
149
+ const charResult = truncateChars(previewText, maxChars);
150
+ previewText = charResult.text;
151
+ const linesFullyShown =
152
+ headCount + tailCount >= lineCount ||
153
+ (headCount > 0 && tailCount === 0 && headCount >= lineCount) ||
154
+ (tailCount > 0 && headCount === 0 && tailCount >= lineCount);
155
+ truncated = charResult.truncated || !linesFullyShown;
156
+ previewLineCount = previewText === "" ? 0 : previewText.split("\n").length;
157
+ previewCharacterCount = previewText.length;
158
+ } else if (limits.maxChars !== undefined) {
159
+ const charResult = truncateChars(content, limits.maxChars);
160
+ previewText = charResult.text;
161
+ truncated = charResult.truncated || previewText.length < content.length;
162
+ previewLineCount = previewText === "" ? 0 : previewText.split("\n").length;
163
+ previewCharacterCount = previewText.length;
164
+ } else {
165
+ const defaultLines = takeHeadLines(lines, DEFAULT_PREVIEW_LINES);
166
+ previewText = defaultLines.join("\n");
167
+ const charResult = truncateChars(previewText, DEFAULT_PREVIEW_CHARS);
168
+ previewText = charResult.text;
169
+ truncated = charResult.truncated || defaultLines.length < lines.length;
170
+ previewLineCount = previewText === "" ? 0 : previewText.split("\n").length;
171
+ previewCharacterCount = previewText.length;
172
+ }
173
+
174
+ const details: FileReadDetails = {
175
+ byteSize: fileStat.size,
176
+ characterCount,
177
+ lineCount,
178
+ modifiedTime: fileStat.mtime.toISOString(),
179
+ truncated,
180
+ previewLineCount,
181
+ previewCharacterCount,
182
+ includeContent: limits.includeContent,
183
+ ...(limits.maxChars === undefined ? {} : { maxChars: limits.maxChars }),
184
+ ...(limits.head === undefined ? {} : { head: limits.head }),
185
+ ...(limits.tail === undefined ? {} : { tail: limits.tail }),
186
+ };
187
+
188
+ const metadataLines = [
189
+ `Path: ${context.relativePath}`,
190
+ `Byte size: ${details.byteSize}`,
191
+ `Characters: ${details.characterCount}`,
192
+ `Lines: ${details.lineCount}`,
193
+ `Modified: ${details.modifiedTime}`,
194
+ ];
195
+
196
+ const sections = [metadataLines.join("\n")];
197
+ if (previewText.length > 0) {
198
+ sections.push("", "--- preview ---", previewText);
199
+ }
200
+ if (truncated) {
201
+ sections.push("", `[truncated] ${truncationHint(details)}`);
202
+ }
203
+
204
+ return {
205
+ text: sections.join("\n"),
206
+ details,
207
+ };
208
+ }