hillclimb 0.3.0 → 0.4.1
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 +1121 -598
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs18 from "fs";
|
|
5
|
+
import path22 from "path";
|
|
6
6
|
import * as p6 from "@clack/prompts";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -2149,9 +2149,14 @@ async function runStatus(args = []) {
|
|
|
2149
2149
|
|
|
2150
2150
|
// src/commands/upload.ts
|
|
2151
2151
|
import { spawn as spawn2 } from "child_process";
|
|
2152
|
-
import
|
|
2152
|
+
import fs10 from "fs";
|
|
2153
2153
|
import os5 from "os";
|
|
2154
|
-
import
|
|
2154
|
+
import path13 from "path";
|
|
2155
|
+
|
|
2156
|
+
// src/debug-logs.ts
|
|
2157
|
+
import crypto from "crypto";
|
|
2158
|
+
import fs9 from "fs";
|
|
2159
|
+
import path11 from "path";
|
|
2155
2160
|
|
|
2156
2161
|
// src/middleware/pattern-redact.ts
|
|
2157
2162
|
import os3 from "os";
|
|
@@ -10844,161 +10849,732 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
|
|
|
10844
10849
|
return { values, sourceFiles, processEnvCount, skippedCount };
|
|
10845
10850
|
}
|
|
10846
10851
|
|
|
10847
|
-
// src/
|
|
10848
|
-
import
|
|
10852
|
+
// src/outputs/platform.ts
|
|
10853
|
+
import { PassThrough } from "stream";
|
|
10854
|
+
import archiver from "archiver";
|
|
10849
10855
|
|
|
10850
|
-
// src/
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10856
|
+
// src/outputs/archive.ts
|
|
10857
|
+
import os4 from "os";
|
|
10858
|
+
import path9 from "path";
|
|
10859
|
+
function getSourceBaseDir(sourceName) {
|
|
10860
|
+
const home = os4.homedir();
|
|
10861
|
+
switch (sourceName) {
|
|
10862
|
+
case "claude":
|
|
10863
|
+
return path9.join(home, ".claude", "projects");
|
|
10864
|
+
case "codex":
|
|
10865
|
+
return path9.join(home, ".codex", "sessions");
|
|
10866
|
+
case "debug-logs":
|
|
10867
|
+
return path9.join(configDir(), "logs");
|
|
10868
|
+
default:
|
|
10869
|
+
return home;
|
|
10857
10870
|
}
|
|
10858
10871
|
}
|
|
10859
|
-
function
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
textParts.push(stringify(block));
|
|
10870
|
-
continue;
|
|
10871
|
-
}
|
|
10872
|
-
const b = block;
|
|
10873
|
-
const blockType = b.type;
|
|
10874
|
-
if (blockType === "tool_use") {
|
|
10875
|
-
toolBlocks.push(b);
|
|
10876
|
-
continue;
|
|
10877
|
-
}
|
|
10878
|
-
if (blockType === "thinking" || blockType === "reasoning" || blockType === "analysis") {
|
|
10879
|
-
const textValue2 = b.text !== void 0 && b.text !== null ? b.text : b.thinking;
|
|
10880
|
-
if (typeof textValue2 === "string") {
|
|
10881
|
-
reasoningParts.push(textValue2.trim());
|
|
10882
|
-
} else {
|
|
10883
|
-
reasoningParts.push(stringify(textValue2));
|
|
10884
|
-
}
|
|
10885
|
-
continue;
|
|
10886
|
-
}
|
|
10887
|
-
if (blockType === "code" && typeof b.code === "string") {
|
|
10888
|
-
textParts.push(b.code);
|
|
10889
|
-
continue;
|
|
10890
|
-
}
|
|
10891
|
-
const textValue = b.text;
|
|
10892
|
-
if (typeof textValue === "string") {
|
|
10893
|
-
textParts.push(textValue);
|
|
10894
|
-
} else {
|
|
10895
|
-
textParts.push(stringify(b));
|
|
10896
|
-
}
|
|
10872
|
+
function addGroupToArchive(archive, group, selectedSources) {
|
|
10873
|
+
for (const file of group.files) {
|
|
10874
|
+
if (!selectedSources.has(file.sourceName)) continue;
|
|
10875
|
+
const baseDir = getSourceBaseDir(file.sourceName);
|
|
10876
|
+
const relativePath = file.absolutePath.startsWith(baseDir) ? path9.relative(baseDir, file.absolutePath) : path9.basename(file.absolutePath);
|
|
10877
|
+
const archivePath = path9.join(file.sourceName, relativePath);
|
|
10878
|
+
if (file.content) {
|
|
10879
|
+
archive.append(file.content, { name: archivePath });
|
|
10880
|
+
} else {
|
|
10881
|
+
archive.file(file.absolutePath, { name: archivePath });
|
|
10897
10882
|
}
|
|
10898
|
-
} else if (content !== void 0 && content !== null) {
|
|
10899
|
-
textParts.push(stringify(content));
|
|
10900
10883
|
}
|
|
10901
|
-
const text3 = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
|
|
10902
|
-
const reasoning = reasoningParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
|
|
10903
|
-
return [text3, reasoning || void 0, toolBlocks];
|
|
10904
10884
|
}
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
const
|
|
10909
|
-
const
|
|
10910
|
-
|
|
10911
|
-
const
|
|
10912
|
-
const
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
}
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
extra: Object.keys(extra).length > 0 ? extra : void 0
|
|
10923
|
-
};
|
|
10885
|
+
|
|
10886
|
+
// src/outputs/platform.ts
|
|
10887
|
+
async function buildZipBuffer(group, selectedSources) {
|
|
10888
|
+
const archive = archiver("zip", { zlib: { level: 6 } });
|
|
10889
|
+
const stream = new PassThrough();
|
|
10890
|
+
archive.pipe(stream);
|
|
10891
|
+
const chunks = [];
|
|
10892
|
+
const done = new Promise((resolve, reject) => {
|
|
10893
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
10894
|
+
stream.on("end", resolve);
|
|
10895
|
+
stream.on("error", reject);
|
|
10896
|
+
archive.on("error", reject);
|
|
10897
|
+
});
|
|
10898
|
+
addGroupToArchive(archive, group, selectedSources);
|
|
10899
|
+
await archive.finalize();
|
|
10900
|
+
await done;
|
|
10901
|
+
return Buffer.concat(chunks);
|
|
10924
10902
|
}
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
if (typeof content === "string") {
|
|
10929
|
-
if (content.trim()) parts.push(content.trim());
|
|
10930
|
-
} else if (Array.isArray(content)) {
|
|
10931
|
-
for (const item of content) {
|
|
10932
|
-
const text3 = stringify(item);
|
|
10933
|
-
if (text3.trim()) parts.push(text3.trim());
|
|
10934
|
-
}
|
|
10935
|
-
} else if (content !== void 0 && content !== null && content !== "") {
|
|
10936
|
-
parts.push(stringify(content));
|
|
10903
|
+
var PlatformUploadOutput = class {
|
|
10904
|
+
constructor(opts) {
|
|
10905
|
+
this.opts = opts;
|
|
10937
10906
|
}
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
const
|
|
10942
|
-
const
|
|
10943
|
-
const
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10951
|
-
|
|
10952
|
-
|
|
10953
|
-
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
"
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10907
|
+
name = "platform";
|
|
10908
|
+
label = "Upload to hillclimb platform";
|
|
10909
|
+
async emit(group, options) {
|
|
10910
|
+
const selectedSources = new Set(options.selectedSources);
|
|
10911
|
+
const buffer = await buildZipBuffer(group, selectedSources);
|
|
10912
|
+
const {
|
|
10913
|
+
client,
|
|
10914
|
+
projectId,
|
|
10915
|
+
contributionTypeSlug,
|
|
10916
|
+
contributionTitle,
|
|
10917
|
+
contributionBody,
|
|
10918
|
+
zipFilename,
|
|
10919
|
+
autoSubmit
|
|
10920
|
+
} = this.opts;
|
|
10921
|
+
const contribution = await client.createContribution(projectId, {
|
|
10922
|
+
contributionTypeSlug,
|
|
10923
|
+
title: contributionTitle,
|
|
10924
|
+
body: contributionBody
|
|
10925
|
+
});
|
|
10926
|
+
const presigned = await client.createUpload(contribution.id, {
|
|
10927
|
+
originalFilename: zipFilename,
|
|
10928
|
+
mimeType: "application/zip",
|
|
10929
|
+
sizeBytes: buffer.byteLength
|
|
10930
|
+
});
|
|
10931
|
+
appendLog(
|
|
10932
|
+
"info",
|
|
10933
|
+
`uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
|
|
10934
|
+
);
|
|
10935
|
+
await client.uploadToPresignedUrl(
|
|
10936
|
+
presigned.presignedUrl,
|
|
10937
|
+
presigned.headers,
|
|
10938
|
+
buffer
|
|
10939
|
+
);
|
|
10940
|
+
appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
|
|
10941
|
+
if (autoSubmit) {
|
|
10942
|
+
appendLog("info", `submitting contribution ${contribution.id}`);
|
|
10943
|
+
await client.submitContribution(contribution.id);
|
|
10944
|
+
appendLog("info", `contribution ${contribution.id} submitted`);
|
|
10972
10945
|
}
|
|
10946
|
+
return contribution.id;
|
|
10973
10947
|
}
|
|
10974
|
-
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10948
|
+
};
|
|
10949
|
+
|
|
10950
|
+
// src/pipeline.ts
|
|
10951
|
+
import fs8 from "fs";
|
|
10952
|
+
import path10 from "path";
|
|
10953
|
+
function canonicalizePath(p7) {
|
|
10954
|
+
let resolved = path10.resolve(p7);
|
|
10955
|
+
if (resolved.endsWith(path10.sep) && resolved !== path10.sep) {
|
|
10956
|
+
resolved = resolved.slice(0, -1);
|
|
10978
10957
|
}
|
|
10979
|
-
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10958
|
+
return resolved;
|
|
10959
|
+
}
|
|
10960
|
+
function computeLabel(repoPath, allPaths) {
|
|
10961
|
+
const segments = repoPath.split(path10.sep).filter(Boolean);
|
|
10962
|
+
for (let depth = 1; depth <= segments.length; depth++) {
|
|
10963
|
+
const label = segments.slice(-depth).join("/");
|
|
10964
|
+
const matches = allPaths.filter((p7) => {
|
|
10965
|
+
const s = p7.split(path10.sep).filter(Boolean);
|
|
10966
|
+
return s.slice(-depth).join("/") === label;
|
|
10967
|
+
});
|
|
10968
|
+
if (matches.length === 1) return label;
|
|
10983
10969
|
}
|
|
10984
|
-
|
|
10985
|
-
return [resultText || void 0, metadata];
|
|
10970
|
+
return repoPath;
|
|
10986
10971
|
}
|
|
10987
|
-
function
|
|
10988
|
-
const
|
|
10989
|
-
for (const
|
|
10990
|
-
const
|
|
10991
|
-
|
|
10992
|
-
|
|
10993
|
-
|
|
10994
|
-
}
|
|
10972
|
+
async function mergeByRepo(files) {
|
|
10973
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
10974
|
+
for (const file of files) {
|
|
10975
|
+
const key = canonicalizePath(file.repoPath);
|
|
10976
|
+
const existing = grouped.get(key);
|
|
10977
|
+
if (existing) {
|
|
10978
|
+
existing.push({ ...file, repoPath: key });
|
|
10979
|
+
} else {
|
|
10980
|
+
grouped.set(key, [{ ...file, repoPath: key }]);
|
|
10995
10981
|
}
|
|
10996
10982
|
}
|
|
10997
|
-
|
|
10998
|
-
const
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
10983
|
+
const allPaths = [...grouped.keys()];
|
|
10984
|
+
const groups = [];
|
|
10985
|
+
for (const [repoPath, groupFiles] of grouped) {
|
|
10986
|
+
const stats = await Promise.all(
|
|
10987
|
+
groupFiles.map((f) => fs8.promises.stat(f.absolutePath).catch(() => null))
|
|
10988
|
+
);
|
|
10989
|
+
let lastModified = /* @__PURE__ */ new Date(0);
|
|
10990
|
+
for (const stat of stats) {
|
|
10991
|
+
if (stat && stat.mtime > lastModified) lastModified = stat.mtime;
|
|
10992
|
+
}
|
|
10993
|
+
const sourceNames = [...new Set(groupFiles.map((f) => f.sourceName))];
|
|
10994
|
+
groups.push({
|
|
10995
|
+
repoPath,
|
|
10996
|
+
label: computeLabel(repoPath, allPaths),
|
|
10997
|
+
files: groupFiles,
|
|
10998
|
+
sourceNames,
|
|
10999
|
+
lastModified
|
|
11000
|
+
});
|
|
11001
|
+
}
|
|
11002
|
+
groups.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
11003
|
+
return groups.filter((g) => g.files.length > 0);
|
|
11004
|
+
}
|
|
11005
|
+
async function preloadFiles(group) {
|
|
11006
|
+
const files = await Promise.all(
|
|
11007
|
+
group.files.map(async (file) => {
|
|
11008
|
+
if (file.content) return file;
|
|
11009
|
+
try {
|
|
11010
|
+
const buf = await fs8.promises.readFile(file.absolutePath);
|
|
11011
|
+
const checkLen = Math.min(buf.length, 8192);
|
|
11012
|
+
for (let i = 0; i < checkLen; i++) {
|
|
11013
|
+
if (buf[i] === 0) {
|
|
11014
|
+
return { ...file, content: buf, isBinary: true };
|
|
11015
|
+
}
|
|
11016
|
+
}
|
|
11017
|
+
return { ...file, content: buf };
|
|
11018
|
+
} catch {
|
|
11019
|
+
return file;
|
|
11020
|
+
}
|
|
11021
|
+
})
|
|
11022
|
+
);
|
|
11023
|
+
return { ...group, files };
|
|
11024
|
+
}
|
|
11025
|
+
async function runPipeline(group, middleware2, output, options, onProgress) {
|
|
11026
|
+
const preloaded = await preloadFiles(group);
|
|
11027
|
+
let processed = preloaded;
|
|
11028
|
+
for (const mw of middleware2) {
|
|
11029
|
+
onProgress?.(`${mw.name} (${processed.files.length} files)`);
|
|
11030
|
+
processed = await mw.process(processed);
|
|
11031
|
+
}
|
|
11032
|
+
onProgress?.(`Compressing ${processed.files.length} files`);
|
|
11033
|
+
return output.emit(processed, options);
|
|
11034
|
+
}
|
|
11035
|
+
|
|
11036
|
+
// src/debug-logs.ts
|
|
11037
|
+
var DEBUG_LOGS_SLUG = "debug-logs";
|
|
11038
|
+
var CURRENT_SCHEMA_VERSION = 1;
|
|
11039
|
+
var DEFAULT_WAIT_MS = 6e4;
|
|
11040
|
+
var LOCK_RETRIES = 100;
|
|
11041
|
+
var LOCK_RETRY_DELAY_MS = 100;
|
|
11042
|
+
function stateDir() {
|
|
11043
|
+
return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path11.join(configDir(), "debug-log-uploads");
|
|
11044
|
+
}
|
|
11045
|
+
function waitMs() {
|
|
11046
|
+
const raw = process.env.HILLCLIMB_DEBUG_LOG_WAIT_MS;
|
|
11047
|
+
if (!raw) return DEFAULT_WAIT_MS;
|
|
11048
|
+
const parsed = Number(raw);
|
|
11049
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_WAIT_MS;
|
|
11050
|
+
}
|
|
11051
|
+
function stateFile(eventId) {
|
|
11052
|
+
return path11.join(stateDir(), `${eventId}.json`);
|
|
11053
|
+
}
|
|
11054
|
+
function lockFile(eventId) {
|
|
11055
|
+
return `${stateFile(eventId)}.lock`;
|
|
11056
|
+
}
|
|
11057
|
+
function sleep2(ms) {
|
|
11058
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11059
|
+
}
|
|
11060
|
+
function sanitize(value) {
|
|
11061
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
11062
|
+
}
|
|
11063
|
+
function formatEpochSeconds(date) {
|
|
11064
|
+
return String(Math.floor(date.getTime() / 1e3));
|
|
11065
|
+
}
|
|
11066
|
+
function toolLabel(tool) {
|
|
11067
|
+
const labels = {
|
|
11068
|
+
cursor: "Cursor",
|
|
11069
|
+
codex: "Codex",
|
|
11070
|
+
claude: "Claude",
|
|
11071
|
+
"copilot-chat": "GitHub Copilot Chat",
|
|
11072
|
+
opencode: "opencode"
|
|
11073
|
+
};
|
|
11074
|
+
return labels[tool] ?? tool;
|
|
11075
|
+
}
|
|
11076
|
+
function classifyHookEvent(event) {
|
|
11077
|
+
switch (event) {
|
|
11078
|
+
case "Stop":
|
|
11079
|
+
case "stop":
|
|
11080
|
+
case "session.idle":
|
|
11081
|
+
return "stop";
|
|
11082
|
+
case "SessionEnd":
|
|
11083
|
+
case "sessionEnd":
|
|
11084
|
+
case "session.deleted":
|
|
11085
|
+
case "server.instance.disposed":
|
|
11086
|
+
return "sessionEnd";
|
|
11087
|
+
default:
|
|
11088
|
+
return null;
|
|
11089
|
+
}
|
|
11090
|
+
}
|
|
11091
|
+
function resolveCwd(payload) {
|
|
11092
|
+
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
11093
|
+
}
|
|
11094
|
+
function resolveSessionId(payload) {
|
|
11095
|
+
return payload.session_id ?? payload.conversation_id ?? null;
|
|
11096
|
+
}
|
|
11097
|
+
function stringOrNull(value) {
|
|
11098
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
11099
|
+
}
|
|
11100
|
+
function expectedKinds(tool, eventKind, payload) {
|
|
11101
|
+
if (eventKind === "stop") {
|
|
11102
|
+
return new Set(
|
|
11103
|
+
tool === "codex" || tool === "copilot-chat" ? ["agent", "git"] : ["git"]
|
|
11104
|
+
);
|
|
11105
|
+
}
|
|
11106
|
+
if (tool === "opencode" && !resolveSessionId(payload)) {
|
|
11107
|
+
return /* @__PURE__ */ new Set(["git"]);
|
|
11108
|
+
}
|
|
11109
|
+
return /* @__PURE__ */ new Set(["agent", "git"]);
|
|
11110
|
+
}
|
|
11111
|
+
async function transcriptFingerprint(payload) {
|
|
11112
|
+
const transcriptPath = stringOrNull(payload.transcript_path);
|
|
11113
|
+
if (!transcriptPath) return {};
|
|
11114
|
+
const resolved = path11.resolve(transcriptPath);
|
|
11115
|
+
try {
|
|
11116
|
+
const stat = await fs9.promises.stat(resolved);
|
|
11117
|
+
return {
|
|
11118
|
+
transcriptPath: resolved,
|
|
11119
|
+
transcriptMtimeMs: stat.mtimeMs,
|
|
11120
|
+
transcriptSizeBytes: stat.size
|
|
11121
|
+
};
|
|
11122
|
+
} catch {
|
|
11123
|
+
return { transcriptPath: resolved };
|
|
11124
|
+
}
|
|
11125
|
+
}
|
|
11126
|
+
async function eventContext(tool, payload) {
|
|
11127
|
+
const eventKind = classifyHookEvent(payload.hook_event_name);
|
|
11128
|
+
if (!eventKind) return null;
|
|
11129
|
+
const cwd = resolveCwd(payload);
|
|
11130
|
+
if (!cwd) return null;
|
|
11131
|
+
const project = await findProjectForCwd(cwd);
|
|
11132
|
+
if (!project) return null;
|
|
11133
|
+
const sessionId = resolveSessionId(payload);
|
|
11134
|
+
const turnId = stringOrNull(payload.turn_id);
|
|
11135
|
+
const transcriptPath = stringOrNull(payload.transcript_path);
|
|
11136
|
+
if (!sessionId && !turnId && !transcriptPath) {
|
|
11137
|
+
return null;
|
|
11138
|
+
}
|
|
11139
|
+
const expected = expectedKinds(tool, eventKind, payload);
|
|
11140
|
+
const eventNonce = eventKind === "stop" && expected.size === 1 && expected.has("git") && !turnId && !transcriptPath ? crypto.randomBytes(8).toString("hex") : null;
|
|
11141
|
+
const fingerprint = {
|
|
11142
|
+
schema: "debug-log-event-v1",
|
|
11143
|
+
apiBaseUrl: project.config.apiBaseUrl,
|
|
11144
|
+
projectId: project.config.projectId,
|
|
11145
|
+
repoRoot: project.repoRoot,
|
|
11146
|
+
tool,
|
|
11147
|
+
eventKind,
|
|
11148
|
+
hookEventName: payload.hook_event_name ?? null,
|
|
11149
|
+
sessionId,
|
|
11150
|
+
conversationId: stringOrNull(payload.conversation_id),
|
|
11151
|
+
turnId,
|
|
11152
|
+
eventNonce,
|
|
11153
|
+
...await transcriptFingerprint(payload)
|
|
11154
|
+
};
|
|
11155
|
+
const eventId = crypto.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
|
|
11156
|
+
return {
|
|
11157
|
+
eventId,
|
|
11158
|
+
repoRoot: project.repoRoot,
|
|
11159
|
+
config: project.config,
|
|
11160
|
+
tool,
|
|
11161
|
+
eventKind,
|
|
11162
|
+
hookEventName: payload.hook_event_name ?? null,
|
|
11163
|
+
sessionId,
|
|
11164
|
+
expectedKinds: expected
|
|
11165
|
+
};
|
|
11166
|
+
}
|
|
11167
|
+
async function readState(eventId) {
|
|
11168
|
+
try {
|
|
11169
|
+
const raw = await fs9.promises.readFile(stateFile(eventId), "utf-8");
|
|
11170
|
+
const parsed = JSON.parse(raw);
|
|
11171
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) return null;
|
|
11172
|
+
return parsed;
|
|
11173
|
+
} catch {
|
|
11174
|
+
return null;
|
|
11175
|
+
}
|
|
11176
|
+
}
|
|
11177
|
+
async function writeState(state) {
|
|
11178
|
+
await fs9.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
11179
|
+
const file = stateFile(state.eventId);
|
|
11180
|
+
const tmp = `${file}.tmp`;
|
|
11181
|
+
await fs9.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
11182
|
+
mode: 384
|
|
11183
|
+
});
|
|
11184
|
+
await fs9.promises.rename(tmp, file);
|
|
11185
|
+
}
|
|
11186
|
+
async function acquireLock(eventId) {
|
|
11187
|
+
await fs9.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
11188
|
+
for (let i = 0; i < LOCK_RETRIES; i++) {
|
|
11189
|
+
try {
|
|
11190
|
+
const fd = await fs9.promises.open(
|
|
11191
|
+
lockFile(eventId),
|
|
11192
|
+
fs9.constants.O_CREAT | fs9.constants.O_EXCL | fs9.constants.O_WRONLY
|
|
11193
|
+
);
|
|
11194
|
+
await fd.write(String(process.pid));
|
|
11195
|
+
await fd.close();
|
|
11196
|
+
return;
|
|
11197
|
+
} catch (err) {
|
|
11198
|
+
if (err.code === "EEXIST" && i < LOCK_RETRIES - 1) {
|
|
11199
|
+
await sleep2(LOCK_RETRY_DELAY_MS);
|
|
11200
|
+
continue;
|
|
11201
|
+
}
|
|
11202
|
+
throw err;
|
|
11203
|
+
}
|
|
11204
|
+
}
|
|
11205
|
+
}
|
|
11206
|
+
async function releaseLock(eventId) {
|
|
11207
|
+
try {
|
|
11208
|
+
await fs9.promises.unlink(lockFile(eventId));
|
|
11209
|
+
} catch {
|
|
11210
|
+
}
|
|
11211
|
+
}
|
|
11212
|
+
function initialState(ctx, now) {
|
|
11213
|
+
return {
|
|
11214
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
11215
|
+
eventId: ctx.eventId,
|
|
11216
|
+
apiBaseUrl: ctx.config.apiBaseUrl,
|
|
11217
|
+
projectId: ctx.config.projectId,
|
|
11218
|
+
projectSlug: ctx.config.projectSlug,
|
|
11219
|
+
repoRoot: ctx.repoRoot,
|
|
11220
|
+
tool: ctx.tool,
|
|
11221
|
+
eventKind: ctx.eventKind,
|
|
11222
|
+
hookEventName: ctx.hookEventName,
|
|
11223
|
+
sessionId: ctx.sessionId,
|
|
11224
|
+
logDate: path11.basename(todayLogPath(), ".log"),
|
|
11225
|
+
firstSeenAt: now.toISOString()
|
|
11226
|
+
};
|
|
11227
|
+
}
|
|
11228
|
+
function markDone(state, kind, now) {
|
|
11229
|
+
if (kind === "agent") {
|
|
11230
|
+
return { ...state, agentDoneAt: state.agentDoneAt ?? now.toISOString() };
|
|
11231
|
+
}
|
|
11232
|
+
return { ...state, gitDoneAt: state.gitDoneAt ?? now.toISOString() };
|
|
11233
|
+
}
|
|
11234
|
+
function expectedComplete(state, expected) {
|
|
11235
|
+
if (expected.has("agent") && !state.agentDoneAt) return false;
|
|
11236
|
+
if (expected.has("git") && !state.gitDoneAt) return false;
|
|
11237
|
+
return true;
|
|
11238
|
+
}
|
|
11239
|
+
function formatKinds(kinds) {
|
|
11240
|
+
return [...kinds].sort().join("+") || "<none>";
|
|
11241
|
+
}
|
|
11242
|
+
function observedKinds(state) {
|
|
11243
|
+
const observed = [];
|
|
11244
|
+
if (state.agentDoneAt) observed.push("agent");
|
|
11245
|
+
if (state.gitDoneAt) observed.push("git");
|
|
11246
|
+
return observed;
|
|
11247
|
+
}
|
|
11248
|
+
function elapsedSince(iso, now) {
|
|
11249
|
+
const started = Date.parse(iso);
|
|
11250
|
+
return Number.isNaN(started) ? 0 : Math.max(0, now.getTime() - started);
|
|
11251
|
+
}
|
|
11252
|
+
async function recordMarker(ctx, kind) {
|
|
11253
|
+
await acquireLock(ctx.eventId);
|
|
11254
|
+
try {
|
|
11255
|
+
const now = /* @__PURE__ */ new Date();
|
|
11256
|
+
const state = await readState(ctx.eventId) ?? initialState(ctx, now);
|
|
11257
|
+
const next = markDone(state, kind, now);
|
|
11258
|
+
await writeState(next);
|
|
11259
|
+
return next;
|
|
11260
|
+
} finally {
|
|
11261
|
+
await releaseLock(ctx.eventId);
|
|
11262
|
+
}
|
|
11263
|
+
}
|
|
11264
|
+
async function waitForExpectedKinds(ctx, state) {
|
|
11265
|
+
if (expectedComplete(state, ctx.expectedKinds) || state.uploadedAt) {
|
|
11266
|
+
return state;
|
|
11267
|
+
}
|
|
11268
|
+
const started = Date.now();
|
|
11269
|
+
const maxWaitMs = waitMs();
|
|
11270
|
+
while (Date.now() - started < maxWaitMs) {
|
|
11271
|
+
await sleep2(Math.min(250, Math.max(25, maxWaitMs)));
|
|
11272
|
+
const latest = await readState(ctx.eventId);
|
|
11273
|
+
if (!latest) continue;
|
|
11274
|
+
if (expectedComplete(latest, ctx.expectedKinds) || latest.uploadedAt) {
|
|
11275
|
+
return latest;
|
|
11276
|
+
}
|
|
11277
|
+
}
|
|
11278
|
+
appendLog(
|
|
11279
|
+
"warn",
|
|
11280
|
+
`debug-logs: timed out waiting for ${[...ctx.expectedKinds].join("+")} completion (event=${ctx.eventId}, tool=${ctx.tool})`
|
|
11281
|
+
);
|
|
11282
|
+
return await readState(ctx.eventId) ?? state;
|
|
11283
|
+
}
|
|
11284
|
+
async function buildMiddleware(repoRoot) {
|
|
11285
|
+
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
11286
|
+
const envFilePaths = envFileNames.map((n) => path11.join(repoRoot, n));
|
|
11287
|
+
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
11288
|
+
const middleware2 = [];
|
|
11289
|
+
if (secretResult.values.size > 0) {
|
|
11290
|
+
middleware2.push(new RedactMiddleware(secretResult.values));
|
|
11291
|
+
}
|
|
11292
|
+
middleware2.push(new PatternRedactMiddleware());
|
|
11293
|
+
return middleware2;
|
|
11294
|
+
}
|
|
11295
|
+
async function uploadDebugLog(ctx, state) {
|
|
11296
|
+
const logPath = todayLogPath();
|
|
11297
|
+
let content;
|
|
11298
|
+
try {
|
|
11299
|
+
content = await fs9.promises.readFile(logPath);
|
|
11300
|
+
} catch (err) {
|
|
11301
|
+
appendLog(
|
|
11302
|
+
"warn",
|
|
11303
|
+
`debug-logs: skipped upload; log file not readable (${logPath}): ${err instanceof Error ? err.message : String(err)}`
|
|
11304
|
+
);
|
|
11305
|
+
return null;
|
|
11306
|
+
}
|
|
11307
|
+
if (content.byteLength === 0) {
|
|
11308
|
+
appendLog("info", "debug-logs: skipped upload; log file is empty");
|
|
11309
|
+
return null;
|
|
11310
|
+
}
|
|
11311
|
+
const identity = await loadIdentity(ctx.config.apiBaseUrl);
|
|
11312
|
+
if (!identity) {
|
|
11313
|
+
appendLog(
|
|
11314
|
+
"warn",
|
|
11315
|
+
`debug-logs: skipped upload; no saved login for ${ctx.config.apiBaseUrl}`
|
|
11316
|
+
);
|
|
11317
|
+
return null;
|
|
11318
|
+
}
|
|
11319
|
+
const now = /* @__PURE__ */ new Date();
|
|
11320
|
+
appendLog(
|
|
11321
|
+
"info",
|
|
11322
|
+
`debug-logs: uploading event=${ctx.eventId} session=${ctx.sessionId ?? "<none>"} tool=${ctx.tool} expected=${formatKinds(ctx.expectedKinds)} observed=${formatKinds(observedKinds(state))} waitedMs=${elapsedSince(state.firstSeenAt, now)} project=${ctx.config.projectSlug} (${ctx.config.projectId}) apiBaseUrl=${ctx.config.apiBaseUrl}`
|
|
11323
|
+
);
|
|
11324
|
+
const sourceFile = {
|
|
11325
|
+
sourceName: DEBUG_LOGS_SLUG,
|
|
11326
|
+
absolutePath: logPath,
|
|
11327
|
+
repoPath: ctx.repoRoot,
|
|
11328
|
+
content
|
|
11329
|
+
};
|
|
11330
|
+
const group = {
|
|
11331
|
+
repoPath: ctx.repoRoot,
|
|
11332
|
+
label: path11.basename(ctx.repoRoot),
|
|
11333
|
+
files: [sourceFile],
|
|
11334
|
+
sourceNames: [DEBUG_LOGS_SLUG],
|
|
11335
|
+
lastModified: now
|
|
11336
|
+
};
|
|
11337
|
+
const shortSession = (ctx.sessionId ?? ctx.eventId).slice(0, 12);
|
|
11338
|
+
const epochSeconds = formatEpochSeconds(now);
|
|
11339
|
+
const label = toolLabel(ctx.tool);
|
|
11340
|
+
const client = new PlatformClient(
|
|
11341
|
+
ctx.config.apiBaseUrl,
|
|
11342
|
+
identity.sessionCookie
|
|
11343
|
+
);
|
|
11344
|
+
const output = new PlatformUploadOutput({
|
|
11345
|
+
client,
|
|
11346
|
+
projectId: ctx.config.projectId,
|
|
11347
|
+
contributionTypeSlug: DEBUG_LOGS_SLUG,
|
|
11348
|
+
contributionTitle: `${label} debug log ${shortSession} - ${epochSeconds}`,
|
|
11349
|
+
contributionBody: [
|
|
11350
|
+
`Session ID: ${ctx.sessionId ?? "<none>"}`,
|
|
11351
|
+
`Tool: ${label}`,
|
|
11352
|
+
`Event: ${ctx.hookEventName ?? ctx.eventKind}`,
|
|
11353
|
+
`Repo: ${ctx.repoRoot}`,
|
|
11354
|
+
`Log: ${path11.basename(logPath)}`,
|
|
11355
|
+
`Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
|
|
11356
|
+
`Git done: ${state.gitDoneAt ?? "<not observed>"}`,
|
|
11357
|
+
`Uploaded: ${now.toISOString()}`
|
|
11358
|
+
].join("\n"),
|
|
11359
|
+
zipFilename: `hillclimb-debug-log-${sanitize(ctx.tool)}-${sanitize(shortSession)}-${epochSeconds}.zip`,
|
|
11360
|
+
autoSubmit: true
|
|
11361
|
+
});
|
|
11362
|
+
try {
|
|
11363
|
+
const contributionId = await runPipeline(
|
|
11364
|
+
group,
|
|
11365
|
+
await buildMiddleware(ctx.repoRoot),
|
|
11366
|
+
output,
|
|
11367
|
+
{ selectedSources: [DEBUG_LOGS_SLUG] }
|
|
11368
|
+
);
|
|
11369
|
+
appendLog(
|
|
11370
|
+
"info",
|
|
11371
|
+
`debug-logs: uploaded ${path11.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
|
|
11372
|
+
);
|
|
11373
|
+
return contributionId;
|
|
11374
|
+
} catch (err) {
|
|
11375
|
+
if (err instanceof PlatformError && err.status === 404) {
|
|
11376
|
+
appendLog(
|
|
11377
|
+
"warn",
|
|
11378
|
+
"debug-logs: upload skipped; platform does not have the debug-logs contribution type yet"
|
|
11379
|
+
);
|
|
11380
|
+
return null;
|
|
11381
|
+
}
|
|
11382
|
+
appendLog(
|
|
11383
|
+
"error",
|
|
11384
|
+
`debug-logs: upload failed: ${err instanceof Error ? err.message : String(err)}`
|
|
11385
|
+
);
|
|
11386
|
+
return null;
|
|
11387
|
+
}
|
|
11388
|
+
}
|
|
11389
|
+
async function uploadOnce(ctx, state) {
|
|
11390
|
+
await acquireLock(ctx.eventId);
|
|
11391
|
+
try {
|
|
11392
|
+
const latest = await readState(ctx.eventId) ?? state;
|
|
11393
|
+
if (latest.uploadedAt) return;
|
|
11394
|
+
const contributionId = await uploadDebugLog(ctx, latest);
|
|
11395
|
+
if (!contributionId) return;
|
|
11396
|
+
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11397
|
+
await writeState({
|
|
11398
|
+
...latest,
|
|
11399
|
+
uploadedAt,
|
|
11400
|
+
uploadContributionId: contributionId ?? void 0
|
|
11401
|
+
});
|
|
11402
|
+
} finally {
|
|
11403
|
+
await releaseLock(ctx.eventId);
|
|
11404
|
+
}
|
|
11405
|
+
}
|
|
11406
|
+
async function recordDebugLogCompletion(args) {
|
|
11407
|
+
if (process.env.HILLCLIMB_DEBUG_LOG_UPLOAD === "0") return;
|
|
11408
|
+
try {
|
|
11409
|
+
const ctx = await eventContext(args.tool, args.payload);
|
|
11410
|
+
if (!ctx) return;
|
|
11411
|
+
const marked = await recordMarker(ctx, args.kind);
|
|
11412
|
+
const ready = await waitForExpectedKinds(ctx, marked);
|
|
11413
|
+
if (ready.uploadedAt) return;
|
|
11414
|
+
await uploadOnce(ctx, ready);
|
|
11415
|
+
} catch (err) {
|
|
11416
|
+
appendLog(
|
|
11417
|
+
"warn",
|
|
11418
|
+
`debug-logs: failed to record completion: ${err instanceof Error ? err.message : String(err)}`
|
|
11419
|
+
);
|
|
11420
|
+
}
|
|
11421
|
+
}
|
|
11422
|
+
|
|
11423
|
+
// src/normalizer/index.ts
|
|
11424
|
+
import path12 from "path";
|
|
11425
|
+
|
|
11426
|
+
// src/normalizer/claude.ts
|
|
11427
|
+
function stringify(value) {
|
|
11428
|
+
if (typeof value === "string") return value;
|
|
11429
|
+
try {
|
|
11430
|
+
return JSON.stringify(value);
|
|
11431
|
+
} catch {
|
|
11432
|
+
return String(value);
|
|
11433
|
+
}
|
|
11434
|
+
}
|
|
11435
|
+
function extractTextReasoningToolUses(content) {
|
|
11436
|
+
if (typeof content === "string") {
|
|
11437
|
+
return [content.trim(), void 0, []];
|
|
11438
|
+
}
|
|
11439
|
+
const textParts = [];
|
|
11440
|
+
const reasoningParts = [];
|
|
11441
|
+
const toolBlocks = [];
|
|
11442
|
+
if (Array.isArray(content)) {
|
|
11443
|
+
for (const block of content) {
|
|
11444
|
+
if (typeof block !== "object" || block === null) {
|
|
11445
|
+
textParts.push(stringify(block));
|
|
11446
|
+
continue;
|
|
11447
|
+
}
|
|
11448
|
+
const b = block;
|
|
11449
|
+
const blockType = b.type;
|
|
11450
|
+
if (blockType === "tool_use") {
|
|
11451
|
+
toolBlocks.push(b);
|
|
11452
|
+
continue;
|
|
11453
|
+
}
|
|
11454
|
+
if (blockType === "thinking" || blockType === "reasoning" || blockType === "analysis") {
|
|
11455
|
+
const textValue2 = b.text !== void 0 && b.text !== null ? b.text : b.thinking;
|
|
11456
|
+
if (typeof textValue2 === "string") {
|
|
11457
|
+
reasoningParts.push(textValue2.trim());
|
|
11458
|
+
} else {
|
|
11459
|
+
reasoningParts.push(stringify(textValue2));
|
|
11460
|
+
}
|
|
11461
|
+
continue;
|
|
11462
|
+
}
|
|
11463
|
+
if (blockType === "code" && typeof b.code === "string") {
|
|
11464
|
+
textParts.push(b.code);
|
|
11465
|
+
continue;
|
|
11466
|
+
}
|
|
11467
|
+
const textValue = b.text;
|
|
11468
|
+
if (typeof textValue === "string") {
|
|
11469
|
+
textParts.push(textValue);
|
|
11470
|
+
} else {
|
|
11471
|
+
textParts.push(stringify(b));
|
|
11472
|
+
}
|
|
11473
|
+
}
|
|
11474
|
+
} else if (content !== void 0 && content !== null) {
|
|
11475
|
+
textParts.push(stringify(content));
|
|
11476
|
+
}
|
|
11477
|
+
const text3 = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
|
|
11478
|
+
const reasoning = reasoningParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
|
|
11479
|
+
return [text3, reasoning || void 0, toolBlocks];
|
|
11480
|
+
}
|
|
11481
|
+
function buildMetrics(usage) {
|
|
11482
|
+
if (typeof usage !== "object" || usage === null) return void 0;
|
|
11483
|
+
const u = usage;
|
|
11484
|
+
const cachedTokens = u.cache_read_input_tokens || 0;
|
|
11485
|
+
const creation = u.cache_creation_input_tokens || 0;
|
|
11486
|
+
const inputTokens = u.input_tokens || 0;
|
|
11487
|
+
const promptTokens = inputTokens + cachedTokens + creation;
|
|
11488
|
+
const completionTokens = u.output_tokens || 0;
|
|
11489
|
+
const extra = {};
|
|
11490
|
+
for (const [key, value] of Object.entries(u)) {
|
|
11491
|
+
if (key === "input_tokens" || key === "output_tokens") continue;
|
|
11492
|
+
extra[key] = value;
|
|
11493
|
+
}
|
|
11494
|
+
return {
|
|
11495
|
+
prompt_tokens: promptTokens,
|
|
11496
|
+
completion_tokens: completionTokens,
|
|
11497
|
+
cached_tokens: cachedTokens,
|
|
11498
|
+
extra: Object.keys(extra).length > 0 ? extra : void 0
|
|
11499
|
+
};
|
|
11500
|
+
}
|
|
11501
|
+
function formatToolResult(block, toolUseResult) {
|
|
11502
|
+
const parts = [];
|
|
11503
|
+
const content = block.content;
|
|
11504
|
+
if (typeof content === "string") {
|
|
11505
|
+
if (content.trim()) parts.push(content.trim());
|
|
11506
|
+
} else if (Array.isArray(content)) {
|
|
11507
|
+
for (const item of content) {
|
|
11508
|
+
const text3 = stringify(item);
|
|
11509
|
+
if (text3.trim()) parts.push(text3.trim());
|
|
11510
|
+
}
|
|
11511
|
+
} else if (content !== void 0 && content !== null && content !== "") {
|
|
11512
|
+
parts.push(stringify(content));
|
|
11513
|
+
}
|
|
11514
|
+
let metadata;
|
|
11515
|
+
if (toolUseResult && typeof toolUseResult === "object") {
|
|
11516
|
+
metadata = { tool_use_result: toolUseResult };
|
|
11517
|
+
const stdout = toolUseResult.stdout;
|
|
11518
|
+
const stderr = toolUseResult.stderr;
|
|
11519
|
+
const exitCode = toolUseResult.exitCode ?? toolUseResult.exit_code;
|
|
11520
|
+
const interrupted = toolUseResult.interrupted;
|
|
11521
|
+
const isImage = toolUseResult.isImage;
|
|
11522
|
+
const formatted = [];
|
|
11523
|
+
if (stdout) formatted.push(`[stdout]
|
|
11524
|
+
${stdout}`.trimEnd());
|
|
11525
|
+
if (stderr) formatted.push(`[stderr]
|
|
11526
|
+
${stderr}`.trimEnd());
|
|
11527
|
+
if (exitCode !== void 0 && exitCode !== null && exitCode !== 0)
|
|
11528
|
+
formatted.push(`[exit_code] ${exitCode}`);
|
|
11529
|
+
if (interrupted) formatted.push(`[interrupted] ${interrupted}`);
|
|
11530
|
+
if (isImage) formatted.push(`[is_image] ${isImage}`);
|
|
11531
|
+
const skipKeys = /* @__PURE__ */ new Set([
|
|
11532
|
+
"stdout",
|
|
11533
|
+
"stderr",
|
|
11534
|
+
"exitCode",
|
|
11535
|
+
"exit_code",
|
|
11536
|
+
"interrupted",
|
|
11537
|
+
"isImage"
|
|
11538
|
+
]);
|
|
11539
|
+
const remainingMeta = {};
|
|
11540
|
+
for (const [k, v] of Object.entries(toolUseResult)) {
|
|
11541
|
+
if (!skipKeys.has(k)) remainingMeta[k] = v;
|
|
11542
|
+
}
|
|
11543
|
+
if (Object.keys(remainingMeta).length > 0) {
|
|
11544
|
+
formatted.push(`[metadata] ${JSON.stringify(remainingMeta)}`);
|
|
11545
|
+
}
|
|
11546
|
+
if (formatted.length > 0) {
|
|
11547
|
+
parts.push(formatted.filter(Boolean).join("\n"));
|
|
11548
|
+
}
|
|
11549
|
+
}
|
|
11550
|
+
if (block.is_error === true) {
|
|
11551
|
+
parts.push("[error] tool reported failure");
|
|
11552
|
+
metadata = metadata || {};
|
|
11553
|
+
metadata.is_error = true;
|
|
11554
|
+
}
|
|
11555
|
+
if (metadata !== void 0) {
|
|
11556
|
+
if (!("raw_tool_result" in metadata)) {
|
|
11557
|
+
metadata.raw_tool_result = block;
|
|
11558
|
+
}
|
|
11559
|
+
}
|
|
11560
|
+
const resultText = parts.filter(Boolean).join("\n\n").trim();
|
|
11561
|
+
return [resultText || void 0, metadata];
|
|
11562
|
+
}
|
|
11563
|
+
function convertClaudeToTrajectory(jsonlContent, sessionId) {
|
|
11564
|
+
const rawEvents = [];
|
|
11565
|
+
for (const line of jsonlContent.split("\n")) {
|
|
11566
|
+
const trimmed = line.trim();
|
|
11567
|
+
if (!trimmed) continue;
|
|
11568
|
+
try {
|
|
11569
|
+
rawEvents.push(JSON.parse(trimmed));
|
|
11570
|
+
} catch {
|
|
11571
|
+
}
|
|
11572
|
+
}
|
|
11573
|
+
if (rawEvents.length === 0) return null;
|
|
11574
|
+
const events = rawEvents.sort(
|
|
11575
|
+
(a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? "")
|
|
11576
|
+
);
|
|
11577
|
+
if (events.length === 0) return null;
|
|
11002
11578
|
let sid = sessionId ?? "";
|
|
11003
11579
|
for (const event of events) {
|
|
11004
11580
|
if (typeof event.sessionId === "string") {
|
|
@@ -12470,288 +13046,106 @@ function convertRunEventsToTrajectory(events, sessionId) {
|
|
|
12470
13046
|
agent: {
|
|
12471
13047
|
name: "opencode",
|
|
12472
13048
|
version: "unknown"
|
|
12473
|
-
},
|
|
12474
|
-
steps,
|
|
12475
|
-
final_metrics: finalMetricsFromSteps2(steps)
|
|
12476
|
-
};
|
|
12477
|
-
}
|
|
12478
|
-
function isRunEvent(lines) {
|
|
12479
|
-
return lines.some(
|
|
12480
|
-
(line) => ["step_start", "step_finish", "text", "reasoning", "tool_use"].includes(
|
|
12481
|
-
String(line.type ?? "")
|
|
12482
|
-
)
|
|
12483
|
-
);
|
|
12484
|
-
}
|
|
12485
|
-
function convertOpenCodeToTrajectory(content, sessionId) {
|
|
12486
|
-
const trimmed = content.trim();
|
|
12487
|
-
if (!trimmed) return null;
|
|
12488
|
-
try {
|
|
12489
|
-
const parsed = JSON.parse(trimmed);
|
|
12490
|
-
const parsedObj = asObject3(parsed);
|
|
12491
|
-
const messages = asArray(parsedObj?.messages);
|
|
12492
|
-
if (parsedObj && messages.length > 0) {
|
|
12493
|
-
const entries2 = messages.flatMap((message) => {
|
|
12494
|
-
const entry = entryFromLine(asObject3(message) ?? {});
|
|
12495
|
-
return entry ? [entry] : [];
|
|
12496
|
-
});
|
|
12497
|
-
return convertMessageEntriesToTrajectory(
|
|
12498
|
-
entries2,
|
|
12499
|
-
sessionId,
|
|
12500
|
-
asObject3(parsedObj.info)
|
|
12501
|
-
);
|
|
12502
|
-
}
|
|
12503
|
-
} catch {
|
|
12504
|
-
}
|
|
12505
|
-
const lines = parseJsonLines2(content);
|
|
12506
|
-
if (lines.length === 0) return null;
|
|
12507
|
-
if (isRunEvent(lines)) return convertRunEventsToTrajectory(lines, sessionId);
|
|
12508
|
-
const wrappedEntries = entriesFromEventWrappers(lines);
|
|
12509
|
-
const entries = wrappedEntries.length > 0 ? wrappedEntries : lines.flatMap((line) => {
|
|
12510
|
-
const entry = entryFromLine(line);
|
|
12511
|
-
return entry ? [entry] : [];
|
|
12512
|
-
});
|
|
12513
|
-
return convertMessageEntriesToTrajectory(entries, sessionId);
|
|
12514
|
-
}
|
|
12515
|
-
|
|
12516
|
-
// src/normalizer/index.ts
|
|
12517
|
-
function normalizeContent(sourceName, content, sessionId) {
|
|
12518
|
-
switch (sourceName) {
|
|
12519
|
-
case "claude":
|
|
12520
|
-
return convertClaudeToTrajectory(content, sessionId);
|
|
12521
|
-
case "codex":
|
|
12522
|
-
return convertCodexToTrajectory(content, sessionId);
|
|
12523
|
-
case "cursor":
|
|
12524
|
-
return convertCursorToTrajectory(content, sessionId);
|
|
12525
|
-
case "opencode":
|
|
12526
|
-
return convertOpenCodeToTrajectory(content, sessionId);
|
|
12527
|
-
case "copilot-chat":
|
|
12528
|
-
return convertCopilotChatToTrajectory(content, sessionId);
|
|
12529
|
-
default:
|
|
12530
|
-
return null;
|
|
12531
|
-
}
|
|
12532
|
-
}
|
|
12533
|
-
var NormalizeMiddleware = class {
|
|
12534
|
-
name = "normalize";
|
|
12535
|
-
async process(group) {
|
|
12536
|
-
const newFiles = [];
|
|
12537
|
-
for (const file of group.files) {
|
|
12538
|
-
newFiles.push(file);
|
|
12539
|
-
if (!file.absolutePath.endsWith(".jsonl")) continue;
|
|
12540
|
-
if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
|
|
12541
|
-
file.sourceName
|
|
12542
|
-
))
|
|
12543
|
-
continue;
|
|
12544
|
-
const content = file.content ? file.content.toString("utf-8") : null;
|
|
12545
|
-
if (!content) continue;
|
|
12546
|
-
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path9.basename(file.absolutePath, ".jsonl"));
|
|
12547
|
-
try {
|
|
12548
|
-
const trajectory = normalizeContent(
|
|
12549
|
-
file.sourceName,
|
|
12550
|
-
content,
|
|
12551
|
-
sessionId
|
|
12552
|
-
);
|
|
12553
|
-
if (!trajectory) continue;
|
|
12554
|
-
const json = JSON.stringify(
|
|
12555
|
-
excludeNone(trajectory),
|
|
12556
|
-
null,
|
|
12557
|
-
2
|
|
12558
|
-
);
|
|
12559
|
-
const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
|
|
12560
|
-
newFiles.push({
|
|
12561
|
-
sourceName: file.sourceName,
|
|
12562
|
-
absolutePath: atifPath,
|
|
12563
|
-
repoPath: file.repoPath,
|
|
12564
|
-
metadata: { ...file.metadata, isAtif: true },
|
|
12565
|
-
content: Buffer.from(json, "utf-8")
|
|
12566
|
-
});
|
|
12567
|
-
} catch {
|
|
12568
|
-
}
|
|
12569
|
-
}
|
|
12570
|
-
return { ...group, files: newFiles };
|
|
12571
|
-
}
|
|
12572
|
-
};
|
|
12573
|
-
|
|
12574
|
-
// src/outputs/platform.ts
|
|
12575
|
-
import { PassThrough } from "stream";
|
|
12576
|
-
import archiver from "archiver";
|
|
12577
|
-
|
|
12578
|
-
// src/outputs/archive.ts
|
|
12579
|
-
import os4 from "os";
|
|
12580
|
-
import path10 from "path";
|
|
12581
|
-
function getSourceBaseDir(sourceName) {
|
|
12582
|
-
const home = os4.homedir();
|
|
12583
|
-
switch (sourceName) {
|
|
12584
|
-
case "claude":
|
|
12585
|
-
return path10.join(home, ".claude", "projects");
|
|
12586
|
-
case "codex":
|
|
12587
|
-
return path10.join(home, ".codex", "sessions");
|
|
12588
|
-
default:
|
|
12589
|
-
return home;
|
|
12590
|
-
}
|
|
12591
|
-
}
|
|
12592
|
-
function addGroupToArchive(archive, group, selectedSources) {
|
|
12593
|
-
for (const file of group.files) {
|
|
12594
|
-
if (!selectedSources.has(file.sourceName)) continue;
|
|
12595
|
-
const baseDir = getSourceBaseDir(file.sourceName);
|
|
12596
|
-
const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
|
|
12597
|
-
const archivePath = path10.join(file.sourceName, relativePath);
|
|
12598
|
-
if (file.content) {
|
|
12599
|
-
archive.append(file.content, { name: archivePath });
|
|
12600
|
-
} else {
|
|
12601
|
-
archive.file(file.absolutePath, { name: archivePath });
|
|
12602
|
-
}
|
|
12603
|
-
}
|
|
12604
|
-
}
|
|
12605
|
-
|
|
12606
|
-
// src/outputs/platform.ts
|
|
12607
|
-
async function buildZipBuffer(group, selectedSources) {
|
|
12608
|
-
const archive = archiver("zip", { zlib: { level: 6 } });
|
|
12609
|
-
const stream = new PassThrough();
|
|
12610
|
-
archive.pipe(stream);
|
|
12611
|
-
const chunks = [];
|
|
12612
|
-
const done = new Promise((resolve, reject) => {
|
|
12613
|
-
stream.on("data", (chunk) => chunks.push(chunk));
|
|
12614
|
-
stream.on("end", resolve);
|
|
12615
|
-
stream.on("error", reject);
|
|
12616
|
-
archive.on("error", reject);
|
|
12617
|
-
});
|
|
12618
|
-
addGroupToArchive(archive, group, selectedSources);
|
|
12619
|
-
await archive.finalize();
|
|
12620
|
-
await done;
|
|
12621
|
-
return Buffer.concat(chunks);
|
|
12622
|
-
}
|
|
12623
|
-
var PlatformUploadOutput = class {
|
|
12624
|
-
constructor(opts) {
|
|
12625
|
-
this.opts = opts;
|
|
12626
|
-
}
|
|
12627
|
-
name = "platform";
|
|
12628
|
-
label = "Upload to hillclimb platform";
|
|
12629
|
-
async emit(group, options) {
|
|
12630
|
-
const selectedSources = new Set(options.selectedSources);
|
|
12631
|
-
const buffer = await buildZipBuffer(group, selectedSources);
|
|
12632
|
-
const {
|
|
12633
|
-
client,
|
|
12634
|
-
projectId,
|
|
12635
|
-
contributionTypeSlug,
|
|
12636
|
-
contributionTitle,
|
|
12637
|
-
contributionBody,
|
|
12638
|
-
zipFilename,
|
|
12639
|
-
autoSubmit
|
|
12640
|
-
} = this.opts;
|
|
12641
|
-
const contribution = await client.createContribution(projectId, {
|
|
12642
|
-
contributionTypeSlug,
|
|
12643
|
-
title: contributionTitle,
|
|
12644
|
-
body: contributionBody
|
|
12645
|
-
});
|
|
12646
|
-
const presigned = await client.createUpload(contribution.id, {
|
|
12647
|
-
originalFilename: zipFilename,
|
|
12648
|
-
mimeType: "application/zip",
|
|
12649
|
-
sizeBytes: buffer.byteLength
|
|
12650
|
-
});
|
|
12651
|
-
appendLog(
|
|
12652
|
-
"info",
|
|
12653
|
-
`uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
|
|
12654
|
-
);
|
|
12655
|
-
await client.uploadToPresignedUrl(
|
|
12656
|
-
presigned.presignedUrl,
|
|
12657
|
-
presigned.headers,
|
|
12658
|
-
buffer
|
|
12659
|
-
);
|
|
12660
|
-
appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
|
|
12661
|
-
if (autoSubmit) {
|
|
12662
|
-
appendLog("info", `submitting contribution ${contribution.id}`);
|
|
12663
|
-
await client.submitContribution(contribution.id);
|
|
12664
|
-
appendLog("info", `contribution ${contribution.id} submitted`);
|
|
12665
|
-
}
|
|
12666
|
-
return contribution.id;
|
|
12667
|
-
}
|
|
12668
|
-
};
|
|
12669
|
-
|
|
12670
|
-
// src/pipeline.ts
|
|
12671
|
-
import fs8 from "fs";
|
|
12672
|
-
import path11 from "path";
|
|
12673
|
-
function canonicalizePath(p7) {
|
|
12674
|
-
let resolved = path11.resolve(p7);
|
|
12675
|
-
if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
|
|
12676
|
-
resolved = resolved.slice(0, -1);
|
|
12677
|
-
}
|
|
12678
|
-
return resolved;
|
|
12679
|
-
}
|
|
12680
|
-
function computeLabel(repoPath, allPaths) {
|
|
12681
|
-
const segments = repoPath.split(path11.sep).filter(Boolean);
|
|
12682
|
-
for (let depth = 1; depth <= segments.length; depth++) {
|
|
12683
|
-
const label = segments.slice(-depth).join("/");
|
|
12684
|
-
const matches = allPaths.filter((p7) => {
|
|
12685
|
-
const s = p7.split(path11.sep).filter(Boolean);
|
|
12686
|
-
return s.slice(-depth).join("/") === label;
|
|
12687
|
-
});
|
|
12688
|
-
if (matches.length === 1) return label;
|
|
12689
|
-
}
|
|
12690
|
-
return repoPath;
|
|
13049
|
+
},
|
|
13050
|
+
steps,
|
|
13051
|
+
final_metrics: finalMetricsFromSteps2(steps)
|
|
13052
|
+
};
|
|
12691
13053
|
}
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
13054
|
+
function isRunEvent(lines) {
|
|
13055
|
+
return lines.some(
|
|
13056
|
+
(line) => ["step_start", "step_finish", "text", "reasoning", "tool_use"].includes(
|
|
13057
|
+
String(line.type ?? "")
|
|
13058
|
+
)
|
|
13059
|
+
);
|
|
13060
|
+
}
|
|
13061
|
+
function convertOpenCodeToTrajectory(content, sessionId) {
|
|
13062
|
+
const trimmed = content.trim();
|
|
13063
|
+
if (!trimmed) return null;
|
|
13064
|
+
try {
|
|
13065
|
+
const parsed = JSON.parse(trimmed);
|
|
13066
|
+
const parsedObj = asObject3(parsed);
|
|
13067
|
+
const messages = asArray(parsedObj?.messages);
|
|
13068
|
+
if (parsedObj && messages.length > 0) {
|
|
13069
|
+
const entries2 = messages.flatMap((message) => {
|
|
13070
|
+
const entry = entryFromLine(asObject3(message) ?? {});
|
|
13071
|
+
return entry ? [entry] : [];
|
|
13072
|
+
});
|
|
13073
|
+
return convertMessageEntriesToTrajectory(
|
|
13074
|
+
entries2,
|
|
13075
|
+
sessionId,
|
|
13076
|
+
asObject3(parsedObj.info)
|
|
13077
|
+
);
|
|
12701
13078
|
}
|
|
13079
|
+
} catch {
|
|
12702
13080
|
}
|
|
12703
|
-
const
|
|
12704
|
-
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
);
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
13081
|
+
const lines = parseJsonLines2(content);
|
|
13082
|
+
if (lines.length === 0) return null;
|
|
13083
|
+
if (isRunEvent(lines)) return convertRunEventsToTrajectory(lines, sessionId);
|
|
13084
|
+
const wrappedEntries = entriesFromEventWrappers(lines);
|
|
13085
|
+
const entries = wrappedEntries.length > 0 ? wrappedEntries : lines.flatMap((line) => {
|
|
13086
|
+
const entry = entryFromLine(line);
|
|
13087
|
+
return entry ? [entry] : [];
|
|
13088
|
+
});
|
|
13089
|
+
return convertMessageEntriesToTrajectory(entries, sessionId);
|
|
13090
|
+
}
|
|
13091
|
+
|
|
13092
|
+
// src/normalizer/index.ts
|
|
13093
|
+
function normalizeContent(sourceName, content, sessionId) {
|
|
13094
|
+
switch (sourceName) {
|
|
13095
|
+
case "claude":
|
|
13096
|
+
return convertClaudeToTrajectory(content, sessionId);
|
|
13097
|
+
case "codex":
|
|
13098
|
+
return convertCodexToTrajectory(content, sessionId);
|
|
13099
|
+
case "cursor":
|
|
13100
|
+
return convertCursorToTrajectory(content, sessionId);
|
|
13101
|
+
case "opencode":
|
|
13102
|
+
return convertOpenCodeToTrajectory(content, sessionId);
|
|
13103
|
+
case "copilot-chat":
|
|
13104
|
+
return convertCopilotChatToTrajectory(content, sessionId);
|
|
13105
|
+
default:
|
|
13106
|
+
return null;
|
|
12721
13107
|
}
|
|
12722
|
-
groups.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
12723
|
-
return groups.filter((g) => g.files.length > 0);
|
|
12724
13108
|
}
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
|
|
13109
|
+
var NormalizeMiddleware = class {
|
|
13110
|
+
name = "normalize";
|
|
13111
|
+
async process(group) {
|
|
13112
|
+
const newFiles = [];
|
|
13113
|
+
for (const file of group.files) {
|
|
13114
|
+
newFiles.push(file);
|
|
13115
|
+
if (!file.absolutePath.endsWith(".jsonl")) continue;
|
|
13116
|
+
if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
|
|
13117
|
+
file.sourceName
|
|
13118
|
+
))
|
|
13119
|
+
continue;
|
|
13120
|
+
const content = file.content ? file.content.toString("utf-8") : null;
|
|
13121
|
+
if (!content) continue;
|
|
13122
|
+
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path12.basename(file.absolutePath, ".jsonl"));
|
|
12729
13123
|
try {
|
|
12730
|
-
const
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
13124
|
+
const trajectory = normalizeContent(
|
|
13125
|
+
file.sourceName,
|
|
13126
|
+
content,
|
|
13127
|
+
sessionId
|
|
13128
|
+
);
|
|
13129
|
+
if (!trajectory) continue;
|
|
13130
|
+
const json = JSON.stringify(
|
|
13131
|
+
excludeNone(trajectory),
|
|
13132
|
+
null,
|
|
13133
|
+
2
|
|
13134
|
+
);
|
|
13135
|
+
const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
|
|
13136
|
+
newFiles.push({
|
|
13137
|
+
sourceName: file.sourceName,
|
|
13138
|
+
absolutePath: atifPath,
|
|
13139
|
+
repoPath: file.repoPath,
|
|
13140
|
+
metadata: { ...file.metadata, isAtif: true },
|
|
13141
|
+
content: Buffer.from(json, "utf-8")
|
|
13142
|
+
});
|
|
12738
13143
|
} catch {
|
|
12739
|
-
return file;
|
|
12740
13144
|
}
|
|
12741
|
-
}
|
|
12742
|
-
|
|
12743
|
-
return { ...group, files };
|
|
12744
|
-
}
|
|
12745
|
-
async function runPipeline(group, middleware2, output, options, onProgress) {
|
|
12746
|
-
const preloaded = await preloadFiles(group);
|
|
12747
|
-
let processed = preloaded;
|
|
12748
|
-
for (const mw of middleware2) {
|
|
12749
|
-
onProgress?.(`${mw.name} (${processed.files.length} files)`);
|
|
12750
|
-
processed = await mw.process(processed);
|
|
13145
|
+
}
|
|
13146
|
+
return { ...group, files: newFiles };
|
|
12751
13147
|
}
|
|
12752
|
-
|
|
12753
|
-
return output.emit(processed, options);
|
|
12754
|
-
}
|
|
13148
|
+
};
|
|
12755
13149
|
|
|
12756
13150
|
// src/commands/upload.ts
|
|
12757
13151
|
async function readStdin() {
|
|
@@ -12762,17 +13156,17 @@ async function readStdin() {
|
|
|
12762
13156
|
}
|
|
12763
13157
|
return Buffer.concat(chunks).toString("utf-8");
|
|
12764
13158
|
}
|
|
12765
|
-
function
|
|
13159
|
+
function sanitize2(value) {
|
|
12766
13160
|
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
12767
13161
|
}
|
|
12768
|
-
function
|
|
13162
|
+
function formatEpochSeconds2(date) {
|
|
12769
13163
|
return String(Math.floor(date.getTime() / 1e3));
|
|
12770
13164
|
}
|
|
12771
13165
|
function lineHasAssistant(line) {
|
|
12772
13166
|
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
12773
13167
|
}
|
|
12774
13168
|
async function hasAssistantMessage(transcriptPath) {
|
|
12775
|
-
const stream =
|
|
13169
|
+
const stream = fs10.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
12776
13170
|
let buffer = "";
|
|
12777
13171
|
try {
|
|
12778
13172
|
for await (const chunk of stream) {
|
|
@@ -12800,6 +13194,23 @@ function resolveSourceTool(payload) {
|
|
|
12800
13194
|
if (payload.hook_event_name === "Stop") return "codex";
|
|
12801
13195
|
return "claude";
|
|
12802
13196
|
}
|
|
13197
|
+
function summarizePayload(payload) {
|
|
13198
|
+
const sessionId = payload.session_id ?? payload.conversation_id ?? null;
|
|
13199
|
+
const turnId = payload.turn_id ?? null;
|
|
13200
|
+
const cwd = payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
13201
|
+
return JSON.stringify({
|
|
13202
|
+
session_id: sessionId,
|
|
13203
|
+
turn_id: turnId,
|
|
13204
|
+
cwd,
|
|
13205
|
+
hook_event_name: payload.hook_event_name ?? null,
|
|
13206
|
+
tool: payload.tool ?? null,
|
|
13207
|
+
model: payload.model ?? null,
|
|
13208
|
+
permission_mode: payload.permission_mode ?? null,
|
|
13209
|
+
transcript_path_present: !!payload.transcript_path,
|
|
13210
|
+
workspace_roots_count: payload.workspace_roots?.length ?? 0,
|
|
13211
|
+
cursor_version_present: !!payload.cursor_version
|
|
13212
|
+
});
|
|
13213
|
+
}
|
|
12803
13214
|
async function selfHealHook(repoRoot, tool) {
|
|
12804
13215
|
if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
|
|
12805
13216
|
try {
|
|
@@ -12823,7 +13234,7 @@ function resolveCursorTranscriptPath(payload) {
|
|
|
12823
13234
|
const workspace = payload.workspace_roots?.[0];
|
|
12824
13235
|
if (!id || !workspace) return void 0;
|
|
12825
13236
|
const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
|
|
12826
|
-
return
|
|
13237
|
+
return path13.join(
|
|
12827
13238
|
os5.homedir(),
|
|
12828
13239
|
".cursor",
|
|
12829
13240
|
"projects",
|
|
@@ -12859,9 +13270,9 @@ async function runUploadInner(payload) {
|
|
|
12859
13270
|
}
|
|
12860
13271
|
const { repoRoot, config } = match;
|
|
12861
13272
|
await selfHealHook(repoRoot, sourceTool);
|
|
12862
|
-
const transcriptResolved =
|
|
13273
|
+
const transcriptResolved = path13.resolve(transcriptPath);
|
|
12863
13274
|
try {
|
|
12864
|
-
const stat = await
|
|
13275
|
+
const stat = await fs10.promises.stat(transcriptResolved);
|
|
12865
13276
|
if (!stat.isFile()) {
|
|
12866
13277
|
appendLog(
|
|
12867
13278
|
"warn",
|
|
@@ -12901,13 +13312,13 @@ async function uploadSession(args) {
|
|
|
12901
13312
|
};
|
|
12902
13313
|
const group = {
|
|
12903
13314
|
repoPath: repoRoot,
|
|
12904
|
-
label:
|
|
13315
|
+
label: path13.basename(repoRoot),
|
|
12905
13316
|
files: [sourceFile],
|
|
12906
13317
|
sourceNames: [sourceTool],
|
|
12907
13318
|
lastModified: now
|
|
12908
13319
|
};
|
|
12909
13320
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
12910
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
13321
|
+
const envFilePaths = envFileNames.map((n) => path13.join(repoRoot, n));
|
|
12911
13322
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
12912
13323
|
const mwChain = [];
|
|
12913
13324
|
if (secretResult.values.size > 0) {
|
|
@@ -12932,14 +13343,14 @@ async function uploadSession(args) {
|
|
|
12932
13343
|
"copilot-chat": "GitHub Copilot Chat",
|
|
12933
13344
|
opencode: "opencode"
|
|
12934
13345
|
};
|
|
12935
|
-
const
|
|
12936
|
-
const epochSeconds =
|
|
12937
|
-
const title = `${
|
|
13346
|
+
const toolLabel2 = toolLabels[sourceTool] ?? "Claude";
|
|
13347
|
+
const epochSeconds = formatEpochSeconds2(now);
|
|
13348
|
+
const title = `${toolLabel2} session ${shortId} \u2014 ${epochSeconds}`;
|
|
12938
13349
|
const body = `Session ID: ${sessionId}
|
|
12939
|
-
Tool: ${
|
|
13350
|
+
Tool: ${toolLabel2}
|
|
12940
13351
|
Repo: ${repoRoot}
|
|
12941
13352
|
Uploaded: ${now.toISOString()}`;
|
|
12942
|
-
const zipFilename = `${sourceTool}-${
|
|
13353
|
+
const zipFilename = `${sourceTool}-${sanitize2(shortId)}-${epochSeconds}.zip`;
|
|
12943
13354
|
const output = new PlatformUploadOutput({
|
|
12944
13355
|
client,
|
|
12945
13356
|
projectId: config.projectId,
|
|
@@ -13055,7 +13466,6 @@ async function runUploadWorker() {
|
|
|
13055
13466
|
appendLog("warn", "worker: invoked with empty stdin; payload expected.");
|
|
13056
13467
|
return;
|
|
13057
13468
|
}
|
|
13058
|
-
appendLog("info", `worker: raw payload: ${raw.trim()}`);
|
|
13059
13469
|
let payload;
|
|
13060
13470
|
try {
|
|
13061
13471
|
payload = JSON.parse(raw);
|
|
@@ -13068,6 +13478,7 @@ async function runUploadWorker() {
|
|
|
13068
13478
|
}
|
|
13069
13479
|
const toolOverride = process.env[TOOL_ENV_FLAG];
|
|
13070
13480
|
if (toolOverride) payload.tool = toolOverride;
|
|
13481
|
+
appendLog("info", `worker: payload summary: ${summarizePayload(payload)}`);
|
|
13071
13482
|
try {
|
|
13072
13483
|
await runUploadInner(payload);
|
|
13073
13484
|
} catch (err) {
|
|
@@ -13075,22 +13486,28 @@ async function runUploadWorker() {
|
|
|
13075
13486
|
"error",
|
|
13076
13487
|
`worker: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
|
|
13077
13488
|
);
|
|
13489
|
+
} finally {
|
|
13490
|
+
await recordDebugLogCompletion({
|
|
13491
|
+
kind: "agent",
|
|
13492
|
+
tool: resolveSourceTool(payload),
|
|
13493
|
+
payload
|
|
13494
|
+
});
|
|
13078
13495
|
}
|
|
13079
13496
|
}
|
|
13080
13497
|
|
|
13081
13498
|
// src/git-traces/index.ts
|
|
13082
13499
|
import { spawn as spawn3 } from "child_process";
|
|
13083
|
-
import
|
|
13500
|
+
import crypto3 from "crypto";
|
|
13084
13501
|
|
|
13085
13502
|
// src/git-traces/handlers.ts
|
|
13086
13503
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13087
|
-
import
|
|
13504
|
+
import path16 from "path";
|
|
13088
13505
|
|
|
13089
13506
|
// src/git-traces/git-ops.ts
|
|
13090
13507
|
import { execFileSync } from "child_process";
|
|
13091
|
-
import
|
|
13508
|
+
import fs11 from "fs";
|
|
13092
13509
|
import os6 from "os";
|
|
13093
|
-
import
|
|
13510
|
+
import path14 from "path";
|
|
13094
13511
|
import { gzipSync } from "zlib";
|
|
13095
13512
|
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
13096
13513
|
var EXEC_OPTS = {
|
|
@@ -13098,7 +13515,7 @@ var EXEC_OPTS = {
|
|
|
13098
13515
|
maxBuffer: 50 * 1024 * 1024
|
|
13099
13516
|
};
|
|
13100
13517
|
var MAX_ERROR_OUTPUT_CHARS = 2e3;
|
|
13101
|
-
var
|
|
13518
|
+
var MAX_SNAPSHOT_FILE_BYTES = 10 * 1024 * 1024;
|
|
13102
13519
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
13103
13520
|
function quoteGitArg(arg) {
|
|
13104
13521
|
if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
|
|
@@ -13202,7 +13619,55 @@ function captureSnapshotSha(repoRoot) {
|
|
|
13202
13619
|
}
|
|
13203
13620
|
return git(repoRoot, ["rev-parse", "HEAD"]);
|
|
13204
13621
|
}
|
|
13205
|
-
function
|
|
13622
|
+
function recordOmittedSnapshotFile(omittedFiles, file) {
|
|
13623
|
+
omittedFiles?.push(file);
|
|
13624
|
+
const action = file.tracked ? "omitting tracked" : "skipping untracked";
|
|
13625
|
+
appendLog(
|
|
13626
|
+
"warn",
|
|
13627
|
+
`git-traces: ${action} ${file.path} (${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit)`
|
|
13628
|
+
);
|
|
13629
|
+
}
|
|
13630
|
+
function parseLsTreeLongZ(output) {
|
|
13631
|
+
const entries = [];
|
|
13632
|
+
for (const record of output.toString("utf-8").split("\0")) {
|
|
13633
|
+
if (!record) continue;
|
|
13634
|
+
const tab = record.indexOf(" ");
|
|
13635
|
+
if (tab === -1) continue;
|
|
13636
|
+
const meta = record.slice(0, tab);
|
|
13637
|
+
const filePath = record.slice(tab + 1);
|
|
13638
|
+
const [mode, type, sha, sizeRaw] = meta.trim().split(/\s+/);
|
|
13639
|
+
if (type !== "blob" || !mode || !sha || !sizeRaw) continue;
|
|
13640
|
+
const sizeBytes = Number.parseInt(sizeRaw, 10);
|
|
13641
|
+
if (!Number.isFinite(sizeBytes)) continue;
|
|
13642
|
+
entries.push({ mode, sha, sizeBytes, path: filePath });
|
|
13643
|
+
}
|
|
13644
|
+
return entries;
|
|
13645
|
+
}
|
|
13646
|
+
function listOversizedTreeFiles(repoRoot, treeSha, omittedFiles) {
|
|
13647
|
+
const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
|
|
13648
|
+
const oversized = [];
|
|
13649
|
+
for (const entry of parseLsTreeLongZ(output)) {
|
|
13650
|
+
if (entry.sizeBytes <= MAX_SNAPSHOT_FILE_BYTES) continue;
|
|
13651
|
+
const file = {
|
|
13652
|
+
path: entry.path,
|
|
13653
|
+
sizeBytes: entry.sizeBytes,
|
|
13654
|
+
tracked: true,
|
|
13655
|
+
reason: "file-over-limit",
|
|
13656
|
+
gitObjectSha: entry.sha
|
|
13657
|
+
};
|
|
13658
|
+
oversized.push(file);
|
|
13659
|
+
recordOmittedSnapshotFile(omittedFiles, file);
|
|
13660
|
+
}
|
|
13661
|
+
return oversized;
|
|
13662
|
+
}
|
|
13663
|
+
function removePathsFromIndex(repoRoot, env, paths) {
|
|
13664
|
+
if (paths.length === 0) return;
|
|
13665
|
+
gitBuffer(repoRoot, ["update-index", "--force-remove", "-z", "--stdin"], {
|
|
13666
|
+
input: `${paths.join("\0")}\0`,
|
|
13667
|
+
env
|
|
13668
|
+
});
|
|
13669
|
+
}
|
|
13670
|
+
function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
13206
13671
|
const list = git(repoRoot, [
|
|
13207
13672
|
"ls-files",
|
|
13208
13673
|
"--others",
|
|
@@ -13214,12 +13679,14 @@ function buildUntrackedTree(repoRoot) {
|
|
|
13214
13679
|
for (const relPath of list.split("\0")) {
|
|
13215
13680
|
if (!relPath) continue;
|
|
13216
13681
|
try {
|
|
13217
|
-
const stat =
|
|
13218
|
-
if (stat.size >
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13222
|
-
|
|
13682
|
+
const stat = fs11.lstatSync(path14.join(repoRoot, relPath));
|
|
13683
|
+
if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
|
|
13684
|
+
recordOmittedSnapshotFile(omittedFiles, {
|
|
13685
|
+
path: relPath,
|
|
13686
|
+
sizeBytes: stat.size,
|
|
13687
|
+
tracked: false,
|
|
13688
|
+
reason: "file-over-limit"
|
|
13689
|
+
});
|
|
13223
13690
|
continue;
|
|
13224
13691
|
}
|
|
13225
13692
|
kept.push(relPath);
|
|
@@ -13227,7 +13694,7 @@ function buildUntrackedTree(repoRoot) {
|
|
|
13227
13694
|
}
|
|
13228
13695
|
}
|
|
13229
13696
|
if (kept.length === 0) return null;
|
|
13230
|
-
const tmpIndex =
|
|
13697
|
+
const tmpIndex = path14.join(
|
|
13231
13698
|
os6.tmpdir(),
|
|
13232
13699
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
13233
13700
|
);
|
|
@@ -13240,46 +13707,58 @@ function buildUntrackedTree(repoRoot) {
|
|
|
13240
13707
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13241
13708
|
} finally {
|
|
13242
13709
|
try {
|
|
13243
|
-
|
|
13710
|
+
fs11.unlinkSync(tmpIndex);
|
|
13244
13711
|
} catch {
|
|
13245
13712
|
}
|
|
13246
13713
|
}
|
|
13247
13714
|
}
|
|
13248
|
-
function buildSnapshotTree(repoRoot, stashSha) {
|
|
13715
|
+
function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
13249
13716
|
const trackedTree = git(repoRoot, ["rev-parse", `${stashSha}^{tree}`]);
|
|
13250
|
-
const
|
|
13251
|
-
|
|
13717
|
+
const oversizedTracked = listOversizedTreeFiles(
|
|
13718
|
+
repoRoot,
|
|
13719
|
+
trackedTree,
|
|
13720
|
+
options.omittedFiles
|
|
13721
|
+
);
|
|
13722
|
+
const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
|
|
13723
|
+
if (oversizedTracked.length === 0 && (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)) {
|
|
13252
13724
|
return trackedTree;
|
|
13253
13725
|
}
|
|
13254
|
-
const tmpIndex =
|
|
13726
|
+
const tmpIndex = path14.join(
|
|
13255
13727
|
os6.tmpdir(),
|
|
13256
13728
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
13257
13729
|
);
|
|
13258
13730
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
13259
13731
|
try {
|
|
13260
13732
|
gitWithEnv(repoRoot, ["read-tree", trackedTree], env);
|
|
13261
|
-
|
|
13733
|
+
removePathsFromIndex(
|
|
13262
13734
|
repoRoot,
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13735
|
+
env,
|
|
13736
|
+
oversizedTracked.map((file) => file.path)
|
|
13737
|
+
);
|
|
13738
|
+
if (untrackedTree && untrackedTree !== EMPTY_TREE_SHA) {
|
|
13739
|
+
const untrackedList = gitBuffer(
|
|
13740
|
+
repoRoot,
|
|
13741
|
+
["ls-tree", "-r", untrackedTree],
|
|
13742
|
+
{
|
|
13743
|
+
env
|
|
13744
|
+
}
|
|
13745
|
+
).toString("utf-8");
|
|
13746
|
+
const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
|
|
13747
|
+
const [meta, filePath] = line.split(" ");
|
|
13748
|
+
const [mode, , sha] = meta.split(/\s+/);
|
|
13749
|
+
return `${mode} ${sha} ${filePath}`;
|
|
13750
|
+
}).join("\n");
|
|
13751
|
+
if (indexInfo) {
|
|
13752
|
+
gitBuffer(repoRoot, ["update-index", "--index-info"], {
|
|
13753
|
+
input: indexInfo,
|
|
13754
|
+
env
|
|
13755
|
+
});
|
|
13266
13756
|
}
|
|
13267
|
-
).toString("utf-8");
|
|
13268
|
-
const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
|
|
13269
|
-
const [meta, filePath] = line.split(" ");
|
|
13270
|
-
const [mode, , sha] = meta.split(/\s+/);
|
|
13271
|
-
return `${mode} ${sha} ${filePath}`;
|
|
13272
|
-
}).join("\n");
|
|
13273
|
-
if (indexInfo) {
|
|
13274
|
-
gitBuffer(repoRoot, ["update-index", "--index-info"], {
|
|
13275
|
-
input: indexInfo,
|
|
13276
|
-
env
|
|
13277
|
-
});
|
|
13278
13757
|
}
|
|
13279
13758
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13280
13759
|
} finally {
|
|
13281
13760
|
try {
|
|
13282
|
-
|
|
13761
|
+
fs11.unlinkSync(tmpIndex);
|
|
13283
13762
|
} catch {
|
|
13284
13763
|
}
|
|
13285
13764
|
}
|
|
@@ -13293,16 +13772,16 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
13293
13772
|
]);
|
|
13294
13773
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
13295
13774
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
13296
|
-
const tmpFile =
|
|
13775
|
+
const tmpFile = path14.join(
|
|
13297
13776
|
os6.tmpdir(),
|
|
13298
13777
|
`hillclimb-bundle-${Date.now()}.bundle`
|
|
13299
13778
|
);
|
|
13300
13779
|
try {
|
|
13301
13780
|
git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
|
|
13302
|
-
return
|
|
13781
|
+
return fs11.readFileSync(tmpFile);
|
|
13303
13782
|
} finally {
|
|
13304
13783
|
try {
|
|
13305
|
-
|
|
13784
|
+
fs11.unlinkSync(tmpFile);
|
|
13306
13785
|
} catch {
|
|
13307
13786
|
}
|
|
13308
13787
|
deleteRef(repoRoot, orphanRef);
|
|
@@ -13361,7 +13840,7 @@ function parseDirtyFilesFromStatus(status) {
|
|
|
13361
13840
|
return pathPart;
|
|
13362
13841
|
}).filter(Boolean);
|
|
13363
13842
|
}
|
|
13364
|
-
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt) {
|
|
13843
|
+
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt, omittedFiles = []) {
|
|
13365
13844
|
const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
|
|
13366
13845
|
const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
|
|
13367
13846
|
const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
|
|
@@ -13385,7 +13864,8 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
13385
13864
|
branch,
|
|
13386
13865
|
remoteUrl,
|
|
13387
13866
|
isDirty: dirtyFiles.length > 0,
|
|
13388
|
-
dirtyFiles
|
|
13867
|
+
dirtyFiles,
|
|
13868
|
+
...omittedFiles.length > 0 ? { omittedFiles } : {}
|
|
13389
13869
|
},
|
|
13390
13870
|
author: { name: authorName, email: authorEmail },
|
|
13391
13871
|
hostname: os6.hostname(),
|
|
@@ -13477,9 +13957,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
13477
13957
|
oldPath
|
|
13478
13958
|
});
|
|
13479
13959
|
} else {
|
|
13480
|
-
const
|
|
13481
|
-
indexByPath.set(
|
|
13482
|
-
files.push({ path:
|
|
13960
|
+
const path23 = parts[parts.length - 1];
|
|
13961
|
+
indexByPath.set(path23, files.length);
|
|
13962
|
+
files.push({ path: path23, status, additions: 0, deletions: 0 });
|
|
13483
13963
|
}
|
|
13484
13964
|
}
|
|
13485
13965
|
for (const line of numstat.split("\n")) {
|
|
@@ -13545,31 +14025,31 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
13545
14025
|
}
|
|
13546
14026
|
|
|
13547
14027
|
// src/git-traces/session-state.ts
|
|
13548
|
-
import
|
|
13549
|
-
import
|
|
14028
|
+
import crypto2 from "crypto";
|
|
14029
|
+
import fs12 from "fs";
|
|
13550
14030
|
import os7 from "os";
|
|
13551
|
-
import
|
|
13552
|
-
var
|
|
13553
|
-
var DEFAULT_STATE_DIR =
|
|
13554
|
-
var
|
|
13555
|
-
var
|
|
13556
|
-
function
|
|
14031
|
+
import path15 from "path";
|
|
14032
|
+
var CURRENT_SCHEMA_VERSION2 = 3;
|
|
14033
|
+
var DEFAULT_STATE_DIR = path15.join(os7.homedir(), ".hillclimb", "git-traces");
|
|
14034
|
+
var LOCK_RETRIES2 = 120;
|
|
14035
|
+
var LOCK_RETRY_DELAY_MS2 = 500;
|
|
14036
|
+
function stateDir2() {
|
|
13557
14037
|
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR;
|
|
13558
14038
|
}
|
|
13559
14039
|
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
13560
|
-
const hash =
|
|
13561
|
-
sessionId ? `${
|
|
14040
|
+
const hash = crypto2.createHash("sha256").update(
|
|
14041
|
+
sessionId ? `${path15.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path15.resolve(repoRoot)}\0${tool}`
|
|
13562
14042
|
).digest("hex").slice(0, 16);
|
|
13563
|
-
return
|
|
14043
|
+
return path15.join(stateDir2(), `${hash}.json`);
|
|
13564
14044
|
}
|
|
13565
14045
|
function lockFileForRepo(repoRoot, tool) {
|
|
13566
14046
|
return `${stateFileForRepo(repoRoot, tool)}.lock`;
|
|
13567
14047
|
}
|
|
13568
14048
|
async function readStateFile(file) {
|
|
13569
14049
|
try {
|
|
13570
|
-
const raw = await
|
|
14050
|
+
const raw = await fs12.promises.readFile(file, "utf-8");
|
|
13571
14051
|
const parsed = JSON.parse(raw);
|
|
13572
|
-
if (parsed.schemaVersion !==
|
|
14052
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION2) {
|
|
13573
14053
|
return null;
|
|
13574
14054
|
}
|
|
13575
14055
|
return parsed;
|
|
@@ -13580,26 +14060,26 @@ async function readStateFile(file) {
|
|
|
13580
14060
|
async function listScopedSessionStates(repoRoot, tool) {
|
|
13581
14061
|
let entries;
|
|
13582
14062
|
try {
|
|
13583
|
-
entries = await
|
|
14063
|
+
entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
|
|
13584
14064
|
} catch {
|
|
13585
14065
|
return [];
|
|
13586
14066
|
}
|
|
13587
14067
|
const states = [];
|
|
13588
14068
|
for (const entry of entries) {
|
|
13589
14069
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
13590
|
-
const file =
|
|
14070
|
+
const file = path15.join(stateDir2(), entry.name);
|
|
13591
14071
|
const state = await readStateFile(file);
|
|
13592
14072
|
if (!state) continue;
|
|
13593
14073
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
13594
14074
|
continue;
|
|
13595
14075
|
}
|
|
13596
|
-
if (
|
|
13597
|
-
if (
|
|
14076
|
+
if (path15.resolve(state.repoRoot) !== path15.resolve(repoRoot)) continue;
|
|
14077
|
+
if (path15.resolve(file) !== path15.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
13598
14078
|
continue;
|
|
13599
14079
|
}
|
|
13600
14080
|
let mtimeMs = 0;
|
|
13601
14081
|
try {
|
|
13602
|
-
mtimeMs = (await
|
|
14082
|
+
mtimeMs = (await fs12.promises.stat(file)).mtimeMs;
|
|
13603
14083
|
} catch {
|
|
13604
14084
|
continue;
|
|
13605
14085
|
}
|
|
@@ -13610,26 +14090,26 @@ async function listScopedSessionStates(repoRoot, tool) {
|
|
|
13610
14090
|
async function listSessionStatesForSession(tool, sessionId) {
|
|
13611
14091
|
let entries;
|
|
13612
14092
|
try {
|
|
13613
|
-
entries = await
|
|
14093
|
+
entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
|
|
13614
14094
|
} catch {
|
|
13615
14095
|
return [];
|
|
13616
14096
|
}
|
|
13617
14097
|
const states = [];
|
|
13618
14098
|
for (const entry of entries) {
|
|
13619
14099
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
13620
|
-
const file =
|
|
14100
|
+
const file = path15.join(stateDir2(), entry.name);
|
|
13621
14101
|
const state = await readStateFile(file);
|
|
13622
14102
|
if (!state) continue;
|
|
13623
14103
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
13624
14104
|
continue;
|
|
13625
14105
|
}
|
|
13626
14106
|
if (state.sessionId !== sessionId) continue;
|
|
13627
|
-
if (
|
|
14107
|
+
if (path15.resolve(file) !== path15.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
13628
14108
|
continue;
|
|
13629
14109
|
}
|
|
13630
14110
|
let mtimeMs = 0;
|
|
13631
14111
|
try {
|
|
13632
|
-
mtimeMs = (await
|
|
14112
|
+
mtimeMs = (await fs12.promises.stat(file)).mtimeMs;
|
|
13633
14113
|
} catch {
|
|
13634
14114
|
continue;
|
|
13635
14115
|
}
|
|
@@ -13650,12 +14130,12 @@ async function readSessionState(repoRoot, tool, sessionId) {
|
|
|
13650
14130
|
}
|
|
13651
14131
|
async function writeSessionState(state, tool) {
|
|
13652
14132
|
const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
|
|
13653
|
-
await
|
|
14133
|
+
await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
13654
14134
|
const tmp = `${file}.tmp`;
|
|
13655
|
-
await
|
|
14135
|
+
await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
13656
14136
|
mode: 384
|
|
13657
14137
|
});
|
|
13658
|
-
await
|
|
14138
|
+
await fs12.promises.rename(tmp, file);
|
|
13659
14139
|
const legacyFile = stateFileForRepo(state.repoRoot, tool);
|
|
13660
14140
|
const legacy = await readStateFile(legacyFile);
|
|
13661
14141
|
if (legacy?.sessionId === state.sessionId) {
|
|
@@ -13664,7 +14144,7 @@ async function writeSessionState(state, tool) {
|
|
|
13664
14144
|
}
|
|
13665
14145
|
async function deleteStateFile(file) {
|
|
13666
14146
|
try {
|
|
13667
|
-
await
|
|
14147
|
+
await fs12.promises.unlink(file);
|
|
13668
14148
|
} catch {
|
|
13669
14149
|
}
|
|
13670
14150
|
}
|
|
@@ -13680,14 +14160,14 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
|
|
|
13680
14160
|
}
|
|
13681
14161
|
await deleteStateFile(stateFileForRepo(repoRoot, tool));
|
|
13682
14162
|
}
|
|
13683
|
-
async function
|
|
14163
|
+
async function acquireLock2(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
|
|
13684
14164
|
const lockPath = lockFileForRepo(repoRoot, tool);
|
|
13685
|
-
await
|
|
14165
|
+
await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
13686
14166
|
for (let i = 0; i < retries; i++) {
|
|
13687
14167
|
try {
|
|
13688
|
-
const fd = await
|
|
14168
|
+
const fd = await fs12.promises.open(
|
|
13689
14169
|
lockPath,
|
|
13690
|
-
|
|
14170
|
+
fs12.constants.O_CREAT | fs12.constants.O_EXCL | fs12.constants.O_WRONLY
|
|
13691
14171
|
);
|
|
13692
14172
|
await fd.write(String(process.pid));
|
|
13693
14173
|
await fd.close();
|
|
@@ -13702,9 +14182,9 @@ async function acquireLock(repoRoot, tool, retries = LOCK_RETRIES, delayMs = LOC
|
|
|
13702
14182
|
}
|
|
13703
14183
|
throw new Error(`Failed to acquire lock after ${retries} retries`);
|
|
13704
14184
|
}
|
|
13705
|
-
async function
|
|
14185
|
+
async function releaseLock2(repoRoot, tool) {
|
|
13706
14186
|
try {
|
|
13707
|
-
await
|
|
14187
|
+
await fs12.promises.unlink(lockFileForRepo(repoRoot, tool));
|
|
13708
14188
|
} catch {
|
|
13709
14189
|
}
|
|
13710
14190
|
}
|
|
@@ -13714,7 +14194,7 @@ var CLI_VERSION = "0.2.0";
|
|
|
13714
14194
|
var GIT_TRACES_SLUG = "git-traces";
|
|
13715
14195
|
var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
13716
14196
|
var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13717
|
-
function
|
|
14197
|
+
function formatEpochSeconds3(date) {
|
|
13718
14198
|
return String(Math.floor(date.getTime() / 1e3));
|
|
13719
14199
|
}
|
|
13720
14200
|
var TOOL_LABELS = {
|
|
@@ -13727,17 +14207,17 @@ var TOOL_LABELS = {
|
|
|
13727
14207
|
async function loadConfiguredRepos() {
|
|
13728
14208
|
const file = await loadProjects();
|
|
13729
14209
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
13730
|
-
repoRoot:
|
|
14210
|
+
repoRoot: path16.resolve(repoRoot),
|
|
13731
14211
|
config
|
|
13732
14212
|
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
13733
14213
|
}
|
|
13734
14214
|
function repoLabel(repoRoot) {
|
|
13735
|
-
return
|
|
14215
|
+
return path16.basename(repoRoot) || repoRoot;
|
|
13736
14216
|
}
|
|
13737
|
-
function
|
|
14217
|
+
function resolveCwd2(payload) {
|
|
13738
14218
|
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
13739
14219
|
}
|
|
13740
|
-
function
|
|
14220
|
+
function resolveSessionId2(payload) {
|
|
13741
14221
|
return payload.session_id ?? payload.conversation_id ?? null;
|
|
13742
14222
|
}
|
|
13743
14223
|
function epochPrefix(epoch) {
|
|
@@ -13829,6 +14309,10 @@ function freezeEpochBaseline(params) {
|
|
|
13829
14309
|
sessionId,
|
|
13830
14310
|
epoch
|
|
13831
14311
|
);
|
|
14312
|
+
const omittedFiles = [];
|
|
14313
|
+
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
|
|
14314
|
+
omittedFiles
|
|
14315
|
+
});
|
|
13832
14316
|
const baselineMetadata = buildBaselineMetadata(
|
|
13833
14317
|
repoRoot,
|
|
13834
14318
|
sessionId,
|
|
@@ -13838,9 +14322,9 @@ function freezeEpochBaseline(params) {
|
|
|
13838
14322
|
epoch,
|
|
13839
14323
|
prevHeadSha,
|
|
13840
14324
|
transitionKind,
|
|
13841
|
-
startedAt
|
|
14325
|
+
startedAt,
|
|
14326
|
+
omittedFiles
|
|
13842
14327
|
);
|
|
13843
|
-
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
|
|
13844
14328
|
pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha);
|
|
13845
14329
|
return { baselineSha, baselineTreeSha, baselineMetadata, headSha };
|
|
13846
14330
|
} catch (err) {
|
|
@@ -13886,6 +14370,10 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
13886
14370
|
} = params;
|
|
13887
14371
|
const prefix = epochPrefix(epoch);
|
|
13888
14372
|
try {
|
|
14373
|
+
const omittedFiles = [];
|
|
14374
|
+
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
|
|
14375
|
+
omittedFiles
|
|
14376
|
+
});
|
|
13889
14377
|
const metadata = buildBaselineMetadata(
|
|
13890
14378
|
repoRoot,
|
|
13891
14379
|
sessionId,
|
|
@@ -13895,9 +14383,9 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
13895
14383
|
epoch,
|
|
13896
14384
|
prevHeadSha,
|
|
13897
14385
|
transitionKind,
|
|
13898
|
-
startedAt
|
|
14386
|
+
startedAt,
|
|
14387
|
+
omittedFiles
|
|
13899
14388
|
);
|
|
13900
|
-
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
|
|
13901
14389
|
return buildFrozenEpochBaselineArtifacts({
|
|
13902
14390
|
repoRoot,
|
|
13903
14391
|
sessionId,
|
|
@@ -13935,15 +14423,15 @@ async function uploadEpochBaselineArtifacts(params) {
|
|
|
13935
14423
|
}
|
|
13936
14424
|
async function createGitTracesContribution(params) {
|
|
13937
14425
|
const { client, config, repoRoot, state, tool, now, artifacts } = params;
|
|
13938
|
-
const
|
|
13939
|
-
const epochSeconds =
|
|
14426
|
+
const toolLabel2 = TOOL_LABELS[tool] ?? "Claude";
|
|
14427
|
+
const epochSeconds = formatEpochSeconds3(now);
|
|
13940
14428
|
const shortId = state.sessionId.slice(0, 12);
|
|
13941
14429
|
const repoName = repoLabel(repoRoot);
|
|
13942
14430
|
const contribution = await client.createContribution(config.projectId, {
|
|
13943
14431
|
contributionTypeSlug: GIT_TRACES_SLUG,
|
|
13944
|
-
title: `${
|
|
14432
|
+
title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
|
|
13945
14433
|
body: `Session ID: ${state.sessionId}
|
|
13946
|
-
Tool: ${
|
|
14434
|
+
Tool: ${toolLabel2}
|
|
13947
14435
|
Repo: ${repoRoot}
|
|
13948
14436
|
Uploaded: ${now.toISOString()}`
|
|
13949
14437
|
});
|
|
@@ -13997,7 +14485,7 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
13997
14485
|
});
|
|
13998
14486
|
if (!frozen) return null;
|
|
13999
14487
|
const state = {
|
|
14000
|
-
schemaVersion:
|
|
14488
|
+
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
14001
14489
|
sessionId,
|
|
14002
14490
|
contributionId: null,
|
|
14003
14491
|
baselineSha: frozen.baselineSha,
|
|
@@ -14047,7 +14535,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14047
14535
|
);
|
|
14048
14536
|
return "skipped";
|
|
14049
14537
|
}
|
|
14050
|
-
await
|
|
14538
|
+
await acquireLock2(repoRoot, tool);
|
|
14051
14539
|
try {
|
|
14052
14540
|
const staleLegacy = await readSessionState(repoRoot, tool);
|
|
14053
14541
|
if (staleLegacy && staleLegacy.sessionId !== sessionId) {
|
|
@@ -14078,11 +14566,11 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14078
14566
|
);
|
|
14079
14567
|
return "failed";
|
|
14080
14568
|
} finally {
|
|
14081
|
-
await
|
|
14569
|
+
await releaseLock2(repoRoot, tool);
|
|
14082
14570
|
}
|
|
14083
14571
|
}
|
|
14084
14572
|
async function handleSessionStart(payload, tool) {
|
|
14085
|
-
const cwd =
|
|
14573
|
+
const cwd = resolveCwd2(payload);
|
|
14086
14574
|
if (!cwd) {
|
|
14087
14575
|
appendLog("warn", "git-traces: no cwd in payload, skipping");
|
|
14088
14576
|
return;
|
|
@@ -14095,7 +14583,7 @@ async function handleSessionStart(payload, tool) {
|
|
|
14095
14583
|
);
|
|
14096
14584
|
return;
|
|
14097
14585
|
}
|
|
14098
|
-
const sessionId =
|
|
14586
|
+
const sessionId = resolveSessionId2(payload);
|
|
14099
14587
|
if (!sessionId) {
|
|
14100
14588
|
appendLog("warn", "git-traces: no session_id in payload, skipping");
|
|
14101
14589
|
return;
|
|
@@ -14178,9 +14666,10 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14178
14666
|
);
|
|
14179
14667
|
return "skipped";
|
|
14180
14668
|
}
|
|
14181
|
-
await
|
|
14669
|
+
await acquireLock2(repoRoot, tool);
|
|
14670
|
+
let state = null;
|
|
14182
14671
|
try {
|
|
14183
|
-
|
|
14672
|
+
state = await readSessionState(repoRoot, tool, sessionId);
|
|
14184
14673
|
if (!state) {
|
|
14185
14674
|
appendLog(
|
|
14186
14675
|
"info",
|
|
@@ -14351,17 +14840,42 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14351
14840
|
"error",
|
|
14352
14841
|
`git-traces: Stop failed for repo ${repoRoot}: ${formatError(err)}`
|
|
14353
14842
|
);
|
|
14843
|
+
if (state && err instanceof PlatformError && err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND") {
|
|
14844
|
+
if (state.baselineTreeSha) {
|
|
14845
|
+
await writeSessionState(
|
|
14846
|
+
{
|
|
14847
|
+
...state,
|
|
14848
|
+
contributionId: null,
|
|
14849
|
+
lastSnapshotSha: state.baselineSha,
|
|
14850
|
+
lastSnapshotTreeSha: state.baselineTreeSha,
|
|
14851
|
+
turnCount: 0
|
|
14852
|
+
},
|
|
14853
|
+
tool
|
|
14854
|
+
);
|
|
14855
|
+
appendLog(
|
|
14856
|
+
"warn",
|
|
14857
|
+
`git-traces: cleared stale contribution state (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
|
|
14858
|
+
);
|
|
14859
|
+
} else {
|
|
14860
|
+
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
14861
|
+
await deleteSessionState(repoRoot, tool, state.sessionId);
|
|
14862
|
+
appendLog(
|
|
14863
|
+
"warn",
|
|
14864
|
+
`git-traces: deleted stale contribution state without a frozen baseline (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
|
|
14865
|
+
);
|
|
14866
|
+
}
|
|
14867
|
+
}
|
|
14354
14868
|
return "failed";
|
|
14355
14869
|
} finally {
|
|
14356
|
-
await
|
|
14870
|
+
await releaseLock2(repoRoot, tool);
|
|
14357
14871
|
}
|
|
14358
14872
|
}
|
|
14359
14873
|
async function handleStop(payload, tool) {
|
|
14360
|
-
const cwd =
|
|
14874
|
+
const cwd = resolveCwd2(payload);
|
|
14361
14875
|
if (!cwd) return;
|
|
14362
14876
|
const project = await findProjectForCwd(cwd);
|
|
14363
14877
|
if (!project) return;
|
|
14364
|
-
const sessionId =
|
|
14878
|
+
const sessionId = resolveSessionId2(payload);
|
|
14365
14879
|
const recordedAt = Date.now();
|
|
14366
14880
|
const repos = await loadConfiguredRepos();
|
|
14367
14881
|
const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
|
|
@@ -14370,7 +14884,7 @@ async function handleStop(payload, tool) {
|
|
|
14370
14884
|
if (sessionId) {
|
|
14371
14885
|
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
14372
14886
|
for (const { state } of storedStates) {
|
|
14373
|
-
const repo = repoByRoot.get(
|
|
14887
|
+
const repo = repoByRoot.get(path16.resolve(state.repoRoot));
|
|
14374
14888
|
if (!repo) {
|
|
14375
14889
|
missingConfig++;
|
|
14376
14890
|
appendLog(
|
|
@@ -14406,7 +14920,7 @@ async function handleStop(payload, tool) {
|
|
|
14406
14920
|
);
|
|
14407
14921
|
}
|
|
14408
14922
|
async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
14409
|
-
await
|
|
14923
|
+
await acquireLock2(repoRoot, tool);
|
|
14410
14924
|
try {
|
|
14411
14925
|
const state = await readSessionState(repoRoot, tool, sessionId);
|
|
14412
14926
|
if (!state) return "no-state";
|
|
@@ -14424,19 +14938,19 @@ async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
|
14424
14938
|
);
|
|
14425
14939
|
return "failed";
|
|
14426
14940
|
} finally {
|
|
14427
|
-
await
|
|
14941
|
+
await releaseLock2(repoRoot, tool);
|
|
14428
14942
|
}
|
|
14429
14943
|
}
|
|
14430
14944
|
async function handleSessionEnd(payload, tool) {
|
|
14431
|
-
const cwd =
|
|
14432
|
-
const sessionId =
|
|
14945
|
+
const cwd = resolveCwd2(payload);
|
|
14946
|
+
const sessionId = resolveSessionId2(payload);
|
|
14433
14947
|
const project = cwd ? await findProjectForCwd(cwd) : null;
|
|
14434
14948
|
const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
|
|
14435
14949
|
const repoRoots = [];
|
|
14436
14950
|
if (sessionId) {
|
|
14437
14951
|
const states = await listSessionStatesForSession(tool, sessionId);
|
|
14438
14952
|
for (const { state } of states) {
|
|
14439
|
-
repoRoots.push(
|
|
14953
|
+
repoRoots.push(path16.resolve(state.repoRoot));
|
|
14440
14954
|
}
|
|
14441
14955
|
}
|
|
14442
14956
|
if (repoRoots.length === 0 && cwd) {
|
|
@@ -14463,7 +14977,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
|
14463
14977
|
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
14464
14978
|
var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
|
|
14465
14979
|
function newFlowId() {
|
|
14466
|
-
return
|
|
14980
|
+
return crypto3.randomBytes(3).toString("hex");
|
|
14467
14981
|
}
|
|
14468
14982
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
14469
14983
|
"claude",
|
|
@@ -14472,7 +14986,7 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
|
14472
14986
|
"cursor",
|
|
14473
14987
|
"opencode"
|
|
14474
14988
|
]);
|
|
14475
|
-
function
|
|
14989
|
+
function classifyHookEvent2(event) {
|
|
14476
14990
|
switch (event) {
|
|
14477
14991
|
case "SessionStart":
|
|
14478
14992
|
case "sessionStart":
|
|
@@ -14507,7 +15021,7 @@ async function readStdin2() {
|
|
|
14507
15021
|
}
|
|
14508
15022
|
return Buffer.concat(chunks).toString("utf-8");
|
|
14509
15023
|
}
|
|
14510
|
-
function
|
|
15024
|
+
function resolveCwd3(payload) {
|
|
14511
15025
|
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
14512
15026
|
}
|
|
14513
15027
|
async function repairHookForTool(repoRoot, tool) {
|
|
@@ -14522,7 +15036,7 @@ async function repairHookForTool(repoRoot, tool) {
|
|
|
14522
15036
|
}
|
|
14523
15037
|
async function selfHealHook2(payload, tool) {
|
|
14524
15038
|
if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
|
|
14525
|
-
const cwd =
|
|
15039
|
+
const cwd = resolveCwd3(payload);
|
|
14526
15040
|
if (!cwd) return;
|
|
14527
15041
|
try {
|
|
14528
15042
|
const project = await findProjectForCwd(cwd);
|
|
@@ -14546,7 +15060,7 @@ async function resolveLegacyBareTool(raw) {
|
|
|
14546
15060
|
);
|
|
14547
15061
|
return null;
|
|
14548
15062
|
}
|
|
14549
|
-
const cwd =
|
|
15063
|
+
const cwd = resolveCwd3(payload);
|
|
14550
15064
|
if (!cwd) {
|
|
14551
15065
|
appendLog(
|
|
14552
15066
|
"error",
|
|
@@ -14705,6 +15219,7 @@ async function runGitTracesWorker() {
|
|
|
14705
15219
|
return;
|
|
14706
15220
|
}
|
|
14707
15221
|
const event = payload.hook_event_name;
|
|
15222
|
+
const eventKind = classifyHookEvent2(event);
|
|
14708
15223
|
appendLog("info", `git-traces worker: handling event=${event}`);
|
|
14709
15224
|
if (tool === "claude" && typeof payload.cursor_version === "string") {
|
|
14710
15225
|
appendLog(
|
|
@@ -14713,9 +15228,9 @@ async function runGitTracesWorker() {
|
|
|
14713
15228
|
);
|
|
14714
15229
|
return;
|
|
14715
15230
|
}
|
|
14716
|
-
await selfHealHook2(payload, tool);
|
|
14717
15231
|
try {
|
|
14718
|
-
|
|
15232
|
+
await selfHealHook2(payload, tool);
|
|
15233
|
+
switch (eventKind) {
|
|
14719
15234
|
case "sessionStart":
|
|
14720
15235
|
await handleSessionStart(payload, tool);
|
|
14721
15236
|
break;
|
|
@@ -14736,19 +15251,27 @@ async function runGitTracesWorker() {
|
|
|
14736
15251
|
`git-traces worker: unexpected error: ${detail}${stack ? `
|
|
14737
15252
|
${stack}` : ""}`
|
|
14738
15253
|
);
|
|
15254
|
+
} finally {
|
|
15255
|
+
if (eventKind === "stop" || eventKind === "sessionEnd") {
|
|
15256
|
+
await recordDebugLogCompletion({
|
|
15257
|
+
kind: "git",
|
|
15258
|
+
tool,
|
|
15259
|
+
payload
|
|
15260
|
+
});
|
|
15261
|
+
}
|
|
14739
15262
|
}
|
|
14740
15263
|
}
|
|
14741
15264
|
|
|
14742
15265
|
// src/outputs/zip.ts
|
|
14743
|
-
import
|
|
14744
|
-
import
|
|
15266
|
+
import fs14 from "fs";
|
|
15267
|
+
import path18 from "path";
|
|
14745
15268
|
import archiver2 from "archiver";
|
|
14746
15269
|
|
|
14747
15270
|
// src/outputs/downloads.ts
|
|
14748
15271
|
import { execSync as execSync2 } from "child_process";
|
|
14749
|
-
import
|
|
15272
|
+
import fs13 from "fs";
|
|
14750
15273
|
import os8 from "os";
|
|
14751
|
-
import
|
|
15274
|
+
import path17 from "path";
|
|
14752
15275
|
function getDownloadsFolder() {
|
|
14753
15276
|
const home = os8.homedir();
|
|
14754
15277
|
if (process.platform === "linux") {
|
|
@@ -14757,12 +15280,12 @@ function getDownloadsFolder() {
|
|
|
14757
15280
|
encoding: "utf-8",
|
|
14758
15281
|
timeout: 3e3
|
|
14759
15282
|
}).trim();
|
|
14760
|
-
if (xdgDir &&
|
|
15283
|
+
if (xdgDir && fs13.existsSync(xdgDir)) return xdgDir;
|
|
14761
15284
|
} catch {
|
|
14762
15285
|
}
|
|
14763
15286
|
}
|
|
14764
|
-
const downloads =
|
|
14765
|
-
if (
|
|
15287
|
+
const downloads = path17.join(home, "Downloads");
|
|
15288
|
+
if (fs13.existsSync(downloads)) return downloads;
|
|
14766
15289
|
return home;
|
|
14767
15290
|
}
|
|
14768
15291
|
|
|
@@ -14771,11 +15294,11 @@ function sanitizeFilename(name) {
|
|
|
14771
15294
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
14772
15295
|
}
|
|
14773
15296
|
function getUniqueFilename(dir, base, ext) {
|
|
14774
|
-
let candidate =
|
|
14775
|
-
if (!
|
|
15297
|
+
let candidate = path18.join(dir, `${base}${ext}`);
|
|
15298
|
+
if (!fs14.existsSync(candidate)) return candidate;
|
|
14776
15299
|
let i = 1;
|
|
14777
|
-
while (
|
|
14778
|
-
candidate =
|
|
15300
|
+
while (fs14.existsSync(candidate)) {
|
|
15301
|
+
candidate = path18.join(dir, `${base}-${i}${ext}`);
|
|
14779
15302
|
i++;
|
|
14780
15303
|
}
|
|
14781
15304
|
return candidate;
|
|
@@ -14785,13 +15308,13 @@ var ZipOutput = class {
|
|
|
14785
15308
|
label = "Save as .zip to Downloads";
|
|
14786
15309
|
async emit(group, options) {
|
|
14787
15310
|
const downloadsDir = getDownloadsFolder();
|
|
14788
|
-
const repoName = sanitizeFilename(
|
|
15311
|
+
const repoName = sanitizeFilename(path18.basename(group.repoPath));
|
|
14789
15312
|
const timeRange = options.timeRange;
|
|
14790
15313
|
const rangePart = timeRange?.label ?? "all";
|
|
14791
15314
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
14792
15315
|
const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
|
|
14793
15316
|
const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
|
|
14794
|
-
const output =
|
|
15317
|
+
const output = fs14.createWriteStream(outputPath);
|
|
14795
15318
|
const archive = archiver2("zip", { zlib: { level: 6 } });
|
|
14796
15319
|
const done = new Promise((resolve, reject) => {
|
|
14797
15320
|
output.on("close", resolve);
|
|
@@ -14985,15 +15508,15 @@ async function confirmExport(group, output) {
|
|
|
14985
15508
|
}
|
|
14986
15509
|
|
|
14987
15510
|
// src/sources/claude.ts
|
|
14988
|
-
import
|
|
15511
|
+
import fs15 from "fs";
|
|
14989
15512
|
import os9 from "os";
|
|
14990
|
-
import
|
|
15513
|
+
import path19 from "path";
|
|
14991
15514
|
import readline from "readline";
|
|
14992
15515
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
14993
15516
|
async function resolveRepoPath(projectDir) {
|
|
14994
|
-
const indexPath =
|
|
15517
|
+
const indexPath = path19.join(projectDir, "sessions-index.json");
|
|
14995
15518
|
try {
|
|
14996
|
-
const raw = await
|
|
15519
|
+
const raw = await fs15.promises.readFile(indexPath, "utf-8");
|
|
14997
15520
|
const data = JSON.parse(raw);
|
|
14998
15521
|
if (data.originalPath && typeof data.originalPath === "string") {
|
|
14999
15522
|
return data.originalPath;
|
|
@@ -15001,12 +15524,12 @@ async function resolveRepoPath(projectDir) {
|
|
|
15001
15524
|
} catch {
|
|
15002
15525
|
}
|
|
15003
15526
|
const cwdCounts = /* @__PURE__ */ new Map();
|
|
15004
|
-
const entries = await
|
|
15527
|
+
const entries = await fs15.promises.readdir(projectDir, {
|
|
15005
15528
|
withFileTypes: true
|
|
15006
15529
|
});
|
|
15007
15530
|
for (const entry of entries) {
|
|
15008
15531
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
15009
|
-
const cwd = await extractCwdFromJsonl(
|
|
15532
|
+
const cwd = await extractCwdFromJsonl(path19.join(projectDir, entry.name));
|
|
15010
15533
|
if (cwd) {
|
|
15011
15534
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
15012
15535
|
}
|
|
@@ -15025,7 +15548,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
15025
15548
|
return null;
|
|
15026
15549
|
}
|
|
15027
15550
|
async function extractCwdFromJsonl(filePath) {
|
|
15028
|
-
const stream =
|
|
15551
|
+
const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
|
|
15029
15552
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15030
15553
|
try {
|
|
15031
15554
|
for await (const line of rl) {
|
|
@@ -15047,12 +15570,12 @@ async function extractCwdFromJsonl(filePath) {
|
|
|
15047
15570
|
async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
15048
15571
|
let entries;
|
|
15049
15572
|
try {
|
|
15050
|
-
entries = await
|
|
15573
|
+
entries = await fs15.promises.readdir(dir, { withFileTypes: true });
|
|
15051
15574
|
} catch {
|
|
15052
15575
|
return;
|
|
15053
15576
|
}
|
|
15054
15577
|
for (const entry of entries) {
|
|
15055
|
-
const fullPath =
|
|
15578
|
+
const fullPath = path19.join(dir, entry.name);
|
|
15056
15579
|
if (entry.isDirectory()) {
|
|
15057
15580
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
15058
15581
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -15074,19 +15597,19 @@ function fallbackDecode(encodedName) {
|
|
|
15074
15597
|
var ClaudeSource = class {
|
|
15075
15598
|
name = "claude";
|
|
15076
15599
|
async scan() {
|
|
15077
|
-
const baseDir =
|
|
15600
|
+
const baseDir = path19.join(os9.homedir(), ".claude", "projects");
|
|
15078
15601
|
try {
|
|
15079
|
-
await
|
|
15602
|
+
await fs15.promises.access(baseDir);
|
|
15080
15603
|
} catch {
|
|
15081
15604
|
return [];
|
|
15082
15605
|
}
|
|
15083
|
-
const projectDirs = await
|
|
15606
|
+
const projectDirs = await fs15.promises.readdir(baseDir, {
|
|
15084
15607
|
withFileTypes: true
|
|
15085
15608
|
});
|
|
15086
15609
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
15087
15610
|
const resultArrays = await Promise.all(
|
|
15088
15611
|
dirEntries.map(async (dir) => {
|
|
15089
|
-
const projectPath =
|
|
15612
|
+
const projectPath = path19.join(baseDir, dir.name);
|
|
15090
15613
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
15091
15614
|
const files = [];
|
|
15092
15615
|
await collectFiles(
|
|
@@ -15104,12 +15627,12 @@ var ClaudeSource = class {
|
|
|
15104
15627
|
};
|
|
15105
15628
|
|
|
15106
15629
|
// src/sources/codex.ts
|
|
15107
|
-
import
|
|
15630
|
+
import fs16 from "fs";
|
|
15108
15631
|
import os10 from "os";
|
|
15109
|
-
import
|
|
15632
|
+
import path20 from "path";
|
|
15110
15633
|
import readline2 from "readline";
|
|
15111
15634
|
async function parseSessionMeta(filePath) {
|
|
15112
|
-
const stream =
|
|
15635
|
+
const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
|
|
15113
15636
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15114
15637
|
try {
|
|
15115
15638
|
for await (const line of rl) {
|
|
@@ -15134,12 +15657,12 @@ async function findJsonlFiles(dir) {
|
|
|
15134
15657
|
async function walk(d) {
|
|
15135
15658
|
let entries;
|
|
15136
15659
|
try {
|
|
15137
|
-
entries = await
|
|
15660
|
+
entries = await fs16.promises.readdir(d, { withFileTypes: true });
|
|
15138
15661
|
} catch {
|
|
15139
15662
|
return;
|
|
15140
15663
|
}
|
|
15141
15664
|
for (const entry of entries) {
|
|
15142
|
-
const full =
|
|
15665
|
+
const full = path20.join(d, entry.name);
|
|
15143
15666
|
if (entry.isDirectory()) {
|
|
15144
15667
|
await walk(full);
|
|
15145
15668
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -15153,11 +15676,11 @@ async function findJsonlFiles(dir) {
|
|
|
15153
15676
|
async function loadHistory(historyPath) {
|
|
15154
15677
|
const map = /* @__PURE__ */ new Map();
|
|
15155
15678
|
try {
|
|
15156
|
-
await
|
|
15679
|
+
await fs16.promises.access(historyPath);
|
|
15157
15680
|
} catch {
|
|
15158
15681
|
return map;
|
|
15159
15682
|
}
|
|
15160
|
-
const stream =
|
|
15683
|
+
const stream = fs16.createReadStream(historyPath, { encoding: "utf-8" });
|
|
15161
15684
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15162
15685
|
try {
|
|
15163
15686
|
for await (const line of rl) {
|
|
@@ -15184,14 +15707,14 @@ async function loadHistory(historyPath) {
|
|
|
15184
15707
|
var CodexSource = class {
|
|
15185
15708
|
name = "codex";
|
|
15186
15709
|
async scan() {
|
|
15187
|
-
const codexDir =
|
|
15188
|
-
const sessionsDir =
|
|
15710
|
+
const codexDir = path20.join(os10.homedir(), ".codex");
|
|
15711
|
+
const sessionsDir = path20.join(codexDir, "sessions");
|
|
15189
15712
|
try {
|
|
15190
|
-
await
|
|
15713
|
+
await fs16.promises.access(sessionsDir);
|
|
15191
15714
|
} catch {
|
|
15192
15715
|
return [];
|
|
15193
15716
|
}
|
|
15194
|
-
const historyPath =
|
|
15717
|
+
const historyPath = path20.join(codexDir, "history.jsonl");
|
|
15195
15718
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
15196
15719
|
findJsonlFiles(sessionsDir),
|
|
15197
15720
|
loadHistory(historyPath)
|
|
@@ -15214,8 +15737,8 @@ var CodexSource = class {
|
|
|
15214
15737
|
});
|
|
15215
15738
|
const historyLines = historyMap.get(meta.sessionId);
|
|
15216
15739
|
if (historyLines) {
|
|
15217
|
-
const sessionDir =
|
|
15218
|
-
const historyAbsPath =
|
|
15740
|
+
const sessionDir = path20.relative(sessionsDir, path20.dirname(filePath));
|
|
15741
|
+
const historyAbsPath = path20.join(
|
|
15219
15742
|
sessionsDir,
|
|
15220
15743
|
sessionDir,
|
|
15221
15744
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -15235,18 +15758,18 @@ var CodexSource = class {
|
|
|
15235
15758
|
};
|
|
15236
15759
|
|
|
15237
15760
|
// src/sources/copilotChat.ts
|
|
15238
|
-
import
|
|
15761
|
+
import fs17 from "fs";
|
|
15239
15762
|
import os11 from "os";
|
|
15240
|
-
import
|
|
15763
|
+
import path21 from "path";
|
|
15241
15764
|
import { fileURLToPath } from "url";
|
|
15242
15765
|
function vsCodeUserDirs() {
|
|
15243
15766
|
const home = os11.homedir();
|
|
15244
15767
|
const dirs = [
|
|
15245
|
-
|
|
15246
|
-
|
|
15768
|
+
path21.join(home, "Library", "Application Support", "Code", "User"),
|
|
15769
|
+
path21.join(home, ".config", "Code", "User")
|
|
15247
15770
|
];
|
|
15248
15771
|
if (process.env.APPDATA) {
|
|
15249
|
-
dirs.push(
|
|
15772
|
+
dirs.push(path21.join(process.env.APPDATA, "Code", "User"));
|
|
15250
15773
|
}
|
|
15251
15774
|
return dirs;
|
|
15252
15775
|
}
|
|
@@ -15261,7 +15784,7 @@ function uriToFsPath(uri) {
|
|
|
15261
15784
|
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
15262
15785
|
let raw;
|
|
15263
15786
|
try {
|
|
15264
|
-
raw = await
|
|
15787
|
+
raw = await fs17.promises.readFile(workspaceJsonPath, "utf-8");
|
|
15265
15788
|
} catch {
|
|
15266
15789
|
return null;
|
|
15267
15790
|
}
|
|
@@ -15283,10 +15806,10 @@ var CopilotChatSource = class {
|
|
|
15283
15806
|
async scan() {
|
|
15284
15807
|
const results = [];
|
|
15285
15808
|
for (const userDir of vsCodeUserDirs()) {
|
|
15286
|
-
const workspaceStorage =
|
|
15809
|
+
const workspaceStorage = path21.join(userDir, "workspaceStorage");
|
|
15287
15810
|
let hashDirs;
|
|
15288
15811
|
try {
|
|
15289
|
-
hashDirs = await
|
|
15812
|
+
hashDirs = await fs17.promises.readdir(workspaceStorage, {
|
|
15290
15813
|
withFileTypes: true
|
|
15291
15814
|
});
|
|
15292
15815
|
} catch {
|
|
@@ -15294,22 +15817,22 @@ var CopilotChatSource = class {
|
|
|
15294
15817
|
}
|
|
15295
15818
|
for (const hash of hashDirs) {
|
|
15296
15819
|
if (!hash.isDirectory()) continue;
|
|
15297
|
-
const wsRoot =
|
|
15298
|
-
const transcriptsDir =
|
|
15820
|
+
const wsRoot = path21.join(workspaceStorage, hash.name);
|
|
15821
|
+
const transcriptsDir = path21.join(
|
|
15299
15822
|
wsRoot,
|
|
15300
15823
|
"GitHub.copilot-chat",
|
|
15301
15824
|
"transcripts"
|
|
15302
15825
|
);
|
|
15303
15826
|
let transcriptEntries;
|
|
15304
15827
|
try {
|
|
15305
|
-
transcriptEntries = await
|
|
15828
|
+
transcriptEntries = await fs17.promises.readdir(transcriptsDir, {
|
|
15306
15829
|
withFileTypes: true
|
|
15307
15830
|
});
|
|
15308
15831
|
} catch {
|
|
15309
15832
|
continue;
|
|
15310
15833
|
}
|
|
15311
15834
|
const repoPath = await readWorkspaceFolder(
|
|
15312
|
-
|
|
15835
|
+
path21.join(wsRoot, "workspace.json")
|
|
15313
15836
|
);
|
|
15314
15837
|
if (!repoPath) continue;
|
|
15315
15838
|
for (const entry of transcriptEntries) {
|
|
@@ -15317,7 +15840,7 @@ var CopilotChatSource = class {
|
|
|
15317
15840
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
15318
15841
|
results.push({
|
|
15319
15842
|
sourceName: this.name,
|
|
15320
|
-
absolutePath:
|
|
15843
|
+
absolutePath: path21.join(transcriptsDir, entry.name),
|
|
15321
15844
|
repoPath,
|
|
15322
15845
|
metadata: { sessionId }
|
|
15323
15846
|
});
|
|
@@ -15357,7 +15880,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
15357
15880
|
async function filterByTimeRange(group, range) {
|
|
15358
15881
|
const results = await Promise.all(
|
|
15359
15882
|
group.files.map(
|
|
15360
|
-
(f) =>
|
|
15883
|
+
(f) => fs18.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
15361
15884
|
)
|
|
15362
15885
|
);
|
|
15363
15886
|
const filtered = [];
|
|
@@ -15384,10 +15907,10 @@ async function runInteractive() {
|
|
|
15384
15907
|
s.start(`Scanning ${source.name} logs...`);
|
|
15385
15908
|
const allFiles = await source.scan();
|
|
15386
15909
|
const allGroups = await mergeByRepo(allFiles);
|
|
15387
|
-
const repoRoot =
|
|
15910
|
+
const repoRoot = path22.resolve(repo.root);
|
|
15388
15911
|
const matching = allGroups.filter((g) => {
|
|
15389
|
-
const resolved =
|
|
15390
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
15912
|
+
const resolved = path22.resolve(g.repoPath);
|
|
15913
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path22.sep);
|
|
15391
15914
|
});
|
|
15392
15915
|
if (matching.length === 0) {
|
|
15393
15916
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -15418,7 +15941,7 @@ async function runInteractive() {
|
|
|
15418
15941
|
}
|
|
15419
15942
|
}
|
|
15420
15943
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
15421
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
15944
|
+
const envFilePaths = envFileNames.map((n) => path22.join(repoRoot, n));
|
|
15422
15945
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
15423
15946
|
const secretResult = await collectSecrets(
|
|
15424
15947
|
repoRoot,
|