@fgv/ts-extras 5.1.0-45 → 5.1.0-47

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.
Files changed (141) hide show
  1. package/README.md +90 -0
  2. package/dist/index.browser.js +3 -1
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.js +2 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/packlets/ai-assist/index.js +1 -1
  7. package/dist/packlets/ai-assist/index.js.map +1 -1
  8. package/dist/packlets/ai-assist/jsonResponse.js +270 -7
  9. package/dist/packlets/ai-assist/jsonResponse.js.map +1 -1
  10. package/dist/packlets/ai-assist/model.js +14 -0
  11. package/dist/packlets/ai-assist/model.js.map +1 -1
  12. package/dist/packlets/ai-assist/registry.js +57 -14
  13. package/dist/packlets/ai-assist/registry.js.map +1 -1
  14. package/dist/packlets/safer-fetch/addressClassification.js +438 -0
  15. package/dist/packlets/safer-fetch/addressClassification.js.map +1 -0
  16. package/dist/packlets/safer-fetch/addressPolicy.js +119 -0
  17. package/dist/packlets/safer-fetch/addressPolicy.js.map +1 -0
  18. package/dist/packlets/safer-fetch/contentType.js +120 -0
  19. package/dist/packlets/safer-fetch/contentType.js.map +1 -0
  20. package/dist/packlets/safer-fetch/deadline.js +254 -0
  21. package/dist/packlets/safer-fetch/deadline.js.map +1 -0
  22. package/dist/packlets/safer-fetch/defaults.js +131 -0
  23. package/dist/packlets/safer-fetch/defaults.js.map +1 -0
  24. package/dist/packlets/safer-fetch/failureReason.js +21 -0
  25. package/dist/packlets/safer-fetch/failureReason.js.map +1 -0
  26. package/dist/packlets/safer-fetch/guards.js +148 -0
  27. package/dist/packlets/safer-fetch/guards.js.map +1 -0
  28. package/dist/packlets/safer-fetch/index.browser.js +58 -0
  29. package/dist/packlets/safer-fetch/index.browser.js.map +1 -0
  30. package/dist/packlets/safer-fetch/index.js +52 -0
  31. package/dist/packlets/safer-fetch/index.js.map +1 -0
  32. package/dist/packlets/safer-fetch/model.js +21 -0
  33. package/dist/packlets/safer-fetch/model.js.map +1 -0
  34. package/dist/packlets/safer-fetch/nodeAddressGuard.js +191 -0
  35. package/dist/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
  36. package/dist/packlets/safer-fetch/redirect.js +93 -0
  37. package/dist/packlets/safer-fetch/redirect.js.map +1 -0
  38. package/dist/packlets/safer-fetch/retry.js +188 -0
  39. package/dist/packlets/safer-fetch/retry.js.map +1 -0
  40. package/dist/packlets/safer-fetch/saferFetch.js +755 -0
  41. package/dist/packlets/safer-fetch/saferFetch.js.map +1 -0
  42. package/dist/packlets/safer-fetch/transport.js +50 -0
  43. package/dist/packlets/safer-fetch/transport.js.map +1 -0
  44. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +65 -7
  45. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  46. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js +16 -1
  47. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -1
  48. package/dist/ts-extras.d.ts +1590 -13
  49. package/lib/index.browser.d.ts +2 -1
  50. package/lib/index.browser.d.ts.map +1 -1
  51. package/lib/index.browser.js +4 -1
  52. package/lib/index.browser.js.map +1 -1
  53. package/lib/index.d.ts +2 -1
  54. package/lib/index.d.ts.map +1 -1
  55. package/lib/index.js +3 -1
  56. package/lib/index.js.map +1 -1
  57. package/lib/packlets/ai-assist/index.d.ts +1 -1
  58. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  59. package/lib/packlets/ai-assist/index.js +3 -2
  60. package/lib/packlets/ai-assist/index.js.map +1 -1
  61. package/lib/packlets/ai-assist/jsonResponse.d.ts +96 -0
  62. package/lib/packlets/ai-assist/jsonResponse.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/jsonResponse.js +271 -7
  64. package/lib/packlets/ai-assist/jsonResponse.js.map +1 -1
  65. package/lib/packlets/ai-assist/model.d.ts +43 -0
  66. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  67. package/lib/packlets/ai-assist/model.js +14 -0
  68. package/lib/packlets/ai-assist/model.js.map +1 -1
  69. package/lib/packlets/ai-assist/registry.d.ts +26 -6
  70. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  71. package/lib/packlets/ai-assist/registry.js +57 -14
  72. package/lib/packlets/ai-assist/registry.js.map +1 -1
  73. package/lib/packlets/safer-fetch/addressClassification.d.ts +144 -0
  74. package/lib/packlets/safer-fetch/addressClassification.d.ts.map +1 -0
  75. package/lib/packlets/safer-fetch/addressClassification.js +441 -0
  76. package/lib/packlets/safer-fetch/addressClassification.js.map +1 -0
  77. package/lib/packlets/safer-fetch/addressPolicy.d.ts +113 -0
  78. package/lib/packlets/safer-fetch/addressPolicy.d.ts.map +1 -0
  79. package/lib/packlets/safer-fetch/addressPolicy.js +123 -0
  80. package/lib/packlets/safer-fetch/addressPolicy.js.map +1 -0
  81. package/lib/packlets/safer-fetch/contentType.d.ts +51 -0
  82. package/lib/packlets/safer-fetch/contentType.d.ts.map +1 -0
  83. package/lib/packlets/safer-fetch/contentType.js +127 -0
  84. package/lib/packlets/safer-fetch/contentType.js.map +1 -0
  85. package/lib/packlets/safer-fetch/deadline.d.ts +138 -0
  86. package/lib/packlets/safer-fetch/deadline.d.ts.map +1 -0
  87. package/lib/packlets/safer-fetch/deadline.js +258 -0
  88. package/lib/packlets/safer-fetch/deadline.js.map +1 -0
  89. package/lib/packlets/safer-fetch/defaults.d.ts +108 -0
  90. package/lib/packlets/safer-fetch/defaults.d.ts.map +1 -0
  91. package/lib/packlets/safer-fetch/defaults.js +134 -0
  92. package/lib/packlets/safer-fetch/defaults.js.map +1 -0
  93. package/lib/packlets/safer-fetch/failureReason.d.ts +148 -0
  94. package/lib/packlets/safer-fetch/failureReason.d.ts.map +1 -0
  95. package/lib/packlets/safer-fetch/failureReason.js +22 -0
  96. package/lib/packlets/safer-fetch/failureReason.js.map +1 -0
  97. package/lib/packlets/safer-fetch/guards.d.ts +62 -0
  98. package/lib/packlets/safer-fetch/guards.d.ts.map +1 -0
  99. package/lib/packlets/safer-fetch/guards.js +153 -0
  100. package/lib/packlets/safer-fetch/guards.js.map +1 -0
  101. package/lib/packlets/safer-fetch/index.browser.d.ts +33 -0
  102. package/lib/packlets/safer-fetch/index.browser.d.ts.map +1 -0
  103. package/lib/packlets/safer-fetch/index.browser.js +82 -0
  104. package/lib/packlets/safer-fetch/index.browser.js.map +1 -0
  105. package/lib/packlets/safer-fetch/index.d.ts +27 -0
  106. package/lib/packlets/safer-fetch/index.d.ts.map +1 -0
  107. package/lib/packlets/safer-fetch/index.js +78 -0
  108. package/lib/packlets/safer-fetch/index.js.map +1 -0
  109. package/lib/packlets/safer-fetch/model.d.ts +373 -0
  110. package/lib/packlets/safer-fetch/model.d.ts.map +1 -0
  111. package/lib/packlets/safer-fetch/model.js +22 -0
  112. package/lib/packlets/safer-fetch/model.js.map +1 -0
  113. package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts +129 -0
  114. package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts.map +1 -0
  115. package/lib/packlets/safer-fetch/nodeAddressGuard.js +196 -0
  116. package/lib/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
  117. package/lib/packlets/safer-fetch/redirect.d.ts +62 -0
  118. package/lib/packlets/safer-fetch/redirect.d.ts.map +1 -0
  119. package/lib/packlets/safer-fetch/redirect.js +98 -0
  120. package/lib/packlets/safer-fetch/redirect.js.map +1 -0
  121. package/lib/packlets/safer-fetch/retry.d.ts +162 -0
  122. package/lib/packlets/safer-fetch/retry.d.ts.map +1 -0
  123. package/lib/packlets/safer-fetch/retry.js +197 -0
  124. package/lib/packlets/safer-fetch/retry.js.map +1 -0
  125. package/lib/packlets/safer-fetch/saferFetch.d.ts +108 -0
  126. package/lib/packlets/safer-fetch/saferFetch.d.ts.map +1 -0
  127. package/lib/packlets/safer-fetch/saferFetch.js +760 -0
  128. package/lib/packlets/safer-fetch/saferFetch.js.map +1 -0
  129. package/lib/packlets/safer-fetch/transport.d.ts +18 -0
  130. package/lib/packlets/safer-fetch/transport.d.ts.map +1 -0
  131. package/lib/packlets/safer-fetch/transport.js +53 -0
  132. package/lib/packlets/safer-fetch/transport.js.map +1 -0
  133. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +54 -6
  134. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -1
  135. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +65 -7
  136. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  137. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts +26 -1
  138. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -1
  139. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js +17 -1
  140. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -1
  141. package/package.json +17 -7
package/README.md CHANGED
@@ -86,6 +86,96 @@ const rawContents = file.value.getRawContents(); // Raw string
86
86
 
87
87
  **Note**: This implementation uses Node.js-specific dependencies (AdmZip, Buffer). For browser environments, see the browser-specific implementations in individual projects.
88
88
 
89
+ ### Safer Fetch
90
+
91
+ An HTTP fetch primitive with an explicit threat model, exported from `@fgv/ts-extras/safer-fetch`.
92
+ It is deliberately **not** a thin boundary over an upstream library — `fetch` is a platform global
93
+ and there is nothing to wrap. The opinion is the product: the deadlines, the scheme refusal, the
94
+ streaming size cap, the redirect posture, and the required address guard are the deliverable.
95
+
96
+ ```typescript
97
+ import {
98
+ saferFetchJson,
99
+ blockPrivateNetworks,
100
+ allowContentTypes
101
+ } from '@fgv/ts-extras/safer-fetch';
102
+
103
+ const result = await saferFetchJson('https://api.example.com/thing', {
104
+ // Required, with no default: omitting it is a compile error, so no call site can inherit a
105
+ // guarantee it was never given. Every call site's posture is greppable in one search.
106
+ addressGuard: blockPrivateNetworks(),
107
+ responseHeadersGuard: allowContentTypes(['application/json']).orThrow(),
108
+ timeoutMs: 10_000,
109
+ maxResponseBytes: 1024 * 1024,
110
+ retry: { attempts: 2 } // off unless asked for
111
+ });
112
+ ```
113
+
114
+ `addressGuard` has no default. `allowAnyAddress()` is the named, deliberately uncomfortable
115
+ opt-out, and it is the only honest choice in a browser.
116
+
117
+ #### The guarantee table
118
+
119
+ This table is the artifact that keeps the primitive honest. It is reproduced verbatim in
120
+ `@fgv/ts-web-extras`'s README.
121
+
122
+ | Property | Node (`@fgv/ts-extras/safer-fetch`) | Browser (`@fgv/ts-web-extras`) |
123
+ |---|---|---|
124
+ | Overall + headers deadline | ✅ | ✅ |
125
+ | Streaming size cap, enforced during read | ✅ | ✅ |
126
+ | Content-type gate before body read | ✅ | ✅ |
127
+ | Structured failure taxonomy | ✅ | ✅ |
128
+ | Retry with idempotency + budget rules | ✅ | ✅ |
129
+ | Scheme allowlist on URL₀ | ✅ | ✅ |
130
+ | Host / port allowlist on URL₀ | ✅ (`blockPrivateNetworks({ allowHosts, allowPorts })`) | ✅ (caller-supplied guard) |
131
+ | **Resolved-address (private-IP) guard** | ✅ | ❌ **impossible** — no DNS API |
132
+ | **Per-hop revalidation of redirects** | ✅ | ❌ **impossible** — opaque redirect |
133
+ | **Credential stripping on cross-origin hop** | ✅ | n/a — platform does it |
134
+ | Reject-all-redirects mode | ✅ | ✅ (enforced; surfaces as `'redirect-opaque'`) |
135
+ | DNS-rebinding resistance | ❌ **documented limit** | ❌ |
136
+
137
+ Two rows differ from the design document they came from, and the difference is deliberate:
138
+
139
+ - **Host / port allowlist.** `blockPrivateNetworks` takes `allowHosts` / `allowPorts` /
140
+ `allowInsecureHttp`. A host allowlist is the *recommended* posture, because it shrinks the
141
+ DNS-rebinding exposure below to "an allowlisted host's own resolver is hostile". `https:` is
142
+ required unless `allowInsecureHttp` is set; there is no default port restriction, since a
143
+ default of `{443}` would reject the very common `:8443` public endpoint with a failure that
144
+ reads as an SSRF block.
145
+ - **Reject-all-redirects.** Both runtimes use `redirect: 'manual'` and reject the redirect
146
+ themselves. The *guarantee* is identical; the failure **reason** is not — Node reports
147
+ `'redirect-rejected'` with the status, a browser reports `'redirect-opaque'` because an opaque
148
+ response has no status to report. Callers branching on the reason under `'reject'` must handle
149
+ both.
150
+
151
+ #### Stated limits
152
+
153
+ Each of these is a hole in a guarantee this package makes, and each is documented rather than
154
+ implied.
155
+
156
+ - **DNS rebinding** — the guard validates a resolved address and the transport then re-resolves,
157
+ so hostile DNS can answer the two lookups differently. Closing it needs a pinning transport;
158
+ the seam (`IGuardVerdict.pinnedAddress` + `IFetchTransport`) exists, and
159
+ `platformFetchTransport` **fails** rather than silently ignoring a pin it cannot honor. A
160
+ strict `allowHosts` list is the recommended mitigation.
161
+ - **The browser has no SSRF guard at all** — not a partial guarantee, an absent one. See the
162
+ table.
163
+ - **Time-of-check/time-of-use beyond DNS** — the guard authorizes a *destination*, never the
164
+ *content* that comes back.
165
+ - **The failure taxonomy is an information-disclosure surface** — a `'blocked-by-guard'` detail
166
+ names the URL, the hop and the guard, which is an internal-network scanning oracle if echoed
167
+ to an untrusted caller. Log it; return a coarse code.
168
+ - **No egress accounting** — no per-host rate limiting or quota. A retry policy is not a rate
169
+ limiter.
170
+ - **Loopback is blocked by default**, including for this repo's own local-development path. A
171
+ caller who wants it says so, and every deviation is independently greppable:
172
+ `blockPrivateNetworks({ allowLoopback: true, allowInsecureHttp: true, allowHosts: ['localhost'], allowPorts: [11434] })`.
173
+ - **HTTP semantics not implemented** — no cookie jar, no cache, no conditional requests, no
174
+ proxy configuration beyond what the platform picks up from the environment.
175
+
176
+ If your deployment has an egress proxy or firewall, that control is strictly stronger than this
177
+ one; this is defense in depth for deployments that do not.
178
+
89
179
  ### Converters
90
180
 
91
181
  Type-safe data conversion utilities for transforming between different data formats while maintaining type safety.
@@ -31,11 +31,13 @@ import * as Hash from './packlets/hash/index.browser';
31
31
  import * as Mustache from './packlets/mustache';
32
32
  // eslint-disable-next-line @rushstack/packlets/mechanics
33
33
  import * as RecordJar from './packlets/record-jar/index.browser';
34
+ // eslint-disable-next-line @rushstack/packlets/mechanics
35
+ import * as SaferFetch from './packlets/safer-fetch/index.browser';
34
36
  import * as Yaml from './packlets/yaml';
35
37
  import * as ZipFileTree from './packlets/zip-file-tree';
36
38
  import { Converters } from './packlets/conversion';
37
39
  // Browser-safe exports - Node.js crypto-based providers excluded
38
40
  // Use BrowserCryptoProvider from @fgv/ts-web-extras for browser crypto
39
- export { AiAssist, Converters, CryptoUtils, CryptoUtils as Crypto, Csv, Experimental, Hash, Mustache, RecordJar, Yaml, ZipFileTree };
41
+ export { AiAssist, Converters, CryptoUtils, CryptoUtils as Crypto, Csv, Experimental, Hash, Mustache, RecordJar, SaferFetch, Yaml, ZipFileTree };
40
42
  /* c8 ignore stop */
41
43
  //# sourceMappingURL=index.browser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,kFAAkF;AAClF,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AACjD,yDAAyD;AACzD,OAAO,KAAK,WAAW,MAAM,uCAAuC,CAAC;AACrE,yDAAyD;AACzD,OAAO,KAAK,GAAG,MAAM,8BAA8B,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,yBAAyB,CAAC;AACxD,yDAAyD;AACzD,OAAO,KAAK,IAAI,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,yDAAyD;AACzD,OAAO,KAAK,SAAS,MAAM,qCAAqC,CAAC;AACjE,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,iEAAiE;AACjE,uEAAuE;AACvE,OAAO,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,IAAI,MAAM,EACrB,GAAG,EACH,YAAY,EACZ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,WAAW,EACZ,CAAC;AACF,oBAAoB","sourcesContent":["/*\n * Copyright (c) 2020 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/* c8 ignore start - Browser-specific export used conditionally in package.json */\nimport * as AiAssist from './packlets/ai-assist';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as CryptoUtils from './packlets/crypto-utils/index.browser';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as Csv from './packlets/csv/index.browser';\nimport * as Experimental from './packlets/experimental';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as Hash from './packlets/hash/index.browser';\nimport * as Mustache from './packlets/mustache';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as RecordJar from './packlets/record-jar/index.browser';\nimport * as Yaml from './packlets/yaml';\nimport * as ZipFileTree from './packlets/zip-file-tree';\n\nimport { Converters } from './packlets/conversion';\n\n// Browser-safe exports - Node.js crypto-based providers excluded\n// Use BrowserCryptoProvider from @fgv/ts-web-extras for browser crypto\nexport {\n AiAssist,\n Converters,\n CryptoUtils,\n CryptoUtils as Crypto,\n Csv,\n Experimental,\n Hash,\n Mustache,\n RecordJar,\n Yaml,\n ZipFileTree\n};\n/* c8 ignore stop */\n"]}
1
+ {"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,kFAAkF;AAClF,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AACjD,yDAAyD;AACzD,OAAO,KAAK,WAAW,MAAM,uCAAuC,CAAC;AACrE,yDAAyD;AACzD,OAAO,KAAK,GAAG,MAAM,8BAA8B,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,yBAAyB,CAAC;AACxD,yDAAyD;AACzD,OAAO,KAAK,IAAI,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,yDAAyD;AACzD,OAAO,KAAK,SAAS,MAAM,qCAAqC,CAAC;AACjE,yDAAyD;AACzD,OAAO,KAAK,UAAU,MAAM,sCAAsC,CAAC;AACnE,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,iEAAiE;AACjE,uEAAuE;AACvE,OAAO,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,IAAI,MAAM,EACrB,GAAG,EACH,YAAY,EACZ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,UAAU,EACV,IAAI,EACJ,WAAW,EACZ,CAAC;AACF,oBAAoB","sourcesContent":["/*\n * Copyright (c) 2020 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/* c8 ignore start - Browser-specific export used conditionally in package.json */\nimport * as AiAssist from './packlets/ai-assist';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as CryptoUtils from './packlets/crypto-utils/index.browser';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as Csv from './packlets/csv/index.browser';\nimport * as Experimental from './packlets/experimental';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as Hash from './packlets/hash/index.browser';\nimport * as Mustache from './packlets/mustache';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as RecordJar from './packlets/record-jar/index.browser';\n// eslint-disable-next-line @rushstack/packlets/mechanics\nimport * as SaferFetch from './packlets/safer-fetch/index.browser';\nimport * as Yaml from './packlets/yaml';\nimport * as ZipFileTree from './packlets/zip-file-tree';\n\nimport { Converters } from './packlets/conversion';\n\n// Browser-safe exports - Node.js crypto-based providers excluded\n// Use BrowserCryptoProvider from @fgv/ts-web-extras for browser crypto\nexport {\n AiAssist,\n Converters,\n CryptoUtils,\n CryptoUtils as Crypto,\n Csv,\n Experimental,\n Hash,\n Mustache,\n RecordJar,\n SaferFetch,\n Yaml,\n ZipFileTree\n};\n/* c8 ignore stop */\n"]}
package/dist/index.js CHANGED
@@ -26,8 +26,9 @@ import * as Experimental from './packlets/experimental';
26
26
  import * as Hash from './packlets/hash';
27
27
  import * as Mustache from './packlets/mustache';
28
28
  import * as RecordJar from './packlets/record-jar';
29
+ import * as SaferFetch from './packlets/safer-fetch';
29
30
  import * as Yaml from './packlets/yaml';
30
31
  import * as ZipFileTree from './packlets/zip-file-tree';
31
32
  import { Converters } from './packlets/conversion';
32
- export { AiAssist, Converters, CryptoUtils, Csv, Experimental, Hash, Mustache, RecordJar, Yaml, ZipFileTree };
33
+ export { AiAssist, Converters, CryptoUtils, Csv, Experimental, Hash, Mustache, RecordJar, SaferFetch, Yaml, ZipFileTree };
33
34
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,GAAG,MAAM,gBAAgB,CAAC;AACtC,OAAO,KAAK,YAAY,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,uBAAuB,CAAC;AACnD,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC","sourcesContent":["/*\n * Copyright (c) 2020 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport * as AiAssist from './packlets/ai-assist';\nimport * as CryptoUtils from './packlets/crypto-utils';\nimport * as Csv from './packlets/csv';\nimport * as Experimental from './packlets/experimental';\nimport * as Hash from './packlets/hash';\nimport * as Mustache from './packlets/mustache';\nimport * as RecordJar from './packlets/record-jar';\nimport * as Yaml from './packlets/yaml';\nimport * as ZipFileTree from './packlets/zip-file-tree';\n\nimport { Converters } from './packlets/conversion';\n\nexport { AiAssist, Converters, CryptoUtils, Csv, Experimental, Hash, Mustache, RecordJar, Yaml, ZipFileTree };\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,QAAQ,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,GAAG,MAAM,gBAAgB,CAAC;AACtC,OAAO,KAAK,YAAY,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,uBAAuB,CAAC;AACnD,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EACL,QAAQ,EACR,UAAU,EACV,WAAW,EACX,GAAG,EACH,YAAY,EACZ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,UAAU,EACV,IAAI,EACJ,WAAW,EACZ,CAAC","sourcesContent":["/*\n * Copyright (c) 2020 Erik Fortune\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport * as AiAssist from './packlets/ai-assist';\nimport * as CryptoUtils from './packlets/crypto-utils';\nimport * as Csv from './packlets/csv';\nimport * as Experimental from './packlets/experimental';\nimport * as Hash from './packlets/hash';\nimport * as Mustache from './packlets/mustache';\nimport * as RecordJar from './packlets/record-jar';\nimport * as SaferFetch from './packlets/safer-fetch';\nimport * as Yaml from './packlets/yaml';\nimport * as ZipFileTree from './packlets/zip-file-tree';\n\nimport { Converters } from './packlets/conversion';\n\nexport {\n AiAssist,\n Converters,\n CryptoUtils,\n Csv,\n Experimental,\n Hash,\n Mustache,\n RecordJar,\n SaferFetch,\n Yaml,\n ZipFileTree\n};\n"]}
@@ -10,7 +10,7 @@ export { callProviderEmbedding, callProxiedEmbedding } from './embeddingClient';
10
10
  export { callProviderCompletionStream, callProxiedCompletionStream, executeClientToolTurn } from './streamingClient';
11
11
  export { aiProviderId, aiServerToolType, aiWebSearchToolConfig, aiServerToolConfig, aiToolAnnotations, aiClientToolConfig, aiToolEnablement, aiAssistProviderConfig, aiAssistSettings, modelSpecKey, modelSpec } from './converters';
12
12
  export { resolveEffectiveTools } from './toolFormats';
13
- export { extractJsonText, fencedStringifiedJson } from './jsonResponse';
13
+ export { classifyJsonParseFailure, extractJsonText, fencedStringifiedJson } from './jsonResponse';
14
14
  export { generateJsonCompletion, SMART_JSON_PROMPT_HINT } from './jsonCompletion';
15
15
  export { anthropicEffortToBudgetTokens } from './thinkingOptionsResolver';
16
16
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EAER,oBAAoB,EAcpB,4BAA4B,EAe5B,iBAAiB,EAEjB,wBAAwB,EAoCxB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EAEZ,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,uBAAuB,EACvB,4BAA4B,EAC5B,SAAS,EAiBV,MAAM,SAAS,CAAC;AAEjB,OAAO,EAEL,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,0BAA0B,EAC1B,iBAAiB,EACjB,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,EAC1B,sBAAsB,EACtB,qBAAqB,EAItB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EAErB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAE3B,qBAAqB,EAItB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EACL,eAAe,EACf,qBAAqB,EAItB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EAIvB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,6BAA6B,EAAgC,MAAM,2BAA2B,CAAC","sourcesContent":["/**\n * AI assist packlet - provider registry, prompt class, settings, and API client.\n * @packageDocumentation\n */\n\nexport {\n AiPrompt,\n type AiModelCapability,\n allModelCapabilities,\n type AiProviderId,\n type AiServerToolType,\n type AiServerToolConfig,\n type AiToolConfig,\n type IAiWebSearchToolConfig,\n type IAiClientToolConfig,\n type IAiToolAnnotations,\n type IAiClientTool,\n type IAiClientToolCallSummary,\n type IAiClientToolContinuation,\n type IAiClientToolTurnResult,\n type IAiToolEnablement,\n type IAiCompletionResponse,\n DEFAULT_ANTHROPIC_MAX_TOKENS,\n type IChatMessage,\n type IChatRequest,\n type AiApiFormat,\n type AiImageApiFormat,\n type AiEmbeddingApiFormat,\n type AiEmbeddingTaskType,\n type IAiEmbeddingModelCapability,\n type IAiEmbeddingParams,\n type IAiEmbeddingUsage,\n type IAiEmbeddingResult,\n type IAiImageModelCapability,\n type IAiProviderDescriptor,\n type IAiAssistProviderConfig,\n type IAiAssistSettings,\n DEFAULT_AI_ASSIST,\n type IAiAssistKeyStore,\n providerApiKeySecretName,\n type IAiImageAttachment,\n type IAiImageData,\n type AiImageSize,\n type AiImageQuality,\n type GptImageSize,\n type GptImageQuality,\n type GptImageModelNames,\n type GrokImagineModelNames,\n type GeminiFlashImageModelNames,\n type IGptImageGenerationConfig,\n type IGrokImagineImageGenerationConfig,\n type IGeminiFlashImageGenerationConfig,\n type IGptImageModelOptions,\n type IGrokImagineModelOptions,\n type IGeminiFlashImageModelOptions,\n type IOtherModelOptions,\n type IModelFamilyConfig,\n type IAiImageGenerationOptions,\n type IAiImageGenerationParams,\n type IAiGeneratedImage,\n type IAiImageGenerationResponse,\n type IAiModelCapabilityRule,\n type IAiModelCapabilityConfig,\n type IAiModelInfo,\n type IAiStreamEvent,\n type IAiStreamTextDelta,\n type IAiStreamToolEvent,\n type IAiStreamToolUseStart,\n type IAiStreamToolUseDelta,\n type IAiStreamToolUseComplete,\n type IAiStreamDone,\n type IAiStreamError,\n type ModelSpec,\n type ModelSpecKey,\n type IModelSpecMap,\n allModelSpecKeys,\n MODEL_SPEC_BASE_KEY,\n resolveModel,\n type IModelAliasMap,\n MODEL_ALIAS_SIGIL,\n resolveModelAlias,\n resolveProviderModel,\n isResponsesOnlyModel,\n isAdaptiveThinkingModel,\n usesMaxCompletionTokensField,\n toDataUrl,\n type AiThinkingMode,\n type IThinkingConfig,\n type IThinkingProviderConfig,\n type IAnthropicThinkingOptions,\n type IOpenAiThinkingOptions,\n type IGeminiThinkingOptions,\n type IXAiThinkingOptions,\n type IOtherThinkingOptions,\n type IAnthropicThinkingConfig,\n type IOpenAiThinkingConfig,\n type IGeminiThinkingConfig,\n type IXAiThinkingConfig,\n type AnthropicThinkingModelNames,\n type OpenAiThinkingModelNames,\n type GeminiThinkingModelNames,\n type XAiThinkingModelNames\n} from './model';\n\nexport {\n type IResolvedImageOptions,\n resolveImageOptions,\n validateResolvedOptions\n} from './imageOptionsResolver';\n\nexport {\n allProviderIds,\n getProviderDescriptors,\n getProviderDescriptor,\n resolveImageCapability,\n supportsImageGeneration,\n resolveEmbeddingCapability,\n supportsEmbedding,\n DEFAULT_MODEL_CAPABILITY_CONFIG\n} from './registry';\n\nexport {\n callProviderCompletion,\n callProxiedCompletion,\n callProviderImageGeneration,\n callProxiedImageGeneration,\n callProviderListModels,\n callProxiedListModels,\n type IProviderCompletionParams,\n type IProviderImageGenerationParams,\n type IProviderListModelsParams\n} from './apiClient';\n\nexport {\n callProviderEmbedding,\n callProxiedEmbedding,\n type IProviderEmbeddingParams\n} from './embeddingClient';\n\nexport {\n callProviderCompletionStream,\n callProxiedCompletionStream,\n type IProviderCompletionStreamParams,\n executeClientToolTurn,\n type IExecuteClientToolTurnParams,\n type IExecuteClientToolTurnResult,\n type IToolExecutionDecision\n} from './streamingClient';\n\nexport {\n aiProviderId,\n aiServerToolType,\n aiWebSearchToolConfig,\n aiServerToolConfig,\n aiToolAnnotations,\n aiClientToolConfig,\n aiToolEnablement,\n aiAssistProviderConfig,\n aiAssistSettings,\n modelSpecKey,\n modelSpec\n} from './converters';\n\nexport { resolveEffectiveTools } from './toolFormats';\n\nexport {\n extractJsonText,\n fencedStringifiedJson,\n type IFencedStringifiedJsonExtractorOptions,\n type IFencedStringifiedJsonOptions,\n type JsonTextExtractor\n} from './jsonResponse';\n\nexport {\n generateJsonCompletion,\n SMART_JSON_PROMPT_HINT,\n type IGenerateJsonCompletionParams,\n type IGenerateJsonCompletionResult,\n type JsonPromptHint\n} from './jsonCompletion';\n\nexport { anthropicEffortToBudgetTokens, type IResolvedThinkingConfig } from './thinkingOptionsResolver';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EAER,oBAAoB,EAcpB,4BAA4B,EAe5B,iBAAiB,EAEjB,wBAAwB,EAoCxB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EAEZ,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,uBAAuB,EACvB,4BAA4B,EAC5B,SAAS,EAiBV,MAAM,SAAS,CAAC;AAEjB,OAAO,EAEL,mBAAmB,EACnB,uBAAuB,EACxB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,0BAA0B,EAC1B,iBAAiB,EACjB,+BAA+B,EAChC,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,EAC1B,sBAAsB,EACtB,qBAAqB,EAItB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EAErB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,4BAA4B,EAC5B,2BAA2B,EAE3B,qBAAqB,EAItB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EACL,wBAAwB,EACxB,eAAe,EACf,qBAAqB,EAKtB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EAIvB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,6BAA6B,EAAgC,MAAM,2BAA2B,CAAC","sourcesContent":["/**\n * AI assist packlet - provider registry, prompt class, settings, and API client.\n * @packageDocumentation\n */\n\nexport {\n AiPrompt,\n type AiModelCapability,\n allModelCapabilities,\n type AiProviderId,\n type AiServerToolType,\n type AiServerToolConfig,\n type AiToolConfig,\n type IAiWebSearchToolConfig,\n type IAiClientToolConfig,\n type IAiToolAnnotations,\n type IAiClientTool,\n type IAiClientToolCallSummary,\n type IAiClientToolContinuation,\n type IAiClientToolTurnResult,\n type IAiToolEnablement,\n type IAiCompletionResponse,\n DEFAULT_ANTHROPIC_MAX_TOKENS,\n type IChatMessage,\n type IChatRequest,\n type AiApiFormat,\n type AiImageApiFormat,\n type AiEmbeddingApiFormat,\n type AiEmbeddingTaskType,\n type IAiEmbeddingModelCapability,\n type IAiEmbeddingParams,\n type IAiEmbeddingUsage,\n type IAiEmbeddingResult,\n type IAiImageModelCapability,\n type IAiProviderDescriptor,\n type IAiAssistProviderConfig,\n type IAiAssistSettings,\n DEFAULT_AI_ASSIST,\n type IAiAssistKeyStore,\n providerApiKeySecretName,\n type IAiImageAttachment,\n type IAiImageData,\n type AiImageSize,\n type AiImageQuality,\n type GptImageSize,\n type GptImageQuality,\n type GptImageModelNames,\n type GrokImagineModelNames,\n type GeminiFlashImageModelNames,\n type IGptImageGenerationConfig,\n type IGrokImagineImageGenerationConfig,\n type IGeminiFlashImageGenerationConfig,\n type IGptImageModelOptions,\n type IGrokImagineModelOptions,\n type IGeminiFlashImageModelOptions,\n type IOtherModelOptions,\n type IModelFamilyConfig,\n type IAiImageGenerationOptions,\n type IAiImageGenerationParams,\n type IAiGeneratedImage,\n type IAiImageGenerationResponse,\n type IAiModelCapabilityRule,\n type IAiModelCapabilityConfig,\n type IAiModelInfo,\n type IAiStreamEvent,\n type IAiStreamTextDelta,\n type IAiStreamToolEvent,\n type IAiStreamToolUseStart,\n type IAiStreamToolUseDelta,\n type IAiStreamToolUseComplete,\n type IAiStreamDone,\n type IAiStreamError,\n type ModelSpec,\n type ModelSpecKey,\n type IModelSpecMap,\n allModelSpecKeys,\n MODEL_SPEC_BASE_KEY,\n resolveModel,\n type IModelAliasMap,\n MODEL_ALIAS_SIGIL,\n resolveModelAlias,\n resolveProviderModel,\n isResponsesOnlyModel,\n isAdaptiveThinkingModel,\n usesMaxCompletionTokensField,\n toDataUrl,\n type AiThinkingMode,\n type IThinkingConfig,\n type IThinkingProviderConfig,\n type IAnthropicThinkingOptions,\n type IOpenAiThinkingOptions,\n type IGeminiThinkingOptions,\n type IXAiThinkingOptions,\n type IOtherThinkingOptions,\n type IAnthropicThinkingConfig,\n type IOpenAiThinkingConfig,\n type IGeminiThinkingConfig,\n type IXAiThinkingConfig,\n type AnthropicThinkingModelNames,\n type OpenAiThinkingModelNames,\n type GeminiThinkingModelNames,\n type XAiThinkingModelNames\n} from './model';\n\nexport {\n type IResolvedImageOptions,\n resolveImageOptions,\n validateResolvedOptions\n} from './imageOptionsResolver';\n\nexport {\n allProviderIds,\n getProviderDescriptors,\n getProviderDescriptor,\n resolveImageCapability,\n supportsImageGeneration,\n resolveEmbeddingCapability,\n supportsEmbedding,\n DEFAULT_MODEL_CAPABILITY_CONFIG\n} from './registry';\n\nexport {\n callProviderCompletion,\n callProxiedCompletion,\n callProviderImageGeneration,\n callProxiedImageGeneration,\n callProviderListModels,\n callProxiedListModels,\n type IProviderCompletionParams,\n type IProviderImageGenerationParams,\n type IProviderListModelsParams\n} from './apiClient';\n\nexport {\n callProviderEmbedding,\n callProxiedEmbedding,\n type IProviderEmbeddingParams\n} from './embeddingClient';\n\nexport {\n callProviderCompletionStream,\n callProxiedCompletionStream,\n type IProviderCompletionStreamParams,\n executeClientToolTurn,\n type IExecuteClientToolTurnParams,\n type IExecuteClientToolTurnResult,\n type IToolExecutionDecision\n} from './streamingClient';\n\nexport {\n aiProviderId,\n aiServerToolType,\n aiWebSearchToolConfig,\n aiServerToolConfig,\n aiToolAnnotations,\n aiClientToolConfig,\n aiToolEnablement,\n aiAssistProviderConfig,\n aiAssistSettings,\n modelSpecKey,\n modelSpec\n} from './converters';\n\nexport { resolveEffectiveTools } from './toolFormats';\n\nexport {\n classifyJsonParseFailure,\n extractJsonText,\n fencedStringifiedJson,\n type IFencedStringifiedJsonExtractorOptions,\n type IFencedStringifiedJsonOptions,\n type JsonParseFailureReason,\n type JsonTextExtractor\n} from './jsonResponse';\n\nexport {\n generateJsonCompletion,\n SMART_JSON_PROMPT_HINT,\n type IGenerateJsonCompletionParams,\n type IGenerateJsonCompletionResult,\n type JsonPromptHint\n} from './jsonCompletion';\n\nexport { anthropicEffortToBudgetTokens, type IResolvedThinkingConfig } from './thinkingOptionsResolver';\n"]}
@@ -33,7 +33,11 @@
33
33
  */
34
34
  import { Conversion, fail, succeed } from '@fgv/ts-utils';
35
35
  import { Converters as JsonBaseConverters } from '@fgv/ts-json-base';
36
- const FENCED_BLOCK = /```[A-Za-z0-9_-]*\s*\r?\n([\s\S]*?)\r?\n?```/;
36
+ // Group 1 is the opening fence (marker, optional language tag, newline) and
37
+ // group 2 is the fenced body. Group 1 is captured only so a caller that needs
38
+ // to map a position inside the body back onto an offset in the original text
39
+ // can add its length instead of re-deriving the fence header.
40
+ const FENCED_BLOCK = /(```[A-Za-z0-9_-]*\s*\r?\n)([\s\S]*?)\r?\n?```/;
37
41
  const BOM = /^\uFEFF/;
38
42
  // Full RFC 8259 grammar so the extractor only succeeds when the entire
39
43
  // candidate parses as a JSON primitive (instead of just starting like one).
@@ -88,11 +92,36 @@ function findBalancedJsonSubstring(text) {
88
92
  else if (ch === close) {
89
93
  depth--;
90
94
  if (depth === 0) {
91
- return { kind: 'found', value: text.slice(start, i + 1) };
95
+ return { kind: 'found', value: text.slice(start, i + 1), start };
92
96
  }
93
97
  }
94
98
  }
95
- return start >= 0 ? { kind: 'unclosed', depth } : { kind: 'none' };
99
+ return start >= 0 ? { kind: 'unclosed', depth, start } : { kind: 'none' };
100
+ }
101
+ /**
102
+ * Strips the wrappers a model adds around JSON — byte-order mark, surrounding
103
+ * whitespace, Markdown code fence — and reports where what is left starts in
104
+ * the original text.
105
+ *
106
+ * Single source of truth for that step: {@link extractJsonText} and
107
+ * {@link classifyJsonParseFailure} must agree on what the candidate is, or a
108
+ * classified `offset` would point into text the extractor never parsed.
109
+ * @internal
110
+ */
111
+ function locateJsonCandidate(text) {
112
+ const noBom = stripBom(text);
113
+ const stripped = noBom.trim();
114
+ const strippedBase = text.length - noBom.length + (noBom.length - noBom.trimStart().length);
115
+ const fenced = FENCED_BLOCK.exec(stripped);
116
+ if (fenced === null) {
117
+ return { stripped, text: stripped, base: strippedBase };
118
+ }
119
+ const body = fenced[2];
120
+ return {
121
+ stripped,
122
+ text: body.trim(),
123
+ base: strippedBase + fenced.index + fenced[1].length + (body.length - body.trimStart().length)
124
+ };
96
125
  }
97
126
  /**
98
127
  * Default {@link AiAssist.JsonTextExtractor | extractor} for LLM responses. Tolerates:
@@ -120,12 +149,11 @@ export const extractJsonText = (text) => {
120
149
  if (typeof text !== 'string') {
121
150
  return fail('extractJsonText: input must be a string.');
122
151
  }
123
- const stripped = stripBom(text).trim();
124
- if (stripped.length === 0) {
152
+ const located = locateJsonCandidate(text);
153
+ if (located.stripped.length === 0) {
125
154
  return fail('extractJsonText: input is empty.');
126
155
  }
127
- const fenced = FENCED_BLOCK.exec(stripped);
128
- const candidate = fenced ? fenced[1].trim() : stripped;
156
+ const candidate = located.text;
129
157
  if (candidate.length === 0) {
130
158
  return fail('extractJsonText: no JSON content found.');
131
159
  }
@@ -145,6 +173,241 @@ export const extractJsonText = (text) => {
145
173
  }
146
174
  return fail('extractJsonText: no JSON-shaped substring found.');
147
175
  };
176
+ const UNKNOWN_PARSE_FAILURE = { kind: 'unknown' };
177
+ const JSON_WHITESPACE = /[ \t\n\r]/;
178
+ const IDENTIFIER_START = /[A-Za-z0-9_$]/;
179
+ const IDENTIFIER_PART = /[A-Za-z0-9_$.-]/;
180
+ // Characters that end an unquoted value run (`true`, `null`, a number, or
181
+ // garbage). Includes ':' so a stray colon at a value position produces an
182
+ // empty run — reported as 'unknown' — instead of being swallowed by a literal.
183
+ const VALUE_RUN_TERMINATOR = /[\s,}\]:"'{[]/;
184
+ function scanQuotedRun(text, start, quote) {
185
+ let escape = false;
186
+ for (let i = start + 1; i < text.length; i++) {
187
+ const ch = text.charAt(i);
188
+ if (escape) {
189
+ escape = false;
190
+ }
191
+ else if (ch === '\\') {
192
+ escape = true;
193
+ }
194
+ else if (ch === quote) {
195
+ return { terminated: true, end: i + 1, body: text.slice(start + 1, i) };
196
+ }
197
+ }
198
+ return { terminated: false, end: text.length, body: text.slice(start + 1) };
199
+ }
200
+ function skipJsonWhitespace(text, from) {
201
+ let i = from;
202
+ while (i < text.length && JSON_WHITESPACE.test(text.charAt(i))) {
203
+ i++;
204
+ }
205
+ return i;
206
+ }
207
+ function scanRun(text, from, isPart) {
208
+ let i = from;
209
+ while (i < text.length && isPart(text.charAt(i))) {
210
+ i++;
211
+ }
212
+ return text.slice(from, i);
213
+ }
214
+ function classifyAtPropertyName(text, i, base) {
215
+ const ch = text.charAt(i);
216
+ if (ch === '"') {
217
+ const scan = scanQuotedRun(text, i, '"');
218
+ if (scan.terminated) {
219
+ return { next: scan.end };
220
+ }
221
+ // An unterminated name is only distinguishable from a mid-name truncation
222
+ // when the swallowed body contains structural text — a ':' the author
223
+ // plainly meant as the name/value separator. Without that, stay honest.
224
+ return scan.body.includes(':')
225
+ ? { reason: { kind: 'unterminated-property-name', token: text.slice(i), offset: base + i } }
226
+ : { reason: UNKNOWN_PARSE_FAILURE };
227
+ }
228
+ if (ch === "'") {
229
+ const scan = scanQuotedRun(text, i, "'");
230
+ return {
231
+ reason: {
232
+ kind: 'single-quoted-property-name',
233
+ token: scan.terminated ? text.slice(i, scan.end) : "'",
234
+ offset: base + i
235
+ }
236
+ };
237
+ }
238
+ if (ch === ',') {
239
+ return { reason: { kind: 'elided-member', token: ',', offset: base + i } };
240
+ }
241
+ if (IDENTIFIER_START.test(ch)) {
242
+ return {
243
+ reason: {
244
+ kind: 'unquoted-property-name',
245
+ token: scanRun(text, i, (c) => IDENTIFIER_PART.test(c)),
246
+ offset: base + i
247
+ }
248
+ };
249
+ }
250
+ return { reason: UNKNOWN_PARSE_FAILURE };
251
+ }
252
+ /**
253
+ * Walks `text` from `from` (the opening `{` or `[`) as a permissive JSON
254
+ * scanner, reporting the first fault it can name with confidence. Anything it
255
+ * cannot name — including a document that scans clean — yields `'unknown'`.
256
+ *
257
+ * The scan is structural: it never reads the engine's `JSON.parse` message, so
258
+ * its verdicts do not drift with the Node version.
259
+ * @internal
260
+ */
261
+ function scanForParseFailure(text, from, base) {
262
+ const stack = [];
263
+ let expect = 'value';
264
+ let i = from;
265
+ for (;;) {
266
+ i = skipJsonWhitespace(text, i);
267
+ if (i >= text.length) {
268
+ return UNKNOWN_PARSE_FAILURE;
269
+ }
270
+ const ch = text.charAt(i);
271
+ const top = stack[stack.length - 1];
272
+ if (expect === 'name') {
273
+ if (ch === '}') {
274
+ stack.pop();
275
+ i++;
276
+ expect = 'comma';
277
+ continue;
278
+ }
279
+ const outcome = classifyAtPropertyName(text, i, base);
280
+ if ('reason' in outcome) {
281
+ return outcome.reason;
282
+ }
283
+ i = outcome.next;
284
+ expect = 'colon';
285
+ continue;
286
+ }
287
+ if (expect === 'colon') {
288
+ if (ch !== ':') {
289
+ return UNKNOWN_PARSE_FAILURE;
290
+ }
291
+ i++;
292
+ expect = 'value';
293
+ continue;
294
+ }
295
+ if (expect === 'value') {
296
+ if (ch === '{' || ch === '[') {
297
+ stack.push(ch === '{' ? 'object' : 'array');
298
+ i++;
299
+ expect = ch === '{' ? 'name' : 'value';
300
+ continue;
301
+ }
302
+ if (ch === ']' && top === 'array') {
303
+ stack.pop();
304
+ i++;
305
+ expect = 'comma';
306
+ continue;
307
+ }
308
+ if (ch === ',') {
309
+ return top === 'array'
310
+ ? { kind: 'elided-member', token: ',', offset: base + i }
311
+ : UNKNOWN_PARSE_FAILURE;
312
+ }
313
+ if (ch === '"') {
314
+ const scan = scanQuotedRun(text, i, '"');
315
+ if (!scan.terminated) {
316
+ // Could equally be a truncated value; the extractor already names
317
+ // truncation, so do not compete with it here.
318
+ return UNKNOWN_PARSE_FAILURE;
319
+ }
320
+ i = scan.end;
321
+ expect = 'comma';
322
+ continue;
323
+ }
324
+ const literal = scanRun(text, i, (c) => !VALUE_RUN_TERMINATOR.test(c));
325
+ if (literal.length === 0) {
326
+ return UNKNOWN_PARSE_FAILURE;
327
+ }
328
+ i += literal.length;
329
+ expect = 'comma';
330
+ continue;
331
+ }
332
+ // expect === 'comma': a complete value just closed.
333
+ if (top === undefined) {
334
+ return UNKNOWN_PARSE_FAILURE;
335
+ }
336
+ if (ch === ',') {
337
+ i++;
338
+ expect = top === 'object' ? 'name' : 'value';
339
+ continue;
340
+ }
341
+ if ((ch === '}' && top === 'object') || (ch === ']' && top === 'array')) {
342
+ stack.pop();
343
+ i++;
344
+ continue;
345
+ }
346
+ return UNKNOWN_PARSE_FAILURE;
347
+ }
348
+ }
349
+ /**
350
+ * Classifies why a JSON-shaped LLM response would not parse, returning a
351
+ * {@link AiAssist.JsonParseFailureReason} a caller can branch on — repair the
352
+ * cheap cases, re-prompt the expensive ones, fail outright on the rest —
353
+ * instead of regex-matching an engine-specific `JSON.parse` message.
354
+ *
355
+ * Pass the same raw model text you handed
356
+ * {@link AiAssist.fencedStringifiedJson} or {@link AiAssist.extractJsonText};
357
+ * this applies the same BOM / whitespace / fence / preamble handling before
358
+ * scanning, and reports `offset` against that original text.
359
+ *
360
+ * Classification is **structural and deliberately conservative**. The scan
361
+ * walks the JSON grammar itself rather than reading the engine's error string,
362
+ * so its verdicts are stable across Node versions — and any fault it cannot
363
+ * name with confidence comes back as `'unknown'` rather than a guess. In
364
+ * particular an input that opened a structure and never closed it (the
365
+ * truncated-response shape {@link AiAssist.extractJsonText} already diagnoses)
366
+ * classifies as `'unknown'` here; the two diagnostics are complementary, not
367
+ * competing.
368
+ *
369
+ * This never fails and never repairs — it only names the fault. It is a
370
+ * diagnostic on the failure path, so calling it on text that parses fine is
371
+ * harmless but pointless: it returns `'unknown'`.
372
+ *
373
+ * @example
374
+ * ```ts
375
+ * const parsed = fencedStringifiedJson({ inner }).convert(raw);
376
+ * if (parsed.isFailure()) {
377
+ * const reason = classifyJsonParseFailure(raw);
378
+ * switch (reason.kind) {
379
+ * case 'unquoted-property-name': // cheap to repair
380
+ * case 'single-quoted-property-name':
381
+ * break;
382
+ * case 'elided-member':
383
+ * case 'unterminated-property-name': // worth a re-prompt
384
+ * break;
385
+ * default: // 'unknown' — fail outright
386
+ * }
387
+ * }
388
+ * ```
389
+ *
390
+ * @param text - Raw model output (the same string handed to the extractor).
391
+ * @returns A {@link AiAssist.JsonParseFailureReason}.
392
+ * @public
393
+ */
394
+ export function classifyJsonParseFailure(text) {
395
+ // Defensive, and load-bearing for the documented totality: the signature says
396
+ // `string`, but a JS consumer — or a TS caller coming through an `unknown` /
397
+ // `any` escape hatch — can still hand us a non-string, and `locateJsonCandidate`
398
+ // would throw on it. `extractJsonText` already guards the same way; this keeps
399
+ // the pair consistent and keeps "never fails" true in practice rather than only
400
+ // under a well-typed caller.
401
+ if (typeof text !== 'string') {
402
+ return UNKNOWN_PARSE_FAILURE;
403
+ }
404
+ const candidate = locateJsonCandidate(text);
405
+ const scan = findBalancedJsonSubstring(candidate.text);
406
+ if (scan.kind === 'none') {
407
+ return UNKNOWN_PARSE_FAILURE;
408
+ }
409
+ return scanForParseFailure(candidate.text, scan.start, candidate.base);
410
+ }
148
411
  export function fencedStringifiedJson(options) {
149
412
  var _a;
150
413
  const extractor = (_a = options === null || options === void 0 ? void 0 : options.extractor) !== null && _a !== void 0 ? _a : extractJsonText;
@@ -1 +1 @@
1
- {"version":3,"file":"jsonResponse.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/jsonResponse.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * JSON-tolerant extraction and converters for LLM responses.\n *\n * Models commonly wrap JSON output in Markdown code fences, add a\n * \"Sure, here's the JSON:\" preamble, or trail off with prose after the\n * closing brace. These helpers normalize that quirk on the read side so every\n * AiAssist consumer can reach a validated `T` from raw model text without\n * reimplementing the same fence-stripping logic.\n *\n * Scope: strip wrappers (fences, prose, BOM, whitespace). Out of scope: repair\n * malformed JSON (missing commas, unquoted keys, smart quotes, etc.).\n *\n * @packageDocumentation\n */\n\nimport { Conversion, type Converter, fail, Result, succeed, type Validator } from '@fgv/ts-utils';\nimport { Converters as JsonBaseConverters, type JsonValue } from '@fgv/ts-json-base';\n\n/**\n * A function that pulls a JSON-shaped substring out of arbitrary model text.\n * Implementations strip whatever wrappers the model added (fences, preamble,\n * trailing prose) and return the JSON-shaped substring ready for `JSON.parse`.\n * @public\n */\nexport type JsonTextExtractor = (text: string) => Result<string>;\n\nconst FENCED_BLOCK: RegExp = /```[A-Za-z0-9_-]*\\s*\\r?\\n([\\s\\S]*?)\\r?\\n?```/;\nconst BOM: RegExp = /^\\uFEFF/;\n// Full RFC 8259 grammar so the extractor only succeeds when the entire\n// candidate parses as a JSON primitive (instead of just starting like one).\nconst JSON_NUMBER: RegExp = /^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$/;\n// eslint-disable-next-line no-control-regex\nconst JSON_STRING: RegExp = /^\"(?:[^\"\\\\\\u0000-\\u001F]|\\\\(?:[\"\\\\/bfnrt]|u[0-9a-fA-F]{4}))*\"$/;\nconst JSON_KEYWORD: RegExp = /^(?:true|false|null)$/;\n\nfunction stripBom(text: string): string {\n return text.replace(BOM, '');\n}\n\n/**\n * Outcome of a {@link findBalancedJsonSubstring} scan.\n *\n * - `'found'`: a balanced JSON-shaped substring was located; `value` is it.\n * - `'unclosed'`: an object or array opened but the matching close never\n * arrived before the end of input — the classic truncated-response shape;\n * `depth` is the unresolved depth at end of input, counting only the\n * opening delimiter's type (the scan tracks the candidate's own `{`/`}`\n * or `[`/`]` pair, not total mixed-delimiter nesting).\n * - `'none'`: no `{` or `[` was ever seen outside a quoted string.\n * @internal\n */\ntype BalancedJsonScanResult =\n | { readonly kind: 'found'; readonly value: string }\n | { readonly kind: 'unclosed'; readonly depth: number }\n | { readonly kind: 'none' };\n\nfunction findBalancedJsonSubstring(text: string): BalancedJsonScanResult {\n // Walk the text once tracking string state. The first '{' or '[' that is\n // *outside* a quoted string is the candidate start; from there, count\n // matching close characters while ignoring delimiters that appear inside\n // strings.\n let inString = false;\n let escape = false;\n let start = -1;\n let open = '';\n let close = '';\n let depth = 0;\n for (let i = 0; i < text.length; i++) {\n const ch = text.charAt(i);\n if (inString) {\n if (escape) {\n escape = false;\n } else if (ch === '\\\\') {\n escape = true;\n } else if (ch === '\"') {\n inString = false;\n }\n continue;\n }\n if (ch === '\"') {\n inString = true;\n continue;\n }\n if (start < 0) {\n if (ch === '{' || ch === '[') {\n start = i;\n open = ch;\n close = ch === '{' ? '}' : ']';\n depth = 1;\n }\n continue;\n }\n if (ch === open) {\n depth++;\n } else if (ch === close) {\n depth--;\n if (depth === 0) {\n return { kind: 'found', value: text.slice(start, i + 1) };\n }\n }\n }\n return start >= 0 ? { kind: 'unclosed', depth } : { kind: 'none' };\n}\n\n/**\n * Default {@link AiAssist.JsonTextExtractor | extractor} for LLM responses. Tolerates:\n *\n * - Leading/trailing whitespace and a leading byte-order mark.\n * - Markdown code fences (with or without a language tag).\n * - Conversational preamble before the first `{` or `[`.\n * - Trailing prose after the matched closing `}` or `]`.\n *\n * Out of scope: repairing malformed JSON, handling smart quotes, etc.\n *\n * When an object or array opens but never closes before the end of input —\n * the shape a response truncated by a token cap leaves behind — the failure\n * names that specifically (distinct from the generic \"no JSON-shaped\n * substring found\" for input that never looked like JSON at all) and points\n * at `IAiCompletionResponse.truncated` and the `maxTokens` request option as\n * the likely cause and fix.\n *\n * @param text - Raw model output.\n * @returns A `Result<string>` containing the JSON-shaped substring, or a\n * `Failure` if no JSON-shaped substring was found.\n * @public\n */\nexport const extractJsonText: JsonTextExtractor = (text: string): Result<string> => {\n if (typeof text !== 'string') {\n return fail('extractJsonText: input must be a string.');\n }\n const stripped = stripBom(text).trim();\n if (stripped.length === 0) {\n return fail('extractJsonText: input is empty.');\n }\n\n const fenced = FENCED_BLOCK.exec(stripped);\n const candidate = fenced ? fenced[1].trim() : stripped;\n\n if (candidate.length === 0) {\n return fail('extractJsonText: no JSON content found.');\n }\n\n // Whole-candidate primitive check runs before the brace scan so that a\n // valid JSON string containing braces (e.g. `\"text with { }\"`) is returned\n // intact instead of being mangled into the first balanced `{ }` match.\n if (JSON_KEYWORD.test(candidate) || JSON_NUMBER.test(candidate) || JSON_STRING.test(candidate)) {\n return succeed(candidate);\n }\n\n const scan = findBalancedJsonSubstring(candidate);\n if (scan.kind === 'found') {\n return succeed(scan.value);\n }\n if (scan.kind === 'unclosed') {\n return fail(\n `extractJsonText: JSON structure opened but never closed (depth ${scan.depth} at end of input) — ` +\n 'response may have been truncated (check IAiCompletionResponse.truncated / raise maxTokens).'\n );\n }\n\n return fail('extractJsonText: no JSON-shaped substring found.');\n};\n\n/**\n * Options shared by every {@link AiAssist.fencedStringifiedJson} call.\n * @public\n */\nexport interface IFencedStringifiedJsonExtractorOptions {\n /**\n * Optional pre-parse extractor. Defaults to {@link AiAssist.extractJsonText}.\n * Provide a custom extractor to handle response shapes the default does not\n * understand.\n */\n readonly extractor?: JsonTextExtractor;\n}\n\n/**\n * Options for the validating overload of {@link AiAssist.fencedStringifiedJson}.\n * `inner` is required so the typed `Converter<T>` return value can never lie\n * about the runtime shape.\n * @public\n */\nexport interface IFencedStringifiedJsonOptions<T> extends IFencedStringifiedJsonExtractorOptions {\n /** Inner converter or validator applied to the parsed JSON value. */\n readonly inner: Converter<T> | Validator<T>;\n}\n\n/**\n * Creates a `Converter` that accepts raw LLM response text, runs it through a\n * tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the\n * extracted substring as JSON, and applies an optional inner converter or\n * validator.\n *\n * @example\n * ```ts\n * const converter = fencedStringifiedJson({ inner: myShapeConverter });\n * const result = converter.convert(llmText); // Result<MyShape>\n * ```\n *\n * @param options - Optional extractor; omit to keep the default. Without an\n * `inner` step, the converter resolves to the parsed `JsonValue`.\n * @returns A `Converter<JsonValue>`.\n * @public\n */\nexport function fencedStringifiedJson(options?: IFencedStringifiedJsonExtractorOptions): Converter<JsonValue>;\n/**\n * Creates a `Converter` that accepts raw LLM response text, runs it through a\n * tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the\n * extracted substring as JSON, and applies the supplied inner converter or\n * validator.\n *\n * @param options - Required `inner` converter/validator and optional extractor.\n * @returns A `Converter<T>`.\n * @public\n */\nexport function fencedStringifiedJson<T>(options: IFencedStringifiedJsonOptions<T>): Converter<T>;\nexport function fencedStringifiedJson<T>(\n options?: IFencedStringifiedJsonExtractorOptions | IFencedStringifiedJsonOptions<T>\n): Converter<T | JsonValue> {\n const extractor: JsonTextExtractor = options?.extractor ?? extractJsonText;\n const inner = (options as IFencedStringifiedJsonOptions<T> | undefined)?.inner;\n const parser: Converter<T | JsonValue> =\n inner !== undefined ? JsonBaseConverters.stringifiedJson<T>(inner) : JsonBaseConverters.stringifiedJson();\n\n return new Conversion.BaseConverter<T | JsonValue>((from: unknown): Result<T | JsonValue> => {\n if (typeof from !== 'string') {\n return fail('fencedStringifiedJson: input must be a string.');\n }\n return extractor(from).onSuccess((extracted) => parser.convert(extracted));\n });\n}\n"]}
1
+ {"version":3,"file":"jsonResponse.js","sourceRoot":"","sources":["../../../src/packlets/ai-assist/jsonResponse.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Erik Fortune\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all\n// copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n// SOFTWARE.\n\n/**\n * JSON-tolerant extraction and converters for LLM responses.\n *\n * Models commonly wrap JSON output in Markdown code fences, add a\n * \"Sure, here's the JSON:\" preamble, or trail off with prose after the\n * closing brace. These helpers normalize that quirk on the read side so every\n * AiAssist consumer can reach a validated `T` from raw model text without\n * reimplementing the same fence-stripping logic.\n *\n * Scope: strip wrappers (fences, prose, BOM, whitespace). Out of scope: repair\n * malformed JSON (missing commas, unquoted keys, smart quotes, etc.).\n *\n * @packageDocumentation\n */\n\nimport { Conversion, type Converter, fail, Result, succeed, type Validator } from '@fgv/ts-utils';\nimport { Converters as JsonBaseConverters, type JsonValue } from '@fgv/ts-json-base';\n\n/**\n * A function that pulls a JSON-shaped substring out of arbitrary model text.\n * Implementations strip whatever wrappers the model added (fences, preamble,\n * trailing prose) and return the JSON-shaped substring ready for `JSON.parse`.\n * @public\n */\nexport type JsonTextExtractor = (text: string) => Result<string>;\n\n// Group 1 is the opening fence (marker, optional language tag, newline) and\n// group 2 is the fenced body. Group 1 is captured only so a caller that needs\n// to map a position inside the body back onto an offset in the original text\n// can add its length instead of re-deriving the fence header.\nconst FENCED_BLOCK: RegExp = /(```[A-Za-z0-9_-]*\\s*\\r?\\n)([\\s\\S]*?)\\r?\\n?```/;\nconst BOM: RegExp = /^\\uFEFF/;\n// Full RFC 8259 grammar so the extractor only succeeds when the entire\n// candidate parses as a JSON primitive (instead of just starting like one).\nconst JSON_NUMBER: RegExp = /^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$/;\n// eslint-disable-next-line no-control-regex\nconst JSON_STRING: RegExp = /^\"(?:[^\"\\\\\\u0000-\\u001F]|\\\\(?:[\"\\\\/bfnrt]|u[0-9a-fA-F]{4}))*\"$/;\nconst JSON_KEYWORD: RegExp = /^(?:true|false|null)$/;\n\nfunction stripBom(text: string): string {\n return text.replace(BOM, '');\n}\n\n/**\n * Outcome of a {@link findBalancedJsonSubstring} scan.\n *\n * - `'found'`: a balanced JSON-shaped substring was located; `value` is it.\n * - `'unclosed'`: an object or array opened but the matching close never\n * arrived before the end of input — the classic truncated-response shape;\n * `depth` is the unresolved depth at end of input, counting only the\n * opening delimiter's type (the scan tracks the candidate's own `{`/`}`\n * or `[`/`]` pair, not total mixed-delimiter nesting).\n * - `'none'`: no `{` or `[` was ever seen outside a quoted string.\n *\n * `start` (present on `'found'` and `'unclosed'`) is the index of the opening\n * `{` or `[` within the scanned text.\n * @internal\n */\ntype BalancedJsonScanResult =\n | { readonly kind: 'found'; readonly value: string; readonly start: number }\n | { readonly kind: 'unclosed'; readonly depth: number; readonly start: number }\n | { readonly kind: 'none' };\n\nfunction findBalancedJsonSubstring(text: string): BalancedJsonScanResult {\n // Walk the text once tracking string state. The first '{' or '[' that is\n // *outside* a quoted string is the candidate start; from there, count\n // matching close characters while ignoring delimiters that appear inside\n // strings.\n let inString = false;\n let escape = false;\n let start = -1;\n let open = '';\n let close = '';\n let depth = 0;\n for (let i = 0; i < text.length; i++) {\n const ch = text.charAt(i);\n if (inString) {\n if (escape) {\n escape = false;\n } else if (ch === '\\\\') {\n escape = true;\n } else if (ch === '\"') {\n inString = false;\n }\n continue;\n }\n if (ch === '\"') {\n inString = true;\n continue;\n }\n if (start < 0) {\n if (ch === '{' || ch === '[') {\n start = i;\n open = ch;\n close = ch === '{' ? '}' : ']';\n depth = 1;\n }\n continue;\n }\n if (ch === open) {\n depth++;\n } else if (ch === close) {\n depth--;\n if (depth === 0) {\n return { kind: 'found', value: text.slice(start, i + 1), start };\n }\n }\n }\n return start >= 0 ? { kind: 'unclosed', depth, start } : { kind: 'none' };\n}\n\n/**\n * The JSON-shaped candidate located within raw model text, plus where it\n * starts in that text.\n * @internal\n */\ninterface IJsonCandidate {\n /** The whole input after BOM strip and trim, before any fence unwrap. */\n readonly stripped: string;\n /** The candidate itself: the trimmed fenced body when fenced, else `stripped`. */\n readonly text: string;\n /** Index of `text`'s first character within the original input. */\n readonly base: number;\n}\n\n/**\n * Strips the wrappers a model adds around JSON — byte-order mark, surrounding\n * whitespace, Markdown code fence — and reports where what is left starts in\n * the original text.\n *\n * Single source of truth for that step: {@link extractJsonText} and\n * {@link classifyJsonParseFailure} must agree on what the candidate is, or a\n * classified `offset` would point into text the extractor never parsed.\n * @internal\n */\nfunction locateJsonCandidate(text: string): IJsonCandidate {\n const noBom = stripBom(text);\n const stripped = noBom.trim();\n const strippedBase = text.length - noBom.length + (noBom.length - noBom.trimStart().length);\n\n const fenced = FENCED_BLOCK.exec(stripped);\n if (fenced === null) {\n return { stripped, text: stripped, base: strippedBase };\n }\n const body = fenced[2];\n return {\n stripped,\n text: body.trim(),\n base: strippedBase + fenced.index + fenced[1].length + (body.length - body.trimStart().length)\n };\n}\n\n/**\n * Default {@link AiAssist.JsonTextExtractor | extractor} for LLM responses. Tolerates:\n *\n * - Leading/trailing whitespace and a leading byte-order mark.\n * - Markdown code fences (with or without a language tag).\n * - Conversational preamble before the first `{` or `[`.\n * - Trailing prose after the matched closing `}` or `]`.\n *\n * Out of scope: repairing malformed JSON, handling smart quotes, etc.\n *\n * When an object or array opens but never closes before the end of input —\n * the shape a response truncated by a token cap leaves behind — the failure\n * names that specifically (distinct from the generic \"no JSON-shaped\n * substring found\" for input that never looked like JSON at all) and points\n * at `IAiCompletionResponse.truncated` and the `maxTokens` request option as\n * the likely cause and fix.\n *\n * @param text - Raw model output.\n * @returns A `Result<string>` containing the JSON-shaped substring, or a\n * `Failure` if no JSON-shaped substring was found.\n * @public\n */\nexport const extractJsonText: JsonTextExtractor = (text: string): Result<string> => {\n if (typeof text !== 'string') {\n return fail('extractJsonText: input must be a string.');\n }\n const located = locateJsonCandidate(text);\n if (located.stripped.length === 0) {\n return fail('extractJsonText: input is empty.');\n }\n\n const candidate = located.text;\n if (candidate.length === 0) {\n return fail('extractJsonText: no JSON content found.');\n }\n\n // Whole-candidate primitive check runs before the brace scan so that a\n // valid JSON string containing braces (e.g. `\"text with { }\"`) is returned\n // intact instead of being mangled into the first balanced `{ }` match.\n if (JSON_KEYWORD.test(candidate) || JSON_NUMBER.test(candidate) || JSON_STRING.test(candidate)) {\n return succeed(candidate);\n }\n\n const scan = findBalancedJsonSubstring(candidate);\n if (scan.kind === 'found') {\n return succeed(scan.value);\n }\n if (scan.kind === 'unclosed') {\n return fail(\n `extractJsonText: JSON structure opened but never closed (depth ${scan.depth} at end of input) — ` +\n 'response may have been truncated (check IAiCompletionResponse.truncated / raise maxTokens).'\n );\n }\n\n return fail('extractJsonText: no JSON-shaped substring found.');\n};\n\n/**\n * Typed reason a JSON-shaped LLM response failed to parse, so a caller can\n * branch on the failure class instead of regex-matching the engine's\n * `JSON.parse` message (whose wording varies across V8 / Node versions).\n *\n * Every classified arm describes a fault at an **object property-name\n * position** — the position an LLM most often gets wrong, and the one whose\n * repair strategy differs most by case:\n *\n * - `'unquoted-property-name'`: a bare identifier where a quoted name belongs\n * (`{ key: 1 }`). `token` is the identifier run, `offset` its first\n * character. Identifier recognition is ASCII-only, so a non-ASCII bare name\n * (`{ ключ: 1 }`) reports `'unknown'` rather than being named.\n * - `'single-quoted-property-name'`: a single-quoted name (`{ 'key': 1 }`).\n * `token` is the quoted literal (or just `'` if it never closes), `offset`\n * the opening quote.\n * - `'unterminated-property-name'`: a name whose closing `\"` is missing, and\n * whose body swallowed structural text (`{ \"key: 1 }`). `token` is the\n * unterminated fragment, `offset` the opening quote.\n * - `'elided-member'`: a `,` where a member is expected — a leading or doubled\n * comma in an object (`{ , \"a\": 1 }`, `{ \"a\":1, , \"b\":2 }`) or an array\n * (`[1, , 2]`). `token` is `','`, `offset` its position.\n * - `'unknown'`: the catch-all. The scan reached the end of the text, or hit a\n * fault it cannot name with confidence, and reports nothing rather than\n * guessing. Truncated responses, missing colons, trailing commas, bad number\n * literals, and anything else not listed above land here.\n *\n * `offset` is a 0-based index into the `text` passed to\n * {@link AiAssist.classifyJsonParseFailure}, not into the extracted substring.\n * @public\n */\nexport type JsonParseFailureReason =\n | { readonly kind: 'unquoted-property-name'; readonly token: string; readonly offset: number }\n | { readonly kind: 'single-quoted-property-name'; readonly token: string; readonly offset: number }\n | { readonly kind: 'unterminated-property-name'; readonly token: string; readonly offset: number }\n | { readonly kind: 'elided-member'; readonly token: string; readonly offset: number }\n | { readonly kind: 'unknown' };\n\nconst UNKNOWN_PARSE_FAILURE: JsonParseFailureReason = { kind: 'unknown' };\n\nconst JSON_WHITESPACE: RegExp = /[ \\t\\n\\r]/;\nconst IDENTIFIER_START: RegExp = /[A-Za-z0-9_$]/;\nconst IDENTIFIER_PART: RegExp = /[A-Za-z0-9_$.-]/;\n// Characters that end an unquoted value run (`true`, `null`, a number, or\n// garbage). Includes ':' so a stray colon at a value position produces an\n// empty run — reported as 'unknown' — instead of being swallowed by a literal.\nconst VALUE_RUN_TERMINATOR: RegExp = /[\\s,}\\]:\"'{[]/;\n\n/** Where the parse scan currently is within the JSON grammar. @internal */\ntype JsonScanExpectation = 'value' | 'name' | 'colon' | 'comma';\n\n/** Result of scanning a quoted run starting at a given opening quote. @internal */\ninterface IQuotedRunScan {\n readonly terminated: boolean;\n /** Index just past the closing quote, or end of text when unterminated. */\n readonly end: number;\n /** Content between the quotes, exclusive. */\n readonly body: string;\n}\n\nfunction scanQuotedRun(text: string, start: number, quote: string): IQuotedRunScan {\n let escape = false;\n for (let i = start + 1; i < text.length; i++) {\n const ch = text.charAt(i);\n if (escape) {\n escape = false;\n } else if (ch === '\\\\') {\n escape = true;\n } else if (ch === quote) {\n return { terminated: true, end: i + 1, body: text.slice(start + 1, i) };\n }\n }\n return { terminated: false, end: text.length, body: text.slice(start + 1) };\n}\n\nfunction skipJsonWhitespace(text: string, from: number): number {\n let i = from;\n while (i < text.length && JSON_WHITESPACE.test(text.charAt(i))) {\n i++;\n }\n return i;\n}\n\nfunction scanRun(text: string, from: number, isPart: (ch: string) => boolean): string {\n let i = from;\n while (i < text.length && isPart(text.charAt(i))) {\n i++;\n }\n return text.slice(from, i);\n}\n\nfunction classifyAtPropertyName(\n text: string,\n i: number,\n base: number\n): { readonly reason: JsonParseFailureReason } | { readonly next: number } {\n const ch = text.charAt(i);\n if (ch === '\"') {\n const scan = scanQuotedRun(text, i, '\"');\n if (scan.terminated) {\n return { next: scan.end };\n }\n // An unterminated name is only distinguishable from a mid-name truncation\n // when the swallowed body contains structural text — a ':' the author\n // plainly meant as the name/value separator. Without that, stay honest.\n return scan.body.includes(':')\n ? { reason: { kind: 'unterminated-property-name', token: text.slice(i), offset: base + i } }\n : { reason: UNKNOWN_PARSE_FAILURE };\n }\n if (ch === \"'\") {\n const scan = scanQuotedRun(text, i, \"'\");\n return {\n reason: {\n kind: 'single-quoted-property-name',\n token: scan.terminated ? text.slice(i, scan.end) : \"'\",\n offset: base + i\n }\n };\n }\n if (ch === ',') {\n return { reason: { kind: 'elided-member', token: ',', offset: base + i } };\n }\n if (IDENTIFIER_START.test(ch)) {\n return {\n reason: {\n kind: 'unquoted-property-name',\n token: scanRun(text, i, (c) => IDENTIFIER_PART.test(c)),\n offset: base + i\n }\n };\n }\n return { reason: UNKNOWN_PARSE_FAILURE };\n}\n\n/**\n * Walks `text` from `from` (the opening `{` or `[`) as a permissive JSON\n * scanner, reporting the first fault it can name with confidence. Anything it\n * cannot name — including a document that scans clean — yields `'unknown'`.\n *\n * The scan is structural: it never reads the engine's `JSON.parse` message, so\n * its verdicts do not drift with the Node version.\n * @internal\n */\nfunction scanForParseFailure(text: string, from: number, base: number): JsonParseFailureReason {\n const stack: Array<'object' | 'array'> = [];\n let expect: JsonScanExpectation = 'value';\n let i = from;\n\n for (;;) {\n i = skipJsonWhitespace(text, i);\n if (i >= text.length) {\n return UNKNOWN_PARSE_FAILURE;\n }\n const ch = text.charAt(i);\n const top = stack[stack.length - 1];\n\n if (expect === 'name') {\n if (ch === '}') {\n stack.pop();\n i++;\n expect = 'comma';\n continue;\n }\n const outcome = classifyAtPropertyName(text, i, base);\n if ('reason' in outcome) {\n return outcome.reason;\n }\n i = outcome.next;\n expect = 'colon';\n continue;\n }\n\n if (expect === 'colon') {\n if (ch !== ':') {\n return UNKNOWN_PARSE_FAILURE;\n }\n i++;\n expect = 'value';\n continue;\n }\n\n if (expect === 'value') {\n if (ch === '{' || ch === '[') {\n stack.push(ch === '{' ? 'object' : 'array');\n i++;\n expect = ch === '{' ? 'name' : 'value';\n continue;\n }\n if (ch === ']' && top === 'array') {\n stack.pop();\n i++;\n expect = 'comma';\n continue;\n }\n if (ch === ',') {\n return top === 'array'\n ? { kind: 'elided-member', token: ',', offset: base + i }\n : UNKNOWN_PARSE_FAILURE;\n }\n if (ch === '\"') {\n const scan = scanQuotedRun(text, i, '\"');\n if (!scan.terminated) {\n // Could equally be a truncated value; the extractor already names\n // truncation, so do not compete with it here.\n return UNKNOWN_PARSE_FAILURE;\n }\n i = scan.end;\n expect = 'comma';\n continue;\n }\n const literal = scanRun(text, i, (c) => !VALUE_RUN_TERMINATOR.test(c));\n if (literal.length === 0) {\n return UNKNOWN_PARSE_FAILURE;\n }\n i += literal.length;\n expect = 'comma';\n continue;\n }\n\n // expect === 'comma': a complete value just closed.\n if (top === undefined) {\n return UNKNOWN_PARSE_FAILURE;\n }\n if (ch === ',') {\n i++;\n expect = top === 'object' ? 'name' : 'value';\n continue;\n }\n if ((ch === '}' && top === 'object') || (ch === ']' && top === 'array')) {\n stack.pop();\n i++;\n continue;\n }\n return UNKNOWN_PARSE_FAILURE;\n }\n}\n\n/**\n * Classifies why a JSON-shaped LLM response would not parse, returning a\n * {@link AiAssist.JsonParseFailureReason} a caller can branch on — repair the\n * cheap cases, re-prompt the expensive ones, fail outright on the rest —\n * instead of regex-matching an engine-specific `JSON.parse` message.\n *\n * Pass the same raw model text you handed\n * {@link AiAssist.fencedStringifiedJson} or {@link AiAssist.extractJsonText};\n * this applies the same BOM / whitespace / fence / preamble handling before\n * scanning, and reports `offset` against that original text.\n *\n * Classification is **structural and deliberately conservative**. The scan\n * walks the JSON grammar itself rather than reading the engine's error string,\n * so its verdicts are stable across Node versions — and any fault it cannot\n * name with confidence comes back as `'unknown'` rather than a guess. In\n * particular an input that opened a structure and never closed it (the\n * truncated-response shape {@link AiAssist.extractJsonText} already diagnoses)\n * classifies as `'unknown'` here; the two diagnostics are complementary, not\n * competing.\n *\n * This never fails and never repairs — it only names the fault. It is a\n * diagnostic on the failure path, so calling it on text that parses fine is\n * harmless but pointless: it returns `'unknown'`.\n *\n * @example\n * ```ts\n * const parsed = fencedStringifiedJson({ inner }).convert(raw);\n * if (parsed.isFailure()) {\n * const reason = classifyJsonParseFailure(raw);\n * switch (reason.kind) {\n * case 'unquoted-property-name': // cheap to repair\n * case 'single-quoted-property-name':\n * break;\n * case 'elided-member':\n * case 'unterminated-property-name': // worth a re-prompt\n * break;\n * default: // 'unknown' — fail outright\n * }\n * }\n * ```\n *\n * @param text - Raw model output (the same string handed to the extractor).\n * @returns A {@link AiAssist.JsonParseFailureReason}.\n * @public\n */\nexport function classifyJsonParseFailure(text: string): JsonParseFailureReason {\n // Defensive, and load-bearing for the documented totality: the signature says\n // `string`, but a JS consumer — or a TS caller coming through an `unknown` /\n // `any` escape hatch — can still hand us a non-string, and `locateJsonCandidate`\n // would throw on it. `extractJsonText` already guards the same way; this keeps\n // the pair consistent and keeps \"never fails\" true in practice rather than only\n // under a well-typed caller.\n if (typeof text !== 'string') {\n return UNKNOWN_PARSE_FAILURE;\n }\n const candidate = locateJsonCandidate(text);\n const scan = findBalancedJsonSubstring(candidate.text);\n if (scan.kind === 'none') {\n return UNKNOWN_PARSE_FAILURE;\n }\n return scanForParseFailure(candidate.text, scan.start, candidate.base);\n}\n\n/**\n * Options shared by every {@link AiAssist.fencedStringifiedJson} call.\n * @public\n */\nexport interface IFencedStringifiedJsonExtractorOptions {\n /**\n * Optional pre-parse extractor. Defaults to {@link AiAssist.extractJsonText}.\n * Provide a custom extractor to handle response shapes the default does not\n * understand.\n */\n readonly extractor?: JsonTextExtractor;\n}\n\n/**\n * Options for the validating overload of {@link AiAssist.fencedStringifiedJson}.\n * `inner` is required so the typed `Converter<T>` return value can never lie\n * about the runtime shape.\n * @public\n */\nexport interface IFencedStringifiedJsonOptions<T> extends IFencedStringifiedJsonExtractorOptions {\n /** Inner converter or validator applied to the parsed JSON value. */\n readonly inner: Converter<T> | Validator<T>;\n}\n\n/**\n * Creates a `Converter` that accepts raw LLM response text, runs it through a\n * tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the\n * extracted substring as JSON, and applies an optional inner converter or\n * validator.\n *\n * @example\n * ```ts\n * const converter = fencedStringifiedJson({ inner: myShapeConverter });\n * const result = converter.convert(llmText); // Result<MyShape>\n * ```\n *\n * @param options - Optional extractor; omit to keep the default. Without an\n * `inner` step, the converter resolves to the parsed `JsonValue`.\n * @returns A `Converter<JsonValue>`.\n * @public\n */\nexport function fencedStringifiedJson(options?: IFencedStringifiedJsonExtractorOptions): Converter<JsonValue>;\n/**\n * Creates a `Converter` that accepts raw LLM response text, runs it through a\n * tolerant extractor (default: {@link AiAssist.extractJsonText}), parses the\n * extracted substring as JSON, and applies the supplied inner converter or\n * validator.\n *\n * @param options - Required `inner` converter/validator and optional extractor.\n * @returns A `Converter<T>`.\n * @public\n */\nexport function fencedStringifiedJson<T>(options: IFencedStringifiedJsonOptions<T>): Converter<T>;\nexport function fencedStringifiedJson<T>(\n options?: IFencedStringifiedJsonExtractorOptions | IFencedStringifiedJsonOptions<T>\n): Converter<T | JsonValue> {\n const extractor: JsonTextExtractor = options?.extractor ?? extractJsonText;\n const inner = (options as IFencedStringifiedJsonOptions<T> | undefined)?.inner;\n const parser: Converter<T | JsonValue> =\n inner !== undefined ? JsonBaseConverters.stringifiedJson<T>(inner) : JsonBaseConverters.stringifiedJson();\n\n return new Conversion.BaseConverter<T | JsonValue>((from: unknown): Result<T | JsonValue> => {\n if (typeof from !== 'string') {\n return fail('fencedStringifiedJson: input must be a string.');\n }\n return extractor(from).onSuccess((extracted) => parser.convert(extracted));\n });\n}\n"]}