hillclimb 0.5.1 → 0.5.2

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 +132 -33
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2319,6 +2319,7 @@ import crypto3 from "crypto";
2319
2319
  import fs11 from "fs";
2320
2320
  import os6 from "os";
2321
2321
  import path15 from "path";
2322
+ import readline from "readline";
2322
2323
 
2323
2324
  // src/debug-logs.ts
2324
2325
  import crypto from "crypto";
@@ -13384,8 +13385,8 @@ var DEFAULT_STATE_DIR = path14.join(
13384
13385
  ".hillclimb",
13385
13386
  "agent-uploads"
13386
13387
  );
13387
- var LOCK_RETRIES2 = 120;
13388
- var LOCK_RETRY_DELAY_MS2 = 500;
13388
+ var DEFAULT_LOCK_WAIT_MS = 5 * 60 * 1e3;
13389
+ var DEFAULT_LOCK_RETRY_DELAY_MS = 500;
13389
13390
  var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
13390
13391
  function stateDir2() {
13391
13392
  return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
@@ -13402,6 +13403,23 @@ function stateTtlMs() {
13402
13403
  DEFAULT_STATE_TTL_MS
13403
13404
  );
13404
13405
  }
13406
+ function lockRetryDelayMs() {
13407
+ return Math.max(
13408
+ 1,
13409
+ readPositiveEnvMs(
13410
+ "HILLCLIMB_UPLOAD_LOCK_RETRY_DELAY_MS",
13411
+ DEFAULT_LOCK_RETRY_DELAY_MS
13412
+ )
13413
+ );
13414
+ }
13415
+ function lockRetries() {
13416
+ return Math.max(
13417
+ 1,
13418
+ Math.ceil(
13419
+ readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS", DEFAULT_LOCK_WAIT_MS) / lockRetryDelayMs()
13420
+ )
13421
+ );
13422
+ }
13405
13423
  function stateFileFor(repoRoot, tool, sessionId) {
13406
13424
  const hash = crypto2.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
13407
13425
  return path14.join(stateDir2(), `${hash}.json`);
@@ -13437,7 +13455,7 @@ async function deleteUploadState(repoRoot, tool, sessionId) {
13437
13455
  } catch {
13438
13456
  }
13439
13457
  }
13440
- async function acquireLock2(repoRoot, tool, sessionId, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
13458
+ async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(), delayMs = lockRetryDelayMs()) {
13441
13459
  const lockPath = lockFileFor(repoRoot, tool, sessionId);
13442
13460
  await fs10.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13443
13461
  for (let i = 0; i < retries; i++) {
@@ -13457,10 +13475,13 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = LOCK_RETRIES2,
13457
13475
  await new Promise((r) => setTimeout(r, delayMs));
13458
13476
  continue;
13459
13477
  }
13478
+ if (err.code === "EEXIST") break;
13460
13479
  throw err;
13461
13480
  }
13462
13481
  }
13463
- throw new Error(`Failed to acquire upload lock after ${retries} retries`);
13482
+ throw new Error(
13483
+ `Failed to acquire upload lock for ${tool} session ${sessionId} after ${retries} retries (${delayMs}ms delay, lock=${lockPath})`
13484
+ );
13464
13485
  }
13465
13486
  async function releaseLock2(repoRoot, tool, sessionId) {
13466
13487
  try {
@@ -13588,6 +13609,54 @@ function summarizePayload(payload) {
13588
13609
  cursor_version_present: !!payload.cursor_version
13589
13610
  });
13590
13611
  }
13612
+ async function codexSessionIdFromFile(filePath) {
13613
+ const stream = fs11.createReadStream(filePath, { encoding: "utf-8" });
13614
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
13615
+ try {
13616
+ for await (const line of rl) {
13617
+ if (!line.trim()) continue;
13618
+ try {
13619
+ const obj = JSON.parse(line);
13620
+ if (obj.type === "session_meta") return obj.payload?.id ?? null;
13621
+ } catch {
13622
+ return null;
13623
+ }
13624
+ return null;
13625
+ }
13626
+ } finally {
13627
+ rl.close();
13628
+ stream.destroy();
13629
+ }
13630
+ return null;
13631
+ }
13632
+ async function findCodexTranscriptPath(sessionId) {
13633
+ const sessionsDir = process.env.HILLCLIMB_CODEX_SESSIONS_DIR ?? path15.join(os6.homedir(), ".codex", "sessions");
13634
+ const candidates = [];
13635
+ async function walk(dir) {
13636
+ let entries;
13637
+ try {
13638
+ entries = await fs11.promises.readdir(dir, { withFileTypes: true });
13639
+ } catch {
13640
+ return;
13641
+ }
13642
+ for (const entry of entries) {
13643
+ const full = path15.join(dir, entry.name);
13644
+ if (entry.isDirectory()) {
13645
+ await walk(full);
13646
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
13647
+ candidates.push(full);
13648
+ }
13649
+ }
13650
+ }
13651
+ await walk(sessionsDir);
13652
+ candidates.sort();
13653
+ for (const filePath of candidates) {
13654
+ if (await codexSessionIdFromFile(filePath) === sessionId) {
13655
+ return filePath;
13656
+ }
13657
+ }
13658
+ return void 0;
13659
+ }
13591
13660
  async function selfHealHook(repoRoot, tool) {
13592
13661
  if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
13593
13662
  try {
@@ -13621,17 +13690,31 @@ function resolveCursorTranscriptPath(payload) {
13621
13690
  `${id}.jsonl`
13622
13691
  );
13623
13692
  }
13693
+ async function resolveTranscriptPath(payload, sourceTool, sessionId) {
13694
+ if (payload.transcript_path) return payload.transcript_path;
13695
+ if (sourceTool === "cursor") return resolveCursorTranscriptPath(payload);
13696
+ if (sourceTool === "codex") {
13697
+ const resolved = await findCodexTranscriptPath(sessionId);
13698
+ if (resolved) {
13699
+ appendLog(
13700
+ "info",
13701
+ `[${sessionId}] resolved missing Codex transcript_path via ~/.codex/sessions: ${resolved}`
13702
+ );
13703
+ }
13704
+ return resolved;
13705
+ }
13706
+ return void 0;
13707
+ }
13624
13708
  async function runUploadInner(payload) {
13625
13709
  const sessionId = resolveHookSessionId(payload);
13626
- const transcriptPath = payload.transcript_path ?? resolveCursorTranscriptPath(payload);
13627
13710
  const cwd = resolveHookCwd(payload);
13628
13711
  const eventKind = classifyHookEvent(payload.hook_event_name);
13629
- if (!sessionId || !transcriptPath || !cwd) {
13712
+ if (!sessionId || !cwd) {
13630
13713
  appendLog(
13631
13714
  "warn",
13632
- `Skipping upload: missing required hook fields (session_id=${!!sessionId}, transcript_path=${!!transcriptPath}, cwd=${!!cwd})`
13715
+ `Skipping upload: missing required hook fields (session_id=${!!sessionId}, transcript_path=${!!payload.transcript_path}, cwd=${!!cwd})`
13633
13716
  );
13634
- return;
13717
+ return false;
13635
13718
  }
13636
13719
  const match = await findProjectForCwd(cwd);
13637
13720
  if (!match) {
@@ -13639,13 +13722,13 @@ async function runUploadInner(payload) {
13639
13722
  "warn",
13640
13723
  `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
13641
13724
  );
13642
- return;
13725
+ return false;
13643
13726
  }
13644
13727
  const { repoRoot, config } = match;
13645
13728
  const sourceTool = await resolveSourceTool(payload, repoRoot);
13646
13729
  if (!sourceTool) {
13647
13730
  payload.tool = "unknown";
13648
- return;
13731
+ return false;
13649
13732
  }
13650
13733
  payload.tool = sourceTool;
13651
13734
  appendLog(
@@ -13653,6 +13736,18 @@ async function runUploadInner(payload) {
13653
13736
  `[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind ?? "?"})`
13654
13737
  );
13655
13738
  await selfHealHook(repoRoot, sourceTool);
13739
+ const transcriptPath = await resolveTranscriptPath(
13740
+ payload,
13741
+ sourceTool,
13742
+ sessionId
13743
+ );
13744
+ if (!transcriptPath) {
13745
+ appendLog(
13746
+ "warn",
13747
+ `Skipping upload: missing required hook fields (session_id=true, transcript_path=false, cwd=true)`
13748
+ );
13749
+ return false;
13750
+ }
13656
13751
  const transcriptResolved = path15.resolve(transcriptPath);
13657
13752
  try {
13658
13753
  const stat = await fs11.promises.stat(transcriptResolved);
@@ -13661,23 +13756,23 @@ async function runUploadInner(payload) {
13661
13756
  "warn",
13662
13757
  `Skipping session ${sessionId}: transcript_path is not a file: ${transcriptResolved}`
13663
13758
  );
13664
- return;
13759
+ return false;
13665
13760
  }
13666
13761
  } catch (err) {
13667
13762
  appendLog(
13668
13763
  "warn",
13669
13764
  `Skipping session ${sessionId}: transcript_path not readable (${transcriptResolved}): ${err instanceof Error ? err.message : String(err)}`
13670
13765
  );
13671
- return;
13766
+ return false;
13672
13767
  }
13673
13768
  if (!await hasAssistantMessage(transcriptResolved)) {
13674
13769
  appendLog(
13675
13770
  "info",
13676
13771
  `Skipping session ${sessionId}: transcript contains no assistant messages (nothing to upload).`
13677
13772
  );
13678
- return;
13773
+ return false;
13679
13774
  }
13680
- await uploadSession({
13775
+ return await uploadSession({
13681
13776
  sessionId,
13682
13777
  transcriptPath: transcriptResolved,
13683
13778
  repoRoot,
@@ -13689,7 +13784,7 @@ async function runUploadInner(payload) {
13689
13784
  async function uploadSession(args) {
13690
13785
  const { sessionId, transcriptPath, repoRoot, config, sourceTool, eventKind } = args;
13691
13786
  const isSessionEnd = eventKind === "sessionEnd";
13692
- await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
13787
+ return await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
13693
13788
  const prior = await readUploadState(repoRoot, sourceTool, sessionId);
13694
13789
  let transcriptSize;
13695
13790
  let transcriptSha256;
@@ -13706,7 +13801,7 @@ async function uploadSession(args) {
13706
13801
  if (isSessionEnd) {
13707
13802
  await deleteUploadState(repoRoot, sourceTool, sessionId);
13708
13803
  }
13709
- return;
13804
+ return true;
13710
13805
  }
13711
13806
  const now = /* @__PURE__ */ new Date();
13712
13807
  const sourceFile = {
@@ -13736,7 +13831,7 @@ async function uploadSession(args) {
13736
13831
  "error",
13737
13832
  `Session ${sessionId} upload skipped: no saved login for ${config.apiBaseUrl}. Run \`npx hillclimb login\`.`
13738
13833
  );
13739
- return;
13834
+ return false;
13740
13835
  }
13741
13836
  const client = new PlatformClient(
13742
13837
  config.apiBaseUrl,
@@ -13803,13 +13898,13 @@ Uploaded: ${now.toISOString()}`;
13803
13898
  "error",
13804
13899
  `Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
13805
13900
  );
13806
- return;
13901
+ return false;
13807
13902
  }
13808
13903
  appendLog(
13809
13904
  "error",
13810
13905
  `Session ${sessionId} upload failed: ${err instanceof Error ? err.message : String(err)}`
13811
13906
  );
13812
- return;
13907
+ return false;
13813
13908
  }
13814
13909
  const next = {
13815
13910
  schemaVersion: CURRENT_SCHEMA_VERSION2,
@@ -13837,6 +13932,7 @@ Uploaded: ${now.toISOString()}`;
13837
13932
  `[${sessionId}] session complete \u2014 contribution ${contributionId}, ${seq} file(s); local state cleared`
13838
13933
  );
13839
13934
  }
13935
+ return true;
13840
13936
  });
13841
13937
  }
13842
13938
  var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
@@ -13940,19 +14036,22 @@ async function runUploadWorker() {
13940
14036
  const toolOverride = process.env[TOOL_ENV_FLAG];
13941
14037
  if (toolOverride) payload.tool = toolOverride;
13942
14038
  appendLog("info", `worker: payload summary: ${summarizePayload(payload)}`);
14039
+ let completed = false;
13943
14040
  try {
13944
- await runUploadInner(payload);
14041
+ completed = await runUploadInner(payload);
13945
14042
  } catch (err) {
13946
14043
  appendLog(
13947
14044
  "error",
13948
14045
  `worker: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
13949
14046
  );
13950
14047
  } finally {
13951
- await recordDebugLogCompletion({
13952
- kind: "agent",
13953
- tool: fallbackSourceTool(payload),
13954
- payload
13955
- });
14048
+ if (completed) {
14049
+ await recordDebugLogCompletion({
14050
+ kind: "agent",
14051
+ tool: fallbackSourceTool(payload),
14052
+ payload
14053
+ });
14054
+ }
13956
14055
  try {
13957
14056
  await sweepStaleUploadStates();
13958
14057
  } catch {
@@ -14527,8 +14626,8 @@ import os8 from "os";
14527
14626
  import path17 from "path";
14528
14627
  var CURRENT_SCHEMA_VERSION3 = 3;
14529
14628
  var DEFAULT_STATE_DIR2 = path17.join(os8.homedir(), ".hillclimb", "git-traces");
14530
- var LOCK_RETRIES3 = 120;
14531
- var LOCK_RETRY_DELAY_MS3 = 500;
14629
+ var LOCK_RETRIES2 = 120;
14630
+ var LOCK_RETRY_DELAY_MS2 = 500;
14532
14631
  function stateDir3() {
14533
14632
  return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
14534
14633
  }
@@ -14656,7 +14755,7 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
14656
14755
  }
14657
14756
  await deleteStateFile(stateFileForRepo(repoRoot, tool));
14658
14757
  }
14659
- async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES3, delayMs = LOCK_RETRY_DELAY_MS3) {
14758
+ async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
14660
14759
  const lockPath = lockFileForRepo(repoRoot, tool);
14661
14760
  await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
14662
14761
  for (let i = 0; i < retries; i++) {
@@ -16058,7 +16157,7 @@ async function confirmExport(group, output) {
16058
16157
  import fs16 from "fs";
16059
16158
  import os10 from "os";
16060
16159
  import path21 from "path";
16061
- import readline from "readline";
16160
+ import readline2 from "readline";
16062
16161
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
16063
16162
  async function resolveRepoPath(projectDir) {
16064
16163
  const indexPath = path21.join(projectDir, "sessions-index.json");
@@ -16096,7 +16195,7 @@ async function resolveRepoPath(projectDir) {
16096
16195
  }
16097
16196
  async function extractCwdFromJsonl(filePath) {
16098
16197
  const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
16099
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
16198
+ const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
16100
16199
  try {
16101
16200
  for await (const line of rl) {
16102
16201
  if (!line.trim()) continue;
@@ -16177,10 +16276,10 @@ var ClaudeSource = class {
16177
16276
  import fs17 from "fs";
16178
16277
  import os11 from "os";
16179
16278
  import path22 from "path";
16180
- import readline2 from "readline";
16279
+ import readline3 from "readline";
16181
16280
  async function parseSessionMeta(filePath) {
16182
16281
  const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
16183
- const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
16282
+ const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
16184
16283
  try {
16185
16284
  for await (const line of rl) {
16186
16285
  if (!line.trim()) continue;
@@ -16228,7 +16327,7 @@ async function loadHistory(historyPath) {
16228
16327
  return map;
16229
16328
  }
16230
16329
  const stream = fs17.createReadStream(historyPath, { encoding: "utf-8" });
16231
- const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
16330
+ const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
16232
16331
  try {
16233
16332
  for await (const line of rl) {
16234
16333
  if (!line.trim()) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",