hillclimb 0.1.8 → 0.1.9
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 +260 -175
- 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 fs17 from "fs";
|
|
5
|
+
import path20 from "path";
|
|
6
6
|
import * as p6 from "@clack/prompts";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -125,16 +125,65 @@ function detectRepoRoot() {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
// src/
|
|
128
|
+
// src/gitignore.ts
|
|
129
129
|
import fs2 from "fs";
|
|
130
130
|
import path3 from "path";
|
|
131
|
-
|
|
131
|
+
function toGitignorePattern(repoRoot, file) {
|
|
132
|
+
const rel = path3.relative(repoRoot, file);
|
|
133
|
+
if (!rel || rel.startsWith("..") || path3.isAbsolute(rel)) return null;
|
|
134
|
+
return `/${rel.split(path3.sep).join("/")}`;
|
|
135
|
+
}
|
|
136
|
+
function normalizePattern(pattern) {
|
|
137
|
+
return pattern.trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
138
|
+
}
|
|
139
|
+
async function ensureGitignored(repoRoot, files) {
|
|
140
|
+
const gitignorePath = path3.join(repoRoot, ".gitignore");
|
|
141
|
+
const patterns = [
|
|
142
|
+
...new Set(
|
|
143
|
+
files.map((file) => toGitignorePattern(repoRoot, file)).filter((pattern) => pattern !== null)
|
|
144
|
+
)
|
|
145
|
+
];
|
|
146
|
+
let content = "";
|
|
147
|
+
let created = false;
|
|
148
|
+
try {
|
|
149
|
+
content = await fs2.promises.readFile(gitignorePath, "utf-8");
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err.code !== "ENOENT") throw err;
|
|
152
|
+
created = true;
|
|
153
|
+
}
|
|
154
|
+
const existing = new Set(
|
|
155
|
+
content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map(normalizePattern)
|
|
156
|
+
);
|
|
157
|
+
const added = patterns.filter(
|
|
158
|
+
(pattern) => !existing.has(normalizePattern(pattern))
|
|
159
|
+
);
|
|
160
|
+
if (added.length === 0) {
|
|
161
|
+
return { path: gitignorePath, added: [], created: false, changed: false };
|
|
162
|
+
}
|
|
163
|
+
let next = content;
|
|
164
|
+
if (next && !next.endsWith("\n")) next += "\n";
|
|
165
|
+
if (next) next += "\n";
|
|
166
|
+
next += ["# Hillclimb hook files", ...added].join("\n");
|
|
167
|
+
next += "\n";
|
|
168
|
+
await fs2.promises.writeFile(gitignorePath, next);
|
|
169
|
+
return {
|
|
170
|
+
path: gitignorePath,
|
|
171
|
+
added,
|
|
172
|
+
created,
|
|
173
|
+
changed: true
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/identity.ts
|
|
178
|
+
import fs3 from "fs";
|
|
179
|
+
import path4 from "path";
|
|
180
|
+
var IDENTITY_PATH = path4.join(configDir(), "identity.json");
|
|
132
181
|
function normalizeUrl(apiBaseUrl) {
|
|
133
182
|
return apiBaseUrl.replace(/\/$/, "");
|
|
134
183
|
}
|
|
135
184
|
async function loadAllIdentities() {
|
|
136
185
|
try {
|
|
137
|
-
const raw = await
|
|
186
|
+
const raw = await fs3.promises.readFile(IDENTITY_PATH, "utf-8");
|
|
138
187
|
const parsed = JSON.parse(raw);
|
|
139
188
|
if (!parsed.identities || typeof parsed.identities !== "object") {
|
|
140
189
|
return { identities: {} };
|
|
@@ -149,12 +198,12 @@ async function loadIdentity(apiBaseUrl) {
|
|
|
149
198
|
return file.identities[normalizeUrl(apiBaseUrl)] ?? null;
|
|
150
199
|
}
|
|
151
200
|
async function writeIdentityFile(file) {
|
|
152
|
-
await
|
|
201
|
+
await fs3.promises.mkdir(configDir(), { recursive: true, mode: 448 });
|
|
153
202
|
const tmp = `${IDENTITY_PATH}.tmp`;
|
|
154
|
-
await
|
|
203
|
+
await fs3.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
|
|
155
204
|
mode: 384
|
|
156
205
|
});
|
|
157
|
-
await
|
|
206
|
+
await fs3.promises.rename(tmp, IDENTITY_PATH);
|
|
158
207
|
}
|
|
159
208
|
async function saveIdentity(identity) {
|
|
160
209
|
const file = await loadAllIdentities();
|
|
@@ -192,8 +241,8 @@ function formatError(err) {
|
|
|
192
241
|
}
|
|
193
242
|
|
|
194
243
|
// src/platform/log.ts
|
|
195
|
-
import
|
|
196
|
-
import
|
|
244
|
+
import fs4 from "fs";
|
|
245
|
+
import path5 from "path";
|
|
197
246
|
var PT_TIME_ZONE = "America/Los_Angeles";
|
|
198
247
|
function pacificParts(date) {
|
|
199
248
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
@@ -244,10 +293,10 @@ function pacificDateString(date) {
|
|
|
244
293
|
return `${p7.year}-${p7.month}-${p7.day}`;
|
|
245
294
|
}
|
|
246
295
|
function logsDir() {
|
|
247
|
-
return
|
|
296
|
+
return path5.join(configDir(), "logs");
|
|
248
297
|
}
|
|
249
298
|
function todayLogPath() {
|
|
250
|
-
return
|
|
299
|
+
return path5.join(logsDir(), `${pacificDateString(/* @__PURE__ */ new Date())}.log`);
|
|
251
300
|
}
|
|
252
301
|
var logPrefix = "";
|
|
253
302
|
function setLogPrefix(prefix) {
|
|
@@ -258,8 +307,8 @@ function appendLog(level, message) {
|
|
|
258
307
|
const line = `[${pacificTimestamp(/* @__PURE__ */ new Date())}] [${level}]${tag} ${message}
|
|
259
308
|
`;
|
|
260
309
|
try {
|
|
261
|
-
|
|
262
|
-
|
|
310
|
+
fs4.mkdirSync(logsDir(), { recursive: true, mode: 448 });
|
|
311
|
+
fs4.appendFileSync(todayLogPath(), line);
|
|
263
312
|
} catch {
|
|
264
313
|
process.stderr.write(`hillclimb:${tag} ${level}: ${message}
|
|
265
314
|
`);
|
|
@@ -547,9 +596,9 @@ var PlatformClient = class {
|
|
|
547
596
|
};
|
|
548
597
|
|
|
549
598
|
// src/platform/hooks.ts
|
|
550
|
-
import
|
|
599
|
+
import fs5 from "fs";
|
|
551
600
|
import os2 from "os";
|
|
552
|
-
import
|
|
601
|
+
import path6 from "path";
|
|
553
602
|
var HOOK_CMD = (sub) => `npx hillclimb@latest ${sub}`;
|
|
554
603
|
var GIT_TRACES_CMD = (tool) => `npx hillclimb@latest git-traces --tool=${tool}`;
|
|
555
604
|
var CLAUDE_SESSIONEND_UPLOAD_TIMEOUT_SECONDS = 30;
|
|
@@ -569,7 +618,7 @@ var TOOLS = [
|
|
|
569
618
|
{ eventName: "Stop", command: GIT_TRACES_CMD("claude") },
|
|
570
619
|
{ eventName: "SessionEnd", command: GIT_TRACES_CMD("claude") }
|
|
571
620
|
],
|
|
572
|
-
detect: () => isDir(
|
|
621
|
+
detect: () => isDir(path6.join(os2.homedir(), ".claude"))
|
|
573
622
|
},
|
|
574
623
|
{
|
|
575
624
|
tool: "cursor",
|
|
@@ -582,7 +631,7 @@ var TOOLS = [
|
|
|
582
631
|
{ eventName: "stop", command: GIT_TRACES_CMD("cursor") },
|
|
583
632
|
{ eventName: "sessionEnd", command: GIT_TRACES_CMD("cursor") }
|
|
584
633
|
],
|
|
585
|
-
detect: () => isDir(
|
|
634
|
+
detect: () => isDir(path6.join(os2.homedir(), ".cursor"))
|
|
586
635
|
},
|
|
587
636
|
{
|
|
588
637
|
// opencode has no settings-file hook system (the `experimental.hook` block
|
|
@@ -597,7 +646,7 @@ var TOOLS = [
|
|
|
597
646
|
format: "opencode",
|
|
598
647
|
events: [],
|
|
599
648
|
// Events are wired inside the plugin itself; see OPENCODE_PLUGIN_CONTENT.
|
|
600
|
-
detect: () => isDir(
|
|
649
|
+
detect: () => isDir(path6.join(os2.homedir(), ".local", "share", "opencode")) || isDir(path6.join(os2.homedir(), ".config", "opencode"))
|
|
601
650
|
},
|
|
602
651
|
{
|
|
603
652
|
// VS Code Copilot Chat. Same "no SessionEnd" constraint as Codex (the
|
|
@@ -637,23 +686,23 @@ var TOOLS = [
|
|
|
637
686
|
{ eventName: "SessionStart", command: GIT_TRACES_CMD("codex") },
|
|
638
687
|
{ eventName: "Stop", command: GIT_TRACES_CMD("codex") }
|
|
639
688
|
],
|
|
640
|
-
detect: () => isDir(
|
|
689
|
+
detect: () => isDir(path6.join(os2.homedir(), ".codex"))
|
|
641
690
|
}
|
|
642
691
|
];
|
|
643
692
|
function isDir(p7) {
|
|
644
693
|
try {
|
|
645
|
-
return
|
|
694
|
+
return fs5.statSync(p7).isDirectory();
|
|
646
695
|
} catch {
|
|
647
696
|
return false;
|
|
648
697
|
}
|
|
649
698
|
}
|
|
650
699
|
function copilotChatDetect() {
|
|
651
700
|
const home = os2.homedir();
|
|
652
|
-
const suffix =
|
|
701
|
+
const suffix = path6.join("User", "globalStorage", "github.copilot-chat");
|
|
653
702
|
const candidates = [
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
process.env.APPDATA ?
|
|
703
|
+
path6.join(home, "Library", "Application Support", "Code", suffix),
|
|
704
|
+
path6.join(home, ".config", "Code", suffix),
|
|
705
|
+
process.env.APPDATA ? path6.join(process.env.APPDATA, "Code", suffix) : null
|
|
657
706
|
].filter((p7) => p7 !== null);
|
|
658
707
|
return candidates.some(isDir);
|
|
659
708
|
}
|
|
@@ -662,11 +711,11 @@ function ensureHooks(settings) {
|
|
|
662
711
|
return settings.hooks;
|
|
663
712
|
}
|
|
664
713
|
function settingsPath(repoRoot, def) {
|
|
665
|
-
return
|
|
714
|
+
return path6.join(repoRoot, def.settingsFile);
|
|
666
715
|
}
|
|
667
716
|
async function readJson(file) {
|
|
668
717
|
try {
|
|
669
|
-
const raw = await
|
|
718
|
+
const raw = await fs5.promises.readFile(file, "utf-8");
|
|
670
719
|
const parsed = JSON.parse(raw);
|
|
671
720
|
if (parsed && typeof parsed === "object") return parsed;
|
|
672
721
|
return {};
|
|
@@ -676,8 +725,8 @@ async function readJson(file) {
|
|
|
676
725
|
}
|
|
677
726
|
}
|
|
678
727
|
async function writeJson(file, obj) {
|
|
679
|
-
await
|
|
680
|
-
await
|
|
728
|
+
await fs5.promises.mkdir(path6.dirname(file), { recursive: true });
|
|
729
|
+
await fs5.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
|
|
681
730
|
`);
|
|
682
731
|
}
|
|
683
732
|
function claudeHookPresent(matchers, command, options = {}) {
|
|
@@ -951,19 +1000,19 @@ export default HillclimbPlugin;
|
|
|
951
1000
|
`;
|
|
952
1001
|
async function opencodeInstall(file) {
|
|
953
1002
|
try {
|
|
954
|
-
const existing = await
|
|
1003
|
+
const existing = await fs5.promises.readFile(file, "utf-8");
|
|
955
1004
|
if (existing === OPENCODE_PLUGIN_CONTENT) {
|
|
956
1005
|
return { installed: 0, alreadyPresent: 1 };
|
|
957
1006
|
}
|
|
958
1007
|
} catch {
|
|
959
1008
|
}
|
|
960
|
-
await
|
|
961
|
-
await
|
|
1009
|
+
await fs5.promises.mkdir(path6.dirname(file), { recursive: true });
|
|
1010
|
+
await fs5.promises.writeFile(file, OPENCODE_PLUGIN_CONTENT);
|
|
962
1011
|
return { installed: 1, alreadyPresent: 0 };
|
|
963
1012
|
}
|
|
964
1013
|
async function opencodeCheck(file) {
|
|
965
1014
|
try {
|
|
966
|
-
const content = await
|
|
1015
|
+
const content = await fs5.promises.readFile(file, "utf-8");
|
|
967
1016
|
return content.includes(OPENCODE_PLUGIN_MARKER);
|
|
968
1017
|
} catch {
|
|
969
1018
|
return false;
|
|
@@ -1097,17 +1146,17 @@ async function checkAllHooks(repoRoot) {
|
|
|
1097
1146
|
}
|
|
1098
1147
|
return results;
|
|
1099
1148
|
}
|
|
1100
|
-
var CODEX_CONFIG_PATH =
|
|
1149
|
+
var CODEX_CONFIG_PATH = path6.join(os2.homedir(), ".codex", "config.toml");
|
|
1101
1150
|
async function ensureCodexHooksEnabled() {
|
|
1102
1151
|
let content;
|
|
1103
1152
|
try {
|
|
1104
|
-
content = await
|
|
1153
|
+
content = await fs5.promises.readFile(CODEX_CONFIG_PATH, "utf-8");
|
|
1105
1154
|
} catch (err) {
|
|
1106
1155
|
if (err.code === "ENOENT") {
|
|
1107
|
-
await
|
|
1156
|
+
await fs5.promises.mkdir(path6.dirname(CODEX_CONFIG_PATH), {
|
|
1108
1157
|
recursive: true
|
|
1109
1158
|
});
|
|
1110
|
-
await
|
|
1159
|
+
await fs5.promises.writeFile(
|
|
1111
1160
|
CODEX_CONFIG_PATH,
|
|
1112
1161
|
"[features]\nhooks = true\n"
|
|
1113
1162
|
);
|
|
@@ -1148,7 +1197,7 @@ hooks = true
|
|
|
1148
1197
|
`;
|
|
1149
1198
|
}
|
|
1150
1199
|
if (content === original) return false;
|
|
1151
|
-
await
|
|
1200
|
+
await fs5.promises.writeFile(CODEX_CONFIG_PATH, content);
|
|
1152
1201
|
return true;
|
|
1153
1202
|
}
|
|
1154
1203
|
var CLAUDE_DEF = TOOLS[0];
|
|
@@ -1571,6 +1620,29 @@ async function runInit(args = []) {
|
|
|
1571
1620
|
p3.log.success(`Installed ${r.label} hook`);
|
|
1572
1621
|
}
|
|
1573
1622
|
}
|
|
1623
|
+
try {
|
|
1624
|
+
const ignored = await ensureGitignored(
|
|
1625
|
+
repoRoot,
|
|
1626
|
+
hookResults.map((r) => r.settingsFile)
|
|
1627
|
+
);
|
|
1628
|
+
if (ignored.changed) {
|
|
1629
|
+
appendLog(
|
|
1630
|
+
"info",
|
|
1631
|
+
`init: gitignored hook files (${ignored.added.join(",")})`
|
|
1632
|
+
);
|
|
1633
|
+
p3.log.success(
|
|
1634
|
+
`${ignored.created ? "Created" : "Updated"} .gitignore for hook files`
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
appendLog(
|
|
1639
|
+
"warn",
|
|
1640
|
+
`init: could not update .gitignore for hook files: ${formatError(err)}`
|
|
1641
|
+
);
|
|
1642
|
+
p3.log.warn(
|
|
1643
|
+
`Could not update .gitignore for hook files: ${err instanceof Error ? err.message : String(err)}`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1574
1646
|
if (hookResults.some((r) => r.tool === "codex")) {
|
|
1575
1647
|
try {
|
|
1576
1648
|
const changed = await ensureCodexHooksEnabled();
|
|
@@ -1821,7 +1893,7 @@ async function runLogout(args = []) {
|
|
|
1821
1893
|
}
|
|
1822
1894
|
|
|
1823
1895
|
// src/commands/status.ts
|
|
1824
|
-
import
|
|
1896
|
+
import path7 from "path";
|
|
1825
1897
|
async function runStatus(args = []) {
|
|
1826
1898
|
const debug = args.includes("--debug");
|
|
1827
1899
|
header("status");
|
|
@@ -1839,7 +1911,7 @@ async function runStatus(args = []) {
|
|
|
1839
1911
|
return;
|
|
1840
1912
|
}
|
|
1841
1913
|
const { repoRoot, config } = match;
|
|
1842
|
-
const repoName = repo?.name ??
|
|
1914
|
+
const repoName = repo?.name ?? path7.basename(repoRoot);
|
|
1843
1915
|
if (IS_TTY) {
|
|
1844
1916
|
process.stdout.write(` ${dim("Checking login...")}`);
|
|
1845
1917
|
}
|
|
@@ -1933,7 +2005,7 @@ async function runStatus(args = []) {
|
|
|
1933
2005
|
try {
|
|
1934
2006
|
const hookStatuses = await checkAllHooks(repoRoot);
|
|
1935
2007
|
for (const h of hookStatuses) {
|
|
1936
|
-
const rel =
|
|
2008
|
+
const rel = path7.relative(repoRoot, h.settingsFile) || h.settingsFile;
|
|
1937
2009
|
debugRow(
|
|
1938
2010
|
`${h.label} hook:`,
|
|
1939
2011
|
`${rel} ${dim(`(${h.installed ? "installed" : "missing"})`)}`
|
|
@@ -1950,20 +2022,20 @@ async function runStatus(args = []) {
|
|
|
1950
2022
|
|
|
1951
2023
|
// src/commands/upload.ts
|
|
1952
2024
|
import { spawn as spawn2 } from "child_process";
|
|
1953
|
-
import
|
|
2025
|
+
import fs9 from "fs";
|
|
1954
2026
|
import os5 from "os";
|
|
1955
|
-
import
|
|
2027
|
+
import path12 from "path";
|
|
1956
2028
|
|
|
1957
2029
|
// src/middleware/pattern-redact.ts
|
|
1958
2030
|
import os3 from "os";
|
|
1959
2031
|
import { Worker } from "worker_threads";
|
|
1960
2032
|
|
|
1961
2033
|
// src/middleware/file-utils.ts
|
|
1962
|
-
import
|
|
2034
|
+
import fs6 from "fs";
|
|
1963
2035
|
async function checkBinary(filePath) {
|
|
1964
2036
|
let handle = null;
|
|
1965
2037
|
try {
|
|
1966
|
-
handle = await
|
|
2038
|
+
handle = await fs6.promises.open(filePath, "r");
|
|
1967
2039
|
const buf = Buffer.alloc(8192);
|
|
1968
2040
|
const { bytesRead } = await handle.read(buf, 0, 8192, 0);
|
|
1969
2041
|
for (let i = 0; i < bytesRead; i++) {
|
|
@@ -1988,7 +2060,7 @@ async function readFileContent(file) {
|
|
|
1988
2060
|
return { kind: "binary" };
|
|
1989
2061
|
}
|
|
1990
2062
|
try {
|
|
1991
|
-
const content = await
|
|
2063
|
+
const content = await fs6.promises.readFile(file.absolutePath, "utf-8");
|
|
1992
2064
|
return { kind: "text", content };
|
|
1993
2065
|
} catch {
|
|
1994
2066
|
return { kind: "error" };
|
|
@@ -10440,8 +10512,8 @@ var RedactMiddleware = class {
|
|
|
10440
10512
|
var middleware = [];
|
|
10441
10513
|
|
|
10442
10514
|
// src/middleware/secrets.ts
|
|
10443
|
-
import
|
|
10444
|
-
import
|
|
10515
|
+
import fs7 from "fs";
|
|
10516
|
+
import path8 from "path";
|
|
10445
10517
|
var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
|
|
10446
10518
|
"true",
|
|
10447
10519
|
"false",
|
|
@@ -10521,7 +10593,7 @@ async function parseEnvFile(filePath) {
|
|
|
10521
10593
|
const values = [];
|
|
10522
10594
|
let content;
|
|
10523
10595
|
try {
|
|
10524
|
-
content = await
|
|
10596
|
+
content = await fs7.promises.readFile(filePath, "utf-8");
|
|
10525
10597
|
} catch {
|
|
10526
10598
|
return values;
|
|
10527
10599
|
}
|
|
@@ -10589,16 +10661,16 @@ function addWithVariants(set, value) {
|
|
|
10589
10661
|
async function discoverEnvFiles(repoRoot) {
|
|
10590
10662
|
let entries;
|
|
10591
10663
|
try {
|
|
10592
|
-
entries = await
|
|
10664
|
+
entries = await fs7.promises.readdir(repoRoot);
|
|
10593
10665
|
} catch {
|
|
10594
10666
|
return [];
|
|
10595
10667
|
}
|
|
10596
10668
|
const envFiles = [];
|
|
10597
10669
|
for (const name of entries) {
|
|
10598
10670
|
if (!name.startsWith(".env")) continue;
|
|
10599
|
-
const filePath =
|
|
10671
|
+
const filePath = path8.join(repoRoot, name);
|
|
10600
10672
|
try {
|
|
10601
|
-
const stat = await
|
|
10673
|
+
const stat = await fs7.promises.stat(filePath);
|
|
10602
10674
|
if (stat.isFile()) envFiles.push(name);
|
|
10603
10675
|
} catch {
|
|
10604
10676
|
}
|
|
@@ -10621,7 +10693,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
|
|
|
10621
10693
|
}
|
|
10622
10694
|
}
|
|
10623
10695
|
for (const filePath of additionalFiles) {
|
|
10624
|
-
const resolved =
|
|
10696
|
+
const resolved = path8.resolve(repoRoot, filePath);
|
|
10625
10697
|
sourceFiles.push(resolved);
|
|
10626
10698
|
for (const value of await parseEnvFile(resolved)) {
|
|
10627
10699
|
if (isUsableValue(value)) {
|
|
@@ -10646,7 +10718,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
|
|
|
10646
10718
|
}
|
|
10647
10719
|
|
|
10648
10720
|
// src/normalizer/index.ts
|
|
10649
|
-
import
|
|
10721
|
+
import path9 from "path";
|
|
10650
10722
|
|
|
10651
10723
|
// src/normalizer/claude.ts
|
|
10652
10724
|
function stringify(value) {
|
|
@@ -11514,7 +11586,7 @@ var NormalizeMiddleware = class {
|
|
|
11514
11586
|
if (!["claude", "codex", "cursor"].includes(file.sourceName)) continue;
|
|
11515
11587
|
const content = file.content ? file.content.toString("utf-8") : null;
|
|
11516
11588
|
if (!content) continue;
|
|
11517
|
-
const sessionId = file.metadata?.sessionId ??
|
|
11589
|
+
const sessionId = file.metadata?.sessionId ?? path9.basename(file.absolutePath, ".jsonl");
|
|
11518
11590
|
try {
|
|
11519
11591
|
const trajectory = normalizeContent(
|
|
11520
11592
|
file.sourceName,
|
|
@@ -11548,14 +11620,14 @@ import archiver from "archiver";
|
|
|
11548
11620
|
|
|
11549
11621
|
// src/outputs/archive.ts
|
|
11550
11622
|
import os4 from "os";
|
|
11551
|
-
import
|
|
11623
|
+
import path10 from "path";
|
|
11552
11624
|
function getSourceBaseDir(sourceName) {
|
|
11553
11625
|
const home = os4.homedir();
|
|
11554
11626
|
switch (sourceName) {
|
|
11555
11627
|
case "claude":
|
|
11556
|
-
return
|
|
11628
|
+
return path10.join(home, ".claude", "projects");
|
|
11557
11629
|
case "codex":
|
|
11558
|
-
return
|
|
11630
|
+
return path10.join(home, ".codex", "sessions");
|
|
11559
11631
|
default:
|
|
11560
11632
|
return home;
|
|
11561
11633
|
}
|
|
@@ -11564,8 +11636,8 @@ function addGroupToArchive(archive, group, selectedSources) {
|
|
|
11564
11636
|
for (const file of group.files) {
|
|
11565
11637
|
if (!selectedSources.has(file.sourceName)) continue;
|
|
11566
11638
|
const baseDir = getSourceBaseDir(file.sourceName);
|
|
11567
|
-
const relativePath = file.absolutePath.startsWith(baseDir) ?
|
|
11568
|
-
const archivePath =
|
|
11639
|
+
const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
|
|
11640
|
+
const archivePath = path10.join(file.sourceName, relativePath);
|
|
11569
11641
|
if (file.content) {
|
|
11570
11642
|
archive.append(file.content, { name: archivePath });
|
|
11571
11643
|
} else {
|
|
@@ -11639,21 +11711,21 @@ var PlatformUploadOutput = class {
|
|
|
11639
11711
|
};
|
|
11640
11712
|
|
|
11641
11713
|
// src/pipeline.ts
|
|
11642
|
-
import
|
|
11643
|
-
import
|
|
11714
|
+
import fs8 from "fs";
|
|
11715
|
+
import path11 from "path";
|
|
11644
11716
|
function canonicalizePath(p7) {
|
|
11645
|
-
let resolved =
|
|
11646
|
-
if (resolved.endsWith(
|
|
11717
|
+
let resolved = path11.resolve(p7);
|
|
11718
|
+
if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
|
|
11647
11719
|
resolved = resolved.slice(0, -1);
|
|
11648
11720
|
}
|
|
11649
11721
|
return resolved;
|
|
11650
11722
|
}
|
|
11651
11723
|
function computeLabel(repoPath, allPaths) {
|
|
11652
|
-
const segments = repoPath.split(
|
|
11724
|
+
const segments = repoPath.split(path11.sep).filter(Boolean);
|
|
11653
11725
|
for (let depth = 1; depth <= segments.length; depth++) {
|
|
11654
11726
|
const label = segments.slice(-depth).join("/");
|
|
11655
11727
|
const matches = allPaths.filter((p7) => {
|
|
11656
|
-
const s = p7.split(
|
|
11728
|
+
const s = p7.split(path11.sep).filter(Boolean);
|
|
11657
11729
|
return s.slice(-depth).join("/") === label;
|
|
11658
11730
|
});
|
|
11659
11731
|
if (matches.length === 1) return label;
|
|
@@ -11675,7 +11747,7 @@ async function mergeByRepo(files) {
|
|
|
11675
11747
|
const groups = [];
|
|
11676
11748
|
for (const [repoPath, groupFiles] of grouped) {
|
|
11677
11749
|
const stats = await Promise.all(
|
|
11678
|
-
groupFiles.map((f) =>
|
|
11750
|
+
groupFiles.map((f) => fs8.promises.stat(f.absolutePath).catch(() => null))
|
|
11679
11751
|
);
|
|
11680
11752
|
let lastModified = /* @__PURE__ */ new Date(0);
|
|
11681
11753
|
for (const stat of stats) {
|
|
@@ -11698,7 +11770,7 @@ async function preloadFiles(group) {
|
|
|
11698
11770
|
group.files.map(async (file) => {
|
|
11699
11771
|
if (file.content) return file;
|
|
11700
11772
|
try {
|
|
11701
|
-
const buf = await
|
|
11773
|
+
const buf = await fs8.promises.readFile(file.absolutePath);
|
|
11702
11774
|
const checkLen = Math.min(buf.length, 8192);
|
|
11703
11775
|
for (let i = 0; i < checkLen; i++) {
|
|
11704
11776
|
if (buf[i] === 0) {
|
|
@@ -11743,7 +11815,7 @@ function lineHasAssistant(line) {
|
|
|
11743
11815
|
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
11744
11816
|
}
|
|
11745
11817
|
async function hasAssistantMessage(transcriptPath) {
|
|
11746
|
-
const stream =
|
|
11818
|
+
const stream = fs9.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
11747
11819
|
let buffer = "";
|
|
11748
11820
|
try {
|
|
11749
11821
|
for await (const chunk of stream) {
|
|
@@ -11776,7 +11848,7 @@ function resolveCursorTranscriptPath(payload) {
|
|
|
11776
11848
|
const workspace = payload.workspace_roots?.[0];
|
|
11777
11849
|
if (!id || !workspace) return void 0;
|
|
11778
11850
|
const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
|
|
11779
|
-
return
|
|
11851
|
+
return path12.join(
|
|
11780
11852
|
os5.homedir(),
|
|
11781
11853
|
".cursor",
|
|
11782
11854
|
"projects",
|
|
@@ -11802,9 +11874,9 @@ async function runUploadInner(payload) {
|
|
|
11802
11874
|
);
|
|
11803
11875
|
return;
|
|
11804
11876
|
}
|
|
11805
|
-
const transcriptResolved =
|
|
11877
|
+
const transcriptResolved = path12.resolve(transcriptPath);
|
|
11806
11878
|
try {
|
|
11807
|
-
const stat = await
|
|
11879
|
+
const stat = await fs9.promises.stat(transcriptResolved);
|
|
11808
11880
|
if (!stat.isFile()) {
|
|
11809
11881
|
appendLog(
|
|
11810
11882
|
"warn",
|
|
@@ -11853,13 +11925,13 @@ async function uploadSession(args) {
|
|
|
11853
11925
|
};
|
|
11854
11926
|
const group = {
|
|
11855
11927
|
repoPath: repoRoot,
|
|
11856
|
-
label:
|
|
11928
|
+
label: path12.basename(repoRoot),
|
|
11857
11929
|
files: [sourceFile],
|
|
11858
11930
|
sourceNames: [sourceTool],
|
|
11859
11931
|
lastModified: now
|
|
11860
11932
|
};
|
|
11861
11933
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
11862
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
11934
|
+
const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
|
|
11863
11935
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
11864
11936
|
const mwChain = [];
|
|
11865
11937
|
if (secretResult.values.size > 0) {
|
|
@@ -12039,9 +12111,9 @@ import { execFileSync as execFileSync2 } from "child_process";
|
|
|
12039
12111
|
|
|
12040
12112
|
// src/git-traces/git-ops.ts
|
|
12041
12113
|
import { execFileSync } from "child_process";
|
|
12042
|
-
import
|
|
12114
|
+
import fs10 from "fs";
|
|
12043
12115
|
import os6 from "os";
|
|
12044
|
-
import
|
|
12116
|
+
import path13 from "path";
|
|
12045
12117
|
import { gzipSync } from "zlib";
|
|
12046
12118
|
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
12047
12119
|
var EXEC_OPTS = {
|
|
@@ -12165,7 +12237,7 @@ function buildUntrackedTree(repoRoot) {
|
|
|
12165
12237
|
for (const relPath of list.split("\0")) {
|
|
12166
12238
|
if (!relPath) continue;
|
|
12167
12239
|
try {
|
|
12168
|
-
const stat =
|
|
12240
|
+
const stat = fs10.lstatSync(path13.join(repoRoot, relPath));
|
|
12169
12241
|
if (stat.size > MAX_UNTRACKED_FILE_BYTES) {
|
|
12170
12242
|
appendLog(
|
|
12171
12243
|
"warn",
|
|
@@ -12178,7 +12250,7 @@ function buildUntrackedTree(repoRoot) {
|
|
|
12178
12250
|
}
|
|
12179
12251
|
}
|
|
12180
12252
|
if (kept.length === 0) return null;
|
|
12181
|
-
const tmpIndex =
|
|
12253
|
+
const tmpIndex = path13.join(
|
|
12182
12254
|
os6.tmpdir(),
|
|
12183
12255
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
12184
12256
|
);
|
|
@@ -12191,7 +12263,7 @@ function buildUntrackedTree(repoRoot) {
|
|
|
12191
12263
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
12192
12264
|
} finally {
|
|
12193
12265
|
try {
|
|
12194
|
-
|
|
12266
|
+
fs10.unlinkSync(tmpIndex);
|
|
12195
12267
|
} catch {
|
|
12196
12268
|
}
|
|
12197
12269
|
}
|
|
@@ -12202,7 +12274,7 @@ function buildSnapshotTree(repoRoot, stashSha) {
|
|
|
12202
12274
|
if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA) {
|
|
12203
12275
|
return trackedTree;
|
|
12204
12276
|
}
|
|
12205
|
-
const tmpIndex =
|
|
12277
|
+
const tmpIndex = path13.join(
|
|
12206
12278
|
os6.tmpdir(),
|
|
12207
12279
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
12208
12280
|
);
|
|
@@ -12230,7 +12302,7 @@ function buildSnapshotTree(repoRoot, stashSha) {
|
|
|
12230
12302
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
12231
12303
|
} finally {
|
|
12232
12304
|
try {
|
|
12233
|
-
|
|
12305
|
+
fs10.unlinkSync(tmpIndex);
|
|
12234
12306
|
} catch {
|
|
12235
12307
|
}
|
|
12236
12308
|
}
|
|
@@ -12244,16 +12316,16 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
12244
12316
|
]);
|
|
12245
12317
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
12246
12318
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
12247
|
-
const tmpFile =
|
|
12319
|
+
const tmpFile = path13.join(
|
|
12248
12320
|
os6.tmpdir(),
|
|
12249
12321
|
`hillclimb-bundle-${Date.now()}.bundle`
|
|
12250
12322
|
);
|
|
12251
12323
|
try {
|
|
12252
12324
|
git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
|
|
12253
|
-
return
|
|
12325
|
+
return fs10.readFileSync(tmpFile);
|
|
12254
12326
|
} finally {
|
|
12255
12327
|
try {
|
|
12256
|
-
|
|
12328
|
+
fs10.unlinkSync(tmpFile);
|
|
12257
12329
|
} catch {
|
|
12258
12330
|
}
|
|
12259
12331
|
deleteRef(repoRoot, orphanRef);
|
|
@@ -12406,9 +12478,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
12406
12478
|
oldPath
|
|
12407
12479
|
});
|
|
12408
12480
|
} else {
|
|
12409
|
-
const
|
|
12410
|
-
indexByPath.set(
|
|
12411
|
-
files.push({ path:
|
|
12481
|
+
const path21 = parts[parts.length - 1];
|
|
12482
|
+
indexByPath.set(path21, files.length);
|
|
12483
|
+
files.push({ path: path21, status, additions: 0, deletions: 0 });
|
|
12412
12484
|
}
|
|
12413
12485
|
}
|
|
12414
12486
|
for (const line of numstat.split("\n")) {
|
|
@@ -12462,21 +12534,21 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
12462
12534
|
|
|
12463
12535
|
// src/git-traces/session-state.ts
|
|
12464
12536
|
import crypto from "crypto";
|
|
12465
|
-
import
|
|
12537
|
+
import fs11 from "fs";
|
|
12466
12538
|
import os7 from "os";
|
|
12467
|
-
import
|
|
12539
|
+
import path14 from "path";
|
|
12468
12540
|
var CURRENT_SCHEMA_VERSION = 3;
|
|
12469
|
-
var STATE_DIR =
|
|
12541
|
+
var STATE_DIR = path14.join(os7.homedir(), ".hillclimb", "git-traces");
|
|
12470
12542
|
function stateFileForRepo(repoRoot, tool) {
|
|
12471
|
-
const hash = crypto.createHash("sha256").update(`${
|
|
12472
|
-
return
|
|
12543
|
+
const hash = crypto.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}`).digest("hex").slice(0, 16);
|
|
12544
|
+
return path14.join(STATE_DIR, `${hash}.json`);
|
|
12473
12545
|
}
|
|
12474
12546
|
function lockFileForRepo(repoRoot, tool) {
|
|
12475
12547
|
return `${stateFileForRepo(repoRoot, tool)}.lock`;
|
|
12476
12548
|
}
|
|
12477
12549
|
async function readSessionState(repoRoot, tool) {
|
|
12478
12550
|
try {
|
|
12479
|
-
const raw = await
|
|
12551
|
+
const raw = await fs11.promises.readFile(
|
|
12480
12552
|
stateFileForRepo(repoRoot, tool),
|
|
12481
12553
|
"utf-8"
|
|
12482
12554
|
);
|
|
@@ -12491,27 +12563,27 @@ async function readSessionState(repoRoot, tool) {
|
|
|
12491
12563
|
}
|
|
12492
12564
|
async function writeSessionState(state, tool) {
|
|
12493
12565
|
const file = stateFileForRepo(state.repoRoot, tool);
|
|
12494
|
-
await
|
|
12566
|
+
await fs11.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
|
|
12495
12567
|
const tmp = `${file}.tmp`;
|
|
12496
|
-
await
|
|
12568
|
+
await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
12497
12569
|
mode: 384
|
|
12498
12570
|
});
|
|
12499
|
-
await
|
|
12571
|
+
await fs11.promises.rename(tmp, file);
|
|
12500
12572
|
}
|
|
12501
12573
|
async function deleteSessionState(repoRoot, tool) {
|
|
12502
12574
|
try {
|
|
12503
|
-
await
|
|
12575
|
+
await fs11.promises.unlink(stateFileForRepo(repoRoot, tool));
|
|
12504
12576
|
} catch {
|
|
12505
12577
|
}
|
|
12506
12578
|
}
|
|
12507
12579
|
async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
|
|
12508
12580
|
const lockPath = lockFileForRepo(repoRoot, tool);
|
|
12509
|
-
await
|
|
12581
|
+
await fs11.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
|
|
12510
12582
|
for (let i = 0; i < retries; i++) {
|
|
12511
12583
|
try {
|
|
12512
|
-
const fd = await
|
|
12584
|
+
const fd = await fs11.promises.open(
|
|
12513
12585
|
lockPath,
|
|
12514
|
-
|
|
12586
|
+
fs11.constants.O_CREAT | fs11.constants.O_EXCL | fs11.constants.O_WRONLY
|
|
12515
12587
|
);
|
|
12516
12588
|
await fd.write(String(process.pid));
|
|
12517
12589
|
await fd.close();
|
|
@@ -12528,7 +12600,7 @@ async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
|
|
|
12528
12600
|
}
|
|
12529
12601
|
async function releaseLock(repoRoot, tool) {
|
|
12530
12602
|
try {
|
|
12531
|
-
await
|
|
12603
|
+
await fs11.promises.unlink(lockFileForRepo(repoRoot, tool));
|
|
12532
12604
|
} catch {
|
|
12533
12605
|
}
|
|
12534
12606
|
}
|
|
@@ -12574,14 +12646,16 @@ function execGit(cwd, args) {
|
|
|
12574
12646
|
maxBuffer: 50 * 1024 * 1024
|
|
12575
12647
|
}).toString("utf-8").trim();
|
|
12576
12648
|
}
|
|
12649
|
+
function canUploadFile(filename, buffer) {
|
|
12650
|
+
if (buffer.byteLength <= MAX_UPLOAD_BYTES) return true;
|
|
12651
|
+
appendLog(
|
|
12652
|
+
"warn",
|
|
12653
|
+
`git-traces: skipping ${filename} (${buffer.byteLength} bytes > ${MAX_UPLOAD_BYTES} limit)`
|
|
12654
|
+
);
|
|
12655
|
+
return false;
|
|
12656
|
+
}
|
|
12577
12657
|
async function uploadFile(client, contributionId, filename, mimeType, buffer) {
|
|
12578
|
-
if (buffer
|
|
12579
|
-
appendLog(
|
|
12580
|
-
"warn",
|
|
12581
|
-
`git-traces: skipping ${filename} (${buffer.byteLength} bytes > ${MAX_UPLOAD_BYTES} limit)`
|
|
12582
|
-
);
|
|
12583
|
-
return;
|
|
12584
|
-
}
|
|
12658
|
+
if (!canUploadFile(filename, buffer)) return false;
|
|
12585
12659
|
const presigned = await client.createUpload(contributionId, {
|
|
12586
12660
|
originalFilename: filename,
|
|
12587
12661
|
mimeType,
|
|
@@ -12596,6 +12670,11 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
|
|
|
12596
12670
|
"info",
|
|
12597
12671
|
`git-traces: uploaded ${filename} (${buffer.byteLength} bytes)`
|
|
12598
12672
|
);
|
|
12673
|
+
return true;
|
|
12674
|
+
}
|
|
12675
|
+
function canUploadEpochBaselineArtifacts(epoch, artifacts) {
|
|
12676
|
+
const prefix = epochPrefix(epoch);
|
|
12677
|
+
return canUploadFile(`${prefix}-baseline.bundle`, artifacts.bundleBuffer) && canUploadFile(`${prefix}-baseline.json`, artifacts.metadataBuffer);
|
|
12599
12678
|
}
|
|
12600
12679
|
function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
12601
12680
|
const baselineSha = captureBaselineSha(repoRoot);
|
|
@@ -12650,30 +12729,34 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
12650
12729
|
async function uploadEpochBaselineArtifacts(params) {
|
|
12651
12730
|
const { client, contributionId, epoch, artifacts } = params;
|
|
12652
12731
|
const prefix = epochPrefix(epoch);
|
|
12653
|
-
|
|
12654
|
-
|
|
12655
|
-
contributionId,
|
|
12656
|
-
`${prefix}-baseline.json`,
|
|
12657
|
-
"application/json",
|
|
12658
|
-
artifacts.metadataBuffer
|
|
12659
|
-
);
|
|
12660
|
-
await uploadFile(
|
|
12732
|
+
if (!canUploadEpochBaselineArtifacts(epoch, artifacts)) return false;
|
|
12733
|
+
const bundleUploaded = await uploadFile(
|
|
12661
12734
|
client,
|
|
12662
12735
|
contributionId,
|
|
12663
12736
|
`${prefix}-baseline.bundle`,
|
|
12664
12737
|
"application/x-git-bundle",
|
|
12665
12738
|
artifacts.bundleBuffer
|
|
12666
12739
|
);
|
|
12740
|
+
if (!bundleUploaded) return false;
|
|
12741
|
+
return uploadFile(
|
|
12742
|
+
client,
|
|
12743
|
+
contributionId,
|
|
12744
|
+
`${prefix}-baseline.json`,
|
|
12745
|
+
"application/json",
|
|
12746
|
+
artifacts.metadataBuffer
|
|
12747
|
+
);
|
|
12667
12748
|
}
|
|
12668
12749
|
async function uploadEpochBaseline(params) {
|
|
12669
12750
|
const artifacts = buildEpochBaselineArtifacts(params);
|
|
12670
12751
|
if (!artifacts) return null;
|
|
12671
|
-
|
|
12752
|
+
if (!canUploadEpochBaselineArtifacts(params.epoch, artifacts)) return null;
|
|
12753
|
+
const uploaded = await uploadEpochBaselineArtifacts({
|
|
12672
12754
|
client: params.client,
|
|
12673
12755
|
contributionId: params.contributionId,
|
|
12674
12756
|
epoch: params.epoch,
|
|
12675
12757
|
artifacts
|
|
12676
12758
|
});
|
|
12759
|
+
if (!uploaded) return null;
|
|
12677
12760
|
return { baselineTreeSha: artifacts.baselineTreeSha };
|
|
12678
12761
|
}
|
|
12679
12762
|
async function openEpoch(params) {
|
|
@@ -12783,6 +12866,7 @@ async function handleStop(payload, tool) {
|
|
|
12783
12866
|
startedAt: state.startedAt
|
|
12784
12867
|
});
|
|
12785
12868
|
if (!artifacts) return;
|
|
12869
|
+
if (!canUploadEpochBaselineArtifacts(1, artifacts)) return;
|
|
12786
12870
|
const toolLabel = TOOL_LABELS[tool] ?? "Claude";
|
|
12787
12871
|
const now = /* @__PURE__ */ new Date();
|
|
12788
12872
|
const epochSeconds = formatEpochSeconds2(now);
|
|
@@ -12798,12 +12882,13 @@ Repo: ${cwd}
|
|
|
12798
12882
|
Uploaded: ${now.toISOString()}`
|
|
12799
12883
|
}
|
|
12800
12884
|
);
|
|
12801
|
-
await uploadEpochBaselineArtifacts({
|
|
12885
|
+
const uploaded = await uploadEpochBaselineArtifacts({
|
|
12802
12886
|
client,
|
|
12803
12887
|
contributionId: contribution.id,
|
|
12804
12888
|
epoch: 1,
|
|
12805
12889
|
artifacts
|
|
12806
12890
|
});
|
|
12891
|
+
if (!uploaded) return;
|
|
12807
12892
|
await client.submitContribution(contribution.id);
|
|
12808
12893
|
state.contributionId = contribution.id;
|
|
12809
12894
|
state.baselineTreeSha = artifacts.baselineTreeSha;
|
|
@@ -13100,15 +13185,15 @@ ${stack}` : ""}`
|
|
|
13100
13185
|
}
|
|
13101
13186
|
|
|
13102
13187
|
// src/outputs/zip.ts
|
|
13103
|
-
import
|
|
13104
|
-
import
|
|
13188
|
+
import fs13 from "fs";
|
|
13189
|
+
import path16 from "path";
|
|
13105
13190
|
import archiver2 from "archiver";
|
|
13106
13191
|
|
|
13107
13192
|
// src/outputs/downloads.ts
|
|
13108
13193
|
import { execSync as execSync2 } from "child_process";
|
|
13109
|
-
import
|
|
13194
|
+
import fs12 from "fs";
|
|
13110
13195
|
import os8 from "os";
|
|
13111
|
-
import
|
|
13196
|
+
import path15 from "path";
|
|
13112
13197
|
function getDownloadsFolder() {
|
|
13113
13198
|
const home = os8.homedir();
|
|
13114
13199
|
if (process.platform === "linux") {
|
|
@@ -13117,12 +13202,12 @@ function getDownloadsFolder() {
|
|
|
13117
13202
|
encoding: "utf-8",
|
|
13118
13203
|
timeout: 3e3
|
|
13119
13204
|
}).trim();
|
|
13120
|
-
if (xdgDir &&
|
|
13205
|
+
if (xdgDir && fs12.existsSync(xdgDir)) return xdgDir;
|
|
13121
13206
|
} catch {
|
|
13122
13207
|
}
|
|
13123
13208
|
}
|
|
13124
|
-
const downloads =
|
|
13125
|
-
if (
|
|
13209
|
+
const downloads = path15.join(home, "Downloads");
|
|
13210
|
+
if (fs12.existsSync(downloads)) return downloads;
|
|
13126
13211
|
return home;
|
|
13127
13212
|
}
|
|
13128
13213
|
|
|
@@ -13131,11 +13216,11 @@ function sanitizeFilename(name) {
|
|
|
13131
13216
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
13132
13217
|
}
|
|
13133
13218
|
function getUniqueFilename(dir, base, ext) {
|
|
13134
|
-
let candidate =
|
|
13135
|
-
if (!
|
|
13219
|
+
let candidate = path16.join(dir, `${base}${ext}`);
|
|
13220
|
+
if (!fs13.existsSync(candidate)) return candidate;
|
|
13136
13221
|
let i = 1;
|
|
13137
|
-
while (
|
|
13138
|
-
candidate =
|
|
13222
|
+
while (fs13.existsSync(candidate)) {
|
|
13223
|
+
candidate = path16.join(dir, `${base}-${i}${ext}`);
|
|
13139
13224
|
i++;
|
|
13140
13225
|
}
|
|
13141
13226
|
return candidate;
|
|
@@ -13145,13 +13230,13 @@ var ZipOutput = class {
|
|
|
13145
13230
|
label = "Save as .zip to Downloads";
|
|
13146
13231
|
async emit(group, options) {
|
|
13147
13232
|
const downloadsDir = getDownloadsFolder();
|
|
13148
|
-
const repoName = sanitizeFilename(
|
|
13233
|
+
const repoName = sanitizeFilename(path16.basename(group.repoPath));
|
|
13149
13234
|
const timeRange = options.timeRange;
|
|
13150
13235
|
const rangePart = timeRange?.label ?? "all";
|
|
13151
13236
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
13152
13237
|
const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
|
|
13153
13238
|
const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
|
|
13154
|
-
const output =
|
|
13239
|
+
const output = fs13.createWriteStream(outputPath);
|
|
13155
13240
|
const archive = archiver2("zip", { zlib: { level: 6 } });
|
|
13156
13241
|
const done = new Promise((resolve, reject) => {
|
|
13157
13242
|
output.on("close", resolve);
|
|
@@ -13345,15 +13430,15 @@ async function confirmExport(group, output) {
|
|
|
13345
13430
|
}
|
|
13346
13431
|
|
|
13347
13432
|
// src/sources/claude.ts
|
|
13348
|
-
import
|
|
13433
|
+
import fs14 from "fs";
|
|
13349
13434
|
import os9 from "os";
|
|
13350
|
-
import
|
|
13435
|
+
import path17 from "path";
|
|
13351
13436
|
import readline from "readline";
|
|
13352
13437
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
13353
13438
|
async function resolveRepoPath(projectDir) {
|
|
13354
|
-
const indexPath =
|
|
13439
|
+
const indexPath = path17.join(projectDir, "sessions-index.json");
|
|
13355
13440
|
try {
|
|
13356
|
-
const raw = await
|
|
13441
|
+
const raw = await fs14.promises.readFile(indexPath, "utf-8");
|
|
13357
13442
|
const data = JSON.parse(raw);
|
|
13358
13443
|
if (data.originalPath && typeof data.originalPath === "string") {
|
|
13359
13444
|
return data.originalPath;
|
|
@@ -13361,12 +13446,12 @@ async function resolveRepoPath(projectDir) {
|
|
|
13361
13446
|
} catch {
|
|
13362
13447
|
}
|
|
13363
13448
|
const cwdCounts = /* @__PURE__ */ new Map();
|
|
13364
|
-
const entries = await
|
|
13449
|
+
const entries = await fs14.promises.readdir(projectDir, {
|
|
13365
13450
|
withFileTypes: true
|
|
13366
13451
|
});
|
|
13367
13452
|
for (const entry of entries) {
|
|
13368
13453
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
13369
|
-
const cwd = await extractCwdFromJsonl(
|
|
13454
|
+
const cwd = await extractCwdFromJsonl(path17.join(projectDir, entry.name));
|
|
13370
13455
|
if (cwd) {
|
|
13371
13456
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
13372
13457
|
}
|
|
@@ -13385,7 +13470,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
13385
13470
|
return null;
|
|
13386
13471
|
}
|
|
13387
13472
|
async function extractCwdFromJsonl(filePath) {
|
|
13388
|
-
const stream =
|
|
13473
|
+
const stream = fs14.createReadStream(filePath, { encoding: "utf-8" });
|
|
13389
13474
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
13390
13475
|
try {
|
|
13391
13476
|
for await (const line of rl) {
|
|
@@ -13407,12 +13492,12 @@ async function extractCwdFromJsonl(filePath) {
|
|
|
13407
13492
|
async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
13408
13493
|
let entries;
|
|
13409
13494
|
try {
|
|
13410
|
-
entries = await
|
|
13495
|
+
entries = await fs14.promises.readdir(dir, { withFileTypes: true });
|
|
13411
13496
|
} catch {
|
|
13412
13497
|
return;
|
|
13413
13498
|
}
|
|
13414
13499
|
for (const entry of entries) {
|
|
13415
|
-
const fullPath =
|
|
13500
|
+
const fullPath = path17.join(dir, entry.name);
|
|
13416
13501
|
if (entry.isDirectory()) {
|
|
13417
13502
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
13418
13503
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -13434,19 +13519,19 @@ function fallbackDecode(encodedName) {
|
|
|
13434
13519
|
var ClaudeSource = class {
|
|
13435
13520
|
name = "claude";
|
|
13436
13521
|
async scan() {
|
|
13437
|
-
const baseDir =
|
|
13522
|
+
const baseDir = path17.join(os9.homedir(), ".claude", "projects");
|
|
13438
13523
|
try {
|
|
13439
|
-
await
|
|
13524
|
+
await fs14.promises.access(baseDir);
|
|
13440
13525
|
} catch {
|
|
13441
13526
|
return [];
|
|
13442
13527
|
}
|
|
13443
|
-
const projectDirs = await
|
|
13528
|
+
const projectDirs = await fs14.promises.readdir(baseDir, {
|
|
13444
13529
|
withFileTypes: true
|
|
13445
13530
|
});
|
|
13446
13531
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
13447
13532
|
const resultArrays = await Promise.all(
|
|
13448
13533
|
dirEntries.map(async (dir) => {
|
|
13449
|
-
const projectPath =
|
|
13534
|
+
const projectPath = path17.join(baseDir, dir.name);
|
|
13450
13535
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
13451
13536
|
const files = [];
|
|
13452
13537
|
await collectFiles(
|
|
@@ -13464,12 +13549,12 @@ var ClaudeSource = class {
|
|
|
13464
13549
|
};
|
|
13465
13550
|
|
|
13466
13551
|
// src/sources/codex.ts
|
|
13467
|
-
import
|
|
13552
|
+
import fs15 from "fs";
|
|
13468
13553
|
import os10 from "os";
|
|
13469
|
-
import
|
|
13554
|
+
import path18 from "path";
|
|
13470
13555
|
import readline2 from "readline";
|
|
13471
13556
|
async function parseSessionMeta(filePath) {
|
|
13472
|
-
const stream =
|
|
13557
|
+
const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
|
|
13473
13558
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
13474
13559
|
try {
|
|
13475
13560
|
for await (const line of rl) {
|
|
@@ -13494,12 +13579,12 @@ async function findJsonlFiles(dir) {
|
|
|
13494
13579
|
async function walk(d) {
|
|
13495
13580
|
let entries;
|
|
13496
13581
|
try {
|
|
13497
|
-
entries = await
|
|
13582
|
+
entries = await fs15.promises.readdir(d, { withFileTypes: true });
|
|
13498
13583
|
} catch {
|
|
13499
13584
|
return;
|
|
13500
13585
|
}
|
|
13501
13586
|
for (const entry of entries) {
|
|
13502
|
-
const full =
|
|
13587
|
+
const full = path18.join(d, entry.name);
|
|
13503
13588
|
if (entry.isDirectory()) {
|
|
13504
13589
|
await walk(full);
|
|
13505
13590
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -13513,11 +13598,11 @@ async function findJsonlFiles(dir) {
|
|
|
13513
13598
|
async function loadHistory(historyPath) {
|
|
13514
13599
|
const map = /* @__PURE__ */ new Map();
|
|
13515
13600
|
try {
|
|
13516
|
-
await
|
|
13601
|
+
await fs15.promises.access(historyPath);
|
|
13517
13602
|
} catch {
|
|
13518
13603
|
return map;
|
|
13519
13604
|
}
|
|
13520
|
-
const stream =
|
|
13605
|
+
const stream = fs15.createReadStream(historyPath, { encoding: "utf-8" });
|
|
13521
13606
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
13522
13607
|
try {
|
|
13523
13608
|
for await (const line of rl) {
|
|
@@ -13544,14 +13629,14 @@ async function loadHistory(historyPath) {
|
|
|
13544
13629
|
var CodexSource = class {
|
|
13545
13630
|
name = "codex";
|
|
13546
13631
|
async scan() {
|
|
13547
|
-
const codexDir =
|
|
13548
|
-
const sessionsDir =
|
|
13632
|
+
const codexDir = path18.join(os10.homedir(), ".codex");
|
|
13633
|
+
const sessionsDir = path18.join(codexDir, "sessions");
|
|
13549
13634
|
try {
|
|
13550
|
-
await
|
|
13635
|
+
await fs15.promises.access(sessionsDir);
|
|
13551
13636
|
} catch {
|
|
13552
13637
|
return [];
|
|
13553
13638
|
}
|
|
13554
|
-
const historyPath =
|
|
13639
|
+
const historyPath = path18.join(codexDir, "history.jsonl");
|
|
13555
13640
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
13556
13641
|
findJsonlFiles(sessionsDir),
|
|
13557
13642
|
loadHistory(historyPath)
|
|
@@ -13574,8 +13659,8 @@ var CodexSource = class {
|
|
|
13574
13659
|
});
|
|
13575
13660
|
const historyLines = historyMap.get(meta.sessionId);
|
|
13576
13661
|
if (historyLines) {
|
|
13577
|
-
const sessionDir =
|
|
13578
|
-
const historyAbsPath =
|
|
13662
|
+
const sessionDir = path18.relative(sessionsDir, path18.dirname(filePath));
|
|
13663
|
+
const historyAbsPath = path18.join(
|
|
13579
13664
|
sessionsDir,
|
|
13580
13665
|
sessionDir,
|
|
13581
13666
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -13595,18 +13680,18 @@ var CodexSource = class {
|
|
|
13595
13680
|
};
|
|
13596
13681
|
|
|
13597
13682
|
// src/sources/copilotChat.ts
|
|
13598
|
-
import
|
|
13683
|
+
import fs16 from "fs";
|
|
13599
13684
|
import os11 from "os";
|
|
13600
|
-
import
|
|
13685
|
+
import path19 from "path";
|
|
13601
13686
|
import { fileURLToPath } from "url";
|
|
13602
13687
|
function vsCodeUserDirs() {
|
|
13603
13688
|
const home = os11.homedir();
|
|
13604
13689
|
const dirs = [
|
|
13605
|
-
|
|
13606
|
-
|
|
13690
|
+
path19.join(home, "Library", "Application Support", "Code", "User"),
|
|
13691
|
+
path19.join(home, ".config", "Code", "User")
|
|
13607
13692
|
];
|
|
13608
13693
|
if (process.env.APPDATA) {
|
|
13609
|
-
dirs.push(
|
|
13694
|
+
dirs.push(path19.join(process.env.APPDATA, "Code", "User"));
|
|
13610
13695
|
}
|
|
13611
13696
|
return dirs;
|
|
13612
13697
|
}
|
|
@@ -13621,7 +13706,7 @@ function uriToFsPath(uri) {
|
|
|
13621
13706
|
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
13622
13707
|
let raw;
|
|
13623
13708
|
try {
|
|
13624
|
-
raw = await
|
|
13709
|
+
raw = await fs16.promises.readFile(workspaceJsonPath, "utf-8");
|
|
13625
13710
|
} catch {
|
|
13626
13711
|
return null;
|
|
13627
13712
|
}
|
|
@@ -13643,10 +13728,10 @@ var CopilotChatSource = class {
|
|
|
13643
13728
|
async scan() {
|
|
13644
13729
|
const results = [];
|
|
13645
13730
|
for (const userDir of vsCodeUserDirs()) {
|
|
13646
|
-
const workspaceStorage =
|
|
13731
|
+
const workspaceStorage = path19.join(userDir, "workspaceStorage");
|
|
13647
13732
|
let hashDirs;
|
|
13648
13733
|
try {
|
|
13649
|
-
hashDirs = await
|
|
13734
|
+
hashDirs = await fs16.promises.readdir(workspaceStorage, {
|
|
13650
13735
|
withFileTypes: true
|
|
13651
13736
|
});
|
|
13652
13737
|
} catch {
|
|
@@ -13654,22 +13739,22 @@ var CopilotChatSource = class {
|
|
|
13654
13739
|
}
|
|
13655
13740
|
for (const hash of hashDirs) {
|
|
13656
13741
|
if (!hash.isDirectory()) continue;
|
|
13657
|
-
const wsRoot =
|
|
13658
|
-
const transcriptsDir =
|
|
13742
|
+
const wsRoot = path19.join(workspaceStorage, hash.name);
|
|
13743
|
+
const transcriptsDir = path19.join(
|
|
13659
13744
|
wsRoot,
|
|
13660
13745
|
"GitHub.copilot-chat",
|
|
13661
13746
|
"transcripts"
|
|
13662
13747
|
);
|
|
13663
13748
|
let transcriptEntries;
|
|
13664
13749
|
try {
|
|
13665
|
-
transcriptEntries = await
|
|
13750
|
+
transcriptEntries = await fs16.promises.readdir(transcriptsDir, {
|
|
13666
13751
|
withFileTypes: true
|
|
13667
13752
|
});
|
|
13668
13753
|
} catch {
|
|
13669
13754
|
continue;
|
|
13670
13755
|
}
|
|
13671
13756
|
const repoPath = await readWorkspaceFolder(
|
|
13672
|
-
|
|
13757
|
+
path19.join(wsRoot, "workspace.json")
|
|
13673
13758
|
);
|
|
13674
13759
|
if (!repoPath) continue;
|
|
13675
13760
|
for (const entry of transcriptEntries) {
|
|
@@ -13677,7 +13762,7 @@ var CopilotChatSource = class {
|
|
|
13677
13762
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
13678
13763
|
results.push({
|
|
13679
13764
|
sourceName: this.name,
|
|
13680
|
-
absolutePath:
|
|
13765
|
+
absolutePath: path19.join(transcriptsDir, entry.name),
|
|
13681
13766
|
repoPath,
|
|
13682
13767
|
metadata: { sessionId }
|
|
13683
13768
|
});
|
|
@@ -13717,7 +13802,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
13717
13802
|
async function filterByTimeRange(group, range) {
|
|
13718
13803
|
const results = await Promise.all(
|
|
13719
13804
|
group.files.map(
|
|
13720
|
-
(f) =>
|
|
13805
|
+
(f) => fs17.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
13721
13806
|
)
|
|
13722
13807
|
);
|
|
13723
13808
|
const filtered = [];
|
|
@@ -13744,10 +13829,10 @@ async function runInteractive() {
|
|
|
13744
13829
|
s.start(`Scanning ${source.name} logs...`);
|
|
13745
13830
|
const allFiles = await source.scan();
|
|
13746
13831
|
const allGroups = await mergeByRepo(allFiles);
|
|
13747
|
-
const repoRoot =
|
|
13832
|
+
const repoRoot = path20.resolve(repo.root);
|
|
13748
13833
|
const matching = allGroups.filter((g) => {
|
|
13749
|
-
const resolved =
|
|
13750
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
13834
|
+
const resolved = path20.resolve(g.repoPath);
|
|
13835
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path20.sep);
|
|
13751
13836
|
});
|
|
13752
13837
|
if (matching.length === 0) {
|
|
13753
13838
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -13778,7 +13863,7 @@ async function runInteractive() {
|
|
|
13778
13863
|
}
|
|
13779
13864
|
}
|
|
13780
13865
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
13781
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
13866
|
+
const envFilePaths = envFileNames.map((n) => path20.join(repoRoot, n));
|
|
13782
13867
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
13783
13868
|
const secretResult = await collectSecrets(
|
|
13784
13869
|
repoRoot,
|