pi-file-injector 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.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/file-injector.ts +1412 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dustin Schultz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# `#@file`
|
|
2
|
+
|
|
3
|
+
A [Pi](https://github.com/earendil-works/pi) extension that injects a whole file into your prompt when you write `#@` before the path. The file reaches the model before it replies, the whole file always reaches the model — injected whole when it fits the remaining context, paged via the `read` tool when it doesn't — with no configuration.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Pi's built-in `@file` injects a file only when you pass it on the command line before a session starts. In the editor, `@` is path autocomplete, and the model has to call a tool to read the file itself.
|
|
8
|
+
|
|
9
|
+
`#@` always delivers the entire file to the model, in every context: the editor, a `pi -p` one-shot, and RPC. When the file fits the remaining context it is injected whole; when it exceeds the budget it is delivered as a head block plus a paging directive that the model reads through.
|
|
10
|
+
|
|
11
|
+
`#@spec.md` pulls in everything `spec.md` references with the same `#@` directive — spec-and-its-dependencies in one token, loop-safe via dedup (each file is injected at most once).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Published on npm:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:pi-file-injector
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or, directly from git:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi install git:github.com/dabstractor/pi-file-injector
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Restart Pi if a session is already open. To uninstall, run `pi remove npm:pi-file-injector`.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Write `#@` and a path anywhere in your prompt:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
Review #@a.ts
|
|
35
|
+
Describe #@pic.png
|
|
36
|
+
Summarize #@~/notes.md
|
|
37
|
+
Diff #@a.ts vs #@b.ts
|
|
38
|
+
See #@a.ts.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
On submit, each file shows up as a compact green `read <path>` line directly below your message — one line per file, indistinguishable from the `read` tool. Press `ctrl+o` to expand any of them to the full contents. The `#@` trigger is stripped from each reference, so `Review #@a.ts` appears in your message as `Review a.ts`, with the file delivered to the model underneath — never pasted into your message bubble.
|
|
42
|
+
|
|
43
|
+
Markdown files can import other files. If `spec.md` itself contains `#@api.md`, a single `#@spec.md` delivers both — `spec.md` first, then `api.md` — and the import marker is stripped from `spec.md` the same way a top-level marker is:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
#@spec.md # spec.md contains: see #@api.md
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Path completion works in the editor. Type `#@` and the same file list Pi shows for `@` appears; Tab completes it as `#@<path>`.
|
|
50
|
+
|
|
51
|
+
Bare `@` is unchanged, so `Review @a.ts` behaves as before.
|
|
52
|
+
|
|
53
|
+
## What gets injected
|
|
54
|
+
|
|
55
|
+
| File | Result |
|
|
56
|
+
|---|---|
|
|
57
|
+
| Text (`.ts`, `.md`, `.json`, `.log`, etc.) | Entire contents injected when they fit remaining context. Oversize files are delivered as a head block plus a paging directive — the model reads the rest via the `read` tool. Never silently truncated. |
|
|
58
|
+
| Image (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.bmp`) | Attached as an image. |
|
|
59
|
+
| Other binary | Not injected. A short note says it was skipped. |
|
|
60
|
+
| Missing file, directory, or permission error | Left as written. Nothing is injected. |
|
|
61
|
+
|
|
62
|
+
A delivered markdown file (`.md` or `.markdown`) is also scanned for relative `#@` imports. Each import it references is delivered as its own block, and is scanned in turn if it is also markdown — so a single `#@spec.md` can pull in a whole tree of docs. The same file-type rules (text / image / binary / missing) apply to each import unchanged.
|
|
63
|
+
|
|
64
|
+
Text uses Pi's native block format, the same one `@file` uses:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
<file name="/abs/path/to/file.ts">
|
|
68
|
+
<entire file contents>
|
|
69
|
+
</file>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That's what the model receives. You won't see it as raw text in the chat — each injected file renders as a green `read <path>` line (just like the `read` tool), with `ctrl+o` to expand. Your own message shows only what you typed.
|
|
73
|
+
|
|
74
|
+
Images are matched by their real bytes, not just the extension. A text file renamed `fake.png` is injected as text, not attached as a broken image. The check cuts both ways: a real image saved with the wrong extension — a PNG named `photo.jpg`, say — is not attached, because its bytes don't match the `.jpg` signature; it's delivered as a binary note instead. Rename it to its real type to attach it. An empty (0-byte) image attaches nothing.
|
|
75
|
+
|
|
76
|
+
## Syntax
|
|
77
|
+
|
|
78
|
+
`#@<path>` is the trigger `#@` followed by a path. The path is a run of non-whitespace characters.
|
|
79
|
+
|
|
80
|
+
**Where it matches:** at the start of the prompt, or right after a non-word character (a space, `(`, `[`, `>`, etc.). It does not match mid-word, so `foo#@bar` injects nothing. This holds in any language: `café#@x`, `Öster#@x`, and `日本語#@x` inject nothing.
|
|
81
|
+
|
|
82
|
+
**Trailing punctuation is trimmed.** `#@a.ts.` resolves to `a.ts`. `(#@a.txt)` resolves to `a.txt`. Trimmed characters:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
. , ; : ! ? " ' ) ] } >
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Extensions are exact.** A reference that already ends in a file extension is matched by that exact name. A missing `#@report.md.bak` is left as written — it never silently resolves to an existing `report.md`, so the model never receives a different file than the one you named. (Markdown formatting glued to a name is different: emphasis like `*@b.md*` or `**@b.md**` is still trimmed, so the file resolves.)
|
|
89
|
+
|
|
90
|
+
**Paths:** relative (against your current directory), absolute (`#@/etc/hosts`), tilde (`#@~/notes.md`), and `../` all work.
|
|
91
|
+
|
|
92
|
+
**Markdown imports:** a `#@` inside a delivered `.md` or `.markdown` file is itself an import, using the same grammar. Five rules narrow it:
|
|
93
|
+
|
|
94
|
+
- **Relative paths only.** Imports resolve against the markdown file's own directory, not your current directory. Absolute (`#@/etc/hosts`) and tilde (`#@~/notes.md`) imports inside a markdown file are ignored and left verbatim. If the same name exists in both the importing file's directory and your current directory, the importing file's directory wins — so `#@file2.md` inside `dir/otherdir/some/file.md` resolves to `dir/otherdir/some/file2.md`, never `./file2.md`. This holds at every nesting depth; your current directory is never consulted for an in-file import.
|
|
95
|
+
- **Extension shorthand.** A markdown import may omit the `.md`/`.markdown` extension: `#@PRD` resolves to `PRD.md` (then `PRD.markdown`) when no bare `PRD` exists. Exact match wins (a bare `readme` beats `readme.md`), and a token already ending in any extension is left as-is (so `#@PRD.md` never becomes `PRD.md.md`). This is a markdown-import convenience only — at the prompt you type the full name.
|
|
96
|
+
- **Code is the escape hatch.** A `#@` inside a fenced or inline code span is not an import — it stays verbatim. So a doc can show `` `#@example.ts` `` as an example without importing anything. Fenced-code detection is line-ending agnostic — Windows (CRLF) and Unix (LF) markdown files detect code fences identically.
|
|
97
|
+
- **Each file is injected at most once.** Across the whole prompt — top-level tokens, every import, and cycles — a given file appears in one block only. Shared dependencies dedup; cycles terminate.
|
|
98
|
+
- **Shared budget.** Imports draw on the same context budget as the top-level prompt. When the running total exceeds the window, later files page (head block plus a `read`-tool directive) instead of overflow.
|
|
99
|
+
|
|
100
|
+
### Optional: bare-`@` markdown imports
|
|
101
|
+
|
|
102
|
+
Off by default — `#@` works with no config at all, and stays the only thing that ever triggers injection at the prompt. This is the one opt-in.
|
|
103
|
+
|
|
104
|
+
If your docs already reference files as a bare `@file.md` (no `#`), you can make a delivered markdown file treat that the same way as `#@file.md`. Set `markdownBareAtImports` to `true`. It can live in either of two forms — a dedicated file, or under the `fileInjector` key inside Pi's own `settings.json`, alongside your other Pi settings:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
// ~/.pi/agent/file-injector.json — dedicated file
|
|
108
|
+
{ "markdownBareAtImports": true }
|
|
109
|
+
```
|
|
110
|
+
```jsonc
|
|
111
|
+
// or, inside ~/.pi/agent/settings.json — namespaced key
|
|
112
|
+
{
|
|
113
|
+
"defaultModel": "anthropic/claude-sonnet-4",
|
|
114
|
+
"fileInjector": { "markdownBareAtImports": true }
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Both forms are read from a global and a project location and shallow-merged in this order (each later one wins; within the same scope the dedicated file beats the `settings.json` key):
|
|
119
|
+
|
|
120
|
+
1. Global `~/.pi/agent/settings.json` → `fileInjector`
|
|
121
|
+
2. Global `~/.pi/agent/file-injector.json`
|
|
122
|
+
3. Project `.pi/settings.json` → `fileInjector` — **trusted project only**
|
|
123
|
+
4. Project `.pi/file-injector.json` — **trusted project only**
|
|
124
|
+
|
|
125
|
+
The project sources are honored only in a trusted project, so an untrusted checkout can't turn it on. (`settings.json` is open-schema, so Pi preserves the `fileInjector` key through `/settings` edits.)
|
|
126
|
+
|
|
127
|
+
When it's on, a bare `@api.md` inside a delivered markdown file imports exactly like `#@api.md`: relative-only paths, extension shorthand, code-exempt, deduped against everything else, and drawing on the same shared budget. `#@` keeps working unchanged and is never matched twice — a `#@api.md` is one import, not two. A missing or malformed source (or one that doesn't set the key) leaves everything at the default, so it never errors. This is uniform at **every** depth: the first file a top-level `#@` token pulls in is not special-cased — its bare `@` imports are honored exactly like those in files deeper in the chain.
|
|
128
|
+
|
|
129
|
+
It affects markdown content only — a bare `@path` you type in your prompt is never injected. See [Limits](#limits).
|
|
130
|
+
|
|
131
|
+
## Limits
|
|
132
|
+
|
|
133
|
+
- **No size knob.** `#@` has no user-facing size setting. Oversize text files are delivered as a head block (first ~8 KB) plus a paging directive; the model reads the rest via the `read` tool. The model never holds a file larger than its context window all at once — that is a property of the medium, not of this extension.
|
|
134
|
+
- **No spaces in paths.** A space ends the path, so `#@my file.txt` injects a file named `my`. Use the `read` tool for files with spaces.
|
|
135
|
+
- **No directories.** `#@src/` is left as-is. Use a `read` or `ls` tool.
|
|
136
|
+
- **No globs.** `#@src/*.ts` is a literal path, not a pattern. It resolves only if a file named `*.ts` exists.
|
|
137
|
+
- **A backtick right after `#@` blocks it.** Inside a code span like `` `#@a.ts` ``, nothing is injected. To suppress `#@` anywhere, write `# @` with a space.
|
|
138
|
+
- **Markdown imports are relative-only.** A `#@` inside a `.md`/`.markdown` file that points at an absolute or tilde path is ignored, never resolved.
|
|
139
|
+
- **Only markdown is scanned.** A `#@` inside an injected `.ts`, `.json`, image, or any other non-markdown file is inert — only `.md`/`.markdown` pull in further files.
|
|
140
|
+
- **Bare-`@` imports stay inside markdown.** Even with `markdownBareAtImports` on, a bare `@path` in your prompt is never injected — the setting only changes what a delivered markdown file pulls in. `#@` remains the sole prompt-level trigger.
|
|
141
|
+
- **No autocomplete for in-file imports.** The `#@` path completer runs in the editor prompt only. Import directives live inside markdown files (written by hand), where your editor's normal file completion applies.
|
|
142
|
+
|
|
143
|
+
## `#@` versus `@`
|
|
144
|
+
|
|
145
|
+
- `#@file` injects the whole file, always, everywhere.
|
|
146
|
+
- `@file` is Pi's built-in autocomplete and command-line argument handling. This extension does not change it.
|
|
147
|
+
|
|
148
|
+
Use `#@` when you want all of a file. Use `@` or the `read` tool when you want to browse or search without loading the whole file.
|