pi-quiver 5.3.0 → 5.4.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 +15 -0
- package/README.md +22 -7
- package/dist/bin/pi-quiver.js +5 -4
- package/extensions/fast-mode.ts +2 -2
- package/extensions/session-name.ts +2 -2
- package/extensions/slack.ts +1 -1
- package/extensions/sword-header.ts +1 -1
- package/lib/doc-to-md-options.ts +2 -2
- package/lib/extension-config.ts +54 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,21 @@ 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.4.0 - 2026-09-10
|
|
12
|
+
|
|
13
|
+
- Settings: one condensed `pi-quiver settings` warning (each distinct message
|
|
14
|
+
once per process) lists flat legacy keys in use, unknown `quiver.<block>`
|
|
15
|
+
names, and unknown fields inside
|
|
16
|
+
known blocks, with accepted names inline; unknown keys fall back to defaults.
|
|
17
|
+
Replaces the separate non-object-`quiver` and flat+nested-duplicate sentences;
|
|
18
|
+
a flat legacy key alone now warns (still honoured). Accepted names are the
|
|
19
|
+
exported `QUIVER_CONFIG_KEYS` registry in `lib/extension-config.ts` (#14).
|
|
20
|
+
- Settings warnings from `fast-mode`, `session-name`, and `slack` reach stderr
|
|
21
|
+
when no UI is bound (print/json modes) instead of being dropped by the no-op
|
|
22
|
+
headless `notify`.
|
|
23
|
+
- doc_to_md: the coercer no longer warns about unknown `quiver.docToMd` keys (the
|
|
24
|
+
settings lint does); the `pi-quiver` CLI reader warns about them itself.
|
|
25
|
+
|
|
11
26
|
## v5.3.0 - 2026-09-09
|
|
12
27
|
|
|
13
28
|
- doc_to_md: results are now a bundle on disk (`<stem>.md` + `images/`, temp dir
|
package/README.md
CHANGED
|
@@ -173,6 +173,22 @@ legacy keys, frozen at `fastMode`, `sessionAutoName`, `swordHeader`, and
|
|
|
173
173
|
`providerStallWatchdog` - never extended to new settings (see [Migrating from
|
|
174
174
|
flat keys](#migrating-from-flat-keys)).
|
|
175
175
|
|
|
176
|
+
pi-quiver lints the whole `quiver` object in both files each time a setting is
|
|
177
|
+
resolved and emits **one** condensed warning (TUI: a `Warning:` block in the
|
|
178
|
+
chat; headless: stderr) listing every flat legacy key in use, every unknown
|
|
179
|
+
`quiver.<block>`, and every unknown field inside a known block, with the
|
|
180
|
+
accepted names inline. Unknown keys fall back to their defaults. Each distinct
|
|
181
|
+
message fires once per pi process; a clean file emits nothing. A wrong value
|
|
182
|
+
type (`"fastMode": "yes"`) is a separate per-key `unrecognized value` warning.
|
|
183
|
+
The accepted names live in `QUIVER_CONFIG_KEYS` in `lib/extension-config.ts` -
|
|
184
|
+
a new setting is registered there or it warns as unknown.
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
Warning: pi-quiver settings (/Users/x/.pi/agent/settings.json): unknown or misplaced keys - unknown ones fall back to defaults
|
|
188
|
+
"providerStallWatchdog" at top level - move under "quiver"
|
|
189
|
+
"quiver.providerStallWatchdog.timeoutMs" - unknown; accepted: enabled, firstEventMs, warningMs, recoveryMs, maxStallRetries
|
|
190
|
+
```
|
|
191
|
+
|
|
176
192
|
Worked mixed-shape example: global `settings.json` has flat
|
|
177
193
|
`"fastMode": false`, project `.pi/settings.json` has
|
|
178
194
|
`"quiver": { "fastMode": { "enabled": true } }`. The project layer's
|
|
@@ -304,13 +320,12 @@ existing keys under `"quiver": { ... }` and delete the flat copies:
|
|
|
304
320
|
{ "quiver": { "fastMode": true } }
|
|
305
321
|
```
|
|
306
322
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
flat form to fall back to.
|
|
323
|
+
A flat copy keeps resolving (nested wins within a layer, project layer wins
|
|
324
|
+
across layers) but every flat legacy key in use is listed in the condensed
|
|
325
|
+
settings warning above (`"fastMode" at top level - move under "quiver"`) until
|
|
326
|
+
it is moved. Every new pi-quiver setting introduced after this change (for
|
|
327
|
+
example `slack`) is nested-only from day one: it has no flat form to fall back
|
|
328
|
+
to, and a flat `slack` block is ignored without a warning.
|
|
314
329
|
|
|
315
330
|
## Claude Code support
|
|
316
331
|
|
package/dist/bin/pi-quiver.js
CHANGED
|
@@ -796,10 +796,7 @@ function coerceDocToMdSettings(raw, warn = console.warn) {
|
|
|
796
796
|
const out = {};
|
|
797
797
|
for (const [k, v] of Object.entries(raw)) {
|
|
798
798
|
const d = DOC_TO_MD_OPTIONS.find((o) => o.key === k);
|
|
799
|
-
if (!d || !d.settable)
|
|
800
|
-
warn(`pi-quiver: quiver.docToMd.${k} is not a tunable setting; ignored.`);
|
|
801
|
-
continue;
|
|
802
|
-
}
|
|
799
|
+
if (!d || !d.settable) continue;
|
|
803
800
|
const c = coerceValue(d, v);
|
|
804
801
|
if (!c.ok) {
|
|
805
802
|
warn(`pi-quiver: quiver.docToMd.${k} ${c.reason}; ignored.`);
|
|
@@ -1406,6 +1403,10 @@ function readCliSettings(cwd, env, warn) {
|
|
|
1406
1403
|
continue;
|
|
1407
1404
|
}
|
|
1408
1405
|
if (raw === void 0) continue;
|
|
1406
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1407
|
+
const unknown = Object.keys(raw).filter((k) => !DOC_TO_MD_OPTIONS.some((o) => o.key === k && o.settable));
|
|
1408
|
+
if (unknown.length > 0) warn(`pi-quiver: quiver.docToMd in ${file} has keys that are not tunable settings; ignored: ${unknown.join(", ")}`);
|
|
1409
|
+
}
|
|
1409
1410
|
const patch = coerceDocToMdSettings(raw, warn);
|
|
1410
1411
|
if (patch) Object.assign(out, patch);
|
|
1411
1412
|
else warn(`pi-quiver: "docToMd" in ${file} has an unrecognized value; ignored.`);
|
package/extensions/fast-mode.ts
CHANGED
|
@@ -65,7 +65,7 @@ const STATUS_KEY = "fast-mode";
|
|
|
65
65
|
const BETA_HEADER = "anthropic-beta";
|
|
66
66
|
|
|
67
67
|
type Config = { enabled: boolean };
|
|
68
|
-
const DEFAULT_CONFIG: Config = { enabled: false };
|
|
68
|
+
export const DEFAULT_CONFIG: Config = { enabled: false };
|
|
69
69
|
|
|
70
70
|
export function coerce(raw: unknown): Partial<Config> | undefined {
|
|
71
71
|
if (raw === undefined) return undefined;
|
|
@@ -189,7 +189,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
189
189
|
const readFlag = (): boolean => pi.getFlag("fast") === true;
|
|
190
190
|
|
|
191
191
|
const resolveState = (ctx: ExtensionContext): boolean => {
|
|
192
|
-
const config = resolveConfig(ctx.cwd, "fastMode", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning")).enabled;
|
|
192
|
+
const config = resolveConfig(ctx.cwd, "fastMode", DEFAULT_CONFIG, coerce, (m) => ctx.hasUI ? ctx.ui.notify(m, "warning") : console.warn(m)).enabled;
|
|
193
193
|
enabled = resolveEnabled({ config, flag: readFlag(), live: liveOverride });
|
|
194
194
|
return enabled;
|
|
195
195
|
};
|
|
@@ -64,7 +64,7 @@ type Config = {
|
|
|
64
64
|
revisitFirstTurn: number;
|
|
65
65
|
revisitEveryTurns: number;
|
|
66
66
|
};
|
|
67
|
-
const DEFAULT_CONFIG: Config = {
|
|
67
|
+
export const DEFAULT_CONFIG: Config = {
|
|
68
68
|
enabled: false,
|
|
69
69
|
ghosttyTab: true,
|
|
70
70
|
herdrTab: true,
|
|
@@ -105,7 +105,7 @@ export function coerce(raw: unknown): Partial<Config> | undefined {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function loadConfig(ctx: ExtensionContext): Config {
|
|
108
|
-
return resolveConfig(ctx.cwd, "sessionAutoName", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning"));
|
|
108
|
+
return resolveConfig(ctx.cwd, "sessionAutoName", DEFAULT_CONFIG, coerce, (m) => ctx.hasUI ? ctx.ui.notify(m, "warning") : console.warn(m));
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
type ContentBlock = { type?: string; text?: string };
|
package/extensions/slack.ts
CHANGED
|
@@ -200,7 +200,7 @@ export default function slackExtension(pi: ExtensionAPI) {
|
|
|
200
200
|
let registered = false;
|
|
201
201
|
|
|
202
202
|
pi.on("session_start", async (_event, ctx) => {
|
|
203
|
-
const cfg = resolveSlackConfig(ctx.cwd, process.env, (m) => ctx.ui.notify(m, "warning"));
|
|
203
|
+
const cfg = resolveSlackConfig(ctx.cwd, process.env, (m) => ctx.hasUI ? ctx.ui.notify(m, "warning") : console.warn(m));
|
|
204
204
|
if (cfg.enabled !== true) return;
|
|
205
205
|
if (registered) return;
|
|
206
206
|
registered = true;
|
|
@@ -18,7 +18,7 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
18
18
|
import { resolveConfig } from "../lib/extension-config.ts";
|
|
19
19
|
|
|
20
20
|
type Config = { enabled: boolean };
|
|
21
|
-
const DEFAULT_CONFIG: Config = { enabled: false };
|
|
21
|
+
export const DEFAULT_CONFIG: Config = { enabled: false };
|
|
22
22
|
|
|
23
23
|
export function coerce(raw: unknown): Partial<Config> | undefined {
|
|
24
24
|
if (raw === undefined) return undefined;
|
package/lib/doc-to-md-options.ts
CHANGED
|
@@ -105,13 +105,13 @@ function coerceValue(d: OptionDescriptor, raw: unknown, fromString = false): { o
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/**
|
|
108
|
+
/** Type-validation boundary for `quiver.docToMd`: ill-typed keys are dropped with one warning each; unknown/intent keys are skipped (the settings lint reports them). */
|
|
109
109
|
export function coerceDocToMdSettings(raw: unknown, warn: (message: string) => void = console.warn): Partial<Tunables> | undefined {
|
|
110
110
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
111
111
|
const out: Record<string, unknown> = {};
|
|
112
112
|
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
113
113
|
const d = DOC_TO_MD_OPTIONS.find((o) => o.key === k);
|
|
114
|
-
if (!d || !d.settable)
|
|
114
|
+
if (!d || !d.settable) continue;
|
|
115
115
|
const c = coerceValue(d, v);
|
|
116
116
|
if (!c.ok) { warn(`pi-quiver: quiver.docToMd.${k} ${c.reason}; ignored.`); continue; }
|
|
117
117
|
out[k] = c.value;
|
package/lib/extension-config.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { DOC_TO_MD_OPTIONS } from "./doc-to-md-options.ts";
|
|
23
24
|
|
|
24
25
|
export function readSettings(path: string): Record<string, unknown> | undefined {
|
|
25
26
|
try {
|
|
@@ -36,6 +37,22 @@ export function settingsPaths(cwd: string): string[] {
|
|
|
36
37
|
/** Flat top-level fallback is frozen to these pre-quiver keys; never extend. */
|
|
37
38
|
const LEGACY_FLAT_KEYS = new Set(["fastMode", "sessionAutoName", "swordHeader", "providerStallWatchdog"]);
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Lint allowlist: every pi-quiver settings block and its fields. A key read
|
|
42
|
+
* by an extension but missing here is reported as unknown - register new
|
|
43
|
+
* keys here first.
|
|
44
|
+
*/
|
|
45
|
+
export const QUIVER_CONFIG_KEYS: Record<string, readonly string[]> = {
|
|
46
|
+
fastMode: ["enabled"],
|
|
47
|
+
sessionAutoName: ["enabled", "ghosttyTab", "herdrTab", "rules", "deny", "revisitFirstTurn", "revisitEveryTurns"],
|
|
48
|
+
swordHeader: ["enabled"],
|
|
49
|
+
providerStallWatchdog: ["enabled", "firstEventMs", "warningMs", "recoveryMs", "maxStallRetries"],
|
|
50
|
+
slack: ["enabled", "cachePath", "policyPath", "userTokenEnv", "botTokenEnv", "uploadThresholdChars"],
|
|
51
|
+
docToMd: DOC_TO_MD_OPTIONS.filter((o) => o.settable).map((o) => o.key),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
55
|
+
|
|
39
56
|
const emittedWarnings = new Set<string>();
|
|
40
57
|
|
|
41
58
|
function emitWarning(warn: ((message: string) => void) | undefined, message: string): void {
|
|
@@ -44,14 +61,44 @@ function emitWarning(warn: ((message: string) => void) | undefined, message: str
|
|
|
44
61
|
warn(message);
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
/** Structural findings for one parsed settings file: flat legacy use, non-object quiver, unknown blocks, unknown fields. */
|
|
65
|
+
function lintSettings(settings: Record<string, unknown>): string[] {
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
for (const key of Object.keys(settings)) if (LEGACY_FLAT_KEYS.has(key)) lines.push(`"${key}" at top level - move under "quiver"`);
|
|
68
|
+
const root = settings.quiver;
|
|
69
|
+
if (root === undefined) return lines;
|
|
70
|
+
if (!isPlainObject(root)) return [...lines, `"quiver" is not an object - ignored`];
|
|
71
|
+
const fieldLines: string[] = [];
|
|
72
|
+
for (const [block, value] of Object.entries(root)) {
|
|
73
|
+
if (!Object.hasOwn(QUIVER_CONFIG_KEYS, block)) {
|
|
74
|
+
lines.push(`"quiver.${block}" - unknown block; accepted: ${Object.keys(QUIVER_CONFIG_KEYS).join(", ")}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!isPlainObject(value)) continue;
|
|
78
|
+
const fields = QUIVER_CONFIG_KEYS[block];
|
|
79
|
+
for (const field of Object.keys(value)) if (!fields.includes(field)) fieldLines.push(`"quiver.${block}.${field}" - unknown; accepted: ${fields.join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
return [...lines, ...fieldLines];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatLint(findings: Array<[path: string, lines: string[]]>): string {
|
|
85
|
+
const tail = "unknown or misplaced keys - unknown ones fall back to defaults";
|
|
86
|
+
if (findings.length === 1) {
|
|
87
|
+
const [path, lines] = findings[0];
|
|
88
|
+
return [`pi-quiver settings (${path}): ${tail}`, ...lines.map((l) => ` ${l}`)].join("\n");
|
|
89
|
+
}
|
|
90
|
+
return [`pi-quiver settings: ${tail}`, ...findings.flatMap(([path, lines]) => [` ${path}`, ...lines.map((l) => ` ${l}`)])].join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
47
93
|
/**
|
|
48
94
|
* Resolve a single extension config key across the settings layers.
|
|
49
95
|
* `quiver.<key>` wins over flat `<key>` within a layer by presence (even
|
|
50
96
|
* when malformed); flat fallback exists only for LEGACY_FLAT_KEYS. `coerce`
|
|
51
97
|
* validates the layer's candidate into a partial patch (or `undefined` to
|
|
52
98
|
* skip); patches merge over `defaults` in layer order (project wins).
|
|
53
|
-
* `warn` receives one sentence per malformed
|
|
54
|
-
*
|
|
99
|
+
* `warn` receives one sentence per malformed key plus one condensed
|
|
100
|
+
* structural lint (flat legacy use, unknown blocks, unknown fields) - each
|
|
101
|
+
* distinct message once per process.
|
|
55
102
|
*/
|
|
56
103
|
export function resolveConfig<T extends object>(
|
|
57
104
|
cwd: string,
|
|
@@ -61,32 +108,21 @@ export function resolveConfig<T extends object>(
|
|
|
61
108
|
warn?: (message: string) => void,
|
|
62
109
|
): T {
|
|
63
110
|
const cfg: T = { ...defaults };
|
|
64
|
-
|
|
65
|
-
let flatSeen = false;
|
|
111
|
+
const findings: Array<[string, string[]]> = [];
|
|
66
112
|
for (const path of settingsPaths(cwd)) {
|
|
67
113
|
const settings = readSettings(path);
|
|
68
114
|
if (!settings) continue;
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
root = undefined;
|
|
73
|
-
}
|
|
74
|
-
const nested = root as Record<string, unknown> | undefined;
|
|
115
|
+
const lines = lintSettings(settings);
|
|
116
|
+
if (lines.length > 0) findings.push([path, lines]);
|
|
117
|
+
const nested = isPlainObject(settings.quiver) ? settings.quiver : undefined;
|
|
75
118
|
const hasNested = nested !== undefined && Object.hasOwn(nested, key);
|
|
76
119
|
const hasFlat = Object.hasOwn(settings, key);
|
|
77
|
-
nestedSeen ||= hasNested;
|
|
78
|
-
flatSeen ||= hasFlat;
|
|
79
120
|
if (!hasNested && !(hasFlat && LEGACY_FLAT_KEYS.has(key))) continue;
|
|
80
121
|
const candidate = hasNested ? nested![key] : settings[key];
|
|
81
122
|
const patch = coerce(candidate);
|
|
82
123
|
if (patch) Object.assign(cfg, patch);
|
|
83
124
|
else emitWarning(warn, `pi-quiver: "${key}" in ${path} has an unrecognized value; ignored.`);
|
|
84
125
|
}
|
|
85
|
-
if (
|
|
86
|
-
emitWarning(
|
|
87
|
-
warn,
|
|
88
|
-
`pi-quiver: "${key}" is set both flat and under "quiver" (nested wins within a layer; across layers the project layer wins regardless of shape) - move the flat entry under "quiver".`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
126
|
+
if (findings.length > 0) emitWarning(warn, formatLint(findings));
|
|
91
127
|
return cfg;
|
|
92
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-quiver",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.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",
|