hillclimb 0.4.4 → 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 +478 -240
- 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 = {
|
|
@@ -13676,8 +13911,8 @@ function removePathsFromIndex(repoRoot, env, paths) {
|
|
|
13676
13911
|
function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
13677
13912
|
const oversizedFiles = listOversizedTreeFiles(repoRoot, treeSha, options);
|
|
13678
13913
|
if (oversizedFiles.length === 0) return treeSha;
|
|
13679
|
-
const tmpIndex =
|
|
13680
|
-
|
|
13914
|
+
const tmpIndex = path15.join(
|
|
13915
|
+
os7.tmpdir(),
|
|
13681
13916
|
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
13682
13917
|
);
|
|
13683
13918
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13691,7 +13926,7 @@ function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
|
13691
13926
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13692
13927
|
} finally {
|
|
13693
13928
|
try {
|
|
13694
|
-
|
|
13929
|
+
fs12.unlinkSync(tmpIndex);
|
|
13695
13930
|
} catch {
|
|
13696
13931
|
}
|
|
13697
13932
|
}
|
|
@@ -13708,7 +13943,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13708
13943
|
for (const relPath of list.split("\0")) {
|
|
13709
13944
|
if (!relPath) continue;
|
|
13710
13945
|
try {
|
|
13711
|
-
const stat =
|
|
13946
|
+
const stat = fs12.lstatSync(path15.join(repoRoot, relPath));
|
|
13712
13947
|
if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
|
|
13713
13948
|
recordOmittedSnapshotFile(omittedFiles, {
|
|
13714
13949
|
path: relPath,
|
|
@@ -13723,8 +13958,8 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13723
13958
|
}
|
|
13724
13959
|
}
|
|
13725
13960
|
if (kept.length === 0) return null;
|
|
13726
|
-
const tmpIndex =
|
|
13727
|
-
|
|
13961
|
+
const tmpIndex = path15.join(
|
|
13962
|
+
os7.tmpdir(),
|
|
13728
13963
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
13729
13964
|
);
|
|
13730
13965
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13736,7 +13971,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13736
13971
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13737
13972
|
} finally {
|
|
13738
13973
|
try {
|
|
13739
|
-
|
|
13974
|
+
fs12.unlinkSync(tmpIndex);
|
|
13740
13975
|
} catch {
|
|
13741
13976
|
}
|
|
13742
13977
|
}
|
|
@@ -13753,8 +13988,8 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13753
13988
|
const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
|
|
13754
13989
|
if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
|
|
13755
13990
|
return filteredTrackedTree;
|
|
13756
|
-
const tmpIndex =
|
|
13757
|
-
|
|
13991
|
+
const tmpIndex = path15.join(
|
|
13992
|
+
os7.tmpdir(),
|
|
13758
13993
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
13759
13994
|
);
|
|
13760
13995
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13781,7 +14016,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13781
14016
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13782
14017
|
} finally {
|
|
13783
14018
|
try {
|
|
13784
|
-
|
|
14019
|
+
fs12.unlinkSync(tmpIndex);
|
|
13785
14020
|
} catch {
|
|
13786
14021
|
}
|
|
13787
14022
|
}
|
|
@@ -13795,16 +14030,19 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
13795
14030
|
]);
|
|
13796
14031
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
13797
14032
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
13798
|
-
const tmpFile =
|
|
13799
|
-
|
|
13800
|
-
|
|
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`
|
|
13801
14039
|
);
|
|
13802
14040
|
try {
|
|
13803
14041
|
git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
|
|
13804
|
-
return
|
|
14042
|
+
return fs12.readFileSync(tmpFile);
|
|
13805
14043
|
} finally {
|
|
13806
14044
|
try {
|
|
13807
|
-
|
|
14045
|
+
fs12.unlinkSync(tmpFile);
|
|
13808
14046
|
} catch {
|
|
13809
14047
|
}
|
|
13810
14048
|
deleteRef(repoRoot, orphanRef);
|
|
@@ -13902,7 +14140,7 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
13902
14140
|
...omittedFiles.length > 0 ? { omittedFiles } : {}
|
|
13903
14141
|
},
|
|
13904
14142
|
author: { name: authorName, email: authorEmail },
|
|
13905
|
-
hostname:
|
|
14143
|
+
hostname: os7.hostname(),
|
|
13906
14144
|
cliVersion,
|
|
13907
14145
|
commits
|
|
13908
14146
|
};
|
|
@@ -13991,9 +14229,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
13991
14229
|
oldPath
|
|
13992
14230
|
});
|
|
13993
14231
|
} else {
|
|
13994
|
-
const
|
|
13995
|
-
indexByPath.set(
|
|
13996
|
-
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 });
|
|
13997
14235
|
}
|
|
13998
14236
|
}
|
|
13999
14237
|
for (const line of numstat.split("\n")) {
|
|
@@ -14059,31 +14297,31 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
14059
14297
|
}
|
|
14060
14298
|
|
|
14061
14299
|
// src/git-traces/session-state.ts
|
|
14062
|
-
import
|
|
14063
|
-
import
|
|
14064
|
-
import
|
|
14065
|
-
import
|
|
14066
|
-
var
|
|
14067
|
-
var
|
|
14068
|
-
var
|
|
14069
|
-
var
|
|
14070
|
-
function
|
|
14071
|
-
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;
|
|
14072
14310
|
}
|
|
14073
14311
|
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
14074
|
-
const hash =
|
|
14075
|
-
sessionId ? `${
|
|
14312
|
+
const hash = crypto3.createHash("sha256").update(
|
|
14313
|
+
sessionId ? `${path16.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path16.resolve(repoRoot)}\0${tool}`
|
|
14076
14314
|
).digest("hex").slice(0, 16);
|
|
14077
|
-
return
|
|
14315
|
+
return path16.join(stateDir3(), `${hash}.json`);
|
|
14078
14316
|
}
|
|
14079
14317
|
function lockFileForRepo(repoRoot, tool) {
|
|
14080
14318
|
return `${stateFileForRepo(repoRoot, tool)}.lock`;
|
|
14081
14319
|
}
|
|
14082
14320
|
async function readStateFile(file) {
|
|
14083
14321
|
try {
|
|
14084
|
-
const raw = await
|
|
14322
|
+
const raw = await fs13.promises.readFile(file, "utf-8");
|
|
14085
14323
|
const parsed = JSON.parse(raw);
|
|
14086
|
-
if (parsed.schemaVersion !==
|
|
14324
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
|
|
14087
14325
|
return null;
|
|
14088
14326
|
}
|
|
14089
14327
|
return parsed;
|
|
@@ -14094,26 +14332,26 @@ async function readStateFile(file) {
|
|
|
14094
14332
|
async function listScopedSessionStates(repoRoot, tool) {
|
|
14095
14333
|
let entries;
|
|
14096
14334
|
try {
|
|
14097
|
-
entries = await
|
|
14335
|
+
entries = await fs13.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
14098
14336
|
} catch {
|
|
14099
14337
|
return [];
|
|
14100
14338
|
}
|
|
14101
14339
|
const states = [];
|
|
14102
14340
|
for (const entry of entries) {
|
|
14103
14341
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
14104
|
-
const file =
|
|
14342
|
+
const file = path16.join(stateDir3(), entry.name);
|
|
14105
14343
|
const state = await readStateFile(file);
|
|
14106
14344
|
if (!state) continue;
|
|
14107
14345
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
14108
14346
|
continue;
|
|
14109
14347
|
}
|
|
14110
|
-
if (
|
|
14111
|
-
if (
|
|
14348
|
+
if (path16.resolve(state.repoRoot) !== path16.resolve(repoRoot)) continue;
|
|
14349
|
+
if (path16.resolve(file) !== path16.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
14112
14350
|
continue;
|
|
14113
14351
|
}
|
|
14114
14352
|
let mtimeMs = 0;
|
|
14115
14353
|
try {
|
|
14116
|
-
mtimeMs = (await
|
|
14354
|
+
mtimeMs = (await fs13.promises.stat(file)).mtimeMs;
|
|
14117
14355
|
} catch {
|
|
14118
14356
|
continue;
|
|
14119
14357
|
}
|
|
@@ -14124,26 +14362,26 @@ async function listScopedSessionStates(repoRoot, tool) {
|
|
|
14124
14362
|
async function listSessionStatesForSession(tool, sessionId) {
|
|
14125
14363
|
let entries;
|
|
14126
14364
|
try {
|
|
14127
|
-
entries = await
|
|
14365
|
+
entries = await fs13.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
14128
14366
|
} catch {
|
|
14129
14367
|
return [];
|
|
14130
14368
|
}
|
|
14131
14369
|
const states = [];
|
|
14132
14370
|
for (const entry of entries) {
|
|
14133
14371
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
14134
|
-
const file =
|
|
14372
|
+
const file = path16.join(stateDir3(), entry.name);
|
|
14135
14373
|
const state = await readStateFile(file);
|
|
14136
14374
|
if (!state) continue;
|
|
14137
14375
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
14138
14376
|
continue;
|
|
14139
14377
|
}
|
|
14140
14378
|
if (state.sessionId !== sessionId) continue;
|
|
14141
|
-
if (
|
|
14379
|
+
if (path16.resolve(file) !== path16.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
14142
14380
|
continue;
|
|
14143
14381
|
}
|
|
14144
14382
|
let mtimeMs = 0;
|
|
14145
14383
|
try {
|
|
14146
|
-
mtimeMs = (await
|
|
14384
|
+
mtimeMs = (await fs13.promises.stat(file)).mtimeMs;
|
|
14147
14385
|
} catch {
|
|
14148
14386
|
continue;
|
|
14149
14387
|
}
|
|
@@ -14164,12 +14402,12 @@ async function readSessionState(repoRoot, tool, sessionId) {
|
|
|
14164
14402
|
}
|
|
14165
14403
|
async function writeSessionState(state, tool) {
|
|
14166
14404
|
const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
|
|
14167
|
-
await
|
|
14405
|
+
await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
14168
14406
|
const tmp = `${file}.tmp`;
|
|
14169
|
-
await
|
|
14407
|
+
await fs13.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
14170
14408
|
mode: 384
|
|
14171
14409
|
});
|
|
14172
|
-
await
|
|
14410
|
+
await fs13.promises.rename(tmp, file);
|
|
14173
14411
|
const legacyFile = stateFileForRepo(state.repoRoot, tool);
|
|
14174
14412
|
const legacy = await readStateFile(legacyFile);
|
|
14175
14413
|
if (legacy?.sessionId === state.sessionId) {
|
|
@@ -14178,7 +14416,7 @@ async function writeSessionState(state, tool) {
|
|
|
14178
14416
|
}
|
|
14179
14417
|
async function deleteStateFile(file) {
|
|
14180
14418
|
try {
|
|
14181
|
-
await
|
|
14419
|
+
await fs13.promises.unlink(file);
|
|
14182
14420
|
} catch {
|
|
14183
14421
|
}
|
|
14184
14422
|
}
|
|
@@ -14194,14 +14432,14 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
|
|
|
14194
14432
|
}
|
|
14195
14433
|
await deleteStateFile(stateFileForRepo(repoRoot, tool));
|
|
14196
14434
|
}
|
|
14197
|
-
async function
|
|
14435
|
+
async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES3, delayMs = LOCK_RETRY_DELAY_MS3) {
|
|
14198
14436
|
const lockPath = lockFileForRepo(repoRoot, tool);
|
|
14199
|
-
await
|
|
14437
|
+
await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
14200
14438
|
for (let i = 0; i < retries; i++) {
|
|
14201
14439
|
try {
|
|
14202
|
-
const fd = await
|
|
14440
|
+
const fd = await fs13.promises.open(
|
|
14203
14441
|
lockPath,
|
|
14204
|
-
|
|
14442
|
+
fs13.constants.O_CREAT | fs13.constants.O_EXCL | fs13.constants.O_WRONLY
|
|
14205
14443
|
);
|
|
14206
14444
|
await fd.write(String(process.pid));
|
|
14207
14445
|
await fd.close();
|
|
@@ -14216,9 +14454,9 @@ async function acquireLock2(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = L
|
|
|
14216
14454
|
}
|
|
14217
14455
|
throw new Error(`Failed to acquire lock after ${retries} retries`);
|
|
14218
14456
|
}
|
|
14219
|
-
async function
|
|
14457
|
+
async function releaseLock3(repoRoot, tool) {
|
|
14220
14458
|
try {
|
|
14221
|
-
await
|
|
14459
|
+
await fs13.promises.unlink(lockFileForRepo(repoRoot, tool));
|
|
14222
14460
|
} catch {
|
|
14223
14461
|
}
|
|
14224
14462
|
}
|
|
@@ -14241,12 +14479,12 @@ var TOOL_LABELS = {
|
|
|
14241
14479
|
async function loadConfiguredRepos() {
|
|
14242
14480
|
const file = await loadProjects();
|
|
14243
14481
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
14244
|
-
repoRoot:
|
|
14482
|
+
repoRoot: path17.resolve(repoRoot),
|
|
14245
14483
|
config
|
|
14246
14484
|
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
14247
14485
|
}
|
|
14248
14486
|
function repoLabel(repoRoot) {
|
|
14249
|
-
return
|
|
14487
|
+
return path17.basename(repoRoot) || repoRoot;
|
|
14250
14488
|
}
|
|
14251
14489
|
function resolveCwd2(payload) {
|
|
14252
14490
|
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
@@ -14519,7 +14757,7 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
14519
14757
|
});
|
|
14520
14758
|
if (!frozen) return null;
|
|
14521
14759
|
const state = {
|
|
14522
|
-
schemaVersion:
|
|
14760
|
+
schemaVersion: CURRENT_SCHEMA_VERSION3,
|
|
14523
14761
|
sessionId,
|
|
14524
14762
|
contributionId: null,
|
|
14525
14763
|
baselineSha: frozen.baselineSha,
|
|
@@ -14569,7 +14807,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14569
14807
|
);
|
|
14570
14808
|
return "skipped";
|
|
14571
14809
|
}
|
|
14572
|
-
await
|
|
14810
|
+
await acquireLock3(repoRoot, tool);
|
|
14573
14811
|
try {
|
|
14574
14812
|
const staleLegacy = await readSessionState(repoRoot, tool);
|
|
14575
14813
|
if (staleLegacy && staleLegacy.sessionId !== sessionId) {
|
|
@@ -14600,7 +14838,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14600
14838
|
);
|
|
14601
14839
|
return "failed";
|
|
14602
14840
|
} finally {
|
|
14603
|
-
await
|
|
14841
|
+
await releaseLock3(repoRoot, tool);
|
|
14604
14842
|
}
|
|
14605
14843
|
}
|
|
14606
14844
|
async function handleSessionStart(payload, tool) {
|
|
@@ -14700,7 +14938,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14700
14938
|
);
|
|
14701
14939
|
return "skipped";
|
|
14702
14940
|
}
|
|
14703
|
-
await
|
|
14941
|
+
await acquireLock3(repoRoot, tool);
|
|
14704
14942
|
let state = null;
|
|
14705
14943
|
try {
|
|
14706
14944
|
state = await readSessionState(repoRoot, tool, sessionId);
|
|
@@ -14901,7 +15139,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14901
15139
|
}
|
|
14902
15140
|
return "failed";
|
|
14903
15141
|
} finally {
|
|
14904
|
-
await
|
|
15142
|
+
await releaseLock3(repoRoot, tool);
|
|
14905
15143
|
}
|
|
14906
15144
|
}
|
|
14907
15145
|
async function handleStop(payload, tool) {
|
|
@@ -14918,7 +15156,7 @@ async function handleStop(payload, tool) {
|
|
|
14918
15156
|
if (sessionId) {
|
|
14919
15157
|
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
14920
15158
|
for (const { state } of storedStates) {
|
|
14921
|
-
const repo = repoByRoot.get(
|
|
15159
|
+
const repo = repoByRoot.get(path17.resolve(state.repoRoot));
|
|
14922
15160
|
if (!repo) {
|
|
14923
15161
|
missingConfig++;
|
|
14924
15162
|
appendLog(
|
|
@@ -14954,7 +15192,7 @@ async function handleStop(payload, tool) {
|
|
|
14954
15192
|
);
|
|
14955
15193
|
}
|
|
14956
15194
|
async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
14957
|
-
await
|
|
15195
|
+
await acquireLock3(repoRoot, tool);
|
|
14958
15196
|
try {
|
|
14959
15197
|
const state = await readSessionState(repoRoot, tool, sessionId);
|
|
14960
15198
|
if (!state) return "no-state";
|
|
@@ -14972,7 +15210,7 @@ async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
|
14972
15210
|
);
|
|
14973
15211
|
return "failed";
|
|
14974
15212
|
} finally {
|
|
14975
|
-
await
|
|
15213
|
+
await releaseLock3(repoRoot, tool);
|
|
14976
15214
|
}
|
|
14977
15215
|
}
|
|
14978
15216
|
async function handleSessionEnd(payload, tool) {
|
|
@@ -14984,7 +15222,7 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14984
15222
|
if (sessionId) {
|
|
14985
15223
|
const states = await listSessionStatesForSession(tool, sessionId);
|
|
14986
15224
|
for (const { state } of states) {
|
|
14987
|
-
repoRoots.push(
|
|
15225
|
+
repoRoots.push(path17.resolve(state.repoRoot));
|
|
14988
15226
|
}
|
|
14989
15227
|
}
|
|
14990
15228
|
if (repoRoots.length === 0 && cwd) {
|
|
@@ -15011,7 +15249,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
|
15011
15249
|
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
15012
15250
|
var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
|
|
15013
15251
|
function newFlowId() {
|
|
15014
|
-
return
|
|
15252
|
+
return crypto4.randomBytes(3).toString("hex");
|
|
15015
15253
|
}
|
|
15016
15254
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
15017
15255
|
"claude",
|
|
@@ -15297,29 +15535,29 @@ ${stack}` : ""}`
|
|
|
15297
15535
|
}
|
|
15298
15536
|
|
|
15299
15537
|
// src/outputs/zip.ts
|
|
15300
|
-
import
|
|
15301
|
-
import
|
|
15538
|
+
import fs15 from "fs";
|
|
15539
|
+
import path19 from "path";
|
|
15302
15540
|
import archiver2 from "archiver";
|
|
15303
15541
|
|
|
15304
15542
|
// src/outputs/downloads.ts
|
|
15305
15543
|
import { execSync as execSync2 } from "child_process";
|
|
15306
|
-
import
|
|
15307
|
-
import
|
|
15308
|
-
import
|
|
15544
|
+
import fs14 from "fs";
|
|
15545
|
+
import os9 from "os";
|
|
15546
|
+
import path18 from "path";
|
|
15309
15547
|
function getDownloadsFolder() {
|
|
15310
|
-
const home =
|
|
15548
|
+
const home = os9.homedir();
|
|
15311
15549
|
if (process.platform === "linux") {
|
|
15312
15550
|
try {
|
|
15313
15551
|
const xdgDir = execSync2("xdg-user-dir DOWNLOAD", {
|
|
15314
15552
|
encoding: "utf-8",
|
|
15315
15553
|
timeout: 3e3
|
|
15316
15554
|
}).trim();
|
|
15317
|
-
if (xdgDir &&
|
|
15555
|
+
if (xdgDir && fs14.existsSync(xdgDir)) return xdgDir;
|
|
15318
15556
|
} catch {
|
|
15319
15557
|
}
|
|
15320
15558
|
}
|
|
15321
|
-
const downloads =
|
|
15322
|
-
if (
|
|
15559
|
+
const downloads = path18.join(home, "Downloads");
|
|
15560
|
+
if (fs14.existsSync(downloads)) return downloads;
|
|
15323
15561
|
return home;
|
|
15324
15562
|
}
|
|
15325
15563
|
|
|
@@ -15328,11 +15566,11 @@ function sanitizeFilename(name) {
|
|
|
15328
15566
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
15329
15567
|
}
|
|
15330
15568
|
function getUniqueFilename(dir, base, ext) {
|
|
15331
|
-
let candidate =
|
|
15332
|
-
if (!
|
|
15569
|
+
let candidate = path19.join(dir, `${base}${ext}`);
|
|
15570
|
+
if (!fs15.existsSync(candidate)) return candidate;
|
|
15333
15571
|
let i = 1;
|
|
15334
|
-
while (
|
|
15335
|
-
candidate =
|
|
15572
|
+
while (fs15.existsSync(candidate)) {
|
|
15573
|
+
candidate = path19.join(dir, `${base}-${i}${ext}`);
|
|
15336
15574
|
i++;
|
|
15337
15575
|
}
|
|
15338
15576
|
return candidate;
|
|
@@ -15342,13 +15580,13 @@ var ZipOutput = class {
|
|
|
15342
15580
|
label = "Save as .zip to Downloads";
|
|
15343
15581
|
async emit(group, options) {
|
|
15344
15582
|
const downloadsDir = getDownloadsFolder();
|
|
15345
|
-
const repoName = sanitizeFilename(
|
|
15583
|
+
const repoName = sanitizeFilename(path19.basename(group.repoPath));
|
|
15346
15584
|
const timeRange = options.timeRange;
|
|
15347
15585
|
const rangePart = timeRange?.label ?? "all";
|
|
15348
15586
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
15349
15587
|
const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
|
|
15350
15588
|
const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
|
|
15351
|
-
const output =
|
|
15589
|
+
const output = fs15.createWriteStream(outputPath);
|
|
15352
15590
|
const archive = archiver2("zip", { zlib: { level: 6 } });
|
|
15353
15591
|
const done = new Promise((resolve, reject) => {
|
|
15354
15592
|
output.on("close", resolve);
|
|
@@ -15542,15 +15780,15 @@ async function confirmExport(group, output) {
|
|
|
15542
15780
|
}
|
|
15543
15781
|
|
|
15544
15782
|
// src/sources/claude.ts
|
|
15545
|
-
import
|
|
15546
|
-
import
|
|
15547
|
-
import
|
|
15783
|
+
import fs16 from "fs";
|
|
15784
|
+
import os10 from "os";
|
|
15785
|
+
import path20 from "path";
|
|
15548
15786
|
import readline from "readline";
|
|
15549
15787
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
15550
15788
|
async function resolveRepoPath(projectDir) {
|
|
15551
|
-
const indexPath =
|
|
15789
|
+
const indexPath = path20.join(projectDir, "sessions-index.json");
|
|
15552
15790
|
try {
|
|
15553
|
-
const raw = await
|
|
15791
|
+
const raw = await fs16.promises.readFile(indexPath, "utf-8");
|
|
15554
15792
|
const data = JSON.parse(raw);
|
|
15555
15793
|
if (data.originalPath && typeof data.originalPath === "string") {
|
|
15556
15794
|
return data.originalPath;
|
|
@@ -15558,12 +15796,12 @@ async function resolveRepoPath(projectDir) {
|
|
|
15558
15796
|
} catch {
|
|
15559
15797
|
}
|
|
15560
15798
|
const cwdCounts = /* @__PURE__ */ new Map();
|
|
15561
|
-
const entries = await
|
|
15799
|
+
const entries = await fs16.promises.readdir(projectDir, {
|
|
15562
15800
|
withFileTypes: true
|
|
15563
15801
|
});
|
|
15564
15802
|
for (const entry of entries) {
|
|
15565
15803
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
15566
|
-
const cwd = await extractCwdFromJsonl(
|
|
15804
|
+
const cwd = await extractCwdFromJsonl(path20.join(projectDir, entry.name));
|
|
15567
15805
|
if (cwd) {
|
|
15568
15806
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
15569
15807
|
}
|
|
@@ -15582,7 +15820,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
15582
15820
|
return null;
|
|
15583
15821
|
}
|
|
15584
15822
|
async function extractCwdFromJsonl(filePath) {
|
|
15585
|
-
const stream =
|
|
15823
|
+
const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
|
|
15586
15824
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15587
15825
|
try {
|
|
15588
15826
|
for await (const line of rl) {
|
|
@@ -15604,12 +15842,12 @@ async function extractCwdFromJsonl(filePath) {
|
|
|
15604
15842
|
async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
15605
15843
|
let entries;
|
|
15606
15844
|
try {
|
|
15607
|
-
entries = await
|
|
15845
|
+
entries = await fs16.promises.readdir(dir, { withFileTypes: true });
|
|
15608
15846
|
} catch {
|
|
15609
15847
|
return;
|
|
15610
15848
|
}
|
|
15611
15849
|
for (const entry of entries) {
|
|
15612
|
-
const fullPath =
|
|
15850
|
+
const fullPath = path20.join(dir, entry.name);
|
|
15613
15851
|
if (entry.isDirectory()) {
|
|
15614
15852
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
15615
15853
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -15631,19 +15869,19 @@ function fallbackDecode(encodedName) {
|
|
|
15631
15869
|
var ClaudeSource = class {
|
|
15632
15870
|
name = "claude";
|
|
15633
15871
|
async scan() {
|
|
15634
|
-
const baseDir =
|
|
15872
|
+
const baseDir = path20.join(os10.homedir(), ".claude", "projects");
|
|
15635
15873
|
try {
|
|
15636
|
-
await
|
|
15874
|
+
await fs16.promises.access(baseDir);
|
|
15637
15875
|
} catch {
|
|
15638
15876
|
return [];
|
|
15639
15877
|
}
|
|
15640
|
-
const projectDirs = await
|
|
15878
|
+
const projectDirs = await fs16.promises.readdir(baseDir, {
|
|
15641
15879
|
withFileTypes: true
|
|
15642
15880
|
});
|
|
15643
15881
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
15644
15882
|
const resultArrays = await Promise.all(
|
|
15645
15883
|
dirEntries.map(async (dir) => {
|
|
15646
|
-
const projectPath =
|
|
15884
|
+
const projectPath = path20.join(baseDir, dir.name);
|
|
15647
15885
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
15648
15886
|
const files = [];
|
|
15649
15887
|
await collectFiles(
|
|
@@ -15661,12 +15899,12 @@ var ClaudeSource = class {
|
|
|
15661
15899
|
};
|
|
15662
15900
|
|
|
15663
15901
|
// src/sources/codex.ts
|
|
15664
|
-
import
|
|
15665
|
-
import
|
|
15666
|
-
import
|
|
15902
|
+
import fs17 from "fs";
|
|
15903
|
+
import os11 from "os";
|
|
15904
|
+
import path21 from "path";
|
|
15667
15905
|
import readline2 from "readline";
|
|
15668
15906
|
async function parseSessionMeta(filePath) {
|
|
15669
|
-
const stream =
|
|
15907
|
+
const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
|
|
15670
15908
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15671
15909
|
try {
|
|
15672
15910
|
for await (const line of rl) {
|
|
@@ -15691,12 +15929,12 @@ async function findJsonlFiles(dir) {
|
|
|
15691
15929
|
async function walk(d) {
|
|
15692
15930
|
let entries;
|
|
15693
15931
|
try {
|
|
15694
|
-
entries = await
|
|
15932
|
+
entries = await fs17.promises.readdir(d, { withFileTypes: true });
|
|
15695
15933
|
} catch {
|
|
15696
15934
|
return;
|
|
15697
15935
|
}
|
|
15698
15936
|
for (const entry of entries) {
|
|
15699
|
-
const full =
|
|
15937
|
+
const full = path21.join(d, entry.name);
|
|
15700
15938
|
if (entry.isDirectory()) {
|
|
15701
15939
|
await walk(full);
|
|
15702
15940
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -15710,11 +15948,11 @@ async function findJsonlFiles(dir) {
|
|
|
15710
15948
|
async function loadHistory(historyPath) {
|
|
15711
15949
|
const map = /* @__PURE__ */ new Map();
|
|
15712
15950
|
try {
|
|
15713
|
-
await
|
|
15951
|
+
await fs17.promises.access(historyPath);
|
|
15714
15952
|
} catch {
|
|
15715
15953
|
return map;
|
|
15716
15954
|
}
|
|
15717
|
-
const stream =
|
|
15955
|
+
const stream = fs17.createReadStream(historyPath, { encoding: "utf-8" });
|
|
15718
15956
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15719
15957
|
try {
|
|
15720
15958
|
for await (const line of rl) {
|
|
@@ -15741,14 +15979,14 @@ async function loadHistory(historyPath) {
|
|
|
15741
15979
|
var CodexSource = class {
|
|
15742
15980
|
name = "codex";
|
|
15743
15981
|
async scan() {
|
|
15744
|
-
const codexDir =
|
|
15745
|
-
const sessionsDir =
|
|
15982
|
+
const codexDir = path21.join(os11.homedir(), ".codex");
|
|
15983
|
+
const sessionsDir = path21.join(codexDir, "sessions");
|
|
15746
15984
|
try {
|
|
15747
|
-
await
|
|
15985
|
+
await fs17.promises.access(sessionsDir);
|
|
15748
15986
|
} catch {
|
|
15749
15987
|
return [];
|
|
15750
15988
|
}
|
|
15751
|
-
const historyPath =
|
|
15989
|
+
const historyPath = path21.join(codexDir, "history.jsonl");
|
|
15752
15990
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
15753
15991
|
findJsonlFiles(sessionsDir),
|
|
15754
15992
|
loadHistory(historyPath)
|
|
@@ -15771,8 +16009,8 @@ var CodexSource = class {
|
|
|
15771
16009
|
});
|
|
15772
16010
|
const historyLines = historyMap.get(meta.sessionId);
|
|
15773
16011
|
if (historyLines) {
|
|
15774
|
-
const sessionDir =
|
|
15775
|
-
const historyAbsPath =
|
|
16012
|
+
const sessionDir = path21.relative(sessionsDir, path21.dirname(filePath));
|
|
16013
|
+
const historyAbsPath = path21.join(
|
|
15776
16014
|
sessionsDir,
|
|
15777
16015
|
sessionDir,
|
|
15778
16016
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -15792,18 +16030,18 @@ var CodexSource = class {
|
|
|
15792
16030
|
};
|
|
15793
16031
|
|
|
15794
16032
|
// src/sources/copilotChat.ts
|
|
15795
|
-
import
|
|
15796
|
-
import
|
|
15797
|
-
import
|
|
16033
|
+
import fs18 from "fs";
|
|
16034
|
+
import os12 from "os";
|
|
16035
|
+
import path22 from "path";
|
|
15798
16036
|
import { fileURLToPath } from "url";
|
|
15799
16037
|
function vsCodeUserDirs() {
|
|
15800
|
-
const home =
|
|
16038
|
+
const home = os12.homedir();
|
|
15801
16039
|
const dirs = [
|
|
15802
|
-
|
|
15803
|
-
|
|
16040
|
+
path22.join(home, "Library", "Application Support", "Code", "User"),
|
|
16041
|
+
path22.join(home, ".config", "Code", "User")
|
|
15804
16042
|
];
|
|
15805
16043
|
if (process.env.APPDATA) {
|
|
15806
|
-
dirs.push(
|
|
16044
|
+
dirs.push(path22.join(process.env.APPDATA, "Code", "User"));
|
|
15807
16045
|
}
|
|
15808
16046
|
return dirs;
|
|
15809
16047
|
}
|
|
@@ -15818,7 +16056,7 @@ function uriToFsPath(uri) {
|
|
|
15818
16056
|
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
15819
16057
|
let raw;
|
|
15820
16058
|
try {
|
|
15821
|
-
raw = await
|
|
16059
|
+
raw = await fs18.promises.readFile(workspaceJsonPath, "utf-8");
|
|
15822
16060
|
} catch {
|
|
15823
16061
|
return null;
|
|
15824
16062
|
}
|
|
@@ -15840,10 +16078,10 @@ var CopilotChatSource = class {
|
|
|
15840
16078
|
async scan() {
|
|
15841
16079
|
const results = [];
|
|
15842
16080
|
for (const userDir of vsCodeUserDirs()) {
|
|
15843
|
-
const workspaceStorage =
|
|
16081
|
+
const workspaceStorage = path22.join(userDir, "workspaceStorage");
|
|
15844
16082
|
let hashDirs;
|
|
15845
16083
|
try {
|
|
15846
|
-
hashDirs = await
|
|
16084
|
+
hashDirs = await fs18.promises.readdir(workspaceStorage, {
|
|
15847
16085
|
withFileTypes: true
|
|
15848
16086
|
});
|
|
15849
16087
|
} catch {
|
|
@@ -15851,22 +16089,22 @@ var CopilotChatSource = class {
|
|
|
15851
16089
|
}
|
|
15852
16090
|
for (const hash of hashDirs) {
|
|
15853
16091
|
if (!hash.isDirectory()) continue;
|
|
15854
|
-
const wsRoot =
|
|
15855
|
-
const transcriptsDir =
|
|
16092
|
+
const wsRoot = path22.join(workspaceStorage, hash.name);
|
|
16093
|
+
const transcriptsDir = path22.join(
|
|
15856
16094
|
wsRoot,
|
|
15857
16095
|
"GitHub.copilot-chat",
|
|
15858
16096
|
"transcripts"
|
|
15859
16097
|
);
|
|
15860
16098
|
let transcriptEntries;
|
|
15861
16099
|
try {
|
|
15862
|
-
transcriptEntries = await
|
|
16100
|
+
transcriptEntries = await fs18.promises.readdir(transcriptsDir, {
|
|
15863
16101
|
withFileTypes: true
|
|
15864
16102
|
});
|
|
15865
16103
|
} catch {
|
|
15866
16104
|
continue;
|
|
15867
16105
|
}
|
|
15868
16106
|
const repoPath = await readWorkspaceFolder(
|
|
15869
|
-
|
|
16107
|
+
path22.join(wsRoot, "workspace.json")
|
|
15870
16108
|
);
|
|
15871
16109
|
if (!repoPath) continue;
|
|
15872
16110
|
for (const entry of transcriptEntries) {
|
|
@@ -15874,7 +16112,7 @@ var CopilotChatSource = class {
|
|
|
15874
16112
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
15875
16113
|
results.push({
|
|
15876
16114
|
sourceName: this.name,
|
|
15877
|
-
absolutePath:
|
|
16115
|
+
absolutePath: path22.join(transcriptsDir, entry.name),
|
|
15878
16116
|
repoPath,
|
|
15879
16117
|
metadata: { sessionId }
|
|
15880
16118
|
});
|
|
@@ -15914,7 +16152,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
15914
16152
|
async function filterByTimeRange(group, range) {
|
|
15915
16153
|
const results = await Promise.all(
|
|
15916
16154
|
group.files.map(
|
|
15917
|
-
(f) =>
|
|
16155
|
+
(f) => fs19.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
15918
16156
|
)
|
|
15919
16157
|
);
|
|
15920
16158
|
const filtered = [];
|
|
@@ -15941,10 +16179,10 @@ async function runInteractive() {
|
|
|
15941
16179
|
s.start(`Scanning ${source.name} logs...`);
|
|
15942
16180
|
const allFiles = await source.scan();
|
|
15943
16181
|
const allGroups = await mergeByRepo(allFiles);
|
|
15944
|
-
const repoRoot =
|
|
16182
|
+
const repoRoot = path23.resolve(repo.root);
|
|
15945
16183
|
const matching = allGroups.filter((g) => {
|
|
15946
|
-
const resolved =
|
|
15947
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
16184
|
+
const resolved = path23.resolve(g.repoPath);
|
|
16185
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path23.sep);
|
|
15948
16186
|
});
|
|
15949
16187
|
if (matching.length === 0) {
|
|
15950
16188
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -15975,7 +16213,7 @@ async function runInteractive() {
|
|
|
15975
16213
|
}
|
|
15976
16214
|
}
|
|
15977
16215
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
15978
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
16216
|
+
const envFilePaths = envFileNames.map((n) => path23.join(repoRoot, n));
|
|
15979
16217
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
15980
16218
|
const secretResult = await collectSecrets(
|
|
15981
16219
|
repoRoot,
|