hillclimb 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +379 -217
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,22 +9,6 @@ import * as p6 from "@clack/prompts";
9
9
  import path7 from "path";
10
10
  import * as p3 from "@clack/prompts";
11
11
 
12
- // src/color.ts
13
- var IS_TTY = Boolean(process.stdout.isTTY);
14
- var NO_COLOR = process.env.FORCE_COLOR !== void 0 ? false : process.env.NO_COLOR !== void 0 || process.env.TERM === "dumb" || !IS_TTY;
15
- function wrap(open, close) {
16
- if (NO_COLOR) return (s) => s;
17
- const o = `\x1B[${open}m`;
18
- const c = `\x1B[${close}m`;
19
- return (s) => `${o}${s}${c}`;
20
- }
21
- var bold = wrap(1, 22);
22
- var dim = wrap(2, 22);
23
- var red = wrap(31, 39);
24
- var green = wrap(32, 39);
25
- var yellow = wrap(33, 39);
26
- var cyan = wrap(36, 39);
27
-
28
12
  // src/config.ts
29
13
  import fs from "fs";
30
14
  import os from "os";
@@ -369,18 +353,18 @@ var PlatformClient = class {
369
353
  let code;
370
354
  let message = `${method} ${relative} failed: HTTP ${res.status}`;
371
355
  try {
372
- const text3 = await res.text();
373
- if (text3) {
356
+ const text2 = await res.text();
357
+ if (text2) {
374
358
  try {
375
- const parsed = JSON.parse(text3);
359
+ const parsed = JSON.parse(text2);
376
360
  if (parsed.error?.code) code = parsed.error.code;
377
361
  if (parsed.error?.message) {
378
362
  message = `${method} ${relative} failed: ${parsed.error.message}`;
379
363
  } else {
380
- message = `${method} ${relative} failed: ${text3}`;
364
+ message = `${method} ${relative} failed: ${text2}`;
381
365
  }
382
366
  } catch {
383
- message = `${method} ${relative} failed: ${text3}`;
367
+ message = `${method} ${relative} failed: ${text2}`;
384
368
  }
385
369
  }
386
370
  } catch {
@@ -397,17 +381,6 @@ var PlatformClient = class {
397
381
  }
398
382
  return void 0;
399
383
  }
400
- async signInWithEmail(email, password2) {
401
- await this.request("POST", "/api/auth/sign-in/email", {
402
- email,
403
- password: password2
404
- });
405
- if (!this.sessionCookie) {
406
- throw new PlatformError(
407
- "Sign-in succeeded but no session cookie was returned"
408
- );
409
- }
410
- }
411
384
  async getBootstrap() {
412
385
  const data = await this.request("GET", "/api/v1/app/bootstrap");
413
386
  if (!data?.session?.user || !data.session.session) return null;
@@ -1483,22 +1456,6 @@ var CLAUDE_DEF = TOOLS[0];
1483
1456
 
1484
1457
  // src/commands/_shared.ts
1485
1458
  import * as p from "@clack/prompts";
1486
- async function requireText(label, opts = {}) {
1487
- const result = opts.password ? await p.password({
1488
- message: label,
1489
- validate: (v) => v && v.length > 0 ? void 0 : "Required"
1490
- }) : await p.text({
1491
- message: label,
1492
- placeholder: opts.placeholder,
1493
- initialValue: opts.initialValue,
1494
- validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
1495
- });
1496
- if (p.isCancel(result)) {
1497
- p.cancel("Cancelled.");
1498
- process.exit(0);
1499
- }
1500
- return typeof result === "string" ? result.trim() : "";
1501
- }
1502
1459
  async function withSpinner(startLabel, onSuccess, onFailureLabel, fn) {
1503
1460
  const spinner5 = p.spinner();
1504
1461
  spinner5.start(startLabel);
@@ -1517,6 +1474,24 @@ async function withSpinner(startLabel, onSuccess, onFailureLabel, fn) {
1517
1474
  // src/commands/web-auth.ts
1518
1475
  import { spawn } from "child_process";
1519
1476
  import * as p2 from "@clack/prompts";
1477
+
1478
+ // src/color.ts
1479
+ var IS_TTY = Boolean(process.stdout.isTTY);
1480
+ var NO_COLOR = process.env.FORCE_COLOR !== void 0 ? false : process.env.NO_COLOR !== void 0 || process.env.TERM === "dumb" || !IS_TTY;
1481
+ function wrap(open, close) {
1482
+ if (NO_COLOR) return (s) => s;
1483
+ const o = `\x1B[${open}m`;
1484
+ const c = `\x1B[${close}m`;
1485
+ return (s) => `${o}${s}${c}`;
1486
+ }
1487
+ var bold = wrap(1, 22);
1488
+ var dim = wrap(2, 22);
1489
+ var red = wrap(31, 39);
1490
+ var green = wrap(32, 39);
1491
+ var yellow = wrap(33, 39);
1492
+ var cyan = wrap(36, 39);
1493
+
1494
+ // src/commands/web-auth.ts
1520
1495
  function sleep(ms) {
1521
1496
  return new Promise((resolve) => setTimeout(resolve, ms));
1522
1497
  }
@@ -1531,10 +1506,6 @@ function openBrowser(url) {
1531
1506
  }
1532
1507
  }
1533
1508
  async function webAuth(apiBaseUrl) {
1534
- if (!IS_TTY) {
1535
- appendLog("info", "web-auth: skipped (not a TTY)");
1536
- return null;
1537
- }
1538
1509
  appendLog("info", `web-auth: started (apiBaseUrl=${apiBaseUrl})`);
1539
1510
  const client = new PlatformClient(apiBaseUrl);
1540
1511
  const codeResponse = await withSpinner(
@@ -1548,9 +1519,16 @@ async function webAuth(apiBaseUrl) {
1548
1519
  `web-auth: code prepared (expiresIn=${codeResponse.expiresIn}s, pollInterval=${codeResponse.interval}s)`
1549
1520
  );
1550
1521
  const url = codeResponse.verificationUrl;
1551
- openBrowser(url);
1552
- p2.log.info(`Opening browser to: ${cyan(url)}`);
1553
- p2.log.info(dim("If the browser did not open, copy the URL above."));
1522
+ if (IS_TTY) {
1523
+ openBrowser(url);
1524
+ p2.log.info(`Opening browser to: ${cyan(url)}`);
1525
+ p2.log.info(dim("If the browser did not open, copy the URL above."));
1526
+ } else {
1527
+ p2.log.info(
1528
+ "To sign in, open this URL in a browser and approve the request:"
1529
+ );
1530
+ p2.log.info(cyan(url));
1531
+ }
1554
1532
  const deadline = Date.now() + codeResponse.expiresIn * 1e3;
1555
1533
  const interval = codeResponse.interval * 1e3;
1556
1534
  const spinner5 = p2.spinner();
@@ -1674,66 +1652,24 @@ async function runInit(args = []) {
1674
1652
  bootstrap = await tryReuseIdentity(apiBaseUrl, client, saved);
1675
1653
  }
1676
1654
  if (!bootstrap) {
1677
- let method = projectSlugFlag ? "web" : "email";
1678
- if (IS_TTY && !projectSlugFlag) {
1679
- const selected = await p3.select({
1680
- message: "How would you like to sign in?",
1681
- options: [
1682
- {
1683
- value: "web",
1684
- label: "Web authentication (recommended)",
1685
- hint: "opens browser"
1686
- },
1687
- { value: "email", label: "Email and password" }
1688
- ],
1689
- initialValue: "web"
1690
- });
1691
- if (p3.isCancel(selected)) {
1692
- p3.cancel("Cancelled.");
1693
- process.exit(0);
1694
- }
1695
- method = selected;
1655
+ const result = await webAuth(apiBaseUrl);
1656
+ if (!result) {
1657
+ appendLog("error", "init: web authentication failed");
1658
+ p3.log.error("Web authentication failed.");
1659
+ p3.outro("Aborted.");
1660
+ process.exit(1);
1696
1661
  }
1697
- appendLog("info", `init: auth method selected (${method})`);
1698
- if (method === "web") {
1699
- const result = await webAuth(apiBaseUrl);
1700
- if (!result) {
1701
- appendLog("error", "init: web authentication failed");
1702
- p3.log.error("Web authentication failed.");
1703
- p3.outro("Aborted.");
1704
- process.exit(1);
1662
+ client = result.client;
1663
+ bootstrap = await withSpinner(
1664
+ "Loading account...",
1665
+ (b) => `Signed in as ${b.session.email}${b.session.role === "admin" ? " (admin)" : ""}.`,
1666
+ "Could not load account.",
1667
+ async () => {
1668
+ const data = await client.getBootstrap();
1669
+ if (!data) throw new Error("Bootstrap returned no session.");
1670
+ return data;
1705
1671
  }
1706
- client = result.client;
1707
- bootstrap = await withSpinner(
1708
- "Loading account...",
1709
- (b) => `Signed in as ${b.session.email}${b.session.role === "admin" ? " (admin)" : ""}.`,
1710
- "Could not load account.",
1711
- async () => {
1712
- const data = await client.getBootstrap();
1713
- if (!data) throw new Error("Bootstrap returned no session.");
1714
- return data;
1715
- }
1716
- );
1717
- } else {
1718
- const email = await requireText("Email");
1719
- const password2 = await requireText("Password", { password: true });
1720
- await withSpinner(
1721
- "Signing in...",
1722
- () => "Signed in.",
1723
- "Sign-in failed.",
1724
- () => client.signInWithEmail(email, password2)
1725
- );
1726
- bootstrap = await withSpinner(
1727
- "Loading account...",
1728
- (b) => `Signed in as ${b.session.email}${b.session.role === "admin" ? " (admin)" : ""}.`,
1729
- "Could not load account.",
1730
- async () => {
1731
- const data = await client.getBootstrap();
1732
- if (!data) throw new Error("Bootstrap returned no session.");
1733
- return data;
1734
- }
1735
- );
1736
- }
1672
+ );
1737
1673
  }
1738
1674
  let project;
1739
1675
  if (projectSlugFlag) {
@@ -2070,87 +2006,23 @@ async function runLogin(_args = []) {
2070
2006
  println();
2071
2007
  }
2072
2008
  }
2073
- let method = "email";
2074
- if (IS_TTY) {
2075
- const selected = await p4.select({
2076
- message: "How would you like to sign in?",
2077
- options: [
2078
- {
2079
- value: "web",
2080
- label: "Web authentication (recommended)",
2081
- hint: "opens browser"
2082
- },
2083
- { value: "email", label: "Email and password" }
2084
- ],
2085
- initialValue: "web"
2086
- });
2087
- if (p4.isCancel(selected)) {
2088
- p4.cancel("Cancelled.");
2089
- process.exit(0);
2090
- }
2091
- method = selected;
2092
- }
2093
- appendLog("info", `login: auth method selected (${method})`);
2094
- if (method === "web") {
2095
- const result = await webAuth(apiBaseUrl);
2096
- if (!result) {
2097
- appendLog("error", "login: web authentication failed");
2098
- p4.log.error("Web authentication failed.");
2099
- p4.outro("Aborted.");
2100
- process.exit(1);
2101
- }
2102
- await saveIdentity({
2103
- apiBaseUrl,
2104
- sessionCookie: result.sessionCookie,
2105
- email: result.email,
2106
- userId: result.userId,
2107
- savedAt: (/* @__PURE__ */ new Date()).toISOString()
2108
- });
2109
- appendLog("info", `login: completed via web auth (email=${result.email})`);
2110
- println();
2111
- row(CHECK, "Signed in", cyan(result.email));
2112
- footer("Run `npx hillclimb` in a repo to wire up auto-upload.");
2113
- return;
2114
- }
2115
- const email = await requireText("Email");
2116
- const password2 = await requireText("Password", { password: true });
2117
- const client = new PlatformClient(apiBaseUrl);
2118
- await withSpinner(
2119
- "Signing in...",
2120
- () => "Signed in.",
2121
- "Sign-in failed.",
2122
- () => client.signInWithEmail(email, password2)
2123
- );
2124
- const bootstrap = await withSpinner(
2125
- "Loading account...",
2126
- (b) => `Signed in as ${b.session.email}.`,
2127
- "Could not load account.",
2128
- async () => {
2129
- const data = await client.getBootstrap();
2130
- if (!data) throw new Error("Bootstrap returned no session.");
2131
- return data;
2132
- }
2133
- );
2134
- const cookie = client.getSessionCookie();
2135
- if (!cookie) {
2136
- appendLog("error", "login: session cookie missing after sign-in");
2137
- p4.log.error("Session cookie was lost during sign-in.");
2009
+ const result = await webAuth(apiBaseUrl);
2010
+ if (!result) {
2011
+ appendLog("error", "login: web authentication failed");
2012
+ p4.log.error("Web authentication failed.");
2138
2013
  p4.outro("Aborted.");
2139
2014
  process.exit(1);
2140
2015
  }
2141
2016
  await saveIdentity({
2142
2017
  apiBaseUrl,
2143
- sessionCookie: cookie,
2144
- email: bootstrap.session.email,
2145
- userId: bootstrap.session.userId,
2018
+ sessionCookie: result.sessionCookie,
2019
+ email: result.email,
2020
+ userId: result.userId,
2146
2021
  savedAt: (/* @__PURE__ */ new Date()).toISOString()
2147
2022
  });
2148
- appendLog(
2149
- "info",
2150
- `login: completed via email/password (email=${bootstrap.session.email})`
2151
- );
2023
+ appendLog("info", `login: completed via web auth (email=${result.email})`);
2152
2024
  println();
2153
- row(CHECK, "Signed in", cyan(bootstrap.session.email));
2025
+ row(CHECK, "Signed in", cyan(result.email));
2154
2026
  footer("Run `npx hillclimb` in a repo to wire up auto-upload.");
2155
2027
  }
2156
2028
 
@@ -2419,6 +2291,48 @@ async function readFileContent(file) {
2419
2291
  }
2420
2292
  }
2421
2293
 
2294
+ // src/middleware/large-file.ts
2295
+ import { constants as bufferConstants } from "buffer";
2296
+ var MAX_STRINGIFIABLE_BYTES = bufferConstants.MAX_STRING_LENGTH;
2297
+ var largeFileThreshold = MAX_STRINGIFIABLE_BYTES;
2298
+ function getLargeFileThreshold() {
2299
+ return largeFileThreshold;
2300
+ }
2301
+ var NEWLINE = 10;
2302
+ var OVERSIZED_LINE_PLACEHOLDER = '{"_hillclimb_omitted":"line exceeded max string length; dropped to allow redaction"}';
2303
+ function redactBufferByLine(buffer, redactLine, options = {}) {
2304
+ const maxLineBytes = options.maxLineBytes ?? MAX_STRINGIFIABLE_BYTES;
2305
+ const oversizedPlaceholder = options.oversizedPlaceholder ?? OVERSIZED_LINE_PLACEHOLDER;
2306
+ const pieces = [];
2307
+ const newlineBuf = Buffer.from([NEWLINE]);
2308
+ let totalCount = 0;
2309
+ let oversizedLines = 0;
2310
+ let start = 0;
2311
+ let first = true;
2312
+ for (; ; ) {
2313
+ const nl = buffer.indexOf(NEWLINE, start);
2314
+ const end = nl === -1 ? buffer.length : nl;
2315
+ const slice = buffer.subarray(start, end);
2316
+ if (!first) pieces.push(newlineBuf);
2317
+ first = false;
2318
+ if (slice.length > maxLineBytes) {
2319
+ oversizedLines++;
2320
+ pieces.push(Buffer.from(oversizedPlaceholder, "utf-8"));
2321
+ } else {
2322
+ const { value, count } = redactLine(slice.toString("utf-8"));
2323
+ totalCount += count;
2324
+ pieces.push(Buffer.from(value, "utf-8"));
2325
+ }
2326
+ if (nl === -1) break;
2327
+ start = nl + 1;
2328
+ }
2329
+ return {
2330
+ content: Buffer.concat(pieces),
2331
+ count: totalCount,
2332
+ oversizedLines
2333
+ };
2334
+ }
2335
+
2422
2336
  // src/middleware/patterns.json
2423
2337
  var patterns_default = {
2424
2338
  patterns: [
@@ -10658,6 +10572,19 @@ function processFiles(files) {
10658
10572
  }
10659
10573
 
10660
10574
  // src/middleware/pattern-redact.ts
10575
+ function redactPatternLine(line, patterns, memo, isJsonl) {
10576
+ if (isJsonl) {
10577
+ if (!line.trim()) return { value: line, count: 0 };
10578
+ try {
10579
+ const parsed = JSON.parse(line);
10580
+ const walked = walkAndRedactAll(parsed, patterns, memo);
10581
+ return { value: JSON.stringify(walked.value), count: walked.count };
10582
+ } catch {
10583
+ return redactString(line, patterns);
10584
+ }
10585
+ }
10586
+ return redactString(line, patterns);
10587
+ }
10661
10588
  var WORKER_COUNT = Math.min(
10662
10589
  os3.availableParallelism?.() ?? os3.cpus().length,
10663
10590
  4
@@ -10686,9 +10613,14 @@ var PatternRedactMiddleware = class {
10686
10613
  const tasks = [];
10687
10614
  const fileMap = /* @__PURE__ */ new Map();
10688
10615
  const passThrough = [];
10616
+ const largeResults = /* @__PURE__ */ new Map();
10689
10617
  for (let i = 0; i < group.files.length; i++) {
10690
10618
  const file = group.files[i];
10691
10619
  this.stats.filesScanned++;
10620
+ if (file.content && !file.isBinary && file.content.length > getLargeFileThreshold()) {
10621
+ largeResults.set(i, this.redactLargeFile(file));
10622
+ continue;
10623
+ }
10692
10624
  const read = await readFileContent(file);
10693
10625
  if (read.kind === "binary") {
10694
10626
  this.stats.binaryFiles++;
@@ -10718,6 +10650,11 @@ var PatternRedactMiddleware = class {
10718
10650
  }
10719
10651
  const newFiles = [];
10720
10652
  for (let i = 0; i < group.files.length; i++) {
10653
+ const large = largeResults.get(i);
10654
+ if (large) {
10655
+ newFiles.push(large);
10656
+ continue;
10657
+ }
10721
10658
  const pt = passThrough.find((p7) => p7.index === i);
10722
10659
  if (pt) {
10723
10660
  newFiles.push(pt.file);
@@ -10739,6 +10676,33 @@ var PatternRedactMiddleware = class {
10739
10676
  }
10740
10677
  return { ...group, files: newFiles };
10741
10678
  }
10679
+ // Stream-redact an oversized file straight from its buffer, one line at a time,
10680
+ // so we never build a string larger than a single line. Single-threaded (no
10681
+ // worker pool) — it trades parallelism for bounded memory on the one giant file.
10682
+ redactLargeFile(file) {
10683
+ const buffer = file.content;
10684
+ const isJsonl = file.absolutePath.endsWith(".jsonl");
10685
+ const patterns = compilePatterns();
10686
+ const memo = /* @__PURE__ */ new Map();
10687
+ const { content, count, oversizedLines } = redactBufferByLine(
10688
+ buffer,
10689
+ (line) => redactPatternLine(line, patterns, memo, isJsonl)
10690
+ );
10691
+ if (oversizedLines > 0) {
10692
+ appendLog(
10693
+ "warn",
10694
+ `pattern-redact: dropped ${oversizedLines} oversized line(s) in ${file.absolutePath} (exceeded max string length)`
10695
+ );
10696
+ }
10697
+ if (count > 0) {
10698
+ this.stats.filesRedacted++;
10699
+ this.stats.totalRedactions += count;
10700
+ }
10701
+ if (count > 0 || oversizedLines > 0) {
10702
+ return { ...file, content };
10703
+ }
10704
+ return file;
10705
+ }
10742
10706
  async processWithWorkers(tasks) {
10743
10707
  const workerPath = resolveWorkerPath();
10744
10708
  const sorted = [...tasks].sort(
@@ -10814,6 +10778,10 @@ var RedactMiddleware = class {
10814
10778
  for (const file of group.files) {
10815
10779
  this.stats.filesScanned++;
10816
10780
  regex.lastIndex = 0;
10781
+ if (file.content && !file.isBinary && file.content.length > getLargeFileThreshold()) {
10782
+ newFiles.push(this.redactLargeFile(file, regex));
10783
+ continue;
10784
+ }
10817
10785
  const read = await readFileContent(file);
10818
10786
  if (read.kind === "binary") {
10819
10787
  this.stats.binaryFiles++;
@@ -10849,6 +10817,47 @@ var RedactMiddleware = class {
10849
10817
  }
10850
10818
  return { ...group, files: newFiles };
10851
10819
  }
10820
+ // Stream-redact an oversized file straight from its buffer. Each line is
10821
+ // redacted with the same JSON-aware (or raw) logic as redactJsonl, so the
10822
+ // output matches the in-memory path exactly for files whose lines all fit in a
10823
+ // string — which holds for real transcripts (largest line is a few MiB).
10824
+ redactLargeFile(file, regex) {
10825
+ const buffer = file.content;
10826
+ const isJsonl = file.absolutePath.endsWith(".jsonl");
10827
+ const { content, count, oversizedLines } = redactBufferByLine(
10828
+ buffer,
10829
+ (line) => this.redactLine(line, regex, isJsonl)
10830
+ );
10831
+ if (oversizedLines > 0) {
10832
+ appendLog(
10833
+ "warn",
10834
+ `redact: dropped ${oversizedLines} oversized line(s) in ${file.absolutePath} (exceeded max string length)`
10835
+ );
10836
+ }
10837
+ if (count > 0) {
10838
+ this.stats.filesRedacted++;
10839
+ this.stats.totalRedactions += count;
10840
+ }
10841
+ if (count > 0 || oversizedLines > 0) {
10842
+ return { ...file, content };
10843
+ }
10844
+ return file;
10845
+ }
10846
+ redactLine(line, regex, isJsonl) {
10847
+ if (isJsonl) {
10848
+ if (!line.trim()) return { value: line, count: 0 };
10849
+ try {
10850
+ const parsed = JSON.parse(line);
10851
+ const walked = walkAndRedact(parsed, regex);
10852
+ return { value: JSON.stringify(walked.value), count: walked.count };
10853
+ } catch {
10854
+ const { result: result2, count: count2 } = this.redactString(line, regex);
10855
+ return { value: result2, count: count2 };
10856
+ }
10857
+ }
10858
+ const { result, count } = this.redactString(line, regex);
10859
+ return { value: result, count };
10860
+ }
10852
10861
  redactJsonl(content, regex) {
10853
10862
  let totalCount = 0;
10854
10863
  const lines = content.split("\n");
@@ -11708,9 +11717,9 @@ function extractTextReasoningToolUses(content) {
11708
11717
  } else if (content !== void 0 && content !== null) {
11709
11718
  textParts.push(stringify(content));
11710
11719
  }
11711
- const text3 = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
11720
+ const text2 = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
11712
11721
  const reasoning = reasoningParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
11713
- return [text3, reasoning || void 0, toolBlocks];
11722
+ return [text2, reasoning || void 0, toolBlocks];
11714
11723
  }
11715
11724
  function buildMetrics(usage) {
11716
11725
  if (typeof usage !== "object" || usage === null) return void 0;
@@ -11739,8 +11748,8 @@ function formatToolResult(block, toolUseResult) {
11739
11748
  if (content.trim()) parts.push(content.trim());
11740
11749
  } else if (Array.isArray(content)) {
11741
11750
  for (const item of content) {
11742
- const text3 = stringify(item);
11743
- if (text3.trim()) parts.push(text3.trim());
11751
+ const text2 = stringify(item);
11752
+ if (text2.trim()) parts.push(text2.trim());
11744
11753
  }
11745
11754
  } else if (content !== void 0 && content !== null && content !== "") {
11746
11755
  parts.push(stringify(content));
@@ -11868,7 +11877,7 @@ function convertClaudeToTrajectory(jsonlContent, sessionId) {
11868
11877
  const eventType = event.type;
11869
11878
  const timestamp = event.timestamp;
11870
11879
  if (eventType === "assistant") {
11871
- const [text3, reasoning, toolBlocks] = extractTextReasoningToolUses(
11880
+ const [text2, reasoning, toolBlocks] = extractTextReasoningToolUses(
11872
11881
  msg.content
11873
11882
  );
11874
11883
  const msgId = msg.id;
@@ -11892,12 +11901,12 @@ function convertClaudeToTrajectory(jsonlContent, sessionId) {
11892
11901
  extra.user_type = event.userType;
11893
11902
  extra.is_sidechain = event.isSidechain ?? false;
11894
11903
  const modelName = msg.model || defaultModelName;
11895
- if (text3 || reasoning || toolBlocks.length === 0) {
11904
+ if (text2 || reasoning || toolBlocks.length === 0) {
11896
11905
  normalizedEvents.push({
11897
11906
  kind: "message",
11898
11907
  timestamp,
11899
11908
  role: msg.role ?? "assistant",
11900
- text: text3 || "",
11909
+ text: text2 || "",
11901
11910
  reasoning: msg.role === "assistant" ? reasoning : void 0,
11902
11911
  metrics,
11903
11912
  extra: Object.keys(extra).length > 0 ? extra : void 0,
@@ -11937,13 +11946,13 @@ function convertClaudeToTrajectory(jsonlContent, sessionId) {
11937
11946
  if (eventType === "user") {
11938
11947
  const content = msg.content;
11939
11948
  if (typeof content === "string") {
11940
- const text3 = content.trim();
11941
- if (text3) {
11949
+ const text2 = content.trim();
11950
+ if (text2) {
11942
11951
  normalizedEvents.push({
11943
11952
  kind: "message",
11944
11953
  timestamp,
11945
11954
  role: "user",
11946
- text: text3,
11955
+ text: text2,
11947
11956
  extra: { is_sidechain: event.isSidechain ?? false }
11948
11957
  });
11949
11958
  }
@@ -12005,13 +12014,13 @@ function convertClaudeToTrajectory(jsonlContent, sessionId) {
12005
12014
  continue;
12006
12015
  }
12007
12016
  if (content !== void 0 && content !== null && content !== "") {
12008
- const text3 = stringify(content).trim();
12009
- if (text3) {
12017
+ const text2 = stringify(content).trim();
12018
+ if (text2) {
12010
12019
  normalizedEvents.push({
12011
12020
  kind: "message",
12012
12021
  timestamp,
12013
12022
  role: "user",
12014
- text: text3
12023
+ text: text2
12015
12024
  });
12016
12025
  }
12017
12026
  }
@@ -12147,8 +12156,8 @@ function extractMessageText(content) {
12147
12156
  const parts = [];
12148
12157
  for (const block of content) {
12149
12158
  if (typeof block === "object" && block !== null) {
12150
- const text3 = block.text;
12151
- if (typeof text3 === "string") parts.push(text3);
12159
+ const text2 = block.text;
12160
+ if (typeof text2 === "string") parts.push(text2);
12152
12161
  }
12153
12162
  }
12154
12163
  return parts.join("");
@@ -12280,12 +12289,12 @@ function convertCodexToTrajectory(jsonlContent, sessionId) {
12280
12289
  }
12281
12290
  if (payloadType === "message") {
12282
12291
  const content = payload.content;
12283
- const text3 = Array.isArray(content) ? extractMessageText(content) : "";
12292
+ const text2 = Array.isArray(content) ? extractMessageText(content) : "";
12284
12293
  normalizedEvents.push({
12285
12294
  kind: "message",
12286
12295
  timestamp,
12287
12296
  role: payload.role ?? "user",
12288
- text: text3,
12297
+ text: text2,
12289
12298
  reasoning: payload.role === "assistant" ? pendingReasoning : void 0
12290
12299
  });
12291
12300
  pendingReasoning = void 0;
@@ -12791,7 +12800,7 @@ function convertCursorToTrajectory(jsonlContent, sessionId) {
12791
12800
  else if (role === "user") source = "user";
12792
12801
  else source = "system";
12793
12802
  const contentParts = entry.message?.content;
12794
- let text3 = "";
12803
+ let text2 = "";
12795
12804
  if (Array.isArray(contentParts)) {
12796
12805
  const textParts = [];
12797
12806
  for (const part of contentParts) {
@@ -12799,13 +12808,13 @@ function convertCursorToTrajectory(jsonlContent, sessionId) {
12799
12808
  textParts.push(part.text);
12800
12809
  }
12801
12810
  }
12802
- text3 = textParts.join("\n\n").trim();
12811
+ text2 = textParts.join("\n\n").trim();
12803
12812
  }
12804
- if (!text3) continue;
12813
+ if (!text2) continue;
12805
12814
  const step = {
12806
12815
  step_id: stepId,
12807
12816
  source,
12808
- message: text3
12817
+ message: text2
12809
12818
  };
12810
12819
  steps.push(step);
12811
12820
  stepId++;
@@ -13351,10 +13360,10 @@ var NormalizeMiddleware = class {
13351
13360
  file.sourceName
13352
13361
  ))
13353
13362
  continue;
13354
- const content = file.content ? file.content.toString("utf-8") : null;
13355
- if (!content) continue;
13356
13363
  const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path13.basename(file.absolutePath, ".jsonl"));
13357
13364
  try {
13365
+ const content = file.content ? file.content.toString("utf-8") : null;
13366
+ if (!content) continue;
13358
13367
  const trajectory = normalizeContent(
13359
13368
  file.sourceName,
13360
13369
  content,
@@ -13383,7 +13392,7 @@ var NormalizeMiddleware = class {
13383
13392
  } catch (err) {
13384
13393
  appendLog(
13385
13394
  "warn",
13386
- `normalize: failed for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
13395
+ `normalize: skipped ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
13387
13396
  );
13388
13397
  }
13389
13398
  }
@@ -14085,7 +14094,7 @@ import { execFileSync as execFileSync3 } from "child_process";
14085
14094
  import path18 from "path";
14086
14095
 
14087
14096
  // src/git-traces/git-ops.ts
14088
- import { execFileSync as execFileSync2 } from "child_process";
14097
+ import { execFileSync as execFileSync2, spawnSync } from "child_process";
14089
14098
  import fs12 from "fs";
14090
14099
  import os7 from "os";
14091
14100
  import path16 from "path";
@@ -14097,11 +14106,155 @@ var EXEC_OPTS = {
14097
14106
  };
14098
14107
  var MAX_ERROR_OUTPUT_CHARS = 2e3;
14099
14108
  var MAX_SNAPSHOT_FILE_BYTES = 10 * 1024 * 1024;
14100
- var EXCLUDED_SNAPSHOT_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
14109
+ var MAX_BINARY_SNAPSHOT_FILE_BYTES = 1024 * 1024;
14110
+ var BINARY_SNIFF_BYTES = 8e3;
14111
+ var EXCLUDED_SNAPSHOT_EXTENSIONS = /* @__PURE__ */ new Set([
14112
+ // Documents
14113
+ ".pdf",
14114
+ // Raster images
14115
+ ".png",
14116
+ ".jpg",
14117
+ ".jpeg",
14118
+ ".gif",
14119
+ ".bmp",
14120
+ ".tiff",
14121
+ ".tif",
14122
+ ".webp",
14123
+ ".ico",
14124
+ ".heic",
14125
+ ".heif",
14126
+ ".avif",
14127
+ // Video
14128
+ ".mp4",
14129
+ ".mov",
14130
+ ".avi",
14131
+ ".mkv",
14132
+ ".webm",
14133
+ ".m4v",
14134
+ ".mpg",
14135
+ ".mpeg",
14136
+ ".wmv",
14137
+ ".flv",
14138
+ // Audio
14139
+ ".mp3",
14140
+ ".wav",
14141
+ ".flac",
14142
+ ".ogg",
14143
+ ".m4a",
14144
+ ".aac",
14145
+ ".aiff",
14146
+ // Archives / compressed
14147
+ ".zip",
14148
+ ".tar",
14149
+ ".gz",
14150
+ ".tgz",
14151
+ ".bz2",
14152
+ ".tbz2",
14153
+ ".xz",
14154
+ ".7z",
14155
+ ".rar",
14156
+ ".zst",
14157
+ ".lz4",
14158
+ // Fonts
14159
+ ".woff",
14160
+ ".woff2",
14161
+ ".ttf",
14162
+ ".otf",
14163
+ ".eot",
14164
+ // Compiled / native artifacts
14165
+ ".so",
14166
+ ".dylib",
14167
+ ".dll",
14168
+ ".a",
14169
+ ".o",
14170
+ ".lib",
14171
+ ".class",
14172
+ ".jar",
14173
+ ".wasm",
14174
+ ".exe",
14175
+ ".pyc",
14176
+ ".pyd",
14177
+ ".node",
14178
+ // Databases
14179
+ ".sqlite",
14180
+ ".sqlite3",
14181
+ ".db",
14182
+ ".mdb",
14183
+ // Office / design binaries
14184
+ ".doc",
14185
+ ".docx",
14186
+ ".xls",
14187
+ ".xlsx",
14188
+ ".ppt",
14189
+ ".pptx",
14190
+ ".odt",
14191
+ ".ods",
14192
+ ".odp",
14193
+ ".psd",
14194
+ ".ai",
14195
+ ".sketch",
14196
+ ".fig",
14197
+ ".blend"
14198
+ ]);
14199
+ var EXCLUDED_SNAPSHOT_BASENAMES = /* @__PURE__ */ new Set([
14200
+ ".DS_Store",
14201
+ "Thumbs.db",
14202
+ "desktop.ini"
14203
+ ]);
14101
14204
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
14102
- function hasExcludedSnapshotExtension(filePath) {
14205
+ function isExcludedSnapshotPath(filePath) {
14206
+ if (EXCLUDED_SNAPSHOT_BASENAMES.has(path16.basename(filePath))) return true;
14103
14207
  return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path16.extname(filePath).toLowerCase());
14104
14208
  }
14209
+ function isBinaryBuffer(buffer) {
14210
+ return buffer.includes(0);
14211
+ }
14212
+ function readTreeBlobHead(repoRoot, sha) {
14213
+ try {
14214
+ const { stdout } = spawnSync("git", ["cat-file", "blob", sha], {
14215
+ cwd: repoRoot,
14216
+ timeout: GIT_COMMAND_TIMEOUT_MS,
14217
+ maxBuffer: BINARY_SNIFF_BYTES
14218
+ });
14219
+ return stdout?.length ? stdout.subarray(0, BINARY_SNIFF_BYTES) : null;
14220
+ } catch {
14221
+ return null;
14222
+ }
14223
+ }
14224
+ function readWorkingFileHead(absPath) {
14225
+ let fd = null;
14226
+ try {
14227
+ fd = fs12.openSync(absPath, "r");
14228
+ const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
14229
+ const bytesRead = fs12.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
14230
+ return buffer.subarray(0, bytesRead);
14231
+ } catch {
14232
+ return null;
14233
+ } finally {
14234
+ if (fd !== null) {
14235
+ try {
14236
+ fs12.closeSync(fd);
14237
+ } catch {
14238
+ }
14239
+ }
14240
+ }
14241
+ }
14242
+ function classifyOmission(filePath, sizeBytes, readHead) {
14243
+ if (isExcludedSnapshotPath(filePath)) return "excluded-extension";
14244
+ if (sizeBytes > MAX_SNAPSHOT_FILE_BYTES) return "file-over-limit";
14245
+ if (sizeBytes > MAX_BINARY_SNAPSHOT_FILE_BYTES) {
14246
+ const head = readHead();
14247
+ if (!head) {
14248
+ appendLog(
14249
+ "warn",
14250
+ `git-traces: binary sniff failed for ${filePath} (${sizeBytes} bytes); keeping it in the snapshot`
14251
+ );
14252
+ return null;
14253
+ }
14254
+ if (isBinaryBuffer(head)) return "binary-over-limit";
14255
+ }
14256
+ return null;
14257
+ }
14105
14258
  function quoteGitArg(arg) {
14106
14259
  if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
14107
14260
  return JSON.stringify(arg);
@@ -14205,7 +14358,7 @@ function recordOmittedSnapshotFile(omittedFiles, file, options = {}) {
14205
14358
  omittedFiles?.push(file);
14206
14359
  if (options.log === false) return;
14207
14360
  const action = file.tracked ? "omitting tracked" : "skipping untracked";
14208
- const reason = file.reason === "file-over-limit" ? `${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit` : "excluded extension";
14361
+ 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";
14209
14362
  appendLog("warn", `git-traces: ${action} ${file.path} (${reason})`);
14210
14363
  }
14211
14364
  function parseLsTreeLongZ(output) {
@@ -14228,7 +14381,11 @@ function listOmittedTreeFiles(repoRoot, treeSha, options = {}) {
14228
14381
  const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
14229
14382
  const omitted = [];
14230
14383
  for (const entry of parseLsTreeLongZ(output)) {
14231
- const reason = hasExcludedSnapshotExtension(entry.path) ? "excluded-extension" : entry.sizeBytes > MAX_SNAPSHOT_FILE_BYTES ? "file-over-limit" : null;
14384
+ const reason = classifyOmission(
14385
+ entry.path,
14386
+ entry.sizeBytes,
14387
+ () => readTreeBlobHead(repoRoot, entry.sha)
14388
+ );
14232
14389
  if (!reason) continue;
14233
14390
  const file = {
14234
14391
  path: entry.path,
@@ -14286,8 +14443,13 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
14286
14443
  for (const relPath of list.split("\0")) {
14287
14444
  if (!relPath) continue;
14288
14445
  try {
14289
- const stat = fs12.lstatSync(path16.join(repoRoot, relPath));
14290
- const reason = hasExcludedSnapshotExtension(relPath) ? "excluded-extension" : stat.size > MAX_SNAPSHOT_FILE_BYTES ? "file-over-limit" : null;
14446
+ const absPath = path16.join(repoRoot, relPath);
14447
+ const stat = fs12.lstatSync(absPath);
14448
+ const reason = classifyOmission(
14449
+ relPath,
14450
+ stat.size,
14451
+ () => readWorkingFileHead(absPath)
14452
+ );
14291
14453
  if (reason) {
14292
14454
  recordOmittedSnapshotFile(omittedFiles, {
14293
14455
  path: relPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",