@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 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.8",
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.8",
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"
@@ -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 hint = extractRetryHint(response, await response.clone().text());
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);
@@ -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
- // Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness
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)}'`,