pi-quiver 4.4.0 → 5.0.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,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
+ ## v5.0.0 - 2026-08-29
12
+
13
+ - **New opt-in `slack` extension** (#7): eight `slack_*` tools (search, thread, post, update, delete, pin, upload, cache refresh) for context-safe Slack search/threads/posting. Dual `user`/`bot` token identities resolved per call from process env or the repo's `.env`, never cross-identity fallback. Workspace-keyed channel/user name->ID cache with a repo-overridable `cachePath`. Fetch-style output size gating on search/thread reads. `slack_post`'s `thread_body` (no `thread_ts`) posts a transactional headline+thread announce - oversized detail bodies upload as a file - with a documented recovery path on delivery failure. OFF by default; nested-only `quiver.slack` config, no legacy flat form. See [doc/slack.md](doc/slack.md).
14
+
15
+ ## v4.5.0 - 2026-08-29
16
+
17
+ - Settings resolution: every pi-quiver setting is now read from an optional `"quiver"` root object in `settings.json` (`quiver.<key>`), grouping the four legacy flat keys (`fastMode`, `sessionAutoName`, `swordHeader`, `providerStallWatchdog`) plus any future key. The flat top-level form keeps working, but only for those four legacy keys - it is frozen there and never extended to new settings. Within a layer, `quiver.<key>` wins over flat `<key>` by presence; malformed values and flat/nested duplicates now emit a warning instead of resolving silently.
18
+
11
19
  ## v4.4.0 - 2026-08-26
12
20
 
13
21
  - doc_to_md: backend ladder now tries a system Python >= 3.12 with `pymupdf4llm` importable, then a one-time managed venv (bootstrapped at the version pin into a per-OS cache dir) between the existing `uv` and `unpdf` rungs. Data plane extracted to pi-free `lib/doc-to-md-core.ts`; new `pi-quiver doc-to-md <path>` CLI subcommand and `doc-to-md` Claude Code skill. `uv`/`soffice` detection is now spawn-based (Windows-correct). The bundled Python conversion script is now resolved from the package root, fixing a path bug that broke it under the bundled CLI.
package/README.md CHANGED
@@ -18,7 +18,7 @@ But the moment an agent does that, one `fetch` or PDF read can dump hundreds of
18
18
 
19
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.
20
20
 
21
- `session-name`, `sword-header`, `fast-mode`, and `provider-stall-watchdog` are opt-in ergonomics and recovery controls: session labeling, a themed startup header, Anthropic fast mode, and semantic-stall recovery.
21
+ `session-name`, `sword-header`, `fast-mode`, `provider-stall-watchdog`, and `slack` are opt-in ergonomics, recovery, and integration controls: session labeling, a themed startup header, Anthropic fast mode, semantic-stall recovery, and context-safe Slack search/threads/posting.
22
22
 
23
23
  ## Part of the pi agent toolkit
24
24
 
@@ -33,7 +33,7 @@ No code dependency between them. pi-quiver is call-level: it gates the size of w
33
33
 
34
34
  ## Mental model
35
35
 
36
- Every ingestion extension here is context-safe by construction, not by convention: the size check runs on every call, there's no flag to forget. `fetch` and `doc_to_md` bring real sources in; `session-name`, `sword-header`, `fast-mode`, and `provider-stall-watchdog` are opt-in.
36
+ Every ingestion extension here is context-safe by construction, not by convention: the size check runs on every call, there's no flag to forget. `fetch` and `doc_to_md` bring real sources in; `session-name`, `sword-header`, `fast-mode`, `provider-stall-watchdog`, and `slack` are opt-in.
37
37
 
38
38
  ```mermaid
39
39
  flowchart LR
@@ -68,8 +68,9 @@ A 300 KB changelog page never touches your context window - you get a preview an
68
68
  | `extensions/sword-header.ts` | `/builtin-header` | Themed ASCII startup header replacing pi's default logo. OFF by default. |
69
69
  | `extensions/fast-mode.ts` | `/fast` | Inject Anthropic fast-mode (`speed: "fast"` + `anthropic-beta: fast-mode-2026-02-01`) into every Claude Opus 4.8 / Opus 5 request, any thinking level. `--fast` flag + `/fast [on\|off\|status]`. OFF by default. |
70
70
  | `extensions/provider-stall-watchdog.ts` | - | Opt-in provider-stall recovery, in two tiers: a pre-first-event deadline (`firstEventMs`, 20s) on every provider request in every mode, and the mid-stream pair (warn at 2 min, recover at 4 min) in TUI runs only. Policy D offers each stall to Pi's retry loop until the stall retry budget (`maxStallRetries`, default = `retry.maxRetries`) is exhausted. OFF by default. |
71
+ | `extensions/slack.ts` | `slack_search`, `slack_thread`, `slack_post`, `slack_update`, `slack_delete`, `slack_pin`, `slack_upload`, `slack_cache_refresh` | Context-safe Slack search/threads/posting with dual `user`/`bot` token identities, a workspace-keyed channel/user name->ID cache, fetch-style output size gating, and a transactional headline+detail-thread announce protocol with a documented recovery path. OFF by default. Behavior lives in `lib/slack-core.ts` and `lib/slack-cache.ts`. |
71
72
 
72
- Full routing rules, size-gate mechanics, and config: [doc/fetch.md](doc/fetch.md), [doc/doc-to-md.md](doc/doc-to-md.md).
73
+ Full routing rules, size-gate mechanics, and config: [doc/fetch.md](doc/fetch.md), [doc/doc-to-md.md](doc/doc-to-md.md), [doc/slack.md](doc/slack.md).
73
74
 
74
75
  ## Key concepts
75
76
 
@@ -78,7 +79,7 @@ Full routing rules, size-gate mechanics, and config: [doc/fetch.md](doc/fetch.md
78
79
  | 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
80
  | 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
81
  | Graceful degradation | Optional binaries (`gh`, `uv`, LibreOffice) are never hard install-time deps; each has a defined, documented fallback or failure mode. |
81
- | Opt-in extensions | `session-name`, `sword-header`, `fast-mode`, and `provider-stall-watchdog` do nothing until explicitly enabled in `settings.json`. |
82
+ | Opt-in extensions | `session-name`, `sword-header`, `fast-mode`, `provider-stall-watchdog`, and `slack` do nothing until explicitly enabled in `settings.json`. |
82
83
  | 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. |
83
84
 
84
85
  ## When to use
@@ -137,45 +138,72 @@ None is a hard install-time dependency of the package; they are tools you provid
137
138
 
138
139
  ### Opt-in extension config
139
140
 
140
- These extensions are opt-in via `settings.json` (project `.pi/settings.json` overrides the global agent-dir layer):
141
+ These extensions are opt-in via `settings.json` (project `.pi/settings.json` overrides the global agent-dir layer), nested under an optional `"quiver"` root:
141
142
 
142
143
  ```jsonc
143
144
  {
144
- "sessionAutoName": {
145
- "enabled": false,
146
- "ghosttyTab": true,
147
- "rules": [],
148
- "deny": [],
149
- "revisitFirstTurn": 0,
150
- "revisitEveryTurns": 0
151
- }, // or boolean shorthand
152
- "swordHeader": false, // or { "enabled": true }
153
- "fastMode": false, // or { "enabled": true }
154
- "providerStallWatchdog": false // or { "enabled": true }
145
+ "quiver": {
146
+ "sessionAutoName": {
147
+ "enabled": false,
148
+ "ghosttyTab": true,
149
+ "rules": [],
150
+ "deny": [],
151
+ "revisitFirstTurn": 0,
152
+ "revisitEveryTurns": 0
153
+ }, // or boolean shorthand
154
+ "swordHeader": false, // or { "enabled": true }
155
+ "fastMode": false, // or { "enabled": true }
156
+ "providerStallWatchdog": false, // or { "enabled": true }
157
+ "slack": false // or { "enabled": true, ... }
158
+ }
155
159
  }
156
160
  ```
157
161
 
162
+ Each key resolves independently: within a layer, `quiver.<key>` wins over a
163
+ flat top-level `<key>` by presence alone (even when the winning value is
164
+ malformed); across layers, each layer's candidate is validated into a partial
165
+ patch and `Object.assign`ed over the accumulator in layer order (project
166
+ last), so project fields override matching global fields while unmatched
167
+ global fields survive - a flat-vs-nested shape difference between layers
168
+ never changes this. The flat top-level form still works, but only for four
169
+ legacy keys, frozen at `fastMode`, `sessionAutoName`, `swordHeader`, and
170
+ `providerStallWatchdog` - never extended to new settings (see [Migrating from
171
+ flat keys](#migrating-from-flat-keys)).
172
+
173
+ Worked mixed-shape example: global `settings.json` has flat
174
+ `"fastMode": false`, project `.pi/settings.json` has
175
+ `"quiver": { "fastMode": { "enabled": true } }`. The project layer's
176
+ patch (`{ "enabled": true }`) is `Object.assign`ed over the accumulator
177
+ seeded from global's patch, so the resolved config is `{ "enabled": true }` -
178
+ same outcome here because `enabled` is the only field either layer sets, but
179
+ the merge is per-field: a global field with no project counterpart would
180
+ survive untouched.
181
+
158
182
  `sessionAutoName.enabled` makes one extra short LLM call per session (once, after the first turn) to title it; `false` (default) makes no model calls. `rules` appends house conventions to the naming prompt (later rules win when they conflict with the built-ins). Literal, case-insensitive `deny` phrases are stripped from every name; whitespace inside a phrase is loose, so `"acme corp"` also catches `AcmeCorp`. `revisitFirstTurn` re-evaluates the name once that many model round trips have completed, while `revisitEveryTurns` does so at every multiple; both default to `0` (off) because each revisit costs another short LLM call. For example, `10` and `100` mark round trips 10, 100, 200, 300. Revisits only run when the agent has fully settled (idle, nothing queued) - an automated multi-turn run such as a subagent chain is never renamed or delayed mid-flight; cadence points it crossed fire once, at the settle. A machine-generated name is replaced when stale. A name set by a human is never overwritten: the extension strongly prefers it, and announces a suggestion only when the work has clearly moved on. Counts come from the persisted transcript, so they survive resume.
159
183
 
160
184
  `fastMode` only affects `claude-opus-4-8` and `claude-opus-5` 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.
161
185
 
162
186
  `pi-ai` prices every fast request at standard rates - it has no `usage.speed` support and no request-level pricing modifier - so `fastMode` corrects the reported cost itself: a `message_end` handler scales all four `usage.cost` components by `FAST_MODE_COST_MULTIPLIER` (2x) and returns the corrected message. Persisted session JSONL and pi's own native cost display are always exact, since they're written from this corrected message. pi-cohort's live `Σ$` reflects the correction only when pi-quiver's `message_end` handler runs before pi-cohort's - best-effort, depending on extension load order - and is reconciled on pi-cohort's next `session_start` regardless. The upstream fix (teaching `pi-ai`'s `Usage`/`calculateCost` about `usage.speed`) is the better long-term path and is tracked separately.
163
187
 
164
- Recommended explicit retry and watchdog settings:
188
+ Recommended explicit retry and watchdog settings - `providerStallWatchdog`
189
+ nests under `quiver`, while pi-core's own `retry` stays flat beside it (it is
190
+ not a pi-quiver setting and is never nested):
165
191
 
166
192
  ```json
167
193
  {
194
+ "quiver": {
195
+ "providerStallWatchdog": {
196
+ "enabled": true,
197
+ "firstEventMs": 20000,
198
+ "warningMs": 120000,
199
+ "recoveryMs": 240000,
200
+ "maxStallRetries": 3
201
+ }
202
+ },
168
203
  "retry": {
169
204
  "enabled": true,
170
205
  "maxRetries": 3,
171
206
  "baseDelayMs": 2000
172
- },
173
- "providerStallWatchdog": {
174
- "enabled": true,
175
- "firstEventMs": 20000,
176
- "warningMs": 120000,
177
- "recoveryMs": 240000,
178
- "maxStallRetries": 3
179
207
  }
180
208
  }
181
209
  ```
@@ -205,13 +233,63 @@ Operational notes:
205
233
  - **A watchdog abort that the provider ignores escalates after a fixed 10s.** Any post-abort stream event re-arms that deadline (bytes prove only that the connection was alive at that instant), so a stream that emits a straggler and then wedges still escalates 10s after its last event. This reduces the hang; it cannot force the provider to stop, and undici's timeouts remain the final backstop.
206
234
  - **Headless runs report on stderr.** In `print`/`json` mode pi binds a no-op UI, so watchdog notices go out via `console.warn`. Nothing is ever written to stdout, which `json` mode uses for its protocol. In TUI and RPC the notices render as main-window notifications, not the bottom status line.
207
235
 
236
+ `slack` is OFF by default and, once enabled, adds eight `slack_*` tools (search, thread, post, update, delete, pin, upload, cache refresh) covering context-safe Slack search/threads/posting under dual `user`/`bot` token identities. Nested-only from day one (no legacy flat form):
237
+
238
+ ```json
239
+ {
240
+ "quiver": {
241
+ "slack": {
242
+ "enabled": true,
243
+ "cachePath": ".pi/slack-cache.json",
244
+ "userTokenEnv": "SLACK_USER_TOKEN",
245
+ "botTokenEnv": "SLACK_BOT_TOKEN",
246
+ "uploadThresholdChars": 4000
247
+ }
248
+ }
249
+ }
250
+ ```
251
+
252
+ | Key | Default | Meaning |
253
+ | --- | --- | --- |
254
+ | `enabled` | `false` | Master switch, checked at `session_start`; toggling takes effect next session. |
255
+ | `cachePath` | user-scope per-OS cache dir | Overrides where the workspace-keyed channel/user name->ID cache file is written; relative paths resolve against the repo root. |
256
+ | `userTokenEnv` | `SLACK_USER_TOKEN` | Env var name holding the user token (required for `slack_search`/`slack_thread`, no bot fallback). |
257
+ | `botTokenEnv` | `SLACK_BOT_TOKEN` | Env var name holding the bot token. |
258
+ | `uploadThresholdChars` | `4000` | Link-collapsed length above which an announce/thread detail body is delivered as a file upload instead of inline text. |
259
+
260
+ Each setting can also be overridden per-process via `PI_QUIVER_SLACK_ENABLED`, `PI_QUIVER_SLACK_CACHE_PATH`, `PI_QUIVER_SLACK_USER_TOKEN_ENV`, `PI_QUIVER_SLACK_BOT_TOKEN_ENV`, and `PI_QUIVER_SLACK_UPLOAD_THRESHOLD_CHARS` - applied on top of the resolved `settings.json` layers, same override rung the extension's config resolver defines. Tokens themselves are resolved per call: process env first, then the repo's `.env` file (or the primary checkout's, for a worktree with none) - never a fallback across identities. Full reference incl. cache layering, the announce protocol, and the `search.messages`/`conversations.replies` throttle caveats: [doc/slack.md](doc/slack.md).
261
+
262
+ ### Migrating from flat keys
263
+
264
+ The flat top-level form (`"fastMode": ...` etc. directly under `settings.json`)
265
+ is the outdated configuration style. It is legacy-frozen to exactly the four
266
+ keys above - `fastMode`, `sessionAutoName`, `swordHeader`,
267
+ `providerStallWatchdog` - and will never gain a fifth. To migrate, wrap your
268
+ existing keys under `"quiver": { ... }` and delete the flat copies:
269
+
270
+ ```jsonc
271
+ // before
272
+ { "fastMode": true }
273
+
274
+ // after
275
+ { "quiver": { "fastMode": true } }
276
+ ```
277
+
278
+ Until you delete the flat copy, having both set is not an error - the
279
+ duplicate resolves per the precedence above (nested wins within a layer) -
280
+ but it emits a warning notification, deduped per process (each unique
281
+ message fires at most once per pi process - in practice once per interactive
282
+ session) until the flat entry is removed. Every new pi-quiver setting introduced after this change
283
+ (for example a future `slack` key) is nested-only from day one: it has no
284
+ flat form to fall back to.
285
+
208
286
  ## Claude Code support
209
287
 
210
288
  `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
289
 
212
290
  **Exposed:** the `quiver` plugin, served from this repo's `.claude-plugin/marketplace.json`, with two skills: `fetch` (invoked as `quiver:fetch` / `/quiver:fetch`) and `doc-to-md` (invoked as `quiver:doc-to-md` / `/quiver:doc-to-md`). The `fetch` 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. The `doc-to-md` skill runs `npx -y pi-quiver@latest doc-to-md <path>` via Bash - same backend ladder, size gate, and degraded-fallback marking as the pi tool. See [doc/doc-to-md.md](doc/doc-to-md.md#cli-pi-quiver-doc-to-md) for exit codes.
213
291
 
214
- **Not exposed:** the other pi extensions in this package (`session-name`, `sword-header`, `fast-mode`, `provider-stall-watchdog`) - the marketplace allowlists only `./skills/fetch` and `./skills/doc-to-md`, 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).
292
+ **Not exposed:** the other pi extensions in this package (`session-name`, `sword-header`, `fast-mode`, `provider-stall-watchdog`, `slack`) - the marketplace allowlists only `./skills/fetch` and `./skills/doc-to-md`, 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
293
 
216
294
  Add the marketplace and enable the plugin in `.claude/settings.json`:
217
295
 
@@ -116,7 +116,7 @@ export default function (pi: ExtensionAPI) {
116
116
  const readFlag = (): boolean => pi.getFlag("fast") === true;
117
117
 
118
118
  const resolveState = (ctx: ExtensionContext): boolean => {
119
- const config = resolveConfig(ctx.cwd, "fastMode", DEFAULT_CONFIG, coerce).enabled;
119
+ const config = resolveConfig(ctx.cwd, "fastMode", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning")).enabled;
120
120
  enabled = resolveEnabled({ config, flag: readFlag(), live: liveOverride });
121
121
  return enabled;
122
122
  };
@@ -91,8 +91,8 @@ export function resolveRetryMaxRetries(cwd: string): number {
91
91
  return maxRetries;
92
92
  }
93
93
 
94
- export function resolveWatchdogConfig(cwd: string): ConfigValidation {
95
- const candidate = resolveConfig(cwd, "providerStallWatchdog", DEFAULT_CANDIDATE, coerce);
94
+ export function resolveWatchdogConfig(cwd: string, warn?: (msg: string) => void): ConfigValidation {
95
+ const candidate = resolveConfig(cwd, "providerStallWatchdog", DEFAULT_CANDIDATE, coerce, warn);
96
96
  if (candidate.blockIsObject === true && candidate.maxStallRetries === undefined) {
97
97
  candidate.maxStallRetries = resolveRetryMaxRetries(cwd);
98
98
  }
@@ -278,7 +278,7 @@ export function createProviderStallWatchdog(runtime: WatchdogRuntime = defaultRu
278
278
  ui = ctx.ui;
279
279
  hasUI = ctx.hasUI;
280
280
  if (!config) {
281
- const resolved = resolveWatchdogConfig(ctx.cwd);
281
+ const resolved = resolveWatchdogConfig(ctx.cwd, (m) => announce(m, "warning"));
282
282
  if (!resolved.ok) {
283
283
  disabled = true;
284
284
  announce(`providerStallWatchdog disabled: ${resolved.error}`, "warning");
@@ -96,7 +96,7 @@ export function coerce(raw: unknown): Partial<Config> | undefined {
96
96
  }
97
97
 
98
98
  function loadConfig(ctx: ExtensionContext): Config {
99
- return resolveConfig(ctx.cwd, "sessionAutoName", DEFAULT_CONFIG, coerce);
99
+ return resolveConfig(ctx.cwd, "sessionAutoName", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning"));
100
100
  }
101
101
 
102
102
  type ContentBlock = { type?: string; text?: string };