hillclimb 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1037 -585
  2. 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 fs17 from "fs";
5
- import path21 from "path";
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 fs9 from "fs";
2152
+ import fs10 from "fs";
2153
2153
  import os5 from "os";
2154
- import path12 from "path";
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/normalizer/index.ts
10848
- import path9 from "path";
10852
+ // src/outputs/platform.ts
10853
+ import { PassThrough } from "stream";
10854
+ import archiver from "archiver";
10849
10855
 
10850
- // src/normalizer/claude.ts
10851
- function stringify(value) {
10852
- if (typeof value === "string") return value;
10853
- try {
10854
- return JSON.stringify(value);
10855
- } catch {
10856
- return String(value);
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 extractTextReasoningToolUses(content) {
10860
- if (typeof content === "string") {
10861
- return [content.trim(), void 0, []];
10862
- }
10863
- const textParts = [];
10864
- const reasoningParts = [];
10865
- const toolBlocks = [];
10866
- if (Array.isArray(content)) {
10867
- for (const block of content) {
10868
- if (typeof block !== "object" || block === null) {
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
- function buildMetrics(usage) {
10906
- if (typeof usage !== "object" || usage === null) return void 0;
10907
- const u = usage;
10908
- const cachedTokens = u.cache_read_input_tokens || 0;
10909
- const creation = u.cache_creation_input_tokens || 0;
10910
- const inputTokens = u.input_tokens || 0;
10911
- const promptTokens = inputTokens + cachedTokens + creation;
10912
- const completionTokens = u.output_tokens || 0;
10913
- const extra = {};
10914
- for (const [key, value] of Object.entries(u)) {
10915
- if (key === "input_tokens" || key === "output_tokens") continue;
10916
- extra[key] = value;
10917
- }
10918
- return {
10919
- prompt_tokens: promptTokens,
10920
- completion_tokens: completionTokens,
10921
- cached_tokens: cachedTokens,
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
- function formatToolResult(block, toolUseResult) {
10926
- const parts = [];
10927
- const content = block.content;
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
- let metadata;
10939
- if (toolUseResult && typeof toolUseResult === "object") {
10940
- metadata = { tool_use_result: toolUseResult };
10941
- const stdout = toolUseResult.stdout;
10942
- const stderr = toolUseResult.stderr;
10943
- const exitCode = toolUseResult.exitCode ?? toolUseResult.exit_code;
10944
- const interrupted = toolUseResult.interrupted;
10945
- const isImage = toolUseResult.isImage;
10946
- const formatted = [];
10947
- if (stdout) formatted.push(`[stdout]
10948
- ${stdout}`.trimEnd());
10949
- if (stderr) formatted.push(`[stderr]
10950
- ${stderr}`.trimEnd());
10951
- if (exitCode !== void 0 && exitCode !== null && exitCode !== 0)
10952
- formatted.push(`[exit_code] ${exitCode}`);
10953
- if (interrupted) formatted.push(`[interrupted] ${interrupted}`);
10954
- if (isImage) formatted.push(`[is_image] ${isImage}`);
10955
- const skipKeys = /* @__PURE__ */ new Set([
10956
- "stdout",
10957
- "stderr",
10958
- "exitCode",
10959
- "exit_code",
10960
- "interrupted",
10961
- "isImage"
10962
- ]);
10963
- const remainingMeta = {};
10964
- for (const [k, v] of Object.entries(toolUseResult)) {
10965
- if (!skipKeys.has(k)) remainingMeta[k] = v;
10966
- }
10967
- if (Object.keys(remainingMeta).length > 0) {
10968
- formatted.push(`[metadata] ${JSON.stringify(remainingMeta)}`);
10969
- }
10970
- if (formatted.length > 0) {
10971
- parts.push(formatted.filter(Boolean).join("\n"));
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
- if (block.is_error === true) {
10975
- parts.push("[error] tool reported failure");
10976
- metadata = metadata || {};
10977
- metadata.is_error = true;
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
- if (metadata !== void 0) {
10980
- if (!("raw_tool_result" in metadata)) {
10981
- metadata.raw_tool_result = block;
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
- const resultText = parts.filter(Boolean).join("\n\n").trim();
10985
- return [resultText || void 0, metadata];
10970
+ return repoPath;
10986
10971
  }
10987
- function convertClaudeToTrajectory(jsonlContent, sessionId) {
10988
- const rawEvents = [];
10989
- for (const line of jsonlContent.split("\n")) {
10990
- const trimmed = line.trim();
10991
- if (!trimmed) continue;
10992
- try {
10993
- rawEvents.push(JSON.parse(trimmed));
10994
- } catch {
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
- if (rawEvents.length === 0) return null;
10998
- const events = rawEvents.sort(
10999
- (a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? "")
11000
- );
11001
- if (events.length === 0) return null;
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") {
@@ -12450,308 +13026,126 @@ function convertRunEventsToTrajectory(events, sessionId) {
12450
13026
  }
12451
13027
  }
12452
13028
  const steps = [];
12453
- for (const turn of turns) {
12454
- const parts = [...turn.parts];
12455
- if (turn.finish) {
12456
- parts.push({ ...turn.finish, type: "step-finish" });
12457
- }
12458
- const step = buildAgentStep(
12459
- parts,
12460
- { time: { created: turn.timestamp } },
12461
- steps.length + 1,
12462
- void 0
12463
- );
12464
- if (step) steps.push(step);
12465
- }
12466
- if (steps.length === 0) return null;
12467
- return {
12468
- schema_version: ATIF_VERSION,
12469
- session_id: session,
12470
- agent: {
12471
- name: "opencode",
12472
- 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);
13029
+ for (const turn of turns) {
13030
+ const parts = [...turn.parts];
13031
+ if (turn.finish) {
13032
+ parts.push({ ...turn.finish, type: "step-finish" });
13033
+ }
13034
+ const step = buildAgentStep(
13035
+ parts,
13036
+ { time: { created: turn.timestamp } },
13037
+ steps.length + 1,
13038
+ void 0
13039
+ );
13040
+ if (step) steps.push(step);
12677
13041
  }
12678
- return resolved;
13042
+ if (steps.length === 0) return null;
13043
+ return {
13044
+ schema_version: ATIF_VERSION,
13045
+ session_id: session,
13046
+ agent: {
13047
+ name: "opencode",
13048
+ version: "unknown"
13049
+ },
13050
+ steps,
13051
+ final_metrics: finalMetricsFromSteps2(steps)
13052
+ };
12679
13053
  }
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;
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
+ );
12691
13060
  }
12692
- async function mergeByRepo(files) {
12693
- const grouped = /* @__PURE__ */ new Map();
12694
- for (const file of files) {
12695
- const key = canonicalizePath(file.repoPath);
12696
- const existing = grouped.get(key);
12697
- if (existing) {
12698
- existing.push({ ...file, repoPath: key });
12699
- } else {
12700
- grouped.set(key, [{ ...file, repoPath: key }]);
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 allPaths = [...grouped.keys()];
12704
- const groups = [];
12705
- for (const [repoPath, groupFiles] of grouped) {
12706
- const stats = await Promise.all(
12707
- groupFiles.map((f) => fs8.promises.stat(f.absolutePath).catch(() => null))
12708
- );
12709
- let lastModified = /* @__PURE__ */ new Date(0);
12710
- for (const stat of stats) {
12711
- if (stat && stat.mtime > lastModified) lastModified = stat.mtime;
12712
- }
12713
- const sourceNames = [...new Set(groupFiles.map((f) => f.sourceName))];
12714
- groups.push({
12715
- repoPath,
12716
- label: computeLabel(repoPath, allPaths),
12717
- files: groupFiles,
12718
- sourceNames,
12719
- lastModified
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
- async function preloadFiles(group) {
12726
- const files = await Promise.all(
12727
- group.files.map(async (file) => {
12728
- if (file.content) return file;
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 buf = await fs8.promises.readFile(file.absolutePath);
12731
- const checkLen = Math.min(buf.length, 8192);
12732
- for (let i = 0; i < checkLen; i++) {
12733
- if (buf[i] === 0) {
12734
- return { ...file, content: buf, isBinary: true };
12735
- }
12736
- }
12737
- return { ...file, content: buf };
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
- onProgress?.(`Compressing ${processed.files.length} files`);
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 sanitize(value) {
13159
+ function sanitize2(value) {
12766
13160
  return value.replace(/[^a-zA-Z0-9._-]/g, "_");
12767
13161
  }
12768
- function formatEpochSeconds(date) {
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 = fs9.createReadStream(transcriptPath, { encoding: "utf-8" });
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 path12.join(
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 = path12.resolve(transcriptPath);
13273
+ const transcriptResolved = path13.resolve(transcriptPath);
12863
13274
  try {
12864
- const stat = await fs9.promises.stat(transcriptResolved);
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: path12.basename(repoRoot),
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) => path12.join(repoRoot, 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 toolLabel = toolLabels[sourceTool] ?? "Claude";
12936
- const epochSeconds = formatEpochSeconds(now);
12937
- const title = `${toolLabel} session ${shortId} \u2014 ${epochSeconds}`;
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: ${toolLabel}
13350
+ Tool: ${toolLabel2}
12940
13351
  Repo: ${repoRoot}
12941
13352
  Uploaded: ${now.toISOString()}`;
12942
- const zipFilename = `${sourceTool}-${sanitize(shortId)}-${epochSeconds}.zip`;
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 crypto2 from "crypto";
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 path15 from "path";
13504
+ import path16 from "path";
13088
13505
 
13089
13506
  // src/git-traces/git-ops.ts
13090
13507
  import { execFileSync } from "child_process";
13091
- import fs10 from "fs";
13508
+ import fs11 from "fs";
13092
13509
  import os6 from "os";
13093
- import path13 from "path";
13510
+ import path14 from "path";
13094
13511
  import { gzipSync } from "zlib";
13095
13512
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
13096
13513
  var EXEC_OPTS = {
@@ -13214,7 +13631,7 @@ function buildUntrackedTree(repoRoot) {
13214
13631
  for (const relPath of list.split("\0")) {
13215
13632
  if (!relPath) continue;
13216
13633
  try {
13217
- const stat = fs10.lstatSync(path13.join(repoRoot, relPath));
13634
+ const stat = fs11.lstatSync(path14.join(repoRoot, relPath));
13218
13635
  if (stat.size > MAX_UNTRACKED_FILE_BYTES) {
13219
13636
  appendLog(
13220
13637
  "warn",
@@ -13227,7 +13644,7 @@ function buildUntrackedTree(repoRoot) {
13227
13644
  }
13228
13645
  }
13229
13646
  if (kept.length === 0) return null;
13230
- const tmpIndex = path13.join(
13647
+ const tmpIndex = path14.join(
13231
13648
  os6.tmpdir(),
13232
13649
  `hillclimb-untracked-${Date.now()}-${process.pid}`
13233
13650
  );
@@ -13240,7 +13657,7 @@ function buildUntrackedTree(repoRoot) {
13240
13657
  return gitWithEnv(repoRoot, ["write-tree"], env);
13241
13658
  } finally {
13242
13659
  try {
13243
- fs10.unlinkSync(tmpIndex);
13660
+ fs11.unlinkSync(tmpIndex);
13244
13661
  } catch {
13245
13662
  }
13246
13663
  }
@@ -13251,7 +13668,7 @@ function buildSnapshotTree(repoRoot, stashSha) {
13251
13668
  if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA) {
13252
13669
  return trackedTree;
13253
13670
  }
13254
- const tmpIndex = path13.join(
13671
+ const tmpIndex = path14.join(
13255
13672
  os6.tmpdir(),
13256
13673
  `hillclimb-index-${Date.now()}-${process.pid}`
13257
13674
  );
@@ -13279,7 +13696,7 @@ function buildSnapshotTree(repoRoot, stashSha) {
13279
13696
  return gitWithEnv(repoRoot, ["write-tree"], env);
13280
13697
  } finally {
13281
13698
  try {
13282
- fs10.unlinkSync(tmpIndex);
13699
+ fs11.unlinkSync(tmpIndex);
13283
13700
  } catch {
13284
13701
  }
13285
13702
  }
@@ -13293,16 +13710,16 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
13293
13710
  ]);
13294
13711
  const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
13295
13712
  pinRef(repoRoot, orphanRef, orphanCommit);
13296
- const tmpFile = path13.join(
13713
+ const tmpFile = path14.join(
13297
13714
  os6.tmpdir(),
13298
13715
  `hillclimb-bundle-${Date.now()}.bundle`
13299
13716
  );
13300
13717
  try {
13301
13718
  git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
13302
- return fs10.readFileSync(tmpFile);
13719
+ return fs11.readFileSync(tmpFile);
13303
13720
  } finally {
13304
13721
  try {
13305
- fs10.unlinkSync(tmpFile);
13722
+ fs11.unlinkSync(tmpFile);
13306
13723
  } catch {
13307
13724
  }
13308
13725
  deleteRef(repoRoot, orphanRef);
@@ -13477,9 +13894,9 @@ function parseCommitFiles(repoRoot, sha) {
13477
13894
  oldPath
13478
13895
  });
13479
13896
  } else {
13480
- const path22 = parts[parts.length - 1];
13481
- indexByPath.set(path22, files.length);
13482
- files.push({ path: path22, status, additions: 0, deletions: 0 });
13897
+ const path23 = parts[parts.length - 1];
13898
+ indexByPath.set(path23, files.length);
13899
+ files.push({ path: path23, status, additions: 0, deletions: 0 });
13483
13900
  }
13484
13901
  }
13485
13902
  for (const line of numstat.split("\n")) {
@@ -13545,31 +13962,31 @@ function cleanupSessionRefs(repoRoot, sessionId) {
13545
13962
  }
13546
13963
 
13547
13964
  // src/git-traces/session-state.ts
13548
- import crypto from "crypto";
13549
- import fs11 from "fs";
13965
+ import crypto2 from "crypto";
13966
+ import fs12 from "fs";
13550
13967
  import os7 from "os";
13551
- import path14 from "path";
13552
- var CURRENT_SCHEMA_VERSION = 3;
13553
- var DEFAULT_STATE_DIR = path14.join(os7.homedir(), ".hillclimb", "git-traces");
13554
- var LOCK_RETRIES = 120;
13555
- var LOCK_RETRY_DELAY_MS = 500;
13556
- function stateDir() {
13968
+ import path15 from "path";
13969
+ var CURRENT_SCHEMA_VERSION2 = 3;
13970
+ var DEFAULT_STATE_DIR = path15.join(os7.homedir(), ".hillclimb", "git-traces");
13971
+ var LOCK_RETRIES2 = 120;
13972
+ var LOCK_RETRY_DELAY_MS2 = 500;
13973
+ function stateDir2() {
13557
13974
  return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR;
13558
13975
  }
13559
13976
  function stateFileForRepo(repoRoot, tool, sessionId) {
13560
- const hash = crypto.createHash("sha256").update(
13561
- sessionId ? `${path14.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path14.resolve(repoRoot)}\0${tool}`
13977
+ const hash = crypto2.createHash("sha256").update(
13978
+ sessionId ? `${path15.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path15.resolve(repoRoot)}\0${tool}`
13562
13979
  ).digest("hex").slice(0, 16);
13563
- return path14.join(stateDir(), `${hash}.json`);
13980
+ return path15.join(stateDir2(), `${hash}.json`);
13564
13981
  }
13565
13982
  function lockFileForRepo(repoRoot, tool) {
13566
13983
  return `${stateFileForRepo(repoRoot, tool)}.lock`;
13567
13984
  }
13568
13985
  async function readStateFile(file) {
13569
13986
  try {
13570
- const raw = await fs11.promises.readFile(file, "utf-8");
13987
+ const raw = await fs12.promises.readFile(file, "utf-8");
13571
13988
  const parsed = JSON.parse(raw);
13572
- if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
13989
+ if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION2) {
13573
13990
  return null;
13574
13991
  }
13575
13992
  return parsed;
@@ -13580,26 +13997,26 @@ async function readStateFile(file) {
13580
13997
  async function listScopedSessionStates(repoRoot, tool) {
13581
13998
  let entries;
13582
13999
  try {
13583
- entries = await fs11.promises.readdir(stateDir(), { withFileTypes: true });
14000
+ entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
13584
14001
  } catch {
13585
14002
  return [];
13586
14003
  }
13587
14004
  const states = [];
13588
14005
  for (const entry of entries) {
13589
14006
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
13590
- const file = path14.join(stateDir(), entry.name);
14007
+ const file = path15.join(stateDir2(), entry.name);
13591
14008
  const state = await readStateFile(file);
13592
14009
  if (!state) continue;
13593
14010
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
13594
14011
  continue;
13595
14012
  }
13596
- if (path14.resolve(state.repoRoot) !== path14.resolve(repoRoot)) continue;
13597
- if (path14.resolve(file) !== path14.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
14013
+ if (path15.resolve(state.repoRoot) !== path15.resolve(repoRoot)) continue;
14014
+ if (path15.resolve(file) !== path15.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
13598
14015
  continue;
13599
14016
  }
13600
14017
  let mtimeMs = 0;
13601
14018
  try {
13602
- mtimeMs = (await fs11.promises.stat(file)).mtimeMs;
14019
+ mtimeMs = (await fs12.promises.stat(file)).mtimeMs;
13603
14020
  } catch {
13604
14021
  continue;
13605
14022
  }
@@ -13610,26 +14027,26 @@ async function listScopedSessionStates(repoRoot, tool) {
13610
14027
  async function listSessionStatesForSession(tool, sessionId) {
13611
14028
  let entries;
13612
14029
  try {
13613
- entries = await fs11.promises.readdir(stateDir(), { withFileTypes: true });
14030
+ entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
13614
14031
  } catch {
13615
14032
  return [];
13616
14033
  }
13617
14034
  const states = [];
13618
14035
  for (const entry of entries) {
13619
14036
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
13620
- const file = path14.join(stateDir(), entry.name);
14037
+ const file = path15.join(stateDir2(), entry.name);
13621
14038
  const state = await readStateFile(file);
13622
14039
  if (!state) continue;
13623
14040
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
13624
14041
  continue;
13625
14042
  }
13626
14043
  if (state.sessionId !== sessionId) continue;
13627
- if (path14.resolve(file) !== path14.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
14044
+ if (path15.resolve(file) !== path15.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
13628
14045
  continue;
13629
14046
  }
13630
14047
  let mtimeMs = 0;
13631
14048
  try {
13632
- mtimeMs = (await fs11.promises.stat(file)).mtimeMs;
14049
+ mtimeMs = (await fs12.promises.stat(file)).mtimeMs;
13633
14050
  } catch {
13634
14051
  continue;
13635
14052
  }
@@ -13650,12 +14067,12 @@ async function readSessionState(repoRoot, tool, sessionId) {
13650
14067
  }
13651
14068
  async function writeSessionState(state, tool) {
13652
14069
  const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
13653
- await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
14070
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13654
14071
  const tmp = `${file}.tmp`;
13655
- await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
14072
+ await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
13656
14073
  mode: 384
13657
14074
  });
13658
- await fs11.promises.rename(tmp, file);
14075
+ await fs12.promises.rename(tmp, file);
13659
14076
  const legacyFile = stateFileForRepo(state.repoRoot, tool);
13660
14077
  const legacy = await readStateFile(legacyFile);
13661
14078
  if (legacy?.sessionId === state.sessionId) {
@@ -13664,7 +14081,7 @@ async function writeSessionState(state, tool) {
13664
14081
  }
13665
14082
  async function deleteStateFile(file) {
13666
14083
  try {
13667
- await fs11.promises.unlink(file);
14084
+ await fs12.promises.unlink(file);
13668
14085
  } catch {
13669
14086
  }
13670
14087
  }
@@ -13680,14 +14097,14 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
13680
14097
  }
13681
14098
  await deleteStateFile(stateFileForRepo(repoRoot, tool));
13682
14099
  }
13683
- async function acquireLock(repoRoot, tool, retries = LOCK_RETRIES, delayMs = LOCK_RETRY_DELAY_MS) {
14100
+ async function acquireLock2(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
13684
14101
  const lockPath = lockFileForRepo(repoRoot, tool);
13685
- await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
14102
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13686
14103
  for (let i = 0; i < retries; i++) {
13687
14104
  try {
13688
- const fd = await fs11.promises.open(
14105
+ const fd = await fs12.promises.open(
13689
14106
  lockPath,
13690
- fs11.constants.O_CREAT | fs11.constants.O_EXCL | fs11.constants.O_WRONLY
14107
+ fs12.constants.O_CREAT | fs12.constants.O_EXCL | fs12.constants.O_WRONLY
13691
14108
  );
13692
14109
  await fd.write(String(process.pid));
13693
14110
  await fd.close();
@@ -13702,9 +14119,9 @@ async function acquireLock(repoRoot, tool, retries = LOCK_RETRIES, delayMs = LOC
13702
14119
  }
13703
14120
  throw new Error(`Failed to acquire lock after ${retries} retries`);
13704
14121
  }
13705
- async function releaseLock(repoRoot, tool) {
14122
+ async function releaseLock2(repoRoot, tool) {
13706
14123
  try {
13707
- await fs11.promises.unlink(lockFileForRepo(repoRoot, tool));
14124
+ await fs12.promises.unlink(lockFileForRepo(repoRoot, tool));
13708
14125
  } catch {
13709
14126
  }
13710
14127
  }
@@ -13714,7 +14131,7 @@ var CLI_VERSION = "0.2.0";
13714
14131
  var GIT_TRACES_SLUG = "git-traces";
13715
14132
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
13716
14133
  var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
13717
- function formatEpochSeconds2(date) {
14134
+ function formatEpochSeconds3(date) {
13718
14135
  return String(Math.floor(date.getTime() / 1e3));
13719
14136
  }
13720
14137
  var TOOL_LABELS = {
@@ -13727,17 +14144,17 @@ var TOOL_LABELS = {
13727
14144
  async function loadConfiguredRepos() {
13728
14145
  const file = await loadProjects();
13729
14146
  return Object.entries(file.projects).map(([repoRoot, config]) => ({
13730
- repoRoot: path15.resolve(repoRoot),
14147
+ repoRoot: path16.resolve(repoRoot),
13731
14148
  config
13732
14149
  })).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
13733
14150
  }
13734
14151
  function repoLabel(repoRoot) {
13735
- return path15.basename(repoRoot) || repoRoot;
14152
+ return path16.basename(repoRoot) || repoRoot;
13736
14153
  }
13737
- function resolveCwd(payload) {
14154
+ function resolveCwd2(payload) {
13738
14155
  return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
13739
14156
  }
13740
- function resolveSessionId(payload) {
14157
+ function resolveSessionId2(payload) {
13741
14158
  return payload.session_id ?? payload.conversation_id ?? null;
13742
14159
  }
13743
14160
  function epochPrefix(epoch) {
@@ -13935,15 +14352,15 @@ async function uploadEpochBaselineArtifacts(params) {
13935
14352
  }
13936
14353
  async function createGitTracesContribution(params) {
13937
14354
  const { client, config, repoRoot, state, tool, now, artifacts } = params;
13938
- const toolLabel = TOOL_LABELS[tool] ?? "Claude";
13939
- const epochSeconds = formatEpochSeconds2(now);
14355
+ const toolLabel2 = TOOL_LABELS[tool] ?? "Claude";
14356
+ const epochSeconds = formatEpochSeconds3(now);
13940
14357
  const shortId = state.sessionId.slice(0, 12);
13941
14358
  const repoName = repoLabel(repoRoot);
13942
14359
  const contribution = await client.createContribution(config.projectId, {
13943
14360
  contributionTypeSlug: GIT_TRACES_SLUG,
13944
- title: `${toolLabel} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
14361
+ title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
13945
14362
  body: `Session ID: ${state.sessionId}
13946
- Tool: ${toolLabel}
14363
+ Tool: ${toolLabel2}
13947
14364
  Repo: ${repoRoot}
13948
14365
  Uploaded: ${now.toISOString()}`
13949
14366
  });
@@ -13997,7 +14414,7 @@ async function initializeSession(repoRoot, tool, sessionId) {
13997
14414
  });
13998
14415
  if (!frozen) return null;
13999
14416
  const state = {
14000
- schemaVersion: CURRENT_SCHEMA_VERSION,
14417
+ schemaVersion: CURRENT_SCHEMA_VERSION2,
14001
14418
  sessionId,
14002
14419
  contributionId: null,
14003
14420
  baselineSha: frozen.baselineSha,
@@ -14047,7 +14464,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
14047
14464
  );
14048
14465
  return "skipped";
14049
14466
  }
14050
- await acquireLock(repoRoot, tool);
14467
+ await acquireLock2(repoRoot, tool);
14051
14468
  try {
14052
14469
  const staleLegacy = await readSessionState(repoRoot, tool);
14053
14470
  if (staleLegacy && staleLegacy.sessionId !== sessionId) {
@@ -14078,11 +14495,11 @@ async function processSessionStartRepo(repo, tool, sessionId) {
14078
14495
  );
14079
14496
  return "failed";
14080
14497
  } finally {
14081
- await releaseLock(repoRoot, tool);
14498
+ await releaseLock2(repoRoot, tool);
14082
14499
  }
14083
14500
  }
14084
14501
  async function handleSessionStart(payload, tool) {
14085
- const cwd = resolveCwd(payload);
14502
+ const cwd = resolveCwd2(payload);
14086
14503
  if (!cwd) {
14087
14504
  appendLog("warn", "git-traces: no cwd in payload, skipping");
14088
14505
  return;
@@ -14095,7 +14512,7 @@ async function handleSessionStart(payload, tool) {
14095
14512
  );
14096
14513
  return;
14097
14514
  }
14098
- const sessionId = resolveSessionId(payload);
14515
+ const sessionId = resolveSessionId2(payload);
14099
14516
  if (!sessionId) {
14100
14517
  appendLog("warn", "git-traces: no session_id in payload, skipping");
14101
14518
  return;
@@ -14178,9 +14595,10 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
14178
14595
  );
14179
14596
  return "skipped";
14180
14597
  }
14181
- await acquireLock(repoRoot, tool);
14598
+ await acquireLock2(repoRoot, tool);
14599
+ let state = null;
14182
14600
  try {
14183
- const state = await readSessionState(repoRoot, tool, sessionId);
14601
+ state = await readSessionState(repoRoot, tool, sessionId);
14184
14602
  if (!state) {
14185
14603
  appendLog(
14186
14604
  "info",
@@ -14351,17 +14769,42 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
14351
14769
  "error",
14352
14770
  `git-traces: Stop failed for repo ${repoRoot}: ${formatError(err)}`
14353
14771
  );
14772
+ if (state && err instanceof PlatformError && err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND") {
14773
+ if (state.baselineTreeSha) {
14774
+ await writeSessionState(
14775
+ {
14776
+ ...state,
14777
+ contributionId: null,
14778
+ lastSnapshotSha: state.baselineSha,
14779
+ lastSnapshotTreeSha: state.baselineTreeSha,
14780
+ turnCount: 0
14781
+ },
14782
+ tool
14783
+ );
14784
+ appendLog(
14785
+ "warn",
14786
+ `git-traces: cleared stale contribution state (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
14787
+ );
14788
+ } else {
14789
+ cleanupSessionRefs(repoRoot, state.sessionId);
14790
+ await deleteSessionState(repoRoot, tool, state.sessionId);
14791
+ appendLog(
14792
+ "warn",
14793
+ `git-traces: deleted stale contribution state without a frozen baseline (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
14794
+ );
14795
+ }
14796
+ }
14354
14797
  return "failed";
14355
14798
  } finally {
14356
- await releaseLock(repoRoot, tool);
14799
+ await releaseLock2(repoRoot, tool);
14357
14800
  }
14358
14801
  }
14359
14802
  async function handleStop(payload, tool) {
14360
- const cwd = resolveCwd(payload);
14803
+ const cwd = resolveCwd2(payload);
14361
14804
  if (!cwd) return;
14362
14805
  const project = await findProjectForCwd(cwd);
14363
14806
  if (!project) return;
14364
- const sessionId = resolveSessionId(payload);
14807
+ const sessionId = resolveSessionId2(payload);
14365
14808
  const recordedAt = Date.now();
14366
14809
  const repos = await loadConfiguredRepos();
14367
14810
  const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
@@ -14370,7 +14813,7 @@ async function handleStop(payload, tool) {
14370
14813
  if (sessionId) {
14371
14814
  const storedStates = await listSessionStatesForSession(tool, sessionId);
14372
14815
  for (const { state } of storedStates) {
14373
- const repo = repoByRoot.get(path15.resolve(state.repoRoot));
14816
+ const repo = repoByRoot.get(path16.resolve(state.repoRoot));
14374
14817
  if (!repo) {
14375
14818
  missingConfig++;
14376
14819
  appendLog(
@@ -14406,7 +14849,7 @@ async function handleStop(payload, tool) {
14406
14849
  );
14407
14850
  }
14408
14851
  async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
14409
- await acquireLock(repoRoot, tool);
14852
+ await acquireLock2(repoRoot, tool);
14410
14853
  try {
14411
14854
  const state = await readSessionState(repoRoot, tool, sessionId);
14412
14855
  if (!state) return "no-state";
@@ -14424,19 +14867,19 @@ async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
14424
14867
  );
14425
14868
  return "failed";
14426
14869
  } finally {
14427
- await releaseLock(repoRoot, tool);
14870
+ await releaseLock2(repoRoot, tool);
14428
14871
  }
14429
14872
  }
14430
14873
  async function handleSessionEnd(payload, tool) {
14431
- const cwd = resolveCwd(payload);
14432
- const sessionId = resolveSessionId(payload);
14874
+ const cwd = resolveCwd2(payload);
14875
+ const sessionId = resolveSessionId2(payload);
14433
14876
  const project = cwd ? await findProjectForCwd(cwd) : null;
14434
14877
  const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
14435
14878
  const repoRoots = [];
14436
14879
  if (sessionId) {
14437
14880
  const states = await listSessionStatesForSession(tool, sessionId);
14438
14881
  for (const { state } of states) {
14439
- repoRoots.push(path15.resolve(state.repoRoot));
14882
+ repoRoots.push(path16.resolve(state.repoRoot));
14440
14883
  }
14441
14884
  }
14442
14885
  if (repoRoots.length === 0 && cwd) {
@@ -14463,7 +14906,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
14463
14906
  var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
14464
14907
  var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
14465
14908
  function newFlowId() {
14466
- return crypto2.randomBytes(3).toString("hex");
14909
+ return crypto3.randomBytes(3).toString("hex");
14467
14910
  }
14468
14911
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
14469
14912
  "claude",
@@ -14472,7 +14915,7 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
14472
14915
  "cursor",
14473
14916
  "opencode"
14474
14917
  ]);
14475
- function classifyHookEvent(event) {
14918
+ function classifyHookEvent2(event) {
14476
14919
  switch (event) {
14477
14920
  case "SessionStart":
14478
14921
  case "sessionStart":
@@ -14507,7 +14950,7 @@ async function readStdin2() {
14507
14950
  }
14508
14951
  return Buffer.concat(chunks).toString("utf-8");
14509
14952
  }
14510
- function resolveCwd2(payload) {
14953
+ function resolveCwd3(payload) {
14511
14954
  return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
14512
14955
  }
14513
14956
  async function repairHookForTool(repoRoot, tool) {
@@ -14522,7 +14965,7 @@ async function repairHookForTool(repoRoot, tool) {
14522
14965
  }
14523
14966
  async function selfHealHook2(payload, tool) {
14524
14967
  if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
14525
- const cwd = resolveCwd2(payload);
14968
+ const cwd = resolveCwd3(payload);
14526
14969
  if (!cwd) return;
14527
14970
  try {
14528
14971
  const project = await findProjectForCwd(cwd);
@@ -14546,7 +14989,7 @@ async function resolveLegacyBareTool(raw) {
14546
14989
  );
14547
14990
  return null;
14548
14991
  }
14549
- const cwd = resolveCwd2(payload);
14992
+ const cwd = resolveCwd3(payload);
14550
14993
  if (!cwd) {
14551
14994
  appendLog(
14552
14995
  "error",
@@ -14705,6 +15148,7 @@ async function runGitTracesWorker() {
14705
15148
  return;
14706
15149
  }
14707
15150
  const event = payload.hook_event_name;
15151
+ const eventKind = classifyHookEvent2(event);
14708
15152
  appendLog("info", `git-traces worker: handling event=${event}`);
14709
15153
  if (tool === "claude" && typeof payload.cursor_version === "string") {
14710
15154
  appendLog(
@@ -14713,9 +15157,9 @@ async function runGitTracesWorker() {
14713
15157
  );
14714
15158
  return;
14715
15159
  }
14716
- await selfHealHook2(payload, tool);
14717
15160
  try {
14718
- switch (classifyHookEvent(event)) {
15161
+ await selfHealHook2(payload, tool);
15162
+ switch (eventKind) {
14719
15163
  case "sessionStart":
14720
15164
  await handleSessionStart(payload, tool);
14721
15165
  break;
@@ -14736,19 +15180,27 @@ async function runGitTracesWorker() {
14736
15180
  `git-traces worker: unexpected error: ${detail}${stack ? `
14737
15181
  ${stack}` : ""}`
14738
15182
  );
15183
+ } finally {
15184
+ if (eventKind === "stop" || eventKind === "sessionEnd") {
15185
+ await recordDebugLogCompletion({
15186
+ kind: "git",
15187
+ tool,
15188
+ payload
15189
+ });
15190
+ }
14739
15191
  }
14740
15192
  }
14741
15193
 
14742
15194
  // src/outputs/zip.ts
14743
- import fs13 from "fs";
14744
- import path17 from "path";
15195
+ import fs14 from "fs";
15196
+ import path18 from "path";
14745
15197
  import archiver2 from "archiver";
14746
15198
 
14747
15199
  // src/outputs/downloads.ts
14748
15200
  import { execSync as execSync2 } from "child_process";
14749
- import fs12 from "fs";
15201
+ import fs13 from "fs";
14750
15202
  import os8 from "os";
14751
- import path16 from "path";
15203
+ import path17 from "path";
14752
15204
  function getDownloadsFolder() {
14753
15205
  const home = os8.homedir();
14754
15206
  if (process.platform === "linux") {
@@ -14757,12 +15209,12 @@ function getDownloadsFolder() {
14757
15209
  encoding: "utf-8",
14758
15210
  timeout: 3e3
14759
15211
  }).trim();
14760
- if (xdgDir && fs12.existsSync(xdgDir)) return xdgDir;
15212
+ if (xdgDir && fs13.existsSync(xdgDir)) return xdgDir;
14761
15213
  } catch {
14762
15214
  }
14763
15215
  }
14764
- const downloads = path16.join(home, "Downloads");
14765
- if (fs12.existsSync(downloads)) return downloads;
15216
+ const downloads = path17.join(home, "Downloads");
15217
+ if (fs13.existsSync(downloads)) return downloads;
14766
15218
  return home;
14767
15219
  }
14768
15220
 
@@ -14771,11 +15223,11 @@ function sanitizeFilename(name) {
14771
15223
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
14772
15224
  }
14773
15225
  function getUniqueFilename(dir, base, ext) {
14774
- let candidate = path17.join(dir, `${base}${ext}`);
14775
- if (!fs13.existsSync(candidate)) return candidate;
15226
+ let candidate = path18.join(dir, `${base}${ext}`);
15227
+ if (!fs14.existsSync(candidate)) return candidate;
14776
15228
  let i = 1;
14777
- while (fs13.existsSync(candidate)) {
14778
- candidate = path17.join(dir, `${base}-${i}${ext}`);
15229
+ while (fs14.existsSync(candidate)) {
15230
+ candidate = path18.join(dir, `${base}-${i}${ext}`);
14779
15231
  i++;
14780
15232
  }
14781
15233
  return candidate;
@@ -14785,13 +15237,13 @@ var ZipOutput = class {
14785
15237
  label = "Save as .zip to Downloads";
14786
15238
  async emit(group, options) {
14787
15239
  const downloadsDir = getDownloadsFolder();
14788
- const repoName = sanitizeFilename(path17.basename(group.repoPath));
15240
+ const repoName = sanitizeFilename(path18.basename(group.repoPath));
14789
15241
  const timeRange = options.timeRange;
14790
15242
  const rangePart = timeRange?.label ?? "all";
14791
15243
  const epochSeconds = Math.floor(Date.now() / 1e3);
14792
15244
  const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
14793
15245
  const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
14794
- const output = fs13.createWriteStream(outputPath);
15246
+ const output = fs14.createWriteStream(outputPath);
14795
15247
  const archive = archiver2("zip", { zlib: { level: 6 } });
14796
15248
  const done = new Promise((resolve, reject) => {
14797
15249
  output.on("close", resolve);
@@ -14985,15 +15437,15 @@ async function confirmExport(group, output) {
14985
15437
  }
14986
15438
 
14987
15439
  // src/sources/claude.ts
14988
- import fs14 from "fs";
15440
+ import fs15 from "fs";
14989
15441
  import os9 from "os";
14990
- import path18 from "path";
15442
+ import path19 from "path";
14991
15443
  import readline from "readline";
14992
15444
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
14993
15445
  async function resolveRepoPath(projectDir) {
14994
- const indexPath = path18.join(projectDir, "sessions-index.json");
15446
+ const indexPath = path19.join(projectDir, "sessions-index.json");
14995
15447
  try {
14996
- const raw = await fs14.promises.readFile(indexPath, "utf-8");
15448
+ const raw = await fs15.promises.readFile(indexPath, "utf-8");
14997
15449
  const data = JSON.parse(raw);
14998
15450
  if (data.originalPath && typeof data.originalPath === "string") {
14999
15451
  return data.originalPath;
@@ -15001,12 +15453,12 @@ async function resolveRepoPath(projectDir) {
15001
15453
  } catch {
15002
15454
  }
15003
15455
  const cwdCounts = /* @__PURE__ */ new Map();
15004
- const entries = await fs14.promises.readdir(projectDir, {
15456
+ const entries = await fs15.promises.readdir(projectDir, {
15005
15457
  withFileTypes: true
15006
15458
  });
15007
15459
  for (const entry of entries) {
15008
15460
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
15009
- const cwd = await extractCwdFromJsonl(path18.join(projectDir, entry.name));
15461
+ const cwd = await extractCwdFromJsonl(path19.join(projectDir, entry.name));
15010
15462
  if (cwd) {
15011
15463
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
15012
15464
  }
@@ -15025,7 +15477,7 @@ async function resolveRepoPath(projectDir) {
15025
15477
  return null;
15026
15478
  }
15027
15479
  async function extractCwdFromJsonl(filePath) {
15028
- const stream = fs14.createReadStream(filePath, { encoding: "utf-8" });
15480
+ const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
15029
15481
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
15030
15482
  try {
15031
15483
  for await (const line of rl) {
@@ -15047,12 +15499,12 @@ async function extractCwdFromJsonl(filePath) {
15047
15499
  async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
15048
15500
  let entries;
15049
15501
  try {
15050
- entries = await fs14.promises.readdir(dir, { withFileTypes: true });
15502
+ entries = await fs15.promises.readdir(dir, { withFileTypes: true });
15051
15503
  } catch {
15052
15504
  return;
15053
15505
  }
15054
15506
  for (const entry of entries) {
15055
- const fullPath = path18.join(dir, entry.name);
15507
+ const fullPath = path19.join(dir, entry.name);
15056
15508
  if (entry.isDirectory()) {
15057
15509
  if (SKIP_DIRS.has(entry.name)) continue;
15058
15510
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -15074,19 +15526,19 @@ function fallbackDecode(encodedName) {
15074
15526
  var ClaudeSource = class {
15075
15527
  name = "claude";
15076
15528
  async scan() {
15077
- const baseDir = path18.join(os9.homedir(), ".claude", "projects");
15529
+ const baseDir = path19.join(os9.homedir(), ".claude", "projects");
15078
15530
  try {
15079
- await fs14.promises.access(baseDir);
15531
+ await fs15.promises.access(baseDir);
15080
15532
  } catch {
15081
15533
  return [];
15082
15534
  }
15083
- const projectDirs = await fs14.promises.readdir(baseDir, {
15535
+ const projectDirs = await fs15.promises.readdir(baseDir, {
15084
15536
  withFileTypes: true
15085
15537
  });
15086
15538
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
15087
15539
  const resultArrays = await Promise.all(
15088
15540
  dirEntries.map(async (dir) => {
15089
- const projectPath = path18.join(baseDir, dir.name);
15541
+ const projectPath = path19.join(baseDir, dir.name);
15090
15542
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
15091
15543
  const files = [];
15092
15544
  await collectFiles(
@@ -15104,12 +15556,12 @@ var ClaudeSource = class {
15104
15556
  };
15105
15557
 
15106
15558
  // src/sources/codex.ts
15107
- import fs15 from "fs";
15559
+ import fs16 from "fs";
15108
15560
  import os10 from "os";
15109
- import path19 from "path";
15561
+ import path20 from "path";
15110
15562
  import readline2 from "readline";
15111
15563
  async function parseSessionMeta(filePath) {
15112
- const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
15564
+ const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
15113
15565
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
15114
15566
  try {
15115
15567
  for await (const line of rl) {
@@ -15134,12 +15586,12 @@ async function findJsonlFiles(dir) {
15134
15586
  async function walk(d) {
15135
15587
  let entries;
15136
15588
  try {
15137
- entries = await fs15.promises.readdir(d, { withFileTypes: true });
15589
+ entries = await fs16.promises.readdir(d, { withFileTypes: true });
15138
15590
  } catch {
15139
15591
  return;
15140
15592
  }
15141
15593
  for (const entry of entries) {
15142
- const full = path19.join(d, entry.name);
15594
+ const full = path20.join(d, entry.name);
15143
15595
  if (entry.isDirectory()) {
15144
15596
  await walk(full);
15145
15597
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -15153,11 +15605,11 @@ async function findJsonlFiles(dir) {
15153
15605
  async function loadHistory(historyPath) {
15154
15606
  const map = /* @__PURE__ */ new Map();
15155
15607
  try {
15156
- await fs15.promises.access(historyPath);
15608
+ await fs16.promises.access(historyPath);
15157
15609
  } catch {
15158
15610
  return map;
15159
15611
  }
15160
- const stream = fs15.createReadStream(historyPath, { encoding: "utf-8" });
15612
+ const stream = fs16.createReadStream(historyPath, { encoding: "utf-8" });
15161
15613
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
15162
15614
  try {
15163
15615
  for await (const line of rl) {
@@ -15184,14 +15636,14 @@ async function loadHistory(historyPath) {
15184
15636
  var CodexSource = class {
15185
15637
  name = "codex";
15186
15638
  async scan() {
15187
- const codexDir = path19.join(os10.homedir(), ".codex");
15188
- const sessionsDir = path19.join(codexDir, "sessions");
15639
+ const codexDir = path20.join(os10.homedir(), ".codex");
15640
+ const sessionsDir = path20.join(codexDir, "sessions");
15189
15641
  try {
15190
- await fs15.promises.access(sessionsDir);
15642
+ await fs16.promises.access(sessionsDir);
15191
15643
  } catch {
15192
15644
  return [];
15193
15645
  }
15194
- const historyPath = path19.join(codexDir, "history.jsonl");
15646
+ const historyPath = path20.join(codexDir, "history.jsonl");
15195
15647
  const [jsonlFiles, historyMap] = await Promise.all([
15196
15648
  findJsonlFiles(sessionsDir),
15197
15649
  loadHistory(historyPath)
@@ -15214,8 +15666,8 @@ var CodexSource = class {
15214
15666
  });
15215
15667
  const historyLines = historyMap.get(meta.sessionId);
15216
15668
  if (historyLines) {
15217
- const sessionDir = path19.relative(sessionsDir, path19.dirname(filePath));
15218
- const historyAbsPath = path19.join(
15669
+ const sessionDir = path20.relative(sessionsDir, path20.dirname(filePath));
15670
+ const historyAbsPath = path20.join(
15219
15671
  sessionsDir,
15220
15672
  sessionDir,
15221
15673
  `history-${meta.sessionId}.jsonl`
@@ -15235,18 +15687,18 @@ var CodexSource = class {
15235
15687
  };
15236
15688
 
15237
15689
  // src/sources/copilotChat.ts
15238
- import fs16 from "fs";
15690
+ import fs17 from "fs";
15239
15691
  import os11 from "os";
15240
- import path20 from "path";
15692
+ import path21 from "path";
15241
15693
  import { fileURLToPath } from "url";
15242
15694
  function vsCodeUserDirs() {
15243
15695
  const home = os11.homedir();
15244
15696
  const dirs = [
15245
- path20.join(home, "Library", "Application Support", "Code", "User"),
15246
- path20.join(home, ".config", "Code", "User")
15697
+ path21.join(home, "Library", "Application Support", "Code", "User"),
15698
+ path21.join(home, ".config", "Code", "User")
15247
15699
  ];
15248
15700
  if (process.env.APPDATA) {
15249
- dirs.push(path20.join(process.env.APPDATA, "Code", "User"));
15701
+ dirs.push(path21.join(process.env.APPDATA, "Code", "User"));
15250
15702
  }
15251
15703
  return dirs;
15252
15704
  }
@@ -15261,7 +15713,7 @@ function uriToFsPath(uri) {
15261
15713
  async function readWorkspaceFolder(workspaceJsonPath) {
15262
15714
  let raw;
15263
15715
  try {
15264
- raw = await fs16.promises.readFile(workspaceJsonPath, "utf-8");
15716
+ raw = await fs17.promises.readFile(workspaceJsonPath, "utf-8");
15265
15717
  } catch {
15266
15718
  return null;
15267
15719
  }
@@ -15283,10 +15735,10 @@ var CopilotChatSource = class {
15283
15735
  async scan() {
15284
15736
  const results = [];
15285
15737
  for (const userDir of vsCodeUserDirs()) {
15286
- const workspaceStorage = path20.join(userDir, "workspaceStorage");
15738
+ const workspaceStorage = path21.join(userDir, "workspaceStorage");
15287
15739
  let hashDirs;
15288
15740
  try {
15289
- hashDirs = await fs16.promises.readdir(workspaceStorage, {
15741
+ hashDirs = await fs17.promises.readdir(workspaceStorage, {
15290
15742
  withFileTypes: true
15291
15743
  });
15292
15744
  } catch {
@@ -15294,22 +15746,22 @@ var CopilotChatSource = class {
15294
15746
  }
15295
15747
  for (const hash of hashDirs) {
15296
15748
  if (!hash.isDirectory()) continue;
15297
- const wsRoot = path20.join(workspaceStorage, hash.name);
15298
- const transcriptsDir = path20.join(
15749
+ const wsRoot = path21.join(workspaceStorage, hash.name);
15750
+ const transcriptsDir = path21.join(
15299
15751
  wsRoot,
15300
15752
  "GitHub.copilot-chat",
15301
15753
  "transcripts"
15302
15754
  );
15303
15755
  let transcriptEntries;
15304
15756
  try {
15305
- transcriptEntries = await fs16.promises.readdir(transcriptsDir, {
15757
+ transcriptEntries = await fs17.promises.readdir(transcriptsDir, {
15306
15758
  withFileTypes: true
15307
15759
  });
15308
15760
  } catch {
15309
15761
  continue;
15310
15762
  }
15311
15763
  const repoPath = await readWorkspaceFolder(
15312
- path20.join(wsRoot, "workspace.json")
15764
+ path21.join(wsRoot, "workspace.json")
15313
15765
  );
15314
15766
  if (!repoPath) continue;
15315
15767
  for (const entry of transcriptEntries) {
@@ -15317,7 +15769,7 @@ var CopilotChatSource = class {
15317
15769
  const sessionId = entry.name.slice(0, -".jsonl".length);
15318
15770
  results.push({
15319
15771
  sourceName: this.name,
15320
- absolutePath: path20.join(transcriptsDir, entry.name),
15772
+ absolutePath: path21.join(transcriptsDir, entry.name),
15321
15773
  repoPath,
15322
15774
  metadata: { sessionId }
15323
15775
  });
@@ -15357,7 +15809,7 @@ function reportRedactionStats(noun, stats) {
15357
15809
  async function filterByTimeRange(group, range) {
15358
15810
  const results = await Promise.all(
15359
15811
  group.files.map(
15360
- (f) => fs17.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
15812
+ (f) => fs18.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
15361
15813
  )
15362
15814
  );
15363
15815
  const filtered = [];
@@ -15384,10 +15836,10 @@ async function runInteractive() {
15384
15836
  s.start(`Scanning ${source.name} logs...`);
15385
15837
  const allFiles = await source.scan();
15386
15838
  const allGroups = await mergeByRepo(allFiles);
15387
- const repoRoot = path21.resolve(repo.root);
15839
+ const repoRoot = path22.resolve(repo.root);
15388
15840
  const matching = allGroups.filter((g) => {
15389
- const resolved = path21.resolve(g.repoPath);
15390
- return resolved === repoRoot || resolved.startsWith(repoRoot + path21.sep);
15841
+ const resolved = path22.resolve(g.repoPath);
15842
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path22.sep);
15391
15843
  });
15392
15844
  if (matching.length === 0) {
15393
15845
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -15418,7 +15870,7 @@ async function runInteractive() {
15418
15870
  }
15419
15871
  }
15420
15872
  const envFileNames = await discoverEnvFiles(repoRoot);
15421
- const envFilePaths = envFileNames.map((n) => path21.join(repoRoot, n));
15873
+ const envFilePaths = envFileNames.map((n) => path22.join(repoRoot, n));
15422
15874
  const additionalFiles = await promptSecretFiles(envFileNames);
15423
15875
  const secretResult = await collectSecrets(
15424
15876
  repoRoot,