pi-quiver 3.1.1 → 3.2.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/CHANGELOG.md +8 -0
- package/README.md +86 -113
- package/fast-mode.ts +182 -0
- package/fetch.ts +10 -3
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,14 @@ Published to npm as `pi-quiver` (`pi install npm:pi-quiver`). Pushing a
|
|
|
8
8
|
via OIDC trusted publishing. The release helper at
|
|
9
9
|
`.agents/skills/release/scripts/release.sh` cuts the tag; CI publishes.
|
|
10
10
|
|
|
11
|
+
## v3.2.0 - 2026-07-12
|
|
12
|
+
|
|
13
|
+
- Add `fast-mode` extension: opt-in Anthropic fast mode for Claude Opus 4.8 (`speed: "fast"` payload + `fast-mode-2026-02-01` beta header), controlled via `fastMode` settings key, `--fast` flag, and `/fast [on|off|status]`. OFF by default. Preserves OAuth identity betas. Requires pi bundling `@earendil-works/pi-coding-agent` >= 0.80.5 (the `before_provider_headers` hook).
|
|
14
|
+
|
|
15
|
+
## v3.1.2 - 2026-07-07
|
|
16
|
+
|
|
17
|
+
- **`fetch` routes GitHub Actions run URLs through `gh`.** `github.com/{owner}/{repo}/actions/runs/{id}` URLs are served by `gh run view {id} --repo {owner}/{repo}` and returned through the existing size gate with a `Source: gh run view ...` header, alongside the existing issue/PR/repo routing. Only the bare run URL routes; deeper paths (`.../runs/{id}/jobs/{jobId}`, `.../actions/workflows/{file}`) fall back to HTTP. Falls back silently when `gh` is absent/unauthenticated/errors; `raw=true` forces the rendered page.
|
|
18
|
+
|
|
11
19
|
## v3.1.1 - 2026-07-05
|
|
12
20
|
|
|
13
21
|
Branding, funding, and gallery preview. No behavior change.
|
package/README.md
CHANGED
|
@@ -6,146 +6,89 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://buymeacoffee.com/jjurasszek)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Ground-truth ingestion for the [Pi coding agent](https://github.com/earendil-works/pi): pull real web pages, docs, and local files into context without flooding it.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## The problem
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|---|---|---|
|
|
15
|
-
| `fetch.ts` | `fetch` | Retrieve URLs over HTTP(S). HTML → Markdown (main-content extraction, stripped boilerplate). Binary content saved untouched to a temp file. **Context-safe:** output over 32 KB or 1000 lines is written to a temp file with a preview + file path. Prevents a single fetch from flooding the context window. |
|
|
16
|
-
| `doc_to_md.ts` | `doc_to_md` | Convert a local PDF/DOCX/PPTX to Markdown. High-fidelity via `pymupdf4llm` (run through `uv`, fetched on first use); degraded pure-JS fallback (`unpdf`) when `uv`/Python is unavailable or conversion times out. DOCX/PPTX convert via LibreOffice (`soffice`) to PDF first. Same 32 KB / 1000-line size gate as `fetch`. |
|
|
17
|
-
| `session-name.ts` | `/session-name` | Name work sessions. Manual `/session-name [name]` always works. **OFF by default:** when opted in via `settings.json`, after the first agent turn it asks the current model for a concise session name + short tab label and applies them, and renames the **Ghostty** tab via OSC 2 (only when the active terminal is really Ghostty), re-asserting it each turn so the tab tracks the session name. |
|
|
18
|
-
| `sword-header.ts` | `/builtin-header` | Replace the TUI startup logo with a theme-colored ASCII greatsword (hilt = accent, blade = text). **OFF by default:** only installs the header when opted in via `settings.json`. `/builtin-header` restores the built-in header at runtime. |
|
|
19
|
-
|
|
20
|
-
## Prerequisites
|
|
21
|
-
|
|
22
|
-
The npm package's bundled JS deps install automatically on `pi install` - nothing to set up there. A few **runtime system binaries** are optional; each degrades gracefully when absent:
|
|
23
|
-
|
|
24
|
-
| Prerequisite | Needed by | If absent |
|
|
25
|
-
|---|---|---|
|
|
26
|
-
| `gh` (GitHub CLI, installed + `gh auth login`) | `fetch` GitHub issue/PR/repo routing | Falls back to an HTTP fetch of the rendered page (private repos hit a login wall). |
|
|
27
|
-
| `uv` (+ managed Python 3.14, fetched on first use) | `doc_to_md` high-fidelity PDF conversion | Degrades to the pure-JS `unpdf` fallback (no faithful tables/headings). |
|
|
28
|
-
| LibreOffice (`soffice` on `PATH`) | `doc_to_md` DOCX/PPTX conversion | Office inputs error (no JS fallback for office->PDF); PDFs unaffected. |
|
|
29
|
-
|
|
30
|
-
None is a hard install-time dependency of the package; they are tools you provide in the environment where pi runs.
|
|
31
|
-
|
|
32
|
-
### fetch — content routing & context hygiene
|
|
33
|
-
|
|
34
|
-
`fetch` is the main way an agent pulls external bytes into context. This extension routes responses by type to keep context tight:
|
|
35
|
-
|
|
36
|
-
**HTML → Markdown:**
|
|
37
|
-
- Mozilla Readability extracts main content, strips navigation/chrome/boilerplate
|
|
38
|
-
- Turndown converts to Markdown with GFM support (pipe tables, fenced code blocks, ATX headings)
|
|
39
|
-
- Page title becomes a top-level `#` heading
|
|
40
|
-
- Download cap: **1 MB**
|
|
41
|
-
|
|
42
|
-
**Binary (images, PDFs, archives, fonts, audio/video) → temp file:**
|
|
43
|
-
- Streamed untouched to `${TMPDIR}/pi-fetch/<stamp>-<host>-<hash>.<ext>` without decoding
|
|
44
|
-
- Detection: content-type check + NUL-byte sniff in first ≤64 KB (catches mislabeled payloads)
|
|
45
|
-
- Returns: status, content-type, size, file path — **no preview**
|
|
46
|
-
- Download cap: **50 MB**
|
|
47
|
-
|
|
48
|
-
**Text / Markdown / JSON size gate:**
|
|
49
|
-
- Inline when **≤ 32 KB AND ≤ 1000 lines** (converted output size)
|
|
50
|
-
- Otherwise **spills to file** with:
|
|
51
|
-
- HTTP status, content-type, charset, byte/line counts
|
|
52
|
-
- File path (`Saved-To:`)
|
|
53
|
-
- 60-line preview
|
|
54
|
-
- Instruction to `grep` (Markdown is grep-able by heading: `^#`) or `read` slices
|
|
13
|
+
Reasoning from a model's training memory instead of the real page, the current docs, or the actual PDF is how agents confidently ship wrong answers about APIs that changed last month. Mature engineering work has to be data-driven - the agent needs to read the real source.
|
|
55
14
|
|
|
56
|
-
|
|
15
|
+
But the moment an agent does that, one `fetch` or PDF read can dump hundreds of kilobytes of boilerplate into context, degrading every turn after it.
|
|
57
16
|
|
|
58
|
-
|
|
17
|
+
## Why pi-quiver exists
|
|
59
18
|
|
|
60
|
-
|
|
61
|
-
- `raw=true`: Skip HTML→Markdown and JSON pretty-printing; return decoded body as-is (still subject to the size gate).
|
|
62
|
-
- `raw=true` also bypasses GitHub `gh` routing (forces the HTTP/rendered path).
|
|
19
|
+
`fetch` and `doc_to_md` bring real web pages, GitHub issues/PRs, and local PDF/DOCX/PPTX files into context - and every result is size-gated by construction: over 32 KB or 1000 lines spills to a temp file with a preview and a grep/read hint, so a single call can never flood the window. Ingestion is what makes data-driven work possible; the gate is what keeps it safe.
|
|
63
20
|
|
|
64
|
-
|
|
21
|
+
`session-name` and `sword-header` are smaller, opt-in ergonomics on top - session labeling and a themed startup header.
|
|
65
22
|
|
|
66
|
-
|
|
23
|
+
## Part of the pi agent toolkit
|
|
67
24
|
|
|
68
|
-
|
|
25
|
+
Four independent extensions for the [pi coding agent](https://github.com/earendil-works/pi), each owning one concern of running agents seriously:
|
|
69
26
|
|
|
70
|
-
|
|
27
|
+
- **pi-quiver** - capabilities (fetch, doc conversion, session tools)
|
|
28
|
+
- [pi-cohort](https://github.com/jjuraszek/pi-cohort) - coordination (delegate to focused child agents)
|
|
29
|
+
- [pi-condense](https://github.com/jjuraszek/pi-condense) - context economy (prune context, keep it recoverable)
|
|
30
|
+
- [pi-gauntlet](https://github.com/jjuraszek/pi-gauntlet) - process (the gated brainstorm->ship workflow)
|
|
71
31
|
|
|
72
|
-
|
|
32
|
+
No code dependency between them. pi-quiver is call-level: it gates the size of what comes *in*. [pi-condense](https://github.com/jjuraszek/pi-condense) is loop-level: it prunes what's already *in context* once a tool call is done. Different problem, same discipline.
|
|
73
33
|
|
|
74
|
-
|
|
75
|
-
- **Fallback — `unpdf`** (pure JS, bundled PDF.js). Used when `uv` is not on `PATH`, the warm probe fails, or a conversion times out. Output is plain text with page breaks — **no faithful tables/headings**. Degraded results are marked in the output (`[Note: degraded extraction via unpdf ...]`) and carry a `Fallback-Reason:` line.
|
|
34
|
+
## Mental model
|
|
76
35
|
|
|
77
|
-
|
|
36
|
+
Every extension here is context-safe by construction, not by convention: the size check runs on every call, there's no flag to forget. Two tools bring real sources in (`fetch`, `doc_to_md`); two are opt-in ergonomics (`session-name`, `sword-header`).
|
|
78
37
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
| `PI_DOC_TO_MD_CONVERT_TIMEOUT_MS` | `60000` | Per-document conversion budget (also bounds the `unpdf` fallback) |
|
|
88
|
-
| `PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS` | `120000` | LibreOffice `.docx`/`.pptx` → PDF budget |
|
|
89
|
-
|
|
90
|
-
Python is pinned to **3.14** and is not configurable.
|
|
91
|
-
|
|
92
|
-
**Runtime dependencies:** `unpdf` (shipped in the npm package, installed automatically on `pi install`). `uv` and LibreOffice (`soffice`) are optional system binaries detected at runtime: without `uv`, PDFs still convert via the `unpdf` fallback; without `soffice`, office inputs error while PDFs are unaffected. See [Prerequisites](#prerequisites) for the consolidated list.
|
|
93
|
-
|
|
94
|
-
**Licensing note:** `pymupdf4llm`/PyMuPDF are **AGPL-3.0**. This package ships none of their code — `uv` downloads the wheel from PyPI onto your machine at runtime, and it runs as a **separate subprocess** (never imported or linked into this TypeScript). The arms-length process boundary keeps pi-quiver' MIT license intact; the AGPL governs PyMuPDF itself, whose source is public. This holds only while the boundary stays subprocess-only (no vendoring/importing the wheel).
|
|
95
|
-
|
|
96
|
-
### session-name — manual + opt-in automatic session naming
|
|
97
|
-
|
|
98
|
-
Names work sessions so the session selector (and optionally the Ghostty tab) shows what each one is about.
|
|
99
|
-
|
|
100
|
-
**Behaviors:**
|
|
101
|
-
|
|
102
|
-
- **Manual `/session-name [name]`** - set the session name, or, with no argument, print the current one. Always available, regardless of config. A manual name wins: it suppresses later auto-naming for the session.
|
|
103
|
-
- **Automatic naming (opt-in).** After the first agent turn completes, if no name is set yet, the extension asks the **current model** for a 3-6 word session title plus a 1-4 word tab label and applies both. It only runs once per session and never overwrites an existing name.
|
|
104
|
-
- **Resume reflection (opt-in).** When a session that already carries a name is loaded/resumed/reloaded, its tab label is re-applied so the Ghostty tab matches.
|
|
105
|
-
- **Per-turn re-assert (opt-in).** The tab is re-pinned to the session name at the start of every turn. Pi owns the OS terminal title (OSC 0, `pi - <name> - <cwd>`) and overwrites it on every name change and session switch; the re-assert is the only hook that fires *after* pi's writer on a session swap, so the Ghostty tab and the session name stay in sync instead of drifting. It self-heals: if the name was changed outside this extension, the tab label is re-derived from the new name.
|
|
106
|
-
- **Ghostty tab rename.** The short label is written via OSC 2 (`ESC ] 2 ; <label> BEL`) **only when the active terminal is really Ghostty** (`TERM_PROGRAM=ghostty`, `TERM=xterm-ghostty`, or a `GHOSTTY_*` dir env) **and** stdout is a TTY. Other terminals are never touched. Auto-naming keeps its curated short label; re-derived labels (resume/external rename) are the first words of the session name.
|
|
107
|
-
|
|
108
|
-
**OFF by default.** All automatic behavior (auto-naming + resume reflection) is inert until explicitly enabled. The manual command is unaffected.
|
|
38
|
+
```mermaid
|
|
39
|
+
flowchart LR
|
|
40
|
+
S[web page / PDF / doc] --> T["fetch / doc_to_md"]
|
|
41
|
+
T --> E[extract main content]
|
|
42
|
+
E --> G{"over 32KB or 1000 lines?"}
|
|
43
|
+
G -->|no| I[return inline to context]
|
|
44
|
+
G -->|yes| F[spill to temp file<br/>return preview + grep/read hint]
|
|
45
|
+
```
|
|
109
46
|
|
|
110
|
-
|
|
47
|
+
## Quick example
|
|
111
48
|
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
// full form, defaults shown
|
|
115
|
-
"sessionAutoName": { "enabled": false, "ghosttyTab": true }
|
|
116
|
-
}
|
|
49
|
+
```bash
|
|
50
|
+
pi install npm:pi-quiver
|
|
117
51
|
```
|
|
118
52
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
53
|
+
```
|
|
54
|
+
> fetch https://example.com/some-huge-changelog
|
|
55
|
+
Saved-To: /tmp/pi-fetch/2026-...-example.com-....md
|
|
56
|
+
60-line preview follows. grep '^#' the file for headings, or read a slice.
|
|
57
|
+
```
|
|
123
58
|
|
|
124
|
-
|
|
59
|
+
A 300 KB changelog page never touches your context window - you get a preview and a path.
|
|
125
60
|
|
|
126
|
-
|
|
61
|
+
## Architecture
|
|
127
62
|
|
|
128
|
-
|
|
63
|
+
| Extension | Tool | What it does |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `fetch.ts` | `fetch` | Retrieve URLs over HTTP(S). HTML -> Markdown (Readability extraction, Turndown conversion). Binary saved untouched to a temp file. GitHub issue/PR/repo/actions-run URLs auto-route through `gh` (falls back to HTTP). Same size gate as `doc_to_md`. |
|
|
66
|
+
| `doc_to_md.ts` | `doc_to_md` | Convert a local PDF/DOCX/PPTX to Markdown. High-fidelity via `pymupdf4llm` (run through `uv`); degraded pure-JS fallback (`unpdf`) when `uv`/Python is unavailable or conversion times out. DOCX/PPTX convert via LibreOffice first. |
|
|
67
|
+
| `session-name.ts` | `/session-name` | Manual + opt-in automatic session naming, with Ghostty tab rename. OFF by default. |
|
|
68
|
+
| `sword-header.ts` | `/builtin-header` | Themed ASCII startup header replacing pi's default logo. OFF by default. |
|
|
69
|
+
| `fast-mode.ts` | `/fast` | Inject Anthropic fast-mode (`speed: "fast"` + `anthropic-beta: fast-mode-2026-02-01`) into every Claude Opus 4.8 request, any thinking level. `--fast` flag + `/fast [on\|off\|status]`. OFF by default. |
|
|
129
70
|
|
|
130
|
-
|
|
71
|
+
Full routing rules, size-gate mechanics, and config: [doc/fetch.md](doc/fetch.md), [doc/doc-to-md.md](doc/doc-to-md.md).
|
|
131
72
|
|
|
132
|
-
|
|
73
|
+
## Key concepts
|
|
133
74
|
|
|
134
|
-
|
|
75
|
+
| Concept | Meaning |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| Size gate | Text/Markdown/JSON output over 32 KB or 1000 lines spills to a temp file with a 60-line preview instead of inlining. |
|
|
78
|
+
| Content routing | HTML -> Markdown, binary -> untouched file, GitHub URLs -> `gh` CLI, everything else -> the size gate. |
|
|
79
|
+
| Graceful degradation | Optional binaries (`gh`, `uv`, LibreOffice) are never hard install-time deps; each has a defined, documented fallback or failure mode. |
|
|
80
|
+
| Opt-in ergonomics | `session-name` and `sword-header` do nothing until explicitly enabled in `settings.json`. |
|
|
135
81
|
|
|
136
|
-
|
|
137
|
-
- **`/builtin-header`** restores the built-in pi header at runtime (always available).
|
|
82
|
+
## When to use
|
|
138
83
|
|
|
139
|
-
|
|
84
|
+
- An agent needs to reason from a real web page, GitHub issue/PR, or local PDF/DOCX/PPTX instead of memory.
|
|
85
|
+
- You want that ingestion to be safe by default, with no risk of a single call blowing the context budget.
|
|
140
86
|
|
|
141
|
-
|
|
87
|
+
## When NOT to use
|
|
142
88
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// object form also accepted: "swordHeader": { "enabled": true }
|
|
147
|
-
}
|
|
148
|
-
```
|
|
89
|
+
- You need a general-purpose web scraper (JS-rendered pages, pagination, auth flows) - `fetch` does plain HTTP + Readability extraction, nothing more.
|
|
90
|
+
- You need spreadsheet conversion - `doc_to_md` explicitly excludes spreadsheets (they paginate badly via PDF).
|
|
91
|
+
- You want automatic session naming or a custom header without opting in - both stay off until you flip the config.
|
|
149
92
|
|
|
150
93
|
## Install
|
|
151
94
|
|
|
@@ -176,6 +119,32 @@ git clone git@github.com:jjuraszek/pi-quiver.git ~/repos/pi-quiver
|
|
|
176
119
|
pi -e ~/repos/pi-quiver/fetch.ts
|
|
177
120
|
```
|
|
178
121
|
|
|
122
|
+
## Prerequisites
|
|
123
|
+
|
|
124
|
+
The npm package's bundled JS deps install automatically on `pi install`. A few **runtime system binaries** are optional; each degrades gracefully when absent:
|
|
125
|
+
|
|
126
|
+
| Prerequisite | Needed by | If absent |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `gh` (GitHub CLI, installed + `gh auth login`) | `fetch` GitHub issue/PR/repo/actions-run routing | Falls back to an HTTP fetch of the rendered page (private repos hit a login wall). |
|
|
129
|
+
| `uv` (+ managed Python 3.14, fetched on first use) | `doc_to_md` high-fidelity PDF conversion | Degrades to the pure-JS `unpdf` fallback (no faithful tables/headings). |
|
|
130
|
+
| LibreOffice (`soffice` on `PATH`) | `doc_to_md` DOCX/PPTX conversion | Office inputs error (no JS fallback for office->PDF); PDFs unaffected. |
|
|
131
|
+
|
|
132
|
+
None is a hard install-time dependency of the package; they are tools you provide in the environment where pi runs.
|
|
133
|
+
|
|
134
|
+
### Opt-in extension config
|
|
135
|
+
|
|
136
|
+
Both are opt-in via `settings.json` (project `.pi/settings.json` overrides the global agent-dir layer):
|
|
137
|
+
|
|
138
|
+
```jsonc
|
|
139
|
+
{
|
|
140
|
+
"sessionAutoName": { "enabled": false, "ghosttyTab": true }, // or boolean shorthand
|
|
141
|
+
"swordHeader": false, // or { "enabled": true }
|
|
142
|
+
"fastMode": false // or { "enabled": true }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`sessionAutoName.enabled` makes one extra short LLM call per session (once, after the first turn) to title it; `false` (default) makes no model calls. `fastMode` only affects `claude-opus-4-8` requests on Anthropic's `anthropic-messages` API; enabling it opts into premium fast-mode pricing. `--fast` forces it on for one launch; `/fast on|off` toggles live. Proxy providers (opencode, cloudflare-ai-gateway) are excluded. `fastMode`'s header injection needs the `before_provider_headers` hook (pi bundling `@earendil-works/pi-coding-agent` >= 0.80.5); on older pi the beta header is silently not sent. See [doc/fetch.md](doc/fetch.md) and [doc/doc-to-md.md](doc/doc-to-md.md) for the ingestion tools' full reference; session-name/sword-header behavior above is complete.
|
|
147
|
+
|
|
179
148
|
## Development
|
|
180
149
|
|
|
181
150
|
Deps are peers (`@earendil-works/*`, `@sinclair/typebox`) plus the bundled
|
|
@@ -189,6 +158,10 @@ npm run test:all # node --test *.test.ts + tsc --noEmit typecheck
|
|
|
189
158
|
`npm test` runs the unit tests alone; `npm run typecheck` runs the type pass.
|
|
190
159
|
Both run in CI on ubuntu + windows (`.github/workflows/test.yml`).
|
|
191
160
|
|
|
161
|
+
## How this fits the platform
|
|
162
|
+
|
|
163
|
+
pi-quiver is how ground truth gets into an agent's context - real pages, PDFs, docs, cleanly and safely. The other three then coordinate work over it ([pi-cohort](https://github.com/jjuraszek/pi-cohort)), prune it once it's stale ([pi-condense](https://github.com/jjuraszek/pi-condense)), and govern the process end to end ([pi-gauntlet](https://github.com/jjuraszek/pi-gauntlet)).
|
|
164
|
+
|
|
192
165
|
## Support
|
|
193
166
|
|
|
194
167
|
If this saves you time, consider [buying me a coffee](https://buymeacoffee.com/jjurasszek).
|
package/fast-mode.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast mode for Claude Opus 4.8.
|
|
3
|
+
*
|
|
4
|
+
* When enabled, injects Anthropic's fast-mode signals into every qualifying
|
|
5
|
+
* Opus 4.8 request on the anthropic-messages API, regardless of thinking level:
|
|
6
|
+
* - payload: { ...payload, speed: "fast" } (before_provider_request)
|
|
7
|
+
* - header: anthropic-beta: ...,fast-mode-2026-02-01 (before_provider_headers)
|
|
8
|
+
*
|
|
9
|
+
* OFF BY DEFAULT. Three control surfaces, lowest precedence first:
|
|
10
|
+
* 1. settings.json "fastMode": true | { "enabled": true } (default false)
|
|
11
|
+
* 2. --fast launch flag (force-on only)
|
|
12
|
+
* 3. /fast [on|off|status] live toggle (wins for session)
|
|
13
|
+
*
|
|
14
|
+
* Header coupling to pi-ai internals: pi assembles `anthropic-beta` AFTER this
|
|
15
|
+
* hook and merges the hook's headers LAST, so setting the header here REPLACES
|
|
16
|
+
* pi's list. For opus-4-8 pi's conditional betas (fine-grained tool streaming,
|
|
17
|
+
* interleaved thinking) are never applied (eager tool streaming defaults on +
|
|
18
|
+
* forceAdaptiveThinking), so the only betas to preserve are the OAuth identity
|
|
19
|
+
* betas. We detect OAuth via the same token marker pi uses and rebuild the
|
|
20
|
+
* exact list. If pi later adds betas for opus-4-8, revisit buildBetaHeader.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { resolveConfig } from "./extension-config.ts";
|
|
25
|
+
|
|
26
|
+
export const FAST_MODE_BETA = "fast-mode-2026-02-01";
|
|
27
|
+
export const FAST_SPEED = "fast";
|
|
28
|
+
// Loose prefix: matches dated snapshots (claude-opus-4-8-*). Opus 4.7 is out of
|
|
29
|
+
// scope (D1); a future 4.9 needs a one-line addition here.
|
|
30
|
+
export const FAST_MODE_MODEL_PREFIXES = ["claude-opus-4-8"];
|
|
31
|
+
export const OAUTH_IDENTITY_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
|
|
32
|
+
const STATUS_KEY = "fast-mode";
|
|
33
|
+
const BETA_HEADER = "anthropic-beta";
|
|
34
|
+
|
|
35
|
+
type Config = { enabled: boolean };
|
|
36
|
+
const DEFAULT_CONFIG: Config = { enabled: false };
|
|
37
|
+
|
|
38
|
+
export function coerce(raw: unknown): Partial<Config> | undefined {
|
|
39
|
+
if (raw === undefined) return undefined;
|
|
40
|
+
if (typeof raw === "boolean") return { enabled: raw };
|
|
41
|
+
if (raw && typeof raw === "object") {
|
|
42
|
+
const o = raw as Record<string, unknown>;
|
|
43
|
+
const out: Partial<Config> = {};
|
|
44
|
+
if (typeof o.enabled === "boolean") out.enabled = o.enabled;
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type ModelLike = { id?: string; api?: string; provider?: string } | undefined;
|
|
51
|
+
|
|
52
|
+
export function shouldInject(enabled: boolean, model: ModelLike): boolean {
|
|
53
|
+
if (!enabled || !model) return false;
|
|
54
|
+
if (model.provider !== "anthropic") return false;
|
|
55
|
+
if (model.api !== "anthropic-messages") return false;
|
|
56
|
+
const id = model.id;
|
|
57
|
+
if (typeof id !== "string") return false;
|
|
58
|
+
return FAST_MODE_MODEL_PREFIXES.some((p) => id.startsWith(p));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function injectSpeed(payload: unknown): unknown {
|
|
62
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
63
|
+
return payload;
|
|
64
|
+
}
|
|
65
|
+
return { ...(payload as Record<string, unknown>), speed: FAST_SPEED };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildBetaHeader(existing: string | null | undefined, isOAuth: boolean): string {
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
const out: string[] = [];
|
|
71
|
+
const add = (b: string): void => {
|
|
72
|
+
const t = b.trim();
|
|
73
|
+
if (t && !seen.has(t)) {
|
|
74
|
+
seen.add(t);
|
|
75
|
+
out.push(t);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
if (isOAuth) OAUTH_IDENTITY_BETAS.forEach(add);
|
|
79
|
+
if (typeof existing === "string") existing.split(",").forEach(add);
|
|
80
|
+
add(FAST_MODE_BETA);
|
|
81
|
+
return out.join(",");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type State = { config: boolean; flag: boolean; live: boolean | null };
|
|
85
|
+
|
|
86
|
+
export function resolveEnabled(s: State): boolean {
|
|
87
|
+
if (s.live !== null) return s.live;
|
|
88
|
+
return s.flag || s.config;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default function (pi: ExtensionAPI) {
|
|
92
|
+
let liveOverride: boolean | null = null;
|
|
93
|
+
let enabled = false;
|
|
94
|
+
|
|
95
|
+
const readFlag = (): boolean => pi.getFlag("fast") === true;
|
|
96
|
+
|
|
97
|
+
const resolveState = (ctx: ExtensionContext): boolean => {
|
|
98
|
+
const config = resolveConfig(ctx.cwd, "fastMode", DEFAULT_CONFIG, coerce).enabled;
|
|
99
|
+
enabled = resolveEnabled({ config, flag: readFlag(), live: liveOverride });
|
|
100
|
+
return enabled;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const refreshStatus = (ctx: ExtensionContext): void => {
|
|
104
|
+
if (!enabled) {
|
|
105
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
ctx.ui.setStatus(STATUS_KEY, shouldInject(enabled, ctx.model) ? "\u26a1 fast" : "\u26a1 n/a");
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const detectOAuth = async (ctx: ExtensionContext): Promise<boolean | null> => {
|
|
112
|
+
if (!ctx.model) return null;
|
|
113
|
+
try {
|
|
114
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
115
|
+
if (!auth.ok) return null;
|
|
116
|
+
return typeof auth.apiKey === "string" && auth.apiKey.includes("sk-ant-oat");
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
pi.registerFlag("fast", {
|
|
123
|
+
type: "boolean",
|
|
124
|
+
description: "Enable Anthropic fast mode for Opus 4.8 requests this launch",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
128
|
+
resolveState(ctx);
|
|
129
|
+
refreshStatus(ctx);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
133
|
+
refreshStatus(ctx);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
137
|
+
if (!shouldInject(enabled, ctx.model)) return;
|
|
138
|
+
return injectSpeed(event.payload);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
pi.on("before_provider_headers", async (event, ctx) => {
|
|
142
|
+
if (!shouldInject(enabled, ctx.model)) return;
|
|
143
|
+
if (!event.headers) return;
|
|
144
|
+
const isOAuth = await detectOAuth(ctx);
|
|
145
|
+
if (isOAuth === null) return;
|
|
146
|
+
event.headers[BETA_HEADER] = buildBetaHeader(event.headers[BETA_HEADER], isOAuth);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
pi.registerCommand("fast", {
|
|
150
|
+
description: "Manage Opus 4.8 fast mode: /fast [on|off|status]",
|
|
151
|
+
getArgumentCompletions: (prefix) => {
|
|
152
|
+
const p = prefix.trim().toLowerCase();
|
|
153
|
+
if (p.includes(" ")) return null;
|
|
154
|
+
const matches = ["on", "off", "status"].filter((v) => v.startsWith(p));
|
|
155
|
+
return matches.length ? matches.map((value) => ({ value, label: value })) : null;
|
|
156
|
+
},
|
|
157
|
+
handler: async (args, ctx) => {
|
|
158
|
+
const arg = args.trim().toLowerCase();
|
|
159
|
+
if (arg === "status") {
|
|
160
|
+
const eff = liveOverride !== null ? "live toggle" : readFlag() ? "--fast flag" : "settings.json";
|
|
161
|
+
const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "(no model selected)";
|
|
162
|
+
const applies = shouldInject(enabled, ctx.model) ? "applies to current model" : "does not apply to current model";
|
|
163
|
+
ctx.ui.notify(
|
|
164
|
+
`Fast mode is ${enabled ? "on" : "off"} (source: ${eff}). Model: ${model} - ${applies}.`,
|
|
165
|
+
"info",
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (arg === "on" || arg === "off") {
|
|
170
|
+
liveOverride = arg === "on";
|
|
171
|
+
} else if (arg === "") {
|
|
172
|
+
liveOverride = !enabled;
|
|
173
|
+
} else {
|
|
174
|
+
ctx.ui.notify("Usage: /fast [on|off|status]", "warning");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
resolveState(ctx);
|
|
178
|
+
refreshStatus(ctx);
|
|
179
|
+
ctx.ui.notify(`Fast mode ${enabled ? "enabled" : "disabled"}`, "info");
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
package/fetch.ts
CHANGED
|
@@ -44,7 +44,8 @@ interface FetchToolDetails {
|
|
|
44
44
|
type GhTarget =
|
|
45
45
|
| { kind: "issue"; url: string }
|
|
46
46
|
| { kind: "pr"; url: string }
|
|
47
|
-
| { kind: "repo"; slug: string }
|
|
47
|
+
| { kind: "repo"; slug: string }
|
|
48
|
+
| { kind: "run"; slug: string; runId: string; url: string };
|
|
48
49
|
|
|
49
50
|
const RESERVED_OWNERS = new Set([
|
|
50
51
|
"orgs", "users", "sponsors", "topics", "marketplace", "apps",
|
|
@@ -67,6 +68,9 @@ export function classifyGitHubTarget(url: URL): GhTarget | null {
|
|
|
67
68
|
if (segs.length === 4 && segs[2] === "pull" && /^\d+$/.test(segs[3])) {
|
|
68
69
|
return { kind: "pr", url: `https://github.com/${owner}/${repo}/pull/${segs[3]}` };
|
|
69
70
|
}
|
|
71
|
+
if (segs.length === 5 && segs[2] === "actions" && segs[3] === "runs" && /^\d+$/.test(segs[4])) {
|
|
72
|
+
return { kind: "run", slug: `${owner}/${repo}`, runId: segs[4], url: `https://github.com/${owner}/${repo}/actions/runs/${segs[4]}` };
|
|
73
|
+
}
|
|
70
74
|
if (segs.length === 2) {
|
|
71
75
|
return { kind: "repo", slug: `${owner}/${repo}` };
|
|
72
76
|
}
|
|
@@ -76,6 +80,7 @@ export function classifyGitHubTarget(url: URL): GhTarget | null {
|
|
|
76
80
|
export function buildGhArgs(target: GhTarget): string[] {
|
|
77
81
|
if (target.kind === "issue") return ["issue", "view", target.url, "--comments"];
|
|
78
82
|
if (target.kind === "pr") return ["pr", "view", target.url, "--comments"];
|
|
83
|
+
if (target.kind === "run") return ["run", "view", target.runId, "--repo", target.slug];
|
|
79
84
|
return ["repo", "view", target.slug];
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -120,12 +125,14 @@ export function planGhRouting(params: GhRoutingParams, url: URL): GhTarget | nul
|
|
|
120
125
|
function ghCommandLabel(target: GhTarget): string {
|
|
121
126
|
if (target.kind === "issue") return "issue view --comments";
|
|
122
127
|
if (target.kind === "pr") return "pr view --comments";
|
|
128
|
+
if (target.kind === "run") return "run view";
|
|
123
129
|
return "repo view";
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
function ghSourceLine(target: GhTarget, ref: string): string {
|
|
127
133
|
if (target.kind === "issue") return `gh issue view ${ref} --comments`;
|
|
128
134
|
if (target.kind === "pr") return `gh pr view ${ref} --comments`;
|
|
135
|
+
if (target.kind === "run") return `gh run view ${target.runId} --repo ${target.slug}`;
|
|
129
136
|
return `gh repo view ${ref}`;
|
|
130
137
|
}
|
|
131
138
|
|
|
@@ -451,14 +458,14 @@ export default function fetchExtension(pi: ExtensionAPI) {
|
|
|
451
458
|
name: "fetch",
|
|
452
459
|
label: "Fetch URL",
|
|
453
460
|
description:
|
|
454
|
-
"Fetch a URL over HTTP(S). HTML is extracted to Markdown (readability + turndown). Binary content (images, PDFs, archives) is saved untouched to a temp file and only a path is returned. Text/Markdown/JSON over 32KB or 1000 lines is written to a temp file with a 60-line preview; smaller content is returned inline. Parsable downloads are capped at 1MB, binary at 50MB. GitHub issue/PR/repo URLs are served via the gh CLI when available (falls back to HTTP otherwise).",
|
|
461
|
+
"Fetch a URL over HTTP(S). HTML is extracted to Markdown (readability + turndown). Binary content (images, PDFs, archives) is saved untouched to a temp file and only a path is returned. Text/Markdown/JSON over 32KB or 1000 lines is written to a temp file with a 60-line preview; smaller content is returned inline. Parsable downloads are capped at 1MB, binary at 50MB. GitHub issue/PR/repo/actions-run URLs are served via the gh CLI when available (falls back to HTTP otherwise).",
|
|
455
462
|
promptSnippet: "Fetch the contents of a URL",
|
|
456
463
|
promptGuidelines: [
|
|
457
464
|
"Use fetch when the user provides a URL or asks to read web content.",
|
|
458
465
|
"Binary responses return a file path only — pass that path to a tool that can process the bytes; do not expect inline content.",
|
|
459
466
|
"When the body is written to a file, grep it or read with offset/limit. Converted Markdown is grep-able by heading (^#).",
|
|
460
467
|
"Pass raw=true to skip Markdown/JSON conversion and get the decoded body as-is (still subject to the size gate).",
|
|
461
|
-
"GitHub issue/PR/repo links are fetched through the gh CLI automatically; pass raw=true to force the rendered HTML page.",
|
|
468
|
+
"GitHub issue/PR/repo/actions-run links are fetched through the gh CLI automatically; pass raw=true to force the rendered HTML page.",
|
|
462
469
|
],
|
|
463
470
|
parameters: Type.Object({
|
|
464
471
|
url: Type.String({ description: "Absolute http(s) URL" }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-quiver",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Personal pack of Pi coding-agent extensions: context-safe fetch, doc_to_md PDF/DOCX/PPTX-to-Markdown conversion, session naming, and a themed ASCII startup header.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"doc_to_md.ts",
|
|
33
33
|
"session-name.ts",
|
|
34
34
|
"sword-header.ts",
|
|
35
|
+
"fast-mode.ts",
|
|
35
36
|
"extension-config.ts",
|
|
36
37
|
"scripts/pdf_to_md.py",
|
|
37
38
|
"types/**/*.d.ts",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"scripts": {
|
|
42
43
|
"check:agents-core": "node scripts/check-agents-core.mjs",
|
|
43
44
|
"test": "node --test \"*.test.ts\"",
|
|
44
|
-
"typecheck": "npx -y tsc --noEmit --allowImportingTsExtensions --target es2022 --module nodenext --moduleResolution nodenext --strict --skipLibCheck --esModuleInterop --resolveJsonModule --lib es2022 --types node fetch.ts fetch.test.ts doc_to_md.ts doc_to_md.test.ts session-name.ts session-name.test.ts sword-header.ts sword-header.test.ts extension-config.ts types/turndown-plugin-gfm.d.ts",
|
|
45
|
+
"typecheck": "npx -y tsc --noEmit --allowImportingTsExtensions --target es2022 --module nodenext --moduleResolution nodenext --strict --skipLibCheck --esModuleInterop --resolveJsonModule --lib es2022 --types node fetch.ts fetch.test.ts doc_to_md.ts doc_to_md.test.ts session-name.ts session-name.test.ts sword-header.ts sword-header.test.ts fast-mode.ts fast-mode.test.ts extension-config.ts types/turndown-plugin-gfm.d.ts",
|
|
45
46
|
"test:all": "npm run check:agents-core && npm run test && npm run typecheck"
|
|
46
47
|
},
|
|
47
48
|
"pi": {
|
|
@@ -49,7 +50,8 @@
|
|
|
49
50
|
"./fetch.ts",
|
|
50
51
|
"./doc_to_md.ts",
|
|
51
52
|
"./session-name.ts",
|
|
52
|
-
"./sword-header.ts"
|
|
53
|
+
"./sword-header.ts",
|
|
54
|
+
"./fast-mode.ts"
|
|
53
55
|
],
|
|
54
56
|
"image": "https://raw.githubusercontent.com/jjuraszek/pi-quiver/main/pi-quiver.png"
|
|
55
57
|
},
|
|
@@ -86,7 +88,7 @@
|
|
|
86
88
|
},
|
|
87
89
|
"devDependencies": {
|
|
88
90
|
"@earendil-works/pi-ai": "^0.80.3",
|
|
89
|
-
"@earendil-works/pi-coding-agent": "^0.80.
|
|
91
|
+
"@earendil-works/pi-coding-agent": "^0.80.5",
|
|
90
92
|
"@earendil-works/pi-tui": "^0.80.3",
|
|
91
93
|
"@sinclair/typebox": "^0.34.49",
|
|
92
94
|
"@types/jsdom": "^28.0.3",
|