npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pi-file-injector

v0.1.4

Published

#@file — inject the whole file into your Pi prompt, every time and everywhere, only when you want it. Renders as a compact read-tool line.

Readme

#@file

A Pi extension that injects whole files and web pages into your prompt. Write #@ before a file path, or # before a URL. 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.

Why

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.

#@ 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.

#@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).

Install

Published on npm:

pi install npm:pi-file-injector

Or, directly from git:

pi install git:github.com/dabstractor/pi-file-injector

Restart Pi if a session is already open. To uninstall, run pi remove npm:pi-file-injector (or pi remove git:github.com/dabstractor/pi-file-injector / the local path you used with -e for git/local installs).

⚠️ Only one copy at a time. This extension deduplicates injections within a single loaded copy; it cannot deduplicate against a second, concurrently-loaded copy. If two copies are active at once (e.g. a global npm/git install and a -e ./file-injector.ts dev copy), every #@file is injected twice and image cost doubles. If you are upgrading from an older pi-file-injector, remove the prior install (pi remove … for every install kind — npm, git, and local path) before installing this version. Check with pi list and confirm pi-file-injector appears only once.

Usage

Write #@ and a path anywhere in your prompt:

Review #@a.ts
Review #@a.ts:10
Describe #@pic.png
Summarize #@~/notes.md
Diff #@a.ts vs #@b.ts
See #@a.ts.

Optional line range (1-indexed, inclusive):

  • #@a.ts:10 — only line 10
  • #@a.ts:10-15 — lines 10 through 15

Exact filenames win: a file literally named a.ts:10 still resolves as-is.

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. #@ triggers stay in your message exactly as you typed them (Review #@a.ts stays Review #@a.ts), so cancelling and re-opening, forking, or re-submitting re-triggers injection. The file bytes are delivered to the model underneath — never pasted into your message bubble.

URLs

Write # before a URL anywhere in your prompt to fetch the page, extract it to markdown, and inject that — the same way #@ injects a file:

Summarize #example.com
What does #https://example.com/api return?
Diff #https://news.ycombinator.com vs #@local-notes.md

Both #example.com (bare domain) and #https://example.com/api (full URL) work. On submit the page is fetched, the boilerplate is stripped, and the main content is converted to markdown by defuddle before it reaches the model. Each URL renders as a green read <url> line — identical to the read tool and to #@file — and ctrl+o expands it to the extracted markdown. The # trigger stays in your message exactly as you typed it, so cancelling and re-opening, forking, or re-submitting re-fetches the page.

Before / after:

# you type:
Summarize #example.com
# you see (green line, ctrl+o to expand):
read https://example.com (ctrl+o to expand)
# the model receives:
<file name="https://example.com">
# Page Title

…extracted markdown body…
</file>

By content type:

  • HTML pages → extracted to markdown (boilerplate/nav/scripts stripped) and injected.
  • Raw text, JSON, XML, RSS, Atom → injected verbatim (no extraction).
  • Images (#https://example.com/cat.png) → attached as an image, same as #@image. An image URL whose body comes back empty (0 bytes) attaches nothing — it delivers the same "empty image file — 0 bytes" note a 0-byte local image does.
  • Anything else (e.g. a PDF, unknown content type) → left as written; nothing is injected.

The extraction libraries (defuddle and friends) are bundled with the package — you install nothing beyond pi install. See Limits for the fetch timeout, size cap, and the no-paging / no-caching behavior. # is disjoint from #@, so #@file.txt and #https://example.com in the same prompt both work.

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. The import marker stays in spec.md verbatim (same as a top-level marker):

#@spec.md          # spec.md contains: see #@api.md

Path completion works in the editor. Type #@ and the same file list Pi shows for @ appears; Tab completes it as #@<path>.

Bare @ is unchanged, so Review @a.ts behaves as before.

What gets injected

| File | Result | |---|---| | 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. | | Image (.png, .jpg, .jpeg, .gif, .webp, .bmp) | Attached as an image. | | Other binary | Not injected. A short note says it was skipped. | | Missing file, directory, or permission error | Left as written. Nothing is injected. |

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.

Text uses Pi's native block format, the same one @file uses:

<file name="/abs/path/to/file.ts">
<entire file contents>
</file>

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 exactly what you typed — including the #@ markers — so re-opening or forking re-triggers injection automatically.

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.

Syntax

#@<path> is the trigger #@ followed by a path. The path is a run of non-whitespace characters.

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.

Line range. #@a.ts:10 delivers only line 10. #@a.ts:10-15 delivers lines 10–15 inclusive. The collapsed read line shows the range that was actually delivered: read a.ts:10 or read a.ts:10-15 — and read a.ts:2-5 for #@a.ts:2-100000 on a 5-line file, since an end past the file clamps to the last line. A closed range that exceeds the remaining context budget is delivered as a head slice plus a paging directive, exactly like a whole file (no paging past the selection unless the budget demands it) — the directive resumes reading at the file's absolute line, so the model continues exactly where the head slice stopped. Images/binaries ignore :N / :N-M and dedup on the path alone: #@pic.png #@pic.png:3 attaches the image once — identical bytes are never delivered twice. Different ranges of the same file each inject: #@a.ts:10 #@a.ts:20 is two blocks; #@a.ts:10 #@a.ts is the slice plus the whole file. The same path+range still collapses to one (#@a.ts:10 #@a.ts:10). A malformed range — :0, or :5-3 (end before start) — injects nothing: the token is left exactly as you typed it and a one-line warning explains why, both in your prompt and inside a delivered markdown file. A start past the end of the file fails the same way — the token stays verbatim, the warning names the file's line count, and you never get an empty <file> block.

Trailing punctuation is trimmed. #@a.ts. resolves to a.ts. #@a.ts:10. resolves to line 10 of a.ts. (#@a.txt) resolves to a.txt. Trimmed characters:

. , ; : ! ? " ' ) ] } >

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.)

Paths: relative (against your current directory), absolute (#@/etc/hosts), tilde (#@~/notes.md), and ../ all work.

Markdown imports: a #@ inside a delivered .md or .markdown file is itself an import, using the same grammar. Five rules narrow it:

  • 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.
  • 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.
  • 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.
  • Each path+range is injected at most once. #@a.ts twice is one block; #@a.ts:10 and #@a.ts:20 are two. Shared dependencies and cycles still terminate.
  • 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.

Optional: bare-@ markdown imports

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.

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:

// ~/.pi/agent/file-injector.json — dedicated file
{ "markdownBareAtImports": true }
// or, inside ~/.pi/agent/settings.json — namespaced key
{
  "defaultModel": "anthropic/claude-sonnet-4",
  "fileInjector": { "markdownBareAtImports": true }
}

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):

  1. Global ~/.pi/agent/settings.jsonfileInjector
  2. Global ~/.pi/agent/file-injector.json
  3. Project .pi/settings.jsonfileInjectortrusted project only
  4. Project .pi/file-injector.jsontrusted project only

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.)

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.

It affects markdown content only — a bare @path you type in your prompt is never injected. See Limits.

URLs: enableUrls (network egress)

URL injection is on by default. Set enableUrls to false to disable all network egress — every #<url> token is then left verbatim and no request is made (the air-gapped opt-out):

// ~/.pi/agent/settings.json — namespaced key
{
  "fileInjector": { "enableUrls": false }
}
// or, ~/.pi/agent/file-injector.json — dedicated file
{ "enableUrls": false }

enableUrls is read from the same four sources and precedence as markdownBareAtImports (above): global settings.json → global file-injector.json → project settings.json → project file-injector.json (project sources honored only in a trusted project). It is read once when a session starts and cached for that session. The default is true, so #example.com works with no configuration at all.

Limits

  • 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.
  • 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.
  • No directories. #@src/ is left as-is. Use a read or ls tool.
  • No globs. #@src/*.ts is a literal path, not a pattern. It resolves only if a file named *.ts exists.
  • A backtick right after #@ blocks it. Inside a code span like `#@a.ts`, nothing is injected. To suppress #@ anywhere, write # @ with a space.
  • Markdown imports are relative-only. A #@ inside a .md/.markdown file that points at an absolute or tilde path is ignored, never resolved.
  • 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.
  • 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.
  • 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.
  • URLs: 20 s fetch timeout. A page that doesn't respond in 20 seconds is left as written.
  • URLs: 1 MB response cap. A page whose body exceeds 1 MB is left as written (not paged).
  • URLs: no caching. Every injection fetches fresh — cancelling and re-opening, forking, or re-submitting re-fetches the page.
  • URLs: JS-rendered pages fall back to verbatim. Extraction works on server-delivered HTML only. A single-page app that loads its content with JavaScript usually yields too little to extract, so the #<url> token is left as a reference (with a short notice) instead.
  • URLs never page. Unlike #@file (which pages oversize files through the read tool), an over-budget URL is left verbatim — the read tool can't fetch a URL.
  • URLs need a dotted, alphabetic hostname. A #<url> token must be an http(s):// or ftp:// URL or a bare host whose final label is 2+ letters (e.g. example.com, api.example.co.uk). This rejects #3.14, #v1.2, #fff and other token-like text. In a coding agent a bare #word.ext is almost always a file reference rather than a website, so a scheme-less token with no path whose final label is a common code/file extension — #main.go, #notes.md, #config.json, #node.js, … — is treated as a file reference and left as written (no fetch, nothing injected). A slash makes it a real domain plus a path, so #example.com/img.png is still fetched; and an explicit scheme bypasses the check entirely, so to fetch a domain whose TLD collides with a code extension, write #https://foo.sh rather than #foo.sh. As a side effect, raw IP addresses and localhost are not detected as URLs#127.0.0.1:8080, #localhost:3000/api, and even #http://127.0.0.1 are left verbatim with no fetch and no error. To inject a local dev server, give it a resolvable hostname (an /etc/hosts alias, a *.local name, or a real domain) rather than an IP or localhost. ftp:// is recognized but never injects: Node's fetch cannot retrieve ftp, so the fetch is genuinely attempted, fails, and the token is left as written (the loading indicator may flash briefly).

#@ versus `@

  • #@file injects the whole file, always, everywhere.
  • @file is Pi's built-in autocomplete and command-line argument handling. This extension does not change it.

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.