jevprune 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2288 @@
1
+ // src/core/errors.ts
2
+ var JevCoreError = class extends Error {
3
+ name = "JevCoreError";
4
+ constructor(message, options) {
5
+ super(message, options);
6
+ }
7
+ };
8
+ var JevConfigError = class extends JevCoreError {
9
+ name = "JevConfigError";
10
+ };
11
+ var JevInputError = class extends JevCoreError {
12
+ name = "JevInputError";
13
+ };
14
+ var JevBudgetError = class extends JevCoreError {
15
+ name = "JevBudgetError";
16
+ };
17
+ var JevRequestError = class extends JevCoreError {
18
+ name = "JevRequestError";
19
+ status;
20
+ retryable;
21
+ requestId;
22
+ constructor(message, details) {
23
+ super(message, { cause: details.cause });
24
+ this.status = details.status;
25
+ this.retryable = details.retryable;
26
+ this.requestId = details.requestId;
27
+ }
28
+ };
29
+ var JevTimeoutError = class extends JevCoreError {
30
+ name = "JevTimeoutError";
31
+ timeoutMs;
32
+ constructor(timeoutMs, message, options) {
33
+ super(message ?? `Jev request exceeded ${String(timeoutMs)} ms`, options);
34
+ this.timeoutMs = timeoutMs;
35
+ }
36
+ };
37
+ var JevAbortError = class extends JevCoreError {
38
+ name = "JevAbortError";
39
+ constructor(message = "Jev request aborted", options) {
40
+ super(message, options);
41
+ }
42
+ };
43
+ var JevResponseError = class extends JevCoreError {
44
+ name = "JevResponseError";
45
+ };
46
+ function describeError(error) {
47
+ if (error instanceof Error) return error.message;
48
+ if (typeof error === "string") return error;
49
+ return String(error);
50
+ }
51
+
52
+ // node_modules/.pnpm/@typesafe-ai+sdk@0.6.0/node_modules/@typesafe-ai/sdk/dist/index.mjs
53
+ var requestIdFrom = (headers) => headers.get("x-typesafe-request-id") ?? void 0;
54
+ var APIPromise = class APIPromise2 extends Promise {
55
+ #responsePromise;
56
+ #parseResponse;
57
+ #parsed;
58
+ constructor(responsePromise, parseResponse) {
59
+ super((resolve2) => resolve2(void 0));
60
+ this.#responsePromise = responsePromise;
61
+ this.#parseResponse = parseResponse;
62
+ }
63
+ /**
64
+ * Resolves to the raw `Response` without parsing the body. SDK requests buffer the full
65
+ * body under the request timeout before handoff; reading it afterwards is caller-owned.
66
+ * The caller owns the body; don't also `await` the parsed result on the same promise.
67
+ */
68
+ asResponse() {
69
+ return this.#responsePromise;
70
+ }
71
+ /** Return the parsed result, HTTP response, and request ID. */
72
+ async withResponse() {
73
+ const [data, response] = await Promise.all([this.#parse(), this.#responsePromise]);
74
+ return {
75
+ data,
76
+ response,
77
+ requestId: requestIdFrom(response.headers)
78
+ };
79
+ }
80
+ /** Transform the parsed result, sharing the HTTP response and a single body parse. */
81
+ map(fn) {
82
+ return new APIPromise2(this.#responsePromise, () => this.#parse().then(fn));
83
+ }
84
+ #parse() {
85
+ this.#parsed ??= this.#responsePromise.then(this.#parseResponse);
86
+ return this.#parsed;
87
+ }
88
+ then(onfulfilled, onrejected) {
89
+ return this.#parse().then(onfulfilled, onrejected);
90
+ }
91
+ catch(onrejected) {
92
+ return this.#parse().catch(onrejected);
93
+ }
94
+ finally(onfinally) {
95
+ return this.#parse().finally(onfinally);
96
+ }
97
+ };
98
+ var ENV = {
99
+ /** Required API key; used when `apiKey` is omitted. */
100
+ apiKey: "TYPESAFE_API_KEY",
101
+ /** API root; defaults to `https://api.typesafe.ai`. */
102
+ baseURL: "TYPESAFE_BASE_URL",
103
+ /** Default model name; defaults to `jev-latest`. */
104
+ defaultModel: "TYPESAFE_DEFAULT_MODEL",
105
+ /** Log level; defaults to `warn`. */
106
+ logLevel: "TYPESAFE_LOG_LEVEL"
107
+ };
108
+ var readEnv = (name) => {
109
+ if (typeof process === "undefined" || !process.env) return void 0;
110
+ return process.env[name]?.trim() || void 0;
111
+ };
112
+ var fromCodeOrEnv = (fromCode, envVar) => fromCode ?? readEnv(envVar);
113
+ var range = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
114
+ var DEFAULT_RETRY_POLICY = {
115
+ maxRetries: 2,
116
+ backoffInitialMs: 500,
117
+ backoffMaxMs: 5e3,
118
+ backoffJitter: 0.25,
119
+ /** HTTP 408, 429, and 5xx responses. */
120
+ httpStatuses: /* @__PURE__ */ new Set([
121
+ 408,
122
+ 429,
123
+ ...range(500, 600)
124
+ ]),
125
+ respectRetryAfter: true,
126
+ /** Maximum server retry delay before falling back to backoff. */
127
+ maxRetryAfterMs: 6e4,
128
+ apiConnectionError: true,
129
+ apiTimeoutError: true
130
+ };
131
+ DEFAULT_RETRY_POLICY.maxRetries;
132
+ var isRetryableStatus = (status, policy = DEFAULT_RETRY_POLICY) => policy.httpStatuses.has(status);
133
+ var parseRetryAfter = (headers, now = Date.now()) => {
134
+ const ms = Number(headers.get("retry-after-ms"));
135
+ if (headers.has("retry-after-ms") && Number.isFinite(ms) && ms >= 0) return ms;
136
+ const raw = headers.get("retry-after");
137
+ if (raw === null) return void 0;
138
+ const seconds = Number(raw);
139
+ if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1e3 : void 0;
140
+ const date = Date.parse(raw);
141
+ if (!Number.isNaN(date)) return Math.max(0, date - now);
142
+ };
143
+ var retryDelayMs = (attempt, headers, policy = DEFAULT_RETRY_POLICY, random = Math.random) => {
144
+ if (policy.respectRetryAfter && headers !== void 0) {
145
+ const retryAfter = parseRetryAfter(headers);
146
+ if (retryAfter !== void 0 && retryAfter <= policy.maxRetryAfterMs) return retryAfter;
147
+ }
148
+ const exponential = Math.min(policy.backoffInitialMs * 2 ** attempt, policy.backoffMaxMs);
149
+ return Math.round(exponential * (1 - random() * policy.backoffJitter));
150
+ };
151
+ var sleep = (ms, signal) => new Promise((resolve2, reject) => {
152
+ if (signal?.aborted) return reject(signal.reason);
153
+ const onAbort = () => {
154
+ clearTimeout(timer);
155
+ reject(signal?.reason);
156
+ };
157
+ const timer = setTimeout(() => {
158
+ signal?.removeEventListener("abort", onAbort);
159
+ resolve2();
160
+ }, ms);
161
+ signal?.addEventListener("abort", onAbort, { once: true });
162
+ });
163
+ var TypeSafeError = class extends Error {
164
+ constructor(message, options) {
165
+ super(message, options);
166
+ this.name = new.target.name;
167
+ }
168
+ };
169
+ var isRecord = (value) => typeof value === "object" && value !== null;
170
+ var extractMessage = (body) => {
171
+ if (typeof body === "string") return body || void 0;
172
+ if (!isRecord(body)) return void 0;
173
+ const { error, message, detail } = body;
174
+ if (typeof error === "string") return error;
175
+ if (isRecord(error) && typeof error.message === "string") return error.message;
176
+ if (typeof message === "string") return message;
177
+ if (typeof detail === "string") return detail;
178
+ if (isRecord(detail) && typeof detail.message === "string") return detail.message;
179
+ if (Array.isArray(detail)) return describeValidationErrors(detail);
180
+ };
181
+ var describeValidationErrors = (errors) => {
182
+ const parts = errors.flatMap((e) => {
183
+ if (!isRecord(e) || typeof e.msg !== "string") return [];
184
+ const loc = Array.isArray(e.loc) ? e.loc.filter((x) => x !== "body").join(".") : "";
185
+ return [loc ? `${loc}: ${e.msg}` : e.msg];
186
+ });
187
+ return parts.length > 0 ? parts.join("; ") : void 0;
188
+ };
189
+ var MAX_RAW_BODY_IN_MESSAGE = 200;
190
+ var APIError = class APIError2 extends TypeSafeError {
191
+ /** HTTP response status code. */
192
+ status;
193
+ /** HTTP response headers. */
194
+ headers;
195
+ /** Parsed JSON, response text, or `undefined` for an empty body. */
196
+ body;
197
+ /** Request ID from `x-typesafe-request-id`, or `undefined` when absent. */
198
+ requestId;
199
+ constructor(status, body, headers, message) {
200
+ super(message ?? APIError2.describe(status, body));
201
+ this.status = status;
202
+ this.body = body;
203
+ this.headers = headers;
204
+ this.requestId = requestIdFrom(headers);
205
+ }
206
+ static describe(status, body) {
207
+ const detail = extractMessage(body);
208
+ if (detail) return `${status} ${detail}`;
209
+ if (body === void 0) return `${status} status code (no body)`;
210
+ const raw = typeof body === "string" ? body : JSON.stringify(body);
211
+ return `${status} ${raw.length > MAX_RAW_BODY_IN_MESSAGE ? `${raw.slice(0, MAX_RAW_BODY_IN_MESSAGE)}\u2026` : raw}`;
212
+ }
213
+ /** Create the error subclass for an HTTP status code. */
214
+ static fromResponse(status, body, headers) {
215
+ if (status === 400) return new BadRequestError(status, body, headers);
216
+ if (status === 401) return new AuthenticationError(status, body, headers);
217
+ if (status === 403) return new PermissionDeniedError(status, body, headers);
218
+ if (status === 404) return new NotFoundError(status, body, headers);
219
+ if (status === 422) return new UnprocessableEntityError(status, body, headers);
220
+ if (status === 429) return new RateLimitError(status, body, headers);
221
+ if (status >= 500) return new InternalServerError(status, body, headers);
222
+ return new APIError2(status, body, headers);
223
+ }
224
+ };
225
+ var BadRequestError = class extends APIError {
226
+ };
227
+ var AuthenticationError = class extends APIError {
228
+ };
229
+ var PermissionDeniedError = class extends APIError {
230
+ };
231
+ var NotFoundError = class extends APIError {
232
+ };
233
+ var UnprocessableEntityError = class extends APIError {
234
+ };
235
+ var RateLimitError = class extends APIError {
236
+ /** Server retry delay in milliseconds, or `undefined` when absent or invalid. */
237
+ retryAfterMs = parseRetryAfter(this.headers);
238
+ };
239
+ var InternalServerError = class extends APIError {
240
+ };
241
+ var APIConnectionError = class extends TypeSafeError {
242
+ constructor(message = "Connection error.", options) {
243
+ super(message, options);
244
+ }
245
+ };
246
+ var APITimeoutError = class extends APIConnectionError {
247
+ /** Configured timeout in milliseconds. */
248
+ timeoutMs;
249
+ constructor(timeoutMs, options) {
250
+ super(`Request timed out after ${timeoutMs}ms.`, options);
251
+ this.timeoutMs = timeoutMs;
252
+ }
253
+ };
254
+ var APIUserAbortError = class extends TypeSafeError {
255
+ constructor(message = "Request was aborted.", options) {
256
+ super(message, options);
257
+ }
258
+ };
259
+ var LOG_LEVELS = [
260
+ "debug",
261
+ "info",
262
+ "warn",
263
+ "error",
264
+ "off"
265
+ ];
266
+ var DEFAULT_LOG_LEVEL = "warn";
267
+ var isLogLevel = (value) => LOG_LEVELS.includes(value);
268
+ var parseLogLevel = (value, source) => {
269
+ if (isLogLevel(value)) return value;
270
+ throw new TypeSafeError(`Invalid log level "${value}" from ${source}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
271
+ };
272
+ var PREFIX = "[typesafe-sdk]";
273
+ var consoleLogger = {
274
+ debug: (message, ...args) => console.debug(`${PREFIX} ${message}`, ...args),
275
+ info: (message, ...args) => console.info(`${PREFIX} ${message}`, ...args),
276
+ warn: (message, ...args) => console.warn(`${PREFIX} ${message}`, ...args),
277
+ error: (message, ...args) => console.error(`${PREFIX} ${message}`, ...args)
278
+ };
279
+ var RANK = {
280
+ debug: 0,
281
+ info: 1,
282
+ warn: 2,
283
+ error: 3,
284
+ off: 4
285
+ };
286
+ var drop = () => {
287
+ };
288
+ var withLevel = (sink, level) => {
289
+ const enabled = (at) => RANK[at] >= RANK[level];
290
+ return {
291
+ debug: enabled("debug") ? (message, ...args) => sink.debug(message, ...args) : drop,
292
+ info: enabled("info") ? (message, ...args) => sink.info(message, ...args) : drop,
293
+ warn: enabled("warn") ? (message, ...args) => sink.warn(message, ...args) : drop,
294
+ error: enabled("error") ? (message, ...args) => sink.error(message, ...args) : drop
295
+ };
296
+ };
297
+ var KEY_HEADERS = /* @__PURE__ */ new Set([
298
+ "authorization",
299
+ "proxy-authorization",
300
+ "x-api-key"
301
+ ]);
302
+ var OPAQUE_HEADERS = /* @__PURE__ */ new Set(["cookie", "set-cookie"]);
303
+ var redactKey = (value) => {
304
+ const [scheme, secret] = value.includes(" ") ? value.split(/\s+/, 2) : [void 0, value];
305
+ const tail = secret && secret.length > 8 ? secret.slice(-4) : "";
306
+ return `${scheme ? `${scheme} ` : ""}***${tail}`;
307
+ };
308
+ var redact = (name, value) => {
309
+ const lower = name.toLowerCase();
310
+ if (KEY_HEADERS.has(lower)) return redactKey(value);
311
+ if (OPAQUE_HEADERS.has(lower)) return "***";
312
+ return value;
313
+ };
314
+ var redactHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, redact(name, value)]));
315
+ var noul = (instructions = null, criteria) => ({
316
+ type: "noul",
317
+ instructions,
318
+ criteria
319
+ });
320
+ var choice = (instructions, criteria) => {
321
+ if (Array.isArray(criteria)) throw new TypeSafeError("Choice criteria must be a map of labels to descriptions, not a list.");
322
+ return {
323
+ type: "choice",
324
+ instructions,
325
+ criteria
326
+ };
327
+ };
328
+ var validateQuestions = (questions) => {
329
+ if (Object.keys(questions).length === 0) throw new TypeSafeError("At least one question is required.");
330
+ for (const [name, question] of Object.entries(questions)) {
331
+ if (question.type !== "score") continue;
332
+ if (!Array.isArray(question.criteria)) throw new TypeSafeError(`Score question "${name}" has criteria that are not a list; score criteria must be a list of descriptions indexed by score from zero.`);
333
+ if (question.criteria.length < 2) throw new TypeSafeError(`Score question "${name}" has ${question.criteria.length} criteria; at least two scores are required.`);
334
+ }
335
+ };
336
+ var Models = class {
337
+ #transport;
338
+ constructor(transport) {
339
+ this.#transport = transport;
340
+ }
341
+ /** List the models available to the account. */
342
+ list(options = {}) {
343
+ return this.#transport.request("GET", "/v1/models", options).map(unwrapModels);
344
+ }
345
+ };
346
+ var unwrapModels = (wire) => {
347
+ if (Array.isArray(wire?.models)) return wire.models;
348
+ throw new TypeSafeError("Unexpected response shape from GET /v1/models; expected { models: [...] }.");
349
+ };
350
+ var g = globalThis;
351
+ var isBrowser = () => typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
352
+ var describeRuntime = () => {
353
+ const platform = g.process?.platform && g.process?.arch ? ` (${g.process.platform}; ${g.process.arch})` : "";
354
+ if (g.Bun?.version) return `bun/${g.Bun.version}${platform}`;
355
+ if (g.Deno?.version?.deno) return `deno/${g.Deno.version.deno}${platform}`;
356
+ if (g.EdgeRuntime !== void 0) return "vercel-edge";
357
+ if (g.navigator?.userAgent === "Cloudflare-Workers") return "cloudflare-workers";
358
+ if (g.process?.versions?.node) return `node/${g.process.versions.node}${platform}`;
359
+ if (isBrowser()) return "browser";
360
+ return "unknown";
361
+ };
362
+ var VERSION = "0.6.0";
363
+ var missingApiKey = () => {
364
+ throw new TypeSafeError(`No API key was provided. Pass \`apiKey\` to the TypeSafeClient constructor or set the ${ENV.apiKey} environment variable.`);
365
+ };
366
+ var missingFetch = () => {
367
+ throw new TypeSafeError("No global `fetch` is available in this runtime. Pass a `fetch` implementation to the TypeSafeClient constructor.");
368
+ };
369
+ var refuseBrowser = () => {
370
+ throw new TypeSafeError("TypeSafeClient is running in a browser, which would expose your API key to anyone using the page. Call the API from a server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk.");
371
+ };
372
+ var defaultFetch = (input, init) => globalThis.fetch(input, init);
373
+ var assertNonNegativeInteger = (name, value) => {
374
+ if (!Number.isInteger(value) || value < 0) throw new TypeSafeError(`\`${name}\` must be a non-negative integer, got ${String(value)}.`);
375
+ return value;
376
+ };
377
+ var assertPositiveMs = (name, value) => {
378
+ if (!Number.isFinite(value) || value <= 0) throw new TypeSafeError(`\`${name}\` must be a positive number of milliseconds, got ${String(value)}.`);
379
+ return value;
380
+ };
381
+ var assertNonNegativeMs = (name, value) => {
382
+ if (!Number.isFinite(value) || value < 0) throw new TypeSafeError(`\`${name}\` must be a non-negative number of milliseconds, got ${String(value)}.`);
383
+ return value;
384
+ };
385
+ var assertFraction = (name, value) => {
386
+ if (!Number.isFinite(value) || value < 0 || value > 1) throw new TypeSafeError(`\`${name}\` must be between 0 and 1, got ${String(value)}.`);
387
+ return value;
388
+ };
389
+ var assertStatusSet = (name, statuses) => {
390
+ for (const status of statuses) if (!Number.isInteger(status) || status < 100 || status > 999) throw new TypeSafeError(`\`${name}\` must contain HTTP status codes, got ${String(status)}.`);
391
+ return statuses;
392
+ };
393
+ var resolveRetryPolicy = (base, overrides) => {
394
+ const o = overrides ?? {};
395
+ return {
396
+ maxRetries: o.maxRetries === void 0 ? base.maxRetries : assertNonNegativeInteger("retry.maxRetries", o.maxRetries),
397
+ backoffInitialMs: o.backoffInitialMs === void 0 ? base.backoffInitialMs : assertNonNegativeMs("retry.backoffInitialMs", o.backoffInitialMs),
398
+ backoffMaxMs: o.backoffMaxMs === void 0 ? base.backoffMaxMs : assertNonNegativeMs("retry.backoffMaxMs", o.backoffMaxMs),
399
+ backoffJitter: o.backoffJitter === void 0 ? base.backoffJitter : assertFraction("retry.backoffJitter", o.backoffJitter),
400
+ httpStatuses: new Set(o.httpStatuses === void 0 ? base.httpStatuses : assertStatusSet("retry.httpStatuses", o.httpStatuses)),
401
+ respectRetryAfter: o.respectRetryAfter ?? base.respectRetryAfter,
402
+ maxRetryAfterMs: o.maxRetryAfterMs === void 0 ? base.maxRetryAfterMs : assertNonNegativeMs("retry.maxRetryAfterMs", o.maxRetryAfterMs),
403
+ apiConnectionError: o.apiConnectionError ?? base.apiConnectionError,
404
+ apiTimeoutError: o.apiTimeoutError ?? base.apiTimeoutError
405
+ };
406
+ };
407
+ var isRetryableError = (err, policy) => {
408
+ if (err instanceof APITimeoutError) return policy.apiTimeoutError;
409
+ if (err instanceof APIConnectionError) return policy.apiConnectionError;
410
+ return false;
411
+ };
412
+ var resolveLogLevel = (fromCode) => {
413
+ if (fromCode !== void 0) return parseLogLevel(fromCode, "the `logLevel` option");
414
+ const fromEnv = readEnv(ENV.logLevel);
415
+ if (fromEnv !== void 0) return parseLogLevel(fromEnv, ENV.logLevel);
416
+ return DEFAULT_LOG_LEVEL;
417
+ };
418
+ var stripTrailingSlashes = (url) => url.replace(/\/+$/, "");
419
+ var mergeHeaders = (...sources) => {
420
+ const entries = /* @__PURE__ */ new Map();
421
+ for (const source of sources) for (const [name, value] of Object.entries(source)) if (value === void 0) entries.delete(name.toLowerCase());
422
+ else entries.set(name.toLowerCase(), [name, value]);
423
+ return Object.fromEntries(entries.values());
424
+ };
425
+ var bufferResponse = async (response, signal) => {
426
+ const reader = response.clone().body?.getReader();
427
+ if (!reader) return;
428
+ const cancel = () => {
429
+ reader.cancel(signal.reason).catch(() => {
430
+ });
431
+ response.body?.cancel(signal.reason).catch(() => {
432
+ });
433
+ };
434
+ signal.addEventListener("abort", cancel, { once: true });
435
+ try {
436
+ if (signal.aborted) cancel();
437
+ signal.throwIfAborted();
438
+ while (!(await reader.read()).done) signal.throwIfAborted();
439
+ signal.throwIfAborted();
440
+ } finally {
441
+ signal.removeEventListener("abort", cancel);
442
+ reader.releaseLock();
443
+ }
444
+ };
445
+ var RUNTIME = describeRuntime();
446
+ var TypeSafeClient = class {
447
+ /** API key excluded from serialization and public properties. */
448
+ #apiKey;
449
+ /** API root with trailing slashes removed. */
450
+ baseURL;
451
+ /** Model used when a request omits `model`. */
452
+ defaultModel;
453
+ /** Configured log verbosity. */
454
+ logLevel;
455
+ /** The configured logger, filtered to `logLevel`. */
456
+ logger;
457
+ /** Retry settings with constructor overrides applied. */
458
+ retry;
459
+ /** Timeout per attempt in milliseconds. */
460
+ timeout;
461
+ /** Additional headers sent with each request. */
462
+ defaultHeaders;
463
+ /** HTTP fetch implementation. */
464
+ fetch;
465
+ /** The models available to the account. */
466
+ models;
467
+ #requestCount = 0;
468
+ /**
469
+ * Create a client for the TypeSafe AI API.
470
+ *
471
+ * Explicit options take precedence over environment variables, then SDK defaults.
472
+ * Empty or whitespace-only environment values are ignored.
473
+ *
474
+ * @throws {TypeSafeError} The API key is missing, configuration is invalid, or the runtime is unsupported.
475
+ */
476
+ constructor(config = {}) {
477
+ if (isBrowser() && !config.dangerouslyAllowBrowser) refuseBrowser();
478
+ this.#apiKey = fromCodeOrEnv(config.apiKey, ENV.apiKey) ?? missingApiKey();
479
+ this.baseURL = stripTrailingSlashes(fromCodeOrEnv(config.baseURL, ENV.baseURL) ?? "https://api.typesafe.ai");
480
+ this.defaultModel = fromCodeOrEnv(config.defaultModel, ENV.defaultModel) ?? "jev-latest";
481
+ this.logLevel = resolveLogLevel(config.logLevel);
482
+ this.logger = withLevel(config.logger ?? consoleLogger, this.logLevel);
483
+ this.retry = resolveRetryPolicy(DEFAULT_RETRY_POLICY, config.retry);
484
+ this.timeout = assertPositiveMs("timeout", config.timeout ?? 1e4);
485
+ this.defaultHeaders = { ...config.defaultHeaders };
486
+ if (config.fetch === void 0 && typeof globalThis.fetch !== "function") missingFetch();
487
+ this.fetch = config.fetch ?? defaultFetch;
488
+ const transport = {
489
+ request: (method, path, options) => this.#request(method, path, options),
490
+ defaultModel: this.defaultModel
491
+ };
492
+ this.models = new Models(transport);
493
+ }
494
+ /**
495
+ * Answer named questions about text or structured state.
496
+ *
497
+ * @param request - State, questions, and an optional model override.
498
+ * @param options - Per-call timeout, retry, headers, and cancellation settings.
499
+ * @returns Answers typed by question name and criteria, with model and token usage.
500
+ * @throws {TypeSafeError} Questions are empty, or score criteria are not a list of at least two entries.
501
+ * @throws {APIError} The server returns a non-2xx response after retries.
502
+ * @throws {APIConnectionError} The request cannot connect or times out after retries.
503
+ * @throws {APIUserAbortError} The caller aborts the request.
504
+ *
505
+ * @example
506
+ * ```ts
507
+ * const { answers } = await client.systemOne({
508
+ * state: "I was charged twice. Please help.",
509
+ * questions: { billing: noul("Is this about billing?") },
510
+ * });
511
+ * console.log(answers.billing.noul);
512
+ * ```
513
+ */
514
+ systemOne(request, options = {}) {
515
+ validateQuestions(request.questions);
516
+ const body = {
517
+ ...request,
518
+ model: request.model ?? this.defaultModel
519
+ };
520
+ return this.#request("POST", "/v1/systemone", {
521
+ ...options,
522
+ body
523
+ });
524
+ }
525
+ /** Send a request and parse its response body. */
526
+ #request(method, path, options = {}) {
527
+ const resolved = {
528
+ method,
529
+ path,
530
+ body: options.body,
531
+ headers: mergeHeaders(this.defaultHeaders, options.headers ?? {}),
532
+ signal: options.signal,
533
+ timeout: options.timeout === void 0 ? this.timeout : assertPositiveMs("timeout", options.timeout),
534
+ retry: resolveRetryPolicy(this.retry, options.retry)
535
+ };
536
+ const tag = `#${++this.#requestCount} ${method} ${path}`;
537
+ return new APIPromise(this.fetchWithRetries(tag, resolved), async (res) => {
538
+ const parsed = await parseBody(res);
539
+ this.logger.debug(`${tag} <- body`, parsed);
540
+ return parsed;
541
+ });
542
+ }
543
+ /** Retry eligible failures, logging attempt summaries at `info` and headers and bodies at `debug`. */
544
+ async fetchWithRetries(tag, req) {
545
+ const url = `${this.baseURL}${req.path}`;
546
+ const headers = mergeHeaders(req.headers, {
547
+ Authorization: `Bearer ${this.#apiKey}`,
548
+ Accept: "application/json",
549
+ "User-Agent": `typesafe-sdk/${VERSION}`,
550
+ "X-TypeSafe-SDK": `typesafe-sdk/${VERSION}`,
551
+ "X-TypeSafe-Runtime": RUNTIME,
552
+ "Content-Type": req.body === void 0 ? void 0 : "application/json",
553
+ "X-TypeSafe-Retry-Count": void 0
554
+ });
555
+ const body = req.body === void 0 ? void 0 : JSON.stringify(req.body);
556
+ for (let attempt = 0; ; attempt++) {
557
+ const retriesLeft = req.retry.maxRetries - attempt;
558
+ const attemptHeaders = attempt === 0 ? headers : {
559
+ ...headers,
560
+ "X-TypeSafe-Retry-Count": String(attempt)
561
+ };
562
+ this.logger.debug(`${tag} -> ${url}`, {
563
+ headers: redactHeaders(attemptHeaders),
564
+ body: req.body
565
+ });
566
+ const started = Date.now();
567
+ let res;
568
+ try {
569
+ res = await this.attempt(tag, url, {
570
+ method: req.method,
571
+ headers: attemptHeaders,
572
+ body
573
+ }, req);
574
+ } catch (err) {
575
+ if (err instanceof APIUserAbortError || retriesLeft <= 0) throw err;
576
+ if (!isRetryableError(err, req.retry)) throw err;
577
+ await this.backOff(tag, attempt, retriesLeft, err.message, void 0, req);
578
+ continue;
579
+ }
580
+ const requestId = requestIdFrom(res.headers);
581
+ this.logger.info(`${tag} <- ${res.status} in ${Date.now() - started}ms${requestId ? ` (request ${requestId})` : ""}`);
582
+ if (res.ok) return res;
583
+ const errorBody = await parseBody(res);
584
+ this.logger.debug(`${tag} <- error body`, errorBody);
585
+ const error = APIError.fromResponse(res.status, errorBody, res.headers);
586
+ if (retriesLeft <= 0 || !isRetryableStatus(res.status, req.retry)) throw error;
587
+ await this.backOff(tag, attempt, retriesLeft, `${res.status}`, res.headers, req);
588
+ }
589
+ }
590
+ /**
591
+ * One HTTP round trip, including body delivery, with a timeout. The caller's signal and our
592
+ * timer both abort the same controller; we check which fired to choose the error class.
593
+ */
594
+ async attempt(tag, url, init, { signal, timeout }) {
595
+ const controller = new AbortController();
596
+ const abortFromCaller = () => controller.abort(signal?.reason);
597
+ if (signal?.aborted) abortFromCaller();
598
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
599
+ let timedOut = false;
600
+ const timer = setTimeout(() => {
601
+ timedOut = true;
602
+ controller.abort();
603
+ }, timeout);
604
+ const started = Date.now();
605
+ const elapsed = () => `${Date.now() - started}ms`;
606
+ try {
607
+ const response = await this.fetch(url, {
608
+ ...init,
609
+ signal: controller.signal
610
+ });
611
+ await bufferResponse(response, controller.signal);
612
+ return response;
613
+ } catch (err) {
614
+ if (signal?.aborted) {
615
+ this.logger.info(`${tag} aborted by caller after ${elapsed()}`);
616
+ throw new APIUserAbortError(void 0, { cause: err });
617
+ }
618
+ if (timedOut) {
619
+ this.logger.info(`${tag} timed out after ${elapsed()}`);
620
+ throw new APITimeoutError(timeout, { cause: err });
621
+ }
622
+ this.logger.info(`${tag} connection error after ${elapsed()}`, err);
623
+ throw new APIConnectionError(err instanceof Error ? `Connection error: ${err.message}` : void 0, { cause: err });
624
+ } finally {
625
+ clearTimeout(timer);
626
+ signal?.removeEventListener("abort", abortFromCaller);
627
+ }
628
+ }
629
+ /** Wait before retrying; caller cancellation throws `APIUserAbortError`. */
630
+ async backOff(tag, attempt, retriesLeft, reason, headers, { retry, signal }) {
631
+ const delay = retryDelayMs(attempt, headers, retry);
632
+ const nth = attempt + 1;
633
+ const total = attempt + retriesLeft;
634
+ this.logger.info(`${tag} retrying in ${delay}ms (retry ${nth}/${total}) after ${reason}`);
635
+ try {
636
+ await sleep(delay, signal);
637
+ } catch (err) {
638
+ this.logger.info(`${tag} aborted by caller while waiting to retry`);
639
+ throw new APIUserAbortError(void 0, { cause: err });
640
+ }
641
+ }
642
+ };
643
+ var parseBody = async (res) => {
644
+ const text = await res.text();
645
+ if (text.length === 0) return void 0;
646
+ if ((res.headers.get("content-type") ?? "").includes("application/json")) try {
647
+ return JSON.parse(text);
648
+ } catch {
649
+ return text;
650
+ }
651
+ try {
652
+ return JSON.parse(text);
653
+ } catch {
654
+ return text;
655
+ }
656
+ };
657
+
658
+ // src/core/jev-client.ts
659
+ var DEFAULT_JEV_MODEL = "jev-latest";
660
+ var DEFAULT_JEV_TIMEOUT_MS = 1e4;
661
+ var DEFAULT_JEV_MAX_RETRIES = 1;
662
+ var TYPESAFE_API_KEY_ENV = "TYPESAFE_API_KEY";
663
+ var TYPESAFE_BASE_URL_ENV = "TYPESAFE_BASE_URL";
664
+ var TypeSafeJevClient = class {
665
+ #client;
666
+ #timeoutMs;
667
+ constructor(config) {
668
+ const apiKey = config.apiKey.trim();
669
+ if (apiKey.length === 0) throw new JevConfigError("TypeSafe API key is empty");
670
+ const timeoutMs = config.timeoutMs ?? DEFAULT_JEV_TIMEOUT_MS;
671
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
672
+ throw new JevConfigError(`timeoutMs must be a positive number, got ${String(config.timeoutMs)}`);
673
+ }
674
+ const maxRetries = config.maxRetries ?? DEFAULT_JEV_MAX_RETRIES;
675
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
676
+ throw new JevConfigError(`maxRetries must be a non-negative integer, got ${String(config.maxRetries)}`);
677
+ }
678
+ this.#timeoutMs = timeoutMs;
679
+ try {
680
+ this.#client = new TypeSafeClient({
681
+ apiKey,
682
+ ...config.baseUrl !== void 0 ? { baseURL: config.baseUrl } : {},
683
+ defaultModel: config.model ?? DEFAULT_JEV_MODEL,
684
+ logLevel: "off",
685
+ timeout: timeoutMs,
686
+ retry: { maxRetries, backoffInitialMs: 250, backoffMaxMs: 1500, maxRetryAfterMs: 2e3 },
687
+ ...config.fetch !== void 0 ? { fetch: config.fetch } : {}
688
+ });
689
+ } catch (error) {
690
+ throw new JevConfigError(`TypeSafe client rejected its configuration: ${describeError(error)}`, {
691
+ cause: error
692
+ });
693
+ }
694
+ }
695
+ async noul(request, options = {}) {
696
+ const ids = requireIds(Object.keys(request.questions));
697
+ const questions = {};
698
+ for (const id of ids) {
699
+ const instructions = request.questions[id];
700
+ if (typeof instructions !== "string" || instructions.length === 0) {
701
+ throw new JevInputError(`question "${id}" has no instructions`);
702
+ }
703
+ questions[id] = noul(instructions);
704
+ }
705
+ const result = await this.#call(
706
+ () => this.#client.systemOne({ state: request.state, questions }, this.#requestOptions(options))
707
+ );
708
+ return {
709
+ model: readModel(result.model),
710
+ answers: validateNoulAnswers(ids, result.answers),
711
+ usage: readUsage(result.usage)
712
+ };
713
+ }
714
+ async choice(request, options = {}) {
715
+ const ids = requireIds(Object.keys(request.questions));
716
+ const questions = {};
717
+ for (const id of ids) {
718
+ const spec = request.questions[id];
719
+ if (spec === void 0) throw new JevInputError(`question "${id}" is undefined`);
720
+ validateChoiceSpec(id, spec);
721
+ const criteria = {};
722
+ for (const label of spec.labels) criteria[label] = spec.descriptions?.[label] ?? null;
723
+ questions[id] = choice(spec.instructions, criteria);
724
+ }
725
+ const result = await this.#call(
726
+ () => this.#client.systemOne({ state: request.state, questions }, this.#requestOptions(options))
727
+ );
728
+ return {
729
+ model: readModel(result.model),
730
+ answers: validateChoiceAnswers(request.questions, result.answers),
731
+ usage: readUsage(result.usage)
732
+ };
733
+ }
734
+ #requestOptions(options) {
735
+ const timeout = options.timeoutMs ?? this.#timeoutMs;
736
+ return options.signal !== void 0 ? { signal: options.signal, timeout } : { timeout };
737
+ }
738
+ async #call(send) {
739
+ try {
740
+ return await send();
741
+ } catch (error) {
742
+ throw toJevError(error);
743
+ }
744
+ }
745
+ };
746
+ function createJevClientFromEnv(env = process.env, overrides = {}) {
747
+ const apiKey = env[TYPESAFE_API_KEY_ENV]?.trim() ?? "";
748
+ if (apiKey.length === 0) throw new JevConfigError(`${TYPESAFE_API_KEY_ENV} is not set`);
749
+ const baseUrl = env[TYPESAFE_BASE_URL_ENV]?.trim();
750
+ return new TypeSafeJevClient({
751
+ apiKey,
752
+ ...baseUrl !== void 0 && baseUrl.length > 0 ? { baseUrl } : {},
753
+ ...overrides
754
+ });
755
+ }
756
+ function toJevError(error) {
757
+ if (error instanceof JevRequestError || error instanceof JevTimeoutError || error instanceof JevAbortError) {
758
+ return error;
759
+ }
760
+ if (error instanceof APIUserAbortError) return new JevAbortError(error.message, { cause: error });
761
+ if (error instanceof APITimeoutError) {
762
+ return new JevTimeoutError(error.timeoutMs, `Jev request timed out after ${String(error.timeoutMs)} ms`, {
763
+ cause: error
764
+ });
765
+ }
766
+ if (error instanceof APIError) {
767
+ const retryable = error instanceof RateLimitError || error.status >= 500 || error.status === 408;
768
+ const detail = error instanceof AuthenticationError ? "TypeSafe rejected the API key" : error.message;
769
+ return new JevRequestError(`Jev request failed with status ${String(error.status)}: ${detail}`, {
770
+ status: error.status,
771
+ retryable,
772
+ ...error.requestId !== void 0 ? { requestId: error.requestId } : {},
773
+ cause: error
774
+ });
775
+ }
776
+ if (error instanceof APIConnectionError) {
777
+ return new JevRequestError(`Jev request could not connect: ${error.message}`, { retryable: true, cause: error });
778
+ }
779
+ if (error instanceof TypeSafeError) {
780
+ return new JevConfigError(`TypeSafe SDK rejected the request: ${error.message}`, { cause: error });
781
+ }
782
+ if (error instanceof Error) return error;
783
+ return new JevRequestError(`Jev request failed: ${describeError(error)}`, { retryable: false, cause: error });
784
+ }
785
+ function requireIds(ids) {
786
+ if (ids.length === 0) throw new JevInputError("a Jev request needs at least one question");
787
+ for (const id of ids) {
788
+ if (id.length === 0) throw new JevInputError("question ids must be non-empty strings");
789
+ }
790
+ return ids;
791
+ }
792
+ function validateChoiceSpec(id, spec) {
793
+ if (typeof spec.instructions !== "string" || spec.instructions.length === 0) {
794
+ throw new JevInputError(`choice question "${id}" has no instructions`);
795
+ }
796
+ if (spec.labels.length < 2) throw new JevInputError(`choice question "${id}" needs at least two labels`);
797
+ if (new Set(spec.labels).size !== spec.labels.length) {
798
+ throw new JevInputError(`choice question "${id}" has duplicate labels`);
799
+ }
800
+ for (const label of spec.labels) {
801
+ if (label.length === 0) throw new JevInputError(`choice question "${id}" has an empty label`);
802
+ }
803
+ }
804
+ function isRecord2(value) {
805
+ return typeof value === "object" && value !== null && !Array.isArray(value);
806
+ }
807
+ function isUnitInterval(value) {
808
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
809
+ }
810
+ function readModel(model) {
811
+ return typeof model === "string" ? model : "";
812
+ }
813
+ function readUsage(usage) {
814
+ const record = isRecord2(usage) ? usage : {};
815
+ const input = record["input_tokens"];
816
+ const output = record["output_tokens"];
817
+ return {
818
+ inputTokens: typeof input === "number" && Number.isFinite(input) ? input : 0,
819
+ outputTokens: typeof output === "number" && Number.isFinite(output) ? output : 0
820
+ };
821
+ }
822
+ function rejectUnexpectedIds(expected, answers) {
823
+ const known = new Set(expected);
824
+ for (const key of Object.keys(answers)) {
825
+ if (!known.has(key)) throw new JevResponseError(`Jev answered an id that was not asked: "${key}"`);
826
+ }
827
+ }
828
+ function validateNoulAnswers(ids, answers) {
829
+ if (!isRecord2(answers)) throw new JevResponseError("Jev response has no answers object");
830
+ rejectUnexpectedIds(ids, answers);
831
+ const out = {};
832
+ for (const id of ids) {
833
+ const answer = answers[id];
834
+ if (!isRecord2(answer)) throw new JevResponseError(`Jev response is missing the answer for "${id}"`);
835
+ if (answer["type"] !== "noul") throw new JevResponseError(`Jev answer "${id}" is not a noul answer`);
836
+ const value = answer["noul"];
837
+ if (!isUnitInterval(value)) throw new JevResponseError(`Jev answer "${id}" has no noul value in [0, 1]`);
838
+ out[id] = value;
839
+ }
840
+ return out;
841
+ }
842
+ function validateChoiceAnswers(questions, answers) {
843
+ if (!isRecord2(answers)) throw new JevResponseError("Jev response has no answers object");
844
+ const ids = Object.keys(questions);
845
+ rejectUnexpectedIds(ids, answers);
846
+ const out = {};
847
+ for (const id of ids) {
848
+ const spec = questions[id];
849
+ if (spec === void 0) continue;
850
+ const answer = answers[id];
851
+ if (!isRecord2(answer)) throw new JevResponseError(`Jev response is missing the answer for "${id}"`);
852
+ if (answer["type"] !== "choice") throw new JevResponseError(`Jev answer "${id}" is not a choice answer`);
853
+ const chosen = answer["choice"];
854
+ if (typeof chosen !== "string" || !spec.labels.includes(chosen)) {
855
+ throw new JevResponseError(`Jev answer "${id}" chose an undeclared label: ${String(chosen)}`);
856
+ }
857
+ const confidence = answer["confidence"];
858
+ if (!isUnitInterval(confidence)) throw new JevResponseError(`Jev answer "${id}" has no confidence in [0, 1]`);
859
+ const rawProbabilities = answer["probabilities"];
860
+ if (!isRecord2(rawProbabilities)) throw new JevResponseError(`Jev answer "${id}" has no probabilities`);
861
+ const probabilities = {};
862
+ for (const label of spec.labels) probabilities[label] = 0;
863
+ for (const [label, value] of Object.entries(rawProbabilities)) {
864
+ if (!spec.labels.includes(label)) {
865
+ throw new JevResponseError(`Jev answer "${id}" reports a probability for an undeclared label: ${label}`);
866
+ }
867
+ if (!isUnitInterval(value)) throw new JevResponseError(`Jev answer "${id}" has a probability outside [0, 1]`);
868
+ probabilities[label] = value;
869
+ }
870
+ out[id] = { choice: chosen, confidence, probabilities };
871
+ }
872
+ return out;
873
+ }
874
+
875
+ // src/core/tokens.ts
876
+ var CHARS_PER_TOKEN = 3;
877
+ var DEFAULT_WINDOW_TOKENS = 25e3;
878
+ var MAX_REQUEST_TOKENS = 32e3;
879
+ function estimateTokens(text) {
880
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
881
+ }
882
+ function estimateJsonTokens(value) {
883
+ return estimateTokens(JSON.stringify(value) ?? "");
884
+ }
885
+
886
+ // src/core/windows.ts
887
+ function planWindows(items, options = {}) {
888
+ const budget = options.budgetTokens ?? DEFAULT_WINDOW_TOKENS;
889
+ const overhead = options.overheadTokens ?? 0;
890
+ const itemOverhead = options.itemOverheadTokens ?? 0;
891
+ if (!Number.isInteger(budget) || budget <= 0) {
892
+ throw new JevBudgetError(`budgetTokens must be a positive integer, got ${String(options.budgetTokens)}`);
893
+ }
894
+ if (!Number.isFinite(overhead) || overhead < 0) {
895
+ throw new JevBudgetError(`overheadTokens must be a non-negative number, got ${String(options.overheadTokens)}`);
896
+ }
897
+ if (!Number.isFinite(itemOverhead) || itemOverhead < 0) {
898
+ throw new JevBudgetError(
899
+ `itemOverheadTokens must be a non-negative number, got ${String(options.itemOverheadTokens)}`
900
+ );
901
+ }
902
+ if (overhead >= budget) {
903
+ throw new JevBudgetError(
904
+ `overheadTokens (${String(overhead)}) leaves no room under budgetTokens (${String(budget)})`
905
+ );
906
+ }
907
+ const cost = options.costTokens ?? ((item) => estimateJsonTokens({ id: item.id, text: item.text }));
908
+ const windows = [];
909
+ const oversize = [];
910
+ let current = [];
911
+ let used = overhead;
912
+ for (const item of items) {
913
+ const itemCost = cost(item) + itemOverhead;
914
+ if (overhead + itemCost > budget) {
915
+ oversize.push(item);
916
+ continue;
917
+ }
918
+ if (used + itemCost > budget && current.length > 0) {
919
+ windows.push(current);
920
+ current = [];
921
+ used = overhead;
922
+ }
923
+ current.push(item);
924
+ used += itemCost;
925
+ }
926
+ if (current.length > 0) windows.push(current);
927
+ return { windows, oversize };
928
+ }
929
+ var DEFAULT_WINDOW_CONCURRENCY = 4;
930
+ var DEFAULT_WINDOW_TIMEOUT_MS = 1e4;
931
+ async function runWindows(windows, judge, options = {}) {
932
+ const concurrency = options.concurrency ?? DEFAULT_WINDOW_CONCURRENCY;
933
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WINDOW_TIMEOUT_MS;
934
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
935
+ throw new JevBudgetError(`concurrency must be a positive integer, got ${String(options.concurrency)}`);
936
+ }
937
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
938
+ throw new JevBudgetError(`timeoutMs must be a positive number, got ${String(options.timeoutMs)}`);
939
+ }
940
+ if (windows.length === 0) return [];
941
+ if (options.signal?.aborted === true) throw new JevAbortError();
942
+ const controller = new AbortController();
943
+ const onOuterAbort = () => {
944
+ controller.abort(options.signal?.reason);
945
+ };
946
+ options.signal?.addEventListener("abort", onOuterAbort, { once: true });
947
+ const results = new Array(windows.length);
948
+ let failure;
949
+ let next = 0;
950
+ const worker = async () => {
951
+ while (!controller.signal.aborted) {
952
+ const index = next;
953
+ next += 1;
954
+ if (index >= windows.length) return;
955
+ const window = windows[index];
956
+ if (window === void 0) return;
957
+ const timeout = AbortSignal.timeout(timeoutMs);
958
+ const signal = AbortSignal.any([controller.signal, timeout]);
959
+ try {
960
+ results[index] = await judge(window, index, { signal, timeoutMs });
961
+ } catch (error) {
962
+ if (failure === void 0) failure = classifyWindowFailure(error, index, timeout, options.signal, timeoutMs);
963
+ controller.abort();
964
+ return;
965
+ }
966
+ }
967
+ };
968
+ try {
969
+ await Promise.all(Array.from({ length: Math.min(concurrency, windows.length) }, worker));
970
+ } finally {
971
+ options.signal?.removeEventListener("abort", onOuterAbort);
972
+ }
973
+ if (failure !== void 0) throw failure;
974
+ return results;
975
+ }
976
+ function classifyWindowFailure(error, index, timeout, outer, timeoutMs) {
977
+ if (timeout.aborted) {
978
+ return new JevTimeoutError(timeoutMs, `Jev window ${String(index + 1)} exceeded ${String(timeoutMs)} ms`, {
979
+ cause: error
980
+ });
981
+ }
982
+ if (outer?.aborted === true) return new JevAbortError("Jev windows aborted by caller", { cause: error });
983
+ if (error instanceof Error) return error;
984
+ return new Error(String(error));
985
+ }
986
+
987
+ // src/core/fake-jev.ts
988
+ var FakeJevClient = class {
989
+ calls = [];
990
+ #options;
991
+ constructor(options = {}) {
992
+ this.#options = options;
993
+ }
994
+ async noul(request, options = {}) {
995
+ const ids = Object.keys(request.questions);
996
+ if (ids.length === 0) throw new JevInputError("a Jev request needs at least one question");
997
+ this.calls.push({ kind: "noul", state: request.state, ids });
998
+ await this.#settle(options.signal);
999
+ const scorer = this.#options.noul ?? (() => 0.5);
1000
+ const answers = {};
1001
+ for (const id of ids) {
1002
+ answers[id] = clamp(scorer(id, request.questions[id] ?? "", request.state));
1003
+ }
1004
+ return { model: this.#options.model ?? "jev-fake", answers, usage: this.#usage(request, ids.length) };
1005
+ }
1006
+ async choice(request, options = {}) {
1007
+ const ids = Object.keys(request.questions);
1008
+ if (ids.length === 0) throw new JevInputError("a Jev request needs at least one question");
1009
+ this.calls.push({ kind: "choice", state: request.state, ids });
1010
+ await this.#settle(options.signal);
1011
+ const answers = {};
1012
+ for (const id of ids) {
1013
+ const spec = request.questions[id];
1014
+ if (spec === void 0) continue;
1015
+ answers[id] = resolveChoice(id, spec, request.state, this.#options.choice);
1016
+ }
1017
+ return { model: this.#options.model ?? "jev-fake", answers, usage: this.#usage(request, ids.length) };
1018
+ }
1019
+ async #settle(signal) {
1020
+ const failure = this.#options.failWith?.(this.calls.length);
1021
+ await wait(this.#options.delayMs ?? 0, signal);
1022
+ if (failure !== void 0) throw failure;
1023
+ }
1024
+ #usage(request, questionCount) {
1025
+ return { inputTokens: estimateJsonTokens(request), outputTokens: questionCount * 20 };
1026
+ }
1027
+ };
1028
+ function resolveChoice(id, spec, state, scorer) {
1029
+ const first = spec.labels[0];
1030
+ if (first === void 0) throw new JevInputError(`choice question "${id}" has no labels`);
1031
+ const picked = scorer?.(id, spec, state) ?? first;
1032
+ if (typeof picked === "string") {
1033
+ if (!spec.labels.includes(picked)) {
1034
+ throw new JevInputError(`fake scorer chose "${picked}" which is not a label of "${id}"`);
1035
+ }
1036
+ const probabilities = {};
1037
+ for (const label of spec.labels) probabilities[label] = label === picked ? 1 : 0;
1038
+ return { choice: picked, confidence: 1, probabilities };
1039
+ }
1040
+ if (!spec.labels.includes(picked.choice)) {
1041
+ throw new JevInputError(`fake scorer chose "${picked.choice}" which is not a label of "${id}"`);
1042
+ }
1043
+ return picked;
1044
+ }
1045
+ function clamp(value) {
1046
+ if (!Number.isFinite(value)) return 0;
1047
+ return Math.min(1, Math.max(0, value));
1048
+ }
1049
+ function abortError(signal) {
1050
+ const reason = signal.reason;
1051
+ if (reason instanceof Error && reason.name === "TimeoutError") {
1052
+ return new JevTimeoutError(0, "fake Jev request timed out", { cause: reason });
1053
+ }
1054
+ return new JevAbortError("fake Jev request aborted", { cause: reason });
1055
+ }
1056
+ function wait(ms, signal) {
1057
+ return new Promise((resolve2, reject) => {
1058
+ if (signal?.aborted === true) {
1059
+ reject(abortError(signal));
1060
+ return;
1061
+ }
1062
+ const onAbort = () => {
1063
+ clearTimeout(timer);
1064
+ if (signal !== void 0) reject(abortError(signal));
1065
+ };
1066
+ const timer = setTimeout(() => {
1067
+ signal?.removeEventListener("abort", onAbort);
1068
+ resolve2();
1069
+ }, ms);
1070
+ signal?.addEventListener("abort", onAbort, { once: true });
1071
+ });
1072
+ }
1073
+
1074
+ // src/bytes.ts
1075
+ var LF = 10;
1076
+ var CR = 13;
1077
+ function isValidUtf8(bytes) {
1078
+ try {
1079
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1080
+ return true;
1081
+ } catch {
1082
+ return false;
1083
+ }
1084
+ }
1085
+ function byteLineStarts(bytes) {
1086
+ const starts = [0];
1087
+ for (let index = 0; index < bytes.length; index += 1) {
1088
+ const byte = bytes[index];
1089
+ if (byte === CR && bytes[index + 1] === LF) index += 1;
1090
+ else if (byte !== CR && byte !== LF) continue;
1091
+ starts.push(index + 1);
1092
+ }
1093
+ if (starts[starts.length - 1] !== bytes.length) starts.push(bytes.length);
1094
+ return starts;
1095
+ }
1096
+ function countByteLines(bytes) {
1097
+ return byteLineStarts(bytes).length - 1;
1098
+ }
1099
+
1100
+ // src/config.ts
1101
+ import { readFile } from "fs/promises";
1102
+ import { homedir } from "os";
1103
+ import { join, resolve } from "path";
1104
+
1105
+ // src/errors.ts
1106
+ var JevpruneError = class extends Error {
1107
+ name = "JevpruneError";
1108
+ constructor(message, options) {
1109
+ super(message, options);
1110
+ }
1111
+ };
1112
+ var ConfigError = class extends JevpruneError {
1113
+ name = "ConfigError";
1114
+ };
1115
+ var UsageError = class extends JevpruneError {
1116
+ name = "UsageError";
1117
+ };
1118
+ var RunStoreError = class extends JevpruneError {
1119
+ name = "RunStoreError";
1120
+ code;
1121
+ constructor(message, details = {}) {
1122
+ super(message, { cause: details.cause });
1123
+ this.code = details.code;
1124
+ }
1125
+ };
1126
+ var SpawnError = class extends JevpruneError {
1127
+ name = "SpawnError";
1128
+ executable;
1129
+ code;
1130
+ constructor(message, details) {
1131
+ super(message, { cause: details.cause });
1132
+ this.executable = details.executable;
1133
+ this.code = details.code;
1134
+ }
1135
+ };
1136
+ var TranscriptError = class extends JevpruneError {
1137
+ name = "TranscriptError";
1138
+ path;
1139
+ constructor(message, details) {
1140
+ super(message, { cause: details.cause });
1141
+ this.path = details.path;
1142
+ }
1143
+ };
1144
+ var RunNotFoundError = class extends JevpruneError {
1145
+ name = "RunNotFoundError";
1146
+ id;
1147
+ constructor(id, options) {
1148
+ super(`run ${id} was not found`, options);
1149
+ this.id = id;
1150
+ }
1151
+ };
1152
+ var LineRangeError = class extends JevpruneError {
1153
+ name = "LineRangeError";
1154
+ };
1155
+ function errorCode(error) {
1156
+ if (typeof error !== "object" || error === null) return void 0;
1157
+ const code = error.code;
1158
+ return typeof code === "string" ? code : void 0;
1159
+ }
1160
+ function errorMessage(error) {
1161
+ if (error instanceof Error) return error.message;
1162
+ if (typeof error === "string") return error;
1163
+ return String(error);
1164
+ }
1165
+
1166
+ // src/config.ts
1167
+ var HOME_ENV = "JEVPRUNE_HOME";
1168
+ var TASK_ENV = "JEVPRUNE_TASK";
1169
+ var CONFIG_FILE = "config.json";
1170
+ var DEFAULT_ALLOWLIST = [
1171
+ "cd",
1172
+ "ls",
1173
+ "pwd",
1174
+ "echo",
1175
+ "git status",
1176
+ "git add",
1177
+ "git commit",
1178
+ "git log",
1179
+ "git diff --stat",
1180
+ "which",
1181
+ "mkdir",
1182
+ "touch",
1183
+ "true",
1184
+ "test",
1185
+ "["
1186
+ ];
1187
+ var DEFAULT_CONFIG = {
1188
+ threshold: 0.3,
1189
+ fastPathLines: 60,
1190
+ tailLines: 40,
1191
+ headLines: 40,
1192
+ contextLines: 3,
1193
+ minCollapseLines: 3,
1194
+ windowTokens: DEFAULT_WINDOW_TOKENS,
1195
+ windowTimeoutMs: 1e4,
1196
+ concurrency: 4,
1197
+ maxPruneBytes: 16777216,
1198
+ retention: { maxRuns: 200, maxBytes: 268435456 },
1199
+ autoWrap: true,
1200
+ allowlist: DEFAULT_ALLOWLIST
1201
+ };
1202
+ function resolveHome(env = process.env) {
1203
+ const override = env[HOME_ENV]?.trim() ?? "";
1204
+ if (override.length > 0) return resolve(override);
1205
+ return join(homedir(), ".jevprune");
1206
+ }
1207
+ async function loadConfig(env = process.env) {
1208
+ const home = resolveHome(env);
1209
+ const path = join(home, CONFIG_FILE);
1210
+ let raw;
1211
+ try {
1212
+ raw = await readFile(path, "utf8");
1213
+ } catch (error) {
1214
+ if (errorCode(error) === "ENOENT") return { ...DEFAULT_CONFIG, home };
1215
+ throw new ConfigError(`config file ${path} could not be read: ${errorMessage(error)}`, { cause: error });
1216
+ }
1217
+ return { ...parseConfig(raw, path), home };
1218
+ }
1219
+ function parseConfig(raw, path) {
1220
+ let parsed;
1221
+ try {
1222
+ parsed = JSON.parse(raw);
1223
+ } catch (error) {
1224
+ throw new ConfigError(`config file ${path} is not valid JSON: ${errorMessage(error)}`, { cause: error });
1225
+ }
1226
+ if (!isRecord3(parsed)) {
1227
+ throw new ConfigError(`config file ${path} must contain a JSON object, got ${describeValue(parsed)}`);
1228
+ }
1229
+ for (const key of Object.keys(parsed)) {
1230
+ if (!(key in DEFAULT_CONFIG)) throw new ConfigError(`unknown config key "${key}" in ${path}`);
1231
+ }
1232
+ return {
1233
+ threshold: readNumber(parsed, "threshold", DEFAULT_CONFIG.threshold, 0, 1),
1234
+ fastPathLines: readInteger(parsed, "fastPathLines", DEFAULT_CONFIG.fastPathLines, 0),
1235
+ tailLines: readInteger(parsed, "tailLines", DEFAULT_CONFIG.tailLines, 0),
1236
+ headLines: readInteger(parsed, "headLines", DEFAULT_CONFIG.headLines, 0),
1237
+ contextLines: readInteger(parsed, "contextLines", DEFAULT_CONFIG.contextLines, 0),
1238
+ minCollapseLines: readInteger(parsed, "minCollapseLines", DEFAULT_CONFIG.minCollapseLines, 1),
1239
+ windowTokens: readInteger(parsed, "windowTokens", DEFAULT_CONFIG.windowTokens, 1),
1240
+ windowTimeoutMs: readInteger(parsed, "windowTimeoutMs", DEFAULT_CONFIG.windowTimeoutMs, 1),
1241
+ concurrency: readInteger(parsed, "concurrency", DEFAULT_CONFIG.concurrency, 1),
1242
+ maxPruneBytes: readInteger(parsed, "maxPruneBytes", DEFAULT_CONFIG.maxPruneBytes, 1),
1243
+ retention: readRetention(parsed["retention"]),
1244
+ autoWrap: readBoolean(parsed, "autoWrap", DEFAULT_CONFIG.autoWrap),
1245
+ allowlist: readStringArray(parsed, "allowlist", DEFAULT_CONFIG.allowlist)
1246
+ };
1247
+ }
1248
+ function parseThreshold(value) {
1249
+ const parsed = Number(value);
1250
+ if (value.trim().length === 0 || !Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
1251
+ throw new ConfigError(`--threshold must be a number in [0, 1], got ${describeValue(value)}`);
1252
+ }
1253
+ return parsed;
1254
+ }
1255
+ function readNumber(source, key, fallback, min, max) {
1256
+ const value = source[key];
1257
+ if (value === void 0) return fallback;
1258
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
1259
+ throw new ConfigError(
1260
+ `config key "${key}" must be a number in [${String(min)}, ${String(max)}], got ${describeValue(value)}`
1261
+ );
1262
+ }
1263
+ return value;
1264
+ }
1265
+ function readInteger(source, key, fallback, min, label = key) {
1266
+ const value = source[key];
1267
+ if (value === void 0) return fallback;
1268
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
1269
+ throw new ConfigError(
1270
+ `config key "${label}" must be an integer >= ${String(min)}, got ${describeValue(value)}`
1271
+ );
1272
+ }
1273
+ return value;
1274
+ }
1275
+ function readBoolean(source, key, fallback) {
1276
+ const value = source[key];
1277
+ if (value === void 0) return fallback;
1278
+ if (typeof value !== "boolean") {
1279
+ throw new ConfigError(`config key "${key}" must be a boolean, got ${describeValue(value)}`);
1280
+ }
1281
+ return value;
1282
+ }
1283
+ function readStringArray(source, key, fallback) {
1284
+ const value = source[key];
1285
+ if (value === void 0) return fallback;
1286
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1287
+ throw new ConfigError(`config key "${key}" must be an array of strings, got ${describeValue(value)}`);
1288
+ }
1289
+ return value;
1290
+ }
1291
+ function readRetention(value) {
1292
+ if (value === void 0) return DEFAULT_CONFIG.retention;
1293
+ if (!isRecord3(value)) {
1294
+ throw new ConfigError(`config key "retention" must be an object, got ${describeValue(value)}`);
1295
+ }
1296
+ for (const key of Object.keys(value)) {
1297
+ if (key !== "maxRuns" && key !== "maxBytes") {
1298
+ throw new ConfigError(`unknown config key "retention.${key}"`);
1299
+ }
1300
+ }
1301
+ return {
1302
+ maxRuns: readInteger(value, "maxRuns", DEFAULT_CONFIG.retention.maxRuns, 1, "retention.maxRuns"),
1303
+ maxBytes: readInteger(value, "maxBytes", DEFAULT_CONFIG.retention.maxBytes, 1, "retention.maxBytes")
1304
+ };
1305
+ }
1306
+ function isRecord3(value) {
1307
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1308
+ }
1309
+ function describeValue(value) {
1310
+ if (typeof value === "string") return JSON.stringify(value);
1311
+ if (value === void 0) return "undefined";
1312
+ if (value === null || typeof value === "number" || typeof value === "boolean") return String(value);
1313
+ try {
1314
+ return JSON.stringify(value) ?? typeof value;
1315
+ } catch {
1316
+ return typeof value;
1317
+ }
1318
+ }
1319
+
1320
+ // src/footer.ts
1321
+ import { homedir as homedir2 } from "os";
1322
+ import { sep } from "path";
1323
+ var LF2 = 10;
1324
+ function formatFooter(input) {
1325
+ if (input.mode === "fast-path") return "";
1326
+ const parts = [];
1327
+ if (input.mode === "passthrough") {
1328
+ if (input.exitCode !== void 0 && input.exitCode !== null) parts.push(`exit ${String(input.exitCode)}`);
1329
+ const note = input.passthroughNote === void 0 ? "" : ` (${input.passthroughNote})`;
1330
+ parts.push(`${formatCount(input.linesIn)} lines passed through${note}`);
1331
+ } else {
1332
+ if (input.mode === "fallback") parts.push(`fallback (no Jev: ${input.fallbackReason ?? "unknown"})`);
1333
+ parts.push(`${formatCount(input.linesIn)} \u2192 ${formatCount(input.linesOut)} lines`);
1334
+ if (input.exitCode !== void 0 && input.exitCode !== null) parts.push(`exit ${String(input.exitCode)}`);
1335
+ }
1336
+ if (input.storeFailureCode !== void 0) {
1337
+ parts.push(`run store unavailable (${input.storeFailureCode})`);
1338
+ } else if (input.logPath !== void 0) {
1339
+ parts.push(`full output ${displayPath(input.logPath, input.home)}`);
1340
+ }
1341
+ return `jevprune: ${parts.join(", ")}`;
1342
+ }
1343
+ function footerAfter(lastByte, footer) {
1344
+ if (footer.length === 0) return "";
1345
+ const separator = lastByte === void 0 || lastByte === LF2 ? "" : "\n";
1346
+ return `${separator}${footer}
1347
+ `;
1348
+ }
1349
+ function withFooter(kept, footer) {
1350
+ if (footer.length === 0) return kept;
1351
+ const separator = kept.length === 0 || kept.endsWith("\n") ? "" : "\n";
1352
+ return `${kept}${separator}${footer}
1353
+ `;
1354
+ }
1355
+ function formatCount(value) {
1356
+ const digits = Math.trunc(Math.abs(value)).toString();
1357
+ const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1358
+ return value < 0 ? `-${grouped}` : grouped;
1359
+ }
1360
+ function displayPath(path, home = homedir2()) {
1361
+ if (path === home) return "~";
1362
+ if (home.length > 0 && path.startsWith(home + sep)) return `~${path.slice(home.length)}`;
1363
+ return path;
1364
+ }
1365
+
1366
+ // src/keeps.ts
1367
+ var SIGNATURE_CASE_SENSITIVE = /(^|\s)(FAIL|FAILED|ERROR|PANIC|FATAL)(\s|:|$)|\b[A-Z][A-Za-z]*(Error|Exception|Panic)\b|^\s*E\s{2,}\S|^npm ERR!|^\s*(Test Files|Tests|Test Suites)\s/;
1368
+ var SIGNATURE_CASE_INSENSITIVE = /^\s*(✗|✘|×|⨯|❌|❯)|^\s*(error|fatal)(\[E\d+\])?:|^traceback \(most recent call last\)|^\s*File ".*", line \d+|^\s+at .*:\d+:\d+\)?$|^\s*-->\s.*:\d+:\d+|^thread '.*' panicked|\b(exit code|exit status|exited with|command not found|no such file or directory|ENOENT|EACCES|ECONNREFUSED|segmentation fault|core dumped|killed)\b|\blevel[=:"\s]+"?(error|fatal|panic)\b|\[(error|fatal|panic)\]|^=+ .*(passed|failed|error).* =+$/i;
1369
+ function isSignatureLine(text) {
1370
+ return SIGNATURE_CASE_SENSITIVE.test(text) || SIGNATURE_CASE_INSENSITIVE.test(text);
1371
+ }
1372
+ function computeKeeps(lines, options) {
1373
+ const keeps = /* @__PURE__ */ new Map();
1374
+ const tailLines = Math.max(0, Math.trunc(options.tailLines));
1375
+ const contextLines = Math.max(0, Math.trunc(options.contextLines));
1376
+ const tailStart = lines.length - tailLines + 1;
1377
+ for (const line of lines) {
1378
+ if (line.n >= tailStart) keeps.set(line.n, "tail");
1379
+ }
1380
+ const seen = /* @__PURE__ */ new Set();
1381
+ const signatures = [];
1382
+ for (const line of lines) {
1383
+ if (!isSignatureLine(line.text)) continue;
1384
+ const key = line.text.trim();
1385
+ if (seen.has(key)) continue;
1386
+ seen.add(key);
1387
+ signatures.push(line.n);
1388
+ }
1389
+ for (const n of signatures) keeps.set(n, "signature");
1390
+ for (const n of signatures) {
1391
+ for (let offset = 1; offset <= contextLines; offset += 1) {
1392
+ for (const candidate of [n - offset, n + offset]) {
1393
+ if (candidate < 1 || candidate > lines.length) continue;
1394
+ if (!keeps.has(candidate)) keeps.set(candidate, "context");
1395
+ }
1396
+ }
1397
+ }
1398
+ return keeps;
1399
+ }
1400
+
1401
+ // src/lines.ts
1402
+ function splitLines(text) {
1403
+ const lines = [];
1404
+ const pattern = /\r\n|\n|\r/g;
1405
+ let start = 0;
1406
+ let match = pattern.exec(text);
1407
+ while (match !== null) {
1408
+ const raw = match[0];
1409
+ const terminator = raw === "\r\n" ? "\r\n" : raw === "\r" ? "\r" : "\n";
1410
+ lines.push({ n: lines.length + 1, text: text.slice(start, match.index), terminator });
1411
+ start = match.index + raw.length;
1412
+ match = pattern.exec(text);
1413
+ }
1414
+ if (start < text.length) {
1415
+ lines.push({ n: lines.length + 1, text: text.slice(start), terminator: "" });
1416
+ }
1417
+ return lines;
1418
+ }
1419
+ function joinLines(lines) {
1420
+ let out = "";
1421
+ for (const line of lines) out += line.text + line.terminator;
1422
+ return out;
1423
+ }
1424
+
1425
+ // src/merge.ts
1426
+ function collapseMarker(range2, runId) {
1427
+ return `[jevprune: ${String(range2.count)} lines dropped, run ${runId}, lines ${String(range2.from)}-${String(range2.to)}]
1428
+ `;
1429
+ }
1430
+ function mergeDecisions(lines, decisions, options) {
1431
+ const minCollapseLines = Math.max(1, Math.trunc(options.minCollapseLines));
1432
+ const dropped = [];
1433
+ let kept = "";
1434
+ let run = null;
1435
+ let expected = 1;
1436
+ const flush = () => {
1437
+ if (run === null) return;
1438
+ const count = run.to - run.from + 1;
1439
+ if (!run.missing && count < minCollapseLines) {
1440
+ for (const line of run.lines) {
1441
+ decisions.set(line.n, { keep: true, reason: "collapse-min" });
1442
+ kept += line.text + line.terminator;
1443
+ }
1444
+ } else {
1445
+ const range2 = { from: run.from, to: run.to, count };
1446
+ dropped.push(range2);
1447
+ kept += collapseMarker(range2, options.runId);
1448
+ }
1449
+ run = null;
1450
+ };
1451
+ const dropLine = (line) => {
1452
+ if (run === null) run = { from: line.n, to: line.n, lines: [line], missing: false };
1453
+ else {
1454
+ run.to = line.n;
1455
+ run.lines.push(line);
1456
+ }
1457
+ };
1458
+ const dropMissing = (from, to) => {
1459
+ if (to < from) return;
1460
+ if (run === null) run = { from, to, lines: [], missing: true };
1461
+ else {
1462
+ run.to = to;
1463
+ run.missing = true;
1464
+ }
1465
+ };
1466
+ for (const line of lines) {
1467
+ dropMissing(expected, line.n - 1);
1468
+ expected = line.n + 1;
1469
+ if (decisions.get(line.n)?.keep === false) {
1470
+ dropLine(line);
1471
+ continue;
1472
+ }
1473
+ flush();
1474
+ kept += line.text + line.terminator;
1475
+ }
1476
+ if (options.totalLines !== void 0) dropMissing(expected, options.totalLines);
1477
+ flush();
1478
+ return { kept, dropped };
1479
+ }
1480
+
1481
+ // src/io.ts
1482
+ async function readStreamBytes(stream) {
1483
+ const chunks = [];
1484
+ for await (const chunk of stream) {
1485
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
1486
+ }
1487
+ return Buffer.concat(chunks);
1488
+ }
1489
+ async function readStream(stream) {
1490
+ return (await readStreamBytes(stream)).toString("utf8");
1491
+ }
1492
+
1493
+ // src/select.ts
1494
+ var UNAUTHORIZED_REASON = "unauthorized (401)";
1495
+ var NOT_UTF8_REASON = "not valid UTF-8";
1496
+ var NOT_UTF8_NOTE = "output is not valid UTF-8";
1497
+ var RUBRIC = "A line is needed when a developer acting on the task would want to read it: errors, failures, assertions, stack frames, diagnostics, timings or statuses that bear on the task, and the lines that give them meaning. Progress bars, download counters, repeated banners, unchanged status lines and routine success noise are not needed.";
1498
+ var ITEM_OVERHEAD_TOKENS = 12;
1499
+ function passthroughSelection(input) {
1500
+ return {
1501
+ mode: "passthrough",
1502
+ kept: "",
1503
+ dropped: [],
1504
+ linesIn: input.lines,
1505
+ linesOut: input.lines,
1506
+ bytesIn: input.bytes,
1507
+ bytesOut: input.bytes,
1508
+ windows: 0,
1509
+ jevRequests: 0,
1510
+ jevInputTokens: 0,
1511
+ ...input.reason !== void 0 ? { fallbackReason: input.reason } : {},
1512
+ decisions: /* @__PURE__ */ new Map()
1513
+ };
1514
+ }
1515
+ function questionFor(n) {
1516
+ return `Is line ${String(n)} needed for the task?`;
1517
+ }
1518
+ async function selectLines(input) {
1519
+ const lines = splitLines(input.text);
1520
+ const bytesIn = Buffer.byteLength(input.text);
1521
+ if (input.oversize !== void 0) {
1522
+ return oversizeSelection(input, input.oversize, lines, bytesIn);
1523
+ }
1524
+ if (input.exitCode !== void 0 && input.exitCode !== null && input.exitCode !== 0 || input.interrupted === true) {
1525
+ return everyLine(lines, "passthrough", input.text, bytesIn);
1526
+ }
1527
+ if (lines.length <= input.config.fastPathLines) {
1528
+ return everyLine(lines, "fast-path", input.text, bytesIn);
1529
+ }
1530
+ const keeps = keepsOf(lines, input.config);
1531
+ const decisions = /* @__PURE__ */ new Map();
1532
+ const candidates = [];
1533
+ for (const line of lines) {
1534
+ const keep = keeps.get(line.n);
1535
+ if (keep !== void 0) {
1536
+ decisions.set(line.n, { keep: true, reason: keep });
1537
+ continue;
1538
+ }
1539
+ if (line.text.trim().length === 0) {
1540
+ decisions.set(line.n, { keep: false, reason: "blank" });
1541
+ continue;
1542
+ }
1543
+ candidates.push({ id: `l${String(line.n)}`, n: line.n, text: line.text });
1544
+ }
1545
+ if (input.client === null) {
1546
+ return fallbackSelection({ lines, keeps, input, reason: "no api key", bytesIn, linesIn: lines.length });
1547
+ }
1548
+ let verdicts;
1549
+ try {
1550
+ verdicts = await askJev(candidates, input, input.client);
1551
+ } catch (error) {
1552
+ return fallbackSelection({
1553
+ lines,
1554
+ keeps,
1555
+ input,
1556
+ reason: fallbackReason(error),
1557
+ bytesIn,
1558
+ linesIn: lines.length
1559
+ });
1560
+ }
1561
+ for (const n of verdicts.oversize) decisions.set(n, { keep: true, reason: "oversize" });
1562
+ for (const item of candidates) {
1563
+ if (decisions.has(item.n)) continue;
1564
+ const noul2 = verdicts.answers.get(item.n) ?? 0;
1565
+ decisions.set(item.n, { keep: noul2 >= input.config.threshold, reason: "jev", noul: noul2 });
1566
+ }
1567
+ const { kept, dropped } = mergeDecisions(lines, decisions, {
1568
+ minCollapseLines: input.config.minCollapseLines,
1569
+ runId: input.runId
1570
+ });
1571
+ return {
1572
+ mode: "jev",
1573
+ kept,
1574
+ dropped,
1575
+ linesIn: lines.length,
1576
+ linesOut: splitLines(kept).length,
1577
+ bytesIn,
1578
+ bytesOut: Buffer.byteLength(kept),
1579
+ windows: verdicts.windows,
1580
+ jevRequests: verdicts.jevRequests,
1581
+ jevInputTokens: verdicts.jevInputTokens,
1582
+ decisions
1583
+ };
1584
+ }
1585
+ async function askJev(candidates, input, client) {
1586
+ const answers = /* @__PURE__ */ new Map();
1587
+ if (candidates.length === 0) {
1588
+ return { windows: 0, jevRequests: 0, jevInputTokens: 0, answers, oversize: [] };
1589
+ }
1590
+ const lastLine = candidates[candidates.length - 1]?.n ?? 0;
1591
+ const plan = planWindows(candidates, {
1592
+ budgetTokens: input.config.windowTokens,
1593
+ overheadTokens: estimateJsonTokens(stateOf(input, [])),
1594
+ itemOverheadTokens: estimateTokens(questionFor(lastLine)) + ITEM_OVERHEAD_TOKENS,
1595
+ costTokens: (item) => estimateJsonTokens({ n: item.n, text: item.text })
1596
+ });
1597
+ const results = await runWindows(
1598
+ plan.windows,
1599
+ async (window, _index, options) => {
1600
+ const questions = {};
1601
+ for (const item of window) questions[item.id] = questionFor(item.n);
1602
+ return await client.noul({ state: stateOf(input, window), questions }, options);
1603
+ },
1604
+ {
1605
+ concurrency: input.config.concurrency,
1606
+ timeoutMs: input.config.windowTimeoutMs,
1607
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
1608
+ }
1609
+ );
1610
+ let jevInputTokens = 0;
1611
+ for (const [index, result] of results.entries()) {
1612
+ jevInputTokens += result.usage.inputTokens;
1613
+ for (const item of plan.windows[index] ?? []) {
1614
+ const noul2 = result.answers[item.id];
1615
+ if (noul2 !== void 0) answers.set(item.n, noul2);
1616
+ }
1617
+ }
1618
+ return {
1619
+ windows: plan.windows.length,
1620
+ jevRequests: results.length,
1621
+ jevInputTokens,
1622
+ answers,
1623
+ oversize: plan.oversize.map((item) => item.n)
1624
+ };
1625
+ }
1626
+ function stateOf(input, window) {
1627
+ return {
1628
+ command: input.command,
1629
+ task: input.task,
1630
+ rubric: RUBRIC,
1631
+ lines: window.map((item) => ({ n: item.n, text: item.text }))
1632
+ };
1633
+ }
1634
+ function keepsOf(lines, config) {
1635
+ return computeKeeps(lines, { tailLines: config.tailLines, contextLines: config.contextLines });
1636
+ }
1637
+ function fallbackSelection(fallback) {
1638
+ const { lines, keeps, input } = fallback;
1639
+ return fallbackResult({
1640
+ lines,
1641
+ decisions: fallbackDecisions(lines, keeps, input.config.headLines),
1642
+ input,
1643
+ reason: fallback.reason,
1644
+ bytesIn: fallback.bytesIn,
1645
+ linesIn: fallback.linesIn
1646
+ });
1647
+ }
1648
+ function oversizeSelection(input, oversize, captured, bytesIn) {
1649
+ const headCount = Math.min(Math.max(0, Math.trunc(oversize.headSegmentLines)), captured.length);
1650
+ const totalLines = Math.max(Math.trunc(oversize.lines), captured.length);
1651
+ const tailFirst = totalLines - captured.length + headCount + 1;
1652
+ const head = captured.slice(0, headCount);
1653
+ const tail = renumber(captured.slice(headCount), tailFirst);
1654
+ const contextLines = input.config.contextLines;
1655
+ const keeps = computeKeeps(head, { tailLines: 0, contextLines });
1656
+ const tailKeeps = computeKeeps(renumber(tail, 1), { tailLines: input.config.tailLines, contextLines });
1657
+ for (const [n, reason] of tailKeeps) keeps.set(n + tailFirst - 1, reason);
1658
+ const lines = [...head, ...tail];
1659
+ return fallbackResult({
1660
+ lines,
1661
+ decisions: fallbackDecisions(lines, keeps, input.config.headLines),
1662
+ input,
1663
+ reason: `output over ${String(input.config.maxPruneBytes)} bytes`,
1664
+ bytesIn,
1665
+ linesIn: totalLines,
1666
+ totalLines
1667
+ });
1668
+ }
1669
+ function fallbackDecisions(lines, keeps, headLines) {
1670
+ const head = Math.max(0, Math.trunc(headLines));
1671
+ const decisions = /* @__PURE__ */ new Map();
1672
+ for (const line of lines) {
1673
+ const keep = keeps.get(line.n);
1674
+ if (keep !== void 0) decisions.set(line.n, { keep: true, reason: keep });
1675
+ else if (line.n <= head) decisions.set(line.n, { keep: true, reason: "head" });
1676
+ else decisions.set(line.n, { keep: false, reason: "fallback" });
1677
+ }
1678
+ return decisions;
1679
+ }
1680
+ function renumber(lines, from) {
1681
+ return lines.map((line, index) => ({ ...line, n: from + index }));
1682
+ }
1683
+ function fallbackResult(fallback) {
1684
+ const { kept, dropped } = mergeDecisions(fallback.lines, fallback.decisions, {
1685
+ minCollapseLines: fallback.input.config.minCollapseLines,
1686
+ runId: fallback.input.runId,
1687
+ ...fallback.totalLines !== void 0 ? { totalLines: fallback.totalLines } : {}
1688
+ });
1689
+ return {
1690
+ mode: "fallback",
1691
+ kept,
1692
+ dropped,
1693
+ linesIn: fallback.linesIn,
1694
+ linesOut: splitLines(kept).length,
1695
+ bytesIn: fallback.bytesIn,
1696
+ bytesOut: Buffer.byteLength(kept),
1697
+ windows: 0,
1698
+ jevRequests: 0,
1699
+ jevInputTokens: 0,
1700
+ fallbackReason: fallback.reason,
1701
+ decisions: fallback.decisions
1702
+ };
1703
+ }
1704
+ function fallbackReason(error) {
1705
+ if (error instanceof JevTimeoutError) return "timeout";
1706
+ if (error instanceof JevResponseError) return "invalid response";
1707
+ if (error instanceof JevRequestError) {
1708
+ switch (error.status) {
1709
+ case 429:
1710
+ return "rate limited (429)";
1711
+ case 529:
1712
+ return "overloaded (529)";
1713
+ case 401:
1714
+ return UNAUTHORIZED_REASON;
1715
+ case 400:
1716
+ return "bad request (400)";
1717
+ case void 0:
1718
+ return "network";
1719
+ default:
1720
+ return error.name;
1721
+ }
1722
+ }
1723
+ if (error instanceof Error) return error.name;
1724
+ return "unknown";
1725
+ }
1726
+ function everyLine(lines, mode, text, bytesIn) {
1727
+ const decisions = /* @__PURE__ */ new Map();
1728
+ for (const line of lines) decisions.set(line.n, { keep: true, reason: mode });
1729
+ return {
1730
+ mode,
1731
+ kept: text,
1732
+ dropped: [],
1733
+ linesIn: lines.length,
1734
+ linesOut: lines.length,
1735
+ bytesIn,
1736
+ bytesOut: bytesIn,
1737
+ windows: 0,
1738
+ jevRequests: 0,
1739
+ jevInputTokens: 0,
1740
+ decisions
1741
+ };
1742
+ }
1743
+
1744
+ // src/store.ts
1745
+ import { randomBytes } from "crypto";
1746
+ import { createReadStream } from "fs";
1747
+ import { appendFile, mkdir, open, readFile as readFile2, readdir, stat, unlink, writeFile } from "fs/promises";
1748
+ import { join as join2 } from "path";
1749
+ import { finished } from "stream/promises";
1750
+ var RUN_ID_PATTERN = /^[a-z0-9]+-[a-f0-9]{4}$/;
1751
+ var RUNS_DIR = "runs";
1752
+ var GAIN_FILE = "gain.jsonl";
1753
+ var DIR_MODE = 448;
1754
+ var FILE_MODE = 384;
1755
+ function newRunId() {
1756
+ return `${Date.now().toString(36)}-${randomBytes(2).toString("hex")}`;
1757
+ }
1758
+ var FileRunWriter = class {
1759
+ path;
1760
+ #handle;
1761
+ #stream;
1762
+ #waiting = [];
1763
+ #failure;
1764
+ #closed = false;
1765
+ constructor(path, handle, stream) {
1766
+ this.path = path;
1767
+ this.#handle = handle;
1768
+ this.#stream = stream;
1769
+ this.#stream.on("error", (error) => {
1770
+ this.#fail(error);
1771
+ this.#release();
1772
+ });
1773
+ this.#stream.on("drain", () => {
1774
+ this.#release();
1775
+ });
1776
+ }
1777
+ get failure() {
1778
+ return this.#failure;
1779
+ }
1780
+ write(chunk) {
1781
+ if (this.#closed || this.#failure !== void 0) return true;
1782
+ try {
1783
+ return this.#stream.write(chunk);
1784
+ } catch (error) {
1785
+ this.#fail(error);
1786
+ return true;
1787
+ }
1788
+ }
1789
+ onDrain(listener) {
1790
+ if (this.#closed || this.#failure !== void 0) {
1791
+ queueMicrotask(listener);
1792
+ return;
1793
+ }
1794
+ this.#waiting.push(listener);
1795
+ }
1796
+ async close() {
1797
+ if (this.#closed) return;
1798
+ this.#closed = true;
1799
+ try {
1800
+ this.#stream.end();
1801
+ await finished(this.#stream);
1802
+ } catch (error) {
1803
+ this.#fail(error);
1804
+ await this.#handle.close().catch(() => void 0);
1805
+ }
1806
+ this.#release();
1807
+ }
1808
+ #fail(error) {
1809
+ this.#failure ??= storeError(`run log ${this.path} could not be written`, error);
1810
+ }
1811
+ #release() {
1812
+ const waiting = this.#waiting;
1813
+ this.#waiting = [];
1814
+ for (const listener of waiting) listener();
1815
+ }
1816
+ };
1817
+ var RunStore = class {
1818
+ home;
1819
+ #retention;
1820
+ constructor(options) {
1821
+ this.home = options.home;
1822
+ this.#retention = options.retention ?? DEFAULT_CONFIG.retention;
1823
+ }
1824
+ get runsDir() {
1825
+ return join2(this.home, RUNS_DIR);
1826
+ }
1827
+ get gainPath() {
1828
+ return join2(this.home, GAIN_FILE);
1829
+ }
1830
+ logPath(id) {
1831
+ return join2(this.runsDir, `${requireRunId(id)}.log`);
1832
+ }
1833
+ metaPath(id) {
1834
+ return join2(this.runsDir, `${requireRunId(id)}.json`);
1835
+ }
1836
+ async openRun(run) {
1837
+ const path = this.logPath(run.id);
1838
+ await this.#ensureDir(this.runsDir);
1839
+ let handle;
1840
+ try {
1841
+ handle = await open(path, "wx", FILE_MODE);
1842
+ } catch (error) {
1843
+ throw storeError(`run log ${path} could not be created`, error);
1844
+ }
1845
+ return new FileRunWriter(path, handle, handle.createWriteStream());
1846
+ }
1847
+ async finalizeRun(id, meta) {
1848
+ const path = this.metaPath(id);
1849
+ await this.#ensureDir(this.runsDir);
1850
+ try {
1851
+ await writeFile(path, `${JSON.stringify(meta)}
1852
+ `, { mode: FILE_MODE });
1853
+ } catch (error) {
1854
+ throw storeError(`run meta ${path} could not be written`, error);
1855
+ }
1856
+ }
1857
+ async readRunBytes(id) {
1858
+ const path = this.logPath(id);
1859
+ try {
1860
+ return await readFile2(path);
1861
+ } catch (error) {
1862
+ if (errorCode(error) === "ENOENT") throw new RunNotFoundError(id, { cause: error });
1863
+ throw storeError(`run log ${path} could not be read`, error);
1864
+ }
1865
+ }
1866
+ async *readRunChunks(id) {
1867
+ const path = this.logPath(id);
1868
+ try {
1869
+ for await (const chunk of createReadStream(path)) {
1870
+ yield typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
1871
+ }
1872
+ } catch (error) {
1873
+ if (errorCode(error) === "ENOENT") throw new RunNotFoundError(id, { cause: error });
1874
+ throw storeError(`run log ${path} could not be read`, error);
1875
+ }
1876
+ }
1877
+ async readRun(id) {
1878
+ const bytes = await this.readRunBytes(id);
1879
+ return { id, path: this.logPath(id), text: bytes.toString("utf8"), meta: await this.#readMeta(id) };
1880
+ }
1881
+ async readRunLineBytes(id, from = 1, to) {
1882
+ const bytes = await this.readRunBytes(id);
1883
+ const starts = byteLineStarts(bytes);
1884
+ const lines = starts.length - 1;
1885
+ const last = to ?? lines;
1886
+ if (!Number.isInteger(from) || !Number.isInteger(last) || from < 1 || last < from) {
1887
+ throw new LineRangeError(`line range ${String(from)}-${String(last)} is not a range`);
1888
+ }
1889
+ if (last > lines) {
1890
+ throw new LineRangeError(
1891
+ `line range ${String(from)}-${String(last)} is outside run ${id} (${String(lines)} lines)`
1892
+ );
1893
+ }
1894
+ return bytes.subarray(starts[from - 1] ?? 0, starts[last] ?? bytes.length);
1895
+ }
1896
+ async readRunLines(id, from = 1, to) {
1897
+ return (await this.readRunLineBytes(id, from, to)).toString("utf8");
1898
+ }
1899
+ async appendGain(entry) {
1900
+ await this.#ensureDir(this.home);
1901
+ try {
1902
+ await appendFile(this.gainPath, `${JSON.stringify(entry)}
1903
+ `, { mode: FILE_MODE });
1904
+ } catch (error) {
1905
+ throw storeError(`gain ledger ${this.gainPath} could not be written`, error);
1906
+ }
1907
+ }
1908
+ async readGain() {
1909
+ let raw;
1910
+ try {
1911
+ raw = await readFile2(this.gainPath, "utf8");
1912
+ } catch (error) {
1913
+ if (errorCode(error) === "ENOENT") return { runs: 0, linesIn: 0, linesOut: 0, bytesIn: 0, bytesOut: 0 };
1914
+ throw storeError(`gain ledger ${this.gainPath} could not be read`, error);
1915
+ }
1916
+ let runs = 0;
1917
+ let linesIn = 0;
1918
+ let linesOut = 0;
1919
+ let bytesIn = 0;
1920
+ let bytesOut = 0;
1921
+ for (const line of raw.split("\n")) {
1922
+ if (line.trim().length === 0) continue;
1923
+ const entry = parseGainEntry(line);
1924
+ if (entry === null) continue;
1925
+ runs += 1;
1926
+ linesIn += entry.linesIn;
1927
+ linesOut += entry.linesOut;
1928
+ bytesIn += entry.bytesIn;
1929
+ bytesOut += entry.bytesOut;
1930
+ }
1931
+ return { runs, linesIn, linesOut, bytesIn, bytesOut };
1932
+ }
1933
+ async enforceRetention() {
1934
+ let names;
1935
+ try {
1936
+ names = await readdir(this.runsDir);
1937
+ } catch (error) {
1938
+ if (errorCode(error) === "ENOENT") return;
1939
+ throw storeError(`run directory ${this.runsDir} could not be read`, error);
1940
+ }
1941
+ const ids = [...new Set(names.filter((name) => name.endsWith(".log")).map((name) => name.slice(0, -4)))].filter((id) => RUN_ID_PATTERN.test(id)).sort();
1942
+ const sizes = /* @__PURE__ */ new Map();
1943
+ let total = 0;
1944
+ for (const id of ids) {
1945
+ const bytes = await this.#sizeOf(this.logPath(id)) + await this.#sizeOf(this.metaPath(id));
1946
+ sizes.set(id, bytes);
1947
+ total += bytes;
1948
+ }
1949
+ let count = ids.length;
1950
+ for (const id of ids) {
1951
+ if (count <= this.#retention.maxRuns && total <= this.#retention.maxBytes) break;
1952
+ await this.discardRun(id);
1953
+ total -= sizes.get(id) ?? 0;
1954
+ count -= 1;
1955
+ }
1956
+ }
1957
+ async discardRun(id) {
1958
+ for (const path of [this.logPath(id), this.metaPath(id)]) {
1959
+ try {
1960
+ await unlink(path);
1961
+ } catch (error) {
1962
+ if (errorCode(error) === "ENOENT") continue;
1963
+ throw storeError(`run file ${path} could not be deleted`, error);
1964
+ }
1965
+ }
1966
+ }
1967
+ async #sizeOf(path) {
1968
+ try {
1969
+ return (await stat(path)).size;
1970
+ } catch (error) {
1971
+ if (errorCode(error) === "ENOENT") return 0;
1972
+ throw storeError(`run file ${path} could not be inspected`, error);
1973
+ }
1974
+ }
1975
+ async #readMeta(id) {
1976
+ const path = this.metaPath(id);
1977
+ let raw;
1978
+ try {
1979
+ raw = await readFile2(path, "utf8");
1980
+ } catch (error) {
1981
+ if (errorCode(error) === "ENOENT") return null;
1982
+ throw storeError(`run meta ${path} could not be read`, error);
1983
+ }
1984
+ let parsed;
1985
+ try {
1986
+ parsed = JSON.parse(raw);
1987
+ } catch (error) {
1988
+ throw storeError(`run meta ${path} is not valid JSON`, error);
1989
+ }
1990
+ return isRecord4(parsed) ? parsed : null;
1991
+ }
1992
+ async #ensureDir(path) {
1993
+ try {
1994
+ await mkdir(path, { recursive: true, mode: DIR_MODE });
1995
+ } catch (error) {
1996
+ throw storeError(`directory ${path} could not be created`, error);
1997
+ }
1998
+ }
1999
+ };
2000
+ function requireRunId(id) {
2001
+ if (!RUN_ID_PATTERN.test(id)) throw new RunStoreError(`"${id}" is not a run id`);
2002
+ return id;
2003
+ }
2004
+ function parseGainEntry(line) {
2005
+ let parsed;
2006
+ try {
2007
+ parsed = JSON.parse(line);
2008
+ } catch {
2009
+ return null;
2010
+ }
2011
+ if (!isRecord4(parsed)) return null;
2012
+ const linesIn = parsed["linesIn"];
2013
+ const linesOut = parsed["linesOut"];
2014
+ const bytesIn = parsed["bytesIn"];
2015
+ const bytesOut = parsed["bytesOut"];
2016
+ if (typeof linesIn !== "number" || typeof linesOut !== "number" || typeof bytesIn !== "number" || typeof bytesOut !== "number") {
2017
+ return null;
2018
+ }
2019
+ return parsed;
2020
+ }
2021
+ function isRecord4(value) {
2022
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2023
+ }
2024
+ function storeError(message, error) {
2025
+ const code = errorCode(error);
2026
+ return new RunStoreError(`${message}: ${errorMessage(error)}`, {
2027
+ ...code !== void 0 ? { code } : {},
2028
+ cause: error
2029
+ });
2030
+ }
2031
+
2032
+ // src/prune.ts
2033
+ async function pruneOutput(input) {
2034
+ const env = input.env ?? process.env;
2035
+ const config = mergeConfig(await loadConfig(env), input.config);
2036
+ const client = input.client !== void 0 ? input.client : clientFromEnv(env);
2037
+ const runId = newRunId();
2038
+ const command = input.command ?? "";
2039
+ const store = input.save === false ? null : new RunStore({ home: config.home, retention: config.retention });
2040
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2041
+ const selection = await selectLines({
2042
+ text: input.text,
2043
+ task: input.task,
2044
+ command,
2045
+ exitCode: input.exitCode ?? null,
2046
+ client,
2047
+ config,
2048
+ runId
2049
+ });
2050
+ const recorded = await recordRun({
2051
+ store,
2052
+ selection,
2053
+ logText: input.text,
2054
+ meta: {
2055
+ id: runId,
2056
+ command,
2057
+ argv: [],
2058
+ startedAt,
2059
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2060
+ exitCode: input.exitCode ?? null,
2061
+ signal: null,
2062
+ bytes: selection.bytesIn,
2063
+ lines: selection.linesIn,
2064
+ task: input.task
2065
+ }
2066
+ });
2067
+ return {
2068
+ kept: selection.kept,
2069
+ dropped: selection.dropped,
2070
+ runId,
2071
+ mode: selection.mode,
2072
+ linesIn: selection.linesIn,
2073
+ linesOut: selection.linesOut,
2074
+ ...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {},
2075
+ ...recorded.logPath !== void 0 ? { logPath: recorded.logPath } : {},
2076
+ footer: recorded.footer
2077
+ };
2078
+ }
2079
+ async function pruneStream(input) {
2080
+ const { stream, ...rest } = input;
2081
+ return await pruneOutput({ ...rest, text: await readStream(stream) });
2082
+ }
2083
+ async function recordRun(input) {
2084
+ const { store, selection, meta } = input;
2085
+ const fastPath = selection.mode === "fast-path";
2086
+ const log = input.logBytes ?? (input.logText !== void 0 ? Buffer.from(input.logText, "utf8") : void 0);
2087
+ let failureCode = input.storeFailureCode;
2088
+ if (store !== null && failureCode === void 0) {
2089
+ try {
2090
+ if (fastPath) {
2091
+ if (log === void 0) await store.discardRun(meta.id);
2092
+ } else {
2093
+ if (log !== void 0) await writeLog(store, meta.id, log);
2094
+ await store.finalizeRun(meta.id, {
2095
+ ...meta,
2096
+ mode: selection.mode,
2097
+ linesOut: selection.linesOut,
2098
+ ...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {}
2099
+ });
2100
+ }
2101
+ await store.appendGain({
2102
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2103
+ id: meta.id,
2104
+ mode: selection.mode,
2105
+ linesIn: selection.linesIn,
2106
+ linesOut: selection.linesOut,
2107
+ bytesIn: selection.bytesIn,
2108
+ bytesOut: selection.bytesOut,
2109
+ ...selection.fallbackReason !== void 0 ? { reason: selection.fallbackReason } : {}
2110
+ });
2111
+ await store.enforceRetention();
2112
+ } catch (error) {
2113
+ if (!(error instanceof RunStoreError)) throw error;
2114
+ failureCode = error.code ?? "failed";
2115
+ }
2116
+ }
2117
+ const logPath = store !== null && !fastPath && failureCode === void 0 ? store.logPath(meta.id) : void 0;
2118
+ const footer = formatFooter({
2119
+ mode: selection.mode,
2120
+ linesIn: selection.linesIn,
2121
+ linesOut: selection.linesOut,
2122
+ exitCode: meta.exitCode,
2123
+ ...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {},
2124
+ ...input.passthroughNote !== void 0 ? { passthroughNote: input.passthroughNote } : {},
2125
+ ...failureCode !== void 0 ? { storeFailureCode: failureCode } : {},
2126
+ ...logPath !== void 0 ? { logPath } : {}
2127
+ });
2128
+ return { footer, ...logPath !== void 0 ? { logPath } : {} };
2129
+ }
2130
+ function clientFromEnv(env) {
2131
+ try {
2132
+ return createJevClientFromEnv(env);
2133
+ } catch (error) {
2134
+ if (error instanceof JevConfigError) return null;
2135
+ throw error;
2136
+ }
2137
+ }
2138
+ function mergeConfig(base, overrides) {
2139
+ if (overrides === void 0) return base;
2140
+ const defined = {};
2141
+ for (const key of Object.keys(overrides)) {
2142
+ if (overrides[key] !== void 0) Object.assign(defined, { [key]: overrides[key] });
2143
+ }
2144
+ return { ...base, ...defined };
2145
+ }
2146
+ async function writeLog(store, id, bytes) {
2147
+ const writer = await store.openRun({ id });
2148
+ writer.write(bytes);
2149
+ await writer.close();
2150
+ if (writer.failure !== void 0) throw writer.failure;
2151
+ }
2152
+
2153
+ // src/task.ts
2154
+ import { readFile as readFile3 } from "fs/promises";
2155
+ var MAX_TASK_LENGTH = 400;
2156
+ async function resolveTask(input) {
2157
+ const flag = input.flag?.trim() ?? "";
2158
+ if (flag.length > 0) return { task: flag, source: "flag" };
2159
+ const fromEnv = input.env?.[TASK_ENV]?.trim() ?? "";
2160
+ if (fromEnv.length > 0) return { task: fromEnv, source: "env" };
2161
+ if (input.transcriptPath !== void 0 && input.transcriptPath.length > 0) {
2162
+ const fromTranscript = await readTranscriptTask(input.transcriptPath).catch((error) => {
2163
+ if (error instanceof TranscriptError) return null;
2164
+ throw error;
2165
+ });
2166
+ if (fromTranscript !== null) return { task: fromTranscript, source: "transcript" };
2167
+ }
2168
+ return { task: input.command, source: "command" };
2169
+ }
2170
+ async function readTranscriptTask(path) {
2171
+ let raw;
2172
+ try {
2173
+ raw = await readFile3(path, "utf8");
2174
+ } catch (error) {
2175
+ throw new TranscriptError(`transcript ${path} could not be read: ${errorMessage(error)}`, {
2176
+ path,
2177
+ cause: error
2178
+ });
2179
+ }
2180
+ for (const line of raw.split("\n")) {
2181
+ if (line.trim().length === 0) continue;
2182
+ let parsed;
2183
+ try {
2184
+ parsed = JSON.parse(line);
2185
+ } catch {
2186
+ continue;
2187
+ }
2188
+ const text = userText(parsed);
2189
+ if (text !== null) return text;
2190
+ }
2191
+ return null;
2192
+ }
2193
+ function userText(entry) {
2194
+ if (!isRecord5(entry) || entry["type"] !== "user") return null;
2195
+ const message = entry["message"];
2196
+ if (!isRecord5(message)) return null;
2197
+ const content = message["content"];
2198
+ if (typeof content === "string") return usableText(content);
2199
+ if (!Array.isArray(content)) return null;
2200
+ for (const item of content) {
2201
+ if (!isRecord5(item) || item["type"] !== "text") continue;
2202
+ const text = item["text"];
2203
+ if (typeof text !== "string") continue;
2204
+ const usable = usableText(text);
2205
+ if (usable !== null) return usable;
2206
+ }
2207
+ return null;
2208
+ }
2209
+ function usableText(text) {
2210
+ const trimmed = text.trim();
2211
+ if (trimmed.length === 0 || trimmed.startsWith("<")) return null;
2212
+ return trimmed.slice(0, MAX_TASK_LENGTH);
2213
+ }
2214
+ function isRecord5(value) {
2215
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2216
+ }
2217
+ export {
2218
+ CHARS_PER_TOKEN,
2219
+ ConfigError,
2220
+ DEFAULT_ALLOWLIST,
2221
+ DEFAULT_CONFIG,
2222
+ DEFAULT_JEV_MAX_RETRIES,
2223
+ DEFAULT_JEV_MODEL,
2224
+ DEFAULT_JEV_TIMEOUT_MS,
2225
+ DEFAULT_WINDOW_CONCURRENCY,
2226
+ DEFAULT_WINDOW_TIMEOUT_MS,
2227
+ DEFAULT_WINDOW_TOKENS,
2228
+ FakeJevClient,
2229
+ JevAbortError,
2230
+ JevBudgetError,
2231
+ JevConfigError,
2232
+ JevCoreError,
2233
+ JevInputError,
2234
+ JevRequestError,
2235
+ JevResponseError,
2236
+ JevTimeoutError,
2237
+ JevpruneError,
2238
+ LineRangeError,
2239
+ MAX_REQUEST_TOKENS,
2240
+ MAX_TASK_LENGTH,
2241
+ NOT_UTF8_NOTE,
2242
+ NOT_UTF8_REASON,
2243
+ RUBRIC,
2244
+ RunNotFoundError,
2245
+ RunStore,
2246
+ RunStoreError,
2247
+ SIGNATURE_CASE_INSENSITIVE,
2248
+ SIGNATURE_CASE_SENSITIVE,
2249
+ SpawnError,
2250
+ TYPESAFE_API_KEY_ENV,
2251
+ TYPESAFE_BASE_URL_ENV,
2252
+ TranscriptError,
2253
+ TypeSafeJevClient,
2254
+ UsageError,
2255
+ byteLineStarts,
2256
+ collapseMarker,
2257
+ computeKeeps,
2258
+ countByteLines,
2259
+ createJevClientFromEnv,
2260
+ describeError,
2261
+ displayPath,
2262
+ estimateJsonTokens,
2263
+ estimateTokens,
2264
+ footerAfter,
2265
+ formatCount,
2266
+ formatFooter,
2267
+ isSignatureLine,
2268
+ isValidUtf8,
2269
+ joinLines,
2270
+ loadConfig,
2271
+ mergeDecisions,
2272
+ newRunId,
2273
+ parseThreshold,
2274
+ passthroughSelection,
2275
+ planWindows,
2276
+ pruneOutput,
2277
+ pruneStream,
2278
+ questionFor,
2279
+ resolveHome,
2280
+ resolveTask,
2281
+ runWindows,
2282
+ selectLines,
2283
+ splitLines,
2284
+ toJevError,
2285
+ validateChoiceAnswers,
2286
+ validateNoulAnswers,
2287
+ withFooter
2288
+ };