jevprune 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -143
- package/dist/cli.js +1107 -1020
- package/dist/index.d.ts +15 -5
- package/dist/index.js +356 -102
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1092,7 +1092,7 @@ var DEFAULT_CONFIG = {
|
|
|
1092
1092
|
concurrency: 4,
|
|
1093
1093
|
maxPruneBytes: 16777216,
|
|
1094
1094
|
retention: { maxRuns: 200, maxBytes: 268435456 },
|
|
1095
|
-
autoWrap:
|
|
1095
|
+
autoWrap: false,
|
|
1096
1096
|
allowlist: DEFAULT_ALLOWLIST
|
|
1097
1097
|
};
|
|
1098
1098
|
function resolveHome(env = process.env) {
|
|
@@ -1216,1098 +1216,1011 @@ function describeValue(value) {
|
|
|
1216
1216
|
// src/footer.ts
|
|
1217
1217
|
import { homedir as homedir2 } from "os";
|
|
1218
1218
|
import { sep } from "path";
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1219
|
+
|
|
1220
|
+
// src/keeps.ts
|
|
1221
|
+
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/;
|
|
1222
|
+
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;
|
|
1223
|
+
function isSignatureLine(text) {
|
|
1224
|
+
return SIGNATURE_CASE_SENSITIVE.test(text) || SIGNATURE_CASE_INSENSITIVE.test(text);
|
|
1225
|
+
}
|
|
1226
|
+
function computeKeeps(lines, options) {
|
|
1227
|
+
const keeps = /* @__PURE__ */ new Map();
|
|
1228
|
+
const tailLines = Math.max(0, Math.trunc(options.tailLines));
|
|
1229
|
+
const contextLines = Math.max(0, Math.trunc(options.contextLines));
|
|
1230
|
+
const tailStart = lines.length - tailLines + 1;
|
|
1231
|
+
for (const line of lines) {
|
|
1232
|
+
if (line.n >= tailStart) keeps.set(line.n, "tail");
|
|
1231
1233
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1235
|
+
const signatures = [];
|
|
1236
|
+
for (const line of lines) {
|
|
1237
|
+
if (!isSignatureLine(line.text)) continue;
|
|
1238
|
+
const key = line.text.trim();
|
|
1239
|
+
if (seen.has(key)) continue;
|
|
1240
|
+
seen.add(key);
|
|
1241
|
+
signatures.push(line.n);
|
|
1236
1242
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
|
|
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;
|
|
1243
|
+
for (const n of signatures) keeps.set(n, "signature");
|
|
1244
|
+
for (const n of signatures) {
|
|
1245
|
+
for (let offset = 1; offset <= contextLines; offset += 1) {
|
|
1246
|
+
for (const candidate of [n - offset, n + offset]) {
|
|
1247
|
+
if (candidate < 1 || candidate > lines.length) continue;
|
|
1248
|
+
if (!keeps.has(candidate)) keeps.set(candidate, "context");
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return keeps;
|
|
1260
1253
|
}
|
|
1261
1254
|
|
|
1262
|
-
// src/
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1275
|
-
return true;
|
|
1276
|
-
} catch {
|
|
1277
|
-
return false;
|
|
1255
|
+
// src/lines.ts
|
|
1256
|
+
function splitLines(text) {
|
|
1257
|
+
const lines = [];
|
|
1258
|
+
const pattern = /\r\n|\n|\r/g;
|
|
1259
|
+
let start = 0;
|
|
1260
|
+
let match = pattern.exec(text);
|
|
1261
|
+
while (match !== null) {
|
|
1262
|
+
const raw = match[0];
|
|
1263
|
+
const terminator = raw === "\r\n" ? "\r\n" : raw === "\r" ? "\r" : "\n";
|
|
1264
|
+
lines.push({ n: lines.length + 1, text: text.slice(start, match.index), terminator });
|
|
1265
|
+
start = match.index + raw.length;
|
|
1266
|
+
match = pattern.exec(text);
|
|
1278
1267
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
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);
|
|
1268
|
+
if (start < text.length) {
|
|
1269
|
+
lines.push({ n: lines.length + 1, text: text.slice(start), terminator: "" });
|
|
1287
1270
|
}
|
|
1288
|
-
|
|
1289
|
-
return starts;
|
|
1290
|
-
}
|
|
1291
|
-
function countByteLines(bytes) {
|
|
1292
|
-
return byteLineStarts(bytes).length - 1;
|
|
1271
|
+
return lines;
|
|
1293
1272
|
}
|
|
1294
1273
|
|
|
1295
|
-
// src/
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
var DIR_MODE = 448;
|
|
1300
|
-
var FILE_MODE = 384;
|
|
1301
|
-
function newRunId() {
|
|
1302
|
-
return `${Date.now().toString(36)}-${randomBytes(2).toString("hex")}`;
|
|
1274
|
+
// src/merge.ts
|
|
1275
|
+
function collapseMarker(range2, runId) {
|
|
1276
|
+
return `[jevprune: ${String(range2.count)} lines dropped, run ${runId}, lines ${String(range2.from)}-${String(range2.to)}]
|
|
1277
|
+
`;
|
|
1303
1278
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
|
|
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;
|
|
1279
|
+
function mergeDecisions(lines, decisions, options) {
|
|
1280
|
+
const minCollapseLines = Math.max(1, Math.trunc(options.minCollapseLines));
|
|
1281
|
+
const dropped = [];
|
|
1282
|
+
let kept = "";
|
|
1283
|
+
let run = null;
|
|
1284
|
+
let expected = 1;
|
|
1285
|
+
const flush = () => {
|
|
1286
|
+
if (run === null) return;
|
|
1287
|
+
const count = run.to - run.from + 1;
|
|
1288
|
+
if (!run.missing && count < minCollapseLines) {
|
|
1289
|
+
for (const line of run.lines) {
|
|
1290
|
+
decisions.set(line.n, { keep: true, reason: "collapse-min" });
|
|
1291
|
+
kept += line.text + line.terminator;
|
|
1292
|
+
}
|
|
1293
|
+
} else {
|
|
1294
|
+
const range2 = { from: run.from, to: run.to, count };
|
|
1295
|
+
dropped.push(range2);
|
|
1296
|
+
kept += collapseMarker(range2, options.runId);
|
|
1333
1297
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1298
|
+
run = null;
|
|
1299
|
+
};
|
|
1300
|
+
const dropLine = (line) => {
|
|
1301
|
+
if (run === null) run = { from: line.n, to: line.n, lines: [line], missing: false };
|
|
1302
|
+
else {
|
|
1303
|
+
run.to = line.n;
|
|
1304
|
+
run.lines.push(line);
|
|
1339
1305
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
await finished(this.#stream);
|
|
1348
|
-
} catch (error) {
|
|
1349
|
-
this.#fail(error);
|
|
1350
|
-
await this.#handle.close().catch(() => void 0);
|
|
1306
|
+
};
|
|
1307
|
+
const dropMissing = (from, to) => {
|
|
1308
|
+
if (to < from) return;
|
|
1309
|
+
if (run === null) run = { from, to, lines: [], missing: true };
|
|
1310
|
+
else {
|
|
1311
|
+
run.to = to;
|
|
1312
|
+
run.missing = true;
|
|
1351
1313
|
}
|
|
1352
|
-
|
|
1314
|
+
};
|
|
1315
|
+
for (const line of lines) {
|
|
1316
|
+
dropMissing(expected, line.n - 1);
|
|
1317
|
+
expected = line.n + 1;
|
|
1318
|
+
if (decisions.get(line.n)?.keep === false) {
|
|
1319
|
+
dropLine(line);
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
flush();
|
|
1323
|
+
kept += line.text + line.terminator;
|
|
1353
1324
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1325
|
+
if (options.totalLines !== void 0) dropMissing(expected, options.totalLines);
|
|
1326
|
+
flush();
|
|
1327
|
+
return { kept, dropped };
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/select.ts
|
|
1331
|
+
var UNAUTHORIZED_REASON = "unauthorized (401)";
|
|
1332
|
+
var NOT_UTF8_REASON = "not valid UTF-8";
|
|
1333
|
+
var NOT_UTF8_NOTE = "output is not valid UTF-8";
|
|
1334
|
+
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.";
|
|
1335
|
+
var ITEM_OVERHEAD_TOKENS = 12;
|
|
1336
|
+
function passthroughSelection(input) {
|
|
1337
|
+
return {
|
|
1338
|
+
mode: "passthrough",
|
|
1339
|
+
kept: "",
|
|
1340
|
+
dropped: [],
|
|
1341
|
+
linesIn: input.lines,
|
|
1342
|
+
linesOut: input.lines,
|
|
1343
|
+
bytesIn: input.bytes,
|
|
1344
|
+
bytesOut: input.bytes,
|
|
1345
|
+
windows: 0,
|
|
1346
|
+
jevRequests: 0,
|
|
1347
|
+
jevInputTokens: 0,
|
|
1348
|
+
...input.reason !== void 0 ? { fallbackReason: input.reason } : {},
|
|
1349
|
+
decisions: /* @__PURE__ */ new Map()
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
function questionFor(n) {
|
|
1353
|
+
return `Is line ${String(n)} needed for the task?`;
|
|
1354
|
+
}
|
|
1355
|
+
async function selectLines(input) {
|
|
1356
|
+
const lines = splitLines(input.text);
|
|
1357
|
+
const bytesIn = Buffer.byteLength(input.text);
|
|
1358
|
+
if (input.oversize !== void 0) {
|
|
1359
|
+
return oversizeSelection(input, input.oversize, lines, bytesIn);
|
|
1356
1360
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
this.#waiting = [];
|
|
1360
|
-
for (const listener of waiting) listener();
|
|
1361
|
+
if (input.exitCode !== void 0 && input.exitCode !== null && input.exitCode !== 0 || input.interrupted === true) {
|
|
1362
|
+
return everyLine(lines, "passthrough", input.text, bytesIn);
|
|
1361
1363
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
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) };
|
|
1364
|
+
if (lines.length <= input.config.fastPathLines) {
|
|
1365
|
+
return everyLine(lines, "fast-path", input.text, bytesIn);
|
|
1426
1366
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
const
|
|
1432
|
-
if (
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
if (last > lines) {
|
|
1436
|
-
throw new LineRangeError(
|
|
1437
|
-
`line range ${String(from)}-${String(last)} is outside run ${id} (${String(lines)} lines)`
|
|
1438
|
-
);
|
|
1367
|
+
const keeps = keepsOf(lines, input.config);
|
|
1368
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
1369
|
+
const candidates = [];
|
|
1370
|
+
for (const line of lines) {
|
|
1371
|
+
const keep = keeps.get(line.n);
|
|
1372
|
+
if (keep !== void 0) {
|
|
1373
|
+
decisions.set(line.n, { keep: true, reason: keep });
|
|
1374
|
+
continue;
|
|
1439
1375
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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);
|
|
1376
|
+
if (line.text.trim().length === 0) {
|
|
1377
|
+
decisions.set(line.n, { keep: false, reason: "blank" });
|
|
1378
|
+
continue;
|
|
1452
1379
|
}
|
|
1380
|
+
candidates.push({ id: `l${String(line.n)}`, n: line.n, text: line.text });
|
|
1453
1381
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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 };
|
|
1382
|
+
if (input.client === null) {
|
|
1383
|
+
return fallbackSelection({
|
|
1384
|
+
lines,
|
|
1385
|
+
keeps,
|
|
1386
|
+
input,
|
|
1387
|
+
reason: { kind: "unavailable", detail: "API key not set" },
|
|
1388
|
+
bytesIn,
|
|
1389
|
+
linesIn: lines.length
|
|
1390
|
+
});
|
|
1478
1391
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
-
}
|
|
1392
|
+
let verdicts;
|
|
1393
|
+
try {
|
|
1394
|
+
verdicts = await askJev(candidates, input, input.client);
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
return fallbackSelection({
|
|
1397
|
+
lines,
|
|
1398
|
+
keeps,
|
|
1399
|
+
input,
|
|
1400
|
+
reason: unavailableReason(error),
|
|
1401
|
+
bytesIn,
|
|
1402
|
+
linesIn: lines.length
|
|
1403
|
+
});
|
|
1502
1404
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
if (errorCode(error) === "ENOENT") continue;
|
|
1509
|
-
throw storeError(`run file ${path} could not be deleted`, error);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1405
|
+
for (const n of verdicts.oversize) decisions.set(n, { keep: true, reason: "oversize" });
|
|
1406
|
+
for (const item of candidates) {
|
|
1407
|
+
if (decisions.has(item.n)) continue;
|
|
1408
|
+
const noul2 = verdicts.answers.get(item.n) ?? 0;
|
|
1409
|
+
decisions.set(item.n, { keep: noul2 >= input.config.threshold, reason: "jev", noul: noul2 });
|
|
1512
1410
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1411
|
+
const { kept, dropped } = mergeDecisions(lines, decisions, {
|
|
1412
|
+
minCollapseLines: input.config.minCollapseLines,
|
|
1413
|
+
runId: input.runId
|
|
1414
|
+
});
|
|
1415
|
+
return {
|
|
1416
|
+
mode: "jev",
|
|
1417
|
+
kept,
|
|
1418
|
+
dropped,
|
|
1419
|
+
linesIn: lines.length,
|
|
1420
|
+
linesOut: splitLines(kept).length,
|
|
1421
|
+
bytesIn,
|
|
1422
|
+
bytesOut: Buffer.byteLength(kept),
|
|
1423
|
+
windows: verdicts.windows,
|
|
1424
|
+
jevRequests: verdicts.jevRequests,
|
|
1425
|
+
jevInputTokens: verdicts.jevInputTokens,
|
|
1426
|
+
decisions
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
async function askJev(candidates, input, client) {
|
|
1430
|
+
const answers = /* @__PURE__ */ new Map();
|
|
1431
|
+
if (candidates.length === 0) {
|
|
1432
|
+
return { windows: 0, jevRequests: 0, jevInputTokens: 0, answers, oversize: [] };
|
|
1520
1433
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1434
|
+
const lastLine = candidates[candidates.length - 1]?.n ?? 0;
|
|
1435
|
+
const plan = planWindows(candidates, {
|
|
1436
|
+
budgetTokens: input.config.windowTokens,
|
|
1437
|
+
overheadTokens: estimateJsonTokens(stateOf(input, [])),
|
|
1438
|
+
itemOverheadTokens: estimateTokens(questionFor(lastLine)) + ITEM_OVERHEAD_TOKENS,
|
|
1439
|
+
costTokens: (item) => estimateJsonTokens({ n: item.n, text: item.text })
|
|
1440
|
+
});
|
|
1441
|
+
const results = await runWindows(
|
|
1442
|
+
plan.windows,
|
|
1443
|
+
async (window, _index, options) => {
|
|
1444
|
+
const questions = {};
|
|
1445
|
+
for (const item of window) questions[item.id] = questionFor(item.n);
|
|
1446
|
+
return await client.noul({ state: stateOf(input, window), questions }, options);
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
concurrency: input.config.concurrency,
|
|
1450
|
+
timeoutMs: input.config.windowTimeoutMs,
|
|
1451
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1535
1452
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1453
|
+
);
|
|
1454
|
+
let jevInputTokens = 0;
|
|
1455
|
+
for (const [index, result] of results.entries()) {
|
|
1456
|
+
jevInputTokens += result.usage.inputTokens;
|
|
1457
|
+
for (const item of plan.windows[index] ?? []) {
|
|
1458
|
+
const noul2 = result.answers[item.id];
|
|
1459
|
+
if (noul2 !== void 0) answers.set(item.n, noul2);
|
|
1543
1460
|
}
|
|
1544
1461
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1462
|
+
return {
|
|
1463
|
+
windows: plan.windows.length,
|
|
1464
|
+
jevRequests: results.length,
|
|
1465
|
+
jevInputTokens,
|
|
1466
|
+
answers,
|
|
1467
|
+
oversize: plan.oversize.map((item) => item.n)
|
|
1468
|
+
};
|
|
1549
1469
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1470
|
+
function stateOf(input, window) {
|
|
1471
|
+
return {
|
|
1472
|
+
command: input.command,
|
|
1473
|
+
task: input.task,
|
|
1474
|
+
rubric: RUBRIC,
|
|
1475
|
+
lines: window.map((item) => ({ n: item.n, text: item.text }))
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
function keepsOf(lines, config) {
|
|
1479
|
+
return computeKeeps(lines, { tailLines: config.tailLines, contextLines: config.contextLines });
|
|
1480
|
+
}
|
|
1481
|
+
function fallbackSelection(fallback) {
|
|
1482
|
+
const { lines, keeps, input } = fallback;
|
|
1483
|
+
return fallbackResult({
|
|
1484
|
+
lines,
|
|
1485
|
+
decisions: fallbackDecisions(lines, keeps, input.config.headLines),
|
|
1486
|
+
input,
|
|
1487
|
+
reason: fallback.reason,
|
|
1488
|
+
bytesIn: fallback.bytesIn,
|
|
1489
|
+
linesIn: fallback.linesIn
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
function oversizeSelection(input, oversize, captured, bytesIn) {
|
|
1493
|
+
const headCount = Math.min(Math.max(0, Math.trunc(oversize.headSegmentLines)), captured.length);
|
|
1494
|
+
const totalLines = Math.max(Math.trunc(oversize.lines), captured.length);
|
|
1495
|
+
const tailFirst = totalLines - captured.length + headCount + 1;
|
|
1496
|
+
const head = captured.slice(0, headCount);
|
|
1497
|
+
const tail = renumber(captured.slice(headCount), tailFirst);
|
|
1498
|
+
const contextLines = input.config.contextLines;
|
|
1499
|
+
const keeps = computeKeeps(head, { tailLines: 0, contextLines });
|
|
1500
|
+
const tailKeeps = computeKeeps(renumber(tail, 1), { tailLines: input.config.tailLines, contextLines });
|
|
1501
|
+
for (const [n, reason] of tailKeeps) keeps.set(n + tailFirst - 1, reason);
|
|
1502
|
+
const lines = [...head, ...tail];
|
|
1503
|
+
return fallbackResult({
|
|
1504
|
+
lines,
|
|
1505
|
+
decisions: fallbackDecisions(lines, keeps, input.config.headLines),
|
|
1506
|
+
input,
|
|
1507
|
+
reason: { kind: "size-limit", maxBytes: input.config.maxPruneBytes },
|
|
1508
|
+
bytesIn,
|
|
1509
|
+
linesIn: totalLines,
|
|
1510
|
+
totalLines
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
function fallbackDecisions(lines, keeps, headLines) {
|
|
1514
|
+
const head = Math.max(0, Math.trunc(headLines));
|
|
1515
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
1516
|
+
for (const line of lines) {
|
|
1517
|
+
const keep = keeps.get(line.n);
|
|
1518
|
+
if (keep !== void 0) decisions.set(line.n, { keep: true, reason: keep });
|
|
1519
|
+
else if (line.n <= head) decisions.set(line.n, { keep: true, reason: "head" });
|
|
1520
|
+
else decisions.set(line.n, { keep: false, reason: "fallback" });
|
|
1564
1521
|
}
|
|
1565
|
-
return
|
|
1522
|
+
return decisions;
|
|
1566
1523
|
}
|
|
1567
|
-
function
|
|
1568
|
-
return
|
|
1524
|
+
function renumber(lines, from) {
|
|
1525
|
+
return lines.map((line, index) => ({ ...line, n: from + index }));
|
|
1569
1526
|
}
|
|
1570
|
-
function
|
|
1571
|
-
const
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1527
|
+
function fallbackResult(fallback) {
|
|
1528
|
+
const { kept, dropped } = mergeDecisions(fallback.lines, fallback.decisions, {
|
|
1529
|
+
minCollapseLines: fallback.input.config.minCollapseLines,
|
|
1530
|
+
runId: fallback.input.runId,
|
|
1531
|
+
...fallback.totalLines !== void 0 ? { totalLines: fallback.totalLines } : {}
|
|
1575
1532
|
});
|
|
1533
|
+
return {
|
|
1534
|
+
mode: "fallback",
|
|
1535
|
+
kept,
|
|
1536
|
+
dropped,
|
|
1537
|
+
linesIn: fallback.linesIn,
|
|
1538
|
+
linesOut: splitLines(kept).length,
|
|
1539
|
+
bytesIn: fallback.bytesIn,
|
|
1540
|
+
bytesOut: Buffer.byteLength(kept),
|
|
1541
|
+
windows: 0,
|
|
1542
|
+
jevRequests: 0,
|
|
1543
|
+
jevInputTokens: 0,
|
|
1544
|
+
fallbackReason: fallback.reason,
|
|
1545
|
+
decisions: fallback.decisions
|
|
1546
|
+
};
|
|
1576
1547
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
`
|
|
1587
|
-
);
|
|
1588
|
-
return 0;
|
|
1548
|
+
function fallbackReasonText(reason) {
|
|
1549
|
+
switch (reason.kind) {
|
|
1550
|
+
case "size-limit":
|
|
1551
|
+
return `output over ${String(reason.maxBytes)} bytes`;
|
|
1552
|
+
case "not-utf8":
|
|
1553
|
+
return NOT_UTF8_REASON;
|
|
1554
|
+
case "unavailable":
|
|
1555
|
+
return reason.detail;
|
|
1556
|
+
}
|
|
1589
1557
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
"
|
|
1595
|
-
"
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
"
|
|
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("'", "'\\''")}'`;
|
|
1558
|
+
function unavailableReason(error) {
|
|
1559
|
+
return { kind: "unavailable", detail: unavailableDetail(error) };
|
|
1560
|
+
}
|
|
1561
|
+
function unavailableDetail(error) {
|
|
1562
|
+
if (error instanceof JevTimeoutError) return "timeout";
|
|
1563
|
+
if (error instanceof JevResponseError) return "invalid response";
|
|
1564
|
+
if (error instanceof JevRequestError) {
|
|
1565
|
+
switch (error.status) {
|
|
1566
|
+
case 429:
|
|
1567
|
+
return "rate limited (429)";
|
|
1568
|
+
case 529:
|
|
1569
|
+
return "overloaded (529)";
|
|
1570
|
+
case 401:
|
|
1571
|
+
return UNAUTHORIZED_REASON;
|
|
1572
|
+
case 400:
|
|
1573
|
+
return "bad request (400)";
|
|
1574
|
+
case void 0:
|
|
1575
|
+
return "network";
|
|
1576
|
+
default:
|
|
1577
|
+
return error.name;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (error instanceof Error) return error.name;
|
|
1581
|
+
return "unknown";
|
|
1648
1582
|
}
|
|
1649
|
-
function
|
|
1650
|
-
|
|
1583
|
+
function everyLine(lines, mode, text, bytesIn) {
|
|
1584
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
1585
|
+
for (const line of lines) decisions.set(line.n, { keep: true, reason: mode });
|
|
1586
|
+
return {
|
|
1587
|
+
mode,
|
|
1588
|
+
kept: text,
|
|
1589
|
+
dropped: [],
|
|
1590
|
+
linesIn: lines.length,
|
|
1591
|
+
linesOut: lines.length,
|
|
1592
|
+
bytesIn,
|
|
1593
|
+
bytesOut: bytesIn,
|
|
1594
|
+
windows: 0,
|
|
1595
|
+
jevRequests: 0,
|
|
1596
|
+
jevInputTokens: 0,
|
|
1597
|
+
decisions
|
|
1598
|
+
};
|
|
1651
1599
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1600
|
+
|
|
1601
|
+
// src/footer.ts
|
|
1602
|
+
var LF = 10;
|
|
1603
|
+
function formatFooter(input) {
|
|
1604
|
+
if (input.mode === "fast-path") return "";
|
|
1605
|
+
const parts = [];
|
|
1606
|
+
if (input.mode === "passthrough") {
|
|
1607
|
+
if (input.exitCode !== void 0 && input.exitCode !== null) parts.push(`exit ${String(input.exitCode)}`);
|
|
1608
|
+
const note = input.passthroughNote === void 0 ? "" : ` (${input.passthroughNote})`;
|
|
1609
|
+
parts.push(`${formatCount(input.linesIn)} lines passed through${note}`);
|
|
1610
|
+
} else {
|
|
1611
|
+
if (input.mode === "fallback") parts.push(`fallback (${fallbackNote(input.fallbackReason)})`);
|
|
1612
|
+
parts.push(`${formatCount(input.linesIn)} \u2192 ${formatCount(input.linesOut)} lines`);
|
|
1613
|
+
if (input.exitCode !== void 0 && input.exitCode !== null) parts.push(`exit ${String(input.exitCode)}`);
|
|
1664
1614
|
}
|
|
1665
|
-
if (
|
|
1666
|
-
|
|
1667
|
-
|
|
1615
|
+
if (input.storeFailureCode !== void 0) {
|
|
1616
|
+
parts.push(`full output was not saved (${input.storeFailureCode})`);
|
|
1617
|
+
} else if (input.logPath !== void 0) {
|
|
1618
|
+
parts.push(`full output ${displayPath(input.logPath, input.home)}`);
|
|
1668
1619
|
}
|
|
1669
|
-
return
|
|
1620
|
+
return `jevprune: ${parts.join(", ")}`;
|
|
1670
1621
|
}
|
|
1671
|
-
function
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1622
|
+
function fallbackNote(reason) {
|
|
1623
|
+
if (reason === void 0) return "Jev unavailable: unknown";
|
|
1624
|
+
switch (reason.kind) {
|
|
1625
|
+
case "size-limit":
|
|
1626
|
+
return `output over ${String(reason.maxBytes)} bytes`;
|
|
1627
|
+
case "not-utf8":
|
|
1628
|
+
return NOT_UTF8_NOTE;
|
|
1629
|
+
case "unavailable":
|
|
1630
|
+
return `Jev unavailable: ${reason.detail}`;
|
|
1677
1631
|
}
|
|
1678
|
-
return false;
|
|
1679
1632
|
}
|
|
1680
|
-
function
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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)}` };
|
|
1633
|
+
function footerAfter(lastByte, footer) {
|
|
1634
|
+
if (footer.length === 0) return "";
|
|
1635
|
+
const separator = lastByte === void 0 || lastByte === LF ? "" : "\n";
|
|
1636
|
+
return `${separator}${footer}
|
|
1637
|
+
`;
|
|
1695
1638
|
}
|
|
1696
|
-
function
|
|
1697
|
-
|
|
1639
|
+
function withFooter(kept, footer) {
|
|
1640
|
+
if (footer.length === 0) return kept;
|
|
1641
|
+
const separator = kept.length === 0 || kept.endsWith("\n") ? "" : "\n";
|
|
1642
|
+
return `${kept}${separator}${footer}
|
|
1643
|
+
`;
|
|
1644
|
+
}
|
|
1645
|
+
function formatCount(value) {
|
|
1646
|
+
const digits = Math.trunc(Math.abs(value)).toString();
|
|
1647
|
+
const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
1648
|
+
return value < 0 ? `-${grouped}` : grouped;
|
|
1649
|
+
}
|
|
1650
|
+
function displayPath(path, home = homedir2()) {
|
|
1651
|
+
if (path === home) return "~";
|
|
1652
|
+
if (home.length > 0 && path.startsWith(home + sep)) return `~${path.slice(home.length)}`;
|
|
1653
|
+
return path;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/store.ts
|
|
1657
|
+
import { randomBytes } from "crypto";
|
|
1658
|
+
import { createReadStream } from "fs";
|
|
1659
|
+
import { appendFile, mkdir, open, readFile as readFile2, readdir, stat, unlink, writeFile } from "fs/promises";
|
|
1660
|
+
import { join as join2 } from "path";
|
|
1661
|
+
import { finished } from "stream/promises";
|
|
1662
|
+
|
|
1663
|
+
// src/bytes.ts
|
|
1664
|
+
var LF2 = 10;
|
|
1665
|
+
var CR = 13;
|
|
1666
|
+
function isValidUtf8(bytes) {
|
|
1698
1667
|
try {
|
|
1699
|
-
|
|
1668
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1669
|
+
return true;
|
|
1700
1670
|
} catch {
|
|
1701
|
-
return
|
|
1671
|
+
return false;
|
|
1702
1672
|
}
|
|
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
1673
|
}
|
|
1729
|
-
function
|
|
1730
|
-
|
|
1674
|
+
function byteLineStarts(bytes) {
|
|
1675
|
+
const starts = [0];
|
|
1676
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
1677
|
+
const byte = bytes[index];
|
|
1678
|
+
if (byte === CR && bytes[index + 1] === LF2) index += 1;
|
|
1679
|
+
else if (byte !== CR && byte !== LF2) continue;
|
|
1680
|
+
starts.push(index + 1);
|
|
1681
|
+
}
|
|
1682
|
+
if (starts[starts.length - 1] !== bytes.length) starts.push(bytes.length);
|
|
1683
|
+
return starts;
|
|
1731
1684
|
}
|
|
1732
|
-
function
|
|
1733
|
-
return
|
|
1685
|
+
function countByteLines(bytes) {
|
|
1686
|
+
return byteLineStarts(bytes).length - 1;
|
|
1734
1687
|
}
|
|
1735
1688
|
|
|
1736
|
-
// src/
|
|
1737
|
-
var
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
-
};
|
|
1689
|
+
// src/store.ts
|
|
1690
|
+
var RUN_ID_PATTERN = /^[a-z0-9]+-[a-f0-9]{4}$/;
|
|
1691
|
+
var RUNS_DIR = "runs";
|
|
1692
|
+
var GAIN_FILE = "gain.jsonl";
|
|
1693
|
+
var DIR_MODE = 448;
|
|
1694
|
+
var FILE_MODE = 384;
|
|
1695
|
+
function newRunId() {
|
|
1696
|
+
return `${Date.now().toString(36)}-${randomBytes(2).toString("hex")}`;
|
|
1753
1697
|
}
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1698
|
+
var FileRunWriter = class {
|
|
1699
|
+
path;
|
|
1700
|
+
#handle;
|
|
1701
|
+
#stream;
|
|
1702
|
+
#waiting = [];
|
|
1703
|
+
#failure;
|
|
1704
|
+
#closed = false;
|
|
1705
|
+
constructor(path, handle, stream) {
|
|
1706
|
+
this.path = path;
|
|
1707
|
+
this.#handle = handle;
|
|
1708
|
+
this.#stream = stream;
|
|
1709
|
+
this.#stream.on("error", (error) => {
|
|
1710
|
+
this.#fail(error);
|
|
1711
|
+
this.#release();
|
|
1712
|
+
});
|
|
1713
|
+
this.#stream.on("drain", () => {
|
|
1714
|
+
this.#release();
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
get failure() {
|
|
1718
|
+
return this.#failure;
|
|
1719
|
+
}
|
|
1720
|
+
write(chunk) {
|
|
1721
|
+
if (this.#closed || this.#failure !== void 0) return true;
|
|
1722
|
+
try {
|
|
1723
|
+
return this.#stream.write(chunk);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
this.#fail(error);
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
onDrain(listener) {
|
|
1730
|
+
if (this.#closed || this.#failure !== void 0) {
|
|
1731
|
+
queueMicrotask(listener);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
this.#waiting.push(listener);
|
|
1735
|
+
}
|
|
1736
|
+
async close() {
|
|
1737
|
+
if (this.#closed) return;
|
|
1738
|
+
this.#closed = true;
|
|
1739
|
+
try {
|
|
1740
|
+
this.#stream.end();
|
|
1741
|
+
await finished(this.#stream);
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
this.#fail(error);
|
|
1744
|
+
await this.#handle.close().catch(() => void 0);
|
|
1745
|
+
}
|
|
1746
|
+
this.#release();
|
|
1747
|
+
}
|
|
1748
|
+
#fail(error) {
|
|
1749
|
+
this.#failure ??= storeError(`run log ${this.path} could not be written`, error);
|
|
1750
|
+
}
|
|
1751
|
+
#release() {
|
|
1752
|
+
const waiting = this.#waiting;
|
|
1753
|
+
this.#waiting = [];
|
|
1754
|
+
for (const listener of waiting) listener();
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
var RunStore = class {
|
|
1758
|
+
home;
|
|
1759
|
+
#retention;
|
|
1760
|
+
constructor(options) {
|
|
1761
|
+
this.home = options.home;
|
|
1762
|
+
this.#retention = options.retention ?? DEFAULT_CONFIG.retention;
|
|
1763
|
+
}
|
|
1764
|
+
get runsDir() {
|
|
1765
|
+
return join2(this.home, RUNS_DIR);
|
|
1766
|
+
}
|
|
1767
|
+
get gainPath() {
|
|
1768
|
+
return join2(this.home, GAIN_FILE);
|
|
1769
|
+
}
|
|
1770
|
+
logPath(id) {
|
|
1771
|
+
return join2(this.runsDir, `${requireRunId(id)}.log`);
|
|
1758
1772
|
}
|
|
1759
|
-
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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;
|
|
1773
|
+
metaPath(id) {
|
|
1774
|
+
return join2(this.runsDir, `${requireRunId(id)}.json`);
|
|
1775
|
+
}
|
|
1776
|
+
async openRun(run) {
|
|
1777
|
+
const path = this.logPath(run.id);
|
|
1778
|
+
await this.#ensureDir(this.runsDir);
|
|
1779
|
+
let handle;
|
|
1780
|
+
try {
|
|
1781
|
+
handle = await open(path, "wx", FILE_MODE);
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
throw storeError(`run log ${path} could not be created`, error);
|
|
1777
1784
|
}
|
|
1785
|
+
return new FileRunWriter(path, handle, handle.createWriteStream());
|
|
1786
|
+
}
|
|
1787
|
+
async finalizeRun(id, meta) {
|
|
1788
|
+
const path = this.metaPath(id);
|
|
1789
|
+
await this.#ensureDir(this.runsDir);
|
|
1778
1790
|
try {
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
else if (isClosedPipe(error)) {
|
|
1782
|
-
closed = true;
|
|
1783
|
-
resolve2();
|
|
1784
|
-
} else reject(error);
|
|
1785
|
-
});
|
|
1791
|
+
await writeFile(path, `${JSON.stringify(meta)}
|
|
1792
|
+
`, { mode: FILE_MODE });
|
|
1786
1793
|
} catch (error) {
|
|
1787
|
-
|
|
1788
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
closed = true;
|
|
1792
|
-
resolve2();
|
|
1794
|
+
throw storeError(`run meta ${path} could not be written`, error);
|
|
1793
1795
|
}
|
|
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
1796
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
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";
|
|
1797
|
+
async readRunBytes(id) {
|
|
1798
|
+
const path = this.logPath(id);
|
|
1799
|
+
try {
|
|
1800
|
+
return await readFile2(path);
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
if (errorCode(error) === "ENOENT") throw new RunNotFoundError(id, { cause: error });
|
|
1803
|
+
throw storeError(`run log ${path} could not be read`, error);
|
|
1804
|
+
}
|
|
1826
1805
|
}
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
|
|
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");
|
|
1806
|
+
async *readRunChunks(id) {
|
|
1807
|
+
const path = this.logPath(id);
|
|
1808
|
+
try {
|
|
1809
|
+
for await (const chunk of createReadStream(path)) {
|
|
1810
|
+
yield typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
1811
|
+
}
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
if (errorCode(error) === "ENOENT") throw new RunNotFoundError(id, { cause: error });
|
|
1814
|
+
throw storeError(`run log ${path} could not be read`, error);
|
|
1815
|
+
}
|
|
1842
1816
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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);
|
|
1817
|
+
async readRun(id) {
|
|
1818
|
+
const bytes = await this.readRunBytes(id);
|
|
1819
|
+
return { id, path: this.logPath(id), text: bytes.toString("utf8"), meta: await this.#readMeta(id) };
|
|
1851
1820
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
}
|
|
1821
|
+
async readRunLineBytes(id, from = 1, to) {
|
|
1822
|
+
const bytes = await this.readRunBytes(id);
|
|
1823
|
+
const starts = byteLineStarts(bytes);
|
|
1824
|
+
const lines = starts.length - 1;
|
|
1825
|
+
const last = to ?? lines;
|
|
1826
|
+
if (!Number.isInteger(from) || !Number.isInteger(last) || from < 1 || last < from) {
|
|
1827
|
+
throw new LineRangeError(`line range ${String(from)}-${String(last)} is not a range`);
|
|
1828
|
+
}
|
|
1829
|
+
if (last > lines) {
|
|
1830
|
+
throw new LineRangeError(
|
|
1831
|
+
`line range ${String(from)}-${String(last)} is outside run ${id} (${String(lines)} lines)`
|
|
1832
|
+
);
|
|
1859
1833
|
}
|
|
1834
|
+
return bytes.subarray(starts[from - 1] ?? 0, starts[last] ?? bytes.length);
|
|
1860
1835
|
}
|
|
1861
|
-
|
|
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);
|
|
1836
|
+
async readRunLines(id, from = 1, to) {
|
|
1837
|
+
return (await this.readRunLineBytes(id, from, to)).toString("utf8");
|
|
1876
1838
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1839
|
+
async appendGain(entry2) {
|
|
1840
|
+
await this.#ensureDir(this.home);
|
|
1841
|
+
try {
|
|
1842
|
+
await appendFile(this.gainPath, `${JSON.stringify(entry2)}
|
|
1843
|
+
`, { mode: FILE_MODE });
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
throw storeError(`gain ledger ${this.gainPath} could not be written`, error);
|
|
1846
|
+
}
|
|
1879
1847
|
}
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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);
|
|
1848
|
+
async readGain() {
|
|
1849
|
+
let raw;
|
|
1850
|
+
try {
|
|
1851
|
+
raw = await readFile2(this.gainPath, "utf8");
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
if (errorCode(error) === "ENOENT") return { runs: 0, linesIn: 0, linesOut: 0, bytesIn: 0, bytesOut: 0 };
|
|
1854
|
+
throw storeError(`gain ledger ${this.gainPath} could not be read`, error);
|
|
1906
1855
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1856
|
+
let runs = 0;
|
|
1857
|
+
let linesIn = 0;
|
|
1858
|
+
let linesOut = 0;
|
|
1859
|
+
let bytesIn = 0;
|
|
1860
|
+
let bytesOut = 0;
|
|
1861
|
+
for (const line of raw.split("\n")) {
|
|
1862
|
+
if (line.trim().length === 0) continue;
|
|
1863
|
+
const entry2 = parseGainEntry(line);
|
|
1864
|
+
if (entry2 === null) continue;
|
|
1865
|
+
runs += 1;
|
|
1866
|
+
linesIn += entry2.linesIn;
|
|
1867
|
+
linesOut += entry2.linesOut;
|
|
1868
|
+
bytesIn += entry2.bytesIn;
|
|
1869
|
+
bytesOut += entry2.bytesOut;
|
|
1914
1870
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1871
|
+
return { runs, linesIn, linesOut, bytesIn, bytesOut };
|
|
1872
|
+
}
|
|
1873
|
+
async enforceRetention() {
|
|
1874
|
+
let names;
|
|
1875
|
+
try {
|
|
1876
|
+
names = await readdir(this.runsDir);
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
if (errorCode(error) === "ENOENT") return;
|
|
1879
|
+
throw storeError(`run directory ${this.runsDir} could not be read`, error);
|
|
1922
1880
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1881
|
+
const ids = [...new Set(names.filter((name) => name.endsWith(".log")).map((name) => name.slice(0, -4)))].filter((id) => RUN_ID_PATTERN.test(id)).sort();
|
|
1882
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
1883
|
+
let total = 0;
|
|
1884
|
+
for (const id of ids) {
|
|
1885
|
+
const bytes = await this.#sizeOf(this.logPath(id)) + await this.#sizeOf(this.metaPath(id));
|
|
1886
|
+
sizes.set(id, bytes);
|
|
1887
|
+
total += bytes;
|
|
1888
|
+
}
|
|
1889
|
+
let count = ids.length;
|
|
1890
|
+
for (const id of ids) {
|
|
1891
|
+
if (count <= this.#retention.maxRuns && total <= this.#retention.maxBytes) break;
|
|
1892
|
+
await this.discardRun(id);
|
|
1893
|
+
total -= sizes.get(id) ?? 0;
|
|
1894
|
+
count -= 1;
|
|
1930
1895
|
}
|
|
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
1896
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1897
|
+
async discardRun(id) {
|
|
1898
|
+
for (const path of [this.logPath(id), this.metaPath(id)]) {
|
|
1899
|
+
try {
|
|
1900
|
+
await unlink(path);
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
if (errorCode(error) === "ENOENT") continue;
|
|
1903
|
+
throw storeError(`run file ${path} could not be deleted`, error);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1972
1906
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1907
|
+
async #sizeOf(path) {
|
|
1908
|
+
try {
|
|
1909
|
+
return (await stat(path)).size;
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
if (errorCode(error) === "ENOENT") return 0;
|
|
1912
|
+
throw storeError(`run file ${path} could not be inspected`, error);
|
|
1913
|
+
}
|
|
1975
1914
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1915
|
+
async #readMeta(id) {
|
|
1916
|
+
const path = this.metaPath(id);
|
|
1917
|
+
let raw;
|
|
1918
|
+
try {
|
|
1919
|
+
raw = await readFile2(path, "utf8");
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
if (errorCode(error) === "ENOENT") return null;
|
|
1922
|
+
throw storeError(`run meta ${path} could not be read`, error);
|
|
1984
1923
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1924
|
+
let parsed;
|
|
1925
|
+
try {
|
|
1926
|
+
parsed = JSON.parse(raw);
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
throw storeError(`run meta ${path} is not valid JSON`, error);
|
|
1988
1929
|
}
|
|
1989
|
-
|
|
1930
|
+
return isRecord4(parsed) ? parsed : null;
|
|
1990
1931
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1932
|
+
async #ensureDir(path) {
|
|
1933
|
+
try {
|
|
1934
|
+
await mkdir(path, { recursive: true, mode: DIR_MODE });
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
throw storeError(`directory ${path} could not be created`, error);
|
|
1937
|
+
}
|
|
1993
1938
|
}
|
|
1994
|
-
|
|
1939
|
+
};
|
|
1940
|
+
function requireRunId(id) {
|
|
1941
|
+
if (!RUN_ID_PATTERN.test(id)) throw new RunStoreError(`"${id}" is not a run id`);
|
|
1942
|
+
return id;
|
|
1943
|
+
}
|
|
1944
|
+
function parseGainEntry(line) {
|
|
1945
|
+
let parsed;
|
|
1995
1946
|
try {
|
|
1996
|
-
|
|
1997
|
-
} catch
|
|
1998
|
-
return
|
|
1999
|
-
lines,
|
|
2000
|
-
keeps,
|
|
2001
|
-
input,
|
|
2002
|
-
reason: fallbackReason(error),
|
|
2003
|
-
bytesIn,
|
|
2004
|
-
linesIn: lines.length
|
|
2005
|
-
});
|
|
1947
|
+
parsed = JSON.parse(line);
|
|
1948
|
+
} catch {
|
|
1949
|
+
return null;
|
|
2006
1950
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
1951
|
+
if (!isRecord4(parsed)) return null;
|
|
1952
|
+
const linesIn = parsed["linesIn"];
|
|
1953
|
+
const linesOut = parsed["linesOut"];
|
|
1954
|
+
const bytesIn = parsed["bytesIn"];
|
|
1955
|
+
const bytesOut = parsed["bytesOut"];
|
|
1956
|
+
if (typeof linesIn !== "number" || typeof linesOut !== "number" || typeof bytesIn !== "number" || typeof bytesOut !== "number") {
|
|
1957
|
+
return null;
|
|
2012
1958
|
}
|
|
2013
|
-
|
|
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
|
-
};
|
|
1959
|
+
return parsed;
|
|
2030
1960
|
}
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
overheadTokens: estimateJsonTokens(stateOf(input, [])),
|
|
2040
|
-
itemOverheadTokens: estimateTokens(questionFor(lastLine)) + ITEM_OVERHEAD_TOKENS,
|
|
2041
|
-
costTokens: (item) => estimateJsonTokens({ n: item.n, text: item.text })
|
|
1961
|
+
function isRecord4(value) {
|
|
1962
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1963
|
+
}
|
|
1964
|
+
function storeError(message, error) {
|
|
1965
|
+
const code = errorCode(error);
|
|
1966
|
+
return new RunStoreError(`${message}: ${errorMessage(error)}`, {
|
|
1967
|
+
...code !== void 0 ? { code } : {},
|
|
1968
|
+
cause: error
|
|
2042
1969
|
});
|
|
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
1970
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
1971
|
+
|
|
1972
|
+
// src/commands/gain.ts
|
|
1973
|
+
async function runGain(io) {
|
|
1974
|
+
const config = await loadConfig(io.env);
|
|
1975
|
+
const store = new RunStore({ home: config.home, retention: config.retention });
|
|
1976
|
+
const totals = await store.readGain();
|
|
1977
|
+
const tokens = Math.max(0, Math.floor((totals.bytesIn - totals.bytesOut) / CHARS_PER_TOKEN));
|
|
1978
|
+
await io.write(
|
|
1979
|
+
`jevprune: ${formatCount(totals.runs)} runs, ${formatCount(totals.linesIn)} \u2192 ${formatCount(totals.linesOut)} lines, ~${formatCount(tokens)} output tokens removed (estimate: ${String(CHARS_PER_TOKEN)} bytes per token)
|
|
1980
|
+
`
|
|
1981
|
+
);
|
|
1982
|
+
return 0;
|
|
2079
1983
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
1984
|
+
|
|
1985
|
+
// src/hook.ts
|
|
1986
|
+
var STATE_CHANGING_TOKENS = [
|
|
1987
|
+
"cd",
|
|
1988
|
+
"export",
|
|
1989
|
+
"source",
|
|
1990
|
+
".",
|
|
1991
|
+
"unset",
|
|
1992
|
+
"alias",
|
|
1993
|
+
"set",
|
|
1994
|
+
"eval",
|
|
1995
|
+
"exec",
|
|
1996
|
+
"pushd",
|
|
1997
|
+
"popd"
|
|
1998
|
+
];
|
|
1999
|
+
var ALWAYS_INTERACTIVE_COMMANDS = [
|
|
2000
|
+
"vim",
|
|
2001
|
+
"vi",
|
|
2002
|
+
"nvim",
|
|
2003
|
+
"nano",
|
|
2004
|
+
"emacs",
|
|
2005
|
+
"less",
|
|
2006
|
+
"more",
|
|
2007
|
+
"man",
|
|
2008
|
+
"top",
|
|
2009
|
+
"htop",
|
|
2010
|
+
"ssh",
|
|
2011
|
+
"telnet",
|
|
2012
|
+
"tmux",
|
|
2013
|
+
"screen",
|
|
2014
|
+
"sudo",
|
|
2015
|
+
"su",
|
|
2016
|
+
"passwd",
|
|
2017
|
+
"claude",
|
|
2018
|
+
"watch"
|
|
2019
|
+
];
|
|
2020
|
+
var INTERACTIVE_WHEN_BARE_COMMANDS = [
|
|
2021
|
+
"python",
|
|
2022
|
+
"python3",
|
|
2023
|
+
"node",
|
|
2024
|
+
"irb",
|
|
2025
|
+
"psql",
|
|
2026
|
+
"mysql",
|
|
2027
|
+
"sqlite3",
|
|
2028
|
+
"bash",
|
|
2029
|
+
"sh",
|
|
2030
|
+
"zsh",
|
|
2031
|
+
"fish",
|
|
2032
|
+
"gh"
|
|
2033
|
+
];
|
|
2034
|
+
var SHELL_COMMANDS = ["bash", "sh", "zsh", "fish"];
|
|
2035
|
+
var DOCKER_TTY_FLAG = /^-(?:i|t|it|ti)$|^--interactive$|^--tty$/;
|
|
2036
|
+
var STATE_CHANGE_PATTERN = new RegExp(
|
|
2037
|
+
`(?:^|;|\\||\\(|&&|\\n)\\s*(?:${STATE_CHANGING_TOKENS.map(escapeRegExp).join("|")})(?=\\s|$|[;|)&])`
|
|
2038
|
+
);
|
|
2039
|
+
var ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
2040
|
+
function quoteForShell(value) {
|
|
2041
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
2082
2042
|
}
|
|
2083
|
-
function
|
|
2084
|
-
|
|
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
|
-
});
|
|
2043
|
+
function hasStateChange(command) {
|
|
2044
|
+
return STATE_CHANGE_PATTERN.test(command);
|
|
2093
2045
|
}
|
|
2094
|
-
function
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
totalLines
|
|
2113
|
-
});
|
|
2046
|
+
function isInteractiveCommand(command) {
|
|
2047
|
+
const words = command.split(/\s+/).filter((word2) => word2.length > 0);
|
|
2048
|
+
let index = 0;
|
|
2049
|
+
while (index < words.length && ENV_ASSIGNMENT_PATTERN.test(words[index] ?? "")) index += 1;
|
|
2050
|
+
const word = words[index];
|
|
2051
|
+
if (word === void 0) return true;
|
|
2052
|
+
const rest = words.slice(index + 1);
|
|
2053
|
+
if (ALWAYS_INTERACTIVE_COMMANDS.includes(word)) return true;
|
|
2054
|
+
if (INTERACTIVE_WHEN_BARE_COMMANDS.includes(word)) {
|
|
2055
|
+
if (rest.length === 0) return true;
|
|
2056
|
+
if (SHELL_COMMANDS.includes(word) && rest.includes("-i")) return true;
|
|
2057
|
+
return word === "gh" && rest[0] === "auth";
|
|
2058
|
+
}
|
|
2059
|
+
if (word === "tail") return rest[0] === "-f";
|
|
2060
|
+
if (word === "docker" && (rest[0] === "exec" || rest[0] === "run")) {
|
|
2061
|
+
return rest.some((flag) => DOCKER_TTY_FLAG.test(flag));
|
|
2062
|
+
}
|
|
2063
|
+
return false;
|
|
2114
2064
|
}
|
|
2115
|
-
function
|
|
2116
|
-
const
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
const
|
|
2120
|
-
if (
|
|
2121
|
-
else if (line.n <= head) decisions.set(line.n, { keep: true, reason: "head" });
|
|
2122
|
-
else decisions.set(line.n, { keep: false, reason: "fallback" });
|
|
2065
|
+
function isAllowlisted(command, allowlist) {
|
|
2066
|
+
for (const prefix of allowlist) {
|
|
2067
|
+
if (prefix.length === 0) continue;
|
|
2068
|
+
if (command === prefix) return true;
|
|
2069
|
+
const next = command.startsWith(prefix) ? command[prefix.length] : void 0;
|
|
2070
|
+
if (next !== void 0 && /\s/.test(next)) return true;
|
|
2123
2071
|
}
|
|
2124
|
-
return
|
|
2072
|
+
return false;
|
|
2125
2073
|
}
|
|
2126
|
-
function
|
|
2127
|
-
|
|
2074
|
+
function planRewrite(input, config) {
|
|
2075
|
+
if (input.tool_name !== "Bash") return null;
|
|
2076
|
+
if (!config.autoWrap) return null;
|
|
2077
|
+
const toolInput = input.tool_input;
|
|
2078
|
+
if (toolInput === void 0) return null;
|
|
2079
|
+
if (toolInput.run_in_background === true) return null;
|
|
2080
|
+
const command = typeof toolInput.command === "string" ? toolInput.command.trim() : "";
|
|
2081
|
+
if (command.length === 0) return null;
|
|
2082
|
+
if (/\bjevprune\b/.test(command)) return null;
|
|
2083
|
+
if (hasStateChange(command)) return null;
|
|
2084
|
+
if (isInteractiveCommand(command)) return null;
|
|
2085
|
+
if (command.endsWith("&") && !command.endsWith("&&")) return null;
|
|
2086
|
+
if (isAllowlisted(command, config.allowlist)) return null;
|
|
2087
|
+
const transcript = typeof input.transcript_path === "string" && input.transcript_path.length > 0 ? ` --transcript ${quoteForShell(input.transcript_path)}` : "";
|
|
2088
|
+
return { command: `jevprune run --hook${transcript} -- bash -c ${quoteForShell(command)}` };
|
|
2128
2089
|
}
|
|
2129
|
-
function
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2090
|
+
function parsePreToolUse(raw) {
|
|
2091
|
+
let parsed;
|
|
2092
|
+
try {
|
|
2093
|
+
parsed = JSON.parse(raw);
|
|
2094
|
+
} catch {
|
|
2095
|
+
return null;
|
|
2096
|
+
}
|
|
2097
|
+
if (!isRecord5(parsed)) return null;
|
|
2098
|
+
const toolName = parsed["tool_name"];
|
|
2099
|
+
const transcriptPath = parsed["transcript_path"];
|
|
2100
|
+
const toolInput = parsed["tool_input"];
|
|
2135
2101
|
return {
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
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
|
|
2102
|
+
...typeof toolName === "string" ? { tool_name: toolName } : {},
|
|
2103
|
+
...typeof transcriptPath === "string" ? { transcript_path: transcriptPath } : {},
|
|
2104
|
+
...isRecord5(toolInput) ? { tool_input: readToolInput(toolInput) } : {}
|
|
2148
2105
|
};
|
|
2149
2106
|
}
|
|
2150
|
-
function
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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;
|
|
2107
|
+
function formatHookOutput(plan) {
|
|
2108
|
+
return JSON.stringify({
|
|
2109
|
+
hookSpecificOutput: {
|
|
2110
|
+
hookEventName: "PreToolUse",
|
|
2111
|
+
updatedInput: { command: plan.command }
|
|
2167
2112
|
}
|
|
2168
|
-
}
|
|
2169
|
-
if (error instanceof Error) return error.name;
|
|
2170
|
-
return "unknown";
|
|
2113
|
+
});
|
|
2171
2114
|
}
|
|
2172
|
-
function
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2115
|
+
function readToolInput(source) {
|
|
2116
|
+
const command = source["command"];
|
|
2117
|
+
const background = source["run_in_background"];
|
|
2175
2118
|
return {
|
|
2176
|
-
|
|
2177
|
-
|
|
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
|
|
2119
|
+
...typeof command === "string" ? { command } : {},
|
|
2120
|
+
...typeof background === "boolean" ? { run_in_background: background } : {}
|
|
2187
2121
|
};
|
|
2188
2122
|
}
|
|
2123
|
+
function isRecord5(value) {
|
|
2124
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2125
|
+
}
|
|
2126
|
+
function escapeRegExp(value) {
|
|
2127
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2128
|
+
}
|
|
2189
2129
|
|
|
2190
|
-
// src/
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
const
|
|
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
|
-
});
|
|
2130
|
+
// src/io.ts
|
|
2131
|
+
var CLOSED_PIPE_CODES = /* @__PURE__ */ new Set([
|
|
2132
|
+
"EPIPE",
|
|
2133
|
+
"EOF",
|
|
2134
|
+
"ERR_STREAM_DESTROYED",
|
|
2135
|
+
"ERR_STREAM_WRITE_AFTER_END"
|
|
2136
|
+
]);
|
|
2137
|
+
function processIo() {
|
|
2138
|
+
const write = openWriter(process.stdout);
|
|
2225
2139
|
return {
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {},
|
|
2233
|
-
...recorded.logPath !== void 0 ? { logPath: recorded.logPath } : {},
|
|
2234
|
-
footer: recorded.footer
|
|
2140
|
+
env: process.env,
|
|
2141
|
+
cwd: process.cwd(),
|
|
2142
|
+
stdin: process.stdin,
|
|
2143
|
+
write: (text) => write(text),
|
|
2144
|
+
writeBytes: (bytes) => write(bytes),
|
|
2145
|
+
writeError: openWriter(process.stderr)
|
|
2235
2146
|
};
|
|
2236
2147
|
}
|
|
2237
|
-
async function
|
|
2238
|
-
const
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2148
|
+
async function readStreamBytes(stream) {
|
|
2149
|
+
const chunks = [];
|
|
2150
|
+
for await (const chunk of stream) {
|
|
2151
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
|
|
2152
|
+
}
|
|
2153
|
+
return Buffer.concat(chunks);
|
|
2154
|
+
}
|
|
2155
|
+
async function readStream(stream) {
|
|
2156
|
+
return (await readStreamBytes(stream)).toString("utf8");
|
|
2157
|
+
}
|
|
2158
|
+
function isClosedPipe(error) {
|
|
2159
|
+
const code = errorCode(error);
|
|
2160
|
+
return code !== void 0 && CLOSED_PIPE_CODES.has(code);
|
|
2161
|
+
}
|
|
2162
|
+
function openWriter(stream) {
|
|
2163
|
+
let closed = false;
|
|
2164
|
+
stream.on("error", (error) => {
|
|
2165
|
+
if (isClosedPipe(error)) closed = true;
|
|
2166
|
+
});
|
|
2167
|
+
return (chunk) => new Promise((resolve2, reject) => {
|
|
2168
|
+
if (closed || chunk.length === 0) {
|
|
2169
|
+
resolve2();
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2243
2172
|
try {
|
|
2244
|
-
|
|
2245
|
-
if (
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
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 } : {}
|
|
2173
|
+
stream.write(chunk, (error) => {
|
|
2174
|
+
if (error === void 0 || error === null) resolve2();
|
|
2175
|
+
else if (isClosedPipe(error)) {
|
|
2176
|
+
closed = true;
|
|
2177
|
+
resolve2();
|
|
2178
|
+
} else reject(error);
|
|
2264
2179
|
});
|
|
2265
|
-
await store.enforceRetention();
|
|
2266
2180
|
} catch (error) {
|
|
2267
|
-
if (!(error
|
|
2268
|
-
|
|
2181
|
+
if (!isClosedPipe(error)) {
|
|
2182
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
closed = true;
|
|
2186
|
+
resolve2();
|
|
2269
2187
|
}
|
|
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
2188
|
});
|
|
2282
|
-
return { footer, ...logPath !== void 0 ? { logPath } : {} };
|
|
2283
2189
|
}
|
|
2284
|
-
|
|
2190
|
+
|
|
2191
|
+
// src/commands/hook.ts
|
|
2192
|
+
async function runHook(io) {
|
|
2285
2193
|
try {
|
|
2286
|
-
|
|
2194
|
+
const input = parsePreToolUse(await readStream(io.stdin));
|
|
2195
|
+
if (input === null) return 0;
|
|
2196
|
+
const plan = planRewrite(input, await loadConfig(io.env));
|
|
2197
|
+
if (plan === null) return 0;
|
|
2198
|
+
await io.write(`${formatHookOutput(plan)}
|
|
2199
|
+
`);
|
|
2287
2200
|
} catch (error) {
|
|
2288
|
-
|
|
2289
|
-
|
|
2201
|
+
await io.writeError(`jevprune: hook skipped: ${errorMessage(error)}
|
|
2202
|
+
`).catch(() => void 0);
|
|
2290
2203
|
}
|
|
2204
|
+
return 0;
|
|
2291
2205
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2206
|
+
|
|
2207
|
+
// src/notices.ts
|
|
2208
|
+
import { mkdir as mkdir2, open as open2 } from "fs/promises";
|
|
2209
|
+
import { join as join3 } from "path";
|
|
2210
|
+
var MISSING_KEY_MARKER = "missing-key-notice";
|
|
2211
|
+
async function shouldAnnounceMissingKey(home) {
|
|
2212
|
+
const path = join3(home, MISSING_KEY_MARKER);
|
|
2213
|
+
try {
|
|
2214
|
+
await mkdir2(home, { recursive: true, mode: 448 });
|
|
2215
|
+
const handle = await open2(path, "wx", 384);
|
|
2216
|
+
await handle.close();
|
|
2217
|
+
return true;
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
return errorCode(error) !== "EEXIST";
|
|
2297
2220
|
}
|
|
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
2221
|
}
|
|
2306
2222
|
|
|
2307
|
-
// src/
|
|
2308
|
-
import { spawn } from "child_process";
|
|
2309
|
-
import { constants } from "os";
|
|
2310
|
-
var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
2223
|
+
// src/capture.ts
|
|
2311
2224
|
var TAIL_RING_BYTES = 256 * 1024;
|
|
2312
2225
|
var LF3 = 10;
|
|
2313
2226
|
var CR2 = 13;
|
|
@@ -2384,7 +2297,10 @@ var CaptureBuffer = class {
|
|
|
2384
2297
|
this.#trimRing();
|
|
2385
2298
|
}
|
|
2386
2299
|
bytes() {
|
|
2387
|
-
if (!this.#oversize)
|
|
2300
|
+
if (!this.#oversize) {
|
|
2301
|
+
const single = this.#head.length === 1 ? this.#head[0] : void 0;
|
|
2302
|
+
return single ?? Buffer.concat(this.#head);
|
|
2303
|
+
}
|
|
2388
2304
|
const head = trimToLastTerminator(Buffer.concat(this.#head));
|
|
2389
2305
|
const tail = trimToFirstLine(Buffer.concat(this.#ring));
|
|
2390
2306
|
return Buffer.concat([head, tail]);
|
|
@@ -2404,15 +2320,202 @@ var CaptureBuffer = class {
|
|
|
2404
2320
|
}
|
|
2405
2321
|
}
|
|
2406
2322
|
};
|
|
2323
|
+
var OutputCapture = class {
|
|
2324
|
+
#buffer;
|
|
2325
|
+
#counter = new LineCounter();
|
|
2326
|
+
#bytes = 0;
|
|
2327
|
+
constructor(maxBytes) {
|
|
2328
|
+
this.#buffer = new CaptureBuffer(maxBytes);
|
|
2329
|
+
}
|
|
2330
|
+
push(chunk) {
|
|
2331
|
+
this.#bytes += chunk.length;
|
|
2332
|
+
this.#counter.push(chunk);
|
|
2333
|
+
this.#buffer.push(chunk);
|
|
2334
|
+
}
|
|
2335
|
+
result() {
|
|
2336
|
+
return {
|
|
2337
|
+
captured: this.#buffer.bytes(),
|
|
2338
|
+
bytes: this.#bytes,
|
|
2339
|
+
lines: this.#counter.lines,
|
|
2340
|
+
headSegmentLines: this.#buffer.headSegmentLines,
|
|
2341
|
+
oversize: this.#buffer.oversize
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
function captureBytes(bytes, maxBytes) {
|
|
2346
|
+
const capture = new OutputCapture(maxBytes);
|
|
2347
|
+
capture.push(bytes);
|
|
2348
|
+
return capture.result();
|
|
2349
|
+
}
|
|
2350
|
+
function trimToLastTerminator(buffer) {
|
|
2351
|
+
for (let index = buffer.length - 1; index >= 0; index -= 1) {
|
|
2352
|
+
const byte = buffer[index];
|
|
2353
|
+
if (byte === LF3 || byte === CR2) return buffer.subarray(0, index + 1);
|
|
2354
|
+
}
|
|
2355
|
+
return buffer.subarray(0, 0);
|
|
2356
|
+
}
|
|
2357
|
+
function trimToFirstLine(buffer) {
|
|
2358
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
2359
|
+
const byte = buffer[index];
|
|
2360
|
+
if (byte === LF3) return buffer.subarray(index + 1);
|
|
2361
|
+
if (byte === CR2) {
|
|
2362
|
+
const next = buffer[index + 1];
|
|
2363
|
+
return buffer.subarray(next === LF3 ? index + 2 : index + 1);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
return Buffer.alloc(0);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// src/prune.ts
|
|
2370
|
+
async function pruneOutput(input) {
|
|
2371
|
+
const prepared = await prepare(input);
|
|
2372
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2373
|
+
const bytes = Buffer.from(input.text, "utf8");
|
|
2374
|
+
const capture = captureBytes(bytes, prepared.config.maxPruneBytes);
|
|
2375
|
+
return await selectAndRecord({
|
|
2376
|
+
prepared,
|
|
2377
|
+
capture,
|
|
2378
|
+
text: capture.oversize ? capture.captured.toString("utf8") : input.text,
|
|
2379
|
+
task: input.task,
|
|
2380
|
+
exitCode: input.exitCode ?? null,
|
|
2381
|
+
startedAt,
|
|
2382
|
+
logBytes: bytes
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
async function recordRun(input) {
|
|
2386
|
+
const { store, selection, meta } = input;
|
|
2387
|
+
const fastPath = selection.mode === "fast-path";
|
|
2388
|
+
const log = input.logBytes ?? (input.logText !== void 0 ? Buffer.from(input.logText, "utf8") : void 0);
|
|
2389
|
+
let failureCode = input.storeFailureCode;
|
|
2390
|
+
if (store !== null && failureCode === void 0) {
|
|
2391
|
+
try {
|
|
2392
|
+
if (fastPath) {
|
|
2393
|
+
if (log === void 0) await store.discardRun(meta.id);
|
|
2394
|
+
} else {
|
|
2395
|
+
if (log !== void 0) await writeLog(store, meta.id, log);
|
|
2396
|
+
await store.finalizeRun(meta.id, {
|
|
2397
|
+
...meta,
|
|
2398
|
+
mode: selection.mode,
|
|
2399
|
+
linesOut: selection.linesOut,
|
|
2400
|
+
...selection.fallbackReason !== void 0 ? { fallbackReason: fallbackReasonText(selection.fallbackReason) } : {}
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
await store.appendGain({
|
|
2404
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2405
|
+
id: meta.id,
|
|
2406
|
+
mode: selection.mode,
|
|
2407
|
+
linesIn: selection.linesIn,
|
|
2408
|
+
linesOut: selection.linesOut,
|
|
2409
|
+
bytesIn: selection.bytesIn,
|
|
2410
|
+
bytesOut: selection.bytesOut,
|
|
2411
|
+
...selection.fallbackReason !== void 0 ? { reason: fallbackReasonText(selection.fallbackReason) } : {}
|
|
2412
|
+
});
|
|
2413
|
+
await store.enforceRetention();
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
if (!(error instanceof RunStoreError)) throw error;
|
|
2416
|
+
failureCode = error.code ?? "failed";
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
const logPath = store !== null && !fastPath && failureCode === void 0 ? store.logPath(meta.id) : void 0;
|
|
2420
|
+
const footer = formatFooter({
|
|
2421
|
+
mode: selection.mode,
|
|
2422
|
+
linesIn: selection.linesIn,
|
|
2423
|
+
linesOut: selection.linesOut,
|
|
2424
|
+
exitCode: meta.exitCode,
|
|
2425
|
+
...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {},
|
|
2426
|
+
...input.passthroughNote !== void 0 ? { passthroughNote: input.passthroughNote } : {},
|
|
2427
|
+
...failureCode !== void 0 ? { storeFailureCode: failureCode } : {},
|
|
2428
|
+
...logPath !== void 0 ? { logPath } : {}
|
|
2429
|
+
});
|
|
2430
|
+
return { footer, ...logPath !== void 0 ? { logPath } : {} };
|
|
2431
|
+
}
|
|
2432
|
+
function clientFromEnv(env) {
|
|
2433
|
+
try {
|
|
2434
|
+
return createJevClientFromEnv(env);
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
if (error instanceof JevConfigError) return null;
|
|
2437
|
+
throw error;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
function mergeConfig(base, overrides) {
|
|
2441
|
+
if (overrides === void 0) return base;
|
|
2442
|
+
const defined = {};
|
|
2443
|
+
for (const key of Object.keys(overrides)) {
|
|
2444
|
+
if (overrides[key] !== void 0) Object.assign(defined, { [key]: overrides[key] });
|
|
2445
|
+
}
|
|
2446
|
+
return { ...base, ...defined };
|
|
2447
|
+
}
|
|
2448
|
+
async function prepare(input) {
|
|
2449
|
+
const env = input.env ?? process.env;
|
|
2450
|
+
const config = mergeConfig(await loadConfig(env), input.config);
|
|
2451
|
+
return {
|
|
2452
|
+
config,
|
|
2453
|
+
client: input.client !== void 0 ? input.client : clientFromEnv(env),
|
|
2454
|
+
store: input.save === false ? null : new RunStore({ home: config.home, retention: config.retention }),
|
|
2455
|
+
runId: newRunId(),
|
|
2456
|
+
command: input.command ?? ""
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
async function selectAndRecord(input) {
|
|
2460
|
+
const { prepared, capture } = input;
|
|
2461
|
+
const selection = await selectLines({
|
|
2462
|
+
text: input.text,
|
|
2463
|
+
task: input.task,
|
|
2464
|
+
command: prepared.command,
|
|
2465
|
+
exitCode: input.exitCode,
|
|
2466
|
+
...capture.oversize ? { oversize: { lines: capture.lines, headSegmentLines: capture.headSegmentLines } } : {},
|
|
2467
|
+
client: prepared.client,
|
|
2468
|
+
config: prepared.config,
|
|
2469
|
+
runId: prepared.runId
|
|
2470
|
+
});
|
|
2471
|
+
const recorded = await recordRun({
|
|
2472
|
+
store: prepared.store,
|
|
2473
|
+
selection,
|
|
2474
|
+
...input.logBytes !== void 0 ? { logBytes: input.logBytes } : {},
|
|
2475
|
+
...input.storeFailureCode !== void 0 ? { storeFailureCode: input.storeFailureCode } : {},
|
|
2476
|
+
meta: {
|
|
2477
|
+
id: prepared.runId,
|
|
2478
|
+
command: prepared.command,
|
|
2479
|
+
argv: [],
|
|
2480
|
+
startedAt: input.startedAt,
|
|
2481
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2482
|
+
exitCode: input.exitCode,
|
|
2483
|
+
signal: null,
|
|
2484
|
+
bytes: capture.bytes,
|
|
2485
|
+
lines: selection.linesIn,
|
|
2486
|
+
task: input.task
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
return {
|
|
2490
|
+
kept: selection.kept,
|
|
2491
|
+
dropped: selection.dropped,
|
|
2492
|
+
runId: prepared.runId,
|
|
2493
|
+
mode: selection.mode,
|
|
2494
|
+
linesIn: selection.linesIn,
|
|
2495
|
+
linesOut: selection.linesOut,
|
|
2496
|
+
...selection.fallbackReason !== void 0 ? { fallbackReason: selection.fallbackReason } : {},
|
|
2497
|
+
...recorded.logPath !== void 0 ? { logPath: recorded.logPath } : {},
|
|
2498
|
+
footer: recorded.footer
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
async function writeLog(store, id, bytes) {
|
|
2502
|
+
const writer = await store.openRun({ id });
|
|
2503
|
+
writer.write(bytes);
|
|
2504
|
+
await writer.close();
|
|
2505
|
+
if (writer.failure !== void 0) throw writer.failure;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// src/runner.ts
|
|
2509
|
+
import { spawn } from "child_process";
|
|
2510
|
+
import { constants } from "os";
|
|
2511
|
+
var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
2407
2512
|
async function runCommand(input) {
|
|
2408
2513
|
const executable = input.argv[0];
|
|
2409
2514
|
if (executable === void 0 || executable.length === 0) {
|
|
2410
2515
|
throw new UsageError("run needs a command to execute");
|
|
2411
2516
|
}
|
|
2412
2517
|
const writer = await openWriter2(input);
|
|
2413
|
-
const
|
|
2414
|
-
const counter = new LineCounter();
|
|
2415
|
-
let bytes = 0;
|
|
2518
|
+
const capture = new OutputCapture(input.maxPruneBytes);
|
|
2416
2519
|
let storeFailure = writer.failure;
|
|
2417
2520
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2418
2521
|
const child = spawn(executable, input.argv.slice(1), {
|
|
@@ -2423,9 +2526,7 @@ async function runCommand(input) {
|
|
|
2423
2526
|
});
|
|
2424
2527
|
let paused = false;
|
|
2425
2528
|
const onChunk = (chunk) => {
|
|
2426
|
-
|
|
2427
|
-
counter.push(chunk);
|
|
2428
|
-
buffer.push(chunk);
|
|
2529
|
+
capture.push(chunk);
|
|
2429
2530
|
const ready = writer.write(chunk);
|
|
2430
2531
|
if (ready || paused) return;
|
|
2431
2532
|
paused = true;
|
|
@@ -2452,17 +2553,13 @@ async function runCommand(input) {
|
|
|
2452
2553
|
await writer.close();
|
|
2453
2554
|
}
|
|
2454
2555
|
storeFailure = writer.failure ?? storeFailure;
|
|
2455
|
-
const
|
|
2556
|
+
const output = capture.result();
|
|
2456
2557
|
return {
|
|
2457
|
-
|
|
2458
|
-
validUtf8: isValidUtf8(captured),
|
|
2459
|
-
bytes,
|
|
2460
|
-
lines: counter.lines,
|
|
2461
|
-
headSegmentLines: buffer.headSegmentLines,
|
|
2558
|
+
...output,
|
|
2559
|
+
validUtf8: isValidUtf8(output.captured),
|
|
2462
2560
|
exitCode: exitCodeOf(exit),
|
|
2463
2561
|
signal: exit.signal,
|
|
2464
2562
|
interrupted: receivedSignal || exit.signal !== null,
|
|
2465
|
-
oversize: buffer.oversize,
|
|
2466
2563
|
startedAt,
|
|
2467
2564
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2468
2565
|
...storeFailure !== void 0 ? { storeFailure } : {}
|
|
@@ -2525,24 +2622,6 @@ function exitCodeOf(exit) {
|
|
|
2525
2622
|
if (exit.signal !== null) return 128 + (constants.signals[exit.signal] ?? 0);
|
|
2526
2623
|
return 0;
|
|
2527
2624
|
}
|
|
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
2625
|
|
|
2547
2626
|
// src/task.ts
|
|
2548
2627
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -2665,7 +2744,8 @@ async function runRun(options, io) {
|
|
|
2665
2744
|
config,
|
|
2666
2745
|
runId
|
|
2667
2746
|
});
|
|
2668
|
-
|
|
2747
|
+
const reason = selection.fallbackReason;
|
|
2748
|
+
if (options.hook === true && reason?.kind === "unavailable" && reason.detail === UNAUTHORIZED_REASON) {
|
|
2669
2749
|
await io.writeError(`jevprune: ${TYPESAFE_API_KEY_ENV} rejected (401), using fallback
|
|
2670
2750
|
`).catch(() => void 0);
|
|
2671
2751
|
}
|
|
@@ -2707,9 +2787,9 @@ async function passThrough(input, io) {
|
|
|
2707
2787
|
lastByte = capture.captured.at(-1);
|
|
2708
2788
|
complete = !capture.oversize;
|
|
2709
2789
|
}
|
|
2710
|
-
const oversizeReason =
|
|
2711
|
-
const reason = capture.validUtf8 ? complete ? void 0 : oversizeReason :
|
|
2712
|
-
const note = capture.validUtf8 ? reason : NOT_UTF8_NOTE;
|
|
2790
|
+
const oversizeReason = { kind: "size-limit", maxBytes: input.maxPruneBytes };
|
|
2791
|
+
const reason = capture.validUtf8 ? complete ? void 0 : oversizeReason : { kind: "not-utf8" };
|
|
2792
|
+
const note = capture.validUtf8 ? reason === void 0 ? void 0 : fallbackReasonText(reason) : NOT_UTF8_NOTE;
|
|
2713
2793
|
try {
|
|
2714
2794
|
const { footer } = await recordRun({
|
|
2715
2795
|
store: input.store,
|
|
@@ -2768,7 +2848,7 @@ async function passThrough2(input, io) {
|
|
|
2768
2848
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2769
2849
|
const { footer } = await recordRun({
|
|
2770
2850
|
store,
|
|
2771
|
-
selection: passthroughSelection({ bytes: input.bytes.length, lines, reason:
|
|
2851
|
+
selection: passthroughSelection({ bytes: input.bytes.length, lines, reason: { kind: "not-utf8" } }),
|
|
2772
2852
|
logBytes: input.bytes,
|
|
2773
2853
|
passthroughNote: NOT_UTF8_NOTE,
|
|
2774
2854
|
meta: {
|
|
@@ -2815,19 +2895,26 @@ async function runShow(options, io) {
|
|
|
2815
2895
|
}
|
|
2816
2896
|
|
|
2817
2897
|
// src/version.ts
|
|
2818
|
-
var VERSION2 = "0.1.
|
|
2898
|
+
var VERSION2 = "0.1.1";
|
|
2819
2899
|
|
|
2820
2900
|
// src/cli.ts
|
|
2821
2901
|
var HELP = `usage: jevprune <command> [options]
|
|
2822
2902
|
|
|
2823
2903
|
commands:
|
|
2824
2904
|
run [--task <text>] [--threshold <n>] [--hook] [--transcript <path>] -- <command> [args...]
|
|
2905
|
+
runs the command, prints the kept lines and exits with the command's exit code
|
|
2825
2906
|
select [--task <text>] [--threshold <n>] [--file <path>] [--command <text>]
|
|
2907
|
+
prunes a local file or stdin and reports only its own exit status
|
|
2826
2908
|
show <id> [--lines A-B]
|
|
2909
|
+
prints a saved run, or one line range of it, exactly as it was captured
|
|
2827
2910
|
gain
|
|
2911
|
+
totals the locally recorded runs and estimates the output tokens removed
|
|
2828
2912
|
hook
|
|
2913
|
+
reads a plugin PreToolUse event on stdin and answers it
|
|
2829
2914
|
|
|
2830
2915
|
options:
|
|
2916
|
+
--task <text> the task the kept lines have to serve
|
|
2917
|
+
--threshold <n> minimum Jev score to keep a line, 0 to 1
|
|
2831
2918
|
--help
|
|
2832
2919
|
--version
|
|
2833
2920
|
`;
|