@molecule/api-ai-tools 1.9.11 → 1.9.12

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/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-18T07:24:21.171Z
6
+ Generated: 2026-10-18T07:36:48.788Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -404,6 +404,28 @@ interface TraceRequest {
404
404
  * fixture), in which case only recording order is available.
405
405
  */
406
406
  startedAt?: number
407
+ /**
408
+ * Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
409
+ *
410
+ * A 401 cannot be read without this: the API's own body said
411
+ * `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
412
+ * when the header's absence proved the CLIENT never attached the token
413
+ * (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
414
+ * refused a token it was sent.
415
+ */
416
+ sentAuthorization?: boolean
417
+ /**
418
+ * The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
419
+ * no header or its first word does not parse as a scheme (a value pasted without one is a
420
+ * credential, and is never copied here).
421
+ */
422
+ authorization?: string
423
+ /**
424
+ * Whether the request carried a `Cookie` header — never the cookies themselves. An app that
425
+ * signs in with a session cookie has no `Authorization` header by design, so this tells a
426
+ * credential-less request from one whose credential is a cookie.
427
+ */
428
+ sentCookie?: boolean
407
429
  }
408
430
  ```
409
431
 
@@ -67,6 +67,28 @@ export interface TraceRequest {
67
67
  * fixture), in which case only recording order is available.
68
68
  */
69
69
  startedAt?: number;
70
+ /**
71
+ * Whether the request carried an `Authorization` header at all. Its VALUE is never recorded.
72
+ *
73
+ * A 401 cannot be read without this: the API's own body said
74
+ * `{"errors":{"token":["is missing"]}}` while the note told the run to read the API handler,
75
+ * when the header's absence proved the CLIENT never attached the token
76
+ * (xr-molecule-flash-m148 +29:18). A header present moves the answer to the server: the API
77
+ * refused a token it was sent.
78
+ */
79
+ sentAuthorization?: boolean;
80
+ /**
81
+ * The scheme WORD of an `Authorization` header (`Bearer`, `Token`), or undefined when there is
82
+ * no header or its first word does not parse as a scheme (a value pasted without one is a
83
+ * credential, and is never copied here).
84
+ */
85
+ authorization?: string;
86
+ /**
87
+ * Whether the request carried a `Cookie` header — never the cookies themselves. An app that
88
+ * signs in with a session cookie has no `Authorization` header by design, so this tells a
89
+ * credential-less request from one whose credential is a cookie.
90
+ */
91
+ sentCookie?: boolean;
70
92
  }
71
93
  /**
72
94
  * The test's LAST user action before the failing assertion, and the requests it caused.
@@ -313,6 +335,15 @@ export declare function stalledDocumentSubresource(request: TraceRequest): boole
313
335
  * request that never reached the API. The prefix is READ (the caller passes what the app's client
314
336
  * config names; the traces add what their own requests show) and never assumed to be `/api`.
315
337
  *
338
+ * A 401/403 on the app's own origin is answered from the request's own credential record instead
339
+ * ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
340
+ * token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
341
+ * header the API then refused moves the answer to the server. The handler sentence is kept for
342
+ * the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
343
+ * trace that proves the header was never sent cannot be explained by the handler.
344
+ * xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
345
+ * `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
346
+ *
316
347
  * @param items - The traces {@link parseTraceArtifacts} read.
317
348
  * @param target - Where the run went — the deployed host, as the note names it.
318
349
  * @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
@@ -1 +1 @@
1
- {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB;;;;OAIG;IACH,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAA;IACX,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB;AAED,gGAAgG;AAChG,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAA;IACZ,6FAA6F;IAC7F,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B;;;;OAIG;IACH,OAAO,EAAE,WAAW,EAAE,CAAA;CACvB;AAED,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,IAAI,CAAA;AAEpC;;;GAGG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,sFAAsF;AACtF,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAErC,iDAAiD;AACjD,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC,wFAAwF;AACxF,eAAO,MAAM,qBAAqB,MAAM,CAAA;AAExC,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAA;AAK5C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,QAAO,MA8fvC,CAAA;AAoDD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAkCnE;AA2KD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAKnE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAG5D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAGhE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAMzE;AA4VD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,MAAM,EAAE,MAAM,EACd,eAAe,GAAE,SAAS,MAAM,EAAO,EACvC,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,MAAM,GAAG,IAAI,CAkPf;AAaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,8BAA8B,CAAA;AAwG/D,oGAAoG;AACpG,MAAM,WAAW,qBAAqB;IACpC,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,4EAA4E;IAC5E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC5C,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA2BjC"}
@@ -243,12 +243,34 @@ for (const one of found.slice(0, MAX_TRACES)) {
243
243
  const response = snapshot.response || {}
244
244
  const type = String(snapshot._resourceType || '')
245
245
  if (!origin) origin = type === 'document' ? originOf(String(request.url)) : ''
246
+ // What the request carried for CREDENTIALS — never the secret itself. A 401 on the app's own
247
+ // API cannot be read without this: m148's favorite POST answered 401 with no Authorization
248
+ // header at all, so the client never attached the token, and the note sent the run to the
249
+ // API handler instead (xr-molecule-flash-m148 +29:18). Presence is recorded as a boolean and
250
+ // only the scheme WORD (\`Bearer\`, \`Token\`) is kept; the value is a live credential and never
251
+ // leaves the trace. A first word that does not parse as a scheme (\`Authorization: <raw jwt>\`)
252
+ // is recorded as presence only, so a pasted token can never ride along as a "scheme".
253
+ const headerList = Array.isArray(request.headers) ? request.headers : []
254
+ const authHeader = headerList.find(function (h) {
255
+ return h && typeof h.name === 'string' && /^authorization$/i.test(h.name)
256
+ })
257
+ const authValue =
258
+ authHeader && typeof authHeader.value === 'string' ? String(authHeader.value).trim() : ''
259
+ const authSpace = authValue.search(/\\s/)
260
+ const authWord = authSpace > 0 ? authValue.slice(0, authSpace) : ''
261
+ const authScheme = /^[A-Za-z][A-Za-z0-9._~+/-]{0,23}$/.test(authWord) ? authWord : undefined
262
+ const sentCookie = headerList.some(function (h) {
263
+ return h && typeof h.name === 'string' && /^cookie$/i.test(h.name)
264
+ })
246
265
  requests.push({
247
266
  method: String(request.method).toUpperCase(),
248
267
  path: String(request.url).slice(0, MAX_URL),
249
268
  status: Number(response.status) || 0,
250
269
  failure: typeof response._failureText === 'string' ? response._failureText : undefined,
251
270
  type: type,
271
+ sentAuthorization: authHeader !== undefined,
272
+ authorization: authScheme,
273
+ sentCookie: sentCookie,
252
274
  // The HAR entry carries the monotonic clock the trace's action entries use, so a request
253
275
  // can be attributed to the action that caused it (see \`lastAction\` below).
254
276
  startedAt: typeof snapshot._monotonicTime === 'number' ? snapshot._monotonicTime : undefined,
@@ -430,6 +452,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
430
452
  ...(r.sent ? { sent: r.sent } : {}),
431
453
  ...(r.got ? { got: r.got } : {}),
432
454
  type: r.type || undefined,
455
+ ...(r.sentAuthorization ? { sentAuthorization: true } : {}),
456
+ ...(r.authorization ? { authorization: r.authorization } : {}),
457
+ ...(r.sentCookie ? { sentCookie: true } : {}),
433
458
  }
434
459
  }
435
460
  // The main frame's URL, timed: what the page showed when the action ran, and where it ended up.
@@ -590,6 +615,12 @@ function parseTraceRequest(value) {
590
615
  ...(typeof r.got === 'string' && r.got ? { got: r.got } : {}),
591
616
  ...(typeof r.type === 'string' && r.type ? { type: r.type } : {}),
592
617
  ...(Number.isFinite(Number(r.startedAt)) ? { startedAt: Number(r.startedAt) } : {}),
618
+ // The scheme word only: a raw header value is a live credential and is never parsed in.
619
+ ...(r.sentAuthorization === true ? { sentAuthorization: true } : {}),
620
+ ...(typeof r.authorization === 'string' && r.authorization
621
+ ? { authorization: r.authorization }
622
+ : {}),
623
+ ...(r.sentCookie === true ? { sentCookie: true } : {}),
593
624
  };
594
625
  }
595
626
  /**
@@ -1031,6 +1062,94 @@ function sameOriginPath(request) {
1031
1062
  return null;
1032
1063
  return raw.replace(/\/+$/, '');
1033
1064
  }
1065
+ /**
1066
+ * The sentence a 401/403 on the app's own API gets: which half of the wire refused the request.
1067
+ *
1068
+ * A fixed "read the handler this route reaches" line is the wrong half for a credential refusal.
1069
+ * The request's own `Authorization` header — recorded by the trace reader as its scheme WORD, never
1070
+ * its value — decides: absent means the CLIENT never attached the token (a session store that the
1071
+ * HTTP client does not read, a bare `fetch`, an auth bond the app replaced), while present means
1072
+ * the SERVER refused a token it was sent (scheme or issuer). A 403 is a permission answer, not a
1073
+ * credential one, and says so.
1074
+ *
1075
+ * xr-molecule-flash-m148 (+29:18): `POST /api/articles/…/favorite → 401 {"errors":{"token":["is
1076
+ * missing"]}}` was answered with the handler sentence for six local walks, while the trace proved
1077
+ * the request carried no `Authorization` header at all and the app's hand-rolled session store
1078
+ * never handed its token to the shared client.
1079
+ *
1080
+ * The trace cannot name the file that made the call: a real HAR entry carries the request, response,
1081
+ * `_resourceType`, `_monotonicTime` and a frame ref, but no initiator stack. So the sentence names
1082
+ * the shared client and the auth bond instead of guessing a file.
1083
+ *
1084
+ * @param request - The refused request, a 401 or 403 on the app's own origin.
1085
+ * @returns The sentence the note appends.
1086
+ */
1087
+ function authRefusalSentence(request) {
1088
+ const label = requestLabel(request);
1089
+ // The header's scheme, when it parsed; presence alone is still the server's half of the wire.
1090
+ const carried = request.authorization !== undefined
1091
+ ? `with \`Authorization: ${request.authorization} …\``
1092
+ : 'with an `Authorization` header';
1093
+ if (request.status === 403) {
1094
+ return request.sentAuthorization === true
1095
+ ? `The browser sent \`${label}\` ${carried} and the API answered 403 — the token was ` +
1096
+ 'accepted but this user may not do it: read the route’s role/permission check in your ' +
1097
+ 'own source, and the account the session belongs to.'
1098
+ : `The browser sent \`${label}\` and the API answered 403 — the request carried no ` +
1099
+ '`Authorization` header, so either the client did not attach the token (check that the ' +
1100
+ 'request goes through the app’s HTTP client that the auth bond configures, and that the ' +
1101
+ 'token is stored where the client reads it) or the route refuses this user: read the ' +
1102
+ 'route’s role/permission check in your own source.';
1103
+ }
1104
+ if (request.sentAuthorization === true) {
1105
+ return (`The browser sent \`${label}\` ${carried} and the API refused it: the token was sent and ` +
1106
+ 'rejected — the scheme (the client’s `authScheme`, e.g. `Token` vs `Bearer`) or the ' +
1107
+ 'token’s issuer differs from what the server verifies; read the verify middleware this ' +
1108
+ 'route reaches.');
1109
+ }
1110
+ return (`The browser sent \`${label}\` with NO \`Authorization\` header — the client did not attach ` +
1111
+ 'the token: check that the request goes through the app’s HTTP client that the auth bond ' +
1112
+ 'configures (not a bare `fetch`), and that the token is stored where the client reads it. ' +
1113
+ 'The shared client carries it through `setAuthToken` — the auth bond ' +
1114
+ '(`createDefaultAuthClientWithHttpSync` in `@molecule/app-bonds-default-react`) mirrors the ' +
1115
+ 'auth client’s token there, and a session store of your own has to call it too. ' +
1116
+ (request.sentCookie
1117
+ ? 'The request did carry a `Cookie` header, so if this app signs in with a session cookie ' +
1118
+ 'that is the credential the server did not accept. '
1119
+ : '') +
1120
+ 'The API handler is not the problem.');
1121
+ }
1122
+ /**
1123
+ * The note's credential-refusal advice, one sentence per distinct shape — at most two.
1124
+ *
1125
+ * The shapes are `(status, Authorization scheme, cookie present)`: m148's ten walks repeated one
1126
+ * shape (`401`, no header) on every failing test, and repeating its sentence per row would bury
1127
+ * the fact rather than state it.
1128
+ *
1129
+ * @param items - The traces the note is about.
1130
+ * @returns The sentences to append (each led by a space), or '' when no same-origin 401/403 was
1131
+ * traced.
1132
+ */
1133
+ function authRefusalAdvice(items) {
1134
+ const sentences = [];
1135
+ const seen = new Set();
1136
+ for (const item of items) {
1137
+ for (const request of item.requests) {
1138
+ if (request.status !== 401 && request.status !== 403)
1139
+ continue;
1140
+ if (sameOriginPath(request) === null)
1141
+ continue;
1142
+ const key = `${request.status} ${request.sentAuthorization ? (request.authorization ?? 'header') : 'none'} ${request.sentCookie ? 'cookie' : ''}`;
1143
+ if (seen.has(key))
1144
+ continue;
1145
+ seen.add(key);
1146
+ sentences.push(authRefusalSentence(request));
1147
+ if (sentences.length === 2)
1148
+ return ` ${sentences.join(' ')}`;
1149
+ }
1150
+ }
1151
+ return sentences.length === 0 ? '' : ` ${sentences.join(' ')}`;
1152
+ }
1034
1153
  /**
1035
1154
  * The API path prefixes the traced requests themselves show.
1036
1155
  *
@@ -1162,6 +1281,15 @@ function missingApiPrefixSentence(request) {
1162
1281
  * request that never reached the API. The prefix is READ (the caller passes what the app's client
1163
1282
  * config names; the traces add what their own requests show) and never assumed to be `/api`.
1164
1283
  *
1284
+ * A 401/403 on the app's own origin is answered from the request's own credential record instead
1285
+ * ({@link authRefusalSentence}): no `Authorization` header means the CLIENT never attached the
1286
+ * token (the shared HTTP client's `setAuthToken` is what the auth bond mirrors it into), while a
1287
+ * header the API then refused moves the answer to the server. The handler sentence is kept for
1288
+ * the rest — 404/422/500 — and dropped when every failure was a credential refusal, because a
1289
+ * trace that proves the header was never sent cannot be explained by the handler.
1290
+ * xr-molecule-flash-m148 (+29:18) hit this exactly: six local walks read
1291
+ * `POST …/favorite → 401 {"errors":{"token":["is missing"]}}` as a handler bug.
1292
+ *
1165
1293
  * @param items - The traces {@link parseTraceArtifacts} read.
1166
1294
  * @param target - Where the run went — the deployed host, as the note names it.
1167
1295
  * @param earlierFailures - The failing `file:line`s earlier walks of this suite left, so a repeat
@@ -1212,6 +1340,11 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1212
1340
  const prefixAdvice = missingPrefix.length > 0
1213
1341
  ? ` ${missingPrefix.slice(0, 2).map(missingApiPrefixSentence).join(' ')}`
1214
1342
  : '';
1343
+ // A credential refusal (401/403 on the app's own API) is not the handler's answer: the trace's
1344
+ // own `Authorization` record says whether the CLIENT attached the token at all (m148 +29:18).
1345
+ const authRefusals = failedRequests.filter((request) => sameOriginPath(request) !== null && (request.status === 401 || request.status === 403));
1346
+ const everyFailureAuth = failedRequests.length > 0 && authRefusals.length === failedRequests.length;
1347
+ const authAdvice = authRefusalAdvice(traced);
1215
1348
  // The document subresources that never finished anywhere in this read — the assets a timed-out
1216
1349
  // navigation was waiting on, when there is no failed request to explain the timeout.
1217
1350
  const stalledAssets = [
@@ -1352,9 +1485,14 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
1352
1485
  (anyFailed
1353
1486
  ? everyFailureLacksPrefix
1354
1487
  ? ''
1355
- : 'A click that changed nothing is usually one of these requests failing, not the selector: ' +
1356
- 'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
1357
- 'user action silently — log it.'
1488
+ : everyFailureAuth
1489
+ ? // Every failure was a credential refusal: the handler is not the problem, and the
1490
+ // auth advice already names what is (m148 was sent to the handler six walks running).
1491
+ authAdvice
1492
+ : 'A click that changed nothing is usually one of these requests failing, not the selector: ' +
1493
+ 'read the handler this route reaches in your own source, and stop `catch`-ing the request of a ' +
1494
+ 'user action silently — log it.' +
1495
+ authAdvice
1358
1496
  : stalledAssets.length > 0
1359
1497
  ? 'No listed request failed — none got a 4xx/5xx or a network error — but the document did ' +
1360
1498
  'not finish loading because its own subresource never answered. Do not read this as a ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.9.11",
3
+ "version": "1.9.12",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",