pi-quiver 4.2.0 → 4.3.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 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
+ ## v4.3.0 - 2026-08-25
12
+
13
+ - fetch: GitHub Actions job URLs (`.../actions/runs/<runId>/job/<jobId>`, singular `/job/` as GitHub's UI produces) now route through `gh run view --job <jobId> --repo <slug>`; plural `/jobs/<id>` paths still fall back to plain HTTP. Both run and job fetches now make a best-effort second `gh run view ... --log-failed` call and append its output under a `## Failed step logs` heading; when nothing failed, the run is still in progress, or logs have expired, the call yields no section and behavior is unchanged (summary-only).
14
+
11
15
  ## v4.2.0 - 2026-08-20
12
16
 
13
17
  - fetch: data plane extracted to `lib/fetch-core.ts`; new `pi-quiver fetch` CLI (esbuild-built `dist/` bin) with full parameter parity; Claude Code skill + plugin marketplace (`quiver:fetch` via `npx -y pi-quiver@latest`). pi tool behavior unchanged.
package/README.md CHANGED
@@ -62,7 +62,7 @@ A 300 KB changelog page never touches your context window - you get a preview an
62
62
 
63
63
  | Extension | Tool | What it does |
64
64
  | --- | --- | --- |
65
- | `extensions/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`. Behavior lives in `lib/fetch-core.ts`; also exposed as the `pi-quiver fetch` CLI (see [Claude Code support](#claude-code-support)). |
65
+ | `extensions/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/actions-job URLs auto-route through `gh` (falls back to HTTP); failed runs/jobs include failed-step logs (best-effort, summary-only otherwise). Same size gate as `doc_to_md`. Behavior lives in `lib/fetch-core.ts`; also exposed as the `pi-quiver fetch` CLI (see [Claude Code support](#claude-code-support)). |
66
66
  | `extensions/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
67
  | `extensions/session-name.ts` | `/session-name` | Manual + opt-in automatic session naming, naming rules and deny list, long-session revisits, and Ghostty tab rename. OFF by default. |
68
68
  | `extensions/sword-header.ts` | `/builtin-header` | Themed ASCII startup header replacing pi's default logo. OFF by default. |
@@ -76,7 +76,7 @@ Full routing rules, size-gate mechanics, and config: [doc/fetch.md](doc/fetch.md
76
76
  | Concept | Meaning |
77
77
  | --- | --- |
78
78
  | 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. |
79
- | Content routing | HTML -> Markdown, binary -> untouched file, GitHub URLs -> `gh` CLI, everything else -> the size gate. |
79
+ | Content routing | HTML -> Markdown, binary -> untouched file, GitHub URLs -> `gh` CLI (failed runs/jobs get failed-step logs appended), everything else -> the size gate. |
80
80
  | Graceful degradation | Optional binaries (`gh`, `uv`, LibreOffice) are never hard install-time deps; each has a defined, documented fallback or failure mode. |
81
81
  | Opt-in extensions | `session-name`, `sword-header`, `fast-mode`, and `provider-stall-watchdog` do nothing until explicitly enabled in `settings.json`. |
82
82
  | Provider stall recovery | The watchdog detects a missing first stream event and missing parsed semantic progress, not network liveness. The pre-first-event tier covers every mode and origin; the mid-stream tier is TUI-only. |
@@ -129,7 +129,7 @@ The npm package's bundled JS deps install automatically on `pi install`. A few *
129
129
 
130
130
  | Prerequisite | Needed by | If absent |
131
131
  | --- | --- | --- |
132
- | `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). |
132
+ | `gh` (GitHub CLI, installed + `gh auth login`) | `fetch` GitHub issue/PR/repo/actions-run/actions-job routing | Falls back to an HTTP fetch of the rendered page (private repos hit a login wall). |
133
133
  | `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). |
134
134
  | LibreOffice (`soffice` on `PATH`) | `doc_to_md` DOCX/PPTX conversion | Office inputs error (no JS fallback for office->PDF); PDFs unaffected. |
135
135
 
@@ -209,7 +209,7 @@ Operational notes:
209
209
 
210
210
  `fetch`'s core (`lib/fetch-core.ts`) is also published as a CLI, so Claude Code can use the same routing, size gate, and spill behavior as pi's native tool - without pi ever seeing Claude-only files.
211
211
 
212
- **Exposed:** the `quiver` plugin, served from this repo's `.claude-plugin/marketplace.json`, with one skill: `fetch` (invoked as `quiver:fetch` / `/quiver:fetch`). The skill runs `npx -y pi-quiver@latest fetch <url> [flags]` via Bash - full parameter parity with the pi tool (`--method`, `--header`, `--body`, `--raw`, `--timeout-ms`), same GitHub `gh` routing, same size gate, same binary-to-temp-file handling. See [doc/fetch.md](doc/fetch.md#claude-code-cli-pi-quiver-fetch) for exit codes and flags.
212
+ **Exposed:** the `quiver` plugin, served from this repo's `.claude-plugin/marketplace.json`, with one skill: `fetch` (invoked as `quiver:fetch` / `/quiver:fetch`). The skill runs `npx -y pi-quiver@latest fetch <url> [flags]` via Bash - full parameter parity with the pi tool (`--method`, `--header`, `--body`, `--raw`, `--timeout-ms`), same GitHub `gh` routing (including failed-step logs on failed runs/jobs), same size gate, same binary-to-temp-file handling. See [doc/fetch.md](doc/fetch.md#claude-code-cli-pi-quiver-fetch) for exit codes and flags.
213
213
 
214
214
  **Not exposed:** pi extensions, `doc_to_md`, and everything else in this package - the marketplace allowlists only `./skills/fetch`, and the npm tarball never ships `skills/` or `.claude-plugin/` (pi's own `files` allowlist excludes them, and pi's explicit `pi.extensions` manifest makes them invisible to pi's convention-directory auto-discovery either way).
215
215
 
@@ -245,6 +245,10 @@ Both run in CI on ubuntu + windows (`.github/workflows/test.yml`).
245
245
 
246
246
  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)).
247
247
 
248
+ ## Contributing
249
+
250
+ See [CONTRIBUTING.md](CONTRIBUTING.md) - issues follow a Context / Problem / Idea / Acceptance Criteria template; PRs run the [pi-gauntlet](https://github.com/jjuraszek/pi-gauntlet) workflow (one-liners exempt from ceremony, never from keeping docs truthful).
251
+
248
252
  ## Support
249
253
 
250
254
  If this saves you time, consider [buying me a coffee](https://buymeacoffee.com/jjurasszek).
@@ -61,6 +61,9 @@ function classifyGitHubTarget(url) {
61
61
  if (segs.length === 5 && segs[2] === "actions" && segs[3] === "runs" && /^\d+$/.test(segs[4])) {
62
62
  return { kind: "run", slug: `${owner}/${repo}`, runId: segs[4], url: `https://github.com/${owner}/${repo}/actions/runs/${segs[4]}` };
63
63
  }
64
+ if (segs.length === 7 && segs[2] === "actions" && segs[3] === "runs" && /^\d+$/.test(segs[4]) && segs[5] === "job" && /^\d+$/.test(segs[6])) {
65
+ return { kind: "job", slug: `${owner}/${repo}`, jobId: segs[6], url: `https://github.com/${owner}/${repo}/actions/runs/${segs[4]}/job/${segs[6]}` };
66
+ }
64
67
  if (segs.length === 2) {
65
68
  return { kind: "repo", slug: `${owner}/${repo}` };
66
69
  }
@@ -70,8 +73,14 @@ function buildGhArgs(target) {
70
73
  if (target.kind === "issue") return ["issue", "view", target.url, "--comments"];
71
74
  if (target.kind === "pr") return ["pr", "view", target.url, "--comments"];
72
75
  if (target.kind === "run") return ["run", "view", target.runId, "--repo", target.slug];
76
+ if (target.kind === "job") return ["run", "view", "--job", target.jobId, "--repo", target.slug];
73
77
  return ["repo", "view", target.slug];
74
78
  }
79
+ function buildGhLogArgs(target) {
80
+ if (target.kind === "run") return ["run", "view", target.runId, "--log-failed", "--repo", target.slug];
81
+ if (target.kind === "job") return ["run", "view", "--job", target.jobId, "--log-failed", "--repo", target.slug];
82
+ return null;
83
+ }
75
84
  var GH_MAX_BUFFER = 1e7;
76
85
  var execFileAsync = promisify(execFile);
77
86
  var runGh = async (args, timeoutMs, signal) => {
@@ -99,16 +108,22 @@ function ghCommandLabel(target) {
99
108
  if (target.kind === "issue") return "issue view --comments";
100
109
  if (target.kind === "pr") return "pr view --comments";
101
110
  if (target.kind === "run") return "run view";
111
+ if (target.kind === "job") return "run view --job";
102
112
  return "repo view";
103
113
  }
104
114
  function ghSourceLine(target, ref) {
105
115
  if (target.kind === "issue") return `gh issue view ${ref} --comments`;
106
116
  if (target.kind === "pr") return `gh pr view ${ref} --comments`;
107
117
  if (target.kind === "run") return `gh run view ${target.runId} --repo ${target.slug}`;
118
+ if (target.kind === "job") return `gh run view --job ${target.jobId} --repo ${target.slug}`;
108
119
  return `gh repo view ${ref}`;
109
120
  }
110
- function renderGhResult(target, stdout) {
111
- const body = stdout.trimEnd();
121
+ function renderGhResult(target, stdout, failedLogs) {
122
+ const body = failedLogs !== void 0 ? `${stdout.trimEnd()}
123
+
124
+ ## Failed step logs
125
+
126
+ ${failedLogs.trimEnd()}` : stdout.trimEnd();
112
127
  const ref = target.kind === "repo" ? target.slug : target.url;
113
128
  const { spill, bytes, lines } = applyGate(body);
114
129
  const baseDetails = {
@@ -119,7 +134,8 @@ function renderGhResult(target, stdout) {
119
134
  via: "gh",
120
135
  ghCommand: ghCommandLabel(target)
121
136
  };
122
- const source = `Source: ${ghSourceLine(target, ref)}`;
137
+ const source = failedLogs !== void 0 ? `Source: ${ghSourceLine(target, ref)}
138
+ Source: gh ${buildGhLogArgs(target).join(" ")}` : `Source: ${ghSourceLine(target, ref)}`;
123
139
  if (!spill) {
124
140
  return {
125
141
  output: [source, "", body].join("\n"),
@@ -145,9 +161,19 @@ function renderGhResult(target, stdout) {
145
161
  async function executeGhRouting(params, url, signal, runner = runGh) {
146
162
  const target = planGhRouting(params, url);
147
163
  if (!target) return null;
148
- const gh = await runner(buildGhArgs(target), params.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal);
164
+ const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
165
+ signal?.throwIfAborted();
166
+ const gh = await runner(buildGhArgs(target), timeoutMs, signal);
167
+ signal?.throwIfAborted();
149
168
  if (!gh.ok) return null;
150
- return renderGhResult(target, gh.stdout);
169
+ const logArgs = buildGhLogArgs(target);
170
+ let failedLogs;
171
+ if (logArgs) {
172
+ const logs = await runner(logArgs, timeoutMs, signal);
173
+ signal?.throwIfAborted();
174
+ if (logs.ok && logs.stdout !== "") failedLogs = logs.stdout;
175
+ }
176
+ return renderGhResult(target, gh.stdout, failedLogs);
151
177
  }
152
178
  var PARSABLE_MAX_BYTES = 1e6;
153
179
  var BINARY_MAX_BYTES = 5e7;
@@ -21,7 +21,7 @@ export default function fetchExtension(pi: ExtensionAPI) {
21
21
  name: "fetch",
22
22
  label: "Fetch URL",
23
23
  description:
24
- "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. When the body is written to a file, grep it or read with offset/limit; converted Markdown is grep-able by heading (^#). GitHub issue/PR/repo/actions-run URLs are served via the gh CLI when available (falls back to HTTP otherwise; raw=true forces the rendered HTML page).",
24
+ "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. When the body is written to a file, grep it or read with offset/limit; converted Markdown is grep-able by heading (^#). GitHub issue/PR/repo/actions-run/actions-job URLs are served via the gh CLI when available (falls back to HTTP otherwise; raw=true forces the rendered HTML page); failed runs/jobs append failed-step logs (best-effort, summary-only when nothing failed).",
25
25
  promptSnippet: "Fetch the contents of a URL",
26
26
  parameters: Type.Object({
27
27
  url: Type.String({ description: "Absolute http(s) URL" }),
package/lib/fetch-core.ts CHANGED
@@ -29,7 +29,8 @@ export type GhTarget =
29
29
  | { kind: "issue"; url: string }
30
30
  | { kind: "pr"; url: string }
31
31
  | { kind: "repo"; slug: string }
32
- | { kind: "run"; slug: string; runId: string; url: string };
32
+ | { kind: "run"; slug: string; runId: string; url: string }
33
+ | { kind: "job"; slug: string; jobId: string; url: string };
33
34
 
34
35
  export function formatSize(bytes: number): string {
35
36
  if (bytes < 1024) {
@@ -80,6 +81,9 @@ export function classifyGitHubTarget(url: URL): GhTarget | null {
80
81
  if (segs.length === 5 && segs[2] === "actions" && segs[3] === "runs" && /^\d+$/.test(segs[4])) {
81
82
  return { kind: "run", slug: `${owner}/${repo}`, runId: segs[4], url: `https://github.com/${owner}/${repo}/actions/runs/${segs[4]}` };
82
83
  }
84
+ if (segs.length === 7 && segs[2] === "actions" && segs[3] === "runs" && /^\d+$/.test(segs[4]) && segs[5] === "job" && /^\d+$/.test(segs[6])) {
85
+ return { kind: "job", slug: `${owner}/${repo}`, jobId: segs[6], url: `https://github.com/${owner}/${repo}/actions/runs/${segs[4]}/job/${segs[6]}` };
86
+ }
83
87
  if (segs.length === 2) {
84
88
  return { kind: "repo", slug: `${owner}/${repo}` };
85
89
  }
@@ -90,9 +94,16 @@ export function buildGhArgs(target: GhTarget): string[] {
90
94
  if (target.kind === "issue") return ["issue", "view", target.url, "--comments"];
91
95
  if (target.kind === "pr") return ["pr", "view", target.url, "--comments"];
92
96
  if (target.kind === "run") return ["run", "view", target.runId, "--repo", target.slug];
97
+ if (target.kind === "job") return ["run", "view", "--job", target.jobId, "--repo", target.slug];
93
98
  return ["repo", "view", target.slug];
94
99
  }
95
100
 
101
+ export function buildGhLogArgs(target: GhTarget): string[] | null {
102
+ if (target.kind === "run") return ["run", "view", target.runId, "--log-failed", "--repo", target.slug];
103
+ if (target.kind === "job") return ["run", "view", "--job", target.jobId, "--log-failed", "--repo", target.slug];
104
+ return null;
105
+ }
106
+
96
107
  const GH_MAX_BUFFER = 10_000_000; // 10 MB — an order above PARSABLE_MAX_BYTES
97
108
 
98
109
  type GhResult = { ok: true; stdout: string } | { ok: false };
@@ -135,6 +146,7 @@ function ghCommandLabel(target: GhTarget): string {
135
146
  if (target.kind === "issue") return "issue view --comments";
136
147
  if (target.kind === "pr") return "pr view --comments";
137
148
  if (target.kind === "run") return "run view";
149
+ if (target.kind === "job") return "run view --job";
138
150
  return "repo view";
139
151
  }
140
152
 
@@ -142,11 +154,14 @@ function ghSourceLine(target: GhTarget, ref: string): string {
142
154
  if (target.kind === "issue") return `gh issue view ${ref} --comments`;
143
155
  if (target.kind === "pr") return `gh pr view ${ref} --comments`;
144
156
  if (target.kind === "run") return `gh run view ${target.runId} --repo ${target.slug}`;
157
+ if (target.kind === "job") return `gh run view --job ${target.jobId} --repo ${target.slug}`;
145
158
  return `gh repo view ${ref}`;
146
159
  }
147
160
 
148
- function renderGhResult(target: GhTarget, stdout: string): FetchResult {
149
- const body = stdout.trimEnd();
161
+ function renderGhResult(target: GhTarget, stdout: string, failedLogs?: string): FetchResult {
162
+ const body = failedLogs !== undefined
163
+ ? `${stdout.trimEnd()}\n\n## Failed step logs\n\n${failedLogs.trimEnd()}`
164
+ : stdout.trimEnd();
150
165
  const ref = target.kind === "repo" ? target.slug : target.url;
151
166
  const { spill, bytes, lines } = applyGate(body);
152
167
  const baseDetails: FetchToolDetails = {
@@ -157,7 +172,9 @@ function renderGhResult(target: GhTarget, stdout: string): FetchResult {
157
172
  via: "gh",
158
173
  ghCommand: ghCommandLabel(target),
159
174
  };
160
- const source = `Source: ${ghSourceLine(target, ref)}`;
175
+ const source = failedLogs !== undefined
176
+ ? `Source: ${ghSourceLine(target, ref)}\nSource: gh ${buildGhLogArgs(target)!.join(" ")}`
177
+ : `Source: ${ghSourceLine(target, ref)}`;
161
178
  if (!spill) {
162
179
  return {
163
180
  output: [source, "", body].join("\n"),
@@ -189,9 +206,19 @@ export async function executeGhRouting(
189
206
  ): Promise<FetchResult | null> {
190
207
  const target = planGhRouting(params, url);
191
208
  if (!target) return null;
192
- const gh = await runner(buildGhArgs(target), params.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal);
209
+ const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
210
+ signal?.throwIfAborted();
211
+ const gh = await runner(buildGhArgs(target), timeoutMs, signal);
212
+ signal?.throwIfAborted();
193
213
  if (!gh.ok) return null;
194
- return renderGhResult(target, gh.stdout);
214
+ const logArgs = buildGhLogArgs(target);
215
+ let failedLogs: string | undefined;
216
+ if (logArgs) {
217
+ const logs = await runner(logArgs, timeoutMs, signal);
218
+ signal?.throwIfAborted();
219
+ if (logs.ok && logs.stdout !== "") failedLogs = logs.stdout;
220
+ }
221
+ return renderGhResult(target, gh.stdout, failedLogs);
195
222
  }
196
223
 
197
224
  const PARSABLE_MAX_BYTES = 1_000_000; // text/markdown/json download ceiling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-quiver",
3
- "version": "4.2.0",
3
+ "version": "4.3.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, a themed ASCII startup header, Opus 4.8 fast mode, and a provider-stall watchdog.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",