@letrquan/book 0.3.0 → 0.4.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +1024 -0
  2. package/README.md +6 -3
  3. package/dist/{additional-roots-H7UNAEHS.js → additional-roots-VAWRQ625.js} +8 -7
  4. package/dist/{app-GE4OHJOC.js → app-J7BVPSUH.js} +5397 -2953
  5. package/dist/app-J7BVPSUH.js.map +1 -0
  6. package/dist/chunk-32WBCUAN.js +660 -0
  7. package/dist/chunk-32WBCUAN.js.map +1 -0
  8. package/dist/{chunk-525CAZPY.js → chunk-4DKHLZRI.js} +6 -6
  9. package/dist/{chunk-4AS7LKKQ.js → chunk-62EEAUID.js} +2 -2
  10. package/dist/{chunk-AQEQX4FF.js → chunk-AHC7OU75.js} +385 -55
  11. package/dist/chunk-AHC7OU75.js.map +1 -0
  12. package/dist/{chunk-EFJ2NH6E.js → chunk-AJZWLKLQ.js} +63 -14
  13. package/dist/chunk-AJZWLKLQ.js.map +1 -0
  14. package/dist/chunk-B5FC3MYZ.js +395 -0
  15. package/dist/chunk-B5FC3MYZ.js.map +1 -0
  16. package/dist/{chunk-67UZEWWF.js → chunk-DNFQTSMJ.js} +53 -11
  17. package/dist/chunk-DNFQTSMJ.js.map +1 -0
  18. package/dist/{chunk-TLX3H2CL.js → chunk-E5WLQSKA.js} +231 -40
  19. package/dist/chunk-E5WLQSKA.js.map +1 -0
  20. package/dist/chunk-ECLC4CIJ.js +1181 -0
  21. package/dist/chunk-ECLC4CIJ.js.map +1 -0
  22. package/dist/chunk-EEX6QNXV.js +127 -0
  23. package/dist/chunk-EEX6QNXV.js.map +1 -0
  24. package/dist/chunk-EFXVSQ5J.js +931 -0
  25. package/dist/chunk-EFXVSQ5J.js.map +1 -0
  26. package/dist/{chunk-RFJ6Q6LE.js → chunk-ENLB7NYV.js} +577 -164
  27. package/dist/chunk-ENLB7NYV.js.map +1 -0
  28. package/dist/{chunk-IME7R2UI.js → chunk-FPR7SKIE.js} +1268 -883
  29. package/dist/chunk-FPR7SKIE.js.map +1 -0
  30. package/dist/{chunk-VPNLCMVR.js → chunk-HFB74CLM.js} +13 -5
  31. package/dist/chunk-HFB74CLM.js.map +1 -0
  32. package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
  33. package/dist/chunk-HOZXKOE3.js.map +1 -0
  34. package/dist/chunk-IDYWN37T.js +36 -0
  35. package/dist/chunk-IDYWN37T.js.map +1 -0
  36. package/dist/{chunk-YC6W4EJH.js → chunk-JGF5HLSI.js} +193 -75
  37. package/dist/chunk-JGF5HLSI.js.map +1 -0
  38. package/dist/{chunk-5YHTKWJN.js → chunk-KDNWMKXV.js} +2447 -907
  39. package/dist/chunk-KDNWMKXV.js.map +1 -0
  40. package/dist/{chunk-Y2VHN7TC.js → chunk-MEOB2DZA.js} +11 -1
  41. package/dist/chunk-MEOB2DZA.js.map +1 -0
  42. package/dist/{chunk-KBQS35OV.js → chunk-MV5PQNIA.js} +2 -2
  43. package/dist/chunk-N2G4Q7ME.js +2672 -0
  44. package/dist/chunk-N2G4Q7ME.js.map +1 -0
  45. package/dist/{chunk-62QL5QUP.js → chunk-OJBMDM5H.js} +17 -1
  46. package/dist/chunk-OJBMDM5H.js.map +1 -0
  47. package/dist/{chunk-56SIEMDX.js → chunk-PTDC7ZUP.js} +2 -2
  48. package/dist/{chunk-X4NJT436.js → chunk-REASC2QG.js} +67 -28
  49. package/dist/chunk-REASC2QG.js.map +1 -0
  50. package/dist/chunk-SBTU5BEC.js +114 -0
  51. package/dist/chunk-SBTU5BEC.js.map +1 -0
  52. package/dist/{chunk-HQI7NYGX.js → chunk-SWPDKLNM.js} +2 -2
  53. package/dist/{chunk-W3XFHEF4.js → chunk-UCFJ43OS.js} +140 -28
  54. package/dist/chunk-UCFJ43OS.js.map +1 -0
  55. package/dist/{chunk-QYBD22H5.js → chunk-W7SLYS3M.js} +80 -16
  56. package/dist/chunk-W7SLYS3M.js.map +1 -0
  57. package/dist/{chunk-SOW4WRQP.js → chunk-WV3O5PGX.js} +3 -3
  58. package/dist/{chunk-JTPOI3KU.js → chunk-XJRJVXJR.js} +262 -38
  59. package/dist/chunk-XJRJVXJR.js.map +1 -0
  60. package/dist/{chunk-224FJZLH.js → chunk-YIL6HTJX.js} +228 -5
  61. package/dist/chunk-YIL6HTJX.js.map +1 -0
  62. package/dist/{command-approvals-VBIU6BLM.js → command-approvals-ZRY66LCW.js} +5 -4
  63. package/dist/context-C3RAP6K5.js +41 -0
  64. package/dist/{hook-approvals-ZVT4V7IM.js → hook-approvals-DPQD5H65.js} +5 -4
  65. package/dist/index.js +657 -754
  66. package/dist/index.js.map +1 -1
  67. package/dist/{interactive-assets-T4I4SEHU.js → interactive-assets-73AQO5WP.js} +11 -6
  68. package/dist/{interactive-assets-T4I4SEHU.js.map → interactive-assets-73AQO5WP.js.map} +1 -1
  69. package/dist/job-runner.js +10 -3
  70. package/dist/job-runner.js.map +1 -1
  71. package/dist/job-supervisor.js +13 -4
  72. package/dist/job-supervisor.js.map +1 -1
  73. package/dist/loader-DQNPDKOX.js +27 -0
  74. package/dist/{mcp-approvals-HJ2VDYMC.js → mcp-approvals-27LJYADQ.js} +5 -4
  75. package/dist/{mcp-config-RNB5LEVE.js → mcp-config-GXSODNCM.js} +3 -2
  76. package/dist/memory-store-I6Z54VUN.js +51 -0
  77. package/dist/{model-window-store-MTSQ5JDJ.js → model-window-store-6VKXXTPJ.js} +4 -3
  78. package/dist/{openai-compatible-2OB54YST.js → openai-compatible-IPUNBJ53.js} +5 -3
  79. package/dist/{permission-approvals-YLV6UC5W.js → permission-approvals-Q5IMFEZI.js} +5 -4
  80. package/dist/{permissions-DHOKF3W2.js → permissions-7Y35X6HZ.js} +26 -9
  81. package/dist/provider-approvals-5AGH3RVT.js +76 -0
  82. package/dist/{sandbox-WRYTZH3D.js → sandbox-N2RY72GN.js} +6 -5
  83. package/dist/{scrollback-35ME2GFX.js → scrollback-GD76GNM4.js} +36 -28
  84. package/dist/scrollback-GD76GNM4.js.map +1 -0
  85. package/dist/sdk.d.ts +876 -64
  86. package/dist/sdk.js +57 -37
  87. package/dist/sdk.js.map +1 -1
  88. package/dist/settings-loader-KDBH45AQ.js +43 -0
  89. package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
  90. package/dist/shell-selection-Z5WORZU4.js +31 -0
  91. package/dist/shell-selection-Z5WORZU4.js.map +1 -0
  92. package/package.json +5 -5
  93. package/dist/app-GE4OHJOC.js.map +0 -1
  94. package/dist/chunk-224FJZLH.js.map +0 -1
  95. package/dist/chunk-5RDRAO4B.js.map +0 -1
  96. package/dist/chunk-5YHTKWJN.js.map +0 -1
  97. package/dist/chunk-62QL5QUP.js.map +0 -1
  98. package/dist/chunk-67UZEWWF.js.map +0 -1
  99. package/dist/chunk-6OQQOMZR.js +0 -785
  100. package/dist/chunk-6OQQOMZR.js.map +0 -1
  101. package/dist/chunk-AQEQX4FF.js.map +0 -1
  102. package/dist/chunk-C7ADUNPR.js +0 -273
  103. package/dist/chunk-C7ADUNPR.js.map +0 -1
  104. package/dist/chunk-EFJ2NH6E.js.map +0 -1
  105. package/dist/chunk-IME7R2UI.js.map +0 -1
  106. package/dist/chunk-J62VZYOB.js +0 -1339
  107. package/dist/chunk-J62VZYOB.js.map +0 -1
  108. package/dist/chunk-JTPOI3KU.js.map +0 -1
  109. package/dist/chunk-QYBD22H5.js.map +0 -1
  110. package/dist/chunk-RFJ6Q6LE.js.map +0 -1
  111. package/dist/chunk-S4JMID4G.js +0 -259
  112. package/dist/chunk-S4JMID4G.js.map +0 -1
  113. package/dist/chunk-TLX3H2CL.js.map +0 -1
  114. package/dist/chunk-VPNLCMVR.js.map +0 -1
  115. package/dist/chunk-W3XFHEF4.js.map +0 -1
  116. package/dist/chunk-X4NJT436.js.map +0 -1
  117. package/dist/chunk-X66I2NQO.js +0 -137
  118. package/dist/chunk-X66I2NQO.js.map +0 -1
  119. package/dist/chunk-Y2VHN7TC.js.map +0 -1
  120. package/dist/chunk-YC6W4EJH.js.map +0 -1
  121. package/dist/context-CBOBDY5Q.js +0 -38
  122. package/dist/loader-S77OVTHH.js +0 -18
  123. package/dist/memory-store-DZUT5HUH.js +0 -75
  124. package/dist/scrollback-35ME2GFX.js.map +0 -1
  125. package/dist/settings-loader-JUNPGAAM.js +0 -35
  126. package/dist/shell-selection-CDHGJ4GC.js +0 -17
  127. /package/dist/{additional-roots-H7UNAEHS.js.map → additional-roots-VAWRQ625.js.map} +0 -0
  128. /package/dist/{chunk-525CAZPY.js.map → chunk-4DKHLZRI.js.map} +0 -0
  129. /package/dist/{chunk-4AS7LKKQ.js.map → chunk-62EEAUID.js.map} +0 -0
  130. /package/dist/{chunk-KBQS35OV.js.map → chunk-MV5PQNIA.js.map} +0 -0
  131. /package/dist/{chunk-56SIEMDX.js.map → chunk-PTDC7ZUP.js.map} +0 -0
  132. /package/dist/{chunk-HQI7NYGX.js.map → chunk-SWPDKLNM.js.map} +0 -0
  133. /package/dist/{chunk-SOW4WRQP.js.map → chunk-WV3O5PGX.js.map} +0 -0
  134. /package/dist/{command-approvals-VBIU6BLM.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
  135. /package/dist/{context-CBOBDY5Q.js.map → context-C3RAP6K5.js.map} +0 -0
  136. /package/dist/{hook-approvals-ZVT4V7IM.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
  137. /package/dist/{loader-S77OVTHH.js.map → loader-DQNPDKOX.js.map} +0 -0
  138. /package/dist/{mcp-approvals-HJ2VDYMC.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
  139. /package/dist/{mcp-config-RNB5LEVE.js.map → mcp-config-GXSODNCM.js.map} +0 -0
  140. /package/dist/{memory-store-DZUT5HUH.js.map → memory-store-I6Z54VUN.js.map} +0 -0
  141. /package/dist/{model-window-store-MTSQ5JDJ.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
  142. /package/dist/{openai-compatible-2OB54YST.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
  143. /package/dist/{permission-approvals-YLV6UC5W.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
  144. /package/dist/{permissions-DHOKF3W2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
  145. /package/dist/{sandbox-WRYTZH3D.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
  146. /package/dist/{settings-loader-JUNPGAAM.js.map → sandbox-N2RY72GN.js.map} +0 -0
  147. /package/dist/{settings-removed-JWWPT4WA.js.map → settings-loader-KDBH45AQ.js.map} +0 -0
  148. /package/dist/{shell-selection-CDHGJ4GC.js.map → settings-removed-3ATWJEF3.js.map} +0 -0
@@ -0,0 +1,1181 @@
1
+ import {
2
+ settingsScopePath
3
+ } from "./chunk-FPR7SKIE.js";
4
+ import {
5
+ redactSecrets,
6
+ resolveSecret
7
+ } from "./chunk-N2G4Q7ME.js";
8
+ import {
9
+ isRedirectResponse,
10
+ refuseRedirect
11
+ } from "./chunk-AHC7OU75.js";
12
+ import {
13
+ parseEnvBoolean
14
+ } from "./chunk-UCFJ43OS.js";
15
+ import {
16
+ systemClock
17
+ } from "./chunk-RT5SATF7.js";
18
+ import {
19
+ PROVIDER_CONNECTION_KEYS,
20
+ parseSecretReference
21
+ } from "./chunk-B5FC3MYZ.js";
22
+ import {
23
+ SettingsRepository,
24
+ deleteNestedValue,
25
+ formatSettingsDiagnostics,
26
+ providerConfigSchema,
27
+ readSettingsDocument
28
+ } from "./chunk-REASC2QG.js";
29
+ import {
30
+ projectFileWriteRefusal
31
+ } from "./chunk-IDYWN37T.js";
32
+
33
+ // src/provider/model-discovery.ts
34
+ var DEFAULT_PROVIDER_BASE_URLS = {
35
+ openai: "https://api.openai.com/v1",
36
+ anthropic: "https://api.anthropic.com"
37
+ };
38
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 529]);
39
+ var MAX_ANTHROPIC_PAGES = 100;
40
+ var ModelDiscoveryError = class extends Error {
41
+ constructor(message, code) {
42
+ super(message);
43
+ this.code = code;
44
+ this.name = "ModelDiscoveryError";
45
+ }
46
+ code;
47
+ };
48
+ function normalizeProviderBaseUrl(_type, raw) {
49
+ const trimmed = raw.trim();
50
+ let url;
51
+ try {
52
+ url = new URL(trimmed);
53
+ } catch {
54
+ throw new ModelDiscoveryError("Enter a valid HTTP(S) base URL.", "request");
55
+ }
56
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
57
+ throw new ModelDiscoveryError("Base URL must use HTTP or HTTPS.", "request");
58
+ }
59
+ if (url.username || url.password) {
60
+ throw new ModelDiscoveryError("Base URL cannot contain embedded credentials.", "request");
61
+ }
62
+ url.search = "";
63
+ url.hash = "";
64
+ url.pathname = url.pathname.replace(/\/+$/, "") || "/";
65
+ return url.toString().replace(/\/$/, "");
66
+ }
67
+ function discoveryUrl(type, baseUrl, afterId) {
68
+ const base = normalizeProviderBaseUrl(type, baseUrl);
69
+ if (type === "openai") return `${base}/models`;
70
+ const url = new URL(`${base}${/\/v1$/i.test(new URL(base).pathname) ? "" : "/v1"}/models`);
71
+ url.searchParams.set("limit", "100");
72
+ if (afterId) url.searchParams.set("after_id", afterId);
73
+ return url.toString();
74
+ }
75
+ function safeStatusError(status) {
76
+ if (status === 401 || status === 403) {
77
+ return new ModelDiscoveryError(
78
+ "Model discovery was not authorized. Check the API key and endpoint permissions.",
79
+ "auth"
80
+ );
81
+ }
82
+ if (status === 404 || status === 405 || status === 501) {
83
+ return new ModelDiscoveryError(
84
+ "This endpoint does not expose a supported model-list API.",
85
+ "unsupported"
86
+ );
87
+ }
88
+ return new ModelDiscoveryError(`Model discovery request failed with HTTP ${status}.`, "request");
89
+ }
90
+ function redirectMessage({ status, target }) {
91
+ const http = status === 0 ? "" : ` (HTTP ${status})`;
92
+ const what = target === void 0 ? `answered${status === 0 ? "" : ` HTTP ${status}`} with no redirect target Book could read` : `redirected to ${target}${http}`;
93
+ return `The model-list endpoint ${what}; Book does not follow provider redirects. Check the provider's base URL.`;
94
+ }
95
+ function retryDelay(attempt, retry, retryAfter) {
96
+ const retryAfterSeconds = retryAfter === null ? NaN : Number(retryAfter);
97
+ if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0) {
98
+ return Math.min(retryAfterSeconds * 1e3, retry.maxDelayMs);
99
+ }
100
+ return Math.min(retry.baseDelayMs * 2 ** attempt, retry.maxDelayMs);
101
+ }
102
+ function wait(ms, signal) {
103
+ if (signal?.aborted) {
104
+ return Promise.reject(new ModelDiscoveryError("Model discovery was cancelled.", "aborted"));
105
+ }
106
+ return new Promise((resolve, reject) => {
107
+ const abort = () => {
108
+ clearTimeout(timer);
109
+ reject(new ModelDiscoveryError("Model discovery was cancelled.", "aborted"));
110
+ };
111
+ const timer = setTimeout(() => {
112
+ signal?.removeEventListener("abort", abort);
113
+ resolve();
114
+ }, ms);
115
+ signal?.addEventListener("abort", abort, { once: true });
116
+ });
117
+ }
118
+ async function fetchWithRetry(url, init, options) {
119
+ const fetchImpl = options.fetchImpl ?? fetch;
120
+ const clock = options.clock ?? systemClock;
121
+ const startedAt = clock.monotonicNowMs();
122
+ let lastNetworkError;
123
+ for (let attempt = 0; attempt <= options.retry.maxAttempts; attempt++) {
124
+ if (options.signal?.aborted) {
125
+ throw new ModelDiscoveryError("Model discovery was cancelled.", "aborted");
126
+ }
127
+ const controller = new AbortController();
128
+ const abortFromCaller = () => controller.abort(options.signal?.reason);
129
+ options.signal?.addEventListener("abort", abortFromCaller, { once: true });
130
+ const timeout = setTimeout(
131
+ () => controller.abort(new Error("request timeout")),
132
+ options.retry.requestTimeoutMs
133
+ );
134
+ let response;
135
+ try {
136
+ response = await fetchImpl(url, { ...init, redirect: "manual", signal: controller.signal });
137
+ } catch (error) {
138
+ if (options.signal?.aborted) {
139
+ throw new ModelDiscoveryError("Model discovery was cancelled.", "aborted");
140
+ }
141
+ lastNetworkError = error;
142
+ } finally {
143
+ clearTimeout(timeout);
144
+ options.signal?.removeEventListener("abort", abortFromCaller);
145
+ }
146
+ if (response && isRedirectResponse(response)) {
147
+ const refused = await refuseRedirect(response, url);
148
+ throw new ModelDiscoveryError(redirectMessage(refused), "request");
149
+ }
150
+ if (response && !(RETRYABLE_STATUSES.has(response.status) || response.status >= 500)) {
151
+ return response;
152
+ }
153
+ if (attempt >= options.retry.maxAttempts) {
154
+ if (response) return response;
155
+ throw new ModelDiscoveryError("Could not reach the model-list endpoint.", "network");
156
+ }
157
+ if (options.retry.totalBudgetMs > 0 && clock.monotonicNowMs() - startedAt >= options.retry.totalBudgetMs) {
158
+ if (response) return response;
159
+ throw new ModelDiscoveryError("Could not reach the model-list endpoint.", "network");
160
+ }
161
+ const delay = retryDelay(attempt, options.retry, response?.headers.get("retry-after") ?? null);
162
+ options.onRetry?.(attempt + 1, options.retry.maxAttempts, delay);
163
+ await wait(delay, options.signal);
164
+ }
165
+ throw new ModelDiscoveryError(
166
+ lastNetworkError ? "Could not reach the model-list endpoint." : "Model discovery failed.",
167
+ "network"
168
+ );
169
+ }
170
+ function parseModels(payload, type) {
171
+ if (!payload || typeof payload !== "object") {
172
+ throw new ModelDiscoveryError(
173
+ "The model-list endpoint returned invalid JSON.",
174
+ "invalid_response"
175
+ );
176
+ }
177
+ const data = payload.data;
178
+ if (!Array.isArray(data)) {
179
+ throw new ModelDiscoveryError(
180
+ "The model-list endpoint returned an unsupported response shape.",
181
+ "invalid_response"
182
+ );
183
+ }
184
+ const models = [];
185
+ for (const item of data) {
186
+ if (!item || typeof item !== "object") continue;
187
+ const record = item;
188
+ const id = typeof record.id === "string" ? record.id.trim() : "";
189
+ if (!id || /[\x00-\x1f\x7f]/.test(id)) continue;
190
+ const rawLabel = type === "anthropic" ? record.display_name : record.name ?? record.display_name ?? record.label;
191
+ const label = typeof rawLabel === "string" && rawLabel.trim() ? rawLabel.trim() : void 0;
192
+ const model = { id, label };
193
+ const limit = record.max_input_tokens;
194
+ if (type === "anthropic" && typeof limit === "number" && Number.isSafeInteger(limit) && limit > 0) {
195
+ model.reportedContextWindow = limit;
196
+ }
197
+ models.push(model);
198
+ }
199
+ return models;
200
+ }
201
+ async function discoverModels(options) {
202
+ const headers = options.type === "anthropic" ? {
203
+ "x-api-key": options.apiKey,
204
+ "anthropic-version": "2023-06-01"
205
+ } : { Authorization: `Bearer ${options.apiKey}` };
206
+ const discovered = [];
207
+ let afterId;
208
+ let finished = false;
209
+ for (let page = 0; page < MAX_ANTHROPIC_PAGES; page++) {
210
+ const response = await fetchWithRetry(
211
+ discoveryUrl(options.type, options.baseUrl, afterId),
212
+ { method: "GET", headers },
213
+ options
214
+ );
215
+ if (!response.ok) throw safeStatusError(response.status);
216
+ let payload;
217
+ try {
218
+ payload = await response.json();
219
+ } catch {
220
+ throw new ModelDiscoveryError(
221
+ "The model-list endpoint returned invalid JSON.",
222
+ "invalid_response"
223
+ );
224
+ }
225
+ discovered.push(...parseModels(payload, options.type));
226
+ if (options.type !== "anthropic") {
227
+ finished = true;
228
+ break;
229
+ }
230
+ const pageInfo = payload;
231
+ if (pageInfo.has_more !== true) {
232
+ finished = true;
233
+ break;
234
+ }
235
+ const nextId = typeof pageInfo.last_id === "string" ? pageInfo.last_id : void 0;
236
+ if (!nextId || nextId === afterId) {
237
+ throw new ModelDiscoveryError(
238
+ "The model-list endpoint returned invalid pagination metadata.",
239
+ "invalid_response"
240
+ );
241
+ }
242
+ afterId = nextId;
243
+ }
244
+ if (options.type === "anthropic" && !finished) {
245
+ throw new ModelDiscoveryError(
246
+ "The model-list endpoint exceeded the pagination limit.",
247
+ "invalid_response"
248
+ );
249
+ }
250
+ const deduped = /* @__PURE__ */ new Map();
251
+ for (const model of discovered) {
252
+ const existing = deduped.get(model.id);
253
+ if (!existing) {
254
+ deduped.set(model.id, model);
255
+ continue;
256
+ }
257
+ const label = existing.label || model.label;
258
+ const reportedContextWindow = existing.reportedContextWindow ?? model.reportedContextWindow;
259
+ const merged = { id: model.id, label };
260
+ if (reportedContextWindow !== void 0) merged.reportedContextWindow = reportedContextWindow;
261
+ deduped.set(model.id, merged);
262
+ }
263
+ const models = [...deduped.values()].sort((a, b) => a.id.localeCompare(b.id));
264
+ if (models.length === 0) {
265
+ throw new ModelDiscoveryError(
266
+ "The endpoint did not return any usable models.",
267
+ "invalid_response"
268
+ );
269
+ }
270
+ return models;
271
+ }
272
+
273
+ // src/provider-key.ts
274
+ import { existsSync } from "fs";
275
+ function validateApiKey(raw) {
276
+ if (!raw.trim()) throw new Error("API key is required.");
277
+ if (/[\r\n\x00]/.test(raw)) throw new Error("API key cannot contain line breaks.");
278
+ return raw;
279
+ }
280
+ function isPlainObject(value) {
281
+ return typeof value === "object" && value !== null && !Array.isArray(value);
282
+ }
283
+ function providerEntryOf(document, providerId) {
284
+ const registry = document.provider;
285
+ if (!isPlainObject(registry) || !Object.hasOwn(registry, providerId)) return void 0;
286
+ const entry = registry[providerId];
287
+ return isPlainObject(entry) ? entry : void 0;
288
+ }
289
+ function providerKeyTarget(workspace, providerId) {
290
+ const path = settingsScopePath("user", workspace);
291
+ const user = readSettingsDocument(path);
292
+ if (user.status === "malformed" || user.status === "non-object") {
293
+ return { kind: "unreadable", providerId, path, error: user.error };
294
+ }
295
+ const document = user.status === "valid" ? user.document : {};
296
+ if (isPlainObject(document.provider) && Object.hasOwn(document.provider, providerId)) {
297
+ const entry = document.provider[providerId];
298
+ const parsed = providerConfigSchema.safeParse(
299
+ isPlainObject(entry) ? { ...entry, apiKey: void 0 } : entry
300
+ );
301
+ if (!parsed.success) {
302
+ return {
303
+ kind: "unreadable",
304
+ providerId,
305
+ path,
306
+ error: `Provider "${providerId}" is not a valid provider entry: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(entry)"}: ${issue.message}`).join("; ")}`
307
+ };
308
+ }
309
+ return { kind: "owned", providerId, path, provider: parsed.data };
310
+ }
311
+ for (const scope of ["project", "local"]) {
312
+ const workspacePath = settingsScopePath(scope, workspace);
313
+ const layer = readSettingsDocument(workspacePath);
314
+ if (layer.status === "valid" && providerEntryOf(layer.document, providerId)) {
315
+ return { kind: "workspace-only", providerId, path, declaredIn: workspacePath };
316
+ }
317
+ }
318
+ const configured = isPlainObject(document.provider) ? Object.keys(document.provider).sort() : [];
319
+ return { kind: "unknown", providerId, path, configured };
320
+ }
321
+ function deleteProviderConnectionKeys(document, providerId) {
322
+ for (const key of PROVIDER_CONNECTION_KEYS) {
323
+ deleteNestedValue(document, ["provider", providerId, key]);
324
+ }
325
+ }
326
+ function writeProviderKey(workspace, providerId, apiKey) {
327
+ const path = settingsScopePath("user", workspace);
328
+ const localPath = settingsScopePath("local", workspace);
329
+ const localRefusal = projectFileWriteRefusal(localPath);
330
+ if (localRefusal) return { ok: false, error: `${localPath}: ${localRefusal}` };
331
+ const written = new SettingsRepository(path).update((candidate) => {
332
+ const entry = providerEntryOf(candidate, providerId);
333
+ if (!entry) throw new Error(`Provider "${providerId}" is not configured in ${path}.`);
334
+ entry.apiKey = apiKey;
335
+ });
336
+ if (!written.ok) {
337
+ return {
338
+ ok: false,
339
+ error: redactSecrets(formatSettingsDiagnostics(written.diagnostics), [apiKey])
340
+ };
341
+ }
342
+ if (!existsSync(localPath)) return { ok: true, path };
343
+ const local = readSettingsDocument(localPath);
344
+ const localEntry = local.status === "valid" ? providerEntryOf(local.document, providerId) : null;
345
+ if (!localEntry || !PROVIDER_CONNECTION_KEYS.some((key) => localEntry[key] !== void 0)) {
346
+ return { ok: true, path };
347
+ }
348
+ const cleared = new SettingsRepository(localPath).update(
349
+ (candidate) => deleteProviderConnectionKeys(candidate, providerId)
350
+ );
351
+ return cleared.ok ? { ok: true, path, localCleared: localPath } : {
352
+ ok: true,
353
+ path,
354
+ localError: redactSecrets(formatSettingsDiagnostics(cleared.diagnostics), [apiKey])
355
+ };
356
+ }
357
+ var CHECK_RETRIES = 0;
358
+ var CHECK_REQUEST_TIMEOUT_MS = 2e4;
359
+ async function checkProviderKey(options) {
360
+ const baseUrl = options.provider.baseURL ?? options.provider.baseUrl;
361
+ if (!baseUrl) {
362
+ return {
363
+ ok: false,
364
+ error: "This provider has no base URL configured, so the key cannot be tested."
365
+ };
366
+ }
367
+ const resolved = resolveSecret(options.apiKey, options.workspace);
368
+ if (!resolved) {
369
+ const reference = parseSecretReference(options.apiKey);
370
+ return {
371
+ ok: false,
372
+ error: reference.kind === "env" ? `Environment variable ${reference.name} is not set.` : reference.kind === "file" ? `Could not read a key from ${reference.path}.` : "API key is required."
373
+ };
374
+ }
375
+ const discover = options.discover ?? discoverModels;
376
+ try {
377
+ const models = await discover({
378
+ type: options.provider.type,
379
+ baseUrl,
380
+ apiKey: resolved,
381
+ retry: {
382
+ ...options.retry,
383
+ maxAttempts: Math.min(options.retry.maxAttempts, CHECK_RETRIES),
384
+ requestTimeoutMs: Math.min(options.retry.requestTimeoutMs, CHECK_REQUEST_TIMEOUT_MS)
385
+ },
386
+ signal: options.signal
387
+ });
388
+ if (models.length === 0) {
389
+ return { ok: false, error: "The endpoint did not return any models." };
390
+ }
391
+ return { ok: true, modelCount: models.length };
392
+ } catch (caught) {
393
+ const message = caught instanceof Error ? caught.message : "The key check failed.";
394
+ return { ok: false, error: redactSecrets(message, [options.apiKey, resolved]) };
395
+ }
396
+ }
397
+
398
+ // src/cli/ink-renderer.ts
399
+ import { readFileSync } from "fs";
400
+ import { createRequire } from "module";
401
+ import { dirname, join } from "path";
402
+ import { pathToFileURL } from "url";
403
+ var INK_TRAILING_NEWLINE_FIX = "ansiEscapes.cursorUp(previousLines.length - 1)";
404
+ function inkBuildDir(from = import.meta.url) {
405
+ const require2 = createRequire(from);
406
+ return dirname(require2.resolve("ink"));
407
+ }
408
+ function inkLogUpdatePath(from) {
409
+ return join(inkBuildDir(from), "log-update.js");
410
+ }
411
+ function hasInkTrailingNewlineFix(from) {
412
+ try {
413
+ return readFileSync(inkLogUpdatePath(from), "utf8").includes(INK_TRAILING_NEWLINE_FIX);
414
+ } catch {
415
+ return false;
416
+ }
417
+ }
418
+ async function loadInkFlushSync(from) {
419
+ try {
420
+ const module = await import(pathToFileURL(join(inkBuildDir(from), "reconciler.js")).href);
421
+ const reconciler = module.default;
422
+ const flush = reconciler?.flushSyncFromReconciler;
423
+ if (typeof flush !== "function") return void 0;
424
+ return (update) => {
425
+ flush.call(reconciler, update);
426
+ };
427
+ } catch {
428
+ return void 0;
429
+ }
430
+ }
431
+ async function wrapInkLogUpdate(wrap) {
432
+ try {
433
+ const module = await import(pathToFileURL(inkLogUpdatePath()).href);
434
+ const logUpdate = module.default;
435
+ logUpdate.create = wrap(logUpdate.create);
436
+ return true;
437
+ } catch {
438
+ return false;
439
+ }
440
+ }
441
+
442
+ // src/cli/ink-scroll-renderer.ts
443
+ var pendingHint = null;
444
+ var rendererInstalled = false;
445
+ function isInkScrollRendererEnabled(value) {
446
+ return value === "on";
447
+ }
448
+ function setTranscriptScrollHint(hint) {
449
+ if (!Number.isFinite(hint.delta) || hint.delta === 0) return;
450
+ if (pendingHint && pendingHint.top === hint.top && pendingHint.bottom === hint.bottom && Math.sign(pendingHint.delta) === Math.sign(hint.delta)) {
451
+ pendingHint = { ...pendingHint, delta: pendingHint.delta + hint.delta };
452
+ return;
453
+ }
454
+ pendingHint = { ...hint };
455
+ }
456
+ function splitFrame(frame) {
457
+ return frame.endsWith("\n") ? frame.slice(0, -1).split("\n") : frame.split("\n");
458
+ }
459
+ function getFrameEndRow(frame, visibleLineCount) {
460
+ return visibleLineCount + (frame.endsWith("\n") ? 1 : 0);
461
+ }
462
+ function cursorTo(row) {
463
+ return `\x1B[${Math.max(1, Math.floor(row))};1H`;
464
+ }
465
+ function setScrollRegion(top, bottom) {
466
+ return `\x1B[${Math.max(1, Math.floor(top))};${Math.max(1, Math.floor(bottom))}r`;
467
+ }
468
+ function isPureVerticalShift(previousLines, nextLines, hint) {
469
+ if (previousLines.length !== nextLines.length) return false;
470
+ const top = Math.floor(hint.top) - 1;
471
+ const bottom = Math.floor(hint.bottom);
472
+ const delta = Math.trunc(hint.delta);
473
+ if (top < 0 || bottom > nextLines.length || bottom - top < 2) return false;
474
+ if (Math.abs(delta) >= bottom - top) return false;
475
+ for (let row = 0; row < top; row++) {
476
+ if (previousLines[row] !== nextLines[row]) return false;
477
+ }
478
+ for (let row = bottom; row < nextLines.length; row++) {
479
+ if (previousLines[row] !== nextLines[row]) return false;
480
+ }
481
+ if (delta > 0) {
482
+ for (let row = top; row < bottom - delta; row++) {
483
+ if (nextLines[row] !== previousLines[row + delta]) return false;
484
+ }
485
+ } else {
486
+ const distance = Math.abs(delta);
487
+ for (let row = top + distance; row < bottom; row++) {
488
+ if (nextLines[row] !== previousLines[row - distance]) return false;
489
+ }
490
+ }
491
+ return true;
492
+ }
493
+ function writeExposedRows(stream, lines, top, bottom, delta) {
494
+ const distance = Math.abs(delta);
495
+ const first = delta > 0 ? bottom - distance : top;
496
+ for (let offset = 0; offset < distance; offset++) {
497
+ const row = first + offset;
498
+ stream.write(cursorTo(row + 1) + (lines[row] ?? "") + "\x1B[K");
499
+ }
500
+ }
501
+ function createScrollAwareRenderer(stream, options, createBase) {
502
+ const base = createBase(stream, options);
503
+ let previousFrame = "";
504
+ const render = ((output) => {
505
+ const hint = pendingHint;
506
+ pendingHint = null;
507
+ const previousLines = splitFrame(previousFrame);
508
+ const nextLines = splitFrame(output);
509
+ const terminalRows = stream.rows ?? 0;
510
+ const frameEndRow = getFrameEndRow(output, nextLines.length);
511
+ const canScroll = Boolean(stream.isTTY) && terminalRows > 0 && frameEndRow <= terminalRows && previousFrame.length > 0 && hint !== null && hint.bottom <= nextLines.length && isPureVerticalShift(previousLines, nextLines, hint);
512
+ if (canScroll && hint) {
513
+ const top = Math.floor(hint.top);
514
+ const bottom = Math.floor(hint.bottom);
515
+ const delta = Math.trunc(hint.delta);
516
+ stream.write("\x1B[?25l" + setScrollRegion(top, bottom));
517
+ stream.write(`\x1B[${Math.abs(delta)}${delta > 0 ? "S" : "T"}`);
518
+ writeExposedRows(stream, nextLines, top - 1, bottom, delta);
519
+ stream.write(setScrollRegion(1, terminalRows) + cursorTo(frameEndRow));
520
+ previousFrame = output;
521
+ base.sync(output);
522
+ return true;
523
+ }
524
+ previousFrame = output;
525
+ return base(output);
526
+ });
527
+ render.clear = () => {
528
+ pendingHint = null;
529
+ previousFrame = "";
530
+ base.clear();
531
+ };
532
+ render.done = () => {
533
+ pendingHint = null;
534
+ previousFrame = "";
535
+ base.done();
536
+ };
537
+ render.reset = () => {
538
+ pendingHint = null;
539
+ previousFrame = "";
540
+ base.reset?.();
541
+ };
542
+ render.sync = (output) => {
543
+ pendingHint = null;
544
+ previousFrame = output;
545
+ base.sync(output);
546
+ };
547
+ render.setCursorPosition = (position) => base.setCursorPosition(position);
548
+ render.isCursorDirty = () => base.isCursorDirty();
549
+ render.willRender = (output) => {
550
+ const shouldRender = base.willRender(output);
551
+ if (!shouldRender) pendingHint = null;
552
+ return shouldRender;
553
+ };
554
+ return render;
555
+ }
556
+ async function installInkScrollRenderer(enabled) {
557
+ if (rendererInstalled) return;
558
+ if (!(enabled ?? isInkScrollRendererEnabled(process.env.BOOK_SCROLL_RENDERER))) return;
559
+ rendererInstalled = true;
560
+ await wrapInkLogUpdate(
561
+ (createBase) => (stream, options) => createScrollAwareRenderer(stream, options, createBase)
562
+ );
563
+ }
564
+
565
+ // src/cli/frame-buffer.ts
566
+ var latestFrame = "";
567
+ var lastCursorPosition = null;
568
+ var captureInstalled = false;
569
+ var frameUpdateListeners = /* @__PURE__ */ new Set();
570
+ function notifyFrameUpdate() {
571
+ for (const listener of frameUpdateListeners) {
572
+ try {
573
+ listener();
574
+ } catch {
575
+ }
576
+ }
577
+ }
578
+ function splitFrame2(frame) {
579
+ return frame.endsWith("\n") ? frame.slice(0, -1).split("\n") : frame.split("\n");
580
+ }
581
+ function toCursorPosition(position) {
582
+ if (!position || typeof position !== "object") return null;
583
+ const candidate = position;
584
+ if (typeof candidate.x !== "number" || typeof candidate.y !== "number") return null;
585
+ return { x: candidate.x, y: candidate.y };
586
+ }
587
+ function getFrameLines() {
588
+ return latestFrame.length === 0 ? [] : splitFrame2(latestFrame);
589
+ }
590
+ function getLastCursorPosition() {
591
+ return lastCursorPosition;
592
+ }
593
+ function subscribeToFrameUpdates(listener) {
594
+ frameUpdateListeners.add(listener);
595
+ return () => frameUpdateListeners.delete(listener);
596
+ }
597
+ function createFrameCapturingRenderer(stream, options, createBase) {
598
+ const base = createBase(stream, options);
599
+ const render = ((output) => {
600
+ latestFrame = output;
601
+ const rendered = base(output);
602
+ if (rendered !== false) notifyFrameUpdate();
603
+ return rendered;
604
+ });
605
+ render.clear = () => {
606
+ latestFrame = "";
607
+ base.clear();
608
+ };
609
+ render.done = () => {
610
+ latestFrame = "";
611
+ lastCursorPosition = null;
612
+ base.done();
613
+ };
614
+ render.reset = () => {
615
+ latestFrame = "";
616
+ base.reset?.();
617
+ };
618
+ render.sync = (output) => {
619
+ latestFrame = output;
620
+ base.sync(output);
621
+ notifyFrameUpdate();
622
+ };
623
+ render.setCursorPosition = (position) => {
624
+ lastCursorPosition = toCursorPosition(position);
625
+ base.setCursorPosition(position);
626
+ };
627
+ render.isCursorDirty = () => base.isCursorDirty();
628
+ render.willRender = (output) => base.willRender(output);
629
+ return render;
630
+ }
631
+ async function installFrameCapture() {
632
+ if (captureInstalled) return;
633
+ captureInstalled = true;
634
+ await wrapInkLogUpdate(
635
+ (createBase) => (stream, options) => createFrameCapturingRenderer(stream, options, createBase)
636
+ );
637
+ }
638
+
639
+ // src/cli/ink-output-cache.ts
640
+ import { createRequire as createRequire2 } from "module";
641
+ import { join as join2 } from "path";
642
+ import { pathToFileURL as pathToFileURL2 } from "url";
643
+ var DEFAULT_LIMITS = {
644
+ styledChars: 1024,
645
+ widths: 8192,
646
+ blockWidths: 1024,
647
+ sliceCache: 2048,
648
+ rowCache: 2048
649
+ };
650
+ function evictOldest(cache) {
651
+ const oldest = cache.keys().next().value;
652
+ if (oldest !== void 0) cache.delete(oldest);
653
+ }
654
+ function refresh(cache, key) {
655
+ const value = cache.get(key);
656
+ if (value === void 0) return void 0;
657
+ cache.delete(key);
658
+ cache.set(key, value);
659
+ return value;
660
+ }
661
+ function remember(cache, key, value, limit) {
662
+ if (cache.size >= limit) evictOldest(cache);
663
+ cache.set(key, value);
664
+ }
665
+ function trim(cache, limit) {
666
+ while (cache.size > limit) evictOldest(cache);
667
+ }
668
+ function createRowCachedGet(options) {
669
+ const { originalGet, sliceAnsi, Output } = options;
670
+ const limits = { ...DEFAULT_LIMITS, ...options.limits };
671
+ const rowCache = /* @__PURE__ */ new Map();
672
+ const sliceCache = /* @__PURE__ */ new Map();
673
+ const counters = { frames: 0, rows: 0, rowHits: 0, fallbacks: 0 };
674
+ let sharedCaches = null;
675
+ const get = function() {
676
+ if (sharedCaches === null) sharedCaches = this.caches;
677
+ const caches = sharedCaches;
678
+ this.caches = caches;
679
+ trim(caches.styledChars, limits.styledChars);
680
+ trim(caches.widths, limits.widths);
681
+ trim(caches.blockWidths, limits.blockWidths);
682
+ const rows = [];
683
+ for (let y = 0; y < this.height; y++) rows.push([]);
684
+ const clips = [];
685
+ for (const operation of this.operations) {
686
+ if (operation.type === "clip") clips.push(operation.clip);
687
+ if (operation.type === "unclip") clips.pop();
688
+ if (operation.type !== "write") continue;
689
+ const { text, transformers } = operation;
690
+ let { x, y } = operation;
691
+ let lines = text.split("\n");
692
+ const clip = clips.at(-1);
693
+ if (clip) {
694
+ const { x1, x2, y1, y2 } = clip;
695
+ const clipHorizontally = typeof x1 === "number" && typeof x2 === "number";
696
+ const clipVertically = typeof y1 === "number" && typeof y2 === "number";
697
+ if (clipHorizontally) {
698
+ if (x + caches.getWidestLine(text) < x1 || x > x2) continue;
699
+ }
700
+ if (clipVertically) {
701
+ if (y + lines.length < y1 || y > y2) continue;
702
+ }
703
+ if (clipHorizontally) {
704
+ lines = lines.map((line) => {
705
+ const from = x < x1 ? x1 - x : 0;
706
+ const width = caches.getStringWidth(line);
707
+ const to = x + width > x2 ? x2 - x : width;
708
+ const key = `${from},${to},${line.length},${line}`;
709
+ const cached = refresh(sliceCache, key);
710
+ if (cached !== void 0) return cached;
711
+ const sliced = sliceAnsi(line, from, to);
712
+ remember(sliceCache, key, sliced, limits.sliceCache);
713
+ return sliced;
714
+ });
715
+ if (x < x1) x = x1;
716
+ }
717
+ if (clipVertically) {
718
+ const from = y < y1 ? y1 - y : 0;
719
+ const to = y + lines.length > y2 ? y2 - y : lines.length;
720
+ lines = lines.slice(from, to);
721
+ if (y < y1) y = y1;
722
+ }
723
+ }
724
+ let offsetY = 0;
725
+ for (const [index, original] of lines.entries()) {
726
+ if (!rows[y + offsetY]) continue;
727
+ let line = original;
728
+ for (const transformer of transformers) line = transformer(line, index);
729
+ if (line.includes("\n")) {
730
+ counters.fallbacks++;
731
+ counters.frames++;
732
+ return originalGet.call(this);
733
+ }
734
+ rows[y + offsetY].push([x, line]);
735
+ offsetY++;
736
+ }
737
+ }
738
+ const frame = new Array(rows.length);
739
+ const misses = [];
740
+ const missKeys = [];
741
+ for (let row = 0; row < rows.length; row++) {
742
+ const fragments = rows[row];
743
+ counters.rows++;
744
+ if (fragments.length === 0) {
745
+ frame[row] = "";
746
+ counters.rowHits++;
747
+ continue;
748
+ }
749
+ let key = `${this.width}|`;
750
+ for (const [x, line] of fragments) key += `${x},${line.length},${line}`;
751
+ const cached = refresh(rowCache, key);
752
+ if (cached === void 0) {
753
+ misses.push(row);
754
+ missKeys.push(key);
755
+ } else {
756
+ frame[row] = cached;
757
+ counters.rowHits++;
758
+ }
759
+ }
760
+ if (misses.length > 0) {
761
+ const scratch = new Output({ width: this.width, height: misses.length });
762
+ scratch.caches = caches;
763
+ misses.forEach((row, index) => {
764
+ for (const [x, line] of rows[row] ?? []) {
765
+ scratch.operations.push({ type: "write", x, y: index, text: line, transformers: [] });
766
+ }
767
+ });
768
+ const built = originalGet.call(scratch).output.split("\n");
769
+ misses.forEach((row, index) => {
770
+ const text = built[index] ?? "";
771
+ frame[row] = text;
772
+ remember(rowCache, missKeys[index], text, limits.rowCache);
773
+ });
774
+ }
775
+ counters.frames++;
776
+ return { output: frame.join("\n"), height: this.height };
777
+ };
778
+ Object.defineProperty(get, INSTALLED_MARK, { value: true });
779
+ return {
780
+ get,
781
+ stats: () => ({ ...counters, rowCacheSize: rowCache.size, sliceCacheSize: sliceCache.size })
782
+ };
783
+ }
784
+ function isInkOutputShape(candidate) {
785
+ if (typeof candidate !== "function") return false;
786
+ const Output = candidate;
787
+ const prototype = Output.prototype;
788
+ if (typeof prototype !== "object" || prototype === null) return false;
789
+ if (typeof prototype.get !== "function") return false;
790
+ let instance;
791
+ try {
792
+ instance = new Output({ width: 1, height: 1 });
793
+ } catch {
794
+ return false;
795
+ }
796
+ if (!Array.isArray(instance.operations)) return false;
797
+ const caches = instance.caches;
798
+ if (typeof caches !== "object" || caches === null) return false;
799
+ const shaped = caches;
800
+ return typeof shaped.getStyledChars === "function" && typeof shaped.getStringWidth === "function" && typeof shaped.getWidestLine === "function" && shaped.styledChars instanceof Map && shaped.widths instanceof Map && shaped.blockWidths instanceof Map;
801
+ }
802
+ var INSTALLED_MARK = /* @__PURE__ */ Symbol("book.rowCachedInkGet");
803
+ function isRowCachedGet(candidate) {
804
+ if (typeof candidate !== "function") return false;
805
+ const prototype = candidate.prototype;
806
+ const get = prototype?.get;
807
+ return get?.[INSTALLED_MARK] === true;
808
+ }
809
+ async function installInkOutputCache(env = process.env) {
810
+ try {
811
+ const outputPath = join2(inkBuildDir(), "output.js");
812
+ const outputModule = await import(pathToFileURL2(outputPath).href);
813
+ if (isRowCachedGet(outputModule.default)) return true;
814
+ if (parseEnvBoolean(env.BOOK_INK_OUTPUT_CACHE) === false) return false;
815
+ const sliceAnsiPath = createRequire2(outputPath).resolve("slice-ansi");
816
+ const sliceAnsi = (await import(pathToFileURL2(sliceAnsiPath).href)).default;
817
+ const Output = outputModule.default;
818
+ if (!isInkOutputShape(Output) || typeof sliceAnsi !== "function") return false;
819
+ const originalGet = Output.prototype.get;
820
+ Output.prototype.get = createRowCachedGet({
821
+ originalGet,
822
+ sliceAnsi,
823
+ Output
824
+ }).get;
825
+ return true;
826
+ } catch {
827
+ return false;
828
+ }
829
+ }
830
+
831
+ // src/cli/ink-render-cull.ts
832
+ import { createRequire as createRequire3 } from "module";
833
+ import { join as join3 } from "path";
834
+ import { pathToFileURL as pathToFileURL3 } from "url";
835
+ var INSTALLED_MARK2 = /* @__PURE__ */ Symbol("book.inkRenderCull");
836
+ var REAL_DISPLAY = /* @__PURE__ */ Symbol("book.inkRenderCullRealDisplay");
837
+ function isCullableYogaNode(candidate) {
838
+ if (typeof candidate !== "object" || candidate === null) return false;
839
+ const node = candidate;
840
+ return typeof node.getDisplay === "function" && typeof node.getComputedTop === "function" && typeof node.getComputedHeight === "function";
841
+ }
842
+ function createInkRenderCull(options) {
843
+ const { clip: originalClip, unclip: originalUnclip, displayNone } = options;
844
+ const cullMark = /* @__PURE__ */ Symbol("book.inkRenderCullDisplay");
845
+ const clips = /* @__PURE__ */ new WeakMap();
846
+ let activeClipStack = null;
847
+ let frame = 0;
848
+ const absoluteTops = /* @__PURE__ */ new WeakMap();
849
+ const scannedChildren = /* @__PURE__ */ new WeakMap();
850
+ const nodeRecords = /* @__PURE__ */ new WeakMap();
851
+ const clip = function(pushed) {
852
+ let stack = clips.get(this);
853
+ if (stack === void 0) {
854
+ frame++;
855
+ stack = [];
856
+ clips.set(this, stack);
857
+ }
858
+ activeClipStack = stack;
859
+ stack.push(pushed);
860
+ originalClip.call(this, pushed);
861
+ };
862
+ Object.defineProperty(clip, INSTALLED_MARK2, { value: true });
863
+ const unclip = function() {
864
+ clips.get(this)?.pop();
865
+ originalUnclip.call(this);
866
+ };
867
+ const getAbsoluteTop = (node) => {
868
+ const cached = absoluteTops.get(node);
869
+ if (cached !== void 0 && cached.frame === frame) {
870
+ return cached.top;
871
+ }
872
+ const nodeTop = node.yogaNode?.getComputedTop();
873
+ if (typeof nodeTop !== "number") {
874
+ absoluteTops.set(node, { frame, top: void 0 });
875
+ return void 0;
876
+ }
877
+ let parentTop = 0;
878
+ if (node.parentNode != null) {
879
+ const parentAbsoluteTop = getAbsoluteTop(node.parentNode);
880
+ if (parentAbsoluteTop === void 0) {
881
+ absoluteTops.set(node, { frame, top: void 0 });
882
+ return void 0;
883
+ }
884
+ parentTop = parentAbsoluteTop;
885
+ }
886
+ const totalTop = parentTop + nodeTop;
887
+ absoluteTops.set(node, { frame, top: totalTop });
888
+ return totalTop;
889
+ };
890
+ function sharedGetDisplay() {
891
+ const record = nodeRecords.get(this);
892
+ if (record === void 0) {
893
+ const stored = this[REAL_DISPLAY];
894
+ if (typeof stored === "function") return stored.call(this);
895
+ const proto = Object.getPrototypeOf(this);
896
+ if (typeof proto?.getDisplay === "function") return proto.getDisplay.call(this);
897
+ return displayNone;
898
+ }
899
+ const { element, realDisplay } = record;
900
+ const display = realDisplay.call(this);
901
+ if (display === displayNone) return display;
902
+ if (activeClipStack === null || activeClipStack.length === 0) return display;
903
+ const innermost = activeClipStack.at(-1);
904
+ if (innermost === void 0) return display;
905
+ const { y1, y2 } = innermost;
906
+ if (typeof y1 !== "number" || typeof y2 !== "number") return display;
907
+ const top = getAbsoluteTop(element);
908
+ if (top === void 0) return display;
909
+ const bottom = top + this.getComputedHeight();
910
+ const style = element.style;
911
+ const clipsSelf = style !== void 0 && (style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden");
912
+ const straddles = top < y1 && bottom > y1 || top < y2 && bottom > y2;
913
+ if (straddles) {
914
+ const currentChildren = element.childNodes;
915
+ let currentLength = 0;
916
+ let firstChild;
917
+ let lastChild;
918
+ if (Array.isArray(currentChildren)) {
919
+ currentLength = currentChildren.length;
920
+ if (currentLength > 0) {
921
+ firstChild = currentChildren[0];
922
+ lastChild = currentChildren[currentLength - 1];
923
+ }
924
+ } else if (currentChildren && typeof currentChildren[Symbol.iterator] === "function") {
925
+ const items = Array.from(currentChildren);
926
+ currentLength = items.length;
927
+ if (currentLength > 0) {
928
+ firstChild = items[0];
929
+ lastChild = items[currentLength - 1];
930
+ }
931
+ }
932
+ const lastScanned = scannedChildren.get(element);
933
+ if (lastScanned === void 0 || lastScanned.length !== currentLength || lastScanned.firstChild !== firstChild || lastScanned.lastChild !== lastChild) {
934
+ scannedChildren.set(element, {
935
+ length: currentLength,
936
+ firstChild,
937
+ lastChild
938
+ });
939
+ if (currentChildren && typeof currentChildren[Symbol.iterator] === "function") {
940
+ for (const child of currentChildren) {
941
+ cull.cullWhenOffscreen(child);
942
+ }
943
+ }
944
+ }
945
+ }
946
+ if (clipsSelf) return display;
947
+ if (bottom <= y1 || top >= y2) return displayNone;
948
+ return display;
949
+ }
950
+ const cull = {
951
+ clip,
952
+ unclip,
953
+ cullWhenOffscreen: (element) => {
954
+ if (element === null || typeof element !== "object") return;
955
+ const yogaNode = element.yogaNode;
956
+ if (!isCullableYogaNode(yogaNode)) return;
957
+ const marked = yogaNode;
958
+ if (marked[cullMark] === true) return;
959
+ let realDisplay = marked[REAL_DISPLAY];
960
+ if (typeof realDisplay !== "function") {
961
+ realDisplay = yogaNode.getDisplay;
962
+ Object.defineProperty(yogaNode, REAL_DISPLAY, { value: realDisplay, configurable: true });
963
+ }
964
+ nodeRecords.set(yogaNode, { element, realDisplay });
965
+ Object.defineProperty(yogaNode, "getDisplay", {
966
+ value: sharedGetDisplay,
967
+ configurable: true
968
+ });
969
+ Object.defineProperty(yogaNode, cullMark, { value: true, configurable: true });
970
+ }
971
+ };
972
+ return cull;
973
+ }
974
+ function isRenderCullInstalled(candidate) {
975
+ if (typeof candidate !== "function") return false;
976
+ const prototype = candidate.prototype;
977
+ const clip = prototype?.clip;
978
+ return typeof clip === "function" && clip[INSTALLED_MARK2] === true;
979
+ }
980
+ function isInkRenderCullShape(candidate) {
981
+ if (typeof candidate !== "function") return false;
982
+ const prototype = candidate.prototype;
983
+ if (typeof prototype !== "object" || prototype === null) return false;
984
+ const { clip, unclip, get } = prototype;
985
+ return typeof clip === "function" && typeof unclip === "function" && typeof get === "function";
986
+ }
987
+ var activeCull = null;
988
+ function cullWhenOffscreen(element) {
989
+ activeCull?.(element);
990
+ }
991
+ async function installInkRenderCull(env = process.env) {
992
+ try {
993
+ const outputPath = join3(inkBuildDir(), "output.js");
994
+ const outputModule = await import(pathToFileURL3(outputPath).href);
995
+ if (isRenderCullInstalled(outputModule.default)) return true;
996
+ if (parseEnvBoolean(env.BOOK_INK_RENDER_CULL) === false) return false;
997
+ const yogaPath = createRequire3(outputPath).resolve("yoga-layout");
998
+ const yoga = await import(pathToFileURL3(yogaPath).href);
999
+ const displayNone = yoga.default?.DISPLAY_NONE;
1000
+ if (typeof displayNone !== "number") return false;
1001
+ if (!isInkRenderCullShape(outputModule.default)) return false;
1002
+ const { prototype } = outputModule.default;
1003
+ const cull = createInkRenderCull({
1004
+ clip: prototype.clip,
1005
+ unclip: prototype.unclip,
1006
+ displayNone
1007
+ });
1008
+ prototype.clip = cull.clip;
1009
+ prototype.unclip = cull.unclip;
1010
+ activeCull = cull.cullWhenOffscreen;
1011
+ return true;
1012
+ } catch {
1013
+ return false;
1014
+ }
1015
+ }
1016
+
1017
+ // src/cli/ink-frame-clip.ts
1018
+ import { createRequire as createRequire4 } from "module";
1019
+ import { join as join4 } from "path";
1020
+ import { pathToFileURL as pathToFileURL4 } from "url";
1021
+ var INSTALLED_MARK3 = /* @__PURE__ */ Symbol("book.inkFrameClip");
1022
+ var TAB_STOP = 8;
1023
+ function expandTabs(row, measure) {
1024
+ const parts = row.split(" ");
1025
+ let expanded = parts[0] ?? "";
1026
+ let column = measure(expanded);
1027
+ for (let index = 1; index < parts.length; index++) {
1028
+ const part = parts[index] ?? "";
1029
+ const pad = TAB_STOP - column % TAB_STOP;
1030
+ expanded += " ".repeat(pad) + part;
1031
+ column += pad + measure(part);
1032
+ }
1033
+ return expanded;
1034
+ }
1035
+ function clipsWithinWidth(clip, width) {
1036
+ return typeof clip?.x1 === "number" && typeof clip.x2 === "number" && clip.x2 <= width;
1037
+ }
1038
+ function rowsPastWidth(output) {
1039
+ let rows = null;
1040
+ const clips = [];
1041
+ for (const operation of output.operations) {
1042
+ if (operation.type === "clip") {
1043
+ clips.push(operation.clip);
1044
+ continue;
1045
+ }
1046
+ if (operation.type === "unclip") {
1047
+ clips.pop();
1048
+ continue;
1049
+ }
1050
+ const { x, y, text } = operation;
1051
+ if (!text.includes(" ")) {
1052
+ if (clipsWithinWidth(clips.at(-1), output.width)) continue;
1053
+ if (x + 2 * text.length <= output.width) continue;
1054
+ if (x + output.caches.getWidestLine(text) <= output.width) continue;
1055
+ }
1056
+ rows ??= new Array(output.height).fill(false);
1057
+ let lines = 1;
1058
+ for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) {
1059
+ lines++;
1060
+ }
1061
+ const last = Math.min(y + lines, output.height);
1062
+ for (let row = Math.max(y, 0); row < last; row++) rows[row] = true;
1063
+ }
1064
+ return rows;
1065
+ }
1066
+ function createFrameClippedGet(options) {
1067
+ const { get, sliceAnsi, stringWidth } = options;
1068
+ const clipped = function() {
1069
+ const frame = get.call(this);
1070
+ const candidates = rowsPastWidth(this);
1071
+ if (candidates === null) return frame;
1072
+ const caches = this.caches;
1073
+ const measure = typeof caches?.getStringWidth === "function" ? caches.getStringWidth.bind(caches) : stringWidth;
1074
+ const width = this.width;
1075
+ const rows = frame.output.split("\n");
1076
+ const checkAll = rows.length !== frame.height;
1077
+ let changed = false;
1078
+ for (let row = 0; row < rows.length; row++) {
1079
+ if (!checkAll && !candidates[row]) continue;
1080
+ const original = rows[row] ?? "";
1081
+ let text = original.includes(" ") ? expandTabs(original, measure) : original;
1082
+ if (measure(text) > width) text = sliceAnsi(text, 0, width).trimEnd();
1083
+ if (text === original) continue;
1084
+ rows[row] = text;
1085
+ changed = true;
1086
+ }
1087
+ return changed ? { output: rows.join("\n"), height: frame.height } : frame;
1088
+ };
1089
+ for (const symbol of Object.getOwnPropertySymbols(get)) {
1090
+ const descriptor = Object.getOwnPropertyDescriptor(get, symbol);
1091
+ if (descriptor) Object.defineProperty(clipped, symbol, descriptor);
1092
+ }
1093
+ Object.defineProperty(clipped, INSTALLED_MARK3, { value: true });
1094
+ return clipped;
1095
+ }
1096
+ function isFrameClippedGet(Output) {
1097
+ if (typeof Output !== "function") return false;
1098
+ const prototype = Output.prototype;
1099
+ const get = prototype?.get;
1100
+ return get?.[INSTALLED_MARK3] === true;
1101
+ }
1102
+ function isClippableOutput(Output) {
1103
+ if (!isInkOutputShape(Output)) return false;
1104
+ try {
1105
+ return new Output({ width: 3, height: 1 }).width === 3;
1106
+ } catch {
1107
+ return false;
1108
+ }
1109
+ }
1110
+ async function installInkFrameClip(env = process.env) {
1111
+ try {
1112
+ const outputPath = join4(inkBuildDir(), "output.js");
1113
+ const outputModule = await import(pathToFileURL4(outputPath).href);
1114
+ const Output = outputModule.default;
1115
+ if (isFrameClippedGet(Output)) return true;
1116
+ if (parseEnvBoolean(env.BOOK_INK_FRAME_CLIP) === false) return false;
1117
+ const require2 = createRequire4(outputPath);
1118
+ const [sliceAnsiModule, stringWidthModule] = await Promise.all([
1119
+ import(pathToFileURL4(require2.resolve("slice-ansi")).href),
1120
+ import(pathToFileURL4(require2.resolve("string-width")).href)
1121
+ ]);
1122
+ const sliceAnsi = sliceAnsiModule.default;
1123
+ const stringWidth = stringWidthModule.default;
1124
+ if (!isClippableOutput(Output)) return false;
1125
+ if (typeof sliceAnsi !== "function" || typeof stringWidth !== "function") return false;
1126
+ Output.prototype.get = createFrameClippedGet({
1127
+ get: Output.prototype.get,
1128
+ sliceAnsi,
1129
+ stringWidth
1130
+ });
1131
+ return true;
1132
+ } catch {
1133
+ return false;
1134
+ }
1135
+ }
1136
+
1137
+ // src/cli/tui-renderer-mode.ts
1138
+ function resolveTuiRendererMode(value = process.env.BOOK_TUI_RENDERER, context = {}) {
1139
+ if (context.isTTY === false || context.screenReader) return "safe";
1140
+ if (context.incrementalRendererFixed === false) return "safe";
1141
+ if (value === "incremental") return "incremental";
1142
+ if (value === "experimental-scroll") return "experimental-scroll";
1143
+ if (value === void 0) return context.platform === "win32" ? "safe" : "incremental";
1144
+ return "safe";
1145
+ }
1146
+ function isCiEnvironment(env = process.env) {
1147
+ const set = (key) => key in env && env[key] !== "0" && env[key] !== "false";
1148
+ return set("CI") || set("CONTINUOUS_INTEGRATION");
1149
+ }
1150
+ function resolveInkMaxFps(platform = process.platform) {
1151
+ return platform === "win32" ? 72 : 60;
1152
+ }
1153
+
1154
+ export {
1155
+ DEFAULT_PROVIDER_BASE_URLS,
1156
+ normalizeProviderBaseUrl,
1157
+ discoverModels,
1158
+ validateApiKey,
1159
+ providerKeyTarget,
1160
+ deleteProviderConnectionKeys,
1161
+ writeProviderKey,
1162
+ checkProviderKey,
1163
+ inkBuildDir,
1164
+ hasInkTrailingNewlineFix,
1165
+ loadInkFlushSync,
1166
+ setTranscriptScrollHint,
1167
+ installInkScrollRenderer,
1168
+ getFrameLines,
1169
+ getLastCursorPosition,
1170
+ subscribeToFrameUpdates,
1171
+ installFrameCapture,
1172
+ installInkOutputCache,
1173
+ cullWhenOffscreen,
1174
+ installInkRenderCull,
1175
+ expandTabs,
1176
+ installInkFrameClip,
1177
+ resolveTuiRendererMode,
1178
+ isCiEnvironment,
1179
+ resolveInkMaxFps
1180
+ };
1181
+ //# sourceMappingURL=chunk-ECLC4CIJ.js.map