hillclimb 0.8.2 → 0.8.4

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/main.js +79 -23
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -121,7 +121,8 @@ function detectRepoRoot() {
121
121
  const root = execSync("git rev-parse --show-toplevel", {
122
122
  encoding: "utf-8",
123
123
  timeout: 3e3,
124
- stdio: ["pipe", "pipe", "pipe"]
124
+ stdio: ["pipe", "pipe", "pipe"],
125
+ windowsHide: true
125
126
  }).trim();
126
127
  if (!root) return null;
127
128
  return { root, name: path2.basename(root) };
@@ -984,7 +985,7 @@ function copilotUninstallMatching(settings, eventName, predicate) {
984
985
  }
985
986
  return true;
986
987
  }
987
- var OPENCODE_PLUGIN_VERSION = 7;
988
+ var OPENCODE_PLUGIN_VERSION = 8;
988
989
  var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
989
990
  var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
990
991
  // Auto-installed by \`npx hillclimb\`. Do not edit manually \u2014 re-running
@@ -1069,6 +1070,7 @@ function spawnHillclimb(subcommand, payload) {
1069
1070
  const child = spawn("npx", ["hillclimb@latest"].concat(args), {
1070
1071
  detached: true,
1071
1072
  stdio: ["pipe", "ignore", "ignore"],
1073
+ windowsHide: true,
1072
1074
  });
1073
1075
  child.on("error", () => {});
1074
1076
  if (child.stdin) {
@@ -1413,7 +1415,8 @@ function trackedHookFiles(repoRoot, files) {
1413
1415
  try {
1414
1416
  const output = execFileSync("git", ["ls-files", "--", ...rels], {
1415
1417
  cwd: repoRoot,
1416
- stdio: ["ignore", "pipe", "ignore"]
1418
+ stdio: ["ignore", "pipe", "ignore"],
1419
+ windowsHide: true
1417
1420
  }).toString("utf-8").trim();
1418
1421
  if (!output) return [];
1419
1422
  return output.split(/\r?\n/).map((rel) => path6.join(repoRoot, rel));
@@ -1617,7 +1620,11 @@ function sleep(ms) {
1617
1620
  function openBrowser(url) {
1618
1621
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
1619
1622
  try {
1620
- const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
1623
+ const child = spawn(cmd, args, {
1624
+ detached: true,
1625
+ stdio: "ignore",
1626
+ windowsHide: true
1627
+ });
1621
1628
  child.on("error", () => {
1622
1629
  });
1623
1630
  child.unref();
@@ -2342,7 +2349,11 @@ var VOLATILE_KEYS = /* @__PURE__ */ new Set([
2342
2349
  "wall_time_ms"
2343
2350
  ]);
2344
2351
  function sessionsDir() {
2345
- return process.env.HILLCLIMB_CODEX_SESSIONS_DIR ?? path9.join(os3.homedir(), ".codex", "sessions");
2352
+ if (process.env.HILLCLIMB_CODEX_SESSIONS_DIR) {
2353
+ return process.env.HILLCLIMB_CODEX_SESSIONS_DIR;
2354
+ }
2355
+ const codexHome = process.env.CODEX_HOME || path9.join(os3.homedir(), ".codex");
2356
+ return path9.join(codexHome, "sessions");
2346
2357
  }
2347
2358
  function asRecord(value) {
2348
2359
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -2512,6 +2523,10 @@ async function findCodexRolloutPath(sessionId) {
2512
2523
  }
2513
2524
  return void 0;
2514
2525
  }
2526
+ async function isCodexInternalThread(payload, sessionId) {
2527
+ if (!sessionId || payload.transcript_path) return false;
2528
+ return await findCodexRolloutPath(sessionId) === void 0;
2529
+ }
2515
2530
  function commonPrefixLength(a, b) {
2516
2531
  const limit = Math.min(a.length, b.length);
2517
2532
  let count = 0;
@@ -11755,14 +11770,20 @@ function getSourceBaseDir(sourceName) {
11755
11770
  }
11756
11771
  }
11757
11772
  function archivePathFor(file) {
11773
+ return path11.posix.join(file.sourceName, path11.basename(file.absolutePath));
11774
+ }
11775
+ function legacyArchivePathFor(file) {
11758
11776
  const baseDir = getSourceBaseDir(file.sourceName);
11759
11777
  const relativePath = file.absolutePath.startsWith(baseDir) ? path11.relative(baseDir, file.absolutePath) : path11.basename(file.absolutePath);
11760
11778
  return path11.join(file.sourceName, relativePath);
11761
11779
  }
11762
11780
  function addGroupToArchive(archive, group, selectedSources) {
11781
+ const used = /* @__PURE__ */ new Set();
11763
11782
  for (const file of group.files) {
11764
11783
  if (!selectedSources.has(file.sourceName)) continue;
11765
- const archivePath = archivePathFor(file);
11784
+ let archivePath = archivePathFor(file);
11785
+ if (used.has(archivePath)) archivePath = legacyArchivePathFor(file);
11786
+ used.add(archivePath);
11766
11787
  if (file.content) {
11767
11788
  archive.append(file.content, { name: archivePath });
11768
11789
  } else {
@@ -13058,7 +13079,7 @@ async function runUploadInner(payload) {
13058
13079
  "warn",
13059
13080
  `Skipping upload: missing required hook fields (session_id=${!!sessionId}, transcript_path=${!!payload.transcript_path}, cwd=${!!cwd})`
13060
13081
  );
13061
- return false;
13082
+ return "skipped";
13062
13083
  }
13063
13084
  const match = await findProjectForCwd(cwd);
13064
13085
  if (!match) {
@@ -13066,13 +13087,13 @@ async function runUploadInner(payload) {
13066
13087
  "warn",
13067
13088
  `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
13068
13089
  );
13069
- return false;
13090
+ return "skipped";
13070
13091
  }
13071
13092
  const { repoRoot, config } = match;
13072
13093
  const sourceTool = await resolveSourceTool(payload, repoRoot);
13073
13094
  if (!sourceTool) {
13074
13095
  payload.tool = "unknown";
13075
- return false;
13096
+ return "skipped";
13076
13097
  }
13077
13098
  payload.tool = sourceTool;
13078
13099
  appendLog(
@@ -13086,11 +13107,18 @@ async function runUploadInner(payload) {
13086
13107
  sessionId
13087
13108
  );
13088
13109
  if (!transcriptPath) {
13110
+ if (sourceTool === "codex") {
13111
+ appendLog(
13112
+ "info",
13113
+ `Skipping session ${sessionId}: Codex internal thread (no transcript_path and no rollout under ~/.codex/sessions); not a user session`
13114
+ );
13115
+ return "internal-thread";
13116
+ }
13089
13117
  appendLog(
13090
13118
  "warn",
13091
13119
  `Skipping upload: missing required hook fields (session_id=true, transcript_path=false, cwd=true)`
13092
13120
  );
13093
- return false;
13121
+ return "skipped";
13094
13122
  }
13095
13123
  const transcriptResolved = path15.resolve(transcriptPath);
13096
13124
  try {
@@ -13100,23 +13128,32 @@ async function runUploadInner(payload) {
13100
13128
  "warn",
13101
13129
  `Skipping session ${sessionId}: transcript_path is not a file: ${transcriptResolved}`
13102
13130
  );
13103
- return false;
13131
+ return "skipped";
13104
13132
  }
13105
13133
  } catch (err) {
13134
+ const detail = err instanceof Error ? err.message : String(err);
13135
+ const missing = err?.code === "ENOENT";
13136
+ if (missing && eventKind2 === "sessionEnd") {
13137
+ appendLog(
13138
+ "info",
13139
+ `Skipping session ${sessionId}: session ended without any turns; transcript was never written (${transcriptResolved})`
13140
+ );
13141
+ return "skipped";
13142
+ }
13106
13143
  appendLog(
13107
13144
  "warn",
13108
- `Skipping session ${sessionId}: transcript_path not readable (${transcriptResolved}): ${err instanceof Error ? err.message : String(err)}`
13145
+ `Skipping session ${sessionId}: transcript_path not readable (${transcriptResolved}): ${detail}`
13109
13146
  );
13110
- return false;
13147
+ return "skipped";
13111
13148
  }
13112
13149
  if (!await hasAssistantMessage(transcriptResolved)) {
13113
13150
  appendLog(
13114
13151
  "info",
13115
13152
  `Skipping session ${sessionId}: transcript contains no assistant messages (nothing to upload).`
13116
13153
  );
13117
- return false;
13154
+ return "skipped";
13118
13155
  }
13119
- return await uploadSession({
13156
+ const uploaded = await uploadSession({
13120
13157
  sessionId,
13121
13158
  transcriptPath: transcriptResolved,
13122
13159
  repoRoot,
@@ -13125,6 +13162,7 @@ async function runUploadInner(payload) {
13125
13162
  eventKind: eventKind2,
13126
13163
  recordedAt
13127
13164
  });
13165
+ return uploaded ? "uploaded" : "skipped";
13128
13166
  }
13129
13167
  var AGENT_MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
13130
13168
  var CLI_VERSION = "0.8.0";
@@ -13344,6 +13382,10 @@ async function uploadSession(args) {
13344
13382
  sourceName: sourceTool,
13345
13383
  absolutePath: transcriptPath
13346
13384
  });
13385
+ const legacyTranscriptArchivePath = legacyArchivePathFor({
13386
+ sourceName: sourceTool,
13387
+ absolutePath: transcriptPath
13388
+ });
13347
13389
  const alreadySubmitted = prior?.submitted ?? false;
13348
13390
  const submitThisUpload = config.autoSubmit && !alreadySubmitted;
13349
13391
  if (mode === "meta-refresh" && prior?.contributionId && prior.epochMeta && cursor) {
@@ -13421,7 +13463,7 @@ async function uploadSession(args) {
13421
13463
  epochMeta2 = {
13422
13464
  transitionKind: "legacy-adopted",
13423
13465
  baseline: "legacy-latest-zip",
13424
- transcriptArchivePath,
13466
+ transcriptArchivePath: legacyTranscriptArchivePath,
13425
13467
  rawByteOffset: baseCursor.rawByteOffset,
13426
13468
  rawPrefixSha256: baseCursor.rawPrefixSha256
13427
13469
  };
@@ -13437,7 +13479,7 @@ async function uploadSession(args) {
13437
13479
  epoch: epoch2,
13438
13480
  transitionKind: "legacy-adopted",
13439
13481
  baseline: "legacy-latest-zip",
13440
- transcriptArchivePath,
13482
+ transcriptArchivePath: legacyTranscriptArchivePath,
13441
13483
  cursor: baseCursor,
13442
13484
  recordedAt,
13443
13485
  lineage: codexLineage
@@ -13828,6 +13870,7 @@ async function runUpload() {
13828
13870
  const child = spawn2(process.execPath, [entrypoint, "upload"], {
13829
13871
  detached: true,
13830
13872
  stdio: ["pipe", "ignore", "ignore"],
13873
+ windowsHide: true,
13831
13874
  env: workerEnv
13832
13875
  });
13833
13876
  child.on("error", (err) => {
@@ -13877,16 +13920,16 @@ async function runUploadWorker() {
13877
13920
  const toolOverride = process.env[TOOL_ENV_FLAG];
13878
13921
  if (toolOverride) payload.tool = toolOverride;
13879
13922
  appendLog("info", `worker: payload summary: ${summarizePayload(payload)}`);
13880
- let completed = false;
13923
+ let outcome = "skipped";
13881
13924
  try {
13882
- completed = await runUploadInner(payload);
13925
+ outcome = await runUploadInner(payload);
13883
13926
  } catch (err) {
13884
13927
  appendLog(
13885
13928
  "error",
13886
13929
  `worker: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
13887
13930
  );
13888
13931
  } finally {
13889
- if (completed) {
13932
+ if (outcome !== "internal-thread") {
13890
13933
  await recordDebugLogCompletion({
13891
13934
  kind: "agent",
13892
13935
  tool: fallbackSourceTool(payload),
@@ -13920,7 +13963,10 @@ var EXEC_OPTS = {
13920
13963
  timeout: GIT_COMMAND_TIMEOUT_MS,
13921
13964
  // A rewrite diff for a text file capped at ≤100 MB must fit. This transient
13922
13965
  // allocation only grows as large as the actual command output.
13923
- maxBuffer: 256 * 1024 * 1024
13966
+ maxBuffer: 256 * 1024 * 1024,
13967
+ // The detached worker has no console on Windows, so without this every git
13968
+ // child would allocate a fresh visible console window (one flash per call).
13969
+ windowsHide: true
13924
13970
  };
13925
13971
  var MAX_ERROR_OUTPUT_CHARS = 2e3;
13926
13972
  var DEFAULT_SNAPSHOT_LIMITS = {
@@ -14062,7 +14108,8 @@ function readTreeBlobHead(repoRoot, sha) {
14062
14108
  const { stdout } = spawnSync("git", ["cat-file", "blob", sha], {
14063
14109
  cwd: repoRoot,
14064
14110
  timeout: GIT_COMMAND_TIMEOUT_MS,
14065
- maxBuffer: BINARY_SNIFF_BYTES
14111
+ maxBuffer: BINARY_SNIFF_BYTES,
14112
+ windowsHide: true
14066
14113
  });
14067
14114
  return stdout?.length ? stdout.subarray(0, BINARY_SNIFF_BYTES) : null;
14068
14115
  } catch {
@@ -14998,7 +15045,8 @@ function execGit(cwd, args) {
14998
15045
  cwd,
14999
15046
  stdio: ["pipe", "pipe", "pipe"],
15000
15047
  timeout: 3e4,
15001
- maxBuffer: 50 * 1024 * 1024
15048
+ maxBuffer: 50 * 1024 * 1024,
15049
+ windowsHide: true
15002
15050
  }).toString("utf-8").trim();
15003
15051
  }
15004
15052
  function canUploadFile(filename, buffer) {
@@ -16611,6 +16659,7 @@ async function runGitTraces() {
16611
16659
  {
16612
16660
  detached: true,
16613
16661
  stdio: ["pipe", "ignore", "ignore"],
16662
+ windowsHide: true,
16614
16663
  env: workerEnv
16615
16664
  }
16616
16665
  );
@@ -16679,6 +16728,13 @@ async function runGitTracesWorker() {
16679
16728
  );
16680
16729
  return;
16681
16730
  }
16731
+ if (tool === "codex" && await isCodexInternalThread(payload, resolveHookSessionId(payload))) {
16732
+ appendLog(
16733
+ "info",
16734
+ `git-traces: skipping Codex internal thread ${resolveHookSessionId(payload)} on ${event} (no transcript_path and no rollout on disk; not a user session)`
16735
+ );
16736
+ return;
16737
+ }
16682
16738
  try {
16683
16739
  await selfHealHook2(payload, tool);
16684
16740
  switch (eventKind2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",