@oh-my-pi/pi-utils 16.2.8 → 16.2.11
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 +10 -0
- package/dist/types/fetch-retry.d.ts +6 -0
- package/package.json +2 -2
- package/src/fetch-retry.ts +11 -1
- package/src/frontmatter.ts +36 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.2.9] - 2026-06-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Improved resilience in `fetchWithRetry()` by adding a response-body retry gate to handle deterministic provider failures that return retryable HTTP statuses.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed YAML frontmatter parsing for skill descriptions containing unquoted colons (`: `), ensuring typed fields are correctly preserved without triggering unnecessary warnings.
|
|
14
|
+
|
|
5
15
|
## [16.2.7] - 2026-06-30
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -43,6 +43,12 @@ export interface FetchWithRetryOptions extends RequestInit {
|
|
|
43
43
|
* mock during tests.
|
|
44
44
|
*/
|
|
45
45
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
46
|
+
/**
|
|
47
|
+
* Optional retry gate for HTTP responses whose status is retryable. Receives a
|
|
48
|
+
* cloned body string so callers can fail fast on deterministic provider
|
|
49
|
+
* failures that happen to use a 5xx status.
|
|
50
|
+
*/
|
|
51
|
+
shouldRetryResponse?: (response: Response, bodyText: string, attempt: number) => boolean | Promise<boolean>;
|
|
46
52
|
/**
|
|
47
53
|
* Bun extension forwarded verbatim to the underlying `fetch` call. `false`
|
|
48
54
|
* disables Bun's native ~300s pre-response timeout (callers that own a
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "16.2.
|
|
4
|
+
"version": "16.2.11",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "16.2.
|
|
34
|
+
"@oh-my-pi/pi-natives": "16.2.11",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
37
|
"winston-daily-rotate-file": "^5.0.0"
|
package/src/fetch-retry.ts
CHANGED
|
@@ -129,6 +129,12 @@ export interface FetchWithRetryOptions extends RequestInit {
|
|
|
129
129
|
* mock during tests.
|
|
130
130
|
*/
|
|
131
131
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
132
|
+
/**
|
|
133
|
+
* Optional retry gate for HTTP responses whose status is retryable. Receives a
|
|
134
|
+
* cloned body string so callers can fail fast on deterministic provider
|
|
135
|
+
* failures that happen to use a 5xx status.
|
|
136
|
+
*/
|
|
137
|
+
shouldRetryResponse?: (response: Response, bodyText: string, attempt: number) => boolean | Promise<boolean>;
|
|
132
138
|
/**
|
|
133
139
|
* Bun extension forwarded verbatim to the underlying `fetch` call. `false`
|
|
134
140
|
* disables Bun's native ~300s pre-response timeout (callers that own a
|
|
@@ -160,6 +166,7 @@ export async function fetchWithRetry(
|
|
|
160
166
|
maxDelayMs = DEFAULT_MAX_DELAY_MS,
|
|
161
167
|
defaultDelayMs,
|
|
162
168
|
prepareInit,
|
|
169
|
+
shouldRetryResponse,
|
|
163
170
|
fetch: fetchImpl = fetch,
|
|
164
171
|
timeout = false,
|
|
165
172
|
...baseInit
|
|
@@ -196,7 +203,10 @@ export async function fetchWithRetry(
|
|
|
196
203
|
if (!isRetryableStatus(response.status)) return response;
|
|
197
204
|
if (attempt + 1 >= maxAttempts) return response;
|
|
198
205
|
|
|
199
|
-
const
|
|
206
|
+
const retryBody = await response.clone().text();
|
|
207
|
+
if (shouldRetryResponse && !(await shouldRetryResponse(response, retryBody, attempt))) return response;
|
|
208
|
+
|
|
209
|
+
const hint = extractRetryHint(response, retryBody);
|
|
200
210
|
if (hint !== undefined && hint > maxDelayMs) return response;
|
|
201
211
|
|
|
202
212
|
const delayMs = Math.min(hint ?? resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), maxDelayMs);
|
package/src/frontmatter.ts
CHANGED
|
@@ -37,6 +37,31 @@ function normalizeKeys<T>(obj: T): T {
|
|
|
37
37
|
return (changed ? result : obj) as T;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const PLAIN_SCALAR_KEY_VALUE = /^(\s*[A-Za-z_][\w-]*:\s+)(\S.*?)(\s*)$/;
|
|
41
|
+
const FLOW_OR_EXPLICIT_VALUE_START = new Set(['"', "'", "[", "{", "|", ">", "!", "&", "*", "#"]);
|
|
42
|
+
|
|
43
|
+
function quoteAmbiguousPlainScalars(metadata: string): string | undefined {
|
|
44
|
+
let changed = false;
|
|
45
|
+
const lines = metadata.split("\n").map(line => {
|
|
46
|
+
const match = line.match(PLAIN_SCALAR_KEY_VALUE);
|
|
47
|
+
if (!match) return line;
|
|
48
|
+
const [, prefix, rawValue, suffix] = match;
|
|
49
|
+
const value = rawValue.trimEnd();
|
|
50
|
+
if (!value.includes(": ")) return line;
|
|
51
|
+
if (FLOW_OR_EXPLICIT_VALUE_START.has(value[0])) return line;
|
|
52
|
+
changed = true;
|
|
53
|
+
return `${prefix}${JSON.stringify(value)}${suffix}`;
|
|
54
|
+
});
|
|
55
|
+
return changed ? lines.join("\n") : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseYamlRecord(metadata: string): Record<string, unknown> | null {
|
|
59
|
+
const loaded = YAML.parse(metadata.replaceAll("\t", " "));
|
|
60
|
+
if (loaded === null || loaded === undefined) return null;
|
|
61
|
+
if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
|
|
62
|
+
return loaded as Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
|
|
40
65
|
export class FrontmatterError extends Error {
|
|
41
66
|
constructor(
|
|
42
67
|
error: Error,
|
|
@@ -100,10 +125,19 @@ export function parseFrontmatter(
|
|
|
100
125
|
const body = normalized.slice(endIndex + 4).trim();
|
|
101
126
|
|
|
102
127
|
try {
|
|
103
|
-
|
|
104
|
-
const loaded = YAML.parse(metadata.replaceAll("\t", " ")) as Record<string, unknown> | null;
|
|
128
|
+
const loaded = parseYamlRecord(metadata);
|
|
105
129
|
return { frontmatter: normalizeKeys({ ...frontmatter, ...loaded }), body };
|
|
106
130
|
} catch (error) {
|
|
131
|
+
const quotedMetadata = quoteAmbiguousPlainScalars(metadata);
|
|
132
|
+
if (quotedMetadata) {
|
|
133
|
+
try {
|
|
134
|
+
const loaded = parseYamlRecord(quotedMetadata);
|
|
135
|
+
return { frontmatter: normalizeKeys({ ...frontmatter, ...loaded }), body };
|
|
136
|
+
} catch {
|
|
137
|
+
// Fall through to the existing warning + simple key/value fallback.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
107
141
|
const err = new FrontmatterError(
|
|
108
142
|
error instanceof Error ? error : new Error(`YAML: ${error}`),
|
|
109
143
|
loc ?? `Inline '${truncate(content, 64)}'`,
|