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/cli.js ADDED
@@ -0,0 +1,2969 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { realpathSync } from "fs";
5
+ import { pathToFileURL } from "url";
6
+ import { parseArgs } from "util";
7
+
8
+ // src/config.ts
9
+ import { readFile } from "fs/promises";
10
+ import { homedir } from "os";
11
+ import { join, resolve } from "path";
12
+
13
+ // src/core/errors.ts
14
+ var JevCoreError = class extends Error {
15
+ name = "JevCoreError";
16
+ constructor(message, options) {
17
+ super(message, options);
18
+ }
19
+ };
20
+ var JevConfigError = class extends JevCoreError {
21
+ name = "JevConfigError";
22
+ };
23
+ var JevInputError = class extends JevCoreError {
24
+ name = "JevInputError";
25
+ };
26
+ var JevBudgetError = class extends JevCoreError {
27
+ name = "JevBudgetError";
28
+ };
29
+ var JevRequestError = class extends JevCoreError {
30
+ name = "JevRequestError";
31
+ status;
32
+ retryable;
33
+ requestId;
34
+ constructor(message, details) {
35
+ super(message, { cause: details.cause });
36
+ this.status = details.status;
37
+ this.retryable = details.retryable;
38
+ this.requestId = details.requestId;
39
+ }
40
+ };
41
+ var JevTimeoutError = class extends JevCoreError {
42
+ name = "JevTimeoutError";
43
+ timeoutMs;
44
+ constructor(timeoutMs, message, options) {
45
+ super(message ?? `Jev request exceeded ${String(timeoutMs)} ms`, options);
46
+ this.timeoutMs = timeoutMs;
47
+ }
48
+ };
49
+ var JevAbortError = class extends JevCoreError {
50
+ name = "JevAbortError";
51
+ constructor(message = "Jev request aborted", options) {
52
+ super(message, options);
53
+ }
54
+ };
55
+ var JevResponseError = class extends JevCoreError {
56
+ name = "JevResponseError";
57
+ };
58
+ function describeError(error) {
59
+ if (error instanceof Error) return error.message;
60
+ if (typeof error === "string") return error;
61
+ return String(error);
62
+ }
63
+
64
+ // node_modules/.pnpm/@typesafe-ai+sdk@0.6.0/node_modules/@typesafe-ai/sdk/dist/index.mjs
65
+ var requestIdFrom = (headers) => headers.get("x-typesafe-request-id") ?? void 0;
66
+ var APIPromise = class APIPromise2 extends Promise {
67
+ #responsePromise;
68
+ #parseResponse;
69
+ #parsed;
70
+ constructor(responsePromise, parseResponse) {
71
+ super((resolve2) => resolve2(void 0));
72
+ this.#responsePromise = responsePromise;
73
+ this.#parseResponse = parseResponse;
74
+ }
75
+ /**
76
+ * Resolves to the raw `Response` without parsing the body. SDK requests buffer the full
77
+ * body under the request timeout before handoff; reading it afterwards is caller-owned.
78
+ * The caller owns the body; don't also `await` the parsed result on the same promise.
79
+ */
80
+ asResponse() {
81
+ return this.#responsePromise;
82
+ }
83
+ /** Return the parsed result, HTTP response, and request ID. */
84
+ async withResponse() {
85
+ const [data, response] = await Promise.all([this.#parse(), this.#responsePromise]);
86
+ return {
87
+ data,
88
+ response,
89
+ requestId: requestIdFrom(response.headers)
90
+ };
91
+ }
92
+ /** Transform the parsed result, sharing the HTTP response and a single body parse. */
93
+ map(fn) {
94
+ return new APIPromise2(this.#responsePromise, () => this.#parse().then(fn));
95
+ }
96
+ #parse() {
97
+ this.#parsed ??= this.#responsePromise.then(this.#parseResponse);
98
+ return this.#parsed;
99
+ }
100
+ then(onfulfilled, onrejected) {
101
+ return this.#parse().then(onfulfilled, onrejected);
102
+ }
103
+ catch(onrejected) {
104
+ return this.#parse().catch(onrejected);
105
+ }
106
+ finally(onfinally) {
107
+ return this.#parse().finally(onfinally);
108
+ }
109
+ };
110
+ var ENV = {
111
+ /** Required API key; used when `apiKey` is omitted. */
112
+ apiKey: "TYPESAFE_API_KEY",
113
+ /** API root; defaults to `https://api.typesafe.ai`. */
114
+ baseURL: "TYPESAFE_BASE_URL",
115
+ /** Default model name; defaults to `jev-latest`. */
116
+ defaultModel: "TYPESAFE_DEFAULT_MODEL",
117
+ /** Log level; defaults to `warn`. */
118
+ logLevel: "TYPESAFE_LOG_LEVEL"
119
+ };
120
+ var readEnv = (name) => {
121
+ if (typeof process === "undefined" || !process.env) return void 0;
122
+ return process.env[name]?.trim() || void 0;
123
+ };
124
+ var fromCodeOrEnv = (fromCode, envVar) => fromCode ?? readEnv(envVar);
125
+ var range = (from, to) => Array.from({ length: to - from }, (_, i) => from + i);
126
+ var DEFAULT_RETRY_POLICY = {
127
+ maxRetries: 2,
128
+ backoffInitialMs: 500,
129
+ backoffMaxMs: 5e3,
130
+ backoffJitter: 0.25,
131
+ /** HTTP 408, 429, and 5xx responses. */
132
+ httpStatuses: /* @__PURE__ */ new Set([
133
+ 408,
134
+ 429,
135
+ ...range(500, 600)
136
+ ]),
137
+ respectRetryAfter: true,
138
+ /** Maximum server retry delay before falling back to backoff. */
139
+ maxRetryAfterMs: 6e4,
140
+ apiConnectionError: true,
141
+ apiTimeoutError: true
142
+ };
143
+ DEFAULT_RETRY_POLICY.maxRetries;
144
+ var isRetryableStatus = (status, policy = DEFAULT_RETRY_POLICY) => policy.httpStatuses.has(status);
145
+ var parseRetryAfter = (headers, now = Date.now()) => {
146
+ const ms = Number(headers.get("retry-after-ms"));
147
+ if (headers.has("retry-after-ms") && Number.isFinite(ms) && ms >= 0) return ms;
148
+ const raw = headers.get("retry-after");
149
+ if (raw === null) return void 0;
150
+ const seconds = Number(raw);
151
+ if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1e3 : void 0;
152
+ const date = Date.parse(raw);
153
+ if (!Number.isNaN(date)) return Math.max(0, date - now);
154
+ };
155
+ var retryDelayMs = (attempt, headers, policy = DEFAULT_RETRY_POLICY, random = Math.random) => {
156
+ if (policy.respectRetryAfter && headers !== void 0) {
157
+ const retryAfter = parseRetryAfter(headers);
158
+ if (retryAfter !== void 0 && retryAfter <= policy.maxRetryAfterMs) return retryAfter;
159
+ }
160
+ const exponential = Math.min(policy.backoffInitialMs * 2 ** attempt, policy.backoffMaxMs);
161
+ return Math.round(exponential * (1 - random() * policy.backoffJitter));
162
+ };
163
+ var sleep = (ms, signal) => new Promise((resolve2, reject) => {
164
+ if (signal?.aborted) return reject(signal.reason);
165
+ const onAbort = () => {
166
+ clearTimeout(timer);
167
+ reject(signal?.reason);
168
+ };
169
+ const timer = setTimeout(() => {
170
+ signal?.removeEventListener("abort", onAbort);
171
+ resolve2();
172
+ }, ms);
173
+ signal?.addEventListener("abort", onAbort, { once: true });
174
+ });
175
+ var TypeSafeError = class extends Error {
176
+ constructor(message, options) {
177
+ super(message, options);
178
+ this.name = new.target.name;
179
+ }
180
+ };
181
+ var isRecord = (value) => typeof value === "object" && value !== null;
182
+ var extractMessage = (body) => {
183
+ if (typeof body === "string") return body || void 0;
184
+ if (!isRecord(body)) return void 0;
185
+ const { error, message, detail } = body;
186
+ if (typeof error === "string") return error;
187
+ if (isRecord(error) && typeof error.message === "string") return error.message;
188
+ if (typeof message === "string") return message;
189
+ if (typeof detail === "string") return detail;
190
+ if (isRecord(detail) && typeof detail.message === "string") return detail.message;
191
+ if (Array.isArray(detail)) return describeValidationErrors(detail);
192
+ };
193
+ var describeValidationErrors = (errors) => {
194
+ const parts = errors.flatMap((e) => {
195
+ if (!isRecord(e) || typeof e.msg !== "string") return [];
196
+ const loc = Array.isArray(e.loc) ? e.loc.filter((x) => x !== "body").join(".") : "";
197
+ return [loc ? `${loc}: ${e.msg}` : e.msg];
198
+ });
199
+ return parts.length > 0 ? parts.join("; ") : void 0;
200
+ };
201
+ var MAX_RAW_BODY_IN_MESSAGE = 200;
202
+ var APIError = class APIError2 extends TypeSafeError {
203
+ /** HTTP response status code. */
204
+ status;
205
+ /** HTTP response headers. */
206
+ headers;
207
+ /** Parsed JSON, response text, or `undefined` for an empty body. */
208
+ body;
209
+ /** Request ID from `x-typesafe-request-id`, or `undefined` when absent. */
210
+ requestId;
211
+ constructor(status, body, headers, message) {
212
+ super(message ?? APIError2.describe(status, body));
213
+ this.status = status;
214
+ this.body = body;
215
+ this.headers = headers;
216
+ this.requestId = requestIdFrom(headers);
217
+ }
218
+ static describe(status, body) {
219
+ const detail = extractMessage(body);
220
+ if (detail) return `${status} ${detail}`;
221
+ if (body === void 0) return `${status} status code (no body)`;
222
+ const raw = typeof body === "string" ? body : JSON.stringify(body);
223
+ return `${status} ${raw.length > MAX_RAW_BODY_IN_MESSAGE ? `${raw.slice(0, MAX_RAW_BODY_IN_MESSAGE)}\u2026` : raw}`;
224
+ }
225
+ /** Create the error subclass for an HTTP status code. */
226
+ static fromResponse(status, body, headers) {
227
+ if (status === 400) return new BadRequestError(status, body, headers);
228
+ if (status === 401) return new AuthenticationError(status, body, headers);
229
+ if (status === 403) return new PermissionDeniedError(status, body, headers);
230
+ if (status === 404) return new NotFoundError(status, body, headers);
231
+ if (status === 422) return new UnprocessableEntityError(status, body, headers);
232
+ if (status === 429) return new RateLimitError(status, body, headers);
233
+ if (status >= 500) return new InternalServerError(status, body, headers);
234
+ return new APIError2(status, body, headers);
235
+ }
236
+ };
237
+ var BadRequestError = class extends APIError {
238
+ };
239
+ var AuthenticationError = class extends APIError {
240
+ };
241
+ var PermissionDeniedError = class extends APIError {
242
+ };
243
+ var NotFoundError = class extends APIError {
244
+ };
245
+ var UnprocessableEntityError = class extends APIError {
246
+ };
247
+ var RateLimitError = class extends APIError {
248
+ /** Server retry delay in milliseconds, or `undefined` when absent or invalid. */
249
+ retryAfterMs = parseRetryAfter(this.headers);
250
+ };
251
+ var InternalServerError = class extends APIError {
252
+ };
253
+ var APIConnectionError = class extends TypeSafeError {
254
+ constructor(message = "Connection error.", options) {
255
+ super(message, options);
256
+ }
257
+ };
258
+ var APITimeoutError = class extends APIConnectionError {
259
+ /** Configured timeout in milliseconds. */
260
+ timeoutMs;
261
+ constructor(timeoutMs, options) {
262
+ super(`Request timed out after ${timeoutMs}ms.`, options);
263
+ this.timeoutMs = timeoutMs;
264
+ }
265
+ };
266
+ var APIUserAbortError = class extends TypeSafeError {
267
+ constructor(message = "Request was aborted.", options) {
268
+ super(message, options);
269
+ }
270
+ };
271
+ var LOG_LEVELS = [
272
+ "debug",
273
+ "info",
274
+ "warn",
275
+ "error",
276
+ "off"
277
+ ];
278
+ var DEFAULT_LOG_LEVEL = "warn";
279
+ var isLogLevel = (value) => LOG_LEVELS.includes(value);
280
+ var parseLogLevel = (value, source) => {
281
+ if (isLogLevel(value)) return value;
282
+ throw new TypeSafeError(`Invalid log level "${value}" from ${source}. Expected one of: ${LOG_LEVELS.join(", ")}.`);
283
+ };
284
+ var PREFIX = "[typesafe-sdk]";
285
+ var consoleLogger = {
286
+ debug: (message, ...args) => console.debug(`${PREFIX} ${message}`, ...args),
287
+ info: (message, ...args) => console.info(`${PREFIX} ${message}`, ...args),
288
+ warn: (message, ...args) => console.warn(`${PREFIX} ${message}`, ...args),
289
+ error: (message, ...args) => console.error(`${PREFIX} ${message}`, ...args)
290
+ };
291
+ var RANK = {
292
+ debug: 0,
293
+ info: 1,
294
+ warn: 2,
295
+ error: 3,
296
+ off: 4
297
+ };
298
+ var drop = () => {
299
+ };
300
+ var withLevel = (sink, level) => {
301
+ const enabled = (at) => RANK[at] >= RANK[level];
302
+ return {
303
+ debug: enabled("debug") ? (message, ...args) => sink.debug(message, ...args) : drop,
304
+ info: enabled("info") ? (message, ...args) => sink.info(message, ...args) : drop,
305
+ warn: enabled("warn") ? (message, ...args) => sink.warn(message, ...args) : drop,
306
+ error: enabled("error") ? (message, ...args) => sink.error(message, ...args) : drop
307
+ };
308
+ };
309
+ var KEY_HEADERS = /* @__PURE__ */ new Set([
310
+ "authorization",
311
+ "proxy-authorization",
312
+ "x-api-key"
313
+ ]);
314
+ var OPAQUE_HEADERS = /* @__PURE__ */ new Set(["cookie", "set-cookie"]);
315
+ var redactKey = (value) => {
316
+ const [scheme, secret] = value.includes(" ") ? value.split(/\s+/, 2) : [void 0, value];
317
+ const tail = secret && secret.length > 8 ? secret.slice(-4) : "";
318
+ return `${scheme ? `${scheme} ` : ""}***${tail}`;
319
+ };
320
+ var redact = (name, value) => {
321
+ const lower = name.toLowerCase();
322
+ if (KEY_HEADERS.has(lower)) return redactKey(value);
323
+ if (OPAQUE_HEADERS.has(lower)) return "***";
324
+ return value;
325
+ };
326
+ var redactHeaders = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, redact(name, value)]));
327
+ var noul = (instructions = null, criteria) => ({
328
+ type: "noul",
329
+ instructions,
330
+ criteria
331
+ });
332
+ var choice = (instructions, criteria) => {
333
+ if (Array.isArray(criteria)) throw new TypeSafeError("Choice criteria must be a map of labels to descriptions, not a list.");
334
+ return {
335
+ type: "choice",
336
+ instructions,
337
+ criteria
338
+ };
339
+ };
340
+ var validateQuestions = (questions) => {
341
+ if (Object.keys(questions).length === 0) throw new TypeSafeError("At least one question is required.");
342
+ for (const [name, question] of Object.entries(questions)) {
343
+ if (question.type !== "score") continue;
344
+ 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.`);
345
+ if (question.criteria.length < 2) throw new TypeSafeError(`Score question "${name}" has ${question.criteria.length} criteria; at least two scores are required.`);
346
+ }
347
+ };
348
+ var Models = class {
349
+ #transport;
350
+ constructor(transport) {
351
+ this.#transport = transport;
352
+ }
353
+ /** List the models available to the account. */
354
+ list(options = {}) {
355
+ return this.#transport.request("GET", "/v1/models", options).map(unwrapModels);
356
+ }
357
+ };
358
+ var unwrapModels = (wire) => {
359
+ if (Array.isArray(wire?.models)) return wire.models;
360
+ throw new TypeSafeError("Unexpected response shape from GET /v1/models; expected { models: [...] }.");
361
+ };
362
+ var g = globalThis;
363
+ var isBrowser = () => typeof g.window !== "undefined" && typeof g.window.document !== "undefined" && typeof g.navigator !== "undefined";
364
+ var describeRuntime = () => {
365
+ const platform = g.process?.platform && g.process?.arch ? ` (${g.process.platform}; ${g.process.arch})` : "";
366
+ if (g.Bun?.version) return `bun/${g.Bun.version}${platform}`;
367
+ if (g.Deno?.version?.deno) return `deno/${g.Deno.version.deno}${platform}`;
368
+ if (g.EdgeRuntime !== void 0) return "vercel-edge";
369
+ if (g.navigator?.userAgent === "Cloudflare-Workers") return "cloudflare-workers";
370
+ if (g.process?.versions?.node) return `node/${g.process.versions.node}${platform}`;
371
+ if (isBrowser()) return "browser";
372
+ return "unknown";
373
+ };
374
+ var VERSION = "0.6.0";
375
+ var missingApiKey = () => {
376
+ throw new TypeSafeError(`No API key was provided. Pass \`apiKey\` to the TypeSafeClient constructor or set the ${ENV.apiKey} environment variable.`);
377
+ };
378
+ var missingFetch = () => {
379
+ throw new TypeSafeError("No global `fetch` is available in this runtime. Pass a `fetch` implementation to the TypeSafeClient constructor.");
380
+ };
381
+ var refuseBrowser = () => {
382
+ 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.");
383
+ };
384
+ var defaultFetch = (input, init) => globalThis.fetch(input, init);
385
+ var assertNonNegativeInteger = (name, value) => {
386
+ if (!Number.isInteger(value) || value < 0) throw new TypeSafeError(`\`${name}\` must be a non-negative integer, got ${String(value)}.`);
387
+ return value;
388
+ };
389
+ var assertPositiveMs = (name, value) => {
390
+ if (!Number.isFinite(value) || value <= 0) throw new TypeSafeError(`\`${name}\` must be a positive number of milliseconds, got ${String(value)}.`);
391
+ return value;
392
+ };
393
+ var assertNonNegativeMs = (name, value) => {
394
+ if (!Number.isFinite(value) || value < 0) throw new TypeSafeError(`\`${name}\` must be a non-negative number of milliseconds, got ${String(value)}.`);
395
+ return value;
396
+ };
397
+ var assertFraction = (name, value) => {
398
+ if (!Number.isFinite(value) || value < 0 || value > 1) throw new TypeSafeError(`\`${name}\` must be between 0 and 1, got ${String(value)}.`);
399
+ return value;
400
+ };
401
+ var assertStatusSet = (name, statuses) => {
402
+ 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)}.`);
403
+ return statuses;
404
+ };
405
+ var resolveRetryPolicy = (base, overrides) => {
406
+ const o = overrides ?? {};
407
+ return {
408
+ maxRetries: o.maxRetries === void 0 ? base.maxRetries : assertNonNegativeInteger("retry.maxRetries", o.maxRetries),
409
+ backoffInitialMs: o.backoffInitialMs === void 0 ? base.backoffInitialMs : assertNonNegativeMs("retry.backoffInitialMs", o.backoffInitialMs),
410
+ backoffMaxMs: o.backoffMaxMs === void 0 ? base.backoffMaxMs : assertNonNegativeMs("retry.backoffMaxMs", o.backoffMaxMs),
411
+ backoffJitter: o.backoffJitter === void 0 ? base.backoffJitter : assertFraction("retry.backoffJitter", o.backoffJitter),
412
+ httpStatuses: new Set(o.httpStatuses === void 0 ? base.httpStatuses : assertStatusSet("retry.httpStatuses", o.httpStatuses)),
413
+ respectRetryAfter: o.respectRetryAfter ?? base.respectRetryAfter,
414
+ maxRetryAfterMs: o.maxRetryAfterMs === void 0 ? base.maxRetryAfterMs : assertNonNegativeMs("retry.maxRetryAfterMs", o.maxRetryAfterMs),
415
+ apiConnectionError: o.apiConnectionError ?? base.apiConnectionError,
416
+ apiTimeoutError: o.apiTimeoutError ?? base.apiTimeoutError
417
+ };
418
+ };
419
+ var isRetryableError = (err, policy) => {
420
+ if (err instanceof APITimeoutError) return policy.apiTimeoutError;
421
+ if (err instanceof APIConnectionError) return policy.apiConnectionError;
422
+ return false;
423
+ };
424
+ var resolveLogLevel = (fromCode) => {
425
+ if (fromCode !== void 0) return parseLogLevel(fromCode, "the `logLevel` option");
426
+ const fromEnv = readEnv(ENV.logLevel);
427
+ if (fromEnv !== void 0) return parseLogLevel(fromEnv, ENV.logLevel);
428
+ return DEFAULT_LOG_LEVEL;
429
+ };
430
+ var stripTrailingSlashes = (url) => url.replace(/\/+$/, "");
431
+ var mergeHeaders = (...sources) => {
432
+ const entries = /* @__PURE__ */ new Map();
433
+ for (const source of sources) for (const [name, value] of Object.entries(source)) if (value === void 0) entries.delete(name.toLowerCase());
434
+ else entries.set(name.toLowerCase(), [name, value]);
435
+ return Object.fromEntries(entries.values());
436
+ };
437
+ var bufferResponse = async (response, signal) => {
438
+ const reader = response.clone().body?.getReader();
439
+ if (!reader) return;
440
+ const cancel = () => {
441
+ reader.cancel(signal.reason).catch(() => {
442
+ });
443
+ response.body?.cancel(signal.reason).catch(() => {
444
+ });
445
+ };
446
+ signal.addEventListener("abort", cancel, { once: true });
447
+ try {
448
+ if (signal.aborted) cancel();
449
+ signal.throwIfAborted();
450
+ while (!(await reader.read()).done) signal.throwIfAborted();
451
+ signal.throwIfAborted();
452
+ } finally {
453
+ signal.removeEventListener("abort", cancel);
454
+ reader.releaseLock();
455
+ }
456
+ };
457
+ var RUNTIME = describeRuntime();
458
+ var TypeSafeClient = class {
459
+ /** API key excluded from serialization and public properties. */
460
+ #apiKey;
461
+ /** API root with trailing slashes removed. */
462
+ baseURL;
463
+ /** Model used when a request omits `model`. */
464
+ defaultModel;
465
+ /** Configured log verbosity. */
466
+ logLevel;
467
+ /** The configured logger, filtered to `logLevel`. */
468
+ logger;
469
+ /** Retry settings with constructor overrides applied. */
470
+ retry;
471
+ /** Timeout per attempt in milliseconds. */
472
+ timeout;
473
+ /** Additional headers sent with each request. */
474
+ defaultHeaders;
475
+ /** HTTP fetch implementation. */
476
+ fetch;
477
+ /** The models available to the account. */
478
+ models;
479
+ #requestCount = 0;
480
+ /**
481
+ * Create a client for the TypeSafe AI API.
482
+ *
483
+ * Explicit options take precedence over environment variables, then SDK defaults.
484
+ * Empty or whitespace-only environment values are ignored.
485
+ *
486
+ * @throws {TypeSafeError} The API key is missing, configuration is invalid, or the runtime is unsupported.
487
+ */
488
+ constructor(config = {}) {
489
+ if (isBrowser() && !config.dangerouslyAllowBrowser) refuseBrowser();
490
+ this.#apiKey = fromCodeOrEnv(config.apiKey, ENV.apiKey) ?? missingApiKey();
491
+ this.baseURL = stripTrailingSlashes(fromCodeOrEnv(config.baseURL, ENV.baseURL) ?? "https://api.typesafe.ai");
492
+ this.defaultModel = fromCodeOrEnv(config.defaultModel, ENV.defaultModel) ?? "jev-latest";
493
+ this.logLevel = resolveLogLevel(config.logLevel);
494
+ this.logger = withLevel(config.logger ?? consoleLogger, this.logLevel);
495
+ this.retry = resolveRetryPolicy(DEFAULT_RETRY_POLICY, config.retry);
496
+ this.timeout = assertPositiveMs("timeout", config.timeout ?? 1e4);
497
+ this.defaultHeaders = { ...config.defaultHeaders };
498
+ if (config.fetch === void 0 && typeof globalThis.fetch !== "function") missingFetch();
499
+ this.fetch = config.fetch ?? defaultFetch;
500
+ const transport = {
501
+ request: (method, path, options) => this.#request(method, path, options),
502
+ defaultModel: this.defaultModel
503
+ };
504
+ this.models = new Models(transport);
505
+ }
506
+ /**
507
+ * Answer named questions about text or structured state.
508
+ *
509
+ * @param request - State, questions, and an optional model override.
510
+ * @param options - Per-call timeout, retry, headers, and cancellation settings.
511
+ * @returns Answers typed by question name and criteria, with model and token usage.
512
+ * @throws {TypeSafeError} Questions are empty, or score criteria are not a list of at least two entries.
513
+ * @throws {APIError} The server returns a non-2xx response after retries.
514
+ * @throws {APIConnectionError} The request cannot connect or times out after retries.
515
+ * @throws {APIUserAbortError} The caller aborts the request.
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * const { answers } = await client.systemOne({
520
+ * state: "I was charged twice. Please help.",
521
+ * questions: { billing: noul("Is this about billing?") },
522
+ * });
523
+ * console.log(answers.billing.noul);
524
+ * ```
525
+ */
526
+ systemOne(request, options = {}) {
527
+ validateQuestions(request.questions);
528
+ const body = {
529
+ ...request,
530
+ model: request.model ?? this.defaultModel
531
+ };
532
+ return this.#request("POST", "/v1/systemone", {
533
+ ...options,
534
+ body
535
+ });
536
+ }
537
+ /** Send a request and parse its response body. */
538
+ #request(method, path, options = {}) {
539
+ const resolved = {
540
+ method,
541
+ path,
542
+ body: options.body,
543
+ headers: mergeHeaders(this.defaultHeaders, options.headers ?? {}),
544
+ signal: options.signal,
545
+ timeout: options.timeout === void 0 ? this.timeout : assertPositiveMs("timeout", options.timeout),
546
+ retry: resolveRetryPolicy(this.retry, options.retry)
547
+ };
548
+ const tag = `#${++this.#requestCount} ${method} ${path}`;
549
+ return new APIPromise(this.fetchWithRetries(tag, resolved), async (res) => {
550
+ const parsed = await parseBody(res);
551
+ this.logger.debug(`${tag} <- body`, parsed);
552
+ return parsed;
553
+ });
554
+ }
555
+ /** Retry eligible failures, logging attempt summaries at `info` and headers and bodies at `debug`. */
556
+ async fetchWithRetries(tag, req) {
557
+ const url = `${this.baseURL}${req.path}`;
558
+ const headers = mergeHeaders(req.headers, {
559
+ Authorization: `Bearer ${this.#apiKey}`,
560
+ Accept: "application/json",
561
+ "User-Agent": `typesafe-sdk/${VERSION}`,
562
+ "X-TypeSafe-SDK": `typesafe-sdk/${VERSION}`,
563
+ "X-TypeSafe-Runtime": RUNTIME,
564
+ "Content-Type": req.body === void 0 ? void 0 : "application/json",
565
+ "X-TypeSafe-Retry-Count": void 0
566
+ });
567
+ const body = req.body === void 0 ? void 0 : JSON.stringify(req.body);
568
+ for (let attempt = 0; ; attempt++) {
569
+ const retriesLeft = req.retry.maxRetries - attempt;
570
+ const attemptHeaders = attempt === 0 ? headers : {
571
+ ...headers,
572
+ "X-TypeSafe-Retry-Count": String(attempt)
573
+ };
574
+ this.logger.debug(`${tag} -> ${url}`, {
575
+ headers: redactHeaders(attemptHeaders),
576
+ body: req.body
577
+ });
578
+ const started = Date.now();
579
+ let res;
580
+ try {
581
+ res = await this.attempt(tag, url, {
582
+ method: req.method,
583
+ headers: attemptHeaders,
584
+ body
585
+ }, req);
586
+ } catch (err) {
587
+ if (err instanceof APIUserAbortError || retriesLeft <= 0) throw err;
588
+ if (!isRetryableError(err, req.retry)) throw err;
589
+ await this.backOff(tag, attempt, retriesLeft, err.message, void 0, req);
590
+ continue;
591
+ }
592
+ const requestId = requestIdFrom(res.headers);
593
+ this.logger.info(`${tag} <- ${res.status} in ${Date.now() - started}ms${requestId ? ` (request ${requestId})` : ""}`);
594
+ if (res.ok) return res;
595
+ const errorBody = await parseBody(res);
596
+ this.logger.debug(`${tag} <- error body`, errorBody);
597
+ const error = APIError.fromResponse(res.status, errorBody, res.headers);
598
+ if (retriesLeft <= 0 || !isRetryableStatus(res.status, req.retry)) throw error;
599
+ await this.backOff(tag, attempt, retriesLeft, `${res.status}`, res.headers, req);
600
+ }
601
+ }
602
+ /**
603
+ * One HTTP round trip, including body delivery, with a timeout. The caller's signal and our
604
+ * timer both abort the same controller; we check which fired to choose the error class.
605
+ */
606
+ async attempt(tag, url, init, { signal, timeout }) {
607
+ const controller = new AbortController();
608
+ const abortFromCaller = () => controller.abort(signal?.reason);
609
+ if (signal?.aborted) abortFromCaller();
610
+ signal?.addEventListener("abort", abortFromCaller, { once: true });
611
+ let timedOut = false;
612
+ const timer = setTimeout(() => {
613
+ timedOut = true;
614
+ controller.abort();
615
+ }, timeout);
616
+ const started = Date.now();
617
+ const elapsed = () => `${Date.now() - started}ms`;
618
+ try {
619
+ const response = await this.fetch(url, {
620
+ ...init,
621
+ signal: controller.signal
622
+ });
623
+ await bufferResponse(response, controller.signal);
624
+ return response;
625
+ } catch (err) {
626
+ if (signal?.aborted) {
627
+ this.logger.info(`${tag} aborted by caller after ${elapsed()}`);
628
+ throw new APIUserAbortError(void 0, { cause: err });
629
+ }
630
+ if (timedOut) {
631
+ this.logger.info(`${tag} timed out after ${elapsed()}`);
632
+ throw new APITimeoutError(timeout, { cause: err });
633
+ }
634
+ this.logger.info(`${tag} connection error after ${elapsed()}`, err);
635
+ throw new APIConnectionError(err instanceof Error ? `Connection error: ${err.message}` : void 0, { cause: err });
636
+ } finally {
637
+ clearTimeout(timer);
638
+ signal?.removeEventListener("abort", abortFromCaller);
639
+ }
640
+ }
641
+ /** Wait before retrying; caller cancellation throws `APIUserAbortError`. */
642
+ async backOff(tag, attempt, retriesLeft, reason, headers, { retry, signal }) {
643
+ const delay = retryDelayMs(attempt, headers, retry);
644
+ const nth = attempt + 1;
645
+ const total = attempt + retriesLeft;
646
+ this.logger.info(`${tag} retrying in ${delay}ms (retry ${nth}/${total}) after ${reason}`);
647
+ try {
648
+ await sleep(delay, signal);
649
+ } catch (err) {
650
+ this.logger.info(`${tag} aborted by caller while waiting to retry`);
651
+ throw new APIUserAbortError(void 0, { cause: err });
652
+ }
653
+ }
654
+ };
655
+ var parseBody = async (res) => {
656
+ const text = await res.text();
657
+ if (text.length === 0) return void 0;
658
+ if ((res.headers.get("content-type") ?? "").includes("application/json")) try {
659
+ return JSON.parse(text);
660
+ } catch {
661
+ return text;
662
+ }
663
+ try {
664
+ return JSON.parse(text);
665
+ } catch {
666
+ return text;
667
+ }
668
+ };
669
+
670
+ // src/core/jev-client.ts
671
+ var DEFAULT_JEV_MODEL = "jev-latest";
672
+ var DEFAULT_JEV_TIMEOUT_MS = 1e4;
673
+ var DEFAULT_JEV_MAX_RETRIES = 1;
674
+ var TYPESAFE_API_KEY_ENV = "TYPESAFE_API_KEY";
675
+ var TYPESAFE_BASE_URL_ENV = "TYPESAFE_BASE_URL";
676
+ var TypeSafeJevClient = class {
677
+ #client;
678
+ #timeoutMs;
679
+ constructor(config) {
680
+ const apiKey = config.apiKey.trim();
681
+ if (apiKey.length === 0) throw new JevConfigError("TypeSafe API key is empty");
682
+ const timeoutMs = config.timeoutMs ?? DEFAULT_JEV_TIMEOUT_MS;
683
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
684
+ throw new JevConfigError(`timeoutMs must be a positive number, got ${String(config.timeoutMs)}`);
685
+ }
686
+ const maxRetries = config.maxRetries ?? DEFAULT_JEV_MAX_RETRIES;
687
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
688
+ throw new JevConfigError(`maxRetries must be a non-negative integer, got ${String(config.maxRetries)}`);
689
+ }
690
+ this.#timeoutMs = timeoutMs;
691
+ try {
692
+ this.#client = new TypeSafeClient({
693
+ apiKey,
694
+ ...config.baseUrl !== void 0 ? { baseURL: config.baseUrl } : {},
695
+ defaultModel: config.model ?? DEFAULT_JEV_MODEL,
696
+ logLevel: "off",
697
+ timeout: timeoutMs,
698
+ retry: { maxRetries, backoffInitialMs: 250, backoffMaxMs: 1500, maxRetryAfterMs: 2e3 },
699
+ ...config.fetch !== void 0 ? { fetch: config.fetch } : {}
700
+ });
701
+ } catch (error) {
702
+ throw new JevConfigError(`TypeSafe client rejected its configuration: ${describeError(error)}`, {
703
+ cause: error
704
+ });
705
+ }
706
+ }
707
+ async noul(request, options = {}) {
708
+ const ids = requireIds(Object.keys(request.questions));
709
+ const questions = {};
710
+ for (const id of ids) {
711
+ const instructions = request.questions[id];
712
+ if (typeof instructions !== "string" || instructions.length === 0) {
713
+ throw new JevInputError(`question "${id}" has no instructions`);
714
+ }
715
+ questions[id] = noul(instructions);
716
+ }
717
+ const result = await this.#call(
718
+ () => this.#client.systemOne({ state: request.state, questions }, this.#requestOptions(options))
719
+ );
720
+ return {
721
+ model: readModel(result.model),
722
+ answers: validateNoulAnswers(ids, result.answers),
723
+ usage: readUsage(result.usage)
724
+ };
725
+ }
726
+ async choice(request, options = {}) {
727
+ const ids = requireIds(Object.keys(request.questions));
728
+ const questions = {};
729
+ for (const id of ids) {
730
+ const spec = request.questions[id];
731
+ if (spec === void 0) throw new JevInputError(`question "${id}" is undefined`);
732
+ validateChoiceSpec(id, spec);
733
+ const criteria = {};
734
+ for (const label of spec.labels) criteria[label] = spec.descriptions?.[label] ?? null;
735
+ questions[id] = choice(spec.instructions, criteria);
736
+ }
737
+ const result = await this.#call(
738
+ () => this.#client.systemOne({ state: request.state, questions }, this.#requestOptions(options))
739
+ );
740
+ return {
741
+ model: readModel(result.model),
742
+ answers: validateChoiceAnswers(request.questions, result.answers),
743
+ usage: readUsage(result.usage)
744
+ };
745
+ }
746
+ #requestOptions(options) {
747
+ const timeout = options.timeoutMs ?? this.#timeoutMs;
748
+ return options.signal !== void 0 ? { signal: options.signal, timeout } : { timeout };
749
+ }
750
+ async #call(send) {
751
+ try {
752
+ return await send();
753
+ } catch (error) {
754
+ throw toJevError(error);
755
+ }
756
+ }
757
+ };
758
+ function createJevClientFromEnv(env = process.env, overrides = {}) {
759
+ const apiKey = env[TYPESAFE_API_KEY_ENV]?.trim() ?? "";
760
+ if (apiKey.length === 0) throw new JevConfigError(`${TYPESAFE_API_KEY_ENV} is not set`);
761
+ const baseUrl = env[TYPESAFE_BASE_URL_ENV]?.trim();
762
+ return new TypeSafeJevClient({
763
+ apiKey,
764
+ ...baseUrl !== void 0 && baseUrl.length > 0 ? { baseUrl } : {},
765
+ ...overrides
766
+ });
767
+ }
768
+ function toJevError(error) {
769
+ if (error instanceof JevRequestError || error instanceof JevTimeoutError || error instanceof JevAbortError) {
770
+ return error;
771
+ }
772
+ if (error instanceof APIUserAbortError) return new JevAbortError(error.message, { cause: error });
773
+ if (error instanceof APITimeoutError) {
774
+ return new JevTimeoutError(error.timeoutMs, `Jev request timed out after ${String(error.timeoutMs)} ms`, {
775
+ cause: error
776
+ });
777
+ }
778
+ if (error instanceof APIError) {
779
+ const retryable = error instanceof RateLimitError || error.status >= 500 || error.status === 408;
780
+ const detail = error instanceof AuthenticationError ? "TypeSafe rejected the API key" : error.message;
781
+ return new JevRequestError(`Jev request failed with status ${String(error.status)}: ${detail}`, {
782
+ status: error.status,
783
+ retryable,
784
+ ...error.requestId !== void 0 ? { requestId: error.requestId } : {},
785
+ cause: error
786
+ });
787
+ }
788
+ if (error instanceof APIConnectionError) {
789
+ return new JevRequestError(`Jev request could not connect: ${error.message}`, { retryable: true, cause: error });
790
+ }
791
+ if (error instanceof TypeSafeError) {
792
+ return new JevConfigError(`TypeSafe SDK rejected the request: ${error.message}`, { cause: error });
793
+ }
794
+ if (error instanceof Error) return error;
795
+ return new JevRequestError(`Jev request failed: ${describeError(error)}`, { retryable: false, cause: error });
796
+ }
797
+ function requireIds(ids) {
798
+ if (ids.length === 0) throw new JevInputError("a Jev request needs at least one question");
799
+ for (const id of ids) {
800
+ if (id.length === 0) throw new JevInputError("question ids must be non-empty strings");
801
+ }
802
+ return ids;
803
+ }
804
+ function validateChoiceSpec(id, spec) {
805
+ if (typeof spec.instructions !== "string" || spec.instructions.length === 0) {
806
+ throw new JevInputError(`choice question "${id}" has no instructions`);
807
+ }
808
+ if (spec.labels.length < 2) throw new JevInputError(`choice question "${id}" needs at least two labels`);
809
+ if (new Set(spec.labels).size !== spec.labels.length) {
810
+ throw new JevInputError(`choice question "${id}" has duplicate labels`);
811
+ }
812
+ for (const label of spec.labels) {
813
+ if (label.length === 0) throw new JevInputError(`choice question "${id}" has an empty label`);
814
+ }
815
+ }
816
+ function isRecord2(value) {
817
+ return typeof value === "object" && value !== null && !Array.isArray(value);
818
+ }
819
+ function isUnitInterval(value) {
820
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
821
+ }
822
+ function readModel(model) {
823
+ return typeof model === "string" ? model : "";
824
+ }
825
+ function readUsage(usage) {
826
+ const record = isRecord2(usage) ? usage : {};
827
+ const input = record["input_tokens"];
828
+ const output = record["output_tokens"];
829
+ return {
830
+ inputTokens: typeof input === "number" && Number.isFinite(input) ? input : 0,
831
+ outputTokens: typeof output === "number" && Number.isFinite(output) ? output : 0
832
+ };
833
+ }
834
+ function rejectUnexpectedIds(expected, answers) {
835
+ const known = new Set(expected);
836
+ for (const key of Object.keys(answers)) {
837
+ if (!known.has(key)) throw new JevResponseError(`Jev answered an id that was not asked: "${key}"`);
838
+ }
839
+ }
840
+ function validateNoulAnswers(ids, answers) {
841
+ if (!isRecord2(answers)) throw new JevResponseError("Jev response has no answers object");
842
+ rejectUnexpectedIds(ids, answers);
843
+ const out = {};
844
+ for (const id of ids) {
845
+ const answer = answers[id];
846
+ if (!isRecord2(answer)) throw new JevResponseError(`Jev response is missing the answer for "${id}"`);
847
+ if (answer["type"] !== "noul") throw new JevResponseError(`Jev answer "${id}" is not a noul answer`);
848
+ const value = answer["noul"];
849
+ if (!isUnitInterval(value)) throw new JevResponseError(`Jev answer "${id}" has no noul value in [0, 1]`);
850
+ out[id] = value;
851
+ }
852
+ return out;
853
+ }
854
+ function validateChoiceAnswers(questions, answers) {
855
+ if (!isRecord2(answers)) throw new JevResponseError("Jev response has no answers object");
856
+ const ids = Object.keys(questions);
857
+ rejectUnexpectedIds(ids, answers);
858
+ const out = {};
859
+ for (const id of ids) {
860
+ const spec = questions[id];
861
+ if (spec === void 0) continue;
862
+ const answer = answers[id];
863
+ if (!isRecord2(answer)) throw new JevResponseError(`Jev response is missing the answer for "${id}"`);
864
+ if (answer["type"] !== "choice") throw new JevResponseError(`Jev answer "${id}" is not a choice answer`);
865
+ const chosen = answer["choice"];
866
+ if (typeof chosen !== "string" || !spec.labels.includes(chosen)) {
867
+ throw new JevResponseError(`Jev answer "${id}" chose an undeclared label: ${String(chosen)}`);
868
+ }
869
+ const confidence = answer["confidence"];
870
+ if (!isUnitInterval(confidence)) throw new JevResponseError(`Jev answer "${id}" has no confidence in [0, 1]`);
871
+ const rawProbabilities = answer["probabilities"];
872
+ if (!isRecord2(rawProbabilities)) throw new JevResponseError(`Jev answer "${id}" has no probabilities`);
873
+ const probabilities = {};
874
+ for (const label of spec.labels) probabilities[label] = 0;
875
+ for (const [label, value] of Object.entries(rawProbabilities)) {
876
+ if (!spec.labels.includes(label)) {
877
+ throw new JevResponseError(`Jev answer "${id}" reports a probability for an undeclared label: ${label}`);
878
+ }
879
+ if (!isUnitInterval(value)) throw new JevResponseError(`Jev answer "${id}" has a probability outside [0, 1]`);
880
+ probabilities[label] = value;
881
+ }
882
+ out[id] = { choice: chosen, confidence, probabilities };
883
+ }
884
+ return out;
885
+ }
886
+
887
+ // src/core/tokens.ts
888
+ var CHARS_PER_TOKEN = 3;
889
+ var DEFAULT_WINDOW_TOKENS = 25e3;
890
+ function estimateTokens(text) {
891
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
892
+ }
893
+ function estimateJsonTokens(value) {
894
+ return estimateTokens(JSON.stringify(value) ?? "");
895
+ }
896
+
897
+ // src/core/windows.ts
898
+ function planWindows(items, options = {}) {
899
+ const budget = options.budgetTokens ?? DEFAULT_WINDOW_TOKENS;
900
+ const overhead = options.overheadTokens ?? 0;
901
+ const itemOverhead = options.itemOverheadTokens ?? 0;
902
+ if (!Number.isInteger(budget) || budget <= 0) {
903
+ throw new JevBudgetError(`budgetTokens must be a positive integer, got ${String(options.budgetTokens)}`);
904
+ }
905
+ if (!Number.isFinite(overhead) || overhead < 0) {
906
+ throw new JevBudgetError(`overheadTokens must be a non-negative number, got ${String(options.overheadTokens)}`);
907
+ }
908
+ if (!Number.isFinite(itemOverhead) || itemOverhead < 0) {
909
+ throw new JevBudgetError(
910
+ `itemOverheadTokens must be a non-negative number, got ${String(options.itemOverheadTokens)}`
911
+ );
912
+ }
913
+ if (overhead >= budget) {
914
+ throw new JevBudgetError(
915
+ `overheadTokens (${String(overhead)}) leaves no room under budgetTokens (${String(budget)})`
916
+ );
917
+ }
918
+ const cost = options.costTokens ?? ((item) => estimateJsonTokens({ id: item.id, text: item.text }));
919
+ const windows = [];
920
+ const oversize = [];
921
+ let current = [];
922
+ let used = overhead;
923
+ for (const item of items) {
924
+ const itemCost = cost(item) + itemOverhead;
925
+ if (overhead + itemCost > budget) {
926
+ oversize.push(item);
927
+ continue;
928
+ }
929
+ if (used + itemCost > budget && current.length > 0) {
930
+ windows.push(current);
931
+ current = [];
932
+ used = overhead;
933
+ }
934
+ current.push(item);
935
+ used += itemCost;
936
+ }
937
+ if (current.length > 0) windows.push(current);
938
+ return { windows, oversize };
939
+ }
940
+ var DEFAULT_WINDOW_CONCURRENCY = 4;
941
+ var DEFAULT_WINDOW_TIMEOUT_MS = 1e4;
942
+ async function runWindows(windows, judge, options = {}) {
943
+ const concurrency = options.concurrency ?? DEFAULT_WINDOW_CONCURRENCY;
944
+ const timeoutMs = options.timeoutMs ?? DEFAULT_WINDOW_TIMEOUT_MS;
945
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
946
+ throw new JevBudgetError(`concurrency must be a positive integer, got ${String(options.concurrency)}`);
947
+ }
948
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
949
+ throw new JevBudgetError(`timeoutMs must be a positive number, got ${String(options.timeoutMs)}`);
950
+ }
951
+ if (windows.length === 0) return [];
952
+ if (options.signal?.aborted === true) throw new JevAbortError();
953
+ const controller = new AbortController();
954
+ const onOuterAbort = () => {
955
+ controller.abort(options.signal?.reason);
956
+ };
957
+ options.signal?.addEventListener("abort", onOuterAbort, { once: true });
958
+ const results = new Array(windows.length);
959
+ let failure;
960
+ let next = 0;
961
+ const worker = async () => {
962
+ while (!controller.signal.aborted) {
963
+ const index = next;
964
+ next += 1;
965
+ if (index >= windows.length) return;
966
+ const window = windows[index];
967
+ if (window === void 0) return;
968
+ const timeout = AbortSignal.timeout(timeoutMs);
969
+ const signal = AbortSignal.any([controller.signal, timeout]);
970
+ try {
971
+ results[index] = await judge(window, index, { signal, timeoutMs });
972
+ } catch (error) {
973
+ if (failure === void 0) failure = classifyWindowFailure(error, index, timeout, options.signal, timeoutMs);
974
+ controller.abort();
975
+ return;
976
+ }
977
+ }
978
+ };
979
+ try {
980
+ await Promise.all(Array.from({ length: Math.min(concurrency, windows.length) }, worker));
981
+ } finally {
982
+ options.signal?.removeEventListener("abort", onOuterAbort);
983
+ }
984
+ if (failure !== void 0) throw failure;
985
+ return results;
986
+ }
987
+ function classifyWindowFailure(error, index, timeout, outer, timeoutMs) {
988
+ if (timeout.aborted) {
989
+ return new JevTimeoutError(timeoutMs, `Jev window ${String(index + 1)} exceeded ${String(timeoutMs)} ms`, {
990
+ cause: error
991
+ });
992
+ }
993
+ if (outer?.aborted === true) return new JevAbortError("Jev windows aborted by caller", { cause: error });
994
+ if (error instanceof Error) return error;
995
+ return new Error(String(error));
996
+ }
997
+
998
+ // src/errors.ts
999
+ var JevpruneError = class extends Error {
1000
+ name = "JevpruneError";
1001
+ constructor(message, options) {
1002
+ super(message, options);
1003
+ }
1004
+ };
1005
+ var ConfigError = class extends JevpruneError {
1006
+ name = "ConfigError";
1007
+ };
1008
+ var UsageError = class extends JevpruneError {
1009
+ name = "UsageError";
1010
+ };
1011
+ var RunStoreError = class extends JevpruneError {
1012
+ name = "RunStoreError";
1013
+ code;
1014
+ constructor(message, details = {}) {
1015
+ super(message, { cause: details.cause });
1016
+ this.code = details.code;
1017
+ }
1018
+ };
1019
+ var SpawnError = class extends JevpruneError {
1020
+ name = "SpawnError";
1021
+ executable;
1022
+ code;
1023
+ constructor(message, details) {
1024
+ super(message, { cause: details.cause });
1025
+ this.executable = details.executable;
1026
+ this.code = details.code;
1027
+ }
1028
+ };
1029
+ var TranscriptError = class extends JevpruneError {
1030
+ name = "TranscriptError";
1031
+ path;
1032
+ constructor(message, details) {
1033
+ super(message, { cause: details.cause });
1034
+ this.path = details.path;
1035
+ }
1036
+ };
1037
+ var RunNotFoundError = class extends JevpruneError {
1038
+ name = "RunNotFoundError";
1039
+ id;
1040
+ constructor(id, options) {
1041
+ super(`run ${id} was not found`, options);
1042
+ this.id = id;
1043
+ }
1044
+ };
1045
+ var LineRangeError = class extends JevpruneError {
1046
+ name = "LineRangeError";
1047
+ };
1048
+ function errorCode(error) {
1049
+ if (typeof error !== "object" || error === null) return void 0;
1050
+ const code = error.code;
1051
+ return typeof code === "string" ? code : void 0;
1052
+ }
1053
+ function errorName(error) {
1054
+ return error instanceof Error ? error.name : "Error";
1055
+ }
1056
+ function errorMessage(error) {
1057
+ if (error instanceof Error) return error.message;
1058
+ if (typeof error === "string") return error;
1059
+ return String(error);
1060
+ }
1061
+
1062
+ // src/config.ts
1063
+ var HOME_ENV = "JEVPRUNE_HOME";
1064
+ var TASK_ENV = "JEVPRUNE_TASK";
1065
+ var CONFIG_FILE = "config.json";
1066
+ var DEFAULT_ALLOWLIST = [
1067
+ "cd",
1068
+ "ls",
1069
+ "pwd",
1070
+ "echo",
1071
+ "git status",
1072
+ "git add",
1073
+ "git commit",
1074
+ "git log",
1075
+ "git diff --stat",
1076
+ "which",
1077
+ "mkdir",
1078
+ "touch",
1079
+ "true",
1080
+ "test",
1081
+ "["
1082
+ ];
1083
+ var DEFAULT_CONFIG = {
1084
+ threshold: 0.3,
1085
+ fastPathLines: 60,
1086
+ tailLines: 40,
1087
+ headLines: 40,
1088
+ contextLines: 3,
1089
+ minCollapseLines: 3,
1090
+ windowTokens: DEFAULT_WINDOW_TOKENS,
1091
+ windowTimeoutMs: 1e4,
1092
+ concurrency: 4,
1093
+ maxPruneBytes: 16777216,
1094
+ retention: { maxRuns: 200, maxBytes: 268435456 },
1095
+ autoWrap: true,
1096
+ allowlist: DEFAULT_ALLOWLIST
1097
+ };
1098
+ function resolveHome(env = process.env) {
1099
+ const override = env[HOME_ENV]?.trim() ?? "";
1100
+ if (override.length > 0) return resolve(override);
1101
+ return join(homedir(), ".jevprune");
1102
+ }
1103
+ async function loadConfig(env = process.env) {
1104
+ const home = resolveHome(env);
1105
+ const path = join(home, CONFIG_FILE);
1106
+ let raw;
1107
+ try {
1108
+ raw = await readFile(path, "utf8");
1109
+ } catch (error) {
1110
+ if (errorCode(error) === "ENOENT") return { ...DEFAULT_CONFIG, home };
1111
+ throw new ConfigError(`config file ${path} could not be read: ${errorMessage(error)}`, { cause: error });
1112
+ }
1113
+ return { ...parseConfig(raw, path), home };
1114
+ }
1115
+ function parseConfig(raw, path) {
1116
+ let parsed;
1117
+ try {
1118
+ parsed = JSON.parse(raw);
1119
+ } catch (error) {
1120
+ throw new ConfigError(`config file ${path} is not valid JSON: ${errorMessage(error)}`, { cause: error });
1121
+ }
1122
+ if (!isRecord3(parsed)) {
1123
+ throw new ConfigError(`config file ${path} must contain a JSON object, got ${describeValue(parsed)}`);
1124
+ }
1125
+ for (const key of Object.keys(parsed)) {
1126
+ if (!(key in DEFAULT_CONFIG)) throw new ConfigError(`unknown config key "${key}" in ${path}`);
1127
+ }
1128
+ return {
1129
+ threshold: readNumber(parsed, "threshold", DEFAULT_CONFIG.threshold, 0, 1),
1130
+ fastPathLines: readInteger(parsed, "fastPathLines", DEFAULT_CONFIG.fastPathLines, 0),
1131
+ tailLines: readInteger(parsed, "tailLines", DEFAULT_CONFIG.tailLines, 0),
1132
+ headLines: readInteger(parsed, "headLines", DEFAULT_CONFIG.headLines, 0),
1133
+ contextLines: readInteger(parsed, "contextLines", DEFAULT_CONFIG.contextLines, 0),
1134
+ minCollapseLines: readInteger(parsed, "minCollapseLines", DEFAULT_CONFIG.minCollapseLines, 1),
1135
+ windowTokens: readInteger(parsed, "windowTokens", DEFAULT_CONFIG.windowTokens, 1),
1136
+ windowTimeoutMs: readInteger(parsed, "windowTimeoutMs", DEFAULT_CONFIG.windowTimeoutMs, 1),
1137
+ concurrency: readInteger(parsed, "concurrency", DEFAULT_CONFIG.concurrency, 1),
1138
+ maxPruneBytes: readInteger(parsed, "maxPruneBytes", DEFAULT_CONFIG.maxPruneBytes, 1),
1139
+ retention: readRetention(parsed["retention"]),
1140
+ autoWrap: readBoolean(parsed, "autoWrap", DEFAULT_CONFIG.autoWrap),
1141
+ allowlist: readStringArray(parsed, "allowlist", DEFAULT_CONFIG.allowlist)
1142
+ };
1143
+ }
1144
+ function parseThreshold(value) {
1145
+ const parsed = Number(value);
1146
+ if (value.trim().length === 0 || !Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
1147
+ throw new ConfigError(`--threshold must be a number in [0, 1], got ${describeValue(value)}`);
1148
+ }
1149
+ return parsed;
1150
+ }
1151
+ function readNumber(source, key, fallback, min, max) {
1152
+ const value = source[key];
1153
+ if (value === void 0) return fallback;
1154
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
1155
+ throw new ConfigError(
1156
+ `config key "${key}" must be a number in [${String(min)}, ${String(max)}], got ${describeValue(value)}`
1157
+ );
1158
+ }
1159
+ return value;
1160
+ }
1161
+ function readInteger(source, key, fallback, min, label = key) {
1162
+ const value = source[key];
1163
+ if (value === void 0) return fallback;
1164
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
1165
+ throw new ConfigError(
1166
+ `config key "${label}" must be an integer >= ${String(min)}, got ${describeValue(value)}`
1167
+ );
1168
+ }
1169
+ return value;
1170
+ }
1171
+ function readBoolean(source, key, fallback) {
1172
+ const value = source[key];
1173
+ if (value === void 0) return fallback;
1174
+ if (typeof value !== "boolean") {
1175
+ throw new ConfigError(`config key "${key}" must be a boolean, got ${describeValue(value)}`);
1176
+ }
1177
+ return value;
1178
+ }
1179
+ function readStringArray(source, key, fallback) {
1180
+ const value = source[key];
1181
+ if (value === void 0) return fallback;
1182
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
1183
+ throw new ConfigError(`config key "${key}" must be an array of strings, got ${describeValue(value)}`);
1184
+ }
1185
+ return value;
1186
+ }
1187
+ function readRetention(value) {
1188
+ if (value === void 0) return DEFAULT_CONFIG.retention;
1189
+ if (!isRecord3(value)) {
1190
+ throw new ConfigError(`config key "retention" must be an object, got ${describeValue(value)}`);
1191
+ }
1192
+ for (const key of Object.keys(value)) {
1193
+ if (key !== "maxRuns" && key !== "maxBytes") {
1194
+ throw new ConfigError(`unknown config key "retention.${key}"`);
1195
+ }
1196
+ }
1197
+ return {
1198
+ maxRuns: readInteger(value, "maxRuns", DEFAULT_CONFIG.retention.maxRuns, 1, "retention.maxRuns"),
1199
+ maxBytes: readInteger(value, "maxBytes", DEFAULT_CONFIG.retention.maxBytes, 1, "retention.maxBytes")
1200
+ };
1201
+ }
1202
+ function isRecord3(value) {
1203
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1204
+ }
1205
+ function describeValue(value) {
1206
+ if (typeof value === "string") return JSON.stringify(value);
1207
+ if (value === void 0) return "undefined";
1208
+ if (value === null || typeof value === "number" || typeof value === "boolean") return String(value);
1209
+ try {
1210
+ return JSON.stringify(value) ?? typeof value;
1211
+ } catch {
1212
+ return typeof value;
1213
+ }
1214
+ }
1215
+
1216
+ // src/footer.ts
1217
+ import { homedir as homedir2 } from "os";
1218
+ import { sep } from "path";
1219
+ var LF = 10;
1220
+ function formatFooter(input) {
1221
+ if (input.mode === "fast-path") return "";
1222
+ const parts = [];
1223
+ if (input.mode === "passthrough") {
1224
+ if (input.exitCode !== void 0 && input.exitCode !== null) parts.push(`exit ${String(input.exitCode)}`);
1225
+ const note = input.passthroughNote === void 0 ? "" : ` (${input.passthroughNote})`;
1226
+ parts.push(`${formatCount(input.linesIn)} lines passed through${note}`);
1227
+ } else {
1228
+ if (input.mode === "fallback") parts.push(`fallback (no Jev: ${input.fallbackReason ?? "unknown"})`);
1229
+ parts.push(`${formatCount(input.linesIn)} \u2192 ${formatCount(input.linesOut)} lines`);
1230
+ if (input.exitCode !== void 0 && input.exitCode !== null) parts.push(`exit ${String(input.exitCode)}`);
1231
+ }
1232
+ if (input.storeFailureCode !== void 0) {
1233
+ parts.push(`run store unavailable (${input.storeFailureCode})`);
1234
+ } else if (input.logPath !== void 0) {
1235
+ parts.push(`full output ${displayPath(input.logPath, input.home)}`);
1236
+ }
1237
+ return `jevprune: ${parts.join(", ")}`;
1238
+ }
1239
+ function footerAfter(lastByte, footer) {
1240
+ if (footer.length === 0) return "";
1241
+ const separator = lastByte === void 0 || lastByte === LF ? "" : "\n";
1242
+ return `${separator}${footer}
1243
+ `;
1244
+ }
1245
+ function withFooter(kept, footer) {
1246
+ if (footer.length === 0) return kept;
1247
+ const separator = kept.length === 0 || kept.endsWith("\n") ? "" : "\n";
1248
+ return `${kept}${separator}${footer}
1249
+ `;
1250
+ }
1251
+ function formatCount(value) {
1252
+ const digits = Math.trunc(Math.abs(value)).toString();
1253
+ const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1254
+ return value < 0 ? `-${grouped}` : grouped;
1255
+ }
1256
+ function displayPath(path, home = homedir2()) {
1257
+ if (path === home) return "~";
1258
+ if (home.length > 0 && path.startsWith(home + sep)) return `~${path.slice(home.length)}`;
1259
+ return path;
1260
+ }
1261
+
1262
+ // src/store.ts
1263
+ import { randomBytes } from "crypto";
1264
+ import { createReadStream } from "fs";
1265
+ import { appendFile, mkdir, open, readFile as readFile2, readdir, stat, unlink, writeFile } from "fs/promises";
1266
+ import { join as join2 } from "path";
1267
+ import { finished } from "stream/promises";
1268
+
1269
+ // src/bytes.ts
1270
+ var LF2 = 10;
1271
+ var CR = 13;
1272
+ function isValidUtf8(bytes) {
1273
+ try {
1274
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1275
+ return true;
1276
+ } catch {
1277
+ return false;
1278
+ }
1279
+ }
1280
+ function byteLineStarts(bytes) {
1281
+ const starts = [0];
1282
+ for (let index = 0; index < bytes.length; index += 1) {
1283
+ const byte = bytes[index];
1284
+ if (byte === CR && bytes[index + 1] === LF2) index += 1;
1285
+ else if (byte !== CR && byte !== LF2) continue;
1286
+ starts.push(index + 1);
1287
+ }
1288
+ if (starts[starts.length - 1] !== bytes.length) starts.push(bytes.length);
1289
+ return starts;
1290
+ }
1291
+ function countByteLines(bytes) {
1292
+ return byteLineStarts(bytes).length - 1;
1293
+ }
1294
+
1295
+ // src/store.ts
1296
+ var RUN_ID_PATTERN = /^[a-z0-9]+-[a-f0-9]{4}$/;
1297
+ var RUNS_DIR = "runs";
1298
+ var GAIN_FILE = "gain.jsonl";
1299
+ var DIR_MODE = 448;
1300
+ var FILE_MODE = 384;
1301
+ function newRunId() {
1302
+ return `${Date.now().toString(36)}-${randomBytes(2).toString("hex")}`;
1303
+ }
1304
+ var FileRunWriter = class {
1305
+ path;
1306
+ #handle;
1307
+ #stream;
1308
+ #waiting = [];
1309
+ #failure;
1310
+ #closed = false;
1311
+ constructor(path, handle, stream) {
1312
+ this.path = path;
1313
+ this.#handle = handle;
1314
+ this.#stream = stream;
1315
+ this.#stream.on("error", (error) => {
1316
+ this.#fail(error);
1317
+ this.#release();
1318
+ });
1319
+ this.#stream.on("drain", () => {
1320
+ this.#release();
1321
+ });
1322
+ }
1323
+ get failure() {
1324
+ return this.#failure;
1325
+ }
1326
+ write(chunk) {
1327
+ if (this.#closed || this.#failure !== void 0) return true;
1328
+ try {
1329
+ return this.#stream.write(chunk);
1330
+ } catch (error) {
1331
+ this.#fail(error);
1332
+ return true;
1333
+ }
1334
+ }
1335
+ onDrain(listener) {
1336
+ if (this.#closed || this.#failure !== void 0) {
1337
+ queueMicrotask(listener);
1338
+ return;
1339
+ }
1340
+ this.#waiting.push(listener);
1341
+ }
1342
+ async close() {
1343
+ if (this.#closed) return;
1344
+ this.#closed = true;
1345
+ try {
1346
+ this.#stream.end();
1347
+ await finished(this.#stream);
1348
+ } catch (error) {
1349
+ this.#fail(error);
1350
+ await this.#handle.close().catch(() => void 0);
1351
+ }
1352
+ this.#release();
1353
+ }
1354
+ #fail(error) {
1355
+ this.#failure ??= storeError(`run log ${this.path} could not be written`, error);
1356
+ }
1357
+ #release() {
1358
+ const waiting = this.#waiting;
1359
+ this.#waiting = [];
1360
+ for (const listener of waiting) listener();
1361
+ }
1362
+ };
1363
+ var RunStore = class {
1364
+ home;
1365
+ #retention;
1366
+ constructor(options) {
1367
+ this.home = options.home;
1368
+ this.#retention = options.retention ?? DEFAULT_CONFIG.retention;
1369
+ }
1370
+ get runsDir() {
1371
+ return join2(this.home, RUNS_DIR);
1372
+ }
1373
+ get gainPath() {
1374
+ return join2(this.home, GAIN_FILE);
1375
+ }
1376
+ logPath(id) {
1377
+ return join2(this.runsDir, `${requireRunId(id)}.log`);
1378
+ }
1379
+ metaPath(id) {
1380
+ return join2(this.runsDir, `${requireRunId(id)}.json`);
1381
+ }
1382
+ async openRun(run) {
1383
+ const path = this.logPath(run.id);
1384
+ await this.#ensureDir(this.runsDir);
1385
+ let handle;
1386
+ try {
1387
+ handle = await open(path, "wx", FILE_MODE);
1388
+ } catch (error) {
1389
+ throw storeError(`run log ${path} could not be created`, error);
1390
+ }
1391
+ return new FileRunWriter(path, handle, handle.createWriteStream());
1392
+ }
1393
+ async finalizeRun(id, meta) {
1394
+ const path = this.metaPath(id);
1395
+ await this.#ensureDir(this.runsDir);
1396
+ try {
1397
+ await writeFile(path, `${JSON.stringify(meta)}
1398
+ `, { mode: FILE_MODE });
1399
+ } catch (error) {
1400
+ throw storeError(`run meta ${path} could not be written`, error);
1401
+ }
1402
+ }
1403
+ async readRunBytes(id) {
1404
+ const path = this.logPath(id);
1405
+ try {
1406
+ return await readFile2(path);
1407
+ } catch (error) {
1408
+ if (errorCode(error) === "ENOENT") throw new RunNotFoundError(id, { cause: error });
1409
+ throw storeError(`run log ${path} could not be read`, error);
1410
+ }
1411
+ }
1412
+ async *readRunChunks(id) {
1413
+ const path = this.logPath(id);
1414
+ try {
1415
+ for await (const chunk of createReadStream(path)) {
1416
+ yield typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
1417
+ }
1418
+ } catch (error) {
1419
+ if (errorCode(error) === "ENOENT") throw new RunNotFoundError(id, { cause: error });
1420
+ throw storeError(`run log ${path} could not be read`, error);
1421
+ }
1422
+ }
1423
+ async readRun(id) {
1424
+ const bytes = await this.readRunBytes(id);
1425
+ return { id, path: this.logPath(id), text: bytes.toString("utf8"), meta: await this.#readMeta(id) };
1426
+ }
1427
+ async readRunLineBytes(id, from = 1, to) {
1428
+ const bytes = await this.readRunBytes(id);
1429
+ const starts = byteLineStarts(bytes);
1430
+ const lines = starts.length - 1;
1431
+ const last = to ?? lines;
1432
+ if (!Number.isInteger(from) || !Number.isInteger(last) || from < 1 || last < from) {
1433
+ throw new LineRangeError(`line range ${String(from)}-${String(last)} is not a range`);
1434
+ }
1435
+ if (last > lines) {
1436
+ throw new LineRangeError(
1437
+ `line range ${String(from)}-${String(last)} is outside run ${id} (${String(lines)} lines)`
1438
+ );
1439
+ }
1440
+ return bytes.subarray(starts[from - 1] ?? 0, starts[last] ?? bytes.length);
1441
+ }
1442
+ async readRunLines(id, from = 1, to) {
1443
+ return (await this.readRunLineBytes(id, from, to)).toString("utf8");
1444
+ }
1445
+ async appendGain(entry2) {
1446
+ await this.#ensureDir(this.home);
1447
+ try {
1448
+ await appendFile(this.gainPath, `${JSON.stringify(entry2)}
1449
+ `, { mode: FILE_MODE });
1450
+ } catch (error) {
1451
+ throw storeError(`gain ledger ${this.gainPath} could not be written`, error);
1452
+ }
1453
+ }
1454
+ async readGain() {
1455
+ let raw;
1456
+ try {
1457
+ raw = await readFile2(this.gainPath, "utf8");
1458
+ } catch (error) {
1459
+ if (errorCode(error) === "ENOENT") return { runs: 0, linesIn: 0, linesOut: 0, bytesIn: 0, bytesOut: 0 };
1460
+ throw storeError(`gain ledger ${this.gainPath} could not be read`, error);
1461
+ }
1462
+ let runs = 0;
1463
+ let linesIn = 0;
1464
+ let linesOut = 0;
1465
+ let bytesIn = 0;
1466
+ let bytesOut = 0;
1467
+ for (const line of raw.split("\n")) {
1468
+ if (line.trim().length === 0) continue;
1469
+ const entry2 = parseGainEntry(line);
1470
+ if (entry2 === null) continue;
1471
+ runs += 1;
1472
+ linesIn += entry2.linesIn;
1473
+ linesOut += entry2.linesOut;
1474
+ bytesIn += entry2.bytesIn;
1475
+ bytesOut += entry2.bytesOut;
1476
+ }
1477
+ return { runs, linesIn, linesOut, bytesIn, bytesOut };
1478
+ }
1479
+ async enforceRetention() {
1480
+ let names;
1481
+ try {
1482
+ names = await readdir(this.runsDir);
1483
+ } catch (error) {
1484
+ if (errorCode(error) === "ENOENT") return;
1485
+ throw storeError(`run directory ${this.runsDir} could not be read`, error);
1486
+ }
1487
+ const ids = [...new Set(names.filter((name) => name.endsWith(".log")).map((name) => name.slice(0, -4)))].filter((id) => RUN_ID_PATTERN.test(id)).sort();
1488
+ const sizes = /* @__PURE__ */ new Map();
1489
+ let total = 0;
1490
+ for (const id of ids) {
1491
+ const bytes = await this.#sizeOf(this.logPath(id)) + await this.#sizeOf(this.metaPath(id));
1492
+ sizes.set(id, bytes);
1493
+ total += bytes;
1494
+ }
1495
+ let count = ids.length;
1496
+ for (const id of ids) {
1497
+ if (count <= this.#retention.maxRuns && total <= this.#retention.maxBytes) break;
1498
+ await this.discardRun(id);
1499
+ total -= sizes.get(id) ?? 0;
1500
+ count -= 1;
1501
+ }
1502
+ }
1503
+ async discardRun(id) {
1504
+ for (const path of [this.logPath(id), this.metaPath(id)]) {
1505
+ try {
1506
+ await unlink(path);
1507
+ } catch (error) {
1508
+ if (errorCode(error) === "ENOENT") continue;
1509
+ throw storeError(`run file ${path} could not be deleted`, error);
1510
+ }
1511
+ }
1512
+ }
1513
+ async #sizeOf(path) {
1514
+ try {
1515
+ return (await stat(path)).size;
1516
+ } catch (error) {
1517
+ if (errorCode(error) === "ENOENT") return 0;
1518
+ throw storeError(`run file ${path} could not be inspected`, error);
1519
+ }
1520
+ }
1521
+ async #readMeta(id) {
1522
+ const path = this.metaPath(id);
1523
+ let raw;
1524
+ try {
1525
+ raw = await readFile2(path, "utf8");
1526
+ } catch (error) {
1527
+ if (errorCode(error) === "ENOENT") return null;
1528
+ throw storeError(`run meta ${path} could not be read`, error);
1529
+ }
1530
+ let parsed;
1531
+ try {
1532
+ parsed = JSON.parse(raw);
1533
+ } catch (error) {
1534
+ throw storeError(`run meta ${path} is not valid JSON`, error);
1535
+ }
1536
+ return isRecord4(parsed) ? parsed : null;
1537
+ }
1538
+ async #ensureDir(path) {
1539
+ try {
1540
+ await mkdir(path, { recursive: true, mode: DIR_MODE });
1541
+ } catch (error) {
1542
+ throw storeError(`directory ${path} could not be created`, error);
1543
+ }
1544
+ }
1545
+ };
1546
+ function requireRunId(id) {
1547
+ if (!RUN_ID_PATTERN.test(id)) throw new RunStoreError(`"${id}" is not a run id`);
1548
+ return id;
1549
+ }
1550
+ function parseGainEntry(line) {
1551
+ let parsed;
1552
+ try {
1553
+ parsed = JSON.parse(line);
1554
+ } catch {
1555
+ return null;
1556
+ }
1557
+ if (!isRecord4(parsed)) return null;
1558
+ const linesIn = parsed["linesIn"];
1559
+ const linesOut = parsed["linesOut"];
1560
+ const bytesIn = parsed["bytesIn"];
1561
+ const bytesOut = parsed["bytesOut"];
1562
+ if (typeof linesIn !== "number" || typeof linesOut !== "number" || typeof bytesIn !== "number" || typeof bytesOut !== "number") {
1563
+ return null;
1564
+ }
1565
+ return parsed;
1566
+ }
1567
+ function isRecord4(value) {
1568
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1569
+ }
1570
+ function storeError(message, error) {
1571
+ const code = errorCode(error);
1572
+ return new RunStoreError(`${message}: ${errorMessage(error)}`, {
1573
+ ...code !== void 0 ? { code } : {},
1574
+ cause: error
1575
+ });
1576
+ }
1577
+
1578
+ // src/commands/gain.ts
1579
+ async function runGain(io) {
1580
+ const config = await loadConfig(io.env);
1581
+ const store = new RunStore({ home: config.home, retention: config.retention });
1582
+ const totals = await store.readGain();
1583
+ const tokens = Math.max(0, Math.floor((totals.bytesIn - totals.bytesOut) / CHARS_PER_TOKEN));
1584
+ await io.write(
1585
+ `jevprune: ${formatCount(totals.runs)} runs, ${formatCount(totals.linesIn)} \u2192 ${formatCount(totals.linesOut)} lines, ~${formatCount(tokens)} tokens saved (estimated at ${String(CHARS_PER_TOKEN)} chars per token)
1586
+ `
1587
+ );
1588
+ return 0;
1589
+ }
1590
+
1591
+ // src/hook.ts
1592
+ var STATE_CHANGING_TOKENS = [
1593
+ "cd",
1594
+ "export",
1595
+ "source",
1596
+ ".",
1597
+ "unset",
1598
+ "alias",
1599
+ "set",
1600
+ "eval",
1601
+ "exec",
1602
+ "pushd",
1603
+ "popd"
1604
+ ];
1605
+ var ALWAYS_INTERACTIVE_COMMANDS = [
1606
+ "vim",
1607
+ "vi",
1608
+ "nvim",
1609
+ "nano",
1610
+ "emacs",
1611
+ "less",
1612
+ "more",
1613
+ "man",
1614
+ "top",
1615
+ "htop",
1616
+ "ssh",
1617
+ "telnet",
1618
+ "tmux",
1619
+ "screen",
1620
+ "sudo",
1621
+ "su",
1622
+ "passwd",
1623
+ "claude",
1624
+ "watch"
1625
+ ];
1626
+ var INTERACTIVE_WHEN_BARE_COMMANDS = [
1627
+ "python",
1628
+ "python3",
1629
+ "node",
1630
+ "irb",
1631
+ "psql",
1632
+ "mysql",
1633
+ "sqlite3",
1634
+ "bash",
1635
+ "sh",
1636
+ "zsh",
1637
+ "fish",
1638
+ "gh"
1639
+ ];
1640
+ var SHELL_COMMANDS = ["bash", "sh", "zsh", "fish"];
1641
+ var DOCKER_TTY_FLAG = /^-(?:i|t|it|ti)$|^--interactive$|^--tty$/;
1642
+ var STATE_CHANGE_PATTERN = new RegExp(
1643
+ `(?:^|;|\\||\\(|&&|\\n)\\s*(?:${STATE_CHANGING_TOKENS.map(escapeRegExp).join("|")})(?=\\s|$|[;|)&])`
1644
+ );
1645
+ var ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=/;
1646
+ function quoteForShell(value) {
1647
+ return `'${value.replaceAll("'", "'\\''")}'`;
1648
+ }
1649
+ function hasStateChange(command) {
1650
+ return STATE_CHANGE_PATTERN.test(command);
1651
+ }
1652
+ function isInteractiveCommand(command) {
1653
+ const words = command.split(/\s+/).filter((word2) => word2.length > 0);
1654
+ let index = 0;
1655
+ while (index < words.length && ENV_ASSIGNMENT_PATTERN.test(words[index] ?? "")) index += 1;
1656
+ const word = words[index];
1657
+ if (word === void 0) return true;
1658
+ const rest = words.slice(index + 1);
1659
+ if (ALWAYS_INTERACTIVE_COMMANDS.includes(word)) return true;
1660
+ if (INTERACTIVE_WHEN_BARE_COMMANDS.includes(word)) {
1661
+ if (rest.length === 0) return true;
1662
+ if (SHELL_COMMANDS.includes(word) && rest.includes("-i")) return true;
1663
+ return word === "gh" && rest[0] === "auth";
1664
+ }
1665
+ if (word === "tail") return rest[0] === "-f";
1666
+ if (word === "docker" && (rest[0] === "exec" || rest[0] === "run")) {
1667
+ return rest.some((flag) => DOCKER_TTY_FLAG.test(flag));
1668
+ }
1669
+ return false;
1670
+ }
1671
+ function isAllowlisted(command, allowlist) {
1672
+ for (const prefix of allowlist) {
1673
+ if (prefix.length === 0) continue;
1674
+ if (command === prefix) return true;
1675
+ const next = command.startsWith(prefix) ? command[prefix.length] : void 0;
1676
+ if (next !== void 0 && /\s/.test(next)) return true;
1677
+ }
1678
+ return false;
1679
+ }
1680
+ function planRewrite(input, config) {
1681
+ if (input.tool_name !== "Bash") return null;
1682
+ if (!config.autoWrap) return null;
1683
+ const toolInput = input.tool_input;
1684
+ if (toolInput === void 0) return null;
1685
+ if (toolInput.run_in_background === true) return null;
1686
+ const command = typeof toolInput.command === "string" ? toolInput.command.trim() : "";
1687
+ if (command.length === 0) return null;
1688
+ if (/\bjevprune\b/.test(command)) return null;
1689
+ if (hasStateChange(command)) return null;
1690
+ if (isInteractiveCommand(command)) return null;
1691
+ if (command.endsWith("&") && !command.endsWith("&&")) return null;
1692
+ if (isAllowlisted(command, config.allowlist)) return null;
1693
+ const transcript = typeof input.transcript_path === "string" && input.transcript_path.length > 0 ? ` --transcript ${quoteForShell(input.transcript_path)}` : "";
1694
+ return { command: `jevprune run --hook${transcript} -- bash -c ${quoteForShell(command)}` };
1695
+ }
1696
+ function parsePreToolUse(raw) {
1697
+ let parsed;
1698
+ try {
1699
+ parsed = JSON.parse(raw);
1700
+ } catch {
1701
+ return null;
1702
+ }
1703
+ if (!isRecord5(parsed)) return null;
1704
+ const toolName = parsed["tool_name"];
1705
+ const transcriptPath = parsed["transcript_path"];
1706
+ const toolInput = parsed["tool_input"];
1707
+ return {
1708
+ ...typeof toolName === "string" ? { tool_name: toolName } : {},
1709
+ ...typeof transcriptPath === "string" ? { transcript_path: transcriptPath } : {},
1710
+ ...isRecord5(toolInput) ? { tool_input: readToolInput(toolInput) } : {}
1711
+ };
1712
+ }
1713
+ function formatHookOutput(plan) {
1714
+ return JSON.stringify({
1715
+ hookSpecificOutput: {
1716
+ hookEventName: "PreToolUse",
1717
+ updatedInput: { command: plan.command }
1718
+ }
1719
+ });
1720
+ }
1721
+ function readToolInput(source) {
1722
+ const command = source["command"];
1723
+ const background = source["run_in_background"];
1724
+ return {
1725
+ ...typeof command === "string" ? { command } : {},
1726
+ ...typeof background === "boolean" ? { run_in_background: background } : {}
1727
+ };
1728
+ }
1729
+ function isRecord5(value) {
1730
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1731
+ }
1732
+ function escapeRegExp(value) {
1733
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1734
+ }
1735
+
1736
+ // src/io.ts
1737
+ var CLOSED_PIPE_CODES = /* @__PURE__ */ new Set([
1738
+ "EPIPE",
1739
+ "EOF",
1740
+ "ERR_STREAM_DESTROYED",
1741
+ "ERR_STREAM_WRITE_AFTER_END"
1742
+ ]);
1743
+ function processIo() {
1744
+ const write = openWriter(process.stdout);
1745
+ return {
1746
+ env: process.env,
1747
+ cwd: process.cwd(),
1748
+ stdin: process.stdin,
1749
+ write: (text) => write(text),
1750
+ writeBytes: (bytes) => write(bytes),
1751
+ writeError: openWriter(process.stderr)
1752
+ };
1753
+ }
1754
+ async function readStreamBytes(stream) {
1755
+ const chunks = [];
1756
+ for await (const chunk of stream) {
1757
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
1758
+ }
1759
+ return Buffer.concat(chunks);
1760
+ }
1761
+ async function readStream(stream) {
1762
+ return (await readStreamBytes(stream)).toString("utf8");
1763
+ }
1764
+ function isClosedPipe(error) {
1765
+ const code = errorCode(error);
1766
+ return code !== void 0 && CLOSED_PIPE_CODES.has(code);
1767
+ }
1768
+ function openWriter(stream) {
1769
+ let closed = false;
1770
+ stream.on("error", (error) => {
1771
+ if (isClosedPipe(error)) closed = true;
1772
+ });
1773
+ return (chunk) => new Promise((resolve2, reject) => {
1774
+ if (closed || chunk.length === 0) {
1775
+ resolve2();
1776
+ return;
1777
+ }
1778
+ try {
1779
+ stream.write(chunk, (error) => {
1780
+ if (error === void 0 || error === null) resolve2();
1781
+ else if (isClosedPipe(error)) {
1782
+ closed = true;
1783
+ resolve2();
1784
+ } else reject(error);
1785
+ });
1786
+ } catch (error) {
1787
+ if (!isClosedPipe(error)) {
1788
+ reject(error instanceof Error ? error : new Error(String(error)));
1789
+ return;
1790
+ }
1791
+ closed = true;
1792
+ resolve2();
1793
+ }
1794
+ });
1795
+ }
1796
+
1797
+ // src/commands/hook.ts
1798
+ async function runHook(io) {
1799
+ try {
1800
+ const input = parsePreToolUse(await readStream(io.stdin));
1801
+ if (input === null) return 0;
1802
+ const plan = planRewrite(input, await loadConfig(io.env));
1803
+ if (plan === null) return 0;
1804
+ await io.write(`${formatHookOutput(plan)}
1805
+ `);
1806
+ } catch (error) {
1807
+ await io.writeError(`jevprune: hook skipped: ${errorMessage(error)}
1808
+ `).catch(() => void 0);
1809
+ }
1810
+ return 0;
1811
+ }
1812
+
1813
+ // src/notices.ts
1814
+ import { mkdir as mkdir2, open as open2 } from "fs/promises";
1815
+ import { join as join3 } from "path";
1816
+ var MISSING_KEY_MARKER = "missing-key-notice";
1817
+ async function shouldAnnounceMissingKey(home) {
1818
+ const path = join3(home, MISSING_KEY_MARKER);
1819
+ try {
1820
+ await mkdir2(home, { recursive: true, mode: 448 });
1821
+ const handle = await open2(path, "wx", 384);
1822
+ await handle.close();
1823
+ return true;
1824
+ } catch (error) {
1825
+ return errorCode(error) !== "EEXIST";
1826
+ }
1827
+ }
1828
+
1829
+ // src/keeps.ts
1830
+ 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/;
1831
+ 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;
1832
+ function isSignatureLine(text) {
1833
+ return SIGNATURE_CASE_SENSITIVE.test(text) || SIGNATURE_CASE_INSENSITIVE.test(text);
1834
+ }
1835
+ function computeKeeps(lines, options) {
1836
+ const keeps = /* @__PURE__ */ new Map();
1837
+ const tailLines = Math.max(0, Math.trunc(options.tailLines));
1838
+ const contextLines = Math.max(0, Math.trunc(options.contextLines));
1839
+ const tailStart = lines.length - tailLines + 1;
1840
+ for (const line of lines) {
1841
+ if (line.n >= tailStart) keeps.set(line.n, "tail");
1842
+ }
1843
+ const seen = /* @__PURE__ */ new Set();
1844
+ const signatures = [];
1845
+ for (const line of lines) {
1846
+ if (!isSignatureLine(line.text)) continue;
1847
+ const key = line.text.trim();
1848
+ if (seen.has(key)) continue;
1849
+ seen.add(key);
1850
+ signatures.push(line.n);
1851
+ }
1852
+ for (const n of signatures) keeps.set(n, "signature");
1853
+ for (const n of signatures) {
1854
+ for (let offset = 1; offset <= contextLines; offset += 1) {
1855
+ for (const candidate of [n - offset, n + offset]) {
1856
+ if (candidate < 1 || candidate > lines.length) continue;
1857
+ if (!keeps.has(candidate)) keeps.set(candidate, "context");
1858
+ }
1859
+ }
1860
+ }
1861
+ return keeps;
1862
+ }
1863
+
1864
+ // src/lines.ts
1865
+ function splitLines(text) {
1866
+ const lines = [];
1867
+ const pattern = /\r\n|\n|\r/g;
1868
+ let start = 0;
1869
+ let match = pattern.exec(text);
1870
+ while (match !== null) {
1871
+ const raw = match[0];
1872
+ const terminator = raw === "\r\n" ? "\r\n" : raw === "\r" ? "\r" : "\n";
1873
+ lines.push({ n: lines.length + 1, text: text.slice(start, match.index), terminator });
1874
+ start = match.index + raw.length;
1875
+ match = pattern.exec(text);
1876
+ }
1877
+ if (start < text.length) {
1878
+ lines.push({ n: lines.length + 1, text: text.slice(start), terminator: "" });
1879
+ }
1880
+ return lines;
1881
+ }
1882
+
1883
+ // src/merge.ts
1884
+ function collapseMarker(range2, runId) {
1885
+ return `[jevprune: ${String(range2.count)} lines dropped, run ${runId}, lines ${String(range2.from)}-${String(range2.to)}]
1886
+ `;
1887
+ }
1888
+ function mergeDecisions(lines, decisions, options) {
1889
+ const minCollapseLines = Math.max(1, Math.trunc(options.minCollapseLines));
1890
+ const dropped = [];
1891
+ let kept = "";
1892
+ let run = null;
1893
+ let expected = 1;
1894
+ const flush = () => {
1895
+ if (run === null) return;
1896
+ const count = run.to - run.from + 1;
1897
+ if (!run.missing && count < minCollapseLines) {
1898
+ for (const line of run.lines) {
1899
+ decisions.set(line.n, { keep: true, reason: "collapse-min" });
1900
+ kept += line.text + line.terminator;
1901
+ }
1902
+ } else {
1903
+ const range2 = { from: run.from, to: run.to, count };
1904
+ dropped.push(range2);
1905
+ kept += collapseMarker(range2, options.runId);
1906
+ }
1907
+ run = null;
1908
+ };
1909
+ const dropLine = (line) => {
1910
+ if (run === null) run = { from: line.n, to: line.n, lines: [line], missing: false };
1911
+ else {
1912
+ run.to = line.n;
1913
+ run.lines.push(line);
1914
+ }
1915
+ };
1916
+ const dropMissing = (from, to) => {
1917
+ if (to < from) return;
1918
+ if (run === null) run = { from, to, lines: [], missing: true };
1919
+ else {
1920
+ run.to = to;
1921
+ run.missing = true;
1922
+ }
1923
+ };
1924
+ for (const line of lines) {
1925
+ dropMissing(expected, line.n - 1);
1926
+ expected = line.n + 1;
1927
+ if (decisions.get(line.n)?.keep === false) {
1928
+ dropLine(line);
1929
+ continue;
1930
+ }
1931
+ flush();
1932
+ kept += line.text + line.terminator;
1933
+ }
1934
+ if (options.totalLines !== void 0) dropMissing(expected, options.totalLines);
1935
+ flush();
1936
+ return { kept, dropped };
1937
+ }
1938
+
1939
+ // src/select.ts
1940
+ var UNAUTHORIZED_REASON = "unauthorized (401)";
1941
+ var NOT_UTF8_REASON = "not valid UTF-8";
1942
+ var NOT_UTF8_NOTE = "output is not valid UTF-8";
1943
+ 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.";
1944
+ var ITEM_OVERHEAD_TOKENS = 12;
1945
+ function passthroughSelection(input) {
1946
+ return {
1947
+ mode: "passthrough",
1948
+ kept: "",
1949
+ dropped: [],
1950
+ linesIn: input.lines,
1951
+ linesOut: input.lines,
1952
+ bytesIn: input.bytes,
1953
+ bytesOut: input.bytes,
1954
+ windows: 0,
1955
+ jevRequests: 0,
1956
+ jevInputTokens: 0,
1957
+ ...input.reason !== void 0 ? { fallbackReason: input.reason } : {},
1958
+ decisions: /* @__PURE__ */ new Map()
1959
+ };
1960
+ }
1961
+ function questionFor(n) {
1962
+ return `Is line ${String(n)} needed for the task?`;
1963
+ }
1964
+ async function selectLines(input) {
1965
+ const lines = splitLines(input.text);
1966
+ const bytesIn = Buffer.byteLength(input.text);
1967
+ if (input.oversize !== void 0) {
1968
+ return oversizeSelection(input, input.oversize, lines, bytesIn);
1969
+ }
1970
+ if (input.exitCode !== void 0 && input.exitCode !== null && input.exitCode !== 0 || input.interrupted === true) {
1971
+ return everyLine(lines, "passthrough", input.text, bytesIn);
1972
+ }
1973
+ if (lines.length <= input.config.fastPathLines) {
1974
+ return everyLine(lines, "fast-path", input.text, bytesIn);
1975
+ }
1976
+ const keeps = keepsOf(lines, input.config);
1977
+ const decisions = /* @__PURE__ */ new Map();
1978
+ const candidates = [];
1979
+ for (const line of lines) {
1980
+ const keep = keeps.get(line.n);
1981
+ if (keep !== void 0) {
1982
+ decisions.set(line.n, { keep: true, reason: keep });
1983
+ continue;
1984
+ }
1985
+ if (line.text.trim().length === 0) {
1986
+ decisions.set(line.n, { keep: false, reason: "blank" });
1987
+ continue;
1988
+ }
1989
+ candidates.push({ id: `l${String(line.n)}`, n: line.n, text: line.text });
1990
+ }
1991
+ if (input.client === null) {
1992
+ return fallbackSelection({ lines, keeps, input, reason: "no api key", bytesIn, linesIn: lines.length });
1993
+ }
1994
+ let verdicts;
1995
+ try {
1996
+ verdicts = await askJev(candidates, input, input.client);
1997
+ } catch (error) {
1998
+ return fallbackSelection({
1999
+ lines,
2000
+ keeps,
2001
+ input,
2002
+ reason: fallbackReason(error),
2003
+ bytesIn,
2004
+ linesIn: lines.length
2005
+ });
2006
+ }
2007
+ for (const n of verdicts.oversize) decisions.set(n, { keep: true, reason: "oversize" });
2008
+ for (const item of candidates) {
2009
+ if (decisions.has(item.n)) continue;
2010
+ const noul2 = verdicts.answers.get(item.n) ?? 0;
2011
+ decisions.set(item.n, { keep: noul2 >= input.config.threshold, reason: "jev", noul: noul2 });
2012
+ }
2013
+ const { kept, dropped } = mergeDecisions(lines, decisions, {
2014
+ minCollapseLines: input.config.minCollapseLines,
2015
+ runId: input.runId
2016
+ });
2017
+ return {
2018
+ mode: "jev",
2019
+ kept,
2020
+ dropped,
2021
+ linesIn: lines.length,
2022
+ linesOut: splitLines(kept).length,
2023
+ bytesIn,
2024
+ bytesOut: Buffer.byteLength(kept),
2025
+ windows: verdicts.windows,
2026
+ jevRequests: verdicts.jevRequests,
2027
+ jevInputTokens: verdicts.jevInputTokens,
2028
+ decisions
2029
+ };
2030
+ }
2031
+ async function askJev(candidates, input, client) {
2032
+ const answers = /* @__PURE__ */ new Map();
2033
+ if (candidates.length === 0) {
2034
+ return { windows: 0, jevRequests: 0, jevInputTokens: 0, answers, oversize: [] };
2035
+ }
2036
+ const lastLine = candidates[candidates.length - 1]?.n ?? 0;
2037
+ const plan = planWindows(candidates, {
2038
+ budgetTokens: input.config.windowTokens,
2039
+ overheadTokens: estimateJsonTokens(stateOf(input, [])),
2040
+ itemOverheadTokens: estimateTokens(questionFor(lastLine)) + ITEM_OVERHEAD_TOKENS,
2041
+ costTokens: (item) => estimateJsonTokens({ n: item.n, text: item.text })
2042
+ });
2043
+ const results = await runWindows(
2044
+ plan.windows,
2045
+ async (window, _index, options) => {
2046
+ const questions = {};
2047
+ for (const item of window) questions[item.id] = questionFor(item.n);
2048
+ return await client.noul({ state: stateOf(input, window), questions }, options);
2049
+ },
2050
+ {
2051
+ concurrency: input.config.concurrency,
2052
+ timeoutMs: input.config.windowTimeoutMs,
2053
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
2054
+ }
2055
+ );
2056
+ let jevInputTokens = 0;
2057
+ for (const [index, result] of results.entries()) {
2058
+ jevInputTokens += result.usage.inputTokens;
2059
+ for (const item of plan.windows[index] ?? []) {
2060
+ const noul2 = result.answers[item.id];
2061
+ if (noul2 !== void 0) answers.set(item.n, noul2);
2062
+ }
2063
+ }
2064
+ return {
2065
+ windows: plan.windows.length,
2066
+ jevRequests: results.length,
2067
+ jevInputTokens,
2068
+ answers,
2069
+ oversize: plan.oversize.map((item) => item.n)
2070
+ };
2071
+ }
2072
+ function stateOf(input, window) {
2073
+ return {
2074
+ command: input.command,
2075
+ task: input.task,
2076
+ rubric: RUBRIC,
2077
+ lines: window.map((item) => ({ n: item.n, text: item.text }))
2078
+ };
2079
+ }
2080
+ function keepsOf(lines, config) {
2081
+ return computeKeeps(lines, { tailLines: config.tailLines, contextLines: config.contextLines });
2082
+ }
2083
+ function fallbackSelection(fallback) {
2084
+ const { lines, keeps, input } = fallback;
2085
+ return fallbackResult({
2086
+ lines,
2087
+ decisions: fallbackDecisions(lines, keeps, input.config.headLines),
2088
+ input,
2089
+ reason: fallback.reason,
2090
+ bytesIn: fallback.bytesIn,
2091
+ linesIn: fallback.linesIn
2092
+ });
2093
+ }
2094
+ function oversizeSelection(input, oversize, captured, bytesIn) {
2095
+ const headCount = Math.min(Math.max(0, Math.trunc(oversize.headSegmentLines)), captured.length);
2096
+ const totalLines = Math.max(Math.trunc(oversize.lines), captured.length);
2097
+ const tailFirst = totalLines - captured.length + headCount + 1;
2098
+ const head = captured.slice(0, headCount);
2099
+ const tail = renumber(captured.slice(headCount), tailFirst);
2100
+ const contextLines = input.config.contextLines;
2101
+ const keeps = computeKeeps(head, { tailLines: 0, contextLines });
2102
+ const tailKeeps = computeKeeps(renumber(tail, 1), { tailLines: input.config.tailLines, contextLines });
2103
+ for (const [n, reason] of tailKeeps) keeps.set(n + tailFirst - 1, reason);
2104
+ const lines = [...head, ...tail];
2105
+ return fallbackResult({
2106
+ lines,
2107
+ decisions: fallbackDecisions(lines, keeps, input.config.headLines),
2108
+ input,
2109
+ reason: `output over ${String(input.config.maxPruneBytes)} bytes`,
2110
+ bytesIn,
2111
+ linesIn: totalLines,
2112
+ totalLines
2113
+ });
2114
+ }
2115
+ function fallbackDecisions(lines, keeps, headLines) {
2116
+ const head = Math.max(0, Math.trunc(headLines));
2117
+ const decisions = /* @__PURE__ */ new Map();
2118
+ for (const line of lines) {
2119
+ const keep = keeps.get(line.n);
2120
+ if (keep !== void 0) decisions.set(line.n, { keep: true, reason: keep });
2121
+ else if (line.n <= head) decisions.set(line.n, { keep: true, reason: "head" });
2122
+ else decisions.set(line.n, { keep: false, reason: "fallback" });
2123
+ }
2124
+ return decisions;
2125
+ }
2126
+ function renumber(lines, from) {
2127
+ return lines.map((line, index) => ({ ...line, n: from + index }));
2128
+ }
2129
+ function fallbackResult(fallback) {
2130
+ const { kept, dropped } = mergeDecisions(fallback.lines, fallback.decisions, {
2131
+ minCollapseLines: fallback.input.config.minCollapseLines,
2132
+ runId: fallback.input.runId,
2133
+ ...fallback.totalLines !== void 0 ? { totalLines: fallback.totalLines } : {}
2134
+ });
2135
+ return {
2136
+ mode: "fallback",
2137
+ kept,
2138
+ dropped,
2139
+ linesIn: fallback.linesIn,
2140
+ linesOut: splitLines(kept).length,
2141
+ bytesIn: fallback.bytesIn,
2142
+ bytesOut: Buffer.byteLength(kept),
2143
+ windows: 0,
2144
+ jevRequests: 0,
2145
+ jevInputTokens: 0,
2146
+ fallbackReason: fallback.reason,
2147
+ decisions: fallback.decisions
2148
+ };
2149
+ }
2150
+ function fallbackReason(error) {
2151
+ if (error instanceof JevTimeoutError) return "timeout";
2152
+ if (error instanceof JevResponseError) return "invalid response";
2153
+ if (error instanceof JevRequestError) {
2154
+ switch (error.status) {
2155
+ case 429:
2156
+ return "rate limited (429)";
2157
+ case 529:
2158
+ return "overloaded (529)";
2159
+ case 401:
2160
+ return UNAUTHORIZED_REASON;
2161
+ case 400:
2162
+ return "bad request (400)";
2163
+ case void 0:
2164
+ return "network";
2165
+ default:
2166
+ return error.name;
2167
+ }
2168
+ }
2169
+ if (error instanceof Error) return error.name;
2170
+ return "unknown";
2171
+ }
2172
+ function everyLine(lines, mode, text, bytesIn) {
2173
+ const decisions = /* @__PURE__ */ new Map();
2174
+ for (const line of lines) decisions.set(line.n, { keep: true, reason: mode });
2175
+ return {
2176
+ mode,
2177
+ kept: text,
2178
+ dropped: [],
2179
+ linesIn: lines.length,
2180
+ linesOut: lines.length,
2181
+ bytesIn,
2182
+ bytesOut: bytesIn,
2183
+ windows: 0,
2184
+ jevRequests: 0,
2185
+ jevInputTokens: 0,
2186
+ decisions
2187
+ };
2188
+ }
2189
+
2190
+ // src/prune.ts
2191
+ async function pruneOutput(input) {
2192
+ const env = input.env ?? process.env;
2193
+ const config = mergeConfig(await loadConfig(env), input.config);
2194
+ const client = input.client !== void 0 ? input.client : clientFromEnv(env);
2195
+ const runId = newRunId();
2196
+ const command = input.command ?? "";
2197
+ const store = input.save === false ? null : new RunStore({ home: config.home, retention: config.retention });
2198
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2199
+ const selection = await selectLines({
2200
+ text: input.text,
2201
+ task: input.task,
2202
+ command,
2203
+ exitCode: input.exitCode ?? null,
2204
+ client,
2205
+ config,
2206
+ runId
2207
+ });
2208
+ const recorded = await recordRun({
2209
+ store,
2210
+ selection,
2211
+ logText: input.text,
2212
+ meta: {
2213
+ id: runId,
2214
+ command,
2215
+ argv: [],
2216
+ startedAt,
2217
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2218
+ exitCode: input.exitCode ?? null,
2219
+ signal: null,
2220
+ bytes: selection.bytesIn,
2221
+ lines: selection.linesIn,
2222
+ task: input.task
2223
+ }
2224
+ });
2225
+ return {
2226
+ kept: selection.kept,
2227
+ dropped: selection.dropped,
2228
+ runId,
2229
+ mode: selection.mode,
2230
+ linesIn: selection.linesIn,
2231
+ linesOut: selection.linesOut,
2232
+ ...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {},
2233
+ ...recorded.logPath !== void 0 ? { logPath: recorded.logPath } : {},
2234
+ footer: recorded.footer
2235
+ };
2236
+ }
2237
+ async function recordRun(input) {
2238
+ const { store, selection, meta } = input;
2239
+ const fastPath = selection.mode === "fast-path";
2240
+ const log = input.logBytes ?? (input.logText !== void 0 ? Buffer.from(input.logText, "utf8") : void 0);
2241
+ let failureCode = input.storeFailureCode;
2242
+ if (store !== null && failureCode === void 0) {
2243
+ try {
2244
+ if (fastPath) {
2245
+ if (log === void 0) await store.discardRun(meta.id);
2246
+ } else {
2247
+ if (log !== void 0) await writeLog(store, meta.id, log);
2248
+ await store.finalizeRun(meta.id, {
2249
+ ...meta,
2250
+ mode: selection.mode,
2251
+ linesOut: selection.linesOut,
2252
+ ...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {}
2253
+ });
2254
+ }
2255
+ await store.appendGain({
2256
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2257
+ id: meta.id,
2258
+ mode: selection.mode,
2259
+ linesIn: selection.linesIn,
2260
+ linesOut: selection.linesOut,
2261
+ bytesIn: selection.bytesIn,
2262
+ bytesOut: selection.bytesOut,
2263
+ ...selection.fallbackReason !== void 0 ? { reason: selection.fallbackReason } : {}
2264
+ });
2265
+ await store.enforceRetention();
2266
+ } catch (error) {
2267
+ if (!(error instanceof RunStoreError)) throw error;
2268
+ failureCode = error.code ?? "failed";
2269
+ }
2270
+ }
2271
+ const logPath = store !== null && !fastPath && failureCode === void 0 ? store.logPath(meta.id) : void 0;
2272
+ const footer = formatFooter({
2273
+ mode: selection.mode,
2274
+ linesIn: selection.linesIn,
2275
+ linesOut: selection.linesOut,
2276
+ exitCode: meta.exitCode,
2277
+ ...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {},
2278
+ ...input.passthroughNote !== void 0 ? { passthroughNote: input.passthroughNote } : {},
2279
+ ...failureCode !== void 0 ? { storeFailureCode: failureCode } : {},
2280
+ ...logPath !== void 0 ? { logPath } : {}
2281
+ });
2282
+ return { footer, ...logPath !== void 0 ? { logPath } : {} };
2283
+ }
2284
+ function clientFromEnv(env) {
2285
+ try {
2286
+ return createJevClientFromEnv(env);
2287
+ } catch (error) {
2288
+ if (error instanceof JevConfigError) return null;
2289
+ throw error;
2290
+ }
2291
+ }
2292
+ function mergeConfig(base, overrides) {
2293
+ if (overrides === void 0) return base;
2294
+ const defined = {};
2295
+ for (const key of Object.keys(overrides)) {
2296
+ if (overrides[key] !== void 0) Object.assign(defined, { [key]: overrides[key] });
2297
+ }
2298
+ return { ...base, ...defined };
2299
+ }
2300
+ async function writeLog(store, id, bytes) {
2301
+ const writer = await store.openRun({ id });
2302
+ writer.write(bytes);
2303
+ await writer.close();
2304
+ if (writer.failure !== void 0) throw writer.failure;
2305
+ }
2306
+
2307
+ // src/runner.ts
2308
+ import { spawn } from "child_process";
2309
+ import { constants } from "os";
2310
+ var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
2311
+ var TAIL_RING_BYTES = 256 * 1024;
2312
+ var LF3 = 10;
2313
+ var CR2 = 13;
2314
+ var LineCounter = class {
2315
+ #terminators = 0;
2316
+ #pendingCr = false;
2317
+ #lastByte;
2318
+ push(chunk) {
2319
+ for (const byte of chunk) {
2320
+ if (this.#pendingCr) {
2321
+ this.#pendingCr = false;
2322
+ this.#terminators += 1;
2323
+ if (byte === LF3) {
2324
+ this.#lastByte = byte;
2325
+ continue;
2326
+ }
2327
+ }
2328
+ if (byte === CR2) {
2329
+ this.#pendingCr = true;
2330
+ this.#lastByte = byte;
2331
+ continue;
2332
+ }
2333
+ if (byte === LF3) this.#terminators += 1;
2334
+ this.#lastByte = byte;
2335
+ }
2336
+ }
2337
+ get terminators() {
2338
+ return this.#terminators + (this.#pendingCr ? 1 : 0);
2339
+ }
2340
+ get lines() {
2341
+ if (this.#lastByte === void 0) return 0;
2342
+ const endsWithTerminator = this.#lastByte === LF3 || this.#lastByte === CR2;
2343
+ return this.terminators + (endsWithTerminator ? 0 : 1);
2344
+ }
2345
+ };
2346
+ var CaptureBuffer = class {
2347
+ #maxBytes;
2348
+ #head = [];
2349
+ #ring = [];
2350
+ #headCounter = new LineCounter();
2351
+ #headBytes = 0;
2352
+ #ringBytes = 0;
2353
+ #oversize = false;
2354
+ constructor(maxBytes) {
2355
+ this.#maxBytes = maxBytes;
2356
+ }
2357
+ get oversize() {
2358
+ return this.#oversize;
2359
+ }
2360
+ get headSegmentLines() {
2361
+ return this.#oversize ? this.#headCounter.terminators : this.#headCounter.lines;
2362
+ }
2363
+ push(chunk) {
2364
+ let rest = chunk;
2365
+ if (!this.#oversize) {
2366
+ const room = this.#maxBytes - this.#headBytes;
2367
+ if (rest.length <= room) {
2368
+ this.#head.push(rest);
2369
+ this.#headCounter.push(rest);
2370
+ this.#headBytes += rest.length;
2371
+ return;
2372
+ }
2373
+ if (room > 0) {
2374
+ const head = rest.subarray(0, room);
2375
+ this.#head.push(head);
2376
+ this.#headCounter.push(head);
2377
+ this.#headBytes += room;
2378
+ rest = rest.subarray(room);
2379
+ }
2380
+ this.#oversize = true;
2381
+ }
2382
+ this.#ring.push(rest);
2383
+ this.#ringBytes += rest.length;
2384
+ this.#trimRing();
2385
+ }
2386
+ bytes() {
2387
+ if (!this.#oversize) return Buffer.concat(this.#head);
2388
+ const head = trimToLastTerminator(Buffer.concat(this.#head));
2389
+ const tail = trimToFirstLine(Buffer.concat(this.#ring));
2390
+ return Buffer.concat([head, tail]);
2391
+ }
2392
+ #trimRing() {
2393
+ while (this.#ringBytes > TAIL_RING_BYTES) {
2394
+ const first = this.#ring[0];
2395
+ if (first === void 0) return;
2396
+ const excess = this.#ringBytes - TAIL_RING_BYTES;
2397
+ if (first.length <= excess) {
2398
+ this.#ring.shift();
2399
+ this.#ringBytes -= first.length;
2400
+ } else {
2401
+ this.#ring[0] = first.subarray(excess);
2402
+ this.#ringBytes -= excess;
2403
+ }
2404
+ }
2405
+ }
2406
+ };
2407
+ async function runCommand(input) {
2408
+ const executable = input.argv[0];
2409
+ if (executable === void 0 || executable.length === 0) {
2410
+ throw new UsageError("run needs a command to execute");
2411
+ }
2412
+ const writer = await openWriter2(input);
2413
+ const buffer = new CaptureBuffer(input.maxPruneBytes);
2414
+ const counter = new LineCounter();
2415
+ let bytes = 0;
2416
+ let storeFailure = writer.failure;
2417
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2418
+ const child = spawn(executable, input.argv.slice(1), {
2419
+ stdio: ["inherit", "pipe", "pipe"],
2420
+ shell: false,
2421
+ ...input.cwd !== void 0 ? { cwd: input.cwd } : {},
2422
+ ...input.env !== void 0 ? { env: input.env } : {}
2423
+ });
2424
+ let paused = false;
2425
+ const onChunk = (chunk) => {
2426
+ bytes += chunk.length;
2427
+ counter.push(chunk);
2428
+ buffer.push(chunk);
2429
+ const ready = writer.write(chunk);
2430
+ if (ready || paused) return;
2431
+ paused = true;
2432
+ child.stdout.pause();
2433
+ child.stderr.pause();
2434
+ writer.onDrain(() => {
2435
+ paused = false;
2436
+ child.stdout.resume();
2437
+ child.stderr.resume();
2438
+ });
2439
+ };
2440
+ child.stdout.on("data", onChunk);
2441
+ child.stderr.on("data", onChunk);
2442
+ let receivedSignal = false;
2443
+ const handlers = installSignalHandlers(child, (signal) => {
2444
+ receivedSignal = true;
2445
+ input.onSignalForward?.(signal);
2446
+ });
2447
+ let exit;
2448
+ try {
2449
+ exit = await waitForExit(child, executable);
2450
+ } finally {
2451
+ removeSignalHandlers(handlers);
2452
+ await writer.close();
2453
+ }
2454
+ storeFailure = writer.failure ?? storeFailure;
2455
+ const captured = buffer.bytes();
2456
+ return {
2457
+ captured,
2458
+ validUtf8: isValidUtf8(captured),
2459
+ bytes,
2460
+ lines: counter.lines,
2461
+ headSegmentLines: buffer.headSegmentLines,
2462
+ exitCode: exitCodeOf(exit),
2463
+ signal: exit.signal,
2464
+ interrupted: receivedSignal || exit.signal !== null,
2465
+ oversize: buffer.oversize,
2466
+ startedAt,
2467
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2468
+ ...storeFailure !== void 0 ? { storeFailure } : {}
2469
+ };
2470
+ }
2471
+ var NO_WRITER = {
2472
+ path: "",
2473
+ failure: void 0,
2474
+ write: () => true,
2475
+ onDrain: (listener) => {
2476
+ queueMicrotask(listener);
2477
+ },
2478
+ close: () => Promise.resolve()
2479
+ };
2480
+ async function openWriter2(input) {
2481
+ if (input.store === null) return NO_WRITER;
2482
+ try {
2483
+ return await input.store.openRun({ id: input.runId });
2484
+ } catch (error) {
2485
+ const failure = error instanceof RunStoreError ? error : new RunStoreError(`run log could not be opened: ${errorMessage(error)}`, { cause: error });
2486
+ return { ...NO_WRITER, failure };
2487
+ }
2488
+ }
2489
+ function waitForExit(child, executable) {
2490
+ return new Promise((resolve2, reject) => {
2491
+ child.on("error", (error) => {
2492
+ reject(spawnError(error, executable));
2493
+ });
2494
+ child.on("close", (code, signal) => {
2495
+ resolve2({ code, signal });
2496
+ });
2497
+ });
2498
+ }
2499
+ function spawnError(error, executable) {
2500
+ const code = errorCode(error);
2501
+ const message = code === "ENOENT" ? `command not found: ${executable}` : code === "EACCES" ? `command not executable: ${executable}` : `command could not start: ${executable}: ${errorMessage(error)}`;
2502
+ return new SpawnError(message, {
2503
+ executable,
2504
+ ...code !== void 0 ? { code } : {},
2505
+ cause: error
2506
+ });
2507
+ }
2508
+ function installSignalHandlers(child, onForward) {
2509
+ const handlers = /* @__PURE__ */ new Map();
2510
+ for (const signal of FORWARDED_SIGNALS) {
2511
+ const handler = () => {
2512
+ onForward(signal);
2513
+ child.kill(signal);
2514
+ };
2515
+ process.on(signal, handler);
2516
+ handlers.set(signal, handler);
2517
+ }
2518
+ return handlers;
2519
+ }
2520
+ function removeSignalHandlers(handlers) {
2521
+ for (const [signal, handler] of handlers) process.off(signal, handler);
2522
+ }
2523
+ function exitCodeOf(exit) {
2524
+ if (exit.code !== null) return exit.code;
2525
+ if (exit.signal !== null) return 128 + (constants.signals[exit.signal] ?? 0);
2526
+ return 0;
2527
+ }
2528
+ function trimToLastTerminator(buffer) {
2529
+ for (let index = buffer.length - 1; index >= 0; index -= 1) {
2530
+ const byte = buffer[index];
2531
+ if (byte === LF3 || byte === CR2) return buffer.subarray(0, index + 1);
2532
+ }
2533
+ return buffer.subarray(0, 0);
2534
+ }
2535
+ function trimToFirstLine(buffer) {
2536
+ for (let index = 0; index < buffer.length; index += 1) {
2537
+ const byte = buffer[index];
2538
+ if (byte === LF3) return buffer.subarray(index + 1);
2539
+ if (byte === CR2) {
2540
+ const next = buffer[index + 1];
2541
+ return buffer.subarray(next === LF3 ? index + 2 : index + 1);
2542
+ }
2543
+ }
2544
+ return Buffer.alloc(0);
2545
+ }
2546
+
2547
+ // src/task.ts
2548
+ import { readFile as readFile3 } from "fs/promises";
2549
+ var MAX_TASK_LENGTH = 400;
2550
+ async function resolveTask(input) {
2551
+ const flag = input.flag?.trim() ?? "";
2552
+ if (flag.length > 0) return { task: flag, source: "flag" };
2553
+ const fromEnv = input.env?.[TASK_ENV]?.trim() ?? "";
2554
+ if (fromEnv.length > 0) return { task: fromEnv, source: "env" };
2555
+ if (input.transcriptPath !== void 0 && input.transcriptPath.length > 0) {
2556
+ const fromTranscript = await readTranscriptTask(input.transcriptPath).catch((error) => {
2557
+ if (error instanceof TranscriptError) return null;
2558
+ throw error;
2559
+ });
2560
+ if (fromTranscript !== null) return { task: fromTranscript, source: "transcript" };
2561
+ }
2562
+ return { task: input.command, source: "command" };
2563
+ }
2564
+ async function readTranscriptTask(path) {
2565
+ let raw;
2566
+ try {
2567
+ raw = await readFile3(path, "utf8");
2568
+ } catch (error) {
2569
+ throw new TranscriptError(`transcript ${path} could not be read: ${errorMessage(error)}`, {
2570
+ path,
2571
+ cause: error
2572
+ });
2573
+ }
2574
+ for (const line of raw.split("\n")) {
2575
+ if (line.trim().length === 0) continue;
2576
+ let parsed;
2577
+ try {
2578
+ parsed = JSON.parse(line);
2579
+ } catch {
2580
+ continue;
2581
+ }
2582
+ const text = userText(parsed);
2583
+ if (text !== null) return text;
2584
+ }
2585
+ return null;
2586
+ }
2587
+ function userText(entry2) {
2588
+ if (!isRecord6(entry2) || entry2["type"] !== "user") return null;
2589
+ const message = entry2["message"];
2590
+ if (!isRecord6(message)) return null;
2591
+ const content = message["content"];
2592
+ if (typeof content === "string") return usableText(content);
2593
+ if (!Array.isArray(content)) return null;
2594
+ for (const item of content) {
2595
+ if (!isRecord6(item) || item["type"] !== "text") continue;
2596
+ const text = item["text"];
2597
+ if (typeof text !== "string") continue;
2598
+ const usable = usableText(text);
2599
+ if (usable !== null) return usable;
2600
+ }
2601
+ return null;
2602
+ }
2603
+ function usableText(text) {
2604
+ const trimmed = text.trim();
2605
+ if (trimmed.length === 0 || trimmed.startsWith("<")) return null;
2606
+ return trimmed.slice(0, MAX_TASK_LENGTH);
2607
+ }
2608
+ function isRecord6(value) {
2609
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2610
+ }
2611
+
2612
+ // src/commands/run.ts
2613
+ async function runRun(options, io) {
2614
+ if (options.argv.length === 0) throw new UsageError("run needs a command after --");
2615
+ const loaded = await loadConfig(io.env);
2616
+ const config = options.threshold !== void 0 ? { ...loaded, threshold: options.threshold } : loaded;
2617
+ const store = new RunStore({ home: config.home, retention: config.retention });
2618
+ const runId = newRunId();
2619
+ const command = options.argv.join(" ");
2620
+ const transcriptPath = options.hook === true ? options.transcript : void 0;
2621
+ const { task } = await resolveTask({
2622
+ ...options.task !== void 0 ? { flag: options.task } : {},
2623
+ env: io.env,
2624
+ ...transcriptPath !== void 0 ? { transcriptPath } : {},
2625
+ command
2626
+ });
2627
+ const client = clientFromEnv(io.env);
2628
+ if (options.hook === true && client === null && await shouldAnnounceMissingKey(config.home)) {
2629
+ await io.writeError(`jevprune: ${TYPESAFE_API_KEY_ENV} not set, using fallback
2630
+ `).catch(() => void 0);
2631
+ }
2632
+ const capture = await runCommand({
2633
+ argv: options.argv,
2634
+ runId,
2635
+ store,
2636
+ maxPruneBytes: config.maxPruneBytes,
2637
+ cwd: io.cwd,
2638
+ env: io.env
2639
+ });
2640
+ const meta = {
2641
+ id: runId,
2642
+ command,
2643
+ argv: [...options.argv],
2644
+ startedAt: capture.startedAt,
2645
+ endedAt: capture.endedAt,
2646
+ exitCode: capture.exitCode,
2647
+ signal: capture.signal,
2648
+ bytes: capture.bytes,
2649
+ lines: capture.lines,
2650
+ task
2651
+ };
2652
+ const failed = capture.exitCode !== 0 || capture.interrupted;
2653
+ if (!capture.validUtf8 || capture.oversize && failed) {
2654
+ return await passThrough({ capture, store, meta, maxPruneBytes: config.maxPruneBytes }, io);
2655
+ }
2656
+ try {
2657
+ const selection = await selectLines({
2658
+ text: capture.captured.toString("utf8"),
2659
+ task,
2660
+ command,
2661
+ exitCode: capture.exitCode,
2662
+ interrupted: capture.interrupted,
2663
+ ...capture.oversize ? { oversize: { lines: capture.lines, headSegmentLines: capture.headSegmentLines } } : {},
2664
+ client,
2665
+ config,
2666
+ runId
2667
+ });
2668
+ if (options.hook === true && selection.fallbackReason === UNAUTHORIZED_REASON) {
2669
+ await io.writeError(`jevprune: ${TYPESAFE_API_KEY_ENV} rejected (401), using fallback
2670
+ `).catch(() => void 0);
2671
+ }
2672
+ const { footer } = await recordRun({
2673
+ store,
2674
+ selection,
2675
+ meta,
2676
+ ...capture.storeFailure !== void 0 ? { storeFailureCode: capture.storeFailure.code ?? "failed" } : {}
2677
+ });
2678
+ await io.write(withFooter(selection.kept, footer));
2679
+ } catch (error) {
2680
+ await io.writeBytes(capture.captured).catch(() => void 0);
2681
+ await io.writeError(`jevprune: pruning failed (${errorName(error)}), output passed through
2682
+ `).catch(() => void 0);
2683
+ }
2684
+ return capture.exitCode;
2685
+ }
2686
+ async function passThrough(input, io) {
2687
+ const { capture } = input;
2688
+ let storeFailureCode = capture.storeFailure === void 0 ? void 0 : capture.storeFailure.code ?? "failed";
2689
+ let written = 0;
2690
+ let lastByte;
2691
+ let complete = false;
2692
+ if (capture.oversize && storeFailureCode === void 0) {
2693
+ try {
2694
+ for await (const chunk of input.store.readRunChunks(input.meta.id)) {
2695
+ await io.writeBytes(chunk).catch(() => void 0);
2696
+ if (chunk.length === 0) continue;
2697
+ written += chunk.length;
2698
+ lastByte = chunk[chunk.length - 1];
2699
+ }
2700
+ complete = true;
2701
+ } catch (error) {
2702
+ storeFailureCode = error instanceof RunStoreError ? error.code ?? "failed" : "failed";
2703
+ }
2704
+ }
2705
+ if (!complete && written === 0) {
2706
+ await io.writeBytes(capture.captured).catch(() => void 0);
2707
+ lastByte = capture.captured.at(-1);
2708
+ complete = !capture.oversize;
2709
+ }
2710
+ const oversizeReason = `output over ${String(input.maxPruneBytes)} bytes`;
2711
+ const reason = capture.validUtf8 ? complete ? void 0 : oversizeReason : NOT_UTF8_REASON;
2712
+ const note = capture.validUtf8 ? reason : NOT_UTF8_NOTE;
2713
+ try {
2714
+ const { footer } = await recordRun({
2715
+ store: input.store,
2716
+ selection: passthroughSelection({
2717
+ bytes: capture.bytes,
2718
+ lines: capture.lines,
2719
+ ...reason !== void 0 ? { reason } : {}
2720
+ }),
2721
+ ...note !== void 0 ? { passthroughNote: note } : {},
2722
+ meta: input.meta,
2723
+ ...storeFailureCode !== void 0 ? { storeFailureCode } : {}
2724
+ });
2725
+ await io.write(footerAfter(lastByte, footer)).catch(() => void 0);
2726
+ } catch (error) {
2727
+ await io.writeError(`jevprune: pruning failed (${errorName(error)}), output passed through
2728
+ `).catch(() => void 0);
2729
+ }
2730
+ return capture.exitCode;
2731
+ }
2732
+
2733
+ // src/commands/select.ts
2734
+ import { readFile as readFile4 } from "fs/promises";
2735
+ async function runSelect(options, io) {
2736
+ let input;
2737
+ try {
2738
+ input = options.file !== void 0 ? await readFile4(options.file) : await readStreamBytes(io.stdin);
2739
+ } catch (error) {
2740
+ await io.writeError(`jevprune: input could not be read: ${errorMessage(error)}
2741
+ `);
2742
+ return 1;
2743
+ }
2744
+ const command = options.command ?? "";
2745
+ const { task } = await resolveTask({
2746
+ ...options.task !== void 0 ? { flag: options.task } : {},
2747
+ env: io.env,
2748
+ command
2749
+ });
2750
+ if (!isValidUtf8(input)) {
2751
+ return await passThrough2({ bytes: input, task, command, env: io.env }, io);
2752
+ }
2753
+ const result = await pruneOutput({
2754
+ text: input.toString("utf8"),
2755
+ task,
2756
+ command,
2757
+ exitCode: null,
2758
+ env: io.env,
2759
+ ...options.threshold !== void 0 ? { config: { threshold: options.threshold } } : {}
2760
+ });
2761
+ await io.write(withFooter(result.kept, result.footer));
2762
+ return 0;
2763
+ }
2764
+ async function passThrough2(input, io) {
2765
+ const config = await loadConfig(input.env);
2766
+ const store = new RunStore({ home: config.home, retention: config.retention });
2767
+ const lines = countByteLines(input.bytes);
2768
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2769
+ const { footer } = await recordRun({
2770
+ store,
2771
+ selection: passthroughSelection({ bytes: input.bytes.length, lines, reason: NOT_UTF8_REASON }),
2772
+ logBytes: input.bytes,
2773
+ passthroughNote: NOT_UTF8_NOTE,
2774
+ meta: {
2775
+ id: newRunId(),
2776
+ command: input.command,
2777
+ argv: [],
2778
+ startedAt,
2779
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
2780
+ exitCode: null,
2781
+ signal: null,
2782
+ bytes: input.bytes.length,
2783
+ lines,
2784
+ task: input.task
2785
+ }
2786
+ });
2787
+ await io.writeBytes(input.bytes);
2788
+ await io.write(footerAfter(input.bytes.at(-1), footer));
2789
+ return 0;
2790
+ }
2791
+
2792
+ // src/commands/show.ts
2793
+ function parseLineRange(value) {
2794
+ const match = /^(\d+)[-:](\d+)$/.exec(value.trim());
2795
+ if (match === null) {
2796
+ throw new LineRangeError(`--lines must be A-B or A:B, got ${JSON.stringify(value)}`);
2797
+ }
2798
+ const from = Number(match[1] ?? "");
2799
+ const to = Number(match[2] ?? "");
2800
+ if (from < 1 || to < from) {
2801
+ throw new LineRangeError(`--lines must be a range with 1 <= A <= B, got ${JSON.stringify(value)}`);
2802
+ }
2803
+ return { from, to };
2804
+ }
2805
+ async function runShow(options, io) {
2806
+ const config = await loadConfig(io.env);
2807
+ const store = new RunStore({ home: config.home, retention: config.retention });
2808
+ if (options.lines === void 0) {
2809
+ await io.writeBytes(await store.readRunBytes(options.id));
2810
+ return 0;
2811
+ }
2812
+ const range2 = parseLineRange(options.lines);
2813
+ await io.writeBytes(await store.readRunLineBytes(options.id, range2.from, range2.to));
2814
+ return 0;
2815
+ }
2816
+
2817
+ // src/version.ts
2818
+ var VERSION2 = "0.1.0";
2819
+
2820
+ // src/cli.ts
2821
+ var HELP = `usage: jevprune <command> [options]
2822
+
2823
+ commands:
2824
+ run [--task <text>] [--threshold <n>] [--hook] [--transcript <path>] -- <command> [args...]
2825
+ select [--task <text>] [--threshold <n>] [--file <path>] [--command <text>]
2826
+ show <id> [--lines A-B]
2827
+ gain
2828
+ hook
2829
+
2830
+ options:
2831
+ --help
2832
+ --version
2833
+ `;
2834
+ async function runCli(argv, io) {
2835
+ try {
2836
+ return await dispatch(argv, io);
2837
+ } catch (error) {
2838
+ return await report(error, io);
2839
+ }
2840
+ }
2841
+ async function dispatch(argv, io) {
2842
+ const separator = argv.indexOf("--");
2843
+ const own = separator === -1 ? [...argv] : argv.slice(0, separator);
2844
+ const rest = separator === -1 ? [] : argv.slice(separator + 1);
2845
+ const command = own[0];
2846
+ if (command === void 0) {
2847
+ await io.writeError(HELP);
2848
+ return 2;
2849
+ }
2850
+ if (command === "--help" || command === "-h" || command === "help") {
2851
+ await io.write(HELP);
2852
+ return 0;
2853
+ }
2854
+ if (command === "--version" || command === "-v") {
2855
+ await io.write(`${VERSION2}
2856
+ `);
2857
+ return 0;
2858
+ }
2859
+ const args = own.slice(1);
2860
+ switch (command) {
2861
+ case "run": {
2862
+ const values = parse(args, {
2863
+ task: { type: "string" },
2864
+ threshold: { type: "string" },
2865
+ hook: { type: "boolean" },
2866
+ transcript: { type: "string" }
2867
+ });
2868
+ return await runRun(
2869
+ {
2870
+ argv: rest,
2871
+ task: values.task,
2872
+ threshold: values.threshold === void 0 ? void 0 : parseThreshold(values.threshold),
2873
+ hook: values.hook,
2874
+ transcript: values.transcript
2875
+ },
2876
+ io
2877
+ );
2878
+ }
2879
+ case "select": {
2880
+ const values = parse(args, {
2881
+ task: { type: "string" },
2882
+ threshold: { type: "string" },
2883
+ file: { type: "string" },
2884
+ command: { type: "string" }
2885
+ });
2886
+ return await runSelect(
2887
+ {
2888
+ task: values.task,
2889
+ threshold: values.threshold === void 0 ? void 0 : parseThreshold(values.threshold),
2890
+ file: values.file,
2891
+ command: values.command
2892
+ },
2893
+ io
2894
+ );
2895
+ }
2896
+ case "show": {
2897
+ const parsed = parseWithPositionals(args, { lines: { type: "string" } });
2898
+ const id = parsed.positionals[0];
2899
+ if (id === void 0) throw new UsageError("show needs a run id");
2900
+ if (parsed.positionals.length > 1) {
2901
+ throw new UsageError(`show takes one run id, got ${String(parsed.positionals.length)}`);
2902
+ }
2903
+ return await runShow(
2904
+ { id, ...parsed.values.lines !== void 0 ? { lines: parsed.values.lines } : {} },
2905
+ io
2906
+ );
2907
+ }
2908
+ case "gain": {
2909
+ parse(args, {});
2910
+ return await runGain(io);
2911
+ }
2912
+ case "hook": {
2913
+ parse(args, {});
2914
+ return await runHook(io);
2915
+ }
2916
+ default:
2917
+ throw new UsageError(`unknown command "${command}"`);
2918
+ }
2919
+ }
2920
+ function parse(args, options) {
2921
+ try {
2922
+ return parseArgs({ args: [...args], options, strict: true, allowPositionals: false }).values;
2923
+ } catch (error) {
2924
+ throw new UsageError(errorMessage(error), { cause: error });
2925
+ }
2926
+ }
2927
+ function parseWithPositionals(args, options) {
2928
+ try {
2929
+ return parseArgs({ args: [...args], options, strict: true, allowPositionals: true });
2930
+ } catch (error) {
2931
+ throw new UsageError(errorMessage(error), { cause: error });
2932
+ }
2933
+ }
2934
+ async function report(error, io) {
2935
+ if (error instanceof SpawnError) {
2936
+ await io.writeError(`jevprune: ${error.message}
2937
+ `);
2938
+ if (error.code === "ENOENT") return 127;
2939
+ if (error.code === "EACCES") return 126;
2940
+ return 1;
2941
+ }
2942
+ if (error instanceof UsageError || error instanceof ConfigError) {
2943
+ await io.writeError(`jevprune: ${error.message}
2944
+ `);
2945
+ return 2;
2946
+ }
2947
+ if (error instanceof JevpruneError) {
2948
+ await io.writeError(`jevprune: ${error.message}
2949
+ `);
2950
+ return 1;
2951
+ }
2952
+ await io.writeError(`jevprune: ${errorMessage(error)}
2953
+ `);
2954
+ return 1;
2955
+ }
2956
+ function entryUrl(path) {
2957
+ try {
2958
+ return pathToFileURL(realpathSync(path)).href;
2959
+ } catch {
2960
+ return "";
2961
+ }
2962
+ }
2963
+ var entry = process.argv[1];
2964
+ if (entry !== void 0 && import.meta.url === entryUrl(entry)) {
2965
+ process.exitCode = await runCli(process.argv.slice(2), processIo());
2966
+ }
2967
+ export {
2968
+ runCli
2969
+ };