hillclimb 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +504 -229
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import fs19 from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path24 from "path";
|
|
6
6
|
import * as p6 from "@clack/prompts";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
9
|
+
import path7 from "path";
|
|
9
10
|
import * as p3 from "@clack/prompts";
|
|
10
11
|
|
|
11
12
|
// src/color.ts
|
|
@@ -124,67 +125,18 @@ function detectRepoRoot() {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
// src/
|
|
128
|
+
// src/identity.ts
|
|
128
129
|
import fs2 from "fs";
|
|
129
130
|
import path3 from "path";
|
|
130
|
-
function toGitignorePattern(repoRoot, file) {
|
|
131
|
-
const rel = path3.relative(repoRoot, file);
|
|
132
|
-
if (!rel || rel.startsWith("..") || path3.isAbsolute(rel)) return null;
|
|
133
|
-
return `/${rel.split(path3.sep).join("/")}`;
|
|
134
|
-
}
|
|
135
|
-
function normalizePattern(pattern) {
|
|
136
|
-
return pattern.trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
137
|
-
}
|
|
138
|
-
async function ensureGitignored(repoRoot, files) {
|
|
139
|
-
const gitignorePath = path3.join(repoRoot, ".gitignore");
|
|
140
|
-
const patterns = [
|
|
141
|
-
...new Set(
|
|
142
|
-
files.map((file) => toGitignorePattern(repoRoot, file)).filter((pattern) => pattern !== null)
|
|
143
|
-
)
|
|
144
|
-
];
|
|
145
|
-
let content = "";
|
|
146
|
-
let created = false;
|
|
147
|
-
try {
|
|
148
|
-
content = await fs2.promises.readFile(gitignorePath, "utf-8");
|
|
149
|
-
} catch (err) {
|
|
150
|
-
if (err.code !== "ENOENT") throw err;
|
|
151
|
-
created = true;
|
|
152
|
-
}
|
|
153
|
-
const existing = new Set(
|
|
154
|
-
content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map(normalizePattern)
|
|
155
|
-
);
|
|
156
|
-
const added = patterns.filter(
|
|
157
|
-
(pattern) => !existing.has(normalizePattern(pattern))
|
|
158
|
-
);
|
|
159
|
-
if (added.length === 0) {
|
|
160
|
-
return { path: gitignorePath, added: [], created: false, changed: false };
|
|
161
|
-
}
|
|
162
|
-
let next = content;
|
|
163
|
-
if (next && !next.endsWith("\n")) next += "\n";
|
|
164
|
-
if (next) next += "\n";
|
|
165
|
-
next += ["# Hillclimb hook files", ...added].join("\n");
|
|
166
|
-
next += "\n";
|
|
167
|
-
await fs2.promises.writeFile(gitignorePath, next);
|
|
168
|
-
return {
|
|
169
|
-
path: gitignorePath,
|
|
170
|
-
added,
|
|
171
|
-
created,
|
|
172
|
-
changed: true
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// src/identity.ts
|
|
177
|
-
import fs3 from "fs";
|
|
178
|
-
import path4 from "path";
|
|
179
131
|
function identityPath() {
|
|
180
|
-
return
|
|
132
|
+
return path3.join(configDir(), "identity.json");
|
|
181
133
|
}
|
|
182
134
|
function normalizeUrl(apiBaseUrl) {
|
|
183
135
|
return apiBaseUrl.replace(/\/$/, "");
|
|
184
136
|
}
|
|
185
137
|
async function loadAllIdentities() {
|
|
186
138
|
try {
|
|
187
|
-
const raw = await
|
|
139
|
+
const raw = await fs2.promises.readFile(identityPath(), "utf-8");
|
|
188
140
|
const parsed = JSON.parse(raw);
|
|
189
141
|
if (!parsed.identities || typeof parsed.identities !== "object") {
|
|
190
142
|
return { identities: {} };
|
|
@@ -199,12 +151,12 @@ async function loadIdentity(apiBaseUrl) {
|
|
|
199
151
|
return file.identities[normalizeUrl(apiBaseUrl)] ?? null;
|
|
200
152
|
}
|
|
201
153
|
async function writeIdentityFile(file) {
|
|
202
|
-
await
|
|
154
|
+
await fs2.promises.mkdir(configDir(), { recursive: true, mode: 448 });
|
|
203
155
|
const tmp = `${identityPath()}.tmp`;
|
|
204
|
-
await
|
|
156
|
+
await fs2.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
|
|
205
157
|
mode: 384
|
|
206
158
|
});
|
|
207
|
-
await
|
|
159
|
+
await fs2.promises.rename(tmp, identityPath());
|
|
208
160
|
}
|
|
209
161
|
async function saveIdentity(identity) {
|
|
210
162
|
const file = await loadAllIdentities();
|
|
@@ -242,8 +194,8 @@ function formatError(err) {
|
|
|
242
194
|
}
|
|
243
195
|
|
|
244
196
|
// src/platform/log.ts
|
|
245
|
-
import
|
|
246
|
-
import
|
|
197
|
+
import fs3 from "fs";
|
|
198
|
+
import path4 from "path";
|
|
247
199
|
var PT_TIME_ZONE = "America/Los_Angeles";
|
|
248
200
|
function pacificParts(date) {
|
|
249
201
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
@@ -294,10 +246,10 @@ function pacificDateString(date) {
|
|
|
294
246
|
return `${p7.year}-${p7.month}-${p7.day}`;
|
|
295
247
|
}
|
|
296
248
|
function logsDir() {
|
|
297
|
-
return
|
|
249
|
+
return path4.join(configDir(), "logs");
|
|
298
250
|
}
|
|
299
251
|
function todayLogPath() {
|
|
300
|
-
return
|
|
252
|
+
return path4.join(logsDir(), `${pacificDateString(/* @__PURE__ */ new Date())}.log`);
|
|
301
253
|
}
|
|
302
254
|
var logPrefix = "";
|
|
303
255
|
function setLogPrefix(prefix) {
|
|
@@ -308,8 +260,8 @@ function appendLog(level, message) {
|
|
|
308
260
|
const line = `[${pacificTimestamp(/* @__PURE__ */ new Date())}] [${level}]${tag} ${message}
|
|
309
261
|
`;
|
|
310
262
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
263
|
+
fs3.mkdirSync(logsDir(), { recursive: true, mode: 448 });
|
|
264
|
+
fs3.appendFileSync(todayLogPath(), line);
|
|
313
265
|
} catch {
|
|
314
266
|
process.stderr.write(`hillclimb:${tag} ${level}: ${message}
|
|
315
267
|
`);
|
|
@@ -597,9 +549,61 @@ var PlatformClient = class {
|
|
|
597
549
|
};
|
|
598
550
|
|
|
599
551
|
// src/platform/hooks.ts
|
|
552
|
+
import { execFileSync } from "child_process";
|
|
600
553
|
import fs5 from "fs";
|
|
601
554
|
import os2 from "os";
|
|
602
555
|
import path6 from "path";
|
|
556
|
+
|
|
557
|
+
// src/gitignore.ts
|
|
558
|
+
import fs4 from "fs";
|
|
559
|
+
import path5 from "path";
|
|
560
|
+
function toGitignorePattern(repoRoot, file) {
|
|
561
|
+
const rel = path5.relative(repoRoot, file);
|
|
562
|
+
if (!rel || rel.startsWith("..") || path5.isAbsolute(rel)) return null;
|
|
563
|
+
return `/${rel.split(path5.sep).join("/")}`;
|
|
564
|
+
}
|
|
565
|
+
function normalizePattern(pattern) {
|
|
566
|
+
return pattern.trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
567
|
+
}
|
|
568
|
+
async function ensureGitignored(repoRoot, files) {
|
|
569
|
+
const gitignorePath = path5.join(repoRoot, ".gitignore");
|
|
570
|
+
const patterns = [
|
|
571
|
+
...new Set(
|
|
572
|
+
files.map((file) => toGitignorePattern(repoRoot, file)).filter((pattern) => pattern !== null)
|
|
573
|
+
)
|
|
574
|
+
];
|
|
575
|
+
let content = "";
|
|
576
|
+
let created = false;
|
|
577
|
+
try {
|
|
578
|
+
content = await fs4.promises.readFile(gitignorePath, "utf-8");
|
|
579
|
+
} catch (err) {
|
|
580
|
+
if (err.code !== "ENOENT") throw err;
|
|
581
|
+
created = true;
|
|
582
|
+
}
|
|
583
|
+
const existing = new Set(
|
|
584
|
+
content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map(normalizePattern)
|
|
585
|
+
);
|
|
586
|
+
const added = patterns.filter(
|
|
587
|
+
(pattern) => !existing.has(normalizePattern(pattern))
|
|
588
|
+
);
|
|
589
|
+
if (added.length === 0) {
|
|
590
|
+
return { path: gitignorePath, added: [], created: false, changed: false };
|
|
591
|
+
}
|
|
592
|
+
let next = content;
|
|
593
|
+
if (next && !next.endsWith("\n")) next += "\n";
|
|
594
|
+
if (next) next += "\n";
|
|
595
|
+
next += ["# Hillclimb hook files", ...added].join("\n");
|
|
596
|
+
next += "\n";
|
|
597
|
+
await fs4.promises.writeFile(gitignorePath, next);
|
|
598
|
+
return {
|
|
599
|
+
path: gitignorePath,
|
|
600
|
+
added,
|
|
601
|
+
created,
|
|
602
|
+
changed: true
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/platform/hooks.ts
|
|
603
607
|
var HOOK_CMD = (sub) => `npx hillclimb@latest ${sub}`;
|
|
604
608
|
var GIT_TRACES_CMD = (tool) => `npx hillclimb@latest git-traces --tool=${tool}`;
|
|
605
609
|
var CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS = 30;
|
|
@@ -723,20 +727,41 @@ function copilotChatDetect() {
|
|
|
723
727
|
return candidates.some(isDir);
|
|
724
728
|
}
|
|
725
729
|
function ensureHooks(settings) {
|
|
726
|
-
if (!settings.hooks
|
|
730
|
+
if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks)) {
|
|
731
|
+
settings.hooks = {};
|
|
732
|
+
}
|
|
727
733
|
return settings.hooks;
|
|
728
734
|
}
|
|
729
735
|
function settingsPath(repoRoot, def) {
|
|
730
736
|
return path6.join(repoRoot, def.settingsFile);
|
|
731
737
|
}
|
|
732
|
-
async function readJson(file) {
|
|
738
|
+
async function readJson(file, options = {}) {
|
|
733
739
|
try {
|
|
734
740
|
const raw = await fs5.promises.readFile(file, "utf-8");
|
|
741
|
+
if (!raw.trim()) return {};
|
|
735
742
|
const parsed = JSON.parse(raw);
|
|
736
743
|
if (parsed && typeof parsed === "object") return parsed;
|
|
737
744
|
return {};
|
|
738
745
|
} catch (err) {
|
|
739
746
|
if (err.code === "ENOENT") return {};
|
|
747
|
+
if (options.repairMalformed && err instanceof SyntaxError) {
|
|
748
|
+
const backup = `${file}.malformed-${Date.now()}`;
|
|
749
|
+
try {
|
|
750
|
+
const raw = await fs5.promises.readFile(file, "utf-8");
|
|
751
|
+
await fs5.promises.writeFile(backup, raw);
|
|
752
|
+
appendLog(
|
|
753
|
+
"warn",
|
|
754
|
+
`hooks: backed up malformed JSON hook file ${file} to ${backup}`
|
|
755
|
+
);
|
|
756
|
+
} catch (backupErr) {
|
|
757
|
+
appendLog(
|
|
758
|
+
"warn",
|
|
759
|
+
`hooks: could not back up malformed JSON hook file ${file}: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
return {};
|
|
763
|
+
}
|
|
764
|
+
if (err instanceof SyntaxError) return {};
|
|
740
765
|
throw err;
|
|
741
766
|
}
|
|
742
767
|
}
|
|
@@ -1167,6 +1192,19 @@ function legacyBareGitTracesCommandsFor(command) {
|
|
|
1167
1192
|
(legacy) => legacy.endsWith(" git-traces")
|
|
1168
1193
|
);
|
|
1169
1194
|
}
|
|
1195
|
+
function legacyBareUploadCommandsFor(command) {
|
|
1196
|
+
const prefix = "npx hillclimb@latest ";
|
|
1197
|
+
if (!command.startsWith(prefix)) return [];
|
|
1198
|
+
const sub = command.slice(prefix.length);
|
|
1199
|
+
if (!sub.startsWith("upload --tool=")) return [];
|
|
1200
|
+
return [
|
|
1201
|
+
"npx hillclimb@latest upload",
|
|
1202
|
+
"npx hillclimb upload",
|
|
1203
|
+
"hillclimb-extract upload",
|
|
1204
|
+
"npx hillclimb-extract upload",
|
|
1205
|
+
"npx @hillclimb/extract upload"
|
|
1206
|
+
];
|
|
1207
|
+
}
|
|
1170
1208
|
function isHillclimbOwnedCommandFor(command, currentCommand) {
|
|
1171
1209
|
if (command === currentCommand || legacyCommandsFor(currentCommand).includes(command)) {
|
|
1172
1210
|
return true;
|
|
@@ -1182,7 +1220,7 @@ async function installHooksForTool(repoRoot, def) {
|
|
|
1182
1220
|
const r = await opencodeInstall(file);
|
|
1183
1221
|
return { settingsFile: file, ...r, changed: r.installed > 0 };
|
|
1184
1222
|
}
|
|
1185
|
-
const settings = await readJson(file);
|
|
1223
|
+
const settings = await readJson(file, { repairMalformed: true });
|
|
1186
1224
|
let installed = 0;
|
|
1187
1225
|
let alreadyPresent = 0;
|
|
1188
1226
|
let mutated = false;
|
|
@@ -1227,6 +1265,10 @@ async function checkHooksForTool(repoRoot, def) {
|
|
|
1227
1265
|
missing.push(`${evt.eventName}:${evt.command}`);
|
|
1228
1266
|
}
|
|
1229
1267
|
}
|
|
1268
|
+
if (def.tool === "codex" && missing.length === 0) {
|
|
1269
|
+
const enabled = await areCodexHooksEnabled();
|
|
1270
|
+
if (!enabled) missing.push("features.hooks");
|
|
1271
|
+
}
|
|
1230
1272
|
return { allInstalled: missing.length === 0, settingsFile: file, missing };
|
|
1231
1273
|
}
|
|
1232
1274
|
async function findLegacyGitTracesHookOwners(repoRoot, eventName) {
|
|
@@ -1250,9 +1292,57 @@ async function findLegacyGitTracesHookOwners(repoRoot, eventName) {
|
|
|
1250
1292
|
}
|
|
1251
1293
|
return owners;
|
|
1252
1294
|
}
|
|
1295
|
+
async function findLegacyUploadHookOwners(repoRoot, eventName) {
|
|
1296
|
+
if (!eventName) return [];
|
|
1297
|
+
const owners = [];
|
|
1298
|
+
for (const def of TOOLS) {
|
|
1299
|
+
if (def.format === "opencode") continue;
|
|
1300
|
+
const events = def.events.filter(
|
|
1301
|
+
(evt) => evt.eventName === eventName && evt.command.startsWith("npx hillclimb@latest upload")
|
|
1302
|
+
);
|
|
1303
|
+
if (events.length === 0) continue;
|
|
1304
|
+
const settings = await readJson(settingsPath(repoRoot, def));
|
|
1305
|
+
const commands = new Set(commandsForEvent(settings, def.format, eventName));
|
|
1306
|
+
if (events.some((evt) => {
|
|
1307
|
+
const candidates = evt.command.includes("--tool=") ? legacyBareUploadCommandsFor(evt.command) : [evt.command, ...legacyCommandsFor(evt.command)];
|
|
1308
|
+
return candidates.some((candidate) => commands.has(candidate));
|
|
1309
|
+
})) {
|
|
1310
|
+
owners.push(def.tool);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return owners;
|
|
1314
|
+
}
|
|
1253
1315
|
function detectTools(repoRoot) {
|
|
1254
1316
|
return TOOLS.filter((def) => def.detect(repoRoot));
|
|
1255
1317
|
}
|
|
1318
|
+
function trackedHookFiles(repoRoot, files) {
|
|
1319
|
+
const rels = files.map((file) => path6.relative(repoRoot, file)).filter((rel) => rel && !rel.startsWith("..") && !path6.isAbsolute(rel));
|
|
1320
|
+
if (rels.length === 0) return [];
|
|
1321
|
+
try {
|
|
1322
|
+
const output = execFileSync("git", ["ls-files", "--", ...rels], {
|
|
1323
|
+
cwd: repoRoot,
|
|
1324
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1325
|
+
}).toString("utf-8").trim();
|
|
1326
|
+
if (!output) return [];
|
|
1327
|
+
return output.split(/\r?\n/).map((rel) => path6.join(repoRoot, rel));
|
|
1328
|
+
} catch {
|
|
1329
|
+
return [];
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async function ensureHookFilesIgnored(repoRoot, files) {
|
|
1333
|
+
const ignored = await ensureGitignored(repoRoot, [
|
|
1334
|
+
...files,
|
|
1335
|
+
...files.map((file) => `${file}.malformed-*`)
|
|
1336
|
+
]);
|
|
1337
|
+
const tracked = trackedHookFiles(repoRoot, files);
|
|
1338
|
+
if (tracked.length > 0) {
|
|
1339
|
+
appendLog(
|
|
1340
|
+
"warn",
|
|
1341
|
+
`hooks: hook files are already tracked by git and may be committed (${tracked.join(", ")})`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return { ...ignored, tracked };
|
|
1345
|
+
}
|
|
1256
1346
|
async function installDetectedHooks(repoRoot) {
|
|
1257
1347
|
const detected = detectTools(repoRoot);
|
|
1258
1348
|
const results = [];
|
|
@@ -1280,6 +1370,20 @@ async function healHookForTool(repoRoot, tool) {
|
|
|
1280
1370
|
};
|
|
1281
1371
|
}
|
|
1282
1372
|
const r = await installHooksForTool(repoRoot, def);
|
|
1373
|
+
try {
|
|
1374
|
+
const ignored = await ensureHookFilesIgnored(repoRoot, [r.settingsFile]);
|
|
1375
|
+
if (ignored.changed) {
|
|
1376
|
+
appendLog(
|
|
1377
|
+
"info",
|
|
1378
|
+
`self-heal: gitignored ${def.tool} hook file (${ignored.added.join(",")})`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
appendLog(
|
|
1383
|
+
"warn",
|
|
1384
|
+
`self-heal: could not update .gitignore for ${def.tool} hook: ${err instanceof Error ? err.message : String(err)}`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1283
1387
|
return {
|
|
1284
1388
|
tool: def.tool,
|
|
1285
1389
|
label: def.label,
|
|
@@ -1303,20 +1407,38 @@ async function checkAllHooks(repoRoot) {
|
|
|
1303
1407
|
}
|
|
1304
1408
|
return results;
|
|
1305
1409
|
}
|
|
1306
|
-
|
|
1410
|
+
function codexConfigPath() {
|
|
1411
|
+
return process.env.HILLCLIMB_CODEX_CONFIG_PATH ?? path6.join(os2.homedir(), ".codex", "config.toml");
|
|
1412
|
+
}
|
|
1413
|
+
function codexHooksEnabledInConfig(content) {
|
|
1414
|
+
const featuresMatch = content.match(/^\[features\]\s*$/m);
|
|
1415
|
+
if (!featuresMatch || featuresMatch.index === void 0) return false;
|
|
1416
|
+
const bodyStart = featuresMatch.index + featuresMatch[0].length;
|
|
1417
|
+
const nextSectionOffset = content.slice(bodyStart).search(/\n\[[^\n]+\]\s*(?:\r?\n|$)/);
|
|
1418
|
+
const bodyEnd = nextSectionOffset === -1 ? content.length : bodyStart + nextSectionOffset;
|
|
1419
|
+
const body = content.slice(bodyStart, bodyEnd);
|
|
1420
|
+
const hooksMatch = body.match(/^\s*hooks\s*=\s*(true|false)\s*(?:#.*)?$/im);
|
|
1421
|
+
return hooksMatch?.[1]?.toLowerCase() === "true";
|
|
1422
|
+
}
|
|
1423
|
+
async function areCodexHooksEnabled() {
|
|
1424
|
+
try {
|
|
1425
|
+
const content = await fs5.promises.readFile(codexConfigPath(), "utf-8");
|
|
1426
|
+
return codexHooksEnabledInConfig(content);
|
|
1427
|
+
} catch {
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1307
1431
|
async function ensureCodexHooksEnabled() {
|
|
1308
1432
|
let content;
|
|
1433
|
+
const configPath2 = codexConfigPath();
|
|
1309
1434
|
try {
|
|
1310
|
-
content = await fs5.promises.readFile(
|
|
1435
|
+
content = await fs5.promises.readFile(configPath2, "utf-8");
|
|
1311
1436
|
} catch (err) {
|
|
1312
1437
|
if (err.code === "ENOENT") {
|
|
1313
|
-
await fs5.promises.mkdir(path6.dirname(
|
|
1438
|
+
await fs5.promises.mkdir(path6.dirname(configPath2), {
|
|
1314
1439
|
recursive: true
|
|
1315
1440
|
});
|
|
1316
|
-
await fs5.promises.writeFile(
|
|
1317
|
-
CODEX_CONFIG_PATH,
|
|
1318
|
-
"[features]\nhooks = true\n"
|
|
1319
|
-
);
|
|
1441
|
+
await fs5.promises.writeFile(configPath2, "[features]\nhooks = true\n");
|
|
1320
1442
|
return true;
|
|
1321
1443
|
}
|
|
1322
1444
|
throw err;
|
|
@@ -1354,7 +1476,7 @@ hooks = true
|
|
|
1354
1476
|
`;
|
|
1355
1477
|
}
|
|
1356
1478
|
if (content === original) return false;
|
|
1357
|
-
await fs5.promises.writeFile(
|
|
1479
|
+
await fs5.promises.writeFile(configPath2, content);
|
|
1358
1480
|
return true;
|
|
1359
1481
|
}
|
|
1360
1482
|
var CLAUDE_DEF = TOOLS[0];
|
|
@@ -1754,6 +1876,7 @@ async function runInit(args = []) {
|
|
|
1754
1876
|
userId: bootstrap.session.userId,
|
|
1755
1877
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1756
1878
|
});
|
|
1879
|
+
const autoUploadWarnings = [];
|
|
1757
1880
|
try {
|
|
1758
1881
|
const hookResults = await installDetectedHooks(repoRoot);
|
|
1759
1882
|
if (hookResults.length === 0) {
|
|
@@ -1778,7 +1901,7 @@ async function runInit(args = []) {
|
|
|
1778
1901
|
}
|
|
1779
1902
|
}
|
|
1780
1903
|
try {
|
|
1781
|
-
const ignored = await
|
|
1904
|
+
const ignored = await ensureHookFilesIgnored(
|
|
1782
1905
|
repoRoot,
|
|
1783
1906
|
hookResults.map((r) => r.settingsFile)
|
|
1784
1907
|
);
|
|
@@ -1791,11 +1914,21 @@ async function runInit(args = []) {
|
|
|
1791
1914
|
`${ignored.created ? "Created" : "Updated"} .gitignore for hook files`
|
|
1792
1915
|
);
|
|
1793
1916
|
}
|
|
1917
|
+
if (ignored.tracked.length > 0) {
|
|
1918
|
+
const tracked = ignored.tracked.map((file) => path7.relative(repoRoot, file)).join(", ");
|
|
1919
|
+
autoUploadWarnings.push(
|
|
1920
|
+
`Hook files are already tracked by git: ${tracked}`
|
|
1921
|
+
);
|
|
1922
|
+
p3.log.warn(
|
|
1923
|
+
`Hook files are already tracked by git and may be committed: ${tracked}`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1794
1926
|
} catch (err) {
|
|
1795
1927
|
appendLog(
|
|
1796
1928
|
"warn",
|
|
1797
1929
|
`init: could not update .gitignore for hook files: ${formatError(err)}`
|
|
1798
1930
|
);
|
|
1931
|
+
autoUploadWarnings.push("Could not update .gitignore for hook files.");
|
|
1799
1932
|
p3.log.warn(
|
|
1800
1933
|
`Could not update .gitignore for hook files: ${err instanceof Error ? err.message : String(err)}`
|
|
1801
1934
|
);
|
|
@@ -1812,6 +1945,9 @@ async function runInit(args = []) {
|
|
|
1812
1945
|
"warn",
|
|
1813
1946
|
`init: could not enable Codex hooks: ${formatError(err)}`
|
|
1814
1947
|
);
|
|
1948
|
+
autoUploadWarnings.push(
|
|
1949
|
+
"Could not enable Codex hooks in config.toml."
|
|
1950
|
+
);
|
|
1815
1951
|
p3.log.warn(
|
|
1816
1952
|
`Could not enable hooks in config.toml: ${err instanceof Error ? err.message : String(err)}`
|
|
1817
1953
|
);
|
|
@@ -1831,7 +1967,7 @@ async function runInit(args = []) {
|
|
|
1831
1967
|
`init: completed (project=${project.slug}, contributionType=${type.slug})`
|
|
1832
1968
|
);
|
|
1833
1969
|
p3.outro(
|
|
1834
|
-
`Done. Your next coding session in this repo will upload automatically to ${project.name}.`
|
|
1970
|
+
autoUploadWarnings.length > 0 ? `Configured ${project.name}, but auto-upload needs attention: ${autoUploadWarnings.join(" ")}` : `Done. Your next coding session in this repo will upload automatically to ${project.name}.`
|
|
1835
1971
|
);
|
|
1836
1972
|
}
|
|
1837
1973
|
|
|
@@ -2050,7 +2186,7 @@ async function runLogout(args = []) {
|
|
|
2050
2186
|
}
|
|
2051
2187
|
|
|
2052
2188
|
// src/commands/status.ts
|
|
2053
|
-
import
|
|
2189
|
+
import path8 from "path";
|
|
2054
2190
|
async function runStatus(args = []) {
|
|
2055
2191
|
const debug = args.includes("--debug");
|
|
2056
2192
|
header("status");
|
|
@@ -2068,7 +2204,7 @@ async function runStatus(args = []) {
|
|
|
2068
2204
|
return;
|
|
2069
2205
|
}
|
|
2070
2206
|
const { repoRoot, config } = match;
|
|
2071
|
-
const repoName = repo?.name ??
|
|
2207
|
+
const repoName = repo?.name ?? path8.basename(repoRoot);
|
|
2072
2208
|
if (IS_TTY) {
|
|
2073
2209
|
process.stdout.write(` ${dim("Checking login...")}`);
|
|
2074
2210
|
}
|
|
@@ -2162,7 +2298,7 @@ async function runStatus(args = []) {
|
|
|
2162
2298
|
try {
|
|
2163
2299
|
const hookStatuses = await checkAllHooks(repoRoot);
|
|
2164
2300
|
for (const h of hookStatuses) {
|
|
2165
|
-
const rel =
|
|
2301
|
+
const rel = path8.relative(repoRoot, h.settingsFile) || h.settingsFile;
|
|
2166
2302
|
debugRow(
|
|
2167
2303
|
`${h.label} hook:`,
|
|
2168
2304
|
`${rel} ${dim(`(${h.installed ? "installed" : "missing"})`)}`
|
|
@@ -2179,14 +2315,15 @@ async function runStatus(args = []) {
|
|
|
2179
2315
|
|
|
2180
2316
|
// src/commands/upload.ts
|
|
2181
2317
|
import { spawn as spawn2 } from "child_process";
|
|
2318
|
+
import crypto3 from "crypto";
|
|
2182
2319
|
import fs11 from "fs";
|
|
2183
2320
|
import os6 from "os";
|
|
2184
|
-
import
|
|
2321
|
+
import path15 from "path";
|
|
2185
2322
|
|
|
2186
2323
|
// src/debug-logs.ts
|
|
2187
2324
|
import crypto from "crypto";
|
|
2188
2325
|
import fs9 from "fs";
|
|
2189
|
-
import
|
|
2326
|
+
import path12 from "path";
|
|
2190
2327
|
|
|
2191
2328
|
// src/hook-events.ts
|
|
2192
2329
|
function classifyHookEvent(event) {
|
|
@@ -2205,6 +2342,41 @@ function classifyHookEvent(event) {
|
|
|
2205
2342
|
}
|
|
2206
2343
|
}
|
|
2207
2344
|
|
|
2345
|
+
// src/hook-payload.ts
|
|
2346
|
+
function resolveHookCwd(payload) {
|
|
2347
|
+
return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
|
|
2348
|
+
}
|
|
2349
|
+
function resolveHookSessionId(payload) {
|
|
2350
|
+
return payload.session_id ?? payload.conversation_id ?? null;
|
|
2351
|
+
}
|
|
2352
|
+
function inferSourceToolFromPayload(payload) {
|
|
2353
|
+
if (payload.tool) return payload.tool;
|
|
2354
|
+
if (payload.cursor_version) return "cursor";
|
|
2355
|
+
const transcriptPath = payload.transcript_path ?? "";
|
|
2356
|
+
if (transcriptPath.includes(".claude/projects/")) return "claude";
|
|
2357
|
+
if (transcriptPath.includes(".cursor/projects/")) return "cursor";
|
|
2358
|
+
if (transcriptPath.includes("GitHub.copilot-chat")) return "copilot-chat";
|
|
2359
|
+
if (transcriptPath.includes(".hillclimb/opencode-transcripts/")) {
|
|
2360
|
+
return "opencode";
|
|
2361
|
+
}
|
|
2362
|
+
if (transcriptPath.includes(".codex/")) return "codex";
|
|
2363
|
+
switch (payload.hook_event_name) {
|
|
2364
|
+
case "session.idle":
|
|
2365
|
+
case "session.deleted":
|
|
2366
|
+
case "server.instance.disposed":
|
|
2367
|
+
return "opencode";
|
|
2368
|
+
default:
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
function fallbackSourceTool(payload) {
|
|
2373
|
+
const inferred = inferSourceToolFromPayload(payload);
|
|
2374
|
+
if (inferred) return inferred;
|
|
2375
|
+
const eventKind = classifyHookEvent(payload.hook_event_name);
|
|
2376
|
+
if (eventKind === "stop") return "codex";
|
|
2377
|
+
return "claude";
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2208
2380
|
// src/middleware/pattern-redact.ts
|
|
2209
2381
|
import os3 from "os";
|
|
2210
2382
|
import { Worker } from "worker_threads";
|
|
@@ -10692,7 +10864,7 @@ var middleware = [];
|
|
|
10692
10864
|
|
|
10693
10865
|
// src/middleware/secrets.ts
|
|
10694
10866
|
import fs7 from "fs";
|
|
10695
|
-
import
|
|
10867
|
+
import path9 from "path";
|
|
10696
10868
|
var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
|
|
10697
10869
|
"true",
|
|
10698
10870
|
"false",
|
|
@@ -10847,7 +11019,7 @@ async function discoverEnvFiles(repoRoot) {
|
|
|
10847
11019
|
const envFiles = [];
|
|
10848
11020
|
for (const name of entries) {
|
|
10849
11021
|
if (!name.startsWith(".env")) continue;
|
|
10850
|
-
const filePath =
|
|
11022
|
+
const filePath = path9.join(repoRoot, name);
|
|
10851
11023
|
try {
|
|
10852
11024
|
const stat = await fs7.promises.stat(filePath);
|
|
10853
11025
|
if (stat.isFile()) envFiles.push(name);
|
|
@@ -10872,7 +11044,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
|
|
|
10872
11044
|
}
|
|
10873
11045
|
}
|
|
10874
11046
|
for (const filePath of additionalFiles) {
|
|
10875
|
-
const resolved =
|
|
11047
|
+
const resolved = path9.resolve(repoRoot, filePath);
|
|
10876
11048
|
sourceFiles.push(resolved);
|
|
10877
11049
|
for (const value of await parseEnvFile(resolved)) {
|
|
10878
11050
|
if (isUsableValue(value)) {
|
|
@@ -10902,16 +11074,16 @@ import archiver from "archiver";
|
|
|
10902
11074
|
|
|
10903
11075
|
// src/outputs/archive.ts
|
|
10904
11076
|
import os4 from "os";
|
|
10905
|
-
import
|
|
11077
|
+
import path10 from "path";
|
|
10906
11078
|
function getSourceBaseDir(sourceName) {
|
|
10907
11079
|
const home = os4.homedir();
|
|
10908
11080
|
switch (sourceName) {
|
|
10909
11081
|
case "claude":
|
|
10910
|
-
return
|
|
11082
|
+
return path10.join(home, ".claude", "projects");
|
|
10911
11083
|
case "codex":
|
|
10912
|
-
return
|
|
11084
|
+
return path10.join(home, ".codex", "sessions");
|
|
10913
11085
|
case "debug-logs":
|
|
10914
|
-
return
|
|
11086
|
+
return path10.join(configDir(), "logs");
|
|
10915
11087
|
default:
|
|
10916
11088
|
return home;
|
|
10917
11089
|
}
|
|
@@ -10920,8 +11092,8 @@ function addGroupToArchive(archive, group, selectedSources) {
|
|
|
10920
11092
|
for (const file of group.files) {
|
|
10921
11093
|
if (!selectedSources.has(file.sourceName)) continue;
|
|
10922
11094
|
const baseDir = getSourceBaseDir(file.sourceName);
|
|
10923
|
-
const relativePath = file.absolutePath.startsWith(baseDir) ?
|
|
10924
|
-
const archivePath =
|
|
11095
|
+
const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
|
|
11096
|
+
const archivePath = path10.join(file.sourceName, relativePath);
|
|
10925
11097
|
if (file.content) {
|
|
10926
11098
|
archive.append(file.content, { name: archivePath });
|
|
10927
11099
|
} else {
|
|
@@ -11005,20 +11177,20 @@ var PlatformUploadOutput = class {
|
|
|
11005
11177
|
|
|
11006
11178
|
// src/pipeline.ts
|
|
11007
11179
|
import fs8 from "fs";
|
|
11008
|
-
import
|
|
11180
|
+
import path11 from "path";
|
|
11009
11181
|
function canonicalizePath(p7) {
|
|
11010
|
-
let resolved =
|
|
11011
|
-
if (resolved.endsWith(
|
|
11182
|
+
let resolved = path11.resolve(p7);
|
|
11183
|
+
if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
|
|
11012
11184
|
resolved = resolved.slice(0, -1);
|
|
11013
11185
|
}
|
|
11014
11186
|
return resolved;
|
|
11015
11187
|
}
|
|
11016
11188
|
function computeLabel(repoPath, allPaths) {
|
|
11017
|
-
const segments = repoPath.split(
|
|
11189
|
+
const segments = repoPath.split(path11.sep).filter(Boolean);
|
|
11018
11190
|
for (let depth = 1; depth <= segments.length; depth++) {
|
|
11019
11191
|
const label = segments.slice(-depth).join("/");
|
|
11020
11192
|
const matches = allPaths.filter((p7) => {
|
|
11021
|
-
const s = p7.split(
|
|
11193
|
+
const s = p7.split(path11.sep).filter(Boolean);
|
|
11022
11194
|
return s.slice(-depth).join("/") === label;
|
|
11023
11195
|
});
|
|
11024
11196
|
if (matches.length === 1) return label;
|
|
@@ -11096,7 +11268,7 @@ var DEFAULT_WAIT_MS = 6e4;
|
|
|
11096
11268
|
var LOCK_RETRIES = 100;
|
|
11097
11269
|
var LOCK_RETRY_DELAY_MS = 100;
|
|
11098
11270
|
function stateDir() {
|
|
11099
|
-
return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ??
|
|
11271
|
+
return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path12.join(configDir(), "debug-log-uploads");
|
|
11100
11272
|
}
|
|
11101
11273
|
function waitMs() {
|
|
11102
11274
|
const raw = process.env.HILLCLIMB_DEBUG_LOG_WAIT_MS;
|
|
@@ -11105,7 +11277,7 @@ function waitMs() {
|
|
|
11105
11277
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_WAIT_MS;
|
|
11106
11278
|
}
|
|
11107
11279
|
function stateFile(eventId) {
|
|
11108
|
-
return
|
|
11280
|
+
return path12.join(stateDir(), `${eventId}.json`);
|
|
11109
11281
|
}
|
|
11110
11282
|
function lockFile(eventId) {
|
|
11111
11283
|
return `${stateFile(eventId)}.lock`;
|
|
@@ -11130,10 +11302,10 @@ function toolLabel(tool) {
|
|
|
11130
11302
|
return labels[tool] ?? tool;
|
|
11131
11303
|
}
|
|
11132
11304
|
function resolveCwd(payload) {
|
|
11133
|
-
return payload
|
|
11305
|
+
return resolveHookCwd(payload);
|
|
11134
11306
|
}
|
|
11135
11307
|
function resolveSessionId(payload) {
|
|
11136
|
-
return payload
|
|
11308
|
+
return resolveHookSessionId(payload);
|
|
11137
11309
|
}
|
|
11138
11310
|
function stringOrNull(value) {
|
|
11139
11311
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
@@ -11152,7 +11324,7 @@ function expectedKinds(tool, eventKind, payload) {
|
|
|
11152
11324
|
async function transcriptFingerprint(payload) {
|
|
11153
11325
|
const transcriptPath = stringOrNull(payload.transcript_path);
|
|
11154
11326
|
if (!transcriptPath) return {};
|
|
11155
|
-
const resolved =
|
|
11327
|
+
const resolved = path12.resolve(transcriptPath);
|
|
11156
11328
|
try {
|
|
11157
11329
|
const stat = await fs9.promises.stat(resolved);
|
|
11158
11330
|
return {
|
|
@@ -11191,13 +11363,10 @@ async function eventContext(tool, payload) {
|
|
|
11191
11363
|
conversationId: stringOrNull(payload.conversation_id),
|
|
11192
11364
|
turnId,
|
|
11193
11365
|
eventNonce,
|
|
11194
|
-
// Only
|
|
11195
|
-
//
|
|
11196
|
-
//
|
|
11197
|
-
|
|
11198
|
-
// transcript_path to the upload spawn but not the git-traces spawn,
|
|
11199
|
-
// causing a 60s orphan wait and a duplicate debug-log upload.
|
|
11200
|
-
...eventKind === "stop" ? await transcriptFingerprint(payload) : {}
|
|
11366
|
+
// Only stop events without stable turn IDs need the transcript to
|
|
11367
|
+
// disambiguate. When a turn_id exists, including mutable transcript
|
|
11368
|
+
// mtime/size can split agent+git completions for the same logical event.
|
|
11369
|
+
...eventKind === "stop" && !turnId ? await transcriptFingerprint(payload) : {}
|
|
11201
11370
|
};
|
|
11202
11371
|
const eventId = crypto.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
|
|
11203
11372
|
return {
|
|
@@ -11268,7 +11437,7 @@ function initialState(ctx, now) {
|
|
|
11268
11437
|
eventKind: ctx.eventKind,
|
|
11269
11438
|
hookEventName: ctx.hookEventName,
|
|
11270
11439
|
sessionId: ctx.sessionId,
|
|
11271
|
-
logDate:
|
|
11440
|
+
logDate: path12.basename(todayLogPath(), ".log"),
|
|
11272
11441
|
firstSeenAt: now.toISOString()
|
|
11273
11442
|
};
|
|
11274
11443
|
}
|
|
@@ -11330,7 +11499,7 @@ async function waitForExpectedKinds(ctx, state) {
|
|
|
11330
11499
|
}
|
|
11331
11500
|
async function buildMiddleware(repoRoot) {
|
|
11332
11501
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
11333
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
11502
|
+
const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
|
|
11334
11503
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
11335
11504
|
const middleware2 = [];
|
|
11336
11505
|
if (secretResult.values.size > 0) {
|
|
@@ -11376,7 +11545,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11376
11545
|
};
|
|
11377
11546
|
const group = {
|
|
11378
11547
|
repoPath: ctx.repoRoot,
|
|
11379
|
-
label:
|
|
11548
|
+
label: path12.basename(ctx.repoRoot),
|
|
11380
11549
|
files: [sourceFile],
|
|
11381
11550
|
sourceNames: [DEBUG_LOGS_SLUG],
|
|
11382
11551
|
lastModified: now
|
|
@@ -11398,7 +11567,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11398
11567
|
`Tool: ${label}`,
|
|
11399
11568
|
`Event: ${ctx.hookEventName ?? ctx.eventKind}`,
|
|
11400
11569
|
`Repo: ${ctx.repoRoot}`,
|
|
11401
|
-
`Log: ${
|
|
11570
|
+
`Log: ${path12.basename(logPath)}`,
|
|
11402
11571
|
`Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
|
|
11403
11572
|
`Git done: ${state.gitDoneAt ?? "<not observed>"}`,
|
|
11404
11573
|
`Uploaded: ${now.toISOString()}`
|
|
@@ -11415,7 +11584,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11415
11584
|
);
|
|
11416
11585
|
appendLog(
|
|
11417
11586
|
"info",
|
|
11418
|
-
`debug-logs: uploaded ${
|
|
11587
|
+
`debug-logs: uploaded ${path12.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
|
|
11419
11588
|
);
|
|
11420
11589
|
return contributionId;
|
|
11421
11590
|
} catch (err) {
|
|
@@ -11468,7 +11637,7 @@ async function recordDebugLogCompletion(args) {
|
|
|
11468
11637
|
}
|
|
11469
11638
|
|
|
11470
11639
|
// src/normalizer/index.ts
|
|
11471
|
-
import
|
|
11640
|
+
import path13 from "path";
|
|
11472
11641
|
|
|
11473
11642
|
// src/normalizer/claude.ts
|
|
11474
11643
|
function stringify(value) {
|
|
@@ -13166,14 +13335,20 @@ var NormalizeMiddleware = class {
|
|
|
13166
13335
|
continue;
|
|
13167
13336
|
const content = file.content ? file.content.toString("utf-8") : null;
|
|
13168
13337
|
if (!content) continue;
|
|
13169
|
-
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 :
|
|
13338
|
+
const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path13.basename(file.absolutePath, ".jsonl"));
|
|
13170
13339
|
try {
|
|
13171
13340
|
const trajectory = normalizeContent(
|
|
13172
13341
|
file.sourceName,
|
|
13173
13342
|
content,
|
|
13174
13343
|
sessionId
|
|
13175
13344
|
);
|
|
13176
|
-
if (!trajectory)
|
|
13345
|
+
if (!trajectory) {
|
|
13346
|
+
appendLog(
|
|
13347
|
+
"warn",
|
|
13348
|
+
`normalize: produced no ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}`
|
|
13349
|
+
);
|
|
13350
|
+
continue;
|
|
13351
|
+
}
|
|
13177
13352
|
const json = JSON.stringify(
|
|
13178
13353
|
excludeNone(trajectory),
|
|
13179
13354
|
null,
|
|
@@ -13187,7 +13362,11 @@ var NormalizeMiddleware = class {
|
|
|
13187
13362
|
metadata: { ...file.metadata, isAtif: true },
|
|
13188
13363
|
content: Buffer.from(json, "utf-8")
|
|
13189
13364
|
});
|
|
13190
|
-
} catch {
|
|
13365
|
+
} catch (err) {
|
|
13366
|
+
appendLog(
|
|
13367
|
+
"warn",
|
|
13368
|
+
`normalize: failed for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
13369
|
+
);
|
|
13191
13370
|
}
|
|
13192
13371
|
}
|
|
13193
13372
|
return { ...group, files: newFiles };
|
|
@@ -13198,9 +13377,9 @@ var NormalizeMiddleware = class {
|
|
|
13198
13377
|
import crypto2 from "crypto";
|
|
13199
13378
|
import fs10 from "fs";
|
|
13200
13379
|
import os5 from "os";
|
|
13201
|
-
import
|
|
13380
|
+
import path14 from "path";
|
|
13202
13381
|
var CURRENT_SCHEMA_VERSION2 = 1;
|
|
13203
|
-
var DEFAULT_STATE_DIR =
|
|
13382
|
+
var DEFAULT_STATE_DIR = path14.join(
|
|
13204
13383
|
os5.homedir(),
|
|
13205
13384
|
".hillclimb",
|
|
13206
13385
|
"agent-uploads"
|
|
@@ -13224,8 +13403,8 @@ function stateTtlMs() {
|
|
|
13224
13403
|
);
|
|
13225
13404
|
}
|
|
13226
13405
|
function stateFileFor(repoRoot, tool, sessionId) {
|
|
13227
|
-
const hash = crypto2.createHash("sha256").update(`${
|
|
13228
|
-
return
|
|
13406
|
+
const hash = crypto2.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
|
|
13407
|
+
return path14.join(stateDir2(), `${hash}.json`);
|
|
13229
13408
|
}
|
|
13230
13409
|
function lockFileFor(repoRoot, tool, sessionId) {
|
|
13231
13410
|
return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
|
|
@@ -13267,8 +13446,11 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = LOCK_RETRIES2,
|
|
|
13267
13446
|
lockPath,
|
|
13268
13447
|
fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
|
|
13269
13448
|
);
|
|
13270
|
-
|
|
13271
|
-
|
|
13449
|
+
try {
|
|
13450
|
+
await fd.write(String(process.pid));
|
|
13451
|
+
} finally {
|
|
13452
|
+
await fd.close();
|
|
13453
|
+
}
|
|
13272
13454
|
return;
|
|
13273
13455
|
} catch (err) {
|
|
13274
13456
|
if (err.code === "EEXIST" && i < retries - 1) {
|
|
@@ -13303,7 +13485,7 @@ async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
|
|
|
13303
13485
|
}
|
|
13304
13486
|
for (const entry of entries) {
|
|
13305
13487
|
if (!entry.isFile()) continue;
|
|
13306
|
-
const file =
|
|
13488
|
+
const file = path14.join(stateDir2(), entry.name);
|
|
13307
13489
|
try {
|
|
13308
13490
|
const st = await fs10.promises.stat(file);
|
|
13309
13491
|
if (now - st.mtimeMs > ttlMs) {
|
|
@@ -13329,6 +13511,9 @@ function sanitize2(value) {
|
|
|
13329
13511
|
function formatEpochSeconds2(date) {
|
|
13330
13512
|
return String(Math.floor(date.getTime() / 1e3));
|
|
13331
13513
|
}
|
|
13514
|
+
function newFlowId() {
|
|
13515
|
+
return crypto3.randomBytes(3).toString("hex");
|
|
13516
|
+
}
|
|
13332
13517
|
function lineHasAssistant(line) {
|
|
13333
13518
|
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
13334
13519
|
}
|
|
@@ -13355,16 +13540,41 @@ async function hasAssistantMessage(transcriptPath) {
|
|
|
13355
13540
|
}
|
|
13356
13541
|
return false;
|
|
13357
13542
|
}
|
|
13358
|
-
function
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13543
|
+
function hashFileSha256(file) {
|
|
13544
|
+
return new Promise((resolve, reject) => {
|
|
13545
|
+
const hash = crypto3.createHash("sha256");
|
|
13546
|
+
const stream = fs11.createReadStream(file);
|
|
13547
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
13548
|
+
stream.on("error", reject);
|
|
13549
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
13550
|
+
});
|
|
13551
|
+
}
|
|
13552
|
+
async function resolveSourceTool(payload, repoRoot) {
|
|
13553
|
+
const inferred = inferSourceToolFromPayload(payload);
|
|
13554
|
+
if (inferred) return inferred;
|
|
13555
|
+
if (repoRoot) {
|
|
13556
|
+
const owners = await findLegacyUploadHookOwners(
|
|
13557
|
+
repoRoot,
|
|
13558
|
+
payload.hook_event_name
|
|
13559
|
+
);
|
|
13560
|
+
if (owners.length === 1) {
|
|
13561
|
+
appendLog("info", `upload: inferred legacy bare hook owner ${owners[0]}`);
|
|
13562
|
+
return owners[0];
|
|
13563
|
+
}
|
|
13564
|
+
if (owners.length > 1) {
|
|
13565
|
+
appendLog(
|
|
13566
|
+
"warn",
|
|
13567
|
+
`upload: legacy bare hook matched multiple tools (${owners.join(", ")}); skipping to avoid misattribution`
|
|
13568
|
+
);
|
|
13569
|
+
return null;
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
13572
|
+
return fallbackSourceTool(payload);
|
|
13363
13573
|
}
|
|
13364
13574
|
function summarizePayload(payload) {
|
|
13365
|
-
const sessionId = payload
|
|
13575
|
+
const sessionId = resolveHookSessionId(payload);
|
|
13366
13576
|
const turnId = payload.turn_id ?? null;
|
|
13367
|
-
const cwd = payload
|
|
13577
|
+
const cwd = resolveHookCwd(payload);
|
|
13368
13578
|
return JSON.stringify({
|
|
13369
13579
|
session_id: sessionId,
|
|
13370
13580
|
turn_id: turnId,
|
|
@@ -13397,11 +13607,11 @@ async function selfHealHook(repoRoot, tool) {
|
|
|
13397
13607
|
}
|
|
13398
13608
|
}
|
|
13399
13609
|
function resolveCursorTranscriptPath(payload) {
|
|
13400
|
-
const id = payload
|
|
13610
|
+
const id = resolveHookSessionId(payload);
|
|
13401
13611
|
const workspace = payload.workspace_roots?.[0];
|
|
13402
13612
|
if (!id || !workspace) return void 0;
|
|
13403
13613
|
const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
|
|
13404
|
-
return
|
|
13614
|
+
return path15.join(
|
|
13405
13615
|
os6.homedir(),
|
|
13406
13616
|
".cursor",
|
|
13407
13617
|
"projects",
|
|
@@ -13412,15 +13622,10 @@ function resolveCursorTranscriptPath(payload) {
|
|
|
13412
13622
|
);
|
|
13413
13623
|
}
|
|
13414
13624
|
async function runUploadInner(payload) {
|
|
13415
|
-
const sessionId = payload
|
|
13625
|
+
const sessionId = resolveHookSessionId(payload);
|
|
13416
13626
|
const transcriptPath = payload.transcript_path ?? resolveCursorTranscriptPath(payload);
|
|
13417
|
-
const cwd = payload
|
|
13418
|
-
const sourceTool = resolveSourceTool(payload);
|
|
13627
|
+
const cwd = resolveHookCwd(payload);
|
|
13419
13628
|
const eventKind = classifyHookEvent(payload.hook_event_name);
|
|
13420
|
-
appendLog(
|
|
13421
|
-
"info",
|
|
13422
|
-
`[${sessionId ?? "no-id"}] payload parsed (tool=${sourceTool}, cwd=${cwd ?? "?"}, event=${eventKind ?? "?"})`
|
|
13423
|
-
);
|
|
13424
13629
|
if (!sessionId || !transcriptPath || !cwd) {
|
|
13425
13630
|
appendLog(
|
|
13426
13631
|
"warn",
|
|
@@ -13437,8 +13642,18 @@ async function runUploadInner(payload) {
|
|
|
13437
13642
|
return;
|
|
13438
13643
|
}
|
|
13439
13644
|
const { repoRoot, config } = match;
|
|
13645
|
+
const sourceTool = await resolveSourceTool(payload, repoRoot);
|
|
13646
|
+
if (!sourceTool) {
|
|
13647
|
+
payload.tool = "unknown";
|
|
13648
|
+
return;
|
|
13649
|
+
}
|
|
13650
|
+
payload.tool = sourceTool;
|
|
13651
|
+
appendLog(
|
|
13652
|
+
"info",
|
|
13653
|
+
`[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind ?? "?"})`
|
|
13654
|
+
);
|
|
13440
13655
|
await selfHealHook(repoRoot, sourceTool);
|
|
13441
|
-
const transcriptResolved =
|
|
13656
|
+
const transcriptResolved = path15.resolve(transcriptPath);
|
|
13442
13657
|
try {
|
|
13443
13658
|
const stat = await fs11.promises.stat(transcriptResolved);
|
|
13444
13659
|
if (!stat.isFile()) {
|
|
@@ -13477,11 +13692,13 @@ async function uploadSession(args) {
|
|
|
13477
13692
|
await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
|
|
13478
13693
|
const prior = await readUploadState(repoRoot, sourceTool, sessionId);
|
|
13479
13694
|
let transcriptSize;
|
|
13695
|
+
let transcriptSha256;
|
|
13480
13696
|
try {
|
|
13481
13697
|
transcriptSize = (await fs11.promises.stat(transcriptPath)).size;
|
|
13698
|
+
transcriptSha256 = await hashFileSha256(transcriptPath);
|
|
13482
13699
|
} catch {
|
|
13483
13700
|
}
|
|
13484
|
-
if (prior?.contributionId && transcriptSize !== void 0 && transcriptSize === prior.lastTranscriptSize) {
|
|
13701
|
+
if (prior?.contributionId && transcriptSize !== void 0 && transcriptSha256 !== void 0 && transcriptSize === prior.lastTranscriptSize && transcriptSha256 === prior.lastTranscriptSha256) {
|
|
13485
13702
|
appendLog(
|
|
13486
13703
|
"info",
|
|
13487
13704
|
`[${sessionId}] skipping ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload (transcript unchanged at ${transcriptSize} bytes${isSessionEnd ? "; local state cleared" : ""})`
|
|
@@ -13499,13 +13716,13 @@ async function uploadSession(args) {
|
|
|
13499
13716
|
};
|
|
13500
13717
|
const group = {
|
|
13501
13718
|
repoPath: repoRoot,
|
|
13502
|
-
label:
|
|
13719
|
+
label: path15.basename(repoRoot),
|
|
13503
13720
|
files: [sourceFile],
|
|
13504
13721
|
sourceNames: [sourceTool],
|
|
13505
13722
|
lastModified: now
|
|
13506
13723
|
};
|
|
13507
13724
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
13508
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
13725
|
+
const envFilePaths = envFileNames.map((n) => path15.join(repoRoot, n));
|
|
13509
13726
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
13510
13727
|
const mwChain = [];
|
|
13511
13728
|
if (secretResult.values.size > 0) {
|
|
@@ -13566,7 +13783,8 @@ Uploaded: ${now.toISOString()}`;
|
|
|
13566
13783
|
uploadCount: prior?.uploadCount ?? 0,
|
|
13567
13784
|
firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
|
|
13568
13785
|
lastUploadedAt: prior?.lastUploadedAt ?? now.toISOString(),
|
|
13569
|
-
lastTranscriptSize: prior?.lastTranscriptSize
|
|
13786
|
+
lastTranscriptSize: prior?.lastTranscriptSize,
|
|
13787
|
+
lastTranscriptSha256: prior?.lastTranscriptSha256
|
|
13570
13788
|
})
|
|
13571
13789
|
});
|
|
13572
13790
|
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)";
|
|
@@ -13604,7 +13822,8 @@ Uploaded: ${now.toISOString()}`;
|
|
|
13604
13822
|
uploadCount: seq,
|
|
13605
13823
|
firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
|
|
13606
13824
|
lastUploadedAt: now.toISOString(),
|
|
13607
|
-
lastTranscriptSize: transcriptSize
|
|
13825
|
+
lastTranscriptSize: transcriptSize,
|
|
13826
|
+
lastTranscriptSha256: transcriptSha256
|
|
13608
13827
|
};
|
|
13609
13828
|
await writeUploadState(next);
|
|
13610
13829
|
appendLog(
|
|
@@ -13622,6 +13841,7 @@ Uploaded: ${now.toISOString()}`;
|
|
|
13622
13841
|
}
|
|
13623
13842
|
var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
|
|
13624
13843
|
var TOOL_ENV_FLAG = "HILLCLIMB_UPLOAD_TOOL";
|
|
13844
|
+
var FLOW_ID_ENV = "HILLCLIMB_UPLOAD_FLOW";
|
|
13625
13845
|
function parseToolArg(argv) {
|
|
13626
13846
|
for (let i = 0; i < argv.length; i++) {
|
|
13627
13847
|
const a = argv[i];
|
|
@@ -13635,6 +13855,8 @@ async function runUpload() {
|
|
|
13635
13855
|
await runUploadWorker();
|
|
13636
13856
|
return;
|
|
13637
13857
|
}
|
|
13858
|
+
const flowId = newFlowId();
|
|
13859
|
+
setLogPrefix(`[${flowId}]`);
|
|
13638
13860
|
appendLog("info", `upload hook invoked (pid ${process.pid})`);
|
|
13639
13861
|
let raw;
|
|
13640
13862
|
try {
|
|
@@ -13661,7 +13883,8 @@ async function runUpload() {
|
|
|
13661
13883
|
const toolArg = parseToolArg(process.argv.slice(2));
|
|
13662
13884
|
const workerEnv = {
|
|
13663
13885
|
...process.env,
|
|
13664
|
-
[WORKER_ENV_FLAG]: "1"
|
|
13886
|
+
[WORKER_ENV_FLAG]: "1",
|
|
13887
|
+
[FLOW_ID_ENV]: flowId
|
|
13665
13888
|
};
|
|
13666
13889
|
if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
|
|
13667
13890
|
try {
|
|
@@ -13688,6 +13911,7 @@ async function runUpload() {
|
|
|
13688
13911
|
}
|
|
13689
13912
|
}
|
|
13690
13913
|
async function runUploadWorker() {
|
|
13914
|
+
setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
|
|
13691
13915
|
appendLog("info", `upload worker started (pid ${process.pid})`);
|
|
13692
13916
|
let raw;
|
|
13693
13917
|
try {
|
|
@@ -13726,7 +13950,7 @@ async function runUploadWorker() {
|
|
|
13726
13950
|
} finally {
|
|
13727
13951
|
await recordDebugLogCompletion({
|
|
13728
13952
|
kind: "agent",
|
|
13729
|
-
tool:
|
|
13953
|
+
tool: fallbackSourceTool(payload),
|
|
13730
13954
|
payload
|
|
13731
13955
|
});
|
|
13732
13956
|
try {
|
|
@@ -13738,17 +13962,17 @@ async function runUploadWorker() {
|
|
|
13738
13962
|
|
|
13739
13963
|
// src/git-traces/index.ts
|
|
13740
13964
|
import { spawn as spawn3 } from "child_process";
|
|
13741
|
-
import
|
|
13965
|
+
import crypto5 from "crypto";
|
|
13742
13966
|
|
|
13743
13967
|
// src/git-traces/handlers.ts
|
|
13744
|
-
import { execFileSync as
|
|
13745
|
-
import
|
|
13968
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
13969
|
+
import path18 from "path";
|
|
13746
13970
|
|
|
13747
13971
|
// src/git-traces/git-ops.ts
|
|
13748
|
-
import { execFileSync } from "child_process";
|
|
13972
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
13749
13973
|
import fs12 from "fs";
|
|
13750
13974
|
import os7 from "os";
|
|
13751
|
-
import
|
|
13975
|
+
import path16 from "path";
|
|
13752
13976
|
import { gzipSync } from "zlib";
|
|
13753
13977
|
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
13754
13978
|
var EXEC_OPTS = {
|
|
@@ -13802,7 +14026,7 @@ function isGitTimeoutError(err) {
|
|
|
13802
14026
|
}
|
|
13803
14027
|
function gitBuffer(repoRoot, args, options = {}) {
|
|
13804
14028
|
try {
|
|
13805
|
-
return
|
|
14029
|
+
return execFileSync2("git", args, {
|
|
13806
14030
|
cwd: repoRoot,
|
|
13807
14031
|
...options.env ? { env: options.env } : {},
|
|
13808
14032
|
...options.input !== void 0 ? { input: options.input } : {},
|
|
@@ -13911,7 +14135,7 @@ function removePathsFromIndex(repoRoot, env, paths) {
|
|
|
13911
14135
|
function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
13912
14136
|
const oversizedFiles = listOversizedTreeFiles(repoRoot, treeSha, options);
|
|
13913
14137
|
if (oversizedFiles.length === 0) return treeSha;
|
|
13914
|
-
const tmpIndex =
|
|
14138
|
+
const tmpIndex = path16.join(
|
|
13915
14139
|
os7.tmpdir(),
|
|
13916
14140
|
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
13917
14141
|
);
|
|
@@ -13943,7 +14167,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13943
14167
|
for (const relPath of list.split("\0")) {
|
|
13944
14168
|
if (!relPath) continue;
|
|
13945
14169
|
try {
|
|
13946
|
-
const stat = fs12.lstatSync(
|
|
14170
|
+
const stat = fs12.lstatSync(path16.join(repoRoot, relPath));
|
|
13947
14171
|
if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
|
|
13948
14172
|
recordOmittedSnapshotFile(omittedFiles, {
|
|
13949
14173
|
path: relPath,
|
|
@@ -13958,7 +14182,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13958
14182
|
}
|
|
13959
14183
|
}
|
|
13960
14184
|
if (kept.length === 0) return null;
|
|
13961
|
-
const tmpIndex =
|
|
14185
|
+
const tmpIndex = path16.join(
|
|
13962
14186
|
os7.tmpdir(),
|
|
13963
14187
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
13964
14188
|
);
|
|
@@ -13988,7 +14212,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13988
14212
|
const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
|
|
13989
14213
|
if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
|
|
13990
14214
|
return filteredTrackedTree;
|
|
13991
|
-
const tmpIndex =
|
|
14215
|
+
const tmpIndex = path16.join(
|
|
13992
14216
|
os7.tmpdir(),
|
|
13993
14217
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
13994
14218
|
);
|
|
@@ -14030,7 +14254,7 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
14030
14254
|
]);
|
|
14031
14255
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
14032
14256
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
14033
|
-
const tmpFile =
|
|
14257
|
+
const tmpFile = path16.join(
|
|
14034
14258
|
os7.tmpdir(),
|
|
14035
14259
|
// Include the pid (like the other temp files in this module) so concurrent
|
|
14036
14260
|
// git-traces workers — e.g. two sessions, or a parent + subagent — don't
|
|
@@ -14229,9 +14453,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
14229
14453
|
oldPath
|
|
14230
14454
|
});
|
|
14231
14455
|
} else {
|
|
14232
|
-
const
|
|
14233
|
-
indexByPath.set(
|
|
14234
|
-
files.push({ path:
|
|
14456
|
+
const path25 = parts[parts.length - 1];
|
|
14457
|
+
indexByPath.set(path25, files.length);
|
|
14458
|
+
files.push({ path: path25, status, additions: 0, deletions: 0 });
|
|
14235
14459
|
}
|
|
14236
14460
|
}
|
|
14237
14461
|
for (const line of numstat.split("\n")) {
|
|
@@ -14297,22 +14521,22 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
14297
14521
|
}
|
|
14298
14522
|
|
|
14299
14523
|
// src/git-traces/session-state.ts
|
|
14300
|
-
import
|
|
14524
|
+
import crypto4 from "crypto";
|
|
14301
14525
|
import fs13 from "fs";
|
|
14302
14526
|
import os8 from "os";
|
|
14303
|
-
import
|
|
14527
|
+
import path17 from "path";
|
|
14304
14528
|
var CURRENT_SCHEMA_VERSION3 = 3;
|
|
14305
|
-
var DEFAULT_STATE_DIR2 =
|
|
14529
|
+
var DEFAULT_STATE_DIR2 = path17.join(os8.homedir(), ".hillclimb", "git-traces");
|
|
14306
14530
|
var LOCK_RETRIES3 = 120;
|
|
14307
14531
|
var LOCK_RETRY_DELAY_MS3 = 500;
|
|
14308
14532
|
function stateDir3() {
|
|
14309
14533
|
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
|
|
14310
14534
|
}
|
|
14311
14535
|
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
14312
|
-
const hash =
|
|
14313
|
-
sessionId ? `${
|
|
14536
|
+
const hash = crypto4.createHash("sha256").update(
|
|
14537
|
+
sessionId ? `${path17.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path17.resolve(repoRoot)}\0${tool}`
|
|
14314
14538
|
).digest("hex").slice(0, 16);
|
|
14315
|
-
return
|
|
14539
|
+
return path17.join(stateDir3(), `${hash}.json`);
|
|
14316
14540
|
}
|
|
14317
14541
|
function lockFileForRepo(repoRoot, tool) {
|
|
14318
14542
|
return `${stateFileForRepo(repoRoot, tool)}.lock`;
|
|
@@ -14339,14 +14563,14 @@ async function listScopedSessionStates(repoRoot, tool) {
|
|
|
14339
14563
|
const states = [];
|
|
14340
14564
|
for (const entry of entries) {
|
|
14341
14565
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
14342
|
-
const file =
|
|
14566
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
14343
14567
|
const state = await readStateFile(file);
|
|
14344
14568
|
if (!state) continue;
|
|
14345
14569
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
14346
14570
|
continue;
|
|
14347
14571
|
}
|
|
14348
|
-
if (
|
|
14349
|
-
if (
|
|
14572
|
+
if (path17.resolve(state.repoRoot) !== path17.resolve(repoRoot)) continue;
|
|
14573
|
+
if (path17.resolve(file) !== path17.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
14350
14574
|
continue;
|
|
14351
14575
|
}
|
|
14352
14576
|
let mtimeMs = 0;
|
|
@@ -14369,14 +14593,14 @@ async function listSessionStatesForSession(tool, sessionId) {
|
|
|
14369
14593
|
const states = [];
|
|
14370
14594
|
for (const entry of entries) {
|
|
14371
14595
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
14372
|
-
const file =
|
|
14596
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
14373
14597
|
const state = await readStateFile(file);
|
|
14374
14598
|
if (!state) continue;
|
|
14375
14599
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
14376
14600
|
continue;
|
|
14377
14601
|
}
|
|
14378
14602
|
if (state.sessionId !== sessionId) continue;
|
|
14379
|
-
if (
|
|
14603
|
+
if (path17.resolve(file) !== path17.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
14380
14604
|
continue;
|
|
14381
14605
|
}
|
|
14382
14606
|
let mtimeMs = 0;
|
|
@@ -14479,18 +14703,18 @@ var TOOL_LABELS = {
|
|
|
14479
14703
|
async function loadConfiguredRepos() {
|
|
14480
14704
|
const file = await loadProjects();
|
|
14481
14705
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
14482
|
-
repoRoot:
|
|
14706
|
+
repoRoot: path18.resolve(repoRoot),
|
|
14483
14707
|
config
|
|
14484
14708
|
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
14485
14709
|
}
|
|
14486
14710
|
function repoLabel(repoRoot) {
|
|
14487
|
-
return
|
|
14711
|
+
return path18.basename(repoRoot) || repoRoot;
|
|
14488
14712
|
}
|
|
14489
14713
|
function resolveCwd2(payload) {
|
|
14490
|
-
return payload
|
|
14714
|
+
return resolveHookCwd(payload);
|
|
14491
14715
|
}
|
|
14492
14716
|
function resolveSessionId2(payload) {
|
|
14493
|
-
return payload
|
|
14717
|
+
return resolveHookSessionId(payload);
|
|
14494
14718
|
}
|
|
14495
14719
|
function epochPrefix(epoch) {
|
|
14496
14720
|
return `epoch-${String(epoch).padStart(3, "0")}`;
|
|
@@ -14506,7 +14730,7 @@ function captureHeadSha(cwd) {
|
|
|
14506
14730
|
}
|
|
14507
14731
|
}
|
|
14508
14732
|
function execGit(cwd, args) {
|
|
14509
|
-
return
|
|
14733
|
+
return execFileSync3("git", args, {
|
|
14510
14734
|
cwd,
|
|
14511
14735
|
stdio: ["pipe", "pipe", "pipe"],
|
|
14512
14736
|
timeout: 3e4,
|
|
@@ -14699,27 +14923,34 @@ async function createGitTracesContribution(params) {
|
|
|
14699
14923
|
const epochSeconds = formatEpochSeconds3(now);
|
|
14700
14924
|
const shortId = state.sessionId.slice(0, 12);
|
|
14701
14925
|
const repoName = repoLabel(repoRoot);
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14926
|
+
let contributionId = state.contributionId;
|
|
14927
|
+
if (!contributionId) {
|
|
14928
|
+
const contribution = await client.createContribution(config.projectId, {
|
|
14929
|
+
contributionTypeSlug: GIT_TRACES_SLUG,
|
|
14930
|
+
title: `${toolLabel2} session ${shortId} \u2014 ${repoName} \u2014 ${epochSeconds}`,
|
|
14931
|
+
body: `Session ID: ${state.sessionId}
|
|
14706
14932
|
Tool: ${toolLabel2}
|
|
14707
14933
|
Repo: ${repoRoot}
|
|
14708
14934
|
Uploaded: ${now.toISOString()}`
|
|
14709
|
-
|
|
14935
|
+
});
|
|
14936
|
+
contributionId = contribution.id;
|
|
14937
|
+
state.contributionId = contributionId;
|
|
14938
|
+
state.baselineUploaded = false;
|
|
14939
|
+
await writeSessionState(state, tool);
|
|
14940
|
+
}
|
|
14710
14941
|
const uploaded = await uploadEpochBaselineArtifacts({
|
|
14711
14942
|
client,
|
|
14712
|
-
contributionId
|
|
14943
|
+
contributionId,
|
|
14713
14944
|
epoch: 1,
|
|
14714
14945
|
artifacts
|
|
14715
14946
|
});
|
|
14716
14947
|
if (!uploaded) return null;
|
|
14717
|
-
await client.submitContribution(
|
|
14948
|
+
await client.submitContribution(contributionId);
|
|
14718
14949
|
appendLog(
|
|
14719
14950
|
"info",
|
|
14720
|
-
`git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${
|
|
14951
|
+
`git-traces: baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=1, bundleBytes=${artifacts.bundleBuffer.byteLength}, metadataBytes=${artifacts.metadataBuffer.byteLength})`
|
|
14721
14952
|
);
|
|
14722
|
-
return
|
|
14953
|
+
return contributionId;
|
|
14723
14954
|
}
|
|
14724
14955
|
async function uploadEpochBaseline(params) {
|
|
14725
14956
|
const artifacts = buildEpochBaselineArtifacts(params);
|
|
@@ -14760,6 +14991,7 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
14760
14991
|
schemaVersion: CURRENT_SCHEMA_VERSION3,
|
|
14761
14992
|
sessionId,
|
|
14762
14993
|
contributionId: null,
|
|
14994
|
+
baselineUploaded: false,
|
|
14763
14995
|
baselineSha: frozen.baselineSha,
|
|
14764
14996
|
baselineTreeSha: frozen.baselineTreeSha,
|
|
14765
14997
|
baselineMetadata: frozen.baselineMetadata,
|
|
@@ -14920,6 +15152,7 @@ async function registerInitialContribution(params) {
|
|
|
14920
15152
|
});
|
|
14921
15153
|
if (!contributionId) return false;
|
|
14922
15154
|
state.contributionId = contributionId;
|
|
15155
|
+
state.baselineUploaded = true;
|
|
14923
15156
|
state.baselineTreeSha = artifacts.baselineTreeSha;
|
|
14924
15157
|
state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
|
|
14925
15158
|
await writeSessionState(state, tool);
|
|
@@ -14961,7 +15194,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14961
15194
|
if (currentHeadSha !== state.headSha) {
|
|
14962
15195
|
const client2 = await loadRepoClient(repo);
|
|
14963
15196
|
if (!client2) return "skipped";
|
|
14964
|
-
if (state.contributionId === null) {
|
|
15197
|
+
if (state.contributionId === null || !state.baselineUploaded) {
|
|
14965
15198
|
const artifacts2 = buildInitialBaselineArtifactsForState(
|
|
14966
15199
|
repoRoot,
|
|
14967
15200
|
tool,
|
|
@@ -15044,7 +15277,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
15044
15277
|
const nextTurnCount = state.turnCount + 1;
|
|
15045
15278
|
const turnLabel = turnSuffix(nextTurnCount);
|
|
15046
15279
|
const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
|
|
15047
|
-
if (state.contributionId === null && !canUploadFile(filename, patchBuffer)) {
|
|
15280
|
+
if ((state.contributionId === null || !state.baselineUploaded) && !canUploadFile(filename, patchBuffer)) {
|
|
15048
15281
|
appendLog(
|
|
15049
15282
|
"warn",
|
|
15050
15283
|
`git-traces: first changed turn skipped before contribution creation (repo=${repoRoot}, project=${config.projectId}, reason=patch-too-large)`
|
|
@@ -15053,7 +15286,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
15053
15286
|
}
|
|
15054
15287
|
const client = await loadRepoClient(repo);
|
|
15055
15288
|
if (!client) return "skipped";
|
|
15056
|
-
if (state.contributionId === null) {
|
|
15289
|
+
if (state.contributionId === null || !state.baselineUploaded) {
|
|
15057
15290
|
const artifacts = buildInitialBaselineArtifactsForState(
|
|
15058
15291
|
repoRoot,
|
|
15059
15292
|
tool,
|
|
@@ -15118,6 +15351,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
15118
15351
|
{
|
|
15119
15352
|
...state,
|
|
15120
15353
|
contributionId: null,
|
|
15354
|
+
baselineUploaded: false,
|
|
15121
15355
|
lastSnapshotSha: state.baselineSha,
|
|
15122
15356
|
lastSnapshotTreeSha: state.baselineTreeSha,
|
|
15123
15357
|
turnCount: 0
|
|
@@ -15156,7 +15390,7 @@ async function handleStop(payload, tool) {
|
|
|
15156
15390
|
if (sessionId) {
|
|
15157
15391
|
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
15158
15392
|
for (const { state } of storedStates) {
|
|
15159
|
-
const repo = repoByRoot.get(
|
|
15393
|
+
const repo = repoByRoot.get(path18.resolve(state.repoRoot));
|
|
15160
15394
|
if (!repo) {
|
|
15161
15395
|
missingConfig++;
|
|
15162
15396
|
appendLog(
|
|
@@ -15218,21 +15452,62 @@ async function handleSessionEnd(payload, tool) {
|
|
|
15218
15452
|
const sessionId = resolveSessionId2(payload);
|
|
15219
15453
|
const project = cwd ? await findProjectForCwd(cwd) : null;
|
|
15220
15454
|
const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
|
|
15455
|
+
const recordedAt = Date.now();
|
|
15221
15456
|
const repoRoots = [];
|
|
15222
15457
|
if (sessionId) {
|
|
15223
15458
|
const states = await listSessionStatesForSession(tool, sessionId);
|
|
15224
15459
|
for (const { state } of states) {
|
|
15225
|
-
repoRoots.push(
|
|
15460
|
+
repoRoots.push(path18.resolve(state.repoRoot));
|
|
15226
15461
|
}
|
|
15227
15462
|
}
|
|
15228
15463
|
if (repoRoots.length === 0 && cwd) {
|
|
15229
15464
|
repoRoots.push(project?.repoRoot ?? cwd);
|
|
15230
15465
|
}
|
|
15231
15466
|
if (repoRoots.length === 0) return;
|
|
15467
|
+
const repos = await loadConfiguredRepos();
|
|
15468
|
+
const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
|
|
15232
15469
|
let cleaned = 0;
|
|
15233
15470
|
let noState = 0;
|
|
15471
|
+
let uploaded = 0;
|
|
15472
|
+
let unchanged = 0;
|
|
15473
|
+
let skipped = 0;
|
|
15234
15474
|
let failed = 0;
|
|
15235
15475
|
for (const repoRoot of repoRoots) {
|
|
15476
|
+
const repo = repoByRoot.get(path18.resolve(repoRoot)) ?? (project && path18.resolve(project.repoRoot) === path18.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
|
|
15477
|
+
if (!repo) {
|
|
15478
|
+
skipped++;
|
|
15479
|
+
appendLog(
|
|
15480
|
+
"warn",
|
|
15481
|
+
`git-traces: preserving SessionEnd state for repo ${repoRoot} (reason=missing-config)`
|
|
15482
|
+
);
|
|
15483
|
+
continue;
|
|
15484
|
+
}
|
|
15485
|
+
const finalOutcome = await processStopRepo(
|
|
15486
|
+
repo,
|
|
15487
|
+
tool,
|
|
15488
|
+
sessionId,
|
|
15489
|
+
recordedAt
|
|
15490
|
+
);
|
|
15491
|
+
if (finalOutcome === "uploaded") uploaded++;
|
|
15492
|
+
else if (finalOutcome === "unchanged") unchanged++;
|
|
15493
|
+
else if (finalOutcome === "skipped") {
|
|
15494
|
+
skipped++;
|
|
15495
|
+
appendLog(
|
|
15496
|
+
"warn",
|
|
15497
|
+
`git-traces: preserving SessionEnd state for repo ${repoRoot} after skipped final upload`
|
|
15498
|
+
);
|
|
15499
|
+
continue;
|
|
15500
|
+
} else if (finalOutcome === "failed") {
|
|
15501
|
+
failed++;
|
|
15502
|
+
appendLog(
|
|
15503
|
+
"warn",
|
|
15504
|
+
`git-traces: preserving SessionEnd state for repo ${repoRoot} after failed final upload`
|
|
15505
|
+
);
|
|
15506
|
+
continue;
|
|
15507
|
+
} else {
|
|
15508
|
+
noState++;
|
|
15509
|
+
continue;
|
|
15510
|
+
}
|
|
15236
15511
|
const outcome = await cleanupSessionStateForRepo(repoRoot, tool, sessionId);
|
|
15237
15512
|
if (outcome === "cleaned") cleaned++;
|
|
15238
15513
|
else if (outcome === "no-state") noState++;
|
|
@@ -15240,16 +15515,16 @@ async function handleSessionEnd(payload, tool) {
|
|
|
15240
15515
|
}
|
|
15241
15516
|
appendLog(
|
|
15242
15517
|
"info",
|
|
15243
|
-
`git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
|
|
15518
|
+
`git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.length}, uploaded=${uploaded}, unchanged=${unchanged}, skipped=${skipped}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
|
|
15244
15519
|
);
|
|
15245
15520
|
}
|
|
15246
15521
|
|
|
15247
15522
|
// src/git-traces/index.ts
|
|
15248
15523
|
var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
15249
15524
|
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
15250
|
-
var
|
|
15251
|
-
function
|
|
15252
|
-
return
|
|
15525
|
+
var FLOW_ID_ENV2 = "HILLCLIMB_GIT_TRACES_FLOW";
|
|
15526
|
+
function newFlowId2() {
|
|
15527
|
+
return crypto5.randomBytes(3).toString("hex");
|
|
15253
15528
|
}
|
|
15254
15529
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
15255
15530
|
"claude",
|
|
@@ -15388,7 +15663,7 @@ async function runGitTraces() {
|
|
|
15388
15663
|
await runGitTracesWorker();
|
|
15389
15664
|
return;
|
|
15390
15665
|
}
|
|
15391
|
-
const flowId =
|
|
15666
|
+
const flowId = newFlowId2();
|
|
15392
15667
|
setLogPrefix(`[${flowId}]`);
|
|
15393
15668
|
const toolArg = parseToolArg2(process.argv.slice(2));
|
|
15394
15669
|
appendLog(
|
|
@@ -15431,7 +15706,7 @@ async function runGitTraces() {
|
|
|
15431
15706
|
...process.env,
|
|
15432
15707
|
[WORKER_ENV_FLAG2]: "1",
|
|
15433
15708
|
[TOOL_ENV_FLAG2]: tool,
|
|
15434
|
-
[
|
|
15709
|
+
[FLOW_ID_ENV2]: flowId
|
|
15435
15710
|
}
|
|
15436
15711
|
}
|
|
15437
15712
|
);
|
|
@@ -15453,7 +15728,7 @@ async function runGitTraces() {
|
|
|
15453
15728
|
}
|
|
15454
15729
|
}
|
|
15455
15730
|
async function runGitTracesWorker() {
|
|
15456
|
-
setLogPrefix(`[${process.env[
|
|
15731
|
+
setLogPrefix(`[${process.env[FLOW_ID_ENV2] ?? newFlowId2()}]`);
|
|
15457
15732
|
const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
|
|
15458
15733
|
appendLog(
|
|
15459
15734
|
"info",
|
|
@@ -15536,14 +15811,14 @@ ${stack}` : ""}`
|
|
|
15536
15811
|
|
|
15537
15812
|
// src/outputs/zip.ts
|
|
15538
15813
|
import fs15 from "fs";
|
|
15539
|
-
import
|
|
15814
|
+
import path20 from "path";
|
|
15540
15815
|
import archiver2 from "archiver";
|
|
15541
15816
|
|
|
15542
15817
|
// src/outputs/downloads.ts
|
|
15543
15818
|
import { execSync as execSync2 } from "child_process";
|
|
15544
15819
|
import fs14 from "fs";
|
|
15545
15820
|
import os9 from "os";
|
|
15546
|
-
import
|
|
15821
|
+
import path19 from "path";
|
|
15547
15822
|
function getDownloadsFolder() {
|
|
15548
15823
|
const home = os9.homedir();
|
|
15549
15824
|
if (process.platform === "linux") {
|
|
@@ -15556,7 +15831,7 @@ function getDownloadsFolder() {
|
|
|
15556
15831
|
} catch {
|
|
15557
15832
|
}
|
|
15558
15833
|
}
|
|
15559
|
-
const downloads =
|
|
15834
|
+
const downloads = path19.join(home, "Downloads");
|
|
15560
15835
|
if (fs14.existsSync(downloads)) return downloads;
|
|
15561
15836
|
return home;
|
|
15562
15837
|
}
|
|
@@ -15566,11 +15841,11 @@ function sanitizeFilename(name) {
|
|
|
15566
15841
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
15567
15842
|
}
|
|
15568
15843
|
function getUniqueFilename(dir, base, ext) {
|
|
15569
|
-
let candidate =
|
|
15844
|
+
let candidate = path20.join(dir, `${base}${ext}`);
|
|
15570
15845
|
if (!fs15.existsSync(candidate)) return candidate;
|
|
15571
15846
|
let i = 1;
|
|
15572
15847
|
while (fs15.existsSync(candidate)) {
|
|
15573
|
-
candidate =
|
|
15848
|
+
candidate = path20.join(dir, `${base}-${i}${ext}`);
|
|
15574
15849
|
i++;
|
|
15575
15850
|
}
|
|
15576
15851
|
return candidate;
|
|
@@ -15580,7 +15855,7 @@ var ZipOutput = class {
|
|
|
15580
15855
|
label = "Save as .zip to Downloads";
|
|
15581
15856
|
async emit(group, options) {
|
|
15582
15857
|
const downloadsDir = getDownloadsFolder();
|
|
15583
|
-
const repoName = sanitizeFilename(
|
|
15858
|
+
const repoName = sanitizeFilename(path20.basename(group.repoPath));
|
|
15584
15859
|
const timeRange = options.timeRange;
|
|
15585
15860
|
const rangePart = timeRange?.label ?? "all";
|
|
15586
15861
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
@@ -15782,11 +16057,11 @@ async function confirmExport(group, output) {
|
|
|
15782
16057
|
// src/sources/claude.ts
|
|
15783
16058
|
import fs16 from "fs";
|
|
15784
16059
|
import os10 from "os";
|
|
15785
|
-
import
|
|
16060
|
+
import path21 from "path";
|
|
15786
16061
|
import readline from "readline";
|
|
15787
16062
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
15788
16063
|
async function resolveRepoPath(projectDir) {
|
|
15789
|
-
const indexPath =
|
|
16064
|
+
const indexPath = path21.join(projectDir, "sessions-index.json");
|
|
15790
16065
|
try {
|
|
15791
16066
|
const raw = await fs16.promises.readFile(indexPath, "utf-8");
|
|
15792
16067
|
const data = JSON.parse(raw);
|
|
@@ -15801,7 +16076,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
15801
16076
|
});
|
|
15802
16077
|
for (const entry of entries) {
|
|
15803
16078
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
15804
|
-
const cwd = await extractCwdFromJsonl(
|
|
16079
|
+
const cwd = await extractCwdFromJsonl(path21.join(projectDir, entry.name));
|
|
15805
16080
|
if (cwd) {
|
|
15806
16081
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
15807
16082
|
}
|
|
@@ -15847,7 +16122,7 @@ async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
|
15847
16122
|
return;
|
|
15848
16123
|
}
|
|
15849
16124
|
for (const entry of entries) {
|
|
15850
|
-
const fullPath =
|
|
16125
|
+
const fullPath = path21.join(dir, entry.name);
|
|
15851
16126
|
if (entry.isDirectory()) {
|
|
15852
16127
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
15853
16128
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -15869,7 +16144,7 @@ function fallbackDecode(encodedName) {
|
|
|
15869
16144
|
var ClaudeSource = class {
|
|
15870
16145
|
name = "claude";
|
|
15871
16146
|
async scan() {
|
|
15872
|
-
const baseDir =
|
|
16147
|
+
const baseDir = path21.join(os10.homedir(), ".claude", "projects");
|
|
15873
16148
|
try {
|
|
15874
16149
|
await fs16.promises.access(baseDir);
|
|
15875
16150
|
} catch {
|
|
@@ -15881,7 +16156,7 @@ var ClaudeSource = class {
|
|
|
15881
16156
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
15882
16157
|
const resultArrays = await Promise.all(
|
|
15883
16158
|
dirEntries.map(async (dir) => {
|
|
15884
|
-
const projectPath =
|
|
16159
|
+
const projectPath = path21.join(baseDir, dir.name);
|
|
15885
16160
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
15886
16161
|
const files = [];
|
|
15887
16162
|
await collectFiles(
|
|
@@ -15901,7 +16176,7 @@ var ClaudeSource = class {
|
|
|
15901
16176
|
// src/sources/codex.ts
|
|
15902
16177
|
import fs17 from "fs";
|
|
15903
16178
|
import os11 from "os";
|
|
15904
|
-
import
|
|
16179
|
+
import path22 from "path";
|
|
15905
16180
|
import readline2 from "readline";
|
|
15906
16181
|
async function parseSessionMeta(filePath) {
|
|
15907
16182
|
const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
|
|
@@ -15934,7 +16209,7 @@ async function findJsonlFiles(dir) {
|
|
|
15934
16209
|
return;
|
|
15935
16210
|
}
|
|
15936
16211
|
for (const entry of entries) {
|
|
15937
|
-
const full =
|
|
16212
|
+
const full = path22.join(d, entry.name);
|
|
15938
16213
|
if (entry.isDirectory()) {
|
|
15939
16214
|
await walk(full);
|
|
15940
16215
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -15979,14 +16254,14 @@ async function loadHistory(historyPath) {
|
|
|
15979
16254
|
var CodexSource = class {
|
|
15980
16255
|
name = "codex";
|
|
15981
16256
|
async scan() {
|
|
15982
|
-
const codexDir =
|
|
15983
|
-
const sessionsDir =
|
|
16257
|
+
const codexDir = path22.join(os11.homedir(), ".codex");
|
|
16258
|
+
const sessionsDir = path22.join(codexDir, "sessions");
|
|
15984
16259
|
try {
|
|
15985
16260
|
await fs17.promises.access(sessionsDir);
|
|
15986
16261
|
} catch {
|
|
15987
16262
|
return [];
|
|
15988
16263
|
}
|
|
15989
|
-
const historyPath =
|
|
16264
|
+
const historyPath = path22.join(codexDir, "history.jsonl");
|
|
15990
16265
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
15991
16266
|
findJsonlFiles(sessionsDir),
|
|
15992
16267
|
loadHistory(historyPath)
|
|
@@ -16009,8 +16284,8 @@ var CodexSource = class {
|
|
|
16009
16284
|
});
|
|
16010
16285
|
const historyLines = historyMap.get(meta.sessionId);
|
|
16011
16286
|
if (historyLines) {
|
|
16012
|
-
const sessionDir =
|
|
16013
|
-
const historyAbsPath =
|
|
16287
|
+
const sessionDir = path22.relative(sessionsDir, path22.dirname(filePath));
|
|
16288
|
+
const historyAbsPath = path22.join(
|
|
16014
16289
|
sessionsDir,
|
|
16015
16290
|
sessionDir,
|
|
16016
16291
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -16032,16 +16307,16 @@ var CodexSource = class {
|
|
|
16032
16307
|
// src/sources/copilotChat.ts
|
|
16033
16308
|
import fs18 from "fs";
|
|
16034
16309
|
import os12 from "os";
|
|
16035
|
-
import
|
|
16310
|
+
import path23 from "path";
|
|
16036
16311
|
import { fileURLToPath } from "url";
|
|
16037
16312
|
function vsCodeUserDirs() {
|
|
16038
16313
|
const home = os12.homedir();
|
|
16039
16314
|
const dirs = [
|
|
16040
|
-
|
|
16041
|
-
|
|
16315
|
+
path23.join(home, "Library", "Application Support", "Code", "User"),
|
|
16316
|
+
path23.join(home, ".config", "Code", "User")
|
|
16042
16317
|
];
|
|
16043
16318
|
if (process.env.APPDATA) {
|
|
16044
|
-
dirs.push(
|
|
16319
|
+
dirs.push(path23.join(process.env.APPDATA, "Code", "User"));
|
|
16045
16320
|
}
|
|
16046
16321
|
return dirs;
|
|
16047
16322
|
}
|
|
@@ -16078,7 +16353,7 @@ var CopilotChatSource = class {
|
|
|
16078
16353
|
async scan() {
|
|
16079
16354
|
const results = [];
|
|
16080
16355
|
for (const userDir of vsCodeUserDirs()) {
|
|
16081
|
-
const workspaceStorage =
|
|
16356
|
+
const workspaceStorage = path23.join(userDir, "workspaceStorage");
|
|
16082
16357
|
let hashDirs;
|
|
16083
16358
|
try {
|
|
16084
16359
|
hashDirs = await fs18.promises.readdir(workspaceStorage, {
|
|
@@ -16089,8 +16364,8 @@ var CopilotChatSource = class {
|
|
|
16089
16364
|
}
|
|
16090
16365
|
for (const hash of hashDirs) {
|
|
16091
16366
|
if (!hash.isDirectory()) continue;
|
|
16092
|
-
const wsRoot =
|
|
16093
|
-
const transcriptsDir =
|
|
16367
|
+
const wsRoot = path23.join(workspaceStorage, hash.name);
|
|
16368
|
+
const transcriptsDir = path23.join(
|
|
16094
16369
|
wsRoot,
|
|
16095
16370
|
"GitHub.copilot-chat",
|
|
16096
16371
|
"transcripts"
|
|
@@ -16104,7 +16379,7 @@ var CopilotChatSource = class {
|
|
|
16104
16379
|
continue;
|
|
16105
16380
|
}
|
|
16106
16381
|
const repoPath = await readWorkspaceFolder(
|
|
16107
|
-
|
|
16382
|
+
path23.join(wsRoot, "workspace.json")
|
|
16108
16383
|
);
|
|
16109
16384
|
if (!repoPath) continue;
|
|
16110
16385
|
for (const entry of transcriptEntries) {
|
|
@@ -16112,7 +16387,7 @@ var CopilotChatSource = class {
|
|
|
16112
16387
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
16113
16388
|
results.push({
|
|
16114
16389
|
sourceName: this.name,
|
|
16115
|
-
absolutePath:
|
|
16390
|
+
absolutePath: path23.join(transcriptsDir, entry.name),
|
|
16116
16391
|
repoPath,
|
|
16117
16392
|
metadata: { sessionId }
|
|
16118
16393
|
});
|
|
@@ -16179,10 +16454,10 @@ async function runInteractive() {
|
|
|
16179
16454
|
s.start(`Scanning ${source.name} logs...`);
|
|
16180
16455
|
const allFiles = await source.scan();
|
|
16181
16456
|
const allGroups = await mergeByRepo(allFiles);
|
|
16182
|
-
const repoRoot =
|
|
16457
|
+
const repoRoot = path24.resolve(repo.root);
|
|
16183
16458
|
const matching = allGroups.filter((g) => {
|
|
16184
|
-
const resolved =
|
|
16185
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
16459
|
+
const resolved = path24.resolve(g.repoPath);
|
|
16460
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path24.sep);
|
|
16186
16461
|
});
|
|
16187
16462
|
if (matching.length === 0) {
|
|
16188
16463
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -16213,7 +16488,7 @@ async function runInteractive() {
|
|
|
16213
16488
|
}
|
|
16214
16489
|
}
|
|
16215
16490
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
16216
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
16491
|
+
const envFilePaths = envFileNames.map((n) => path24.join(repoRoot, n));
|
|
16217
16492
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
16218
16493
|
const secretResult = await collectSecrets(
|
|
16219
16494
|
repoRoot,
|