hillclimb 0.5.3 → 0.5.5

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 CHANGED
@@ -2419,6 +2419,48 @@ async function readFileContent(file) {
2419
2419
  }
2420
2420
  }
2421
2421
 
2422
+ // src/middleware/large-file.ts
2423
+ import { constants as bufferConstants } from "buffer";
2424
+ var MAX_STRINGIFIABLE_BYTES = bufferConstants.MAX_STRING_LENGTH;
2425
+ var largeFileThreshold = MAX_STRINGIFIABLE_BYTES;
2426
+ function getLargeFileThreshold() {
2427
+ return largeFileThreshold;
2428
+ }
2429
+ var NEWLINE = 10;
2430
+ var OVERSIZED_LINE_PLACEHOLDER = '{"_hillclimb_omitted":"line exceeded max string length; dropped to allow redaction"}';
2431
+ function redactBufferByLine(buffer, redactLine, options = {}) {
2432
+ const maxLineBytes = options.maxLineBytes ?? MAX_STRINGIFIABLE_BYTES;
2433
+ const oversizedPlaceholder = options.oversizedPlaceholder ?? OVERSIZED_LINE_PLACEHOLDER;
2434
+ const pieces = [];
2435
+ const newlineBuf = Buffer.from([NEWLINE]);
2436
+ let totalCount = 0;
2437
+ let oversizedLines = 0;
2438
+ let start = 0;
2439
+ let first = true;
2440
+ for (; ; ) {
2441
+ const nl = buffer.indexOf(NEWLINE, start);
2442
+ const end = nl === -1 ? buffer.length : nl;
2443
+ const slice = buffer.subarray(start, end);
2444
+ if (!first) pieces.push(newlineBuf);
2445
+ first = false;
2446
+ if (slice.length > maxLineBytes) {
2447
+ oversizedLines++;
2448
+ pieces.push(Buffer.from(oversizedPlaceholder, "utf-8"));
2449
+ } else {
2450
+ const { value, count } = redactLine(slice.toString("utf-8"));
2451
+ totalCount += count;
2452
+ pieces.push(Buffer.from(value, "utf-8"));
2453
+ }
2454
+ if (nl === -1) break;
2455
+ start = nl + 1;
2456
+ }
2457
+ return {
2458
+ content: Buffer.concat(pieces),
2459
+ count: totalCount,
2460
+ oversizedLines
2461
+ };
2462
+ }
2463
+
2422
2464
  // src/middleware/patterns.json
2423
2465
  var patterns_default = {
2424
2466
  patterns: [
@@ -10475,6 +10517,22 @@ var patterns_default = {
10475
10517
  ]
10476
10518
  };
10477
10519
 
10520
+ // src/middleware/patterns-local.json
10521
+ var patterns_local_default = {
10522
+ patterns: [
10523
+ {
10524
+ name: "Hugging Face User Access Token",
10525
+ regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))hf_[A-Za-z0-9]{34,40}",
10526
+ confidence: "high"
10527
+ },
10528
+ {
10529
+ name: "Hugging Face Organization API Token",
10530
+ regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))api_org_[A-Za-z0-9]{34,40}",
10531
+ confidence: "high"
10532
+ }
10533
+ ]
10534
+ };
10535
+
10478
10536
  // src/middleware/pattern-core.ts
10479
10537
  var DELIMITER_SUFFIX = /\(=\| =\|:\| :\)$/;
10480
10538
  var HAS_VALUE_MATCHER = /[[{+*]|\\[wdWDsS]/;
@@ -10510,7 +10568,8 @@ function countCaptureGroups(source) {
10510
10568
  }
10511
10569
  function compilePatterns() {
10512
10570
  const patterns = [];
10513
- for (const p7 of patterns_default.patterns) {
10571
+ const allPatterns = [...patterns_default.patterns, ...patterns_local_default.patterns];
10572
+ for (const p7 of allPatterns) {
10514
10573
  if (p7.confidence !== "high") continue;
10515
10574
  if (DELIMITER_SUFFIX.test(p7.regex)) continue;
10516
10575
  if (!HAS_VALUE_MATCHER.test(p7.regex)) continue;
@@ -10641,6 +10700,19 @@ function processFiles(files) {
10641
10700
  }
10642
10701
 
10643
10702
  // src/middleware/pattern-redact.ts
10703
+ function redactPatternLine(line, patterns, memo, isJsonl) {
10704
+ if (isJsonl) {
10705
+ if (!line.trim()) return { value: line, count: 0 };
10706
+ try {
10707
+ const parsed = JSON.parse(line);
10708
+ const walked = walkAndRedactAll(parsed, patterns, memo);
10709
+ return { value: JSON.stringify(walked.value), count: walked.count };
10710
+ } catch {
10711
+ return redactString(line, patterns);
10712
+ }
10713
+ }
10714
+ return redactString(line, patterns);
10715
+ }
10644
10716
  var WORKER_COUNT = Math.min(
10645
10717
  os3.availableParallelism?.() ?? os3.cpus().length,
10646
10718
  4
@@ -10669,9 +10741,14 @@ var PatternRedactMiddleware = class {
10669
10741
  const tasks = [];
10670
10742
  const fileMap = /* @__PURE__ */ new Map();
10671
10743
  const passThrough = [];
10744
+ const largeResults = /* @__PURE__ */ new Map();
10672
10745
  for (let i = 0; i < group.files.length; i++) {
10673
10746
  const file = group.files[i];
10674
10747
  this.stats.filesScanned++;
10748
+ if (file.content && !file.isBinary && file.content.length > getLargeFileThreshold()) {
10749
+ largeResults.set(i, this.redactLargeFile(file));
10750
+ continue;
10751
+ }
10675
10752
  const read = await readFileContent(file);
10676
10753
  if (read.kind === "binary") {
10677
10754
  this.stats.binaryFiles++;
@@ -10701,6 +10778,11 @@ var PatternRedactMiddleware = class {
10701
10778
  }
10702
10779
  const newFiles = [];
10703
10780
  for (let i = 0; i < group.files.length; i++) {
10781
+ const large = largeResults.get(i);
10782
+ if (large) {
10783
+ newFiles.push(large);
10784
+ continue;
10785
+ }
10704
10786
  const pt = passThrough.find((p7) => p7.index === i);
10705
10787
  if (pt) {
10706
10788
  newFiles.push(pt.file);
@@ -10722,6 +10804,33 @@ var PatternRedactMiddleware = class {
10722
10804
  }
10723
10805
  return { ...group, files: newFiles };
10724
10806
  }
10807
+ // Stream-redact an oversized file straight from its buffer, one line at a time,
10808
+ // so we never build a string larger than a single line. Single-threaded (no
10809
+ // worker pool) — it trades parallelism for bounded memory on the one giant file.
10810
+ redactLargeFile(file) {
10811
+ const buffer = file.content;
10812
+ const isJsonl = file.absolutePath.endsWith(".jsonl");
10813
+ const patterns = compilePatterns();
10814
+ const memo = /* @__PURE__ */ new Map();
10815
+ const { content, count, oversizedLines } = redactBufferByLine(
10816
+ buffer,
10817
+ (line) => redactPatternLine(line, patterns, memo, isJsonl)
10818
+ );
10819
+ if (oversizedLines > 0) {
10820
+ appendLog(
10821
+ "warn",
10822
+ `pattern-redact: dropped ${oversizedLines} oversized line(s) in ${file.absolutePath} (exceeded max string length)`
10823
+ );
10824
+ }
10825
+ if (count > 0) {
10826
+ this.stats.filesRedacted++;
10827
+ this.stats.totalRedactions += count;
10828
+ }
10829
+ if (count > 0 || oversizedLines > 0) {
10830
+ return { ...file, content };
10831
+ }
10832
+ return file;
10833
+ }
10725
10834
  async processWithWorkers(tasks) {
10726
10835
  const workerPath = resolveWorkerPath();
10727
10836
  const sorted = [...tasks].sort(
@@ -10797,6 +10906,10 @@ var RedactMiddleware = class {
10797
10906
  for (const file of group.files) {
10798
10907
  this.stats.filesScanned++;
10799
10908
  regex.lastIndex = 0;
10909
+ if (file.content && !file.isBinary && file.content.length > getLargeFileThreshold()) {
10910
+ newFiles.push(this.redactLargeFile(file, regex));
10911
+ continue;
10912
+ }
10800
10913
  const read = await readFileContent(file);
10801
10914
  if (read.kind === "binary") {
10802
10915
  this.stats.binaryFiles++;
@@ -10832,6 +10945,47 @@ var RedactMiddleware = class {
10832
10945
  }
10833
10946
  return { ...group, files: newFiles };
10834
10947
  }
10948
+ // Stream-redact an oversized file straight from its buffer. Each line is
10949
+ // redacted with the same JSON-aware (or raw) logic as redactJsonl, so the
10950
+ // output matches the in-memory path exactly for files whose lines all fit in a
10951
+ // string — which holds for real transcripts (largest line is a few MiB).
10952
+ redactLargeFile(file, regex) {
10953
+ const buffer = file.content;
10954
+ const isJsonl = file.absolutePath.endsWith(".jsonl");
10955
+ const { content, count, oversizedLines } = redactBufferByLine(
10956
+ buffer,
10957
+ (line) => this.redactLine(line, regex, isJsonl)
10958
+ );
10959
+ if (oversizedLines > 0) {
10960
+ appendLog(
10961
+ "warn",
10962
+ `redact: dropped ${oversizedLines} oversized line(s) in ${file.absolutePath} (exceeded max string length)`
10963
+ );
10964
+ }
10965
+ if (count > 0) {
10966
+ this.stats.filesRedacted++;
10967
+ this.stats.totalRedactions += count;
10968
+ }
10969
+ if (count > 0 || oversizedLines > 0) {
10970
+ return { ...file, content };
10971
+ }
10972
+ return file;
10973
+ }
10974
+ redactLine(line, regex, isJsonl) {
10975
+ if (isJsonl) {
10976
+ if (!line.trim()) return { value: line, count: 0 };
10977
+ try {
10978
+ const parsed = JSON.parse(line);
10979
+ const walked = walkAndRedact(parsed, regex);
10980
+ return { value: JSON.stringify(walked.value), count: walked.count };
10981
+ } catch {
10982
+ const { result: result2, count: count2 } = this.redactString(line, regex);
10983
+ return { value: result2, count: count2 };
10984
+ }
10985
+ }
10986
+ const { result, count } = this.redactString(line, regex);
10987
+ return { value: result, count };
10988
+ }
10835
10989
  redactJsonl(content, regex) {
10836
10990
  let totalCount = 0;
10837
10991
  const lines = content.split("\n");
@@ -13334,10 +13488,10 @@ var NormalizeMiddleware = class {
13334
13488
  file.sourceName
13335
13489
  ))
13336
13490
  continue;
13337
- const content = file.content ? file.content.toString("utf-8") : null;
13338
- if (!content) continue;
13339
13491
  const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path13.basename(file.absolutePath, ".jsonl"));
13340
13492
  try {
13493
+ const content = file.content ? file.content.toString("utf-8") : null;
13494
+ if (!content) continue;
13341
13495
  const trajectory = normalizeContent(
13342
13496
  file.sourceName,
13343
13497
  content,
@@ -13366,7 +13520,7 @@ var NormalizeMiddleware = class {
13366
13520
  } catch (err) {
13367
13521
  appendLog(
13368
13522
  "warn",
13369
- `normalize: failed for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
13523
+ `normalize: skipped ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
13370
13524
  );
13371
13525
  }
13372
13526
  }
@@ -14068,7 +14222,7 @@ import { execFileSync as execFileSync3 } from "child_process";
14068
14222
  import path18 from "path";
14069
14223
 
14070
14224
  // src/git-traces/git-ops.ts
14071
- import { execFileSync as execFileSync2 } from "child_process";
14225
+ import { execFileSync as execFileSync2, spawnSync } from "child_process";
14072
14226
  import fs12 from "fs";
14073
14227
  import os7 from "os";
14074
14228
  import path16 from "path";
@@ -14080,11 +14234,155 @@ var EXEC_OPTS = {
14080
14234
  };
14081
14235
  var MAX_ERROR_OUTPUT_CHARS = 2e3;
14082
14236
  var MAX_SNAPSHOT_FILE_BYTES = 10 * 1024 * 1024;
14083
- var EXCLUDED_SNAPSHOT_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
14237
+ var MAX_BINARY_SNAPSHOT_FILE_BYTES = 1024 * 1024;
14238
+ var BINARY_SNIFF_BYTES = 8e3;
14239
+ var EXCLUDED_SNAPSHOT_EXTENSIONS = /* @__PURE__ */ new Set([
14240
+ // Documents
14241
+ ".pdf",
14242
+ // Raster images
14243
+ ".png",
14244
+ ".jpg",
14245
+ ".jpeg",
14246
+ ".gif",
14247
+ ".bmp",
14248
+ ".tiff",
14249
+ ".tif",
14250
+ ".webp",
14251
+ ".ico",
14252
+ ".heic",
14253
+ ".heif",
14254
+ ".avif",
14255
+ // Video
14256
+ ".mp4",
14257
+ ".mov",
14258
+ ".avi",
14259
+ ".mkv",
14260
+ ".webm",
14261
+ ".m4v",
14262
+ ".mpg",
14263
+ ".mpeg",
14264
+ ".wmv",
14265
+ ".flv",
14266
+ // Audio
14267
+ ".mp3",
14268
+ ".wav",
14269
+ ".flac",
14270
+ ".ogg",
14271
+ ".m4a",
14272
+ ".aac",
14273
+ ".aiff",
14274
+ // Archives / compressed
14275
+ ".zip",
14276
+ ".tar",
14277
+ ".gz",
14278
+ ".tgz",
14279
+ ".bz2",
14280
+ ".tbz2",
14281
+ ".xz",
14282
+ ".7z",
14283
+ ".rar",
14284
+ ".zst",
14285
+ ".lz4",
14286
+ // Fonts
14287
+ ".woff",
14288
+ ".woff2",
14289
+ ".ttf",
14290
+ ".otf",
14291
+ ".eot",
14292
+ // Compiled / native artifacts
14293
+ ".so",
14294
+ ".dylib",
14295
+ ".dll",
14296
+ ".a",
14297
+ ".o",
14298
+ ".lib",
14299
+ ".class",
14300
+ ".jar",
14301
+ ".wasm",
14302
+ ".exe",
14303
+ ".pyc",
14304
+ ".pyd",
14305
+ ".node",
14306
+ // Databases
14307
+ ".sqlite",
14308
+ ".sqlite3",
14309
+ ".db",
14310
+ ".mdb",
14311
+ // Office / design binaries
14312
+ ".doc",
14313
+ ".docx",
14314
+ ".xls",
14315
+ ".xlsx",
14316
+ ".ppt",
14317
+ ".pptx",
14318
+ ".odt",
14319
+ ".ods",
14320
+ ".odp",
14321
+ ".psd",
14322
+ ".ai",
14323
+ ".sketch",
14324
+ ".fig",
14325
+ ".blend"
14326
+ ]);
14327
+ var EXCLUDED_SNAPSHOT_BASENAMES = /* @__PURE__ */ new Set([
14328
+ ".DS_Store",
14329
+ "Thumbs.db",
14330
+ "desktop.ini"
14331
+ ]);
14084
14332
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
14085
- function hasExcludedSnapshotExtension(filePath) {
14333
+ function isExcludedSnapshotPath(filePath) {
14334
+ if (EXCLUDED_SNAPSHOT_BASENAMES.has(path16.basename(filePath))) return true;
14086
14335
  return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path16.extname(filePath).toLowerCase());
14087
14336
  }
14337
+ function isBinaryBuffer(buffer) {
14338
+ return buffer.includes(0);
14339
+ }
14340
+ function readTreeBlobHead(repoRoot, sha) {
14341
+ try {
14342
+ const { stdout } = spawnSync("git", ["cat-file", "blob", sha], {
14343
+ cwd: repoRoot,
14344
+ timeout: GIT_COMMAND_TIMEOUT_MS,
14345
+ maxBuffer: BINARY_SNIFF_BYTES
14346
+ });
14347
+ return stdout?.length ? stdout.subarray(0, BINARY_SNIFF_BYTES) : null;
14348
+ } catch {
14349
+ return null;
14350
+ }
14351
+ }
14352
+ function readWorkingFileHead(absPath) {
14353
+ let fd = null;
14354
+ try {
14355
+ fd = fs12.openSync(absPath, "r");
14356
+ const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
14357
+ const bytesRead = fs12.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
14358
+ return buffer.subarray(0, bytesRead);
14359
+ } catch {
14360
+ return null;
14361
+ } finally {
14362
+ if (fd !== null) {
14363
+ try {
14364
+ fs12.closeSync(fd);
14365
+ } catch {
14366
+ }
14367
+ }
14368
+ }
14369
+ }
14370
+ function classifyOmission(filePath, sizeBytes, readHead) {
14371
+ if (isExcludedSnapshotPath(filePath)) return "excluded-extension";
14372
+ if (sizeBytes > MAX_SNAPSHOT_FILE_BYTES) return "file-over-limit";
14373
+ if (sizeBytes > MAX_BINARY_SNAPSHOT_FILE_BYTES) {
14374
+ const head = readHead();
14375
+ if (!head) {
14376
+ appendLog(
14377
+ "warn",
14378
+ `git-traces: binary sniff failed for ${filePath} (${sizeBytes} bytes); keeping it in the snapshot`
14379
+ );
14380
+ return null;
14381
+ }
14382
+ if (isBinaryBuffer(head)) return "binary-over-limit";
14383
+ }
14384
+ return null;
14385
+ }
14088
14386
  function quoteGitArg(arg) {
14089
14387
  if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
14090
14388
  return JSON.stringify(arg);
@@ -14188,7 +14486,7 @@ function recordOmittedSnapshotFile(omittedFiles, file, options = {}) {
14188
14486
  omittedFiles?.push(file);
14189
14487
  if (options.log === false) return;
14190
14488
  const action = file.tracked ? "omitting tracked" : "skipping untracked";
14191
- const reason = file.reason === "file-over-limit" ? `${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit` : "excluded extension";
14489
+ const reason = file.reason === "file-over-limit" ? `${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit` : file.reason === "binary-over-limit" ? `binary ${file.sizeBytes} bytes > ${MAX_BINARY_SNAPSHOT_FILE_BYTES} binary limit` : "excluded extension";
14192
14490
  appendLog("warn", `git-traces: ${action} ${file.path} (${reason})`);
14193
14491
  }
14194
14492
  function parseLsTreeLongZ(output) {
@@ -14211,7 +14509,11 @@ function listOmittedTreeFiles(repoRoot, treeSha, options = {}) {
14211
14509
  const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
14212
14510
  const omitted = [];
14213
14511
  for (const entry of parseLsTreeLongZ(output)) {
14214
- const reason = hasExcludedSnapshotExtension(entry.path) ? "excluded-extension" : entry.sizeBytes > MAX_SNAPSHOT_FILE_BYTES ? "file-over-limit" : null;
14512
+ const reason = classifyOmission(
14513
+ entry.path,
14514
+ entry.sizeBytes,
14515
+ () => readTreeBlobHead(repoRoot, entry.sha)
14516
+ );
14215
14517
  if (!reason) continue;
14216
14518
  const file = {
14217
14519
  path: entry.path,
@@ -14269,8 +14571,13 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
14269
14571
  for (const relPath of list.split("\0")) {
14270
14572
  if (!relPath) continue;
14271
14573
  try {
14272
- const stat = fs12.lstatSync(path16.join(repoRoot, relPath));
14273
- const reason = hasExcludedSnapshotExtension(relPath) ? "excluded-extension" : stat.size > MAX_SNAPSHOT_FILE_BYTES ? "file-over-limit" : null;
14574
+ const absPath = path16.join(repoRoot, relPath);
14575
+ const stat = fs12.lstatSync(absPath);
14576
+ const reason = classifyOmission(
14577
+ relPath,
14578
+ stat.size,
14579
+ () => readWorkingFileHead(absPath)
14580
+ );
14274
14581
  if (reason) {
14275
14582
  recordOmittedSnapshotFile(omittedFiles, {
14276
14583
  path: relPath,
@@ -8057,6 +8057,22 @@ var patterns_default = {
8057
8057
  ]
8058
8058
  };
8059
8059
 
8060
+ // src/middleware/patterns-local.json
8061
+ var patterns_local_default = {
8062
+ patterns: [
8063
+ {
8064
+ name: "Hugging Face User Access Token",
8065
+ regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))hf_[A-Za-z0-9]{34,40}",
8066
+ confidence: "high"
8067
+ },
8068
+ {
8069
+ name: "Hugging Face Organization API Token",
8070
+ regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))api_org_[A-Za-z0-9]{34,40}",
8071
+ confidence: "high"
8072
+ }
8073
+ ]
8074
+ };
8075
+
8060
8076
  // src/middleware/pattern-core.ts
8061
8077
  var DELIMITER_SUFFIX = /\(=\| =\|:\| :\)$/;
8062
8078
  var HAS_VALUE_MATCHER = /[[{+*]|\\[wdWDsS]/;
@@ -8092,7 +8108,8 @@ function countCaptureGroups(source) {
8092
8108
  }
8093
8109
  function compilePatterns() {
8094
8110
  const patterns = [];
8095
- for (const p of patterns_default.patterns) {
8111
+ const allPatterns = [...patterns_default.patterns, ...patterns_local_default.patterns];
8112
+ for (const p of allPatterns) {
8096
8113
  if (p.confidence !== "high") continue;
8097
8114
  if (DELIMITER_SUFFIX.test(p.regex)) continue;
8098
8115
  if (!HAS_VALUE_MATCHER.test(p.regex)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",