hillclimb 0.1.1 → 0.1.3
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 +331 -87
- 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 fs16 from "fs";
|
|
5
|
+
import path19 from "path";
|
|
6
6
|
import * as p6 from "@clack/prompts";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -424,6 +424,27 @@ var TOOLS = [
|
|
|
424
424
|
// Events are wired inside the plugin itself; see OPENCODE_PLUGIN_CONTENT.
|
|
425
425
|
detect: () => isDir(path4.join(os2.homedir(), ".local", "share", "opencode")) || isDir(path4.join(os2.homedir(), ".config", "opencode"))
|
|
426
426
|
},
|
|
427
|
+
{
|
|
428
|
+
// VS Code Copilot Chat. Same "no SessionEnd" constraint as Codex (the
|
|
429
|
+
// event is documented but never fires — VS Code issue microsoft/vscode#300650).
|
|
430
|
+
// Stale-state cleanup happens on next SessionStart, like Codex.
|
|
431
|
+
// Hook config lives at .github/hooks/hooks.json — distinct from
|
|
432
|
+
// .claude/settings.local.json so Claude Code and Copilot Chat don't
|
|
433
|
+
// fire each other's hooks even though VS Code's docs claim they share.
|
|
434
|
+
tool: "copilot-chat",
|
|
435
|
+
label: "GitHub Copilot Chat",
|
|
436
|
+
settingsFile: ".github/hooks/hooks.json",
|
|
437
|
+
format: "copilot",
|
|
438
|
+
events: [
|
|
439
|
+
{ eventName: "SessionStart", command: GIT_TRACES_CMD("copilot-chat") },
|
|
440
|
+
{ eventName: "Stop", command: GIT_TRACES_CMD("copilot-chat") },
|
|
441
|
+
{
|
|
442
|
+
eventName: "Stop",
|
|
443
|
+
command: `${HOOK_CMD("upload")} --tool=copilot-chat`
|
|
444
|
+
}
|
|
445
|
+
],
|
|
446
|
+
detect: copilotChatDetect
|
|
447
|
+
},
|
|
427
448
|
{
|
|
428
449
|
// Codex has no SessionEnd hook (only SessionStart, PreToolUse, PostToolUse,
|
|
429
450
|
// UserPromptSubmit, Stop). We can't clean up refs/state at session end;
|
|
@@ -451,6 +472,20 @@ function isDir(p7) {
|
|
|
451
472
|
return false;
|
|
452
473
|
}
|
|
453
474
|
}
|
|
475
|
+
function copilotChatDetect() {
|
|
476
|
+
const home = os2.homedir();
|
|
477
|
+
const suffix = path4.join(
|
|
478
|
+
"User",
|
|
479
|
+
"globalStorage",
|
|
480
|
+
"github.copilot-chat"
|
|
481
|
+
);
|
|
482
|
+
const candidates = [
|
|
483
|
+
path4.join(home, "Library", "Application Support", "Code", suffix),
|
|
484
|
+
path4.join(home, ".config", "Code", suffix),
|
|
485
|
+
process.env.APPDATA ? path4.join(process.env.APPDATA, "Code", suffix) : null
|
|
486
|
+
].filter((p7) => p7 !== null);
|
|
487
|
+
return candidates.some(isDir);
|
|
488
|
+
}
|
|
454
489
|
function settingsPath(repoRoot, def) {
|
|
455
490
|
return path4.join(repoRoot, def.settingsFile);
|
|
456
491
|
}
|
|
@@ -545,6 +580,36 @@ function cursorUninstall(settings, eventName, command) {
|
|
|
545
580
|
}
|
|
546
581
|
return true;
|
|
547
582
|
}
|
|
583
|
+
function copilotHookPresent(entries, command) {
|
|
584
|
+
return entries.some((e) => e.type === "command" && e.command === command);
|
|
585
|
+
}
|
|
586
|
+
function copilotInstall(settings, eventName, command) {
|
|
587
|
+
if (!settings.hooks) settings.hooks = {};
|
|
588
|
+
if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
|
|
589
|
+
const entries = settings.hooks[eventName];
|
|
590
|
+
if (copilotHookPresent(entries, command)) return false;
|
|
591
|
+
entries.push({ type: "command", command });
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
function copilotCheck(settings, eventName, command) {
|
|
595
|
+
const entries = settings.hooks?.[eventName] ?? [];
|
|
596
|
+
return copilotHookPresent(entries, command);
|
|
597
|
+
}
|
|
598
|
+
function copilotUninstall(settings, eventName, command) {
|
|
599
|
+
const entries = settings.hooks?.[eventName];
|
|
600
|
+
if (!entries || entries.length === 0) return false;
|
|
601
|
+
const next = entries.filter(
|
|
602
|
+
(e) => !(e.type === "command" && e.command === command)
|
|
603
|
+
);
|
|
604
|
+
if (next.length === entries.length) return false;
|
|
605
|
+
if (next.length > 0) {
|
|
606
|
+
settings.hooks[eventName] = next;
|
|
607
|
+
} else {
|
|
608
|
+
delete settings.hooks[eventName];
|
|
609
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
548
613
|
var OPENCODE_PLUGIN_VERSION = 2;
|
|
549
614
|
var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
|
|
550
615
|
var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
|
|
@@ -714,13 +779,34 @@ async function opencodeCheck(file) {
|
|
|
714
779
|
}
|
|
715
780
|
}
|
|
716
781
|
function install(settings, format, eventName, command) {
|
|
717
|
-
|
|
782
|
+
switch (format) {
|
|
783
|
+
case "cursor":
|
|
784
|
+
return cursorInstall(settings, eventName, command);
|
|
785
|
+
case "copilot":
|
|
786
|
+
return copilotInstall(settings, eventName, command);
|
|
787
|
+
default:
|
|
788
|
+
return claudeInstall(settings, eventName, command);
|
|
789
|
+
}
|
|
718
790
|
}
|
|
719
791
|
function check(settings, format, eventName, command) {
|
|
720
|
-
|
|
792
|
+
switch (format) {
|
|
793
|
+
case "cursor":
|
|
794
|
+
return cursorCheck(settings, eventName, command);
|
|
795
|
+
case "copilot":
|
|
796
|
+
return copilotCheck(settings, eventName, command);
|
|
797
|
+
default:
|
|
798
|
+
return claudeCheck(settings, eventName, command);
|
|
799
|
+
}
|
|
721
800
|
}
|
|
722
801
|
function uninstall(settings, format, eventName, command) {
|
|
723
|
-
|
|
802
|
+
switch (format) {
|
|
803
|
+
case "cursor":
|
|
804
|
+
return cursorUninstall(settings, eventName, command);
|
|
805
|
+
case "copilot":
|
|
806
|
+
return copilotUninstall(settings, eventName, command);
|
|
807
|
+
default:
|
|
808
|
+
return claudeUninstall(settings, eventName, command);
|
|
809
|
+
}
|
|
724
810
|
}
|
|
725
811
|
function legacyCommandsFor(command) {
|
|
726
812
|
const prefix = "npx hillclimb ";
|
|
@@ -1277,7 +1363,7 @@ async function runInit(args = []) {
|
|
|
1277
1363
|
const hookResults = await installDetectedHooks(repoRoot);
|
|
1278
1364
|
if (hookResults.length === 0) {
|
|
1279
1365
|
p3.log.warn(
|
|
1280
|
-
"No supported tools detected (
|
|
1366
|
+
"No supported tools detected (Claude Code, Cursor, Codex, opencode, GitHub Copilot Chat). Hook not installed."
|
|
1281
1367
|
);
|
|
1282
1368
|
} else {
|
|
1283
1369
|
for (const r of hookResults) {
|
|
@@ -11464,6 +11550,9 @@ function formatTitleTimestamp(date) {
|
|
|
11464
11550
|
const pad = (n) => String(n).padStart(2, "0");
|
|
11465
11551
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
11466
11552
|
}
|
|
11553
|
+
function lineHasAssistant(line) {
|
|
11554
|
+
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
11555
|
+
}
|
|
11467
11556
|
async function hasAssistantMessage(transcriptPath) {
|
|
11468
11557
|
const stream = fs8.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
11469
11558
|
let buffer = "";
|
|
@@ -11474,15 +11563,14 @@ async function hasAssistantMessage(transcriptPath) {
|
|
|
11474
11563
|
while (newlineIdx !== -1) {
|
|
11475
11564
|
const line = buffer.slice(0, newlineIdx);
|
|
11476
11565
|
buffer = buffer.slice(newlineIdx + 1);
|
|
11477
|
-
if (
|
|
11566
|
+
if (lineHasAssistant(line)) {
|
|
11478
11567
|
stream.destroy();
|
|
11479
11568
|
return true;
|
|
11480
11569
|
}
|
|
11481
11570
|
newlineIdx = buffer.indexOf("\n");
|
|
11482
11571
|
}
|
|
11483
11572
|
}
|
|
11484
|
-
if (buffer
|
|
11485
|
-
return true;
|
|
11573
|
+
if (lineHasAssistant(buffer)) return true;
|
|
11486
11574
|
} catch {
|
|
11487
11575
|
return true;
|
|
11488
11576
|
}
|
|
@@ -11600,7 +11688,13 @@ async function uploadSession(args) {
|
|
|
11600
11688
|
}
|
|
11601
11689
|
const client = new PlatformClient(config.apiBaseUrl, identity.sessionCookie);
|
|
11602
11690
|
const shortId = sessionId.slice(0, 12);
|
|
11603
|
-
const toolLabels = {
|
|
11691
|
+
const toolLabels = {
|
|
11692
|
+
cursor: "Cursor",
|
|
11693
|
+
codex: "Codex",
|
|
11694
|
+
claude: "Claude",
|
|
11695
|
+
"copilot-chat": "GitHub Copilot Chat",
|
|
11696
|
+
opencode: "opencode"
|
|
11697
|
+
};
|
|
11604
11698
|
const toolLabel = toolLabels[sourceTool] ?? "Claude";
|
|
11605
11699
|
const title = `${toolLabel} session ${shortId} \u2014 ${formatTitleTimestamp(now)}`;
|
|
11606
11700
|
const body = `Session ID: ${sessionId}
|
|
@@ -11641,6 +11735,15 @@ Uploaded: ${now.toISOString()}`;
|
|
|
11641
11735
|
}
|
|
11642
11736
|
}
|
|
11643
11737
|
var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
|
|
11738
|
+
var TOOL_ENV_FLAG = "HILLCLIMB_UPLOAD_TOOL";
|
|
11739
|
+
function parseToolArg(argv) {
|
|
11740
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11741
|
+
const a = argv[i];
|
|
11742
|
+
if (a.startsWith("--tool=")) return a.slice("--tool=".length);
|
|
11743
|
+
if (a === "--tool" && i + 1 < argv.length) return argv[i + 1];
|
|
11744
|
+
}
|
|
11745
|
+
return null;
|
|
11746
|
+
}
|
|
11644
11747
|
async function runUpload() {
|
|
11645
11748
|
if (process.env[WORKER_ENV_FLAG] === "1") {
|
|
11646
11749
|
await runUploadWorker();
|
|
@@ -11669,11 +11772,17 @@ async function runUpload() {
|
|
|
11669
11772
|
appendLog("error", "Cannot detach worker: process.argv[1] is empty");
|
|
11670
11773
|
return;
|
|
11671
11774
|
}
|
|
11775
|
+
const toolArg = parseToolArg(process.argv.slice(2));
|
|
11776
|
+
const workerEnv = {
|
|
11777
|
+
...process.env,
|
|
11778
|
+
[WORKER_ENV_FLAG]: "1"
|
|
11779
|
+
};
|
|
11780
|
+
if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
|
|
11672
11781
|
try {
|
|
11673
11782
|
const child = spawn2(process.execPath, [entrypoint, "upload"], {
|
|
11674
11783
|
detached: true,
|
|
11675
11784
|
stdio: ["pipe", "ignore", "ignore"],
|
|
11676
|
-
env:
|
|
11785
|
+
env: workerEnv
|
|
11677
11786
|
});
|
|
11678
11787
|
child.on("error", (err) => {
|
|
11679
11788
|
appendLog("error", `Failed to spawn worker: ${err.message}`);
|
|
@@ -11719,6 +11828,8 @@ async function runUploadWorker() {
|
|
|
11719
11828
|
);
|
|
11720
11829
|
return;
|
|
11721
11830
|
}
|
|
11831
|
+
const toolOverride = process.env[TOOL_ENV_FLAG];
|
|
11832
|
+
if (toolOverride) payload.tool = toolOverride;
|
|
11722
11833
|
try {
|
|
11723
11834
|
await runUploadInner(payload);
|
|
11724
11835
|
} catch (err) {
|
|
@@ -11941,7 +12052,7 @@ function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
|
|
|
11941
12052
|
return "branch-switch";
|
|
11942
12053
|
}
|
|
11943
12054
|
}
|
|
11944
|
-
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind) {
|
|
12055
|
+
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt) {
|
|
11945
12056
|
const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
|
|
11946
12057
|
const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
|
|
11947
12058
|
const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
|
|
@@ -11953,7 +12064,7 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
11953
12064
|
return {
|
|
11954
12065
|
sessionId,
|
|
11955
12066
|
tool,
|
|
11956
|
-
startedAt
|
|
12067
|
+
startedAt,
|
|
11957
12068
|
cwd: repoRoot,
|
|
11958
12069
|
patchFormat: "incremental",
|
|
11959
12070
|
epoch,
|
|
@@ -12058,9 +12169,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
12058
12169
|
oldPath
|
|
12059
12170
|
});
|
|
12060
12171
|
} else {
|
|
12061
|
-
const
|
|
12062
|
-
indexByPath.set(
|
|
12063
|
-
files.push({ path:
|
|
12172
|
+
const path20 = parts[parts.length - 1];
|
|
12173
|
+
indexByPath.set(path20, files.length);
|
|
12174
|
+
files.push({ path: path20, status, additions: 0, deletions: 0 });
|
|
12064
12175
|
}
|
|
12065
12176
|
}
|
|
12066
12177
|
for (const line of numstat.split("\n")) {
|
|
@@ -12111,7 +12222,7 @@ import crypto from "crypto";
|
|
|
12111
12222
|
import fs10 from "fs";
|
|
12112
12223
|
import os7 from "os";
|
|
12113
12224
|
import path13 from "path";
|
|
12114
|
-
var CURRENT_SCHEMA_VERSION =
|
|
12225
|
+
var CURRENT_SCHEMA_VERSION = 3;
|
|
12115
12226
|
var STATE_DIR = path13.join(os7.homedir(), ".hillclimb", "git-traces");
|
|
12116
12227
|
function stateFileForRepo(repoRoot, tool) {
|
|
12117
12228
|
const hash = crypto.createHash("sha256").update(`${path13.resolve(repoRoot)}\0${tool}`).digest("hex").slice(0, 16);
|
|
@@ -12188,6 +12299,7 @@ var TOOL_LABELS = {
|
|
|
12188
12299
|
cursor: "Cursor",
|
|
12189
12300
|
codex: "Codex",
|
|
12190
12301
|
claude: "Claude",
|
|
12302
|
+
"copilot-chat": "GitHub Copilot Chat",
|
|
12191
12303
|
opencode: "opencode"
|
|
12192
12304
|
};
|
|
12193
12305
|
function resolveCwd(payload) {
|
|
@@ -12237,7 +12349,16 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
|
|
|
12237
12349
|
);
|
|
12238
12350
|
appendLog("info", `git-traces: uploaded ${filename} (${buffer.byteLength} bytes)`);
|
|
12239
12351
|
}
|
|
12240
|
-
|
|
12352
|
+
function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
12353
|
+
const baselineSha = captureBaselineSha(repoRoot);
|
|
12354
|
+
pinRef(
|
|
12355
|
+
repoRoot,
|
|
12356
|
+
`refs/hillclimb/baseline/${sessionId}/${epochPrefix(epoch)}`,
|
|
12357
|
+
baselineSha
|
|
12358
|
+
);
|
|
12359
|
+
return { baselineSha, headSha: captureHeadSha(repoRoot) };
|
|
12360
|
+
}
|
|
12361
|
+
async function uploadEpochBaseline(params) {
|
|
12241
12362
|
const {
|
|
12242
12363
|
repoRoot,
|
|
12243
12364
|
client,
|
|
@@ -12245,16 +12366,12 @@ async function openEpoch(params) {
|
|
|
12245
12366
|
sessionId,
|
|
12246
12367
|
tool,
|
|
12247
12368
|
epoch,
|
|
12369
|
+
baselineSha,
|
|
12248
12370
|
prevHeadSha,
|
|
12249
|
-
transitionKind
|
|
12371
|
+
transitionKind,
|
|
12372
|
+
startedAt
|
|
12250
12373
|
} = params;
|
|
12251
|
-
const baselineSha = captureBaselineSha(repoRoot);
|
|
12252
12374
|
const prefix = epochPrefix(epoch);
|
|
12253
|
-
pinRef(
|
|
12254
|
-
repoRoot,
|
|
12255
|
-
`refs/hillclimb/baseline/${sessionId}/${prefix}`,
|
|
12256
|
-
baselineSha
|
|
12257
|
-
);
|
|
12258
12375
|
const metadata = buildBaselineMetadata(
|
|
12259
12376
|
repoRoot,
|
|
12260
12377
|
sessionId,
|
|
@@ -12263,7 +12380,8 @@ async function openEpoch(params) {
|
|
|
12263
12380
|
CLI_VERSION,
|
|
12264
12381
|
epoch,
|
|
12265
12382
|
prevHeadSha,
|
|
12266
|
-
transitionKind
|
|
12383
|
+
transitionKind,
|
|
12384
|
+
startedAt
|
|
12267
12385
|
);
|
|
12268
12386
|
let baselineTreeSha;
|
|
12269
12387
|
let bundleBuffer;
|
|
@@ -12297,11 +12415,17 @@ async function openEpoch(params) {
|
|
|
12297
12415
|
"application/x-git-bundle",
|
|
12298
12416
|
bundleBuffer
|
|
12299
12417
|
);
|
|
12300
|
-
return {
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
|
|
12418
|
+
return { baselineTreeSha };
|
|
12419
|
+
}
|
|
12420
|
+
async function openEpoch(params) {
|
|
12421
|
+
const { baselineSha, headSha } = pinEpochBaseline(
|
|
12422
|
+
params.repoRoot,
|
|
12423
|
+
params.sessionId,
|
|
12424
|
+
params.epoch
|
|
12425
|
+
);
|
|
12426
|
+
const uploaded = await uploadEpochBaseline({ ...params, baselineSha });
|
|
12427
|
+
if (!uploaded) return null;
|
|
12428
|
+
return { baselineSha, baselineTreeSha: uploaded.baselineTreeSha, headSha };
|
|
12305
12429
|
}
|
|
12306
12430
|
async function initializeSession(cwd, tool, sessionId) {
|
|
12307
12431
|
const project = await findProjectForCwd(cwd);
|
|
@@ -12309,57 +12433,25 @@ async function initializeSession(cwd, tool, sessionId) {
|
|
|
12309
12433
|
appendLog("warn", "git-traces: no hillclimb project config found, skipping");
|
|
12310
12434
|
return null;
|
|
12311
12435
|
}
|
|
12312
|
-
const {
|
|
12313
|
-
const identity = await loadIdentity(config.apiBaseUrl);
|
|
12314
|
-
if (!identity) {
|
|
12315
|
-
appendLog(
|
|
12316
|
-
"warn",
|
|
12317
|
-
`git-traces: no saved login for ${config.apiBaseUrl}, skipping`
|
|
12318
|
-
);
|
|
12319
|
-
return null;
|
|
12320
|
-
}
|
|
12321
|
-
const client = new PlatformClient(config.apiBaseUrl, identity.sessionCookie);
|
|
12322
|
-
const toolLabel = TOOL_LABELS[tool] ?? "Claude";
|
|
12323
|
-
const now = /* @__PURE__ */ new Date();
|
|
12324
|
-
const shortId = sessionId.slice(0, 12);
|
|
12325
|
-
const contribution = await client.createContribution(config.workspaceId, {
|
|
12326
|
-
contributionTypeSlug: GIT_TRACES_SLUG,
|
|
12327
|
-
title: `${toolLabel} session ${shortId} \u2014 ${formatTitleTimestamp2(now)}`,
|
|
12328
|
-
body: `Session ID: ${sessionId}
|
|
12329
|
-
Tool: ${toolLabel}
|
|
12330
|
-
Repo: ${cwd}
|
|
12331
|
-
Uploaded: ${now.toISOString()}`
|
|
12332
|
-
});
|
|
12333
|
-
await client.submitContribution(contribution.id);
|
|
12334
|
-
const artifacts = await openEpoch({
|
|
12335
|
-
repoRoot: cwd,
|
|
12336
|
-
client,
|
|
12337
|
-
contributionId: contribution.id,
|
|
12338
|
-
sessionId,
|
|
12339
|
-
tool,
|
|
12340
|
-
epoch: 1,
|
|
12341
|
-
prevHeadSha: null,
|
|
12342
|
-
transitionKind: "initial"
|
|
12343
|
-
});
|
|
12344
|
-
if (!artifacts) return null;
|
|
12436
|
+
const { baselineSha, headSha } = pinEpochBaseline(cwd, sessionId, 1);
|
|
12345
12437
|
const state = {
|
|
12346
12438
|
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
12347
12439
|
sessionId,
|
|
12348
|
-
contributionId:
|
|
12349
|
-
baselineSha
|
|
12350
|
-
baselineTreeSha:
|
|
12351
|
-
lastSnapshotSha:
|
|
12352
|
-
lastSnapshotTreeSha:
|
|
12440
|
+
contributionId: null,
|
|
12441
|
+
baselineSha,
|
|
12442
|
+
baselineTreeSha: null,
|
|
12443
|
+
lastSnapshotSha: baselineSha,
|
|
12444
|
+
lastSnapshotTreeSha: null,
|
|
12353
12445
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12354
12446
|
epoch: 1,
|
|
12355
12447
|
turnCount: 0,
|
|
12356
|
-
headSha
|
|
12448
|
+
headSha,
|
|
12357
12449
|
repoRoot: cwd
|
|
12358
12450
|
};
|
|
12359
12451
|
await writeSessionState(state, tool);
|
|
12360
12452
|
appendLog(
|
|
12361
12453
|
"info",
|
|
12362
|
-
`git-traces: session
|
|
12454
|
+
`git-traces: session pending (baseline=${baselineSha.slice(0, 8)}, awaiting first turn)`
|
|
12363
12455
|
);
|
|
12364
12456
|
return state;
|
|
12365
12457
|
}
|
|
@@ -12417,6 +12509,53 @@ async function handleStop(payload, tool) {
|
|
|
12417
12509
|
project.config.apiBaseUrl,
|
|
12418
12510
|
identity.sessionCookie
|
|
12419
12511
|
);
|
|
12512
|
+
if (state.contributionId === null) {
|
|
12513
|
+
const toolLabel = TOOL_LABELS[tool] ?? "Claude";
|
|
12514
|
+
const now = /* @__PURE__ */ new Date();
|
|
12515
|
+
const shortId = state.sessionId.slice(0, 12);
|
|
12516
|
+
const contribution = await client.createContribution(
|
|
12517
|
+
project.config.workspaceId,
|
|
12518
|
+
{
|
|
12519
|
+
contributionTypeSlug: GIT_TRACES_SLUG,
|
|
12520
|
+
title: `${toolLabel} session ${shortId} \u2014 ${formatTitleTimestamp2(now)}`,
|
|
12521
|
+
body: `Session ID: ${state.sessionId}
|
|
12522
|
+
Tool: ${toolLabel}
|
|
12523
|
+
Repo: ${cwd}
|
|
12524
|
+
Uploaded: ${now.toISOString()}`
|
|
12525
|
+
}
|
|
12526
|
+
);
|
|
12527
|
+
await client.submitContribution(contribution.id);
|
|
12528
|
+
const uploaded = await uploadEpochBaseline({
|
|
12529
|
+
repoRoot: cwd,
|
|
12530
|
+
client,
|
|
12531
|
+
contributionId: contribution.id,
|
|
12532
|
+
sessionId: state.sessionId,
|
|
12533
|
+
tool,
|
|
12534
|
+
epoch: 1,
|
|
12535
|
+
baselineSha: state.baselineSha,
|
|
12536
|
+
prevHeadSha: null,
|
|
12537
|
+
transitionKind: "initial",
|
|
12538
|
+
startedAt: state.startedAt
|
|
12539
|
+
});
|
|
12540
|
+
if (!uploaded) return;
|
|
12541
|
+
state.contributionId = contribution.id;
|
|
12542
|
+
state.baselineTreeSha = uploaded.baselineTreeSha;
|
|
12543
|
+
state.lastSnapshotTreeSha = uploaded.baselineTreeSha;
|
|
12544
|
+
await writeSessionState(state, tool);
|
|
12545
|
+
appendLog(
|
|
12546
|
+
"info",
|
|
12547
|
+
`git-traces: session registered on first turn (epoch=1, baseline=${state.baselineSha.slice(0, 8)}, contribution=${contribution.id})`
|
|
12548
|
+
);
|
|
12549
|
+
}
|
|
12550
|
+
const contributionId = state.contributionId;
|
|
12551
|
+
const lastSnapshotTreeSha = state.lastSnapshotTreeSha;
|
|
12552
|
+
if (contributionId === null || lastSnapshotTreeSha === null) {
|
|
12553
|
+
appendLog(
|
|
12554
|
+
"error",
|
|
12555
|
+
"git-traces: invariant violation \u2014 session state missing contributionId or tree SHA after lazy init"
|
|
12556
|
+
);
|
|
12557
|
+
return;
|
|
12558
|
+
}
|
|
12420
12559
|
const currentHeadSha = captureHeadSha(cwd);
|
|
12421
12560
|
if (currentHeadSha !== state.headSha) {
|
|
12422
12561
|
const transitionKind = detectTransitionKind(
|
|
@@ -12432,12 +12571,13 @@ async function handleStop(payload, tool) {
|
|
|
12432
12571
|
const artifacts = await openEpoch({
|
|
12433
12572
|
repoRoot: cwd,
|
|
12434
12573
|
client,
|
|
12435
|
-
contributionId
|
|
12574
|
+
contributionId,
|
|
12436
12575
|
sessionId: state.sessionId,
|
|
12437
12576
|
tool,
|
|
12438
12577
|
epoch: nextEpoch,
|
|
12439
12578
|
prevHeadSha: state.headSha || null,
|
|
12440
|
-
transitionKind
|
|
12579
|
+
transitionKind,
|
|
12580
|
+
startedAt: state.startedAt
|
|
12441
12581
|
});
|
|
12442
12582
|
if (!artifacts) return;
|
|
12443
12583
|
const next = {
|
|
@@ -12457,7 +12597,7 @@ async function handleStop(payload, tool) {
|
|
|
12457
12597
|
const currentTreeSha = buildSnapshotTree(cwd, currentSha);
|
|
12458
12598
|
const patchBuffer = createTreeDiffPatchGz(
|
|
12459
12599
|
cwd,
|
|
12460
|
-
|
|
12600
|
+
lastSnapshotTreeSha,
|
|
12461
12601
|
currentTreeSha
|
|
12462
12602
|
);
|
|
12463
12603
|
if (!patchBuffer) {
|
|
@@ -12478,7 +12618,7 @@ async function handleStop(payload, tool) {
|
|
|
12478
12618
|
const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
|
|
12479
12619
|
await uploadFile(
|
|
12480
12620
|
client,
|
|
12481
|
-
|
|
12621
|
+
contributionId,
|
|
12482
12622
|
filename,
|
|
12483
12623
|
"application/gzip",
|
|
12484
12624
|
patchBuffer
|
|
@@ -12514,9 +12654,15 @@ async function handleSessionEnd(payload, tool) {
|
|
|
12514
12654
|
|
|
12515
12655
|
// src/git-traces/index.ts
|
|
12516
12656
|
var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
12517
|
-
var
|
|
12518
|
-
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
12519
|
-
|
|
12657
|
+
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
12658
|
+
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
12659
|
+
"claude",
|
|
12660
|
+
"codex",
|
|
12661
|
+
"copilot-chat",
|
|
12662
|
+
"cursor",
|
|
12663
|
+
"opencode"
|
|
12664
|
+
]);
|
|
12665
|
+
function parseToolArg2(argv) {
|
|
12520
12666
|
for (let i = 0; i < argv.length; i++) {
|
|
12521
12667
|
const a = argv[i];
|
|
12522
12668
|
if (a.startsWith("--tool=")) return a.slice("--tool=".length);
|
|
@@ -12537,7 +12683,7 @@ async function runGitTraces() {
|
|
|
12537
12683
|
await runGitTracesWorker();
|
|
12538
12684
|
return;
|
|
12539
12685
|
}
|
|
12540
|
-
const toolArg =
|
|
12686
|
+
const toolArg = parseToolArg2(process.argv.slice(2));
|
|
12541
12687
|
appendLog(
|
|
12542
12688
|
"info",
|
|
12543
12689
|
`git-traces hook invoked (pid ${process.pid}, tool=${toolArg ?? "<none>"})`
|
|
@@ -12578,7 +12724,7 @@ async function runGitTraces() {
|
|
|
12578
12724
|
env: {
|
|
12579
12725
|
...process.env,
|
|
12580
12726
|
[WORKER_ENV_FLAG2]: "1",
|
|
12581
|
-
[
|
|
12727
|
+
[TOOL_ENV_FLAG2]: toolArg
|
|
12582
12728
|
}
|
|
12583
12729
|
}
|
|
12584
12730
|
);
|
|
@@ -12600,7 +12746,7 @@ async function runGitTraces() {
|
|
|
12600
12746
|
}
|
|
12601
12747
|
}
|
|
12602
12748
|
async function runGitTracesWorker() {
|
|
12603
|
-
const tool = process.env[
|
|
12749
|
+
const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
|
|
12604
12750
|
appendLog(
|
|
12605
12751
|
"info",
|
|
12606
12752
|
`git-traces worker started (pid ${process.pid}, tool=${tool ?? "<none>"})`
|
|
@@ -13174,8 +13320,106 @@ var CodexSource = class {
|
|
|
13174
13320
|
}
|
|
13175
13321
|
};
|
|
13176
13322
|
|
|
13323
|
+
// src/sources/copilotChat.ts
|
|
13324
|
+
import fs15 from "fs";
|
|
13325
|
+
import os11 from "os";
|
|
13326
|
+
import path18 from "path";
|
|
13327
|
+
import { fileURLToPath } from "url";
|
|
13328
|
+
function vsCodeUserDirs() {
|
|
13329
|
+
const home = os11.homedir();
|
|
13330
|
+
const dirs = [
|
|
13331
|
+
path18.join(home, "Library", "Application Support", "Code", "User"),
|
|
13332
|
+
path18.join(home, ".config", "Code", "User")
|
|
13333
|
+
];
|
|
13334
|
+
if (process.env.APPDATA) {
|
|
13335
|
+
dirs.push(path18.join(process.env.APPDATA, "Code", "User"));
|
|
13336
|
+
}
|
|
13337
|
+
return dirs;
|
|
13338
|
+
}
|
|
13339
|
+
function uriToFsPath(uri) {
|
|
13340
|
+
if (!uri.startsWith("file://")) return null;
|
|
13341
|
+
try {
|
|
13342
|
+
return fileURLToPath(uri);
|
|
13343
|
+
} catch {
|
|
13344
|
+
return null;
|
|
13345
|
+
}
|
|
13346
|
+
}
|
|
13347
|
+
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
13348
|
+
let raw;
|
|
13349
|
+
try {
|
|
13350
|
+
raw = await fs15.promises.readFile(workspaceJsonPath, "utf-8");
|
|
13351
|
+
} catch {
|
|
13352
|
+
return null;
|
|
13353
|
+
}
|
|
13354
|
+
let data;
|
|
13355
|
+
try {
|
|
13356
|
+
data = JSON.parse(raw);
|
|
13357
|
+
} catch {
|
|
13358
|
+
return null;
|
|
13359
|
+
}
|
|
13360
|
+
if (!data || typeof data !== "object") return null;
|
|
13361
|
+
const obj = data;
|
|
13362
|
+
if (typeof obj.folder === "string") {
|
|
13363
|
+
return uriToFsPath(obj.folder) ?? obj.folder;
|
|
13364
|
+
}
|
|
13365
|
+
return null;
|
|
13366
|
+
}
|
|
13367
|
+
var CopilotChatSource = class {
|
|
13368
|
+
name = "copilot-chat";
|
|
13369
|
+
async scan() {
|
|
13370
|
+
const results = [];
|
|
13371
|
+
for (const userDir of vsCodeUserDirs()) {
|
|
13372
|
+
const workspaceStorage = path18.join(userDir, "workspaceStorage");
|
|
13373
|
+
let hashDirs;
|
|
13374
|
+
try {
|
|
13375
|
+
hashDirs = await fs15.promises.readdir(workspaceStorage, {
|
|
13376
|
+
withFileTypes: true
|
|
13377
|
+
});
|
|
13378
|
+
} catch {
|
|
13379
|
+
continue;
|
|
13380
|
+
}
|
|
13381
|
+
for (const hash of hashDirs) {
|
|
13382
|
+
if (!hash.isDirectory()) continue;
|
|
13383
|
+
const wsRoot = path18.join(workspaceStorage, hash.name);
|
|
13384
|
+
const transcriptsDir = path18.join(
|
|
13385
|
+
wsRoot,
|
|
13386
|
+
"GitHub.copilot-chat",
|
|
13387
|
+
"transcripts"
|
|
13388
|
+
);
|
|
13389
|
+
let transcriptEntries;
|
|
13390
|
+
try {
|
|
13391
|
+
transcriptEntries = await fs15.promises.readdir(transcriptsDir, {
|
|
13392
|
+
withFileTypes: true
|
|
13393
|
+
});
|
|
13394
|
+
} catch {
|
|
13395
|
+
continue;
|
|
13396
|
+
}
|
|
13397
|
+
const repoPath = await readWorkspaceFolder(
|
|
13398
|
+
path18.join(wsRoot, "workspace.json")
|
|
13399
|
+
);
|
|
13400
|
+
if (!repoPath) continue;
|
|
13401
|
+
for (const entry of transcriptEntries) {
|
|
13402
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
13403
|
+
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
13404
|
+
results.push({
|
|
13405
|
+
sourceName: this.name,
|
|
13406
|
+
absolutePath: path18.join(transcriptsDir, entry.name),
|
|
13407
|
+
repoPath,
|
|
13408
|
+
metadata: { sessionId }
|
|
13409
|
+
});
|
|
13410
|
+
}
|
|
13411
|
+
}
|
|
13412
|
+
}
|
|
13413
|
+
return results;
|
|
13414
|
+
}
|
|
13415
|
+
};
|
|
13416
|
+
|
|
13177
13417
|
// src/sources/index.ts
|
|
13178
|
-
var sources = [
|
|
13418
|
+
var sources = [
|
|
13419
|
+
new ClaudeSource(),
|
|
13420
|
+
new CodexSource(),
|
|
13421
|
+
new CopilotChatSource()
|
|
13422
|
+
];
|
|
13179
13423
|
|
|
13180
13424
|
// src/cli.ts
|
|
13181
13425
|
function reportRedactionStats(noun, stats) {
|
|
@@ -13199,7 +13443,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
13199
13443
|
async function filterByTimeRange(group, range) {
|
|
13200
13444
|
const results = await Promise.all(
|
|
13201
13445
|
group.files.map(
|
|
13202
|
-
(f) =>
|
|
13446
|
+
(f) => fs16.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
13203
13447
|
)
|
|
13204
13448
|
);
|
|
13205
13449
|
const filtered = [];
|
|
@@ -13226,10 +13470,10 @@ async function runInteractive() {
|
|
|
13226
13470
|
s.start(`Scanning ${source.name} logs...`);
|
|
13227
13471
|
const allFiles = await source.scan();
|
|
13228
13472
|
const allGroups = await mergeByRepo(allFiles);
|
|
13229
|
-
const repoRoot =
|
|
13473
|
+
const repoRoot = path19.resolve(repo.root);
|
|
13230
13474
|
const matching = allGroups.filter((g) => {
|
|
13231
|
-
const resolved =
|
|
13232
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
13475
|
+
const resolved = path19.resolve(g.repoPath);
|
|
13476
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path19.sep);
|
|
13233
13477
|
});
|
|
13234
13478
|
if (matching.length === 0) {
|
|
13235
13479
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -13260,7 +13504,7 @@ async function runInteractive() {
|
|
|
13260
13504
|
}
|
|
13261
13505
|
}
|
|
13262
13506
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
13263
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
13507
|
+
const envFilePaths = envFileNames.map((n) => path19.join(repoRoot, n));
|
|
13264
13508
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
13265
13509
|
const secretResult = await collectSecrets(
|
|
13266
13510
|
repoRoot,
|