hillclimb 0.5.1 → 0.5.3
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/README.md +13 -24
- package/dist/cli.js +155 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,43 +1,32 @@
|
|
|
1
1
|
# hillclimb
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Capture AI coding sessions and upload agent traces, ATIF traces, git traces, and
|
|
4
|
+
debug logs to Hillclimb.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Supported tools: Claude Code, Cursor, Codex, opencode, and GitHub Copilot Chat.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
npx hillclimb
|
|
9
|
-
```
|
|
8
|
+
## Quickstart
|
|
10
9
|
|
|
11
|
-
Run
|
|
10
|
+
Run once from a git repo:
|
|
12
11
|
|
|
13
12
|
```sh
|
|
14
|
-
npx hillclimb
|
|
13
|
+
npx hillclimb
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## Hooks
|
|
20
|
-
|
|
21
|
-
`hillclimb` detects which tools you use (`~/.claude`, `~/.cursor`, `~/.codex`, `~/.local/share/opencode`, VS Code GitHub Copilot Chat storage) and installs upload + git-trace hooks for each. You might want to gitignore the respective directories for the tools you use in your repo.
|
|
22
|
-
|
|
23
|
-
To manually export historical logs instead of configuring auto-upload, run:
|
|
24
|
-
|
|
25
|
-
```sh
|
|
26
|
-
npx hillclimb export
|
|
27
|
-
```
|
|
16
|
+
Hillclimb signs you in, connects the repo to a project, installs local hooks for
|
|
17
|
+
detected tools, and uploads future sessions automatically.
|
|
28
18
|
|
|
29
19
|
## Logs
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Each hook firing tags every line it writes (parent + worker) with a short correlation ID, e.g. `[a1b2c3]`. To follow one flow across interleaved hook runs:
|
|
21
|
+
Daily logs are written to:
|
|
34
22
|
|
|
35
23
|
```sh
|
|
36
|
-
|
|
24
|
+
~/.hillclimb/logs/YYYY-MM-DD.log
|
|
37
25
|
```
|
|
38
26
|
|
|
39
|
-
|
|
27
|
+
If a hook reports `npx: command not found`, install Node/npm or make sure `npx`
|
|
28
|
+
is available to the agent hook shell.
|
|
40
29
|
|
|
41
30
|
## License
|
|
42
31
|
|
|
43
|
-
MIT
|
|
32
|
+
MIT - see [LICENSE](LICENSE).
|
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 {
|
|
@@ -13981,7 +14080,11 @@ var EXEC_OPTS = {
|
|
|
13981
14080
|
};
|
|
13982
14081
|
var MAX_ERROR_OUTPUT_CHARS = 2e3;
|
|
13983
14082
|
var MAX_SNAPSHOT_FILE_BYTES = 10 * 1024 * 1024;
|
|
14083
|
+
var EXCLUDED_SNAPSHOT_EXTENSIONS = /* @__PURE__ */ new Set([".pdf"]);
|
|
13984
14084
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
14085
|
+
function hasExcludedSnapshotExtension(filePath) {
|
|
14086
|
+
return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path16.extname(filePath).toLowerCase());
|
|
14087
|
+
}
|
|
13985
14088
|
function quoteGitArg(arg) {
|
|
13986
14089
|
if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
|
|
13987
14090
|
return JSON.stringify(arg);
|
|
@@ -14085,10 +14188,8 @@ function recordOmittedSnapshotFile(omittedFiles, file, options = {}) {
|
|
|
14085
14188
|
omittedFiles?.push(file);
|
|
14086
14189
|
if (options.log === false) return;
|
|
14087
14190
|
const action = file.tracked ? "omitting tracked" : "skipping untracked";
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
`git-traces: ${action} ${file.path} (${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit)`
|
|
14091
|
-
);
|
|
14191
|
+
const reason = file.reason === "file-over-limit" ? `${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit` : "excluded extension";
|
|
14192
|
+
appendLog("warn", `git-traces: ${action} ${file.path} (${reason})`);
|
|
14092
14193
|
}
|
|
14093
14194
|
function parseLsTreeLongZ(output) {
|
|
14094
14195
|
const entries = [];
|
|
@@ -14106,24 +14207,25 @@ function parseLsTreeLongZ(output) {
|
|
|
14106
14207
|
}
|
|
14107
14208
|
return entries;
|
|
14108
14209
|
}
|
|
14109
|
-
function
|
|
14210
|
+
function listOmittedTreeFiles(repoRoot, treeSha, options = {}) {
|
|
14110
14211
|
const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
|
|
14111
|
-
const
|
|
14212
|
+
const omitted = [];
|
|
14112
14213
|
for (const entry of parseLsTreeLongZ(output)) {
|
|
14113
|
-
|
|
14214
|
+
const reason = hasExcludedSnapshotExtension(entry.path) ? "excluded-extension" : entry.sizeBytes > MAX_SNAPSHOT_FILE_BYTES ? "file-over-limit" : null;
|
|
14215
|
+
if (!reason) continue;
|
|
14114
14216
|
const file = {
|
|
14115
14217
|
path: entry.path,
|
|
14116
14218
|
sizeBytes: entry.sizeBytes,
|
|
14117
14219
|
tracked: true,
|
|
14118
|
-
reason
|
|
14220
|
+
reason,
|
|
14119
14221
|
gitObjectSha: entry.sha
|
|
14120
14222
|
};
|
|
14121
|
-
|
|
14223
|
+
omitted.push(file);
|
|
14122
14224
|
recordOmittedSnapshotFile(options.omittedFiles, file, {
|
|
14123
14225
|
log: options.log
|
|
14124
14226
|
});
|
|
14125
14227
|
}
|
|
14126
|
-
return
|
|
14228
|
+
return omitted;
|
|
14127
14229
|
}
|
|
14128
14230
|
function removePathsFromIndex(repoRoot, env, paths) {
|
|
14129
14231
|
if (paths.length === 0) return;
|
|
@@ -14132,9 +14234,9 @@ function removePathsFromIndex(repoRoot, env, paths) {
|
|
|
14132
14234
|
env
|
|
14133
14235
|
});
|
|
14134
14236
|
}
|
|
14135
|
-
function
|
|
14136
|
-
const
|
|
14137
|
-
if (
|
|
14237
|
+
function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
14238
|
+
const omittedFiles = listOmittedTreeFiles(repoRoot, treeSha, options);
|
|
14239
|
+
if (omittedFiles.length === 0) return treeSha;
|
|
14138
14240
|
const tmpIndex = path16.join(
|
|
14139
14241
|
os7.tmpdir(),
|
|
14140
14242
|
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
@@ -14145,7 +14247,7 @@ function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
|
14145
14247
|
removePathsFromIndex(
|
|
14146
14248
|
repoRoot,
|
|
14147
14249
|
env,
|
|
14148
|
-
|
|
14250
|
+
omittedFiles.map((file) => file.path)
|
|
14149
14251
|
);
|
|
14150
14252
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
14151
14253
|
} finally {
|
|
@@ -14168,12 +14270,13 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
14168
14270
|
if (!relPath) continue;
|
|
14169
14271
|
try {
|
|
14170
14272
|
const stat = fs12.lstatSync(path16.join(repoRoot, relPath));
|
|
14171
|
-
|
|
14273
|
+
const reason = hasExcludedSnapshotExtension(relPath) ? "excluded-extension" : stat.size > MAX_SNAPSHOT_FILE_BYTES ? "file-over-limit" : null;
|
|
14274
|
+
if (reason) {
|
|
14172
14275
|
recordOmittedSnapshotFile(omittedFiles, {
|
|
14173
14276
|
path: relPath,
|
|
14174
14277
|
sizeBytes: stat.size,
|
|
14175
14278
|
tracked: false,
|
|
14176
|
-
reason
|
|
14279
|
+
reason
|
|
14177
14280
|
});
|
|
14178
14281
|
continue;
|
|
14179
14282
|
}
|
|
@@ -14202,7 +14305,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
14202
14305
|
}
|
|
14203
14306
|
function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
14204
14307
|
const trackedTree = git(repoRoot, ["rev-parse", `${stashSha}^{tree}`]);
|
|
14205
|
-
const filteredTrackedTree =
|
|
14308
|
+
const filteredTrackedTree = filterOmittedFilesFromTree(
|
|
14206
14309
|
repoRoot,
|
|
14207
14310
|
trackedTree,
|
|
14208
14311
|
{
|
|
@@ -14274,14 +14377,14 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
14274
14377
|
}
|
|
14275
14378
|
function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha) {
|
|
14276
14379
|
if (fromTreeSha === toTreeSha) return null;
|
|
14277
|
-
const filteredFromTreeSha =
|
|
14380
|
+
const filteredFromTreeSha = filterOmittedFilesFromTree(
|
|
14278
14381
|
repoRoot,
|
|
14279
14382
|
fromTreeSha,
|
|
14280
14383
|
{
|
|
14281
14384
|
log: false
|
|
14282
14385
|
}
|
|
14283
14386
|
);
|
|
14284
|
-
const filteredToTreeSha =
|
|
14387
|
+
const filteredToTreeSha = filterOmittedFilesFromTree(repoRoot, toTreeSha, {
|
|
14285
14388
|
log: false
|
|
14286
14389
|
});
|
|
14287
14390
|
if (filteredFromTreeSha === filteredToTreeSha) return null;
|
|
@@ -14527,8 +14630,8 @@ import os8 from "os";
|
|
|
14527
14630
|
import path17 from "path";
|
|
14528
14631
|
var CURRENT_SCHEMA_VERSION3 = 3;
|
|
14529
14632
|
var DEFAULT_STATE_DIR2 = path17.join(os8.homedir(), ".hillclimb", "git-traces");
|
|
14530
|
-
var
|
|
14531
|
-
var
|
|
14633
|
+
var LOCK_RETRIES2 = 120;
|
|
14634
|
+
var LOCK_RETRY_DELAY_MS2 = 500;
|
|
14532
14635
|
function stateDir3() {
|
|
14533
14636
|
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
|
|
14534
14637
|
}
|
|
@@ -14656,7 +14759,7 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
|
|
|
14656
14759
|
}
|
|
14657
14760
|
await deleteStateFile(stateFileForRepo(repoRoot, tool));
|
|
14658
14761
|
}
|
|
14659
|
-
async function acquireLock3(repoRoot, tool, retries =
|
|
14762
|
+
async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
|
|
14660
14763
|
const lockPath = lockFileForRepo(repoRoot, tool);
|
|
14661
14764
|
await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
14662
14765
|
for (let i = 0; i < retries; i++) {
|
|
@@ -16058,7 +16161,7 @@ async function confirmExport(group, output) {
|
|
|
16058
16161
|
import fs16 from "fs";
|
|
16059
16162
|
import os10 from "os";
|
|
16060
16163
|
import path21 from "path";
|
|
16061
|
-
import
|
|
16164
|
+
import readline2 from "readline";
|
|
16062
16165
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
16063
16166
|
async function resolveRepoPath(projectDir) {
|
|
16064
16167
|
const indexPath = path21.join(projectDir, "sessions-index.json");
|
|
@@ -16096,7 +16199,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
16096
16199
|
}
|
|
16097
16200
|
async function extractCwdFromJsonl(filePath) {
|
|
16098
16201
|
const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
|
|
16099
|
-
const rl =
|
|
16202
|
+
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
16100
16203
|
try {
|
|
16101
16204
|
for await (const line of rl) {
|
|
16102
16205
|
if (!line.trim()) continue;
|
|
@@ -16177,10 +16280,10 @@ var ClaudeSource = class {
|
|
|
16177
16280
|
import fs17 from "fs";
|
|
16178
16281
|
import os11 from "os";
|
|
16179
16282
|
import path22 from "path";
|
|
16180
|
-
import
|
|
16283
|
+
import readline3 from "readline";
|
|
16181
16284
|
async function parseSessionMeta(filePath) {
|
|
16182
16285
|
const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
|
|
16183
|
-
const rl =
|
|
16286
|
+
const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
|
|
16184
16287
|
try {
|
|
16185
16288
|
for await (const line of rl) {
|
|
16186
16289
|
if (!line.trim()) continue;
|
|
@@ -16228,7 +16331,7 @@ async function loadHistory(historyPath) {
|
|
|
16228
16331
|
return map;
|
|
16229
16332
|
}
|
|
16230
16333
|
const stream = fs17.createReadStream(historyPath, { encoding: "utf-8" });
|
|
16231
|
-
const rl =
|
|
16334
|
+
const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
|
|
16232
16335
|
try {
|
|
16233
16336
|
for await (const line of rl) {
|
|
16234
16337
|
if (!line.trim()) continue;
|