pi-quiver 3.1.1 → 3.1.2

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 CHANGED
@@ -8,6 +8,10 @@ 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.1.2 - 2026-07-07
12
+
13
+ - **`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.
14
+
11
15
  ## v3.1.1 - 2026-07-05
12
16
 
13
17
  Branding, funding, and gallery preview. No behavior change.
package/README.md CHANGED
@@ -6,146 +6,88 @@
6
6
 
7
7
  [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-donate-yellow?logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/jjurasszek)
8
8
 
9
- A small pack of [Pi coding-agent](https://github.com/earendil-works/pi) extensions I keep across every pi profile. First-party-quality tools, published to npm like sibling packages ([`pi-cohort`](https://github.com/jjuraszek/pi-cohort), [`pi-gauntlet`](https://github.com/jjuraszek/pi-gauntlet), [`pi-condense`](https://github.com/jjuraszek/pi-condense)).
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
- ## Extensions
11
+ ## The problem
12
12
 
13
- | Extension | Tool | What it does |
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
- **JSON:** Pretty-printed with 2-space indent before the gate.
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
- **GitHub URLs -> `gh`:** `github.com` issue (`/issues/{n}`), PR (`/pull/{n}`), and repo-root (`/{owner}/{repo}`) URLs are served by running the `gh` CLI (`gh issue|pr view --comments`, `gh repo view`) and returning its output, tagged with a `Source: gh ...` header and run through the same size gate. Requires `gh` (see [Prerequisites](#prerequisites)); if `gh` is missing or the call fails, `fetch` silently falls back to the normal HTTP path. Pass `raw=true` to force the rendered HTML page. All other GitHub paths (`tree`, `blob`, `raw`, `releases`, gists, ...) use the HTTP path unchanged. Routing is also skipped (plain HTTP used) when the request is non-GET, carries a body, or sets custom headers. gh output is bounded by a 10 MB buffer and run through the same size gate (spilled to a file when large), not the 1 MB HTTP download cap.
17
+ ## Why pi-quiver exists
59
18
 
60
- **Parameters:**
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
- **Truncation:** Parsable content over 1 MB is truncated with a `(truncated to 1MB)` note; binary over 50 MB notes `(truncated to 50MB)`.
21
+ `session-name` and `sword-header` are smaller, opt-in ergonomics on top - session labeling and a themed startup header.
65
22
 
66
- **Runtime dependencies:** `jsdom`, `@mozilla/readability`, `turndown`, `turndown-plugin-gfm`. Shipped in the npm package and installed automatically on `pi install` - no manual setup needed.
23
+ ## Part of the pi agent toolkit
67
24
 
68
- ### doc_to_md — local document → Markdown
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
- `doc_to_md` takes a **local file path** (`.pdf`, `.docx`, `.pptx`) and returns Markdown. For remote documents, `fetch` the URL first (it saves binaries to a temp path), then pass that path here.
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
- **Two engines, auto-selected:**
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
- - **Primary — `pymupdf4llm`** (high fidelity: headings, tables, reading order). Runs as an arms-length subprocess via `uv run --with pymupdf4llm==<pin> --python 3.14`. `uv` fetches the wheel into its own cache on first use (one-time download); Python 3.14 is fixed. Warmed once per process: the first call probes/installs (generous budget), later calls reuse the warm cache with a shorter per-document budget.
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
- **Office documents (`.docx`, `.pptx`):** converted to PDF by headless LibreOffice (`soffice`, isolated per-call profile), then fed through the same PDF pipeline. `soffice` must be on `PATH` for office inputs — otherwise the tool errors (there is no JS fallback for office→PDF). Spreadsheets and other formats are out of scope (spreadsheets paginate badly via PDF).
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
- **Size gate:** identical to `fetch` — Markdown ≤ 32 KB and ≤ 1000 lines is inlined; larger output spills to `${TMPDIR}/pi-doc-to-md/<stamp>-<basename>-<hash>.md` with a 60-line preview + a grep/read-slice hint.
80
-
81
- **Configuration (environment variables):**
82
-
83
- | Variable | Default | Meaning |
84
- |---|---|---|
85
- | `PI_DOC_TO_MD_PYMUPDF_VERSION` | `1.27.2.3` | `pymupdf4llm` version pin passed to `uv --with` (digits/dots only) |
86
- | `PI_DOC_TO_MD_WARM_TIMEOUT_MS` | `120000` | Warm/install call budget — covers the cold wheel (+ managed Python) download |
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
- **Configuration** (`settings.json`, **project `.pi/settings.json` overrides the global agent-dir `settings.json`**). The global path is resolved via pi's own `getAgentDir()` (honours `PI_CODING_AGENT_DIR`, else `~/.pi/agent`), so it stays correct however this package is installed.
47
+ ## Quick example
111
48
 
112
- ```jsonc
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
- | Key | Default | Meaning |
120
- |---|---|---|
121
- | `enabled` | `false` | Master switch for automatic naming + resume reflection. |
122
- | `ghosttyTab` | `true` | Whether to rename the Ghostty tab (only ever fires when the terminal is actually Ghostty). |
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
- Boolean shorthand: `"sessionAutoName": true` enables everything (equivalent to `{ "enabled": true, "ghosttyTab": true }`); `false` disables everything.
59
+ A 300 KB changelog page never touches your context window - you get a preview and a path.
125
60
 
126
- **Cost note:** when enabled, automatic naming makes **one extra short LLM call** per session (low reasoning effort, current model, a few-thousand-char conversation digest), once, after the first turn. When OFF (the default) it makes **no** model calls and writes **nothing** to the terminal.
61
+ ## Architecture
127
62
 
128
- **Runtime dependency:** `@earendil-works/pi-ai` (the unified LLM API provided by the pi runtime; a peer dependency, no separate install).
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. |
129
69
 
130
- ### sword-header — themed ASCII startup header
70
+ 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
71
 
132
- Replaces pi's built-in startup logo with a hero's greatsword (Michael J. Penick longsword, asciiart.eu). The ASCII art is verbatim; only the coloring is ours - hilt/grip/pommel use the `accent` token, the blade uses `text`, so it tracks whatever theme is active.
72
+ ## Key concepts
133
73
 
134
- **Behaviors:**
74
+ | Concept | Meaning |
75
+ | --- | --- |
76
+ | 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. |
77
+ | Content routing | HTML -> Markdown, binary -> untouched file, GitHub URLs -> `gh` CLI, everything else -> the size gate. |
78
+ | Graceful degradation | Optional binaries (`gh`, `uv`, LibreOffice) are never hard install-time deps; each has a defined, documented fallback or failure mode. |
79
+ | Opt-in ergonomics | `session-name` and `sword-header` do nothing until explicitly enabled in `settings.json`. |
135
80
 
136
- - **TUI only.** Installs a custom header on `session_start` when `ctx.mode === "tui"`. In print/non-interactive mode (`-p`) it does nothing.
137
- - **`/builtin-header`** restores the built-in pi header at runtime (always available).
81
+ ## When to use
138
82
 
139
- **OFF by default.** The header is only installed when explicitly enabled via `settings.json`.
83
+ - An agent needs to reason from a real web page, GitHub issue/PR, or local PDF/DOCX/PPTX instead of memory.
84
+ - You want that ingestion to be safe by default, with no risk of a single call blowing the context budget.
140
85
 
141
- **Configuration** (`settings.json`, project `.pi/settings.json` overrides the global agent-dir layer; same resolution as `session-name`):
86
+ ## When NOT to use
142
87
 
143
- ```jsonc
144
- {
145
- "swordHeader": false // default; true installs the header
146
- // object form also accepted: "swordHeader": { "enabled": true }
147
- }
148
- ```
88
+ - You need a general-purpose web scraper (JS-rendered pages, pagination, auth flows) - `fetch` does plain HTTP + Readability extraction, nothing more.
89
+ - You need spreadsheet conversion - `doc_to_md` explicitly excludes spreadsheets (they paginate badly via PDF).
90
+ - You want automatic session naming or a custom header without opting in - both stay off until you flip the config.
149
91
 
150
92
  ## Install
151
93
 
@@ -176,6 +118,31 @@ git clone git@github.com:jjuraszek/pi-quiver.git ~/repos/pi-quiver
176
118
  pi -e ~/repos/pi-quiver/fetch.ts
177
119
  ```
178
120
 
121
+ ## Prerequisites
122
+
123
+ The npm package's bundled JS deps install automatically on `pi install`. A few **runtime system binaries** are optional; each degrades gracefully when absent:
124
+
125
+ | Prerequisite | Needed by | If absent |
126
+ | --- | --- | --- |
127
+ | `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). |
128
+ | `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). |
129
+ | LibreOffice (`soffice` on `PATH`) | `doc_to_md` DOCX/PPTX conversion | Office inputs error (no JS fallback for office->PDF); PDFs unaffected. |
130
+
131
+ None is a hard install-time dependency of the package; they are tools you provide in the environment where pi runs.
132
+
133
+ ### session-name and sword-header config
134
+
135
+ Both are opt-in via `settings.json` (project `.pi/settings.json` overrides the global agent-dir layer):
136
+
137
+ ```jsonc
138
+ {
139
+ "sessionAutoName": { "enabled": false, "ghosttyTab": true }, // or boolean shorthand
140
+ "swordHeader": false // or { "enabled": true }
141
+ }
142
+ ```
143
+
144
+ `sessionAutoName.enabled` makes one extra short LLM call per session (once, after the first turn) to title it; `false` (default) makes no model calls. 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.
145
+
179
146
  ## Development
180
147
 
181
148
  Deps are peers (`@earendil-works/*`, `@sinclair/typebox`) plus the bundled
@@ -189,6 +156,10 @@ npm run test:all # node --test *.test.ts + tsc --noEmit typecheck
189
156
  `npm test` runs the unit tests alone; `npm run typecheck` runs the type pass.
190
157
  Both run in CI on ubuntu + windows (`.github/workflows/test.yml`).
191
158
 
159
+ ## How this fits the platform
160
+
161
+ 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)).
162
+
192
163
  ## Support
193
164
 
194
165
  If this saves you time, consider [buying me a coffee](https://buymeacoffee.com/jjurasszek).
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.1.1",
3
+ "version": "3.1.2",
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",