hillclimb 0.4.3 → 0.5.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.
- package/dist/cli.js +483 -248
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs19 from "fs";
|
|
5
|
+
import path23 from "path";
|
|
6
6
|
import * as p6 from "@clack/prompts";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -617,6 +617,15 @@ var TOOLS = [
|
|
|
617
617
|
command: HOOK_CMD("upload"),
|
|
618
618
|
timeout: CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS
|
|
619
619
|
},
|
|
620
|
+
// Per-turn transcript capture. SessionEnd is the clean one-shot
|
|
621
|
+
// upload, but it never fires for sessions that are killed or never
|
|
622
|
+
// cleanly closed (e.g. long-lived remote sessions), so those
|
|
623
|
+
// transcripts are lost entirely. Uploading on Stop captures them
|
|
624
|
+
// turn-by-turn. `--tool=claude` is required: resolveSourceTool()
|
|
625
|
+
// defaults a bare `upload` on a Stop event to Codex, and the explicit
|
|
626
|
+
// tool also keys the debug-log agent/git completion correlation to
|
|
627
|
+
// "claude" (see expectedKinds() in debug-logs.ts).
|
|
628
|
+
{ eventName: "Stop", command: `${HOOK_CMD("upload")} --tool=claude` },
|
|
620
629
|
{ eventName: "SessionStart", command: GIT_TRACES_CMD("claude") },
|
|
621
630
|
{ eventName: "Stop", command: GIT_TRACES_CMD("claude") },
|
|
622
631
|
{ eventName: "SessionEnd", command: GIT_TRACES_CMD("claude") }
|
|
@@ -630,6 +639,10 @@ var TOOLS = [
|
|
|
630
639
|
format: "cursor",
|
|
631
640
|
events: [
|
|
632
641
|
{ eventName: "sessionEnd", command: HOOK_CMD("upload") },
|
|
642
|
+
// Per-turn capture, mirroring Claude. Cursor's stop payload carries a
|
|
643
|
+
// populated transcript_path (verified on Cursor 3.0.16); --tool=cursor
|
|
644
|
+
// is required so a bare Stop upload isn't misattributed to Codex.
|
|
645
|
+
{ eventName: "stop", command: `${HOOK_CMD("upload")} --tool=cursor` },
|
|
633
646
|
{ eventName: "sessionStart", command: GIT_TRACES_CMD("cursor") },
|
|
634
647
|
{ eventName: "stop", command: GIT_TRACES_CMD("cursor") },
|
|
635
648
|
{ eventName: "sessionEnd", command: GIT_TRACES_CMD("cursor") }
|
|
@@ -854,7 +867,7 @@ function copilotUninstallMatching(settings, eventName, predicate) {
|
|
|
854
867
|
}
|
|
855
868
|
return true;
|
|
856
869
|
}
|
|
857
|
-
var OPENCODE_PLUGIN_VERSION =
|
|
870
|
+
var OPENCODE_PLUGIN_VERSION = 7;
|
|
858
871
|
var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
|
|
859
872
|
var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
|
|
860
873
|
// Auto-installed by \`npx hillclimb\`. Do not edit manually \u2014 re-running
|
|
@@ -983,12 +996,29 @@ export const HillclimbPlugin = async ({ directory }) => ({
|
|
|
983
996
|
}
|
|
984
997
|
|
|
985
998
|
if (type === "session.idle" && sessionID) {
|
|
986
|
-
// Per-turn
|
|
987
|
-
//
|
|
988
|
-
//
|
|
999
|
+
// Per-turn snapshot: upload the transcript-so-far AND take a git-traces
|
|
1000
|
+
// snapshot. The CLI keeps this to one contribution per session (reuse),
|
|
1001
|
+
// attaching the latest transcript on each turn, so this is one
|
|
1002
|
+
// contribution per session, not per turn. Keep the buffer \u2014 it's dropped
|
|
1003
|
+
// only on session.deleted / server.instance.disposed.
|
|
1004
|
+
//
|
|
1005
|
+
// Pass transcript_path to BOTH spawns: the debug-log eventId for a stop
|
|
1006
|
+
// event folds in transcript_path, so diverging payloads would orphan the
|
|
1007
|
+
// two completions and double-upload the debug log.
|
|
1008
|
+
const transcriptPath = writeTranscript(sessionID);
|
|
1009
|
+
if (transcriptPath) {
|
|
1010
|
+
spawnHillclimb("upload", {
|
|
1011
|
+
session_id: sessionID,
|
|
1012
|
+
cwd,
|
|
1013
|
+
transcript_path: transcriptPath,
|
|
1014
|
+
hook_event_name: "session.idle",
|
|
1015
|
+
tool: TOOL,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
989
1018
|
spawnHillclimb("git-traces --tool=opencode", {
|
|
990
1019
|
session_id: sessionID,
|
|
991
1020
|
cwd,
|
|
1021
|
+
transcript_path: transcriptPath || undefined,
|
|
992
1022
|
hook_event_name: "session.idle",
|
|
993
1023
|
tool: TOOL,
|
|
994
1024
|
});
|
|
@@ -2149,15 +2179,32 @@ async function runStatus(args = []) {
|
|
|
2149
2179
|
|
|
2150
2180
|
// src/commands/upload.ts
|
|
2151
2181
|
import { spawn as spawn2 } from "child_process";
|
|
2152
|
-
import
|
|
2153
|
-
import
|
|
2154
|
-
import
|
|
2182
|
+
import fs11 from "fs";
|
|
2183
|
+
import os6 from "os";
|
|
2184
|
+
import path14 from "path";
|
|
2155
2185
|
|
|
2156
2186
|
// src/debug-logs.ts
|
|
2157
2187
|
import crypto from "crypto";
|
|
2158
2188
|
import fs9 from "fs";
|
|
2159
2189
|
import path11 from "path";
|
|
2160
2190
|
|
|
2191
|
+
// src/hook-events.ts
|
|
2192
|
+
function classifyHookEvent(event) {
|
|
2193
|
+
switch (event) {
|
|
2194
|
+
case "Stop":
|
|
2195
|
+
case "stop":
|
|
2196
|
+
case "session.idle":
|
|
2197
|
+
return "stop";
|
|
2198
|
+
case "SessionEnd":
|
|
2199
|
+
case "sessionEnd":
|
|
2200
|
+
case "session.deleted":
|
|
2201
|
+
case "server.instance.disposed":
|
|
2202
|
+
return "sessionEnd";
|
|
2203
|
+
default:
|
|
2204
|
+
return null;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2161
2208
|
// src/middleware/pattern-redact.ts
|
|
2162
2209
|
import os3 from "os";
|
|
2163
2210
|
import { Worker } from "worker_threads";
|
|
@@ -10916,14 +10963,23 @@ var PlatformUploadOutput = class {
|
|
|
10916
10963
|
contributionTitle,
|
|
10917
10964
|
contributionBody,
|
|
10918
10965
|
zipFilename,
|
|
10919
|
-
autoSubmit
|
|
10966
|
+
autoSubmit,
|
|
10967
|
+
existingContributionId,
|
|
10968
|
+
onContributionCreated
|
|
10920
10969
|
} = this.opts;
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10970
|
+
let contributionId;
|
|
10971
|
+
if (existingContributionId) {
|
|
10972
|
+
contributionId = existingContributionId;
|
|
10973
|
+
} else {
|
|
10974
|
+
const contribution = await client.createContribution(projectId, {
|
|
10975
|
+
contributionTypeSlug,
|
|
10976
|
+
title: contributionTitle,
|
|
10977
|
+
body: contributionBody
|
|
10978
|
+
});
|
|
10979
|
+
contributionId = contribution.id;
|
|
10980
|
+
if (onContributionCreated) await onContributionCreated(contributionId);
|
|
10981
|
+
}
|
|
10982
|
+
const presigned = await client.createUpload(contributionId, {
|
|
10927
10983
|
originalFilename: zipFilename,
|
|
10928
10984
|
mimeType: "application/zip",
|
|
10929
10985
|
sizeBytes: buffer.byteLength
|
|
@@ -10939,11 +10995,11 @@ var PlatformUploadOutput = class {
|
|
|
10939
10995
|
);
|
|
10940
10996
|
appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
|
|
10941
10997
|
if (autoSubmit) {
|
|
10942
|
-
appendLog("info", `submitting contribution ${
|
|
10943
|
-
await client.submitContribution(
|
|
10944
|
-
appendLog("info", `contribution ${
|
|
10998
|
+
appendLog("info", `submitting contribution ${contributionId}`);
|
|
10999
|
+
await client.submitContribution(contributionId);
|
|
11000
|
+
appendLog("info", `contribution ${contributionId} submitted`);
|
|
10945
11001
|
}
|
|
10946
|
-
return
|
|
11002
|
+
return contributionId;
|
|
10947
11003
|
}
|
|
10948
11004
|
};
|
|
10949
11005
|
|
|
@@ -11073,21 +11129,6 @@ function toolLabel(tool) {
|
|
|
11073
11129
|
};
|
|
11074
11130
|
return labels[tool] ?? tool;
|
|
11075
11131
|
}
|
|
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
11132
|
function resolveCwd(payload) {
|
|
11092
11133
|
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
11093
11134
|
}
|
|
@@ -11100,7 +11141,7 @@ function stringOrNull(value) {
|
|
|
11100
11141
|
function expectedKinds(tool, eventKind, payload) {
|
|
11101
11142
|
if (eventKind === "stop") {
|
|
11102
11143
|
return new Set(
|
|
11103
|
-
tool === "codex" || tool === "copilot-chat" ? ["agent", "git"] : ["git"]
|
|
11144
|
+
tool === "codex" || tool === "copilot-chat" || tool === "claude" || tool === "cursor" || tool === "opencode" ? ["agent", "git"] : ["git"]
|
|
11104
11145
|
);
|
|
11105
11146
|
}
|
|
11106
11147
|
if (tool === "opencode" && !resolveSessionId(payload)) {
|
|
@@ -13153,6 +13194,126 @@ var NormalizeMiddleware = class {
|
|
|
13153
13194
|
}
|
|
13154
13195
|
};
|
|
13155
13196
|
|
|
13197
|
+
// src/upload-state.ts
|
|
13198
|
+
import crypto2 from "crypto";
|
|
13199
|
+
import fs10 from "fs";
|
|
13200
|
+
import os5 from "os";
|
|
13201
|
+
import path13 from "path";
|
|
13202
|
+
var CURRENT_SCHEMA_VERSION2 = 1;
|
|
13203
|
+
var DEFAULT_STATE_DIR = path13.join(
|
|
13204
|
+
os5.homedir(),
|
|
13205
|
+
".hillclimb",
|
|
13206
|
+
"agent-uploads"
|
|
13207
|
+
);
|
|
13208
|
+
var LOCK_RETRIES2 = 120;
|
|
13209
|
+
var LOCK_RETRY_DELAY_MS2 = 500;
|
|
13210
|
+
var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
13211
|
+
function stateDir2() {
|
|
13212
|
+
return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
|
|
13213
|
+
}
|
|
13214
|
+
function readPositiveEnvMs(name, fallback) {
|
|
13215
|
+
const raw = process.env[name];
|
|
13216
|
+
if (raw === void 0) return fallback;
|
|
13217
|
+
const n = Number(raw);
|
|
13218
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
13219
|
+
}
|
|
13220
|
+
function stateTtlMs() {
|
|
13221
|
+
return readPositiveEnvMs(
|
|
13222
|
+
"HILLCLIMB_UPLOAD_STATE_TTL_MS",
|
|
13223
|
+
DEFAULT_STATE_TTL_MS
|
|
13224
|
+
);
|
|
13225
|
+
}
|
|
13226
|
+
function stateFileFor(repoRoot, tool, sessionId) {
|
|
13227
|
+
const hash = crypto2.createHash("sha256").update(`${path13.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
|
|
13228
|
+
return path13.join(stateDir2(), `${hash}.json`);
|
|
13229
|
+
}
|
|
13230
|
+
function lockFileFor(repoRoot, tool, sessionId) {
|
|
13231
|
+
return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
|
|
13232
|
+
}
|
|
13233
|
+
async function readUploadState(repoRoot, tool, sessionId) {
|
|
13234
|
+
try {
|
|
13235
|
+
const raw = await fs10.promises.readFile(
|
|
13236
|
+
stateFileFor(repoRoot, tool, sessionId),
|
|
13237
|
+
"utf-8"
|
|
13238
|
+
);
|
|
13239
|
+
const parsed = JSON.parse(raw);
|
|
13240
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION2) return null;
|
|
13241
|
+
return parsed;
|
|
13242
|
+
} catch {
|
|
13243
|
+
return null;
|
|
13244
|
+
}
|
|
13245
|
+
}
|
|
13246
|
+
async function writeUploadState(state) {
|
|
13247
|
+
const file = stateFileFor(state.repoRoot, state.tool, state.sessionId);
|
|
13248
|
+
await fs10.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
13249
|
+
const tmp = `${file}.tmp`;
|
|
13250
|
+
await fs10.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
13251
|
+
mode: 384
|
|
13252
|
+
});
|
|
13253
|
+
await fs10.promises.rename(tmp, file);
|
|
13254
|
+
}
|
|
13255
|
+
async function deleteUploadState(repoRoot, tool, sessionId) {
|
|
13256
|
+
try {
|
|
13257
|
+
await fs10.promises.unlink(stateFileFor(repoRoot, tool, sessionId));
|
|
13258
|
+
} catch {
|
|
13259
|
+
}
|
|
13260
|
+
}
|
|
13261
|
+
async function acquireLock2(repoRoot, tool, sessionId, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
|
|
13262
|
+
const lockPath = lockFileFor(repoRoot, tool, sessionId);
|
|
13263
|
+
await fs10.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
13264
|
+
for (let i = 0; i < retries; i++) {
|
|
13265
|
+
try {
|
|
13266
|
+
const fd = await fs10.promises.open(
|
|
13267
|
+
lockPath,
|
|
13268
|
+
fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
|
|
13269
|
+
);
|
|
13270
|
+
await fd.write(String(process.pid));
|
|
13271
|
+
await fd.close();
|
|
13272
|
+
return;
|
|
13273
|
+
} catch (err) {
|
|
13274
|
+
if (err.code === "EEXIST" && i < retries - 1) {
|
|
13275
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
13276
|
+
continue;
|
|
13277
|
+
}
|
|
13278
|
+
throw err;
|
|
13279
|
+
}
|
|
13280
|
+
}
|
|
13281
|
+
throw new Error(`Failed to acquire upload lock after ${retries} retries`);
|
|
13282
|
+
}
|
|
13283
|
+
async function releaseLock2(repoRoot, tool, sessionId) {
|
|
13284
|
+
try {
|
|
13285
|
+
await fs10.promises.unlink(lockFileFor(repoRoot, tool, sessionId));
|
|
13286
|
+
} catch {
|
|
13287
|
+
}
|
|
13288
|
+
}
|
|
13289
|
+
async function withUploadLock(repoRoot, tool, sessionId, fn) {
|
|
13290
|
+
await acquireLock2(repoRoot, tool, sessionId);
|
|
13291
|
+
try {
|
|
13292
|
+
return await fn();
|
|
13293
|
+
} finally {
|
|
13294
|
+
await releaseLock2(repoRoot, tool, sessionId);
|
|
13295
|
+
}
|
|
13296
|
+
}
|
|
13297
|
+
async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
|
|
13298
|
+
let entries;
|
|
13299
|
+
try {
|
|
13300
|
+
entries = await fs10.promises.readdir(stateDir2(), { withFileTypes: true });
|
|
13301
|
+
} catch {
|
|
13302
|
+
return;
|
|
13303
|
+
}
|
|
13304
|
+
for (const entry of entries) {
|
|
13305
|
+
if (!entry.isFile()) continue;
|
|
13306
|
+
const file = path13.join(stateDir2(), entry.name);
|
|
13307
|
+
try {
|
|
13308
|
+
const st = await fs10.promises.stat(file);
|
|
13309
|
+
if (now - st.mtimeMs > ttlMs) {
|
|
13310
|
+
await fs10.promises.unlink(file);
|
|
13311
|
+
}
|
|
13312
|
+
} catch {
|
|
13313
|
+
}
|
|
13314
|
+
}
|
|
13315
|
+
}
|
|
13316
|
+
|
|
13156
13317
|
// src/commands/upload.ts
|
|
13157
13318
|
async function readStdin() {
|
|
13158
13319
|
if (process.stdin.isTTY) return "";
|
|
@@ -13172,7 +13333,7 @@ function lineHasAssistant(line) {
|
|
|
13172
13333
|
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
13173
13334
|
}
|
|
13174
13335
|
async function hasAssistantMessage(transcriptPath) {
|
|
13175
|
-
const stream =
|
|
13336
|
+
const stream = fs11.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
13176
13337
|
let buffer = "";
|
|
13177
13338
|
try {
|
|
13178
13339
|
for await (const chunk of stream) {
|
|
@@ -13240,8 +13401,8 @@ function resolveCursorTranscriptPath(payload) {
|
|
|
13240
13401
|
const workspace = payload.workspace_roots?.[0];
|
|
13241
13402
|
if (!id || !workspace) return void 0;
|
|
13242
13403
|
const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
|
|
13243
|
-
return
|
|
13244
|
-
|
|
13404
|
+
return path14.join(
|
|
13405
|
+
os6.homedir(),
|
|
13245
13406
|
".cursor",
|
|
13246
13407
|
"projects",
|
|
13247
13408
|
encoded,
|
|
@@ -13255,9 +13416,10 @@ async function runUploadInner(payload) {
|
|
|
13255
13416
|
const transcriptPath = payload.transcript_path ?? resolveCursorTranscriptPath(payload);
|
|
13256
13417
|
const cwd = payload.cwd ?? payload.workspace_roots?.[0];
|
|
13257
13418
|
const sourceTool = resolveSourceTool(payload);
|
|
13419
|
+
const eventKind = classifyHookEvent(payload.hook_event_name);
|
|
13258
13420
|
appendLog(
|
|
13259
13421
|
"info",
|
|
13260
|
-
`[${sessionId ?? "no-id"}] payload parsed (tool=${sourceTool}, cwd=${cwd ?? "?"})`
|
|
13422
|
+
`[${sessionId ?? "no-id"}] payload parsed (tool=${sourceTool}, cwd=${cwd ?? "?"}, event=${eventKind ?? "?"})`
|
|
13261
13423
|
);
|
|
13262
13424
|
if (!sessionId || !transcriptPath || !cwd) {
|
|
13263
13425
|
appendLog(
|
|
@@ -13276,9 +13438,9 @@ async function runUploadInner(payload) {
|
|
|
13276
13438
|
}
|
|
13277
13439
|
const { repoRoot, config } = match;
|
|
13278
13440
|
await selfHealHook(repoRoot, sourceTool);
|
|
13279
|
-
const transcriptResolved =
|
|
13441
|
+
const transcriptResolved = path14.resolve(transcriptPath);
|
|
13280
13442
|
try {
|
|
13281
|
-
const stat = await
|
|
13443
|
+
const stat = await fs11.promises.stat(transcriptResolved);
|
|
13282
13444
|
if (!stat.isFile()) {
|
|
13283
13445
|
appendLog(
|
|
13284
13446
|
"warn",
|
|
@@ -13305,89 +13467,158 @@ async function runUploadInner(payload) {
|
|
|
13305
13467
|
transcriptPath: transcriptResolved,
|
|
13306
13468
|
repoRoot,
|
|
13307
13469
|
config,
|
|
13308
|
-
sourceTool
|
|
13470
|
+
sourceTool,
|
|
13471
|
+
eventKind
|
|
13309
13472
|
});
|
|
13310
13473
|
}
|
|
13311
13474
|
async function uploadSession(args) {
|
|
13312
|
-
const { sessionId, transcriptPath, repoRoot, config, sourceTool } = args;
|
|
13313
|
-
const
|
|
13314
|
-
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13329
|
-
|
|
13330
|
-
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
|
|
13334
|
-
|
|
13335
|
-
|
|
13336
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
|
|
13475
|
+
const { sessionId, transcriptPath, repoRoot, config, sourceTool, eventKind } = args;
|
|
13476
|
+
const isSessionEnd = eventKind === "sessionEnd";
|
|
13477
|
+
await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
|
|
13478
|
+
const prior = await readUploadState(repoRoot, sourceTool, sessionId);
|
|
13479
|
+
let transcriptSize;
|
|
13480
|
+
try {
|
|
13481
|
+
transcriptSize = (await fs11.promises.stat(transcriptPath)).size;
|
|
13482
|
+
} catch {
|
|
13483
|
+
}
|
|
13484
|
+
if (prior?.contributionId && transcriptSize !== void 0 && transcriptSize === prior.lastTranscriptSize) {
|
|
13485
|
+
appendLog(
|
|
13486
|
+
"info",
|
|
13487
|
+
`[${sessionId}] skipping ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload (transcript unchanged at ${transcriptSize} bytes${isSessionEnd ? "; local state cleared" : ""})`
|
|
13488
|
+
);
|
|
13489
|
+
if (isSessionEnd) {
|
|
13490
|
+
await deleteUploadState(repoRoot, sourceTool, sessionId);
|
|
13491
|
+
}
|
|
13492
|
+
return;
|
|
13493
|
+
}
|
|
13494
|
+
const now = /* @__PURE__ */ new Date();
|
|
13495
|
+
const sourceFile = {
|
|
13496
|
+
sourceName: sourceTool,
|
|
13497
|
+
absolutePath: transcriptPath,
|
|
13498
|
+
repoPath: repoRoot
|
|
13499
|
+
};
|
|
13500
|
+
const group = {
|
|
13501
|
+
repoPath: repoRoot,
|
|
13502
|
+
label: path14.basename(repoRoot),
|
|
13503
|
+
files: [sourceFile],
|
|
13504
|
+
sourceNames: [sourceTool],
|
|
13505
|
+
lastModified: now
|
|
13506
|
+
};
|
|
13507
|
+
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
13508
|
+
const envFilePaths = envFileNames.map((n) => path14.join(repoRoot, n));
|
|
13509
|
+
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
13510
|
+
const mwChain = [];
|
|
13511
|
+
if (secretResult.values.size > 0) {
|
|
13512
|
+
mwChain.push(new RedactMiddleware(secretResult.values));
|
|
13513
|
+
}
|
|
13514
|
+
mwChain.push(new PatternRedactMiddleware());
|
|
13515
|
+
mwChain.push(new NormalizeMiddleware());
|
|
13516
|
+
const identity = await loadIdentity(config.apiBaseUrl);
|
|
13517
|
+
if (!identity) {
|
|
13518
|
+
appendLog(
|
|
13519
|
+
"error",
|
|
13520
|
+
`Session ${sessionId} upload skipped: no saved login for ${config.apiBaseUrl}. Run \`npx hillclimb login\`.`
|
|
13521
|
+
);
|
|
13522
|
+
return;
|
|
13523
|
+
}
|
|
13524
|
+
const client = new PlatformClient(
|
|
13525
|
+
config.apiBaseUrl,
|
|
13526
|
+
identity.sessionCookie
|
|
13340
13527
|
);
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
const title = `${toolLabel2} session ${shortId} \u2014 ${epochSeconds}`;
|
|
13355
|
-
const body = `Session ID: ${sessionId}
|
|
13528
|
+
const shortId = sessionId.slice(0, 12);
|
|
13529
|
+
const toolLabels = {
|
|
13530
|
+
cursor: "Cursor",
|
|
13531
|
+
codex: "Codex",
|
|
13532
|
+
claude: "Claude",
|
|
13533
|
+
"copilot-chat": "GitHub Copilot Chat",
|
|
13534
|
+
opencode: "opencode"
|
|
13535
|
+
};
|
|
13536
|
+
const toolLabel2 = toolLabels[sourceTool] ?? "Claude";
|
|
13537
|
+
const epochSeconds = formatEpochSeconds2(now);
|
|
13538
|
+
const seq = (prior?.uploadCount ?? 0) + 1;
|
|
13539
|
+
const title = `${toolLabel2} session ${shortId} \u2014 ${epochSeconds}`;
|
|
13540
|
+
const body = `Session ID: ${sessionId}
|
|
13356
13541
|
Tool: ${toolLabel2}
|
|
13357
13542
|
Repo: ${repoRoot}
|
|
13358
13543
|
Uploaded: ${now.toISOString()}`;
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13544
|
+
const zipFilename = `${sourceTool}-${sanitize2(shortId)}-${epochSeconds}-${String(seq).padStart(3, "0")}.zip`;
|
|
13545
|
+
const alreadySubmitted = prior?.submitted ?? false;
|
|
13546
|
+
const submitThisUpload = config.autoSubmit && !alreadySubmitted;
|
|
13547
|
+
const output = new PlatformUploadOutput({
|
|
13548
|
+
client,
|
|
13549
|
+
projectId: config.projectId,
|
|
13550
|
+
contributionTypeSlug: config.contributionTypeSlug,
|
|
13551
|
+
contributionTitle: title,
|
|
13552
|
+
contributionBody: body,
|
|
13553
|
+
zipFilename,
|
|
13554
|
+
autoSubmit: submitThisUpload,
|
|
13555
|
+
existingContributionId: prior?.contributionId ?? void 0,
|
|
13556
|
+
// Persist the new contribution id before the file PUT so a failed
|
|
13557
|
+
// upload can't make the next Stop create a second contribution.
|
|
13558
|
+
onContributionCreated: (id) => writeUploadState({
|
|
13559
|
+
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
13560
|
+
sessionId,
|
|
13561
|
+
tool: sourceTool,
|
|
13562
|
+
repoRoot,
|
|
13563
|
+
projectId: config.projectId,
|
|
13564
|
+
contributionId: id,
|
|
13565
|
+
submitted: alreadySubmitted,
|
|
13566
|
+
uploadCount: prior?.uploadCount ?? 0,
|
|
13567
|
+
firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
|
|
13568
|
+
lastUploadedAt: prior?.lastUploadedAt ?? now.toISOString(),
|
|
13569
|
+
lastTranscriptSize: prior?.lastTranscriptSize
|
|
13570
|
+
})
|
|
13373
13571
|
});
|
|
13572
|
+
const reuseDesc = prior?.contributionId ? `reusing contribution ${prior.contributionId} (file #${seq})` : prior ? "new contribution (file #1; prior state had no contribution \u2014 earlier create may have failed)" : "new contribution (file #1; first upload for session)";
|
|
13374
13573
|
appendLog(
|
|
13375
13574
|
"info",
|
|
13376
|
-
`
|
|
13575
|
+
`[${sessionId}] ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload \u2192 ${reuseDesc}`
|
|
13377
13576
|
);
|
|
13378
|
-
|
|
13379
|
-
|
|
13577
|
+
let contributionId;
|
|
13578
|
+
try {
|
|
13579
|
+
contributionId = await runPipeline(group, mwChain, output, {
|
|
13580
|
+
selectedSources: [sourceTool]
|
|
13581
|
+
});
|
|
13582
|
+
} catch (err) {
|
|
13583
|
+
if (err instanceof PlatformError && err.status === 401) {
|
|
13584
|
+
appendLog(
|
|
13585
|
+
"error",
|
|
13586
|
+
`Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
|
|
13587
|
+
);
|
|
13588
|
+
return;
|
|
13589
|
+
}
|
|
13380
13590
|
appendLog(
|
|
13381
13591
|
"error",
|
|
13382
|
-
`Session ${sessionId} upload failed:
|
|
13592
|
+
`Session ${sessionId} upload failed: ${err instanceof Error ? err.message : String(err)}`
|
|
13383
13593
|
);
|
|
13384
13594
|
return;
|
|
13385
13595
|
}
|
|
13596
|
+
const next = {
|
|
13597
|
+
schemaVersion: CURRENT_SCHEMA_VERSION2,
|
|
13598
|
+
sessionId,
|
|
13599
|
+
tool: sourceTool,
|
|
13600
|
+
repoRoot,
|
|
13601
|
+
projectId: config.projectId,
|
|
13602
|
+
contributionId,
|
|
13603
|
+
submitted: alreadySubmitted || submitThisUpload,
|
|
13604
|
+
uploadCount: seq,
|
|
13605
|
+
firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
|
|
13606
|
+
lastUploadedAt: now.toISOString(),
|
|
13607
|
+
lastTranscriptSize: transcriptSize
|
|
13608
|
+
};
|
|
13609
|
+
await writeUploadState(next);
|
|
13386
13610
|
appendLog(
|
|
13387
|
-
"
|
|
13388
|
-
`
|
|
13611
|
+
"info",
|
|
13612
|
+
`Uploaded session ${sessionId} to project ${config.projectSlug} (${config.projectId}) as contribution ${contributionId} (seq=${seq})`
|
|
13389
13613
|
);
|
|
13390
|
-
|
|
13614
|
+
if (isSessionEnd) {
|
|
13615
|
+
await deleteUploadState(repoRoot, sourceTool, sessionId);
|
|
13616
|
+
appendLog(
|
|
13617
|
+
"info",
|
|
13618
|
+
`[${sessionId}] session complete \u2014 contribution ${contributionId}, ${seq} file(s); local state cleared`
|
|
13619
|
+
);
|
|
13620
|
+
}
|
|
13621
|
+
});
|
|
13391
13622
|
}
|
|
13392
13623
|
var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
|
|
13393
13624
|
var TOOL_ENV_FLAG = "HILLCLIMB_UPLOAD_TOOL";
|
|
@@ -13498,22 +13729,26 @@ async function runUploadWorker() {
|
|
|
13498
13729
|
tool: resolveSourceTool(payload),
|
|
13499
13730
|
payload
|
|
13500
13731
|
});
|
|
13732
|
+
try {
|
|
13733
|
+
await sweepStaleUploadStates();
|
|
13734
|
+
} catch {
|
|
13735
|
+
}
|
|
13501
13736
|
}
|
|
13502
13737
|
}
|
|
13503
13738
|
|
|
13504
13739
|
// src/git-traces/index.ts
|
|
13505
13740
|
import { spawn as spawn3 } from "child_process";
|
|
13506
|
-
import
|
|
13741
|
+
import crypto4 from "crypto";
|
|
13507
13742
|
|
|
13508
13743
|
// src/git-traces/handlers.ts
|
|
13509
13744
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
13510
|
-
import
|
|
13745
|
+
import path17 from "path";
|
|
13511
13746
|
|
|
13512
13747
|
// src/git-traces/git-ops.ts
|
|
13513
13748
|
import { execFileSync } from "child_process";
|
|
13514
|
-
import
|
|
13515
|
-
import
|
|
13516
|
-
import
|
|
13749
|
+
import fs12 from "fs";
|
|
13750
|
+
import os7 from "os";
|
|
13751
|
+
import path15 from "path";
|
|
13517
13752
|
import { gzipSync } from "zlib";
|
|
13518
13753
|
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
13519
13754
|
var EXEC_OPTS = {
|
|
@@ -13592,7 +13827,7 @@ function isGitRepo(dir) {
|
|
|
13592
13827
|
return false;
|
|
13593
13828
|
}
|
|
13594
13829
|
}
|
|
13595
|
-
function
|
|
13830
|
+
function captureWorkingCommitSha(repoRoot) {
|
|
13596
13831
|
try {
|
|
13597
13832
|
const sha = git(repoRoot, ["stash", "create"]);
|
|
13598
13833
|
if (sha) return sha;
|
|
@@ -13607,6 +13842,9 @@ function captureBaselineSha(repoRoot) {
|
|
|
13607
13842
|
return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
|
|
13608
13843
|
}
|
|
13609
13844
|
}
|
|
13845
|
+
function captureBaselineSha(repoRoot) {
|
|
13846
|
+
return captureWorkingCommitSha(repoRoot);
|
|
13847
|
+
}
|
|
13610
13848
|
function pinRef(repoRoot, refName, sha) {
|
|
13611
13849
|
git(repoRoot, ["update-ref", refName, sha]);
|
|
13612
13850
|
}
|
|
@@ -13617,13 +13855,7 @@ function deleteRef(repoRoot, refName) {
|
|
|
13617
13855
|
}
|
|
13618
13856
|
}
|
|
13619
13857
|
function captureSnapshotSha(repoRoot) {
|
|
13620
|
-
|
|
13621
|
-
const sha = git(repoRoot, ["stash", "create"]);
|
|
13622
|
-
if (sha) return sha;
|
|
13623
|
-
} catch (err) {
|
|
13624
|
-
if (isGitTimeoutError(err)) throw err;
|
|
13625
|
-
}
|
|
13626
|
-
return git(repoRoot, ["rev-parse", "HEAD"]);
|
|
13858
|
+
return captureWorkingCommitSha(repoRoot);
|
|
13627
13859
|
}
|
|
13628
13860
|
function recordOmittedSnapshotFile(omittedFiles, file, options = {}) {
|
|
13629
13861
|
omittedFiles?.push(file);
|
|
@@ -13679,8 +13911,8 @@ function removePathsFromIndex(repoRoot, env, paths) {
|
|
|
13679
13911
|
function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
13680
13912
|
const oversizedFiles = listOversizedTreeFiles(repoRoot, treeSha, options);
|
|
13681
13913
|
if (oversizedFiles.length === 0) return treeSha;
|
|
13682
|
-
const tmpIndex =
|
|
13683
|
-
|
|
13914
|
+
const tmpIndex = path15.join(
|
|
13915
|
+
os7.tmpdir(),
|
|
13684
13916
|
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
13685
13917
|
);
|
|
13686
13918
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13694,7 +13926,7 @@ function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
|
13694
13926
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13695
13927
|
} finally {
|
|
13696
13928
|
try {
|
|
13697
|
-
|
|
13929
|
+
fs12.unlinkSync(tmpIndex);
|
|
13698
13930
|
} catch {
|
|
13699
13931
|
}
|
|
13700
13932
|
}
|
|
@@ -13711,7 +13943,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13711
13943
|
for (const relPath of list.split("\0")) {
|
|
13712
13944
|
if (!relPath) continue;
|
|
13713
13945
|
try {
|
|
13714
|
-
const stat =
|
|
13946
|
+
const stat = fs12.lstatSync(path15.join(repoRoot, relPath));
|
|
13715
13947
|
if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
|
|
13716
13948
|
recordOmittedSnapshotFile(omittedFiles, {
|
|
13717
13949
|
path: relPath,
|
|
@@ -13726,8 +13958,8 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13726
13958
|
}
|
|
13727
13959
|
}
|
|
13728
13960
|
if (kept.length === 0) return null;
|
|
13729
|
-
const tmpIndex =
|
|
13730
|
-
|
|
13961
|
+
const tmpIndex = path15.join(
|
|
13962
|
+
os7.tmpdir(),
|
|
13731
13963
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
13732
13964
|
);
|
|
13733
13965
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13739,7 +13971,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13739
13971
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13740
13972
|
} finally {
|
|
13741
13973
|
try {
|
|
13742
|
-
|
|
13974
|
+
fs12.unlinkSync(tmpIndex);
|
|
13743
13975
|
} catch {
|
|
13744
13976
|
}
|
|
13745
13977
|
}
|
|
@@ -13756,8 +13988,8 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13756
13988
|
const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
|
|
13757
13989
|
if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
|
|
13758
13990
|
return filteredTrackedTree;
|
|
13759
|
-
const tmpIndex =
|
|
13760
|
-
|
|
13991
|
+
const tmpIndex = path15.join(
|
|
13992
|
+
os7.tmpdir(),
|
|
13761
13993
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
13762
13994
|
);
|
|
13763
13995
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13784,7 +14016,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13784
14016
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13785
14017
|
} finally {
|
|
13786
14018
|
try {
|
|
13787
|
-
|
|
14019
|
+
fs12.unlinkSync(tmpIndex);
|
|
13788
14020
|
} catch {
|
|
13789
14021
|
}
|
|
13790
14022
|
}
|
|
@@ -13798,16 +14030,19 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
13798
14030
|
]);
|
|
13799
14031
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
13800
14032
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
13801
|
-
const tmpFile =
|
|
13802
|
-
|
|
13803
|
-
|
|
14033
|
+
const tmpFile = path15.join(
|
|
14034
|
+
os7.tmpdir(),
|
|
14035
|
+
// Include the pid (like the other temp files in this module) so concurrent
|
|
14036
|
+
// git-traces workers — e.g. two sessions, or a parent + subagent — don't
|
|
14037
|
+
// collide on the same `git bundle create` path and its `.lock`.
|
|
14038
|
+
`hillclimb-bundle-${Date.now()}-${process.pid}.bundle`
|
|
13804
14039
|
);
|
|
13805
14040
|
try {
|
|
13806
14041
|
git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
|
|
13807
|
-
return
|
|
14042
|
+
return fs12.readFileSync(tmpFile);
|
|
13808
14043
|
} finally {
|
|
13809
14044
|
try {
|
|
13810
|
-
|
|
14045
|
+
fs12.unlinkSync(tmpFile);
|
|
13811
14046
|
} catch {
|
|
13812
14047
|
}
|
|
13813
14048
|
deleteRef(repoRoot, orphanRef);
|
|
@@ -13905,7 +14140,7 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
13905
14140
|
...omittedFiles.length > 0 ? { omittedFiles } : {}
|
|
13906
14141
|
},
|
|
13907
14142
|
author: { name: authorName, email: authorEmail },
|
|
13908
|
-
hostname:
|
|
14143
|
+
hostname: os7.hostname(),
|
|
13909
14144
|
cliVersion,
|
|
13910
14145
|
commits
|
|
13911
14146
|
};
|
|
@@ -13994,9 +14229,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
13994
14229
|
oldPath
|
|
13995
14230
|
});
|
|
13996
14231
|
} else {
|
|
13997
|
-
const
|
|
13998
|
-
indexByPath.set(
|
|
13999
|
-
files.push({ path:
|
|
14232
|
+
const path24 = parts[parts.length - 1];
|
|
14233
|
+
indexByPath.set(path24, files.length);
|
|
14234
|
+
files.push({ path: path24, status, additions: 0, deletions: 0 });
|
|
14000
14235
|
}
|
|
14001
14236
|
}
|
|
14002
14237
|
for (const line of numstat.split("\n")) {
|
|
@@ -14062,31 +14297,31 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
14062
14297
|
}
|
|
14063
14298
|
|
|
14064
14299
|
// src/git-traces/session-state.ts
|
|
14065
|
-
import
|
|
14066
|
-
import
|
|
14067
|
-
import
|
|
14068
|
-
import
|
|
14069
|
-
var
|
|
14070
|
-
var
|
|
14071
|
-
var
|
|
14072
|
-
var
|
|
14073
|
-
function
|
|
14074
|
-
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ??
|
|
14300
|
+
import crypto3 from "crypto";
|
|
14301
|
+
import fs13 from "fs";
|
|
14302
|
+
import os8 from "os";
|
|
14303
|
+
import path16 from "path";
|
|
14304
|
+
var CURRENT_SCHEMA_VERSION3 = 3;
|
|
14305
|
+
var DEFAULT_STATE_DIR2 = path16.join(os8.homedir(), ".hillclimb", "git-traces");
|
|
14306
|
+
var LOCK_RETRIES3 = 120;
|
|
14307
|
+
var LOCK_RETRY_DELAY_MS3 = 500;
|
|
14308
|
+
function stateDir3() {
|
|
14309
|
+
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
|
|
14075
14310
|
}
|
|
14076
14311
|
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
14077
|
-
const hash =
|
|
14078
|
-
sessionId ? `${
|
|
14312
|
+
const hash = crypto3.createHash("sha256").update(
|
|
14313
|
+
sessionId ? `${path16.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path16.resolve(repoRoot)}\0${tool}`
|
|
14079
14314
|
).digest("hex").slice(0, 16);
|
|
14080
|
-
return
|
|
14315
|
+
return path16.join(stateDir3(), `${hash}.json`);
|
|
14081
14316
|
}
|
|
14082
14317
|
function lockFileForRepo(repoRoot, tool) {
|
|
14083
14318
|
return `${stateFileForRepo(repoRoot, tool)}.lock`;
|
|
14084
14319
|
}
|
|
14085
14320
|
async function readStateFile(file) {
|
|
14086
14321
|
try {
|
|
14087
|
-
const raw = await
|
|
14322
|
+
const raw = await fs13.promises.readFile(file, "utf-8");
|
|
14088
14323
|
const parsed = JSON.parse(raw);
|
|
14089
|
-
if (parsed.schemaVersion !==
|
|
14324
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
|
|
14090
14325
|
return null;
|
|
14091
14326
|
}
|
|
14092
14327
|
return parsed;
|
|
@@ -14097,26 +14332,26 @@ async function readStateFile(file) {
|
|
|
14097
14332
|
async function listScopedSessionStates(repoRoot, tool) {
|
|
14098
14333
|
let entries;
|
|
14099
14334
|
try {
|
|
14100
|
-
entries = await
|
|
14335
|
+
entries = await fs13.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
14101
14336
|
} catch {
|
|
14102
14337
|
return [];
|
|
14103
14338
|
}
|
|
14104
14339
|
const states = [];
|
|
14105
14340
|
for (const entry of entries) {
|
|
14106
14341
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
14107
|
-
const file =
|
|
14342
|
+
const file = path16.join(stateDir3(), entry.name);
|
|
14108
14343
|
const state = await readStateFile(file);
|
|
14109
14344
|
if (!state) continue;
|
|
14110
14345
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
14111
14346
|
continue;
|
|
14112
14347
|
}
|
|
14113
|
-
if (
|
|
14114
|
-
if (
|
|
14348
|
+
if (path16.resolve(state.repoRoot) !== path16.resolve(repoRoot)) continue;
|
|
14349
|
+
if (path16.resolve(file) !== path16.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
14115
14350
|
continue;
|
|
14116
14351
|
}
|
|
14117
14352
|
let mtimeMs = 0;
|
|
14118
14353
|
try {
|
|
14119
|
-
mtimeMs = (await
|
|
14354
|
+
mtimeMs = (await fs13.promises.stat(file)).mtimeMs;
|
|
14120
14355
|
} catch {
|
|
14121
14356
|
continue;
|
|
14122
14357
|
}
|
|
@@ -14127,26 +14362,26 @@ async function listScopedSessionStates(repoRoot, tool) {
|
|
|
14127
14362
|
async function listSessionStatesForSession(tool, sessionId) {
|
|
14128
14363
|
let entries;
|
|
14129
14364
|
try {
|
|
14130
|
-
entries = await
|
|
14365
|
+
entries = await fs13.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
14131
14366
|
} catch {
|
|
14132
14367
|
return [];
|
|
14133
14368
|
}
|
|
14134
14369
|
const states = [];
|
|
14135
14370
|
for (const entry of entries) {
|
|
14136
14371
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
14137
|
-
const file =
|
|
14372
|
+
const file = path16.join(stateDir3(), entry.name);
|
|
14138
14373
|
const state = await readStateFile(file);
|
|
14139
14374
|
if (!state) continue;
|
|
14140
14375
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
14141
14376
|
continue;
|
|
14142
14377
|
}
|
|
14143
14378
|
if (state.sessionId !== sessionId) continue;
|
|
14144
|
-
if (
|
|
14379
|
+
if (path16.resolve(file) !== path16.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
14145
14380
|
continue;
|
|
14146
14381
|
}
|
|
14147
14382
|
let mtimeMs = 0;
|
|
14148
14383
|
try {
|
|
14149
|
-
mtimeMs = (await
|
|
14384
|
+
mtimeMs = (await fs13.promises.stat(file)).mtimeMs;
|
|
14150
14385
|
} catch {
|
|
14151
14386
|
continue;
|
|
14152
14387
|
}
|
|
@@ -14167,12 +14402,12 @@ async function readSessionState(repoRoot, tool, sessionId) {
|
|
|
14167
14402
|
}
|
|
14168
14403
|
async function writeSessionState(state, tool) {
|
|
14169
14404
|
const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
|
|
14170
|
-
await
|
|
14405
|
+
await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
14171
14406
|
const tmp = `${file}.tmp`;
|
|
14172
|
-
await
|
|
14407
|
+
await fs13.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
14173
14408
|
mode: 384
|
|
14174
14409
|
});
|
|
14175
|
-
await
|
|
14410
|
+
await fs13.promises.rename(tmp, file);
|
|
14176
14411
|
const legacyFile = stateFileForRepo(state.repoRoot, tool);
|
|
14177
14412
|
const legacy = await readStateFile(legacyFile);
|
|
14178
14413
|
if (legacy?.sessionId === state.sessionId) {
|
|
@@ -14181,7 +14416,7 @@ async function writeSessionState(state, tool) {
|
|
|
14181
14416
|
}
|
|
14182
14417
|
async function deleteStateFile(file) {
|
|
14183
14418
|
try {
|
|
14184
|
-
await
|
|
14419
|
+
await fs13.promises.unlink(file);
|
|
14185
14420
|
} catch {
|
|
14186
14421
|
}
|
|
14187
14422
|
}
|
|
@@ -14197,14 +14432,14 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
|
|
|
14197
14432
|
}
|
|
14198
14433
|
await deleteStateFile(stateFileForRepo(repoRoot, tool));
|
|
14199
14434
|
}
|
|
14200
|
-
async function
|
|
14435
|
+
async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES3, delayMs = LOCK_RETRY_DELAY_MS3) {
|
|
14201
14436
|
const lockPath = lockFileForRepo(repoRoot, tool);
|
|
14202
|
-
await
|
|
14437
|
+
await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
14203
14438
|
for (let i = 0; i < retries; i++) {
|
|
14204
14439
|
try {
|
|
14205
|
-
const fd = await
|
|
14440
|
+
const fd = await fs13.promises.open(
|
|
14206
14441
|
lockPath,
|
|
14207
|
-
|
|
14442
|
+
fs13.constants.O_CREAT | fs13.constants.O_EXCL | fs13.constants.O_WRONLY
|
|
14208
14443
|
);
|
|
14209
14444
|
await fd.write(String(process.pid));
|
|
14210
14445
|
await fd.close();
|
|
@@ -14219,9 +14454,9 @@ async function acquireLock2(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = L
|
|
|
14219
14454
|
}
|
|
14220
14455
|
throw new Error(`Failed to acquire lock after ${retries} retries`);
|
|
14221
14456
|
}
|
|
14222
|
-
async function
|
|
14457
|
+
async function releaseLock3(repoRoot, tool) {
|
|
14223
14458
|
try {
|
|
14224
|
-
await
|
|
14459
|
+
await fs13.promises.unlink(lockFileForRepo(repoRoot, tool));
|
|
14225
14460
|
} catch {
|
|
14226
14461
|
}
|
|
14227
14462
|
}
|
|
@@ -14244,12 +14479,12 @@ var TOOL_LABELS = {
|
|
|
14244
14479
|
async function loadConfiguredRepos() {
|
|
14245
14480
|
const file = await loadProjects();
|
|
14246
14481
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
14247
|
-
repoRoot:
|
|
14482
|
+
repoRoot: path17.resolve(repoRoot),
|
|
14248
14483
|
config
|
|
14249
14484
|
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
14250
14485
|
}
|
|
14251
14486
|
function repoLabel(repoRoot) {
|
|
14252
|
-
return
|
|
14487
|
+
return path17.basename(repoRoot) || repoRoot;
|
|
14253
14488
|
}
|
|
14254
14489
|
function resolveCwd2(payload) {
|
|
14255
14490
|
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
@@ -14522,7 +14757,7 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
14522
14757
|
});
|
|
14523
14758
|
if (!frozen) return null;
|
|
14524
14759
|
const state = {
|
|
14525
|
-
schemaVersion:
|
|
14760
|
+
schemaVersion: CURRENT_SCHEMA_VERSION3,
|
|
14526
14761
|
sessionId,
|
|
14527
14762
|
contributionId: null,
|
|
14528
14763
|
baselineSha: frozen.baselineSha,
|
|
@@ -14572,7 +14807,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14572
14807
|
);
|
|
14573
14808
|
return "skipped";
|
|
14574
14809
|
}
|
|
14575
|
-
await
|
|
14810
|
+
await acquireLock3(repoRoot, tool);
|
|
14576
14811
|
try {
|
|
14577
14812
|
const staleLegacy = await readSessionState(repoRoot, tool);
|
|
14578
14813
|
if (staleLegacy && staleLegacy.sessionId !== sessionId) {
|
|
@@ -14603,7 +14838,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14603
14838
|
);
|
|
14604
14839
|
return "failed";
|
|
14605
14840
|
} finally {
|
|
14606
|
-
await
|
|
14841
|
+
await releaseLock3(repoRoot, tool);
|
|
14607
14842
|
}
|
|
14608
14843
|
}
|
|
14609
14844
|
async function handleSessionStart(payload, tool) {
|
|
@@ -14703,7 +14938,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14703
14938
|
);
|
|
14704
14939
|
return "skipped";
|
|
14705
14940
|
}
|
|
14706
|
-
await
|
|
14941
|
+
await acquireLock3(repoRoot, tool);
|
|
14707
14942
|
let state = null;
|
|
14708
14943
|
try {
|
|
14709
14944
|
state = await readSessionState(repoRoot, tool, sessionId);
|
|
@@ -14904,7 +15139,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14904
15139
|
}
|
|
14905
15140
|
return "failed";
|
|
14906
15141
|
} finally {
|
|
14907
|
-
await
|
|
15142
|
+
await releaseLock3(repoRoot, tool);
|
|
14908
15143
|
}
|
|
14909
15144
|
}
|
|
14910
15145
|
async function handleStop(payload, tool) {
|
|
@@ -14921,7 +15156,7 @@ async function handleStop(payload, tool) {
|
|
|
14921
15156
|
if (sessionId) {
|
|
14922
15157
|
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
14923
15158
|
for (const { state } of storedStates) {
|
|
14924
|
-
const repo = repoByRoot.get(
|
|
15159
|
+
const repo = repoByRoot.get(path17.resolve(state.repoRoot));
|
|
14925
15160
|
if (!repo) {
|
|
14926
15161
|
missingConfig++;
|
|
14927
15162
|
appendLog(
|
|
@@ -14957,7 +15192,7 @@ async function handleStop(payload, tool) {
|
|
|
14957
15192
|
);
|
|
14958
15193
|
}
|
|
14959
15194
|
async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
14960
|
-
await
|
|
15195
|
+
await acquireLock3(repoRoot, tool);
|
|
14961
15196
|
try {
|
|
14962
15197
|
const state = await readSessionState(repoRoot, tool, sessionId);
|
|
14963
15198
|
if (!state) return "no-state";
|
|
@@ -14975,7 +15210,7 @@ async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
|
14975
15210
|
);
|
|
14976
15211
|
return "failed";
|
|
14977
15212
|
} finally {
|
|
14978
|
-
await
|
|
15213
|
+
await releaseLock3(repoRoot, tool);
|
|
14979
15214
|
}
|
|
14980
15215
|
}
|
|
14981
15216
|
async function handleSessionEnd(payload, tool) {
|
|
@@ -14987,7 +15222,7 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14987
15222
|
if (sessionId) {
|
|
14988
15223
|
const states = await listSessionStatesForSession(tool, sessionId);
|
|
14989
15224
|
for (const { state } of states) {
|
|
14990
|
-
repoRoots.push(
|
|
15225
|
+
repoRoots.push(path17.resolve(state.repoRoot));
|
|
14991
15226
|
}
|
|
14992
15227
|
}
|
|
14993
15228
|
if (repoRoots.length === 0 && cwd) {
|
|
@@ -15014,7 +15249,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
|
15014
15249
|
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
15015
15250
|
var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
|
|
15016
15251
|
function newFlowId() {
|
|
15017
|
-
return
|
|
15252
|
+
return crypto4.randomBytes(3).toString("hex");
|
|
15018
15253
|
}
|
|
15019
15254
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
15020
15255
|
"claude",
|
|
@@ -15300,29 +15535,29 @@ ${stack}` : ""}`
|
|
|
15300
15535
|
}
|
|
15301
15536
|
|
|
15302
15537
|
// src/outputs/zip.ts
|
|
15303
|
-
import
|
|
15304
|
-
import
|
|
15538
|
+
import fs15 from "fs";
|
|
15539
|
+
import path19 from "path";
|
|
15305
15540
|
import archiver2 from "archiver";
|
|
15306
15541
|
|
|
15307
15542
|
// src/outputs/downloads.ts
|
|
15308
15543
|
import { execSync as execSync2 } from "child_process";
|
|
15309
|
-
import
|
|
15310
|
-
import
|
|
15311
|
-
import
|
|
15544
|
+
import fs14 from "fs";
|
|
15545
|
+
import os9 from "os";
|
|
15546
|
+
import path18 from "path";
|
|
15312
15547
|
function getDownloadsFolder() {
|
|
15313
|
-
const home =
|
|
15548
|
+
const home = os9.homedir();
|
|
15314
15549
|
if (process.platform === "linux") {
|
|
15315
15550
|
try {
|
|
15316
15551
|
const xdgDir = execSync2("xdg-user-dir DOWNLOAD", {
|
|
15317
15552
|
encoding: "utf-8",
|
|
15318
15553
|
timeout: 3e3
|
|
15319
15554
|
}).trim();
|
|
15320
|
-
if (xdgDir &&
|
|
15555
|
+
if (xdgDir && fs14.existsSync(xdgDir)) return xdgDir;
|
|
15321
15556
|
} catch {
|
|
15322
15557
|
}
|
|
15323
15558
|
}
|
|
15324
|
-
const downloads =
|
|
15325
|
-
if (
|
|
15559
|
+
const downloads = path18.join(home, "Downloads");
|
|
15560
|
+
if (fs14.existsSync(downloads)) return downloads;
|
|
15326
15561
|
return home;
|
|
15327
15562
|
}
|
|
15328
15563
|
|
|
@@ -15331,11 +15566,11 @@ function sanitizeFilename(name) {
|
|
|
15331
15566
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
15332
15567
|
}
|
|
15333
15568
|
function getUniqueFilename(dir, base, ext) {
|
|
15334
|
-
let candidate =
|
|
15335
|
-
if (!
|
|
15569
|
+
let candidate = path19.join(dir, `${base}${ext}`);
|
|
15570
|
+
if (!fs15.existsSync(candidate)) return candidate;
|
|
15336
15571
|
let i = 1;
|
|
15337
|
-
while (
|
|
15338
|
-
candidate =
|
|
15572
|
+
while (fs15.existsSync(candidate)) {
|
|
15573
|
+
candidate = path19.join(dir, `${base}-${i}${ext}`);
|
|
15339
15574
|
i++;
|
|
15340
15575
|
}
|
|
15341
15576
|
return candidate;
|
|
@@ -15345,13 +15580,13 @@ var ZipOutput = class {
|
|
|
15345
15580
|
label = "Save as .zip to Downloads";
|
|
15346
15581
|
async emit(group, options) {
|
|
15347
15582
|
const downloadsDir = getDownloadsFolder();
|
|
15348
|
-
const repoName = sanitizeFilename(
|
|
15583
|
+
const repoName = sanitizeFilename(path19.basename(group.repoPath));
|
|
15349
15584
|
const timeRange = options.timeRange;
|
|
15350
15585
|
const rangePart = timeRange?.label ?? "all";
|
|
15351
15586
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
15352
15587
|
const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
|
|
15353
15588
|
const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
|
|
15354
|
-
const output =
|
|
15589
|
+
const output = fs15.createWriteStream(outputPath);
|
|
15355
15590
|
const archive = archiver2("zip", { zlib: { level: 6 } });
|
|
15356
15591
|
const done = new Promise((resolve, reject) => {
|
|
15357
15592
|
output.on("close", resolve);
|
|
@@ -15545,15 +15780,15 @@ async function confirmExport(group, output) {
|
|
|
15545
15780
|
}
|
|
15546
15781
|
|
|
15547
15782
|
// src/sources/claude.ts
|
|
15548
|
-
import
|
|
15549
|
-
import
|
|
15550
|
-
import
|
|
15783
|
+
import fs16 from "fs";
|
|
15784
|
+
import os10 from "os";
|
|
15785
|
+
import path20 from "path";
|
|
15551
15786
|
import readline from "readline";
|
|
15552
15787
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
15553
15788
|
async function resolveRepoPath(projectDir) {
|
|
15554
|
-
const indexPath =
|
|
15789
|
+
const indexPath = path20.join(projectDir, "sessions-index.json");
|
|
15555
15790
|
try {
|
|
15556
|
-
const raw = await
|
|
15791
|
+
const raw = await fs16.promises.readFile(indexPath, "utf-8");
|
|
15557
15792
|
const data = JSON.parse(raw);
|
|
15558
15793
|
if (data.originalPath && typeof data.originalPath === "string") {
|
|
15559
15794
|
return data.originalPath;
|
|
@@ -15561,12 +15796,12 @@ async function resolveRepoPath(projectDir) {
|
|
|
15561
15796
|
} catch {
|
|
15562
15797
|
}
|
|
15563
15798
|
const cwdCounts = /* @__PURE__ */ new Map();
|
|
15564
|
-
const entries = await
|
|
15799
|
+
const entries = await fs16.promises.readdir(projectDir, {
|
|
15565
15800
|
withFileTypes: true
|
|
15566
15801
|
});
|
|
15567
15802
|
for (const entry of entries) {
|
|
15568
15803
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
15569
|
-
const cwd = await extractCwdFromJsonl(
|
|
15804
|
+
const cwd = await extractCwdFromJsonl(path20.join(projectDir, entry.name));
|
|
15570
15805
|
if (cwd) {
|
|
15571
15806
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
15572
15807
|
}
|
|
@@ -15585,7 +15820,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
15585
15820
|
return null;
|
|
15586
15821
|
}
|
|
15587
15822
|
async function extractCwdFromJsonl(filePath) {
|
|
15588
|
-
const stream =
|
|
15823
|
+
const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
|
|
15589
15824
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15590
15825
|
try {
|
|
15591
15826
|
for await (const line of rl) {
|
|
@@ -15607,12 +15842,12 @@ async function extractCwdFromJsonl(filePath) {
|
|
|
15607
15842
|
async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
15608
15843
|
let entries;
|
|
15609
15844
|
try {
|
|
15610
|
-
entries = await
|
|
15845
|
+
entries = await fs16.promises.readdir(dir, { withFileTypes: true });
|
|
15611
15846
|
} catch {
|
|
15612
15847
|
return;
|
|
15613
15848
|
}
|
|
15614
15849
|
for (const entry of entries) {
|
|
15615
|
-
const fullPath =
|
|
15850
|
+
const fullPath = path20.join(dir, entry.name);
|
|
15616
15851
|
if (entry.isDirectory()) {
|
|
15617
15852
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
15618
15853
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -15634,19 +15869,19 @@ function fallbackDecode(encodedName) {
|
|
|
15634
15869
|
var ClaudeSource = class {
|
|
15635
15870
|
name = "claude";
|
|
15636
15871
|
async scan() {
|
|
15637
|
-
const baseDir =
|
|
15872
|
+
const baseDir = path20.join(os10.homedir(), ".claude", "projects");
|
|
15638
15873
|
try {
|
|
15639
|
-
await
|
|
15874
|
+
await fs16.promises.access(baseDir);
|
|
15640
15875
|
} catch {
|
|
15641
15876
|
return [];
|
|
15642
15877
|
}
|
|
15643
|
-
const projectDirs = await
|
|
15878
|
+
const projectDirs = await fs16.promises.readdir(baseDir, {
|
|
15644
15879
|
withFileTypes: true
|
|
15645
15880
|
});
|
|
15646
15881
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
15647
15882
|
const resultArrays = await Promise.all(
|
|
15648
15883
|
dirEntries.map(async (dir) => {
|
|
15649
|
-
const projectPath =
|
|
15884
|
+
const projectPath = path20.join(baseDir, dir.name);
|
|
15650
15885
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
15651
15886
|
const files = [];
|
|
15652
15887
|
await collectFiles(
|
|
@@ -15664,12 +15899,12 @@ var ClaudeSource = class {
|
|
|
15664
15899
|
};
|
|
15665
15900
|
|
|
15666
15901
|
// src/sources/codex.ts
|
|
15667
|
-
import
|
|
15668
|
-
import
|
|
15669
|
-
import
|
|
15902
|
+
import fs17 from "fs";
|
|
15903
|
+
import os11 from "os";
|
|
15904
|
+
import path21 from "path";
|
|
15670
15905
|
import readline2 from "readline";
|
|
15671
15906
|
async function parseSessionMeta(filePath) {
|
|
15672
|
-
const stream =
|
|
15907
|
+
const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
|
|
15673
15908
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15674
15909
|
try {
|
|
15675
15910
|
for await (const line of rl) {
|
|
@@ -15694,12 +15929,12 @@ async function findJsonlFiles(dir) {
|
|
|
15694
15929
|
async function walk(d) {
|
|
15695
15930
|
let entries;
|
|
15696
15931
|
try {
|
|
15697
|
-
entries = await
|
|
15932
|
+
entries = await fs17.promises.readdir(d, { withFileTypes: true });
|
|
15698
15933
|
} catch {
|
|
15699
15934
|
return;
|
|
15700
15935
|
}
|
|
15701
15936
|
for (const entry of entries) {
|
|
15702
|
-
const full =
|
|
15937
|
+
const full = path21.join(d, entry.name);
|
|
15703
15938
|
if (entry.isDirectory()) {
|
|
15704
15939
|
await walk(full);
|
|
15705
15940
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -15713,11 +15948,11 @@ async function findJsonlFiles(dir) {
|
|
|
15713
15948
|
async function loadHistory(historyPath) {
|
|
15714
15949
|
const map = /* @__PURE__ */ new Map();
|
|
15715
15950
|
try {
|
|
15716
|
-
await
|
|
15951
|
+
await fs17.promises.access(historyPath);
|
|
15717
15952
|
} catch {
|
|
15718
15953
|
return map;
|
|
15719
15954
|
}
|
|
15720
|
-
const stream =
|
|
15955
|
+
const stream = fs17.createReadStream(historyPath, { encoding: "utf-8" });
|
|
15721
15956
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15722
15957
|
try {
|
|
15723
15958
|
for await (const line of rl) {
|
|
@@ -15744,14 +15979,14 @@ async function loadHistory(historyPath) {
|
|
|
15744
15979
|
var CodexSource = class {
|
|
15745
15980
|
name = "codex";
|
|
15746
15981
|
async scan() {
|
|
15747
|
-
const codexDir =
|
|
15748
|
-
const sessionsDir =
|
|
15982
|
+
const codexDir = path21.join(os11.homedir(), ".codex");
|
|
15983
|
+
const sessionsDir = path21.join(codexDir, "sessions");
|
|
15749
15984
|
try {
|
|
15750
|
-
await
|
|
15985
|
+
await fs17.promises.access(sessionsDir);
|
|
15751
15986
|
} catch {
|
|
15752
15987
|
return [];
|
|
15753
15988
|
}
|
|
15754
|
-
const historyPath =
|
|
15989
|
+
const historyPath = path21.join(codexDir, "history.jsonl");
|
|
15755
15990
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
15756
15991
|
findJsonlFiles(sessionsDir),
|
|
15757
15992
|
loadHistory(historyPath)
|
|
@@ -15774,8 +16009,8 @@ var CodexSource = class {
|
|
|
15774
16009
|
});
|
|
15775
16010
|
const historyLines = historyMap.get(meta.sessionId);
|
|
15776
16011
|
if (historyLines) {
|
|
15777
|
-
const sessionDir =
|
|
15778
|
-
const historyAbsPath =
|
|
16012
|
+
const sessionDir = path21.relative(sessionsDir, path21.dirname(filePath));
|
|
16013
|
+
const historyAbsPath = path21.join(
|
|
15779
16014
|
sessionsDir,
|
|
15780
16015
|
sessionDir,
|
|
15781
16016
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -15795,18 +16030,18 @@ var CodexSource = class {
|
|
|
15795
16030
|
};
|
|
15796
16031
|
|
|
15797
16032
|
// src/sources/copilotChat.ts
|
|
15798
|
-
import
|
|
15799
|
-
import
|
|
15800
|
-
import
|
|
16033
|
+
import fs18 from "fs";
|
|
16034
|
+
import os12 from "os";
|
|
16035
|
+
import path22 from "path";
|
|
15801
16036
|
import { fileURLToPath } from "url";
|
|
15802
16037
|
function vsCodeUserDirs() {
|
|
15803
|
-
const home =
|
|
16038
|
+
const home = os12.homedir();
|
|
15804
16039
|
const dirs = [
|
|
15805
|
-
|
|
15806
|
-
|
|
16040
|
+
path22.join(home, "Library", "Application Support", "Code", "User"),
|
|
16041
|
+
path22.join(home, ".config", "Code", "User")
|
|
15807
16042
|
];
|
|
15808
16043
|
if (process.env.APPDATA) {
|
|
15809
|
-
dirs.push(
|
|
16044
|
+
dirs.push(path22.join(process.env.APPDATA, "Code", "User"));
|
|
15810
16045
|
}
|
|
15811
16046
|
return dirs;
|
|
15812
16047
|
}
|
|
@@ -15821,7 +16056,7 @@ function uriToFsPath(uri) {
|
|
|
15821
16056
|
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
15822
16057
|
let raw;
|
|
15823
16058
|
try {
|
|
15824
|
-
raw = await
|
|
16059
|
+
raw = await fs18.promises.readFile(workspaceJsonPath, "utf-8");
|
|
15825
16060
|
} catch {
|
|
15826
16061
|
return null;
|
|
15827
16062
|
}
|
|
@@ -15843,10 +16078,10 @@ var CopilotChatSource = class {
|
|
|
15843
16078
|
async scan() {
|
|
15844
16079
|
const results = [];
|
|
15845
16080
|
for (const userDir of vsCodeUserDirs()) {
|
|
15846
|
-
const workspaceStorage =
|
|
16081
|
+
const workspaceStorage = path22.join(userDir, "workspaceStorage");
|
|
15847
16082
|
let hashDirs;
|
|
15848
16083
|
try {
|
|
15849
|
-
hashDirs = await
|
|
16084
|
+
hashDirs = await fs18.promises.readdir(workspaceStorage, {
|
|
15850
16085
|
withFileTypes: true
|
|
15851
16086
|
});
|
|
15852
16087
|
} catch {
|
|
@@ -15854,22 +16089,22 @@ var CopilotChatSource = class {
|
|
|
15854
16089
|
}
|
|
15855
16090
|
for (const hash of hashDirs) {
|
|
15856
16091
|
if (!hash.isDirectory()) continue;
|
|
15857
|
-
const wsRoot =
|
|
15858
|
-
const transcriptsDir =
|
|
16092
|
+
const wsRoot = path22.join(workspaceStorage, hash.name);
|
|
16093
|
+
const transcriptsDir = path22.join(
|
|
15859
16094
|
wsRoot,
|
|
15860
16095
|
"GitHub.copilot-chat",
|
|
15861
16096
|
"transcripts"
|
|
15862
16097
|
);
|
|
15863
16098
|
let transcriptEntries;
|
|
15864
16099
|
try {
|
|
15865
|
-
transcriptEntries = await
|
|
16100
|
+
transcriptEntries = await fs18.promises.readdir(transcriptsDir, {
|
|
15866
16101
|
withFileTypes: true
|
|
15867
16102
|
});
|
|
15868
16103
|
} catch {
|
|
15869
16104
|
continue;
|
|
15870
16105
|
}
|
|
15871
16106
|
const repoPath = await readWorkspaceFolder(
|
|
15872
|
-
|
|
16107
|
+
path22.join(wsRoot, "workspace.json")
|
|
15873
16108
|
);
|
|
15874
16109
|
if (!repoPath) continue;
|
|
15875
16110
|
for (const entry of transcriptEntries) {
|
|
@@ -15877,7 +16112,7 @@ var CopilotChatSource = class {
|
|
|
15877
16112
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
15878
16113
|
results.push({
|
|
15879
16114
|
sourceName: this.name,
|
|
15880
|
-
absolutePath:
|
|
16115
|
+
absolutePath: path22.join(transcriptsDir, entry.name),
|
|
15881
16116
|
repoPath,
|
|
15882
16117
|
metadata: { sessionId }
|
|
15883
16118
|
});
|
|
@@ -15917,7 +16152,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
15917
16152
|
async function filterByTimeRange(group, range) {
|
|
15918
16153
|
const results = await Promise.all(
|
|
15919
16154
|
group.files.map(
|
|
15920
|
-
(f) =>
|
|
16155
|
+
(f) => fs19.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
15921
16156
|
)
|
|
15922
16157
|
);
|
|
15923
16158
|
const filtered = [];
|
|
@@ -15944,10 +16179,10 @@ async function runInteractive() {
|
|
|
15944
16179
|
s.start(`Scanning ${source.name} logs...`);
|
|
15945
16180
|
const allFiles = await source.scan();
|
|
15946
16181
|
const allGroups = await mergeByRepo(allFiles);
|
|
15947
|
-
const repoRoot =
|
|
16182
|
+
const repoRoot = path23.resolve(repo.root);
|
|
15948
16183
|
const matching = allGroups.filter((g) => {
|
|
15949
|
-
const resolved =
|
|
15950
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
16184
|
+
const resolved = path23.resolve(g.repoPath);
|
|
16185
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path23.sep);
|
|
15951
16186
|
});
|
|
15952
16187
|
if (matching.length === 0) {
|
|
15953
16188
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -15978,7 +16213,7 @@ async function runInteractive() {
|
|
|
15978
16213
|
}
|
|
15979
16214
|
}
|
|
15980
16215
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
15981
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
16216
|
+
const envFilePaths = envFileNames.map((n) => path23.join(repoRoot, n));
|
|
15982
16217
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
15983
16218
|
const secretResult = await collectSecrets(
|
|
15984
16219
|
repoRoot,
|