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.
- package/dist/cli.js +132 -33
- 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
|
|
13388
|
-
var
|
|
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 =
|
|
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(
|
|
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 || !
|
|
13712
|
+
if (!sessionId || !cwd) {
|
|
13630
13713
|
appendLog(
|
|
13631
13714
|
"warn",
|
|
13632
|
-
`Skipping upload: missing required hook fields (session_id=${!!sessionId}, transcript_path=${!!
|
|
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
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
|
|
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
|
|
14531
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
16279
|
+
import readline3 from "readline";
|
|
16181
16280
|
async function parseSessionMeta(filePath) {
|
|
16182
16281
|
const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
|
|
16183
|
-
const rl =
|
|
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 =
|
|
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;
|