hillclimb 0.1.2 → 0.1.4
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/README.md +20 -2
- package/dist/cli.js +667 -187
- 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
|
|
@@ -146,6 +146,97 @@ async function clearIdentity(apiBaseUrl) {
|
|
|
146
146
|
return true;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// src/platform/error-format.ts
|
|
150
|
+
function formatError(err) {
|
|
151
|
+
if (!(err instanceof Error)) return String(err);
|
|
152
|
+
const parts = [err.message || err.name];
|
|
153
|
+
const status = err.status;
|
|
154
|
+
const code = err.code;
|
|
155
|
+
if (typeof status === "number") parts.push(`status=${status}`);
|
|
156
|
+
if (typeof code === "string" && code) parts.push(`code=${code}`);
|
|
157
|
+
if (err.cause !== void 0 && err.cause !== null) {
|
|
158
|
+
const causeMsg = err.cause instanceof Error ? err.cause.message : String(err.cause);
|
|
159
|
+
if (causeMsg) parts.push(`cause=${causeMsg}`);
|
|
160
|
+
}
|
|
161
|
+
return parts.join(" ");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/platform/log.ts
|
|
165
|
+
import fs3 from "fs";
|
|
166
|
+
import path4 from "path";
|
|
167
|
+
var PT_TIME_ZONE = "America/Los_Angeles";
|
|
168
|
+
function pacificParts(date) {
|
|
169
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
170
|
+
timeZone: PT_TIME_ZONE,
|
|
171
|
+
year: "numeric",
|
|
172
|
+
month: "2-digit",
|
|
173
|
+
day: "2-digit",
|
|
174
|
+
hour: "2-digit",
|
|
175
|
+
minute: "2-digit",
|
|
176
|
+
second: "2-digit",
|
|
177
|
+
hour12: false
|
|
178
|
+
}).formatToParts(date);
|
|
179
|
+
const lookup = {};
|
|
180
|
+
for (const p7 of parts) if (p7.type !== "literal") lookup[p7.type] = p7.value;
|
|
181
|
+
if (lookup.hour === "24") lookup.hour = "00";
|
|
182
|
+
return {
|
|
183
|
+
year: lookup.year ?? "0000",
|
|
184
|
+
month: lookup.month ?? "00",
|
|
185
|
+
day: lookup.day ?? "00",
|
|
186
|
+
hour: lookup.hour ?? "00",
|
|
187
|
+
minute: lookup.minute ?? "00",
|
|
188
|
+
second: lookup.second ?? "00"
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function pacificOffsetMinutes(date) {
|
|
192
|
+
const p7 = pacificParts(date);
|
|
193
|
+
const asIfUtc = Date.UTC(
|
|
194
|
+
Number(p7.year),
|
|
195
|
+
Number(p7.month) - 1,
|
|
196
|
+
Number(p7.day),
|
|
197
|
+
Number(p7.hour),
|
|
198
|
+
Number(p7.minute),
|
|
199
|
+
Number(p7.second)
|
|
200
|
+
);
|
|
201
|
+
return Math.round((asIfUtc - date.getTime()) / 6e4);
|
|
202
|
+
}
|
|
203
|
+
function pacificTimestamp(date) {
|
|
204
|
+
const p7 = pacificParts(date);
|
|
205
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
206
|
+
const off = pacificOffsetMinutes(date);
|
|
207
|
+
const sign = off >= 0 ? "+" : "-";
|
|
208
|
+
const abs = Math.abs(off);
|
|
209
|
+
const oh = String(Math.floor(abs / 60)).padStart(2, "0");
|
|
210
|
+
const om = String(abs % 60).padStart(2, "0");
|
|
211
|
+
return `${p7.year}-${p7.month}-${p7.day}T${p7.hour}:${p7.minute}:${p7.second}.${ms}${sign}${oh}:${om}`;
|
|
212
|
+
}
|
|
213
|
+
function pacificDateString(date) {
|
|
214
|
+
const p7 = pacificParts(date);
|
|
215
|
+
return `${p7.year}-${p7.month}-${p7.day}`;
|
|
216
|
+
}
|
|
217
|
+
function logsDir() {
|
|
218
|
+
return path4.join(configDir(), "logs");
|
|
219
|
+
}
|
|
220
|
+
function todayLogPath() {
|
|
221
|
+
return path4.join(logsDir(), `${pacificDateString(/* @__PURE__ */ new Date())}.log`);
|
|
222
|
+
}
|
|
223
|
+
var logPrefix = "";
|
|
224
|
+
function setLogPrefix(prefix) {
|
|
225
|
+
logPrefix = prefix;
|
|
226
|
+
}
|
|
227
|
+
function appendLog(level, message) {
|
|
228
|
+
const tag = logPrefix ? ` ${logPrefix}` : "";
|
|
229
|
+
const line = `[${pacificTimestamp(/* @__PURE__ */ new Date())}] [${level}]${tag} ${message}
|
|
230
|
+
`;
|
|
231
|
+
try {
|
|
232
|
+
fs3.mkdirSync(logsDir(), { recursive: true, mode: 448 });
|
|
233
|
+
fs3.appendFileSync(todayLogPath(), line);
|
|
234
|
+
} catch {
|
|
235
|
+
process.stderr.write(`hillclimb:${tag} ${level}: ${message}
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
149
240
|
// src/platform/client.ts
|
|
150
241
|
var PlatformError = class extends Error {
|
|
151
242
|
constructor(message, status, code) {
|
|
@@ -229,9 +320,21 @@ var PlatformClient = class {
|
|
|
229
320
|
if (body !== void 0) {
|
|
230
321
|
init.body = JSON.stringify(body);
|
|
231
322
|
}
|
|
232
|
-
const
|
|
323
|
+
const start = Date.now();
|
|
324
|
+
let res;
|
|
325
|
+
try {
|
|
326
|
+
res = await fetch(this.url(relative), init);
|
|
327
|
+
} catch (err) {
|
|
328
|
+
appendLog(
|
|
329
|
+
"error",
|
|
330
|
+
`[${method} ${relative}] FAILED ${Date.now() - start}ms ${formatError(err)}`
|
|
331
|
+
);
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
const elapsedMs = Date.now() - start;
|
|
233
335
|
this.mergeSetCookies(res);
|
|
234
336
|
if (!res.ok) {
|
|
337
|
+
appendLog("warn", `[${method} ${relative}] ${res.status} ${elapsedMs}ms`);
|
|
235
338
|
let code;
|
|
236
339
|
let message = `${method} ${relative} failed: HTTP ${res.status}`;
|
|
237
340
|
try {
|
|
@@ -253,6 +356,7 @@ var PlatformClient = class {
|
|
|
253
356
|
}
|
|
254
357
|
throw new PlatformError(message, res.status, code);
|
|
255
358
|
}
|
|
359
|
+
appendLog("info", `[${method} ${relative}] ${res.status} ${elapsedMs}ms`);
|
|
256
360
|
if (res.status === 204) {
|
|
257
361
|
return void 0;
|
|
258
362
|
}
|
|
@@ -337,12 +441,31 @@ var PlatformClient = class {
|
|
|
337
441
|
async uploadToPresignedUrl(presignedUrl, headers, body) {
|
|
338
442
|
const ab = new ArrayBuffer(body.byteLength);
|
|
339
443
|
new Uint8Array(ab).set(body);
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
444
|
+
const host = (() => {
|
|
445
|
+
try {
|
|
446
|
+
return new URL(presignedUrl).host;
|
|
447
|
+
} catch {
|
|
448
|
+
return "presigned-url";
|
|
449
|
+
}
|
|
450
|
+
})();
|
|
451
|
+
const start = Date.now();
|
|
452
|
+
let res;
|
|
453
|
+
try {
|
|
454
|
+
res = await fetch(presignedUrl, {
|
|
455
|
+
method: "PUT",
|
|
456
|
+
headers,
|
|
457
|
+
body: new Blob([ab])
|
|
458
|
+
});
|
|
459
|
+
} catch (err) {
|
|
460
|
+
appendLog(
|
|
461
|
+
"error",
|
|
462
|
+
`[PUT ${host}] FAILED ${Date.now() - start}ms ${formatError(err)}`
|
|
463
|
+
);
|
|
464
|
+
throw err;
|
|
465
|
+
}
|
|
466
|
+
const elapsedMs = Date.now() - start;
|
|
345
467
|
if (!res.ok) {
|
|
468
|
+
appendLog("warn", `[PUT ${host}] ${res.status} ${elapsedMs}ms`);
|
|
346
469
|
let detail = "";
|
|
347
470
|
try {
|
|
348
471
|
detail = await res.text();
|
|
@@ -353,6 +476,10 @@ var PlatformClient = class {
|
|
|
353
476
|
res.status
|
|
354
477
|
);
|
|
355
478
|
}
|
|
479
|
+
appendLog(
|
|
480
|
+
"info",
|
|
481
|
+
`[PUT ${host}] ${res.status} ${elapsedMs}ms (${body.byteLength} bytes)`
|
|
482
|
+
);
|
|
356
483
|
}
|
|
357
484
|
async submitContribution(contributionId) {
|
|
358
485
|
const data = await this.request(
|
|
@@ -377,9 +504,9 @@ var PlatformClient = class {
|
|
|
377
504
|
};
|
|
378
505
|
|
|
379
506
|
// src/platform/hooks.ts
|
|
380
|
-
import
|
|
507
|
+
import fs4 from "fs";
|
|
381
508
|
import os2 from "os";
|
|
382
|
-
import
|
|
509
|
+
import path5 from "path";
|
|
383
510
|
var HOOK_CMD = (sub) => `npx hillclimb ${sub}`;
|
|
384
511
|
var GIT_TRACES_CMD = (tool) => `npx hillclimb git-traces --tool=${tool}`;
|
|
385
512
|
var TOOLS = [
|
|
@@ -394,7 +521,7 @@ var TOOLS = [
|
|
|
394
521
|
{ eventName: "Stop", command: GIT_TRACES_CMD("claude") },
|
|
395
522
|
{ eventName: "SessionEnd", command: GIT_TRACES_CMD("claude") }
|
|
396
523
|
],
|
|
397
|
-
detect: () => isDir(
|
|
524
|
+
detect: () => isDir(path5.join(os2.homedir(), ".claude"))
|
|
398
525
|
},
|
|
399
526
|
{
|
|
400
527
|
tool: "cursor",
|
|
@@ -407,7 +534,7 @@ var TOOLS = [
|
|
|
407
534
|
{ eventName: "stop", command: GIT_TRACES_CMD("cursor") },
|
|
408
535
|
{ eventName: "sessionEnd", command: GIT_TRACES_CMD("cursor") }
|
|
409
536
|
],
|
|
410
|
-
detect: () => isDir(
|
|
537
|
+
detect: () => isDir(path5.join(os2.homedir(), ".cursor"))
|
|
411
538
|
},
|
|
412
539
|
{
|
|
413
540
|
// opencode has no settings-file hook system (the `experimental.hook` block
|
|
@@ -422,7 +549,28 @@ var TOOLS = [
|
|
|
422
549
|
format: "opencode",
|
|
423
550
|
events: [],
|
|
424
551
|
// Events are wired inside the plugin itself; see OPENCODE_PLUGIN_CONTENT.
|
|
425
|
-
detect: () => isDir(
|
|
552
|
+
detect: () => isDir(path5.join(os2.homedir(), ".local", "share", "opencode")) || isDir(path5.join(os2.homedir(), ".config", "opencode"))
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
// VS Code Copilot Chat. Same "no SessionEnd" constraint as Codex (the
|
|
556
|
+
// event is documented but never fires — VS Code issue microsoft/vscode#300650).
|
|
557
|
+
// Stale-state cleanup happens on next SessionStart, like Codex.
|
|
558
|
+
// Hook config lives at .github/hooks/hooks.json — distinct from
|
|
559
|
+
// .claude/settings.local.json so Claude Code and Copilot Chat don't
|
|
560
|
+
// fire each other's hooks even though VS Code's docs claim they share.
|
|
561
|
+
tool: "copilot-chat",
|
|
562
|
+
label: "GitHub Copilot Chat",
|
|
563
|
+
settingsFile: ".github/hooks/hooks.json",
|
|
564
|
+
format: "copilot",
|
|
565
|
+
events: [
|
|
566
|
+
{ eventName: "SessionStart", command: GIT_TRACES_CMD("copilot-chat") },
|
|
567
|
+
{ eventName: "Stop", command: GIT_TRACES_CMD("copilot-chat") },
|
|
568
|
+
{
|
|
569
|
+
eventName: "Stop",
|
|
570
|
+
command: `${HOOK_CMD("upload")} --tool=copilot-chat`
|
|
571
|
+
}
|
|
572
|
+
],
|
|
573
|
+
detect: copilotChatDetect
|
|
426
574
|
},
|
|
427
575
|
{
|
|
428
576
|
// Codex has no SessionEnd hook (only SessionStart, PreToolUse, PostToolUse,
|
|
@@ -441,22 +589,36 @@ var TOOLS = [
|
|
|
441
589
|
{ eventName: "SessionStart", command: GIT_TRACES_CMD("codex") },
|
|
442
590
|
{ eventName: "Stop", command: GIT_TRACES_CMD("codex") }
|
|
443
591
|
],
|
|
444
|
-
detect: () => isDir(
|
|
592
|
+
detect: () => isDir(path5.join(os2.homedir(), ".codex"))
|
|
445
593
|
}
|
|
446
594
|
];
|
|
447
595
|
function isDir(p7) {
|
|
448
596
|
try {
|
|
449
|
-
return
|
|
597
|
+
return fs4.statSync(p7).isDirectory();
|
|
450
598
|
} catch {
|
|
451
599
|
return false;
|
|
452
600
|
}
|
|
453
601
|
}
|
|
602
|
+
function copilotChatDetect() {
|
|
603
|
+
const home = os2.homedir();
|
|
604
|
+
const suffix = path5.join(
|
|
605
|
+
"User",
|
|
606
|
+
"globalStorage",
|
|
607
|
+
"github.copilot-chat"
|
|
608
|
+
);
|
|
609
|
+
const candidates = [
|
|
610
|
+
path5.join(home, "Library", "Application Support", "Code", suffix),
|
|
611
|
+
path5.join(home, ".config", "Code", suffix),
|
|
612
|
+
process.env.APPDATA ? path5.join(process.env.APPDATA, "Code", suffix) : null
|
|
613
|
+
].filter((p7) => p7 !== null);
|
|
614
|
+
return candidates.some(isDir);
|
|
615
|
+
}
|
|
454
616
|
function settingsPath(repoRoot, def) {
|
|
455
|
-
return
|
|
617
|
+
return path5.join(repoRoot, def.settingsFile);
|
|
456
618
|
}
|
|
457
619
|
async function readJson(file) {
|
|
458
620
|
try {
|
|
459
|
-
const raw = await
|
|
621
|
+
const raw = await fs4.promises.readFile(file, "utf-8");
|
|
460
622
|
const parsed = JSON.parse(raw);
|
|
461
623
|
if (parsed && typeof parsed === "object") return parsed;
|
|
462
624
|
return {};
|
|
@@ -466,8 +628,8 @@ async function readJson(file) {
|
|
|
466
628
|
}
|
|
467
629
|
}
|
|
468
630
|
async function writeJson(file, obj) {
|
|
469
|
-
await
|
|
470
|
-
await
|
|
631
|
+
await fs4.promises.mkdir(path5.dirname(file), { recursive: true });
|
|
632
|
+
await fs4.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
|
|
471
633
|
`);
|
|
472
634
|
}
|
|
473
635
|
function claudeHookPresent(matchers, command) {
|
|
@@ -545,11 +707,41 @@ function cursorUninstall(settings, eventName, command) {
|
|
|
545
707
|
}
|
|
546
708
|
return true;
|
|
547
709
|
}
|
|
710
|
+
function copilotHookPresent(entries, command) {
|
|
711
|
+
return entries.some((e) => e.type === "command" && e.command === command);
|
|
712
|
+
}
|
|
713
|
+
function copilotInstall(settings, eventName, command) {
|
|
714
|
+
if (!settings.hooks) settings.hooks = {};
|
|
715
|
+
if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
|
|
716
|
+
const entries = settings.hooks[eventName];
|
|
717
|
+
if (copilotHookPresent(entries, command)) return false;
|
|
718
|
+
entries.push({ type: "command", command });
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
function copilotCheck(settings, eventName, command) {
|
|
722
|
+
const entries = settings.hooks?.[eventName] ?? [];
|
|
723
|
+
return copilotHookPresent(entries, command);
|
|
724
|
+
}
|
|
725
|
+
function copilotUninstall(settings, eventName, command) {
|
|
726
|
+
const entries = settings.hooks?.[eventName];
|
|
727
|
+
if (!entries || entries.length === 0) return false;
|
|
728
|
+
const next = entries.filter(
|
|
729
|
+
(e) => !(e.type === "command" && e.command === command)
|
|
730
|
+
);
|
|
731
|
+
if (next.length === entries.length) return false;
|
|
732
|
+
if (next.length > 0) {
|
|
733
|
+
settings.hooks[eventName] = next;
|
|
734
|
+
} else {
|
|
735
|
+
delete settings.hooks[eventName];
|
|
736
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
737
|
+
}
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
548
740
|
var OPENCODE_PLUGIN_VERSION = 2;
|
|
549
741
|
var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
|
|
550
742
|
var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
|
|
551
|
-
// Auto-installed by \`npx hillclimb
|
|
552
|
-
//
|
|
743
|
+
// Auto-installed by \`npx hillclimb\`. Do not edit manually \u2014 re-running
|
|
744
|
+
// hillclimb will overwrite this file. See https://github.com/hillclimbai for context.
|
|
553
745
|
|
|
554
746
|
import { spawn } from "node:child_process";
|
|
555
747
|
import fs from "node:fs";
|
|
@@ -695,32 +887,53 @@ export default HillclimbPlugin;
|
|
|
695
887
|
`;
|
|
696
888
|
async function opencodeInstall(file) {
|
|
697
889
|
try {
|
|
698
|
-
const existing = await
|
|
890
|
+
const existing = await fs4.promises.readFile(file, "utf-8");
|
|
699
891
|
if (existing === OPENCODE_PLUGIN_CONTENT) {
|
|
700
892
|
return { installed: 0, alreadyPresent: 1 };
|
|
701
893
|
}
|
|
702
894
|
} catch {
|
|
703
895
|
}
|
|
704
|
-
await
|
|
705
|
-
await
|
|
896
|
+
await fs4.promises.mkdir(path5.dirname(file), { recursive: true });
|
|
897
|
+
await fs4.promises.writeFile(file, OPENCODE_PLUGIN_CONTENT);
|
|
706
898
|
return { installed: 1, alreadyPresent: 0 };
|
|
707
899
|
}
|
|
708
900
|
async function opencodeCheck(file) {
|
|
709
901
|
try {
|
|
710
|
-
const content = await
|
|
902
|
+
const content = await fs4.promises.readFile(file, "utf-8");
|
|
711
903
|
return content.includes(OPENCODE_PLUGIN_MARKER);
|
|
712
904
|
} catch {
|
|
713
905
|
return false;
|
|
714
906
|
}
|
|
715
907
|
}
|
|
716
908
|
function install(settings, format, eventName, command) {
|
|
717
|
-
|
|
909
|
+
switch (format) {
|
|
910
|
+
case "cursor":
|
|
911
|
+
return cursorInstall(settings, eventName, command);
|
|
912
|
+
case "copilot":
|
|
913
|
+
return copilotInstall(settings, eventName, command);
|
|
914
|
+
default:
|
|
915
|
+
return claudeInstall(settings, eventName, command);
|
|
916
|
+
}
|
|
718
917
|
}
|
|
719
918
|
function check(settings, format, eventName, command) {
|
|
720
|
-
|
|
919
|
+
switch (format) {
|
|
920
|
+
case "cursor":
|
|
921
|
+
return cursorCheck(settings, eventName, command);
|
|
922
|
+
case "copilot":
|
|
923
|
+
return copilotCheck(settings, eventName, command);
|
|
924
|
+
default:
|
|
925
|
+
return claudeCheck(settings, eventName, command);
|
|
926
|
+
}
|
|
721
927
|
}
|
|
722
928
|
function uninstall(settings, format, eventName, command) {
|
|
723
|
-
|
|
929
|
+
switch (format) {
|
|
930
|
+
case "cursor":
|
|
931
|
+
return cursorUninstall(settings, eventName, command);
|
|
932
|
+
case "copilot":
|
|
933
|
+
return copilotUninstall(settings, eventName, command);
|
|
934
|
+
default:
|
|
935
|
+
return claudeUninstall(settings, eventName, command);
|
|
936
|
+
}
|
|
724
937
|
}
|
|
725
938
|
function legacyCommandsFor(command) {
|
|
726
939
|
const prefix = "npx hillclimb ";
|
|
@@ -814,17 +1027,17 @@ async function checkAllHooks(repoRoot) {
|
|
|
814
1027
|
}
|
|
815
1028
|
return results;
|
|
816
1029
|
}
|
|
817
|
-
var CODEX_CONFIG_PATH =
|
|
1030
|
+
var CODEX_CONFIG_PATH = path5.join(os2.homedir(), ".codex", "config.toml");
|
|
818
1031
|
async function ensureCodexHooksEnabled() {
|
|
819
1032
|
let content;
|
|
820
1033
|
try {
|
|
821
|
-
content = await
|
|
1034
|
+
content = await fs4.promises.readFile(CODEX_CONFIG_PATH, "utf-8");
|
|
822
1035
|
} catch (err) {
|
|
823
1036
|
if (err.code === "ENOENT") {
|
|
824
|
-
await
|
|
1037
|
+
await fs4.promises.mkdir(path5.dirname(CODEX_CONFIG_PATH), {
|
|
825
1038
|
recursive: true
|
|
826
1039
|
});
|
|
827
|
-
await
|
|
1040
|
+
await fs4.promises.writeFile(
|
|
828
1041
|
CODEX_CONFIG_PATH,
|
|
829
1042
|
"[features]\ncodex_hooks = true\n"
|
|
830
1043
|
);
|
|
@@ -840,7 +1053,7 @@ async function ensureCodexHooksEnabled() {
|
|
|
840
1053
|
} else {
|
|
841
1054
|
content = content.trimEnd() + "\n\n[features]\ncodex_hooks = true\n";
|
|
842
1055
|
}
|
|
843
|
-
await
|
|
1056
|
+
await fs4.promises.writeFile(CODEX_CONFIG_PATH, content);
|
|
844
1057
|
return true;
|
|
845
1058
|
}
|
|
846
1059
|
var CLAUDE_DEF = TOOLS[0];
|
|
@@ -896,8 +1109,10 @@ function openBrowser(url) {
|
|
|
896
1109
|
}
|
|
897
1110
|
async function webAuth(apiBaseUrl) {
|
|
898
1111
|
if (!IS_TTY) {
|
|
1112
|
+
appendLog("info", "web-auth: skipped (not a TTY)");
|
|
899
1113
|
return null;
|
|
900
1114
|
}
|
|
1115
|
+
appendLog("info", `web-auth: started (apiBaseUrl=${apiBaseUrl})`);
|
|
901
1116
|
const client = new PlatformClient(apiBaseUrl);
|
|
902
1117
|
const codeResponse = await withSpinner(
|
|
903
1118
|
"Preparing web authentication...",
|
|
@@ -905,6 +1120,10 @@ async function webAuth(apiBaseUrl) {
|
|
|
905
1120
|
"Could not start web authentication.",
|
|
906
1121
|
() => client.createCliAuthCode()
|
|
907
1122
|
);
|
|
1123
|
+
appendLog(
|
|
1124
|
+
"info",
|
|
1125
|
+
`web-auth: code prepared (expiresIn=${codeResponse.expiresIn}s, pollInterval=${codeResponse.interval}s)`
|
|
1126
|
+
);
|
|
908
1127
|
const url = codeResponse.verificationUrl;
|
|
909
1128
|
openBrowser(url);
|
|
910
1129
|
p2.log.info(`Opening browser to: ${cyan(url)}`);
|
|
@@ -918,6 +1137,7 @@ async function webAuth(apiBaseUrl) {
|
|
|
918
1137
|
try {
|
|
919
1138
|
const poll = await client.pollCliAuthCode(codeResponse.code);
|
|
920
1139
|
if (poll.status === "approved" && poll.sessionToken) {
|
|
1140
|
+
appendLog("info", "web-auth: authorized");
|
|
921
1141
|
spinner5.stop("Authorized.");
|
|
922
1142
|
const secure = apiBaseUrl.startsWith("https://");
|
|
923
1143
|
const cookieName = secure ? "__Secure-better-auth.session_token" : "better-auth.session_token";
|
|
@@ -933,6 +1153,10 @@ async function webAuth(apiBaseUrl) {
|
|
|
933
1153
|
return data;
|
|
934
1154
|
}
|
|
935
1155
|
);
|
|
1156
|
+
appendLog(
|
|
1157
|
+
"info",
|
|
1158
|
+
`web-auth: completed (email=${bootstrap.session.email})`
|
|
1159
|
+
);
|
|
936
1160
|
return {
|
|
937
1161
|
client,
|
|
938
1162
|
sessionCookie,
|
|
@@ -941,13 +1165,16 @@ async function webAuth(apiBaseUrl) {
|
|
|
941
1165
|
};
|
|
942
1166
|
}
|
|
943
1167
|
if (poll.status === "expired") {
|
|
1168
|
+
appendLog("warn", "web-auth: code expired before authorization");
|
|
944
1169
|
spinner5.stop("Authorization expired.");
|
|
945
1170
|
p2.log.warn("The authorization code expired. Please try again.");
|
|
946
1171
|
return null;
|
|
947
1172
|
}
|
|
948
|
-
} catch {
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
appendLog("warn", `web-auth: poll attempt failed: ${formatError(err)}`);
|
|
949
1175
|
}
|
|
950
1176
|
}
|
|
1177
|
+
appendLog("warn", "web-auth: timed out waiting for authorization");
|
|
951
1178
|
spinner5.stop("Authorization timed out.");
|
|
952
1179
|
p2.log.warn("Timed out waiting for browser authorization.");
|
|
953
1180
|
return null;
|
|
@@ -1001,16 +1228,20 @@ async function tryReuseIdentity(apiBaseUrl, client, identity) {
|
|
|
1001
1228
|
async function runInit(args = []) {
|
|
1002
1229
|
const forceLogin = args.includes("--login");
|
|
1003
1230
|
const workspaceSlugFlag = parseStringFlag(args, "workspace");
|
|
1004
|
-
|
|
1231
|
+
appendLog(
|
|
1232
|
+
"info",
|
|
1233
|
+
`init: started (cwd=${process.cwd()}, forceLogin=${forceLogin}, workspaceSlug=${workspaceSlugFlag ?? "<none>"})`
|
|
1234
|
+
);
|
|
1235
|
+
p3.intro("hillclimb");
|
|
1005
1236
|
const repo = detectRepoRoot();
|
|
1006
1237
|
if (!repo) {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
);
|
|
1238
|
+
appendLog("error", "init: not a git repository, aborting");
|
|
1239
|
+
p3.log.error("Not a git repository. Run `npx hillclimb` inside a repo.");
|
|
1010
1240
|
p3.outro("Aborted.");
|
|
1011
1241
|
process.exit(1);
|
|
1012
1242
|
}
|
|
1013
1243
|
const { root: repoRoot, name: repoName } = repo;
|
|
1244
|
+
appendLog("info", `init: repo detected (root=${repoRoot}, name=${repoName})`);
|
|
1014
1245
|
p3.log.info(`Repo: ${repoName} (${repoRoot})`);
|
|
1015
1246
|
const apiBaseUrl = resolveDefaultApiUrl();
|
|
1016
1247
|
const saved = forceLogin ? null : await loadIdentity(apiBaseUrl);
|
|
@@ -1040,9 +1271,11 @@ async function runInit(args = []) {
|
|
|
1040
1271
|
}
|
|
1041
1272
|
method = selected;
|
|
1042
1273
|
}
|
|
1274
|
+
appendLog("info", `init: auth method selected (${method})`);
|
|
1043
1275
|
if (method === "web") {
|
|
1044
1276
|
const result = await webAuth(apiBaseUrl);
|
|
1045
1277
|
if (!result) {
|
|
1278
|
+
appendLog("error", "init: web authentication failed");
|
|
1046
1279
|
p3.log.error("Web authentication failed.");
|
|
1047
1280
|
p3.outro("Aborted.");
|
|
1048
1281
|
process.exit(1);
|
|
@@ -1107,6 +1340,10 @@ async function runInit(args = []) {
|
|
|
1107
1340
|
}
|
|
1108
1341
|
pickedId = resolved.organizationId;
|
|
1109
1342
|
workspace = resolved;
|
|
1343
|
+
appendLog(
|
|
1344
|
+
"info",
|
|
1345
|
+
`init: workspace pre-scoped (slug=${workspaceSlugFlag}, id=${resolved.id}, orgId=${pickedId})`
|
|
1346
|
+
);
|
|
1110
1347
|
const belongs = orgs.some((o) => o.id === pickedId);
|
|
1111
1348
|
if (!belongs && !isAdmin) {
|
|
1112
1349
|
p3.log.error(
|
|
@@ -1123,6 +1360,7 @@ async function runInit(args = []) {
|
|
|
1123
1360
|
await client.setActiveOrganization(pickedId);
|
|
1124
1361
|
}
|
|
1125
1362
|
} catch (err) {
|
|
1363
|
+
appendLog("error", `init: setActiveOrganization failed: ${formatError(err)}`);
|
|
1126
1364
|
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1127
1365
|
p3.outro("Aborted.");
|
|
1128
1366
|
process.exit(1);
|
|
@@ -1163,6 +1401,7 @@ async function runInit(args = []) {
|
|
|
1163
1401
|
await client.setActiveOrganization(pickedId);
|
|
1164
1402
|
}
|
|
1165
1403
|
} catch (err) {
|
|
1404
|
+
appendLog("error", `init: setActiveOrganization failed: ${formatError(err)}`);
|
|
1166
1405
|
p3.log.error(err instanceof Error ? err.message : String(err));
|
|
1167
1406
|
p3.outro("Aborted.");
|
|
1168
1407
|
process.exit(1);
|
|
@@ -1203,6 +1442,10 @@ async function runInit(args = []) {
|
|
|
1203
1442
|
process.exit(1);
|
|
1204
1443
|
}
|
|
1205
1444
|
workspace = picked;
|
|
1445
|
+
appendLog(
|
|
1446
|
+
"info",
|
|
1447
|
+
`init: workspace selected (slug=${workspace.slug}, id=${workspace.id}, orgId=${pickedId})`
|
|
1448
|
+
);
|
|
1206
1449
|
}
|
|
1207
1450
|
const types = await withSpinner(
|
|
1208
1451
|
"Loading contribution types...",
|
|
@@ -1253,6 +1496,10 @@ async function runInit(args = []) {
|
|
|
1253
1496
|
p3.outro("Aborted.");
|
|
1254
1497
|
process.exit(1);
|
|
1255
1498
|
}
|
|
1499
|
+
appendLog(
|
|
1500
|
+
"info",
|
|
1501
|
+
`init: contribution type selected (slug=${type.slug}, name=${type.name})`
|
|
1502
|
+
);
|
|
1256
1503
|
const pickedOrg = orgs.find((o) => o.id === activeOrgId);
|
|
1257
1504
|
await upsertProject(repoRoot, {
|
|
1258
1505
|
apiBaseUrl,
|
|
@@ -1265,6 +1512,7 @@ async function runInit(args = []) {
|
|
|
1265
1512
|
contributionTypeName: type.name,
|
|
1266
1513
|
autoSubmit: true
|
|
1267
1514
|
});
|
|
1515
|
+
appendLog("info", `init: project config saved (repoRoot=${repoRoot})`);
|
|
1268
1516
|
p3.log.success(`Saved config for ${repoRoot}`);
|
|
1269
1517
|
await saveIdentity({
|
|
1270
1518
|
apiBaseUrl,
|
|
@@ -1276,10 +1524,19 @@ async function runInit(args = []) {
|
|
|
1276
1524
|
try {
|
|
1277
1525
|
const hookResults = await installDetectedHooks(repoRoot);
|
|
1278
1526
|
if (hookResults.length === 0) {
|
|
1527
|
+
appendLog(
|
|
1528
|
+
"warn",
|
|
1529
|
+
"init: no supported tools detected, hooks not installed"
|
|
1530
|
+
);
|
|
1279
1531
|
p3.log.warn(
|
|
1280
|
-
"No supported tools detected (
|
|
1532
|
+
"No supported tools detected (Claude Code, Cursor, Codex, opencode, GitHub Copilot Chat). Hook not installed."
|
|
1281
1533
|
);
|
|
1282
1534
|
} else {
|
|
1535
|
+
const tools = hookResults.map((r) => `${r.tool}${r.alreadyPresent ? "(existing)" : ""}`).join(",");
|
|
1536
|
+
appendLog(
|
|
1537
|
+
"info",
|
|
1538
|
+
`init: hooks installed (count=${hookResults.length}, tools=${tools})`
|
|
1539
|
+
);
|
|
1283
1540
|
for (const r of hookResults) {
|
|
1284
1541
|
if (r.alreadyPresent) {
|
|
1285
1542
|
p3.log.info(`${r.label} hook already present`);
|
|
@@ -1291,9 +1548,17 @@ async function runInit(args = []) {
|
|
|
1291
1548
|
try {
|
|
1292
1549
|
const changed = await ensureCodexHooksEnabled();
|
|
1293
1550
|
if (changed) {
|
|
1551
|
+
appendLog(
|
|
1552
|
+
"info",
|
|
1553
|
+
"init: enabled codex_hooks in ~/.codex/config.toml"
|
|
1554
|
+
);
|
|
1294
1555
|
p3.log.success("Enabled codex_hooks in ~/.codex/config.toml");
|
|
1295
1556
|
}
|
|
1296
1557
|
} catch (err) {
|
|
1558
|
+
appendLog(
|
|
1559
|
+
"warn",
|
|
1560
|
+
`init: could not enable codex_hooks: ${formatError(err)}`
|
|
1561
|
+
);
|
|
1297
1562
|
p3.log.warn(
|
|
1298
1563
|
`Could not enable codex_hooks in config.toml: ${err instanceof Error ? err.message : String(err)}`
|
|
1299
1564
|
);
|
|
@@ -1301,12 +1566,17 @@ async function runInit(args = []) {
|
|
|
1301
1566
|
}
|
|
1302
1567
|
}
|
|
1303
1568
|
} catch (err) {
|
|
1569
|
+
appendLog("error", `init: failed to install hooks: ${formatError(err)}`);
|
|
1304
1570
|
p3.log.error(
|
|
1305
1571
|
`Failed to install hooks: ${err instanceof Error ? err.message : String(err)}`
|
|
1306
1572
|
);
|
|
1307
1573
|
p3.outro("Partially configured. Re-run after fixing the error.");
|
|
1308
1574
|
process.exit(1);
|
|
1309
1575
|
}
|
|
1576
|
+
appendLog(
|
|
1577
|
+
"info",
|
|
1578
|
+
`init: completed (workspace=${workspace.slug}, contributionType=${type.slug})`
|
|
1579
|
+
);
|
|
1310
1580
|
p3.outro(
|
|
1311
1581
|
`Done. Your next coding session in this repo will upload automatically to ${workspace.name}.`
|
|
1312
1582
|
);
|
|
@@ -1347,6 +1617,7 @@ function resolveDefaultApiUrl2() {
|
|
|
1347
1617
|
async function runLogin(_args = []) {
|
|
1348
1618
|
const apiBaseUrl = resolveDefaultApiUrl2();
|
|
1349
1619
|
header("login");
|
|
1620
|
+
appendLog("info", `login: started (apiBaseUrl=${apiBaseUrl})`);
|
|
1350
1621
|
const saved = await loadIdentity(apiBaseUrl);
|
|
1351
1622
|
if (saved) {
|
|
1352
1623
|
if (IS_TTY) {
|
|
@@ -1377,6 +1648,10 @@ async function runLogin(_args = []) {
|
|
|
1377
1648
|
process.stdout.write("\r\x1B[K");
|
|
1378
1649
|
}
|
|
1379
1650
|
if (verified) {
|
|
1651
|
+
appendLog(
|
|
1652
|
+
"info",
|
|
1653
|
+
`login: reused saved login (email=${verified.email})`
|
|
1654
|
+
);
|
|
1380
1655
|
row(CHECK, "Signed in", cyan(verified.email));
|
|
1381
1656
|
const fresh = probe.getSessionCookie();
|
|
1382
1657
|
if (fresh) {
|
|
@@ -1395,10 +1670,15 @@ async function runLogin(_args = []) {
|
|
|
1395
1670
|
return;
|
|
1396
1671
|
}
|
|
1397
1672
|
if (expired) {
|
|
1673
|
+
appendLog("info", "login: saved login expired, falling through");
|
|
1398
1674
|
row(CROSS, "Signed in", dim("expired \u2014 please sign in again"));
|
|
1399
1675
|
println();
|
|
1400
1676
|
await clearIdentity(apiBaseUrl);
|
|
1401
1677
|
} else if (verifyError) {
|
|
1678
|
+
appendLog(
|
|
1679
|
+
"warn",
|
|
1680
|
+
`login: could not verify saved login: ${formatError(verifyError)}`
|
|
1681
|
+
);
|
|
1402
1682
|
row(CROSS, "Signed in", dim("could not verify saved login"));
|
|
1403
1683
|
println(` ${dim(verifyError.message)}`);
|
|
1404
1684
|
println();
|
|
@@ -1424,9 +1704,11 @@ async function runLogin(_args = []) {
|
|
|
1424
1704
|
}
|
|
1425
1705
|
method = selected;
|
|
1426
1706
|
}
|
|
1707
|
+
appendLog("info", `login: auth method selected (${method})`);
|
|
1427
1708
|
if (method === "web") {
|
|
1428
1709
|
const result = await webAuth(apiBaseUrl);
|
|
1429
1710
|
if (!result) {
|
|
1711
|
+
appendLog("error", "login: web authentication failed");
|
|
1430
1712
|
p4.log.error("Web authentication failed.");
|
|
1431
1713
|
p4.outro("Aborted.");
|
|
1432
1714
|
process.exit(1);
|
|
@@ -1438,9 +1720,10 @@ async function runLogin(_args = []) {
|
|
|
1438
1720
|
userId: result.userId,
|
|
1439
1721
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1440
1722
|
});
|
|
1723
|
+
appendLog("info", `login: completed via web auth (email=${result.email})`);
|
|
1441
1724
|
println();
|
|
1442
1725
|
row(CHECK, "Signed in", cyan(result.email));
|
|
1443
|
-
footer("Run `npx hillclimb
|
|
1726
|
+
footer("Run `npx hillclimb` in a repo to wire up auto-upload.");
|
|
1444
1727
|
return;
|
|
1445
1728
|
}
|
|
1446
1729
|
const email = await requireText("Email");
|
|
@@ -1464,6 +1747,7 @@ async function runLogin(_args = []) {
|
|
|
1464
1747
|
);
|
|
1465
1748
|
const cookie = client.getSessionCookie();
|
|
1466
1749
|
if (!cookie) {
|
|
1750
|
+
appendLog("error", "login: session cookie missing after sign-in");
|
|
1467
1751
|
p4.log.error("Session cookie was lost during sign-in.");
|
|
1468
1752
|
p4.outro("Aborted.");
|
|
1469
1753
|
process.exit(1);
|
|
@@ -1475,9 +1759,13 @@ async function runLogin(_args = []) {
|
|
|
1475
1759
|
userId: bootstrap.session.userId,
|
|
1476
1760
|
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1477
1761
|
});
|
|
1762
|
+
appendLog(
|
|
1763
|
+
"info",
|
|
1764
|
+
`login: completed via email/password (email=${bootstrap.session.email})`
|
|
1765
|
+
);
|
|
1478
1766
|
println();
|
|
1479
1767
|
row(CHECK, "Signed in", cyan(bootstrap.session.email));
|
|
1480
|
-
footer("Run `npx hillclimb
|
|
1768
|
+
footer("Run `npx hillclimb` in a repo to wire up auto-upload.");
|
|
1481
1769
|
}
|
|
1482
1770
|
|
|
1483
1771
|
// src/commands/logout.ts
|
|
@@ -1509,39 +1797,12 @@ async function runLogout(args = []) {
|
|
|
1509
1797
|
);
|
|
1510
1798
|
footer(
|
|
1511
1799
|
"Per-repo upload hooks still active.",
|
|
1512
|
-
"Re-run `npx hillclimb
|
|
1800
|
+
"Re-run `npx hillclimb` to re-authenticate a repo."
|
|
1513
1801
|
);
|
|
1514
1802
|
}
|
|
1515
1803
|
|
|
1516
1804
|
// src/commands/status.ts
|
|
1517
1805
|
import path6 from "path";
|
|
1518
|
-
|
|
1519
|
-
// src/platform/log.ts
|
|
1520
|
-
import fs4 from "fs";
|
|
1521
|
-
import path5 from "path";
|
|
1522
|
-
function today() {
|
|
1523
|
-
const d = /* @__PURE__ */ new Date();
|
|
1524
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1525
|
-
}
|
|
1526
|
-
function logsDir() {
|
|
1527
|
-
return path5.join(configDir(), "logs");
|
|
1528
|
-
}
|
|
1529
|
-
function todayLogPath() {
|
|
1530
|
-
return path5.join(logsDir(), `${today()}.log`);
|
|
1531
|
-
}
|
|
1532
|
-
function appendLog(level, message) {
|
|
1533
|
-
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${message}
|
|
1534
|
-
`;
|
|
1535
|
-
try {
|
|
1536
|
-
fs4.mkdirSync(logsDir(), { recursive: true, mode: 448 });
|
|
1537
|
-
fs4.appendFileSync(todayLogPath(), line);
|
|
1538
|
-
} catch {
|
|
1539
|
-
process.stderr.write(`hillclimb: ${level}: ${message}
|
|
1540
|
-
`);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
// src/commands/status.ts
|
|
1545
1806
|
async function runStatus(args = []) {
|
|
1546
1807
|
const debug = args.includes("--debug");
|
|
1547
1808
|
header("status");
|
|
@@ -1555,7 +1816,7 @@ async function runStatus(args = []) {
|
|
|
1555
1816
|
return;
|
|
1556
1817
|
}
|
|
1557
1818
|
row(CROSS, "Repo", `${bold(repo.name)} ${dim("(not configured)")}`);
|
|
1558
|
-
footer(`Run \`npx hillclimb
|
|
1819
|
+
footer(`Run \`npx hillclimb\` in ${repo.name} to connect it.`);
|
|
1559
1820
|
return;
|
|
1560
1821
|
}
|
|
1561
1822
|
const { repoRoot, config } = match;
|
|
@@ -1617,11 +1878,11 @@ async function runStatus(args = []) {
|
|
|
1617
1878
|
row(CROSS, "Auto-upload", dim("error checking hooks"));
|
|
1618
1879
|
}
|
|
1619
1880
|
if (loginError === "expired") {
|
|
1620
|
-
footer("Run `npx hillclimb
|
|
1881
|
+
footer("Run `npx hillclimb` to sign in again.");
|
|
1621
1882
|
} else if (loginError === "unreachable") {
|
|
1622
1883
|
footer(`Couldn't reach ${config.apiBaseUrl}. Check your connection.`);
|
|
1623
1884
|
} else if (!anyHookInstalled) {
|
|
1624
|
-
footer("Run `npx hillclimb
|
|
1885
|
+
footer("Run `npx hillclimb` to install the upload hook.");
|
|
1625
1886
|
} else {
|
|
1626
1887
|
footer("All set.");
|
|
1627
1888
|
}
|
|
@@ -11464,6 +11725,9 @@ function formatTitleTimestamp(date) {
|
|
|
11464
11725
|
const pad = (n) => String(n).padStart(2, "0");
|
|
11465
11726
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
11466
11727
|
}
|
|
11728
|
+
function lineHasAssistant(line) {
|
|
11729
|
+
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
11730
|
+
}
|
|
11467
11731
|
async function hasAssistantMessage(transcriptPath) {
|
|
11468
11732
|
const stream = fs8.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
11469
11733
|
let buffer = "";
|
|
@@ -11474,15 +11738,14 @@ async function hasAssistantMessage(transcriptPath) {
|
|
|
11474
11738
|
while (newlineIdx !== -1) {
|
|
11475
11739
|
const line = buffer.slice(0, newlineIdx);
|
|
11476
11740
|
buffer = buffer.slice(newlineIdx + 1);
|
|
11477
|
-
if (
|
|
11741
|
+
if (lineHasAssistant(line)) {
|
|
11478
11742
|
stream.destroy();
|
|
11479
11743
|
return true;
|
|
11480
11744
|
}
|
|
11481
11745
|
newlineIdx = buffer.indexOf("\n");
|
|
11482
11746
|
}
|
|
11483
11747
|
}
|
|
11484
|
-
if (buffer
|
|
11485
|
-
return true;
|
|
11748
|
+
if (lineHasAssistant(buffer)) return true;
|
|
11486
11749
|
} catch {
|
|
11487
11750
|
return true;
|
|
11488
11751
|
}
|
|
@@ -11546,7 +11809,7 @@ async function runUploadInner(payload) {
|
|
|
11546
11809
|
if (!match) {
|
|
11547
11810
|
appendLog(
|
|
11548
11811
|
"warn",
|
|
11549
|
-
`Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb
|
|
11812
|
+
`Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
|
|
11550
11813
|
);
|
|
11551
11814
|
return;
|
|
11552
11815
|
}
|
|
@@ -11600,7 +11863,13 @@ async function uploadSession(args) {
|
|
|
11600
11863
|
}
|
|
11601
11864
|
const client = new PlatformClient(config.apiBaseUrl, identity.sessionCookie);
|
|
11602
11865
|
const shortId = sessionId.slice(0, 12);
|
|
11603
|
-
const toolLabels = {
|
|
11866
|
+
const toolLabels = {
|
|
11867
|
+
cursor: "Cursor",
|
|
11868
|
+
codex: "Codex",
|
|
11869
|
+
claude: "Claude",
|
|
11870
|
+
"copilot-chat": "GitHub Copilot Chat",
|
|
11871
|
+
opencode: "opencode"
|
|
11872
|
+
};
|
|
11604
11873
|
const toolLabel = toolLabels[sourceTool] ?? "Claude";
|
|
11605
11874
|
const title = `${toolLabel} session ${shortId} \u2014 ${formatTitleTimestamp(now)}`;
|
|
11606
11875
|
const body = `Session ID: ${sessionId}
|
|
@@ -11630,7 +11899,7 @@ Uploaded: ${now.toISOString()}`;
|
|
|
11630
11899
|
if (err instanceof PlatformError && err.status === 401) {
|
|
11631
11900
|
appendLog(
|
|
11632
11901
|
"error",
|
|
11633
|
-
`Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb
|
|
11902
|
+
`Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
|
|
11634
11903
|
);
|
|
11635
11904
|
return;
|
|
11636
11905
|
}
|
|
@@ -11641,6 +11910,15 @@ Uploaded: ${now.toISOString()}`;
|
|
|
11641
11910
|
}
|
|
11642
11911
|
}
|
|
11643
11912
|
var WORKER_ENV_FLAG = "HILLCLIMB_UPLOAD_WORKER";
|
|
11913
|
+
var TOOL_ENV_FLAG = "HILLCLIMB_UPLOAD_TOOL";
|
|
11914
|
+
function parseToolArg(argv) {
|
|
11915
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11916
|
+
const a = argv[i];
|
|
11917
|
+
if (a.startsWith("--tool=")) return a.slice("--tool=".length);
|
|
11918
|
+
if (a === "--tool" && i + 1 < argv.length) return argv[i + 1];
|
|
11919
|
+
}
|
|
11920
|
+
return null;
|
|
11921
|
+
}
|
|
11644
11922
|
async function runUpload() {
|
|
11645
11923
|
if (process.env[WORKER_ENV_FLAG] === "1") {
|
|
11646
11924
|
await runUploadWorker();
|
|
@@ -11669,11 +11947,17 @@ async function runUpload() {
|
|
|
11669
11947
|
appendLog("error", "Cannot detach worker: process.argv[1] is empty");
|
|
11670
11948
|
return;
|
|
11671
11949
|
}
|
|
11950
|
+
const toolArg = parseToolArg(process.argv.slice(2));
|
|
11951
|
+
const workerEnv = {
|
|
11952
|
+
...process.env,
|
|
11953
|
+
[WORKER_ENV_FLAG]: "1"
|
|
11954
|
+
};
|
|
11955
|
+
if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
|
|
11672
11956
|
try {
|
|
11673
11957
|
const child = spawn2(process.execPath, [entrypoint, "upload"], {
|
|
11674
11958
|
detached: true,
|
|
11675
11959
|
stdio: ["pipe", "ignore", "ignore"],
|
|
11676
|
-
env:
|
|
11960
|
+
env: workerEnv
|
|
11677
11961
|
});
|
|
11678
11962
|
child.on("error", (err) => {
|
|
11679
11963
|
appendLog("error", `Failed to spawn worker: ${err.message}`);
|
|
@@ -11719,6 +12003,8 @@ async function runUploadWorker() {
|
|
|
11719
12003
|
);
|
|
11720
12004
|
return;
|
|
11721
12005
|
}
|
|
12006
|
+
const toolOverride = process.env[TOOL_ENV_FLAG];
|
|
12007
|
+
if (toolOverride) payload.tool = toolOverride;
|
|
11722
12008
|
try {
|
|
11723
12009
|
await runUploadInner(payload);
|
|
11724
12010
|
} catch (err) {
|
|
@@ -11731,6 +12017,7 @@ async function runUploadWorker() {
|
|
|
11731
12017
|
|
|
11732
12018
|
// src/git-traces/index.ts
|
|
11733
12019
|
import { spawn as spawn3 } from "child_process";
|
|
12020
|
+
import crypto2 from "crypto";
|
|
11734
12021
|
|
|
11735
12022
|
// src/git-traces/handlers.ts
|
|
11736
12023
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -11741,23 +12028,74 @@ import fs9 from "fs";
|
|
|
11741
12028
|
import os6 from "os";
|
|
11742
12029
|
import path12 from "path";
|
|
11743
12030
|
import { gzipSync } from "zlib";
|
|
11744
|
-
var
|
|
12031
|
+
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
12032
|
+
var EXEC_OPTS = {
|
|
12033
|
+
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
12034
|
+
maxBuffer: 50 * 1024 * 1024
|
|
12035
|
+
};
|
|
12036
|
+
var MAX_ERROR_OUTPUT_CHARS = 2e3;
|
|
11745
12037
|
var MAX_UNTRACKED_FILE_BYTES = 10 * 1024 * 1024;
|
|
11746
12038
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
12039
|
+
function quoteGitArg(arg) {
|
|
12040
|
+
if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
|
|
12041
|
+
return JSON.stringify(arg);
|
|
12042
|
+
}
|
|
12043
|
+
function formatGitCommand(args) {
|
|
12044
|
+
return ["git", ...args.map(quoteGitArg)].join(" ");
|
|
12045
|
+
}
|
|
12046
|
+
function outputToString(output) {
|
|
12047
|
+
if (Buffer.isBuffer(output)) return output.toString("utf-8").trim();
|
|
12048
|
+
if (typeof output === "string") return output.trim();
|
|
12049
|
+
return null;
|
|
12050
|
+
}
|
|
12051
|
+
function truncateOutput(output) {
|
|
12052
|
+
if (output.length <= MAX_ERROR_OUTPUT_CHARS) return output;
|
|
12053
|
+
return `${output.slice(0, MAX_ERROR_OUTPUT_CHARS)}...`;
|
|
12054
|
+
}
|
|
12055
|
+
function formatGitFailure(command, failure, err) {
|
|
12056
|
+
const details = [];
|
|
12057
|
+
if (typeof failure.status === "number")
|
|
12058
|
+
details.push(`exit=${failure.status}`);
|
|
12059
|
+
if (failure.signal) details.push(`signal=${failure.signal}`);
|
|
12060
|
+
const stderr = outputToString(failure.stderr);
|
|
12061
|
+
if (stderr) details.push(`stderr=${truncateOutput(stderr)}`);
|
|
12062
|
+
const stdout = outputToString(failure.stdout);
|
|
12063
|
+
if (!stderr && stdout) details.push(`stdout=${truncateOutput(stdout)}`);
|
|
12064
|
+
if (details.length === 0 && err instanceof Error && err.message) {
|
|
12065
|
+
details.push(err.message);
|
|
12066
|
+
}
|
|
12067
|
+
return `${command} failed${details.length ? ` (${details.join("; ")})` : ""}`;
|
|
12068
|
+
}
|
|
12069
|
+
function createGitError(args, err) {
|
|
12070
|
+
const failure = err;
|
|
12071
|
+
const command = formatGitCommand(args);
|
|
12072
|
+
const isTimeout = failure.code === "ETIMEDOUT";
|
|
12073
|
+
const message = isTimeout ? `${command} timed out after ${GIT_COMMAND_TIMEOUT_MS}ms` : formatGitFailure(command, failure, err);
|
|
12074
|
+
const wrapped = new Error(message);
|
|
12075
|
+
wrapped.isGitTimeout = isTimeout;
|
|
12076
|
+
return wrapped;
|
|
12077
|
+
}
|
|
12078
|
+
function isGitTimeoutError(err) {
|
|
12079
|
+
return err instanceof Error && err.isGitTimeout === true;
|
|
12080
|
+
}
|
|
12081
|
+
function gitBuffer(repoRoot, args, options = {}) {
|
|
12082
|
+
try {
|
|
12083
|
+
return execFileSync("git", args, {
|
|
12084
|
+
cwd: repoRoot,
|
|
12085
|
+
...options.env ? { env: options.env } : {},
|
|
12086
|
+
...options.input !== void 0 ? { input: options.input } : {},
|
|
12087
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
12088
|
+
...EXEC_OPTS
|
|
12089
|
+
});
|
|
12090
|
+
} catch (err) {
|
|
12091
|
+
throw createGitError(args, err);
|
|
12092
|
+
}
|
|
12093
|
+
}
|
|
11747
12094
|
function git(repoRoot, args) {
|
|
11748
|
-
return
|
|
11749
|
-
cwd: repoRoot,
|
|
11750
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
11751
|
-
...EXEC_OPTS
|
|
11752
|
-
}).toString("utf-8").trim();
|
|
12095
|
+
return gitBuffer(repoRoot, args).toString("utf-8").trim();
|
|
11753
12096
|
}
|
|
11754
12097
|
function gitWithEnv(repoRoot, args, env) {
|
|
11755
|
-
return
|
|
11756
|
-
cwd: repoRoot,
|
|
11757
|
-
env,
|
|
11758
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
11759
|
-
...EXEC_OPTS
|
|
11760
|
-
}).toString("utf-8").trim();
|
|
12098
|
+
return gitBuffer(repoRoot, args, { env }).toString("utf-8").trim();
|
|
11761
12099
|
}
|
|
11762
12100
|
function isGitRepo(dir) {
|
|
11763
12101
|
try {
|
|
@@ -11771,11 +12109,13 @@ function captureBaselineSha(repoRoot) {
|
|
|
11771
12109
|
try {
|
|
11772
12110
|
const sha = git(repoRoot, ["stash", "create"]);
|
|
11773
12111
|
if (sha) return sha;
|
|
11774
|
-
} catch {
|
|
12112
|
+
} catch (err) {
|
|
12113
|
+
if (isGitTimeoutError(err)) throw err;
|
|
11775
12114
|
}
|
|
11776
12115
|
try {
|
|
11777
12116
|
return git(repoRoot, ["rev-parse", "HEAD"]);
|
|
11778
|
-
} catch {
|
|
12117
|
+
} catch (err) {
|
|
12118
|
+
if (isGitTimeoutError(err)) throw err;
|
|
11779
12119
|
const tree = git(repoRoot, ["write-tree"]);
|
|
11780
12120
|
return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
|
|
11781
12121
|
}
|
|
@@ -11793,7 +12133,8 @@ function captureSnapshotSha(repoRoot) {
|
|
|
11793
12133
|
try {
|
|
11794
12134
|
const sha = git(repoRoot, ["stash", "create"]);
|
|
11795
12135
|
if (sha) return sha;
|
|
11796
|
-
} catch {
|
|
12136
|
+
} catch (err) {
|
|
12137
|
+
if (isGitTimeoutError(err)) throw err;
|
|
11797
12138
|
}
|
|
11798
12139
|
return git(repoRoot, ["rev-parse", "HEAD"]);
|
|
11799
12140
|
}
|
|
@@ -11828,12 +12169,9 @@ function buildUntrackedTree(repoRoot) {
|
|
|
11828
12169
|
);
|
|
11829
12170
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
11830
12171
|
try {
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
env
|
|
11834
|
-
input: kept.join("\0") + "\0",
|
|
11835
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
11836
|
-
...EXEC_OPTS
|
|
12172
|
+
gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
|
|
12173
|
+
input: `${kept.join("\0")}\0`,
|
|
12174
|
+
env
|
|
11837
12175
|
});
|
|
11838
12176
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
11839
12177
|
} finally {
|
|
@@ -11856,10 +12194,12 @@ function buildSnapshotTree(repoRoot, stashSha) {
|
|
|
11856
12194
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
11857
12195
|
try {
|
|
11858
12196
|
gitWithEnv(repoRoot, ["read-tree", trackedTree], env);
|
|
11859
|
-
const untrackedList =
|
|
11860
|
-
|
|
12197
|
+
const untrackedList = gitBuffer(
|
|
12198
|
+
repoRoot,
|
|
11861
12199
|
["ls-tree", "-r", untrackedTree],
|
|
11862
|
-
{
|
|
12200
|
+
{
|
|
12201
|
+
env
|
|
12202
|
+
}
|
|
11863
12203
|
).toString("utf-8");
|
|
11864
12204
|
const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
|
|
11865
12205
|
const [meta, filePath] = line.split(" ");
|
|
@@ -11867,12 +12207,9 @@ function buildSnapshotTree(repoRoot, stashSha) {
|
|
|
11867
12207
|
return `${mode} ${sha} ${filePath}`;
|
|
11868
12208
|
}).join("\n");
|
|
11869
12209
|
if (indexInfo) {
|
|
11870
|
-
|
|
11871
|
-
cwd: repoRoot,
|
|
11872
|
-
env,
|
|
12210
|
+
gitBuffer(repoRoot, ["update-index", "--index-info"], {
|
|
11873
12211
|
input: indexInfo,
|
|
11874
|
-
|
|
11875
|
-
...EXEC_OPTS
|
|
12212
|
+
env
|
|
11876
12213
|
});
|
|
11877
12214
|
}
|
|
11878
12215
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
@@ -11909,15 +12246,12 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
11909
12246
|
}
|
|
11910
12247
|
function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha) {
|
|
11911
12248
|
if (fromTreeSha === toTreeSha) return null;
|
|
11912
|
-
const diff =
|
|
11913
|
-
"
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
...EXEC_OPTS
|
|
11919
|
-
}
|
|
11920
|
-
);
|
|
12249
|
+
const diff = gitBuffer(repoRoot, [
|
|
12250
|
+
"diff",
|
|
12251
|
+
"--binary",
|
|
12252
|
+
fromTreeSha,
|
|
12253
|
+
toTreeSha
|
|
12254
|
+
]);
|
|
11921
12255
|
if (diff.length === 0) return null;
|
|
11922
12256
|
return Buffer.from(gzipSync(diff));
|
|
11923
12257
|
}
|
|
@@ -11976,7 +12310,11 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
11976
12310
|
function enumerateCommits(repoRoot, fromSha, toSha) {
|
|
11977
12311
|
let shas;
|
|
11978
12312
|
try {
|
|
11979
|
-
const out = git(repoRoot, [
|
|
12313
|
+
const out = git(repoRoot, [
|
|
12314
|
+
"rev-list",
|
|
12315
|
+
"--reverse",
|
|
12316
|
+
`${fromSha}..${toSha}`
|
|
12317
|
+
]);
|
|
11980
12318
|
if (!out) return [];
|
|
11981
12319
|
shas = out.split("\n").filter(Boolean);
|
|
11982
12320
|
} catch (err) {
|
|
@@ -12028,12 +12366,7 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
12028
12366
|
let nameStatus = "";
|
|
12029
12367
|
let numstat = "";
|
|
12030
12368
|
try {
|
|
12031
|
-
nameStatus = git(repoRoot, [
|
|
12032
|
-
"show",
|
|
12033
|
-
"--name-status",
|
|
12034
|
-
"--format=",
|
|
12035
|
-
sha
|
|
12036
|
-
]);
|
|
12369
|
+
nameStatus = git(repoRoot, ["show", "--name-status", "--format=", sha]);
|
|
12037
12370
|
numstat = git(repoRoot, ["show", "--numstat", "--format=", sha]);
|
|
12038
12371
|
} catch {
|
|
12039
12372
|
return [];
|
|
@@ -12058,9 +12391,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
12058
12391
|
oldPath
|
|
12059
12392
|
});
|
|
12060
12393
|
} else {
|
|
12061
|
-
const
|
|
12062
|
-
indexByPath.set(
|
|
12063
|
-
files.push({ path:
|
|
12394
|
+
const path20 = parts[parts.length - 1];
|
|
12395
|
+
indexByPath.set(path20, files.length);
|
|
12396
|
+
files.push({ path: path20, status, additions: 0, deletions: 0 });
|
|
12064
12397
|
}
|
|
12065
12398
|
}
|
|
12066
12399
|
for (const line of numstat.split("\n")) {
|
|
@@ -12081,7 +12414,13 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
12081
12414
|
function safeGit(repoRoot, args) {
|
|
12082
12415
|
try {
|
|
12083
12416
|
return git(repoRoot, args);
|
|
12084
|
-
} catch {
|
|
12417
|
+
} catch (err) {
|
|
12418
|
+
if (isGitTimeoutError(err)) {
|
|
12419
|
+
appendLog(
|
|
12420
|
+
"warn",
|
|
12421
|
+
`git-traces: optional metadata command skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
12422
|
+
);
|
|
12423
|
+
}
|
|
12085
12424
|
return null;
|
|
12086
12425
|
}
|
|
12087
12426
|
}
|
|
@@ -12188,6 +12527,7 @@ var TOOL_LABELS = {
|
|
|
12188
12527
|
cursor: "Cursor",
|
|
12189
12528
|
codex: "Codex",
|
|
12190
12529
|
claude: "Claude",
|
|
12530
|
+
"copilot-chat": "GitHub Copilot Chat",
|
|
12191
12531
|
opencode: "opencode"
|
|
12192
12532
|
};
|
|
12193
12533
|
function resolveCwd(payload) {
|
|
@@ -12235,7 +12575,10 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
|
|
|
12235
12575
|
presigned.headers,
|
|
12236
12576
|
buffer
|
|
12237
12577
|
);
|
|
12238
|
-
appendLog(
|
|
12578
|
+
appendLog(
|
|
12579
|
+
"info",
|
|
12580
|
+
`git-traces: uploaded ${filename} (${buffer.byteLength} bytes)`
|
|
12581
|
+
);
|
|
12239
12582
|
}
|
|
12240
12583
|
function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
12241
12584
|
const baselineSha = captureBaselineSha(repoRoot);
|
|
@@ -12246,11 +12589,9 @@ function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
|
12246
12589
|
);
|
|
12247
12590
|
return { baselineSha, headSha: captureHeadSha(repoRoot) };
|
|
12248
12591
|
}
|
|
12249
|
-
|
|
12592
|
+
function buildEpochBaselineArtifacts(params) {
|
|
12250
12593
|
const {
|
|
12251
12594
|
repoRoot,
|
|
12252
|
-
client,
|
|
12253
|
-
contributionId,
|
|
12254
12595
|
sessionId,
|
|
12255
12596
|
tool,
|
|
12256
12597
|
epoch,
|
|
@@ -12260,50 +12601,63 @@ async function uploadEpochBaseline(params) {
|
|
|
12260
12601
|
startedAt
|
|
12261
12602
|
} = params;
|
|
12262
12603
|
const prefix = epochPrefix(epoch);
|
|
12263
|
-
const metadata = buildBaselineMetadata(
|
|
12264
|
-
repoRoot,
|
|
12265
|
-
sessionId,
|
|
12266
|
-
tool,
|
|
12267
|
-
baselineSha,
|
|
12268
|
-
CLI_VERSION,
|
|
12269
|
-
epoch,
|
|
12270
|
-
prevHeadSha,
|
|
12271
|
-
transitionKind,
|
|
12272
|
-
startedAt
|
|
12273
|
-
);
|
|
12274
|
-
let baselineTreeSha;
|
|
12275
|
-
let bundleBuffer;
|
|
12276
12604
|
try {
|
|
12277
|
-
|
|
12278
|
-
|
|
12605
|
+
const metadata = buildBaselineMetadata(
|
|
12606
|
+
repoRoot,
|
|
12607
|
+
sessionId,
|
|
12608
|
+
tool,
|
|
12609
|
+
baselineSha,
|
|
12610
|
+
CLI_VERSION,
|
|
12611
|
+
epoch,
|
|
12612
|
+
prevHeadSha,
|
|
12613
|
+
transitionKind,
|
|
12614
|
+
startedAt
|
|
12615
|
+
);
|
|
12616
|
+
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
|
|
12617
|
+
const bundleBuffer = createBundleFromTree(
|
|
12279
12618
|
repoRoot,
|
|
12280
12619
|
baselineTreeSha,
|
|
12281
12620
|
`${sessionId}-${prefix}`,
|
|
12282
12621
|
`baseline ${prefix}`
|
|
12283
12622
|
);
|
|
12623
|
+
const metadataBuffer = Buffer.from(JSON.stringify(metadata, null, 2));
|
|
12624
|
+
return { baselineTreeSha, metadataBuffer, bundleBuffer };
|
|
12284
12625
|
} catch (err) {
|
|
12285
12626
|
appendLog(
|
|
12286
12627
|
"error",
|
|
12287
|
-
`git-traces: failed to
|
|
12628
|
+
`git-traces: failed to build baseline artifacts for ${prefix}: ${formatError(err)}`
|
|
12288
12629
|
);
|
|
12289
12630
|
return null;
|
|
12290
12631
|
}
|
|
12291
|
-
|
|
12632
|
+
}
|
|
12633
|
+
async function uploadEpochBaselineArtifacts(params) {
|
|
12634
|
+
const { client, contributionId, epoch, artifacts } = params;
|
|
12635
|
+
const prefix = epochPrefix(epoch);
|
|
12292
12636
|
await uploadFile(
|
|
12293
12637
|
client,
|
|
12294
12638
|
contributionId,
|
|
12295
12639
|
`${prefix}-baseline.json`,
|
|
12296
12640
|
"application/json",
|
|
12297
|
-
metadataBuffer
|
|
12641
|
+
artifacts.metadataBuffer
|
|
12298
12642
|
);
|
|
12299
12643
|
await uploadFile(
|
|
12300
12644
|
client,
|
|
12301
12645
|
contributionId,
|
|
12302
12646
|
`${prefix}-baseline.bundle`,
|
|
12303
12647
|
"application/x-git-bundle",
|
|
12304
|
-
bundleBuffer
|
|
12648
|
+
artifacts.bundleBuffer
|
|
12305
12649
|
);
|
|
12306
|
-
|
|
12650
|
+
}
|
|
12651
|
+
async function uploadEpochBaseline(params) {
|
|
12652
|
+
const artifacts = buildEpochBaselineArtifacts(params);
|
|
12653
|
+
if (!artifacts) return null;
|
|
12654
|
+
await uploadEpochBaselineArtifacts({
|
|
12655
|
+
client: params.client,
|
|
12656
|
+
contributionId: params.contributionId,
|
|
12657
|
+
epoch: params.epoch,
|
|
12658
|
+
artifacts
|
|
12659
|
+
});
|
|
12660
|
+
return { baselineTreeSha: artifacts.baselineTreeSha };
|
|
12307
12661
|
}
|
|
12308
12662
|
async function openEpoch(params) {
|
|
12309
12663
|
const { baselineSha, headSha } = pinEpochBaseline(
|
|
@@ -12318,7 +12672,10 @@ async function openEpoch(params) {
|
|
|
12318
12672
|
async function initializeSession(cwd, tool, sessionId) {
|
|
12319
12673
|
const project = await findProjectForCwd(cwd);
|
|
12320
12674
|
if (!project) {
|
|
12321
|
-
appendLog(
|
|
12675
|
+
appendLog(
|
|
12676
|
+
"warn",
|
|
12677
|
+
"git-traces: no hillclimb project config found, skipping"
|
|
12678
|
+
);
|
|
12322
12679
|
return null;
|
|
12323
12680
|
}
|
|
12324
12681
|
const { baselineSha, headSha } = pinEpochBaseline(cwd, sessionId, 1);
|
|
@@ -12398,6 +12755,17 @@ async function handleStop(payload, tool) {
|
|
|
12398
12755
|
identity.sessionCookie
|
|
12399
12756
|
);
|
|
12400
12757
|
if (state.contributionId === null) {
|
|
12758
|
+
const artifacts = buildEpochBaselineArtifacts({
|
|
12759
|
+
repoRoot: cwd,
|
|
12760
|
+
sessionId: state.sessionId,
|
|
12761
|
+
tool,
|
|
12762
|
+
epoch: 1,
|
|
12763
|
+
baselineSha: state.baselineSha,
|
|
12764
|
+
prevHeadSha: null,
|
|
12765
|
+
transitionKind: "initial",
|
|
12766
|
+
startedAt: state.startedAt
|
|
12767
|
+
});
|
|
12768
|
+
if (!artifacts) return;
|
|
12401
12769
|
const toolLabel = TOOL_LABELS[tool] ?? "Claude";
|
|
12402
12770
|
const now = /* @__PURE__ */ new Date();
|
|
12403
12771
|
const shortId = state.sessionId.slice(0, 12);
|
|
@@ -12412,23 +12780,16 @@ Repo: ${cwd}
|
|
|
12412
12780
|
Uploaded: ${now.toISOString()}`
|
|
12413
12781
|
}
|
|
12414
12782
|
);
|
|
12415
|
-
await
|
|
12416
|
-
const uploaded = await uploadEpochBaseline({
|
|
12417
|
-
repoRoot: cwd,
|
|
12783
|
+
await uploadEpochBaselineArtifacts({
|
|
12418
12784
|
client,
|
|
12419
12785
|
contributionId: contribution.id,
|
|
12420
|
-
sessionId: state.sessionId,
|
|
12421
|
-
tool,
|
|
12422
12786
|
epoch: 1,
|
|
12423
|
-
|
|
12424
|
-
prevHeadSha: null,
|
|
12425
|
-
transitionKind: "initial",
|
|
12426
|
-
startedAt: state.startedAt
|
|
12787
|
+
artifacts
|
|
12427
12788
|
});
|
|
12428
|
-
|
|
12789
|
+
await client.submitContribution(contribution.id);
|
|
12429
12790
|
state.contributionId = contribution.id;
|
|
12430
|
-
state.baselineTreeSha =
|
|
12431
|
-
state.lastSnapshotTreeSha =
|
|
12791
|
+
state.baselineTreeSha = artifacts.baselineTreeSha;
|
|
12792
|
+
state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
|
|
12432
12793
|
await writeSessionState(state, tool);
|
|
12433
12794
|
appendLog(
|
|
12434
12795
|
"info",
|
|
@@ -12542,9 +12903,19 @@ async function handleSessionEnd(payload, tool) {
|
|
|
12542
12903
|
|
|
12543
12904
|
// src/git-traces/index.ts
|
|
12544
12905
|
var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
12545
|
-
var
|
|
12546
|
-
var
|
|
12547
|
-
function
|
|
12906
|
+
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
12907
|
+
var FLOW_ID_ENV = "HILLCLIMB_GIT_TRACES_FLOW";
|
|
12908
|
+
function newFlowId() {
|
|
12909
|
+
return crypto2.randomBytes(3).toString("hex");
|
|
12910
|
+
}
|
|
12911
|
+
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
12912
|
+
"claude",
|
|
12913
|
+
"codex",
|
|
12914
|
+
"copilot-chat",
|
|
12915
|
+
"cursor",
|
|
12916
|
+
"opencode"
|
|
12917
|
+
]);
|
|
12918
|
+
function parseToolArg2(argv) {
|
|
12548
12919
|
for (let i = 0; i < argv.length; i++) {
|
|
12549
12920
|
const a = argv[i];
|
|
12550
12921
|
if (a.startsWith("--tool=")) return a.slice("--tool=".length);
|
|
@@ -12565,7 +12936,9 @@ async function runGitTraces() {
|
|
|
12565
12936
|
await runGitTracesWorker();
|
|
12566
12937
|
return;
|
|
12567
12938
|
}
|
|
12568
|
-
const
|
|
12939
|
+
const flowId = newFlowId();
|
|
12940
|
+
setLogPrefix(`[${flowId}]`);
|
|
12941
|
+
const toolArg = parseToolArg2(process.argv.slice(2));
|
|
12569
12942
|
appendLog(
|
|
12570
12943
|
"info",
|
|
12571
12944
|
`git-traces hook invoked (pid ${process.pid}, tool=${toolArg ?? "<none>"})`
|
|
@@ -12581,10 +12954,7 @@ async function runGitTraces() {
|
|
|
12581
12954
|
try {
|
|
12582
12955
|
raw = await readStdin2();
|
|
12583
12956
|
} catch (err) {
|
|
12584
|
-
appendLog(
|
|
12585
|
-
"error",
|
|
12586
|
-
`git-traces: failed to read stdin: ${err instanceof Error ? err.message : String(err)}`
|
|
12587
|
-
);
|
|
12957
|
+
appendLog("error", `git-traces: failed to read stdin: ${formatError(err)}`);
|
|
12588
12958
|
return;
|
|
12589
12959
|
}
|
|
12590
12960
|
if (!raw.trim()) {
|
|
@@ -12606,7 +12976,8 @@ async function runGitTraces() {
|
|
|
12606
12976
|
env: {
|
|
12607
12977
|
...process.env,
|
|
12608
12978
|
[WORKER_ENV_FLAG2]: "1",
|
|
12609
|
-
[
|
|
12979
|
+
[TOOL_ENV_FLAG2]: toolArg,
|
|
12980
|
+
[FLOW_ID_ENV]: flowId
|
|
12610
12981
|
}
|
|
12611
12982
|
}
|
|
12612
12983
|
);
|
|
@@ -12623,12 +12994,13 @@ async function runGitTraces() {
|
|
|
12623
12994
|
} catch (err) {
|
|
12624
12995
|
appendLog(
|
|
12625
12996
|
"error",
|
|
12626
|
-
`git-traces: failed to spawn worker: ${
|
|
12997
|
+
`git-traces: failed to spawn worker: ${formatError(err)}`
|
|
12627
12998
|
);
|
|
12628
12999
|
}
|
|
12629
13000
|
}
|
|
12630
13001
|
async function runGitTracesWorker() {
|
|
12631
|
-
|
|
13002
|
+
setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
|
|
13003
|
+
const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
|
|
12632
13004
|
appendLog(
|
|
12633
13005
|
"info",
|
|
12634
13006
|
`git-traces worker started (pid ${process.pid}, tool=${tool ?? "<none>"})`
|
|
@@ -12646,7 +13018,7 @@ async function runGitTracesWorker() {
|
|
|
12646
13018
|
} catch (err) {
|
|
12647
13019
|
appendLog(
|
|
12648
13020
|
"error",
|
|
12649
|
-
`git-traces worker: failed to read stdin: ${
|
|
13021
|
+
`git-traces worker: failed to read stdin: ${formatError(err)}`
|
|
12650
13022
|
);
|
|
12651
13023
|
return;
|
|
12652
13024
|
}
|
|
@@ -12660,7 +13032,7 @@ async function runGitTracesWorker() {
|
|
|
12660
13032
|
} catch (err) {
|
|
12661
13033
|
appendLog(
|
|
12662
13034
|
"error",
|
|
12663
|
-
`git-traces worker: failed to parse payload: ${
|
|
13035
|
+
`git-traces worker: failed to parse payload: ${formatError(err)}`
|
|
12664
13036
|
);
|
|
12665
13037
|
return;
|
|
12666
13038
|
}
|
|
@@ -12699,9 +13071,12 @@ async function runGitTracesWorker() {
|
|
|
12699
13071
|
appendLog("warn", `git-traces worker: unknown event ${event}`);
|
|
12700
13072
|
}
|
|
12701
13073
|
} catch (err) {
|
|
13074
|
+
const detail = formatError(err);
|
|
13075
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
12702
13076
|
appendLog(
|
|
12703
13077
|
"error",
|
|
12704
|
-
`git-traces worker: unexpected error: ${
|
|
13078
|
+
`git-traces worker: unexpected error: ${detail}${stack ? `
|
|
13079
|
+
${stack}` : ""}`
|
|
12705
13080
|
);
|
|
12706
13081
|
}
|
|
12707
13082
|
}
|
|
@@ -13202,8 +13577,106 @@ var CodexSource = class {
|
|
|
13202
13577
|
}
|
|
13203
13578
|
};
|
|
13204
13579
|
|
|
13580
|
+
// src/sources/copilotChat.ts
|
|
13581
|
+
import fs15 from "fs";
|
|
13582
|
+
import os11 from "os";
|
|
13583
|
+
import path18 from "path";
|
|
13584
|
+
import { fileURLToPath } from "url";
|
|
13585
|
+
function vsCodeUserDirs() {
|
|
13586
|
+
const home = os11.homedir();
|
|
13587
|
+
const dirs = [
|
|
13588
|
+
path18.join(home, "Library", "Application Support", "Code", "User"),
|
|
13589
|
+
path18.join(home, ".config", "Code", "User")
|
|
13590
|
+
];
|
|
13591
|
+
if (process.env.APPDATA) {
|
|
13592
|
+
dirs.push(path18.join(process.env.APPDATA, "Code", "User"));
|
|
13593
|
+
}
|
|
13594
|
+
return dirs;
|
|
13595
|
+
}
|
|
13596
|
+
function uriToFsPath(uri) {
|
|
13597
|
+
if (!uri.startsWith("file://")) return null;
|
|
13598
|
+
try {
|
|
13599
|
+
return fileURLToPath(uri);
|
|
13600
|
+
} catch {
|
|
13601
|
+
return null;
|
|
13602
|
+
}
|
|
13603
|
+
}
|
|
13604
|
+
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
13605
|
+
let raw;
|
|
13606
|
+
try {
|
|
13607
|
+
raw = await fs15.promises.readFile(workspaceJsonPath, "utf-8");
|
|
13608
|
+
} catch {
|
|
13609
|
+
return null;
|
|
13610
|
+
}
|
|
13611
|
+
let data;
|
|
13612
|
+
try {
|
|
13613
|
+
data = JSON.parse(raw);
|
|
13614
|
+
} catch {
|
|
13615
|
+
return null;
|
|
13616
|
+
}
|
|
13617
|
+
if (!data || typeof data !== "object") return null;
|
|
13618
|
+
const obj = data;
|
|
13619
|
+
if (typeof obj.folder === "string") {
|
|
13620
|
+
return uriToFsPath(obj.folder) ?? obj.folder;
|
|
13621
|
+
}
|
|
13622
|
+
return null;
|
|
13623
|
+
}
|
|
13624
|
+
var CopilotChatSource = class {
|
|
13625
|
+
name = "copilot-chat";
|
|
13626
|
+
async scan() {
|
|
13627
|
+
const results = [];
|
|
13628
|
+
for (const userDir of vsCodeUserDirs()) {
|
|
13629
|
+
const workspaceStorage = path18.join(userDir, "workspaceStorage");
|
|
13630
|
+
let hashDirs;
|
|
13631
|
+
try {
|
|
13632
|
+
hashDirs = await fs15.promises.readdir(workspaceStorage, {
|
|
13633
|
+
withFileTypes: true
|
|
13634
|
+
});
|
|
13635
|
+
} catch {
|
|
13636
|
+
continue;
|
|
13637
|
+
}
|
|
13638
|
+
for (const hash of hashDirs) {
|
|
13639
|
+
if (!hash.isDirectory()) continue;
|
|
13640
|
+
const wsRoot = path18.join(workspaceStorage, hash.name);
|
|
13641
|
+
const transcriptsDir = path18.join(
|
|
13642
|
+
wsRoot,
|
|
13643
|
+
"GitHub.copilot-chat",
|
|
13644
|
+
"transcripts"
|
|
13645
|
+
);
|
|
13646
|
+
let transcriptEntries;
|
|
13647
|
+
try {
|
|
13648
|
+
transcriptEntries = await fs15.promises.readdir(transcriptsDir, {
|
|
13649
|
+
withFileTypes: true
|
|
13650
|
+
});
|
|
13651
|
+
} catch {
|
|
13652
|
+
continue;
|
|
13653
|
+
}
|
|
13654
|
+
const repoPath = await readWorkspaceFolder(
|
|
13655
|
+
path18.join(wsRoot, "workspace.json")
|
|
13656
|
+
);
|
|
13657
|
+
if (!repoPath) continue;
|
|
13658
|
+
for (const entry of transcriptEntries) {
|
|
13659
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
13660
|
+
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
13661
|
+
results.push({
|
|
13662
|
+
sourceName: this.name,
|
|
13663
|
+
absolutePath: path18.join(transcriptsDir, entry.name),
|
|
13664
|
+
repoPath,
|
|
13665
|
+
metadata: { sessionId }
|
|
13666
|
+
});
|
|
13667
|
+
}
|
|
13668
|
+
}
|
|
13669
|
+
}
|
|
13670
|
+
return results;
|
|
13671
|
+
}
|
|
13672
|
+
};
|
|
13673
|
+
|
|
13205
13674
|
// src/sources/index.ts
|
|
13206
|
-
var sources = [
|
|
13675
|
+
var sources = [
|
|
13676
|
+
new ClaudeSource(),
|
|
13677
|
+
new CodexSource(),
|
|
13678
|
+
new CopilotChatSource()
|
|
13679
|
+
];
|
|
13207
13680
|
|
|
13208
13681
|
// src/cli.ts
|
|
13209
13682
|
function reportRedactionStats(noun, stats) {
|
|
@@ -13227,7 +13700,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
13227
13700
|
async function filterByTimeRange(group, range) {
|
|
13228
13701
|
const results = await Promise.all(
|
|
13229
13702
|
group.files.map(
|
|
13230
|
-
(f) =>
|
|
13703
|
+
(f) => fs16.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
13231
13704
|
)
|
|
13232
13705
|
);
|
|
13233
13706
|
const filtered = [];
|
|
@@ -13254,10 +13727,10 @@ async function runInteractive() {
|
|
|
13254
13727
|
s.start(`Scanning ${source.name} logs...`);
|
|
13255
13728
|
const allFiles = await source.scan();
|
|
13256
13729
|
const allGroups = await mergeByRepo(allFiles);
|
|
13257
|
-
const repoRoot =
|
|
13730
|
+
const repoRoot = path19.resolve(repo.root);
|
|
13258
13731
|
const matching = allGroups.filter((g) => {
|
|
13259
|
-
const resolved =
|
|
13260
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
13732
|
+
const resolved = path19.resolve(g.repoPath);
|
|
13733
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path19.sep);
|
|
13261
13734
|
});
|
|
13262
13735
|
if (matching.length === 0) {
|
|
13263
13736
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -13288,7 +13761,7 @@ async function runInteractive() {
|
|
|
13288
13761
|
}
|
|
13289
13762
|
}
|
|
13290
13763
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
13291
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
13764
|
+
const envFilePaths = envFileNames.map((n) => path19.join(repoRoot, n));
|
|
13292
13765
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
13293
13766
|
const secretResult = await collectSecrets(
|
|
13294
13767
|
repoRoot,
|
|
@@ -13332,11 +13805,14 @@ async function main() {
|
|
|
13332
13805
|
const [, , subcommand, ...rest] = process.argv;
|
|
13333
13806
|
switch (subcommand) {
|
|
13334
13807
|
case void 0:
|
|
13335
|
-
await
|
|
13808
|
+
await runInit([]);
|
|
13336
13809
|
return;
|
|
13337
13810
|
case "init":
|
|
13338
13811
|
await runInit(rest);
|
|
13339
13812
|
return;
|
|
13813
|
+
case "export":
|
|
13814
|
+
await runInteractive();
|
|
13815
|
+
return;
|
|
13340
13816
|
case "login":
|
|
13341
13817
|
await runLogin(rest);
|
|
13342
13818
|
return;
|
|
@@ -13356,11 +13832,15 @@ async function main() {
|
|
|
13356
13832
|
case "-h":
|
|
13357
13833
|
case "help": {
|
|
13358
13834
|
process.stdout.write(
|
|
13359
|
-
"Usage: npx hillclimb [subcommand]\n\nSubcommands:\n (none) Interactive export flow (manual use)\n login Sign in and save the credential for reuse across repos\n logout Clear the saved sign-in (pass --url <api> to scope to one instance)\n
|
|
13835
|
+
"Usage: npx hillclimb [subcommand]\n\nSubcommands:\n (none) Configure this repo for automatic upload\n init Configure this repo for automatic upload (--login forces fresh sign-in)\n export Interactive export flow (manual use)\n login Sign in and save the credential for reuse across repos\n logout Clear the saved sign-in (pass --url <api> to scope to one instance)\n status Show whether you're signed in and this repo is connected (pass --debug for details)\n upload Hook entry point \u2014 reads JSON payload from stdin and uploads one session\n help Show this message\n"
|
|
13360
13836
|
);
|
|
13361
13837
|
return;
|
|
13362
13838
|
}
|
|
13363
13839
|
default:
|
|
13840
|
+
if (subcommand === "--login" || subcommand === "--workspace" || subcommand.startsWith("--workspace=")) {
|
|
13841
|
+
await runInit([subcommand, ...rest]);
|
|
13842
|
+
return;
|
|
13843
|
+
}
|
|
13364
13844
|
process.stderr.write(
|
|
13365
13845
|
`Unknown subcommand: ${subcommand}${rest.length > 0 ? ` ${rest.join(" ")}` : ""}
|
|
13366
13846
|
`
|