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 +4 -0
- package/README.md +8 -4
- package/dist/bin/pi-quiver.js +31 -5
- package/extensions/fetch.ts +1 -1
- package/lib/fetch-core.ts +33 -6
- package/package.json +1 -1
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).
|
package/dist/bin/pi-quiver.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|
package/extensions/fetch.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|